Skip to content

Commit

Permalink
OIDC SIOP: Implement verifier attestation JWT
Browse files Browse the repository at this point in the history
  • Loading branch information
nodh committed Apr 8, 2024
1 parent c8d73b5 commit 5b29770
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Release 3.6.0:
- Move `JsonWebKeySet` to library `at.asitplus.crypto:datatypes-jws`
- `DefaultVerifierJwsService` may load public keys for verifying JWS from a JWK Set URL in the header, see constructor argument `jwkSetRetriever` (cf. to `OidcSiopWallet`)
- `OidcSiopWallet` and `OidcSiopVerifier` implement response mode `direct_post.jwt`, as per OpenID for Verifiable Presentations draft 20
- `OidcSiopVerifier`: Add constructor parameter `attestationJwt` to create authentication requests as JWS with an Verifier Attestation JWT in header `jwt` (see OpenId4VP draft 20)
- `OidcSiopVerifier`: Rename `createAuthnRequestAsRequestObject()` to `createAuthnRequestAsSignedRequestObject()`, also changing the return type
- `OidcSiopWallet`: Rename constructor parameter `jwkSetRetriever` to a more general `remoteResourceRetriever`, to use it for various parameters defined by reference
- `OidcSiopWallet`: Replace constructor parameter `verifierJwsService` with `requestObjectJwsVerifier` to allow callers to verify JWS objects with a pre-registered key (as in the OpenId4VP client ID scheme "pre-registered")
Expand Down
2 changes: 1 addition & 1 deletion kmp-crypto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package at.asitplus.wallet.lib.oidc
import at.asitplus.KmmResult
import at.asitplus.crypto.datatypes.CryptoPublicKey
import at.asitplus.crypto.datatypes.jws.JsonWebKeySet
import at.asitplus.crypto.datatypes.jws.JwsHeader
import at.asitplus.crypto.datatypes.jws.JwsSigned
import at.asitplus.crypto.datatypes.jws.toJsonWebKey
import at.asitplus.wallet.lib.agent.CryptoService
Expand All @@ -27,6 +28,7 @@ import at.asitplus.wallet.lib.jws.DefaultVerifierJwsService
import at.asitplus.wallet.lib.jws.JwsService
import at.asitplus.wallet.lib.jws.VerifierJwsService
import at.asitplus.wallet.lib.oidc.OpenIdConstants.ClientIdSchemes.REDIRECT_URI
import at.asitplus.wallet.lib.oidc.OpenIdConstants.ClientIdSchemes.VERIFIER_ATTESTATION
import at.asitplus.wallet.lib.oidc.OpenIdConstants.ID_TOKEN
import at.asitplus.wallet.lib.oidc.OpenIdConstants.PREFIX_DID_KEY
import at.asitplus.wallet.lib.oidc.OpenIdConstants.SCOPE_OPENID
Expand Down Expand Up @@ -62,6 +64,11 @@ class OidcSiopVerifier(
private val verifierJwsService: VerifierJwsService,
timeLeewaySeconds: Long = 300L,
private val clock: Clock = Clock.System,
/**
* Verifier Attestation JWT (from OID4VP) to include (in header `jwt`) when creating request objects as JWS,
* to allow the Wallet to verify the authenticity of this Verifier.
*/
private val attestationJwt: JwsSigned? = null,
) {

private val timeLeeway = timeLeewaySeconds.toDuration(DurationUnit.SECONDS)
Expand All @@ -77,6 +84,7 @@ class OidcSiopVerifier(
jwsService: JwsService = DefaultJwsService(cryptoService),
timeLeewaySeconds: Long = 300L,
clock: Clock = Clock.System,
attestationJwt: JwsSigned? = null,
) = OidcSiopVerifier(
verifier = verifier,
relyingPartyUrl = relyingPartyUrl,
Expand All @@ -85,6 +93,7 @@ class OidcSiopVerifier(
verifierJwsService = verifierJwsService,
timeLeewaySeconds = timeLeewaySeconds,
clock = clock,
attestationJwt = attestationJwt,
)
}

Expand Down Expand Up @@ -232,8 +241,12 @@ class OidcSiopVerifier(
requestObject.copy(audience = relyingPartyUrl, issuer = relyingPartyUrl)
)
val signedJws = jwsService.createSignedJwsAddingParams(
header = JwsHeader(
algorithm = jwsService.algorithm,
attestationJwt = attestationJwt?.serialize(),
),
payload = requestObjectSerialized.encodeToByteArray(),
addKeyId = true
addJsonWebKey = true,
).getOrElse {
Napier.w("Could not sign JWS form authnRequest", it)
return KmmResult.failure(it)
Expand Down Expand Up @@ -273,7 +286,7 @@ class OidcSiopVerifier(
responseType = "$ID_TOKEN $VP_TOKEN",
clientId = relyingPartyUrl,
redirectUrl = relyingPartyUrl,
clientIdScheme = REDIRECT_URI,
clientIdScheme = attestationJwt?.let { VERIFIER_ATTESTATION } ?: REDIRECT_URI,
scope = listOfNotNull(SCOPE_OPENID, SCOPE_PROFILE, credentialScheme?.vcType).joinToString(" "),
nonce = uuid4().toString().also { challengeMutex.withLock { challengeSet += it } },
clientMetadata = metadata,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package at.asitplus.wallet.lib.oidc

import at.asitplus.crypto.datatypes.jws.JsonWebKeySet
import at.asitplus.crypto.datatypes.jws.JweAlgorithm
import at.asitplus.crypto.datatypes.jws.JwsAlgorithm
import at.asitplus.crypto.datatypes.jws.JwsSigned
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package at.asitplus.wallet.lib.oidc

import at.asitplus.crypto.datatypes.io.Base64UrlStrict
import at.asitplus.crypto.datatypes.jws.JsonWebKey
import at.asitplus.crypto.datatypes.jws.JsonWebToken
import at.asitplus.crypto.datatypes.jws.JwsHeader
import at.asitplus.crypto.datatypes.jws.JwsSigned
import at.asitplus.crypto.datatypes.jws.toJwsAlgorithm
import at.asitplus.wallet.lib.agent.CryptoService
import at.asitplus.wallet.lib.agent.DefaultCryptoService
import at.asitplus.wallet.lib.agent.Holder
Expand All @@ -11,6 +15,7 @@ import at.asitplus.wallet.lib.agent.Verifier
import at.asitplus.wallet.lib.agent.VerifierAgent
import at.asitplus.wallet.lib.data.AtomicAttribute2023
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.jws.DefaultJwsService
import at.asitplus.wallet.lib.jws.DefaultVerifierJwsService
import at.asitplus.wallet.lib.oidvci.OAuth2Exception
import at.asitplus.wallet.lib.oidvci.decodeFromPostBody
Expand All @@ -32,7 +37,9 @@ import io.kotest.matchers.types.shouldBeInstanceOf
import io.ktor.http.*
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import kotlin.random.Random
import kotlin.time.Duration.Companion.seconds

class OidcSiopProtocolTest : FreeSpec({

Expand Down Expand Up @@ -237,6 +244,63 @@ class OidcSiopProtocolTest : FreeSpec({
}
}

"test with request object and Attestation JWT" {
val sprsCryptoService = DefaultCryptoService()
val attestationJwt = buildAttestationJwt(sprsCryptoService, relyingPartyUrl, verifierCryptoService)
verifierSiop = OidcSiopVerifier.newInstance(
verifier = verifierAgent,
cryptoService = verifierCryptoService,
relyingPartyUrl = relyingPartyUrl,
attestationJwt = attestationJwt
)
val authnRequestWithRequestObject = verifierSiop.createAuthnRequestUrlWithRequestObject(
walletUrl = walletUrl,
credentialScheme = ConstantIndex.AtomicAttribute2023
).getOrThrow().also { println(it) }


holderSiop = OidcSiopWallet.newInstance(
holder = holderAgent,
cryptoService = holderCryptoService,
requestObjectJwsVerifier = verifierAttestationVerifier(sprsCryptoService.jsonWebKey)
)
val authnResponse =
holderSiop.createAuthnResponse(authnRequestWithRequestObject).getOrThrow()
authnResponse.shouldBeInstanceOf<OidcSiopWallet.AuthenticationResponseResult.Redirect>()
.also { println(it) }

val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf<OidcSiopVerifier.AuthnResponseResult.Success>()
result.vp.verifiableCredentials.shouldNotBeEmpty()
result.vp.verifiableCredentials.forEach {
it.vc.credentialSubject.shouldBeInstanceOf<AtomicAttribute2023>()
}
}
"test with request object and invalid Attestation JWT" {
val sprsCryptoService = DefaultCryptoService()
val attestationJwt = buildAttestationJwt(sprsCryptoService, relyingPartyUrl, verifierCryptoService)

verifierSiop = OidcSiopVerifier.newInstance(
verifier = verifierAgent,
cryptoService = verifierCryptoService,
relyingPartyUrl = relyingPartyUrl,
attestationJwt = attestationJwt
)
val authnRequestWithRequestObject = verifierSiop.createAuthnRequestUrlWithRequestObject(
walletUrl = walletUrl,
credentialScheme = ConstantIndex.AtomicAttribute2023
).getOrThrow().also { println(it) }

holderSiop = OidcSiopWallet.newInstance(
holder = holderAgent,
cryptoService = holderCryptoService,
requestObjectJwsVerifier = verifierAttestationVerifier(DefaultCryptoService().jsonWebKey)
)
shouldThrow<OAuth2Exception> {
holderSiop.createAuthnResponse(authnRequestWithRequestObject).getOrThrow()
}
}

"test with request object from request_uri as URL query parameters" {
val authnRequest = verifierSiop.createAuthnRequestUrl(
walletUrl = walletUrl,
Expand All @@ -259,8 +323,7 @@ class OidcSiopProtocolTest : FreeSpec({
}
)

val authnResponse =
holderSiop.createAuthnResponse(authRequestUrlWithRequestUri).getOrThrow()
val authnResponse = holderSiop.createAuthnResponse(authRequestUrlWithRequestUri).getOrThrow()
authnResponse.shouldBeInstanceOf<OidcSiopWallet.AuthenticationResponseResult.Redirect>()
.also { println(it) }

Expand Down Expand Up @@ -291,8 +354,7 @@ class OidcSiopProtocolTest : FreeSpec({
}
)

val authnResponse =
holderSiop.createAuthnResponse(authRequestUrlWithRequestUri).getOrThrow()
val authnResponse = holderSiop.createAuthnResponse(authRequestUrlWithRequestUri).getOrThrow()
authnResponse.shouldBeInstanceOf<OidcSiopWallet.AuthenticationResponseResult.Redirect>()
.also { println(it) }

Expand Down Expand Up @@ -324,13 +386,43 @@ class OidcSiopProtocolTest : FreeSpec({
)

shouldThrow<OAuth2Exception> {
holderSiop.createAuthnResponse(authRequestUrlWithRequestUri).getOrThrow().also {
println(it)
}
holderSiop.createAuthnResponse(authRequestUrlWithRequestUri).getOrThrow()
}
}
})

private suspend fun buildAttestationJwt(
sprsCryptoService: DefaultCryptoService,
relyingPartyUrl: String,
verifierCryptoService: CryptoService
): JwsSigned = DefaultJwsService(sprsCryptoService).createSignedJws(
header = JwsHeader(
algorithm = sprsCryptoService.algorithm.toJwsAlgorithm(),
),
payload = JsonWebToken(
issuer = "sprs", // allows Wallet to determine the issuer's key
subject = relyingPartyUrl,
issuedAt = Clock.System.now(),
expiration = Clock.System.now().plus(10.seconds),
notBefore = Clock.System.now(),
confirmationKey = verifierCryptoService.jsonWebKey,
).serialize().encodeToByteArray()
).getOrThrow()

private fun verifierAttestationVerifier(trustedKey: JsonWebKey) =
object : RequestObjectJwsVerifier {
override fun invoke(jws: JwsSigned, authnRequest: AuthenticationRequestParameters): Boolean {
val attestationJwt = jws.header.attestationJwt?.let { JwsSigned.parse(it) }
?: return false
val verifierJwsService = DefaultVerifierJwsService()
if (!verifierJwsService.verifyJws(attestationJwt, trustedKey))
return false
val verifierPublicKey = JsonWebToken.deserialize(attestationJwt.payload.decodeToString())
.getOrNull()?.confirmationKey ?: return false
return verifierJwsService.verifyJws(jws, verifierPublicKey)
}
}

private suspend fun verifySecondProtocolRun(
verifierSiop: OidcSiopVerifier,
walletUrl: String,
Expand Down

0 comments on commit 5b29770

Please sign in to comment.