diff --git a/.github/workflows/test-jvm.yml b/.github/workflows/test-jvm.yml
index 4873b0fed..bded76a39 100644
--- a/.github/workflows/test-jvm.yml
+++ b/.github/workflows/test-jvm.yml
@@ -13,7 +13,7 @@ jobs:
distribution: 'temurin'
java-version: '17'
- name: Run tests
- run: ./gradlew -Dpublishing.excludeIncludedBuilds=true jvmTest
+ run: ./gradlew jvmTest
- name: Test Report
uses: dorny/test-reporter@v1
if: success() || failure()
diff --git a/.gitignore b/.gitignore
index 49f65a680..56355301b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,8 @@ local.properties
!**/src/test/**/build/
.kotlin
+**/src/androidInstrumentedTest/
+
### STS ###
.apt_generated
.classpath
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d411742da..2c09f49aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,63 @@
# Changelog
+Release 5.0.0:
+ - Remove `OidcSiopWallet.newDefaultInstance()` and replace it with a constructor
+ - Remove `OidcSiopVerifier.newInstance()` methods and replace them with constructors
+ - Remove `Validator.newDefaultInstance()` methods and replace them with constructors
+ - Remove `WalletService.newDefaultInstance()` methods and replace them with constructors
+ * Add `TransactionDataEntry` class
+ * Add `DocumentDigestEntry` class
+ * Add `DocumentDigestEntryCSC` class
+ * Add `DocumentLocationsEntry` class
+ * Add `Method` class
+ * Update `InputDescriptors`
+ * New member `transaction_data`
+ * Removed member `schema`
+ * Update `AuthorizationDetails`
+ * Now sealed class with subclasses
+ * `OpenIdCredential`
+ * `CSCCredential`
+ * Extend `AuthenticationRequestParameters` to be able to handle CSC/QES flows
+ * Extend `TokenRequestParameters` to be able to handle CSC/QES flows
+ * Extend `TokenResponseParameters` to be able to handle CSC/QES flows
+ - In `TokenRequestParameters`, change `transactionCode` to `String`, as it needs to be entered by the user potentially
+ - Add extension method to build DPoP headers acc. to [RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449), see `WalletService`
+ * Proper registration of serializers for ISO credentials (breaking change), see API in `LibraryInitializer`
+ * Update dependencies to have everything aligned with Kotlin 2.0.20:
+ * Kotlin 2.0.20
+ * EU PID + MDL Credentials in test scope
+ * Serialization 1.7.2 proper
+ * JsonPath4K 2.3.0 (with proper Kotlin 2.0.20 support)
+ * Signum 3.7.0 (only dependency updates to align everything, no alignments in code)
+ * Add `KeyStoreMaterial` to JVM target for convenience
+ - Update implementation of [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html) to draft 14 from 2024-08-21
+ - Move some fields from `IssuerMetadata` to `OAuth2AuthorizationServerMetadata` to match the semantics
+ - Remove proof type `cwt` for OpenID for Verifiable Credential Issuance, as per draft 14, but keep parsing it for a bit of backwards-compatibility
+ - Remove binding method for `did:key`, as it was never completely implemented, but add binding method `jwk` for JSON Web Keys.
+ - Rework interface of `WalletService` to make selecting the credential configuration by its ID more explicit
+ - Support requesting issuance of credential using scope values
+ - Introudce `OAuth2Client` to extract creating authentication requests and token requests from OID4VCI `WalletService`
+ - Refactor `SimpleAuthorizationService` to extract actual authentication and authorization into `AuthorizationServiceStrategy`
+ - Implement JWE encryption with AES-CBC-HMAC algorithms
+ - SIOPv2/OpenID4VP: Support requesting and receiving claims from different credentials, i.e. a combined presentation
+ - Require request options on every method in `OidcSiopVerifier`
+ - Move `credentialScheme`, `representation`, `requestedAttributes` from `RequestOptions` to `RequestOptionsCredentials`
+ - In `OidcSiopVerifier` move `responseUrl` from constructor parameter to `RequestOptions`
+ - Add `IdToken` as result case to `OidcSiopVerifier.AuthnResponseResult`, when only an `id_token` is requested and received
+ - Disclosures for SD-JWT (in class `SelectiveDisclosureItem`) now contain a `JsonPrimitive` for the value, so that implementers can deserialize the value accordingly
+
+Release 4.1.2:
+ * In `OidcSiopVerifier` add parameter `nonceService` to externalize creation and validation of nonces, e.g. for deployments in load-balanced environments
+ * In `SimpleAuthorizationService` change type of `tokenService` to `NonceService`
+ * Add constructor parameters to `SimpleAuthorizationService` to externalize storage of maps, e.g. for deployments in load-balanced environments
+ * Add constructor parameter to `WalletService` to externalize storage of state-to-code map, e.g. for deployments in load-balanced environments
+* Update to latest Signum for KMP signer and verifier.
+* Update dependencies:
+ * Kotlin 2.0.20
+ * Serialization 1.7.2 stable
+ * JsonPath4K 2.3.0
+* Add Android targets
+
Release 4.1.1 (Bugfix Release):
* correctly configure and name JSON serializer:
* `jsonSerializer` -> `vckJsonSerializer`
diff --git a/README.md b/README.md
index 45bbfdc6e..65454105e 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
[![A-SIT Plus Official](https://img.shields.io/badge/A--SIT_Plus-official-005b79?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNDMuNzYyODYgMTg0LjgxOTk5Ij48ZGVmcz48Y2xpcFBhdGggaWQ9ImEiIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48cGF0aCBkPSJNMCA1OTUuMjhoODQxLjg5VjBIMFoiLz48L2NsaXBQYXRoPjwvZGVmcz48ZyBjbGlwLXBhdGg9InVybCgjYSkiIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMyAwIDAgLTEuMzMzMzMzMyAtNDgyLjI1IDUxNy41MykiPjxwYXRoIGZpbGw9IiMwMDViNzkiIGQ9Ik00MTUuNjcgMjQ5LjUzYy03LjE1LjA4LTEzLjk0IDEtMjAuMTcgMi43NWE1Mi4zMyA1Mi4zMyAwIDAgMC0xNy40OCA4LjQ2IDQwLjQzIDQwLjQzIDAgMCAwLTExLjk2IDE0LjU2Yy0yLjY4IDUuNDEtNC4xNCAxMS44NC00LjM1IDE5LjA5bC0uMDIgNi4xMnYyLjE3YS43MS43MSAwIDAgMCAuNy43M2gxNi41MmMuMzkgMCAuNy0uMzIuNzEtLjdsLjAxLTIuMmMwLTIuNi4wMi01LjgyLjAzLTYuMDcuMi00LjYgMS4yNC04LjY2IDMuMDgtMTIuMDZhMjguNTIgMjguNTIgMCAwIDEgOC4yMy05LjU4IDM1LjI1IDM1LjI1IDAgMCAxIDExLjk2LTUuNTggNTUuMzggNTUuMzggMCAwIDEgMTIuNTgtMS43NmM0LjMyLjEgOC42LjcgMTIuNzQgMS44YTM1LjA3IDM1LjA3IDAgMCAxIDExLjk2IDUuNTcgMjguNTQgMjguNTQgMCAwIDEgOC4yNCA5LjU3YzEuOTYgMy42NCAzIDguMDIgMy4xMiAxMy4wMnYyNC4wOUgzNjIuNGEuNy43IDAgMCAwLS43MS43VjMzNWMwIDguNDMuMDEgOC4wNS4wMSA4LjE0LjIgNy4zIDEuNjcgMTMuNzcgNC4zNiAxOS4yMmE0MC40MyA0MC40MyAwIDAgMCAxMS45NiAxNC41N2M1IDMuNzYgMTAuODcgNi42MSAxNy40OCA4LjQ2YTc3LjUgNzcuNSAwIDAgMCAyMC4wMiAyLjc3YzcuMTUtLjA3IDEzLjk0LTEgMjAuMTctMi43NGE1Mi4zIDUyLjMgMCAwIDAgMTcuNDgtOC40NiA0MC40IDQwLjQgMCAwIDAgMTEuOTUtMTQuNTdjMS42Mi0zLjI2IDMuNzctMTAuMDQgMy43Ny0xNC42OCAwLS4zOC0uMTctLjc0LS41NC0uODJsLTE2Ljg5LS40Yy0uMi0uMDQtLjM0LjM0LS4zNC41NCAwIC4yNy0uMDMuNC0uMDYuNi0uNSAyLjgyLTEuMzggNS40LTIuNjEgNy42OWEyOC41MyAyOC41MyAwIDAgMS04LjI0IDkuNTggMzUuMDEgMzUuMDEgMCAwIDEtMTEuOTYgNS41NyA1NS4yNSA1NS4yNSAwIDAgMS0xMi41NyAxLjc3Yy00LjMyLS4xLTguNjEtLjcxLTEyLjc1LTEuOGEzNS4wNSAzNS4wNSAwIDAgMS0xMS45Ni01LjU3IDI4LjUyIDI4LjUyIDAgMCAxLTguMjMtOS41OGMtMS44Ni0zLjQ0LTIuOS03LjU1LTMuMDktMTIuMmwtLjAxLTcuNDdoODkuMTZhLjcuNyAwIDAgMCAuNy0uNzJ2LTM5LjVjLS4xLTcuNjUtMS41OC0xNC40LTQuMzgtMjAuMDZhNDAuNCA0MC40IDAgMCAwLTExLjk1LTE0LjU2IDUyLjM3IDUyLjM3IDAgMCAwLTE3LjQ4LTguNDcgNzcuNTYgNzcuNTYgMCAwIDAtMjAuMDEtMi43N1oiLz48cGF0aCBmaWxsPSIjY2U0OTJlIiBkPSJNNDE5LjM4IDI4MC42M2gtNy41N2EuNy43IDAgMCAwLS43MS43MXYxNS40MmE4LjE3IDguMTcgMCAwIDAtMy43OCA2LjkgOC4yOCA4LjI4IDAgMCAwIDE2LjU0IDAgOC4yOSA4LjI5IDAgMCAwLTMuNzYtNi45di0xNS40MmEuNy43IDAgMCAwLS43Mi0uNzEiLz48L2c%2BPC9zdmc%2B&logoColor=white&labelColor=white)](https://a-sit-plus.github.io)
[![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-brightgreen.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0)
[![Kotlin](https://img.shields.io/badge/kotlin-multiplatform--mobile-orange.svg?logo=kotlin)](http://kotlinlang.org)
-[![Kotlin](https://img.shields.io/badge/kotlin-2.0.0-blue.svg?logo=kotlin)](http://kotlinlang.org)
+[![Kotlin](https://img.shields.io/badge/kotlin-2.0.20-blue.svg?logo=kotlin)](http://kotlinlang.org)
[![Java](https://img.shields.io/badge/java-17-blue.svg?logo=OPENJDK)](https://www.oracle.com/java/technologies/downloads/#java17)
[![Maven Central](https://img.shields.io/maven-central/v/at.asitplus.wallet/vck)](https://mvnrepository.com/artifact/at.asitplus.wallet/vck)
@@ -21,70 +21,62 @@
This library implements verifiable credentials to support several use cases, i.e. issuing of credentials, presentation of credentials and validation thereof. This library may be shared between backend services issuing credentials, wallet apps holding credentials, and verifier apps validating them.
-Credentials may be represented in the [W3C VC Data Model](https://w3c.github.io/vc-data-model/) or as ISO credentials according to [ISO/IEC 18013-5:2021](https://www.iso.org/standard/69084.html). Issuing may happen according to [ARIES RFC 0453 Issue Credential V2](https://github.com/hyperledger/aries-rfcs/tree/main/features/0453-issue-credential-v2) or with [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html). Presentation of credentials may happen according to [ARIES RFC 0454 Present Proof V2](https://github.com/hyperledger/aries-rfcs/tree/main/features/0454-present-proof-v2) or with [Self-Issued OpenID Provider v2](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html), supporting [OpenID for Verifiable Presentations](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html).
## Architecture
This library was built with [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) and [Multiplatform Mobile](https://kotlinlang.org/lp/mobile/) in mind. Its primary targets are JVM, Android and iOS. In order to achieve smooth usage especially under iOS, there have been some notable design decisions:
- Code interfacing with client implementations uses the return type `KmmResult` to transport the `Success` case (i.e. a custom data type) as well as potential errors from native implementations as a `Failure`.
- - Native implementations can be plugged in by implementing interfaces, e.g. `CryptoService` and `KeyPairAdapter`, as opposed to callback functions.
+ - Native implementations can be plugged in by implementing interfaces, e.g. `PlatformCryptoShim` and `KeyMaterial`, as opposed to callback functions.
- Use of primitive data types for constructor properties instead of e.g. [kotlinx datetime](https://github.com/Kotlin/kotlinx-datetime) types.
- - This library provides some "default" implementations, e.g. `DefaultCryptoService` to test as much code as possible from the `commonMain` module.
- - Some classes feature additional constructors or factory methods with a shorter argument list because the default arguments are lost when called from Swift.
+ - As much code as possible is implemented in the `commonMain` module
Notable features for multiplatform are:
- Use of [Napier](https://github.com/AAkira/Napier) as the logging framework
- Use of [Kotest](https://kotest.io/) for unit tests
- Use of [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) for date and time classes
- - Use of [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization) for serialization from/to JSON and CBOR (extended CBOR functionality in [our fork of kotlinx.serialization](https://github.com/a-sit-plus/kotlinx.serialization/))
+ - Use of [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization) for serialization from/to JSON and CBOR
- Implementation of a ZLIB service in Kotlin with native parts, see `ZlibService`
- Implementation of JWS and JWE operations in pure Kotlin (delegating to native crypto), see `JwsService`
- - Abstraction of several cryptographic primitives, to be implemented in native code, see `CryptoService`
- Implementation of COSE operations in pure Kotlin (delegating to native crypto), see `CoseService`
Some parts for increased multiplatform support have been extracted into separate repositories:
- Reimplementation of Kotlin's [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/) called [KmmResult](https://github.com/a-sit-plus/kmmresult) for easy use from Swift code (since inline classes are [not supported](https://kotlinlang.org/docs/native-objc-interop.html#unsupported)).
- - Several crypto datatypes including an ASN.1 parser and encoder called [Signum](https://github.com/a-sit-plus/signum).
+ - Several crypto datatypes (including an ASN.1 parser and encoder), as well as a mulitplatform crypto library, called [Signum](https://github.com/a-sit-plus/signum).
The main entry point for applications is an instance of `HolderAgent`, `VerifierAgent` or `IssuerAgent`, according to the nomenclature from the [W3C VC Data Model](https://w3c.github.io/vc-data-model/).
Many classes define several constructor parameters, some of them with default values, to enable a simple form of dependency injection. Implementers are advised to specify the parameter names of arguments passed to increase readability and prepare for future extensions.
-### Aries
+## Features
-A single run of an [ARIES protocol](https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0003-protocols/README.md) (for issuing or presentation) is implemented by the `*Protocol` classes, whereas the `*Messenger` classes should be used by applications to manage several runs of a protocol. These classes reside in the artifact `vck-aries`.
+Credentials may be represented as plain JWTs in the [W3C VC Data Model](https://w3c.github.io/vc-data-model/), as ISO mDoc credentials according to [ISO/IEC 18013-5:2021](https://www.iso.org/standard/69084.html), or simply as a list of claims and values for [SD-JWT](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/).
-### OpenId
+When using the plain JWT representation, the single credential itself is an instance of `CredentialSubject`. For ISO mDoc claims see `IssuerSignedItems` and related classes like `Document` and `MobileSecurityObject`. For SD-JWT claims see `SelectiveDisclosureItem` and `SdJwtSigned`.
-For SIOPv2 see `OidcSiopProtocol`, and for OpenId4VCI see `WalletService`. Most code resides in the artifact/subdirectory `vck-openid`. Both protocols are able to transport credentials as plain JWTs, SD-JWT or ISO 18013-5.
+Other libraries implementing credential schemes may call `LibraryInitializer.registerExtensionLibrary()` to register with this library. See our implementation of the [EU PID credential](https://github.com/a-sit-plus/eu-pid-credential) or our implementation of the [Mobile Driving Licence](https://github.com/a-sit-plus/mobile-driving-licence-credential/) for examples. We also maintain a comprehensive list of [all credentials powered by this library](https://github.com/a-sit-plus/credentials-collection).
-## Limitations
-
- - For Verifiable Credentials and Presentations, only the JWT proof mechanism is implemented.
- - Json Web Keys always use a `kid` of `did:key:mEpA...` with a custom, uncompressed representation of `secp256r1` keys.
- - Several parts of the W3C VC Data Model have not been fully implemented, i.e. everything around resolving cryptographic key material.
- - Anything related to ledgers (e.g. resolving DID documents) is out of scope.
- - Cryptographic operations are implemented for EC cryptography on the `secp256r1` curve to fully support hardware-backed keys on Android and iOS. However, the enum classes for cryptographic primitives may be extended to support other algorithms.
-
-## iOS Implementation
+Two [ARIES protocols](https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0003-protocols/README.md) are implemented: Issuing of credentials according to [ARIES RFC 0453 Issue Credential V2](https://github.com/hyperledger/aries-rfcs/tree/main/features/0453-issue-credential-v2), see `IssueCredentialProtocol`. Presentation of credential according to [ARIES RFC 0454 Present Proof V2](https://github.com/hyperledger/aries-rfcs/tree/main/features/0454-present-proof-v2), see `PresentProofProtocol`.
-The `DefaultCryptoService` for iOS should not be used in production as it does not implement encryption, decryption, key agreement and message digests correctly. See the [Swift Package](https://github.com/a-sit-plus/swift-package-kmm-vc-library) for details on a more correct iOS implementation.
+For the OpenID protocol family, issuing is implemented using [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html), see `WalletService` and `CredentialIssuer`. Presentation of credentials is implemented using [Self-Issued OpenID Provider v2](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html), supporting [OpenID for Verifiable Presentations](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html), see `OidcSiopVerifier` and `OidcSiopWallet`.
-## Credentials
-A single credential itself is an instance of `CredentialSubject` (when using the plain JWT representation with ECDSA signatures) and has no special meaning attached to it. This library implements `AtomicAttribute2023` as a trivial sample of a custom credential. For [Selective Disclosure JWT](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/) and ISO representations, only the claims (holding names and values) exist, without any data class holding the values.
+## Limitations
-Other libraries using this library may call `LibraryInitializer.registerExtensionLibrary()` to register that extension with this library. See our implementation of the [EU PID credential](https://github.com/a-sit-plus/eu-pid-credential) or our implementation of the [Mobile Driving Licence](https://github.com/a-sit-plus/mobile-driving-licence-credential/) for examples. We also maintain a comprehensive list of all credentials powered by this library [over here](https://github.com/a-sit-plus/credentials-collection).
+ - Several parts of the W3C VC Data Model have not been fully implemented, i.e. everything around resolving cryptographic key material.
+ - Anything related to ledgers (e.g. resolving DID documents) is out of scope.
+ - Trust relationships are mostly up to clients using this library.
+ - The `PlatformCryptoShim` for iOS should not be used in production as it does not implement encryption, decryption, key agreement and message digests correctly. See the [Swift Package](https://github.com/a-sit-plus/swift-package-kmm-vc-library) for details on a more correct iOS implementation, or track the progress for [Signum milestone 1](https://github.com/a-sit-plus/signum/milestone/1).
## Dataflow for OID4VCI
We'll present an issuing process according to [OID4VCI](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html), along with [OID4VP](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html), with all terms taken from there.
-The credential issuer serves the following metadata:
+
+The credential issuer serves the following metadata:
-```
+```json
{
"issuer": "https://wallet.a-sit.at/credential-issuer",
"credential_issuer": "https://wallet.a-sit.at/credential-issuer",
@@ -156,10 +148,12 @@ The credential issuer serves the following metadata:
}
}
```
+
-The credential issuer starts with a credential offer:
+
+The credential issuer starts with a credential offer:
-```
+```json
{
"credential_issuer": "https://wallet.a-sit.at/credential-issuer",
"credential_configuration_ids": [
@@ -179,10 +173,12 @@ The credential issuer starts with a credential offer:
}
}
```
+
-Since the issuer gives an pre-authorized code, the wallet uses this for the token request:
+
+Since the issuer provides a pre-authorized code, the wallet uses this for the token request:
-```
+```json
{
"grant_type": "urn:ietf:params:oauth:grant-type:pre-authorized_code",
"redirect_uri": "https://wallet.a-sit.at/app/callback",
@@ -197,10 +193,12 @@ Since the issuer gives an pre-authorized code, the wallet uses this for the toke
"pre-authorized_code": "2c74a00b-70b8-4062-9757-75652174bc5d"
}
```
+
-The credential issuer answers with an access token:
+
+The credential issuer answers with an access token:
-```
+```json
{
"access_token": "413ed326-107b-4429-8efa-872cb89949d8",
"token_type": "bearer",
@@ -209,10 +207,12 @@ The credential issuer answers with an access token:
"authorization_details": []
}
```
+
-The wallet creates a credential request, including a proof-of-posession of its private key:
+
+The wallet creates a credential request, including a proof-of-possession of its private key:
-```
+```json
{
"format": "vc+sd-jwt",
"vct": "AtomicAttribute2023",
@@ -222,10 +222,12 @@ The wallet creates a credential request, including a proof-of-posession of its p
}
}
```
+
-The JWT included decodes to the following:
+
+The JWT included decodes to the following:
-```
+```json
{
"typ": "openid4vci-proof+jwt",
"alg": "ES256",
@@ -236,7 +238,11 @@ The JWT included decodes to the following:
"y": "bRFQx-fxoF9DI97TZdPfcXQyDcEtxD2vSS5cY0DGarY"
}
}
-.
+```
+
+and
+
+```json
{
"iss": "https://wallet.a-sit.at/app",
"aud": "https://wallet.a-sit.at/credential-issuer",
@@ -244,25 +250,23 @@ The JWT included decodes to the following:
"iat": 1720422537
}
```
+
-The credential issuer issues the following credential:
+
+The credential issuer issues the following credential:
-```
+```json
{
"format": "vc+sd-jwt",
"credential": "eyJraWQiOiJkaWQ6a2V5OnpEbmFleVlrcDlqZ0hjN3lheEcybkZTMXc4MXJISkVyS3hZSkVtTExVRTJVU05wWmUiLCJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJkaWQ6a2V5OnpEbmFlVEN2aDdZS1N5b21hUnFONFZ3TnZSZFRLdzJBakVkR0VkcndtNHZEUXU5RXciLCJuYmYiOjE3MjA0MjI1MzcsImlzcyI6ImRpZDprZXk6ekRuYWV5WWtwOWpnSGM3eWF4RzJuRlMxdzgxckhKRXJLeFlKRW1MTFVFMlVTTnBaZSIsImV4cCI6MTcyMDQyMjU5NywiaWF0IjoxNzIwNDIyNTM3LCJqdGkiOiJ1cm46dXVpZDpkODE1ZTEwZC1hNDRkLTQxNDQtYmZhNS05Zjk5MTNjZjE5ZmUiLCJfc2QiOlsiaHBNUTFpeWV0cTkzeWVCNG5FZXVmeDYweHY0WTVqbHFWcThIc2tJc1pZMCIsIkFHR1hZRTlIeXF1bGxTSF9iTC02dkRTSEhyZU9HckRWUVVOdEVQM3p1QlEiLCJva2VaSElRQkNQVlVxdUo4VmI3bHd2bWtLWnNmUktKVUE3X0VOMlZjX2M0Il0sInZjdCI6IkF0b21pY0F0dHJpYnV0ZTIwMjMiLCJzdGF0dXMiOnsiaWQiOiJodHRwczovL3dhbGxldC5hLXNpdC5hdC9iYWNrZW5kL2NyZWRlbnRpYWxzL3N0YXR1cy8xIzMiLCJ0eXBlIjoiUmV2b2NhdGlvbkxpc3QyMDIwU3RhdHVzIiwicmV2b2NhdGlvbkxpc3RJbmRleCI6MywicmV2b2NhdGlvbkxpc3RDcmVkZW50aWFsIjoiaHR0cHM6Ly93YWxsZXQuYS1zaXQuYXQvYmFja2VuZC9jcmVkZW50aWFscy9zdGF0dXMvMSJ9LCJfc2RfYWxnIjoic2hhLTI1NiIsImNuZiI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwia2lkIjoidXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYTI1NjpaaC1yNlZsWE5UQUZheTVYVjFzWFpJUG5mZjdHcGVZMFAzTHNPVDJmSFlRPSIsIngiOiJLVHZaVGlfbzF5NzlERDBKTUhqdnJhSmRpcURYak1mVkhlU1h6ZURVVXhnIiwieSI6ImJSRlF4LWZ4b0Y5REk5N1RaZFBmY1hReURjRXR4RDJ2U1M1Y1kwREdhclkifX0.oDdilbVBmoRpy162gcDR8a0vvYHlP7LXvJ3gxmjz4dYKRLhoMwM_tIcu0Dy5_ftXPq5IO1p9GYMkfUhHk881kw~WyI4cWRNYkN6SWZGajQxT1ZVUE13OEI5R2xaN2tiemxxaXg5T1RSU2huWGgwIiwiZ2l2ZW5fbmFtZSIsIkVyaWthIl0~WyIxUDdYQjB5eFFjaVBlZkVrV2o5R2N0MzNUYVZXeGNnVER1N19aTGptWG13IiwiZmFtaWx5X25hbWUiLCJNdXN0ZXJmcmF1Il0~WyJ3a2VlWG4wejAwa2tiaHVaeVBXM2dwZVBpOXhSVWU1cmVrQ3Npc2d6ZXg4Iiwic3ViamVjdCIsInN1YmplY3QiXQ"
}
```
+
-The SD-JWT included decodes to the following:
+
+The SD-JWT included decodes to the following:
-```
-{
- "kid": "did:key:zDnaeyYkp9jgHc7yaxG2nFS1w81rHJErKxYJEmLLUE2USNpZe",
- "typ": "vc+sd-jwt",
- "alg": "ES256"
-}
-.
+```json
{
"sub": "did:key:zDnaeTCvh7YKSyomaRqN4VwNvRdTKw2AjEdGEdrwm4vDQu9Ew",
"nbf": 1720422537,
@@ -292,20 +296,23 @@ The SD-JWT included decodes to the following:
}
}
```
+
-with the following claims appended:
+
+with the following claims appended:
-```
+```json
["8qdMbCzIfFj41OVUPMw8B9GlZ7kbzlqix9OTRShnXh0","given_name","Erika"]
```
-```
+```json
["1P7XB0yxQciPefEkWj9Gct33TaVWxcgTDu7_ZLjmXmw","family_name","Musterfrau"]
```
-```
+```json
["wkeeXn0z00kkbhuZyPW3gpePi9xRUe5rekCsisgzex8","date_of_birth","1980-01-13"]
```
+
@@ -313,24 +320,29 @@ with the following claims appended:
We'll present a presentation process according to [SIOPv2](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html), along with [OID4VP](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html), with all terms taken from there.
-The verifier creates a URL to be displayet to the wallet, containing a reference to the authentication request itself:
+
+The verifier creates a URL to be displayed to the wallet, containing a reference to the authentication request itself:
```
https://wallet.a-sit.at/mobile
?client_id=https://example.com/rp
&request_uri=https://example.com/request/15a4e87c-8e3e-4368-87a3-48083bac7232
```
+
-The authentication request is signed as a JWS and contains the following header and payload:
+
+The authentication request is signed as a JWS and contains the following header and payload:
-```
+```json
{
"alg": "ES256",
"x5c": [
"MIIBNDCB2qADAgECAgjm7Gfdm0IFUDAKBggqhkjOPQQDAjASMRAwDgYDVQQDDAdEZWZhdWx0MB4XDTI0MDcxNzEzNTUzMVoXDTI0MDcxNzEzNTYwMVowEjEQMA4GA1UEAwwHRGVmYXVsdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCaD9zTN05m6zZSX+QrQzdF4l1Bt3pVSo6VV9VFUSFCkImDkSsyCbgkJoZFkUNObbMiZbccQk6H33HAfxGWpLTyjGjAYMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMAoGCCqGSM49BAMCA0kAMEYCIQDSotU4YnsO0Ar5+barXkCn3PQlCaPJEnBwRL60SnHW9wIhAK5mLYgfslZvT5jFmMrS3Ivzm2EpNyjdCPGVT3wpt6/t"
]
}
-.
+```
+
+```json
{
"response_type": "id_token vp_token",
"client_id": "example.com",
@@ -410,14 +422,17 @@ The authentication request is signed as a JWS and contains the following header
"iss": "https://example.com/rp"
}
```
-
The certificate includes the X.509 SAN extension with the value `example.com`.
+
The wallet posts back the response to `https://example.com/response` with the parameters `presentation_submission`, `vp_token` and `state`.
+
+
The value for `presentation_submission` is:
+
-```
+```json
{
"id": "be3972bc-18cf-4e24-abf4-fe49eb870bab",
"definition_id": "b0611eea-89e4-4b78-8f44-40a2cb608d4a",
@@ -430,16 +445,20 @@ The value for `presentation_submission` is:
]
}
```
+
-The value for `vp_token` is an SD-JWT, with header and payload:
+
+The value for `vp_token` is an SD-JWT, with header and payload:
-```
+```json
{
"kid": "did:key:zDnaezoC5xdfzSdKoCZCZiw52eqnTks3xMKMDRTFKfSiq9obD",
"typ": "vc+sd-jwt",
"alg": "ES256"
}
-.
+```
+
+```json
{
"sub": "did:key:zDnaexYu9PCJsDmg7YwyBCJR2qQT8DxqwXhm4BLAJVWUgW1Ms",
"nbf": 1721224531,
@@ -467,24 +486,28 @@ The value for `vp_token` is an SD-JWT, with header and payload:
}
}
```
+
-with the following claims appended:
+
+With the following claims appended:
-```
+```json
["_bBdCtzFzC1DYL1r6gNq2fZPMvMdR31mo2zp6gq1sZ4","given_name","Susanne"]
```
-```
+```json
["SNDcrxkCBGQrLj0kHiX72Gn4l5dz-sfKo12tbqAoNf8","family_name","Meier"]
```
-```
+```json
["wkeeXn0z00kkbhuZyPW3gpePi9xRUe5rekCsisgzex8","date_of_birth","1990-02-14"]
```
+
-as well as a key binding (the JWT is decoded):
+
+As well as a key binding (the JWT is decoded):
-```
+```json
{
"typ": "kb+jwt",
"alg": "ES256",
@@ -495,7 +518,9 @@ as well as a key binding (the JWT is decoded):
"y": "6K5c5nYUfRBY3vRo-Q043hr8cFCqYj3iwnxwGFF2Ca8"
}
}
-.
+```
+
+```json
{
"iat": 1721224531,
"aud": "did:key:zDnaeT2KFSXyCSo3WLjrZeQsNwaCQv2r6bV4bLZNbdhZKhBbh",
@@ -503,6 +528,13 @@ as well as a key binding (the JWT is decoded):
"sd_hash": "UkLqyUz3zKR1iMTam7AsP89aEzYvbQqiJ4jfV82A96c"
}
```
+
+
+## Contributing
+External contributions are greatly appreciated! Be sure to observe the contribution guidelines (see [CONTRIBUTING.md](CONTRIBUTING.md)).
+In particular, external contributions to this project are subject to the A-SIT Plus Contributor License Agreement (see also [CONTRIBUTING.md](CONTRIBUTING.md)).
+
+
## Contributing
External contributions are greatly appreciated! Be sure to observe the contribution guidelines (see [CONTRIBUTING.md](CONTRIBUTING.md)).
diff --git a/conventions-vclib/build.gradle.kts b/conventions-vclib/build.gradle.kts
index b962c379f..ad1ba3686 100644
--- a/conventions-vclib/build.gradle.kts
+++ b/conventions-vclib/build.gradle.kts
@@ -10,11 +10,19 @@ group = "at.asitplus.gradle"
dependencies {
api("at.asitplus.gradle:k2")
+ api("com.squareup:kotlinpoet:1.16.0")
+ api("com.android.library:com.android.library.gradle.plugin:8.2.2")
+ api("de.mannodermaus.gradle.plugins:android-junit5:1.11.0.0")
+
}
repositories {
- maven("https://maven.pkg.jetbrains.space/kotlin/p/dokka/dev")
+ maven {
+ url = uri("https://raw.githubusercontent.com/a-sit-plus/gradle-conventions-plugin/mvn/repo")
+ name = "aspConventions"
+ } //KOTEST snapshot
mavenCentral()
+ google()
gradlePluginPortal()
}
diff --git a/conventions-vclib/gradle-conventions-plugin b/conventions-vclib/gradle-conventions-plugin
index 1fbb7f4f0..3f1fe1b97 160000
--- a/conventions-vclib/gradle-conventions-plugin
+++ b/conventions-vclib/gradle-conventions-plugin
@@ -1 +1 @@
-Subproject commit 1fbb7f4f02674089979dbf265b6a961f53dbf8e5
+Subproject commit 3f1fe1b97a8fda7e1877379148755b42b4c8b0f6
diff --git a/conventions-vclib/src/main/kotlin/VcLibConventions.kt b/conventions-vclib/src/main/kotlin/VcLibConventions.kt
index 07944af83..e24a0348f 100644
--- a/conventions-vclib/src/main/kotlin/VcLibConventions.kt
+++ b/conventions-vclib/src/main/kotlin/VcLibConventions.kt
@@ -3,39 +3,41 @@
package at.asitplus.gradle
import VcLibVersions
+import com.android.build.gradle.LibraryExtension
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.TypeSpec
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.getByName
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
-
+import java.io.FileInputStream
+import java.util.regex.Pattern
val Project.signumVersionCatalog: VersionCatalog
get() = extensions.getByType().named("signum")
inline fun Project.commonApiDependencies(): List {
project.AspVersions.versions["signum"] = VcLibVersions.signum
+ project.AspVersions.versions["supreme"] = VcLibVersions.supreme
project.AspVersions.versions["jsonpath"] = VcLibVersions.jsonpath
project.AspVersions.versions["okio"] = signumVersionCatalog.findVersion("okio").get().toString()
- project.AspVersions.versions["encoding"] = "2.2.1"
return listOf(
coroutines(),
- serialization("json"),
- serialization("cbor"),
- addDependency("at.asitplus.signum:indispensable", "signum"), //for iOS Export
+ addDependency("at.asitplus.signum:supreme", "supreme"),
addDependency("at.asitplus.signum:indispensable-cosef", "signum"),
addDependency("at.asitplus.signum:indispensable-josef", "signum"),
- addDependency("at.asitplus:jsonpath4k", "jsonpath"),
- datetime(),
addDependency("com.squareup.okio:okio", "okio"),
- addDependency("io.matthewnelson.encoding:base16", "encoding"),
- addDependency("io.matthewnelson.encoding:base64", "encoding"),
- addDependency("io.matthewnelson.encoding:core", "encoding"),
+ addDependency("at.asitplus:jsonpath4k", "jsonpath"),
)
}
@@ -48,29 +50,148 @@ inline fun KotlinDependencyHandler.commonImplementationDependencies() {
implementation(project.ktor("http"))
implementation(project.napier())
implementation(project.ktor("utils"))
- project.AspVersions.versions["uuid"] = VcLibVersions.uuid
- implementation(project.addDependency("com.benasher44:uuid", "uuid"))
}
+/**
+ * Hooks up Kotest tests from common using a frankensteined JUnit runner.
+ * It generates code using KotlinPoet to hook it up as per https://github.com/kotest/kotest/issues/189
+ */
+inline fun Project.wireAndroidInstrumentedTests() {
+ logger.lifecycle(" Wiring up Android Instrumented Tests")
+ val targetDir = project.layout.projectDirectory.dir("src")
+ .dir("androidInstrumentedTest").dir("kotlin")
+ .dir("generated").asFile.apply { deleteRecursively() }
+
+ val packagePattern = Pattern.compile("package\\s+(\\S+)", Pattern.UNICODE_CHARACTER_CLASS)
+ val searchPattern =
+ Pattern.compile("\\s+class\\s+(\\S+)\\s*:\\s*FreeSpec", Pattern.UNICODE_CHARACTER_CLASS)
+ project.layout.projectDirectory.dir("src").dir("commonTest")
+ .dir("kotlin").asFileTree.filter { it.extension == "kt" }.forEach { file ->
+ FileInputStream(file).bufferedReader().use { reader ->
+ val source = reader.readText()
+
+ val packageName = packagePattern.matcher(source).run {
+ if (find()) group(1) else null
+ }
+
+ val matcher = searchPattern.matcher(source)
+
+ while (matcher.find()) {
+ val className = matcher.group(1)
+ logger.lifecycle("Found Test class $className in file ${file.name}")
+
+ FileSpec.builder("at.asitplus.wallet.instrumented", "Android$className")
+ .addType(
+ TypeSpec.classBuilder("Android$className")
+ .apply {
+ // this.superclass(ClassName(packageName ?: "", className))
+ addFunction(
+ FunSpec.Companion.builder("test").addCode(
+ "%L",
+ """
+ val listener = io.kotest.engine.listener.CollectingTestEngineListener()
+ io.kotest.engine.TestEngineLauncher(listener)
+ .withClasses(
+ """.trimIndent()
+ + ClassName(
+ packageName ?: "",
+ className
+ ).canonicalName + "::class)" +
+ """
+ .launch()
+ listener.tests.map { entry ->
+ {
+ val testCase = entry.key
+ val descriptor = testCase.descriptor.chain().joinToString(" > ") {
+ it.id.value
+ }
+ val cause = when (val value = entry.value) {
+ is io.kotest.core.test.TestResult.Error -> value.cause
+ is io.kotest.core.test.TestResult.Failure -> value.cause
+ else -> null
+ }
+ org.junit.jupiter.api.Assertions.assertFalse(entry.value.isErrorOrFailure) {
+ """.trimIndent()
+ + "\"\"\"\$descriptor\n" +
+ " |\${cause?.stackTraceToString()}\"\"\".trimMargin()\n" +
+ """
+ }
+ }
+ }.let {
+ org.junit.jupiter.api.assertAll(it)
+ }
+ """.trimIndent() +
+ "\nprint(\"Total \${listener.tests.size}\")\n" +
+ "println(\" Failure \${listener.tests.count { it.value.isErrorOrFailure }}\")"
+
+ ).addAnnotation(ClassName("org.junit.jupiter.api", "Test"))
+ .build()
+
+
+ )
+ .build()
+ }.build()
+ ).build().apply {
+ targetDir.also { file ->
+ file.mkdirs()
+ writeTo(file)
+ }
+ }
+ }
+ }
+ }
+}
+
+fun Project.setupAndroid() {
+ project.extensions.getByName("android").apply {
+ namespace = "$group.${name.replace('-','.')}".also { logger.lifecycle("Setting Android namespace to $it") }
+ compileSdk = 34
+ defaultConfig {
+ minSdk = 30
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
-fun Project.commonIosExports() = arrayOf(
- datetime(),
- "com.ionspin.kotlin:bignum:${signumVersionCatalog.findVersion("bignum").get()}",
- kmmresult(),
- "at.asitplus.signum:indispensable:${VcLibVersions.signum}",
- "at.asitplus.signum:indispensable-cosef:${VcLibVersions.signum}",
- "at.asitplus.signum:indispensable-josef:${VcLibVersions.signum}",
- "at.asitplus:jsonpath4k:${VcLibVersions.jsonpath}",
- "io.matthewnelson.encoding:core:${AspVersions.versions["encoding"]}",
- "io.matthewnelson.encoding:base16:${AspVersions.versions["encoding"]}",
- "io.matthewnelson.encoding:base64:${AspVersions.versions["encoding"]}",
-)
+ dependencies {
+ add("androidTestImplementation", "androidx.test:runner:${VcLibVersions.Android.testRunner}")
+ add("androidTestImplementation", "androidx.test:core:${VcLibVersions.Android.testCore}")
+ add("testImplementation", "org.junit.jupiter:junit-jupiter-api:${VcLibVersions.Android.junit}")
+ add("testRuntimeOnly", "org.junit.jupiter:junit-jupiter-engine:${VcLibVersions.Android.junit}")
+ add("androidTestImplementation", "org.junit.jupiter:junit-jupiter-api:${VcLibVersions.Android.junit}")
+ }
+
+ packaging {
+ resources.excludes.add("/META-INF/{AL2.0,LGPL2.1}")
+ resources.excludes.add("win32-x86-64/attach_hotspot_windows.dll")
+ resources.excludes.add("win32-x86/attach_hotspot_windows.dll")
+ resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF")
+ resources.excludes.add("META-INF/licenses/*")
+ }
+
+ testOptions {
+ managedDevices {
+ localDevices.create("pixel2api33") {
+ device = "Pixel 2"
+ apiLevel = 33
+ systemImageSource = "aosp-atd"
+ }
+ }
+ }
+ }
+}
class VcLibConventions : Plugin {
override fun apply(target: Project) {
+ if (target.rootProject != target) target.plugins.apply("com.android.library")
target.plugins.apply("at.asitplus.gradle.conventions")
+ if (target.rootProject != target) target.plugins.apply("de.mannodermaus.android-junit5")
+
+ target.task("wireAndroidInstrumentedTests") {
+ doFirst { target.wireAndroidInstrumentedTests() }
+ }
target.gradle.taskGraph.whenReady {
+ target.wireAndroidInstrumentedTests()
target.tasks.withType>().configureEach {
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
diff --git a/conventions-vclib/src/main/kotlin/VcLibVersions.kt b/conventions-vclib/src/main/kotlin/VcLibVersions.kt
index a45dffb42..c48bf0e27 100644
--- a/conventions-vclib/src/main/kotlin/VcLibVersions.kt
+++ b/conventions-vclib/src/main/kotlin/VcLibVersions.kt
@@ -10,6 +10,7 @@ object VcLibVersions {
val uuid get() = versionOf("uuid")
val signum get() = versionOf("signum")
+ val supreme get() = versionOf("supreme")
val jsonpath get() = versionOf("jsonpath")
val eupidcredential get() = versionOf("eupid")
val mdl get() = versionOf("mdl")
@@ -18,4 +19,12 @@ object VcLibVersions {
val json get() = versionOf("jvm.json")
val `authlete-cbor` get() = versionOf("jvm.cbor")
}
+
+ object Android {
+ val compileSDK get() = versionOf("android.compileSDK")
+ val minSDK get() = versionOf("android.minSDK")
+ val testRunner get() = versionOf("android.testRunner")
+ val testCore get() = versionOf("android.testCore")
+ val junit get() = versionOf("android.junit")
+ }
}
diff --git a/conventions-vclib/src/main/resources/vcLibVersions.properties b/conventions-vclib/src/main/resources/vcLibVersions.properties
index 1fb7f9b1a..2d33c9202 100644
--- a/conventions-vclib/src/main/resources/vcLibVersions.properties
+++ b/conventions-vclib/src/main/resources/vcLibVersions.properties
@@ -1,7 +1,14 @@
-signum=3.6.0
+signum=3.9.0
+supreme=0.4.0
uuid=0.8.1
-jsonpath=2.2.0
+jsonpath=2.3.0
jvm.json=20230618
jvm.cbor=1.18
-eupid=2.1.3-SNAPSHOT
-mdl=1.0.2-SNAPSHOT
\ No newline at end of file
+eupid=2.2.0-SNAPSHOT
+mdl=1.1.0-SNAPSHOT
+
+android.compileSDK=34
+android.minSDK=33
+android.testRunner=1.6.2
+android.testCore=1.6.1
+android.junit=5.11.0
\ No newline at end of file
diff --git a/dif-data-classes/build.gradle.kts b/dif-data-classes/build.gradle.kts
new file mode 100644
index 000000000..b335caff1
--- /dev/null
+++ b/dif-data-classes/build.gradle.kts
@@ -0,0 +1,122 @@
+import at.asitplus.gradle.*
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree.Companion.test
+
+plugins {
+ kotlin("multiplatform")
+ kotlin("plugin.serialization")
+ id("at.asitplus.gradle.vclib-conventions")
+ id("org.jetbrains.dokka")
+ id("signing")
+}
+
+/* required for maven publication */
+val artifactVersion: String by extra
+group = "at.asitplus.wallet"
+version = artifactVersion
+
+
+kotlin {
+
+ jvm()
+ androidTarget {
+ publishLibraryVariants("release")
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ instrumentedTestVariant.sourceSetTree.set(test)
+ }
+ iosArm64()
+ iosSimulatorArm64()
+ iosX64()
+ sourceSets {
+
+ commonMain {
+ dependencies {
+ implementation(project.napier())
+ implementation(project.ktor("http"))
+ api("com.benasher44:uuid:${VcLibVersions.uuid}")
+ api("at.asitplus.signum:indispensable-cosef:${VcLibVersions.signum}")
+ api("at.asitplus.signum:indispensable-josef:${VcLibVersions.signum}")
+ api("at.asitplus:jsonpath4k:${VcLibVersions.jsonpath}")
+ }
+ }
+ }
+}
+
+
+setupAndroid()
+
+exportIosFramework(
+ "DifDataClasses",
+ transitiveExports = true,
+ "at.asitplus.signum:indispensable-cosef:${VcLibVersions.signum}",
+ "at.asitplus.signum:indispensable-josef:${VcLibVersions.signum}",
+ "at.asitplus:jsonpath4k:${VcLibVersions.jsonpath}",
+ "com.benasher44:uuid:${VcLibVersions.uuid}"
+)
+
+val javadocJar = setupDokka(
+ baseUrl = "https://github.com/a-sit-plus/vck/tree/main/",
+ multiModuleDoc = true
+)
+
+publishing {
+ publications {
+ withType {
+ if (this.name != "relocation") artifact(javadocJar)
+ pom {
+ name.set("DIF Data Classes")
+ description.set("Kotlin Multiplatform data classes for DIF")
+ url.set("https://github.com/a-sit-plus/vck")
+ licenses {
+ license {
+ name.set("The Apache License, Version 2.0")
+ url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
+ }
+ }
+ developers {
+ developer {
+ id.set("JesusMcCloud")
+ name.set("Bernd Prünster")
+ email.set("bernd.pruenster@a-sit.at")
+ }
+ developer {
+ id.set("nodh")
+ name.set("Christian Kollmann")
+ email.set("christian.kollmann@a-sit.at")
+ }
+ }
+ scm {
+ connection.set("scm:git:git@github.com:a-sit-plus/vck.git")
+ developerConnection.set("scm:git:git@github.com:a-sit-plus/vck.git")
+ url.set("https://github.com/a-sit-plus/vck")
+ }
+ }
+ }
+ }
+ repositories {
+ mavenLocal {
+ signing.isRequired = false
+ }
+ maven {
+ url = uri(layout.projectDirectory.dir("..").dir("repo"))
+ name = "local"
+ signing.isRequired = false
+ }
+ }
+}
+
+
+
+repositories {
+ maven(url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
+ mavenCentral()
+}
+
+signing {
+ val signingKeyId: String? by project
+ val signingKey: String? by project
+ val signingPassword: String? by project
+ useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
+ sign(publishing.publications)
+}
+
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ClaimFormatEnum.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ClaimFormatEnum.kt
similarity index 81%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ClaimFormatEnum.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ClaimFormatEnum.kt
index 3f10a74b0..b41b322d7 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ClaimFormatEnum.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ClaimFormatEnum.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.Serializable
@@ -19,6 +19,6 @@ enum class ClaimFormatEnum(val text: String) {
MSO_MDOC("mso_mdoc");
companion object {
- fun parse(text: String) = values().firstOrNull { it.text == text }
+ fun parse(text: String) = entries.firstOrNull { it.text == text }
}
}
\ No newline at end of file
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ClaimFormatEnumSerializer.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ClaimFormatEnumSerializer.kt
similarity index 95%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ClaimFormatEnumSerializer.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ClaimFormatEnumSerializer.kt
index 508a3e95c..fe101504f 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ClaimFormatEnumSerializer.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ClaimFormatEnumSerializer.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/Constraint.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/Constraint.kt
similarity index 95%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/Constraint.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/Constraint.kt
index 3213763fc..aa035fa3c 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/Constraint.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/Constraint.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintField.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintField.kt
similarity index 94%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintField.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintField.kt
index d5b879e4c..d35f46c18 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintField.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintField.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintFilter.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintFilter.kt
similarity index 96%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintFilter.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintFilter.kt
index e6efdf6cb..8842f117b 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintFilter.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintFilter.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintHolder.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintHolder.kt
similarity index 91%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintHolder.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintHolder.kt
index a3f6f6149..f88bb1591 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintHolder.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintHolder.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintNotFilter.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintNotFilter.kt
similarity index 91%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintNotFilter.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintNotFilter.kt
index 0b662bab9..55737ea75 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintNotFilter.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintNotFilter.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintStatus.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintStatus.kt
similarity index 89%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintStatus.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintStatus.kt
index 2cfea152d..46b93d0bd 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintStatus.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintStatus.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintStatusHolder.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintStatusHolder.kt
similarity index 92%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintStatusHolder.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintStatusHolder.kt
index 790408353..340231f4c 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/ConstraintStatusHolder.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/ConstraintStatusHolder.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/CredentialDefinition.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/CredentialDefinition.kt
similarity index 90%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/CredentialDefinition.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/CredentialDefinition.kt
index 6174dc86b..32996f35f 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/CredentialDefinition.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/CredentialDefinition.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/CredentialManifest.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/CredentialManifest.kt
similarity index 91%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/CredentialManifest.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/CredentialManifest.kt
index 726208bc6..7671a68f5 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/CredentialManifest.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/CredentialManifest.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerJwt.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/FormatContainerJwt.kt
similarity index 91%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerJwt.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/FormatContainerJwt.kt
index bab2df089..d8bed8df1 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerJwt.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/FormatContainerJwt.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerLdp.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/FormatContainerLdp.kt
similarity index 89%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerLdp.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/FormatContainerLdp.kt
index 4e1425c23..bd1063eeb 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatContainerLdp.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/FormatContainerLdp.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatHolder.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/FormatHolder.kt
similarity index 96%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatHolder.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/FormatHolder.kt
index 18414f9ce..1a66dbc66 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/FormatHolder.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/FormatHolder.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/InputDescriptor.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/InputDescriptor.kt
new file mode 100644
index 000000000..73b6962d1
--- /dev/null
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/InputDescriptor.kt
@@ -0,0 +1,75 @@
+@file:UseSerializers(InputDescriptorSerializer::class)
+
+package at.asitplus.dif
+
+import at.asitplus.dif.rqes.Base64URLTransactionDataSerializer
+import at.asitplus.dif.rqes.TransactionDataEntry
+import com.benasher44.uuid.uuid4
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UseSerializers
+import kotlinx.serialization.json.JsonContentPolymorphicSerializer
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.jsonObject
+
+@Serializable(with = InputDescriptorSerializer::class)
+sealed interface InputDescriptor {
+ val id: String
+ val group: String?
+ val name: String?
+ val purpose: String?
+ val format: FormatHolder?
+ val constraints: Constraint?
+}
+
+/**
+ * Data class for
+ * [DIF Presentation Exchange v2.1.1](https://identity.foundation/presentation-exchange/spec/v2.1.1/#term:presentation-definition)
+ */
+@Serializable
+data class DifInputDescriptor(
+ @SerialName("id")
+ override val id: String,
+ @SerialName("group")
+ override val group: String? = null,
+ @SerialName("name")
+ override val name: String? = null,
+ @SerialName("purpose")
+ override val purpose: String? = null,
+ @SerialName("format")
+ override val format: FormatHolder? = null,
+ @SerialName("constraints")
+ override val constraints: Constraint? = null,
+) : InputDescriptor {
+ constructor(name: String, constraints: Constraint? = null) : this(
+ id = uuid4().toString(),
+ name = name,
+ constraints = constraints,
+ )
+}
+
+@Serializable
+data class QesInputDescriptor(
+ @SerialName("id")
+ override val id: String,
+ @SerialName("group")
+ override val group: String? = null,
+ @SerialName("name")
+ override val name: String? = null,
+ @SerialName("purpose")
+ override val purpose: String? = null,
+ @SerialName("format")
+ override val format: FormatHolder? = null,
+ @SerialName("constraints")
+ override val constraints: Constraint? = null,
+ @SerialName("transaction_data")
+ val transactionData: List<@Serializable(Base64URLTransactionDataSerializer::class) TransactionDataEntry>,
+) : InputDescriptor
+
+
+object InputDescriptorSerializer : JsonContentPolymorphicSerializer(InputDescriptor::class) {
+ override fun selectDeserializer(element: JsonElement) = when {
+ "transaction_data" in element.jsonObject -> QesInputDescriptor.serializer()
+ else -> DifInputDescriptor.serializer()
+ }
+}
\ No newline at end of file
diff --git a/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/Json.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/Json.kt
new file mode 100644
index 000000000..a1142ef69
--- /dev/null
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/Json.kt
@@ -0,0 +1,12 @@
+package at.asitplus.dif
+
+import kotlinx.serialization.json.Json
+
+val jsonSerializer by lazy {
+ Json {
+ prettyPrint = false
+ encodeDefaults = false
+ classDiscriminator = "type"
+ ignoreUnknownKeys = true
+ }
+}
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationDefinition.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationDefinition.kt
similarity index 86%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationDefinition.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationDefinition.kt
index 083998e24..a8f773e72 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationDefinition.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationDefinition.kt
@@ -1,7 +1,6 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import at.asitplus.KmmResult.Companion.wrap
-import at.asitplus.wallet.lib.data.vckJsonSerializer
import com.benasher44.uuid.uuid4
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -21,11 +20,13 @@ data class PresentationDefinition(
val purpose: String? = null,
@SerialName("input_descriptors")
val inputDescriptors: Collection,
+ @Deprecated(message = "Removed in DIF Presentation Exchange 2.0.0", ReplaceWith("inputDescriptors.format"))
@SerialName("format")
val formats: FormatHolder? = null,
@SerialName("submission_requirements")
val submissionRequirements: Collection? = null,
) {
+ @Deprecated(message = "Removed in DIF Presentation Exchange 2.0.0")
constructor(
inputDescriptors: Collection,
formats: FormatHolder
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationSubmission.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationSubmission.kt
similarity index 92%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationSubmission.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationSubmission.kt
index ecc26ac66..2f46374b5 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationSubmission.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationSubmission.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationSubmissionDescriptor.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationSubmissionDescriptor.kt
similarity index 93%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationSubmissionDescriptor.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationSubmissionDescriptor.kt
index fd4a4347c..7664d6a17 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/PresentationSubmissionDescriptor.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/PresentationSubmissionDescriptor.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/RequirementEnum.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/RequirementEnum.kt
similarity index 92%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/RequirementEnum.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/RequirementEnum.kt
index 738b70a81..f40c25c60 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/RequirementEnum.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/RequirementEnum.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/RequirementEnumSerializer.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/RequirementEnumSerializer.kt
similarity index 95%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/RequirementEnumSerializer.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/RequirementEnumSerializer.kt
index 4ae0e937c..4138796a4 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/RequirementEnumSerializer.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/RequirementEnumSerializer.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SchemaReference.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SchemaReference.kt
similarity index 85%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SchemaReference.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SchemaReference.kt
index 5297f8605..e3ee75693 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SchemaReference.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SchemaReference.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/StatusDirectiveEnum.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/StatusDirectiveEnum.kt
similarity index 79%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/StatusDirectiveEnum.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/StatusDirectiveEnum.kt
index b999a60d6..e609687c5 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/StatusDirectiveEnum.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/StatusDirectiveEnum.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.Serializable
@@ -14,6 +14,6 @@ enum class StatusDirectiveEnum(val text: String) {
DISALLOWED("disallowed");
companion object {
- fun parse(text: String) = values().firstOrNull { it.text == text }
+ fun parse(text: String) = entries.firstOrNull { it.text == text }
}
}
\ No newline at end of file
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/StatusDirectiveEnumSerializer.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/StatusDirectiveEnumSerializer.kt
similarity index 95%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/StatusDirectiveEnumSerializer.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/StatusDirectiveEnumSerializer.kt
index 0f3e04b19..05b30c7a1 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/StatusDirectiveEnumSerializer.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/StatusDirectiveEnumSerializer.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirement.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SubmissionRequirement.kt
similarity index 98%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirement.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SubmissionRequirement.kt
index 5f1a832ce..3e622a87f 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirement.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SubmissionRequirement.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirementRuleEnum.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SubmissionRequirementRuleEnum.kt
similarity index 78%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirementRuleEnum.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SubmissionRequirementRuleEnum.kt
index e86828ca9..717b0b5de 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirementRuleEnum.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SubmissionRequirementRuleEnum.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.Serializable
@@ -13,6 +13,6 @@ enum class SubmissionRequirementRuleEnum(val text: String) {
ALL("all");
companion object {
- fun parse(text: String) = values().firstOrNull { it.text == text }
+ fun parse(text: String) = entries.firstOrNull { it.text == text }
}
}
\ No newline at end of file
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirementRuleEnumSerializer.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SubmissionRequirementRuleEnumSerializer.kt
similarity index 95%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirementRuleEnumSerializer.kt
rename to dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SubmissionRequirementRuleEnumSerializer.kt
index 0cb55be1a..12aabb041 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/dif/SubmissionRequirementRuleEnumSerializer.kt
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/SubmissionRequirementRuleEnumSerializer.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data.dif
+package at.asitplus.dif
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
diff --git a/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/Base64URLTransactionDataSerializer.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/Base64URLTransactionDataSerializer.kt
new file mode 100644
index 000000000..653a4caea
--- /dev/null
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/Base64URLTransactionDataSerializer.kt
@@ -0,0 +1,35 @@
+package at.asitplus.dif.rqes
+
+import at.asitplus.dif.jsonSerializer
+import at.asitplus.signum.indispensable.io.Base64UrlStrict
+import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
+import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+/**
+ * According to "Transaction Data entries as defined in D3.1: UC Specification WP3" the encoding
+ * is JSON and every entry is serialized to a base64 encoded string
+ */
+object Base64URLTransactionDataSerializer : KSerializer {
+
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("Base64URLTransactionDataSerializer", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): TransactionDataEntry {
+ val jsonString = decoder.decodeString()
+ val base64URLString = jsonString.decodeToByteArray(Base64UrlStrict).decodeToString()
+ return jsonSerializer.decodeFromString(base64URLString)
+ }
+
+ override fun serialize(encoder: Encoder, value: TransactionDataEntry) {
+ val jsonString = jsonSerializer.encodeToString(value)
+ val base64URLString = jsonString.encodeToByteArray().encodeToString(Base64UrlStrict)
+ encoder.encodeString(base64URLString)
+ }
+}
\ No newline at end of file
diff --git a/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/DocumentDigestEntry.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/DocumentDigestEntry.kt
new file mode 100644
index 000000000..ee1c3e721
--- /dev/null
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/DocumentDigestEntry.kt
@@ -0,0 +1,192 @@
+@file:UseSerializers(UrlSerializer::class)
+
+package at.asitplus.dif.rqes
+
+import at.asitplus.KmmResult
+import at.asitplus.KmmResult.Companion.wrap
+import at.asitplus.signum.indispensable.asn1.ObjectIdSerializer
+import at.asitplus.signum.indispensable.asn1.ObjectIdentifier
+import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer
+import io.ktor.http.*
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UseSerializers
+
+@ConsistentCopyVisibility
+@Serializable
+data class DocumentDigestEntry private constructor(
+ /**
+ * D3.1: UC Specification WP3: REQUIRED.
+ * String containing a human-readable
+ * description of the document to
+ * be signed (SD). The Wallet MUST
+ * show the label element in the
+ * user interaction. It MUST be UTF-
+ * 8 encoded.
+ */
+ @SerialName("label")
+ val label: String,
+
+ /**
+ * D3.1: UC Specification WP3: OPTIONAL.
+ * String containing the base64-encoded
+ * octet-representation of applying
+ * the algorithm from
+ * [hashAlgorithmOID] to the octet-
+ * representation of the document
+ * to be signed (SD).
+ */
+ @SerialName("hash")
+ @Serializable(ByteArrayBase64Serializer::class)
+ val hash: ByteArray? = null,
+
+ /**
+ * D3.1: UC Specification WP3: OPTIONAL.
+ * String containing the OID of the
+ * hash algorithm used to generate
+ * the hash listed in the [hash].
+ */
+ @SerialName("hashAlgorithmOID")
+ @Serializable(ObjectIdSerializer::class)
+ val hashAlgorithmOID: ObjectIdentifier? = null,
+
+ /**
+ * D3.1: UC Specification WP3: OPTIONAL.
+ * URL to the document
+ * to be signed (SD); the parameter
+ * [hash] MUST be the hash value
+ * of the designated document.
+ */
+ @SerialName("documentLocation_uri")
+ val documentLocationUri: Url? = null,
+
+ /**
+ * D3.1: UC Specification WP3: OPTIONAL.
+ * An object with
+ * information how to access
+ * [documentLocationUri].
+ */
+ @SerialName("documentLocation_method")
+ val documentLocationMethod: DocumentLocationMethod? = null,
+
+ /**
+ * D3.1: UC Specification WP3: OPTIONAL.
+ * String containing data to be signed
+ * representation as defined in CEN
+ * EN 419241-1 and ETSI/TR 119
+ * 001:2016 (as base64-encoded octet).
+ */
+ @SerialName("dtbsr")
+ @Serializable(ByteArrayBase64Serializer::class)
+ val dataToBeSignedRepresentation: ByteArray? = null,
+
+ /**
+ * D3.1: UC Specification WP3: OPTIONAL.
+ * String containing the
+ * OID of the hash algorithm used
+ * to generate the hash listed in
+ * [dataToBeSignedRepresentation]
+ */
+ @SerialName("dtbsrHashAlgorithmOID")
+ @Serializable(ObjectIdSerializer::class)
+ val dtbsrHashAlgorithmOID: ObjectIdentifier? = null,
+) {
+ /**
+ * D3.1: UC Specification WP3:
+ * If in each of the following bullet points one of the mentioned parameters is
+ * present, the other must be present:
+ * - [hash] and [hashAlgorithmOID]
+ * - [documentLocationUri] and [documentLocationMethod]
+ * - [dtbsr] and [dtbsrHashAlgorithmOID]
+ * In each of the following bullet points at least one of the mentioned
+ * parameters must be present:
+ * - [hash] or [dtbsr]
+ */
+ init {
+ require(hash != null || dataToBeSignedRepresentation != null)
+ require(hashAlgorithmOID?.toString() iff hash?.toString())
+ require(dtbsrHashAlgorithmOID?.toString() iff dataToBeSignedRepresentation?.toString())
+ require(documentLocationUri?.toString() iff hash?.toString())
+ require(documentLocationMethod?.toString() iff documentLocationUri?.toString())
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as DocumentDigestEntry
+
+ if (label != other.label) return false
+ if (hash != null) {
+ if (other.hash == null) return false
+ if (!hash.contentEquals(other.hash)) return false
+ } else if (other.hash != null) return false
+ if (hashAlgorithmOID != other.hashAlgorithmOID) return false
+ if (documentLocationUri != other.documentLocationUri) return false
+ if (documentLocationMethod != other.documentLocationMethod) return false
+ if (dataToBeSignedRepresentation != null) {
+ if (other.dataToBeSignedRepresentation == null) return false
+ if (!dataToBeSignedRepresentation.contentEquals(other.dataToBeSignedRepresentation)) return false
+ } else if (other.dataToBeSignedRepresentation != null) return false
+ if (dtbsrHashAlgorithmOID != other.dtbsrHashAlgorithmOID) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = label.hashCode()
+ result = 31 * result + (hash?.contentHashCode() ?: 0)
+ result = 31 * result + (hashAlgorithmOID?.hashCode() ?: 0)
+ result = 31 * result + (documentLocationUri?.hashCode() ?: 0)
+ result = 31 * result + (documentLocationMethod?.hashCode() ?: 0)
+ result = 31 * result + (dataToBeSignedRepresentation?.contentHashCode() ?: 0)
+ result = 31 * result + (dtbsrHashAlgorithmOID?.hashCode() ?: 0)
+ return result
+ }
+
+ /**
+ * D3.1: UC Specification WP3: OPTIONAL.
+ * An object with
+ * information how to access
+ * [documentLocationUri].
+ */
+ @ConsistentCopyVisibility
+ @Serializable
+ @SerialName("documentLocation_method")
+ data class DocumentLocationMethod private constructor(
+ val method: Method,
+ )
+
+ companion object {
+ /**
+ * Safe way to construct the object as init throws
+ */
+ fun create(
+ label: String,
+ hash: ByteArray?,
+ hashAlgorithmOID: ObjectIdentifier?,
+ documentLocationUri: Url?,
+ documentLocationMethod: DocumentLocationMethod?,
+ dtbsr: ByteArray?,
+ dtbsrHashAlgorithmOID: ObjectIdentifier?,
+ ): KmmResult =
+ kotlin.runCatching {
+ DocumentDigestEntry(
+ label = label,
+ hash = hash,
+ hashAlgorithmOID = hashAlgorithmOID,
+ documentLocationUri = documentLocationUri,
+ documentLocationMethod = documentLocationMethod,
+ dataToBeSignedRepresentation = dtbsr,
+ dtbsrHashAlgorithmOID = dtbsrHashAlgorithmOID,
+ )
+ }.wrap()
+
+ }
+}
+
+/**
+ * Checks that either both strings are present or null
+ */
+private infix fun String?.iff(other: String?): Boolean =
+ (this != null && other != null) or (this == null && other == null)
diff --git a/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/DocumentLocationEntry.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/DocumentLocationEntry.kt
new file mode 100644
index 000000000..e869a69bd
--- /dev/null
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/DocumentLocationEntry.kt
@@ -0,0 +1,14 @@
+package at.asitplus.dif.rqes
+
+import io.ktor.http.*
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class DocumentLocationEntry(
+ @SerialName("uri")
+ @Serializable(UrlSerializer::class)
+ val uri: Url,
+ @SerialName("method")
+ val method: Method,
+)
diff --git a/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/Method.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/Method.kt
new file mode 100644
index 000000000..2f033ec2e
--- /dev/null
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/Method.kt
@@ -0,0 +1,73 @@
+package at.asitplus.dif.rqes
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * After D3.1: UC Specification WP3.
+ * However, this class is potentially a mistake in the draft spec vs test vector,
+ * currently we need it to be a sealed class with polymorphic serialization to get the structure
+ * `method: {type: NAME}`
+ * sealed class would instead serialize to
+ * `method: NAME`
+ * which might be the corrected implementation in the next draft.
+ *
+ * The method describes the restrictions/way of accessing a document
+ */
+@Serializable
+@SerialName("method")
+sealed class Method {
+ /**
+ * D3.1: UC Specification WP3:
+ * The document corresponding to the parameter [hash] can be
+ * fetched from [documentLocationUri] with a https-request
+ * without further restrictions.
+ */
+ @Serializable
+ @SerialName("public")
+ data object Public : Method()
+
+ /**
+ * D3.1: UC Specification WP3:
+ * The wallet displays the parameter [oneTimePassword] to the
+ * user. A webclient accessing the uri offers a way for the user to
+ * input the shown value and only then allows to fetch the
+ * document corresponding to [hash].
+ */
+ @Serializable
+ @SerialName("otp")
+ data class OTP(
+ val oneTimePassword: String
+ ) : Method()
+
+ /**
+ * D3.1: UC Specification WP3:
+ * The wallet fetches the document from
+ * [documentLocationUri]. The document should be fetched
+ * using the ‘Basic’ HTTP Authentication Scheme (RFC 7617).
+ */
+ @Serializable
+ @SerialName("basic_auth")
+ data object Basic : Method()
+
+ /**
+ * D3.1: UC Specification WP3:
+ * The wallet fetches the document from
+ * [documentLocationUri]. The document should be fetched
+ * using the ‘Digest’ HTTP Authentication Scheme (RFC 7616).
+ */
+ @Serializable
+ @SerialName("digest_auth")
+ data object Digest : Method()
+
+ /**
+ * D3.1: UC Specification WP3:
+ * The wallet fetches the document from
+ * [documentLocationUri]. The document should be fetched
+ * using the ‘OAuth 2.0’ Authentication Framework (RFC6749
+ * and RFC8252).
+ */
+ @Serializable
+ @SerialName("oauth_20")
+ data object Oauth2 : Method()
+}
\ No newline at end of file
diff --git a/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/RqesConstants.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/RqesConstants.kt
new file mode 100644
index 000000000..1fa8de5a4
--- /dev/null
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/RqesConstants.kt
@@ -0,0 +1,9 @@
+package at.asitplus.dif.rqes
+
+object RqesConstants {
+
+ const val SCOPE = "credential"
+
+ const val SIGNATURE_QUALIFIER = "eu_eidas_qes"
+
+}
\ No newline at end of file
diff --git a/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/TransactionDataEntry.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/TransactionDataEntry.kt
new file mode 100644
index 000000000..498744236
--- /dev/null
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/TransactionDataEntry.kt
@@ -0,0 +1,168 @@
+@file:UseSerializers(UrlSerializer::class)
+
+package at.asitplus.dif.rqes
+
+import at.asitplus.KmmResult
+import at.asitplus.KmmResult.Companion.wrap
+import at.asitplus.signum.indispensable.asn1.ObjectIdSerializer
+import at.asitplus.signum.indispensable.asn1.ObjectIdentifier
+import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UseSerializers
+
+
+/**
+ * Implements "Transaction Data entries as defined in D3.1: UC Specification WP3"
+ * leveraging upcoming changes to [OpenID4VP](https://github.com/openid/OpenID4VP/pull/197)
+ */
+@Serializable
+sealed class TransactionDataEntry {
+
+ /**
+ * D3.1: UC Specification WP3:
+ * Transaction data entry used to authorize a qualified electronic signature
+ */
+ @ConsistentCopyVisibility
+ @Serializable
+ @SerialName("qes_authorization")
+ data class QesAuthorization private constructor(
+ /**
+ * CSC: OPTIONAL.
+ * Identifier of the signature type to be created, e.g. 'eu_eidas_qes'
+ * to denote a Qualified Electronic Signature according to eIDAS.
+ */
+ @SerialName("signatureQualifier")
+ val signatureQualifier: String? = null,
+
+ /**
+ * CSC: OPTIONAL.
+ * The unique identifier associated to the credential
+ */
+ @SerialName("credentialID")
+ val credentialID: String? = null,
+
+ /**
+ * D3.1: UC Specification WP3: REQUIRED.
+ * An array composed of entries for every
+ * document to be signed (SD). This
+ * applies for both cases, where a
+ * document is signed, or a digest is
+ * signed. Every entry is [DocumentDigestEntry]
+ *
+ * !!! Currently not compatible with the CSC definition of documentDigests
+ */
+ @SerialName("documentDigests")
+ val documentDigests: List,
+
+ /**
+ * D3.1: UC Specification WP3: OPTIONAL.
+ * An opaque value used by the QTSP to
+ * internally link the transaction to this
+ * request. The parameter is not supposed
+ * to contain a human-readable value
+ */
+ @SerialName("processID")
+ val processID: String? = null,
+ ) : TransactionDataEntry() {
+
+ /**
+ * D3.1: UC Specification WP3:
+ * At least one of the mentioned parameters must be present:
+ * - [signatureQualifier] or [credentialID]
+ */
+ init {
+ require(signatureQualifier != null || credentialID != null)
+ }
+
+ companion object {
+ /**
+ * Safe way to construct the object as init throws
+ */
+ fun create(
+ signatureQualifier: String?,
+ credentialId: String?,
+ documentDigest: List,
+ processID: String?,
+ ): KmmResult =
+ runCatching {
+ QesAuthorization(
+ signatureQualifier = signatureQualifier,
+ credentialID = credentialId,
+ documentDigests = documentDigest,
+ processID = processID,
+ )
+ }.wrap()
+ }
+ }
+
+ /**
+ * D3.1: UC Specification WP3:
+ * Transaction data entry used to gather the user’s consent to the terms of
+ * service of the Verifier (e.g. the QTSP)
+ */
+ @Serializable
+ @SerialName("qcert_creation_acceptance")
+ data class QCertCreationAcceptance(
+ /**
+ * D3.1: UC Specification WP3: REQUIRED.
+ * URL that points to a human-readable
+ * terms of service document for the end
+ * user that describes a contractual
+ * relationship between the end-user and
+ * the Qualified Trust Service Provider
+ * The value of this field MUST
+ * point to a document which is
+ * accessible and displayable by the
+ * Wallet
+ */
+ @SerialName("QC_terms_conditions_uri")
+ val qcTermsConditionsUri: String,
+
+ /**
+ * D3.1: UC Specification WP3: REQUIRED.
+ * String containing the base64-encoded
+ * octet-representation of applying the
+ * algorithm from
+ * [qcHashAlgorithmOID] to the octet-
+ * representation of the document
+ * referenced by [qcTermsConditionsUri]
+ */
+ @SerialName("QC_hash")
+ @Serializable(ByteArrayBase64Serializer::class)
+ val qcHash: ByteArray,
+
+ /**
+ * D3.1: UC Specification WP3: REQUIRED.
+ * String containing the
+ * OID of the hash algorithm used
+ * to generate the hash listed in
+ * [qcHash]
+ */
+ @SerialName("QC_hashAlgorithmOID")
+ @Serializable(ObjectIdSerializer::class)
+ val qcHashAlgorithmOID: ObjectIdentifier,
+ ) : TransactionDataEntry() {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as QCertCreationAcceptance
+
+ if (qcTermsConditionsUri != other.qcTermsConditionsUri) return false
+ if (!qcHash.contentEquals(other.qcHash)) return false
+ if (qcHashAlgorithmOID != other.qcHashAlgorithmOID) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = qcTermsConditionsUri.hashCode()
+ result = 31 * result + qcHash.contentHashCode()
+ result = 31 * result + qcHashAlgorithmOID.hashCode()
+ return result
+ }
+ }
+}
+
+
diff --git a/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/UrlSerializer.kt b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/UrlSerializer.kt
new file mode 100644
index 000000000..2063baa07
--- /dev/null
+++ b/dif-data-classes/src/commonMain/kotlin/at/asitplus/dif/rqes/UrlSerializer.kt
@@ -0,0 +1,20 @@
+package at.asitplus.dif.rqes
+
+import io.ktor.http.*
+import kotlinx.serialization.KSerializer
+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
+
+object UrlSerializer : KSerializer {
+
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UrlSerializer", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): Url = Url(decoder.decodeString())
+
+ override fun serialize(encoder: Encoder, value: Url) {
+ encoder.encodeString(value.toString())
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index da6208259..2bb7059da 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -12,7 +12,7 @@ kotlin.mpp.enableCInteropCommonization=true
kotlin.mpp.stability.nowarn=true
kotlin.native.ignoreDisabledTargets=true
-artifactVersion = 4.1.1
+artifactVersion = 5.0.0
jdk.version=17
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
deleted file mode 100644
index 1e9758689..000000000
--- a/gradle/libs.versions.toml
+++ /dev/null
@@ -1,2 +0,0 @@
-[versions]
-serialization="1.8.0-SNAPSHOT"
\ No newline at end of file
diff --git a/openid-data-classes/build.gradle.kts b/openid-data-classes/build.gradle.kts
new file mode 100644
index 000000000..6d962cab2
--- /dev/null
+++ b/openid-data-classes/build.gradle.kts
@@ -0,0 +1,139 @@
+import at.asitplus.gradle.*
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree.Companion.test
+
+plugins {
+ kotlin("multiplatform")
+ kotlin("plugin.serialization")
+ id("at.asitplus.gradle.vclib-conventions")
+ id("org.jetbrains.dokka")
+ id("signing")
+}
+
+/* required for maven publication */
+val artifactVersion: String by extra
+group = "at.asitplus.wallet"
+version = artifactVersion
+
+
+kotlin {
+
+ jvm()
+ androidTarget {
+ publishLibraryVariants("release")
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ instrumentedTestVariant.sourceSetTree.set(test)
+ }
+ iosArm64()
+ iosSimulatorArm64()
+ iosX64()
+ sourceSets {
+
+ commonMain {
+ dependencies {
+ api(project(":dif-data-classes"))
+ implementation(project.napier())
+ api(serialization("json"))
+ api(serialization("cbor"))
+ api(datetime())
+ api("com.ionspin.kotlin:bignum:${signumVersionCatalog.findVersion("bignum").get()}")
+ api(kmmresult())
+ api("at.asitplus.signum:indispensable:${VcLibVersions.signum}")
+ api("at.asitplus.signum:indispensable-cosef:${VcLibVersions.signum}")
+ api("at.asitplus.signum:indispensable-josef:${VcLibVersions.signum}")
+ api("at.asitplus:jsonpath4k:${VcLibVersions.jsonpath}")
+ api("io.matthewnelson.encoding:core:${AspVersions.versions["encoding"]}")
+ api("io.matthewnelson.encoding:base16:${AspVersions.versions["encoding"]}")
+ api("io.matthewnelson.encoding:base64:${AspVersions.versions["encoding"]}")
+ }
+ }
+
+ commonTest {
+ dependencies {
+ }
+ }
+
+ jvmMain {
+ dependencies {
+ }
+ }
+
+ jvmTest {
+ dependencies {
+ }
+ }
+ }
+}
+
+setupAndroid()
+
+exportIosFramework(
+ "OpenIdDataClasses",
+ transitiveExports = true,
+ project(":dif-data-classes")
+)
+
+val javadocJar = setupDokka(
+ baseUrl = "https://github.com/a-sit-plus/vck/tree/main/",
+ multiModuleDoc = true
+)
+
+publishing {
+ publications {
+ withType {
+ if (this.name != "relocation") artifact(javadocJar)
+ pom {
+ name.set("OpenID Data Classes")
+ description.set("Kotlin Multiplatform data classes for OpenId")
+ url.set("https://github.com/a-sit-plus/vck")
+ licenses {
+ license {
+ name.set("The Apache License, Version 2.0")
+ url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
+ }
+ }
+ developers {
+ developer {
+ id.set("JesusMcCloud")
+ name.set("Bernd Prünster")
+ email.set("bernd.pruenster@a-sit.at")
+ }
+ developer {
+ id.set("nodh")
+ name.set("Christian Kollmann")
+ email.set("christian.kollmann@a-sit.at")
+ }
+ }
+ scm {
+ connection.set("scm:git:git@github.com:a-sit-plus/vck.git")
+ developerConnection.set("scm:git:git@github.com:a-sit-plus/vck.git")
+ url.set("https://github.com/a-sit-plus/vck")
+ }
+ }
+ }
+ }
+ repositories {
+ mavenLocal {
+ signing.isRequired = false
+ }
+ maven {
+ url = uri(layout.projectDirectory.dir("..").dir("repo"))
+ name = "local"
+ signing.isRequired = false
+ }
+ }
+}
+
+repositories {
+ maven(url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
+ mavenCentral()
+}
+
+signing {
+ val signingKeyId: String? by project
+ val signingKey: String? by project
+ val signingPassword: String? by project
+ useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
+ sign(publishing.publications)
+}
+
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameters.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthenticationRequestParameters.kt
similarity index 80%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameters.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthenticationRequestParameters.kt
index b8c8581a8..295d322ea 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameters.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthenticationRequestParameters.kt
@@ -1,18 +1,26 @@
-package at.asitplus.wallet.lib.oidc
+package at.asitplus.openid
import at.asitplus.KmmResult.Companion.wrap
-import at.asitplus.wallet.lib.data.InstantLongSerializer
-import at.asitplus.wallet.lib.data.dif.PresentationDefinition
-import at.asitplus.wallet.lib.oidvci.AuthorizationDetails
+import at.asitplus.dif.PresentationDefinition
+import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer
+import at.asitplus.signum.indispensable.io.ByteArrayBase64UrlSerializer
+import at.asitplus.signum.indispensable.josef.JsonWebToken
+import at.asitplus.signum.indispensable.josef.io.InstantLongSerializer
import kotlinx.datetime.Instant
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encodeToString
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
/**
* Contents of an OIDC Authentication Request.
*
- * Usually, these parameters are appended to the Authorization Endpoint URL of the OpenId Provider (may be the
+ * Usually, these parameters are appended to the Authorization Endpoint URL of the OpenId Provider (maybe the
* Wallet App in case of SIOPv2, or the Credential Issuer for OID4VCI).
*/
@Serializable
@@ -259,6 +267,78 @@ data class AuthenticationRequestParameters(
*/
@SerialName("code_challenge_method")
val codeChallengeMethod: String? = null,
+
+ /**
+ * CSC: Optional
+ * Request a preferred language according to RFC 5646
+ */
+ @SerialName("lang")
+ val lang: String? = null,
+
+ /**
+ * CSC: REQUIRED-"credential"
+ * The identifier associated to the credential to authorize
+ */
+ @SerialName("credentialID")
+ val credentialID: String? = null,
+
+ /**
+ * CSC: Required-"credential"
+ * This parameter contains the symbolic identifier determining the kind of
+ * signature to be created
+ */
+ @SerialName("signatureQualifier")
+ val signatureQualifier: String? = null,
+
+ /**
+ * CSC: Required-"credential"
+ * The number of signatures to authorize
+ */
+ @SerialName("numSignatures")
+ val numSignatures: Int? = null,
+
+ /**
+ * CSC: REQUIRED-"credential"
+ * One or more base64-encoded hash values to be signed
+ */
+ @SerialName("hashes")
+ @Serializable(HashesSerializer::class)
+ val hashes: List? = null,
+
+ /**
+ * CSC: REQUIRED-"credential"
+ * String containing the OID of the hash algorithm used to generate the hashes
+ */
+ @SerialName("hashAlgorithmOID")
+ val hashAlgorithmOID: String? = null,
+
+ /**
+ * CSC: OPTIONAL
+ * A free form description of the authorization transaction in the lang language.
+ * The maximum size of the string is 500 characters
+ */
+ @SerialName("description")
+ val description: String? = null,
+
+ /**
+ * CSC: OPTIONAL
+ * To restrict access to the authorization server of a remote service, this specification introduces the
+ * additional account_token parameter to be used when calling the oauth2/authorize endpoint. This
+ * parameter contains a secure token designed to authenticate the authorization request based on an
+ * Account ID that SHALL be uniquely assigned by the signature application to the signing user or to the
+ * user’s application account
+ */
+ @SerialName("account_token")
+ val accountToken: JsonWebToken? = null,
+
+ /**
+ * CSC: OPTIONAL
+ * Arbitrary data from the signature application. It can be used to handle a
+ * transaction identifier or other application-spe cific data that may be useful for
+ * debugging purposes
+ */
+ @SerialName("clientData")
+ val clientData: String? = null,
) {
fun serialize() = jsonSerializer.encodeToString(this)
@@ -269,3 +349,4 @@ data class AuthenticationRequestParameters(
}.wrap()
}
}
+
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseParameters.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthenticationResponseParameters.kt
similarity index 96%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseParameters.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthenticationResponseParameters.kt
index 2cbd460a8..023f91829 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseParameters.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthenticationResponseParameters.kt
@@ -1,8 +1,8 @@
-package at.asitplus.wallet.lib.oidc
+package at.asitplus.openid
import at.asitplus.KmmResult.Companion.wrap
-import at.asitplus.wallet.lib.data.InstantLongSerializer
-import at.asitplus.wallet.lib.data.dif.PresentationSubmission
+import at.asitplus.signum.indispensable.josef.io.InstantLongSerializer
+import at.asitplus.dif.PresentationSubmission
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthnRequestClaims.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthnRequestClaims.kt
new file mode 100644
index 000000000..03584d793
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthnRequestClaims.kt
@@ -0,0 +1,37 @@
+package at.asitplus.openid
+
+import at.asitplus.KmmResult.Companion.wrap
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+
+@Serializable
+data class AuthnRequestClaims(
+ /**
+ * OIDC: OPTIONAL. Requests that the listed individual Claims be returned in the ID Token. If present, the listed
+ * Claims are being requested to be added to the default Claims in the ID Token. If not present, the default
+ * ID Token Claims are requested.
+ */
+ @SerialName("id_token")
+ val idTokenMap: Map? = null,
+
+ /**
+ * OIDC: OPTIONAL. Requests that the listed individual Claims be returned from the UserInfo Endpoint. If present,
+ * the listed Claims are being requested to be added to any Claims that are being requested using `scope` values.
+ * If not present, the Claims being requested from the UserInfo Endpoint are only those requested using `scope`
+ * values. When the `userinfo` member is used, the request MUST also use a `response_type` value that results in an
+ * Access Token being issued to the Client for use at the UserInfo Endpoint.
+ */
+ @SerialName("userinfo")
+ val userInfoMap: Map? = null,
+) {
+
+ fun serialize() = jsonSerializer.encodeToString(this)
+
+ companion object {
+ fun deserialize(it: String) = kotlin.runCatching {
+ jsonSerializer.decodeFromString(it)
+ }.wrap()
+ }
+
+}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthnRequestClaims.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthnRequestSingleClaim.kt
similarity index 57%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthnRequestClaims.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthnRequestSingleClaim.kt
index 19ae921d4..f7cfedee4 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthnRequestClaims.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthnRequestSingleClaim.kt
@@ -1,41 +1,10 @@
-package at.asitplus.wallet.lib.oidc
+package at.asitplus.openid
import at.asitplus.KmmResult.Companion.wrap
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
-@Serializable
-data class AuthnRequestClaims(
- /**
- * OIDC: OPTIONAL. Requests that the listed individual Claims be returned in the ID Token. If present, the listed
- * Claims are being requested to be added to the default Claims in the ID Token. If not present, the default
- * ID Token Claims are requested.
- */
- @SerialName("id_token")
- val idTokenMap: Map? = null,
-
- /**
- * OIDC: OPTIONAL. Requests that the listed individual Claims be returned from the UserInfo Endpoint. If present,
- * the listed Claims are being requested to be added to any Claims that are being requested using `scope` values.
- * If not present, the Claims being requested from the UserInfo Endpoint are only those requested using `scope`
- * values. When the `userinfo` member is used, the request MUST also use a `response_type` value that results in an
- * Access Token being issued to the Client for use at the UserInfo Endpoint.
- */
- @SerialName("userinfo")
- val userInfoMap: Map? = null,
-) {
-
- fun serialize() = jsonSerializer.encodeToString(this)
-
- companion object {
- fun deserialize(it: String) = kotlin.runCatching {
- jsonSerializer.decodeFromString(it)
- }.wrap()
- }
-
-}
-
@Serializable
data class AuthnRequestSingleClaim(
/**
@@ -60,6 +29,7 @@ data class AuthnRequestSingleClaim(
) {
fun serialize() = jsonSerializer.encodeToString(this)
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthorizationDetails.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthorizationDetails.kt
new file mode 100644
index 000000000..835e167ee
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/AuthorizationDetails.kt
@@ -0,0 +1,149 @@
+package at.asitplus.openid
+
+import at.asitplus.dif.rqes.DocumentLocationEntry
+import at.asitplus.signum.indispensable.asn1.ObjectIdSerializer
+import at.asitplus.signum.indispensable.asn1.ObjectIdentifier
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonArray
+
+
+@Serializable
+sealed class AuthorizationDetails {
+ /**
+ * OID4VCI: The request parameter `authorization_details` defined in Section 2 of (RFC9396) MUST be used to convey
+ * the details about the Credentials the Wallet wants to obtain. This specification introduces a new authorization
+ * details type `openid_credential` and defines the following parameters to be used with this authorization details
+ * type.
+ */
+ @Serializable
+ @SerialName("openid_credential")
+ data class OpenIdCredential(
+ /**
+ * OID4VCI: REQUIRED when [format] parameter is not present. String specifying a unique identifier of the
+ * Credential being described in [IssuerMetadata.supportedCredentialConfigurations].
+ */
+ @SerialName("credential_configuration_id")
+ val credentialConfigurationId: String? = null,
+
+ /**
+ * OID4VCI: REQUIRED when [credentialConfigurationId] parameter is not present.
+ * String identifying the format of the Credential the Wallet needs.
+ * This Credential format identifier determines further claims in the authorization details object needed to
+ * identify the Credential type in the requested format.
+ */
+ @SerialName("format")
+ val format: CredentialFormatEnum? = null,
+
+ /**
+ * OID4VCI: ISO mDL: OPTIONAL. This claim contains the type value the Wallet requests authorization for at the
+ * Credential Issuer. It MUST only be present if the [format] claim is present. It MUST not be present
+ * otherwise.
+ */
+ @SerialName("doctype")
+ val docType: String? = null,
+
+ /**
+ * OID4VCI: ISO mDL: OPTIONAL. Object as defined in Appendix A.3.2 excluding the `display` and `value_type`
+ * parameters. The `mandatory` parameter here is used by the Wallet to indicate to the Issuer that it only
+ * accepts Credential(s) issued with those claim(s).
+ */
+ @SerialName("claims")
+ val claims: Map>? = null,
+
+ /**
+ * OID4VCI: W3C VC: OPTIONAL. Object containing a detailed description of the Credential consisting of the
+ * following parameters, see [SupportedCredentialFormatDefinition].
+ */
+ @SerialName("credential_definition")
+ val credentialDefinition: SupportedCredentialFormatDefinition? = null,
+
+ /**
+ * OID4VCI: IETF SD-JWT VC: REQUIRED. String as defined in Appendix A.3.2. This claim contains the type values
+ * the Wallet requests authorization for at the Credential Issuer.
+ * It MUST only be present if the [format] claim is present. It MUST not be present otherwise.
+ */
+ @SerialName("vct")
+ val sdJwtVcType: String? = null,
+
+ /**
+ * OID4VCI: If the Credential Issuer metadata contains an [IssuerMetadata.authorizationServers] parameter, the
+ * authorization detail's locations common data field MUST be set to the Credential Issuer Identifier value.
+ */
+ @SerialName("locations")
+ val locations: Set? = null,
+
+ /**
+ * OID4VCI: REQUIRED. Array of strings, each uniquely identifying a Credential Dataset that can be issued using
+ * the Access Token returned in this response. Each of these Credential Datasets corresponds to the same
+ * Credential Configuration in the [IssuerMetadata.supportedCredentialConfigurations]. The Wallet MUST use these
+ * identifiers together with an Access Token in subsequent Credential Requests.
+ */
+ @SerialName("credential_identifiers")
+ val credentialIdentifiers: Set? = null,
+ ) : AuthorizationDetails()
+
+ /**
+ * CSC: The authorization details type credential allows applications to pass the details of a certain
+ * credential authorization in a single JSON object
+ */
+ @Serializable
+ @SerialName("credential")
+ data class CSCCredential(
+ /**
+ * CSC: The identifier associated to the credential to authorize
+ */
+ @SerialName("credentialID")
+ val credentialID: String? = null,
+
+ /**
+ * CSC: This parameter contains the symbolic identifier determining the kind of
+ * signature to be created
+ */
+ @SerialName("signatureQualifier")
+ val signatureQualifier: String? = null,
+
+ /**
+ * CSC: An array composed of entries for every document to be signed. This applies for
+ * array both cases, where are document is signed or a digest is signed
+ */
+ @SerialName("documentDigests")
+ val documentDigestsCSC: Collection,
+
+ /**
+ * CSC: String containing the OID of the hash algorithm used to generate the hashes
+ * listed in documentDigests.
+ */
+ @SerialName("hashAlgorithmOID")
+ @Serializable(ObjectIdSerializer::class)
+ val hashAlgorithmOID: ObjectIdentifier,
+
+ /**
+ * CSC: An array of strings designating the locations of
+ * array the API where the access token issued in a certain OAuth transaction shall be used.
+ */
+ @SerialName("locations")
+ val locations: Collection? = null,
+
+ /**
+ * QES: This parameter is used to convey the
+ * signer document. This parameter
+ * SHALL not be used when the signer
+ * document is not required for the
+ * creation of the signature (for example,
+ * in the Wallet-centric model)
+ */
+ @SerialName("documentLocations")
+ val documentLocations: Collection,
+ ) : AuthorizationDetails()
+
+ companion object {
+ fun parse(input: String): List =
+ jsonSerializer.decodeFromString(input).map {
+ jsonSerializer.decodeFromJsonElement(
+ serializer(),
+ it
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/BatchCredentialIssuanceMetadata.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/BatchCredentialIssuanceMetadata.kt
new file mode 100644
index 000000000..3e41938b8
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/BatchCredentialIssuanceMetadata.kt
@@ -0,0 +1,14 @@
+package at.asitplus.openid
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class BatchCredentialIssuanceMetadata(
+ /**
+ * OID4VCI: REQUIRED. Integer value specifying the maximum array size for the proofs parameter in a
+ * Credential Request.
+ */
+ @SerialName("batch_size")
+ val batchSize: Int
+)
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialFormatEnum.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialFormatEnum.kt
new file mode 100644
index 000000000..25eabd02e
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialFormatEnum.kt
@@ -0,0 +1,17 @@
+package at.asitplus.openid
+
+import kotlinx.serialization.Serializable
+
+@Serializable(with = CredentialFormatSerializer::class)
+enum class CredentialFormatEnum(val text: String) {
+ NONE("none"),
+ JWT_VC("jwt_vc_json"),
+ VC_SD_JWT("vc+sd-jwt"),
+ JWT_VC_JSON_LD("jwt_vc_json-ld"),
+ JSON_LD("ldp_vc"),
+ MSO_MDOC("mso_mdoc");
+
+ companion object {
+ fun parse(text: String) = entries.firstOrNull { it.text == text }
+ }
+}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialFormatEnum.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialFormatSerializer.kt
similarity index 57%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialFormatEnum.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialFormatSerializer.kt
index 1247199e3..84e283b73 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialFormatEnum.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialFormatSerializer.kt
@@ -1,31 +1,12 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
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 = CredentialFormatSerializer::class)
-enum class CredentialFormatEnum(val text: String) {
- NONE("none"),
- JWT_VC("jwt_vc_json"),
- /**
- * Unofficial constant, used by this library prior to implementing OID4VCI Draft 13.
- */
- JWT_VC_SD_UNOFFICIAL("jwt_vc_sd"),
- VC_SD_JWT("vc+sd-jwt"),
- JWT_VC_JSON_LD("jwt_vc_json-ld"),
- JSON_LD("ldp_vc"),
- MSO_MDOC("mso_mdoc");
-
- companion object {
- fun parse(text: String) = entries.firstOrNull { it.text == text }
- }
-}
-
object CredentialFormatSerializer : KSerializer {
override val descriptor: SerialDescriptor =
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOffer.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOffer.kt
new file mode 100644
index 000000000..5fb88f172
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOffer.kt
@@ -0,0 +1,48 @@
+package at.asitplus.openid
+
+import at.asitplus.KmmResult
+import at.asitplus.KmmResult.Companion.wrap
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.decodeFromJsonElement
+
+@Serializable
+data class CredentialOffer(
+ /**
+ * OID4VCI: REQUIRED. The URL of the Credential Issuer, as defined in Section 11.2.1, from which the Wallet is
+ * requested to obtain one or more Credentials. The Wallet uses it to obtain the Credential Issuer's Metadata
+ * following the steps defined in Section 11.2.2.
+ */
+ @SerialName("credential_issuer")
+ val credentialIssuer: String,
+
+ /**
+ * OID4VCI: REQUIRED. Array of unique strings that each identify one of the keys in the name/value pairs stored in
+ * [IssuerMetadata.supportedCredentialConfigurations]. The Wallet uses these string values to
+ * obtain the respective object that contains information about the Credential being offered as defined in
+ * Section 11.2.3. For example, these string values can be used to obtain `scope` values to be used in the
+ * Authorization Request, see [AuthenticationRequestParameters.scope].
+ */
+ @SerialName("credential_configuration_ids")
+ val configurationIds: Collection,
+
+ /**
+ * OID4VCI: OPTIONAL. If [grants] is not present or is empty, the Wallet MUST determine the Grant Types the
+ * Credential Issuer's Authorization Server supports using the respective metadata. When multiple grants are
+ * present, it is at the Wallet's discretion which one to use.
+ */
+ @SerialName("grants")
+ val grants: CredentialOfferGrants? = null,
+) {
+ fun serialize() = jsonSerializer.encodeToString(this)
+
+ companion object {
+ fun deserialize(input: String): KmmResult =
+ runCatching { jsonSerializer.decodeFromString(input) }.wrap()
+
+ fun deserialize(input: JsonElement): KmmResult =
+ runCatching { jsonSerializer.decodeFromJsonElement(input) }.wrap()
+ }
+}
\ No newline at end of file
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrants.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrants.kt
new file mode 100644
index 000000000..981e50e0b
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrants.kt
@@ -0,0 +1,19 @@
+package at.asitplus.openid
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * OID4VCI: Object indicating to the Wallet the Grant Types the Credential Issuer's Authorization Server is prepared to
+ * process for this Credential Offer. Every grant is represented by a name/value pair. The name is the Grant Type
+ * identifier; the value is an object that contains parameters either determining the way the Wallet MUST use the
+ * particular grant and/or parameters the Wallet MUST send with the respective request(s).
+ */
+@Serializable
+data class CredentialOfferGrants(
+ @SerialName("authorization_code")
+ val authorizationCode: CredentialOfferGrantsAuthCode? = null,
+
+ @SerialName("urn:ietf:params:oauth:grant-type:pre-authorized_code")
+ val preAuthorizedCode: CredentialOfferGrantsPreAuthCode? = null
+)
\ No newline at end of file
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrantsAuthCode.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrantsAuthCode.kt
new file mode 100644
index 000000000..6bd54cf90
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrantsAuthCode.kt
@@ -0,0 +1,25 @@
+package at.asitplus.openid
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CredentialOfferGrantsAuthCode(
+ /**
+ * OID4VCI: OPTIONAL. String value created by the Credential Issuer and opaque to the Wallet that is used to bind
+ * the subsequent Authorization Request with the Credential Issuer to a context set up during previous steps. If the
+ * Wallet decides to use the Authorization Code Flow and received a value for this parameter, it MUST include it in
+ * the subsequent Authorization Request to the Credential Issuer as the `issuer_state` parameter value.
+ */
+ @SerialName("issuer_state")
+ val issuerState: String? = null,
+
+ /**
+ * OID4VCI: OPTIONAL string that the Wallet can use to identify the Authorization Server to use with this grant
+ * type when `authorization_servers` parameter in the Credential Issuer metadata has multiple entries. It MUST NOT
+ * be used otherwise. The value of this parameter MUST match with one of the values in the `authorization_servers`
+ * array obtained from the Credential Issuer metadata.
+ */
+ @SerialName("authorization_server")
+ val authorizationServer: String? = null,
+)
\ No newline at end of file
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrantsPreAuthCode.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrantsPreAuthCode.kt
new file mode 100644
index 000000000..684da2457
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrantsPreAuthCode.kt
@@ -0,0 +1,38 @@
+package at.asitplus.openid
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CredentialOfferGrantsPreAuthCode(
+ /**
+ * OID4VCI: REQUIRED. The code representing the Credential Issuer's authorization for the Wallet to obtain
+ * Credentials of a certain type. This code MUST be short lived and single use. If the Wallet decides to use the
+ * Pre-Authorized Code Flow, this parameter value MUST be included in the subsequent Token Request with the
+ * Pre-Authorized Code Flow.
+ */
+ @SerialName("pre-authorized_code")
+ val preAuthorizedCode: String,
+
+ /**
+ * OID4VCI: OPTIONAL. Object specifying whether the Authorization Server expects presentation of a Transaction Code
+ * by the End-User along with the Token Request in a Pre-Authorized Code Flow. If the Authorization Server does not
+ * expect a Transaction Code, this object is absent; this is the default.
+ */
+ @SerialName("tx_code")
+ val transactionCode: CredentialOfferGrantsPreAuthCodeTransactionCode? = null,
+
+ /**
+ * OID4VCI: OPTIONAL. The minimum amount of time in seconds that the Wallet SHOULD wait between polling requests to
+ * the token endpoint. If no value is provided, Wallets MUST use 5 as the default.
+ */
+ @SerialName("interval")
+ val waitIntervalSeconds: Int? = 5,
+
+ /**
+ * OID4VCI: OPTIONAL string that the Wallet can use to identify the Authorization Server to use with this grant
+ * type when `authorization_servers` parameter in the Credential Issuer metadata has multiple entries.
+ */
+ @SerialName("authorization_server")
+ val authorizationServer: String? = null
+)
\ No newline at end of file
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrantsPreAuthCodeTransactionCode.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrantsPreAuthCodeTransactionCode.kt
new file mode 100644
index 000000000..707283976
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferGrantsPreAuthCodeTransactionCode.kt
@@ -0,0 +1,28 @@
+package at.asitplus.openid
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CredentialOfferGrantsPreAuthCodeTransactionCode(
+ /**
+ * OID4VCI: OPTIONAL. String specifying the input character set. Possible values are `numeric` (only digits) and
+ * `text` (any characters). The default is `numeric`.
+ */
+ @SerialName("input_mode")
+ val inputMode: String? = "numeric",
+
+ /**
+ * OID4VCI: OPTIONAL. Integer specifying the length of the Transaction Code. This helps the Wallet to render the
+ * input screen and improve the user experience.
+ */
+ @SerialName("length")
+ val length: Int? = null,
+
+ /**
+ * OID4VCI: OPTIONAL. String containing guidance for the Holder of the Wallet on how to obtain the Transaction
+ * Code, e.g., describing over which communication channel it is delivered.
+ */
+ @SerialName("description")
+ val description: String? = null,
+)
\ No newline at end of file
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferUrlParameters.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferUrlParameters.kt
new file mode 100644
index 000000000..3ec89b06e
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialOfferUrlParameters.kt
@@ -0,0 +1,37 @@
+package at.asitplus.openid
+
+import at.asitplus.KmmResult
+import at.asitplus.KmmResult.Companion.wrap
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.JsonObject
+
+/**
+ * OID4VCI: The Credential Issuer sends Credential Offer using an HTTP GET request or an HTTP redirect to the Wallet's
+ * Credential Offer Endpoint defined in Section 11.1.The Credential Offer object, which is a JSON-encoded object with
+ * the Credential Offer parameters, can be sent by value or by reference.
+ */
+@Serializable
+data class CredentialOfferUrlParameters(
+ /**
+ * OID4VCI: Object with the Credential Offer parameters. This MUST NOT be present when the [credentialOfferUrl]
+ * parameter is present.
+ */
+ @SerialName("credential_offer")
+ val credentialOffer: JsonObject? = null,
+
+ /**
+ * OID4VCI: String that is a URL using the `https` scheme referencing a resource containing a JSON object with the
+ * Credential Offer parameters. This MUST NOT be present when the [credentialOffer] parameter is present.
+ */
+ @SerialName("credential_offer_uri")
+ val credentialOfferUrl: String? = null,
+) {
+ fun serialize() = jsonSerializer.encodeToString(this)
+
+ companion object {
+ fun deserialize(input: String): KmmResult =
+ runCatching { jsonSerializer.decodeFromString(input) }.wrap()
+ }
+}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialRequestParameters.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestParameters.kt
similarity index 56%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialRequestParameters.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestParameters.kt
index 896401c0d..4f35da7ef 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialRequestParameters.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestParameters.kt
@@ -1,9 +1,7 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import at.asitplus.KmmResult
import at.asitplus.KmmResult.Companion.wrap
-import at.asitplus.wallet.lib.oidc.jsonSerializer
-import at.asitplus.wallet.lib.oidvci.mdl.RequestedCredentialClaimSpecification
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
@@ -11,25 +9,25 @@ import kotlinx.serialization.encodeToString
@Serializable
data class CredentialRequestParameters(
/**
- * OID4VCI: REQUIRED when the `credential_identifiers` parameter was not returned from the Token Response.
- * It MUST NOT be used otherwise. It is a String that determines the format of the Credential to be issued,
- * which may determine the type and any other information related to the Credential to be issued.
- * Credential Format Profiles consist of the Credential format specific parameters that are defined in Appendix A.
- * When this parameter is used, the [credentialIdentifier] Credential Request parameter MUST NOT be present.
- * REQUIRED. Format of the Credential to be issued. This Credential format identifier determines further parameters
- * required to determine the type and (optionally) the content of the credential to be issued.
+ * OID4VCI: REQUIRED when an [AuthorizationDetails.OpenIdCredential] was returned from the
+ * [TokenResponseParameters]. It MUST NOT be used otherwise. A string that identifies a Credential Dataset that is
+ * requested for issuance. When this parameter is used, the [format] parameter and any other Credential format
+ * specific parameters such as those defined in Appendix A MUST NOT be present
*/
- @SerialName("format")
- val format: CredentialFormatEnum? = null,
+ @SerialName("credential_identifier")
+ val credentialIdentifier: String? = null,
/**
- * OID4VCI: REQUIRED when `credential_identifiers` parameter was returned from the Token Response.
- * It MUST NOT be used otherwise. It is a String that identifies a Credential that is being requested to be issued.
- * When this parameter is used, the [format] parameter and any other Credential format specific parameters such
- * as those defined in Appendix A MUST NOT be present.
+ * OID4VCI: REQUIRED if an [AuthorizationDetails.OpenIdCredential] was not returned from the
+ * [TokenResponseParameters] (e.g. when the credential was requested using a [AuthenticationRequestParameters.scope]
+ * or a pre-authorisation code was used that did not return an [AuthorizationDetails.OpenIdCredential]).
+ * It MUST NOT be used otherwise. A string that determines the format of the Credential to be issued, which may
+ * determine the type and any other information related to the Credential to be issued. Credential Format Profiles
+ * consist of the Credential format specific parameters that are defined in Appendix A. When this parameter is used,
+ * the [credentialIdentifier] parameter MUST NOT be present.
*/
- @SerialName("credential_identifier")
- val credentialIdentifier: String? = null,
+ @SerialName("format")
+ val format: CredentialFormatEnum? = null,
/**
* OID4VCI: OPTIONAL. Object containing information for encrypting the Credential Response. If this request element
@@ -70,13 +68,20 @@ data class CredentialRequestParameters(
val sdJwtVcType: String? = null,
/**
- * OID4VCI: OPTIONAL. Object containing the proof of possession of the cryptographic key material the issued
- * Credential would be bound to. The proof object is REQUIRED if the [SupportedCredentialFormat.supportedProofTypes]
- * parameter is non-empty and present in the [IssuerMetadata.supportedCredentialConfigurations] for the requested
- * Credential.
+ * OID4VCI: OPTIONAL. Object providing a single proof of possession of the cryptographic key material to which the
+ * issued Credential instance will be bound to. [proof] parameter MUST NOT be present if [proofs] parameter is used.
*/
@SerialName("proof")
val proof: CredentialRequestProof? = null,
+
+ /**
+ * OID4VCI: OPTIONAL. Object providing one or more proof of possessions of the cryptographic key material to which
+ * the issued Credential instances will be bound to. The [proofs] parameter MUST NOT be present if [proof] parameter
+ * is used. [proofs] object contains exactly one parameter named as the proof type in Section 7.2.1, the value set
+ * for this parameter is an array containing parameters as defined by the corresponding proof type.
+ */
+ @SerialName("proofs")
+ val proofs: CredentialRequestProofContainer? = null,
) {
fun serialize() = jsonSerializer.encodeToString(this)
@@ -86,4 +91,4 @@ data class CredentialRequestParameters(
runCatching { jsonSerializer.decodeFromString(input) }.wrap()
}
-}
\ No newline at end of file
+}
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialRequestProof.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestProof.kt
similarity index 74%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialRequestProof.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestProof.kt
index 6eb1a11de..259954901 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialRequestProof.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestProof.kt
@@ -1,13 +1,13 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
-import at.asitplus.wallet.lib.oidc.OpenIdConstants
+import at.asitplus.openid.OpenIdConstants
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CredentialRequestProof(
/**
- * OID4VCI: e.g. `jwt`, or `cwt`, or `ldp_vp`. See [at.asitplus.wallet.lib.oidc.OpenIdConstants.ProofType].
+ * OID4VCI: e.g. `jwt`, or `ldp_vp`. See [at.asitplus.openid.OpenIdConstants.ProofType].
*/
@SerialName("proof_type")
val proofType: OpenIdConstants.ProofType,
@@ -22,6 +22,8 @@ data class CredentialRequestProof(
/**
* OID4VCI: A CWT (RFC8392) is used as proof of possession. When [proofType] is `cwt`, a proof object MUST include
* a `cwt` claim containing a CWT defined in Section 7.2.1.3.
+ *
+ * Removed in OID4VCI Draft 14, kept here for a bit of backwards-compatibility
*/
@SerialName("cwt")
val cwt: String? = null,
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestProofContainer.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestProofContainer.kt
new file mode 100644
index 000000000..101b234d9
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestProofContainer.kt
@@ -0,0 +1,20 @@
+package at.asitplus.openid
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CredentialRequestProofContainer(
+ /**
+ * OID4VCI: e.g. `jwt`, or `ldp_vp`. See [at.asitplus.openid.OpenIdConstants.ProofType].
+ */
+ @SerialName("proof_type")
+ val proofType: OpenIdConstants.ProofType,
+
+ /**
+ * OID4VCI: A JWT (RFC7519) is used as proof of possession. When [proofType] is `jwt`, a proof object MUST include
+ * a `jwt` claim containing a JWT defined in Section 7.2.1.1.
+ */
+ @SerialName("jwt")
+ val jwt: Set? = null,
+)
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialRequestProofSupported.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestProofSupported.kt
similarity index 93%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialRequestProofSupported.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestProofSupported.kt
index 7caadc8fe..ce56be4d1 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialRequestProofSupported.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialRequestProofSupported.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialResponseParameters.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialResponseParameters.kt
similarity index 92%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialResponseParameters.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialResponseParameters.kt
index ba6e87860..f790931e9 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialResponseParameters.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialResponseParameters.kt
@@ -1,9 +1,7 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import at.asitplus.KmmResult
import at.asitplus.KmmResult.Companion.wrap
-import at.asitplus.wallet.lib.data.DurationSecondsIntSerializer
-import at.asitplus.wallet.lib.oidc.jsonSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialSubjectMetadataSingle.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialSubjectMetadataSingle.kt
similarity index 97%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialSubjectMetadataSingle.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialSubjectMetadataSingle.kt
index d8916972f..7ce0e2c95 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialSubjectMetadataSingle.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/CredentialSubjectMetadataSingle.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -31,5 +31,5 @@ data class CredentialSubjectMetadataSingle(
@SerialName("display")
val display: Set? = null,
-)
+ )
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/DisplayLogoProperties.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DisplayLogoProperties.kt
similarity index 96%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/DisplayLogoProperties.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DisplayLogoProperties.kt
index caa4c2fba..458525938 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/DisplayLogoProperties.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DisplayLogoProperties.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/DisplayProperties.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DisplayProperties.kt
similarity index 97%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/DisplayProperties.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DisplayProperties.kt
index b516dafb9..5b5348b76 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/DisplayProperties.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DisplayProperties.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DocumentDigestCSCEntry.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DocumentDigestCSCEntry.kt
new file mode 100644
index 000000000..8a722ea4e
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DocumentDigestCSCEntry.kt
@@ -0,0 +1,44 @@
+package at.asitplus.openid
+
+import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * CSC: Entry for document to be signed
+ */
+@Serializable
+data class DocumentDigestCSCEntry (
+ /**
+ * CSC: Conditional String containing the actual Base64-
+ * encoded octet-representation of the hash of the document
+ */
+ @SerialName("hash")
+ @Serializable(ByteArrayBase64Serializer::class)
+ val hash: ByteArray,
+
+ /**
+ * CSC: String containing a human-readable description of the respective
+ * document
+ */
+ @SerialName("label")
+ val label: String,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as DocumentDigestCSCEntry
+
+ if (!hash.contentEquals(other.hash)) return false
+ if (label != other.label) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = hash.contentHashCode()
+ result = 31 * result + label.hashCode()
+ return result
+ }
+}
\ No newline at end of file
diff --git a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/DurationSecondsIntSerializer.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DurationSecondsIntSerializer.kt
similarity index 95%
rename from vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/DurationSecondsIntSerializer.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DurationSecondsIntSerializer.kt
index 35035b360..4a694a5ba 100644
--- a/vck/src/commonMain/kotlin/at/asitplus/wallet/lib/data/DurationSecondsIntSerializer.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/DurationSecondsIntSerializer.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.data
+package at.asitplus.openid
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
@@ -9,6 +9,7 @@ import kotlinx.serialization.encoding.Encoder
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
+
object DurationSecondsIntSerializer : KSerializer {
override val descriptor: SerialDescriptor =
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/HashesSerializer.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/HashesSerializer.kt
new file mode 100644
index 000000000..4de8dc998
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/HashesSerializer.kt
@@ -0,0 +1,28 @@
+package at.asitplus.openid
+
+import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer
+import kotlinx.serialization.KSerializer
+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
+
+/**
+ * CSC: Multiple hash values can be passed as comma separated values,
+ * e.g. oauth2/authorize?hash=dnN3ZX.. .ZmRm,ZjIxM3… Z2Zk,…
+ */
+object HashesSerializer : KSerializer> {
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("HashesSerializer", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): List {
+ val listOfHashes = decoder.decodeString().split(",")
+ return listOfHashes.map { at.asitplus.dif.jsonSerializer.decodeFromString(ByteArrayBase64Serializer, it) }
+ }
+
+ override fun serialize(encoder: Encoder, value: List) {
+ val listOfHashes = value.map { at.asitplus.dif.jsonSerializer.encodeToString(ByteArrayBase64Serializer, it) }
+ encoder.encodeString(listOfHashes.joinToString(","))
+ }
+}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/IdToken.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IdToken.kt
similarity index 97%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/IdToken.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IdToken.kt
index 24d62f998..267e648c1 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/IdToken.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IdToken.kt
@@ -1,8 +1,8 @@
-package at.asitplus.wallet.lib.oidc
+package at.asitplus.openid
import at.asitplus.KmmResult.Companion.wrap
import at.asitplus.signum.indispensable.josef.JsonWebKey
-import at.asitplus.wallet.lib.data.InstantLongSerializer
+import at.asitplus.signum.indispensable.josef.io.InstantLongSerializer
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IdTokenType.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IdTokenType.kt
new file mode 100644
index 000000000..77d74322e
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IdTokenType.kt
@@ -0,0 +1,11 @@
+package at.asitplus.openid
+
+import kotlinx.serialization.Serializable
+
+@Serializable(with = IdTokenTypeSerializer::class)
+enum class IdTokenType(val text: String) {
+
+ SUBJECT_SIGNED("subject_signed_id_token"),
+ ATTESTER_SIGNED("attester_signed_id_token")
+
+}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/IdTokenType.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IdTokenTypeSerializer.kt
similarity index 74%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/IdTokenType.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IdTokenTypeSerializer.kt
index ce03a6291..b09c5b3d8 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/IdTokenType.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IdTokenTypeSerializer.kt
@@ -1,21 +1,12 @@
-package at.asitplus.wallet.lib.oidc
+package at.asitplus.openid
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) {
-
- SUBJECT_SIGNED("subject_signed_id_token"),
- ATTESTER_SIGNED("attester_signed_id_token")
-
-}
-
object IdTokenTypeSerializer : KSerializer {
override val descriptor: SerialDescriptor =
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IssuerMetadata.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IssuerMetadata.kt
new file mode 100644
index 000000000..1dd73402e
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/IssuerMetadata.kt
@@ -0,0 +1,122 @@
+package at.asitplus.openid
+
+import at.asitplus.KmmResult
+import at.asitplus.KmmResult.Companion.wrap
+import at.asitplus.signum.indispensable.josef.JsonWebAlgorithm
+import at.asitplus.signum.indispensable.josef.JwsAlgorithm
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+
+/**
+ * Metadata about the credential issuer in
+ * [OpenID4VCI](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
+ *
+ * To be serialized into `/.well-known/openid-credential-issuer`.
+ */
+@Serializable
+data class IssuerMetadata(
+ /**
+ * OIDC Discovery: REQUIRED. URL using the https scheme with no query or fragment component that the OP asserts as
+ * its Issuer Identifier. If Issuer discovery is supported (see Section 2), this value MUST be identical to the
+ * issuer value returned by WebFinger. This also MUST be identical to the `iss` Claim value in ID Tokens issued
+ * from this Issuer.
+ */
+ @SerialName("issuer")
+ val issuer: String? = null,
+
+ /**
+ * OID4VCI: REQUIRED. The Credential Issuer's identifier, by a case sensitive URL using the `https`
+ * scheme that contains scheme, host and, optionally, port number and path components, but no query
+ * or fragment components
+ */
+ @SerialName("credential_issuer")
+ val credentialIssuer: String,
+
+ /**
+ * OID4VCI: OPTIONAL. Array of strings, where each string is an identifier of the OAuth 2.0 Authorization Server
+ * (as defined in RFC8414) the Credential Issuer relies on for authorization. If this parameter is omitted, the
+ * entity providing the Credential Issuer is also acting as the Authorization Server, i.e., the Credential Issuer's
+ * identifier is used to obtain the Authorization Server metadata.
+ */
+ @SerialName("authorization_servers")
+ val authorizationServers: Set? = null,
+
+ /**
+ * OID4VCI: REQUIRED. URL of the Credential Issuer's Credential Endpoint. This URL MUST use the https scheme and
+ * MAY contain port, path and query parameter components.
+ */
+ @SerialName("credential_endpoint")
+ val credentialEndpointUrl: String,
+
+ /**
+ * OID4VCI: OPTIONAL. URL of the Credential Issuer's Deferred Credential Endpoint, as defined in Section 8.
+ * This URL MUST use the `https` scheme and MAY contain port, path, and query parameter components.
+ * If omitted, the Credential Issuer does not support the Deferred Credential Endpoint.
+ */
+ @SerialName("deferred_credential_endpoint")
+ val deferredCredentialEndpointUrl: String? = null,
+
+ /**
+ * OID4VCI: OPTIONAL. URL of the Credential Issuer's Notification Endpoint, as defined in Section 10.
+ * This URL MUST use the `https` scheme and MAY contain port, path, and query parameter components.
+ * If omitted, the Credential Issuer does not support the Notification Endpoint.
+ */
+ @SerialName("notification_endpoint")
+ val notificationEndpointUrl: String? = null,
+
+ /**
+ * OID4VCI: OPTIONAL. Object containing information about whether the Credential Issuer supports encryption of the
+ * Credential and Batch Credential Response on top of TLS.
+ */
+ @SerialName("credential_response_encryption")
+ val credentialResponseEncryption: SupportedAlgorithmsContainer? = null,
+
+ /**
+ * OID4VCI: OPTIONAL. Object containing information about the Credential Issuer's supports for batch issuance of
+ * Credentials on the Credential Endpoint. The presence of this parameter means that the issuer supports the proofs
+ * parameter in the Credential Request so can issue more than one Verifiable Credential for the same Credential
+ * Dataset in a single request/response.
+ */
+ @SerialName("batch_credential_issuance")
+ val batchCredentialIssuance: BatchCredentialIssuanceMetadata? = null,
+
+ /**
+ * OPTIONAL. String that is a signed JWT. This JWT contains Credential Issuer metadata parameters as claims. The
+ * signed metadata MUST be secured using JSON Web Signature (JWS) (`RFC7515`) and MUST contain an `iat` (Issued At)
+ * claim, an `iss` (Issuer) claim denoting the party attesting to the claims in the signed metadata, and `sub`
+ * (Subject) claim matching the Credential Issuer identifier. If the Wallet supports signed metadata, metadata
+ * values conveyed in the signed JWT MUST take precedence over the corresponding values conveyed using plain JSON
+ * elements. If the Credential Issuer wants to enforce use of signed metadata, it omits the respective metadata
+ * parameters from the unsigned part of the Credential Issuer metadata. A [signedMetadata] metadata value MUST NOT
+ * appear as a claim in the JWT. The Wallet MUST establish trust in the signer of the metadata, and obtain the keys
+ * to validate the signature before processing the metadata. The concrete mechanism how to do that is out of scope
+ * of this specification and MAY be defined in the profiles of this specification.
+ */
+ // TODO Analyze usage
+ @SerialName("signed_metadata")
+ val signedMetadata: String? = null,
+
+ /**
+ * OID4VCI: OPTIONAL. An array of objects, where each object contains display properties of a Credential Issuer for
+ * a certain language.
+ */
+ @SerialName("display")
+ val displayProperties: Set? = null,
+
+ /**
+ * OID4VCI: REQUIRED. Object that describes specifics of the Credential that the Credential Issuer supports
+ * issuance of. This object contains a list of name/value pairs, where each name is a unique identifier of the
+ * supported Credential being described. This identifier is used in the Credential Offer as defined in
+ * Section 4.1.1 to communicate to the Wallet which Credential is being offered, see [CredentialOffer].
+ */
+ @SerialName("credential_configurations_supported")
+ val supportedCredentialConfigurations: Map? = null,
+) {
+ fun serialize() = jsonSerializer.encodeToString(this)
+
+ companion object {
+ fun deserialize(input: String): KmmResult =
+ runCatching { jsonSerializer.decodeFromString(input) }.wrap()
+ }
+}
\ No newline at end of file
diff --git a/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/Json.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/Json.kt
new file mode 100644
index 000000000..2b2c32dd1
--- /dev/null
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/Json.kt
@@ -0,0 +1,13 @@
+package at.asitplus.openid
+
+
+import kotlinx.serialization.json.Json
+
+val jsonSerializer by lazy {
+ Json {
+ prettyPrint = false
+ encodeDefaults = false
+ classDiscriminator = "type"
+ ignoreUnknownKeys = true
+ }
+}
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2AuthorizationServerMetadata.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OAuth2AuthorizationServerMetadata.kt
similarity index 73%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2AuthorizationServerMetadata.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OAuth2AuthorizationServerMetadata.kt
index 50c3e14fd..02690fc7e 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2AuthorizationServerMetadata.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OAuth2AuthorizationServerMetadata.kt
@@ -1,8 +1,9 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import at.asitplus.KmmResult
import at.asitplus.catching
-import at.asitplus.wallet.lib.oidc.jsonSerializer
+import at.asitplus.signum.indispensable.josef.JsonWebAlgorithm
+import at.asitplus.signum.indispensable.josef.JwsAlgorithm
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
@@ -15,7 +16,6 @@ import kotlinx.serialization.encodeToString
* which is the registered default-path, see
* [IANA](https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml)
*/
-
@Serializable
data class OAuth2AuthorizationServerMetadata(
/**
@@ -36,10 +36,11 @@ data class OAuth2AuthorizationServerMetadata(
* `RFC6749`. This is REQUIRED unless no grant types are supported
* that use the authorization endpoint.
*
- * NOTE: Mandatory for our use-case
+ * OIDC SIOPv2: REQUIRED. URL of the Self-Issued OP used by the RP to perform Authentication of the End-User.
+ * Can be custom URI scheme, or Universal Links/App links.
*/
@SerialName("authorization_endpoint")
- val authorizationEndpoint: String,
+ val authorizationEndpoint: String? = null,
/**
* RFC 9126: The URL of the pushed authorization request endpoint at which a client can post an authorization
@@ -62,11 +63,9 @@ data class OAuth2AuthorizationServerMetadata(
/**
* URL of the authorization server's token endpoint `RFC6749`. This
* is REQUIRED unless only the implicit grant type is supported.
- *
- * NOTE: Mandatory for our use-case
*/
@SerialName("token_endpoint")
- val tokenEndpoint: String,
+ val tokenEndpoint: String? = null,
/**
* OPTIONAL. URL of the authorization server's JWK Set `JWK`
@@ -78,9 +77,12 @@ data class OAuth2AuthorizationServerMetadata(
* encryption keys are made available, a "use" (public key use)
* parameter value is REQUIRED for all keys in the referenced JWK Set
* to indicate each key's intended usage.
+ *
+ * OIDC SIOPv2: MUST NOT be present in Self-Issued OP Metadata. If it is, the RP MUST ignore it and use the `sub`
+ * Claim in the ID Token to obtain signing keys to validate the signatures from the Self-Issued OpenID Provider.
*/
@SerialName("jwks_uri")
- val jwksUri: String? = null,
+ val jsonWebKeySetUrl: String? = null,
/**
* OPTIONAL. URL of the authorization server's OAuth 2.0 Dynamic
@@ -94,8 +96,11 @@ data class OAuth2AuthorizationServerMetadata(
* `RFC6749` "scope" values that this authorization server supports.
* Servers MAY choose not to advertise some supported scope values
* even when this parameter is used.
+ *
+ * OIDC SIOPv2: REQUIRED. A JSON array of strings representing supported scopes.
+ * MUST support the `openid` scope value.
*/
- @SerialName("scope_supported")
+ @SerialName("scopes_supported")
val scopesSupported: Set? = null,
/**
@@ -104,6 +109,8 @@ data class OAuth2AuthorizationServerMetadata(
* The array values used are the same as those used with the
* "response_types" parameter defined by "OAuth 2.0 Dynamic Client
* Registration Protocol" `RFC7591`.
+ *
+ * OIDC SIOPv2: MUST be `id_token`.
*/
@SerialName("response_types_supported")
val responseTypesSupported: Set? = null,
@@ -155,6 +162,77 @@ data class OAuth2AuthorizationServerMetadata(
@SerialName("token_endpoint_auth_signing_alg_methods_supported")
val tokenEndPointAuthSigningAlgValuesSupported: Set? = null,
+ /**
+ * OIDC Discovery: REQUIRED. JSON array containing a list of the Subject Identifier types that this OP supports.
+ * Valid types include `pairwise` and `public`.
+ */
+ @SerialName("subject_types_supported")
+ val subjectTypesSupported: Set? = null,
+
+ /**
+ * OIDC Discovery: REQUIRED. A JSON array containing a list of the JWS signing algorithms (`alg` values) supported
+ * by the OP for the ID Token to encode the Claims in a JWT (RFC7519).
+ * Valid values include `RS256`, `ES256`, `ES256K`, and `EdDSA`.
+ */
+ @SerialName("id_token_signing_alg_values_supported")
+ val idTokenSigningAlgorithmsSupported: Set? = null,
+
+ /**
+ * OIDC SIOPv2: REQUIRED. A JSON array containing a list of the JWS signing algorithms (alg values) supported by the
+ * OP for Request Objects, which are described in Section 6.1 of OpenID.Core.
+ * Valid values include `none`, `RS256`, `ES256`, `ES256K`, and `EdDSA`.
+ */
+ @SerialName("request_object_signing_alg_values_supported")
+ val requestObjectSigningAlgorithmsSupported: Set? = null,
+
+ /**
+ * OIDC SIOPv2: REQUIRED. A JSON array of strings representing URI scheme identifiers and optionally method names of
+ * supported Subject Syntax Types.
+ * Valid values include `urn:ietf:params:oauth:jwk-thumbprint`, `did:example` and others.
+ */
+ @SerialName("subject_syntax_types_supported")
+ // TODO Verify usage of "jwk", maybe remove did
+ val subjectSyntaxTypesSupported: Set? = null,
+
+ /**
+ * OIDC SIOPv2: OPTIONAL. A JSON array of strings containing the list of ID Token types supported by the OP,
+ * the default value is `attester_signed_id_token` (the id token is issued by the party operating the OP, i.e. this
+ * is the classical id token as defined in OpenID.Core), may also include `subject_signed_id_token` (Self-Issued
+ * ID Token, i.e. the id token is signed with key material under the end-user's control).
+ */
+ @SerialName("id_token_types_supported")
+ val idTokenTypesSupported: Set? = null,
+
+ /**
+ * OID4VP: OPTIONAL. Boolean value specifying whether the Wallet supports the transfer of `presentation_definition`
+ * by reference, with true indicating support. If omitted, the default value is true.
+ */
+ @SerialName("presentation_definition_uri_supported")
+ val presentationDefinitionUriSupported: Boolean = true,
+
+ /**
+ * OID4VP: REQUIRED. An object containing a list of key value pairs, where the key is a string identifying a
+ * Credential format supported by the Wallet. Valid Credential format identifier values are defined in Annex E
+ * of OpenID.VCI. Other values may be used when defined in the profiles of this specification.
+ */
+ @SerialName("vp_formats_supported")
+ val vpFormatsSupported: VpFormatsSupported? = null,
+
+ /**
+ * OID4VP: OPTIONAL. Array of JSON Strings containing the values of the Client Identifier schemes that the Wallet
+ * supports. The values defined by this specification are `pre-registered`, `redirect_uri`, `entity_id`, `did`.
+ * If omitted, the default value is pre-registered.
+ */
+ @SerialName("client_id_schemes_supported")
+ val clientIdSchemesSupported: Set? = null,
+
+ /**
+ * RFC 9449: A JSON array containing a list of the JWS alg values (from the `IANA.JOSE.ALGS` registry) supported
+ * by the authorization server for DPoP proof JWTs.
+ */
+ @SerialName("dpop_signing_alg_values_supported")
+ val dpopSigningAlgValuesSupported: Set? = null,
+
/**
* OPTIONAL. URL of a page containing human-readable information
* that developers might want or need to know when using the
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OidcAddressClaim.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OidcAddressClaim.kt
similarity index 94%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OidcAddressClaim.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OidcAddressClaim.kt
index c1919603c..a920fa14e 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OidcAddressClaim.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OidcAddressClaim.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OidcUserInfo.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OidcUserInfo.kt
similarity index 94%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OidcUserInfo.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OidcUserInfo.kt
index 1127c0d33..59fd08efe 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OidcUserInfo.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OidcUserInfo.kt
@@ -1,6 +1,6 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
-import at.asitplus.wallet.lib.data.InstantLongSerializer
+import at.asitplus.signum.indispensable.josef.io.InstantLongSerializer
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OidcUserInfoExtended.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OidcUserInfoExtended.kt
similarity index 96%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OidcUserInfoExtended.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OidcUserInfoExtended.kt
index acf26ebb2..01c4e84de 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OidcUserInfoExtended.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OidcUserInfoExtended.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import at.asitplus.KmmResult
import at.asitplus.KmmResult.Companion.wrap
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OpenIdConstants.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OpenIdConstants.kt
similarity index 89%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OpenIdConstants.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OpenIdConstants.kt
index 2242a2f08..a0f9ce73e 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OpenIdConstants.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/OpenIdConstants.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.oidc
+package at.asitplus.openid
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
@@ -30,18 +30,22 @@ object OpenIdConstants {
const val PREFIX_DID_KEY = "did:key"
+ /** OID4VCI support binding keys to [at.asitplus.signum.indispensable.josef.JsonWebKey] */
+ const val BINDING_METHOD_JWK = "jwk"
+
const val PATH_WELL_KNOWN_CREDENTIAL_ISSUER = "/.well-known/openid-credential-issuer"
+ const val PATH_WELL_KNOWN_OPENID_CONFIGURATION = "/.well-known/openid-configuration"
+
const val SCOPE_OPENID = "openid"
const val SCOPE_PROFILE = "profile"
const val CODE_CHALLENGE_METHOD_SHA256 = "S256"
- /**
- * To be used in [at.asitplus.wallet.lib.oidvci.AuthorizationDetails.type]
- */
- const val CREDENTIAL_TYPE_OPENID = "openid_credential"
+ const val PROOF_JWT_TYPE = "openid4vci-proof+jwt"
+
+ const val PROOF_CWT_TYPE = "openid4vci-proof+cwt"
@Serializable(with = ProofType.Serializer::class)
sealed class ProofType(val stringRepresentation: String) {
@@ -54,43 +58,28 @@ object OpenIdConstants {
companion object {
private const val STRING_JWT = "jwt"
private const val STRING_CWT = "cwt"
- private const val STRING_JWT_HEADER = "openid4vci-proof+jwt"
- private const val STRING_CWT_HEADER = "openid4vci-proof+cwt"
-
}
/**
- * Proof type in [at.asitplus.wallet.lib.oidvci.CredentialRequestProof]
+ * Proof type in [at.asitplus.openid.CredentialRequestProof]
*/
- @Serializable(with = ProofType.Serializer::class)
+ @Serializable(with = Serializer::class)
object JWT : ProofType(STRING_JWT)
/**
- * Proof type in [at.asitplus.wallet.lib.oidvci.CredentialRequestProof]
+ * Proof type in [at.asitplus.openid.CredentialRequestProof]
+ *
+ * Removed in OID4VCI Draft 14, kept here for a bit of backwards-compatibility
*/
- @Serializable(with = ProofType.Serializer::class)
+ @Serializable(with = Serializer::class)
object CWT : ProofType(STRING_CWT)
- //TODO why are these located here?
- /**
- * Constant from OID4VCI
- */
- @Serializable(with = ProofType.Serializer::class)
- object JWT_HEADER_TYPE : ProofType(STRING_JWT_HEADER)
-
- /**
- * Constant from OID4VCI
- */
- @Serializable(with = ProofType.Serializer::class)
- object CWT_HEADER_TYPE : ProofType(STRING_CWT_HEADER)
-
/**
* Any proof type not natively supported by this library
*/
- @Serializable(with = ProofType.Serializer::class)
+ @Serializable(with = Serializer::class)
class OTHER(stringRepresentation: String) : ProofType(stringRepresentation)
-
object Serializer : KSerializer {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor(serialName = "ProofType", PrimitiveKind.STRING)
@@ -99,8 +88,6 @@ object OpenIdConstants {
return when (val str = decoder.decodeString()) {
STRING_JWT -> JWT
STRING_CWT -> CWT
- STRING_CWT_HEADER -> CWT_HEADER_TYPE
- STRING_JWT_HEADER -> JWT_HEADER_TYPE
else -> OTHER(str)
}
}
@@ -138,7 +125,7 @@ object OpenIdConstants {
* Wallet in advance of the Authorization Request. The Verifier metadata is obtained using RFC7591 or
* through out-of-band mechanisms.
*/
- @Serializable(with = ClientIdScheme.Serializer::class)
+ @Serializable(with = Serializer::class)
object PRE_REGISTERED : ClientIdScheme(STRING_PRE_REGISTERED)
/**
@@ -147,7 +134,7 @@ object OpenIdConstants {
* Authorization Request parameter, and all Verifier metadata parameters MUST be passed using the
* `client_metadata` or `client_metadata_uri` parameter.
*/
- @Serializable(with = ClientIdScheme.Serializer::class)
+ @Serializable(with = Serializer::class)
object REDIRECT_URI : ClientIdScheme(STRING_REDIRECT_URI)
/**
@@ -164,7 +151,7 @@ object OpenIdConstants {
* to freely choose the `redirect_uri` value. If not, the FQDN of the `redirect_uri` value MUST match the
* Client Identifier.
*/
- @Serializable(with = ClientIdScheme.Serializer::class)
+ @Serializable(with = Serializer::class)
object X509_SAN_DNS : ClientIdScheme(STRING_X509_SAN_DNS)
/**
@@ -181,7 +168,7 @@ object OpenIdConstants {
* to freely choose the `redirect_uri` value. If not, the FQDN of the `redirect_uri` value MUST match the
* Client Identifier.
*/
- @Serializable(with = ClientIdScheme.Serializer::class)
+ @Serializable(with = Serializer::class)
object X509_SAN_URI : ClientIdScheme(STRING_X509_SAN_URI)
/**
@@ -192,7 +179,7 @@ object OpenIdConstants {
* `client_metadata_uri` parameter MUST NOT be present in the Authorization Request when this Client
* Identifier scheme is used.
*/
- @Serializable(with = ClientIdScheme.Serializer::class)
+ @Serializable(with = Serializer::class)
object ENTITY_ID : ClientIdScheme(STRING_ENTITY_ID)
/**
@@ -204,7 +191,7 @@ object OpenIdConstants {
* the Verifier. All Verifier metadata other than the public key MUST be obtained from the `client_metadata`
* or the `client_metadata_uri` parameter.
*/
- @Serializable(with = ClientIdScheme.Serializer::class)
+ @Serializable(with = Serializer::class)
object DID : ClientIdScheme(STRING_DID)
/**
@@ -220,13 +207,13 @@ object OpenIdConstants {
* the `redirect_uris` claim entries. All Verifier metadata other than the public key MUST be obtained from the
* `client_metadata` or the `client_metadata_uri parameter`.
*/
- @Serializable(with = ClientIdScheme.Serializer::class)
+ @Serializable(with = Serializer::class)
object VERIFIER_ATTESTATION : ClientIdScheme(STRING_VERIFIER_ATTESTATION)
/**
* Any not natively supported client id scheme, so it can still be parsed
*/
- @Serializable(with = ClientIdScheme.Serializer::class)
+ @Serializable(with = Serializer::class)
class OTHER(stringRepresentation: String) : ClientIdScheme(stringRepresentation)
object Serializer : KSerializer {
@@ -277,7 +264,7 @@ object OpenIdConstants {
* to the Verifier, or it can end with a redirect that follows the HTTPS POST request, if the Verifier responds
* with a redirect URI to the Wallet.
*/
- @Serializable(with = ResponseMode.Serializer::class)
+ @Serializable(with = Serializer::class)
object DIRECT_POST : ResponseMode(STRING_DIRECT_POST)
/**
@@ -286,27 +273,27 @@ object OpenIdConstants {
* containing the JWT as defined in Section 4.1. of JARM and Section 6.3 in the body of an HTTPS POST request
* using the `application/x-www-form-urlencoded` content type.
*/
- @Serializable(with = ResponseMode.Serializer::class)
+ @Serializable(with = Serializer::class)
object DIRECT_POST_JWT : ResponseMode(STRING_DIRECT_POST_JWT)
/**
* OAuth 2.0: In this mode, Authorization Response parameters are encoded in the query string added to the
* `redirect_uri` when redirecting back to the Client.
*/
- @Serializable(with = ResponseMode.Serializer::class)
+ @Serializable(with = Serializer::class)
object QUERY : ResponseMode(STRING_QUERY)
/**
* OAuth 2.0: In this mode, Authorization Response parameters are encoded in the fragment added to the
* `redirect_uri` when redirecting back to the Client.
*/
- @Serializable(with = ResponseMode.Serializer::class)
+ @Serializable(with = Serializer::class)
object FRAGMENT : ResponseMode(STRING_FRAGMENT)
/**
* Any not natively supported Client ID Scheme, so it can still be parsed
*/
- @Serializable(with = ResponseMode.Serializer::class)
+ @Serializable(with = Serializer::class)
class OTHER(stringRepresentation: String) : ResponseMode(stringRepresentation)
object Serializer : KSerializer {
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/RelyingPartyMetadata.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/RelyingPartyMetadata.kt
similarity index 98%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/RelyingPartyMetadata.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/RelyingPartyMetadata.kt
index 4a8b90b62..bf4d6adc2 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/RelyingPartyMetadata.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/RelyingPartyMetadata.kt
@@ -1,11 +1,11 @@
-package at.asitplus.wallet.lib.oidc
+package at.asitplus.openid
import at.asitplus.KmmResult.Companion.wrap
import at.asitplus.signum.indispensable.josef.JsonWebKeySet
import at.asitplus.signum.indispensable.josef.JweAlgorithm
import at.asitplus.signum.indispensable.josef.JweEncryption
import at.asitplus.signum.indispensable.josef.JwsAlgorithm
-import at.asitplus.wallet.lib.data.dif.FormatHolder
+import at.asitplus.dif.FormatHolder
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/mdl/RequestedCredentialClaimSpecification.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/RequestedCredentialClaimSpecification.kt
similarity index 92%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/mdl/RequestedCredentialClaimSpecification.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/RequestedCredentialClaimSpecification.kt
index f6d961d69..54d7c5bd6 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/mdl/RequestedCredentialClaimSpecification.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/RequestedCredentialClaimSpecification.kt
@@ -1,6 +1,5 @@
-package at.asitplus.wallet.lib.oidvci.mdl
+package at.asitplus.openid
-import at.asitplus.wallet.lib.oidvci.DisplayProperties
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SupportedAlgorithmsContainer.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedAlgorithmsContainer.kt
similarity index 97%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SupportedAlgorithmsContainer.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedAlgorithmsContainer.kt
index f74a4836c..ff09a1aff 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SupportedAlgorithmsContainer.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedAlgorithmsContainer.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import at.asitplus.signum.indispensable.josef.JsonWebAlgorithm
import at.asitplus.signum.indispensable.josef.JweAlgorithm
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SupportedCredentialFormat.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedCredentialFormat.kt
similarity index 98%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SupportedCredentialFormat.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedCredentialFormat.kt
index e9944b107..8b0ec692f 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SupportedCredentialFormat.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedCredentialFormat.kt
@@ -1,6 +1,5 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
-import at.asitplus.wallet.lib.oidvci.mdl.RequestedCredentialClaimSpecification
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@@ -14,6 +13,7 @@ import kotlinx.serialization.json.encodeToJsonElement
* being offered.
*/
@Serializable
+@ConsistentCopyVisibility
data class SupportedCredentialFormat private constructor(
/**
* OID4VCI: REQUIRED. A JSON string identifying the format of this credential, e.g. `jwt_vc_json` or `ldp_vc`.
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SupportedCredentialFormatDefinition.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedCredentialFormatDefinition.kt
similarity index 74%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SupportedCredentialFormatDefinition.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedCredentialFormatDefinition.kt
index b679e3b60..42f25d3cc 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SupportedCredentialFormatDefinition.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/SupportedCredentialFormatDefinition.kt
@@ -1,6 +1,5 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
-import at.asitplus.wallet.lib.oidvci.mdl.RequestedCredentialClaimSpecification
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -16,7 +15,7 @@ data class SupportedCredentialFormatDefinition(
* according to (VC_DATA), Section 4.3, e.g. `VerifiableCredential`, `UniversityDegreeCredential`
*/
@SerialName("type")
- val types: Collection? = null,
+ val types: Set? = null,
/**
* OID4VCI:
@@ -27,7 +26,4 @@ data class SupportedCredentialFormatDefinition(
@SerialName("credentialSubject")
val credentialSubject: Map? = null,
- // TODO is present in EUDIW issuer ... but is this really valid?
- @SerialName("claims")
- val claims: Map? = null,
)
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/TokenRequestParameters.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/TokenRequestParameters.kt
similarity index 62%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/TokenRequestParameters.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/TokenRequestParameters.kt
index afe3ac72c..947f7f5f9 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/TokenRequestParameters.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/TokenRequestParameters.kt
@@ -1,8 +1,7 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import at.asitplus.KmmResult
import at.asitplus.KmmResult.Companion.wrap
-import at.asitplus.wallet.lib.oidc.jsonSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
@@ -23,6 +22,23 @@ data class TokenRequestParameters(
@SerialName("code")
val code: String? = null,
+ /**
+ * RFC6749: OPTIONAL. The authorization and token endpoints allow the client to specify the
+ * scope of the access request using the "scope" request parameter. In
+ * turn, the authorization server uses the "scope" response parameter to
+ * inform the client of the scope of the access token issued.
+ */
+ @SerialName("scope")
+ val scope: String? = null,
+
+ /**
+ * RFC8707: When requesting a token, the client can indicate the desired target service(s) where it intends to use
+ * that token by way of the [resource] parameter and can indicate the desired scope of the requested token using the
+ * [scope] parameter.
+ */
+ @SerialName("resource")
+ val resource: String? = null,
+
/**
* RFC6749:
* REQUIRED, if the "redirect_uri" parameter was included in the authorization request,
@@ -39,8 +55,9 @@ data class TokenRequestParameters(
val clientId: String,
/**
- * OID4VP: TODO Definition
- * RFC9396
+ * OID4VCI: Credential Issuers MAY support requesting authorization to issue a Credential using this parameter.
+ * The request parameter `authorization_details` defined in Section 2 of `RFC9396` MUST be used to convey the
+ * details about the Credentials the Wallet wants to obtain.
*/
@SerialName("authorization_details")
val authorizationDetails: Set? = null,
@@ -58,7 +75,7 @@ data class TokenRequestParameters(
* This parameter MUST only be used if the [grantType] is `urn:ietf:params:oauth:grant-type:pre-authorized_code`.
*/
@SerialName("tx_code")
- val transactionCode: CredentialOfferGrantsPreAuthCodeTransactionCode? = null,
+ val transactionCode: String? = null,
/**
* RFC7636: A cryptographically random string that is used to correlate the authorization request to the token
@@ -66,6 +83,15 @@ data class TokenRequestParameters(
*/
@SerialName("code_verifier")
val codeVerifier: String? = null,
+
+ /**
+ * CSC: OPTIONAL
+ * Arbitrary data from the signature application. It can be used to handle a
+ * transaction identifier or other application-spe cific data that may be useful for
+ * debugging purposes
+ */
+ @SerialName("clientData")
+ val clientData: String? = null,
) {
fun serialize() = jsonSerializer.encodeToString(this)
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/TokenResponseParameters.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/TokenResponseParameters.kt
similarity index 86%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/TokenResponseParameters.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/TokenResponseParameters.kt
index d93cb4ba4..fbbb85322 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/TokenResponseParameters.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/TokenResponseParameters.kt
@@ -1,9 +1,7 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import at.asitplus.KmmResult
import at.asitplus.KmmResult.Companion.wrap
-import at.asitplus.wallet.lib.data.DurationSecondsIntSerializer
-import at.asitplus.wallet.lib.oidc.jsonSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
@@ -90,6 +88,17 @@ data class TokenResponseParameters(
*/
@SerialName("authorization_details")
val authorizationDetails: Set? = null,
+
+ /**
+ * CSC: OPTIONAL
+ * The identifier associated to the credential authorized in the corresponding authorization
+ * request. This response parameter MAY be present in case the scope credential is used
+ * in the authorization request along with the parameter “signatureQualifier” and the
+ * authorization server determined a credentialID in the authorization process to be used
+ * in subsequent signature operations.
+ */
+ @SerialName("credentialID")
+ val credentialId: String? = null,
) {
fun serialize() = jsonSerializer.encodeToString(this)
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/VpFormatsSupported.kt b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/VpFormatsSupported.kt
similarity index 95%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/VpFormatsSupported.kt
rename to openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/VpFormatsSupported.kt
index d1a0a60c2..576083de5 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/VpFormatsSupported.kt
+++ b/openid-data-classes/src/commonMain/kotlin/at/asitplus/openid/VpFormatsSupported.kt
@@ -1,4 +1,4 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.openid
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4f658428c..6671f6bad 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -4,10 +4,14 @@ import java.util.*
pluginManagement {
includeBuild("conventions-vclib")
repositories {
- maven("https://maven.pkg.jetbrains.space/kotlin/p/dokka/dev")
google()
gradlePluginPortal()
mavenCentral()
+ maven("https://s01.oss.sonatype.org/content/repositories/snapshots")
+ maven {
+ url = uri("https://raw.githubusercontent.com/a-sit-plus/gradle-conventions-plugin/mvn/repo")
+ name = "aspConventions"
+ }
}
}
@@ -17,18 +21,26 @@ if (System.getProperty("publishing.excludeIncludedBuilds") != "true") {
substitute(module("at.asitplus.signum:indispensable")).using(project(":indispensable"))
substitute(module("at.asitplus.signum:indispensable-josef")).using(project(":indispensable-josef"))
substitute(module("at.asitplus.signum:indispensable-cosef")).using(project(":indispensable-cosef"))
+ substitute(module("at.asitplus.signum:supreme")).using(project(":supreme"))
}
}
} else logger.lifecycle("Excluding Signum from this build")
rootProject.name = "vc-k"
+include(":dif-data-classes")
+include(":openid-data-classes")
include(":vck")
include(":vck-aries")
include(":vck-openid")
+include(":mobiledrivinglicence")
dependencyResolutionManagement {
- repositories.add(repositories.mavenCentral())
- repositories.add(repositories.mavenLocal())
+ repositories {
+ maven("https://s01.oss.sonatype.org/content/repositories/snapshots") //Signum snapshot
+ mavenLocal()
+ mavenCentral()
+ }
+
versionCatalogs {
val versions = Properties().apply {
kotlin.runCatching {
diff --git a/signum b/signum
index fc3e39a32..e8f9bd4a0 160000
--- a/signum
+++ b/signum
@@ -1 +1 @@
-Subproject commit fc3e39a32ddf56fcf01df265437f04d846758716
+Subproject commit e8f9bd4a0be5a9018e6e4ffe137b22191b06c1e1
diff --git a/vck-aries/build.gradle.kts b/vck-aries/build.gradle.kts
index fd5359cc5..2465c34bd 100644
--- a/vck-aries/build.gradle.kts
+++ b/vck-aries/build.gradle.kts
@@ -1,7 +1,6 @@
-import at.asitplus.gradle.commonImplementationDependencies
-import at.asitplus.gradle.commonIosExports
-import at.asitplus.gradle.exportIosFramework
-import at.asitplus.gradle.setupDokka
+import at.asitplus.gradle.*
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree.Companion.test
plugins {
kotlin("multiplatform")
@@ -17,9 +16,18 @@ group = "at.asitplus.wallet"
version = artifactVersion
+setupAndroid()
+
kotlin {
jvm()
+
+ androidTarget {
+ publishLibraryVariants("release")
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ instrumentedTestVariant.sourceSetTree.set(test)
+ }
+
iosArm64()
iosSimulatorArm64()
iosX64()
@@ -48,8 +56,8 @@ kotlin {
exportIosFramework(
"VckAriesKmm",
- static = false,
- *commonIosExports(), project(":vck")
+ transitiveExports = false,
+ project(":vck")
)
val javadocJar = setupDokka(
diff --git a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocol.kt b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocol.kt
index 0efb92ff3..762c929c6 100644
--- a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocol.kt
+++ b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocol.kt
@@ -10,9 +10,9 @@ import at.asitplus.wallet.lib.data.AriesGoalCodeParser
import at.asitplus.wallet.lib.data.AttributeIndex
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.SchemaIndex
-import at.asitplus.wallet.lib.data.dif.CredentialDefinition
-import at.asitplus.wallet.lib.data.dif.CredentialManifest
-import at.asitplus.wallet.lib.data.dif.SchemaReference
+import at.asitplus.dif.CredentialDefinition
+import at.asitplus.dif.CredentialManifest
+import at.asitplus.dif.SchemaReference
import at.asitplus.wallet.lib.iso.IssuerSigned
import at.asitplus.wallet.lib.msg.AttachmentFormatReference
import at.asitplus.wallet.lib.msg.IssueCredential
@@ -143,7 +143,7 @@ class IssueCredentialProtocol(
}
private fun createOobInvitation(): InternalNextMessage {
- val recipientKey = issuer?.keyPair?.identifier
+ val recipientKey = issuer?.keyMaterial?.identifier
?: return InternalNextMessage.IncorrectState("issuer")
val message = OutOfBandInvitation(
body = OutOfBandInvitationBody(
diff --git a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/MessageWrapper.kt b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/MessageWrapper.kt
index 719ba12c9..e959f1db7 100644
--- a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/MessageWrapper.kt
+++ b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/MessageWrapper.kt
@@ -2,34 +2,25 @@ package at.asitplus.wallet.lib.aries
import at.asitplus.KmmResult
import at.asitplus.catching
-import at.asitplus.signum.indispensable.josef.JsonWebKey
-import at.asitplus.signum.indispensable.josef.JweAlgorithm
-import at.asitplus.signum.indispensable.josef.JweEncrypted
-import at.asitplus.signum.indispensable.josef.JweEncryption
-import at.asitplus.signum.indispensable.josef.JwsSigned
-import at.asitplus.signum.indispensable.josef.toJsonWebKey
+import at.asitplus.signum.indispensable.josef.*
import at.asitplus.wallet.lib.agent.DefaultCryptoService
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.jws.DefaultJwsService
-import at.asitplus.wallet.lib.jws.DefaultVerifierJwsService
-import at.asitplus.wallet.lib.jws.JwsContentTypeConstants
-import at.asitplus.wallet.lib.jws.JwsService
-import at.asitplus.wallet.lib.jws.VerifierJwsService
+import at.asitplus.wallet.lib.agent.KeyMaterial
+import at.asitplus.wallet.lib.jws.*
import at.asitplus.wallet.lib.msg.JsonWebMessage
import io.github.aakira.napier.Napier
class MessageWrapper(
- private val keyPairAdapter: KeyPairAdapter,
- private val jwsService: JwsService = DefaultJwsService(DefaultCryptoService(keyPairAdapter)),
+ private val keyMaterial: KeyMaterial,
+ private val jwsService: JwsService = DefaultJwsService(DefaultCryptoService(keyMaterial)),
private val verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(),
) {
suspend fun parseMessage(it: String): ReceivedMessage {
- val jwsSigned = JwsSigned.parse(it).getOrNull()
+ val jwsSigned = JwsSigned.deserialize(it).getOrNull()
if (jwsSigned != null) {
- return parseJwsMessage(jwsSigned, it)
+ return parseJwsMessage(jwsSigned)
}
- val jweEncrypted = JweEncrypted.parse(it).getOrNull()
+ val jweEncrypted = JweEncrypted.deserialize(it).getOrNull()
if (jweEncrypted != null)
return parseJweMessage(jweEncrypted, it)
return ReceivedMessage.Error
@@ -47,10 +38,10 @@ class MessageWrapper(
}
val payloadString = joseObject.payload.decodeToString()
if (joseObject.header.contentType == JwsContentTypeConstants.DIDCOMM_SIGNED_JSON) {
- val parsed = JwsSigned.parse(payloadString).getOrNull()
+ val parsed = JwsSigned.deserialize(payloadString).getOrNull()
?: return ReceivedMessage.Error
.also { Napier.w("Could not parse inner JWS") }
- return parseJwsMessage(parsed, payloadString)
+ return parseJwsMessage(parsed)
}
if (joseObject.header.contentType == JwsContentTypeConstants.DIDCOMM_PLAIN_JSON) {
val message = JsonWebMessage.deserialize(payloadString).getOrElse { ex ->
@@ -63,7 +54,7 @@ class MessageWrapper(
.also { Napier.w("ContentType not matching") }
}
- private fun parseJwsMessage(joseObject: JwsSigned, serialized: String): ReceivedMessage {
+ private fun parseJwsMessage(joseObject: JwsSigned): ReceivedMessage {
Napier.d("Parsing JWS ${joseObject.serialize()}")
if (!verifierJwsService.verifyJwsObject(joseObject))
return ReceivedMessage.Error
diff --git a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt
index dc2fadace..962c4ede1 100644
--- a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt
+++ b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocol.kt
@@ -1,5 +1,6 @@
package at.asitplus.wallet.lib.aries
+import at.asitplus.dif.*
import at.asitplus.signum.indispensable.josef.JsonWebKey
import at.asitplus.signum.indispensable.josef.JwsAlgorithm
import at.asitplus.wallet.lib.agent.Holder
@@ -7,26 +8,7 @@ import at.asitplus.wallet.lib.agent.Verifier
import at.asitplus.wallet.lib.data.AriesGoalCodeParser
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.SchemaIndex
-import at.asitplus.wallet.lib.data.dif.Constraint
-import at.asitplus.wallet.lib.data.dif.ConstraintField
-import at.asitplus.wallet.lib.data.dif.ConstraintFilter
-import at.asitplus.wallet.lib.data.dif.FormatContainerJwt
-import at.asitplus.wallet.lib.data.dif.FormatHolder
-import at.asitplus.wallet.lib.data.dif.InputDescriptor
-import at.asitplus.wallet.lib.data.dif.PresentationDefinition
-import at.asitplus.wallet.lib.data.dif.SchemaReference
-import at.asitplus.wallet.lib.msg.AttachmentFormatReference
-import at.asitplus.wallet.lib.msg.JsonWebMessage
-import at.asitplus.wallet.lib.msg.JwmAttachment
-import at.asitplus.wallet.lib.msg.OutOfBandInvitation
-import at.asitplus.wallet.lib.msg.OutOfBandInvitationBody
-import at.asitplus.wallet.lib.msg.OutOfBandService
-import at.asitplus.wallet.lib.msg.Presentation
-import at.asitplus.wallet.lib.msg.PresentationBody
-import at.asitplus.wallet.lib.msg.RequestPresentation
-import at.asitplus.wallet.lib.msg.RequestPresentationAttachment
-import at.asitplus.wallet.lib.msg.RequestPresentationAttachmentOptions
-import at.asitplus.wallet.lib.msg.RequestPresentationBody
+import at.asitplus.wallet.lib.msg.*
import com.benasher44.uuid.uuid4
import io.github.aakira.napier.Napier
import kotlinx.serialization.encodeToString
@@ -204,18 +186,18 @@ class PresentProofProtocol(
.also { this.state = State.REQUEST_PRESENTATION_SENT }
}
+ @Suppress("DEPRECATION")
private fun buildRequestPresentationMessage(
credentialScheme: ConstantIndex.CredentialScheme,
parentThreadId: String? = null,
): RequestPresentation? {
- val verifierIdentifier = verifier?.keyPair?.identifier ?: return null
+ val verifierIdentifier = verifier?.keyMaterial?.identifier ?: return null
val claimsConstraints = requestedClaims?.map(this::buildConstraintFieldForClaim) ?: listOf()
val typeConstraints = buildConstraintFieldForType(credentialScheme.vcType!!)
val presentationDefinition = PresentationDefinition(
inputDescriptors = listOf(
- InputDescriptor(
+ DifInputDescriptor(
name = credentialScheme.vcType!!,
- schema = SchemaReference(uri = credentialScheme.schemaUri),
constraints = Constraint(
fields = claimsConstraints + typeConstraints
)
diff --git a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/RequestCredentialAttachment.kt b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/RequestCredentialAttachment.kt
index e63fcee00..89cfd389f 100644
--- a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/RequestCredentialAttachment.kt
+++ b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/RequestCredentialAttachment.kt
@@ -2,8 +2,8 @@ package at.asitplus.wallet.lib.msg
import at.asitplus.KmmResult.Companion.wrap
import at.asitplus.wallet.lib.aries.jsonSerializer
-import at.asitplus.wallet.lib.data.dif.CredentialManifest
-import at.asitplus.wallet.lib.data.dif.PresentationSubmission
+import at.asitplus.dif.CredentialManifest
+import at.asitplus.dif.PresentationSubmission
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
diff --git a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/RequestPresentationAttachment.kt b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/RequestPresentationAttachment.kt
index 4a6eed5d0..947bbf28c 100644
--- a/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/RequestPresentationAttachment.kt
+++ b/vck-aries/src/commonMain/kotlin/at/asitplus/wallet/lib/msg/RequestPresentationAttachment.kt
@@ -2,7 +2,7 @@ package at.asitplus.wallet.lib.msg
import at.asitplus.KmmResult.Companion.wrap
import at.asitplus.wallet.lib.aries.jsonSerializer
-import at.asitplus.wallet.lib.data.dif.PresentationDefinition
+import at.asitplus.dif.PresentationDefinition
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
diff --git a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/DummyCredentialDataProvider.kt b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/DummyCredentialDataProvider.kt
index 8325f1a15..f722e0783 100644
--- a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/DummyCredentialDataProvider.kt
+++ b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/DummyCredentialDataProvider.kt
@@ -8,8 +8,13 @@ import at.asitplus.wallet.lib.agent.CredentialToBeIssued
import at.asitplus.wallet.lib.agent.IssuerCredentialDataProvider
import at.asitplus.wallet.lib.data.AtomicAttribute2023
import at.asitplus.wallet.lib.data.ConstantIndex
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_DATE_OF_BIRTH
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_FAMILY_NAME
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_GIVEN_NAME
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_PORTRAIT
import at.asitplus.wallet.lib.iso.IssuerSignedItem
import kotlinx.datetime.Clock
+import kotlinx.datetime.LocalDate
import kotlin.random.Random
import kotlin.time.Duration.Companion.minutes
@@ -31,9 +36,10 @@ class DummyCredentialDataProvider(
val subjectId = subjectPublicKey.didEncoded
val expiration = clock.now() + defaultLifetime
val claims = listOf(
- ClaimToBeIssued("given_name", "Susanne"),
- ClaimToBeIssued("family_name", "Meier"),
- ClaimToBeIssued("date_of_birth", "1990-01-01"),
+ ClaimToBeIssued(CLAIM_GIVEN_NAME, "Susanne"),
+ ClaimToBeIssued(CLAIM_FAMILY_NAME, "Meier"),
+ ClaimToBeIssued(CLAIM_DATE_OF_BIRTH, LocalDate.parse("1990-01-01")),
+ ClaimToBeIssued(CLAIM_PORTRAIT, Random.nextBytes(32)),
)
when (representation) {
ConstantIndex.CredentialRepresentation.SD_JWT -> CredentialToBeIssued.VcSd(
@@ -42,7 +48,7 @@ class DummyCredentialDataProvider(
)
ConstantIndex.CredentialRepresentation.PLAIN_JWT -> CredentialToBeIssued.VcJwt(
- subject = AtomicAttribute2023(subjectId, "given_name", "Susanne"),
+ subject = AtomicAttribute2023(subjectId, CLAIM_GIVEN_NAME, "Susanne"),
expiration = expiration,
)
diff --git a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialMessengerConcurrentTest.kt b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialMessengerConcurrentTest.kt
index 287f80ea7..58e95639f 100644
--- a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialMessengerConcurrentTest.kt
+++ b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialMessengerConcurrentTest.kt
@@ -1,12 +1,6 @@
package at.asitplus.wallet.lib.aries
-import at.asitplus.wallet.lib.agent.Holder
-import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.Issuer
-import at.asitplus.wallet.lib.agent.IssuerAgent
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.agent.SubjectCredentialStore
+import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.data.AtomicAttribute2023
import at.asitplus.wallet.lib.data.ConstantIndex
import com.benasher44.uuid.uuid4
@@ -18,14 +12,14 @@ import kotlinx.coroutines.launch
class IssueCredentialMessengerConcurrentTest : FreeSpec() {
- private lateinit var issuerKeyPair: KeyPairAdapter
+ private lateinit var issuerKeyPair: KeyMaterial
private lateinit var issuer: Issuer
private lateinit var issuerServiceEndpoint: String
private lateinit var issuerMessenger: IssueCredentialMessenger
init {
beforeEach {
- issuerKeyPair = RandomKeyPairAdapter()
+ issuerKeyPair = EphemeralKeyWithoutCert()
issuer = IssuerAgent(issuerKeyPair, DummyCredentialDataProvider())
issuerServiceEndpoint = "https://example.com/issue?${uuid4()}"
issuerMessenger = initIssuerMessenger(ConstantIndex.AtomicAttribute2023)
@@ -45,7 +39,7 @@ class IssueCredentialMessengerConcurrentTest : FreeSpec() {
}
private fun initHolderMessenger(): IssueCredentialMessenger {
- val keyPair = RandomKeyPairAdapter()
+ val keyPair = EphemeralKeyWithoutCert()
return IssueCredentialMessenger.newHolderInstance(
holder = HolderAgent(keyPair),
messageWrapper = MessageWrapper(keyPair),
diff --git a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialMessengerTest.kt b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialMessengerTest.kt
index 4272357fc..e197e2f85 100644
--- a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialMessengerTest.kt
+++ b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialMessengerTest.kt
@@ -1,25 +1,17 @@
package at.asitplus.wallet.lib.aries
-import at.asitplus.wallet.lib.agent.Holder
-import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.Issuer
-import at.asitplus.wallet.lib.agent.IssuerAgent
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.agent.SubjectCredentialStore
+import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.data.AtomicAttribute2023
import at.asitplus.wallet.lib.data.ConstantIndex
import com.benasher44.uuid.uuid4
import io.kotest.core.spec.style.FreeSpec
-import io.kotest.matchers.collections.shouldBeEmpty
-import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
class IssueCredentialMessengerTest : FreeSpec() {
- private lateinit var issuerKeyPair: KeyPairAdapter
- private lateinit var holderKeyPair: KeyPairAdapter
+ private lateinit var issuerKeyMaterial: KeyMaterial
+ private lateinit var holderKeyMaterial: KeyMaterial
private lateinit var issuer: Issuer
private lateinit var holder: Holder
private lateinit var issuerServiceEndpoint: String
@@ -28,10 +20,10 @@ class IssueCredentialMessengerTest : FreeSpec() {
init {
beforeEach {
- issuerKeyPair = RandomKeyPairAdapter()
- holderKeyPair = RandomKeyPairAdapter()
- issuer = IssuerAgent(issuerKeyPair, DummyCredentialDataProvider())
- holder = HolderAgent(holderKeyPair)
+ issuerKeyMaterial = EphemeralKeyWithoutCert()
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ issuer = IssuerAgent(issuerKeyMaterial, DummyCredentialDataProvider())
+ holder = HolderAgent(holderKeyMaterial)
issuerServiceEndpoint = "https://example.com/issue?${uuid4()}"
holderMessenger = initHolderMessenger(ConstantIndex.AtomicAttribute2023)
}
@@ -50,14 +42,14 @@ class IssueCredentialMessengerTest : FreeSpec() {
private fun initHolderMessenger(scheme: ConstantIndex.CredentialScheme) =
IssueCredentialMessenger.newHolderInstance(
holder = holder,
- messageWrapper = MessageWrapper(holderKeyPair),
+ messageWrapper = MessageWrapper(holderKeyMaterial),
credentialScheme = scheme,
)
private fun initIssuerMessenger(scheme: ConstantIndex.CredentialScheme) =
IssueCredentialMessenger.newIssuerInstance(
issuer = issuer,
- messageWrapper = MessageWrapper(issuerKeyPair),
+ messageWrapper = MessageWrapper(issuerKeyMaterial),
serviceEndpoint = issuerServiceEndpoint,
credentialScheme = scheme,
)
diff --git a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocolTest.kt b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocolTest.kt
index b75e1c7fe..3019d5c0b 100644
--- a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocolTest.kt
+++ b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/IssueCredentialProtocolTest.kt
@@ -11,18 +11,18 @@ import io.kotest.matchers.types.shouldBeInstanceOf
class IssueCredentialProtocolTest : FreeSpec({
- lateinit var issuerKeyPair: KeyPairAdapter
- lateinit var holderKeyPair: KeyPairAdapter
+ lateinit var issuerKeyMaterial: KeyMaterial
+ lateinit var holderKeyMaterial: KeyMaterial
lateinit var issuer: Issuer
lateinit var holder: Holder
lateinit var issuerProtocol: IssueCredentialProtocol
lateinit var holderProtocol: IssueCredentialProtocol
beforeEach {
- issuerKeyPair = RandomKeyPairAdapter()
- holderKeyPair = RandomKeyPairAdapter()
- issuer = IssuerAgent(issuerKeyPair, DummyCredentialDataProvider())
- holder = HolderAgent(holderKeyPair)
+ issuerKeyMaterial = EphemeralKeyWithoutCert()
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ issuer = IssuerAgent(issuerKeyMaterial, DummyCredentialDataProvider())
+ holder = HolderAgent(holderKeyMaterial)
issuerProtocol = IssueCredentialProtocol.newIssuerInstance(
issuer = issuer,
serviceEndpoint = "https://example.com/issue?${uuid4()}",
@@ -40,17 +40,17 @@ class IssueCredentialProtocolTest : FreeSpec({
val invitationMessage = oobInvitation.message
val parsedInvitation =
- holderProtocol.parseMessage(invitationMessage, issuerKeyPair.jsonWebKey)
+ holderProtocol.parseMessage(invitationMessage, issuerKeyMaterial.jsonWebKey)
parsedInvitation.shouldBeInstanceOf()
val requestCredential = parsedInvitation.message
val parsedRequestCredential =
- issuerProtocol.parseMessage(requestCredential, holderKeyPair.jsonWebKey)
+ issuerProtocol.parseMessage(requestCredential, holderKeyMaterial.jsonWebKey)
parsedRequestCredential.shouldBeInstanceOf()
val issueCredential = parsedRequestCredential.message
val parsedIssueCredential =
- holderProtocol.parseMessage(issueCredential, issuerKeyPair.jsonWebKey)
+ holderProtocol.parseMessage(issueCredential, issuerKeyMaterial.jsonWebKey)
parsedIssueCredential.shouldBeInstanceOf()
val issuedCredential = parsedIssueCredential.lastMessage
@@ -62,12 +62,12 @@ class IssueCredentialProtocolTest : FreeSpec({
requestCredential.shouldBeInstanceOf()
val parsedRequestCredential =
- issuerProtocol.parseMessage(requestCredential.message, holderKeyPair.jsonWebKey)
+ issuerProtocol.parseMessage(requestCredential.message, holderKeyMaterial.jsonWebKey)
parsedRequestCredential.shouldBeInstanceOf()
val issueCredential = parsedRequestCredential.message
val parsedIssueCredential =
- holderProtocol.parseMessage(issueCredential, issuerKeyPair.jsonWebKey)
+ holderProtocol.parseMessage(issueCredential, issuerKeyMaterial.jsonWebKey)
parsedIssueCredential.shouldBeInstanceOf()
val issuedCredential = parsedIssueCredential.lastMessage
@@ -81,7 +81,7 @@ class IssueCredentialProtocolTest : FreeSpec({
threadId = uuid4().toString(),
attachment = JwmAttachment(id = uuid4().toString(), "mimeType", JwmAttachmentData())
),
- issuerKeyPair.jsonWebKey
+ issuerKeyMaterial.jsonWebKey
)
parsed.shouldBeInstanceOf()
}
@@ -92,7 +92,7 @@ class IssueCredentialProtocolTest : FreeSpec({
val invitationMessage = oobInvitation.message
val parsedInvitation =
- holderProtocol.parseMessage(invitationMessage, issuerKeyPair.jsonWebKey)
+ holderProtocol.parseMessage(invitationMessage, issuerKeyMaterial.jsonWebKey)
parsedInvitation.shouldBeInstanceOf()
val requestCredential = parsedInvitation.message
@@ -110,7 +110,7 @@ class IssueCredentialProtocolTest : FreeSpec({
)
)
val parsedRequestCredential =
- issuerProtocol.parseMessage(wrongRequestCredential, holderKeyPair.jsonWebKey)
+ issuerProtocol.parseMessage(wrongRequestCredential, holderKeyMaterial.jsonWebKey)
parsedRequestCredential.shouldBeInstanceOf()
val problemReport = parsedRequestCredential.message
diff --git a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofMessengerTest.kt b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofMessengerTest.kt
index d720c892c..ded2fcbce 100644
--- a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofMessengerTest.kt
+++ b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofMessengerTest.kt
@@ -16,9 +16,9 @@ import kotlin.time.toDuration
class PresentProofMessengerTest : FreeSpec() {
- private lateinit var holderKeyPair: KeyPairAdapter
- private lateinit var verifierKeyPair: KeyPairAdapter
- private lateinit var issuerKeyPair: KeyPairAdapter
+ private lateinit var holderKeyMaterial: KeyMaterial
+ private lateinit var verifierKeyMaterial: KeyMaterial
+ private lateinit var issuerKeyMaterial: KeyMaterial
private lateinit var holderCredentialStore: SubjectCredentialStore
private lateinit var holder: Holder
private lateinit var verifier: Verifier
@@ -30,13 +30,13 @@ class PresentProofMessengerTest : FreeSpec() {
init {
beforeEach {
- holderKeyPair = RandomKeyPairAdapter()
- verifierKeyPair = RandomKeyPairAdapter()
- issuerKeyPair = RandomKeyPairAdapter()
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ verifierKeyMaterial = EphemeralKeyWithoutCert()
+ issuerKeyMaterial = EphemeralKeyWithoutCert()
holderCredentialStore = InMemorySubjectCredentialStore()
- holder = HolderAgent(holderKeyPair, holderCredentialStore)
- verifier = VerifierAgent(verifierKeyPair)
- issuer = IssuerAgent(issuerKeyPair, DummyCredentialDataProvider())
+ holder = HolderAgent(holderKeyMaterial, holderCredentialStore)
+ verifier = VerifierAgent(verifierKeyMaterial)
+ issuer = IssuerAgent(issuerKeyMaterial, DummyCredentialDataProvider())
verifierChallenge = uuid4().toString()
holderServiceEndpoint = "https://example.com/present-proof?${uuid4()}"
}
@@ -44,20 +44,20 @@ class PresentProofMessengerTest : FreeSpec() {
"presentProof" {
holder.storeCredential(
issuer.issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.PLAIN_JWT
).getOrThrow().toStoreCredentialInput()
)
val holderMessenger = PresentProofMessenger.newHolderInstance(
holder = holder,
- messageWrapper = MessageWrapper(holderKeyPair),
+ messageWrapper = MessageWrapper(holderKeyMaterial),
serviceEndpoint = holderServiceEndpoint,
credentialScheme = ConstantIndex.AtomicAttribute2023,
)
val verifierMessenger = PresentProofMessenger.newVerifierInstance(
verifier = verifier,
- messageWrapper = MessageWrapper(verifierKeyPair),
+ messageWrapper = MessageWrapper(verifierKeyMaterial),
credentialScheme = ConstantIndex.AtomicAttribute2023,
)
@@ -84,7 +84,7 @@ class PresentProofMessengerTest : FreeSpec() {
"selectiveDisclosure" {
val issuedCredential = issuer.issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.PLAIN_JWT
).getOrThrow()
@@ -97,13 +97,13 @@ class PresentProofMessengerTest : FreeSpec() {
val holderMessenger = PresentProofMessenger.newHolderInstance(
holder = holder,
- messageWrapper = MessageWrapper(holderKeyPair),
+ messageWrapper = MessageWrapper(holderKeyMaterial),
serviceEndpoint = "https://example.com",
credentialScheme = ConstantIndex.AtomicAttribute2023,
)
val verifierMessenger = PresentProofMessenger.newVerifierInstance(
verifier = verifier,
- messageWrapper = MessageWrapper(verifierKeyPair),
+ messageWrapper = MessageWrapper(verifierKeyMaterial),
challengeForPresentation = verifierChallenge,
credentialScheme = ConstantIndex.AtomicAttribute2023,
requestedClaims = listOf(attributeName)
diff --git a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocolTest.kt b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocolTest.kt
index a90bc9c3d..7aa529d87 100644
--- a/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocolTest.kt
+++ b/vck-aries/src/commonTest/kotlin/at/asitplus/wallet/lib/aries/PresentProofProtocolTest.kt
@@ -10,18 +10,18 @@ import io.kotest.matchers.types.shouldBeInstanceOf
class PresentProofProtocolTest : FreeSpec({
- lateinit var holderKeyPair: KeyPairAdapter
- lateinit var verifierKeyPair: KeyPairAdapter
+ lateinit var holderKeyMaterial: KeyMaterial
+ lateinit var verifierKeyMaterial: KeyMaterial
lateinit var holder: Holder
lateinit var verifier: Verifier
lateinit var holderProtocol: PresentProofProtocol
lateinit var verifierProtocol: PresentProofProtocol
beforeEach {
- holderKeyPair = RandomKeyPairAdapter()
- verifierKeyPair = RandomKeyPairAdapter()
- holder = HolderAgent(holderKeyPair)
- verifier = VerifierAgent(verifierKeyPair)
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ verifierKeyMaterial = EphemeralKeyWithoutCert()
+ holder = HolderAgent(holderKeyMaterial)
+ verifier = VerifierAgent(verifierKeyMaterial)
holderProtocol = PresentProofProtocol.newHolderInstance(
holder = holder,
serviceEndpoint = "https://example.com/",
@@ -36,10 +36,10 @@ class PresentProofProtocolTest : FreeSpec({
"presentProofGenericWithInvitation" {
holder.storeCredential(
IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithoutCert(),
DummyCredentialDataProvider(),
).issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.PLAIN_JWT
).getOrThrow().toStoreCredentialInput()
@@ -50,17 +50,17 @@ class PresentProofProtocolTest : FreeSpec({
val invitationMessage = oobInvitation.message
val parsedInvitation =
- verifierProtocol.parseMessage(invitationMessage, holderKeyPair.jsonWebKey)
+ verifierProtocol.parseMessage(invitationMessage, holderKeyMaterial.jsonWebKey)
parsedInvitation.shouldBeInstanceOf()
val requestPresentation = parsedInvitation.message
val parsedRequestPresentation =
- holderProtocol.parseMessage(requestPresentation, verifierKeyPair.jsonWebKey)
+ holderProtocol.parseMessage(requestPresentation, verifierKeyMaterial.jsonWebKey)
parsedRequestPresentation.shouldBeInstanceOf()
val presentation = parsedRequestPresentation.message
val parsedPresentation =
- verifierProtocol.parseMessage(presentation, holderKeyPair.jsonWebKey)
+ verifierProtocol.parseMessage(presentation, holderKeyMaterial.jsonWebKey)
parsedPresentation.shouldBeInstanceOf()
val receivedPresentation = parsedPresentation.lastMessage
@@ -70,10 +70,10 @@ class PresentProofProtocolTest : FreeSpec({
"presentProofGenericDirect" {
holder.storeCredential(
IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithoutCert(),
DummyCredentialDataProvider(),
).issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.PLAIN_JWT
).getOrThrow().toStoreCredentialInput()
@@ -83,12 +83,12 @@ class PresentProofProtocolTest : FreeSpec({
requestPresentation.shouldBeInstanceOf()
val parsedRequestPresentation =
- holderProtocol.parseMessage(requestPresentation.message, verifierKeyPair.jsonWebKey)
+ holderProtocol.parseMessage(requestPresentation.message, verifierKeyMaterial.jsonWebKey)
parsedRequestPresentation.shouldBeInstanceOf()
val presentation = parsedRequestPresentation.message
val parsedPresentation =
- verifierProtocol.parseMessage(presentation, holderKeyPair.jsonWebKey)
+ verifierProtocol.parseMessage(presentation, holderKeyMaterial.jsonWebKey)
parsedPresentation.shouldBeInstanceOf()
val receivedPresentation = parsedPresentation.lastMessage
@@ -102,7 +102,7 @@ class PresentProofProtocolTest : FreeSpec({
parentThreadId = uuid4().toString(),
attachment = JwmAttachment(id = uuid4().toString(), "mimeType", JwmAttachmentData())
),
- holderKeyPair.jsonWebKey
+ holderKeyMaterial.jsonWebKey
)
parsed.shouldBeInstanceOf()
}
@@ -113,12 +113,12 @@ class PresentProofProtocolTest : FreeSpec({
val invitationMessage = oobInvitation.message
val parsedInvitation =
- verifierProtocol.parseMessage(invitationMessage, holderKeyPair.jsonWebKey)
+ verifierProtocol.parseMessage(invitationMessage, holderKeyMaterial.jsonWebKey)
parsedInvitation.shouldBeInstanceOf()
val requestPresentation = parsedInvitation.message
val parsedRequestPresentation =
- holderProtocol.parseMessage(requestPresentation, verifierKeyPair.jsonWebKey)
+ holderProtocol.parseMessage(requestPresentation, verifierKeyMaterial.jsonWebKey)
parsedRequestPresentation.shouldBeInstanceOf()
val problemReport = parsedRequestPresentation.message
diff --git a/vck-openid/build.gradle.kts b/vck-openid/build.gradle.kts
index 3280084cc..bb24f4f4e 100644
--- a/vck-openid/build.gradle.kts
+++ b/vck-openid/build.gradle.kts
@@ -1,7 +1,6 @@
-import at.asitplus.gradle.commonImplementationDependencies
-import at.asitplus.gradle.commonIosExports
-import at.asitplus.gradle.exportIosFramework
-import at.asitplus.gradle.setupDokka
+import at.asitplus.gradle.*
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree.Companion.test
plugins {
kotlin("multiplatform")
@@ -17,9 +16,18 @@ group = "at.asitplus.wallet"
version = artifactVersion
+setupAndroid()
+
kotlin {
jvm()
+
+ androidTarget {
+ publishLibraryVariants("release")
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ instrumentedTestVariant.sourceSetTree.set(test)
+ }
+
iosArm64()
iosSimulatorArm64()
iosX64()
@@ -28,6 +36,7 @@ kotlin {
commonMain {
dependencies {
api(project(":vck"))
+ api(project(":openid-data-classes"))
commonImplementationDependencies()
}
}
@@ -56,8 +65,8 @@ kotlin {
exportIosFramework(
"VckOpenIdKmm",
- static = false,
- *commonIosExports(), project(":vck")
+ transitiveExports = false,
+ project(":vck")
)
val javadocJar = setupDokka(
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/AuthorizationServiceStrategy.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/AuthorizationServiceStrategy.kt
new file mode 100644
index 000000000..46ac49a40
--- /dev/null
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/AuthorizationServiceStrategy.kt
@@ -0,0 +1,17 @@
+package at.asitplus.wallet.lib.oauth2
+
+import at.asitplus.openid.AuthenticationRequestParameters
+import at.asitplus.openid.AuthorizationDetails
+import at.asitplus.openid.OidcUserInfoExtended
+
+/**
+ * Strategy to implement authentication and authorization in [SimpleAuthorizationService].
+ */
+interface AuthorizationServiceStrategy {
+
+ suspend fun loadUserInfo(request: AuthenticationRequestParameters, code: String): OidcUserInfoExtended?
+
+ fun filterAuthorizationDetails(authorizationDetails: Set): Set
+
+
+}
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/OAuth2Client.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/OAuth2Client.kt
new file mode 100644
index 000000000..454300458
--- /dev/null
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/OAuth2Client.kt
@@ -0,0 +1,174 @@
+package at.asitplus.wallet.lib.oauth2
+
+import at.asitplus.openid.*
+import at.asitplus.openid.OpenIdConstants.CODE_CHALLENGE_METHOD_SHA256
+import at.asitplus.openid.OpenIdConstants.GRANT_TYPE_CODE
+import at.asitplus.signum.indispensable.io.Base64UrlStrict
+import at.asitplus.wallet.lib.iso.sha256
+import at.asitplus.wallet.lib.jws.JwsService
+import at.asitplus.wallet.lib.oidvci.*
+import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
+import kotlin.random.Random
+
+/**
+ * Simple OAuth 2.0 client to authorize the client against an OAuth 2.0 Authorization Server and request tokens.
+ *
+ * Can be used in OID4VCI flows, e.g. [WalletService].
+ */
+class OAuth2Client(
+ /**
+ * Used to create [AuthenticationRequestParameters], [TokenRequestParameters] and [CredentialRequestProof],
+ * typically a URI.
+ */
+ private val clientId: String = "https://wallet.a-sit.at/app",
+ /**
+ * Used to create [AuthenticationRequestParameters] and [TokenRequestParameters].
+ */
+ private val redirectUrl: String = "$clientId/callback",
+ /**
+ * Used to store the code, associated to the state, to first send [AuthenticationRequestParameters.codeChallenge],
+ * and then [TokenRequestParameters.codeVerifier], see [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636).
+ */
+ private val stateToCodeStore: MapStore = DefaultMapStore(),
+) {
+
+ /**
+ * Send the result as parameters (either POST or GET) to the server at `/authorize` (or more specific
+ * [OAuth2AuthorizationServerMetadata.authorizationEndpoint]).
+ *
+ * Sample ktor code:
+ * ```
+ * val authnRequest = client.createAuthRequest(...)
+ * val authnResponse = httpClient.get(issuerMetadata.authorizationEndpointUrl!!) {
+ * url {
+ * authnRequest.encodeToParameters().forEach { parameters.append(it.key, it.value) }
+ * }
+ * }
+ * val authn = AuthenticationResponseParameters.deserialize(authnResponse.bodyAsText()).getOrThrow()
+ * ```
+ *
+ * @param state to keep internal state in further requests
+ * @param scope in OID4VCI flows the value `scope` from [IssuerMetadata.supportedCredentialConfigurations]
+ * @param authorizationDetails from RFC 9396 OAuth 2.0 Rich Authorization Requests
+ * @param resource from RFC 8707 Resource Indicators for OAuth 2.0, in OID4VCI flows the value
+ * of [IssuerMetadata.credentialIssuer]
+ */
+ suspend fun createAuthRequest(
+ state: String,
+ authorizationDetails: Set? = null,
+ scope: String? = null,
+ resource: String? = null,
+ ) = AuthenticationRequestParameters(
+ responseType = GRANT_TYPE_CODE,
+ state = state,
+ clientId = clientId,
+ authorizationDetails = authorizationDetails,
+ scope = scope,
+ resource = resource,
+ redirectUrl = redirectUrl,
+ codeChallenge = generateCodeVerifier(state),
+ codeChallengeMethod = CODE_CHALLENGE_METHOD_SHA256,
+ )
+
+ @OptIn(ExperimentalStdlibApi::class)
+ private suspend fun generateCodeVerifier(state: String): String {
+ val codeVerifier = Random.nextBytes(32).toHexString(HexFormat.Default)
+ stateToCodeStore.put(state, codeVerifier)
+ return codeVerifier.encodeToByteArray().sha256().encodeToString(Base64UrlStrict)
+ }
+
+ sealed class AuthorizationForToken {
+ /**
+ * Authorization code from an actual OAuth2 Authorization Server, or [SimpleAuthorizationService.authorize]
+ */
+ data class Code(val code: String) : AuthorizationForToken()
+
+ /**
+ * Pre-auth code from [CredentialOfferGrantsPreAuthCode.preAuthorizedCode] in
+ * [CredentialOfferGrants.preAuthorizedCode] in [CredentialOffer.grants],
+ * optionally with a [transactionCode] which is transmitted out-of-band, and may be entered by the user.
+ */
+ data class PreAuthCode(
+ val preAuthorizedCode: String,
+ val transactionCode: String? = null
+ ) : AuthorizationForToken()
+ }
+
+ /**
+ * Request token with an authorization code, e.g. from [createAuthRequest], or pre-auth code.
+ *
+ * Send the result as POST parameters (form-encoded) to the server at `/token` (or more specific
+ * [OAuth2AuthorizationServerMetadata.tokenEndpoint]).
+ *
+ * Sample ktor code for authorization code:
+ * ```
+ * val authnRequest = client.createAuthRequest(requestOptions)
+ * val authnResponse = authorizationService.authorize(authnRequest).getOrThrow()
+ * val code = authnResponse.params.code
+ * val tokenRequest = client.createTokenRequestParameters(state, AuthorizationForToken.Code(code))
+ * val tokenResponse = httpClient.submitForm(
+ * url = issuerMetadata.tokenEndpointUrl!!,
+ * formParameters = parameters {
+ * tokenRequest.encodeToParameters().forEach { append(it.key, it.value) }
+ * }
+ * )
+ * val token = TokenResponseParameters.deserialize(tokenResponse.bodyAsText()).getOrThrow()
+ * ```
+ *
+ * Sample ktor code for pre-authn code:
+ * ```
+ * val preAuth = credentialOffer.grants.preAuthorizedCode
+ * val transactionCode = "..." // get from user input
+ * val authorization = WalletService.AuthorizationForToken.PreAuthCode(preAuth, transactionCode)
+ * val tokenRequest = client.createTokenRequestParameters(state, authorization)
+ * val tokenResponse = httpClient.submitForm(
+ * url = issuerMetadata.tokenEndpointUrl!!,
+ * formParameters = parameters {
+ * tokenRequest.encodeToParameters().forEach { append(it.key, it.value) }
+ * }
+ * )
+ * val token = TokenResponseParameters.deserialize(tokenResponse.bodyAsText()).getOrThrow()
+ * ```
+ *
+ * Be sure to include a DPoP header if [OAuth2AuthorizationServerMetadata.dpopSigningAlgValuesSupported] is set,
+ * see [JwsService.buildDPoPHeader].
+ *
+ * @param state to keep internal state in further requests
+ * @param authorization for the token endpoint
+ * @param authorizationDetails from RFC 9396 OAuth 2.0 Rich Authorization Requests
+ * @param scope in OID4VCI flows the value `scope` from [IssuerMetadata.supportedCredentialConfigurations]
+ * @param resource from RFC 8707 Resource Indicators for OAuth 2.0, in OID4VCI flows the value
+ * of [IssuerMetadata.credentialIssuer]
+ */
+ suspend fun createTokenRequestParameters(
+ state: String,
+ authorization: AuthorizationForToken,
+ authorizationDetails: Set? = null,
+ scope: String? = null,
+ resource: String? = null,
+ ) = when (authorization) {
+ is AuthorizationForToken.Code -> TokenRequestParameters(
+ grantType = OpenIdConstants.GRANT_TYPE_AUTHORIZATION_CODE,
+ redirectUrl = redirectUrl,
+ clientId = clientId,
+ codeVerifier = stateToCodeStore.remove(state),
+ authorizationDetails = authorizationDetails,
+ scope = scope,
+ resource = resource,
+ code = authorization.code,
+ )
+
+ is AuthorizationForToken.PreAuthCode -> TokenRequestParameters(
+ grantType = OpenIdConstants.GRANT_TYPE_PRE_AUTHORIZED_CODE,
+ redirectUrl = redirectUrl,
+ clientId = clientId,
+ codeVerifier = stateToCodeStore.remove(state),
+ authorizationDetails = authorizationDetails,
+ scope = scope,
+ resource = resource,
+ transactionCode = authorization.transactionCode,
+ preAuthorizedCode = authorization.preAuthorizedCode,
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SimpleAuthorizationService.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/SimpleAuthorizationService.kt
similarity index 59%
rename from vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SimpleAuthorizationService.kt
rename to vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/SimpleAuthorizationService.kt
index dc519de76..2d0ddb9d9 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/SimpleAuthorizationService.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oauth2/SimpleAuthorizationService.kt
@@ -1,39 +1,32 @@
-package at.asitplus.wallet.lib.oidvci
+package at.asitplus.wallet.lib.oauth2
import at.asitplus.KmmResult
import at.asitplus.catching
+import at.asitplus.openid.*
+import at.asitplus.openid.OpenIdConstants.Errors
import at.asitplus.signum.indispensable.io.Base64UrlStrict
-import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.iso.sha256
-import at.asitplus.wallet.lib.oidc.AuthenticationRequestParameters
-import at.asitplus.wallet.lib.oidc.AuthenticationResponseParameters
import at.asitplus.wallet.lib.oidc.AuthenticationResponseResult
-import at.asitplus.wallet.lib.oidc.OpenIdConstants
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.Errors
+import at.asitplus.wallet.lib.oidvci.*
import io.github.aakira.napier.Napier
import io.ktor.http.*
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
import kotlin.time.Duration.Companion.seconds
/**
* Simple authorization server implementation, to be used for [CredentialIssuer],
- * when issuing credentials directly from a local [dataProvider].
+ * with the actual authentication and authorization logic implemented in [strategy].
*
- * Implemented from [OpenID for Verifiable Credential Issuance]
- * (https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html), Draft 13, 2024-02-08.
+ * Implemented from
+ * [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
+ * , Draft 14, 2024-08-21.
*/
class SimpleAuthorizationService(
/**
- * Source of user data.
+ * Used to load user data and filter authorization details
*/
- private val dataProvider: OAuth2DataProvider,
- /**
- * List of supported schemes.
- */
- private val credentialSchemes: Set,
+ private val strategy: AuthorizationServiceStrategy,
/**
* Used to create and verify authorization codes during issuing.
*/
@@ -41,7 +34,7 @@ class SimpleAuthorizationService(
/**
* Used to create and verify bearer tokens during issuing.
*/
- private val tokenService: TokenService = DefaultTokenService(),
+ private val tokenService: NonceService = DefaultNonceService(),
/**
* Used to provide challenge to clients to include in proof of possession of key material.
*/
@@ -54,25 +47,22 @@ class SimpleAuthorizationService(
* Used to build [OAuth2AuthorizationServerMetadata.authorizationEndpoint], i.e. implementers need to forward requests
* to that URI (which starts with [publicContext]) to [authorize].
*/
- val authorizationEndpointPath: String = "/authorize",
+ private val authorizationEndpointPath: String = "/authorize",
/**
* Used to build [OAuth2AuthorizationServerMetadata.tokenEndpoint], i.e. implementers need to forward requests
* to that URI (which starts with [publicContext]) to [token].
*/
- val tokenEndpointPath: String = "/token",
-) : OAuth2AuthorizationServer {
-
- private val codeToCodeChallengeMap = mutableMapOf()
- private val codeToCodeChallengeMutex = Mutex()
+ private val tokenEndpointPath: String = "/token",
+ private val codeToCodeChallengeStore: MapStore = DefaultMapStore(),
+ private val codeToUserInfoStore: MapStore = DefaultMapStore(),
+ private val accessTokenToUserInfoStore: MapStore = DefaultMapStore(),
+) : OAuth2AuthorizationServerAdapter {
- private val codeToUserInfoMap = mutableMapOf()
- private val codeToUserInfoMutex = Mutex()
-
- private val accessTokenToUserInfoMap = mutableMapOf()
- private val accessTokenToUserInfoMutex = Mutex()
+ override val supportsClientNonce: Boolean = true
/**
- * Serve this result JSON-serialized under `/.well-known/openid-configuration`
+ * Serve this result JSON-serialized under `/.well-known/openid-configuration`,
+ * see [OpenIdConstants.PATH_WELL_KNOWN_OPENID_CONFIGURATION]
*/
val metadata: OAuth2AuthorizationServerMetadata by lazy {
OAuth2AuthorizationServerMetadata(
@@ -94,21 +84,21 @@ class SimpleAuthorizationService(
throw OAuth2Exception(Errors.INVALID_REQUEST, "redirect_uri not set")
.also { Napier.w("authorize: client did not set redirect_uri in $request") }
- val code = codeService.provideCode().also {
- val userInfo = dataProvider.loadUserInfo(request)
+ val code = codeService.provideCode().also { code ->
+ val userInfo = strategy.loadUserInfo(request, code)
?: throw OAuth2Exception(Errors.INVALID_REQUEST)
.also { Napier.w("authorize: could not load user info from $request") }
- codeToUserInfoMutex.withLock { codeToUserInfoMap[it] = userInfo }
+ codeToUserInfoStore.put(code, userInfo)
}
val responseParams = AuthenticationResponseParameters(
code = code,
state = request.state,
)
- if (request.codeChallenge != null) {
- codeToCodeChallengeMutex.withLock { codeToCodeChallengeMap[code] = request.codeChallenge }
+ request.codeChallenge?.let {
+ codeToCodeChallengeStore.put(code, it)
}
// TODO Also implement POST?
- val url = URLBuilder(request.redirectUrl)
+ val url = URLBuilder(request.redirectUrl!!)
.apply { responseParams.encodeToParameters().forEach { this.parameters.append(it.key, it.value) } }
.buildString()
@@ -125,17 +115,17 @@ class SimpleAuthorizationService(
suspend fun token(params: TokenRequestParameters) = catching {
val userInfo: OidcUserInfoExtended = when (params.grantType) {
OpenIdConstants.GRANT_TYPE_AUTHORIZATION_CODE -> {
- if (params.code == null || !codeService.verifyCode(params.code))
+ if (params.code == null || !codeService.verifyAndRemove(params.code!!))
throw OAuth2Exception(Errors.INVALID_CODE)
.also { Napier.w("token: client did not provide correct code") }
- codeToUserInfoMutex.withLock { codeToUserInfoMap[params.code] }
+ params.code?.let { codeToUserInfoStore.remove(it) }
}
OpenIdConstants.GRANT_TYPE_PRE_AUTHORIZED_CODE -> {
- if (params.preAuthorizedCode == null || !codeService.verifyCode(params.preAuthorizedCode))
+ if (params.preAuthorizedCode == null || !codeService.verifyAndRemove(params.preAuthorizedCode!!))
throw OAuth2Exception(Errors.INVALID_GRANT)
.also { Napier.w("token: client did not provide pre authorized code") }
- codeToUserInfoMutex.withLock { codeToUserInfoMap[params.preAuthorizedCode] }
+ params.preAuthorizedCode?.let { codeToUserInfoStore.remove(it) }
}
else -> {
@@ -145,26 +135,25 @@ class SimpleAuthorizationService(
} ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
.also { Napier.w("token: could not load user info for $params}") }
- // TODO work out mapping of credential identifiers in authorization details to schemes
- val filteredAuthorizationDetails = params.authorizationDetails?.filter {
- credentialSchemes.map { it.vcType }.contains(it.credentialConfigurationId) ||
- credentialSchemes.map { it.sdJwtType }.contains(it.credentialConfigurationId) ||
- credentialSchemes.map { it.isoDocType }.contains(it.credentialConfigurationId)
- }?.toSet()
-
params.codeVerifier?.let { codeVerifier ->
- codeToCodeChallengeMutex.withLock { codeToCodeChallengeMap.remove(params.code) }?.let { codeChallenge ->
- val codeChallengeCalculated = codeVerifier.encodeToByteArray().sha256().encodeToString(Base64UrlStrict)
- if (codeChallenge != codeChallengeCalculated) {
- throw OAuth2Exception(Errors.INVALID_GRANT)
- .also { Napier.w("token: client did not provide correct code verifier: $codeVerifier") }
+ params.code?.let { code ->
+ codeToCodeChallengeStore.remove(code)?.let { codeChallenge ->
+ val codeChallengeCalculated =
+ codeVerifier.encodeToByteArray().sha256().encodeToString(Base64UrlStrict)
+ if (codeChallenge != codeChallengeCalculated) {
+ throw OAuth2Exception(Errors.INVALID_GRANT)
+ .also { Napier.w("token: client did not provide correct code verifier: $codeVerifier") }
+ }
}
}
}
+ val filteredAuthorizationDetails = params.authorizationDetails
+ ?.let { strategy.filterAuthorizationDetails(it) }
+
TokenResponseParameters(
- accessToken = tokenService.provideToken().also {
- accessTokenToUserInfoMutex.withLock { accessTokenToUserInfoMap[it] = userInfo }
+ accessToken = tokenService.provideNonce().also {
+ accessTokenToUserInfoStore.put(it, userInfo)
},
tokenType = OpenIdConstants.TOKEN_TYPE_BEARER,
expires = 3600.seconds,
@@ -173,24 +162,20 @@ class SimpleAuthorizationService(
).also { Napier.i("token returns $it") }
}
- override suspend fun providePreAuthorizedCode(): String? {
- return codeService.provideCode().also {
- val userInfo = dataProvider.loadUserInfo()
- ?: return null.also { Napier.w("authorize: could not load user info from data provider") }
- codeToUserInfoMutex.withLock { codeToUserInfoMap[it] = userInfo }
+ override suspend fun providePreAuthorizedCode(user: OidcUserInfoExtended): String =
+ codeService.provideCode().also {
+ codeToUserInfoStore.put(it, user)
}
- }
- override suspend fun verifyAndRemoveClientNonce(nonce: String): Boolean {
- return clientNonceService.verifyAndRemoveNonce(nonce)
- }
+ override suspend fun verifyClientNonce(nonce: String): Boolean =
+ clientNonceService.verifyNonce(nonce)
override suspend fun getUserInfo(accessToken: String): KmmResult = catching {
- if (!tokenService.verifyToken(accessToken)) {
+ if (!tokenService.verifyNonce(accessToken)) {
throw OAuth2Exception(Errors.INVALID_TOKEN)
.also { Napier.w("getUserInfo: client did not provide correct token: $accessToken") }
}
- val result = accessTokenToUserInfoMutex.withLock { accessTokenToUserInfoMap[accessToken] }
+ val result = accessTokenToUserInfoStore.get(accessToken)
?: throw OAuth2Exception(Errors.INVALID_TOKEN)
.also { Napier.w("getUserInfo: could not load user info for $accessToken") }
@@ -199,4 +184,4 @@ class SimpleAuthorizationService(
}
override suspend fun provideMetadata() = KmmResult.success(metadata)
-}
\ No newline at end of file
+}
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequest.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequest.kt
index 0e3001b66..44b40ee30 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequest.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequest.kt
@@ -4,16 +4,14 @@ package at.asitplus.wallet.lib.oidc
import at.asitplus.catching
+import at.asitplus.dif.rqes.UrlSerializer
+import at.asitplus.openid.AuthenticationRequestParameters
import at.asitplus.signum.indispensable.josef.JwsSigned
import io.ktor.http.*
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.UseSerializers
+import kotlinx.serialization.*
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@@ -57,23 +55,10 @@ internal object JwsSignedSerializer : KSerializer {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("JwsSignedSerializer", PrimitiveKind.STRING)
- override fun deserialize(decoder: Decoder): JwsSigned = JwsSigned.parse(decoder.decodeString()).getOrThrow()
+ override fun deserialize(decoder: Decoder): JwsSigned = JwsSigned.deserialize(decoder.decodeString()).getOrThrow()
override fun serialize(encoder: Encoder, value: JwsSigned) {
encoder.encodeString(value.serialize())
}
-}
-
-internal object UrlSerializer : KSerializer {
-
- override val descriptor: SerialDescriptor =
- PrimitiveSerialDescriptor("UrlSerializer", PrimitiveKind.STRING)
-
- override fun deserialize(decoder: Decoder): Url = Url(decoder.decodeString())
-
- override fun serialize(encoder: Encoder, value: Url) {
- encoder.encodeString(value.toString())
- }
-
-}
+}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponse.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponse.kt
index ecacaf6d8..67b6726b9 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponse.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponse.kt
@@ -1,5 +1,7 @@
package at.asitplus.wallet.lib.oidc
+import at.asitplus.openid.AuthenticationResponseParameters
+import at.asitplus.openid.RelyingPartyMetadata
import at.asitplus.signum.indispensable.josef.JsonWebKey
/**
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseResult.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseResult.kt
index 64d6ab4f8..272dacff5 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseResult.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseResult.kt
@@ -1,5 +1,7 @@
package at.asitplus.wallet.lib.oidc
+import at.asitplus.openid.AuthenticationResponseParameters
+
/**
* Possible outcomes of creating the OIDC Authentication Response
*/
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt
index ccb018ff0..450610964 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt
@@ -2,62 +2,39 @@ package at.asitplus.wallet.lib.oidc
import at.asitplus.KmmResult
import at.asitplus.catching
-import at.asitplus.signum.indispensable.CryptoPublicKey
-import at.asitplus.signum.indispensable.josef.JsonWebKeySet
-import at.asitplus.signum.indispensable.josef.JweEncrypted
-import at.asitplus.signum.indispensable.josef.JwsHeader
-import at.asitplus.signum.indispensable.josef.JwsSigned
-import at.asitplus.signum.indispensable.josef.toJsonWebKey
-import at.asitplus.signum.indispensable.pki.CertificateChain
-import at.asitplus.signum.indispensable.pki.leaf
+import at.asitplus.dif.*
import at.asitplus.jsonpath.JsonPath
import at.asitplus.jsonpath.core.NormalizedJsonPath
import at.asitplus.jsonpath.core.NormalizedJsonPathSegment
-import at.asitplus.wallet.lib.agent.DefaultCryptoService
-import at.asitplus.wallet.lib.agent.DefaultVerifierCryptoService
-import at.asitplus.wallet.lib.agent.Verifier
-import at.asitplus.wallet.lib.data.ConstantIndex
+import at.asitplus.openid.*
+import at.asitplus.openid.OpenIdConstants.BINDING_METHOD_JWK
+import at.asitplus.openid.OpenIdConstants.ClientIdScheme.*
+import at.asitplus.openid.OpenIdConstants.ID_TOKEN
+import at.asitplus.openid.OpenIdConstants.PREFIX_DID_KEY
+import at.asitplus.openid.OpenIdConstants.SCOPE_OPENID
+import at.asitplus.openid.OpenIdConstants.SCOPE_PROFILE
+import at.asitplus.openid.OpenIdConstants.URN_TYPE_JWK_THUMBPRINT
+import at.asitplus.openid.OpenIdConstants.VP_TOKEN
+import at.asitplus.signum.indispensable.josef.*
+import at.asitplus.signum.indispensable.pki.CertificateChain
+import at.asitplus.signum.indispensable.pki.leaf
+import at.asitplus.wallet.lib.agent.*
+import at.asitplus.wallet.lib.data.*
import at.asitplus.wallet.lib.data.ConstantIndex.supportsSdJwt
import at.asitplus.wallet.lib.data.ConstantIndex.supportsVcJwt
-import at.asitplus.wallet.lib.data.IsoDocumentParsed
-import at.asitplus.wallet.lib.data.SelectiveDisclosureItem
-import at.asitplus.wallet.lib.data.VerifiableCredentialSdJwt
-import at.asitplus.wallet.lib.data.VerifiablePresentationParsed
-import at.asitplus.wallet.lib.data.dif.ClaimFormatEnum
-import at.asitplus.wallet.lib.data.dif.Constraint
-import at.asitplus.wallet.lib.data.dif.ConstraintField
-import at.asitplus.wallet.lib.data.dif.ConstraintFilter
-import at.asitplus.wallet.lib.data.dif.FormatContainerJwt
-import at.asitplus.wallet.lib.data.dif.FormatHolder
-import at.asitplus.wallet.lib.data.dif.InputDescriptor
-import at.asitplus.wallet.lib.data.dif.PresentationDefinition
-import at.asitplus.wallet.lib.data.dif.PresentationSubmissionDescriptor
-import at.asitplus.wallet.lib.data.dif.SchemaReference
import at.asitplus.wallet.lib.jws.DefaultJwsService
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.ClientIdScheme.REDIRECT_URI
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ClientIdScheme.VERIFIER_ATTESTATION
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ClientIdScheme.X509_SAN_DNS
-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
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.SCOPE_PROFILE
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.URN_TYPE_JWK_THUMBPRINT
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.VP_TOKEN
-import at.asitplus.wallet.lib.oidvci.decodeFromPostBody
-import at.asitplus.wallet.lib.oidvci.decodeFromUrlQuery
-import at.asitplus.wallet.lib.oidvci.encodeToParameters
+import at.asitplus.wallet.lib.oidvci.*
import com.benasher44.uuid.uuid4
import io.github.aakira.napier.Napier
import io.ktor.http.*
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
+import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@@ -72,105 +49,78 @@ import kotlin.time.toDuration
class OidcSiopVerifier private constructor(
private val verifier: Verifier,
private val relyingPartyUrl: String?,
- private val responseUrl: String?,
- private val agentPublicKey: CryptoPublicKey,
private val jwsService: JwsService,
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?,
- private val x5c: CertificateChain?,
- private val clientIdScheme: OpenIdConstants.ClientIdScheme
+ private val nonceService: NonceService = DefaultNonceService(),
+ private val clientIdScheme: ClientIdScheme = ClientIdScheme.RedirectUri,
+ private val stateToNonceStore: MapStore = DefaultMapStore(),
+ private val stateToResponseTypeStore: MapStore = DefaultMapStore(),
) {
private val timeLeeway = timeLeewaySeconds.toDuration(DurationUnit.SECONDS)
- private val challengeMutex = Mutex()
- private val challengeSet = mutableSetOf()
-
- companion object {
- fun newInstance(
- verifier: Verifier,
- relyingPartyUrl: String,
- responseUrl: String? = null,
- verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(DefaultVerifierCryptoService()),
- jwsService: JwsService = DefaultJwsService(DefaultCryptoService(verifier.keyPair)),
- timeLeewaySeconds: Long = 300L,
- clock: Clock = Clock.System,
- attestationJwt: JwsSigned,
- ) = OidcSiopVerifier(
- verifier = verifier,
- relyingPartyUrl = relyingPartyUrl,
- responseUrl = responseUrl,
- agentPublicKey = verifier.keyPair.publicKey,
- jwsService = jwsService,
- verifierJwsService = verifierJwsService,
- timeLeewaySeconds = timeLeewaySeconds,
- clock = clock,
- attestationJwt = attestationJwt,
- x5c = null,
- clientIdScheme = VERIFIER_ATTESTATION
- )
- fun newInstance(
- verifier: Verifier,
- relyingPartyUrl: String?,
- responseUrl: String? = null,
- verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(DefaultVerifierCryptoService()),
- jwsService: JwsService = DefaultJwsService(DefaultCryptoService(verifier.keyPair)),
- timeLeewaySeconds: Long = 300L,
- clock: Clock = Clock.System,
- x5c: CertificateChain,
- ) = OidcSiopVerifier(
- verifier = verifier,
- relyingPartyUrl = relyingPartyUrl,
- responseUrl = responseUrl,
- agentPublicKey = verifier.keyPair.publicKey,
- jwsService = jwsService,
- verifierJwsService = verifierJwsService,
- timeLeewaySeconds = timeLeewaySeconds,
- clock = clock,
- attestationJwt = null,
- clientIdScheme = X509_SAN_DNS,
- x5c = x5c
- )
+ sealed class ClientIdScheme(val clientIdScheme: OpenIdConstants.ClientIdScheme) {
+ /**
+ * Verifier Attestation JWT to include (in header `jwt`) when creating request objects as JWS,
+ * to allow the Wallet to verify the authenticity of this Verifier.
+ * OID4VP client id scheme `verifier attestation`,
+ * see [at.asitplus.openid.OpenIdConstants.ClientIdScheme.VERIFIER_ATTESTATION].
+ */
+ data class VerifierAttestation(val attestationJwt: JwsSigned) : ClientIdScheme(VERIFIER_ATTESTATION)
+ /**
+ * Certificate chain to include in JWS headers and to extract `client_id` from (in SAN extension), from OID4VP
+ * client id scheme `x509_san_dns`,
+ * see [at.asitplus.openid.OpenIdConstants.ClientIdScheme.X509_SAN_DNS].
+ */
+ data class CertificateSanDns(val chain: CertificateChain) : ClientIdScheme(X509_SAN_DNS)
- fun newInstance(
- verifier: Verifier,
- relyingPartyUrl: String,
- responseUrl: String? = null,
- verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(DefaultVerifierCryptoService()),
- jwsService: JwsService = DefaultJwsService(DefaultCryptoService(verifier.keyPair)),
- timeLeewaySeconds: Long = 300L,
- clock: Clock = Clock.System,
- clientIdScheme: OpenIdConstants.ClientIdScheme = REDIRECT_URI,
- ) = OidcSiopVerifier(
- verifier = verifier,
- relyingPartyUrl = relyingPartyUrl,
- responseUrl = responseUrl,
- agentPublicKey = verifier.keyPair.publicKey,
- jwsService = jwsService,
- verifierJwsService = verifierJwsService,
- timeLeewaySeconds = timeLeewaySeconds,
- clock = clock,
- attestationJwt = null,
- clientIdScheme = clientIdScheme,
- x5c = null
- )
+ /**
+ * Simple: `redirect_uri` has to match `client_id`
+ */
+ data object RedirectUri : ClientIdScheme(REDIRECT_URI)
}
+ constructor(
+ keyMaterial: KeyMaterial = EphemeralKeyWithoutCert(),
+ verifier: Verifier = VerifierAgent(keyMaterial),
+ relyingPartyUrl: String? = null,
+ verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(DefaultVerifierCryptoService()),
+ jwsService: JwsService = DefaultJwsService(DefaultCryptoService(keyMaterial)),
+ timeLeewaySeconds: Long = 300L,
+ clock: Clock = Clock.System,
+ nonceService: NonceService = DefaultNonceService(),
+ clientIdScheme: ClientIdScheme = ClientIdScheme.RedirectUri,
+ /**
+ * Used to store the nonce, associated to the state, to first send [AuthenticationRequestParameters.nonce],
+ * and then verify the challenge in the submitted verifiable presentation in
+ * [AuthenticationResponseParameters.vpToken].
+ */
+ stateToNonceStore: MapStore = DefaultMapStore(),
+ stateToResponseTypeStore: MapStore = DefaultMapStore(),
+ ) : this(
+ verifier = verifier,
+ relyingPartyUrl = relyingPartyUrl,
+ jwsService = jwsService,
+ verifierJwsService = verifierJwsService,
+ timeLeewaySeconds = timeLeewaySeconds,
+ clock = clock,
+ nonceService = nonceService,
+ clientIdScheme = clientIdScheme,
+ stateToNonceStore = stateToNonceStore,
+ stateToResponseTypeStore = stateToResponseTypeStore,
+ )
+
private val containerJwt =
FormatContainerJwt(algorithms = verifierJwsService.supportedAlgorithms.map { it.identifier })
val metadata by lazy {
RelyingPartyMetadata(
redirectUris = relyingPartyUrl?.let { listOf(it) },
- jsonWebKeySet = JsonWebKeySet(listOf(agentPublicKey.toJsonWebKey())),
- subjectSyntaxTypesSupported = setOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY),
+ jsonWebKeySet = JsonWebKeySet(listOf(verifier.keyMaterial.publicKey.toJsonWebKey())),
+ subjectSyntaxTypesSupported = setOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY, BINDING_METHOD_JWK),
vpFormats = FormatHolder(
msoMdoc = containerJwt,
jwtVp = containerJwt,
@@ -201,10 +151,8 @@ class OidcSiopVerifier private constructor(
requestUrl: String,
): String {
val urlBuilder = URLBuilder(walletUrl)
- val clientId = (x5c?.let { it.leaf.tbsCertificate.subjectAlternativeNames?.dnsNames?.firstOrNull() }
- ?: relyingPartyUrl)
AuthenticationRequestParameters(
- clientId = clientId,
+ clientId = this.clientId,
clientMetadataUri = clientMetadataUrl,
requestUri = requestUrl,
).encodeToParameters()
@@ -224,43 +172,64 @@ class OidcSiopVerifier private constructor(
data class RequestOptions(
/**
- * Response mode to request, see [OpenIdConstants.ResponseMode]
+ * Requested credentials, should be at least one
*/
- val responseMode: OpenIdConstants.ResponseMode? = null,
+ val credentials: Set,
/**
- * Required representation, see [ConstantIndex.CredentialRepresentation]
+ * Response mode to request, see [OpenIdConstants.ResponseMode],
+ * by default [OpenIdConstants.ResponseMode.FRAGMENT].
+ * Setting this to any other value may require setting [responseUrl] too.
*/
- val representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT,
+ val responseMode: OpenIdConstants.ResponseMode = OpenIdConstants.ResponseMode.FRAGMENT,
/**
- * Opaque value which will be returned by the OpenId Provider and also in [AuthnResponseResult]
+ * Response URL to set in the [AuthenticationRequestParameters.responseUrl],
+ * required if [responseMode] is set to [OpenIdConstants.ResponseMode.DIRECT_POST] or
+ * [OpenIdConstants.ResponseMode.DIRECT_POST_JWT].
*/
- val state: String? = uuid4().toString(),
+ val responseUrl: String? = null,
/**
- * Credential type to request, or `null` to make no restrictions
+ * Response type to set in [AuthenticationRequestParameters.responseType],
+ * by default only `vp_token` (as per OpenID4VP spec).
+ * Be sure to separate values by a space, e.g. `vp_token id_token`.
*/
- val credentialScheme: ConstantIndex.CredentialScheme? = null,
+ val responseType: String = VP_TOKEN,
/**
- * List of attributes that shall be requested explicitly (selective disclosure),
- * or `null` to make no restrictions
+ * Opaque value which will be returned by the OpenId Provider and also in [AuthnResponseResult]
*/
- val requestedAttributes: List? = null,
+ val state: String = uuid4().toString(),
/**
* Optional URL to include [metadata] by reference instead of by value (directly embedding in authn request)
*/
val clientMetadataUrl: String? = null,
/**
* Set this value to include metadata with encryption parameters set. Beware if setting this value and also
- * [clientMetadataUrl], that the URL shall point to [getCreateMetadataWithEncryption].
+ * [clientMetadataUrl], that the URL shall point to [OidcSiopVerifier.metadataWithEncryption].
*/
val encryption: Boolean = false,
)
+ data class RequestOptionsCredential(
+ /**
+ * Credential type to request, or `null` to make no restrictions
+ */
+ val credentialScheme: ConstantIndex.CredentialScheme,
+ /**
+ * Required representation, see [ConstantIndex.CredentialRepresentation]
+ */
+ val representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT,
+ /**
+ * List of attributes that shall be requested explicitly (selective disclosure),
+ * or `null` to make no restrictions
+ */
+ val requestedAttributes: List? = null,
+ )
+
/**
* Creates an OIDC Authentication Request, encoded as query parameters to the [walletUrl].
*/
suspend fun createAuthnRequestUrl(
walletUrl: String,
- requestOptions: RequestOptions = RequestOptions(),
+ requestOptions: RequestOptions,
): String {
val urlBuilder = URLBuilder(walletUrl)
createAuthnRequest(requestOptions).encodeToParameters()
@@ -274,7 +243,7 @@ class OidcSiopVerifier private constructor(
*/
suspend fun createAuthnRequestUrlWithRequestObject(
walletUrl: String,
- requestOptions: RequestOptions = RequestOptions()
+ requestOptions: RequestOptions,
): KmmResult = catching {
val jar = createAuthnRequestAsSignedRequestObject(requestOptions).getOrThrow()
val urlBuilder = URLBuilder(walletUrl)
@@ -297,7 +266,7 @@ class OidcSiopVerifier private constructor(
suspend fun createAuthnRequestUrlWithRequestObjectByReference(
walletUrl: String,
requestUrl: String,
- requestOptions: RequestOptions = RequestOptions()
+ requestOptions: RequestOptions,
): KmmResult> = catching {
val jar = createAuthnRequestAsSignedRequestObject(requestOptions).getOrThrow()
val urlBuilder = URLBuilder(walletUrl)
@@ -323,20 +292,22 @@ class OidcSiopVerifier private constructor(
* ```
*/
suspend fun createAuthnRequestAsSignedRequestObject(
- requestOptions: RequestOptions = RequestOptions(),
+ requestOptions: RequestOptions,
): KmmResult = catching {
val requestObject = createAuthnRequest(requestOptions)
val requestObjectSerialized = jsonSerializer.encodeToString(
requestObject.copy(audience = relyingPartyUrl, issuer = relyingPartyUrl)
)
+ val attestationJwt = (clientIdScheme as? ClientIdScheme.VerifierAttestation)?.attestationJwt?.serialize()
+ val certificateChain = (clientIdScheme as? ClientIdScheme.CertificateSanDns)?.chain
jwsService.createSignedJwsAddingParams(
header = JwsHeader(
algorithm = jwsService.algorithm,
- attestationJwt = attestationJwt?.serialize(),
- certificateChain = x5c
+ attestationJwt = attestationJwt,
+ certificateChain = certificateChain,
),
payload = requestObjectSerialized.encodeToByteArray(),
- addJsonWebKey = x5c == null
+ addJsonWebKey = certificateChain == null,
).getOrThrow()
}
@@ -347,43 +318,58 @@ class OidcSiopVerifier private constructor(
* Callers may serialize the result with `result.encodeToParameters().formUrlEncode()`
*/
suspend fun createAuthnRequest(
- requestOptions: RequestOptions = RequestOptions(),
+ requestOptions: RequestOptions,
) = AuthenticationRequestParameters(
- responseType = "$ID_TOKEN $VP_TOKEN",
- clientId = buildClientId(),
+ responseType = requestOptions.responseType
+ .also { stateToResponseTypeStore.put(requestOptions.state, it) },
+ clientId = clientId,
redirectUrl = requestOptions.buildRedirectUrl(),
- responseUrl = responseUrl,
- clientIdScheme = clientIdScheme,
+ responseUrl = requestOptions.responseUrl,
+ clientIdScheme = clientIdScheme.clientIdScheme,
scope = requestOptions.buildScope(),
- nonce = uuid4().toString().also { challengeMutex.withLock { challengeSet += it } },
- clientMetadata = requestOptions.clientMetadataUrl?.let { null }
- ?: if (requestOptions.encryption) metadataWithEncryption else metadata,
+ nonce = nonceService.provideNonce()
+ .also { stateToNonceStore.put(requestOptions.state, it) },
+ clientMetadata = if (requestOptions.clientMetadataUrl != null) {
+ null
+ } else {
+ if (requestOptions.encryption) metadataWithEncryption else metadata
+ },
clientMetadataUri = requestOptions.clientMetadataUrl,
idTokenType = IdTokenType.SUBJECT_SIGNED.text,
responseMode = requestOptions.responseMode,
state = requestOptions.state,
presentationDefinition = PresentationDefinition(
id = uuid4().toString(),
- formats = requestOptions.representation.toFormatHolder(),
- inputDescriptors = listOf(
- requestOptions.toInputDescriptor()
- ),
+ inputDescriptors = requestOptions.credentials.map {
+ it.toInputDescriptor()
+ },
),
)
- private fun RequestOptions.buildScope() = listOfNotNull(SCOPE_OPENID, SCOPE_PROFILE, credentialScheme?.sdJwtType, credentialScheme?.vcType, credentialScheme?.isoNamespace)
- .joinToString(" ")
+ private fun RequestOptions.buildScope() = (
+ listOf(SCOPE_OPENID, SCOPE_PROFILE)
+ + credentials.mapNotNull { it.credentialScheme.sdJwtType }
+ + credentials.mapNotNull { it.credentialScheme.vcType }
+ + credentials.mapNotNull { it.credentialScheme.isoNamespace }
+ ).joinToString(" ")
+
+ private val clientId: String? by lazy {
+ clientIdFromCertificateChain ?: relyingPartyUrl
+ }
- private fun buildClientId() = (x5c?.let { it.leaf.tbsCertificate.subjectAlternativeNames?.dnsNames?.firstOrNull() }
- ?: relyingPartyUrl)
+ private val clientIdFromCertificateChain: String? by lazy {
+ (clientIdScheme as? ClientIdScheme.CertificateSanDns)?.chain
+ ?.let { it.leaf.tbsCertificate.subjectAlternativeNames?.dnsNames?.firstOrNull() }
+ }
private fun RequestOptions.buildRedirectUrl() = if ((responseMode == OpenIdConstants.ResponseMode.DIRECT_POST)
|| (responseMode == OpenIdConstants.ResponseMode.DIRECT_POST_JWT)
) null else relyingPartyUrl
- private fun RequestOptions.toInputDescriptor() = InputDescriptor(
+ //TODO extend for InputDescriptor interface in case QES
+ private fun RequestOptionsCredential.toInputDescriptor() = DifInputDescriptor(
id = buildId(),
- schema = listOfNotNull(credentialScheme?.schemaUri?.let { SchemaReference(it) }),
+ format = toFormatHolder(),
constraints = toConstraint(),
)
@@ -392,26 +378,24 @@ class OidcSiopVerifier private constructor(
* encoding it into the descriptor id as in the following non-normative example fow now:
* https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#appendix-A.3.1-4
*/
- private fun RequestOptions.buildId() =
- if (credentialScheme?.isoDocType != null && representation == ConstantIndex.CredentialRepresentation.ISO_MDOC)
+ private fun RequestOptionsCredential.buildId() =
+ if (credentialScheme.isoDocType != null && representation == ConstantIndex.CredentialRepresentation.ISO_MDOC)
credentialScheme.isoDocType!! else uuid4().toString()
- private fun RequestOptions.toConstraint() =
+ private fun RequestOptionsCredential.toConstraint() =
Constraint(fields = (toAttributeConstraints() + toTypeConstraint()).filterNotNull())
- private fun RequestOptions.toAttributeConstraints() =
+ private fun RequestOptionsCredential.toAttributeConstraints() =
requestedAttributes?.createConstraints(representation, credentialScheme)
?: listOf()
- private fun RequestOptions.toTypeConstraint() = credentialScheme?.let {
- when (representation) {
- ConstantIndex.CredentialRepresentation.PLAIN_JWT -> it.toVcConstraint()
- ConstantIndex.CredentialRepresentation.SD_JWT -> it.toSdJwtConstraint()
- ConstantIndex.CredentialRepresentation.ISO_MDOC -> null
- }
+ private fun RequestOptionsCredential.toTypeConstraint() = when (representation) {
+ ConstantIndex.CredentialRepresentation.PLAIN_JWT -> this.credentialScheme.toVcConstraint()
+ ConstantIndex.CredentialRepresentation.SD_JWT -> this.credentialScheme.toSdJwtConstraint()
+ ConstantIndex.CredentialRepresentation.ISO_MDOC -> null
}
- private fun ConstantIndex.CredentialRepresentation.toFormatHolder() = when (this) {
+ private fun RequestOptionsCredential.toFormatHolder() = when (representation) {
ConstantIndex.CredentialRepresentation.PLAIN_JWT -> FormatHolder(jwtVp = containerJwt)
ConstantIndex.CredentialRepresentation.SD_JWT -> FormatHolder(jwtSd = containerJwt)
ConstantIndex.CredentialRepresentation.ISO_MDOC -> FormatHolder(msoMdoc = containerJwt)
@@ -436,10 +420,10 @@ class OidcSiopVerifier private constructor(
) else null
private fun List.createConstraints(
- credentialRepresentation: ConstantIndex.CredentialRepresentation,
+ representation: ConstantIndex.CredentialRepresentation,
credentialScheme: ConstantIndex.CredentialScheme?,
): Collection = map {
- if (credentialRepresentation == ConstantIndex.CredentialRepresentation.ISO_MDOC)
+ if (representation == ConstantIndex.CredentialRepresentation.ISO_MDOC)
credentialScheme.toConstraintField(it)
else
ConstraintField(path = listOf("\$[${it.quote()}]"))
@@ -467,6 +451,11 @@ class OidcSiopVerifier private constructor(
*/
data class ValidationError(val field: String, val state: String?) : AuthnResponseResult()
+ /**
+ * Wallet provided an `id_token`, no `vp_token` (as requested by us!)
+ */
+ data class IdToken(val idToken: at.asitplus.openid.IdToken, val state: String?) : AuthnResponseResult()
+
/**
* Validation results of all returned verifiable presentations
*/
@@ -528,96 +517,132 @@ class OidcSiopVerifier private constructor(
* Validates [AuthenticationResponseParameters] from the Wallet
*/
suspend fun validateAuthnResponse(params: AuthenticationResponseParameters): AuthnResponseResult {
- if (params.response != null) {
- JwsSigned.parse(params.response).getOrNull()?.let { jarmResponse ->
+ val state = params.state
+ ?: return AuthnResponseResult.ValidationError("state", params.state)
+ .also { Napier.w("Invalid state: ${params.state}") }
+ params.response?.let { response ->
+ JwsSigned.deserialize(response).getOrNull()?.let { jarmResponse ->
if (!verifierJwsService.verifyJwsObject(jarmResponse)) {
- return AuthnResponseResult.ValidationError("response", params.state)
+ return AuthnResponseResult.ValidationError("response", state)
.also { Napier.w { "JWS of response not verified: ${params.response}" } }
}
AuthenticationResponseParameters.deserialize(jarmResponse.payload.decodeToString())
.getOrNull()?.let { return validateAuthnResponse(it) }
}
- JweEncrypted.parse(params.response).getOrNull()?.let { jarmResponse ->
- jwsService.decryptJweObject(jarmResponse, params.response).getOrNull()?.let { decrypted ->
+ JweEncrypted.deserialize(response).getOrNull()?.let { jarmResponse ->
+ jwsService.decryptJweObject(jarmResponse, response).getOrNull()?.let { decrypted ->
AuthenticationResponseParameters.deserialize(decrypted.payload.decodeToString())
.getOrNull()?.let { return validateAuthnResponse(it) }
}
}
}
- val idTokenJws = params.idToken
- ?: return AuthnResponseResult.ValidationError("idToken", params.state)
- .also { Napier.w("Could not parse idToken: $params") }
- val jwsSigned = JwsSigned.parse(idTokenJws).getOrNull()
- ?: return AuthnResponseResult.ValidationError("idToken", params.state)
+ val responseType = stateToResponseTypeStore.get(state)
+ ?: return AuthnResponseResult.ValidationError("state", state)
+ .also { Napier.w("State not associated with response type: $state") }
+
+ val idToken: IdToken? = if (responseType.contains(ID_TOKEN)) {
+ params.idToken?.let { idToken ->
+ catching {
+ extractValidatedIdToken(idToken)
+ }.getOrElse {
+ return AuthnResponseResult.ValidationError("idToken", state)
+ }
+ } ?: return AuthnResponseResult.ValidationError("idToken", state)
+ .also { Napier.w("State not associated with response type: $state") }
+ } else null
+
+ if (responseType.contains(VP_TOKEN)) {
+ val expectedNonce = stateToNonceStore.get(state)
+ ?: return AuthnResponseResult.ValidationError("state", state)
+ .also { Napier.w("State not associated with nonce: $state") }
+ val presentationSubmission = params.presentationSubmission
+ ?: return AuthnResponseResult.ValidationError("presentation_submission", state)
+ .also { Napier.w("presentation_submission empty") }
+ val descriptors = presentationSubmission.descriptorMap
+ ?: return AuthnResponseResult.ValidationError("presentation_submission", state)
+ .also { Napier.w("presentation_submission contains no descriptors") }
+ val verifiablePresentation = params.vpToken
+ ?: return AuthnResponseResult.ValidationError("vp_token is null", state)
+ .also { Napier.w("No VP in response") }
+
+ val validationResults = descriptors.map { descriptor ->
+ val relatedPresentation =
+ JsonPath(descriptor.cumulativeJsonPath).query(verifiablePresentation).first().value
+ val result = runCatching {
+ verifyPresentationResult(descriptor, relatedPresentation, expectedNonce)
+ }.getOrElse {
+ return AuthnResponseResult.ValidationError("Invalid presentation format", state)
+ .also { Napier.w("Invalid presentation format: $relatedPresentation") }
+ }
+ result.mapToAuthnResponseResult(state)
+ }
+
+ return if (validationResults.size != 1) {
+ AuthnResponseResult.VerifiablePresentationValidationResults(validationResults)
+ } else validationResults[0]
+ }
+
+ return idToken?.let { AuthnResponseResult.IdToken(it, state) }
+ ?: AuthnResponseResult.Error("Neither id_token nor vp_token", state)
+ }
+
+
+ @Throws(IllegalArgumentException::class, CancellationException::class)
+ private suspend fun extractValidatedIdToken(idTokenJws: String): IdToken {
+ val jwsSigned = JwsSigned.deserialize(idTokenJws).getOrNull()
+ ?: throw IllegalArgumentException("idToken")
.also { Napier.w("Could not parse JWS from idToken: $idTokenJws") }
if (!verifierJwsService.verifyJwsObject(jwsSigned))
- return AuthnResponseResult.ValidationError("idToken", params.state)
+ throw IllegalArgumentException("idToken")
.also { Napier.w { "JWS of idToken not verified: $idTokenJws" } }
- val idToken = IdToken.deserialize(jwsSigned.payload.decodeToString()).getOrElse { ex ->
- return AuthnResponseResult.ValidationError("idToken", params.state)
- .also { Napier.w("Could not deserialize idToken: $idTokenJws", ex) }
- }
+ val idToken = IdToken.deserialize(jwsSigned.payload.decodeToString()).getOrThrow()
if (idToken.issuer != idToken.subject)
- return AuthnResponseResult.ValidationError("iss", params.state)
+ throw IllegalArgumentException("idToken.iss")
.also { Napier.d("Wrong issuer: ${idToken.issuer}, expected: ${idToken.subject}") }
- val validAudiences = listOfNotNull(relyingPartyUrl,
- x5c?.leaf?.tbsCertificate?.subjectAlternativeNames?.dnsNames?.firstOrNull())
+ val validAudiences = listOfNotNull(relyingPartyUrl, clientIdFromCertificateChain)
if (idToken.audience !in validAudiences)
- return AuthnResponseResult.ValidationError("aud", params.state)
+ throw IllegalArgumentException("idToken.aud")
.also { Napier.d("audience not valid: ${idToken.audience}") }
if (idToken.expiration < (clock.now() - timeLeeway))
- return AuthnResponseResult.ValidationError("exp", params.state)
+ throw IllegalArgumentException("idToken.exp")
.also { Napier.d("expirationDate before now: ${idToken.expiration}") }
if (idToken.issuedAt > (clock.now() + timeLeeway))
- return AuthnResponseResult.ValidationError("iat", params.state)
+ throw IllegalArgumentException("idToken.iat")
.also { Napier.d("issuedAt after now: ${idToken.issuedAt}") }
- challengeMutex.withLock {
- if (!challengeSet.remove(idToken.nonce))
- return AuthnResponseResult.ValidationError("nonce", params.state)
- .also { Napier.d("nonce not valid: ${idToken.nonce}, not known to us") }
+ if (!nonceService.verifyAndRemoveNonce(idToken.nonce)) {
+ throw IllegalArgumentException("idToken.nonce")
+ .also { Napier.d("nonce not valid: ${idToken.nonce}, not known to us") }
}
if (idToken.subjectJwk == null)
- return AuthnResponseResult.ValidationError("nonce", params.state)
+ throw IllegalArgumentException("idToken.sub_jwk")
.also { Napier.d("sub_jwk is null") }
- if (idToken.subject != idToken.subjectJwk.jwkThumbprint)
- return AuthnResponseResult.ValidationError("sub", params.state)
+ if (idToken.subject != idToken.subjectJwk!!.jwkThumbprint)
+ throw IllegalArgumentException("idToken.sub")
.also { Napier.d("subject does not equal thumbprint of sub_jwk: ${idToken.subject}") }
+ return idToken
+ }
- val presentationSubmission = params.presentationSubmission
- ?: return AuthnResponseResult.ValidationError("presentation_submission", params.state)
- .also { Napier.w("presentation_submission empty") }
- val descriptors = presentationSubmission.descriptorMap
- ?: return AuthnResponseResult.ValidationError("presentation_submission", params.state)
- .also { Napier.w("presentation_submission contains no descriptors") }
- val verifiablePresentation = params.vpToken
- ?: return AuthnResponseResult.ValidationError("vp_token is null", params.state)
- .also { Napier.w("No VP in response") }
-
- val validationResults = descriptors.map { descriptor ->
- val relatedPresentation =
- JsonPath(descriptor.cumulativeJsonPath).query(verifiablePresentation).first().value
- val result = runCatching {
- when (descriptor.format) {
- ClaimFormatEnum.JWT_VP -> verifyJwtVpResult(relatedPresentation, idToken)
- ClaimFormatEnum.JWT_SD -> verifyJwtSdResult(relatedPresentation, idToken)
- ClaimFormatEnum.MSO_MDOC -> verifyMsoMdocResult(relatedPresentation, idToken)
- else -> throw IllegalArgumentException()
- }
- }.getOrElse {
- return AuthnResponseResult.ValidationError("Invalid presentation format", params.state)
- .also { Napier.w("Invalid presentation format: $relatedPresentation") }
- }
- result.mapToAuthnResponseResult(params.state)
+ /**
+ * Extract and verifies verifiable presentations, according to format defined in
+ * [OpenID for VCI](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html),
+ * as referenced by [OpenID for VP](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html).
+ */
+ private fun verifyPresentationResult(
+ descriptor: PresentationSubmissionDescriptor,
+ relatedPresentation: JsonElement,
+ challenge: String
+ ) = when (descriptor.format) {
+ ClaimFormatEnum.JWT_SD,
+ ClaimFormatEnum.MSO_MDOC,
+ ClaimFormatEnum.JWT_VP -> when (relatedPresentation) {
+ is JsonPrimitive -> verifier.verifyPresentation(relatedPresentation.content, challenge)
+ else -> throw IllegalArgumentException()
}
- return if (validationResults.size != 1) {
- AuthnResponseResult.VerifiablePresentationValidationResults(validationResults)
- } else validationResults[0]
+ else -> throw IllegalArgumentException()
}
- private fun Verifier.VerifyPresentationResult.mapToAuthnResponseResult(
- state: String?
- ) = when (this) {
+ private fun Verifier.VerifyPresentationResult.mapToAuthnResponseResult(state: String) = when (this) {
is Verifier.VerifyPresentationResult.InvalidStructure ->
AuthnResponseResult.Error("parse vp failed", state)
.also { Napier.w("VP error: $this") }
@@ -639,35 +664,6 @@ class OidcSiopVerifier private constructor(
.also { Napier.i("VP success: $this") }
}
- private fun verifyMsoMdocResult(
- relatedPresentation: JsonElement,
- idToken: IdToken
- ) = when (relatedPresentation) {
- // must be a string
- // source: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#appendix-A.2.5-1
- is JsonPrimitive -> verifier.verifyPresentation(relatedPresentation.content, idToken.nonce)
- else -> throw IllegalArgumentException()
- }
-
- private fun verifyJwtSdResult(
- relatedPresentation: JsonElement,
- idToken: IdToken
- ) = when (relatedPresentation) {
- // must be a string
- // source: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#appendix-A.3.5-1
- is JsonPrimitive -> verifier.verifyPresentation(relatedPresentation.content, idToken.nonce)
- else -> throw IllegalArgumentException()
- }
-
- private fun verifyJwtVpResult(
- relatedPresentation: JsonElement,
- idToken: IdToken
- ) = when (relatedPresentation) {
- // must be a string
- // source: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#appendix-A.1.1.5-1
- is JsonPrimitive -> verifier.verifyPresentation(relatedPresentation.content, idToken.nonce)
- else -> throw IllegalArgumentException()
- }
}
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt
index ea663c9c8..d91639c93 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt
@@ -2,32 +2,28 @@ package at.asitplus.wallet.lib.oidc
import at.asitplus.KmmResult
import at.asitplus.catching
+import at.asitplus.dif.PresentationDefinition
+import at.asitplus.openid.*
+import at.asitplus.openid.OpenIdConstants.BINDING_METHOD_JWK
+import at.asitplus.openid.OpenIdConstants.Errors
+import at.asitplus.openid.OpenIdConstants.ID_TOKEN
+import at.asitplus.openid.OpenIdConstants.PREFIX_DID_KEY
+import at.asitplus.openid.OpenIdConstants.SCOPE_OPENID
+import at.asitplus.openid.OpenIdConstants.URN_TYPE_JWK_THUMBPRINT
+import at.asitplus.openid.OpenIdConstants.VP_TOKEN
import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.signum.indispensable.josef.JsonWebKey
import at.asitplus.signum.indispensable.josef.JsonWebKeySet
import at.asitplus.signum.indispensable.josef.JwsSigned
import at.asitplus.signum.indispensable.josef.toJsonWebKey
-import at.asitplus.wallet.lib.agent.DefaultCryptoService
-import at.asitplus.wallet.lib.agent.Holder
-import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.CredentialSubmission
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.data.dif.PresentationDefinition
+import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.jws.DefaultJwsService
import at.asitplus.wallet.lib.jws.JwsService
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.Errors
-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
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.URN_TYPE_JWK_THUMBPRINT
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.VP_TOKEN
import at.asitplus.wallet.lib.oidc.helper.AuthenticationRequestParser
import at.asitplus.wallet.lib.oidc.helper.AuthenticationResponseFactory
import at.asitplus.wallet.lib.oidc.helper.AuthorizationRequestValidator
import at.asitplus.wallet.lib.oidc.helper.PresentationFactory
import at.asitplus.wallet.lib.oidc.helpers.AuthorizationResponsePreparationState
-import at.asitplus.wallet.lib.oidvci.IssuerMetadata
import at.asitplus.wallet.lib.oidvci.OAuth2Exception
import io.github.aakira.napier.Napier
import io.matthewnelson.encoding.base16.Base16
@@ -68,40 +64,50 @@ class OidcSiopWallet(
*/
private val scopePresentationDefinitionRetriever: ScopePresentationDefinitionRetriever,
) {
- companion object {
- fun newDefaultInstance(
- keyPairAdapter: KeyPairAdapter = RandomKeyPairAdapter(),
- holder: Holder = HolderAgent(keyPairAdapter),
- jwsService: JwsService = DefaultJwsService(DefaultCryptoService(keyPairAdapter)),
- clock: Clock = Clock.System,
- clientId: String = "https://wallet.a-sit.at/",
- remoteResourceRetriever: RemoteResourceRetrieverFunction = { null },
- requestObjectJwsVerifier: RequestObjectJwsVerifier = RequestObjectJwsVerifier { _, _ -> true },
- scopePresentationDefinitionRetriever: ScopePresentationDefinitionRetriever = { null },
- ): OidcSiopWallet {
- return OidcSiopWallet(
- holder = holder,
- agentPublicKey = keyPairAdapter.publicKey,
- jwsService = jwsService,
- clock = clock,
- clientId = clientId,
- remoteResourceRetriever = remoteResourceRetriever,
- requestObjectJwsVerifier = requestObjectJwsVerifier,
- scopePresentationDefinitionRetriever = scopePresentationDefinitionRetriever,
- )
- }
- }
+ constructor(
+ keyMaterial: KeyMaterial = EphemeralKeyWithoutCert(),
+ holder: Holder = HolderAgent(keyMaterial),
+ jwsService: JwsService = DefaultJwsService(DefaultCryptoService(keyMaterial)),
+ clock: Clock = Clock.System,
+ clientId: String = "https://wallet.a-sit.at/",
+ /**
+ * Need to implement if resources are defined by reference, i.e. the URL for a [JsonWebKeySet],
+ * or the authentication request itself as `request_uri`, or `presentation_definition_uri`.
+ * Implementations need to fetch the url passed in, and return either the body, if there is one,
+ * or the HTTP header `Location`, i.e. if the server sends the request object as a redirect.
+ */
+ remoteResourceRetriever: RemoteResourceRetrieverFunction = { null },
+ /**
+ * Need to verify the request object serialized as a JWS,
+ * which may be signed with a pre-registered key (see [OpenIdConstants.ClientIdScheme.PRE_REGISTERED]).
+ */
+ requestObjectJwsVerifier: RequestObjectJwsVerifier = RequestObjectJwsVerifier { _, _ -> true },
+ /**
+ * Need to implement if the presentation definition needs to be derived from a scope value.
+ * See [ScopePresentationDefinitionRetriever] for implementation instructions.
+ */
+ scopePresentationDefinitionRetriever: ScopePresentationDefinitionRetriever = { null },
+ ) : this(
+ holder = holder,
+ agentPublicKey = keyMaterial.publicKey,
+ jwsService = jwsService,
+ clock = clock,
+ clientId = clientId,
+ remoteResourceRetriever = remoteResourceRetriever,
+ requestObjectJwsVerifier = requestObjectJwsVerifier,
+ scopePresentationDefinitionRetriever = scopePresentationDefinitionRetriever,
+ )
- val metadata: IssuerMetadata by lazy {
- IssuerMetadata(
+ val metadata: OAuth2AuthorizationServerMetadata by lazy {
+ OAuth2AuthorizationServerMetadata(
issuer = clientId,
- authorizationEndpointUrl = clientId,
+ authorizationEndpoint = clientId,
responseTypesSupported = setOf(ID_TOKEN),
scopesSupported = setOf(SCOPE_OPENID),
subjectTypesSupported = setOf("pairwise", "public"),
idTokenSigningAlgorithmsSupported = setOf(jwsService.algorithm),
requestObjectSigningAlgorithmsSupported = setOf(jwsService.algorithm),
- subjectSyntaxTypesSupported = setOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY),
+ subjectSyntaxTypesSupported = setOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY, BINDING_METHOD_JWK),
idTokenTypesSupported = setOf(IdTokenType.SUBJECT_SIGNED),
presentationDefinitionUriSupported = false,
)
@@ -134,12 +140,10 @@ class OidcSiopWallet(
*/
suspend fun createAuthnResponse(
request: AuthenticationRequestParametersFrom,
- ): KmmResult = createAuthnResponseParams(request).map {
- AuthenticationResponseFactory(jwsService).createAuthenticationResponse(
- request,
- response = it,
- )
- }
+ ): KmmResult =
+ createAuthnResponseParams(request).map {
+ AuthenticationResponseFactory(jwsService).createAuthenticationResponse(request, it)
+ }
/**
* Creates the authentication response from the RP's [params]
@@ -164,7 +168,7 @@ class OidcSiopWallet(
}
/**
- * Starts the authorization response building process from the RP's authentication request in [input]
+ * Starts the authorization response building process from the RP's authentication request in [params]
*/
suspend fun startAuthorizationResponsePreparation(
params: AuthenticationRequestParametersFrom
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthenticationRequestParser.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthenticationRequestParser.kt
index 205ed835f..1715582c9 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthenticationRequestParser.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthenticationRequestParser.kt
@@ -2,21 +2,21 @@ package at.asitplus.wallet.lib.oidc.helper
import at.asitplus.KmmResult
import at.asitplus.catching
+import at.asitplus.openid.AuthenticationRequestParameters
+import at.asitplus.openid.AuthenticationResponseParameters
+import at.asitplus.openid.OpenIdConstants
+import at.asitplus.openid.OpenIdConstants.Errors
import at.asitplus.signum.indispensable.josef.JsonWebKeySet
import at.asitplus.signum.indispensable.josef.JwsSigned
-import at.asitplus.wallet.lib.oidc.AuthenticationRequestParameters
import at.asitplus.wallet.lib.oidc.AuthenticationRequestParametersFrom
-import at.asitplus.wallet.lib.oidc.AuthenticationResponseParameters
import at.asitplus.wallet.lib.oidc.AuthenticationResponseResult
-import at.asitplus.wallet.lib.oidc.OpenIdConstants
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.Errors
import at.asitplus.wallet.lib.oidc.RemoteResourceRetrieverFunction
import at.asitplus.wallet.lib.oidc.RequestObjectJwsVerifier
import at.asitplus.wallet.lib.oidvci.OAuth2Exception
import at.asitplus.wallet.lib.oidvci.decodeFromUrlQuery
import io.github.aakira.napier.Napier
-import io.ktor.http.Url
-import io.ktor.util.flattenEntries
+import io.ktor.http.*
+import io.ktor.util.*
internal class AuthenticationRequestParser(
/**
@@ -78,7 +78,7 @@ internal class AuthenticationRequestParser(
}
private fun parseRequestObjectJws(requestObject: String): AuthenticationRequestParametersFrom.JwsSigned? {
- return JwsSigned.parse(requestObject).getOrNull()?.let { jws ->
+ return JwsSigned.deserialize(requestObject).getOrNull()?.let { jws ->
val params = AuthenticationRequestParameters.deserialize(jws.payload.decodeToString()).getOrElse {
return null
.apply { Napier.w("parseRequestObjectJws: Deserialization failed", it) }
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthenticationResponseFactory.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthenticationResponseFactory.kt
index 50e8880e5..ab4df1a65 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthenticationResponseFactory.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthenticationResponseFactory.kt
@@ -1,23 +1,19 @@
package at.asitplus.wallet.lib.oidc.helper
+import at.asitplus.openid.AuthenticationResponseParameters
+import at.asitplus.openid.OpenIdConstants.Errors
+import at.asitplus.openid.OpenIdConstants.ResponseMode.*
+import at.asitplus.openid.RelyingPartyMetadata
import at.asitplus.signum.indispensable.josef.JweHeader
import at.asitplus.wallet.lib.jws.JwsService
import at.asitplus.wallet.lib.oidc.AuthenticationRequestParametersFrom
import at.asitplus.wallet.lib.oidc.AuthenticationResponse
-import at.asitplus.wallet.lib.oidc.AuthenticationResponseParameters
import at.asitplus.wallet.lib.oidc.AuthenticationResponseResult
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.Errors
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ResponseMode.DIRECT_POST
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ResponseMode.DIRECT_POST_JWT
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ResponseMode.FRAGMENT
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ResponseMode.OTHER
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ResponseMode.QUERY
-import at.asitplus.wallet.lib.oidc.RelyingPartyMetadata
import at.asitplus.wallet.lib.oidvci.OAuth2Exception
import at.asitplus.wallet.lib.oidvci.encodeToParameters
import at.asitplus.wallet.lib.oidvci.formUrlEncode
import io.github.aakira.napier.Napier
-import io.ktor.http.URLBuilder
+import io.ktor.http.*
import io.matthewnelson.encoding.base64.Base64
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToByteArray
@@ -44,10 +40,14 @@ internal class AuthenticationResponseFactory(
request: AuthenticationRequestParametersFrom,
response: AuthenticationResponse,
): AuthenticationResponseResult.Post {
- val url = request.parameters.responseUrl ?: request.parameters.redirectUrl
- ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
+ val url = request.parameters.responseUrl
+ ?: request.parameters.redirectUrl
+ ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
val responseSerialized = buildJarm(request, response)
- val jarm = AuthenticationResponseParameters(response = responseSerialized)
+ val jarm = AuthenticationResponseParameters(
+ response = responseSerialized,
+ state = request.parameters.state
+ )
return AuthenticationResponseResult.Post(url, jarm.encodeToParameters())
}
@@ -55,8 +55,9 @@ internal class AuthenticationResponseFactory(
request: AuthenticationRequestParametersFrom,
response: AuthenticationResponse,
): AuthenticationResponseResult.Post {
- val url = request.parameters.responseUrl ?: request.parameters.redirectUrl
- ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
+ val url = request.parameters.responseUrl
+ ?: request.parameters.redirectUrl
+ ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
return AuthenticationResponseResult.Post(url, response.params.encodeToParameters())
}
@@ -84,8 +85,8 @@ internal class AuthenticationResponseFactory(
): AuthenticationResponseResult.Redirect {
val url = request.parameters.redirectUrl?.let { redirectUrl ->
URLBuilder(redirectUrl).apply {
- encodedFragment = response.params.encodeToParameters().formUrlEncode()
- }.buildString()
+ encodedFragment = response.params.encodeToParameters().formUrlEncode()
+ }.buildString()
} ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
return AuthenticationResponseResult.Redirect(url, response.params)
}
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthorizationRequestValidator.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthorizationRequestValidator.kt
index 0c7887149..7bbef220e 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthorizationRequestValidator.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthorizationRequestValidator.kt
@@ -1,14 +1,14 @@
package at.asitplus.wallet.lib.oidc.helper
import at.asitplus.signum.indispensable.pki.leaf
-import at.asitplus.wallet.lib.oidc.AuthenticationRequestParameters
+import at.asitplus.openid.AuthenticationRequestParameters
import at.asitplus.wallet.lib.oidc.AuthenticationRequestParametersFrom
-import at.asitplus.wallet.lib.oidc.OpenIdConstants
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.Errors
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ID_TOKEN
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ResponseMode.DIRECT_POST
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ResponseMode.DIRECT_POST_JWT
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.VP_TOKEN
+import at.asitplus.openid.OpenIdConstants
+import at.asitplus.openid.OpenIdConstants.Errors
+import at.asitplus.openid.OpenIdConstants.ID_TOKEN
+import at.asitplus.openid.OpenIdConstants.ResponseMode.DIRECT_POST
+import at.asitplus.openid.OpenIdConstants.ResponseMode.DIRECT_POST_JWT
+import at.asitplus.openid.OpenIdConstants.VP_TOKEN
import at.asitplus.wallet.lib.oidvci.OAuth2Exception
import io.github.aakira.napier.Napier
import io.ktor.http.Url
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthorizationResponsePreparationState.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthorizationResponsePreparationState.kt
index 0fbf3085f..456ba2086 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthorizationResponsePreparationState.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/AuthorizationResponsePreparationState.kt
@@ -1,7 +1,7 @@
package at.asitplus.wallet.lib.oidc.helpers
-import at.asitplus.wallet.lib.data.dif.PresentationDefinition
-import at.asitplus.wallet.lib.oidc.RelyingPartyMetadata
+import at.asitplus.dif.PresentationDefinition
+import at.asitplus.openid.RelyingPartyMetadata
import kotlinx.serialization.Serializable
@Serializable
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/PresentationFactory.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/PresentationFactory.kt
index 200f270aa..c6efcd9b7 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/PresentationFactory.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/helper/PresentationFactory.kt
@@ -2,24 +2,24 @@ package at.asitplus.wallet.lib.oidc.helper
import at.asitplus.KmmResult
import at.asitplus.catching
+import at.asitplus.dif.ClaimFormatEnum
+import at.asitplus.dif.FormatHolder
+import at.asitplus.dif.PresentationDefinition
+import at.asitplus.openid.AuthenticationRequestParameters
+import at.asitplus.openid.IdToken
+import at.asitplus.openid.OpenIdConstants.Errors
+import at.asitplus.openid.OpenIdConstants.ID_TOKEN
+import at.asitplus.openid.OpenIdConstants.VP_TOKEN
+import at.asitplus.openid.RelyingPartyMetadata
import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.signum.indispensable.josef.JwsSigned
import at.asitplus.signum.indispensable.josef.toJsonWebKey
import at.asitplus.wallet.lib.agent.CredentialSubmission
import at.asitplus.wallet.lib.agent.Holder
import at.asitplus.wallet.lib.agent.toDefaultSubmission
-import at.asitplus.wallet.lib.data.dif.ClaimFormatEnum
-import at.asitplus.wallet.lib.data.dif.FormatHolder
-import at.asitplus.wallet.lib.data.dif.PresentationDefinition
import at.asitplus.wallet.lib.data.dif.PresentationSubmissionValidator
import at.asitplus.wallet.lib.jws.JwsService
-import at.asitplus.wallet.lib.oidc.AuthenticationRequestParameters
import at.asitplus.wallet.lib.oidc.AuthenticationRequestParametersFrom
-import at.asitplus.wallet.lib.oidc.IdToken
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.Errors
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ID_TOKEN
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.VP_TOKEN
-import at.asitplus.wallet.lib.oidc.RelyingPartyMetadata
import at.asitplus.wallet.lib.oidvci.OAuth2Exception
import io.github.aakira.napier.Napier
import kotlinx.datetime.Clock
@@ -44,7 +44,7 @@ internal class PresentationFactory(
val credentialSubmissions = inputDescriptorSubmissions
?: holder.matchInputDescriptorsAgainstCredentialStore(
inputDescriptors = presentationDefinition.inputDescriptors,
- fallbackFormatHolder = presentationDefinition.formats ?: clientMetadata?.vpFormats,
+ fallbackFormatHolder = clientMetadata?.vpFormats,
).getOrThrow().toDefaultSubmission()
presentationDefinition.validateSubmission(
@@ -103,7 +103,7 @@ internal class PresentationFactory(
@Throws(OAuth2Exception::class)
private fun AuthenticationRequestParameters.verifyResponseType() {
- if (responseType == null || !responseType.contains(VP_TOKEN)) {
+ if (responseType == null || !responseType!!.contains(VP_TOKEN)) {
Napier.w("vp_token not requested in response_type='$responseType'")
throw OAuth2Exception(Errors.INVALID_REQUEST)
}
@@ -129,9 +129,9 @@ internal class PresentationFactory(
}
val constraintFieldMatches = holder.evaluateInputDescriptorAgainstCredential(
- inputDescriptor,
- submission.value.credential,
- fallbackFormatHolder = this.formats ?: clientMetadata?.vpFormats,
+ inputDescriptor = inputDescriptor,
+ credential = submission.value.credential,
+ fallbackFormatHolder = clientMetadata?.vpFormats,
pathAuthorizationValidator = { true },
).getOrThrow()
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/AuthorizationDetails.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/AuthorizationDetails.kt
deleted file mode 100644
index d2374e3a3..000000000
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/AuthorizationDetails.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-package at.asitplus.wallet.lib.oidvci
-
-import at.asitplus.wallet.lib.oidvci.mdl.RequestedCredentialClaimSpecification
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-/**
- * OID4VCI: The request parameter `authorization_details` defined in Section 2 of (RFC9396) MUST be used to convey the
- * details about the Credentials the Wallet wants to obtain. This specification introduces a new authorization details
- * type `openid_credential` and defines the following parameters to be used with this authorization details type.
- */
-@Serializable
-data class AuthorizationDetails(
- /**
- * OID4VCI: REQUIRED. String that determines the authorization details type. It MUST be set to `openid_credential`
- * for the purpose of this specification.
- */
- @SerialName("type")
- val type: String,
-
- /**
- * OID4VC: REQUIRED when [format] parameter is not present. String specifying a unique identifier of the Credential
- * being described in [IssuerMetadata.supportedCredentialConfigurations].
- */
- @SerialName("credential_configuration_id")
- val credentialConfigurationId: String? = null,
-
- /**
- * OID4VCI: REQUIRED when [credentialConfigurationId] parameter is not present.
- * String identifying the format of the Credential the Wallet needs.
- * This Credential format identifier determines further claims in the authorization details object needed to
- * identify the Credential type in the requested format.
- */
- @SerialName("format")
- val format: CredentialFormatEnum? = null,
-
- /**
- * OID4VCI: ISO mDL: OPTIONAL. This claim contains the type value the Wallet requests authorization for at the
- * Credential Issuer. It MUST only be present if the [format] claim is present. It MUST not be present otherwise.
- */
- @SerialName("doctype")
- val docType: String? = null,
-
- /**
- * OID4VCI: ISO mDL: OPTIONAL. Object as defined in Appendix A.3.2 excluding the `display` and `value_type`
- * parameters. The `mandatory` parameter here is used by the Wallet to indicate to the Issuer that it only accepts
- * Credential(s) issued with those claim(s).
- */
- @SerialName("claims")
- val claims: Map>? = null,
-
- /**
- * OID4VCI: W3C VC: OPTIONAL. Object containing a detailed description of the Credential consisting of the
- * following parameters. see [SupportedCredentialFormatDefinition].
- */
- @SerialName("credential_definition")
- val credentialDefinition: SupportedCredentialFormatDefinition? = null,
-
- /**
- * OID4VCI: IETF SD-JWT VC: REQUIRED. String as defined in Appendix A.3.2. This claim contains the type values
- * the Wallet requests authorization for at the Credential Issuer.
- * It MUST only be present if the [format] claim is present. It MUST not be present otherwise.
- */
- @SerialName("vct")
- val sdJwtVcType: String? = null,
-
- /**
- * Must contain an entry form [IssuerMetadata.authorizationServers].
- */
- @SerialName("locations")
- val locations: Set? = null,
-
- /**
- * OID4VCI: OPTIONAL. Array of strings, each uniquely identifying a Credential that can be issued using the Access
- * Token returned in this response. Each of these Credentials corresponds to the same entry in the
- * [IssuerMetadata.supportedCredentialConfigurations] but can contain different claim values or a
- * different subset of claims within the claims set identified by that Credential type.
- * This parameter can be used to simplify the Credential Request, as defined in Section 7.2, where the
- * `credential_identifier` parameter replaces the format parameter and any other Credential format-specific
- * parameters in the Credential Request. When received, the Wallet MUST use these values together with an Access
- * Token in subsequent Credential Requests.
- */
- @SerialName("credential_identifiers")
- val credentialIdentifiers: Set? = null,
-)
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CodeService.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CodeService.kt
index 11d4f646f..f85fd94ee 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CodeService.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CodeService.kt
@@ -6,7 +6,7 @@ interface CodeService {
fun provideCode(): String
- fun verifyCode(it: String): Boolean
+ fun verifyAndRemove(it: String): Boolean
}
@@ -18,7 +18,7 @@ class DefaultCodeService : CodeService {
return uuid4().toString().also { validCodes += it }
}
- override fun verifyCode(it: String): Boolean {
+ override fun verifyAndRemove(it: String): Boolean {
return validCodes.remove(it)
}
}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialAuthorizationServiceStrategy.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialAuthorizationServiceStrategy.kt
new file mode 100644
index 000000000..01e96ee38
--- /dev/null
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialAuthorizationServiceStrategy.kt
@@ -0,0 +1,44 @@
+package at.asitplus.wallet.lib.oidvci
+
+import at.asitplus.openid.AuthenticationRequestParameters
+import at.asitplus.openid.AuthorizationDetails
+import at.asitplus.wallet.lib.data.ConstantIndex
+import at.asitplus.wallet.lib.oauth2.AuthorizationServiceStrategy
+
+/**
+ * Provide authentication and authorization for credential issuance.
+ */
+class CredentialAuthorizationServiceStrategy(
+ /**
+ * Source of user data.
+ */
+ private val dataProvider: OAuth2DataProvider,
+ /**
+ * List of supported schemes.
+ */
+ credentialSchemes: Set,
+) : AuthorizationServiceStrategy {
+
+ private val supportedCredentialSchemes = credentialSchemes
+ .flatMap { it.toSupportedCredentialFormat().entries }
+ .associate { it.key to it.value }
+
+ override suspend fun loadUserInfo(request: AuthenticationRequestParameters, code: String) =
+ dataProvider.loadUserInfo(request, code)
+
+ override fun filterAuthorizationDetails(authorizationDetails: Set) =
+ authorizationDetails
+ .filterIsInstance()
+ .filter { authnDetails ->
+ authnDetails.credentialConfigurationId?.let {
+ supportedCredentialSchemes.containsKey(it)
+ } ?: authnDetails.format?.let {
+ supportedCredentialSchemes.values.any {
+ it.format == authnDetails.format &&
+ it.docType == authnDetails.docType &&
+ it.sdJwtVcType == authnDetails.sdJwtVcType &&
+ it.credentialDefinition == authnDetails.credentialDefinition
+ }
+ } ?: false
+ }.toSet()
+}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialIssuer.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialIssuer.kt
index c6b128791..a4d829a48 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialIssuer.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialIssuer.kt
@@ -2,6 +2,12 @@ package at.asitplus.wallet.lib.oidvci
import at.asitplus.KmmResult
import at.asitplus.catching
+import at.asitplus.openid.*
+import at.asitplus.openid.OpenIdConstants.Errors
+import at.asitplus.openid.OpenIdConstants.PROOF_CWT_TYPE
+import at.asitplus.openid.OpenIdConstants.PROOF_JWT_TYPE
+import at.asitplus.openid.OpenIdConstants.ProofType
+import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.signum.indispensable.cosef.CborWebToken
import at.asitplus.signum.indispensable.cosef.CoseSigned
import at.asitplus.signum.indispensable.io.Base64UrlStrict
@@ -13,8 +19,6 @@ import at.asitplus.wallet.lib.agent.IssuerCredentialDataProvider
import at.asitplus.wallet.lib.data.AttributeIndex
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.VcDataModelConstants.VERIFIABLE_CREDENTIAL
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.Errors
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.ProofType
import com.benasher44.uuid.uuid4
import io.github.aakira.napier.Napier
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
@@ -22,14 +26,15 @@ import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
/**
* Server implementation to issue credentials using OID4VCI.
*
- * Implemented from [OpenID for Verifiable Credential Issuance]
- * (https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html), Draft 13, 2024-02-08.
+ * Implemented from
+ * [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
+ * , Draft 14, 2024-08-21.
*/
class CredentialIssuer(
/**
* Used to get the user data, and access tokens.
*/
- private val authorizationService: OAuth2AuthorizationServer,
+ private val authorizationService: OAuth2AuthorizationServerAdapter,
/**
* Used to actually issue the credential.
*/
@@ -63,32 +68,47 @@ class CredentialIssuer(
credentialIssuer = publicContext,
authorizationServers = setOf(authorizationService.publicContext),
credentialEndpointUrl = "$publicContext$credentialEndpointPath",
- authorizationEndpointUrl = if (authorizationService is SimpleAuthorizationService)
- authorizationService.publicContext + authorizationService.authorizationEndpointPath
- else null,
- tokenEndpointUrl = if (authorizationService is SimpleAuthorizationService)
- authorizationService.publicContext + authorizationService.tokenEndpointPath
- else null,
- supportedCredentialConfigurations = mutableMapOf().apply {
- credentialSchemes.forEach { putAll(it.toSupportedCredentialFormat(issuer.cryptoAlgorithms)) }
- },
- supportsCredentialIdentifiers = true,
+ supportedCredentialConfigurations = credentialSchemes
+ .flatMap { it.toSupportedCredentialFormat(issuer.cryptoAlgorithms).entries }
+ .associate { it.key to it.value },
+ batchCredentialIssuance = BatchCredentialIssuanceMetadata(1)
)
}
/**
* Offer all [credentialSchemes] to clients.
- * Callers may need to transport this in [CredentialOfferUrlParameters] to (HTTPS) clients.
+ *
+ * Callers need to encode this in [CredentialOfferUrlParameters], and offer the resulting URL to clients,
+ * i.e. by displaying a QR Code that can be scanned with wallet appps.
*/
- suspend fun credentialOffer(): CredentialOffer = CredentialOffer(
+ suspend fun credentialOfferWithAuthorizationCode(): CredentialOffer = CredentialOffer(
credentialIssuer = publicContext,
- configurationIds = credentialSchemes.flatMap { it.toSupportedCredentialFormat(issuer.cryptoAlgorithms).keys },
+ configurationIds = credentialSchemes.flatMap { it.toCredentialIdentifier() },
grants = CredentialOfferGrants(
- authorizationCode = CredentialOfferGrantsAuthCode(
- issuerState = uuid4().toString(), // TODO remember this state, for subsequent requests from the Wallet
+ authorizationCode =
+ CredentialOfferGrantsAuthCode(
+ // TODO remember this state, for subsequent requests from the Wallet
+ issuerState = uuid4().toString(),
authorizationServer = authorizationService.publicContext
),
- preAuthorizedCode = authorizationService.providePreAuthorizedCode()?.let {
+ )
+ )
+
+ /**
+ * Offer all [credentialSchemes] to clients.
+ *
+ * Callers need to encode this in [CredentialOfferUrlParameters], and offer the resulting URL to clients,
+ * i.e. by displaying a QR Code that can be scanned with wallet appps.
+ *
+ * @param user used to create the credential when the wallet app requests the credential
+ */
+ suspend fun credentialOfferWithPreAuthnForUser(
+ user: OidcUserInfoExtended,
+ ): CredentialOffer = CredentialOffer(
+ credentialIssuer = publicContext,
+ configurationIds = credentialSchemes.flatMap { it.toCredentialIdentifier() },
+ grants = CredentialOfferGrants(
+ preAuthorizedCode = authorizationService.providePreAuthorizedCode(user)?.let {
CredentialOfferGrantsPreAuthCode(
preAuthorizedCode = it,
authorizationServer = authorizationService.publicContext
@@ -101,7 +121,8 @@ class CredentialIssuer(
* Verifies the [accessToken] to contain a token from [authorizationService],
* verifies the proof sent by the client (must contain a nonce sent from [authorizationService]),
* and issues credentials to the client.
- * Send the result JSON-serialized back to the client.
+ *
+ * Callers need to send the result JSON-serialized back to the client.
*
* @param accessToken The value of HTTP header `Authorization` sent by the client,
* with the prefix `Bearer ` removed, so the plain access token
@@ -111,112 +132,111 @@ class CredentialIssuer(
accessToken: String,
params: CredentialRequestParameters
): KmmResult = catching {
- val proof = params.proof
- ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
- .also { Napier.w("credential: client did not provide proof of possession") }
- val subjectPublicKey = when (proof.proofType) {
- ProofType.JWT -> {
- if (proof.jwt == null)
- throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid proof: $proof") }
- val jwsSigned = JwsSigned.parse(proof.jwt).getOrNull()
- ?: throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid proof: $proof") }
- val jwt = JsonWebToken.deserialize(jwsSigned.payload.decodeToString()).getOrNull()
- ?: throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid JWT in proof: $proof") }
- if (jwt.nonce == null || !authorizationService.verifyAndRemoveClientNonce(jwt.nonce!!))
- throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid nonce in JWT in proof: ${jwt.nonce}") }
- if (jwsSigned.header.type != ProofType.JWT_HEADER_TYPE.stringRepresentation)
- throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid header type in JWT in proof: ${jwsSigned.header}") }
- if (jwt.audience == null || jwt.audience != publicContext)
- throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid audience in JWT in proof: ${jwsSigned.header}") }
- jwsSigned.header.publicKey
- ?: throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide no valid key in header in JWT in proof: ${jwsSigned.header}") }
- }
-
- ProofType.CWT -> {
- if (proof.cwt == null)
- throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid proof: $proof") }
- val coseSigned = CoseSigned.deserialize(proof.cwt.decodeToByteArray(Base64UrlStrict)).getOrNull()
- ?: throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid proof: $proof") }
-
- val cwt = coseSigned.payload?.let { CborWebToken.deserialize(it).getOrNull() }
- ?: throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid CWT in proof: $proof") }
- if (cwt.nonce == null || !authorizationService.verifyAndRemoveClientNonce(cwt.nonce!!.decodeToString()))
- throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid nonce in CWT in proof: ${cwt.nonce}") }
- val header = coseSigned.protectedHeader.value
- if (header.contentType != ProofType.CWT_HEADER_TYPE.stringRepresentation)
- throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid header type in CWT in proof: $header") }
- if (cwt.audience == null || cwt.audience != publicContext)
- throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid audience in CWT in proof: $header") }
- header.certificateChain?.let { X509Certificate.decodeFromByteArray(it)?.publicKey }
- ?: throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide no valid key in header in CWT in proof: $header") }
- }
-
- else -> {
- throw OAuth2Exception(Errors.INVALID_PROOF)
- .also { Napier.w("credential: client did provide invalid proof type: ${proof.proofType}") }
- }
- }
+ val subjectPublicKey = validateProofExtractSubjectPublicKey(params)
val userInfo = authorizationService.getUserInfo(accessToken).getOrNull()
?: throw OAuth2Exception(Errors.INVALID_TOKEN)
.also { Napier.w("credential: client did not provide correct token: $accessToken") }
- val issuedCredentialResult = when {
- params.format != null -> {
- val credentialScheme = params.extractCredentialScheme(params.format)
- ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
- .also { Napier.w("credential: client did not provide correct credential scheme: ${params}") }
- issuer.issueCredential(
- subjectPublicKey = subjectPublicKey,
- credentialScheme = credentialScheme,
- representation = params.format.toRepresentation(),
- claimNames = params.claims?.map { it.value.keys }?.flatten()?.ifEmpty { null },
- dataProviderOverride = buildIssuerCredentialDataProviderOverride(userInfo)
- )
- }
-
- params.credentialIdentifier != null -> {
- val (credentialScheme, representation) = decodeFromCredentialIdentifier(params.credentialIdentifier)
- ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
- .also { Napier.w("credential: client did not provide correct credential identifier: ${params.credentialIdentifier}") }
- issuer.issueCredential(
- subjectPublicKey = subjectPublicKey,
- credentialScheme = credentialScheme,
- representation = representation.toRepresentation(),
- claimNames = params.claims?.map { it.value.keys }?.flatten()?.ifEmpty { null },
- dataProviderOverride = buildIssuerCredentialDataProviderOverride(userInfo)
- )
- }
-
- else -> {
+ val issuedCredentialResult = params.format?.let { format ->
+ val credentialScheme = params.extractCredentialScheme(format)
+ ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
+ .also { Napier.w("credential: client did not provide correct credential scheme: $params") }
+ issuer.issueCredential(
+ subjectPublicKey = subjectPublicKey,
+ credentialScheme = credentialScheme,
+ representation = params.format!!.toRepresentation(),
+ claimNames = params.claims?.map { it.value.keys }?.flatten()?.ifEmpty { null },
+ dataProviderOverride = buildIssuerCredentialDataProviderOverride(userInfo)
+ )
+ } ?: params.credentialIdentifier?.let { credentialIdentifier ->
+ val (credentialScheme, representation) = decodeFromCredentialIdentifier(credentialIdentifier) ?: run {
+ Napier.w("client did not provide correct credential identifier: $credentialIdentifier")
throw OAuth2Exception(Errors.INVALID_REQUEST)
- .also { Napier.w("credential: client did not provide format or credential identifier in params: $params") }
}
- }
+ issuer.issueCredential(
+ subjectPublicKey = subjectPublicKey,
+ credentialScheme = credentialScheme,
+ representation = representation.toRepresentation(),
+ claimNames = params.claims?.map { it.value.keys }?.flatten()?.ifEmpty { null },
+ dataProviderOverride = buildIssuerCredentialDataProviderOverride(userInfo)
+ )
+ } ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
+ .also { Napier.w("client did not provide format or credential identifier in params: $params") }
+
val issuedCredential = issuedCredentialResult.getOrElse {
throw OAuth2Exception(Errors.INVALID_REQUEST)
.also { Napier.w("credential: issuer did not issue credential: $issuedCredentialResult") }
}
- // TODO Implement Batch Credential Endpoint for more than one credential response
issuedCredential.toCredentialResponseParameters()
.also { Napier.i("credential returns $it") }
}
+ private suspend fun validateProofExtractSubjectPublicKey(params: CredentialRequestParameters): CryptoPublicKey =
+ params.proof?.validateProof()
+ ?: params.proofs?.validateProof()
+ ?: throw OAuth2Exception(Errors.INVALID_REQUEST)
+ .also { Napier.w("credential: client did not provide proof of possession") }
+
+ private suspend fun CredentialRequestProof.validateProof() = when (proofType) {
+ ProofType.JWT -> jwt?.validateJwtProof()
+ ProofType.CWT -> cwt?.validateCwtProof()
+ else -> null
+ }
+
+ private suspend fun CredentialRequestProofContainer.validateProof() = when (proofType) {
+ ProofType.JWT -> jwt?.map { it.validateJwtProof() }?.toSet()?.singleOrNull()
+ else -> null
+ }
+
+ private suspend fun String.validateJwtProof(): CryptoPublicKey {
+ val jwsSigned = JwsSigned.deserialize(this).getOrNull()
+ ?: throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide invalid proof: $this") }
+ val jwt = JsonWebToken.deserialize(jwsSigned.payload.decodeToString()).getOrNull()
+ ?: throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide invalid JWT in proof: $this") }
+ if (jwsSigned.header.type != PROOF_JWT_TYPE)
+ throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide invalid header type in JWT in proof: ${jwsSigned.header}") }
+ if (authorizationService.supportsClientNonce)
+ if (jwt.nonce == null || !authorizationService.verifyClientNonce(jwt.nonce!!))
+ throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide invalid nonce in JWT in proof: ${jwt.nonce}") }
+ if (jwt.audience == null || jwt.audience != publicContext)
+ throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide invalid audience in JWT in proof: ${jwsSigned.header}") }
+ return jwsSigned.header.publicKey
+ ?: throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide no valid key in header in JWT in proof: ${jwsSigned.header}") }
+ }
+
+ /**
+ * Removed in OID4VCI Draft 14, kept here for a bit of backwards-compatibility
+ */
+ private suspend fun String.validateCwtProof(): CryptoPublicKey {
+ val coseSigned = CoseSigned.deserialize(decodeToByteArray(Base64UrlStrict)).getOrNull()
+ ?: throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide invalid proof: $this") }
+ val cwt = coseSigned.payload?.let { CborWebToken.deserialize(it).getOrNull() }
+ ?: throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide invalid CWT in proof: $this") }
+ if (cwt.nonce == null || !authorizationService.verifyClientNonce(cwt.nonce!!.decodeToString()))
+ throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide invalid nonce in CWT in proof: ${cwt.nonce}") }
+ val header = coseSigned.protectedHeader.value
+ if (header.contentType != PROOF_CWT_TYPE)
+ throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide invalid header type in CWT in proof: $header") }
+ if (cwt.audience == null || cwt.audience != publicContext)
+ throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide invalid audience in CWT in proof: $header") }
+ return header.certificateChain?.let { X509Certificate.decodeFromByteArray(it)?.publicKey }
+ ?: throw OAuth2Exception(Errors.INVALID_PROOF)
+ .also { Napier.w("client did provide no valid key in header in CWT in proof: $header") }
+ }
+
}
@@ -224,9 +244,7 @@ private fun CredentialRequestParameters.extractCredentialScheme(format: Credenti
CredentialFormatEnum.JWT_VC -> credentialDefinition?.types?.firstOrNull { it != VERIFIABLE_CREDENTIAL }
?.let { AttributeIndex.resolveAttributeType(it) }
- CredentialFormatEnum.VC_SD_JWT,
- CredentialFormatEnum.JWT_VC_SD_UNOFFICIAL -> sdJwtVcType?.let { AttributeIndex.resolveSdJwtAttributeType(it) }
-
+ CredentialFormatEnum.VC_SD_JWT -> sdJwtVcType?.let { AttributeIndex.resolveSdJwtAttributeType(it) }
CredentialFormatEnum.MSO_MDOC -> docType?.let { AttributeIndex.resolveIsoDoctype(it) }
else -> null
}
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialOffer.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialOffer.kt
deleted file mode 100644
index bcdfe072e..000000000
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/CredentialOffer.kt
+++ /dev/null
@@ -1,172 +0,0 @@
-package at.asitplus.wallet.lib.oidvci
-
-import at.asitplus.KmmResult
-import at.asitplus.KmmResult.Companion.wrap
-import at.asitplus.wallet.lib.oidc.jsonSerializer
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.JsonElement
-import kotlinx.serialization.json.JsonObject
-import kotlinx.serialization.json.decodeFromJsonElement
-
-/**
- * OID4VCI: The Credential Issuer sends Credential Offer using an HTTP GET request or an HTTP redirect to the Wallet's
- * Credential Offer Endpoint defined in Section 11.1.The Credential Offer object, which is a JSON-encoded object with
- * the Credential Offer parameters, can be sent by value or by reference.
- */
-@Serializable
-data class CredentialOfferUrlParameters(
- /**
- * OID4VCI: Object with the Credential Offer parameters. This MUST NOT be present when the [credentialOfferUrl]
- * parameter is present.
- */
- @SerialName("credential_offer")
- val credentialOffer: JsonObject? = null,
-
- /**
- * OID4VCI: String that is a URL using the `https` scheme referencing a resource containing a JSON object with the
- * Credential Offer parameters. This MUST NOT be present when the [credentialOffer] parameter is present.
- */
- @SerialName("credential_offer_uri")
- val credentialOfferUrl: String? = null,
-) {
- fun serialize() = jsonSerializer.encodeToString(this)
-
- companion object {
- fun deserialize(input: String): KmmResult =
- runCatching { jsonSerializer.decodeFromString(input) }.wrap()
- }
-}
-
-@Serializable
-data class CredentialOffer(
- /**
- * OID4VCI: REQUIRED. The URL of the Credential Issuer, as defined in Section 11.2.1, from which the Wallet is
- * requested to obtain one or more Credentials. The Wallet uses it to obtain the Credential Issuer's Metadata
- * following the steps defined in Section 11.2.2.
- */
- @SerialName("credential_issuer")
- val credentialIssuer: String,
-
- /**
- * OID4VCI: REQUIRED. Array of unique strings that each identify one of the keys in the name/value pairs stored in
- * the `credential_configurations_supported` Credential Issuer metadata. The Wallet uses these string values to
- * obtain the respective object that contains information about the Credential being offered as defined in
- * Section 11.2.3. For example, these string values can be used to obtain `scope` values to be used in the
- * Authorization Request.
- */
- @SerialName("credential_configuration_ids")
- val configurationIds: Collection,
-
- /**
- * OID4VCI: OPTIONAL. If `grants` is not present or is empty, the Wallet MUST determine the Grant Types the
- * Credential Issuer's Authorization Server supports using the respective metadata. When multiple grants are
- * present, it is at the Wallet's discretion which one to use.
- */
- @SerialName("grants")
- val grants: CredentialOfferGrants? = null,
-) {
- fun serialize() = jsonSerializer.encodeToString(this)
-
- companion object {
- fun deserialize(input: String): KmmResult =
- runCatching { jsonSerializer.decodeFromString(input) }.wrap()
- fun deserialize(input: JsonElement): KmmResult =
- runCatching { jsonSerializer.decodeFromJsonElement(input) }.wrap()
- }
-}
-
-/**
- * OID4VCI: Object indicating to the Wallet the Grant Types the Credential Issuer's Authorization Server is prepared to
- * process for this Credential Offer. Every grant is represented by a name/value pair. The name is the Grant Type
- * identifier; the value is an object that contains parameters either determining the way the Wallet MUST use the
- * particular grant and/or parameters the Wallet MUST send with the respective request(s).
- */
-@Serializable
-data class CredentialOfferGrants(
- @SerialName("authorization_code")
- val authorizationCode: CredentialOfferGrantsAuthCode? = null,
-
- @SerialName("urn:ietf:params:oauth:grant-type:pre-authorized_code")
- val preAuthorizedCode: CredentialOfferGrantsPreAuthCode? = null
-)
-
-@Serializable
-data class CredentialOfferGrantsAuthCode(
- /**
- * OID4VCI: OPTIONAL. String value created by the Credential Issuer and opaque to the Wallet that is used to bind
- * the subsequent Authorization Request with the Credential Issuer to a context set up during previous steps. If the
- * Wallet decides to use the Authorization Code Flow and received a value for this parameter, it MUST include it in
- * the subsequent Authorization Request to the Credential Issuer as the `issuer_state` parameter value.
- */
- @SerialName("issuer_state")
- val issuerState: String? = null,
-
- /**
- * OID4VCI: OPTIONAL string that the Wallet can use to identify the Authorization Server to use with this grant
- * type when `authorization_servers` parameter in the Credential Issuer metadata has multiple entries. It MUST NOT
- * be used otherwise. The value of this parameter MUST match with one of the values in the `authorization_servers`
- * array obtained from the Credential Issuer metadata.
- */
- @SerialName("authorization_server")
- val authorizationServer: String? = null,
-)
-
-@Serializable
-data class CredentialOfferGrantsPreAuthCode(
- /**
- * OID4VCI: REQUIRED. The code representing the Credential Issuer's authorization for the Wallet to obtain
- * Credentials of a certain type. This code MUST be short lived and single use. If the Wallet decides to use the
- * Pre-Authorized Code Flow, this parameter value MUST be included in the subsequent Token Request with the
- * Pre-Authorized Code Flow.
- */
- @SerialName("pre-authorized_code")
- val preAuthorizedCode: String,
-
- /**
- * OID4VCI: OPTIONAL. Object specifying whether the Authorization Server expects presentation of a Transaction Code
- * by the End-User along with the Token Request in a Pre-Authorized Code Flow. If the Authorization Server does not
- * expect a Transaction Code, this object is absent; this is the default.
- */
- @SerialName("tx_code")
- val transactionCode: CredentialOfferGrantsPreAuthCodeTransactionCode? = null,
-
- /**
- * OID4VCI: OPTIONAL. The minimum amount of time in seconds that the Wallet SHOULD wait between polling requests to
- * the token endpoint. If no value is provided, Wallets MUST use 5 as the default.
- */
- @SerialName("interval")
- val waitIntervalSeconds: Int? = 5,
-
- /**
- * OID4VCI: OPTIONAL string that the Wallet can use to identify the Authorization Server to use with this grant
- * type when `authorization_servers` parameter in the Credential Issuer metadata has multiple entries.
- */
- @SerialName("authorization_server")
- val authorizationServer: String? = null
-)
-
-@Serializable
-data class CredentialOfferGrantsPreAuthCodeTransactionCode(
- /**
- * OID4VCI: OPTIONAL. String specifying the input character set. Possible values are `numeric` (only digits) and
- * `text` (any characters). The default is `numeric`.
- */
- @SerialName("input_mode")
- val inputMode: String? = "numeric",
-
- /**
- * OID4VCI: OPTIONAL. Integer specifying the length of the Transaction Code. This helps the Wallet to render the
- * input screen and improve the user experience.
- */
- @SerialName("length")
- val length: Int? = null,
-
- /**
- * OID4VCI: OPTIONAL. String containing guidance for the Holder of the Wallet on how to obtain the Transaction
- * Code, e.g., describing over which communication channel it is delivered.
- */
- @SerialName("description")
- val description: String? = null,
-)
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/Extensions.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/Extensions.kt
index 0ec81ee60..428a8ec64 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/Extensions.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/Extensions.kt
@@ -1,6 +1,10 @@
package at.asitplus.wallet.lib.oidvci
-import at.asitplus.signum.indispensable.X509SignatureAlgorithm
+import at.asitplus.openid.*
+import at.asitplus.openid.OpenIdConstants.BINDING_METHOD_COSE_KEY
+import at.asitplus.openid.OpenIdConstants.BINDING_METHOD_JWK
+import at.asitplus.openid.OpenIdConstants.URN_TYPE_JWK_THUMBPRINT
+import at.asitplus.signum.indispensable.SignatureAlgorithm
import at.asitplus.signum.indispensable.io.Base64UrlStrict
import at.asitplus.signum.indispensable.josef.toJwsAlgorithm
import at.asitplus.wallet.lib.agent.Issuer
@@ -10,18 +14,19 @@ import at.asitplus.wallet.lib.data.ConstantIndex.supportsIso
import at.asitplus.wallet.lib.data.ConstantIndex.supportsSdJwt
import at.asitplus.wallet.lib.data.ConstantIndex.supportsVcJwt
import at.asitplus.wallet.lib.data.VcDataModelConstants
-import at.asitplus.wallet.lib.oidc.OpenIdConstants
-import at.asitplus.wallet.lib.oidvci.mdl.RequestedCredentialClaimSpecification
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
-fun ConstantIndex.CredentialScheme.toSupportedCredentialFormat(cryptoAlgorithms: Set): Map {
+fun ConstantIndex.CredentialScheme.toSupportedCredentialFormat(cryptoAlgorithms: Set? = null)
+ : Map {
val iso = if (supportsIso) {
isoNamespace!! to SupportedCredentialFormat.forIsoMdoc(
format = CredentialFormatEnum.MSO_MDOC,
scope = isoNamespace!!,
docType = isoDocType!!,
- supportedBindingMethods = setOf(OpenIdConstants.BINDING_METHOD_COSE_KEY),
- supportedSigningAlgorithms = cryptoAlgorithms.mapNotNull { it.toJwsAlgorithm().getOrNull()?.identifier }.toSet(),
+ supportedBindingMethods = setOf(BINDING_METHOD_JWK, BINDING_METHOD_COSE_KEY),
+ supportedSigningAlgorithms = cryptoAlgorithms
+ ?.mapNotNull { it.toJwsAlgorithm().getOrNull()?.identifier }
+ ?.toSet(),
isoClaims = mapOf(
isoNamespace!! to claimNames.associateWith { RequestedCredentialClaimSpecification() }
)
@@ -32,11 +37,13 @@ fun ConstantIndex.CredentialScheme.toSupportedCredentialFormat(cryptoAlgorithms:
format = CredentialFormatEnum.JWT_VC,
scope = vcType!!,
credentialDefinition = SupportedCredentialFormatDefinition(
- types = listOf(VcDataModelConstants.VERIFIABLE_CREDENTIAL, vcType!!),
+ types = setOf(VcDataModelConstants.VERIFIABLE_CREDENTIAL, vcType!!),
credentialSubject = claimNames.associateWith { CredentialSubjectMetadataSingle() }
),
- supportedBindingMethods = setOf(OpenIdConstants.PREFIX_DID_KEY, OpenIdConstants.URN_TYPE_JWK_THUMBPRINT),
- supportedSigningAlgorithms = cryptoAlgorithms.mapNotNull { it.toJwsAlgorithm().getOrNull()?.identifier }.toSet(),
+ supportedBindingMethods = setOf(BINDING_METHOD_JWK, URN_TYPE_JWK_THUMBPRINT),
+ supportedSigningAlgorithms = cryptoAlgorithms
+ ?.mapNotNull { it.toJwsAlgorithm().getOrNull()?.identifier }
+ ?.toSet(),
)
} else null
val sdJwt = if (supportsSdJwt) {
@@ -44,35 +51,54 @@ fun ConstantIndex.CredentialScheme.toSupportedCredentialFormat(cryptoAlgorithms:
format = CredentialFormatEnum.VC_SD_JWT,
scope = sdJwtType!!,
sdJwtVcType = sdJwtType!!,
- supportedBindingMethods = setOf(OpenIdConstants.PREFIX_DID_KEY, OpenIdConstants.URN_TYPE_JWK_THUMBPRINT),
- supportedSigningAlgorithms = cryptoAlgorithms.mapNotNull { it.toJwsAlgorithm().getOrNull()?.identifier }.toSet(),
+ supportedBindingMethods = setOf(BINDING_METHOD_JWK, URN_TYPE_JWK_THUMBPRINT),
+ supportedSigningAlgorithms = cryptoAlgorithms
+ ?.mapNotNull { it.toJwsAlgorithm().getOrNull()?.identifier }
+ ?.toSet(),
sdJwtClaims = claimNames.associateWith { RequestedCredentialClaimSpecification() }
)
} else null
return listOfNotNull(iso, jwtVc, sdJwt).toMap()
}
+fun ConstantIndex.CredentialScheme.toCredentialIdentifier() =
+ listOfNotNull(
+ if (supportsIso) isoNamespace!! else null,
+ if (supportsVcJwt) encodeToCredentialIdentifier(vcType!!, CredentialFormatEnum.JWT_VC) else null,
+ if (supportsSdJwt) encodeToCredentialIdentifier(sdJwtType!!, CredentialFormatEnum.VC_SD_JWT) else null
+ )
+
/**
- * Reverse functionality of [decodeFromCredentialIdentifier]
+ * Reverse functionality of [decodeFromCredentialIdentifier],
+ * i.e. encodes a credential [type] and [format] to a single string,
+ * e.g. from [at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023] and [CredentialFormatEnum.JWT_VC] to
+ * `AtomicAttribute2023#jwt_vc_json`
*/
private fun encodeToCredentialIdentifier(type: String, format: CredentialFormatEnum) =
"$type#${format.text}"
/**
- * Reverse functionality of [encodeToCredentialIdentifier]
+ * Reverse functionality of [encodeToCredentialIdentifier], which can also handle ISO namespaces,
+ * i.e. decodes a single string into a credential scheme and format,
+ * e.g. from `AtomicAttribute2023#jwt_vc_json` to
+ * [at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023] and [CredentialFormatEnum.JWT_VC]
*/
fun decodeFromCredentialIdentifier(input: String): Pair? {
- val vcTypeOrSdJwtType = input.substringBeforeLast("#")
- val credentialScheme = AttributeIndex.resolveSdJwtAttributeType(vcTypeOrSdJwtType)
- ?: AttributeIndex.resolveAttributeType(vcTypeOrSdJwtType)
- ?: return null
- val format = CredentialFormatEnum.parse(input.substringAfterLast("#"))
- ?: return null
- return Pair(credentialScheme, format)
+ if (input.contains("#")) {
+ val vcTypeOrSdJwtType = input.substringBeforeLast("#")
+ val credentialScheme = AttributeIndex.resolveSdJwtAttributeType(vcTypeOrSdJwtType)
+ ?: AttributeIndex.resolveAttributeType(vcTypeOrSdJwtType)
+ ?: return null
+ val format = CredentialFormatEnum.parse(input.substringAfterLast("#"))
+ ?: return null
+ return Pair(credentialScheme, format)
+ } else {
+ return AttributeIndex.resolveIsoNamespace(input)
+ ?.let { Pair(it, CredentialFormatEnum.MSO_MDOC) }
+ }
}
fun CredentialFormatEnum.toRepresentation() = when (this) {
- CredentialFormatEnum.JWT_VC_SD_UNOFFICIAL -> ConstantIndex.CredentialRepresentation.SD_JWT
CredentialFormatEnum.VC_SD_JWT -> ConstantIndex.CredentialRepresentation.SD_JWT
CredentialFormatEnum.MSO_MDOC -> ConstantIndex.CredentialRepresentation.ISO_MDOC
else -> ConstantIndex.CredentialRepresentation.PLAIN_JWT
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerMetadata.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerMetadata.kt
deleted file mode 100644
index dd59f71bf..000000000
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/IssuerMetadata.kt
+++ /dev/null
@@ -1,214 +0,0 @@
-package at.asitplus.wallet.lib.oidvci
-
-import at.asitplus.KmmResult
-import at.asitplus.KmmResult.Companion.wrap
-import at.asitplus.signum.indispensable.josef.JwsAlgorithm
-import at.asitplus.wallet.lib.oidc.IdTokenType
-import at.asitplus.wallet.lib.oidc.jsonSerializer
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.encodeToString
-
-/**
- * To be serialized into `/.well-known/openid-credential-issuer`
- */
-@Serializable
-data class IssuerMetadata(
- /**
- * OIDC Discovery: REQUIRED. URL using the https scheme with no query or fragment component that the OP asserts as
- * its Issuer Identifier. If Issuer discovery is supported (see Section 2), this value MUST be identical to the
- * issuer value returned by WebFinger. This also MUST be identical to the `iss` Claim value in ID Tokens issued
- * from this Issuer.
- */
- @SerialName("issuer")
- val issuer: String? = null,
-
- /**
- * OID4VCI: REQUIRED. The Credential Issuer's identifier.
- */
- @SerialName("credential_issuer")
- val credentialIssuer: String? = null,
-
- /**
- * OID4VCI: OPTIONAL. Array of strings, where each string is an identifier of the OAuth 2.0 Authorization Server
- * (as defined in RFC8414) the Credential Issuer relies on for authorization. If this parameter is omitted, the
- * entity providing the Credential Issuer is also acting as the Authorization Server, i.e., the Credential Issuer's
- * identifier is used to obtain the Authorization Server metadata.
- */
- @SerialName("authorization_servers")
- val authorizationServers: Set? = null,
-
- /**
- * OID4VCI: REQUIRED. URL of the Credential Issuer's Credential Endpoint. This URL MUST use the https scheme and
- * MAY contain port, path and query parameter components.
- */
- @SerialName("credential_endpoint")
- val credentialEndpointUrl: String? = null,
-
- /**
- * OIDC Discovery: URL of the OP's OAuth 2.0 Token Endpoint (OpenID.Core). This is REQUIRED unless only the
- * Implicit Flow is used.
- */
- @SerialName("token_endpoint")
- val tokenEndpointUrl: String? = null,
-
- /**
- * OIDC Discovery: REQUIRED. URL of the OP's JSON Web Key Set document. This contains the signing key(s) the RP
- * uses to validate signatures from the OP. The JWK Set MAY also contain the Server's encryption key(s), which are
- * used by RPs to encrypt requests to the Server.
- *
- * OIDC SIOPv2: MUST NOT be present in Self-Issued OP Metadata. If it is, the RP MUST ignore it and use the `sub`
- * Claim in the ID Token to obtain signing keys to validate the signatures from the Self-Issued OpenID Provider.
- */
- @SerialName("jwks_uri")
- val jsonWebKeySetUrl: String? = null,
-
- /**
- * OIDC Discovery: REQUIRED. URL of the OP's OAuth 2.0 Authorization Endpoint (OpenID.Core).
- *
- * OIDC SIOPv2: REQUIRED. URL of the Self-Issued OP used by the RP to perform Authentication of the End-User.
- * Can be custom URI scheme, or Universal Links/App links.
- */
- @SerialName("authorization_endpoint")
- val authorizationEndpointUrl: String? = null,
-
- /**
- * OID4VCI: OPTIONAL. URL of the Credential Issuer's Batch Credential Endpoint, as defined in Section 8.
- * This URL MUST use the `https` scheme and MAY contain port, path, and query parameter components.
- * If omitted, the Credential Issuer does not support the Batch Credential Endpoint.
- */
- @SerialName("batch_credential_endpoint")
- val batchCredentialEndpointUrl: String? = null,
-
- /**
- * OID4VCI: OPTIONAL. URL of the Credential Issuer's Deferred Credential Endpoint, as defined in Section 9.
- * This URL MUST use the `https` scheme and MAY contain port, path, and query parameter components.
- * If omitted, the Credential Issuer does not support the Deferred Credential Endpoint.
- */
- @SerialName("deferred_credential_endpoint")
- val deferredCredentialEndpointUrl: String? = null,
-
- /**
- * OID4VCI: OPTIONAL. URL of the Credential Issuer's Notification Endpoint, as defined in Section 10.
- * This URL MUST use the `https` scheme and MAY contain port, path, and query parameter components.
- * If omitted, the Credential Issuer does not support the Notification Endpoint.
- */
- @SerialName("notification_endpoint")
- val notificationEndpointUrl: String? = null,
-
- /**
- * OID4VCI: OPTIONAL. Object containing information about whether the Credential Issuer supports encryption of the
- * Credential and Batch Credential Response on top of TLS.
- */
- @SerialName("credential_response_encryption")
- val credentialResponseEncryption: SupportedAlgorithmsContainer? = null,
-
- /**
- * OID4VCI: OPTIONAL. Boolean value specifying whether the Credential Issuer supports returning
- * [AuthorizationDetails.credentialIdentifiers] in the Token Response parameter, with `true`
- * indicating support. If omitted, the default value is `false`.
- */
- @SerialName("credential_identifiers_supported")
- val supportsCredentialIdentifiers: Boolean? = false,
-
- /**
- * OID4VCI: REQUIRED. Object that describes specifics of the Credential that the Credential Issuer supports
- * issuance of. This object contains a list of name/value pairs, where each name is a unique identifier of the
- * supported Credential being described.
- */
- @SerialName("credential_configurations_supported")
- val supportedCredentialConfigurations: Map? = null,
-
- /**
- * OID4VCI: OPTIONAL. An array of objects, where each object contains display properties of a Credential Issuer for
- * a certain language.
- */
- @SerialName("display")
- val displayProperties: Set? = null,
-
- /**
- * OIDC Discovery: REQUIRED. JSON array containing a list of the OAuth 2.0 `response_type` values that this OP
- * supports. Dynamic OpenID Providers MUST support the `code`, `id_token`, and the `token id_token` Response Type
- * values.
- * OIDC SIOPv2: MUST be `id_token`.
- */
- @SerialName("response_types_supported")
- val responseTypesSupported: Set? = null,
-
- /**
- * OIDC SIOPv2: REQUIRED. A JSON array of strings representing supported scopes.
- * MUST support the `openid` scope value.
- */
- @SerialName("scopes_supported")
- val scopesSupported: Set? = null,
-
- /**
- * OIDC Discovery: REQUIRED. JSON array containing a list of the Subject Identifier types that this OP supports.
- * Valid types include `pairwise` and `public`.
- */
- @SerialName("subject_types_supported")
- val subjectTypesSupported: Set? = null,
-
- /**
- * OIDC Discovery: REQUIRED. A JSON array containing a list of the JWS signing algorithms (`alg` values) supported
- * by the OP for the ID Token to encode the Claims in a JWT (RFC7519).
- * Valid values include `RS256`, `ES256`, `ES256K`, and `EdDSA`.
- */
- @SerialName("id_token_signing_alg_values_supported")
- val idTokenSigningAlgorithmsSupported: Set? = null,
-
- /**
- * OIDC SIOPv2: REQUIRED. A JSON array containing a list of the JWS signing algorithms (alg values) supported by the
- * OP for Request Objects, which are described in Section 6.1 of OpenID.Core.
- * Valid values include `none`, `RS256`, `ES256`, `ES256K`, and `EdDSA`.
- */
- @SerialName("request_object_signing_alg_values_supported")
- val requestObjectSigningAlgorithmsSupported: Set? = null,
-
- /**
- * OIDC SIOPv2: REQUIRED. A JSON array of strings representing URI scheme identifiers and optionally method names of
- * supported Subject Syntax Types.
- * Valid values include `urn:ietf:params:oauth:jwk-thumbprint`, `did:example` and others.
- */
- @SerialName("subject_syntax_types_supported")
- val subjectSyntaxTypesSupported: Set? = null,
-
- /**
- * OIDC SIOPv2: OPTIONAL. A JSON array of strings containing the list of ID Token types supported by the OP,
- * the default value is `attester_signed_id_token` (the id token is issued by the party operating the OP, i.e. this
- * is the classical id token as defined in OpenID.Core), may also include `subject_signed_id_token` (Self-Issued
- * ID Token, i.e. the id token is signed with key material under the end-user's control).
- */
- @SerialName("id_token_types_supported")
- val idTokenTypesSupported: Set? = null,
-
- /**
- * OID4VP: OPTIONAL. Boolean value specifying whether the Wallet supports the transfer of `presentation_definition`
- * by reference, with true indicating support. If omitted, the default value is true.
- */
- @SerialName("presentation_definition_uri_supported")
- val presentationDefinitionUriSupported: Boolean = true,
-
- /**
- * OID4VP: REQUIRED. An object containing a list of key value pairs, where the key is a string identifying a
- * Credential format supported by the Wallet. Valid Credential format identifier values are defined in Annex E
- * of OpenID.VCI. Other values may be used when defined in the profiles of this specification.
- */
- @SerialName("vp_formats_supported")
- val vpFormatsSupported: VpFormatsSupported? = null,
-
- /**
- * OID4VP: OPTIONAL. Array of JSON Strings containing the values of the Client Identifier schemes that the Wallet
- * supports. The values defined by this specification are `pre-registered`, `redirect_uri`, `entity_id`, `did`.
- * If omitted, the default value is pre-registered.
- */
- @SerialName("client_id_schemes_supported")
- val clientIdSchemesSupported: Set? = null,
-) {
- fun serialize() = jsonSerializer.encodeToString(this)
-
- companion object {
- fun deserialize(input: String): KmmResult =
- runCatching { jsonSerializer.decodeFromString(input) }.wrap()
- }
-}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/MapStore.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/MapStore.kt
new file mode 100644
index 000000000..3c015c73c
--- /dev/null
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/MapStore.kt
@@ -0,0 +1,47 @@
+package at.asitplus.wallet.lib.oidvci
+
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/**
+ * Provides a simple map of keys of type [T] to values of type [U].
+ * Mainly used in OID4VCI to hold state in the [SimpleAuthorizationService] and [WalletService].
+ * Can be implemented to provide replication across different instances of the enclosing application.
+ */
+interface MapStore {
+
+ /**
+ * Implementers: Associate [key] with [value]
+ */
+ suspend fun put(key: T, value: U)
+
+ /**
+ * Implementers: Return the value associated with [key]
+ */
+ suspend fun get(key: T): U?
+
+ /**
+ * Implementers: Return and remove the value associated with [key]
+ */
+ suspend fun remove(key: T): U?
+
+}
+
+
+/**
+ * Holds map in memory, protected with a [Mutex],
+ * to ensure a basic form of thread-safety.
+ */
+class DefaultMapStore : MapStore {
+
+ private val mutex = Mutex()
+ private val map = mutableMapOf()
+
+ override suspend fun put(key: T, value: U) {
+ mutex.withLock { map.put(key, value) }
+ }
+
+ override suspend fun get(key: T) = map[key]
+
+ override suspend fun remove(key: T): U? = mutex.withLock { map.remove(key) }
+}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/NonceService.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/NonceService.kt
index ca06fa229..d861bb82e 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/NonceService.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/NonceService.kt
@@ -1,24 +1,47 @@
package at.asitplus.wallet.lib.oidvci
import com.benasher44.uuid.uuid4
-
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/**
+ * Provides generation, storage and validation of challenges used throughout the code, e.g. as challenges for
+ * presentation of credentials.
+ * Can be implemented to provide replication across different instances of the enclosing application.
+ */
+// TODO Rename "nonce" to "challenge" in next major release
interface NonceService {
- fun provideNonce(): String
+ /**
+ * Implementers: Generate a new random string, store it for later verification
+ */
+ suspend fun provideNonce(): String
+
+ /**
+ * Implementers: Verify if the string has been generated by this instance
+ */
+ suspend fun verifyNonce(it: String): Boolean
- fun verifyAndRemoveNonce(it: String): Boolean
+ /**
+ * Implementers: Verify if the value has been generated by this instance, remove it from the list of valid values
+ */
+ suspend fun verifyAndRemoveNonce(it: String): Boolean
}
+/**
+ * Holds valid random values in memory, protected with a [Mutex],
+ * to ensure a basic form of thread-safety.
+ */
class DefaultNonceService : NonceService {
- private val validNonces = mutableListOf()
+ // TODO Remove values after a certain timeout
+ private val mutex = Mutex()
+ private val values = mutableListOf()
+
+ override suspend fun provideNonce() = uuid4().toString().also { mutex.withLock { values += it } }
- override fun provideNonce(): String {
- return uuid4().toString().also { validNonces += it }
- }
+ override suspend fun verifyNonce(it: String) = values.contains(it)
- override fun verifyAndRemoveNonce(it: String): Boolean {
- return validNonces.remove(it)
- }
+ override suspend fun verifyAndRemoveNonce(it: String) = mutex.withLock { values.remove(it) }
}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2AuthorizationServer.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2AuthorizationServer.kt
deleted file mode 100644
index 0173b3cae..000000000
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2AuthorizationServer.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package at.asitplus.wallet.lib.oidvci
-
-import at.asitplus.KmmResult
-
-/**
- * Used by [CredentialIssuer] to obtain user data when issuing credentials using OID4VCI.
- */
-interface OAuth2AuthorizationServer {
- /**
- * Used in several fields in [IssuerMetadata], to provide endpoint URLs to clients.
- */
- val publicContext: String
-
- /**
- * Provide a pre-authorized code (for flow defined in OID4VCI), to be used by the Wallet implementation
- * to load credentials.
- */
- suspend fun providePreAuthorizedCode(): String?
-
- /**
- * Get the [OidcUserInfoExtended] (holding [OidcUserInfo]) associated with the [accessToken],
- * that was created before at the Authorization Server.
- */
- suspend fun getUserInfo(accessToken: String): KmmResult
-
- // TODO How is this supposed to happen when using an external Authorization Server?
- suspend fun verifyAndRemoveClientNonce(nonce: String): Boolean
-
- /**
- * Provide necessary [OAuth2AuthorizationServerMetadata] JSON for a client to be able to authenticate
- */
- suspend fun provideMetadata(): KmmResult
-}
-
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2AuthorizationServerAdapter.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2AuthorizationServerAdapter.kt
new file mode 100644
index 000000000..8eab29585
--- /dev/null
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2AuthorizationServerAdapter.kt
@@ -0,0 +1,48 @@
+package at.asitplus.wallet.lib.oidvci
+
+import at.asitplus.KmmResult
+import at.asitplus.openid.OAuth2AuthorizationServerMetadata
+import at.asitplus.openid.OidcUserInfoExtended
+
+/**
+ * Used in OID4VCI by [CredentialIssuer] to obtain user data when issuing credentials using OID4VCI.
+ *
+ * Could also be a remote service
+ */
+interface OAuth2AuthorizationServerAdapter {
+
+ /**
+ * Used in several fields in [at.asitplus.openid.IssuerMetadata], to provide endpoint URLs to clients.
+ */
+ val publicContext: String
+
+ /**
+ * Provide a pre-authorized code (for flow defined in OID4VCI), to be used by the Wallet implementation
+ * to load credentials.
+ */
+ suspend fun providePreAuthorizedCode(user: OidcUserInfoExtended): String
+
+ /**
+ * Get the [OidcUserInfoExtended] (holding [at.asitplus.openid.OidcUserInfo]) associated with the [accessToken],
+ * that was created before at the Authorization Server.
+ */
+ suspend fun getUserInfo(accessToken: String): KmmResult
+
+ /**
+ * Whether this authorization server includes [at.asitplus.openid.TokenResponseParameters.clientNonce] it its
+ * token response, i.e. whether the [CredentialIssuer] needs to verify it using [verifyClientNonce].
+ */
+ val supportsClientNonce: Boolean
+
+ /**
+ * Called by [CredentialIssuer] to verify that nonces contained in proof-of-possession statements from clients
+ * are indeed valid.
+ */
+ suspend fun verifyClientNonce(nonce: String): Boolean
+
+ /**
+ * Provide necessary [OAuth2AuthorizationServerMetadata] JSON for a client to be able to authenticate
+ */
+ suspend fun provideMetadata(): KmmResult
+}
+
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2DataProvider.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2DataProvider.kt
index dda72ef22..efd895687 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2DataProvider.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2DataProvider.kt
@@ -1,15 +1,15 @@
package at.asitplus.wallet.lib.oidvci
-import at.asitplus.wallet.lib.oidc.AuthenticationRequestParameters
+import at.asitplus.openid.AuthenticationRequestParameters
+import at.asitplus.openid.OidcUserInfoExtended
/**
- * Interface used in [SimpleAuthorizationService] to actually load user data, converting it into [OidcUserInfo].
+ * Interface used in [CredentialAuthorizationServiceStrategy] to actually load user data when client requests
+ * and authorization code.
*/
interface OAuth2DataProvider {
/**
* Load user information (i.e. authenticate the client) with data sent from [request].
- *
- * @param request May be null when using pre-authorized code flow (defined in OID4VCI).
*/
- suspend fun loadUserInfo(request: AuthenticationRequestParameters? = null): OidcUserInfoExtended?
+ suspend fun loadUserInfo(request: AuthenticationRequestParameters, code: String): OidcUserInfoExtended?
}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2IssuerCredentialDataProvider.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2IssuerCredentialDataProvider.kt
index 662df8490..245cda68c 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2IssuerCredentialDataProvider.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/OAuth2IssuerCredentialDataProvider.kt
@@ -2,6 +2,7 @@ package at.asitplus.wallet.lib.oidvci
import at.asitplus.KmmResult
import at.asitplus.catching
+import at.asitplus.openid.OidcUserInfoExtended
import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.wallet.lib.agent.ClaimToBeIssued
import at.asitplus.wallet.lib.agent.CredentialToBeIssued
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/TokenService.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/TokenService.kt
deleted file mode 100644
index 8aef1e618..000000000
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/TokenService.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package at.asitplus.wallet.lib.oidvci
-
-import com.benasher44.uuid.uuid4
-
-interface TokenService {
-
- fun provideToken(): String
-
- fun verifyToken(it: String): Boolean
-
- fun verifyAndRemoveToken(it: String): Boolean
-
-}
-
-class DefaultTokenService : TokenService {
-
- private val validTokens = mutableListOf()
-
- override fun provideToken(): String {
- return uuid4().toString().also { validTokens += it }
- }
-
- override fun verifyToken(it: String): Boolean {
- return validTokens.contains(it)
- }
-
- override fun verifyAndRemoveToken(it: String): Boolean {
- return validTokens.remove(it)
- }
-}
\ No newline at end of file
diff --git a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/WalletService.kt b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/WalletService.kt
index f4598cfe6..87de9e104 100644
--- a/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/WalletService.kt
+++ b/vck-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidvci/WalletService.kt
@@ -2,9 +2,8 @@ package at.asitplus.wallet.lib.oidvci
import at.asitplus.KmmResult
import at.asitplus.catching
-import at.asitplus.signum.indispensable.cosef.CborWebToken
-import at.asitplus.signum.indispensable.cosef.CoseHeader
-import at.asitplus.signum.indispensable.cosef.toCoseAlgorithm
+import at.asitplus.openid.*
+import at.asitplus.openid.OpenIdConstants.Errors
import at.asitplus.signum.indispensable.io.Base64UrlStrict
import at.asitplus.signum.indispensable.josef.JsonWebKeySet
import at.asitplus.signum.indispensable.josef.JsonWebToken
@@ -12,10 +11,8 @@ import at.asitplus.signum.indispensable.josef.JwsHeader
import at.asitplus.signum.indispensable.josef.toJwsAlgorithm
import at.asitplus.wallet.lib.agent.CryptoService
import at.asitplus.wallet.lib.agent.DefaultCryptoService
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.cbor.CoseService
-import at.asitplus.wallet.lib.cbor.DefaultCoseService
+import at.asitplus.wallet.lib.agent.EphemeralKeyWithoutCert
+import at.asitplus.wallet.lib.agent.KeyMaterial
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.ConstantIndex.CredentialRepresentation
import at.asitplus.wallet.lib.data.ConstantIndex.CredentialRepresentation.*
@@ -25,32 +22,25 @@ import at.asitplus.wallet.lib.data.ConstantIndex.supportsVcJwt
import at.asitplus.wallet.lib.data.VcDataModelConstants.VERIFIABLE_CREDENTIAL
import at.asitplus.wallet.lib.iso.sha256
import at.asitplus.wallet.lib.jws.DefaultJwsService
+import at.asitplus.wallet.lib.jws.JwsContentTypeConstants
import at.asitplus.wallet.lib.jws.JwsService
-import at.asitplus.wallet.lib.oidc.AuthenticationRequestParameters
+import at.asitplus.wallet.lib.oauth2.OAuth2Client
import at.asitplus.wallet.lib.oidc.OidcSiopVerifier.AuthnResponseResult
-import at.asitplus.wallet.lib.oidc.OpenIdConstants
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.CODE_CHALLENGE_METHOD_SHA256
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.CREDENTIAL_TYPE_OPENID
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.Errors
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.GRANT_TYPE_AUTHORIZATION_CODE
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.GRANT_TYPE_CODE
-import at.asitplus.wallet.lib.oidc.OpenIdConstants.GRANT_TYPE_PRE_AUTHORIZED_CODE
import at.asitplus.wallet.lib.oidc.RemoteResourceRetrieverFunction
-import at.asitplus.wallet.lib.oidvci.mdl.RequestedCredentialClaimSpecification
import com.benasher44.uuid.uuid4
import io.github.aakira.napier.Napier
import io.ktor.http.*
import io.ktor.util.*
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlin.random.Random
/**
- * Client service to retrieve credentials using
- * [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html).
- * Implemented from Draft `openid-4-verifiable-credential-issuance-1_0-11`, 2023-02-03.
+ * Client service to retrieve credentials using OID4VCI
+ *
+ * Implemented from
+ * [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
+ * , Draft 14, 2024-08-21.
*/
class WalletService(
/**
@@ -66,15 +56,11 @@ class WalletService(
* Used to prove possession of the key material to create [CredentialRequestProof],
* i.e. the holder key.
*/
- private val cryptoService: CryptoService = DefaultCryptoService(RandomKeyPairAdapter()),
+ private val cryptoService: CryptoService = DefaultCryptoService(EphemeralKeyWithoutCert()),
/**
* Used to prove possession of the key material to create [CredentialRequestProof].
*/
private val jwsService: JwsService = DefaultJwsService(cryptoService),
- /**
- * Used to prove possession of the key material to create [CredentialRequestProof].
- */
- private val coseService: CoseService = DefaultCoseService(cryptoService),
/**
* Need to implement if resources are defined by reference, i.e. the URL for a [JsonWebKeySet],
* or the authentication request itself as `request_uri`, or `presentation_definition_uri`.
@@ -82,38 +68,24 @@ class WalletService(
* or the HTTP header `Location`, i.e. if the server sends the request object as a redirect.
*/
private val remoteResourceRetriever: RemoteResourceRetrieverFunction = { null },
+ private val stateToCodeStore: MapStore = DefaultMapStore(),
) {
- private val stateToCodeChallengeMap = mutableMapOf()
- private val codeChallengeMutex = Mutex()
-
- companion object {
- fun newDefaultInstance(
- clientId: String,
- redirectUrl: String,
- keyPairAdapter: KeyPairAdapter,
- remoteResourceRetriever: RemoteResourceRetrieverFunction = { null },
- ): WalletService = WalletService(
- clientId = clientId,
- redirectUrl = redirectUrl,
- cryptoService = DefaultCryptoService(keyPairAdapter),
- remoteResourceRetriever = remoteResourceRetriever,
- )
+ val oauth2Client: OAuth2Client = OAuth2Client(clientId, redirectUrl)
- fun newDefaultInstance(
- clientId: String,
- redirectUrl: String,
- cryptoService: CryptoService,
- remoteResourceRetriever: RemoteResourceRetrieverFunction = { null },
- ): WalletService = WalletService(
- clientId = clientId,
- redirectUrl = redirectUrl,
- cryptoService = cryptoService,
- jwsService = DefaultJwsService(cryptoService),
- coseService = DefaultCoseService(cryptoService),
- remoteResourceRetriever = remoteResourceRetriever,
- )
- }
+ constructor(
+ clientId: String = "https://wallet.a-sit.at/app",
+ redirectUrl: String = "$clientId/callback",
+ keyMaterial: KeyMaterial,
+ remoteResourceRetriever: RemoteResourceRetrieverFunction = { null },
+ stateToCodeStore: MapStore = DefaultMapStore(),
+ ) : this(
+ clientId = clientId,
+ redirectUrl = redirectUrl,
+ cryptoService = DefaultCryptoService(keyMaterial),
+ remoteResourceRetriever = remoteResourceRetriever,
+ stateToCodeStore = stateToCodeStore
+ )
data class RequestOptions(
/**
@@ -160,226 +132,43 @@ class WalletService(
}
/**
- * Send the result as parameters (either POST or GET) to the server at `/authorize` (or more specific
- * [IssuerMetadata.authorizationEndpointUrl]).
+ * Build authorization details for use in [OAuth2Client.createAuthRequest].
*
- * Sample ktor code:
- * ```
- * val credentialConfig = issuerMetadata.supportedCredentialConfigurations!!
- * .entries.first { it.key == credentialOffer.configurationIds.first() }.toPair()
- * val authnRequest = client.createAuthRequest(
- * state = state,
- * credential = credentialConfig,
- * credentialIssuer = issuerMetadata.credentialIssuer,
- * authorizationServers = issuerMetadata.authorizationServers
- * )
- * val authnResponse = httpClient.get(issuerMetadata.authorizationEndpointUrl!!) {
- * url {
- * authnRequest.encodeToParameters().forEach { parameters.append(it.key, it.value) }
- * }
- * }
- * val authn = AuthenticationResponseParameters.deserialize(authnResponse.bodyAsText()).getOrThrow()
- * ```
*
- * @param credential which credential from [IssuerMetadata.supportedCredentialConfigurations] to request
- * @param credentialIssuer from [IssuerMetadata.credentialIssuer]
+ * @param credentialConfigurationId which credential (the key) from
+ * [IssuerMetadata.supportedCredentialConfigurations] to request
* @param authorizationServers from [IssuerMetadata.authorizationServers]
*/
- suspend fun createAuthRequest(
- state: String,
- credential: Pair,
- credentialIssuer: String? = null,
+ suspend fun buildAuthorizationDetails(
+ credentialConfigurationId: String,
authorizationServers: Set? = null,
- ) = AuthenticationRequestParameters(
- responseType = GRANT_TYPE_CODE,
- state = state,
- clientId = clientId,
- authorizationDetails = setOf(credential.toAuthnDetails(authorizationServers)),
- scope = credential.first,
- resource = credentialIssuer,
- redirectUrl = redirectUrl,
- codeChallenge = generateCodeVerifier(state),
- codeChallengeMethod = CODE_CHALLENGE_METHOD_SHA256,
+ ) = setOf(
+ AuthorizationDetails.OpenIdCredential(
+ credentialConfigurationId = credentialConfigurationId,
+ locations = authorizationServers,
+ // TODO Test in real-world settings, is this correct?
+ credentialIdentifiers = setOf(credentialConfigurationId)
+ )
)
/**
- * Send the result as parameters (either POST or GET) to the server at `/authorize` (or more specific
- * [IssuerMetadata.authorizationEndpointUrl]).
- *
- * Sample ktor code:
- * ```
- * val authnRequest = client.createAuthRequest(
- * requestOptions = requestOptions,
- * credentialIssuer = issuerMetadata.credentialIssuer,
- * )
- * val authnResponse = httpClient.get(issuerMetadata.authorizationEndpointUrl!!) {
- * url {
- * authnRequest.encodeToParameters().forEach { parameters.append(it.key, it.value) }
- * }
- * }
- * val authn = AuthenticationResponseParameters.deserialize(authnResponse.bodyAsText()).getOrThrow()
- * ```
- *
- * @param requestOptions which credential in which representation to request
- * @param credentialIssuer from [IssuerMetadata.credentialIssuer]
+ * Build authorization details for use in [OAuth2Client.createAuthRequest].
*/
- suspend fun createAuthRequest(
- requestOptions: RequestOptions,
- credentialIssuer: String? = null,
- ) = AuthenticationRequestParameters(
- responseType = GRANT_TYPE_CODE,
- state = requestOptions.state,
- clientId = clientId,
- authorizationDetails = requestOptions.toAuthnDetails()?.let { setOf(it) },
- resource = credentialIssuer,
- redirectUrl = redirectUrl,
- codeChallenge = generateCodeVerifier(requestOptions.state),
- codeChallengeMethod = CODE_CHALLENGE_METHOD_SHA256,
- )
-
- @OptIn(ExperimentalStdlibApi::class)
- private suspend fun generateCodeVerifier(state: String): String {
- val codeVerifier = Random.nextBytes(32).toHexString(HexFormat.Default)
- codeChallengeMutex.withLock { stateToCodeChallengeMap.put(state, codeVerifier) }
- return codeVerifier.encodeToByteArray().sha256().encodeToString(Base64UrlStrict)
- }
+ suspend fun buildAuthorizationDetails(
+ requestOptions: RequestOptions
+ ) = setOfNotNull(requestOptions.toAuthnDetails())
- sealed class AuthorizationForToken {
+ sealed class CredentialRequestInput {
/**
- * Authorization code from an actual OAuth2 Authorization Server, or [SimpleAuthorizationService.authorize]
+ * @param id from the token response, see [TokenResponseParameters.authorizationDetails]
+ * and [AuthorizationDetails.OpenIdCredential.credentialConfigurationId]
*/
- data class Code(val code: String) : AuthorizationForToken()
-
- /**
- * Pre-auth code from [CredentialOfferGrants.preAuthorizedCode] in [CredentialOffer.grants]
- */
- data class PreAuthCode(val preAuth: CredentialOfferGrantsPreAuthCode) : AuthorizationForToken()
- }
-
- /**
- * Request token with an authorization code, e.g. from [createAuthRequest], or pre-auth code.
- *
- * Send the result as POST parameters (form-encoded) to the server at `/token` (or more specific
- * [IssuerMetadata.tokenEndpointUrl]).
- *
- * Sample ktor code for authorization code:
- * ```
- * val authnRequest = client.createAuthRequest(requestOptions)
- * val authnResponse = authorizationService.authorize(authnRequest).getOrThrow()
- * val code = authnResponse.params.code
- * val tokenRequest = client.createTokenRequestParameters(requestOptions, code = code)
- * val tokenResponse = httpClient.submitForm(
- * url = issuerMetadata.tokenEndpointUrl!!,
- * formParameters = parameters {
- * tokenRequest.encodeToParameters().forEach { append(it.key, it.value) }
- * }
- * )
- * val token = TokenResponseParameters.deserialize(tokenResponse.bodyAsText()).getOrThrow()
- * ```
- *
- * Sample ktor code for pre-authn code:
- * ```
- * val tokenRequest =
- * client.createTokenRequestParameters(requestOptions, credentialOffer.grants!!.preAuthorizedCode)
- * val tokenResponse = httpClient.submitForm(
- * url = issuerMetadata.tokenEndpointUrl!!,
- * formParameters = parameters {
- * tokenRequest.encodeToParameters().forEach { append(it.key, it.value) }
- * }
- * )
- * val token = TokenResponseParameters.deserialize(tokenResponse.bodyAsText()).getOrThrow()
- * ```
- *
- * @param requestOptions which credential in which representation to request
- * @param authorization for the token endpoint
- */
- suspend fun createTokenRequestParameters(
- requestOptions: RequestOptions,
- authorization: AuthorizationForToken,
- ) = when (authorization) {
- is AuthorizationForToken.Code -> TokenRequestParameters(
- grantType = GRANT_TYPE_AUTHORIZATION_CODE,
- code = authorization.code,
- redirectUrl = redirectUrl,
- clientId = clientId,
- authorizationDetails = requestOptions.toAuthnDetails()?.let { setOf(it) },
- codeVerifier = codeChallengeMutex.withLock { stateToCodeChallengeMap.remove(requestOptions.state) }
- )
-
- is AuthorizationForToken.PreAuthCode -> TokenRequestParameters(
- grantType = GRANT_TYPE_PRE_AUTHORIZED_CODE,
- redirectUrl = redirectUrl,
- clientId = clientId,
- authorizationDetails = (requestOptions.toAuthnDetails())?.let { setOf(it) },
- transactionCode = authorization.preAuth.transactionCode,
- preAuthorizedCode = authorization.preAuth.preAuthorizedCode,
- codeVerifier = codeChallengeMutex.withLock { stateToCodeChallengeMap.remove(requestOptions.state) }
- )
- }
-
- /**
- * Request token with an authorization code, e.g. from [createAuthRequest], or pre-auth code.
- *
- * Send the result as POST parameters (form-encoded) to the server at `/token` (or more specific
- * [IssuerMetadata.tokenEndpointUrl]).
- *
- * Sample ktor code for authorization code:
- * ```
- * val authnRequest = client.createAuthRequest(requestOptions)
- * val authnResponse = authorizationService.authorize(authnRequest).getOrThrow()
- * val code = authnResponse.params.code
- * val tokenRequest = client.createTokenRequestParameters(requestOptions, code = code)
- * val tokenResponse = httpClient.submitForm(
- * url = issuerMetadata.tokenEndpointUrl!!,
- * formParameters = parameters {
- * tokenRequest.encodeToParameters().forEach { append(it.key, it.value) }
- * }
- * )
- * val token = TokenResponseParameters.deserialize(tokenResponse.bodyAsText()).getOrThrow()
- * ```
- *
- * Sample ktor code for pre-authn code:
- * ```
- * val tokenRequest =
- * client.createTokenRequestParameters(requestOptions, credentialOffer.grants!!.preAuthorizedCode)
- * val tokenResponse = httpClient.submitForm(
- * url = issuerMetadata.tokenEndpointUrl!!,
- * formParameters = parameters {
- * tokenRequest.encodeToParameters().forEach { append(it.key, it.value) }
- * }
- * )
- * val token = TokenResponseParameters.deserialize(tokenResponse.bodyAsText()).getOrThrow()
- * ```
- *
- * @param credential which credential from [IssuerMetadata.supportedCredentialConfigurations] to request
- * @param requestedAttributes attributes that shall be requested explicitly (selective disclosure)
- * @param state used in [createAuthRequest], e.g. when using authorization codes
- * @param authorization for the token endpoint
- */
- suspend fun createTokenRequestParameters(
- credential: SupportedCredentialFormat,
- requestedAttributes: Set? = null,
- state: String? = null,
- authorization: AuthorizationForToken,
- ) = when (authorization) {
- is AuthorizationForToken.Code -> TokenRequestParameters(
- grantType = GRANT_TYPE_AUTHORIZATION_CODE,
- code = authorization.code,
- redirectUrl = redirectUrl,
- clientId = clientId,
- authorizationDetails = setOf(credential.toAuthnDetails(requestedAttributes)),
- codeVerifier = state?.let { codeChallengeMutex.withLock { stateToCodeChallengeMap.remove(it) } }
- )
-
- is AuthorizationForToken.PreAuthCode -> TokenRequestParameters(
- grantType = GRANT_TYPE_PRE_AUTHORIZED_CODE,
- redirectUrl = redirectUrl,
- clientId = clientId,
- authorizationDetails = setOf(credential.toAuthnDetails(requestedAttributes)),
- transactionCode = authorization.preAuth.transactionCode,
- preAuthorizedCode = authorization.preAuth.preAuthorizedCode,
- codeVerifier = state?.let { codeChallengeMutex.withLock { stateToCodeChallengeMap.remove(it) } }
- )
+ data class CredentialIdentifier(val id: String) : CredentialRequestInput()
+ data class RequestOptions(val requestOptions: WalletService.RequestOptions) : CredentialRequestInput()
+ data class Format(
+ val supportedCredentialFormat: SupportedCredentialFormat,
+ val requestedAttributes: Set? = null
+ ) : CredentialRequestInput()
}
/**
@@ -388,58 +177,16 @@ class WalletService(
*
* Also send along the [TokenResponseParameters.accessToken] from the token response in HTTP header `Authorization`
* as value `Bearer accessTokenValue` (depending on the [TokenResponseParameters.tokenType]).
- * See [createTokenRequestParameters].
*
- * Sample ktor code:
- * ```
- * val credentialRequest = client.createCredentialRequestJwt(
- * requestOptions = requestOptions,
- * clientNonce = token.clientNonce,
- * credentialIssuer = issuerMetadata.credentialIssuer
- * ).getOrThrow()
- *
- * val credentialResponse = httpClient.post(issuerMetadata.credentialEndpointUrl) {
- * setBody(credentialRequest)
- * headers {
- * append(HttpHeaders.Authorization, "Bearer ${token.accessToken}")
- * }
- * }
- * ```
- *
- * @param credential which credential from [IssuerMetadata.supportedCredentialConfigurations] to request
- * @param requestedAttributes attributes that shall be requested explicitly (selective disclosure)
- * @param clientNonce `c_nonce` from the token response, optional string, see [TokenResponseParameters.clientNonce]
- * @param credentialIssuer `credential_issuer` from the metadata, see [IssuerMetadata.credentialIssuer]
- */
- suspend fun createCredentialRequest(
- credential: SupportedCredentialFormat,
- requestedAttributes: Set? = null,
- clientNonce: String?,
- credentialIssuer: String?,
- ): KmmResult = catching {
- val cwtProofType = OpenIdConstants.ProofType.CWT.stringRepresentation
- val isCwt = credential.supportedProofTypes?.containsKey(cwtProofType) == true
- || credential.format == CredentialFormatEnum.MSO_MDOC
- val proof = if (isCwt) {
- createCredentialRequestCwt(null, clientNonce, credentialIssuer)
- } else {
- createCredentialRequestJwt(null, clientNonce, credentialIssuer)
- }
- credential.toCredentialRequestParameters(requestedAttributes, proof)
- .also { Napier.i("createCredentialRequest returns $it") }
- }
-
- /**
- * Send the result as JSON-serialized content to the server at `/credential` (or more specific
- * [IssuerMetadata.credentialEndpointUrl]).
+ * Be sure to include a DPoP header if [TokenResponseParameters.tokenType] is `DPoP`,
+ * see [JwsService.buildDPoPHeader].
*
- * Also send along the [TokenResponseParameters.accessToken] from the token response in HTTP header `Authorization`
- * as value `Bearer accessTokenValue` (depending on the [TokenResponseParameters.tokenType]).
- * See [createTokenRequestParameters].
+ * See [OAuth2Client.createTokenRequestParameters].
*
* Sample ktor code:
* ```
- * val credentialRequest = client.createCredentialRequestJwt(
+ * val token = ...
+ * val credentialRequest = client.createCredentialRequest(
* requestOptions = requestOptions,
* clientNonce = token.clientNonce,
* credentialIssuer = issuerMetadata.credentialIssuer
@@ -453,39 +200,47 @@ class WalletService(
* }
* ```
*
- * @param requestOptions which credential in which representation to request
+ * @param input which credential to request, see subclasses of [CredentialRequestInput]
* @param clientNonce `c_nonce` from the token response, optional string, see [TokenResponseParameters.clientNonce]
* @param credentialIssuer `credential_issuer` from the metadata, see [IssuerMetadata.credentialIssuer]
*/
suspend fun createCredentialRequest(
- requestOptions: RequestOptions,
+ input: CredentialRequestInput,
clientNonce: String?,
credentialIssuer: String?,
): KmmResult = catching {
- val proof = if (requestOptions.representation == ISO_MDOC) {
- createCredentialRequestCwt(requestOptions, clientNonce, credentialIssuer)
- } else {
- createCredentialRequestJwt(requestOptions, clientNonce, credentialIssuer)
- }
- requestOptions.toCredentialRequestParameters(proof)
- .also { Napier.i("createCredentialRequest returns $it") }
+ val clock = (input as? CredentialRequestInput.RequestOptions)?.requestOptions?.clock ?: Clock.System
+ when (input) {
+ is CredentialRequestInput.CredentialIdentifier ->
+ CredentialRequestParameters(credentialIdentifier = input.id)
+
+ is CredentialRequestInput.Format ->
+ input.supportedCredentialFormat.toCredentialRequestParameters(input.requestedAttributes)
+
+ is CredentialRequestInput.RequestOptions -> with(input.requestOptions) {
+ credentialScheme.toCredentialRequestParameters(representation, requestedAttributes)
+ }
+ }.copy(
+ proof = createCredentialRequestProof(clientNonce, credentialIssuer, clock)
+ ).also { Napier.i("createCredentialRequest returns $it") }
}
- private suspend fun createCredentialRequestJwt(
- requestOptions: RequestOptions?,
+
+ internal suspend fun createCredentialRequestProof(
clientNonce: String?,
credentialIssuer: String?,
+ clock: Clock = Clock.System,
): CredentialRequestProof = CredentialRequestProof(
proofType = OpenIdConstants.ProofType.JWT,
jwt = jwsService.createSignedJwsAddingParams(
header = JwsHeader(
- algorithm = cryptoService.keyPairAdapter.signingAlgorithm.toJwsAlgorithm().getOrThrow(),
- type = OpenIdConstants.ProofType.JWT_HEADER_TYPE.stringRepresentation,
+ algorithm = cryptoService.keyMaterial.signatureAlgorithm.toJwsAlgorithm().getOrThrow(),
+ type = OpenIdConstants.PROOF_JWT_TYPE
),
payload = JsonWebToken(
issuer = clientId, // omit when token was pre-authn?
audience = credentialIssuer,
- issuedAt = requestOptions?.clock?.now() ?: Clock.System.now(),
+ issuedAt = clock.now(),
nonce = clientNonce,
).serialize().encodeToByteArray(),
addKeyId = false,
@@ -494,61 +249,12 @@ class WalletService(
).getOrThrow().serialize()
)
- private suspend fun createCredentialRequestCwt(
- requestOptions: RequestOptions?,
- clientNonce: String?,
- credentialIssuer: String?,
- ) = CredentialRequestProof(
- proofType = OpenIdConstants.ProofType.CWT,
- cwt = coseService.createSignedCose(
- protectedHeader = CoseHeader(
- algorithm = cryptoService.keyPairAdapter.signingAlgorithm.toCoseAlgorithm().getOrThrow(),
- contentType = OpenIdConstants.ProofType.CWT_HEADER_TYPE.stringRepresentation,
- certificateChain = cryptoService.keyPairAdapter.certificate?.encodeToDerOrNull()
- ),
- payload = CborWebToken(
- issuer = clientId, // omit when token was pre-authn?
- audience = credentialIssuer,
- issuedAt = requestOptions?.clock?.now() ?: Clock.System.now(),
- nonce = clientNonce?.encodeToByteArray(),
- ).serialize(),
- addKeyId = false,
- ).getOrThrow().serialize().encodeToString(Base64UrlStrict),
- )
-
- private fun RequestOptions.toCredentialRequestParameters(proof: CredentialRequestProof) =
- representation.toCredentialRequestParameters(credentialScheme, requestedAttributes, proof)
-
- private fun SupportedCredentialFormat.toAuthnDetails(requestedAttributes: Set?) = when (this.format) {
- CredentialFormatEnum.JWT_VC -> AuthorizationDetails(
- type = CREDENTIAL_TYPE_OPENID,
- format = format,
- credentialDefinition = credentialDefinition
- )
-
- CredentialFormatEnum.VC_SD_JWT -> AuthorizationDetails(
- type = CREDENTIAL_TYPE_OPENID,
- format = format,
- sdJwtVcType = sdJwtVcType,
- claims = requestedAttributes?.toRequestedClaimsSdJwt(sdJwtVcType!!),
- )
-
- CredentialFormatEnum.MSO_MDOC -> AuthorizationDetails(
- type = CREDENTIAL_TYPE_OPENID,
- format = format,
- docType = docType,
- claims = requestedAttributes?.toRequestedClaimsIso(isoClaims?.keys?.firstOrNull() ?: docType!!)
- )
-
- else -> throw IllegalArgumentException("Credential format $format not supported for AuthorizationDetails")
- }
-
private fun RequestOptions.toAuthnDetails() =
representation.toAuthorizationDetails(credentialScheme, requestedAttributes)
private fun CredentialRepresentation.toAuthorizationDetails(
scheme: ConstantIndex.CredentialScheme,
- requestedAttributes: Set?
+ requestedAttributes: Set?,
) = when (this) {
PLAIN_JWT -> scheme.toJwtAuthn(toFormat())
SD_JWT -> scheme.toSdJwtAuthn(toFormat(), requestedAttributes)
@@ -556,22 +262,20 @@ class WalletService(
}
private fun ConstantIndex.CredentialScheme.toJwtAuthn(
- format: CredentialFormatEnum
+ format: CredentialFormatEnum,
) = if (supportsVcJwt)
- AuthorizationDetails(
- type = CREDENTIAL_TYPE_OPENID,
+ AuthorizationDetails.OpenIdCredential(
format = format,
credentialDefinition = SupportedCredentialFormatDefinition(
- types = listOf(VERIFIABLE_CREDENTIAL, vcType!!),
+ types = setOf(VERIFIABLE_CREDENTIAL, vcType!!),
),
) else null
private fun ConstantIndex.CredentialScheme.toSdJwtAuthn(
format: CredentialFormatEnum,
- requestedAttributes: Set?
+ requestedAttributes: Set?,
) = if (supportsSdJwt)
- AuthorizationDetails(
- type = CREDENTIAL_TYPE_OPENID,
+ AuthorizationDetails.OpenIdCredential(
format = format,
sdJwtVcType = sdJwtType!!,
claims = requestedAttributes?.toRequestedClaimsSdJwt(sdJwtType!!),
@@ -579,84 +283,64 @@ class WalletService(
private fun ConstantIndex.CredentialScheme.toIsoAuthn(
format: CredentialFormatEnum,
- requestedAttributes: Set?
+ requestedAttributes: Set?,
) = if (supportsIso)
- AuthorizationDetails(
- type = CREDENTIAL_TYPE_OPENID,
+ AuthorizationDetails.OpenIdCredential(
format = format,
docType = isoDocType,
claims = requestedAttributes?.toRequestedClaimsIso(isoNamespace!!)
) else null
- private fun CredentialRepresentation.toCredentialRequestParameters(
- credentialScheme: ConstantIndex.CredentialScheme,
+ private fun ConstantIndex.CredentialScheme.toCredentialRequestParameters(
+ credentialRepresentation: CredentialRepresentation,
requestedAttributes: Set?,
- proof: CredentialRequestProof
) = when {
- this == PLAIN_JWT && credentialScheme.supportsVcJwt -> CredentialRequestParameters(
- format = toFormat(),
+ credentialRepresentation == PLAIN_JWT && supportsVcJwt -> CredentialRequestParameters(
+ format = CredentialFormatEnum.JWT_VC,
credentialDefinition = SupportedCredentialFormatDefinition(
- types = listOf(VERIFIABLE_CREDENTIAL) + credentialScheme.vcType!!,
+ types = setOf(VERIFIABLE_CREDENTIAL, vcType!!),
),
- proof = proof
)
- this == SD_JWT && credentialScheme.supportsSdJwt -> CredentialRequestParameters(
- format = toFormat(),
- sdJwtVcType = credentialScheme.sdJwtType!!,
- claims = requestedAttributes?.toRequestedClaimsSdJwt(credentialScheme.sdJwtType!!),
- proof = proof
+ credentialRepresentation == SD_JWT && supportsSdJwt -> CredentialRequestParameters(
+ format = CredentialFormatEnum.VC_SD_JWT,
+ sdJwtVcType = sdJwtType!!,
+ claims = requestedAttributes?.toRequestedClaimsSdJwt(sdJwtType!!),
)
- this == ISO_MDOC && credentialScheme.supportsIso -> CredentialRequestParameters(
- format = toFormat(),
- docType = credentialScheme.isoDocType,
- claims = requestedAttributes?.toRequestedClaimsIso(credentialScheme.isoNamespace!!),
- proof = proof
+ credentialRepresentation == ISO_MDOC && supportsIso -> CredentialRequestParameters(
+ format = CredentialFormatEnum.MSO_MDOC,
+ docType = isoDocType,
+ claims = requestedAttributes?.toRequestedClaimsIso(isoNamespace!!),
)
- else -> throw IllegalArgumentException("format $this not applicable to $credentialScheme")
+ else -> throw IllegalArgumentException("format $credentialRepresentation not applicable to $this")
}
private fun SupportedCredentialFormat.toCredentialRequestParameters(
requestedAttributes: Set?,
- proof: CredentialRequestProof
) = when (format) {
CredentialFormatEnum.JWT_VC -> CredentialRequestParameters(
format = format,
credentialDefinition = credentialDefinition,
- proof = proof
)
CredentialFormatEnum.VC_SD_JWT -> CredentialRequestParameters(
format = format,
sdJwtVcType = sdJwtVcType,
claims = requestedAttributes?.toRequestedClaimsSdJwt(sdJwtVcType!!),
- proof = proof
)
CredentialFormatEnum.MSO_MDOC -> CredentialRequestParameters(
format = format,
docType = docType,
claims = requestedAttributes?.toRequestedClaimsIso(isoClaims?.keys?.firstOrNull() ?: docType!!),
- proof = proof
)
else -> throw IllegalArgumentException("format $format not applicable to create credential request")
}
}
-private fun Pair.toAuthnDetails(authorizationServers: Set?)
- : AuthorizationDetails = AuthorizationDetails(
- type = "openid_credential",
- credentialConfigurationId = first,
- format = second.format,
- docType = second.docType,
- sdJwtVcType = second.sdJwtVcType,
- credentialDefinition = second.credentialDefinition,
- locations = authorizationServers
-)
-
private fun Collection.toRequestedClaimsSdJwt(sdJwtType: String) =
mapOf(sdJwtType to this.associateWith { RequestedCredentialClaimSpecification() })
@@ -669,3 +353,28 @@ private fun CredentialRepresentation.toFormat() = when (this) {
SD_JWT -> CredentialFormatEnum.VC_SD_JWT
ISO_MDOC -> CredentialFormatEnum.MSO_MDOC
}
+
+/**
+ * To be set as header `DPoP` in making request to [url],
+ * see [RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449)
+ */
+suspend fun JwsService.buildDPoPHeader(
+ url: String,
+ httpMethod: String = "POST",
+ accessToken: String? = null
+) = createSignedJwsAddingParams(
+ header = JwsHeader(
+ algorithm = algorithm,
+ type = JwsContentTypeConstants.DPOP_JWT
+ ),
+ payload = JsonWebToken(
+ jwtId = Random.nextBytes(12).encodeToString(Base64UrlStrict),
+ httpMethod = httpMethod,
+ httpTargetUrl = url,
+ accessTokenHash = accessToken?.encodeToByteArray()?.sha256()?.encodeToString(Base64UrlStrict),
+ issuedAt = Clock.System.now(),
+ ).serialize().encodeToByteArray(),
+ addKeyId = false,
+ addJsonWebKey = true,
+ addX5c = false,
+).getOrThrow().serialize()
diff --git a/vck-openid/src/commonTest/kotlin/KotestConfig.kt b/vck-openid/src/commonTest/kotlin/KotestConfig.kt
index c638781e0..0e2972e47 100644
--- a/vck-openid/src/commonTest/kotlin/KotestConfig.kt
+++ b/vck-openid/src/commonTest/kotlin/KotestConfig.kt
@@ -6,7 +6,7 @@ class KotestConfig : AbstractProjectConfig() {
init {
Napier.takeLogarithm()
Napier.base(DebugAntilog())
- at.asitplus.wallet.mdl.Initializer.initWithVcLib()
- at.asitplus.wallet.eupid.Initializer.initWithVcLib()
+ at.asitplus.wallet.mdl.Initializer.initWithVCK()
+ at.asitplus.wallet.eupid.Initializer.initWithVCK()
}
}
\ No newline at end of file
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oauth2/OAuth2ClientTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oauth2/OAuth2ClientTest.kt
new file mode 100644
index 000000000..5eba83369
--- /dev/null
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oauth2/OAuth2ClientTest.kt
@@ -0,0 +1,77 @@
+package at.asitplus.wallet.lib.oauth2
+
+import at.asitplus.openid.OidcUserInfo
+import at.asitplus.openid.OidcUserInfoExtended
+import at.asitplus.wallet.lib.data.ConstantIndex
+import at.asitplus.wallet.lib.oidc.AuthenticationResponseResult
+import at.asitplus.wallet.lib.oidc.DummyOAuth2DataProvider
+import at.asitplus.wallet.lib.oidvci.CredentialAuthorizationServiceStrategy
+import at.asitplus.wallet.mdl.MobileDrivingLicenceScheme
+import com.benasher44.uuid.uuid4
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.nulls.shouldBeNull
+import io.kotest.matchers.nulls.shouldNotBeNull
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.types.shouldBeInstanceOf
+import kotlinx.serialization.json.JsonObject
+
+class OAuth2ClientTest : FunSpec({
+
+ lateinit var server: SimpleAuthorizationService
+ lateinit var client: OAuth2Client
+
+ beforeEach {
+ client = OAuth2Client()
+ server = SimpleAuthorizationService(
+ strategy = CredentialAuthorizationServiceStrategy(
+ DummyOAuth2DataProvider,
+ setOf(ConstantIndex.AtomicAttribute2023, MobileDrivingLicenceScheme)
+ ),
+ )
+ }
+
+ test("process with pre-authorized code") {
+ val user = OidcUserInfoExtended(OidcUserInfo("sub"), JsonObject(mapOf()))
+ val preAuth = server.providePreAuthorizedCode(user)
+ .shouldNotBeNull()
+ val state = uuid4().toString()
+ val tokenRequest = client.createTokenRequestParameters(
+ state = state,
+ authorization = OAuth2Client.AuthorizationForToken.PreAuthCode(preAuth),
+ )
+ val token = server.token(tokenRequest).getOrThrow()
+ token.authorizationDetails.shouldBeNull()
+ }
+
+ test("process with pre-authorized code, can't use it twice") {
+ val user = OidcUserInfoExtended(OidcUserInfo("sub"), JsonObject(mapOf()))
+ val preAuth = server.providePreAuthorizedCode(user)
+ .shouldNotBeNull()
+ val state = uuid4().toString()
+ val tokenRequest = client.createTokenRequestParameters(
+ state = state,
+ authorization = OAuth2Client.AuthorizationForToken.PreAuthCode(preAuth),
+ )
+ server.token(tokenRequest).isSuccess shouldBe true
+ server.token(tokenRequest).isFailure shouldBe true
+ }
+
+ test("process with authorization code flow") {
+ val state = uuid4().toString()
+ val authnRequest = client.createAuthRequest(
+ state = state,
+ )
+ val authnResponse = server.authorize(authnRequest).getOrThrow()
+ .shouldBeInstanceOf()
+ val code = authnResponse.params.code
+ .shouldNotBeNull()
+
+ val tokenRequest = client.createTokenRequestParameters(
+ state = state,
+ authorization = OAuth2Client.AuthorizationForToken.Code(code),
+ )
+ val token = server.token(tokenRequest).getOrThrow()
+ token.authorizationDetails.shouldBeNull()
+ }
+
+})
\ No newline at end of file
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt
index c8933d353..7f877af55 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationRequestParameterFromSerializerTest.kt
@@ -1,7 +1,8 @@
package at.asitplus.wallet.lib.oidc
+import at.asitplus.openid.AuthenticationRequestParameters
+import at.asitplus.wallet.lib.agent.EphemeralKeyWithoutCert
import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
import at.asitplus.wallet.lib.agent.VerifierAgent
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.oidvci.decodeFromUrlQuery
@@ -10,24 +11,21 @@ import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import io.ktor.http.*
-import kotlinx.serialization.encodeToString
class AuthenticationRequestParameterFromSerializerTest : FreeSpec({
val relyingPartyUrl = "https://example.com/rp/${uuid4()}"
val walletUrl = "https://example.com/wallet/${uuid4()}"
- val responseUrl = "https://example.com/rp/${uuid4()}"
- val holderKeyPair = RandomKeyPairAdapter()
- val oidcSiopWallet = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
- holder = HolderAgent(holderKeyPair),
+ val holderKeyMaterial = EphemeralKeyWithoutCert()
+ val oidcSiopWallet = OidcSiopWallet(
+ keyMaterial = holderKeyMaterial,
+ holder = HolderAgent(holderKeyMaterial),
)
- val verifierSiop = OidcSiopVerifier.newInstance(
- verifier = VerifierAgent(RandomKeyPairAdapter()),
+ val verifierSiop = OidcSiopVerifier(
+ verifier = VerifierAgent(EphemeralKeyWithoutCert()),
relyingPartyUrl = relyingPartyUrl,
- responseUrl = responseUrl,
)
val representations = listOf(
@@ -39,8 +37,11 @@ class AuthenticationRequestParameterFromSerializerTest : FreeSpec({
representations.forEach { representation ->
val reqOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- representation = representation,
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023, representation
+ )
+ )
)
"URL test $representation" {
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/CredentialJsonInteropTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/CredentialJsonInteropTest.kt
index 8f1d7c821..754011ead 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/CredentialJsonInteropTest.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/CredentialJsonInteropTest.kt
@@ -1,15 +1,7 @@
package at.asitplus.wallet.lib.oidc
import at.asitplus.jsonpath.JsonPath
-import at.asitplus.wallet.lib.agent.Holder
-import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.InMemorySubjectCredentialStore
-import at.asitplus.wallet.lib.agent.Issuer
-import at.asitplus.wallet.lib.agent.IssuerAgent
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.agent.SubjectCredentialStore
-import at.asitplus.wallet.lib.agent.toStoreCredentialInput
+import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.CredentialToJsonConverter
import io.kotest.core.spec.style.FreeSpec
@@ -17,26 +9,24 @@ import io.kotest.matchers.shouldNotBe
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
-@Suppress("unused")
class CredentialJsonInteropTest : FreeSpec({
- lateinit var holderKeyPair: KeyPairAdapter
-
+ lateinit var holderKeyMaterial: KeyMaterial
lateinit var issuerAgent: Issuer
lateinit var subjectCredentialStore: SubjectCredentialStore
lateinit var holderAgent: Holder
beforeEach {
- holderKeyPair = RandomKeyPairAdapter()
+ holderKeyMaterial = EphemeralKeyWithoutCert()
subjectCredentialStore = InMemorySubjectCredentialStore()
- holderAgent = HolderAgent(holderKeyPair, subjectCredentialStore)
- issuerAgent = IssuerAgent(RandomKeyPairAdapter(), DummyCredentialDataProvider())
+ holderAgent = HolderAgent(holderKeyMaterial, subjectCredentialStore)
+ issuerAgent = IssuerAgent(EphemeralKeyWithSelfSignedCert(), DummyCredentialDataProvider())
}
"Plain jwt credential path resolving" {
holderAgent.storeCredential(
issuerAgent.issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.PLAIN_JWT
).getOrThrow().toStoreCredentialInput()
@@ -56,7 +46,7 @@ class CredentialJsonInteropTest : FreeSpec({
"SD jwt credential path resolving" {
holderAgent.storeCredential(
issuerAgent.issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.SD_JWT,
ConstantIndex.AtomicAttribute2023.claimNames
@@ -73,7 +63,7 @@ class CredentialJsonInteropTest : FreeSpec({
"ISO credential path resolving" {
holderAgent.storeCredential(
issuerAgent.issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.ISO_MDOC,
ConstantIndex.AtomicAttribute2023.claimNames
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt
index bdfde5df7..4234443f3 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyCredentialDataProvider.kt
@@ -10,6 +10,10 @@ import at.asitplus.wallet.lib.agent.CredentialToBeIssued
import at.asitplus.wallet.lib.agent.IssuerCredentialDataProvider
import at.asitplus.wallet.lib.data.AtomicAttribute2023
import at.asitplus.wallet.lib.data.ConstantIndex
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_DATE_OF_BIRTH
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_FAMILY_NAME
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_GIVEN_NAME
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_PORTRAIT
import at.asitplus.wallet.lib.iso.IssuerSignedItem
import at.asitplus.wallet.mdl.DrivingPrivilege
import at.asitplus.wallet.mdl.DrivingPrivilegeCode
@@ -48,9 +52,10 @@ class DummyCredentialDataProvider(
if (credentialScheme == ConstantIndex.AtomicAttribute2023) {
val subjectId = subjectPublicKey.didEncoded
val claims = listOfNotNull(
- optionalClaim(claimNames, "given_name", "Susanne"),
- optionalClaim(claimNames, "family_name", "Meier"),
- optionalClaim(claimNames, "date_of_birth", "1990-01-01"),
+ optionalClaim(claimNames, CLAIM_GIVEN_NAME, "Susanne"),
+ optionalClaim(claimNames, CLAIM_FAMILY_NAME, "Meier"),
+ optionalClaim(claimNames, CLAIM_DATE_OF_BIRTH, LocalDate.parse("1990-01-01")),
+ optionalClaim(claimNames, CLAIM_PORTRAIT, Random.nextBytes(32)),
)
when (representation) {
ConstantIndex.CredentialRepresentation.SD_JWT -> CredentialToBeIssued.VcSd(
@@ -59,7 +64,7 @@ class DummyCredentialDataProvider(
)
ConstantIndex.CredentialRepresentation.PLAIN_JWT -> CredentialToBeIssued.VcJwt(
- subject = AtomicAttribute2023(subjectId, "given_name", "Susanne"),
+ subject = AtomicAttribute2023(subjectId, CLAIM_GIVEN_NAME, "Susanne"),
expiration = expiration,
)
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyOAuth2IssuerCredentialDataProvider.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyOAuth2IssuerCredentialDataProvider.kt
index 5308cfa71..d4bd0ec68 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyOAuth2IssuerCredentialDataProvider.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/DummyOAuth2IssuerCredentialDataProvider.kt
@@ -2,6 +2,9 @@ package at.asitplus.wallet.lib.oidc
import at.asitplus.KmmResult
import at.asitplus.catching
+import at.asitplus.openid.AuthenticationRequestParameters
+import at.asitplus.openid.OidcUserInfo
+import at.asitplus.openid.OidcUserInfoExtended
import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.wallet.eupid.EuPidCredential
import at.asitplus.wallet.eupid.EuPidScheme
@@ -10,21 +13,27 @@ import at.asitplus.wallet.lib.agent.CredentialToBeIssued
import at.asitplus.wallet.lib.agent.IssuerCredentialDataProvider
import at.asitplus.wallet.lib.data.AtomicAttribute2023
import at.asitplus.wallet.lib.data.ConstantIndex
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_DATE_OF_BIRTH
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_FAMILY_NAME
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_GIVEN_NAME
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_PORTRAIT
import at.asitplus.wallet.lib.iso.IssuerSignedItem
import at.asitplus.wallet.lib.oidvci.OAuth2DataProvider
-import at.asitplus.wallet.lib.oidvci.OidcUserInfo
-import at.asitplus.wallet.lib.oidvci.OidcUserInfoExtended
import at.asitplus.wallet.mdl.MobileDrivingLicenceDataElements.DOCUMENT_NUMBER
import at.asitplus.wallet.mdl.MobileDrivingLicenceDataElements.EXPIRY_DATE
import at.asitplus.wallet.mdl.MobileDrivingLicenceDataElements.FAMILY_NAME
import at.asitplus.wallet.mdl.MobileDrivingLicenceDataElements.GIVEN_NAME
import at.asitplus.wallet.mdl.MobileDrivingLicenceDataElements.ISSUE_DATE
import at.asitplus.wallet.mdl.MobileDrivingLicenceScheme
+import io.matthewnelson.encoding.base64.Base64
+import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
+import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import kotlin.random.Random
import kotlin.time.Duration.Companion.minutes
+
class DummyOAuth2IssuerCredentialDataProvider(
private val userInfo: OidcUserInfoExtended,
private val clock: Clock = Clock.System,
@@ -57,16 +66,25 @@ class DummyOAuth2IssuerCredentialDataProvider(
val givenName = userInfo.userInfo.givenName
val subjectId = subjectPublicKey.didEncoded
val claims = listOfNotNull(
- givenName?.let { optionalClaim(claimNames, "given_name", it) },
- familyName?.let { optionalClaim(claimNames, "family_name", it) },
- userInfo.userInfo.birthDate?.let { optionalClaim(claimNames, "date_of_birth", it) },
+ givenName?.let {
+ optionalClaim(claimNames, CLAIM_GIVEN_NAME, it)
+ },
+ familyName?.let {
+ optionalClaim(claimNames, CLAIM_FAMILY_NAME, it)
+ },
+ userInfo.userInfo.birthDate?.let {
+ optionalClaim(claimNames, CLAIM_DATE_OF_BIRTH, LocalDate.parse(it))
+ },
+ userInfo.userInfo.picture?.let {
+ optionalClaim(claimNames, CLAIM_PORTRAIT, it.decodeToByteArray(Base64()))
+ },
)
return when (representation) {
ConstantIndex.CredentialRepresentation.SD_JWT ->
CredentialToBeIssued.VcSd(claims, expiration)
ConstantIndex.CredentialRepresentation.PLAIN_JWT -> CredentialToBeIssued.VcJwt(
- AtomicAttribute2023(subjectId, "given_name", givenName ?: "no value"), expiration,
+ AtomicAttribute2023(subjectId, GIVEN_NAME, givenName ?: "no value"), expiration,
)
ConstantIndex.CredentialRepresentation.ISO_MDOC -> CredentialToBeIssued.Iso(
@@ -154,7 +172,6 @@ class DummyOAuth2IssuerCredentialDataProvider(
private fun optionalClaim(claimNames: Collection?, name: String, value: Any) =
if (claimNames.isNullOrContains(name)) ClaimToBeIssued(name, value) else null
-
private fun issuerSignedItem(name: String, value: Any, digestId: UInt) = IssuerSignedItem(
digestId = digestId,
random = Random.nextBytes(16),
@@ -164,12 +181,15 @@ class DummyOAuth2IssuerCredentialDataProvider(
}
object DummyOAuth2DataProvider : OAuth2DataProvider {
- override suspend fun loadUserInfo(request: AuthenticationRequestParameters?) =
- OidcUserInfoExtended.fromOidcUserInfo(
- OidcUserInfo(
- subject = "subject",
- givenName = "Erika",
- familyName = "Musterfrau"
- )
- ).getOrThrow()
-}
\ No newline at end of file
+ val user = OidcUserInfoExtended.fromOidcUserInfo(
+ OidcUserInfo(
+ subject = "subject",
+ givenName = "Susanne",
+ familyName = "Meier",
+ picture = Random.nextBytes(64).encodeToString(Base64()),
+ birthDate = "1990-01-01"
+ )
+ ).getOrThrow()
+
+ override suspend fun loadUserInfo(request: AuthenticationRequestParameters, code: String) = user
+}
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTest.kt
index d7e170b47..9daf29595 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTest.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTest.kt
@@ -1,132 +1,95 @@
package at.asitplus.wallet.lib.oidc
-import at.asitplus.wallet.lib.agent.Holder
-import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.IssuerAgent
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.agent.Verifier
-import at.asitplus.wallet.lib.agent.VerifierAgent
-import at.asitplus.wallet.lib.agent.toStoreCredentialInput
+import at.asitplus.wallet.eupid.EuPidScheme
+import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.data.AtomicAttribute2023
import at.asitplus.wallet.lib.data.ConstantIndex
-import at.asitplus.wallet.lib.data.dif.FormatHolder
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_DATE_OF_BIRTH
+import at.asitplus.wallet.lib.data.ConstantIndex.CredentialRepresentation
import at.asitplus.wallet.lib.oidvci.OAuth2Exception
import at.asitplus.wallet.mdl.MobileDrivingLicenceScheme
import com.benasher44.uuid.uuid4
-import io.github.aakira.napier.Napier
+import io.kotest.assertions.fail
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldNotBeEmpty
+import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
-import kotlinx.coroutines.runBlocking
-@Suppress("unused")
class OidcSiopCombinedProtocolTest : FreeSpec({
lateinit var relyingPartyUrl: String
- lateinit var holderKeyPair: KeyPairAdapter
- lateinit var verifierKeyPair: KeyPairAdapter
+ lateinit var holderKeyMaterial: KeyMaterial
+ lateinit var verifierKeyMaterial: KeyMaterial
lateinit var holderAgent: Holder
- lateinit var verifierAgent: Verifier
lateinit var holderSiop: OidcSiopWallet
lateinit var verifierSiop: OidcSiopVerifier
beforeEach {
- holderKeyPair = RandomKeyPairAdapter()
- verifierKeyPair = RandomKeyPairAdapter()
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ verifierKeyMaterial = EphemeralKeyWithoutCert()
relyingPartyUrl = "https://example.com/rp/${uuid4()}"
- holderAgent = HolderAgent(holderKeyPair)
- verifierAgent = VerifierAgent(verifierKeyPair)
+ holderAgent = HolderAgent(holderKeyMaterial)
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
+ keyMaterial = holderKeyMaterial,
holder = holderAgent,
)
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
)
}
- "test support for format holder specification" - {
+ "support for format holder specification" - {
- "test support for plain jwt credential request" - {
+ "support for plain jwt credential request" - {
"if not available despite others with correct format or correct attribute, but not both" {
- runBlocking {
- holderAgent.storeJwtCredential(holderKeyPair, MobileDrivingLicenceScheme)
- holderAgent.storeSdJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeIsoCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- }
-
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
- relyingPartyUrl = relyingPartyUrl,
- )
+ holderAgent.storeJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme)
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
val authnRequest = verifierSiop.createAuthnRequest(
requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- representation = ConstantIndex.CredentialRepresentation.PLAIN_JWT,
- )
- ).let { request ->
- request.copy(
- clientMetadata = request.clientMetadata?.let { clientMetadata ->
- clientMetadata.copy(
- vpFormats = FormatHolder(
- // only allow plain jwt
- jwtVp = clientMetadata.vpFormats?.jwtVp
- )
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023, CredentialRepresentation.PLAIN_JWT
)
- }
+ )
)
- }
-
+ )
shouldThrow {
holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow()
}
}
"if available despite others" {
- runBlocking {
- holderAgent.storeJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeJwtCredential(holderKeyPair, MobileDrivingLicenceScheme)
- holderAgent.storeSdJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeIsoCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- }
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
- relyingPartyUrl = relyingPartyUrl,
- )
+ holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme)
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
val authnRequest = verifierSiop.createAuthnRequest(
requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- )
- ).let { request ->
- request.copy(
- clientMetadata = request.clientMetadata?.let { clientMetadata ->
- clientMetadata.copy(
- vpFormats = FormatHolder(
- // only allow plain jwt
- jwtVp = clientMetadata.vpFormats?.jwtVp
- )
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023,
+ CredentialRepresentation.PLAIN_JWT
)
- }
+ )
)
- }
+ )
val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow()
- authnResponse.shouldBeInstanceOf()
- .also { println(it) }
+ .shouldBeInstanceOf()
val result = verifierSiop.validateAuthnResponse(authnResponse.url)
- result.shouldBeInstanceOf()
+ .shouldBeInstanceOf()
result.vp.verifiableCredentials.shouldNotBeEmpty()
result.vp.verifiableCredentials.forEach {
it.vc.credentialSubject.shouldBeInstanceOf()
@@ -134,275 +97,210 @@ class OidcSiopCombinedProtocolTest : FreeSpec({
}
}
- "test support for sd jwt credential request" - {
+ "support for sd jwt credential request" - {
"if not available despite others with correct format or correct attribute, but not both" {
- runBlocking {
- holderAgent.storeJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeSdJwtCredential(holderKeyPair, MobileDrivingLicenceScheme)
- holderAgent.storeIsoCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- }
-
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
- relyingPartyUrl = relyingPartyUrl,
- )
+ holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme)
+ holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
val authnRequest = verifierSiop.createAuthnRequest(
requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- representation = ConstantIndex.CredentialRepresentation.SD_JWT,
- )
- ).let { request ->
- request.copy(
- presentationDefinition = request.presentationDefinition?.let { presentationDefinition ->
- presentationDefinition.copy(
- formats = FormatHolder(
- // only support SD_JWT here
- jwtSd = presentationDefinition.formats?.jwtSd,
- ),
- inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor ->
- inputDescriptor.copy(
- format = null
- )
- }
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023, CredentialRepresentation.SD_JWT
)
- },
+ )
)
- }
-
+ )
shouldThrow {
holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow()
}
}
"if available despite others with correct format or correct attribute, but not both" {
- runBlocking {
- holderAgent.storeJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeSdJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeSdJwtCredential(holderKeyPair, MobileDrivingLicenceScheme)
- holderAgent.storeIsoCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- }
-
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
- relyingPartyUrl = relyingPartyUrl,
- )
+ holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, MobileDrivingLicenceScheme)
+ holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
val authnRequest = verifierSiop.createAuthnRequest(
requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- representation = ConstantIndex.CredentialRepresentation.SD_JWT,
- )
- ).let { request ->
- request.copy(
- presentationDefinition = request.presentationDefinition?.let { presentationDefinition ->
- presentationDefinition.copy(
- formats = FormatHolder(
- // only support SD_JWT here
- jwtSd = presentationDefinition.formats?.jwtSd,
- ),
- inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor ->
- inputDescriptor.copy(
- format = null
- )
- }
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023, CredentialRepresentation.SD_JWT
)
- },
+ )
)
- }
-
+ )
val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow()
- authnResponse.shouldBeInstanceOf()
- .also { println(it) }
+ .shouldBeInstanceOf()
val result = verifierSiop.validateAuthnResponse(authnResponse.url)
- result.shouldBeInstanceOf()
+ .shouldBeInstanceOf()
result.sdJwt.type?.shouldContain(ConstantIndex.AtomicAttribute2023.vcType)
}
}
- "test support for mso credential request" - {
+ "support for mso credential request" - {
"if not available despite others with correct format or correct attribute, but not both" {
- runBlocking {
- holderAgent.storeJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeSdJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeIsoCredential(holderKeyPair, MobileDrivingLicenceScheme)
- }
-
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
- relyingPartyUrl = relyingPartyUrl,
- )
+ holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeIsoCredential(holderKeyMaterial, MobileDrivingLicenceScheme)
val authnRequest = verifierSiop.createAuthnRequest(
requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- representation = ConstantIndex.CredentialRepresentation.ISO_MDOC,
- ),
- ).let { request ->
- request.copy(
- presentationDefinition = request.presentationDefinition?.let { presentationDefinition ->
- presentationDefinition.copy(
- // only support msoMdoc here
- formats = FormatHolder(
- msoMdoc = presentationDefinition.formats?.msoMdoc
- ),
- inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor ->
- inputDescriptor.copy(
- format = null
- )
- }
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023, CredentialRepresentation.ISO_MDOC
)
- },
- )
- }
- Napier.d("Create response")
-
+ )
+ ),
+ )
shouldThrow {
- holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow().also {
- Napier.d("response: $it")
- }
+ holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow()
}
}
"if available despite others with correct format or correct attribute, but not both" {
- runBlocking {
- holderAgent.storeJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeSdJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeIsoCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeIsoCredential(holderKeyPair, MobileDrivingLicenceScheme)
- }
-
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
- relyingPartyUrl = relyingPartyUrl,
- )
+ holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeIsoCredential(holderKeyMaterial, MobileDrivingLicenceScheme)
val authnRequest = verifierSiop.createAuthnRequest(
requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- representation = ConstantIndex.CredentialRepresentation.ISO_MDOC,
- ),
- ).let { request ->
- request.copy(
- presentationDefinition = request.presentationDefinition?.let { presentationDefinition ->
- presentationDefinition.copy(
- formats = FormatHolder(
- // only support msoMdoc here
- msoMdoc = presentationDefinition.formats?.msoMdoc
- ),
- inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor ->
- inputDescriptor.copy(
- format = null
- )
- }
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023,
+ CredentialRepresentation.ISO_MDOC
)
- },
- )
- }
-
- Napier.d("request: $authnRequest")
+ )
+ ),
+ )
val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow()
- authnResponse.shouldBeInstanceOf()
- .also { println(it) }
+ .shouldBeInstanceOf()
- val result = verifierSiop.validateAuthnResponse(authnResponse.url)
- result.shouldBeInstanceOf()
+ verifierSiop.validateAuthnResponse(authnResponse.url)
+ .shouldBeInstanceOf()
}
}
}
- "test presentation of multiple credentials with different formats" {
- runBlocking {
- holderAgent.storeJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeIsoCredential(holderKeyPair, MobileDrivingLicenceScheme)
- }
-
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
- relyingPartyUrl = relyingPartyUrl,
- )
+ "presentation of multiple credentials with different formats in one request/response" {
+ holderAgent.storeJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeIsoCredential(holderKeyMaterial, MobileDrivingLicenceScheme)
- val authnRequest1 = verifierSiop.createAuthnRequest(
+ val authnRequest = verifierSiop.createAuthnRequest(
requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- representation = ConstantIndex.CredentialRepresentation.PLAIN_JWT,
- ),
- )
- val authnRequest2 = verifierSiop.createAuthnRequest(
- requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = MobileDrivingLicenceScheme,
- representation = ConstantIndex.CredentialRepresentation.ISO_MDOC,
- ),
- )
- val inputDescriptors2 = authnRequest2.presentationDefinition?.inputDescriptors ?: listOf()
-
- val authnRequest = authnRequest1.copy(
- presentationDefinition = authnRequest1.presentationDefinition?.let { presentationDefinition ->
- presentationDefinition.copy(
- inputDescriptors = presentationDefinition.inputDescriptors.map {
- it.copy(format = presentationDefinition.formats)
- } + inputDescriptors2.map {
- it.copy(format = authnRequest2.presentationDefinition?.formats)
- },
- formats = null,
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023, CredentialRepresentation.PLAIN_JWT
+ ),
+ OidcSiopVerifier.RequestOptionsCredential(
+ MobileDrivingLicenceScheme, CredentialRepresentation.ISO_MDOC
+ )
)
- }
+ ),
)
-
val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow()
- authnResponse.shouldBeInstanceOf()
- .also { println(it) }
+ .shouldBeInstanceOf()
val validationResults = verifierSiop.validateAuthnResponse(authnResponse.url)
- validationResults.shouldBeInstanceOf()
+ .shouldBeInstanceOf()
validationResults.validationResults.size shouldBe 2
}
+
+ "presentation of multiple SD-JWT credentials in one request/response" {
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, EuPidScheme)
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+
+ val requestOptions = OidcSiopVerifier.RequestOptions(
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ credentialScheme = ConstantIndex.AtomicAttribute2023,
+ representation = CredentialRepresentation.SD_JWT,
+ requestedAttributes = listOf(CLAIM_DATE_OF_BIRTH),
+ ),
+ OidcSiopVerifier.RequestOptionsCredential(
+ credentialScheme = EuPidScheme,
+ representation = CredentialRepresentation.SD_JWT,
+ requestedAttributes = listOf(
+ EuPidScheme.Attributes.FAMILY_NAME,
+ EuPidScheme.Attributes.GIVEN_NAME
+ ),
+ )
+ )
+ )
+ val authnRequest = verifierSiop.createAuthnRequest(requestOptions)
+
+ val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow()
+ .shouldBeInstanceOf()
+
+ val groupedResult = verifierSiop.validateAuthnResponse(authnResponse.url)
+ .shouldBeInstanceOf()
+ groupedResult.validationResults.size shouldBe 2
+ groupedResult.validationResults.forEach { result ->
+ result.shouldBeInstanceOf()
+ result.disclosures.shouldNotBeEmpty()
+ when (result.sdJwt.verifiableCredentialType) {
+ EuPidScheme.sdJwtType -> {
+ result.disclosures.firstOrNull { it.claimName == EuPidScheme.Attributes.FAMILY_NAME }.shouldNotBeNull()
+ result.disclosures.firstOrNull { it.claimName == EuPidScheme.Attributes.GIVEN_NAME }.shouldNotBeNull()
+ }
+ ConstantIndex.AtomicAttribute2023.sdJwtType -> {
+ result.disclosures.firstOrNull() { it.claimName == CLAIM_DATE_OF_BIRTH }.shouldNotBeNull()
+ }
+ else -> {
+ fail("Unexpected SD-JWT type: ${result.sdJwt.verifiableCredentialType}")
+ }
+ }
+ }
+ }
})
private suspend fun Holder.storeJwtCredential(
- holderKeyPair: KeyPairAdapter,
+ holderKeyMaterial: KeyMaterial,
credentialScheme: ConstantIndex.CredentialScheme,
) {
storeCredential(
IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithoutCert(),
DummyCredentialDataProvider(),
).issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
credentialScheme,
- ConstantIndex.CredentialRepresentation.PLAIN_JWT,
+ CredentialRepresentation.PLAIN_JWT,
).getOrThrow().toStoreCredentialInput()
)
}
private suspend fun Holder.storeSdJwtCredential(
- holderKeyPair: KeyPairAdapter,
+ holderKeyMaterial: KeyMaterial,
credentialScheme: ConstantIndex.CredentialScheme,
) {
storeCredential(
IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithoutCert(),
DummyCredentialDataProvider(),
).issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
credentialScheme,
- ConstantIndex.CredentialRepresentation.SD_JWT,
+ CredentialRepresentation.SD_JWT,
).getOrThrow().toStoreCredentialInput()
)
}
private suspend fun Holder.storeIsoCredential(
- holderKeyPair: KeyPairAdapter,
+ holderKeyMaterial: KeyMaterial,
credentialScheme: ConstantIndex.CredentialScheme,
) = storeCredential(
IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithSelfSignedCert(),
DummyCredentialDataProvider(),
).issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
credentialScheme,
- ConstantIndex.CredentialRepresentation.ISO_MDOC,
+ CredentialRepresentation.ISO_MDOC,
).getOrThrow().toStoreCredentialInput()
)
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt
index 17ce6c92d..9bf6f58eb 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopCombinedProtocolTwoStepTest.kt
@@ -1,17 +1,7 @@
package at.asitplus.wallet.lib.oidc
-import at.asitplus.wallet.lib.agent.CredentialSubmission
-import at.asitplus.wallet.lib.agent.Holder
-import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.IssuerAgent
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.agent.SubjectCredentialStore
-import at.asitplus.wallet.lib.agent.Verifier
-import at.asitplus.wallet.lib.agent.VerifierAgent
-import at.asitplus.wallet.lib.agent.toStoreCredentialInput
+import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.data.ConstantIndex
-import at.asitplus.wallet.lib.data.dif.FormatHolder
import com.benasher44.uuid.uuid4
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.assertions.throwables.shouldThrowAny
@@ -19,79 +9,58 @@ import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.maps.shouldHaveSize
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.types.shouldBeInstanceOf
-import kotlinx.coroutines.runBlocking
class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({
lateinit var relyingPartyUrl: String
- lateinit var holderKeyPair: KeyPairAdapter
- lateinit var verifierKeyPair: KeyPairAdapter
+ lateinit var holderKeyMaterial: KeyMaterial
+ lateinit var verifierKeyMaterial: KeyMaterial
lateinit var holderAgent: Holder
- lateinit var verifierAgent: Verifier
lateinit var holderSiop: OidcSiopWallet
lateinit var verifierSiop: OidcSiopVerifier
beforeEach {
- holderKeyPair = RandomKeyPairAdapter()
- verifierKeyPair = RandomKeyPairAdapter()
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ verifierKeyMaterial = EphemeralKeyWithoutCert()
relyingPartyUrl = "https://example.com/rp/${uuid4()}"
- holderAgent = HolderAgent(holderKeyPair)
- verifierAgent = VerifierAgent(verifierKeyPair)
+ holderAgent = HolderAgent(holderKeyMaterial)
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
+ keyMaterial = holderKeyMaterial,
holder = holderAgent,
)
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
)
}
"test credential matching" - {
"only credentials of the correct format are matched" {
- runBlocking {
- holderAgent.storeIsoCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeIsoCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeSdJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- }
-
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
- relyingPartyUrl = relyingPartyUrl,
- )
+ holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
val authnRequest = verifierSiop.createAuthnRequest(
requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- representation = ConstantIndex.CredentialRepresentation.ISO_MDOC,
- )
- ).let { request ->
- request.copy(
- presentationDefinition = request.presentationDefinition?.let { presentationDefinition ->
- presentationDefinition.copy(
- // only support msoMdoc here
- formats = FormatHolder(
- msoMdoc = presentationDefinition.formats?.msoMdoc
- ),
- inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor ->
- inputDescriptor.copy(format = null)
- }
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023,
+ ConstantIndex.CredentialRepresentation.ISO_MDOC
)
- },
+ )
)
- }
+ )
val preparationState = holderSiop.startAuthorizationResponsePreparation(authnRequest.serialize())
.getOrThrow()
val presentationDefinition = preparationState.presentationDefinition.shouldNotBeNull()
val inputDescriptorId = presentationDefinition.inputDescriptors.first().id
val matches = holderAgent.matchInputDescriptorsAgainstCredentialStore(
- presentationDefinition.inputDescriptors,
- presentationDefinition.formats,
+ presentationDefinition.inputDescriptors
).getOrThrow()
val inputDescriptorMatches = matches[inputDescriptorId].shouldNotBeNull()
inputDescriptorMatches shouldHaveSize 2
@@ -102,49 +71,29 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({
}
"test credential submission" - {
- "submission requirements need to macth" - {
+ "submission requirements need to match" - {
"all credentials matching an input descriptor should be presentable" {
- runBlocking {
- holderAgent.storeIsoCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeIsoCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeSdJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- }
-
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
- relyingPartyUrl = relyingPartyUrl,
- )
+ holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
val authnRequest = verifierSiop.createAuthnRequest(
requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- representation = ConstantIndex.CredentialRepresentation.ISO_MDOC,
- )
- ).let { request ->
- request.copy(
- presentationDefinition = request.presentationDefinition?.let { presentationDefinition ->
- presentationDefinition.copy(
- // only support msoMdoc here
- formats = FormatHolder(
- msoMdoc = presentationDefinition.formats?.msoMdoc
- ),
- inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor ->
- inputDescriptor.copy(
- format = null
- )
- }
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023,
+ ConstantIndex.CredentialRepresentation.ISO_MDOC
)
- },
+ )
)
- }
+ )
val params = holderSiop.parseAuthenticationRequestParameters(authnRequest.serialize()).getOrThrow()
val preparationState = holderSiop.startAuthorizationResponsePreparation(params).getOrThrow()
val presentationDefinition = preparationState.presentationDefinition.shouldNotBeNull()
val inputDescriptorId = presentationDefinition.inputDescriptors.first().id
val matches = holderAgent.matchInputDescriptorsAgainstCredentialStore(
- presentationDefinition.inputDescriptors,
- presentationDefinition.formats,
+ presentationDefinition.inputDescriptors
).getOrThrow().also {
it shouldHaveSize 1
}
@@ -172,39 +121,20 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({
}
}
"credentials not matching an input descriptor should not yield a valid submission" {
- runBlocking {
- holderAgent.storeIsoCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeIsoCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- holderAgent.storeSdJwtCredential(holderKeyPair, ConstantIndex.AtomicAttribute2023)
- }
-
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
- relyingPartyUrl = relyingPartyUrl,
- )
+ holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeIsoCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
+ holderAgent.storeSdJwtCredential(holderKeyMaterial, ConstantIndex.AtomicAttribute2023)
val sdJwtMatches = run {
val authnRequestSdJwt = verifierSiop.createAuthnRequest(
requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- representation = ConstantIndex.CredentialRepresentation.SD_JWT,
- )
- ).let { request ->
- request.copy(
- presentationDefinition = request.presentationDefinition?.let { presentationDefinition ->
- presentationDefinition.copy(
- // only support msoMdoc here
- inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor ->
- inputDescriptor.copy(
- format = FormatHolder(
- jwtSd = presentationDefinition.formats?.jwtSd
- ),
- )
- }
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023, ConstantIndex.CredentialRepresentation.SD_JWT
)
- },
+ )
)
- }
+ )
val preparationStateSdJwt = holderSiop.startAuthorizationResponsePreparation(
holderSiop.parseAuthenticationRequestParameters(authnRequestSdJwt.serialize()).getOrThrow()
@@ -213,7 +143,6 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({
holderAgent.matchInputDescriptorsAgainstCredentialStore(
presentationDefinitionSdJwt.inputDescriptors,
- presentationDefinitionSdJwt.formats,
).getOrThrow().also {
it.shouldHaveSize(1)
it.entries.first().value.let {
@@ -228,25 +157,13 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({
val authnRequest = verifierSiop.createAuthnRequest(
requestOptions = OidcSiopVerifier.RequestOptions(
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- representation = ConstantIndex.CredentialRepresentation.ISO_MDOC,
- )
- ).let { request ->
- request.copy(
- presentationDefinition = request.presentationDefinition?.let { presentationDefinition ->
- presentationDefinition.copy(
- // only support msoMdoc here
- inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor ->
- inputDescriptor.copy(
- format = FormatHolder(
- msoMdoc = presentationDefinition.formats?.msoMdoc
- ),
- )
- }
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023, ConstantIndex.CredentialRepresentation.ISO_MDOC
)
- },
+ )
)
- }
+ )
val params = holderSiop.parseAuthenticationRequestParameters(authnRequest.serialize()).getOrThrow()
val preparationState = holderSiop.startAuthorizationResponsePreparation(params).getOrThrow()
@@ -255,7 +172,6 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({
val matches = holderAgent.matchInputDescriptorsAgainstCredentialStore(
presentationDefinition.inputDescriptors,
- presentationDefinition.formats,
).getOrThrow().also {
it shouldHaveSize 1
}
@@ -286,15 +202,15 @@ class OidcSiopCombinedProtocolTwoStepTest : FreeSpec({
})
private suspend fun Holder.storeSdJwtCredential(
- holderKeyPair: KeyPairAdapter,
+ holderKeyMaterial: KeyMaterial,
credentialScheme: ConstantIndex.CredentialScheme,
) {
storeCredential(
IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithoutCert(),
DummyCredentialDataProvider(),
).issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
credentialScheme,
ConstantIndex.CredentialRepresentation.SD_JWT,
).getOrThrow().toStoreCredentialInput()
@@ -302,14 +218,14 @@ private suspend fun Holder.storeSdJwtCredential(
}
private suspend fun Holder.storeIsoCredential(
- holderKeyPair: KeyPairAdapter,
+ holderKeyMaterial: KeyMaterial,
credentialScheme: ConstantIndex.CredentialScheme,
) = storeCredential(
IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithSelfSignedCert(),
DummyCredentialDataProvider(),
).issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
credentialScheme,
ConstantIndex.CredentialRepresentation.ISO_MDOC,
).getOrThrow().toStoreCredentialInput()
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt
index dddf02d14..4a96c6e74 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopInteropTest.kt
@@ -1,6 +1,12 @@
package at.asitplus.wallet.lib.oidc
-import at.asitplus.signum.indispensable.asn1.*
+import at.asitplus.openid.AuthenticationRequestParameters
+import at.asitplus.openid.OpenIdConstants
+import at.asitplus.signum.indispensable.asn1.Asn1EncapsulatingOctetString
+import at.asitplus.signum.indispensable.asn1.Asn1Primitive
+import at.asitplus.signum.indispensable.asn1.Asn1String
+import at.asitplus.signum.indispensable.asn1.KnownOIDs
+import at.asitplus.signum.indispensable.asn1.encoding.Asn1
import at.asitplus.signum.indispensable.josef.JweAlgorithm
import at.asitplus.signum.indispensable.josef.JweEncryption
import at.asitplus.signum.indispensable.josef.JwsAlgorithm
@@ -9,11 +15,12 @@ import at.asitplus.signum.indispensable.pki.X509CertificateExtension
import at.asitplus.wallet.eupid.EuPidScheme
import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.data.ConstantIndex
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_FAMILY_NAME
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_GIVEN_NAME
import at.asitplus.wallet.lib.jws.DefaultJwsService
import at.asitplus.wallet.lib.oidvci.decode
import at.asitplus.wallet.lib.oidvci.decodeFromUrlQuery
import com.benasher44.uuid.uuid4
-import io.github.aakira.napier.Napier
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.collections.shouldBeSingleton
import io.kotest.matchers.collections.shouldHaveSingleElement
@@ -30,23 +37,22 @@ import kotlinx.datetime.Instant
*/
class OidcSiopInteropTest : FreeSpec({
- lateinit var holderKeyPair: KeyPairAdapter
+ lateinit var holderKeyMaterial: KeyMaterial
lateinit var holderAgent: Holder
lateinit var holderSiop: OidcSiopWallet
- lateinit var verifierKeyPair: KeyPairAdapter
- lateinit var verifierAgent: Verifier
+ lateinit var verifierKeyMaterial: KeyMaterial
lateinit var verifierSiop: OidcSiopVerifier
beforeEach {
- holderKeyPair = RandomKeyPairAdapter()
- holderAgent = HolderAgent(holderKeyPair)
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ holderAgent = HolderAgent(holderKeyMaterial)
val issuerAgent = IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithoutCert(),
DummyCredentialDataProvider(),
)
holderAgent.storeCredential(
issuerAgent.issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
EuPidScheme,
ConstantIndex.CredentialRepresentation.SD_JWT,
EuPidScheme.requiredClaimNames
@@ -54,13 +60,13 @@ class OidcSiopInteropTest : FreeSpec({
)
holderAgent.storeCredential(
issuerAgent.issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.SD_JWT,
- listOf("family_name", "given_name")
+ listOf(CLAIM_FAMILY_NAME, CLAIM_GIVEN_NAME)
).getOrThrow().toStoreCredentialInput()
)
- holderSiop = OidcSiopWallet.newDefaultInstance(holderKeyPair, holderAgent)
+ holderSiop = OidcSiopWallet(holderKeyMaterial, holderAgent)
}
"EUDI from URL 2024-05-17" {
@@ -162,8 +168,7 @@ class OidcSiopInteropTest : FreeSpec({
"https://verifier-backend.eudiw.dev/wallet/jarm/Vu3g2FXDeqday-wS0Xmty0bYzzq3MeVGrPSGTdk3Y60tWNLHkr_bg9WJMK3xktNsqWpEXPsDgBw5g3r80MQyTw/jwks.json"
val requestUrl =
"https://verifier-backend.eudiw.dev/wallet/request.jwt/Vu3g2FXDeqday-wS0Xmty0bYzzq3MeVGrPSGTdk3Y60tWNLHkr_bg9WJMK3xktNsqWpEXPsDgBw5g3r80MQyTw"
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
holder = holderAgent,
remoteResourceRetriever = {
if (it == jwksUrl) jwkset else if (it == requestUrl) requestObject else null
@@ -171,7 +176,6 @@ class OidcSiopInteropTest : FreeSpec({
)
holderSiop.parseAuthenticationRequestParameters(url).getOrThrow()
- .also { println(it) }
}
"EUDI AuthnRequest can be parsed" {
@@ -279,7 +283,7 @@ class OidcSiopInteropTest : FreeSpec({
"Request in request URI" {
val input = "mdoc-openid4vp://?request_uri=https%3A%2F%2Fexample.com%2Fd15b5b6f-7821-4031-9a18-ebe491b720a6"
- val jws = DefaultJwsService(DefaultCryptoService(RandomKeyPairAdapter())).createSignedJwsAddingParams(
+ val jws = DefaultJwsService(DefaultCryptoService(EphemeralKeyWithoutCert())).createSignedJwsAddingParams(
payload = AuthenticationRequestParameters(
nonce = "RjEQKQeG8OUaKT4ij84E8mCvry6pVSgDyqRBMW5eBTPItP4DIfbKaT6M6v6q2Dvv8fN7Im7Ifa6GI2j6dHsJaQ==",
state = "ef391e30-bacc-4441-af5d-7f42fb682e02",
@@ -289,7 +293,7 @@ class OidcSiopInteropTest : FreeSpec({
addX5c = false
).getOrThrow().serialize()
- val wallet = OidcSiopWallet.newDefaultInstance(
+ val wallet = OidcSiopWallet(
remoteResourceRetriever = { url ->
if (url == "https://example.com/d15b5b6f-7821-4031-9a18-ebe491b720a6") jws else null
}
@@ -321,13 +325,11 @@ class OidcSiopInteropTest : FreeSpec({
)
}
))))
- verifierKeyPair = RandomKeyPairAdapter(extensions)
- verifierAgent = VerifierAgent(verifierKeyPair)
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierKeyMaterial = EphemeralKeyWithSelfSignedCert(extensions = extensions)
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = "https://example.com/rp",
- responseUrl = "https://example.com/response",
- x5c = listOf(verifierKeyPair.certificate!!)
+ clientIdScheme = OidcSiopVerifier.ClientIdScheme.CertificateSanDns(listOf(verifierKeyMaterial.getCertificate()!!)),
)
val nonce = uuid4().toString()
val requestUrl = "https://example.com/request/$nonce"
@@ -336,26 +338,29 @@ class OidcSiopInteropTest : FreeSpec({
requestUrl = requestUrl,
requestOptions = OidcSiopVerifier.RequestOptions(
responseMode = OpenIdConstants.ResponseMode.DIRECT_POST,
- representation = ConstantIndex.CredentialRepresentation.SD_JWT,
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- requestedAttributes = listOf("family_name", "given_name")
+ responseUrl = "https://example.com/response",
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023,
+ ConstantIndex.CredentialRepresentation.SD_JWT,
+ listOf(CLAIM_FAMILY_NAME, CLAIM_GIVEN_NAME)
+ )
+ )
)
- ).getOrThrow().also { println(it) }
+ ).getOrThrow()
- holderSiop = OidcSiopWallet.newDefaultInstance(
- holderKeyPair,
+ holderSiop = OidcSiopWallet(
+ holderKeyMaterial,
holderAgent,
remoteResourceRetriever = { if (it == requestUrl) requestUrlForWallet.second else null })
val parameters = holderSiop.parseAuthenticationRequestParameters(requestUrlForWallet.first).getOrThrow()
- val stae = holderSiop.startAuthorizationResponsePreparation(parameters).getOrThrow()
- val response = holderSiop.finalizeAuthorizationResponse(parameters, stae).getOrThrow()
+ val preparation = holderSiop.startAuthorizationResponsePreparation(parameters).getOrThrow()
+ val response = holderSiop.finalizeAuthorizationResponse(parameters, preparation).getOrThrow()
.shouldBeInstanceOf()
- .also { println(it) }
verifierSiop.validateAuthnResponse(params = response.params.decode())
.shouldBeInstanceOf()
- .also { println(it) }
}
})
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt
index 0e6031b13..70c2f286f 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopIsoProtocolTest.kt
@@ -1,14 +1,9 @@
package at.asitplus.wallet.lib.oidc
-import at.asitplus.wallet.lib.agent.Holder
-import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.IssuerAgent
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.agent.Verifier
-import at.asitplus.wallet.lib.agent.VerifierAgent
-import at.asitplus.wallet.lib.agent.toStoreCredentialInput
+import at.asitplus.openid.OpenIdConstants
+import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.data.ConstantIndex
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_GIVEN_NAME
import at.asitplus.wallet.lib.data.IsoDocumentParsed
import at.asitplus.wallet.lib.oidvci.formUrlEncode
import at.asitplus.wallet.mdl.MobileDrivingLicenceDataElements
@@ -27,63 +22,63 @@ class OidcSiopIsoProtocolTest : FreeSpec({
lateinit var relyingPartyUrl: String
lateinit var walletUrl: String
- lateinit var holderKeyPair: KeyPairAdapter
- lateinit var verifierKeyPair: KeyPairAdapter
+ lateinit var holderKeyMaterial: KeyMaterial
+ lateinit var verifierKeyMaterial: KeyMaterial
lateinit var holderAgent: Holder
- lateinit var verifierAgent: Verifier
lateinit var holderSiop: OidcSiopWallet
lateinit var verifierSiop: OidcSiopVerifier
beforeEach {
- holderKeyPair = RandomKeyPairAdapter()
- verifierKeyPair = RandomKeyPairAdapter()
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ verifierKeyMaterial = EphemeralKeyWithoutCert()
relyingPartyUrl = "https://example.com/rp/${uuid4()}"
walletUrl = "https://example.com/wallet/${uuid4()}"
- holderAgent = HolderAgent(holderKeyPair)
- verifierAgent = VerifierAgent(verifierKeyPair)
+ holderAgent = HolderAgent(holderKeyMaterial)
val issuerAgent = IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithSelfSignedCert(),
DummyCredentialDataProvider(),
)
holderAgent.storeCredential(
issuerAgent.issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
MobileDrivingLicenceScheme,
ConstantIndex.CredentialRepresentation.ISO_MDOC,
).getOrThrow().toStoreCredentialInput()
)
holderAgent.storeCredential(
issuerAgent.issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.ISO_MDOC,
).getOrThrow().toStoreCredentialInput()
)
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
holder = holderAgent,
+ keyMaterial = holderKeyMaterial
)
}
"test with Fragment for mDL" {
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
)
val document = runProcess(
verifierSiop,
walletUrl,
OidcSiopVerifier.RequestOptions(
- representation = ConstantIndex.CredentialRepresentation.ISO_MDOC,
- credentialScheme = MobileDrivingLicenceScheme,
- requestedAttributes = listOf(
- MobileDrivingLicenceDataElements.GIVEN_NAME
- ),
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ MobileDrivingLicenceScheme, ConstantIndex.CredentialRepresentation.ISO_MDOC, listOf(
+ MobileDrivingLicenceDataElements.GIVEN_NAME
+ )
+ )
+ )
),
holderSiop
)
@@ -93,17 +88,21 @@ class OidcSiopIsoProtocolTest : FreeSpec({
}
"test with Fragment for custom attributes" {
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
)
val document = runProcess(
verifierSiop,
walletUrl,
OidcSiopVerifier.RequestOptions(
- representation = ConstantIndex.CredentialRepresentation.ISO_MDOC,
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- requestedAttributes = listOf("given_name"),
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023,
+ ConstantIndex.CredentialRepresentation.ISO_MDOC,
+ listOf(CLAIM_GIVEN_NAME)
+ )
+ )
),
holderSiop
)
@@ -114,17 +113,21 @@ class OidcSiopIsoProtocolTest : FreeSpec({
"Selective Disclosure with mDL" {
val requestedClaim = MobileDrivingLicenceDataElements.FAMILY_NAME
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
)
val document = runProcess(
verifierSiop,
walletUrl,
OidcSiopVerifier.RequestOptions(
- representation = ConstantIndex.CredentialRepresentation.ISO_MDOC,
- credentialScheme = MobileDrivingLicenceScheme,
- requestedAttributes = listOf(requestedClaim),
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ MobileDrivingLicenceScheme,
+ ConstantIndex.CredentialRepresentation.ISO_MDOC,
+ listOf(requestedClaim)
+ )
+ )
),
holderSiop,
)
@@ -137,22 +140,24 @@ class OidcSiopIsoProtocolTest : FreeSpec({
"Selective Disclosure with mDL and encryption" {
val requestedClaim = MobileDrivingLicenceDataElements.FAMILY_NAME
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
- responseUrl = relyingPartyUrl + "/${uuid4()}"
)
val requestOptions = OidcSiopVerifier.RequestOptions(
- representation = ConstantIndex.CredentialRepresentation.ISO_MDOC,
- credentialScheme = MobileDrivingLicenceScheme,
- requestedAttributes = listOf(requestedClaim),
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ MobileDrivingLicenceScheme, ConstantIndex.CredentialRepresentation.ISO_MDOC, listOf(requestedClaim)
+ )
+ ),
responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT,
+ responseUrl = "https://example.com/response",
encryption = true
)
val authnRequest = verifierSiop.createAuthnRequestUrl(
walletUrl = walletUrl,
requestOptions = requestOptions
- ).also { println(it) }
+ )
val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow()
authnResponse.shouldBeInstanceOf()
@@ -169,17 +174,21 @@ class OidcSiopIsoProtocolTest : FreeSpec({
}
"Selective Disclosure with mDL JSON Path syntax" {
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
)
val document = runProcess(
verifierSiop,
walletUrl,
OidcSiopVerifier.RequestOptions(
- representation = ConstantIndex.CredentialRepresentation.ISO_MDOC,
- credentialScheme = MobileDrivingLicenceScheme,
- requestedAttributes = listOf(MobileDrivingLicenceDataElements.FAMILY_NAME)
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ MobileDrivingLicenceScheme,
+ ConstantIndex.CredentialRepresentation.ISO_MDOC,
+ listOf(MobileDrivingLicenceDataElements.FAMILY_NAME)
+ )
+ )
),
holderSiop,
)
@@ -201,12 +210,12 @@ private suspend fun runProcess(
val authnRequest = verifierSiop.createAuthnRequestUrl(
walletUrl = walletUrl,
requestOptions = requestOptions
- ).also { println(it) }
+ )
val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow()
- authnResponse.shouldBeInstanceOf().also { println(it) }
+ authnResponse.shouldBeInstanceOf()
val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf()
- return result.document.also { println(it) }
+ return result.document
}
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt
index 38ce349b7..ac6d45c5b 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt
@@ -1,30 +1,18 @@
package at.asitplus.wallet.lib.oidc
-import at.asitplus.signum.indispensable.io.Base64UrlStrict
-import at.asitplus.signum.indispensable.josef.JsonWebKey
-import at.asitplus.signum.indispensable.josef.JsonWebToken
-import at.asitplus.signum.indispensable.josef.JwsHeader
-import at.asitplus.signum.indispensable.josef.JwsSigned
-import at.asitplus.signum.indispensable.josef.toJwsAlgorithm
-import at.asitplus.wallet.lib.agent.DefaultCryptoService
-import at.asitplus.wallet.lib.agent.Holder
-import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.IssuerAgent
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.agent.Verifier
-import at.asitplus.wallet.lib.agent.VerifierAgent
-import at.asitplus.wallet.lib.agent.toStoreCredentialInput
+import at.asitplus.openid.AuthenticationRequestParameters
+import at.asitplus.openid.AuthenticationResponseParameters
+import at.asitplus.openid.OpenIdConstants
+import at.asitplus.openid.OpenIdConstants.ID_TOKEN
+import at.asitplus.openid.OpenIdConstants.VP_TOKEN
+import at.asitplus.signum.indispensable.josef.*
+import at.asitplus.wallet.lib.agent.*
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.oidc.OidcSiopVerifier.RequestOptions
-import at.asitplus.wallet.lib.oidvci.OAuth2Exception
-import at.asitplus.wallet.lib.oidvci.decodeFromPostBody
-import at.asitplus.wallet.lib.oidvci.decodeFromUrlQuery
-import at.asitplus.wallet.lib.oidvci.encodeToParameters
-import at.asitplus.wallet.lib.oidvci.formUrlEncode
+import at.asitplus.wallet.lib.oidvci.*
import com.benasher44.uuid.uuid4
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FreeSpec
@@ -38,66 +26,56 @@ import io.kotest.matchers.string.shouldNotContain
import io.kotest.matchers.string.shouldStartWith
import io.kotest.matchers.types.shouldBeInstanceOf
import io.ktor.http.*
-import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.datetime.Clock
-import kotlin.random.Random
import kotlin.time.Duration.Companion.seconds
@Suppress("unused")
class OidcSiopProtocolTest : FreeSpec({
lateinit var relyingPartyUrl: String
- lateinit var responseUrl: String
lateinit var walletUrl: String
- lateinit var holderKeyPair: KeyPairAdapter
- lateinit var verifierKeyPair: KeyPairAdapter
+ lateinit var holderKeyMaterial: KeyMaterial
+ lateinit var verifierKeyMaterial: KeyMaterial
lateinit var holderAgent: Holder
- lateinit var verifierAgent: Verifier
lateinit var holderSiop: OidcSiopWallet
lateinit var verifierSiop: OidcSiopVerifier
beforeEach {
- holderKeyPair = RandomKeyPairAdapter()
- verifierKeyPair = RandomKeyPairAdapter()
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ verifierKeyMaterial = EphemeralKeyWithoutCert()
val rpUUID = uuid4()
relyingPartyUrl = "https://example.com/rp/$rpUUID"
- responseUrl = "https://example.com/rp/$rpUUID"
walletUrl = "https://example.com/wallet/${uuid4()}"
- holderAgent = HolderAgent(holderKeyPair)
- verifierAgent = VerifierAgent(verifierKeyPair)
+ holderAgent = HolderAgent(holderKeyMaterial)
holderAgent.storeCredential(
IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithoutCert(),
DummyCredentialDataProvider(),
).issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.PLAIN_JWT,
).getOrThrow().toStoreCredentialInput()
)
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
holder = holderAgent,
)
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
- responseUrl = responseUrl,
)
}
"test with Fragment" {
- val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl = walletUrl)
- .also { println(it) }
+ val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl, defaultRequestOptions)
val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow()
authnResponse.shouldBeInstanceOf()
- .also { println(it) }
authnResponse.url.shouldNotContain("?")
authnResponse.url.shouldContain("#")
@@ -110,6 +88,50 @@ class OidcSiopProtocolTest : FreeSpec({
verifySecondProtocolRun(verifierSiop, walletUrl, holderSiop)
}
+ "wrong client nonce in id_token should lead to error" {
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
+ relyingPartyUrl = relyingPartyUrl,
+ nonceService = object : NonceService {
+ override suspend fun provideNonce() = uuid4().toString()
+ override suspend fun verifyNonce(it: String) = false
+ override suspend fun verifyAndRemoveNonce(it: String) = false
+ }
+ )
+ val requestOptions = RequestOptions(
+ credentials = setOf(OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)),
+ responseType = "$ID_TOKEN $VP_TOKEN"
+ )
+ val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl, requestOptions)
+
+ val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow()
+ authnResponse.shouldBeInstanceOf()
+
+ val result = verifierSiop.validateAuthnResponse(authnResponse.url)
+ result.shouldBeInstanceOf()
+ result.field shouldBe "idToken"
+ }
+
+ "wrong client nonce in vp_token should lead to error" {
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
+ relyingPartyUrl = relyingPartyUrl,
+ stateToNonceStore = object : MapStore {
+ override suspend fun put(key: String, value: String) {}
+ override suspend fun get(key: String): String? = null
+ override suspend fun remove(key: String): String? = null
+ },
+ )
+ val authnRequest = verifierSiop.createAuthnRequestUrl(walletUrl, defaultRequestOptions)
+
+ val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow()
+ authnResponse.shouldBeInstanceOf()
+
+ val result = verifierSiop.validateAuthnResponse(authnResponse.url)
+ result.shouldBeInstanceOf()
+ result.field shouldBe "state"
+ }
+
"test with QR Code" {
val metadataUrlNonce = uuid4().toString()
val metadataUrl = "https://example.com/$metadataUrlNonce"
@@ -120,17 +142,16 @@ class OidcSiopProtocolTest : FreeSpec({
qrcode shouldContain requestUrlNonce
val metadataObject = verifierSiop.createSignedMetadata().getOrThrow()
- .also { println(it) }
DefaultVerifierJwsService().verifyJwsObject(metadataObject).shouldBeTrue()
- val authnRequestUrl =
- verifierSiop.createAuthnRequestUrlWithRequestObject(walletUrl).getOrThrow()
+ val authnRequestUrl = verifierSiop.createAuthnRequestUrlWithRequestObject(walletUrl, defaultRequestOptions)
+ .getOrThrow()
val authnRequest: AuthenticationRequestParameters =
Url(authnRequestUrl).encodedQuery.decodeFromUrlQuery()
authnRequest.clientId shouldBe relyingPartyUrl
val jar = authnRequest.request
jar.shouldNotBeNull()
- DefaultVerifierJwsService().verifyJwsObject(JwsSigned.parse(jar).getOrThrow()).shouldBeTrue()
+ DefaultVerifierJwsService().verifyJwsObject(JwsSigned.deserialize(jar).getOrThrow()).shouldBeTrue()
val authnResponse = holderSiop.createAuthnResponse(jar).getOrThrow()
authnResponse.shouldBeInstanceOf()
@@ -142,12 +163,15 @@ class OidcSiopProtocolTest : FreeSpec({
"test with direct_post" {
val authnRequest = verifierSiop.createAuthnRequestUrl(
walletUrl = walletUrl,
- requestOptions = RequestOptions(responseMode = OpenIdConstants.ResponseMode.DIRECT_POST)
- ).also { println(it) }
+ requestOptions = RequestOptions(
+ credentials = setOf(OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)),
+ responseMode = OpenIdConstants.ResponseMode.DIRECT_POST,
+ responseUrl = relyingPartyUrl,
+ )
+ )
val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow()
authnResponse.shouldBeInstanceOf()
- .also { println(it) }
authnResponse.url.shouldBe(relyingPartyUrl)
val result =
@@ -159,16 +183,19 @@ class OidcSiopProtocolTest : FreeSpec({
"test with direct_post_jwt" {
val authnRequest = verifierSiop.createAuthnRequestUrl(
walletUrl = walletUrl,
- requestOptions = RequestOptions(responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT)
- ).also { println(it) }
+ requestOptions = RequestOptions(
+ credentials = setOf(OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)),
+ responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT,
+ responseUrl = relyingPartyUrl,
+ )
+ )
val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow()
authnResponse.shouldBeInstanceOf()
- .also { println(it) }
authnResponse.url.shouldBe(relyingPartyUrl)
- authnResponse.params.shouldHaveSize(1)
- val jarmResponse = authnResponse.params.values.first()
- DefaultVerifierJwsService().verifyJwsObject(JwsSigned.parse(jarmResponse).getOrThrow()).shouldBeTrue()
+ authnResponse.params.shouldHaveSize(2)
+ val jarmResponse = authnResponse.params.entries.first { it.key == "response" }.value
+ DefaultVerifierJwsService().verifyJwsObject(JwsSigned.deserialize(jarmResponse).getOrThrow()).shouldBeTrue()
val result =
verifierSiop.validateAuthnResponseFromPost(authnResponse.params.formUrlEncode())
@@ -181,14 +208,14 @@ class OidcSiopProtocolTest : FreeSpec({
val authnRequest = verifierSiop.createAuthnRequestUrl(
walletUrl = walletUrl,
requestOptions = RequestOptions(
+ credentials = setOf(OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)),
responseMode = OpenIdConstants.ResponseMode.QUERY,
state = expectedState
)
- ).also { println(it) }
+ )
val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow()
authnResponse.shouldBeInstanceOf()
- .also { println(it) }
authnResponse.url.shouldContain("?")
authnResponse.url.shouldNotContain("#")
@@ -201,9 +228,8 @@ class OidcSiopProtocolTest : FreeSpec({
}
"test with deserializing" {
- val authnRequest = verifierSiop.createAuthnRequest()
- val authnRequestUrlParams =
- authnRequest.encodeToParameters().formUrlEncode().also { println(it) }
+ val authnRequest = verifierSiop.createAuthnRequest(defaultRequestOptions)
+ val authnRequestUrlParams = authnRequest.encodeToParameters().formUrlEncode()
val parsedAuthnRequest: AuthenticationRequestParameters =
authnRequestUrlParams.decodeFromUrlQuery()
@@ -213,8 +239,7 @@ class OidcSiopProtocolTest : FreeSpec({
parsedAuthnRequest
)
).getOrThrow().params
- val authnResponseParams =
- authnResponse.encodeToParameters().formUrlEncode().also { println(it) }
+ val authnResponseParams = authnResponse.encodeToParameters().formUrlEncode()
val parsedAuthnResponse: AuthenticationResponseParameters =
authnResponseParams.decodeFromPostBody()
@@ -226,12 +251,11 @@ class OidcSiopProtocolTest : FreeSpec({
"test specific credential" {
val authnRequest = verifierSiop.createAuthnRequestUrl(
walletUrl = walletUrl,
- requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023)
- ).also { println(it) }
+ requestOptions = requestOptionsAtomicAttribute()
+ )
val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow()
authnResponse.shouldBeInstanceOf()
- .also { println(it) }
val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf()
@@ -244,13 +268,11 @@ class OidcSiopProtocolTest : FreeSpec({
"test with request object" {
val authnRequestWithRequestObject = verifierSiop.createAuthnRequestUrlWithRequestObject(
walletUrl = walletUrl,
- requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023)
- ).getOrThrow().also { println(it) }
+ requestOptions = requestOptionsAtomicAttribute()
+ ).getOrThrow()
- val authnResponse =
- holderSiop.createAuthnResponse(authnRequestWithRequestObject).getOrThrow()
+ val authnResponse = holderSiop.createAuthnResponse(authnRequestWithRequestObject).getOrThrow()
authnResponse.shouldBeInstanceOf()
- .also { println(it) }
val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf()
@@ -261,28 +283,24 @@ class OidcSiopProtocolTest : FreeSpec({
}
"test with request object and Attestation JWT" {
- val sprsCryptoService = DefaultCryptoService(RandomKeyPairAdapter())
- val attestationJwt = buildAttestationJwt(sprsCryptoService, relyingPartyUrl, verifierKeyPair)
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ val sprsCryptoService = DefaultCryptoService(EphemeralKeyWithoutCert())
+ val attestationJwt = buildAttestationJwt(sprsCryptoService, relyingPartyUrl, verifierKeyMaterial)
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
- attestationJwt = attestationJwt
+ clientIdScheme = OidcSiopVerifier.ClientIdScheme.VerifierAttestation(attestationJwt),
)
val authnRequestWithRequestObject = verifierSiop.createAuthnRequestUrlWithRequestObject(
walletUrl = walletUrl,
- requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023)
- ).getOrThrow().also { println(it) }
+ requestOptions = requestOptionsAtomicAttribute()
+ ).getOrThrow()
-
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
holder = holderAgent,
- requestObjectJwsVerifier = verifierAttestationVerifier(sprsCryptoService.keyPairAdapter.jsonWebKey)
+ requestObjectJwsVerifier = verifierAttestationVerifier(sprsCryptoService.keyMaterial.jsonWebKey)
)
- val authnResponse =
- holderSiop.createAuthnResponse(authnRequestWithRequestObject).getOrThrow()
+ val authnResponse = holderSiop.createAuthnResponse(authnRequestWithRequestObject).getOrThrow()
authnResponse.shouldBeInstanceOf()
- .also { println(it) }
val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf()
@@ -292,23 +310,22 @@ class OidcSiopProtocolTest : FreeSpec({
}
}
"test with request object and invalid Attestation JWT" {
- val sprsCryptoService = DefaultCryptoService(RandomKeyPairAdapter())
- val attestationJwt = buildAttestationJwt(sprsCryptoService, relyingPartyUrl, verifierKeyPair)
+ val sprsCryptoService = DefaultCryptoService(EphemeralKeyWithoutCert())
+ val attestationJwt = buildAttestationJwt(sprsCryptoService, relyingPartyUrl, verifierKeyMaterial)
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
- attestationJwt = attestationJwt
+ clientIdScheme = OidcSiopVerifier.ClientIdScheme.VerifierAttestation(attestationJwt)
)
val authnRequestWithRequestObject = verifierSiop.createAuthnRequestUrlWithRequestObject(
walletUrl = walletUrl,
- requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023)
- ).getOrThrow().also { println(it) }
+ requestOptions = requestOptionsAtomicAttribute()
+ ).getOrThrow()
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
holder = holderAgent,
- requestObjectJwsVerifier = verifierAttestationVerifier(RandomKeyPairAdapter().jsonWebKey)
+ requestObjectJwsVerifier = verifierAttestationVerifier(EphemeralKeyWithoutCert().jsonWebKey)
)
shouldThrow {
holderSiop.createAuthnResponse(authnRequestWithRequestObject).getOrThrow()
@@ -318,19 +335,18 @@ class OidcSiopProtocolTest : FreeSpec({
"test with request object from request_uri as URL query parameters" {
val authnRequest = verifierSiop.createAuthnRequestUrl(
walletUrl = walletUrl,
- requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023)
- ).also { println(it) }
+ requestOptions = requestOptionsAtomicAttribute()
+ )
val clientId = Url(authnRequest).parameters["client_id"]!!
- val requestUrl = "https://www.example.com/request/${Random.nextBytes(32).encodeToString(Base64UrlStrict)}"
+ val requestUrl = "https://www.example.com/request/${uuid4()}"
val authRequestUrlWithRequestUri = URLBuilder(walletUrl).apply {
parameters.append("client_id", clientId)
parameters.append("request_uri", requestUrl)
}.buildString()
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
holder = holderAgent,
remoteResourceRetriever = {
if (it == requestUrl) authnRequest else null
@@ -339,7 +355,6 @@ class OidcSiopProtocolTest : FreeSpec({
val authnResponse = holderSiop.createAuthnResponse(authRequestUrlWithRequestUri).getOrThrow()
authnResponse.shouldBeInstanceOf()
- .also { println(it) }
val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf()
@@ -351,17 +366,16 @@ class OidcSiopProtocolTest : FreeSpec({
"test with request object from request_uri as JWS" {
val jar = verifierSiop.createAuthnRequestAsSignedRequestObject(
- requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023)
- ).getOrThrow().also { println(it.serialize()) }
+ requestOptions = requestOptionsAtomicAttribute()
+ ).getOrThrow()
- val requestUrl = "https://www.example.com/request/${Random.nextBytes(32).encodeToString(Base64UrlStrict)}"
+ val requestUrl = "https://www.example.com/request/${uuid4()}"
val authRequestUrlWithRequestUri = URLBuilder(walletUrl).apply {
parameters.append("client_id", relyingPartyUrl)
parameters.append("request_uri", requestUrl)
}.buildString()
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
holder = holderAgent,
remoteResourceRetriever = {
if (it == requestUrl) jar.serialize() else null
@@ -370,7 +384,6 @@ class OidcSiopProtocolTest : FreeSpec({
val authnResponse = holderSiop.createAuthnResponse(authRequestUrlWithRequestUri).getOrThrow()
authnResponse.shouldBeInstanceOf()
- .also { println(it) }
val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf()
@@ -379,19 +392,19 @@ class OidcSiopProtocolTest : FreeSpec({
it.vc.credentialSubject.shouldBeInstanceOf()
}
}
+
"test with request object not verified" {
val jar = verifierSiop.createAuthnRequestAsSignedRequestObject(
- requestOptions = RequestOptions(credentialScheme = ConstantIndex.AtomicAttribute2023)
- ).getOrThrow().also { println(it.serialize()) }
+ requestOptions = requestOptionsAtomicAttribute()
+ ).getOrThrow()
- val requestUrl = "https://www.example.com/request/${Random.nextBytes(32).encodeToString(Base64UrlStrict)}"
+ val requestUrl = "https://www.example.com/request/${uuid4()}"
val authRequestUrlWithRequestUri = URLBuilder(walletUrl).apply {
parameters.append("client_id", relyingPartyUrl)
parameters.append("request_uri", requestUrl)
}.buildString()
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
holder = holderAgent,
remoteResourceRetriever = {
if (it == requestUrl) jar.serialize() else null
@@ -405,13 +418,21 @@ class OidcSiopProtocolTest : FreeSpec({
}
})
+private fun requestOptionsAtomicAttribute() = RequestOptions(
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023,
+ )
+ ),
+)
+
private suspend fun buildAttestationJwt(
sprsCryptoService: DefaultCryptoService,
relyingPartyUrl: String,
- verifierKeyPair: KeyPairAdapter
+ verifierKeyMaterial: KeyMaterial
): JwsSigned = DefaultJwsService(sprsCryptoService).createSignedJws(
header = JwsHeader(
- algorithm = sprsCryptoService.keyPairAdapter.signingAlgorithm.toJwsAlgorithm().getOrThrow(),
+ algorithm = sprsCryptoService.keyMaterial.signatureAlgorithm.toJwsAlgorithm().getOrThrow(),
),
payload = JsonWebToken(
issuer = "sprs", // allows Wallet to determine the issuer's key
@@ -419,20 +440,21 @@ private suspend fun buildAttestationJwt(
issuedAt = Clock.System.now(),
expiration = Clock.System.now().plus(10.seconds),
notBefore = Clock.System.now(),
- confirmationKey = verifierKeyPair.jsonWebKey,
+ confirmationClaim = ConfirmationClaim(jsonWebKey = verifierKeyMaterial.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).getOrThrow() }
+ val attestationJwt = jws.header.attestationJwt?.let { JwsSigned.deserialize(it).getOrThrow() }
?: return false
val verifierJwsService = DefaultVerifierJwsService()
if (!verifierJwsService.verifyJws(attestationJwt, trustedKey))
return false
val verifierPublicKey = JsonWebToken.deserialize(attestationJwt.payload.decodeToString())
- .getOrNull()?.confirmationKey ?: return false
+ .getOrNull()?.confirmationClaim?.jsonWebKey
+ ?: return false
return verifierJwsService.verifyJws(jws, verifierPublicKey)
}
}
@@ -442,10 +464,16 @@ private suspend fun verifySecondProtocolRun(
walletUrl: String,
holderSiop: OidcSiopWallet
) {
- val authnRequestUrl = verifierSiop.createAuthnRequestUrl(walletUrl = walletUrl)
+ val authnRequestUrl = verifierSiop.createAuthnRequestUrl(walletUrl, defaultRequestOptions)
val authnResponse = holderSiop.createAuthnResponse(authnRequestUrl)
val validation = verifierSiop.validateAuthnResponse(
(authnResponse.getOrThrow() as AuthenticationResponseResult.Redirect).url
)
validation.shouldBeInstanceOf()
-}
\ No newline at end of file
+}
+
+private val defaultRequestOptions = RequestOptions(
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)
+ )
+)
\ No newline at end of file
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt
index a692a1ee8..3eba3ae9d 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopSdJwtProtocolTest.kt
@@ -1,14 +1,8 @@
package at.asitplus.wallet.lib.oidc
-import at.asitplus.wallet.lib.agent.Holder
-import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.IssuerAgent
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.agent.Verifier
-import at.asitplus.wallet.lib.agent.VerifierAgent
-import at.asitplus.wallet.lib.agent.toStoreCredentialInput
+import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.data.ConstantIndex
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_GIVEN_NAME
import at.asitplus.wallet.lib.oidc.OidcSiopVerifier.RequestOptions
import com.benasher44.uuid.uuid4
import io.kotest.core.spec.style.FreeSpec
@@ -24,8 +18,8 @@ class OidcSiopSdJwtProtocolTest : FreeSpec({
lateinit var relyingPartyUrl: String
lateinit var walletUrl: String
- lateinit var holderKeyPair: KeyPairAdapter
- lateinit var verifierKeyPair: KeyPairAdapter
+ lateinit var holderKeyMaterial: KeyMaterial
+ lateinit var verifierKeyMaterial: KeyMaterial
lateinit var holderAgent: Holder
lateinit var verifierAgent: Verifier
@@ -34,96 +28,62 @@ class OidcSiopSdJwtProtocolTest : FreeSpec({
lateinit var verifierSiop: OidcSiopVerifier
beforeEach {
- holderKeyPair = RandomKeyPairAdapter()
- verifierKeyPair = RandomKeyPairAdapter()
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ verifierKeyMaterial = EphemeralKeyWithoutCert()
relyingPartyUrl = "https://example.com/rp/${uuid4()}"
walletUrl = "https://example.com/wallet/${uuid4()}"
- holderAgent = HolderAgent(holderKeyPair)
- verifierAgent = VerifierAgent(verifierKeyPair)
+ holderAgent = HolderAgent(holderKeyMaterial)
+ verifierAgent = VerifierAgent(verifierKeyMaterial)
holderAgent.storeCredential(
IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithoutCert(),
DummyCredentialDataProvider(),
).issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.SD_JWT,
).getOrThrow().toStoreCredentialInput()
)
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
holder = holderAgent,
)
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
)
}
- "test with Fragment" {
- val authnRequest = verifierSiop.createAuthnRequestUrl(
- walletUrl = walletUrl,
- requestOptions = RequestOptions(
- representation = ConstantIndex.CredentialRepresentation.SD_JWT,
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- requestedAttributes = listOf("given_name")
- ),
- ).also { println(it) }
-
- val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow()
- authnResponse.shouldBeInstanceOf().also { println(it) }
-
- val result = verifierSiop.validateAuthnResponse(authnResponse.url)
- result.shouldBeInstanceOf()
- result.disclosures.shouldNotBeEmpty()
-
- assertSecondRun(verifierSiop, holderSiop, walletUrl)
- }
-
"Selective Disclosure with custom credential" {
- val requestedClaim = "given_name"
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ val requestedClaim = CLAIM_GIVEN_NAME
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
)
val authnRequest = verifierSiop.createAuthnRequestUrl(
walletUrl = walletUrl,
requestOptions = RequestOptions(
- representation = ConstantIndex.CredentialRepresentation.SD_JWT,
- credentialScheme = ConstantIndex.AtomicAttribute2023,
- requestedAttributes = listOf(requestedClaim)
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023,
+ ConstantIndex.CredentialRepresentation.SD_JWT,
+ listOf(requestedClaim)
+ )
+ )
)
- ).also { println(it) }
+ )
authnRequest shouldContain requestedClaim
val authnResponse = holderSiop.createAuthnResponse(authnRequest).getOrThrow()
- authnResponse.shouldBeInstanceOf().also { println(it) }
+ authnResponse.shouldBeInstanceOf()
val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf()
- val sdJwt = result.sdJwt.also { println(it) }
-
+ result.sdJwt.shouldNotBeNull()
result.disclosures.shouldNotBeEmpty()
result.disclosures.shouldBeSingleton()
result.disclosures.shouldHaveSingleElement { it.claimName == requestedClaim }
- sdJwt.shouldNotBeNull()
}
})
-
-private suspend fun assertSecondRun(
- verifierSiop: OidcSiopVerifier,
- holderSiop: OidcSiopWallet,
- walletUrl: String
-) {
- val authnRequestUrl = verifierSiop.createAuthnRequestUrl(
- walletUrl = walletUrl,
- requestOptions = RequestOptions(representation = ConstantIndex.CredentialRepresentation.SD_JWT)
- )
- val authnResponse = holderSiop.createAuthnResponse(authnRequestUrl)
- val url = (authnResponse.getOrThrow() as AuthenticationResponseResult.Redirect).url
- val validation = verifierSiop.validateAuthnResponse(url)
- validation.shouldBeInstanceOf()
-}
\ No newline at end of file
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWalletScopeSupportTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWalletScopeSupportTest.kt
index 314dd4035..67b876226 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWalletScopeSupportTest.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWalletScopeSupportTest.kt
@@ -1,21 +1,13 @@
package at.asitplus.wallet.lib.oidc
+import at.asitplus.dif.Constraint
+import at.asitplus.dif.ConstraintField
+import at.asitplus.dif.DifInputDescriptor
+import at.asitplus.dif.PresentationDefinition
import at.asitplus.jsonpath.core.NormalizedJsonPath
import at.asitplus.jsonpath.core.NormalizedJsonPathSegment
-import at.asitplus.wallet.lib.agent.Holder
-import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.IssuerAgent
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.agent.Verifier
-import at.asitplus.wallet.lib.agent.VerifierAgent
-import at.asitplus.wallet.lib.agent.toStoreCredentialInput
+import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.data.ConstantIndex
-import at.asitplus.wallet.lib.data.dif.Constraint
-import at.asitplus.wallet.lib.data.dif.ConstraintField
-import at.asitplus.wallet.lib.data.dif.InputDescriptor
-import at.asitplus.wallet.lib.data.dif.PresentationDefinition
-import at.asitplus.wallet.lib.data.dif.SchemaReference
import at.asitplus.wallet.lib.oidvci.OAuth2Exception
import at.asitplus.wallet.mdl.MobileDrivingLicenceDataElements
import at.asitplus.wallet.mdl.MobileDrivingLicenceScheme
@@ -37,18 +29,18 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({
"test scopes" - {
val testScopes = object {
- val EmptyPresentationRequest: String = "emptyPresentationRequest"
- val MdocMdlWithGivenName: String = "mdocMdlWithGivenName"
+ val emptyPresentationRequest: String = "emptyPresentationRequest"
+ val mdocMdlWithGivenName: String = "mdocMdlWithGivenName"
}
val testScopePresentationDefinitionRetriever = mapOf(
- testScopes.EmptyPresentationRequest to PresentationDefinition(
+ testScopes.emptyPresentationRequest to PresentationDefinition(
id = uuid4().toString(),
inputDescriptors = listOf()
),
- testScopes.MdocMdlWithGivenName to PresentationDefinition(
+ testScopes.mdocMdlWithGivenName to PresentationDefinition(
id = uuid4().toString(),
inputDescriptors = listOf(
- InputDescriptor(
+ DifInputDescriptor(
id = MobileDrivingLicenceScheme.isoDocType,
constraints = Constraint(
fields = listOf(
@@ -62,9 +54,6 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({
)
)
),
- schema = listOf(
- SchemaReference(MobileDrivingLicenceScheme.schemaUri)
- )
)
)
),
@@ -72,8 +61,8 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({
lateinit var relyingPartyUrl: String
- lateinit var holderKeyPair: KeyPairAdapter
- lateinit var verifierKeyPair: KeyPairAdapter
+ lateinit var holderKeyMaterial: KeyMaterial
+ lateinit var verifierKeyMaterial: KeyMaterial
lateinit var holderAgent: Holder
lateinit var verifierAgent: Verifier
@@ -82,45 +71,45 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({
lateinit var verifierSiop: OidcSiopVerifier
beforeEach {
- holderKeyPair = RandomKeyPairAdapter()
- verifierKeyPair = RandomKeyPairAdapter()
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ verifierKeyMaterial = EphemeralKeyWithoutCert()
relyingPartyUrl = "https://example.com/rp/${uuid4()}"
- holderAgent = HolderAgent(holderKeyPair)
- verifierAgent = VerifierAgent(verifierKeyPair)
+ holderAgent = HolderAgent(holderKeyMaterial)
+ verifierAgent = VerifierAgent(verifierKeyMaterial)
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
+ keyMaterial = holderKeyMaterial,
holder = holderAgent,
scopePresentationDefinitionRetriever = testScopePresentationDefinitionRetriever
)
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
relyingPartyUrl = relyingPartyUrl,
)
}
"get empty scope works even without available credentials" {
val issuerAgent = IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithSelfSignedCert(),
DummyCredentialDataProvider(),
)
holderAgent.storeCredential(
issuerAgent.issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.ISO_MDOC,
).getOrThrow().toStoreCredentialInput()
)
- val authnRequest = verifierSiop.createAuthnRequest().let { request ->
+ val authnRequest = verifierSiop.createAuthnRequest(defaultRequestOptions).let { request ->
request.copy(
presentationDefinition = null,
- scope = request.scope + " " + testScopes.EmptyPresentationRequest
+ scope = request.scope + " " + testScopes.emptyPresentationRequest
)
}
val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow()
- authnResponse.shouldBeInstanceOf().also { println(it) }
+ authnResponse.shouldBeInstanceOf()
val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf()
@@ -128,10 +117,10 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({
}
"get MdocMdlWithGivenName scope without available credentials fails" {
- val authnRequest = verifierSiop.createAuthnRequest().let { request ->
+ val authnRequest = verifierSiop.createAuthnRequest(defaultRequestOptions).let { request ->
request.copy(
presentationDefinition = null,
- scope = request.scope + " " + testScopes.MdocMdlWithGivenName
+ scope = request.scope + " " + testScopes.mdocMdlWithGivenName
)
}
@@ -144,31 +133,37 @@ class OidcSiopWalletScopeSupportTest : FreeSpec({
"get MdocMdlWithGivenName scope with available credentials succeeds" {
val issuerAgent = IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithSelfSignedCert(),
DummyCredentialDataProvider(),
)
holderAgent.storeCredential(
issuerAgent.issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
MobileDrivingLicenceScheme,
ConstantIndex.CredentialRepresentation.ISO_MDOC,
).getOrThrow().toStoreCredentialInput()
)
- val authnRequest = verifierSiop.createAuthnRequest().let { request ->
+ val authnRequest = verifierSiop.createAuthnRequest(defaultRequestOptions).let { request ->
request.copy(
presentationDefinition = null,
- scope = request.scope + " " + testScopes.MdocMdlWithGivenName
+ scope = request.scope + " " + testScopes.mdocMdlWithGivenName
)
}
val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow()
- authnResponse.shouldBeInstanceOf().also { println(it) }
+ authnResponse.shouldBeInstanceOf()
val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf()
result.document.validItems.shouldNotBeEmpty()
}
}
-})
\ No newline at end of file
+})
+
+private val defaultRequestOptions = OidcSiopVerifier.RequestOptions(
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(ConstantIndex.AtomicAttribute2023)
+ )
+)
\ No newline at end of file
diff --git a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt
index 725992079..ecc892996 100644
--- a/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt
+++ b/vck-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopX509SanDnsTest.kt
@@ -1,38 +1,28 @@
package at.asitplus.wallet.lib.oidc
-import at.asitplus.signum.indispensable.asn1.Asn1
+import at.asitplus.openid.OpenIdConstants
import at.asitplus.signum.indispensable.asn1.Asn1EncapsulatingOctetString
import at.asitplus.signum.indispensable.asn1.Asn1Primitive
import at.asitplus.signum.indispensable.asn1.Asn1String
import at.asitplus.signum.indispensable.asn1.KnownOIDs
+import at.asitplus.signum.indispensable.asn1.encoding.Asn1
import at.asitplus.signum.indispensable.pki.SubjectAltNameImplicitTags
import at.asitplus.signum.indispensable.pki.X509CertificateExtension
-import at.asitplus.wallet.lib.agent.Holder
-import at.asitplus.wallet.lib.agent.HolderAgent
-import at.asitplus.wallet.lib.agent.IssuerAgent
-import at.asitplus.wallet.lib.agent.KeyPairAdapter
-import at.asitplus.wallet.lib.agent.RandomKeyPairAdapter
-import at.asitplus.wallet.lib.agent.Verifier
-import at.asitplus.wallet.lib.agent.VerifierAgent
-import at.asitplus.wallet.lib.agent.toStoreCredentialInput
+import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.data.ConstantIndex
+import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_GIVEN_NAME
import at.asitplus.wallet.lib.oidc.OidcSiopVerifier.RequestOptions
import at.asitplus.wallet.lib.oidvci.formUrlEncode
-import com.benasher44.uuid.uuid4
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.types.shouldBeInstanceOf
class OidcSiopX509SanDnsTest : FreeSpec({
- lateinit var responseUrl: String
- lateinit var walletUrl: String
-
- lateinit var holderKeyPair: KeyPairAdapter
- lateinit var verifierKeyPair: KeyPairAdapter
+ lateinit var holderKeyMaterial: KeyMaterial
+ lateinit var verifierKeyMaterial: KeyMaterial
lateinit var holderAgent: Holder
- lateinit var verifierAgent: Verifier
lateinit var holderSiop: OidcSiopWallet
lateinit var verifierSiop: OidcSiopVerifier
@@ -49,46 +39,47 @@ class OidcSiopX509SanDnsTest : FreeSpec({
)
}
))))
- holderKeyPair = RandomKeyPairAdapter()
- verifierKeyPair = RandomKeyPairAdapter(extensions)
- responseUrl = "https://example.com"
- walletUrl = "https://example.com/wallet/${uuid4()}"
- holderAgent = HolderAgent(holderKeyPair)
- verifierAgent = VerifierAgent(verifierKeyPair)
+ holderKeyMaterial = EphemeralKeyWithoutCert()
+ verifierKeyMaterial = EphemeralKeyWithSelfSignedCert(extensions = extensions)
+ holderAgent = HolderAgent(holderKeyMaterial)
holderAgent.storeCredential(
IssuerAgent(
- RandomKeyPairAdapter(),
+ EphemeralKeyWithoutCert(),
DummyCredentialDataProvider(),
).issueCredential(
- holderKeyPair.publicKey,
+ holderKeyMaterial.publicKey,
ConstantIndex.AtomicAttribute2023,
ConstantIndex.CredentialRepresentation.SD_JWT,
).getOrThrow().toStoreCredentialInput()
)
- holderSiop = OidcSiopWallet.newDefaultInstance(
- keyPairAdapter = holderKeyPair,
+ holderSiop = OidcSiopWallet(
+ keyMaterial = holderKeyMaterial,
holder = holderAgent,
)
- verifierSiop = OidcSiopVerifier.newInstance(
- verifier = verifierAgent,
- relyingPartyUrl = null,
- responseUrl = responseUrl,
- x5c = listOf(verifierKeyPair.certificate!!)
+ verifierSiop = OidcSiopVerifier(
+ keyMaterial = verifierKeyMaterial,
+ clientIdScheme = OidcSiopVerifier.ClientIdScheme.CertificateSanDns(listOf(verifierKeyMaterial.getCertificate()!!)),
)
}
"test with Fragment" {
val authnRequest = verifierSiop.createAuthnRequestAsSignedRequestObject(
requestOptions = RequestOptions(
- representation = ConstantIndex.CredentialRepresentation.SD_JWT,
+ credentials = setOf(
+ OidcSiopVerifier.RequestOptionsCredential(
+ ConstantIndex.AtomicAttribute2023,
+ ConstantIndex.CredentialRepresentation.SD_JWT,
+ listOf(CLAIM_GIVEN_NAME)
+ )
+ ),
responseMode = OpenIdConstants.ResponseMode.DIRECT_POST_JWT,
- requestedAttributes = listOf("given_name")
+ responseUrl = "https://example.com/response",
)
- ).also { println(it) }.getOrThrow()
+ ).getOrThrow()
val authnResponse = holderSiop.createAuthnResponse(authnRequest.serialize()).getOrThrow()
- authnResponse.shouldBeInstanceOf