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

Update OID4VCI implementation #52

Merged
merged 18 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Build klibs
run: ./gradlew iosArm64MainKlibrary iosX64MainKlibrary
- name: Run tests
run: ./gradlew iosX64Test
run: ./gradlew iosSimulatorArm64Test
- name: Test Report
uses: dorny/test-reporter@v1
if: success() || failure()
Expand Down
34 changes: 24 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
# Changelog

Release 3.6.0:
- `OidcSiopWallet.AuthenticationResponseResult.Post`: Replace property `body: String` with `params: Map<String, String>`, to be posted to the Relying Party. Clients may call extension function `at.asitplus.wallet.lib.oidvci.formUrlEncode` on `params` to get the encoded `body` for HTTP calls.
- 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
- `OidcSiopVerifier`: Add option to set `client_metadata_uri` instead of embedding client metadata in authentication requests
- `OidcSiopVerifier`: Refactor list of parameters for customizing authentication requests to single data class `RequestOptions`
- `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")
- Self-Issued OpenID Provider v2:
- `OidcSiopWallet.AuthenticationResponseResult.Post`: Replace property `body: String` with `params: Map<String, String>`, to be posted to the Relying Party. Clients may call extension function `at.asitplus.wallet.lib.oidvci.formUrlEncode` on `params` to get the encoded `body` for HTTP calls.
- 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
- `OidcSiopVerifier`: Add option to set `client_metadata_uri` instead of embedding client metadata in authentication requests
- `OidcSiopVerifier`: Refactor list of parameters for customizing authentication requests to single data class `RequestOptions`
- `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")
- Get rid of collections in serializable types and use sets instead
- OpenID for Verifiable Credential Issuance:
- Implement OpenID for Verifiable Credential Issuance draft 13, from 2024-02-08
- Rename `IssuerService` to `CredentialIssuer`
- Implement RFC 7636 Proof Key for Code Exchange for OpenID for Verifiable Credential Issuance implementations, i.e. `IssuerService`/`CredentialIssuer` and `WalletService`
- `IssuerService`/`CredentialIssuer`: Make public API functions suspending, also return `KmmResult` to transport exceptions
- `IssuerService`/`CredentialIssuer`: Change parameter of `credential()` from `authorizationHeader` to `accessToken`, requiring the plain access token
- `IssuerService`/`CredentialIssuer`: Extract responsibilities of an OAuth Authorizaiton Server into `AuthorizationService`
- `WalletService`: Make public API functions suspending
- `WalletService`: Implement proving possesion of private key with CBOR Web Tokens
- `WalletService`: Move constructor parameters to `requestOptions` for every method call
- Get rid of collections in serializable types and use sets instead
- Dependency updates
- Conventions 1.9.23+20240410
- Ktor 2.3.10
- Auto-publish version catalogs
- `Issuer`: Change `cryptoAlgorithms` from `Collection` to `Set`

Release 3.5.0:
- Kotlin 1.9.23
Expand Down
2 changes: 1 addition & 1 deletion kmp-crypto
1 change: 1 addition & 0 deletions vclib-openid/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ kotlin {

commonTest {
dependencies {
implementation("at.asitplus.wallet:eupidcredential:1.0.0")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,27 @@ data class AuthenticationRequestParameters(
@SerialName("iat")
@Serializable(with = InstantLongSerializer::class)
val issuedAt: Instant? = null,

/**
* RFC8707: In requests to the authorization server, a client MAY indicate the protected resource (a.k.a.
* resource server, application, API, etc.) to which it is requesting access. Its value MUST be an absolute URI,
* as specified by Section 4.3 of (RFC3986).
*/
@SerialName("resource")
val resource: String? = null,

/**
* RFC7636: A challenge derived from the code verifier that is sent in the authorization request, to be verified
* against later.
*/
@SerialName("code_challenge")
val codeChallenge: String? = null,

/**
* RFC7636: A method that was used to derive code challenge.
*/
@SerialName("code_challenge_method")
val codeChallengeMethod: String? = null,
) {

fun serialize() = jsonSerializer.encodeToString(this)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package at.asitplus.wallet.lib.oidc

/**
* Possible outcomes of creating the OIDC Authentication Response
*/
sealed class AuthenticationResponseResult {
/**
* Wallet returns the [AuthenticationResponseParameters] as form parameters, which shall be posted to
* `redirect_uri` of the Relying Party, i.e. clients should execute that POST with [params] to [url].
*/
data class Post(val url: String, val params: Map<String, String>) : AuthenticationResponseResult()

/**
* Wallet returns the [AuthenticationResponseParameters] as fragment parameters appended to the
* `redirect_uri` of the Relying Party, i.e. clients should simply open the [url]. The [params] are also included
* for further use.
*/
data class Redirect(val url: String, val params: AuthenticationResponseParameters) : AuthenticationResponseResult()
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
package at.asitplus.wallet.lib.oidc

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@Serializable(with = IdTokenTypeSerializer::class)
enum class IdTokenType(val text: String) {
JesusMcCloud marked this conversation as resolved.
Show resolved Hide resolved

SUBJECT_SIGNED("subject_signed_id_token"),
ATTESTER_SIGNED("attester_signed_id_token")

}
}

object IdTokenTypeSerializer : KSerializer<IdTokenType> {

override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("IdTokenType", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: IdTokenType) {
encoder.encodeString(value.text)
}

override fun deserialize(decoder: Decoder): IdTokenType {
val decoded = decoder.decodeString()
return IdTokenType.entries.first { it.text == decoded }
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -85,34 +85,17 @@ class OidcSiopWallet(
)
}

/**
* Possible outcomes of creating the OIDC Authentication Response
*/
sealed class AuthenticationResponseResult {
/**
* Wallet returns the [AuthenticationResponseParameters] as form parameters, which shall be posted to
* `redirect_uri of the Relying Party, i.e. clients should execute that POST with [params] to [url].
*/
data class Post(val url: String, val params: Map<String, String>) : AuthenticationResponseResult()

/**
* Wallet returns the [AuthenticationResponseParameters] as fragment parameters appended to the
* `redirect_uri` of the Relying Party, i.e. clients should simply open the [url].
*/
data class Redirect(val url: String) : AuthenticationResponseResult()
}

val metadata: IssuerMetadata by lazy {
IssuerMetadata(
issuer = clientId,
authorizationEndpointUrl = clientId,
responseTypesSupported = arrayOf(ID_TOKEN),
scopesSupported = arrayOf(SCOPE_OPENID),
subjectTypesSupported = arrayOf("pairwise", "public"),
idTokenSigningAlgorithmsSupported = arrayOf(jwsService.algorithm.identifier),
requestObjectSigningAlgorithmsSupported = arrayOf(jwsService.algorithm.identifier),
subjectSyntaxTypesSupported = arrayOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY),
idTokenTypesSupported = arrayOf(IdTokenType.SUBJECT_SIGNED),
responseTypesSupported = setOf(ID_TOKEN),
scopesSupported = setOf(SCOPE_OPENID),
subjectTypesSupported = setOf("pairwise", "public"),
idTokenSigningAlgorithmsSupported = setOf(jwsService.algorithm.identifier),
requestObjectSigningAlgorithmsSupported = setOf(jwsService.algorithm.identifier),
subjectSyntaxTypesSupported = setOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY),
idTokenTypesSupported = setOf(IdTokenType.SUBJECT_SIGNED),
presentationDefinitionUriSupported = false,
)
}
Expand Down Expand Up @@ -246,7 +229,7 @@ class OidcSiopWallet(
}
}
.buildString()
KmmResult.success(AuthenticationResponseResult.Redirect(url))
KmmResult.success(AuthenticationResponseResult.Redirect(url, responseParams))
}

else -> {
Expand All @@ -256,7 +239,7 @@ class OidcSiopWallet(
val url = URLBuilder(request.redirectUrl)
.apply { encodedFragment = responseParams.encodeToParameters().formUrlEncode() }
.buildString()
KmmResult.success(AuthenticationResponseResult.Redirect(url))
KmmResult.success(AuthenticationResponseResult.Redirect(url, responseParams))
}
}
},
Expand All @@ -277,6 +260,7 @@ class OidcSiopWallet(
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.INVALID_REQUEST))
.also { Napier.w("client_id_scheme is redirect_uri, but metadata is not set") }
}
// TODO implement x509_san_dns, x509_san_uri, as implemented by EUDI verifier
val clientMetadata = params.clientMetadata
?: params.clientMetadataUri?.let { uri ->
remoteResourceRetriever.invoke(uri)?.let { RelyingPartyMetadata.deserialize(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ object OpenIdConstants {

const val GRANT_TYPE_CODE = "code"

const val GRANT_TYPE_PRE_AUTHORIZED_CODE = "urn:ietf:params:oauth:grant-type:pre-authorized_code"

const val TOKEN_PREFIX_BEARER = "Bearer "

const val TOKEN_TYPE_BEARER = "bearer"
Expand All @@ -24,6 +26,8 @@ object OpenIdConstants {

const val SCOPE_PROFILE = "profile"

const val CODE_CHALLENGE_METHOD_SHA256 = "S256"

/**
* To be used in [at.asitplus.wallet.lib.oidvci.AuthorizationDetails.type]
*/
Expand All @@ -36,7 +40,20 @@ object OpenIdConstants {
*/
const val JWT = "jwt"

/**
* Proof type in [at.asitplus.wallet.lib.oidvci.CredentialRequestProof]
*/
const val CWT = "cwt"

/**
* Constant from OID4VCI
*/
const val JWT_HEADER_TYPE = "openid4vci-proof+jwt"

/**
* Constant from OID4VCI
*/
const val CWT_HEADER_TYPE = "openid4vci-proof+cwt"
}

/**
Expand Down Expand Up @@ -145,6 +162,11 @@ object OpenIdConstants {
*/
const val INVALID_REQUEST = "invalid_request"

/**
* Invalid grant: `invalid_grant`
*/
const val INVALID_GRANT = "invalid_grant"

/**
* Invalid or missing proofs in OpenId4VCI: `invalid_or_missing_proof`
*/
Expand Down
Loading
Loading