Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: make sure user is not Blank when using ACMEApi #2637

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import com.wire.kalium.network.api.base.authenticated.e2ei.E2EIApi
import com.wire.kalium.network.api.base.unbound.acme.ACMEApi
import com.wire.kalium.network.api.base.unbound.acme.ACMEResponse
import com.wire.kalium.network.api.base.unbound.acme.ChallengeResponse
import io.ktor.http.Url
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

Expand Down Expand Up @@ -89,7 +88,7 @@ interface E2EIRepository {
suspend fun getOAuthRefreshToken(): Either<E2EIFailure, String?>
suspend fun nukeE2EIClient()
suspend fun fetchFederationCertificates(): Either<E2EIFailure, Unit>
fun discoveryUrl(): Either<E2EIFailure, Url>
fun discoveryUrl(): Either<E2EIFailure, String>
}

@Suppress("LongParameterList")
Expand Down Expand Up @@ -370,8 +369,8 @@ class E2EIRepositoryImpl(
}, { settings ->
when {
!settings.isRequired -> E2EIFailure.Disabled.left()
settings.discoverUrl == null -> E2EIFailure.MissingDiscoveryUrl.left()
else -> Url(settings.discoverUrl).right()
settings.discoverUrl.isNullOrBlank() -> E2EIFailure.MissingDiscoveryUrl.left()
else -> settings.discoverUrl.right()
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -987,7 +987,7 @@ class E2EIRepositoryTest {

verify(arrangement.acmeApi)
.suspendFunction(arrangement.acmeApi::getTrustAnchors)
.with(eq(Url(RANDOM_URL)))
.with(eq(RANDOM_URL))
.wasInvoked(once)

verify(arrangement.coreCryptoCentral)
Expand Down Expand Up @@ -1057,7 +1057,7 @@ class E2EIRepositoryTest {
.arrange()

e2eiRepository.discoveryUrl().shouldSucceed {
assertEquals(Url(RANDOM_URL), it)
assertEquals(RANDOM_URL, it)
}

verify(arrangement.userConfigRepository)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@ import io.ktor.http.Url
import io.ktor.http.contentType
import io.ktor.http.isSuccess
import io.ktor.http.protocolWithAuthority
import okio.IOException

interface ACMEApi {
suspend fun getTrustAnchors(discoveryUrl: Url): NetworkResponse<ByteArray>
suspend fun getACMEDirectories(discoveryUrl: Url): NetworkResponse<AcmeDirectoriesResponse>
suspend fun getTrustAnchors(discoveryUrl: String): NetworkResponse<ByteArray>
suspend fun getACMEDirectories(discoveryUrl: String): NetworkResponse<AcmeDirectoriesResponse>
suspend fun getACMENonce(url: String): NetworkResponse<String>
suspend fun sendACMERequest(url: String, body: ByteArray? = null): NetworkResponse<ACMEResponse>
suspend fun sendAuthorizationRequest(url: String, body: ByteArray? = null): NetworkResponse<ACMEAuthorizationResponse>
suspend fun sendChallengeRequest(url: String, body: ByteArray): NetworkResponse<ChallengeResponse>
suspend fun getACMEFederation(discoveryUrl: Url): NetworkResponse<String>
suspend fun getACMEFederation(discoveryUrl: String): NetworkResponse<String>
suspend fun getClientDomainCRL(url: String): NetworkResponse<ByteArray>
}

Expand All @@ -58,42 +59,89 @@ class ACMEApiImpl internal constructor(
private val httpClient get() = unboundNetworkClient.httpClient
private val clearTextTrafficHttpClient get() = unboundClearTextTrafficNetworkClient.httpClient

override suspend fun getTrustAnchors(discoveryUrl: Url): NetworkResponse<ByteArray> = wrapKaliumResponse {
httpClient.get("${discoveryUrl.protocolWithAuthority}/$PATH_ACME_ROOTS_PEM")
override suspend fun getTrustAnchors(discoveryUrl: String): NetworkResponse<ByteArray> {
val protocolWithAuthority = Url(discoveryUrl).protocolWithAuthority

if (discoveryUrl.isBlank() || protocolWithAuthority.isBlank()) {
return NetworkResponse.Error(
KaliumException.GenericError(
IllegalArgumentException(
"getTrustAnchors: Url cannot be empty or protocolWithAuthority cannot be empty" +
", is urlBlank = ${discoveryUrl.isBlank()}" +
", is protocolWithAuthorityBlank = ${protocolWithAuthority.isBlank()}"
)
)
)
}

return wrapKaliumResponse {
httpClient.get("$protocolWithAuthority/$PATH_ACME_ROOTS_PEM")
}

}

override suspend fun getACMEDirectories(discoveryUrl: Url): NetworkResponse<AcmeDirectoriesResponse> = wrapKaliumResponse {
httpClient.get(discoveryUrl)
override suspend fun getACMEDirectories(discoveryUrl: String): NetworkResponse<AcmeDirectoriesResponse> {
if (discoveryUrl.isBlank()) {
return NetworkResponse.Error(KaliumException.GenericError(IllegalArgumentException("getACMEDirectories: Url cannot be empty")))
}

return wrapKaliumResponse {
httpClient.get(discoveryUrl)
}
}

override suspend fun getACMENonce(url: String): NetworkResponse<String> =
httpClient.prepareHead(url).execute { httpResponse ->
handleACMENonceResponse(httpResponse)
override suspend fun getACMENonce(url: String): NetworkResponse<String> {
return try {
if (url.isBlank()) {
return NetworkResponse.Error(KaliumException.GenericError(IllegalArgumentException("sendACMERequest: Url cannot be empty")))
}
httpClient.prepareHead(url).execute { httpResponse ->
handleACMENonceResponse(httpResponse)

}
} catch (e: IOException) {
NetworkResponse.Error(KaliumException.GenericError(e))
}
}

private suspend fun handleACMENonceResponse(httpResponse: HttpResponse): NetworkResponse<String> =
if (httpResponse.status.isSuccess()) httpResponse.headers[NONCE_HEADER_KEY]?.let { nonce ->
NetworkResponse.Success(nonce, httpResponse)
} ?: run {
CustomErrors.MISSING_NONCE
}
else {
} else {
handleUnsuccessfulResponse(httpResponse)
}

override suspend fun sendACMERequest(
url: String,
body: ByteArray?
): NetworkResponse<ACMEResponse> =
httpClient.preparePost(url) {
contentType(ContentType.Application.JoseJson)
body?.let { setBody(body) }
}.execute { httpResponse ->
handleACMERequestResponse(httpResponse)
): NetworkResponse<ACMEResponse> {
return try {
if (url.isBlank()) {
return NetworkResponse.Error(KaliumException.GenericError(IllegalArgumentException("sendACMERequest: Url cannot be empty")))
}
httpClient.preparePost(url) {
contentType(ContentType.Application.JoseJson)
body?.let { setBody(body) }
}.execute { httpResponse ->
handleACMERequestResponse(httpResponse)
}
} catch (e: IOException) {
NetworkResponse.Error(KaliumException.GenericError(e))
}
}

override suspend fun sendAuthorizationRequest(url: String, body: ByteArray?): NetworkResponse<ACMEAuthorizationResponse> =
wrapKaliumResponse<String> {
override suspend fun sendAuthorizationRequest(url: String, body: ByteArray?): NetworkResponse<ACMEAuthorizationResponse> {
if (url.isBlank()) {
return NetworkResponse.Error(
KaliumException.GenericError(
IllegalArgumentException("sendAuthorizationRequest: Url cannot be empty")
)
)
}

return wrapKaliumResponse<String> {
httpClient.post(url) {
contentType(ContentType.Application.JoseJson)
setBody(body)
Expand Down Expand Up @@ -124,6 +172,8 @@ class ACMEApiImpl internal constructor(
}
}

}

private suspend fun handleACMERequestResponse(httpResponse: HttpResponse): NetworkResponse<ACMEResponse> =
if (httpResponse.status.isSuccess()) {
httpResponse.headers[NONCE_HEADER_KEY]?.let { nonce ->
Expand All @@ -141,8 +191,16 @@ class ACMEApiImpl internal constructor(
handleUnsuccessfulResponse(httpResponse)
}

override suspend fun sendChallengeRequest(url: String, body: ByteArray): NetworkResponse<ChallengeResponse> =
wrapKaliumResponse<ChallengeResponse> {
override suspend fun sendChallengeRequest(url: String, body: ByteArray): NetworkResponse<ChallengeResponse> {
if (url.isBlank()) {
return NetworkResponse.Error(
KaliumException.GenericError(
IllegalArgumentException("sendChallengeRequest: Url cannot be empty")
)
)
}

return wrapKaliumResponse<ChallengeResponse> {
httpClient.post(url) {
contentType(ContentType.Application.JoseJson)
setBody(body)
Expand All @@ -163,9 +221,25 @@ class ACMEApiImpl internal constructor(
CustomErrors.MISSING_NONCE
}
}
}

override suspend fun getACMEFederation(discoveryUrl: String): NetworkResponse<String> {
val protocolWithAuthority = Url(discoveryUrl).protocolWithAuthority
if (discoveryUrl.isBlank() || protocolWithAuthority.isBlank()) {
return NetworkResponse.Error(
KaliumException.GenericError(
IllegalArgumentException(
"getACMEFederation: Url cannot be empty, " +
"is urlBlank = ${discoveryUrl.isBlank()}, " +
"is protocolWithAuthorityBlank = ${protocolWithAuthority.isBlank()}"
)
)
)
}

override suspend fun getACMEFederation(discoveryUrl: Url): NetworkResponse<String> = wrapKaliumResponse {
httpClient.get("${discoveryUrl.protocolWithAuthority}/$PATH_ACME_FEDERATION")
return wrapKaliumResponse {
httpClient.get("$protocolWithAuthority/$PATH_ACME_FEDERATION")
}
}

override suspend fun getClientDomainCRL(url: String): NetworkResponse<ByteArray> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,46 +31,91 @@ data class AcmeDirectoriesResponse(
val keyChange: String
)

@Suppress("EnforceSerializableFields")
@Serializable
data class ACMEResponse(
val nonce: String,
val location: String,
val response: ByteArray
)
@SerialName("nonce") val nonce: String,
@SerialName("location") val location: String,
@SerialName("response") val response: ByteArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as ACMEResponse

if (nonce != other.nonce) return false
if (location != other.location) return false
if (!response.contentEquals(other.response)) return false

return true
}

override fun hashCode(): Int {
var result = nonce.hashCode()
result = 31 * result + location.hashCode()
result = 31 * result + response.contentHashCode()
return result
}
}

@Suppress("EnforceSerializableFields")
@Serializable
data class ChallengeResponse(
val type: String,
val url: String,
val status: String,
val token: String,
val target: String,
@SerialName("type") val type: String,
@SerialName("url") val url: String,
@SerialName("status") val status: String,
@SerialName("token") val token: String,
@SerialName("target") val target: String,
// it is ok to have this default value here since in the request we are
// parsing the request and extracting it form the headers and if it is missing form headers
// then and error is returned
val nonce: String = ""
)

@Suppress("EnforceSerializableFields")
@Serializable
data class ACMEAuthorizationResponse(
val nonce: String,
val location: String?,
val response: ByteArray,
val challengeType: DtoAuthorizationChallengeType
)
@SerialName("nonce") val nonce: String,
@SerialName("location") val location: String?,
@SerialName("response") val response: ByteArray,
@SerialName("challengeType") val challengeType: DtoAuthorizationChallengeType
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as ACMEAuthorizationResponse

if (nonce != other.nonce) return false
if (location != other.location) return false
if (!response.contentEquals(other.response)) return false
if (challengeType != other.challengeType) return false

return true
}

override fun hashCode(): Int {
var result = nonce.hashCode()
result = 31 * result + (location?.hashCode() ?: 0)
result = 31 * result + response.contentHashCode()
result = 31 * result + challengeType.hashCode()
return result
}
}

@Suppress("EnforceSerializableFields")
@Serializable
data class AuthorizationResponse(
val challenges: List<AuthorizationChallenge>,
@SerialName("challenges") val challenges: List<AuthorizationChallenge>,
)

@Suppress("EnforceSerializableFields")
@Serializable
data class AuthorizationChallenge(
val type: DtoAuthorizationChallengeType,
@SerialName("type") val type: DtoAuthorizationChallengeType,
)

@Serializable
enum class DtoAuthorizationChallengeType {
@SerialName("wire-dpop-01")
DPoP,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ internal class ACMEApiTest : ApiTest() {
)
val acmeApi: ACMEApi = ACMEApiImpl(networkClient, networkClient)

acmeApi.getTrustAnchors(Url(ACME_DISCOVERY_URL)).also { actual ->
acmeApi.getTrustAnchors(ACME_DISCOVERY_URL).also { actual ->
assertIs<NetworkResponse.Success<CertificateChain>>(actual)
assertEquals(expected, actual.value)
}
Expand All @@ -67,7 +67,7 @@ internal class ACMEApiTest : ApiTest() {
)
val acmeApi: ACMEApi = ACMEApiImpl(networkClient, networkClient)

acmeApi.getACMEDirectories(Url(ACME_DISCOVERY_URL)).also { actual ->
acmeApi.getACMEDirectories(ACME_DISCOVERY_URL).also { actual ->
assertIs<NetworkResponse.Success<AcmeDirectoriesResponse>>(actual)
assertEquals(expected, actual.value)
}
Expand Down Expand Up @@ -103,14 +103,14 @@ internal class ACMEApiTest : ApiTest() {
headers = mapOf(NONCE_HEADER_KEY to RANDOM_NONCE, LOCATION_HEADER_KEY to RANDOM_LOCATION),
assertion = {
assertJsonJose()
assertUrlEqual("")
assertUrlEqual("http://wire.com")
assertPost()
assertNoQueryParams()
}
)
val acmeApi: ACMEApi = ACMEApiImpl(networkClient, networkClient)

acmeApi.sendACMERequest("", byteArrayOf(0x12, 0x24, 0x32, 0x42)).also { actual ->
acmeApi.sendACMERequest("http://wire.com", byteArrayOf(0x12, 0x24, 0x32, 0x42)).also { actual ->
assertIs<NetworkResponse.Success<ACMEResponse>>(actual)
assertEquals(RANDOM_NONCE, actual.value.nonce)
assertEquals(RANDOM_LOCATION, actual.value.location)
Expand Down Expand Up @@ -147,14 +147,14 @@ internal class ACMEApiTest : ApiTest() {
headers = mapOf(NONCE_HEADER_KEY to RANDOM_NONCE),
assertion = {
assertJsonJose()
assertUrlEqual("")
assertUrlEqual("http://wire.com")
assertPost()
assertNoQueryParams()
}
)
val acmeApi: ACMEApi = ACMEApiImpl(networkClient, networkClient)

acmeApi.sendACMERequest("", byteArrayOf(0x12, 0x24, 0x32, 0x42)).also { actual ->
acmeApi.sendACMERequest("http://wire.com", byteArrayOf(0x12, 0x24, 0x32, 0x42)).also { actual ->
assertIs<NetworkResponse.Success<ACMEResponse>>(actual)
assertEquals(RANDOM_NONCE, actual.value.nonce)
assertEquals("null", actual.value.location)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ object ACMEActions {
/**
* URL Paths
*/
private val ACME_BASE_URL = Url("https://balderdash.hogwash.work:9000/acme/google-android/")
private val ACME_BASE_URL = "https://balderdash.hogwash.work:9000/acme/google-android/"
private const val ACME_DIRECTORIES_PATH = "https://balderdash.hogwash.work:9000/acme/google-android/directory"

/**
Expand Down
Loading