From d95cf8512a84596ab8b29443a559e7784528c808 Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Wed, 10 Jul 2024 15:41:23 +0200 Subject: [PATCH 1/8] add ClientAttesatiton to KeyStore interface Signed-off-by: Johannes Tuerk --- .../KeyStore/Services/IKeyStore.cs | 4 +++ .../Models/ClientAttestationPopOptions.cs | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/WalletFramework.SdJwtVc/Models/ClientAttestationPopOptions.cs diff --git a/src/WalletFramework.SdJwtVc/KeyStore/Services/IKeyStore.cs b/src/WalletFramework.SdJwtVc/KeyStore/Services/IKeyStore.cs index fa8fee3c..d1e807f9 100644 --- a/src/WalletFramework.SdJwtVc/KeyStore/Services/IKeyStore.cs +++ b/src/WalletFramework.SdJwtVc/KeyStore/Services/IKeyStore.cs @@ -1,3 +1,5 @@ +using WalletFramework.SdJwtVc.Models; + namespace WalletFramework.SdJwtVc.KeyStore.Services { /// @@ -43,6 +45,8 @@ public interface IKeyStore /// Task GenerateDPopProofOfPossessionAsync(string keyId, string audience, string? nonce, string? accessToken); + Task CreateClientAttestationProofOfPossession(string keyId, ClientAttestationPopOptions popOptions); + /// /// Asynchronously loads a key by its identifier and returns it as a JSON Web Key (JWK) containing the public key /// information. diff --git a/src/WalletFramework.SdJwtVc/Models/ClientAttestationPopOptions.cs b/src/WalletFramework.SdJwtVc/Models/ClientAttestationPopOptions.cs new file mode 100644 index 00000000..2fd9fc95 --- /dev/null +++ b/src/WalletFramework.SdJwtVc/Models/ClientAttestationPopOptions.cs @@ -0,0 +1,28 @@ +using static System.String; + +namespace WalletFramework.SdJwtVc.Models; + +public record ClientAttestationPopOptions +{ + public string Audience { get; } + public string Issuer { get; } + //TODO: Change nullable to Option<> when available + public string? Nonce { get; } + + private ClientAttestationPopOptions(string audience, string issuer, string? nonce) + { + Audience = audience; + Issuer = issuer; + Nonce = nonce; + } + + public static ClientAttestationPopOptions CreateClientAttestationPopOptions(string audience, string issuer, string? nonce) + { + if (IsNullOrWhiteSpace(audience) || IsNullOrWhiteSpace(issuer)) + { + throw new ArgumentException("Audience and Issuer must be provided."); + } + + return new ClientAttestationPopOptions(audience, issuer, nonce); + } +} From 4b0559c99416e70d893a14f0893b6476a3834cfc Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 16 Jul 2024 18:21:42 +0200 Subject: [PATCH 2/8] mdoc oid4vci Signed-off-by: Kevin --- .../Storage/DefaultWalletRecordService.cs | 137 ++++- .../Storage/IWalletRecordService.cs | 40 +- .../Storage/Models/RecordTagAttribute.cs | 11 + .../Storage/Records/RecordBase.cs | 2 +- src/WalletFramework.Core/Colors/Color.cs | 49 ++ .../Credentials/CredentialId.cs | 32 + .../Credentials/Errors/CredentialIdError.cs | 5 + .../Cryptography/Abstractions/IKeyStore.cs | 80 +++ .../Cryptography/Models/KeyId.cs | 31 + .../Functional/Enumerable/EnumerableFun.cs | 6 + .../Functional}/Error.cs | 4 +- .../Errors/EnumerableIsEmptyError.cs | 3 + .../Errors/NoItemsSucceededValidationError.cs | 3 + .../Errors/StringIsNullOrWhitespaceError.cs | 3 + .../Functional/OptionFun.cs | 96 +++ .../Functional}/TaskFun.cs | 3 +- .../Functional/Validation.cs | 308 ++++++++++ src/WalletFramework.Core/IsExternalInit.cs | 7 + .../Json/Converters/DictJsonConverter.cs | 28 + .../Json/Converters/OneOfJsonConverter.cs | 29 + .../Json/Converters/OptionJsonConverter.cs | 28 + .../Json/Converters/ValueTypeJsonConverter.cs | 48 ++ .../Json/Errors/InvalidJsonError.cs | 6 + .../Json/Errors/JTokenIsNotAJValueError.cs | 7 + .../Json/Errors/JTokenIsNotAnJArrayError.cs | 6 + .../Json/Errors/JTokenIsNotAnJObjectError.cs | 6 + .../Json/Errors/JValueIsNotAnIntError.cs | 14 + .../Json/Errors/JsonFieldNotFoundError.cs | 5 + .../JsonFieldValueIsNullOrWhitespaceError.cs | 6 + .../Json/Errors/JsonIsNotAMapError.cs | 7 + src/WalletFramework.Core/Json/JsonFun.cs | 174 ++++++ src/WalletFramework.Core/Json/JsonSettings.cs | 11 + .../Localization/Constants.cs | 10 + .../Localization/Errors/LocaleError.cs | 14 + .../Localization/Locale.cs | 93 +++ src/WalletFramework.Core/Uri/UriFun.cs | 6 + .../WalletFramework.Core.csproj | 12 + src/WalletFramework.Functional/OptionFun.cs | 10 - src/WalletFramework.Functional/Validation.cs | 215 ------- .../CborByteString.cs | 6 +- .../CborFun.cs | 9 +- .../Common/Constants.cs | 2 +- .../Common/Errors.cs | 4 +- .../CoseLabel.cs | 8 +- .../CoseSignature.cs | 4 +- .../DeviceSigned.cs | 5 +- .../Digest.cs | 6 +- .../DigestAlgorithm.cs | 8 +- .../DigestId.cs | 4 +- .../DocType.cs | 17 +- .../IsExternalInit.cs | 0 .../IssuerAuth.cs | 19 +- .../IssuerSigned.cs | 14 +- .../IssuerSignedItem.cs | 35 +- .../Mdoc.cs | 30 +- .../MobileSecurityObject.cs | 40 +- .../NameSpace.cs | 11 +- .../NameSpaces.cs | 11 +- .../ProtectedHeaders.cs | 10 +- .../UnprotectedHeaders.cs | 23 +- .../ValidityInfo.cs | 8 +- .../ValueDigests.cs | 10 +- .../WalletFramework.MdocLib.csproj} | 6 +- src/WalletFramework.MdocVc/ClaimDisplay.cs | 20 + src/WalletFramework.MdocVc/ClaimName.cs | 25 + src/WalletFramework.MdocVc/Common/Errors.cs | 6 + .../IsExternalInit.cs | 0 src/WalletFramework.MdocVc/MdocDisplay.cs | 202 +++++++ src/WalletFramework.MdocVc/MdocLogo.cs | 20 + src/WalletFramework.MdocVc/MdocName.cs | 25 + src/WalletFramework.MdocVc/MdocRecord.cs | 123 ++++ .../WalletFramework.MdocVc.csproj} | 5 +- .../Oid4Vci/Abstractions/IMdocStorage.cs | 22 + .../Abstractions/IOid4VciClientService.cs | 50 ++ .../Abstractions/IAuthFlowSessionStorage.cs | 45 ++ .../AuthFlow/Errors/VciSessionIdError.cs | 5 + .../Implementations/AuthFlowSessionStorage.cs | 52 ++ .../Models/AuthorizationCodeParameters.cs | 38 ++ .../AuthFlow/Models/AuthorizationData.cs | 11 + .../AuthFlow/Models/AuthorizationDetails.cs | 58 ++ .../Oid4Vci/AuthFlow/Models/ClientOptions.cs | 54 ++ .../AuthFlow/Models/IssuanceSession.cs | 44 ++ .../Models/PushedAuthorizationRequest.cs | 99 ++++ .../PushedAuthorizationRequestResponse.cs | 16 + .../Oid4Vci/AuthFlow/Models/VciSessionId.cs | 49 ++ .../AuthFlow/Records/AuthFlowSessionRecord.cs | 103 ++++ .../Abstractions/ITokenService.cs | 12 + .../DPop/Abstractions/IDPopHttpClient.cs | 11 + .../DPop/Implementations/DPopHttpClient.cs | 111 ++++ .../Oid4Vci/Authorization/DPop/Models/DPop.cs | 11 + .../Authorization/DPop/Models/DPopConfig.cs | 24 + .../DPop/Models/DPopHttpResponse.cs | 3 + .../Authorization/DPop/Models/DPopNonce.cs | 18 + .../Authorization/DPop/Models/DPopToken.cs | 16 + .../Errors/AuthorizationServerIdError.cs | 5 + .../Implementations/TokenService.cs | 63 ++ .../Models/AuthorizationServerId.cs | 36 ++ .../Models/AuthorizationServerMetadata.cs | 78 +++ .../Models/Mdoc/MdocTokenRequest.cs | 21 + .../Authorization/Models/OAuthToken.cs | 75 +++ .../Authorization/Models/TokenRequest.cs | 88 +++ .../BatchSizeIsNotAPositiveNumberError.cs | 5 + .../Errors/FormatNotSupportedError.cs | 5 + .../Errors/ProofTypeIdNotSupportedError.cs | 5 + .../Models/CredentialConfiguration.cs | 132 +++++ .../Models/CredentialDisplay.cs | 118 ++++ .../Models/CredentialLogo.cs | 65 ++ .../Models/CredentialName.cs | 32 + .../Models/CryptograhicSigningAlgValue.cs | 33 ++ .../Models/CryptographicBindingMethod.cs | 32 + .../CredConfiguration/Models/Format.cs | 35 ++ .../Models/Mdoc/ClaimsMetadata.cs | 88 +++ .../Models/Mdoc/CryptoGraphicCurve.cs | 20 + .../Models/Mdoc/CryptographicSuite.cs | 31 + .../Models/Mdoc/ElementDisplay.cs | 44 ++ .../Models/Mdoc/ElementMetadata.cs | 63 ++ .../Models/Mdoc/ElementName.cs | 31 + .../Models/Mdoc/MdocConfiguration.cs | 146 +++++ .../CredConfiguration/Models/Mdoc/Policy.cs | 85 +++ .../CredConfiguration/Models/ProofTypeId.cs | 37 ++ .../Models/ProofTypeMetadata.cs | 31 + .../Oid4Vci/CredConfiguration/Models/Scope.cs | 32 + .../Models/SdJwt/SdJwtConfiguration.cs | 97 +++ .../SupportedCredentialConfiguration.cs | 31 + .../Abstractions/ICredentialOfferService.cs | 10 + .../CouldNotFetchCredentialOfferError.cs | 7 + .../Errors/CredentialConfigurationIdError.cs | 7 + ...lConfigurationIdIsNullOrWhitespaceError.cs | 6 + .../CredOffer/Errors/CredentialIssuerError.cs | 7 + ...CredentialOfferHasNoQueryParameterError.cs | 6 + .../Errors/CredentialOfferNotFoundError.cs | 6 + .../CredOffer/GrantTypes/AuthorizationCode.cs | 55 ++ .../CredOffer/GrantTypes/PreAuthorizedCode.cs | 155 +++++ .../Implementations/CredentialOfferService.cs | 53 ++ .../Models/CredentialConfigurationId.cs | 47 ++ .../CredOffer/Models/CredentialOffer.cs | 82 +++ .../Models/CredentialOfferMetadata.cs | 5 + .../Oid4Vci/CredOffer/Models/Grants.cs | 55 ++ .../Abstractions/ICredentialRequestService.cs | 21 + .../CredentialRequestService.cs | 137 +++++ .../CredRequest/Models/CredentialRequest.cs | 27 + .../Models/Mdoc/MdocCredentialRequest.cs | 43 ++ .../CredRequest/Models/ProofOfPossession.cs | 23 + .../Models/SdJwt/SdJwtCredentialRequest.cs | 41 ++ .../CredResponse/CredentialResponse.cs | 139 +++++ .../Oid4Vci/CredResponse/Mdoc/EncodedMdoc.cs | 31 + .../CredResponse/SdJwt/EncodedSdJwt.cs | 38 ++ .../CredResponse/SdJwt/Errors/SdJwtError.cs | 8 + .../Oid4Vci/CredResponse/TransactionId.cs | 6 + .../Extensions/Oid4VciHttpClientExtensions.cs | 36 +- .../Oid4Vci/Implementations/MdocFun.cs | 41 ++ .../Oid4Vci/Implementations/MdocStorage.cs | 68 +++ .../Implementations/Oid4VciClientService.cs | 351 +++++++++++ .../Implementations/SdJwtRecordExtensions.cs | 65 ++ .../Abstractions/IIssuerMetadataService.cs | 11 + .../Issuer/Errors/CredentialEndpointError.cs | 5 + .../Issuer/Errors/CredentialIssuerIdError.cs | 5 + .../Implementations/IssuerMetadataService.cs | 42 ++ .../Issuer/Models/CredentialIssuerId.cs | 36 ++ .../Oid4Vci/Issuer/Models/IssuerMetadata.cs | 202 +++++++ .../Oid4Vci/Issuer/Models/IssuerName.cs | 26 + .../AuthorizationCodeParameters.cs | 39 -- .../Models/Authorization/AuthorizationData.cs | 27 - .../Authorization/AuthorizationDetails.cs | 41 -- .../AuthorizationServerMetadata.cs | 79 --- .../Models/Authorization/ClientOptions.cs | 55 -- .../PushedAuthorizationRequest.cs | 100 ---- .../PushedAuthorizationRequestResponse.cs | 17 - .../Models/Authorization/TokenRequest.cs | 91 --- .../Models/Authorization/TokenResponse.cs | 74 --- .../VciAuthorizationSessionRecord.cs | 64 -- .../Models/Authorization/VciSessionId.cs | 32 - .../GrantTypes/AuthorizationCode.cs | 23 - .../GrantTypes/PreAuthorizedCode.cs | 47 -- .../Oid4Vci/Models/CredentialOffer/Grants.cs | 26 - .../CredentialOffer/OidCredentialOffer.cs | 33 -- .../CredentialRequest/OidCredentialRequest.cs | 37 -- .../CredentialRequest/OidProofOfPossession.cs | 23 - .../OidCredentialResponse.cs | 39 -- .../Oid4Vci/Models/DPop/OAuthToken.cs | 33 -- .../Models/IssuanceSessionParameters.cs | 44 -- .../Credential/OidCredentialDisplay.cs | 40 -- .../Metadata/Credential/OidCredentialLogo.cs | 22 - .../Credential/OidCredentialMetadata.cs | 85 --- .../Models/Metadata/Issuer/IssuerDisplay.cs | 62 ++ .../Models/Metadata/Issuer/IssuerLogo.cs | 67 +++ .../Metadata/Issuer/OidIssuerDisplay.cs | 28 - .../Models/Metadata/Issuer/OidIssuerLogo.cs | 22 - .../Metadata/Issuer/OidIssuerMetadata.cs | 130 ---- .../Models/Metadata/IssuerMetadataSet.cs | 32 + .../Oid4Vci/Models/Metadata/MetadataSet.cs | 29 - .../Oid4Vci/Services/ISessionRecordService.cs | 41 -- .../IOid4VciClientService.cs | 65 -- .../Oid4VciClientService.cs | 559 ------------------ .../Oid4Vci/Services/SessionRecordService.cs | 67 --- .../Services/PexService.cs | 2 +- .../Oid4Vp/Services/IOid4VpHaipClient.cs | 43 +- .../Oid4Vp/Services/Oid4VpClientService.cs | 311 +++++----- .../SeviceCollectionExtensions.cs | 34 +- .../WalletFramework.Oid4Vc.csproj | 7 +- .../KeyStore/Services/IKeyStore.cs | 69 --- .../Credential/CredentialDisplayMetadata.cs | 58 -- .../Models/Credential/SdJwtDisplay.cs | 57 ++ ...CredentialMetadata.cs => SdJwtMetadata.cs} | 4 +- .../Models/Issuer/IssuerDisplay.cs | 46 -- .../Models/Issuer/IssuerMetadata.cs | 113 ---- .../Models/Records/SdJwtRecord.cs | 391 ++++++------ src/WalletFramework.SdJwtVc/Models/Vct.cs | 30 + .../ServiceCollectionExtensions.cs | 12 +- .../DefaultSdJwtVcHolderService.cs | 140 ----- .../ISdJwtVcHolderService.cs | 135 ++--- .../SdJwtVcHolderService.cs | 113 ++++ .../WalletFramework.SdJwtVc.csproj | 2 +- src/WalletFramework.sln | 36 +- .../Routing/RoutingInboxHandlerTests.cs | 8 +- test/WalletFramework.Mdoc.Tests/Samples.cs | 68 --- .../Helpers.cs | 2 +- .../MdocTests.cs | 59 +- test/WalletFramework.MdocLib.Tests/Samples.cs | 71 +++ .../WalletFramework.MdocLib.Tests.csproj} | 5 +- .../MdocRecordTests.cs | 36 ++ .../MdocVcSamples.cs | 12 + .../WalletFramework.MdocVc.Tests.csproj | 25 + .../Extensions/ObjectExtensions.cs | 19 +- .../AuthFlow/AuthFlowSessionRecordTests.cs | 78 +++ .../AuthFlow/Samples/AuthFlowSamples.cs | 38 ++ .../MdocConfigurationTests.cs | 78 +++ .../SdJwtConfigurationTests.cs | 10 + .../Oid4Vci/CredOffer/CredentialOfferTests.cs | 81 +++ .../Oid4Vci/Issuer/IssuerMetadataTests.cs | 84 +++ .../Oid4Vci/IssuerMetadataTests.cs | 108 ++-- .../Oid4Vci/Localization/LocaleTests.cs | 75 +++ .../Localization/Samples/LocaleSample.cs | 15 + .../Oid4Vci/Samples/CredentialOfferSample.cs | 43 ++ .../Oid4Vci/Samples/IssuerMetadataSample.cs | 60 ++ .../Samples/Mdoc/MdocConfigurationSample.cs | 99 ++++ .../Samples/SdJwt/SdJwtConfigurationSample.cs | 95 +++ .../Services/Oid4VciClientServiceTests.cs | 216 ------- .../Services/Oid4VpClientServiceTests.cs | 293 +++++---- .../Services/PexServiceTests.cs | 35 +- test/WalletFramework.Oid4Vc.Tests/Samples.cs | 166 ------ .../WalletFramework.Oid4Vc.Tests.csproj | 7 +- .../SdJwtRecordTests.cs | 12 +- .../SdJwtVcHolderServiceTests.cs | 10 +- .../WalletFramework.SdJwtVc.Tests.csproj | 2 +- 245 files changed, 8212 insertions(+), 4064 deletions(-) create mode 100644 src/Hyperledger.Aries/Storage/Models/RecordTagAttribute.cs create mode 100644 src/WalletFramework.Core/Colors/Color.cs create mode 100644 src/WalletFramework.Core/Credentials/CredentialId.cs create mode 100644 src/WalletFramework.Core/Credentials/Errors/CredentialIdError.cs create mode 100644 src/WalletFramework.Core/Cryptography/Abstractions/IKeyStore.cs create mode 100644 src/WalletFramework.Core/Cryptography/Models/KeyId.cs create mode 100644 src/WalletFramework.Core/Functional/Enumerable/EnumerableFun.cs rename src/{WalletFramework.Functional => WalletFramework.Core/Functional}/Error.cs (89%) create mode 100644 src/WalletFramework.Core/Functional/Errors/EnumerableIsEmptyError.cs create mode 100644 src/WalletFramework.Core/Functional/Errors/NoItemsSucceededValidationError.cs create mode 100644 src/WalletFramework.Core/Functional/Errors/StringIsNullOrWhitespaceError.cs create mode 100644 src/WalletFramework.Core/Functional/OptionFun.cs rename src/{WalletFramework.Functional => WalletFramework.Core/Functional}/TaskFun.cs (96%) create mode 100644 src/WalletFramework.Core/Functional/Validation.cs create mode 100644 src/WalletFramework.Core/IsExternalInit.cs create mode 100644 src/WalletFramework.Core/Json/Converters/DictJsonConverter.cs create mode 100644 src/WalletFramework.Core/Json/Converters/OneOfJsonConverter.cs create mode 100644 src/WalletFramework.Core/Json/Converters/OptionJsonConverter.cs create mode 100644 src/WalletFramework.Core/Json/Converters/ValueTypeJsonConverter.cs create mode 100644 src/WalletFramework.Core/Json/Errors/InvalidJsonError.cs create mode 100644 src/WalletFramework.Core/Json/Errors/JTokenIsNotAJValueError.cs create mode 100644 src/WalletFramework.Core/Json/Errors/JTokenIsNotAnJArrayError.cs create mode 100644 src/WalletFramework.Core/Json/Errors/JTokenIsNotAnJObjectError.cs create mode 100644 src/WalletFramework.Core/Json/Errors/JValueIsNotAnIntError.cs create mode 100644 src/WalletFramework.Core/Json/Errors/JsonFieldNotFoundError.cs create mode 100644 src/WalletFramework.Core/Json/Errors/JsonFieldValueIsNullOrWhitespaceError.cs create mode 100644 src/WalletFramework.Core/Json/Errors/JsonIsNotAMapError.cs create mode 100644 src/WalletFramework.Core/Json/JsonFun.cs create mode 100644 src/WalletFramework.Core/Json/JsonSettings.cs create mode 100644 src/WalletFramework.Core/Localization/Constants.cs create mode 100644 src/WalletFramework.Core/Localization/Errors/LocaleError.cs create mode 100644 src/WalletFramework.Core/Localization/Locale.cs create mode 100644 src/WalletFramework.Core/Uri/UriFun.cs create mode 100644 src/WalletFramework.Core/WalletFramework.Core.csproj delete mode 100644 src/WalletFramework.Functional/OptionFun.cs delete mode 100644 src/WalletFramework.Functional/Validation.cs rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/CborByteString.cs (89%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/CborFun.cs (91%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/Common/Constants.cs (90%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/Common/Errors.cs (96%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/CoseLabel.cs (86%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/CoseSignature.cs (83%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/DeviceSigned.cs (83%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/Digest.cs (81%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/DigestAlgorithm.cs (91%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/DigestId.cs (90%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/DocType.cs (68%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/IsExternalInit.cs (100%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/IssuerAuth.cs (78%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/IssuerSigned.cs (74%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/IssuerSignedItem.cs (87%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/Mdoc.cs (90%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/MobileSecurityObject.cs (66%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/NameSpace.cs (75%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/NameSpaces.cs (85%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/ProtectedHeaders.cs (93%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/UnprotectedHeaders.cs (85%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/ValidityInfo.cs (93%) rename src/{WalletFramework.Mdoc => WalletFramework.MdocLib}/ValueDigests.cs (76%) rename src/{WalletFramework.Mdoc/WalletFramework.Mdoc.csproj => WalletFramework.MdocLib/WalletFramework.MdocLib.csproj} (71%) create mode 100644 src/WalletFramework.MdocVc/ClaimDisplay.cs create mode 100644 src/WalletFramework.MdocVc/ClaimName.cs create mode 100644 src/WalletFramework.MdocVc/Common/Errors.cs rename src/{WalletFramework.Functional => WalletFramework.MdocVc}/IsExternalInit.cs (100%) create mode 100644 src/WalletFramework.MdocVc/MdocDisplay.cs create mode 100644 src/WalletFramework.MdocVc/MdocLogo.cs create mode 100644 src/WalletFramework.MdocVc/MdocName.cs create mode 100644 src/WalletFramework.MdocVc/MdocRecord.cs rename src/{WalletFramework.Functional/WalletFramework.Functional.csproj => WalletFramework.MdocVc/WalletFramework.MdocVc.csproj} (59%) create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IMdocStorage.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Errors/VciSessionIdError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationCodeParameters.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationData.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/ClientOptions.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/IssuanceSession.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequest.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequestResponse.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/VciSessionId.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Abstractions/ITokenService.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Abstractions/IDPopHttpClient.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPop.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopConfig.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopHttpResponse.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopNonce.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopToken.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Errors/AuthorizationServerIdError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Implementations/TokenService.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerId.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerMetadata.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/Mdoc/MdocTokenRequest.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/TokenRequest.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/BatchSizeIsNotAPositiveNumberError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/FormatNotSupportedError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/ProofTypeIdNotSupportedError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialConfiguration.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialName.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptograhicSigningAlgValue.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptographicBindingMethod.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Format.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ClaimsMetadata.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptoGraphicCurve.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptographicSuite.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementDisplay.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementMetadata.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementName.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/MdocConfiguration.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/Policy.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeId.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeMetadata.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Scope.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SupportedCredentialConfiguration.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Abstractions/ICredentialOfferService.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CouldNotFetchCredentialOfferError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialConfigurationIdError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialConfigurationIdIsNullOrWhitespaceError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialIssuerError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialOfferHasNoQueryParameterError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialOfferNotFoundError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/GrantTypes/AuthorizationCode.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/GrantTypes/PreAuthorizedCode.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Implementations/CredentialOfferService.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialConfigurationId.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialOffer.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialOfferMetadata.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/Grants.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/Mdoc/MdocCredentialRequest.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/ProofOfPossession.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/SdJwt/SdJwtCredentialRequest.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/CredentialResponse.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/Mdoc/EncodedMdoc.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/SdJwt/EncodedSdJwt.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/SdJwt/Errors/SdJwtError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/TransactionId.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocFun.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocStorage.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/SdJwtRecordExtensions.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Abstractions/IIssuerMetadataService.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Errors/CredentialEndpointError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Errors/CredentialIssuerIdError.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Implementations/IssuerMetadataService.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/CredentialIssuerId.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerMetadata.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerName.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationCodeParameters.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationData.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationDetails.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationServerMetadata.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/ClientOptions.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/PushedAuthorizationRequest.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/PushedAuthorizationRequestResponse.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/TokenRequest.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/TokenResponse.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/VciAuthorizationSessionRecord.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/VciSessionId.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/GrantTypes/AuthorizationCode.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/GrantTypes/PreAuthorizedCode.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/Grants.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/OidCredentialOffer.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialRequest/OidCredentialRequest.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialRequest/OidProofOfPossession.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialResponse/OidCredentialResponse.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/DPop/OAuthToken.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/IssuanceSessionParameters.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialDisplay.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialLogo.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialMetadata.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerDisplay.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerLogo.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerDisplay.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerLogo.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerMetadata.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/IssuerMetadataSet.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/MetadataSet.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Services/ISessionRecordService.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Services/Oid4VciClientService/IOid4VciClientService.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Services/Oid4VciClientService/Oid4VciClientService.cs delete mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/Services/SessionRecordService.cs delete mode 100644 src/WalletFramework.SdJwtVc/KeyStore/Services/IKeyStore.cs delete mode 100644 src/WalletFramework.SdJwtVc/Models/Credential/CredentialDisplayMetadata.cs create mode 100644 src/WalletFramework.SdJwtVc/Models/Credential/SdJwtDisplay.cs rename src/WalletFramework.SdJwtVc/Models/Credential/{CredentialMetadata.cs => SdJwtMetadata.cs} (97%) delete mode 100644 src/WalletFramework.SdJwtVc/Models/Issuer/IssuerDisplay.cs delete mode 100644 src/WalletFramework.SdJwtVc/Models/Issuer/IssuerMetadata.cs create mode 100644 src/WalletFramework.SdJwtVc/Models/Vct.cs delete mode 100644 src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/DefaultSdJwtVcHolderService.cs create mode 100644 src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs delete mode 100644 test/WalletFramework.Mdoc.Tests/Samples.cs rename test/{WalletFramework.Mdoc.Tests => WalletFramework.MdocLib.Tests}/Helpers.cs (87%) rename test/{WalletFramework.Mdoc.Tests => WalletFramework.MdocLib.Tests}/MdocTests.cs (80%) create mode 100644 test/WalletFramework.MdocLib.Tests/Samples.cs rename test/{WalletFramework.Mdoc.Tests/WalletFramework.Mdoc.Tests.csproj => WalletFramework.MdocLib.Tests/WalletFramework.MdocLib.Tests.csproj} (84%) create mode 100644 test/WalletFramework.MdocVc.Tests/MdocRecordTests.cs create mode 100644 test/WalletFramework.MdocVc.Tests/MdocVcSamples.cs create mode 100644 test/WalletFramework.MdocVc.Tests/WalletFramework.MdocVc.Tests.csproj create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/MdocConfigurationTests.cs create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwtConfigurationTests.cs create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredOffer/CredentialOfferTests.cs create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Localization/LocaleTests.cs create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Localization/Samples/LocaleSample.cs create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/CredentialOfferSample.cs create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/IssuerMetadataSample.cs create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/Mdoc/MdocConfigurationSample.cs create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/SdJwt/SdJwtConfigurationSample.cs delete mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Services/Oid4VciClientServiceTests.cs delete mode 100644 test/WalletFramework.Oid4Vc.Tests/Samples.cs diff --git a/src/Hyperledger.Aries/Storage/DefaultWalletRecordService.cs b/src/Hyperledger.Aries/Storage/DefaultWalletRecordService.cs index a6c5918c..f9b3af8e 100644 --- a/src/Hyperledger.Aries/Storage/DefaultWalletRecordService.cs +++ b/src/Hyperledger.Aries/Storage/DefaultWalletRecordService.cs @@ -6,9 +6,11 @@ using Hyperledger.Aries.Agents; using Hyperledger.Aries.Extensions; using Hyperledger.Aries.Features.PresentProof; +using Hyperledger.Aries.Storage.Models; using Hyperledger.Indy.NonSecretsApi; using Hyperledger.Indy.WalletApi; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Hyperledger.Aries.Storage { @@ -32,47 +34,80 @@ public DefaultWalletRecordService() } /// - public virtual Task AddAsync(Wallet wallet, T record) - where T : RecordBase, new() + public virtual Task AddAsync(Wallet wallet, T record, Func? encode = null) where T : RecordBase, new() { record.CreatedAtUtc = DateTime.UtcNow; + + var properties = record + .GetType() + .GetProperties() + .Where(info => Attribute.IsDefined(info, typeof(RecordTagAttribute))); + + foreach (var property in properties) + { + var value = property.GetValue(record); + record.SetTag(property.Name, value.ToString(), false); + } + + var recordJson = encode is null + ? record.ToJson(_jsonSettings) + : encode(record).ToString(); return NonSecrets.AddRecordAsync(wallet, record.TypeName, record.Id, - record.ToJson(_jsonSettings), + recordJson, record.Tags.ToJson()); } /// - public virtual async Task> SearchAsync(Wallet wallet, ISearchQuery query, SearchOptions options, int count, int skip) - where T : RecordBase, new() + public virtual async Task> SearchAsync( + Wallet wallet, + ISearchQuery? query = null, + SearchOptions? options = null, + int count = 10, + int skip = 0, + Func? decode = null) where T : RecordBase, new() { using var search = await NonSecrets.OpenSearchAsync( wallet, new T().TypeName, (query ?? SearchQuery.Empty).ToJson(), (options ?? new SearchOptions()).ToJson() - ); + ); if(skip > 0) { await search.NextAsync(wallet, skip); } - var result = JsonConvert.DeserializeObject(await search.NextAsync(wallet, count), _jsonSettings); - - return result.Records? - .Select(x => - { - var record = JsonConvert.DeserializeObject(x.Value, _jsonSettings); - - foreach (var tag in x.Tags) - record.Tags[tag.Key] = tag.Value; - - return record; - }) - .ToList() - ?? new List(); + var searchResultStr = await search.NextAsync(wallet, count); + var searchResult = JsonConvert.DeserializeObject(searchResultStr, _jsonSettings); + + if (searchResult?.Records is null) + { + return new List(); + } + + var records = searchResult.Records.Select(searchItem => + { + T record; + if (decode is null) + { + record = JsonConvert.DeserializeObject(searchItem.Value, _jsonSettings)!; + } + else + { + var json = JObject.Parse(searchItem.Value); + record = decode(json); + } + + foreach (var tag in searchItem.Tags) + record.Tags[tag.Key] = tag.Value; + + return record; + }); + + return records.ToList(); } /// @@ -91,8 +126,27 @@ await NonSecrets.UpdateRecordTagsAsync(wallet, record.Tags.ToJson(_jsonSettings)); } + public async Task Update(Wallet wallet, T record, Func? encode = null) where T : RecordBase + { + record.UpdatedAtUtc = DateTime.UtcNow; + + var recordJson = encode is null + ? record.ToJson(_jsonSettings) + : encode(record).ToString(); + + await NonSecrets.UpdateRecordValueAsync(wallet, + record.TypeName, + record.Id, + recordJson); + + await NonSecrets.UpdateRecordTagsAsync(wallet, + record.TypeName, + record.Id, + record.Tags.ToJson(_jsonSettings)); + } + /// - public virtual async Task GetAsync(Wallet wallet, string id) where T : RecordBase, new() + public async Task GetAsync(Wallet wallet, string id, Func? decode = null) where T : RecordBase, new() { try { @@ -106,9 +160,18 @@ await NonSecrets.UpdateRecordTagsAsync(wallet, return null; } - var item = JsonConvert.DeserializeObject(searchItemJson, _jsonSettings); + var item = JsonConvert.DeserializeObject(searchItemJson, _jsonSettings)!; - var record = JsonConvert.DeserializeObject(item.Value, _jsonSettings); + T record; + if (decode is null) + { + record = JsonConvert.DeserializeObject(item.Value, _jsonSettings)!; + } + else + { + var json = JObject.Parse(item.Value); + record = decode(json); + } foreach (var tag in item.Tags) record.Tags[tag.Key] = tag.Value; @@ -127,7 +190,7 @@ await NonSecrets.UpdateRecordTagsAsync(wallet, try { var record = await GetAsync(wallet, id); - var typeName = new T().TypeName; + var typeName = record.TypeName; await NonSecrets.DeleteRecordTagsAsync( wallet: wallet, @@ -147,5 +210,31 @@ await NonSecrets.DeleteRecordAsync( return false; } } + + /// + public async Task Delete(Wallet wallet, RecordBase record) + { + try + { + var typeName = record.TypeName; + + await NonSecrets.DeleteRecordTagsAsync( + wallet: wallet, + type: typeName, + id: record.Id, + tagsJson: record.Tags.Select(x => x.Key).ToArray().ToJson()); + await NonSecrets.DeleteRecordAsync( + wallet: wallet, + type: typeName, + id: record.Id); + + return true; + } + catch (Exception e) + { + Debug.WriteLine($"Couldn't delete record: {e}"); + return false; + } + } } } diff --git a/src/Hyperledger.Aries/Storage/IWalletRecordService.cs b/src/Hyperledger.Aries/Storage/IWalletRecordService.cs index dd9d6d5d..3adde782 100644 --- a/src/Hyperledger.Aries/Storage/IWalletRecordService.cs +++ b/src/Hyperledger.Aries/Storage/IWalletRecordService.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Hyperledger.Indy.WalletApi; +using Newtonsoft.Json.Linq; namespace Hyperledger.Aries.Storage { @@ -15,11 +17,12 @@ public interface IWalletRecordService /// The record async. /// Wallet. /// Record. + /// The func for encoding the record to JSON format /// The 1st type parameter. - Task AddAsync(Wallet wallet, T record) where T : RecordBase, new(); - + Task AddAsync(Wallet wallet, T record, Func? encode = null) where T : RecordBase, new(); + /// - /// Searchs the records async. + /// Searches the records async. /// /// The records async. /// Wallet. @@ -27,8 +30,15 @@ public interface IWalletRecordService /// Options. /// The number of items to return /// The number of items to skip + /// Func for decoding the JSON to the record /// The 1st type parameter. - Task> SearchAsync(Wallet wallet, ISearchQuery query = null, SearchOptions options = null, int count = 10, int skip = 0) where T : RecordBase, new(); + Task> SearchAsync( + Wallet wallet, + ISearchQuery? query = null, + SearchOptions? options = null, + int count = 10, + int skip = 0, + Func? decode = null) where T : RecordBase, new(); /// /// Updates the record async. @@ -37,6 +47,15 @@ public interface IWalletRecordService /// Wallet. /// Credential record. Task UpdateAsync(Wallet wallet, RecordBase record); + + /// + /// Updates the record async. + /// + /// The record async. + /// Wallet. + /// Credential record. + /// The func for encoding the record to JSON format + Task Update(Wallet wallet, T record, Func? encode = null) where T : RecordBase; /// /// Gets the record async. @@ -44,8 +63,9 @@ public interface IWalletRecordService /// The record async. /// Wallet. /// Identifier. + /// Func for decoding the JSON to the record /// The 1st type parameter. - Task GetAsync(Wallet wallet, string id) where T : RecordBase, new(); + Task GetAsync(Wallet wallet, string id, Func? decode = null) where T : RecordBase, new(); /// /// Deletes the record async. @@ -55,5 +75,13 @@ public interface IWalletRecordService /// Record Identifier. /// Boolean status indicating if the removal succeed Task DeleteAsync(Wallet wallet, string id) where T : RecordBase, new(); + + /// + /// Deletes the record async. + /// + /// Wallet. + /// + /// Boolean status indicating if the removal succeed + Task Delete(Wallet wallet, RecordBase record); } } diff --git a/src/Hyperledger.Aries/Storage/Models/RecordTagAttribute.cs b/src/Hyperledger.Aries/Storage/Models/RecordTagAttribute.cs new file mode 100644 index 00000000..b4891474 --- /dev/null +++ b/src/Hyperledger.Aries/Storage/Models/RecordTagAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Hyperledger.Aries.Storage.Models +{ + /// + /// Defines an attribute to be also saved as a tag in the record + /// + public class RecordTagAttribute : Attribute + { + } +} diff --git a/src/Hyperledger.Aries/Storage/Records/RecordBase.cs b/src/Hyperledger.Aries/Storage/Records/RecordBase.cs index b8b718f7..a4ca5550 100644 --- a/src/Hyperledger.Aries/Storage/Records/RecordBase.cs +++ b/src/Hyperledger.Aries/Storage/Records/RecordBase.cs @@ -34,7 +34,7 @@ public DateTime? UpdatedAtUtc /// Gets or sets the tags. /// The tags. [JsonIgnore] - protected internal Dictionary Tags { get; set; } = new(); + public Dictionary Tags { get; set; } = new(); /// /// Get and set the schema version of a wallet record diff --git a/src/WalletFramework.Core/Colors/Color.cs b/src/WalletFramework.Core/Colors/Color.cs new file mode 100644 index 00000000..6d644a6c --- /dev/null +++ b/src/WalletFramework.Core/Colors/Color.cs @@ -0,0 +1,49 @@ +using System.Drawing; +using LanguageExt; +using Newtonsoft.Json; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.Core.Colors; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct Color +{ + private System.Drawing.Color Value { get; } + + private Color(System.Drawing.Color value) + { + Value = value; + } + + public override string ToString() => Value.ToHex(); + + public System.Drawing.Color ToSystemColor() => Value; + + public static implicit operator System.Drawing.Color(Color color) => color.Value; + + public static implicit operator Color(System.Drawing.Color systemColor) => new(systemColor); + + public static implicit operator string(Color color) => color.ToString(); + + public static Option OptionColor(string hexStr) + { + try + { + var colorConverter = new ColorConverter(); + var systemColor = (System.Drawing.Color)colorConverter.ConvertFromString(hexStr); + return systemColor.ToFrameworkColor(); + } + catch (Exception) + { + return Option.None; + } + } +} + +public static class ColorFun +{ + public static string ToHex(this System.Drawing.Color systemColor) => + $"#{systemColor.R:X2}{systemColor.G:X2}{systemColor.B:X2}"; + + public static Color ToFrameworkColor(this System.Drawing.Color systemColor) => systemColor; +} diff --git a/src/WalletFramework.Core/Credentials/CredentialId.cs b/src/WalletFramework.Core/Credentials/CredentialId.cs new file mode 100644 index 00000000..732ed46b --- /dev/null +++ b/src/WalletFramework.Core/Credentials/CredentialId.cs @@ -0,0 +1,32 @@ +using WalletFramework.Core.Credentials.Errors; +using WalletFramework.Core.Functional; + +namespace WalletFramework.Core.Credentials; + +public readonly struct CredentialId +{ + private string Value { get; } + + private CredentialId(string value) + { + Value = value; + } + + public override string ToString() => Value; + + public static implicit operator string(CredentialId credentialId) => credentialId.Value; + + public static CredentialId CreateCredentialId() + { + var id = Guid.NewGuid().ToString(); + return new CredentialId(id); + } + + public static Validation ValidCredentialId(string id) + { + var isValid = Guid.TryParse(id, out _); + return isValid + ? new CredentialId(id) + : new CredentialIdError(id); + } +} diff --git a/src/WalletFramework.Core/Credentials/Errors/CredentialIdError.cs b/src/WalletFramework.Core/Credentials/Errors/CredentialIdError.cs new file mode 100644 index 00000000..6529716d --- /dev/null +++ b/src/WalletFramework.Core/Credentials/Errors/CredentialIdError.cs @@ -0,0 +1,5 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Core.Credentials.Errors; + +public record CredentialIdError(string Value) : Error($"The CredentialId is not a valid GUID, value is: {Value}"); diff --git a/src/WalletFramework.Core/Cryptography/Abstractions/IKeyStore.cs b/src/WalletFramework.Core/Cryptography/Abstractions/IKeyStore.cs new file mode 100644 index 00000000..7459d6d2 --- /dev/null +++ b/src/WalletFramework.Core/Cryptography/Abstractions/IKeyStore.cs @@ -0,0 +1,80 @@ +using WalletFramework.Core.Cryptography.Models; + +namespace WalletFramework.Core.Cryptography.Abstractions; + +/// +/// Represents a store for managing keys. +/// This interface is intended to be implemented outside of the framework on the device side, +/// allowing flexibility in key generation or retrieval mechanisms. +/// +public interface IKeyStore +{ + /// + /// Asynchronously generates a key for the specified algorithm and returns the key identifier. + /// + /// The algorithm for key generation (default is "ES256"). + /// A representing the generated key's identifier as a string. + Task GenerateKey(string alg = "ES256"); + + /// + /// Asynchronously creates a proof of possession for a specific key, based on the provided audience and nonce. + /// + /// The identifier of the key to be used in creating the proof of possession. + /// The intended recipient of the proof. Typically represents the entity that will verify it. + /// + /// A unique token, typically used to prevent replay attacks by ensuring that the proof is only used once. + /// + /// The type of the proof. (For example "openid4vci-proof+jwt") + /// Base64url-encoded hash digest over the Issuer-signed JWT and the selected Disclosures for integrity protection + /// + /// A representing the asynchronous operation. When evaluated, the task's result contains + /// the proof. + /// + Task GenerateKbProofOfPossessionAsync( + KeyId keyId, + string audience, + string nonce, + string type, + string? sdHash = null, + string? clientId = null); + + /// + /// Asynchronously creates a DPoP Proof JWT for a specific key, based on the provided audience, nonce and access token. + /// + /// The identifier of the key to be used in creating the proof of possession. + /// The intended recipient of the proof. Typically represents the entity that will verify it. + /// A unique token, typically used to prevent replay attacks by ensuring that the proof is only used once. + /// The access token, that the DPoP Proof JWT is bound to + /// + /// A representing the asynchronous operation. When evaluated, the task's result contains + /// the DPoP Proof JWT. + /// + Task GenerateDPopProofOfPossessionAsync( + KeyId keyId, + string audience, + string? nonce, + string? accessToken); + + /// + /// Asynchronously loads a key by its identifier and returns it as a JSON Web Key (JWK) containing the public key + /// information. + /// + /// The identifier of the key to load. + /// A representing the loaded key as a JWK string. + Task LoadKey(KeyId keyId); + + /// + /// Asynchronously signs the given payload using the key identified by the provided key ID. + /// + /// The identifier of the key to use for signing. + /// The payload to sign. + /// A representing the signed payload as a byte array. + Task Sign(KeyId keyId, byte[] payload); + + /// + /// Asynchronously deletes the key associated with the provided key ID. + /// + /// The identifier of the key that should be deleted + /// A representing the asynchronous operation. + Task DeleteKey(KeyId keyId); +} diff --git a/src/WalletFramework.Core/Cryptography/Models/KeyId.cs b/src/WalletFramework.Core/Cryptography/Models/KeyId.cs new file mode 100644 index 00000000..8b56acb2 --- /dev/null +++ b/src/WalletFramework.Core/Cryptography/Models/KeyId.cs @@ -0,0 +1,31 @@ +using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; + +namespace WalletFramework.Core.Cryptography.Models; + +public readonly struct KeyId +{ + private string Value { get; } + + private KeyId(string value) => Value = value; + + public override string ToString() => Value; + + public static implicit operator string(KeyId keyId) => keyId.Value; + + public static Validation ValidKeyId(string keyId) + { + if (string.IsNullOrWhiteSpace(keyId)) + { + return new StringIsNullOrWhitespaceError(); + } + + return new KeyId(keyId); + } + + public static KeyId CreateKeyId() + { + var id = Guid.NewGuid().ToString(); + return new KeyId(id); + } +} diff --git a/src/WalletFramework.Core/Functional/Enumerable/EnumerableFun.cs b/src/WalletFramework.Core/Functional/Enumerable/EnumerableFun.cs new file mode 100644 index 00000000..d606203a --- /dev/null +++ b/src/WalletFramework.Core/Functional/Enumerable/EnumerableFun.cs @@ -0,0 +1,6 @@ +namespace WalletFramework.Core.Functional.Enumerable; + +public static class EnumerableFun +{ + public static bool IsEmpty(this IEnumerable enumerable) => !enumerable.Any(); +} diff --git a/src/WalletFramework.Functional/Error.cs b/src/WalletFramework.Core/Functional/Error.cs similarity index 89% rename from src/WalletFramework.Functional/Error.cs rename to src/WalletFramework.Core/Functional/Error.cs index 5a4ccebe..9638f6e7 100644 --- a/src/WalletFramework.Functional/Error.cs +++ b/src/WalletFramework.Core/Functional/Error.cs @@ -1,7 +1,7 @@ using LanguageExt; -using static WalletFramework.Functional.ValidationFun; +using static WalletFramework.Core.Functional.ValidationFun; -namespace WalletFramework.Functional; +namespace WalletFramework.Core.Functional; public abstract record Error(string Message, Option Exception) { diff --git a/src/WalletFramework.Core/Functional/Errors/EnumerableIsEmptyError.cs b/src/WalletFramework.Core/Functional/Errors/EnumerableIsEmptyError.cs new file mode 100644 index 00000000..7fbd8e12 --- /dev/null +++ b/src/WalletFramework.Core/Functional/Errors/EnumerableIsEmptyError.cs @@ -0,0 +1,3 @@ +namespace WalletFramework.Core.Functional.Errors; + +public record EnumerableIsEmptyError() : Error($"The enumerable with items of type `{typeof(T).Name}` is empty"); diff --git a/src/WalletFramework.Core/Functional/Errors/NoItemsSucceededValidationError.cs b/src/WalletFramework.Core/Functional/Errors/NoItemsSucceededValidationError.cs new file mode 100644 index 00000000..0fc80a36 --- /dev/null +++ b/src/WalletFramework.Core/Functional/Errors/NoItemsSucceededValidationError.cs @@ -0,0 +1,3 @@ +namespace WalletFramework.Core.Functional.Errors; + +public record NoItemsSucceededValidationError() : Error($"No Validations of Type `{typeof(T).Name}` were successful"); diff --git a/src/WalletFramework.Core/Functional/Errors/StringIsNullOrWhitespaceError.cs b/src/WalletFramework.Core/Functional/Errors/StringIsNullOrWhitespaceError.cs new file mode 100644 index 00000000..87cc5806 --- /dev/null +++ b/src/WalletFramework.Core/Functional/Errors/StringIsNullOrWhitespaceError.cs @@ -0,0 +1,3 @@ +namespace WalletFramework.Core.Functional.Errors; + +public record StringIsNullOrWhitespaceError() : Error($"The string is null or whitespace for Type: `{nameof(T)}`"); diff --git a/src/WalletFramework.Core/Functional/OptionFun.cs b/src/WalletFramework.Core/Functional/OptionFun.cs new file mode 100644 index 00000000..a937a96f --- /dev/null +++ b/src/WalletFramework.Core/Functional/OptionFun.cs @@ -0,0 +1,96 @@ +using LanguageExt; +using WalletFramework.Core.Functional.Enumerable; + +namespace WalletFramework.Core.Functional; + +public static class OptionFun +{ + public static Option Some(T value) => value; + + public static Option None() => Option.None; + + public static Option ParseOption(T? value) => value ?? Option.None; + + public static Option OnSome(this Option option, Func> t2Func) => + from t1 in option + from t2 in t2Func(t1) + select t2; + + public static Option OnSome(this Option option, Func t2Func) => + from t1 in option + let t2 = t2Func(t1) + select t2; + + public static T? ToNullable(this Option option) => + option.MatchUnsafe( + Some: value => value, + None: () => default); + + public static T Fallback(this Option option, Func fallbackFunc) + { + var f = fallbackFunc(); + return option.Fallback(f); + } + + public static T Fallback(this Option option, T fallback) => + option.Match( + t => t, + () => fallback + ); + + /// + /// Traverses an enumerable for option + /// + /// + /// A option of an enumerable of every item. + /// In case the enumerable is empty, the traverse will return None + /// + /// The traverse only succeeds when every item of the enumerable returns Some. If you want to + /// ignore all the None items and only keep the Some items use + public static Option> TraverseAll( + this IEnumerable enumerable, + Func> optionFunc) + { + var list = enumerable.ToList(); + if (list.IsEmpty()) + return Option>.None; + + return list + .Select(optionFunc) + .Traverse(t => t); + } + + /// + /// Traverses an enumerable for option + /// + /// + /// A option of an enumerable which contains the items where the optionFunc returned Some + /// + /// + /// The traverse will ignore the None items and only keep Some items. If you want the traverse to + /// only return Some when everything is Some use + public static Option> TraverseAny( + this IEnumerable enumerable, + Func> optionFunc) + { + var items = enumerable.ToList(); + if (items.IsEmpty()) + return Option>.None; + + var traverse = items + .Select(optionFunc) + .Where(validation => validation.IsSome) + .Traverse(validation => validation); + + return traverse.OnSome(traversedItems => + { + var list = traversedItems.ToList(); + return list.Any() ? list : Option>.None; + }); + } + + public static T UnwrapOrThrow(this Option option, Exception e) => + option.Match( + t => t, + () => throw e); +} diff --git a/src/WalletFramework.Functional/TaskFun.cs b/src/WalletFramework.Core/Functional/TaskFun.cs similarity index 96% rename from src/WalletFramework.Functional/TaskFun.cs rename to src/WalletFramework.Core/Functional/TaskFun.cs index 965035f3..b9f00943 100644 --- a/src/WalletFramework.Functional/TaskFun.cs +++ b/src/WalletFramework.Core/Functional/TaskFun.cs @@ -1,8 +1,9 @@ -namespace WalletFramework.Functional; +namespace WalletFramework.Core.Functional; public enum TaskCompletionResult { Successful, + Error, Exceptional } diff --git a/src/WalletFramework.Core/Functional/Validation.cs b/src/WalletFramework.Core/Functional/Validation.cs new file mode 100644 index 00000000..e77dbae6 --- /dev/null +++ b/src/WalletFramework.Core/Functional/Validation.cs @@ -0,0 +1,308 @@ +using LanguageExt; +using WalletFramework.Core.Functional.Enumerable; +using WalletFramework.Core.Functional.Errors; + +namespace WalletFramework.Core.Functional; + +public readonly struct Validation +{ + public Validation(Validation value) + { + Value = value; + } + + public Validation Value { get; } + + public bool IsSuccess => Value.IsSuccess; + + public bool IsFail => Value.IsFail; + + public static implicit operator Validation(Validation value) => value.Value; + + public static implicit operator Validation(Validation value) => new(value); + + public static implicit operator Validation(Error error) => new(Validation.Fail(Seq.create(error))); + + public static implicit operator Validation(Seq errors) => new(Validation.Fail(errors)); + + public static implicit operator Validation(T value) => new(Validation.Success(value)); +} + +public delegate Validation Validator(T value); + +public delegate Validation Validator(T1 value); + +public static class ValidationFun +{ + public static Validation>>>>> Apply( + this Validation> valF, + Validation valT) => + Apply(valF.Select(Prelude.curry), valT); + + public static Validation>>>> Apply( + this Validation> valF, + Validation valT) => + Apply(valF.Select(Prelude.curry), valT); + + public static Validation>>> Apply( + this Validation> valF, + Validation valT) => + Apply(valF.Select(Prelude.curry), valT); + + public static Validation>> Apply( + this Validation> valF, + Validation valT) => + Apply(valF.Select(Prelude.curry), valT); + + public static Validation> Apply( + this Validation> valF, + Validation valT) => + Apply(valF.Select(Prelude.curry), valT); + + public static Validation Apply( + this Validation> valF, + Validation valT) => + valF.Value.Match( + f => + valT.Value.Match( + t => Valid(f(t)), + errors => errors + ), + errors => + valT.Value.Match( + _ => errors, + errorsT => errors + errorsT + ) + ); + + public static T2 Match( + this Validation validation, + Func valid, + Func, T2> invalid) => + validation.Value.Match(valid, invalid); + + public static async Task Match( + this Task> validation, + Func> valid, + Func, Task> invalid) => + await (await validation).Value.MatchAsync(valid, invalid); + + public static Unit Match( + this Validation validation, + Action valid, + Action> invalid) => + validation.Value.Match(valid, invalid); + + public static async Task> Select( + this Task> validation, + Func> task) => + await (await validation).Value.MatchAsync( + async value => Valid(await task(value)), + error => error + ); + + public static Validation Select( + this Validation validation, + Func func) => + validation.Value.Select(func); + + public static async Task> SelectMany( + this Validation validation, + Func>> bind, + Func project) + { + var bindResult = + await validation.Value.MatchAsync( + async t => (await bind(t)).Value, + error => error); + + return validation.Value.SelectMany(_ => bindResult, project); + } + + public static async Task> SelectMany( + this Task> validation, + Func>> bind, + Func project) + { + var validationValue = await validation; + return await validationValue.SelectMany(bind, project); + } + + public static Validation SelectMany( + this Validation validation, + Func> bind, + Func project) => + new(validation.Value.SelectMany(t => bind(t).Value, project)); + + public static Option ToOption(this Validation validation) => validation.Value.ToOption(); + + public static T Fallback(this Validation validation, Func fallbackFunc) => + validation.ToOption().Fallback(fallbackFunc); + + public static T Fallback(this Validation validation, T fallback) => + validation.ToOption().Fallback(fallback); + + /// + /// Traverses an enumerable for validation + /// + /// + /// A validation of an enumerable of every item. + /// In case the enumerable is empty, the validation will result in a + /// + /// The traverse only succeeds when every item of the enumerable is valid. If you want to + /// ignore all the invalid items and only keep the valid items use + public static Validation> TraverseAll( + this IEnumerable enumerable, + Func> validationFunc) + { + var list = enumerable.ToList(); + if (list.IsEmpty()) + return new EnumerableIsEmptyError(); + + return list + .Select(t => validationFunc(t).Value) + .Traverse(t => t); + } + + /// + /// Traverses an enumerable for validation + /// + /// + /// A validation of an enumerable which contains the items where the validation succeeded + /// The validation will result in a , in case + /// no items succeeded the validation + /// + /// + /// The traverse will ignore the invalid items and only keep the valid items. If you want the validation to + /// only succeed when everything is valid use + public static Validation> TraverseAny( + this IEnumerable enumerable, + Func> validationFunc) + { + var items = enumerable.ToList(); + if (items.IsEmpty()) + return new EnumerableIsEmptyError(); + + Validation> traverse = items + .Select(t => validationFunc(t).Value) + .Where(validation => validation.IsSuccess) + .Traverse(validation => validation); + + return traverse.OnSuccess(traversedItems => + { + Validation> result; + var list = traversedItems.ToList(); + if (list.Any()) + result = list; + else + result = new NoItemsSucceededValidationError(); + + return result; + }); + } + + public static Validation OnSuccess(this Validation validation, Func> onSucc) => + from t1 in validation + from t2 in onSucc(t1) + select t2; + + public static Validation OnSuccess(this Validation validation, Func onSucc) => + from t1 in validation + let t2 = onSucc(t1) + select t2; + + public static Task> OnSuccess(this Validation validation, Func onSucc) + { + var adapter = new Func>(async arg => + { + await onSucc(arg); + return Unit.Default; + }); + + return validation.OnSuccess(async t1 => await adapter(t1)); + } + + public static Task> OnSuccess( + this Task> validation, + Func>> onSucc) => + from t1 in validation + from t2 in onSucc(t1) + select t2; + + public static Task> OnSuccess( + this Validation validation, + Func>> onSucc) => + from t1 in validation + from t2 in onSucc(t1) + select t2; + + public static Task> OnSuccess( + this Validation validation, + Func> onSucc) => + validation.OnSuccess(async t1 => + { + var taskT2 = onSucc(t1); + var t2 = await taskT2; + return Valid(t2); + }); + + public static async Task> OnSuccess( + this Task> validation, + Func onSucc) + { + var validationT1 = await validation; + return + from t1 in validationT1 + let t2 = onSucc(t1) + select t2; + } + + public static Validation Invalid(Seq errors) => Validation.Fail(errors); + + public static Validation Valid(T value) => value; + + public static Validation Flatten(this Validation> stackedValidation) => + from outer in stackedValidation + from inner in outer + select inner; + + public static T UnwrapOrThrow(this Validation validation, Exception e) => + validation.Match( + t => t, + _ => throw e); + + public static Validator AggregateValidators(this IEnumerable> validators) => t => + { + var errors = validators + .Select(validate => validate(t)) + .SelectMany(validation => validation.Match( + _ => Option>.None, + errors => errors) + ) + .SelectMany(seq => seq) + .ToSeq(); + + return errors.Any() + ? Invalid(errors) + : t; + }; + + /// + /// Iterates through all validators and returns the first validator that succeeded + /// + /// The first successful validator or when nothing was sucessful + /// This is early out, which means after the first validator was successful it will not compute the other ones + public static Validator FirstValid(this IEnumerable> validators) => t => + { + foreach (var validator in validators) + { + var x = validator(t); + if (x.IsSuccess) + { + return x; + } + } + + return new NoItemsSucceededValidationError(); + }; +} diff --git a/src/WalletFramework.Core/IsExternalInit.cs b/src/WalletFramework.Core/IsExternalInit.cs new file mode 100644 index 00000000..eaa352a3 --- /dev/null +++ b/src/WalletFramework.Core/IsExternalInit.cs @@ -0,0 +1,7 @@ +namespace System.Runtime.CompilerServices; + +// This is needed for the init property setter. This can be removed when updating to a newer C# Version. +// https://stackoverflow.com/questions/64749385/predefined-type-system-runtime-compilerservices-isexternalinit-is-not-defined +internal static class IsExternalInit +{ +} diff --git a/src/WalletFramework.Core/Json/Converters/DictJsonConverter.cs b/src/WalletFramework.Core/Json/Converters/DictJsonConverter.cs new file mode 100644 index 00000000..699ac9b6 --- /dev/null +++ b/src/WalletFramework.Core/Json/Converters/DictJsonConverter.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace WalletFramework.Core.Json.Converters; + +public sealed class DictJsonConverter : JsonConverter> +{ + public override bool CanRead => false; + + public override void WriteJson(JsonWriter writer, Dictionary? dict, JsonSerializer serializer) + { + var dictJson = new JObject(); + foreach (var (key, config) in dict!) + { + var x = JObject.FromObject(config!); + dictJson.Add(key!.ToString(), x); + } + serializer.Serialize(writer, dictJson); + } + + public override Dictionary ReadJson( + JsonReader reader, + Type objectType, + Dictionary? existingValue, + bool hasExistingValue, + JsonSerializer serializer) => + throw new NotImplementedException(); +} diff --git a/src/WalletFramework.Core/Json/Converters/OneOfJsonConverter.cs b/src/WalletFramework.Core/Json/Converters/OneOfJsonConverter.cs new file mode 100644 index 00000000..6ec2cba8 --- /dev/null +++ b/src/WalletFramework.Core/Json/Converters/OneOfJsonConverter.cs @@ -0,0 +1,29 @@ +using LanguageExt; +using Newtonsoft.Json; +using OneOf; + +namespace WalletFramework.Core.Json.Converters; + +public class OneOfJsonConverter : JsonConverter where TOneOf : OneOfBase +{ + public override void WriteJson(JsonWriter writer, TOneOf? oneOf, JsonSerializer serializer) + { + oneOf!.Match( + t1 => + { + serializer.Serialize(writer, t1); + return Unit.Default; + }, + t2 => + { + serializer.Serialize(writer, t2); + return Unit.Default; + }); + } + + public override TOneOf ReadJson(JsonReader reader, Type objectType, TOneOf? existingValue, bool hasExistingValue, + JsonSerializer serializer) => + throw new NotImplementedException(); + + public override bool CanRead => false; +} diff --git a/src/WalletFramework.Core/Json/Converters/OptionJsonConverter.cs b/src/WalletFramework.Core/Json/Converters/OptionJsonConverter.cs new file mode 100644 index 00000000..0f5c50ac --- /dev/null +++ b/src/WalletFramework.Core/Json/Converters/OptionJsonConverter.cs @@ -0,0 +1,28 @@ +using LanguageExt; +using Newtonsoft.Json; + +namespace WalletFramework.Core.Json.Converters; + +public sealed class OptionJsonConverter : JsonConverter> +{ + public override void WriteJson(JsonWriter writer, Option option, JsonSerializer serializer) + { + option.Match( + t => + { + serializer.Serialize(writer, t); + }, + () => serializer.Serialize(writer, null) + ); + } + + public override Option ReadJson( + JsonReader reader, + Type objectType, + Option existingValue, + bool hasExistingValue, + JsonSerializer serializer) => + throw new NotImplementedException(); + + public override bool CanRead => false; +} diff --git a/src/WalletFramework.Core/Json/Converters/ValueTypeJsonConverter.cs b/src/WalletFramework.Core/Json/Converters/ValueTypeJsonConverter.cs new file mode 100644 index 00000000..2289d809 --- /dev/null +++ b/src/WalletFramework.Core/Json/Converters/ValueTypeJsonConverter.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace WalletFramework.Core.Json.Converters; + +public interface IValueTypeDecoder +{ + public T Decode(JToken token); +} + +public sealed class ValueTypeJsonConverter : JsonConverter +{ + public override bool CanRead => false; + + public override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer) + { + var str = value!.ToString(); + writer.WriteValue(str); + } + + public override T ReadJson( + JsonReader reader, + Type objectType, + T? existingValue, + bool hasExistingValue, + JsonSerializer serializer) => throw new NotImplementedException(); +} + +public sealed class ValueTypeJsonConverter : JsonConverter where TDecoder : IValueTypeDecoder +{ + public override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer) + { + var str = value!.ToString(); + writer.WriteValue(str); + } + + public override T ReadJson( + JsonReader reader, + Type objectType, + T? existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + var token = JToken.Load(reader); + var decoder = (TDecoder)Activator.CreateInstance(typeof(TDecoder)); + return decoder.Decode(token); + } +} diff --git a/src/WalletFramework.Core/Json/Errors/InvalidJsonError.cs b/src/WalletFramework.Core/Json/Errors/InvalidJsonError.cs new file mode 100644 index 00000000..dc243765 --- /dev/null +++ b/src/WalletFramework.Core/Json/Errors/InvalidJsonError.cs @@ -0,0 +1,6 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Core.Json.Errors; + +public record InvalidJsonError(string Json, Exception E) + : Error($"The JSON could not be parsed. JSON Value is `{Json}`", E); diff --git a/src/WalletFramework.Core/Json/Errors/JTokenIsNotAJValueError.cs b/src/WalletFramework.Core/Json/Errors/JTokenIsNotAJValueError.cs new file mode 100644 index 00000000..32f66850 --- /dev/null +++ b/src/WalletFramework.Core/Json/Errors/JTokenIsNotAJValueError.cs @@ -0,0 +1,7 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Core.Json.Errors; + +// ReSharper disable once InconsistentNaming +public record JTokenIsNotAJValueError(string Token, Exception E) + : Error($"The token `{Token}` could not be transformed into a JValue", E); diff --git a/src/WalletFramework.Core/Json/Errors/JTokenIsNotAnJArrayError.cs b/src/WalletFramework.Core/Json/Errors/JTokenIsNotAnJArrayError.cs new file mode 100644 index 00000000..a1769011 --- /dev/null +++ b/src/WalletFramework.Core/Json/Errors/JTokenIsNotAnJArrayError.cs @@ -0,0 +1,6 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Core.Json.Errors; + +public record JTokenIsNotAnJArrayError(string Name, Exception E) + : Error($"The field '{Name}' is not an JArray.", E); diff --git a/src/WalletFramework.Core/Json/Errors/JTokenIsNotAnJObjectError.cs b/src/WalletFramework.Core/Json/Errors/JTokenIsNotAnJObjectError.cs new file mode 100644 index 00000000..3e7b9f44 --- /dev/null +++ b/src/WalletFramework.Core/Json/Errors/JTokenIsNotAnJObjectError.cs @@ -0,0 +1,6 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Core.Json.Errors; + +public record JTokenIsNotAnJObjectError(string Name, Exception E) + : Error($"The field '{Name}' is not an JObject.", E); diff --git a/src/WalletFramework.Core/Json/Errors/JValueIsNotAnIntError.cs b/src/WalletFramework.Core/Json/Errors/JValueIsNotAnIntError.cs new file mode 100644 index 00000000..508d7b1d --- /dev/null +++ b/src/WalletFramework.Core/Json/Errors/JValueIsNotAnIntError.cs @@ -0,0 +1,14 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Core.Json.Errors; + +public record JValueIsNotAnIntError : Error +{ + public JValueIsNotAnIntError(string value, Exception e) : base($"The JValue is not an int. Actual value is `{value}`", e) + { + } + + public JValueIsNotAnIntError(string value) : base($"The JValue is not an int. Actual value is `{value}`") + { + } +} diff --git a/src/WalletFramework.Core/Json/Errors/JsonFieldNotFoundError.cs b/src/WalletFramework.Core/Json/Errors/JsonFieldNotFoundError.cs new file mode 100644 index 00000000..291b6fae --- /dev/null +++ b/src/WalletFramework.Core/Json/Errors/JsonFieldNotFoundError.cs @@ -0,0 +1,5 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Core.Json.Errors; + +public record JsonFieldNotFoundError(string FieldName) : Error($"The field '{FieldName}' was not found."); diff --git a/src/WalletFramework.Core/Json/Errors/JsonFieldValueIsNullOrWhitespaceError.cs b/src/WalletFramework.Core/Json/Errors/JsonFieldValueIsNullOrWhitespaceError.cs new file mode 100644 index 00000000..36ad2a34 --- /dev/null +++ b/src/WalletFramework.Core/Json/Errors/JsonFieldValueIsNullOrWhitespaceError.cs @@ -0,0 +1,6 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Core.Json.Errors; + +public record JsonFieldValueIsNullOrWhitespaceError(string Name) + : Error($"The value of the field `{Name}` is null or empty"); diff --git a/src/WalletFramework.Core/Json/Errors/JsonIsNotAMapError.cs b/src/WalletFramework.Core/Json/Errors/JsonIsNotAMapError.cs new file mode 100644 index 00000000..43c77b95 --- /dev/null +++ b/src/WalletFramework.Core/Json/Errors/JsonIsNotAMapError.cs @@ -0,0 +1,7 @@ +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; + +namespace WalletFramework.Core.Json.Errors; + +public record JsonIsNotAMapError(JObject JObject, Exception E) + : Error($"The jObject `{JObject}` could not be transformed into a dictionary", E); diff --git a/src/WalletFramework.Core/Json/JsonFun.cs b/src/WalletFramework.Core/Json/JsonFun.cs new file mode 100644 index 00000000..06be57c4 --- /dev/null +++ b/src/WalletFramework.Core/Json/JsonFun.cs @@ -0,0 +1,174 @@ +using System.Globalization; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json.Errors; + +namespace WalletFramework.Core.Json; + +public static class JsonFun +{ + public static Validation GetByKey(this JToken token, string key) + { + try + { + var jObject = token.ToObject()!; + return jObject.GetByKey(key); + } + catch (Exception e) + { + return new JTokenIsNotAnJObjectError(key, e); + } + } + + public static Validation GetByKey(this JObject jObject, string key) + { + var success = jObject.TryGetValue(key, out var value); + if (success) + { + var str = value!.ToString(); + if (string.IsNullOrWhiteSpace(str)) + { + return new JsonFieldValueIsNullOrWhitespaceError(key); + } + else + { + return value; + } + } + else + { + return new JsonFieldNotFoundError(key); + } + } + + public static Validation ToJArray(this JToken token) + { + try + { + var array = token.ToObject()!; + return array; + } + catch (Exception e) + { + return new JTokenIsNotAnJArrayError(token.ToString(), e); + } + } + + public static Validation ToJObject(this JToken token) + { + try + { + var jObject = token.ToObject()!; + return jObject; + } + catch (Exception e) + { + return new JTokenIsNotAnJObjectError(token.ToString(), e); + } + } + + public static Validation ToJValue(this JToken token) + { + try + { + var jValue = token.ToObject()!; + return jValue; + } + catch (Exception e) + { + return new JTokenIsNotAJValueError(token.ToString(), e); + } + } + + public static Validation> ToValidDictionaryAll( + this JObject jObject, + Func> keyValidation, + Func> valueValidation) where T1 : notnull + { + try + { + return jObject + .Properties() + .TraverseAll(property => + from key in keyValidation(property.Name) + from value in valueValidation(property.Value) + select new KeyValuePair(key, value)) + .OnSuccess(pairs => pairs.ToDictionary( + pair => pair.Key, + pair => pair.Value)); + } + catch (Exception e) + { + return new JsonIsNotAMapError(jObject, e); + } + } + + public static Validation> ToValidDictionaryAny( + this JObject jObject, + Func> keyValidation, + Func> valueValidation) where T1 : notnull => jObject + .Properties() + .TraverseAny(property => + from key in keyValidation(property.Name) + from value in valueValidation(property.Value) + select new KeyValuePair(key, value)) + .OnSuccess(pairs => pairs.ToDictionary( + pair => pair.Key, + pair => pair.Value)); + + public static Validation ToInt(this JValue value) + { + try + { + return value.ToObject(); + } + catch (Exception e) + { + return new JValueIsNotAnIntError(value.ToString(CultureInfo.InvariantCulture), e); + } + } + + public static Validation ParseAsJObject(string json) + { + try + { + var jObject = JObject.Parse(json); + return jObject; + } + catch (Exception e) + { + return new InvalidJsonError(json, e); + } + } + + public static JToken RemoveNulls(this JToken token) + { + switch (token.Type) + { + case JTokenType.Object: + var obj = new JObject(); + foreach (var property in ((JObject)token).Properties()) + { + if (property.Value.Type != JTokenType.Null) + { + obj.Add(property.Name, RemoveNulls(property.Value)); + } + } + return obj; + + case JTokenType.Array: + var array = new JArray(); + foreach (var item in (JArray)token) + { + if (item.Type != JTokenType.Null) + { + array.Add(RemoveNulls(item)); + } + } + return array; + + default: + return token; + } + } +} diff --git a/src/WalletFramework.Core/Json/JsonSettings.cs b/src/WalletFramework.Core/Json/JsonSettings.cs new file mode 100644 index 00000000..71f97261 --- /dev/null +++ b/src/WalletFramework.Core/Json/JsonSettings.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace WalletFramework.Core.Json; + +public static class JsonSettings +{ + public static JsonSerializerSettings SerializerSettings => new() + { + NullValueHandling = NullValueHandling.Ignore + }; +} diff --git a/src/WalletFramework.Core/Localization/Constants.cs b/src/WalletFramework.Core/Localization/Constants.cs new file mode 100644 index 00000000..6c3b90d4 --- /dev/null +++ b/src/WalletFramework.Core/Localization/Constants.cs @@ -0,0 +1,10 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Core.Localization; + +public static class Constants +{ + public static Locale DefaultLocale = Locale + .ValidLocale("en-US") + .UnwrapOrThrow(new InvalidOperationException("The default locale is corrupt.")); +} diff --git a/src/WalletFramework.Core/Localization/Errors/LocaleError.cs b/src/WalletFramework.Core/Localization/Errors/LocaleError.cs new file mode 100644 index 00000000..64cc6d4d --- /dev/null +++ b/src/WalletFramework.Core/Localization/Errors/LocaleError.cs @@ -0,0 +1,14 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Errors; + +public record LocaleError : Error +{ + public LocaleError(string value, Exception e) : base($"The locale could not be processed. Value is: `{value}`", e) + { + } + + public LocaleError(string value) : base($"The locale could not be processed. Value is: `{value}`") + { + } +} diff --git a/src/WalletFramework.Core/Localization/Locale.cs b/src/WalletFramework.Core/Localization/Locale.cs new file mode 100644 index 00000000..0af15b7e --- /dev/null +++ b/src/WalletFramework.Core/Localization/Locale.cs @@ -0,0 +1,93 @@ +using System.Globalization; +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Oid4Vc.Oid4Vci.Errors; + +namespace WalletFramework.Core.Localization; + +/// +/// A value type that represent a locale or respectively a language tag. For example +/// ("en-US"). These are based on RFC 4646: https://www.rfc-editor.org/rfc/rfc4646.html. +/// Locales are case-sensitive. +/// +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct Locale +{ + private CultureInfo Value { get; } + + private Locale(CultureInfo value) => Value = value; + + public static implicit operator string(Locale locale) => locale.Value.ToString(); + + [JsonConstructor] + private Locale(string locale) + { + var result = ValidLocale(locale).UnwrapOrThrow(new InvalidOperationException("Locale is corrupt")); + Value = result.Value; + } + + public static Validation ValidLocale(string locale) + { + try + { + if (string.IsNullOrWhiteSpace(locale)) + return new StringIsNullOrWhitespaceError(); + + var cultureInfo = CultureInfo.CreateSpecificCulture(locale); + if (cultureInfo.TwoLetterISOLanguageName == "iv") + return new LocaleError(locale); + + return new Locale(cultureInfo); + } + catch (Exception e) + { + return new LocaleError(locale, e); + } + } + + public static Validation ValidLocale(JToken locale) => + from value in locale.ToJValue() + from result in ValidLocale(value.ToString(CultureInfo.InvariantCulture)) + select result; + + public static Option OptionLocale(string locale) => ValidLocale(locale).ToOption(); + + public static Option OptionLocale(JToken locale) => ValidLocale(locale).ToOption(); + + public override string ToString() => this; +} + +public static class LocaleExtensions +{ + /// + /// Tries to find a match for a given appLocale inside a dictionary. + /// If no match is found it will try again with the DefaultLocale ("en"). + /// If no match for DefaultLocale is found, it will return the first result that is found inside the dictionary. + /// + /// Dictionary with locales as keys and display objects as values. + /// The locale that should be matched. + /// The type of the display object. + /// Dictionary must be not empty otherwise this will throw an exception. + /// + /// The TDisplay that matches the appLocale or one that matches the DefaultLocale or the first locale inside the + /// dictionary. + /// + public static TDisplay FindOrDefault(this IDictionary displays, Locale locale) + { + var matchedLocale = + displays + .Keys + .Find(x => x.ToString().Contains(locale)) + .IfNone(() => displays + .Keys + .Find(x => x.ToString().Contains(Constants.DefaultLocale)) + .IfNone(() => displays.Keys.First())); + + return displays[matchedLocale]; + } +} diff --git a/src/WalletFramework.Core/Uri/UriFun.cs b/src/WalletFramework.Core/Uri/UriFun.cs new file mode 100644 index 00000000..31ac25a7 --- /dev/null +++ b/src/WalletFramework.Core/Uri/UriFun.cs @@ -0,0 +1,6 @@ +namespace WalletFramework.Core.Uri; + +public static class UriFun +{ + public static string ToStringWithoutTrail(this System.Uri uri) => uri.ToString().TrimEnd('/'); +} diff --git a/src/WalletFramework.Core/WalletFramework.Core.csproj b/src/WalletFramework.Core/WalletFramework.Core.csproj new file mode 100644 index 00000000..517c6eb7 --- /dev/null +++ b/src/WalletFramework.Core/WalletFramework.Core.csproj @@ -0,0 +1,12 @@ + + + netstandard2.1 + enable + enable + + + + + + + diff --git a/src/WalletFramework.Functional/OptionFun.cs b/src/WalletFramework.Functional/OptionFun.cs deleted file mode 100644 index 7a2f5ce1..00000000 --- a/src/WalletFramework.Functional/OptionFun.cs +++ /dev/null @@ -1,10 +0,0 @@ -using LanguageExt; - -namespace WalletFramework.Functional; - -public static class OptionFun -{ - public static Option Some(T value) => value; - - public static Option None() => Option.None; -} diff --git a/src/WalletFramework.Functional/Validation.cs b/src/WalletFramework.Functional/Validation.cs deleted file mode 100644 index 4b357807..00000000 --- a/src/WalletFramework.Functional/Validation.cs +++ /dev/null @@ -1,215 +0,0 @@ -using LanguageExt; - -namespace WalletFramework.Functional; - -public readonly struct Validation -{ - public Validation(Validation value) - { - Value = value; - } - - public Validation Value { get; } - - public static implicit operator Validation(Validation value) => value.Value; - - public static implicit operator Validation(Validation value) => new(value); - - public static implicit operator Validation(Error error) => new(Validation.Fail(Seq.create(error))); - - public static implicit operator Validation(Seq errors) => new(Validation.Fail(errors)); - - public static implicit operator Validation(T value) => new(Validation.Success(value)); -} - -public delegate Validation Validator(T value); - -public static class ValidationFun -{ - public static Validation>>>>> Apply( - this Validation> valF, - Validation valT) => - Apply(valF.Select(Prelude.curry), valT); - - public static Validation>>>> Apply( - this Validation> valF, - Validation valT) => - Apply(valF.Select(Prelude.curry), valT); - - public static Validation>>> Apply( - this Validation> valF, - Validation valT) => - Apply(valF.Select(Prelude.curry), valT); - - public static Validation>> Apply( - this Validation> valF, - Validation valT) => - Apply(valF.Select(Prelude.curry), valT); - - public static Validation> Apply( - this Validation> valF, - Validation valT) => - Apply(valF.Select(Prelude.curry), valT); - - public static Validation Apply( - this Validation> valF, - Validation valT) => - valF.Value.Match( - f => - valT.Value.Match( - t => Valid(f(t)), - errors => errors - ), - errors => - valT.Value.Match( - _ => errors, - errorsT => errors + errorsT - ) - ); - - public static T2 Match( - this Validation validation, - Func valid, - Func, T2> invalid) => - validation.Value.Match(valid, invalid); - - public static async Task Match( - this Task> validation, - Func> valid, - Func, Task> invalid) => - await (await validation).Value.MatchAsync(valid, invalid); - - public static Unit Match( - this Validation validation, - Action valid, - Action> invalid) => - validation.Value.Match(valid, invalid); - - public static async Task> Select( - this Task> validation, - Func> task) => - await (await validation).Value.MatchAsync( - async value => Valid(await task(value)), - error => error - ); - - public static Validation Select( - this Validation validation, - Func func) => - validation.Value.Select(func); - - public static async Task> SelectMany( - this Validation validation, - Func>> bind, - Func project) - { - var bindResult = - await validation.Value.MatchAsync( - async t => (await bind(t)).Value, - error => error - ); - - return validation.Value.SelectMany(_ => bindResult, project); - } - - public static async Task> SelectMany( - this Task> validation, - Func>> bind, - Func project) - { - var validationValue = await validation; - - var bindResult = - await validationValue.Value.MatchAsync( - async t => (await bind(t)).Value, - error => error - ); - - return validationValue.Value.SelectMany(_ => bindResult, project); - } - - public static Validation SelectMany( - this Validation validation, - Func> bind, - Func project) => - new(validation.Value.SelectMany(t => bind(t).Value, project)); - - public static Option ToOption(this Validation validation) => - validation.Value.ToOption(); - - public static Validation> Traverse( - this IEnumerable> enumerable, - Func func) => - enumerable - .Select(validation => validation.Value) - .Traverse(func); - - public static Validation> Traverse( - this Dictionary, Validation> dict) where TKey : notnull - { - var createDict = new Func, IEnumerable, Dictionary>( - (keys, values) => - { - var result = new Dictionary(); - var keysArray = keys.ToArray(); - var valuesArray = values.ToArray(); - for (var i = 0; i < keysArray.Length; i++) - { - result.Add(keysArray[i], valuesArray[i]); - } - - return result; - } - ); - - var keys = - dict.Keys.Select(validation => validation).Traverse(key => key); - - var values = - dict.Values.Select(validation => validation).Traverse(value => value); - - return - Valid(createDict) - .Apply(keys) - .Apply(values); - } - - public static Validation OnSuccess(this Validation validation, Func> onSucc) => - from t in validation - from t2 in onSucc(t) - select t2; - - public static Validation OnSuccess(this Validation validation, Func onSucc) => - from t in validation - let t2 = onSucc(t) - select t2; - - public static Validation Invalid(Seq errors) => Validation.Fail(errors); - - public static Validation Valid(T value) => value; - - public static Validation Flatten(this Validation> stackedValidation) => - from outer in stackedValidation - from inner in outer - select inner; - - public static Validator HarvestErrors(IEnumerable> validators) - => t => - { - var errors = validators - .Select(validate => validate(t)) - .SelectMany(validation => validation.Match( - _ => Option>.None, - errors => errors - )) - .SelectMany(seq => seq) - .ToSeq(); - - return errors.Count() == 0 - ? t - : Invalid(errors); - }; - - public static Validator AggregateValidators(this IEnumerable> validators) - => HarvestErrors(validators); -} diff --git a/src/WalletFramework.Mdoc/CborByteString.cs b/src/WalletFramework.MdocLib/CborByteString.cs similarity index 89% rename from src/WalletFramework.Mdoc/CborByteString.cs rename to src/WalletFramework.MdocLib/CborByteString.cs index 5ad7d3a8..aa491d74 100644 --- a/src/WalletFramework.Mdoc/CborByteString.cs +++ b/src/WalletFramework.MdocLib/CborByteString.cs @@ -1,8 +1,8 @@ using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib.Common; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; /// /// A CBOR object which is a byte string which is either CBOR or hex encoded. diff --git a/src/WalletFramework.Mdoc/CborFun.cs b/src/WalletFramework.MdocLib/CborFun.cs similarity index 91% rename from src/WalletFramework.Mdoc/CborFun.cs rename to src/WalletFramework.MdocLib/CborFun.cs index 41f1bd5d..e17a2514 100644 --- a/src/WalletFramework.Mdoc/CborFun.cs +++ b/src/WalletFramework.MdocLib/CborFun.cs @@ -1,8 +1,8 @@ using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib.Common; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; internal static class CborFun { @@ -48,6 +48,7 @@ public static Validation GetByIndex(this CBORObject cbor, uint index return value; } + // TODO: Refactor or check with any public static Validation> ToDictionary( this CBORObject cborMap, Func> keyValidation, @@ -61,7 +62,7 @@ public static Validation> ToDictionary( from key in keyValidation(pair.Key) from value in valueValidation(pair.Value) select new KeyValuePair(key, value)) - .Traverse(pair => pair) + .TraverseAll(pair => pair) .OnSuccess(pairs => pairs.ToDictionary( pair => pair.Key, pair => pair.Value diff --git a/src/WalletFramework.Mdoc/Common/Constants.cs b/src/WalletFramework.MdocLib/Common/Constants.cs similarity index 90% rename from src/WalletFramework.Mdoc/Common/Constants.cs rename to src/WalletFramework.MdocLib/Common/Constants.cs index 8bd6a8be..fad70d31 100644 --- a/src/WalletFramework.Mdoc/Common/Constants.cs +++ b/src/WalletFramework.MdocLib/Common/Constants.cs @@ -1,4 +1,4 @@ -namespace WalletFramework.Mdoc.Common; +namespace WalletFramework.MdocLib.Common; internal static class Constants { diff --git a/src/WalletFramework.Mdoc/Common/Errors.cs b/src/WalletFramework.MdocLib/Common/Errors.cs similarity index 96% rename from src/WalletFramework.Mdoc/Common/Errors.cs rename to src/WalletFramework.MdocLib/Common/Errors.cs index 81eb1eed..1a55a467 100644 --- a/src/WalletFramework.Mdoc/Common/Errors.cs +++ b/src/WalletFramework.MdocLib/Common/Errors.cs @@ -1,6 +1,6 @@ -using WalletFramework.Functional; +using WalletFramework.Core.Functional; -namespace WalletFramework.Mdoc.Common; +namespace WalletFramework.MdocLib.Common; public record InvalidCborByteStringError(string Name, Exception E) : Error($"The value of *{Name}* is not a valid CBOR object encoded as a byte string", E); diff --git a/src/WalletFramework.Mdoc/CoseLabel.cs b/src/WalletFramework.MdocLib/CoseLabel.cs similarity index 86% rename from src/WalletFramework.Mdoc/CoseLabel.cs rename to src/WalletFramework.MdocLib/CoseLabel.cs index b7eafab6..3ef182a1 100644 --- a/src/WalletFramework.Mdoc/CoseLabel.cs +++ b/src/WalletFramework.MdocLib/CoseLabel.cs @@ -1,8 +1,9 @@ using Newtonsoft.Json.Linq; using PeterO.Cbor; -using WalletFramework.Functional; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib.Common; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; public readonly struct CoseLabel { @@ -16,7 +17,8 @@ private CoseLabel(int id) } public static implicit operator string(CoseLabel label) => label.Value; - + public override string ToString() => Value; + internal static Validation ValidCoseLabel(CBORObject cbor) { int id; diff --git a/src/WalletFramework.Mdoc/CoseSignature.cs b/src/WalletFramework.MdocLib/CoseSignature.cs similarity index 83% rename from src/WalletFramework.Mdoc/CoseSignature.cs rename to src/WalletFramework.MdocLib/CoseSignature.cs index 500edbae..934af800 100644 --- a/src/WalletFramework.Mdoc/CoseSignature.cs +++ b/src/WalletFramework.MdocLib/CoseSignature.cs @@ -1,7 +1,7 @@ using PeterO.Cbor; -using WalletFramework.Functional; +using WalletFramework.Core.Functional; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; public readonly struct CoseSignature { diff --git a/src/WalletFramework.Mdoc/DeviceSigned.cs b/src/WalletFramework.MdocLib/DeviceSigned.cs similarity index 83% rename from src/WalletFramework.Mdoc/DeviceSigned.cs rename to src/WalletFramework.MdocLib/DeviceSigned.cs index df06e9be..8807f83f 100644 --- a/src/WalletFramework.Mdoc/DeviceSigned.cs +++ b/src/WalletFramework.MdocLib/DeviceSigned.cs @@ -1,4 +1,4 @@ -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; // TODO: mdoc authentication public readonly struct DeviceSigned @@ -10,9 +10,6 @@ public readonly struct DeviceSigned public readonly struct DeviceAuth { - // TODO: Do we support this? - // public DeviceMac DeviceMac { get; } - public DeviceSignature DeviceSignature { get; } } diff --git a/src/WalletFramework.Mdoc/Digest.cs b/src/WalletFramework.MdocLib/Digest.cs similarity index 81% rename from src/WalletFramework.Mdoc/Digest.cs rename to src/WalletFramework.MdocLib/Digest.cs index 8a1791ee..29f3f1d6 100644 --- a/src/WalletFramework.Mdoc/Digest.cs +++ b/src/WalletFramework.MdocLib/Digest.cs @@ -1,8 +1,8 @@ using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib.Common; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; public readonly struct Digest { diff --git a/src/WalletFramework.Mdoc/DigestAlgorithm.cs b/src/WalletFramework.MdocLib/DigestAlgorithm.cs similarity index 91% rename from src/WalletFramework.Mdoc/DigestAlgorithm.cs rename to src/WalletFramework.MdocLib/DigestAlgorithm.cs index 88fb9ceb..fbb1c07d 100644 --- a/src/WalletFramework.Mdoc/DigestAlgorithm.cs +++ b/src/WalletFramework.MdocLib/DigestAlgorithm.cs @@ -1,9 +1,9 @@ using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; -using static WalletFramework.Mdoc.Common.Constants; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib.Common; +using static WalletFramework.MdocLib.Common.Constants; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; public readonly struct DigestAlgorithm { diff --git a/src/WalletFramework.Mdoc/DigestId.cs b/src/WalletFramework.MdocLib/DigestId.cs similarity index 90% rename from src/WalletFramework.Mdoc/DigestId.cs rename to src/WalletFramework.MdocLib/DigestId.cs index fedf2f4f..31c29eef 100644 --- a/src/WalletFramework.Mdoc/DigestId.cs +++ b/src/WalletFramework.MdocLib/DigestId.cs @@ -1,7 +1,7 @@ using PeterO.Cbor; -using WalletFramework.Functional; +using WalletFramework.Core.Functional; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; public readonly struct DigestId { diff --git a/src/WalletFramework.Mdoc/DocType.cs b/src/WalletFramework.MdocLib/DocType.cs similarity index 68% rename from src/WalletFramework.Mdoc/DocType.cs rename to src/WalletFramework.MdocLib/DocType.cs index b5817b6e..9d58f9e4 100644 --- a/src/WalletFramework.Mdoc/DocType.cs +++ b/src/WalletFramework.MdocLib/DocType.cs @@ -1,17 +1,24 @@ +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; -using static WalletFramework.Mdoc.Common.Constants; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json.Converters; +using WalletFramework.MdocLib.Common; +using static WalletFramework.MdocLib.Common.Constants; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; +[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct DocType { - public string Value { get; } + private string Value { get; } private DocType(string docType) => Value = docType; + public override string ToString() => Value; + + public static implicit operator string(DocType docType) => docType.Value; + internal static Validation ValidDoctype(CBORObject cborObject) => cborObject.GetByLabel(DocTypeLabel).OnSuccess(docType => { diff --git a/src/WalletFramework.Mdoc/IsExternalInit.cs b/src/WalletFramework.MdocLib/IsExternalInit.cs similarity index 100% rename from src/WalletFramework.Mdoc/IsExternalInit.cs rename to src/WalletFramework.MdocLib/IsExternalInit.cs diff --git a/src/WalletFramework.Mdoc/IssuerAuth.cs b/src/WalletFramework.MdocLib/IssuerAuth.cs similarity index 78% rename from src/WalletFramework.Mdoc/IssuerAuth.cs rename to src/WalletFramework.MdocLib/IssuerAuth.cs index 251f964a..1e1c4240 100644 --- a/src/WalletFramework.Mdoc/IssuerAuth.cs +++ b/src/WalletFramework.MdocLib/IssuerAuth.cs @@ -1,15 +1,15 @@ using PeterO.Cbor; -using WalletFramework.Functional; -using static WalletFramework.Mdoc.Common.Constants; -using static WalletFramework.Mdoc.ProtectedHeaders; -using static WalletFramework.Mdoc.UnprotectedHeaders; -using static WalletFramework.Mdoc.MobileSecurityObject; -using static WalletFramework.Mdoc.CoseSignature; -using static WalletFramework.Functional.ValidationFun; +using WalletFramework.Core.Functional; +using static WalletFramework.MdocLib.Common.Constants; +using static WalletFramework.MdocLib.ProtectedHeaders; +using static WalletFramework.MdocLib.UnprotectedHeaders; +using static WalletFramework.MdocLib.MobileSecurityObject; +using static WalletFramework.MdocLib.CoseSignature; +using static WalletFramework.Core.Functional.ValidationFun; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; -public readonly struct IssuerAuth +public record IssuerAuth { public ProtectedHeaders ProtectedHeaders { get; } @@ -49,6 +49,7 @@ internal static Validation ValidIssuerAuth(CBORObject issuerSigned) public CBORObject Encode() { + var cbor = CBORObject.NewArray(); cbor.Add(ProtectedHeaders.ByteString); cbor.Add(UnprotectedHeaders.Encode()); diff --git a/src/WalletFramework.Mdoc/IssuerSigned.cs b/src/WalletFramework.MdocLib/IssuerSigned.cs similarity index 74% rename from src/WalletFramework.Mdoc/IssuerSigned.cs rename to src/WalletFramework.MdocLib/IssuerSigned.cs index 35e24df3..0157f184 100644 --- a/src/WalletFramework.Mdoc/IssuerSigned.cs +++ b/src/WalletFramework.MdocLib/IssuerSigned.cs @@ -1,13 +1,13 @@ using PeterO.Cbor; -using WalletFramework.Functional; -using static WalletFramework.Mdoc.Common.Constants; -using static WalletFramework.Mdoc.NameSpaces; -using static WalletFramework.Mdoc.IssuerAuth; -using static WalletFramework.Functional.ValidationFun; +using WalletFramework.Core.Functional; +using static WalletFramework.MdocLib.Common.Constants; +using static WalletFramework.MdocLib.NameSpaces; +using static WalletFramework.MdocLib.IssuerAuth; +using static WalletFramework.Core.Functional.ValidationFun; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; -public readonly struct IssuerSigned +public record IssuerSigned { public NameSpaces NameSpaces { get; init; } diff --git a/src/WalletFramework.Mdoc/IssuerSignedItem.cs b/src/WalletFramework.MdocLib/IssuerSignedItem.cs similarity index 87% rename from src/WalletFramework.Mdoc/IssuerSignedItem.cs rename to src/WalletFramework.MdocLib/IssuerSignedItem.cs index 205c6c1f..c28d9e7a 100644 --- a/src/WalletFramework.Mdoc/IssuerSignedItem.cs +++ b/src/WalletFramework.MdocLib/IssuerSignedItem.cs @@ -1,20 +1,22 @@ +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OneOf; using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; -using static WalletFramework.Mdoc.ElementArray; -using static WalletFramework.Mdoc.ElementMap; -using static WalletFramework.Mdoc.ElementIdentifier; -using static WalletFramework.Mdoc.ElementValue; -using static WalletFramework.Mdoc.CborByteString; -using static WalletFramework.Mdoc.DigestId; -using static WalletFramework.Mdoc.Random; -using static WalletFramework.Functional.ValidationFun; - -namespace WalletFramework.Mdoc; - -public readonly struct IssuerSignedItem +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json.Converters; +using WalletFramework.MdocLib.Common; +using static WalletFramework.MdocLib.ElementArray; +using static WalletFramework.MdocLib.ElementMap; +using static WalletFramework.MdocLib.ElementIdentifier; +using static WalletFramework.MdocLib.ElementValue; +using static WalletFramework.MdocLib.CborByteString; +using static WalletFramework.MdocLib.DigestId; +using static WalletFramework.MdocLib.Random; +using static WalletFramework.Core.Functional.ValidationFun; + +namespace WalletFramework.MdocLib; + +public record IssuerSignedItem { public CborByteString ByteString { get; } @@ -63,6 +65,7 @@ internal static Validation ValidIssuerSignedItem(CBORObject is }); } +[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct ElementIdentifier { public string Value { get; } @@ -71,6 +74,8 @@ public readonly struct ElementIdentifier public static implicit operator string(ElementIdentifier elementIdentifier) => elementIdentifier.Value; + public override string ToString() => Value; + internal static Validation ValidElementIdentifier(CBORObject cbor) { try @@ -134,7 +139,7 @@ public readonly struct ElementArray internal static Validation ValidElementArray(CBORObject cbor) => cbor.Values .Select(ValidElementValue) - .Traverse(value => value) + .TraverseAll(value => value) .OnSuccess(values => new ElementArray(values.ToList())); } diff --git a/src/WalletFramework.Mdoc/Mdoc.cs b/src/WalletFramework.MdocLib/Mdoc.cs similarity index 90% rename from src/WalletFramework.Mdoc/Mdoc.cs rename to src/WalletFramework.MdocLib/Mdoc.cs index 80efbb67..767e2dcb 100644 --- a/src/WalletFramework.Mdoc/Mdoc.cs +++ b/src/WalletFramework.MdocLib/Mdoc.cs @@ -2,16 +2,16 @@ using LanguageExt; using Microsoft.IdentityModel.Tokens; using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; -using static WalletFramework.Mdoc.DocType; -using static WalletFramework.Mdoc.IssuerSigned; -using static WalletFramework.Functional.ValidationFun; -using static WalletFramework.Mdoc.Common.Constants; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib.Common; +using static WalletFramework.MdocLib.DocType; +using static WalletFramework.MdocLib.IssuerSigned; +using static WalletFramework.Core.Functional.ValidationFun; +using static WalletFramework.MdocLib.Common.Constants; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; -public readonly struct Mdoc +public record Mdoc { public DocType DocType { get; } @@ -20,9 +20,7 @@ public readonly struct Mdoc // TODO: mdoc authentication // public DeviceSigned DeviceSigned { get; } - private Mdoc( - DocType docType, - IssuerSigned issuerSigned) + private Mdoc(DocType docType, IssuerSigned issuerSigned) { DocType = docType; IssuerSigned = issuerSigned; @@ -67,13 +65,9 @@ public static Validation ValidMdoc(string base64UrlencodedCborByteString) } .AggregateValidators(); - var validCbor = - from bytes in decodeBase64Url(base64UrlencodedCborByteString) - from cborObject in parseCborByteString(bytes) - select cborObject; - return - from cbor in validCbor + from bytes in decodeBase64Url(base64UrlencodedCborByteString) + from cbor in parseCborByteString(bytes) from mdoc in Valid(Create) .Apply(ValidDoctype(cbor)) .Apply(ValidIssuerSigned(cbor)) @@ -161,7 +155,7 @@ public static Validation DocTypeMatches(this Mdoc mdoc) var mdocDocType = mdoc.DocType; var msoDocType = mdoc.IssuerSigned.IssuerAuth.Payload.DocType; - if (mdocDocType.Value == msoDocType.Value) + if (mdocDocType.ToString() == msoDocType.ToString()) { return mdoc; } diff --git a/src/WalletFramework.Mdoc/MobileSecurityObject.cs b/src/WalletFramework.MdocLib/MobileSecurityObject.cs similarity index 66% rename from src/WalletFramework.Mdoc/MobileSecurityObject.cs rename to src/WalletFramework.MdocLib/MobileSecurityObject.cs index 7f83d6f5..a24aa49d 100644 --- a/src/WalletFramework.Mdoc/MobileSecurityObject.cs +++ b/src/WalletFramework.MdocLib/MobileSecurityObject.cs @@ -1,16 +1,16 @@ using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; -using static WalletFramework.Mdoc.DigestAlgorithm; -using static WalletFramework.Mdoc.DocType; -using static WalletFramework.Mdoc.ValidityInfo; -using static WalletFramework.Mdoc.CborByteString; -using static WalletFramework.Mdoc.ValueDigests; -using static WalletFramework.Functional.ValidationFun; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib.Common; +using static WalletFramework.MdocLib.DigestAlgorithm; +using static WalletFramework.MdocLib.DocType; +using static WalletFramework.MdocLib.ValidityInfo; +using static WalletFramework.MdocLib.CborByteString; +using static WalletFramework.MdocLib.ValueDigests; +using static WalletFramework.Core.Functional.ValidationFun; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; -public readonly struct MobileSecurityObject +public record MobileSecurityObject { public CborByteString ByteString { get; } @@ -53,16 +53,18 @@ private static MobileSecurityObject Create( new(byteString, version, digestAlgorithm, valueDigests, docType, validityInfo); internal static Validation ValidMobileSecurityObject(CBORObject issuerAuth) => - from mso in issuerAuth.GetByIndex(2) - from byteString in ValidCborByteString(mso) - let decoded = byteString.Decode() + from payloadEncoded in issuerAuth.GetByIndex(2) + from payloadEncodedByteString in ValidCborByteString(payloadEncoded) + let payloadDecoded = payloadEncodedByteString.Decode() + from taggedByteString in ValidCborByteString(payloadDecoded) + let mso = taggedByteString.Decode() from result in Valid(Create) - .Apply(byteString) - .Apply(ValidMsoVersion(decoded)) - .Apply(ValidDigestAlgorithm(decoded)) - .Apply(decoded.GetByLabel("valueDigests").OnSuccess(ValidValueDigests)) - .Apply(ValidDoctype(decoded)) - .Apply(ValidValidityInfo(decoded)) + .Apply(payloadEncodedByteString) + .Apply(ValidMsoVersion(mso)) + .Apply(ValidDigestAlgorithm(mso)) + .Apply(mso.GetByLabel("valueDigests").OnSuccess(ValidValueDigests)) + .Apply(ValidDoctype(mso)) + .Apply(ValidValidityInfo(mso)) select result; private static Validation ValidMsoVersion(CBORObject issuerAuth) => diff --git a/src/WalletFramework.Mdoc/NameSpace.cs b/src/WalletFramework.MdocLib/NameSpace.cs similarity index 75% rename from src/WalletFramework.Mdoc/NameSpace.cs rename to src/WalletFramework.MdocLib/NameSpace.cs index fd09f563..3ceeb3c4 100644 --- a/src/WalletFramework.Mdoc/NameSpace.cs +++ b/src/WalletFramework.MdocLib/NameSpace.cs @@ -1,15 +1,20 @@ +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json.Converters; +using WalletFramework.MdocLib.Common; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; +[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct NameSpace { public string Value { get; } private NameSpace(string value) => Value = value; + + public static implicit operator string(NameSpace nameSpace) => nameSpace.ToString(); internal static Validation ValidNameSpace(CBORObject nameSpace) { diff --git a/src/WalletFramework.Mdoc/NameSpaces.cs b/src/WalletFramework.MdocLib/NameSpaces.cs similarity index 85% rename from src/WalletFramework.Mdoc/NameSpaces.cs rename to src/WalletFramework.MdocLib/NameSpaces.cs index a8dc5829..2bf20f77 100644 --- a/src/WalletFramework.Mdoc/NameSpaces.cs +++ b/src/WalletFramework.MdocLib/NameSpaces.cs @@ -1,9 +1,9 @@ using PeterO.Cbor; -using WalletFramework.Functional; -using static WalletFramework.Mdoc.Common.Constants; -using static WalletFramework.Mdoc.NameSpace; +using WalletFramework.Core.Functional; +using static WalletFramework.MdocLib.Common.Constants; +using static WalletFramework.MdocLib.NameSpace; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; public readonly struct NameSpaces { @@ -24,8 +24,7 @@ internal static Validation ValidNameSpaces(CBORObject issuerSigned) .ToDictionary(ValidNameSpace, issuerSignedItems => issuerSignedItems .Values .Select(IssuerSignedItem.ValidIssuerSignedItem) - .Traverse(item => item) - ); + .TraverseAll(item => item)); return from dict in validDict diff --git a/src/WalletFramework.Mdoc/ProtectedHeaders.cs b/src/WalletFramework.MdocLib/ProtectedHeaders.cs similarity index 93% rename from src/WalletFramework.Mdoc/ProtectedHeaders.cs rename to src/WalletFramework.MdocLib/ProtectedHeaders.cs index 84c238f3..b1282611 100644 --- a/src/WalletFramework.Mdoc/ProtectedHeaders.cs +++ b/src/WalletFramework.MdocLib/ProtectedHeaders.cs @@ -1,10 +1,10 @@ using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; -using static WalletFramework.Mdoc.ProtectedHeaders.Alg; -using static WalletFramework.Mdoc.CborByteString; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib.Common; +using static WalletFramework.MdocLib.ProtectedHeaders.Alg; +using static WalletFramework.MdocLib.CborByteString; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; public readonly struct ProtectedHeaders { diff --git a/src/WalletFramework.Mdoc/UnprotectedHeaders.cs b/src/WalletFramework.MdocLib/UnprotectedHeaders.cs similarity index 85% rename from src/WalletFramework.Mdoc/UnprotectedHeaders.cs rename to src/WalletFramework.MdocLib/UnprotectedHeaders.cs index 0919ea47..e50433cb 100644 --- a/src/WalletFramework.Mdoc/UnprotectedHeaders.cs +++ b/src/WalletFramework.MdocLib/UnprotectedHeaders.cs @@ -1,12 +1,12 @@ using System.Security.Cryptography.X509Certificates; using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; -using static WalletFramework.Mdoc.CoseLabel; -using static WalletFramework.Functional.ValidationFun; -using static WalletFramework.Mdoc.Common.Constants; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib.Common; +using static WalletFramework.MdocLib.CoseLabel; +using static WalletFramework.Core.Functional.ValidationFun; +using static WalletFramework.MdocLib.Common.Constants; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; public readonly struct UnprotectedHeaders { @@ -14,14 +14,14 @@ public readonly struct UnprotectedHeaders public X509Chain X5Chain { get; } - public byte[] CertByteString { get; } + public CBORObject CertByteString { get; } public CBORObject this[CoseLabel key] => Value[key]; private UnprotectedHeaders( Dictionary value, X509Chain x5Chain, - byte[] certByteString) + CBORObject certByteString) { Value = value; X5Chain = x5Chain; @@ -67,7 +67,7 @@ internal static Validation ValidUnprotectedHeaders(CBORObjec from byteString in byteStringCbor.TryGetByteString() select new X509Certificate2(byteString) ) - .Traverse(cert => cert) + .TraverseAll(cert => cert) .OnSuccess(certs => { var chain = new X509Chain(); @@ -102,8 +102,7 @@ from dict in toDict(headersCbor) from label in ValidCoseLabel(CBORObject.FromObject(CertificateIndex)) from chainCbor in getChainCbor(dict, label) from chain in decodeChain(chainCbor) - from chainBytes in chainCbor.TryGetByteString() - select new UnprotectedHeaders(dict, chain, chainBytes); + select new UnprotectedHeaders(dict, chain, chainCbor); } public record InvalidX509CertificateError(string Value, Exception E) @@ -112,7 +111,7 @@ public record InvalidX509CertificateError(string Value, Exception E) public CBORObject Encode() { var cbor = CBORObject.NewMap(); - cbor[CertificateIndex] = CBORObject.FromObject(CertByteString); + cbor[CertificateIndex] = CertByteString; return cbor; } } diff --git a/src/WalletFramework.Mdoc/ValidityInfo.cs b/src/WalletFramework.MdocLib/ValidityInfo.cs similarity index 93% rename from src/WalletFramework.Mdoc/ValidityInfo.cs rename to src/WalletFramework.MdocLib/ValidityInfo.cs index 01463f38..2d32f134 100644 --- a/src/WalletFramework.Mdoc/ValidityInfo.cs +++ b/src/WalletFramework.MdocLib/ValidityInfo.cs @@ -1,10 +1,10 @@ using LanguageExt; using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; -using static WalletFramework.Functional.ValidationFun; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib.Common; +using static WalletFramework.Core.Functional.ValidationFun; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; public struct ValidityInfo { diff --git a/src/WalletFramework.Mdoc/ValueDigests.cs b/src/WalletFramework.MdocLib/ValueDigests.cs similarity index 76% rename from src/WalletFramework.Mdoc/ValueDigests.cs rename to src/WalletFramework.MdocLib/ValueDigests.cs index 5db1c069..a129e0d7 100644 --- a/src/WalletFramework.Mdoc/ValueDigests.cs +++ b/src/WalletFramework.MdocLib/ValueDigests.cs @@ -1,10 +1,10 @@ using PeterO.Cbor; -using WalletFramework.Functional; -using static WalletFramework.Mdoc.Digest; -using static WalletFramework.Mdoc.DigestId; -using static WalletFramework.Mdoc.NameSpace; +using WalletFramework.Core.Functional; +using static WalletFramework.MdocLib.Digest; +using static WalletFramework.MdocLib.DigestId; +using static WalletFramework.MdocLib.NameSpace; -namespace WalletFramework.Mdoc; +namespace WalletFramework.MdocLib; public readonly struct ValueDigests { diff --git a/src/WalletFramework.Mdoc/WalletFramework.Mdoc.csproj b/src/WalletFramework.MdocLib/WalletFramework.MdocLib.csproj similarity index 71% rename from src/WalletFramework.Mdoc/WalletFramework.Mdoc.csproj rename to src/WalletFramework.MdocLib/WalletFramework.MdocLib.csproj index 9cdea1b7..2f074371 100644 --- a/src/WalletFramework.Mdoc/WalletFramework.Mdoc.csproj +++ b/src/WalletFramework.MdocLib/WalletFramework.MdocLib.csproj @@ -4,21 +4,21 @@ netstandard2.1 enable enable + WalletFramework.MdocLib - <_Parameter1>WalletFramework.Mdoc.Tests + <_Parameter1>WalletFramework.MdocLib.Tests - - + diff --git a/src/WalletFramework.MdocVc/ClaimDisplay.cs b/src/WalletFramework.MdocVc/ClaimDisplay.cs new file mode 100644 index 00000000..c63f6830 --- /dev/null +++ b/src/WalletFramework.MdocVc/ClaimDisplay.cs @@ -0,0 +1,20 @@ +using LanguageExt; +using Newtonsoft.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Localization; + +namespace WalletFramework.MdocVc; + +public record ClaimDisplay( + [property: JsonProperty(ClaimDisplayJsonKeys.ClaimName)] + [property: JsonConverter(typeof(OptionJsonConverter))] + Option Name, + [property: JsonProperty(ClaimDisplayJsonKeys.Locale)] + [property: JsonConverter(typeof(OptionJsonConverter))] + Option Locale); + +public static class ClaimDisplayJsonKeys +{ + public const string ClaimName = "name"; + public const string Locale = "locale"; +} diff --git a/src/WalletFramework.MdocVc/ClaimName.cs b/src/WalletFramework.MdocVc/ClaimName.cs new file mode 100644 index 00000000..b83096d9 --- /dev/null +++ b/src/WalletFramework.MdocVc/ClaimName.cs @@ -0,0 +1,25 @@ +using LanguageExt; +using Newtonsoft.Json; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.MdocVc; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct ClaimName +{ + private string Value { get; } + + private ClaimName(string value) => Value = value; + + public override string ToString() => Value; + + public static implicit operator string(ClaimName name) => name.Value; + + public static Option OptionClaimName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return Option.None; + + return new ClaimName(name); + } +} diff --git a/src/WalletFramework.MdocVc/Common/Errors.cs b/src/WalletFramework.MdocVc/Common/Errors.cs new file mode 100644 index 00000000..fe622c5c --- /dev/null +++ b/src/WalletFramework.MdocVc/Common/Errors.cs @@ -0,0 +1,6 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.MdocVc.Common; + +public record OneTimeUseIsNotABooleanValueError(string Actual, Exception E) + : Error($"The field 'one_time_use' is not a boolean value. Actual value is: `{Actual}`", E); diff --git a/src/WalletFramework.Functional/IsExternalInit.cs b/src/WalletFramework.MdocVc/IsExternalInit.cs similarity index 100% rename from src/WalletFramework.Functional/IsExternalInit.cs rename to src/WalletFramework.MdocVc/IsExternalInit.cs diff --git a/src/WalletFramework.MdocVc/MdocDisplay.cs b/src/WalletFramework.MdocVc/MdocDisplay.cs new file mode 100644 index 00000000..63b97ff6 --- /dev/null +++ b/src/WalletFramework.MdocVc/MdocDisplay.cs @@ -0,0 +1,202 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Colors; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Localization; +using WalletFramework.MdocLib; + +namespace WalletFramework.MdocVc; + +public record MdocDisplay( + [property: JsonProperty(MdocDisplayJsonKeys.Logo)] + [property: JsonConverter(typeof(OptionJsonConverter))] + Option Logo, + [property: JsonProperty(MdocDisplayJsonKeys.Name)] + [property: JsonConverter(typeof(OptionJsonConverter))] + Option Name, + [property: JsonProperty(MdocDisplayJsonKeys.BackgroundColor)] + [property: JsonConverter(typeof(OptionJsonConverter))] + Option BackgroundColor, + [property: JsonProperty(MdocDisplayJsonKeys.TextColor)] + [property: JsonConverter(typeof(OptionJsonConverter))] + Option TextColor, + [property: JsonProperty(MdocDisplayJsonKeys.Locale)] + [property: JsonConverter(typeof(OptionJsonConverter))] + Option Locale, + [property: JsonProperty(MdocDisplayJsonKeys.ClaimsDisplays)] + [property: JsonConverter(typeof(OptionJsonConverter>>>))] + Option>>> ClaimsDisplays); + +public static class MdocDisplayJsonKeys +{ + public const string Logo = "logo"; + public const string Name = "name"; + public const string BackgroundColor = "background_color"; + public const string TextColor = "text_color"; + public const string Locale = "locale"; + public const string ClaimsDisplays = "claims_displays"; +} + +public static class MdocDisplayFun +{ + public static Option GetByLocale(this List displays, Locale locale) + { + var dict = new Dictionary(); + foreach (var display in displays) + { + display.Locale.Match( + displayLocale => + { + dict.Add(displayLocale, display); + }, + () => + { + if (!dict.Keys.Contains(Constants.DefaultLocale)) + { + dict.Add(Constants.DefaultLocale, display); + } + } + ); + } + + if (dict.Any()) + { + return dict.FindOrDefault(locale); + } + else + { + return Option.None; + } + } + + public static Option> DecodeFromJson(JArray array) + { + var result = array.TraverseAny(token => + from jObject in token.ToJObject() + select DecodeFromJson(jObject) + ).ToOption(); + + return + from displays in result + select displays.ToList(); + } + + private static MdocDisplay DecodeFromJson(JObject display) + { + var logo = + from jToken in display.GetByKey(MdocDisplayJsonKeys.Logo).ToOption() + let uri = new Uri(jToken.ToString()) + select new MdocLogo(uri); + + var mdocName = + from jToken in display.GetByKey(MdocDisplayJsonKeys.Name).ToOption() + from name in MdocName.OptionMdocName(jToken.ToString()) + select name; + + var backgroundColor = + from jToken in display.GetByKey(MdocDisplayJsonKeys.BackgroundColor).ToOption() + from color in Color.OptionColor(jToken.ToString()) + select color; + + var textColor = + from jToken in display.GetByKey(MdocDisplayJsonKeys.TextColor).ToOption() + from color in Color.OptionColor(jToken.ToString()) + select color; + + var locale = + from jToken in display.GetByKey(MdocDisplayJsonKeys.Locale).ToOption() + from l in Locale.OptionLocale(jToken.ToString()) + select l; + + var claimsDisplays = + from jToken in display.GetByKey(MdocDisplayJsonKeys.ClaimsDisplays).ToOption() + from claimsJson in jToken.ToJObject().ToOption() + from displays in DecodeClaimsDisplaysFromJson(claimsJson) + select displays; + + return new MdocDisplay(logo, mdocName, backgroundColor, textColor, locale, claimsDisplays); + } + + private static Option>>> + DecodeClaimsDisplaysFromJson(JObject namespaceDict) + { + var result = new Dictionary>>(); + + var tuples = namespaceDict.Properties().Select(prop => + { + var claimsDict = + from jObject in prop.Value.ToJObject().ToOption() + from claimsDisplays in DecodeClaimsDisplaysDictFromJson(jObject) + select claimsDisplays; + + return ( + NameSpace: NameSpace.ValidNameSpace(prop.Name).ToOption(), + ClaimsDict: claimsDict + ); + }); + + foreach (var (nameSpace, claimsDict) in tuples) + { + nameSpace.OnSome(space => claimsDict.OnSome(dictionary => + { + result.Add(space, dictionary); + return Unit.Default; + })); + } + + return result.Any() + ? result + : Option>>>.None; + } + + private static Option>> + DecodeClaimsDisplaysDictFromJson(JObject json) + { + var result = new Dictionary>(); + + var tuples = json.Properties().Select(prop => + { + var displays = + from jArray in prop.Value.ToJArray().ToOption() + from claimDisplays in jArray.TraverseAny(token => + { + var optionName = + from jToken in token.GetByKey(ClaimDisplayJsonKeys.ClaimName).ToOption() + from name in ClaimName.OptionClaimName(jToken.ToString()) + select name; + + var optionLocale = + from jToken in token.GetByKey(ClaimDisplayJsonKeys.Locale).ToOption() + from locale in Locale.OptionLocale(jToken.ToString()) + select locale; + + return + from name in optionName + from locale in optionLocale + select new ClaimDisplay(name, locale); + }) + select claimDisplays.ToList(); + + return ( + Id: ElementIdentifier.ValidElementIdentifier(prop.Name).ToOption(), + Displays: displays + ); + }); + + foreach (var (elementId, claimDisplays) in tuples) + { + elementId.OnSome(id => claimDisplays.OnSome(displays => + { + result.Add(id, displays); + return Unit.Default; + })); + } + + return result.Any() + ? result + : Option>>.None; + } +} diff --git a/src/WalletFramework.MdocVc/MdocLogo.cs b/src/WalletFramework.MdocVc/MdocLogo.cs new file mode 100644 index 00000000..e17cb727 --- /dev/null +++ b/src/WalletFramework.MdocVc/MdocLogo.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Uri; + +namespace WalletFramework.MdocVc; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct MdocLogo +{ + public MdocLogo(Uri value) + { + Value = value; + } + + private Uri Value { get; } + + public override string ToString() => Value.ToStringWithoutTrail(); + + public static implicit operator string(MdocLogo logo) => logo.ToString(); +} diff --git a/src/WalletFramework.MdocVc/MdocName.cs b/src/WalletFramework.MdocVc/MdocName.cs new file mode 100644 index 00000000..f11ca172 --- /dev/null +++ b/src/WalletFramework.MdocVc/MdocName.cs @@ -0,0 +1,25 @@ +using LanguageExt; +using Newtonsoft.Json; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.MdocVc; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct MdocName +{ + private string Value { get; } + + private MdocName(string value) => Value = value; + + public override string ToString() => Value; + + public static implicit operator string(MdocName mdocName) => mdocName.Value; + + public static Option OptionMdocName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return Option.None; + + return new MdocName(name); + } +} diff --git a/src/WalletFramework.MdocVc/MdocRecord.cs b/src/WalletFramework.MdocVc/MdocRecord.cs new file mode 100644 index 00000000..0a24c47e --- /dev/null +++ b/src/WalletFramework.MdocVc/MdocRecord.cs @@ -0,0 +1,123 @@ +using Hyperledger.Aries.Storage; +using Hyperledger.Aries.Storage.Models; +using Hyperledger.Aries.Storage.Models.Interfaces; +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Credentials; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.MdocLib; + +namespace WalletFramework.MdocVc; + +[JsonConverter(typeof(MdocRecordJsonConverter))] +public sealed class MdocRecord : RecordBase, ICredential +{ + public CredentialId CredentialId + { + get => CredentialId + .ValidCredentialId(Id) + .UnwrapOrThrow(new InvalidOperationException("The Id is corrupt")); + private set => Id = value; + } + + public Mdoc Mdoc { get; } + + [RecordTag] + public DocType DocType => Mdoc.DocType; + + public Option> Displays { get; } + + public override string TypeName => "WF.MdocRecord"; + + public MdocRecord(Mdoc mdoc, Option> displays) + { + CredentialId = CredentialId.CreateCredentialId(); + Mdoc = mdoc; + Displays = displays; + } + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public MdocRecord() + { + } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + public static implicit operator Mdoc(MdocRecord record) => record.Mdoc; +} + +public static class MdocRecordJsonKeys +{ + public const string MdocJsonKey = "mdoc"; + public const string MdocDisplaysKey = "displays"; +} + +public static class MdocRecordFun +{ + public static MdocRecord DecodeFromJson(JObject json) + { + var id = json[nameof(RecordBase.Id)]!.ToString(); + + var mdocStr = json[MdocRecordJsonKeys.MdocJsonKey]!.ToString(); + var mdoc = Mdoc + .ValidMdoc(mdocStr) + .UnwrapOrThrow(new InvalidOperationException($"The MdocRecord with ID: {id} is corrupt")); + + var displays = + from jToken in json.GetByKey(MdocRecordJsonKeys.MdocDisplaysKey).ToOption() + from jArray in jToken.ToJArray().ToOption() + from mdocDisplays in MdocDisplayFun.DecodeFromJson(jArray) + select mdocDisplays; + + var result = new MdocRecord(mdoc, displays) + { + Id = id + }; + + return result; + } + + public static MdocRecord ToRecord(this Mdoc mdoc, Option> displays) => new(mdoc, displays); +} + +public sealed class MdocRecordJsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, MdocRecord? record, JsonSerializer serializer) + { + writer.WriteStartObject(); + + writer.WritePropertyName(nameof(RecordBase.Id)); + writer.WriteValue(record!.Id); + + writer.WritePropertyName(MdocRecordJsonKeys.MdocJsonKey); + writer.WriteValue(record.Mdoc.Encode()); + + writer.WritePropertyName(MdocRecordJsonKeys.MdocDisplaysKey); + record.Displays.Match( + list => + { + writer.WriteStartArray(); + foreach (var display in list) + { + serializer.Serialize(writer, display); + } + writer.WriteEndArray(); + }, + () => {} + ); + + writer.WriteEndObject(); + } + + public override MdocRecord ReadJson( + JsonReader reader, + Type objectType, + MdocRecord? existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + var json = JObject.Load(reader); + return MdocRecordFun.DecodeFromJson(json); + } +} diff --git a/src/WalletFramework.Functional/WalletFramework.Functional.csproj b/src/WalletFramework.MdocVc/WalletFramework.MdocVc.csproj similarity index 59% rename from src/WalletFramework.Functional/WalletFramework.Functional.csproj rename to src/WalletFramework.MdocVc/WalletFramework.MdocVc.csproj index 7c5bdb54..d2bdd42a 100644 --- a/src/WalletFramework.Functional/WalletFramework.Functional.csproj +++ b/src/WalletFramework.MdocVc/WalletFramework.MdocVc.csproj @@ -1,5 +1,4 @@ - netstandard2.1 enable @@ -7,7 +6,7 @@ - - + + diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IMdocStorage.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IMdocStorage.cs new file mode 100644 index 00000000..436f38ff --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IMdocStorage.cs @@ -0,0 +1,22 @@ +using Hyperledger.Aries.Storage; +using LanguageExt; +using WalletFramework.Core.Credentials; +using WalletFramework.MdocVc; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Abstractions; + +public interface IMdocStorage +{ + public Task Add(MdocRecord record); + + public Task> Get(CredentialId credentialId); + + public Task>> List( + Option query, + int count = 100, + int skip = 0); + + public Task Update(MdocRecord record); + + public Task Delete(MdocRecord record); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs new file mode 100644 index 00000000..8865cad2 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Abstractions/IOid4VciClientService.cs @@ -0,0 +1,50 @@ +using LanguageExt; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; +using OneOf; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Localization; +using WalletFramework.MdocVc; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; +using WalletFramework.SdJwtVc.Models.Records; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Abstractions; + +/// +/// Provides an interface for services related to OpenID for Verifiable Credential Issuance. +/// +public interface IOid4VciClientService +{ + /// + /// Initiates the authorization process of the VCI authorization code flow. + /// + /// The offer metadata + /// The client options + /// + Task InitiateAuthFlow(CredentialOfferMetadata offer, ClientOptions clientOptions); + + /// + /// Requests a verifiable credential using the authorization code flow. + /// + /// Holds authorization session relevant information. + /// + /// A list of credentials. + /// + Task>> RequestCredential(IssuanceSession issuanceSession); + + /// + /// Processes a credential offer + /// + /// The credential offer uri + /// Optional language tag + Task> ProcessOffer(Uri credentialOffer, Option language); + + /// + /// Requests a verifiable credential using the pre-authorized code flow. + /// + /// /// + /// Credential offer and Issuer Metadata + /// The Transaction Code. + Task>> AcceptOffer( + CredentialOfferMetadata credentialOfferMetadata, + string? transactionCode); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs new file mode 100644 index 00000000..835886e1 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Abstractions/IAuthFlowSessionStorage.cs @@ -0,0 +1,45 @@ +using Hyperledger.Aries.Agents; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Records; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Abstractions; + +/// +/// Service for managing authorization records. They are used during the VCI Authorization Code Flow to hold session +/// relevant inforation. +/// +public interface IAuthFlowSessionStorage +{ + /// + /// Deletes the authorization session record by the session identifier. + /// + /// Agent Context + /// Session Identifier of a Authorization Code Flow session + /// + Task DeleteAsync(IAgentContext context, VciSessionId sessionId); + + /// + /// Retrieves the authorization session record by the session identifier. + /// + /// Agent Context + /// Session Identifier of a Authorization Code Flow session + /// + Task GetAsync(IAgentContext context, VciSessionId sessionId); + + /// + /// Stores the authorization session record. + /// + /// The Agent Context + /// Options specified by the Client (Wallet) + /// + /// Parameters required for the authorization during the VCI authorization code + /// flow. + /// + /// + /// + Task StoreAsync( + IAgentContext agentContext, + AuthorizationData authorizationData, + AuthorizationCodeParameters authorizationCodeParameters, + VciSessionId sessionId); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Errors/VciSessionIdError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Errors/VciSessionIdError.cs new file mode 100644 index 00000000..589f75e4 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Errors/VciSessionIdError.cs @@ -0,0 +1,5 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Errors; + +public record VciSessionIdError(string Value) : Error($"Invalid VciSessionId: {Value}"); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs new file mode 100644 index 00000000..ca00ebf8 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs @@ -0,0 +1,52 @@ +using Hyperledger.Aries.Agents; +using Hyperledger.Aries.Storage; +using LanguageExt; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Records; +using WalletFramework.Oid4Vc.Oid4Vp.Services; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Implementations; + +/// +public class AuthFlowSessionStorage : IAuthFlowSessionStorage +{ + /// + /// Initializes a new instance of the class. + /// + /// The service responsible for wallet record operations. + public AuthFlowSessionStorage(IWalletRecordService recordService) + { + _recordService = recordService; + } + + private readonly IWalletRecordService _recordService; + + /// + public async Task StoreAsync( + IAgentContext agentContext, + AuthorizationData authorizationData, + AuthorizationCodeParameters authorizationCodeParameters, + VciSessionId sessionId) + { + var record = new AuthFlowSessionRecord( + authorizationData, + authorizationCodeParameters, + sessionId); + + await _recordService.AddAsync(agentContext.Wallet, record); + + return record.Id; + } + + /// + public async Task GetAsync(IAgentContext context, VciSessionId sessionId) + { + var record = await _recordService.GetAsync(context.Wallet, sessionId); + return record!; + } + + /// + public async Task DeleteAsync(IAgentContext context, VciSessionId sessionId) => + await _recordService.DeleteAsync(context.Wallet, sessionId); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationCodeParameters.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationCodeParameters.cs new file mode 100644 index 00000000..5cc1a381 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationCodeParameters.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +/// +/// Represents the parameters required for the authorization during the VCI authorization code flow. +/// The code itself will be part of the Client redirect uri that is created by the authorization server upon +/// successful authorization. +/// +public record AuthorizationCodeParameters +{ + /// + /// Gets the code challenge. + /// + public string Challenge { get; } + + /// + /// Gets the code challenge method. SHA-256 is the only supported method. + /// + public string CodeChallengeMethod => "S256"; + + /// + /// Gets the code verifier. + /// + public string Verifier { get; } + + [JsonConstructor] + internal AuthorizationCodeParameters(string challenge, string verifier) + { + if (string.IsNullOrWhiteSpace(challenge) || string.IsNullOrWhiteSpace(verifier)) + { + throw new ArgumentException("Authorization Code Parameters cannot be null or empty."); + } + + Challenge = challenge; + Verifier = verifier; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationData.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationData.cs new file mode 100644 index 00000000..86017ddb --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationData.cs @@ -0,0 +1,11 @@ +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +public record AuthorizationData( + ClientOptions ClientOptions, + IssuerMetadata IssuerMetadata, + AuthorizationServerMetadata AuthorizationServerMetadata, + List CredentialConfigurationIds); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs new file mode 100644 index 00000000..6e728596 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationDetails.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +/// +/// Represents the authorization details. +/// +public record AuthorizationDetails +{ + /// + /// Gets the type of the credential. + /// + [JsonProperty("type")] + public string Type { get; } = "openid_credential"; + + /// + /// Gets the format of the credential. + /// + [JsonProperty("format", NullValueHandling = NullValueHandling.Ignore)] + public string? Format { get; } + + /// + /// Gets the verifiable credential type (vct). + /// + [JsonProperty("vct", NullValueHandling = NullValueHandling.Ignore)] + public string? Vct { get; } + + [JsonProperty("doctype", NullValueHandling = NullValueHandling.Ignore)] + public string? DocType { get; } + + /// + /// Gets the credential configuration id. + /// + [JsonProperty("credential_configuration_id", NullValueHandling = NullValueHandling.Ignore)] + public string? CredentialConfigurationId { get; } + + [JsonProperty("locations", NullValueHandling = NullValueHandling.Ignore)] + public string[]? Locations { get; } + + internal AuthorizationDetails( + string? format, + string? vct, + string? credentialConfigurationId, + string[]? locations, + string? docType) + { + if (!string.IsNullOrWhiteSpace(format) && !string.IsNullOrWhiteSpace(credentialConfigurationId)) + { + throw new ArgumentException("Both format and credentialConfigurationId cannot be present at the same time."); + } + + Format = format; + Vct = vct; + CredentialConfigurationId = credentialConfigurationId; + Locations = locations; + DocType = docType; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/ClientOptions.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/ClientOptions.cs new file mode 100644 index 00000000..fa2afa87 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/ClientOptions.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +/// +/// Represents the client options that are used during the VCI Authorization Code Flow. Here the wallet acts as the client. +/// +public record ClientOptions +{ + /// + /// Identifier of the client (wallet) + /// + public string ClientId { get; init; } + + /// + /// Identifier of the wallet issuer + /// + public string WalletIssuer { get; init; } + + /// + /// Redirect URI that the Authorization Server will use after the authorization was successful. + /// + public string RedirectUri { get; init; } + +#pragma warning disable CS8618 + /// + /// Parameterless Default Constructor. + /// + public ClientOptions() + { + } +#pragma warning restore CS8618 + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + [JsonConstructor] + public ClientOptions(string clientId, string walletIssuer, string redirectUri) + { + if (string.IsNullOrWhiteSpace(clientId) + || string.IsNullOrWhiteSpace(walletIssuer) + || !Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) + { + throw new ArgumentException("Invalid Client Options"); + } + + ClientId = clientId; + WalletIssuer = walletIssuer; + RedirectUri = redirectUri; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/IssuanceSession.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/IssuanceSession.cs new file mode 100644 index 00000000..f40ce17f --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/IssuanceSession.cs @@ -0,0 +1,44 @@ +using WalletFramework.Core.Functional; +using static System.Web.HttpUtility; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +/// +/// Represents the parameters of an VCI Authorization Code Flow issuance session. +/// +public record IssuanceSession +{ + /// + /// Gets the session identifier. + /// + public VciSessionId SessionId { get; } + + /// + /// Gets the actual authorization code that is received from the authorization server upon successful authorization. + /// + public string Code { get; } + + private IssuanceSession(VciSessionId sessionId, string code) => (SessionId, Code) = (sessionId, code); + + /// + /// Creates a new instance of from the given . + /// + /// + /// + /// + public static IssuanceSession FromUri(Uri uri) + { + var queryParams = ParseQueryString(uri.Query); + + var code = queryParams.Get("code"); + if (string.IsNullOrWhiteSpace(code)) + { + throw new InvalidOperationException("Query parameter 'code' is missing"); + } + + var sessionIdParam = queryParams.Get("session"); + var sessionId = VciSessionId.ValidSessionId(sessionIdParam).Fallback(VciSessionId.CreateSessionId()); + + return new IssuanceSession(sessionId, code); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequest.cs new file mode 100644 index 00000000..7432187a --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequest.cs @@ -0,0 +1,99 @@ +using Newtonsoft.Json; +using static Newtonsoft.Json.JsonConvert; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +internal record PushedAuthorizationRequest +{ + [JsonProperty("client_id")] + public string ClientId { get; } + + [JsonProperty("response_type")] + public string ResponseType { get; } = "code"; + + [JsonProperty("redirect_uri")] + public string RedirectUri { get; } + + [JsonProperty("code_challenge")] + public string CodeChallenge { get; } + + [JsonProperty("code_challenge_method")] + public string CodeChallengeMethod { get; } + + [JsonProperty("authorization_details", NullValueHandling = NullValueHandling.Ignore)] + public AuthorizationDetails[]? AuthorizationDetails { get; } + + [JsonProperty("issuer_state", NullValueHandling = NullValueHandling.Ignore)] + public string? IssuerState { get; } + + [JsonProperty("wallet_issuer", NullValueHandling = NullValueHandling.Ignore)] + public string? WalletIssuer { get; } + + [JsonProperty("user_hint", NullValueHandling = NullValueHandling.Ignore)] + public string? UserHint { get; } + + [JsonProperty("scope", NullValueHandling = NullValueHandling.Ignore)] + public string? Scope { get; } + + [JsonProperty("resource", NullValueHandling = NullValueHandling.Ignore)] + public string? Resource { get; } + + public PushedAuthorizationRequest( + VciSessionId sessionId, + ClientOptions clientOptions, + AuthorizationCodeParameters authorizationCodeParameters, + AuthorizationDetails[]? authorizationDetails, + string? scope, + string? issuerState, + string? userHint, + string? resource) + { + ClientId = clientOptions.ClientId; + RedirectUri = clientOptions.RedirectUri + "?session=" + sessionId; + WalletIssuer = clientOptions.WalletIssuer; + CodeChallenge = authorizationCodeParameters.Challenge; + CodeChallengeMethod = authorizationCodeParameters.CodeChallengeMethod; + AuthorizationDetails = authorizationDetails; + IssuerState = issuerState; + UserHint = userHint; + Scope = scope; + Resource = resource; + } + + public FormUrlEncodedContent ToFormUrlEncoded() + { + var keyValuePairs = new List>(); + + if (!string.IsNullOrEmpty(ClientId)) + keyValuePairs.Add(new KeyValuePair("client_id", ClientId)); + + if (!string.IsNullOrEmpty(ResponseType)) + keyValuePairs.Add(new KeyValuePair("response_type", ResponseType)); + + if (!string.IsNullOrEmpty(RedirectUri)) + keyValuePairs.Add(new KeyValuePair("redirect_uri", RedirectUri)); + + if (!string.IsNullOrEmpty(CodeChallenge)) + keyValuePairs.Add(new KeyValuePair("code_challenge", CodeChallenge)); + + if (!string.IsNullOrEmpty(CodeChallengeMethod)) + keyValuePairs.Add(new KeyValuePair("code_challenge_method", CodeChallengeMethod)); + + if (AuthorizationDetails != null) + keyValuePairs.Add(new KeyValuePair("authorization_details", SerializeObject(AuthorizationDetails))); + + if (!string.IsNullOrEmpty(IssuerState)) + keyValuePairs.Add(new KeyValuePair("issuer_state", IssuerState)); + + if (!string.IsNullOrEmpty(WalletIssuer)) + keyValuePairs.Add(new KeyValuePair("wallet_issuer", WalletIssuer)); + + if (!string.IsNullOrEmpty(UserHint)) + keyValuePairs.Add(new KeyValuePair("user_hint", UserHint)); + + if (!string.IsNullOrEmpty(Scope)) + keyValuePairs.Add(new KeyValuePair("scope", Scope)); + + return new FormUrlEncodedContent(keyValuePairs); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequestResponse.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequestResponse.cs new file mode 100644 index 00000000..ffa88091 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/PushedAuthorizationRequestResponse.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +internal record PushedAuthorizationRequestResponse +{ + [JsonProperty("request_uri")] + public Uri RequestUri { get; init; } + + [JsonProperty("expires_in")] + public string ExpiresIn { get; init; } + + [JsonConstructor] + private PushedAuthorizationRequestResponse(Uri requestUri, string expiresIn) + => (RequestUri, ExpiresIn) = (requestUri, expiresIn); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/VciSessionId.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/VciSessionId.cs new file mode 100644 index 00000000..452dad1c --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/VciSessionId.cs @@ -0,0 +1,49 @@ +using System.Globalization; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Errors; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +/// +/// Identifier of the authorization session during the VCI Authorization Code Flow. +/// +public struct VciSessionId +{ + /// + /// Gets the value of the session identifier. + /// + private string Value { get; } + + private VciSessionId(string value) => Value = value; + + /// + /// Returns the value of the session identifier. + /// + /// + /// + public static implicit operator string(VciSessionId sessionId) => sessionId.Value; + + public static Validation ValidSessionId(string sessionId) + { + if (!Guid.TryParse(sessionId, out _)) + { + return new VciSessionIdError(sessionId); + } + + return new VciSessionId(sessionId); + } + + public static VciSessionId CreateSessionId() + { + var guid = Guid.NewGuid().ToString(); + return new VciSessionId(guid); + } +} + +public static class VciSessionIdFun +{ + public static VciSessionId DecodeFromJson(JValue json) => VciSessionId + .ValidSessionId(json.ToString(CultureInfo.InvariantCulture)) + .UnwrapOrThrow(new InvalidOperationException("SessionId is corrupt")); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs new file mode 100644 index 00000000..6c5c62f3 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs @@ -0,0 +1,103 @@ +using Hyperledger.Aries.Storage; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Records; + +/// +/// Represents the authorization session record. Used during the VCI Authorization Code Flow to hold session relevant information. +/// +[JsonConverter(typeof(AuthFlowSessionRecordJsonConverter))] +public sealed class AuthFlowSessionRecord : RecordBase +{ + /// + /// The session specific id. + /// + [JsonIgnore] + public VciSessionId SessionId + { + get => VciSessionId + .ValidSessionId(Id) + .UnwrapOrThrow(new InvalidOperationException("SessionId is corrupt")); + set + { + string str = value; + Id = str; + } + } + + /// + /// The authorization data. + /// + public AuthorizationData AuthorizationData { get; } + + /// + /// The parameters for the 'authorization_code' grant type. + /// + public AuthorizationCodeParameters AuthorizationCodeParameters { get; } + + /// + /// Initializes a new instance of the class. + /// + public override string TypeName => "AF.VciAuthorizationSessionRecord"; + +#pragma warning disable CS8618 + /// + /// Initializes a new instance of the class. + /// + public AuthFlowSessionRecord() + { + } +#pragma warning restore CS8618 + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public AuthFlowSessionRecord( + AuthorizationData authorizationData, + AuthorizationCodeParameters authorizationCodeParameters, + VciSessionId sessionId) + { + SessionId = sessionId; + RecordVersion = 1; + AuthorizationCodeParameters = authorizationCodeParameters; + AuthorizationData = authorizationData; + } +} + +public sealed class AuthFlowSessionRecordJsonConverter : JsonConverter +{ + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, AuthFlowSessionRecord? record, JsonSerializer serializer) + => throw new NotImplementedException(); + + public override AuthFlowSessionRecord ReadJson( + JsonReader reader, + Type objectType, + AuthFlowSessionRecord? existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + var json = JObject.Load(reader); + + var id = VciSessionIdFun.DecodeFromJson(json[nameof(RecordBase.Id)]!.ToObject()!); + + var authCodeParameters = JsonConvert.DeserializeObject( + json[nameof(AuthorizationCodeParameters)]!.ToString() + ); + + var authorizationData = JsonConvert.DeserializeObject( + json[nameof(AuthorizationData)]!.ToString() + )!; + + var result = new AuthFlowSessionRecord(authorizationData, authCodeParameters!, id); + + return result; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Abstractions/ITokenService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Abstractions/ITokenService.cs new file mode 100644 index 00000000..b1f0bc26 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Abstractions/ITokenService.cs @@ -0,0 +1,12 @@ +using OneOf; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Abstractions; + +public interface ITokenService +{ + public Task> RequestToken( + TokenRequest tokenRequest, + AuthorizationServerMetadata metadata); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Abstractions/IDPopHttpClient.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Abstractions/IDPopHttpClient.cs new file mode 100644 index 00000000..901fafc8 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Abstractions/IDPopHttpClient.cs @@ -0,0 +1,11 @@ +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Abstractions; + +public interface IDPopHttpClient +{ + internal Task Post( + Uri requestUri, + HttpContent content, + DPopConfig config); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs new file mode 100644 index 00000000..6e747ce9 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Cryptography.Abstractions; +using WalletFramework.Core.Functional; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Exceptions; +using WalletFramework.Oid4Vc.Oid4Vci.Extensions; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Implementations; + +public class DPopHttpClient : IDPopHttpClient +{ + private const string ErrorCodeKey = "error"; + private const string InvalidGrantError = "invalid_grant"; + private const string UseDPopNonceError = "use_dpop_nonce"; + + public DPopHttpClient( + IHttpClientFactory httpClientFactory, + IKeyStore keyStore, + ILogger logger) + { + _keyStore = keyStore; + _httpClient = httpClientFactory.CreateClient(); + _logger = logger; + } + + private readonly IKeyStore _keyStore; + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + + public async Task Post( + Uri requestUri, + HttpContent content, + DPopConfig config) + { + var dPop = await _keyStore.GenerateDPopProofOfPossessionAsync( + config.KeyId, + config.Audience, + config.Nonce.ToNullable(), + config.OAuthToken.ToNullable()?.AccessToken); + + var httpClient = config.OAuthToken.Match( + token => _httpClient.WithDPopHeader(dPop).WithAuthorizationHeader(token), + () => _httpClient.WithDPopHeader(dPop)); + + var response = await httpClient.PostAsync( + requestUri, + content); + + await ThrowIfInvalidGrantError(response); + + var nonceStr = await GetDPopNonce(response); + if (!string.IsNullOrEmpty(nonceStr)) + { + config = config with { Nonce = new DPopNonce(nonceStr) }; + + var newDpop = await _keyStore.GenerateDPopProofOfPossessionAsync( + config.KeyId, + config.Audience, + config.Nonce.ToNullable(), + config.OAuthToken.ToNullable()?.AccessToken); + + httpClient.WithDPopHeader(newDpop); + + response = await httpClient.PostAsync(requestUri, content); + } + + await ThrowIfInvalidGrantError(response); + + var message = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + throw new HttpRequestException($"Http Request with DPop failed. Status Code is {response.StatusCode} with message: {message}"); + + return new DPopHttpResponse(response, config); + } + + private async Task ThrowIfInvalidGrantError(HttpResponseMessage response) + { + var content = await response.Content.ReadAsStringAsync(); + var errorReason = string.IsNullOrEmpty(content) + ? null + : JObject.Parse(content)[ErrorCodeKey]?.ToString(); + + if (response.StatusCode is System.Net.HttpStatusCode.BadRequest && errorReason == InvalidGrantError) + { + _logger.LogError("Error while sending request: {Content}", content); + throw new Oid4VciInvalidGrantException(response.StatusCode); + } + } + + private static async Task GetDPopNonce(HttpResponseMessage response) + { + var content = await response.Content.ReadAsStringAsync(); + var errorReason = string.IsNullOrEmpty(content) + ? null + : JObject.Parse(content)[ErrorCodeKey]?.ToString(); + + if (response.StatusCode + is System.Net.HttpStatusCode.BadRequest + or System.Net.HttpStatusCode.Unauthorized + && errorReason == UseDPopNonceError + && response.Headers.TryGetValues("DPoP-Nonce", out var dPopNonce)) + { + return dPopNonce?.FirstOrDefault(); + } + + return null; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPop.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPop.cs new file mode 100644 index 00000000..c094a7ed --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPop.cs @@ -0,0 +1,11 @@ +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; + +internal record DPop +{ + public DPopConfig Config { get; } + + internal DPop(DPopConfig config) + { + Config = config; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopConfig.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopConfig.cs new file mode 100644 index 00000000..d3367184 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopConfig.cs @@ -0,0 +1,24 @@ +using LanguageExt; +using WalletFramework.Core.Cryptography.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; + +public record DPopConfig +{ + internal KeyId KeyId { get; } + + internal string Audience { get; init; } + + internal Option Nonce { get; init; } + + internal Option OAuthToken { get; init; } + + internal DPopConfig(KeyId keyId, string audience) + { + KeyId = keyId; + Audience = audience; + Nonce = Option.None; + OAuthToken = Option.None; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopHttpResponse.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopHttpResponse.cs new file mode 100644 index 00000000..34d67724 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopHttpResponse.cs @@ -0,0 +1,3 @@ +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; + +public record DPopHttpResponse(HttpResponseMessage ResponseMessage, DPopConfig Config); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopNonce.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopNonce.cs new file mode 100644 index 00000000..9d5ee205 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopNonce.cs @@ -0,0 +1,18 @@ +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; + +internal readonly struct DPopNonce +{ + private string Value { get; } + + public static implicit operator string(DPopNonce nonce) => nonce.Value; + + internal DPopNonce(string nonce) + { + if (string.IsNullOrEmpty(nonce)) + { + throw new InvalidOperationException("nonce must not be null or empty"); + } + + Value = nonce; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopToken.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopToken.cs new file mode 100644 index 00000000..bbb45b43 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Models/DPopToken.cs @@ -0,0 +1,16 @@ +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; + +public record DPopToken +{ + internal OAuthToken Token { get; } + + internal DPop DPop { get; } + + internal DPopToken(OAuthToken token, DPop dPop) + { + Token = token; + DPop = dPop; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Errors/AuthorizationServerIdError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Errors/AuthorizationServerIdError.cs new file mode 100644 index 00000000..e8ad0d64 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Errors/AuthorizationServerIdError.cs @@ -0,0 +1,5 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Errors; + +public record AuthorizationServerIdError(string Value, Exception E) : Error($"The AuthorizationServerId could not be parsed. Value is `{Value}`", E); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Implementations/TokenService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Implementations/TokenService.cs new file mode 100644 index 00000000..5ecd19b0 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Implementations/TokenService.cs @@ -0,0 +1,63 @@ +using OneOf; +using WalletFramework.Core.Cryptography.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; +using static Newtonsoft.Json.JsonConvert; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Implementations; + +internal class TokenService : ITokenService +{ + private readonly IDPopHttpClient _dPopHttpClient; + private readonly IKeyStore _keyStore; + private readonly HttpClient _httpClient; + + public TokenService( + IDPopHttpClient dPopHttpClient, + IHttpClientFactory httpClientFactory, + IKeyStore keyStore) + { + _dPopHttpClient = dPopHttpClient; + _keyStore = keyStore; + _httpClient = httpClientFactory.CreateClient(); + } + + public async Task> RequestToken( + TokenRequest tokenRequest, + AuthorizationServerMetadata metadata) + { + if (metadata.IsDPoPSupported) + { + var keyId = await _keyStore.GenerateKey(); + + var config = new DPopConfig(keyId, metadata.TokenEndpoint); + + var uri = new Uri(metadata.TokenEndpoint); + + var result = await _dPopHttpClient.Post( + uri, + tokenRequest.ToFormUrlEncoded(), + config); + + var token = DeserializeObject(await result.ResponseMessage.Content.ReadAsStringAsync()) + ?? throw new InvalidOperationException("Failed to deserialize the token response"); + + var dPop = new DPop.Models.DPop(result.Config); + + return new DPopToken(token, dPop); + } + else + { + var response = await _httpClient.PostAsync( + metadata.TokenEndpoint, + tokenRequest.ToFormUrlEncoded()); + + var token = DeserializeObject(await response.Content.ReadAsStringAsync()) + ?? throw new InvalidOperationException("Failed to deserialize the token response"); + + return token; + } + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerId.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerId.cs new file mode 100644 index 00000000..acc7b37d --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerId.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Uri; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Errors; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct AuthorizationServerId +{ + private Uri Value { get; } + + private AuthorizationServerId(Uri value) => Value = value; + + public override string ToString() => Value.ToStringWithoutTrail(); + + public static implicit operator Uri(AuthorizationServerId authorizationServerId) => authorizationServerId.Value; + + public static Validation ValidAuthorizationServerId(JToken authorizationServerId) => authorizationServerId.ToJValue().OnSuccess(value => + { + try + { + var str = value.ToString(CultureInfo.InvariantCulture); + var uri = new Uri(str); + return new AuthorizationServerId(uri); + } + catch (Exception e) + { + return new AuthorizationServerIdError(authorizationServerId.ToString(), e).ToInvalid(); + } + }); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerMetadata.cs new file mode 100644 index 00000000..c6b15430 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerMetadata.cs @@ -0,0 +1,78 @@ +using Newtonsoft.Json; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; + +/// +/// Represents the metadata associated with an OAuth 2.0 Authorization Server. +/// +public class AuthorizationServerMetadata +{ + /// + /// Gets or sets the issuer location for the OAuth 2.0 Authorization Server. + /// + [JsonProperty("issuer")] + public string Issuer { get; set; } + + /// + /// Gets or sets the URL of the OAuth 2.0 token endpoint. + /// Clients use this endpoint to obtain an access token by presenting its authorization grant or refresh token. + /// + [JsonProperty("token_endpoint")] + public string TokenEndpoint { get; set; } + + /// + /// Gets or sets the URL of the OAuth 2.0 JSON Web Key Set (JWKS) document. + /// Clients use this to verify the signatures from the Authorization Server. + /// + [JsonProperty("jwks_uri")] + public string JwksUri { get; set; } + + /// + /// Gets or sets the URL of the OAuth 2.0 authorization endpoint. + /// + [JsonProperty("authorization_endpoint")] + public string AuthorizationEndpoint { get; set; } + + /// + /// Gets or sets the response types that the OAuth 2.0 Authorization Server supports. + /// These types determine how the Authorization Server responds to client requests. + /// + [JsonProperty("response_types_supported", NullValueHandling = NullValueHandling.Ignore)] + public string[]? ResponseTypesSupported { get; set; } + + /// + /// Gets or sets the supported authentication methods the OAuth 2.0 Authorization Server supports + /// when calling the token endpoint. + /// + [JsonProperty("token_endpoint_auth_methods_supported")] + public string[] TokenEndpointAuthMethodsSupported { get; set; } + + /// + /// Gets or sets the supported token endpoint authentication signing algorithms. + /// This indicates which algorithms the Authorization Server supports when receiving requests + /// at the token endpoint. + /// + [JsonProperty("token_endpoint_auth_signing_alg_values_supported")] + public string[] TokenEndpointAuthSigningAlgValuesSupported { get; set; } + + /// + /// Gets or sets the supported DPoP signing algorithms. + /// This indicates which algorithms the Authorization Server supports for DPoP Proof JWTs. + /// + [JsonProperty("dpop_signing_alg_values_supported")] + public string[]? DPopSigningAlgValuesSupported { get; set; } + + /// + /// Gets or sets the URL of the endpoint where the wallet sends the Pushed Authorization Request (PAR) to. + /// + [JsonProperty("pushed_authorization_request_endpoint")] + public string? PushedAuthorizationRequestEndpoint { get; set; } + + /// + /// Gets or sets a value indicating whether the Authorization Server requires the use of Pushed Authorization Requests. + /// + [JsonProperty("require_pushed_authorization_requests")] + public bool? RequirePushedAuthorizationRequests { get; set; } + + internal bool IsDPoPSupported => DPopSigningAlgValuesSupported != null && DPopSigningAlgValuesSupported.Contains("ES256"); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/Mdoc/MdocTokenRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/Mdoc/MdocTokenRequest.cs new file mode 100644 index 00000000..eceac5a3 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/Mdoc/MdocTokenRequest.cs @@ -0,0 +1,21 @@ +// TODO: Add this when Client Attestation is available +// public record MdocTokenRequest +// { +// public TokenRequest VciTokenRequest { get; } +// +// public string ClientAssertionType => "urn:ietf:params:oauth:client-assertion-type:jwt-client-attestation"; +// +// public ClientAssertion ClientAssertion { get; } +// } +// +// public readonly struct ClientAssertion +// { +// public ClientAttestationJwt Jwt { get; } +// +// public ClientAttestationPop ClientAttestationPop { get; } +// } +// +// public readonly struct ClientAttestationJwt +// { +// public IEnumerable DeviceKeys { get; } +// } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs new file mode 100644 index 00000000..b55c1d24 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/OAuthToken.cs @@ -0,0 +1,75 @@ +using Newtonsoft.Json; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; + +/// +/// Represents a successful response from the OAuth 2.0 Authorization Server containing +/// the issued access token and related information. +/// +public class OAuthToken +{ + /// + /// Indicates if the Token Request is still pending as the Credential Issuer + /// is waiting for the End-User interaction to complete. + /// + [JsonProperty("authorization_pending")] + public bool? AuthorizationPending { get; set; } + + /// + /// Gets or sets the lifetime in seconds of the c_nonce. + /// + [JsonProperty("c_nonce_expires_in")] + public int? CNonceExpiresIn { get; set; } + + /// + /// Gets or sets the lifetime in seconds of the access token. + /// + [JsonProperty("expires_in")] + public int? ExpiresIn { get; set; } + + /// + /// Gets or sets the minimum amount of time in seconds that the client should wait + /// between polling requests to the Token Endpoint. + /// + [JsonProperty("interval")] + public int? Interval { get; set; } + + /// + /// Gets or sets the access token issued by the authorization server. + /// + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + /// + /// Gets or sets the nonce to be used to create a proof of possession of key material + /// when requesting a Credential. + /// + [JsonProperty("c_nonce")] + public string CNonce { get; set; } + + /// + /// Gets or sets the refresh token, which can be used to obtain new access tokens. + /// + [JsonProperty("refresh_token")] + public string RefreshToken { get; set; } + + /// + /// Gets or sets the scope of the access token. + /// + [JsonProperty("scope")] + public string Scope { get; set; } + + /// + /// Gets or sets the type of the token issued. + /// + [JsonProperty("token_type")] + public string TokenType { get; set; } + + /// + /// Gets or sets the credential identifier. + /// + [JsonProperty("credential_identifiers")] + public AuthorizationDetails? CredentialIdentifier { get; set; } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/TokenRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/TokenRequest.cs new file mode 100644 index 00000000..db5d74ba --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/TokenRequest.cs @@ -0,0 +1,88 @@ +namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; + +/// +/// Represents a request for an access token from an OAuth 2.0 Authorization Server. +/// +public class TokenRequest +{ + /// + /// Gets or sets the grant type of the request. Determines the type of token request being made. + /// + public string GrantType { get; set; } = null!; + + /// + /// Gets or sets the pre-authorized code. Represents the authorization to obtain specific credentials. + /// This is required if the grant type is urn:ietf:params:oauth:grant-type:pre-authorized_code. + /// + public string? PreAuthorizedCode { get; set; } + + /// + /// Gets or sets the pre-authorized code. Represents the authorization to obtain specific credentials. + /// This is required if the grant type is urn:ietf:params:oauth:grant-type:pre-authorized_code. + /// + public string? Code { get; set; } + + /// + /// Gets or sets the pre-authorized code. Represents the authorization to obtain specific credentials. + /// This is required if the grant type is urn:ietf:params:oauth:grant-type:pre-authorized_code. + /// + public string? CodeVerifier { get; set; } + + /// + /// Gets or sets the pre-authorized code. Represents the authorization to obtain specific credentials. + /// This is required if the grant type is urn:ietf:params:oauth:grant-type:pre-authorized_code. + /// + public string? ClientId { get; set; } + + /// + /// Gets or sets the pre-authorized code. Represents the authorization to obtain specific credentials. + /// This is required if the grant type is urn:ietf:params:oauth:grant-type:pre-authorized_code. + /// + public string? RedirectUri { get; set; } + + /// + /// Gets or sets the scope of the access request. Defines the permissions the client is asking for. + /// + public string? Scope { get; set; } + + /// + /// Gets or sets the transaction code. This value must be present if a transaction code was required in a previous step. + /// + public string? TransactionCode { get; set; } + + /// + /// Converts the properties of the TokenRequest instance into an FormUrlEncodedContent type suitable for HTTP POST + /// operations. + /// + /// Returns an instance of FormUrlEncodedContent containing the URL-encoded properties of the TokenRequest. + public FormUrlEncodedContent ToFormUrlEncoded() + { + var keyValuePairs = new List>(); + + if (!string.IsNullOrEmpty(GrantType)) + keyValuePairs.Add(new KeyValuePair("grant_type", GrantType)); + + if (!string.IsNullOrEmpty(PreAuthorizedCode)) + keyValuePairs.Add(new KeyValuePair("pre-authorized_code", PreAuthorizedCode)); + + if (!string.IsNullOrEmpty(Scope)) + keyValuePairs.Add(new KeyValuePair("scope", Scope)); + + if (!string.IsNullOrEmpty(TransactionCode)) + keyValuePairs.Add(new KeyValuePair("tx_code", TransactionCode)); + + if (!string.IsNullOrEmpty(Code)) + keyValuePairs.Add(new KeyValuePair("code", Code)); + + if (!string.IsNullOrEmpty(RedirectUri)) + keyValuePairs.Add(new KeyValuePair("redirect_uri", RedirectUri)); + + if (!string.IsNullOrEmpty(CodeVerifier)) + keyValuePairs.Add(new KeyValuePair("code_verifier", CodeVerifier)); + + if (!string.IsNullOrEmpty(ClientId)) + keyValuePairs.Add(new KeyValuePair("client_id", ClientId)); + + return new FormUrlEncodedContent(keyValuePairs); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/BatchSizeIsNotAPositiveNumberError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/BatchSizeIsNotAPositiveNumberError.cs new file mode 100644 index 00000000..34343bae --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/BatchSizeIsNotAPositiveNumberError.cs @@ -0,0 +1,5 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Errors; + +public record BatchSizeIsNotAPositiveNumberError() : Error("The value of `batch_size` is not a positive number"); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/FormatNotSupportedError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/FormatNotSupportedError.cs new file mode 100644 index 00000000..6fd7e250 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/FormatNotSupportedError.cs @@ -0,0 +1,5 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Errors; + +public record FormatNotSupportedError(string Value) : Error($"The given format `{Value}` is not supported"); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/ProofTypeIdNotSupportedError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/ProofTypeIdNotSupportedError.cs new file mode 100644 index 00000000..cd94a90c --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Errors/ProofTypeIdNotSupportedError.cs @@ -0,0 +1,5 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Errors; + +public record ProofTypeIdNotSupportedError(string Value) : Error($"The ProofTypeId: `{Value}` is not supported"); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialConfiguration.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialConfiguration.cs new file mode 100644 index 00000000..ea7ad271 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialConfiguration.cs @@ -0,0 +1,132 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using static WalletFramework.Core.Functional.ValidationFun; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Format; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Scope; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CryptographicBindingMethod; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CryptograhicSigningAlgValue; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.ProofTypeId; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.ProofTypeMetadata; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CredentialDisplay; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +/// +/// Represents the metadata of a specific type of credential that a Credential Issuer can issue. +/// +public record CredentialConfiguration +{ + /// + /// Gets the identifier for the format of the credential. + /// + [JsonProperty(FormatJsonKey)] + public Format Format { get; } + + /// + /// Gets a string indicating the credential that can be issued. + /// + [JsonProperty(ScopeJsonKey)] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Scope { get; set; } + + /// + /// Gets list of methods that identify how the Credential is bound to the identifier of the End-User who + /// possesses the Credential. + /// + [JsonProperty(CryptographicBindingMethodsSupportedJsonKey)] + [JsonConverter(typeof(OptionJsonConverter>))] + public Option> CryptographicBindingMethodsSupported { get; } + + /// + /// Gets a list of identifiers for the signing algorithms that are supported by the issuer and used + /// to sign credentials. + /// + [JsonProperty(CredentialSigningAlgValuesSupportedJsonKey)] + [JsonConverter(typeof(OptionJsonConverter>))] + public Option> CredentialSigningAlgValuesSupported { get; } + + /// + /// Gets a dictionary which maps a credential type to its supported signing algorithms for key proofs. + /// + [JsonProperty(ProofTypesSupportedJsonKey)] + [JsonConverter(typeof(OptionJsonConverter>))] + public Option> ProofTypesSupported { get; } + + /// + /// Gets a list of display properties of the supported credential for different languages. + /// + [JsonProperty(DisplayJsonKey)] + [JsonConverter(typeof(OptionJsonConverter>))] + public Option> Display { get; } + + private CredentialConfiguration( + Format format, + Option scope, + Option> cryptographicBindingMethodsSupported, + Option> credentialSigningAlgValuesSupported, + Option> proofTypesSupported, + Option> display) + { + Format = format; + Scope = scope; + CryptographicBindingMethodsSupported = cryptographicBindingMethodsSupported; + CredentialSigningAlgValuesSupported = credentialSigningAlgValuesSupported; + ProofTypesSupported = proofTypesSupported; + Display = display; + } + + private static CredentialConfiguration Create( + Format format, + Option scope, + Option> cryptographicBindingMethodsSupported, + Option> credentialSigningAlgValuesSupported, + Option> proofTypesSupported, + Option> display) => new( + format, + scope, + cryptographicBindingMethodsSupported, + credentialSigningAlgValuesSupported, + proofTypesSupported, + display); + + public static Validation ValidCredentialConfiguration(JToken credentialMetadata) + { + var validBindingMethods = new Func>>(bindingMethods => + from array in bindingMethods.ToJArray() + from methods in array.TraverseAny(ValidCryptographicBindingMethod) + select methods.ToList()); + + var validSigningAlgValues = new Func>>(signingAlgValues => + from array in signingAlgValues.ToJArray() + from values in array.TraverseAny(ValidCryptograhicSigningAlgValue) + select values.ToList()); + + var validProofTypes = new Func>>(proofTypes => proofTypes + .ToJObject() + .OnSuccess(jObject => jObject.ToValidDictionaryAny(ValidProofTypeId, ValidProofTypeMetadata))); + + var optionalCredentialDisplays = new Func>>(credentialDisplays => + from array in credentialDisplays.ToJArray().ToOption() + from displays in array.TraverseAny(OptionalCredentialDisplay) + select displays.ToList()); + + return Valid(Create) + .Apply(credentialMetadata.GetByKey(FormatJsonKey).OnSuccess(ValidFormat)) + .Apply(credentialMetadata.GetByKey(ScopeJsonKey).ToOption().OnSome(OptionalScope)) + .Apply(credentialMetadata.GetByKey(CryptographicBindingMethodsSupportedJsonKey).OnSuccess(validBindingMethods).ToOption()) + .Apply(credentialMetadata.GetByKey(CredentialSigningAlgValuesSupportedJsonKey).OnSuccess(validSigningAlgValues).ToOption()) + .Apply(credentialMetadata.GetByKey(ProofTypesSupportedJsonKey).OnSuccess(validProofTypes).ToOption()) + .Apply(credentialMetadata.GetByKey(DisplayJsonKey).ToOption().OnSome(optionalCredentialDisplays)); + } + + private const string FormatJsonKey = "format"; + private const string ScopeJsonKey = "scope"; + private const string CryptographicBindingMethodsSupportedJsonKey = "cryptographic_binding_methods_supported"; + private const string CredentialSigningAlgValuesSupportedJsonKey = "credential_signing_alg_values_supported"; + private const string ProofTypesSupportedJsonKey = "proof_types_supported"; + private const string DisplayJsonKey = "display"; +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs new file mode 100644 index 00000000..63580e48 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs @@ -0,0 +1,118 @@ +using System.Drawing; +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Colors; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Localization; +using WalletFramework.SdJwtVc.Models.Credential; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CredentialName; +using static WalletFramework.Core.Localization.Locale; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CredentialLogo; +using Color = WalletFramework.Core.Colors.Color; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +/// +/// Represents the visual representations for the credential. +/// +public record CredentialDisplay +{ + /// + /// Gets the logo associated with this Credential. + /// + [JsonProperty("logo")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Logo { get; } + + /// + /// Gets the name of the Credential. + /// + [JsonProperty("name")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Name { get; } + + /// + /// Gets the background color for the Credential. + /// + [JsonProperty("background_color")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option BackgroundColor { get; } + + /// + /// Gets the locale, which represents the specific culture or region. + /// + [JsonProperty("locale")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Locale { get; } + + /// + /// Gets the text color for the Credential. + /// + [JsonProperty("text_color")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option TextColor { get; } + + private CredentialDisplay( + Option logo, + Option name, + Option backgroundColor, + Option locale, + Option textColor) + { + Logo = logo; + Name = name; + BackgroundColor = backgroundColor; + Locale = locale; + TextColor = textColor; + } + + public static Option OptionalCredentialDisplay(JToken display) => display + .ToJObject() + .ToOption() + .OnSome(jObject => + { + var backgroundColor = jObject + .GetByKey("background_color") + .ToOption() + .OnSome(color => Color.OptionColor(color.ToString())); + + var textColor = jObject + .GetByKey("text_color") + .ToOption() + .OnSome(color => Color.OptionColor(color.ToString())); + + var name = jObject.GetByKey("name").ToOption().OnSome(OptionalCredentialName); + var logo = jObject.GetByKey("logo").ToOption().OnSome(OptionalCredentialLogo); + var locale = jObject.GetByKey("locale").OnSuccess(ValidLocale).ToOption(); + + if (name.IsNone && logo.IsNone && backgroundColor.IsNone && locale.IsNone && textColor.IsNone) + return Option.None; + + return new CredentialDisplay(logo, name, backgroundColor, locale, textColor); + }); +} + +public static class CredentialDisplayFun +{ + // TODO: Unpure + public static SdJwtDisplay ToSdJwtDisplay(this CredentialDisplay credentialDisplay) + { + var logo = new SdJwtDisplay.SdJwtLogo + { + Uri = credentialDisplay.Logo.ToNullable()?.Uri.ToNullable()!, + AltText = credentialDisplay.Logo.ToNullable()?.AltText.ToNullable() + }; + + return new SdJwtDisplay + { + Logo = logo, + Name = credentialDisplay.Name.ToNullable(), + BackgroundColor = credentialDisplay.BackgroundColor.ToNullable(), + Locale = credentialDisplay.Locale.ToNullable()?.ToString(), + TextColor = credentialDisplay.TextColor.ToNullable() + }; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs new file mode 100644 index 00000000..2a58ee40 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs @@ -0,0 +1,65 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +/// +/// Represents the Logo for a Credential. +/// +public record CredentialLogo +{ + /// + /// Gets the alternate text that describes the logo image. This is typically used for accessibility purposes. + /// + [JsonProperty("alt_text")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option AltText { get; } + + /// + /// Gets the URL of the logo image. + /// + [JsonProperty("uri")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Uri { get; } + + private CredentialLogo(Option altText, Option uri) + { + AltText = altText; + Uri = uri; + } + + public static Option OptionalCredentialLogo(JToken logo) + { + var altText = logo.GetByKey("alt_text").ToOption().OnSome(text => + { + var str = text.ToString(); + if (string.IsNullOrWhiteSpace(str)) + return Option.None; + + return str; + }); + + var imageUri = logo.GetByKey("uri").ToOption().OnSome(uri => + { + try + { + var str = uri.ToString(); + var result = new Uri(str); + return result; + } + catch (Exception) + { + return Option.None; + } + }); + + if (altText.IsNone && imageUri.IsNone) + return Option.None; + + return new CredentialLogo(altText, imageUri); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialName.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialName.cs new file mode 100644 index 00000000..8bfc5d0f --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialName.cs @@ -0,0 +1,32 @@ +using System.Globalization; +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct CredentialName +{ + private string Value { get; } + + private CredentialName(string value) => Value = value; + + public override string ToString() => Value; + + public static implicit operator string(CredentialName credentialName) => credentialName.ToString(); + + public static Option OptionalCredentialName(JToken name) => name.ToJValue().ToOption().OnSome(value => + { + var str = value.ToString(CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(str)) + { + return Option.None; + } + + return new CredentialName(str); + }); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptograhicSigningAlgValue.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptograhicSigningAlgValue.cs new file mode 100644 index 00000000..ded1adeb --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptograhicSigningAlgValue.cs @@ -0,0 +1,33 @@ +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using static WalletFramework.Core.Functional.ValidationFun; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct CryptograhicSigningAlgValue +{ + private string Value { get; } + + private CryptograhicSigningAlgValue(string value) => Value = value; + + public override string ToString() => Value; + + public static implicit operator string(CryptograhicSigningAlgValue alg) => alg.ToString(); + + public static Validation ValidCryptograhicSigningAlgValue(JToken alg) => alg.ToJValue().OnSuccess(value => + { + var str = value.ToString(CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(str)) + { + return new StringIsNullOrWhitespaceError(); + } + + return Valid(new CryptograhicSigningAlgValue(str)); + }); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptographicBindingMethod.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptographicBindingMethod.cs new file mode 100644 index 00000000..bf1b7f2c --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptographicBindingMethod.cs @@ -0,0 +1,32 @@ +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct CryptographicBindingMethod +{ + private string Value { get; } + + private CryptographicBindingMethod(string value) => Value = value; + + public override string ToString() => Value; + + public static implicit operator string(CryptographicBindingMethod method) => method.ToString(); + + public static Validation ValidCryptographicBindingMethod(JToken method) => method.ToJValue().OnSuccess(value => + { + var str = value.ToString(CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(str)) + { + return new StringIsNullOrWhitespaceError().ToInvalid(); + } + + return new CryptographicBindingMethod(str); + }); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Format.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Format.cs new file mode 100644 index 00000000..2ef65059 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Format.cs @@ -0,0 +1,35 @@ +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Errors; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct Format +{ + private string Value { get; } + + private Format(string value) => Value = value; + + public override string ToString() => Value; + + public static implicit operator string(Format format) => format.ToString(); + + public static Validation ValidFormat(JToken format) => format.ToJValue().OnSuccess(value => + { + var str = value.ToString(CultureInfo.InvariantCulture); + return SupportedFormats.Contains(str) + ? new Format(str) + : new FormatNotSupportedError(str).ToInvalid(); + }); + + private static List SupportedFormats => new() + { + "vc+sd-jwt", + "mso_mdoc" + }; +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ClaimsMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ClaimsMetadata.cs new file mode 100644 index 00000000..c3b30a86 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ClaimsMetadata.cs @@ -0,0 +1,88 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.MdocLib; +using WalletFramework.MdocVc; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.ElementMetadata; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; + +[JsonConverter(typeof(ClaimsMetadataJsonConverter))] +public readonly struct ClaimsMetadata +{ + public Dictionary> Value { get; } + + private ClaimsMetadata(Dictionary> value) => + Value = value; + + public static Validation ValidClaimsMetadata(JObject claims) => claims + .ToValidDictionaryAll(NameSpace.ValidNameSpace, ValidElementMetadatas) + .OnSuccess(dictionary => new ClaimsMetadata(dictionary)); +} + +public class ClaimsMetadataJsonConverter : JsonConverter +{ + public override bool CanRead => false; + + public override void WriteJson(JsonWriter writer, ClaimsMetadata claimsMetadata, JsonSerializer serializer) + { + writer.WriteStartObject(); + + var value = JObject.FromObject(claimsMetadata.Value, serializer); + foreach (var property in value.Properties()) + { + property.WriteTo(writer); + } + } + + public override ClaimsMetadata ReadJson(JsonReader reader, Type objectType, ClaimsMetadata existingValue, bool hasExistingValue, + JsonSerializer serializer) => + throw new NotImplementedException(); +} + +public static class ClaimsMetadataFun +{ + public static Option>>> ToClaimsDisplays( + this ClaimsMetadata claimsMetadata) + { + var result = new Dictionary>>(); + + foreach (var nameSpacePair in claimsMetadata.Value) + { + var elementDisplays = new Dictionary>(); + foreach (var elementPair in nameSpacePair.Value) + { + elementPair.Value.Display.Match( + list => + { + var displays = list.Select(elementDisplay => + { + var name = + from elementName in elementDisplay.Name + from claimName in ClaimName.OptionClaimName(elementName) + select claimName; + + return new ClaimDisplay(name, elementDisplay.Locale); + }).ToList(); + elementDisplays.Add(elementPair.Key, displays); + }, + () => {} + ); + } + + if (elementDisplays.Any()) + result.Add(nameSpacePair.Key, elementDisplays); + } + + if (result.Any()) + { + return result; + } + else + { + return Option>>>.None; + } + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptoGraphicCurve.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptoGraphicCurve.cs new file mode 100644 index 00000000..a076ee4e --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptoGraphicCurve.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json.Converters; +using WalletFramework.MdocLib; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct CryptographicCurve +{ + public CoseLabel Value { get; } + + private CryptographicCurve(CoseLabel value) => Value = value; + + public static Validation ValidCryptographicCurve(JToken curve) => + CoseLabel.ValidCoseLabel(curve).OnSuccess(label => new CryptographicCurve(label)); + + public override string ToString() => Value; +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptographicSuite.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptographicSuite.cs new file mode 100644 index 00000000..00191e4b --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptographicSuite.cs @@ -0,0 +1,31 @@ +using System.Globalization; +using System.Text.Json.Serialization; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct CryptographicSuite +{ + // TODO: Validate if Value is part of IANA Registry + public string Value { get; } + + private CryptographicSuite(string value) => Value = value; + + public override string ToString() => Value; + + public static Validation ValidCryptographicSuite(JToken suite) => suite.ToJValue().OnSuccess(value => + { + var str = value.ToString(CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(str)) + { + return new StringIsNullOrWhitespaceError().ToInvalid(); + } + + return new CryptographicSuite(str); + }); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementDisplay.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementDisplay.cs new file mode 100644 index 00000000..40a0b7e5 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementDisplay.cs @@ -0,0 +1,44 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Localization; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; + +public record ElementDisplay +{ + [JsonProperty("locale")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Locale { get; } + + [JsonProperty("name")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Name { get; } + + private ElementDisplay(Option name, Option locale) + { + Name = name; + Locale = locale; + } + + public static Option OptionalElementDisplay(JObject display) + { + var name = display.GetByKey("name").Match( + ElementName.OptionalElementName, + _ => Option.None); + + var locale = display.GetByKey("locale").Match( + Core.Localization.Locale.OptionLocale, + _ => Option.None); + + if (name.IsNone && locale.IsNone) + { + return Option.None; + } + + return new ElementDisplay(name, locale); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementMetadata.cs new file mode 100644 index 00000000..fe90bb99 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementMetadata.cs @@ -0,0 +1,63 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.MdocLib; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; + +public record ElementMetadata +{ + [JsonProperty("mandatory")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Mandatory { get; } + + [JsonProperty("display")] + [JsonConverter(typeof(OptionJsonConverter>))] + public Option> Display { get; } + + private ElementMetadata(Option mandatory, Option> display) + { + Mandatory = mandatory; + Display = display; + } + + public static ElementMetadata CreateElementMetadata(JToken metadata) + { + var mandatory = metadata.GetByKey("mandatory").Match( + jToken => + { + var str = jToken.ToString(); + return bool.Parse(str); + }, + _ => Option.None); + + var validDisplay = + from token in metadata.GetByKey("display") + from array in token.ToJArray() + select array; + + var display = + from array in validDisplay.ToOption() + from displays in array.TraverseAny(token => + { + var optJObject = token.ToJObject().ToOption(); + return + from jObject in optJObject + from elementDisplay in ElementDisplay.OptionalElementDisplay(jObject) + select elementDisplay; + }) + select displays.ToList(); + + return new ElementMetadata(mandatory, display); + } + + public static Validation> ValidElementMetadatas( + JToken metadatas) => metadatas + .ToJObject() + .OnSuccess(o => o.ToValidDictionaryAll( + ElementIdentifier.ValidElementIdentifier, + token => ValidationFun.Valid(ElementMetadata.CreateElementMetadata(token)))); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementName.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementName.cs new file mode 100644 index 00000000..2d3623a2 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementName.cs @@ -0,0 +1,31 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct ElementName +{ + private string Value { get; } + + private ElementName(string value) => Value = value; + + public override string ToString() => Value; + + public static implicit operator string(ElementName elementName) => elementName.Value; + + public static Option OptionalElementName(JToken name) + { + var str = name.ToString(); + if (string.IsNullOrWhiteSpace(str)) + { + return Option.None; + } + else + { + return new ElementName(str); + } + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/MdocConfiguration.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/MdocConfiguration.cs new file mode 100644 index 00000000..759148d4 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/MdocConfiguration.cs @@ -0,0 +1,146 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.MdocLib; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.CryptographicCurve; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.CryptographicSuite; +using static WalletFramework.MdocLib.DocType; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.Policy; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.MdocConfiguration.MdocConfigurationJsonKeys; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; + +[JsonConverter(typeof(MdocConfigurationJsonConverter))] +public record MdocConfiguration +{ + public CredentialConfiguration CredentialConfiguration { get; } + + public DocType DocType { get; } + + // TODO: This is actually required, but BDR doesnt use it + public Option Policy { get; } + + public Option> CryptographicSuitesSupported { get; } + + public Option> CryptographicCurvesSupported { get; } + + public Option Claims { get; } + + public Format Format => CredentialConfiguration.Format; + + private MdocConfiguration( + CredentialConfiguration credentialConfiguration, + DocType docType, + Option policy, + Option> cryptographicSuitesSupported, + Option> cryptographicCurvesSupported, + Option claims) + { + CredentialConfiguration = credentialConfiguration; + DocType = docType; + Policy = policy; + CryptographicSuitesSupported = cryptographicSuitesSupported; + CryptographicCurvesSupported = cryptographicCurvesSupported; + Claims = claims; + } + + private static MdocConfiguration Create( + CredentialConfiguration credentialConfiguration, + DocType docType, + Option policy, + Option> cryptographicSuitesSupported, + Option> cryptographicCurvesSupported, + Option claims) => + new(credentialConfiguration, docType, policy, cryptographicSuitesSupported, cryptographicCurvesSupported, claims); + + public static Validation ValidMdocConfiguration(JObject config) + { + var credentialConfiguration = CredentialConfiguration.ValidCredentialConfiguration(config); + + var docType = config.GetByKey(DocTypeJsonKey).OnSuccess(ValidDoctype); + var policy = config.GetByKey(PolicyJsonKey).OnSuccess(ValidPolicy).ToOption(); + + var cryptographicSuitesSupported = config + .GetByKey(CryptographicSuitesSupportedJsonKey) + .OnSuccess(token => token.ToJArray()) + .OnSuccess(array => array.Select(ValidCryptographicSuite).TraverseAll(suite => suite)) + .OnSuccess(suites => suites.ToList()) + .ToOption(); + + var cryptographicCurvesSupported = config + .GetByKey(CryptographicCurvesSupportedJsonKey) + .OnSuccess(token => token.ToJArray()) + .OnSuccess(array => array.Select(ValidCryptographicCurve).TraverseAll(curve => curve)) + .OnSuccess(curves => curves.ToList()) + .ToOption(); + + var claims = config + .GetByKey(ClaimsJsonKey) + .OnSuccess(token => token.ToJObject()) + .OnSuccess(ClaimsMetadata.ValidClaimsMetadata) + .ToOption(); + + return ValidationFun.Valid(Create) + .Apply(credentialConfiguration) + .Apply(docType) + .Apply(policy) + .Apply(cryptographicSuitesSupported) + .Apply(cryptographicCurvesSupported) + .Apply(claims); + } + + public static class MdocConfigurationJsonKeys + { + public const string DocTypeJsonKey = "doctype"; + public const string PolicyJsonKey = "policy"; + public const string CryptographicSuitesSupportedJsonKey = "cryptographic_suites_supported"; + public const string CryptographicCurvesSupportedJsonKey = "cryptographic_curves_supported"; + public const string ClaimsJsonKey = "claims"; + } +} + +public class MdocConfigurationJsonConverter : JsonConverter +{ + public override bool CanRead => false; + + public override void WriteJson(JsonWriter writer, MdocConfiguration? mdocConfig, JsonSerializer serializer) + { + writer.WriteStartObject(); + + var credentialConfig = JObject.FromObject(mdocConfig!.CredentialConfiguration); + foreach (var property in credentialConfig.Properties()) + { + property.WriteTo(writer); + } + + serializer.Converters.Add(new OptionJsonConverter()); + serializer.Converters.Add(new OptionJsonConverter>()); + serializer.Converters.Add(new OptionJsonConverter>()); + serializer.Converters.Add(new OptionJsonConverter()); + serializer.Converters.Add(new ValueTypeJsonConverter()); + + writer.WritePropertyName(DocTypeJsonKey); + serializer.Serialize(writer, mdocConfig.DocType); + + writer.WritePropertyName(PolicyJsonKey); + serializer.Serialize(writer, mdocConfig.Policy); + + writer.WritePropertyName(CryptographicSuitesSupportedJsonKey); + serializer.Serialize(writer, mdocConfig.CryptographicSuitesSupported); + + writer.WritePropertyName(CryptographicCurvesSupportedJsonKey); + serializer.Serialize(writer, mdocConfig.CryptographicCurvesSupported); + + writer.WritePropertyName(ClaimsJsonKey); + serializer.Serialize(writer, mdocConfig.Claims); + + writer.WriteEndObject(); + } + + public override MdocConfiguration ReadJson(JsonReader reader, Type objectType, MdocConfiguration? existingValue, + bool hasExistingValue, JsonSerializer serializer) => + throw new NotImplementedException(); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/Policy.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/Policy.cs new file mode 100644 index 00000000..565b7dd6 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/Policy.cs @@ -0,0 +1,85 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Json.Errors; +using WalletFramework.MdocLib.Common; +using WalletFramework.MdocVc.Common; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Errors; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; + +public record Policy +{ + [JsonProperty("one_time_use")] + public bool OneTimeUse { get; } + + [JsonProperty("batch_size")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option BatchSize { get; } + + private Policy(bool oneTimeUse, Option batchSize) + { + OneTimeUse = oneTimeUse; + BatchSize = batchSize; + } + + private static Policy Create(bool oneTimeUse, Option batchSize) => new(oneTimeUse, batchSize); + + public static Validation ValidPolicy(JToken policy) + { + JObject jObject; + try + { + jObject = policy.ToObject()!; + } + catch (Exception e) + { + return new JTokenIsNotAnJObjectError("policy", e); + } + + var oneTimeUse = jObject + .GetByKey("one_time_use") + .OnSuccess(token => + { + var str = token.ToString(); + if (string.IsNullOrWhiteSpace(str)) + { + return new FieldValueIsNullOrEmptyError("one_time_use").ToInvalid(); + } + else + { + try + { + return bool.Parse(str); + } + catch (Exception e) + { + return new OneTimeUseIsNotABooleanValueError(str, e); + } + } + }); + + var batchSize = jObject + .GetByKey("batch_size") + .OnSuccess(token => + { + try + { + var str = token.ToString(); + return uint.Parse(str); + } + catch (Exception) + { + return new BatchSizeIsNotAPositiveNumberError().ToInvalid(); + } + }) + .ToOption(); + + return ValidationFun.Valid(Create) + .Apply(oneTimeUse) + .Apply(batchSize); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeId.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeId.cs new file mode 100644 index 00000000..7ffb7ff0 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeId.cs @@ -0,0 +1,37 @@ +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Errors; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct ProofTypeId +{ + private string Value { get; } + + private ProofTypeId(string value) + { + Value = value; + } + + public override string ToString() => Value; + + public static implicit operator string(ProofTypeId proofTypeId) => proofTypeId.ToString(); + + public static Validation ValidProofTypeId(JToken proofTypeId) => proofTypeId.ToJValue().OnSuccess(value => + { + var str = value.ToString(CultureInfo.InvariantCulture); + return SupportedProofTypes.Contains(str) + ? new ProofTypeId(str) + : new ProofTypeIdNotSupportedError(str).ToInvalid(); + }); + + private static List SupportedProofTypes => new() + { + "jwt" + }; +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeMetadata.cs new file mode 100644 index 00000000..300ef7ac --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeMetadata.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CryptograhicSigningAlgValue; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +/// +/// Represents proof type specific signing algorithm information. +/// +public record ProofTypeMetadata +{ + /// + /// Gets the available signing algorithms for the associated credential type. + /// + [JsonProperty("proof_signing_alg_values_supported")] + public List ProofSigningAlgValuesSupported { get; } + + private ProofTypeMetadata(List proofSigningAlgValuesSupported) + { + ProofSigningAlgValuesSupported = proofSigningAlgValuesSupported; + } + + public static Validation ValidProofTypeMetadata(JToken proofTypeMetadata) => + from jObject in proofTypeMetadata.ToJObject() + from jToken in jObject.GetByKey("proof_signing_alg_values_supported") + from jArray in jToken.ToJArray() + from algValues in jArray.TraverseAny(ValidCryptograhicSigningAlgValue) + select new ProofTypeMetadata(algValues.ToList()); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Scope.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Scope.cs new file mode 100644 index 00000000..3a6d371c --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Scope.cs @@ -0,0 +1,32 @@ +using System.Globalization; +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct Scope +{ + private string Value { get; } + + private Scope(string value) => Value = value; + + public override string ToString() => Value; + + public static implicit operator string(Scope scope) => scope.ToString(); + + public static Option OptionalScope(JToken scope) => scope.ToJValue().ToOption().OnSome(value => + { + var str = value.ToString(CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(str)) + { + return Option.None; + } + + return new Scope(str); + }); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs new file mode 100644 index 00000000..8b0c0668 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs @@ -0,0 +1,97 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.SdJwtVc.Models; +using WalletFramework.SdJwtVc.Models.Credential.Attributes; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CredentialConfiguration; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.SdJwt.SdJwtConfiguration.SdJwtConfigurationJsonKeys; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.SdJwt; + +[JsonConverter(typeof(SdJwtConfigurationJsonConverter))] +public record SdJwtConfiguration +{ + public CredentialConfiguration CredentialConfiguration { get; } + + public Format Format => CredentialConfiguration.Format; + + public Vct Vct { get; } + + /// + /// Gets or sets the dictionary representing the attributes of the credential in different languages. + /// + public Dictionary? Claims { get; set; } + + /// + /// A list of claim display names, arranged in the order in which they should be displayed by the Wallet. + /// + public List? Order { get; set; } + + private SdJwtConfiguration(CredentialConfiguration credentialConfiguration, Vct vct) + { + CredentialConfiguration = credentialConfiguration; + Vct = vct; + } + + private static SdJwtConfiguration Create(CredentialConfiguration credentialConfiguration, Vct vct) => + new(credentialConfiguration, vct); + + public static Validation ValidSdJwtCredentialConfiguration(JToken config) + { + var credentialConfiguration = ValidCredentialConfiguration(config); + var vct = config.GetByKey(VctJsonName).OnSuccess(Vct.ValidVct); + + var claims = config[ClaimsJsonName]?.ToObject>(); + var order = config[OrderJsonName]?.ToObject>(); + + var result = ValidationFun.Valid(Create) + .Apply(credentialConfiguration) + .Apply(vct) + .OnSuccess(configuration => configuration with + { + Claims = claims, + Order = order + }); + + return result; + } + + public static class SdJwtConfigurationJsonKeys + { + public const string VctJsonName = "vct"; + public const string ClaimsJsonName = "claims"; + public const string OrderJsonName = "order"; + } +} + +public class SdJwtConfigurationJsonConverter : JsonConverter +{ + public override bool CanRead => false; + + public override void WriteJson(JsonWriter writer, SdJwtConfiguration? sdJwtConfig, JsonSerializer serializer) + { + writer.WriteStartObject(); + + var credentialConfig = JObject.FromObject(sdJwtConfig!.CredentialConfiguration, serializer); + foreach (var property in credentialConfig.Properties()) + { + property.WriteTo(writer); + } + + writer.WritePropertyName(ClaimsJsonName); + serializer.Serialize(writer, sdJwtConfig.Claims); + + writer.WritePropertyName(OrderJsonName); + serializer.Serialize(writer, sdJwtConfig.Order); + + writer.WritePropertyName(VctJsonName); + serializer.Serialize(writer, sdJwtConfig.Vct); + + writer.WriteEndObject(); + } + + public override SdJwtConfiguration ReadJson(JsonReader reader, Type objectType, SdJwtConfiguration? existingValue, + bool hasExistingValue, JsonSerializer serializer) => + throw new NotImplementedException(); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SupportedCredentialConfiguration.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SupportedCredentialConfiguration.cs new file mode 100644 index 00000000..826cb54e --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SupportedCredentialConfiguration.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using OneOf; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.SdJwt; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +[JsonConverter(typeof(OneOfJsonConverter))] +public sealed class SupportedCredentialConfiguration : OneOfBase +{ + public static implicit operator OneOf( + SupportedCredentialConfiguration supportedCredentialConfiguration) => + supportedCredentialConfiguration.Match( + sdJwt => (OneOf)sdJwt, + mdoc => mdoc + ); + + public static implicit operator SupportedCredentialConfiguration(OneOf input) => + new(input); + + public static implicit operator SupportedCredentialConfiguration(SdJwtConfiguration sdJwtConfiguration) => + new(sdJwtConfiguration); + + public static implicit operator SupportedCredentialConfiguration(MdocConfiguration mdocConfiguration) => + new(mdocConfiguration); + + private SupportedCredentialConfiguration(OneOf input) : base(input) + { + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Abstractions/ICredentialOfferService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Abstractions/ICredentialOfferService.cs new file mode 100644 index 00000000..16c6e9be --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Abstractions/ICredentialOfferService.cs @@ -0,0 +1,10 @@ +using WalletFramework.Core.Functional; +using WalletFramework.Core.Localization; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Abstractions; + +public interface ICredentialOfferService +{ + public Task> ProcessCredentialOffer(Uri credentialOffer, Locale language); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CouldNotFetchCredentialOfferError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CouldNotFetchCredentialOfferError.cs new file mode 100644 index 00000000..61cbb4af --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CouldNotFetchCredentialOfferError.cs @@ -0,0 +1,7 @@ +using System.Net; +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; + +public record CouldNotFetchCredentialOfferError(HttpStatusCode Code) + : Error($"The credential offer could not be fetched with the status code: {Code}"); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialConfigurationIdError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialConfigurationIdError.cs new file mode 100644 index 00000000..f6cf0b1b --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialConfigurationIdError.cs @@ -0,0 +1,7 @@ +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; + +public record CredentialConfigurationIdError(JValue Value, Exception E) + : Error($"The CredentialConfigurationId could not be processed. The value is `{Value}`", E); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialConfigurationIdIsNullOrWhitespaceError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialConfigurationIdIsNullOrWhitespaceError.cs new file mode 100644 index 00000000..b18c21b8 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialConfigurationIdIsNullOrWhitespaceError.cs @@ -0,0 +1,6 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; + +public record CredentialConfigurationIdIsNullOrWhitespaceError() + : Error("The CredentialConfigurationId is null or whitespace"); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialIssuerError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialIssuerError.cs new file mode 100644 index 00000000..3cd48ad9 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialIssuerError.cs @@ -0,0 +1,7 @@ +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; + +public record CredentialIssuerError(JValue Value, Exception E) + : Error($"The credential issuer field could not be processed. The value is: `{Value}`", E); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialOfferHasNoQueryParameterError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialOfferHasNoQueryParameterError.cs new file mode 100644 index 00000000..3ec402c0 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialOfferHasNoQueryParameterError.cs @@ -0,0 +1,6 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; + +public record CredentialOfferHasNoQueryParameterError(Exception E) + : Error("The credential offer contains no query parameters", E); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialOfferNotFoundError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialOfferNotFoundError.cs new file mode 100644 index 00000000..574b8cc6 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Errors/CredentialOfferNotFoundError.cs @@ -0,0 +1,6 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; + +public record CredentialOfferNotFoundError() + : Error("Neither an embedded credential offer or a credential offer uri could be found"); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/GrantTypes/AuthorizationCode.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/GrantTypes/AuthorizationCode.cs new file mode 100644 index 00000000..03fb69e5 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/GrantTypes/AuthorizationCode.cs @@ -0,0 +1,55 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.GrantTypes; + +/// +/// Represents the parameters for the 'authorization_code' grant type. +/// +public record AuthorizationCode +{ + /// + /// 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 + /// + [JsonProperty("issuer_state")] + public Option IssuerState { get; } + + /// + /// 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. + /// + [JsonProperty("authorization_server")] + public Option AuthorizationServer { get; } + + private AuthorizationCode(Option issuerState, Option authorizationServer) + { + IssuerState = issuerState; + AuthorizationServer = authorizationServer; + } + + public static Option OptionalAuthorizationCode(JToken authorizationCode) + { + var issuerState = authorizationCode + .GetByKey("issuer_state") + .OnSuccess(token => token.ToString()) + .ToOption(); + + var authServer = authorizationCode + .GetByKey("authorization_server") + .OnSuccess(token => token.ToString()) + .ToOption(); + + if (issuerState.IsNone && authServer.IsNone) + return Option.None; + + return new AuthorizationCode(issuerState, authServer); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/GrantTypes/PreAuthorizedCode.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/GrantTypes/PreAuthorizedCode.cs new file mode 100644 index 00000000..48e05d61 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/GrantTypes/PreAuthorizedCode.cs @@ -0,0 +1,155 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using static WalletFramework.Oid4Vc.Oid4Vci.CredOffer.GrantTypes.TransactionCode; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.GrantTypes; + +/// +/// Represents the parameters for the 'pre-authorized_code' grant type. +/// +public record PreAuthorizedCode +{ + /// + /// Gets the pre-authorized code representing the Credential Issuer's authorization for the Wallet to obtain + /// Credentials of a certain type. + /// + [JsonProperty("pre-authorized_code")] + public string Value { get; } + + /// + /// Specifying whether the user must send a Transaction Code along with the Token Request in a Pre-Authorized Code Flow. + /// + [JsonProperty("tx_code")] + public Option TransactionCode { get; } + + private PreAuthorizedCode(string value, Option transactionCode) + { + Value = value; + TransactionCode = transactionCode; + } + + public static Option OptionalPreAuthorizedCode(JToken preAuthCode) + { + var transactionCode = preAuthCode + .GetByKey("tx_code") + .ToOption() + .OnSome(OptionalTransactionCode); + + return preAuthCode + .GetByKey("pre-authorized_code") + .OnSuccess(token => + { + var value = token.ToString(); + return new PreAuthorizedCode(value, transactionCode); + }) + .ToOption(); + } + + public override string ToString() => Value; + + public static implicit operator string(PreAuthorizedCode preAuthorizedCode) => preAuthorizedCode.Value; +} + +/// +/// Represents the details of the expected Transaction Code. +/// +public record TransactionCode +{ + /// + /// Gets the length of the transaction code. + /// + [JsonProperty("length")] + public Option Length { get; } + + /// + /// Gets the description of the transaction code. + /// + [JsonProperty("description")] + public Option Description { get; } + + /// + /// Gets the input mode of the transaction code which specifies the valid character set. (Must be 'numeric' ot 'text') + /// + [JsonProperty("input_mode")] + public Option InputMode { get; } + + private TransactionCode( + Option length, + Option description, + Option inputMode) + { + Length = length; + Description = description; + InputMode = inputMode; + } + + public static Option OptionalTransactionCode(JToken transactionCode) + { + var length = transactionCode + .GetByKey("length") + .OnSuccess(token => token.ToJValue()) + .OnSuccess(value => value.ToInt()) + .ToOption(); + + var description = transactionCode + .GetByKey("description") + .OnSuccess(token => token.ToString()) + .ToOption(); + + var inputMode = transactionCode + .GetByKey("input_mode") + .ToOption() + .OnSome(GrantTypes.InputMode.OptionalInputMode); + + if (length.IsNone && description.IsNone && inputMode.IsNone) + return Option.None; + + return new TransactionCode(length, description, inputMode); + } +} + +public record InputMode +{ + private InputModeValues Value { get; } + + private InputMode(InputModeValues value) => Value = value; + + public static Option OptionalInputMode(JToken inputMode) + { + try + { + var str = inputMode.ToString(); + return str switch + { + "numeric" => new InputMode(InputModeValues.Numeric), + "text" => new InputMode(InputModeValues.Text), + _ => Option.None + }; + } + catch (Exception) + { + return Option.None; + } + } + + public static implicit operator string(InputMode inputMode) => inputMode.ToString(); + + public override string ToString() => ParseInputMode(Value); + + private enum InputModeValues + { + Numeric, + Text + } + + private static string ParseInputMode(InputModeValues values) => + values switch + { + InputModeValues.Numeric => "numeric", + InputModeValues.Text => "text", + _ => throw new ArgumentOutOfRangeException(nameof(values), values, "Invalid InputModeValues value") + }; +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Implementations/CredentialOfferService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Implementations/CredentialOfferService.cs new file mode 100644 index 00000000..6a980239 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Implementations/CredentialOfferService.cs @@ -0,0 +1,53 @@ +using System.Collections.Specialized; +using System.Net; +using System.Web; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Localization; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; +using static WalletFramework.Core.Json.JsonFun; +using static WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models.CredentialOffer; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Implementations; + +public class CredentialOfferService : ICredentialOfferService +{ + public CredentialOfferService(IHttpClientFactory httpClientFactory) + { + _httpClient = httpClientFactory.CreateClient(); + } + + private readonly HttpClient _httpClient; + + public async Task> ProcessCredentialOffer(Uri credentialOffer, Locale language) + { + NameValueCollection queryParams; + try + { + queryParams = HttpUtility.ParseQueryString(credentialOffer.Query); + } + catch (Exception e) + { + return new CredentialOfferHasNoQueryParameterError(e); + } + + if (queryParams["credential_offer"] is { } offer) + return ParseAsJObject(offer).OnSuccess(ValidCredentialOffer); + + if (queryParams["credential_offer_uri"] is { } offerUri) + { + _httpClient.DefaultRequestHeaders.Add("Accept-Language", language); + var response = await _httpClient.GetAsync(offerUri); + if (response.StatusCode == HttpStatusCode.OK) + { + var content = await response.Content.ReadAsStringAsync(); + return ParseAsJObject(content).OnSuccess(ValidCredentialOffer); + } + + return new CouldNotFetchCredentialOfferError(response.StatusCode); + } + + return new CredentialOfferNotFoundError(); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialConfigurationId.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialConfigurationId.cs new file mode 100644 index 00000000..d0010ec4 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialConfigurationId.cs @@ -0,0 +1,47 @@ +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct CredentialConfigurationId +{ + private string Value { get; } + + public static implicit operator string(CredentialConfigurationId id) => id.ToString(); + + private CredentialConfigurationId(string id) => Value = id; + + public override string ToString() => Value; + + public static Validation ValidCredentialConfigurationId(JToken id) => + id.ToJValue().OnSuccess(value => + { + try + { + var str = value.ToString(CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(str)) + return new CredentialConfigurationIdIsNullOrWhitespaceError(); + + return new CredentialConfigurationId(str); + } + catch (Exception e) + { + return new CredentialConfigurationIdError(value, e).ToInvalid(); + } + }); +} + +public class CredentialConfigurationIdDecoder : IValueTypeDecoder +{ + public CredentialConfigurationId Decode(JToken token) => + CredentialConfigurationId + .ValidCredentialConfigurationId(token) + .UnwrapOrThrow(new InvalidOperationException("CredentialConfigurationId is corrupt")); +} + diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialOffer.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialOffer.cs new file mode 100644 index 00000000..6671afdf --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialOffer.cs @@ -0,0 +1,82 @@ +using System.Globalization; +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; +using static WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models.CredentialConfigurationId; +using static WalletFramework.Core.Functional.ValidationFun; +using static WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models.Grants; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; + +/// +/// Represents an OpenID4VCI Credential Offer, which is used to obtain one or more credentials from a Credential +/// Issuer. +/// +public record CredentialOffer +{ + /// + /// Gets the URL of the Credential Issuer from where the Wallet is requested to obtain one or more Credentials + /// from. + /// + [JsonProperty("credential_issuer")] + public Uri CredentialIssuer { get; } + + /// + /// Gets the list of credentials that the Wallet may request. The List contains CredentialMetadataIds + /// that must map to the keys in the credential_configurations_supported dictionary of the Issuer Metadata + /// + [JsonProperty("credential_configuration_ids")] + public List CredentialConfigurationIds { get; } + + /// + /// Gets the JSON object indicating to the Wallet the Grant Types the Credential Issuer's Authorization Server + /// is prepared to process for this credential offer. If not present or empty, the Wallet must determine the + /// Grant Types the Credential Issuer's AS supports using the respective metadata. + /// + [JsonProperty("grants")] + public Option Grants { get; } + + private CredentialOffer( + Uri credentialIssuer, + List credentialConfigurationIds, + Option grants) + { + CredentialIssuer = credentialIssuer; + CredentialConfigurationIds = credentialConfigurationIds; + Grants = grants; + } + + private static CredentialOffer Create( + Uri credentialIssuer, + List credentialConfigurationIds, + Option grants) => new(credentialIssuer, credentialConfigurationIds, grants); + + public static Validation ValidCredentialOffer(JObject json) + { + var validCredentialIssuer = new Func>(token => token.ToJValue().OnSuccess(value => + { + try + { + var str = value.ToString(CultureInfo.InvariantCulture); + return new Uri(str); + } + catch (Exception e) + { + return new CredentialIssuerError(value, e).ToInvalid(); + } + })); + + var validCredentialConfigurationsIds = new Func>>(token => + from jArray in token.ToJArray() + from configurationIds in jArray.TraverseAny(ValidCredentialConfigurationId) + select configurationIds.ToList()); + + return Valid(Create) + .Apply(json.GetByKey("credential_issuer").OnSuccess(validCredentialIssuer)) + .Apply(json.GetByKey("credential_configuration_ids").OnSuccess(validCredentialConfigurationsIds)) + .Apply(json.GetByKey("grants").OnSuccess(OptionalGrants)); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialOfferMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialOfferMetadata.cs new file mode 100644 index 00000000..4fda7240 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialOfferMetadata.cs @@ -0,0 +1,5 @@ +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; + +public record CredentialOfferMetadata(CredentialOffer CredentialOffer, IssuerMetadata IssuerMetadata); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/Grants.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/Grants.cs new file mode 100644 index 00000000..5f168592 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/Grants.cs @@ -0,0 +1,55 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.GrantTypes; +using static WalletFramework.Oid4Vc.Oid4Vci.CredOffer.GrantTypes.AuthorizationCode; +using static WalletFramework.Oid4Vc.Oid4Vci.CredOffer.GrantTypes.PreAuthorizedCode; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; + +/// +/// Represents the grant types that the Credential Issuer's AS is prepared to process for the credential offer. +/// +public record Grants +{ + /// + /// Gets the authorization_code grant type parameters. This includes an optional issuer state that is used to + /// bind the subsequent Authorization Request with the Credential Issuer to a context set up during previous steps. + /// + [JsonProperty("authorization_code")] + public Option AuthorizationCode { get; } + + /// + /// Gets the pre-authorized_code grant type parameters. This includes a required pre-authorized code + /// representing the Credential Issuer's authorization for the Wallet to obtain Credentials of a certain type, and an + /// optional boolean specifying whether a user PIN is required along with the Token Request. + /// + [JsonProperty("urn:ietf:params:oauth:grant-type:pre-authorized_code")] + public Option PreAuthorizedCode { get; } + + private Grants(Option authorizationCode, Option preAuthorizedCode) + { + AuthorizationCode = authorizationCode; + PreAuthorizedCode = preAuthorizedCode; + } + + public static Option OptionalGrants(JToken grants) + { + var authorizationCode = grants + .GetByKey("authorization_code") + .ToOption() + .OnSome(OptionalAuthorizationCode); + + var preAuthorizedCode = grants + .GetByKey("urn:ietf:params:oauth:grant-type:pre-authorized_code") + .ToOption() + .OnSome(OptionalPreAuthorizedCode); + + if (authorizationCode.IsNone && preAuthorizedCode.IsNone) + return Option.None; + + return new Grants(authorizationCode, preAuthorizedCode); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs new file mode 100644 index 00000000..1654bce1 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Abstractions/ICredentialRequestService.cs @@ -0,0 +1,21 @@ +using LanguageExt; +using OneOf; +using WalletFramework.Core.Functional; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.SdJwt; +using WalletFramework.Oid4Vc.Oid4Vci.CredResponse; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Abstractions; + +public interface ICredentialRequestService +{ + public Task> RequestCredentials( + OneOf configuration, + IssuerMetadata issuerMetadata, + OneOf token, + Option clientOptions); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs new file mode 100644 index 00000000..733c7738 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs @@ -0,0 +1,137 @@ +using System.Text; +using LanguageExt; +using OneOf; +using WalletFramework.Core.Cryptography.Abstractions; +using WalletFramework.Core.Cryptography.Models; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Uri; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.SdJwt; +using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models.Mdoc; +using WalletFramework.Oid4Vc.Oid4Vci.Extensions; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models.SdJwt; +using WalletFramework.Oid4Vc.Oid4Vci.CredResponse; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Implementations; + +public class CredentialRequestService : ICredentialRequestService +{ + public CredentialRequestService( + HttpClient httpClient, + IDPopHttpClient dPopHttpClient, + IKeyStore keyStore) + { + _dPopHttpClient = dPopHttpClient; + _httpClient = httpClient; + _keyStore = keyStore; + } + + private readonly HttpClient _httpClient; + private readonly IDPopHttpClient _dPopHttpClient; + private readonly IKeyStore _keyStore; + + private async Task CreateCredentialRequest( + KeyId keyId, + Format format, + IssuerMetadata issuerMetadata, + OneOf token, + Option clientOptions) + { + var cNonce = token.Match( + oauthToken => oauthToken.CNonce, + dPopToken => dPopToken.Token.CNonce); + + var keyBindingJwt = await _keyStore.GenerateKbProofOfPossessionAsync( + keyId, + issuerMetadata.CredentialIssuer.ToString(), + cNonce, + "openid4vci-proof+jwt", + null, + clientOptions.ToNullable()?.ClientId); + + var proof = new ProofOfPossession + { + ProofType = "jwt", + Jwt = keyBindingJwt + }; + + return new CredentialRequest(proof, format); + } + + async Task> ICredentialRequestService.RequestCredentials( + OneOf configuration, + IssuerMetadata issuerMetadata, + OneOf token, + Option clientOptions) + { + var keyId = await _keyStore.GenerateKey(); + + var requestJson = await configuration.Match( + async sdJwt => + { + var vciRequest = await CreateCredentialRequest( + keyId, + sdJwt.Format, + issuerMetadata, + token, + clientOptions); + + var result = new SdJwtCredentialRequest(vciRequest, sdJwt.Vct); + return result.AsJson(); + }, + async mdoc => + { + var vciRequest = await CreateCredentialRequest( + keyId, + mdoc.Format, + issuerMetadata, + token, + clientOptions); + + var result = new MdocCredentialRequest(vciRequest, mdoc); + return result.AsJson(); + } + ); + + var content = new StringContent( + requestJson, + Encoding.UTF8, + "application/json"); + + var response = await token.Match( + async authToken => await _httpClient + .WithAuthorizationHeader(authToken) + .PostAsync(issuerMetadata.CredentialEndpoint, content), + async dPopToken => + { + var config = dPopToken.DPop.Config with + { + Audience = issuerMetadata.CredentialEndpoint.ToStringWithoutTrail(), + OAuthToken = dPopToken.Token + }; + + var dPopResponse = await _dPopHttpClient.Post( + issuerMetadata.CredentialEndpoint, + content, + config); + + return dPopResponse.ResponseMessage; + }); + + var responseContent = await response.Content.ReadAsStringAsync(); + + return + from jObject in JsonFun.ParseAsJObject(responseContent) + from credResponse in CredentialResponse.ValidCredentialResponse(jObject, keyId) + select credResponse; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs new file mode 100644 index 00000000..a72c0652 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs @@ -0,0 +1,27 @@ +using LanguageExt; +using Newtonsoft.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models; + +/// +/// Represents a credential request made by a client to the Credential Endpoint. +/// This request contains the format of the credential, the type of credential, +/// and a proof of possession of the key material the issued credential shall be bound to. +/// +public record CredentialRequest(Option Proof, Format Format) +{ + /// + /// Gets the proof of possession of the key material the issued credential shall be bound to. + /// + [JsonProperty("proof")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Proof { get; } = Proof; + + /// + /// Gets the format of the credential to be issued. + /// + [JsonProperty("format")] + public Format Format { get; } = Format; +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/Mdoc/MdocCredentialRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/Mdoc/MdocCredentialRequest.cs new file mode 100644 index 00000000..15b9f8cc --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/Mdoc/MdocCredentialRequest.cs @@ -0,0 +1,43 @@ +using LanguageExt; +using Newtonsoft.Json.Linq; +using WalletFramework.MdocLib; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models.Mdoc; + +public record MdocCredentialRequest +{ + public CredentialRequest VciRequest { get; } + + public DocType DocType { get; } + + public Option NamespacedData { get; } + + public MdocCredentialRequest(CredentialRequest credentialRequest, MdocConfiguration configuration) + { + VciRequest = credentialRequest; + DocType = configuration.DocType; + + // TODO: Decide if this should be true or false + NamespacedData = false; + } +} + +public static class MdocCredentialRequestFun +{ + public static string AsJson(this MdocCredentialRequest request) + { + var json = new JObject(); + + var vciRequest = JObject.FromObject(request.VciRequest); + foreach (var property in vciRequest.Properties()) + { + json.Add(property); + } + + var vct = JToken.FromObject(request.DocType); + json.Add("doctype", vct); + + return json.ToString(); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/ProofOfPossession.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/ProofOfPossession.cs new file mode 100644 index 00000000..f653ac53 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/ProofOfPossession.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models; + +/// +/// Represents the proof of possession of the key material that the issued credential is bound to. +/// This contains the JWT that acts as the proof of possession, with the proof type being "jwt". +/// +// TODO: Unpure +public class ProofOfPossession +{ + /// + /// Gets or sets the JWT that acts as the proof of possession of the key material the issued credential is bound to. + /// + [JsonProperty("jwt")] + public string Jwt { get; set; } + + /// + /// Gets or sets the type of proof, expected to be "jwt". + /// + [JsonProperty("proof_type")] + public string ProofType { get; set; } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/SdJwt/SdJwtCredentialRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/SdJwt/SdJwtCredentialRequest.cs new file mode 100644 index 00000000..088c48a9 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/SdJwt/SdJwtCredentialRequest.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.SdJwtVc.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models.SdJwt; + +public record SdJwtCredentialRequest +{ + public CredentialRequest VciRequest { get; } + + /// + /// Gets the verifiable credential type (vct). + /// + [JsonProperty("vct")] + public Vct Vct { get; } + + internal SdJwtCredentialRequest(CredentialRequest vciRequest, Vct vct) + { + VciRequest = vciRequest; + Vct = vct; + } +} + +public static class SdJwtCredentialRequestFun +{ + public static string AsJson(this SdJwtCredentialRequest request) + { + var json = new JObject(); + + var vciRequest = JObject.FromObject(request.VciRequest); + foreach (var property in vciRequest.Properties()) + { + json.Add(property); + } + + var vct = JToken.FromObject(request.Vct); + json.Add("vct", vct); + + return json.ToString(); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/CredentialResponse.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/CredentialResponse.cs new file mode 100644 index 00000000..d817c7c1 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/CredentialResponse.cs @@ -0,0 +1,139 @@ +using System.Globalization; +using LanguageExt; +using Newtonsoft.Json.Linq; +using OneOf; +using WalletFramework.Core.Cryptography.Models; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Errors; +using WalletFramework.Oid4Vc.Oid4Vci.CredResponse.Mdoc; +using WalletFramework.Oid4Vc.Oid4Vci.CredResponse.SdJwt; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredResponse; + +/// +/// Represents a Credential Response. The response can be either immediate or deferred. In the synchronous response, +/// the issued Credential is immediately returned to the client. In the deferred response, a transaction ID +/// is sent to the client, which will be used later to retrieve the Credential once it's ready. +/// +public record CredentialResponse +{ + /// + /// + /// Credential: Contains issued Credential. It MUST be present when transaction_id is not returned. It MAY be a + /// string or an object, depending on the Credential format + /// + /// + /// TransactionId: String identifying a Deferred Issuance transaction. This claim is contained in the response if + /// the Credential Issuer was unable to immediately issue the Credential. The value is subsequently used to obtain the + /// respective Credential with the Deferred Credential Endpoint. It MUST be present when the + /// credential parameter is not returned. It MUST be invalidated after the Credential for which it was meant + /// has been obtained by the Wallet + /// + /// + public OneOf CredentialOrTransactionId { get; } + + /// + /// OPTIONAL. JSON string containing a nonce to be used to create a proof of possession of key material + /// when requesting a Credential + /// + public Option CNonce { get; } + + /// + /// OPTIONAL. JSON integer denoting the lifetime in seconds of the c_nonce + /// + public Option CNonceExpiresIn { get; } + + /// + /// The KeyId for the key which was used for the Key-Binding Proof + /// + public KeyId KeyId { get; } + + private CredentialResponse( + OneOf credentialOrTransactionId, + Option cNonce, + Option cNonceExpiresIn, + KeyId keyId) + { + CNonceExpiresIn = cNonceExpiresIn; + CredentialOrTransactionId = credentialOrTransactionId; + CNonce = cNonce; + KeyId = keyId; + } + + private static CredentialResponse Create( + OneOf credentialOrTransactionId, + Option cNonce, + Option cNonceExpiresIn, + KeyId keyId) => + new(credentialOrTransactionId, cNonce, cNonceExpiresIn, keyId); + + public static Validation ValidCredentialResponse(JObject response, KeyId keyId) + { + // TODO: Implement transactionID + var credential = + from jToken in response.GetByKey("credential") + from jValue in jToken.ToJValue() + from cred in Credential.ValidCredential(jValue) + select (OneOf)cred; + + var cNonce = response + .GetByKey("c_nonce") + .OnSuccess(token => token.ToJValue()) + .OnSuccess(value => + { + var str = value.ToString(CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(str)) + { + return new StringIsNullOrWhitespaceError(); + } + + return ValidationFun.Valid(str); + }) + .ToOption(); + + var cNonceExpiresIn = response + .GetByKey("c_nonce_expires_in") + .OnSuccess(token => token.ToJValue()) + .OnSuccess(value => + { + var str = value.ToString(CultureInfo.InvariantCulture); + if (int.TryParse(str, out var result)) + { + return ValidationFun.Valid(result); + } + + return new JValueIsNotAnIntError(str); + }) + .ToOption(); + + return ValidationFun.Valid(Create) + .Apply(credential) + .Apply(cNonce) + .Apply(cNonceExpiresIn) + .Apply(keyId); + } + + public readonly struct Credential + { + public OneOf Value { get; } + + private Credential(OneOf value) + { + Value = value; + } + + public static Validation ValidCredential(JValue credential) + { + var firstValid = new List> + { + value => EncodedSdJwt.ValidEncodedSdJwt(value).OnSuccess(sdJwt => new Credential(sdJwt)), + value => EncodedMdoc.ValidEncodedMdoc(value).OnSuccess(mdoc => new Credential(mdoc)) + } + .FirstValid(); + + return firstValid(credential); + } + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/Mdoc/EncodedMdoc.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/Mdoc/EncodedMdoc.cs new file mode 100644 index 00000000..4b6daf84 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/Mdoc/EncodedMdoc.cs @@ -0,0 +1,31 @@ +using System.Globalization; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredResponse.Mdoc; + +public record EncodedMdoc +{ + private string Value { get; } + + public MdocLib.Mdoc Decoded { get; } + + private EncodedMdoc(string value, MdocLib.Mdoc decoded) + { + Value = value; + Decoded = decoded; + } + + public override string ToString() => Value; + + public static implicit operator string(EncodedMdoc encodedMdoc) => encodedMdoc.Value; + + public static Validation ValidEncodedMdoc(JValue mdoc) + { + var str = mdoc.ToString(CultureInfo.InvariantCulture); + + return MdocLib.Mdoc + .ValidMdoc(str) + .OnSuccess(mdoc1 => new EncodedMdoc(str, mdoc1)); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/SdJwt/EncodedSdJwt.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/SdJwt/EncodedSdJwt.cs new file mode 100644 index 00000000..6b0364d3 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/SdJwt/EncodedSdJwt.cs @@ -0,0 +1,38 @@ +using System.Globalization; +using Newtonsoft.Json.Linq; +using SD_JWT.Models; +using WalletFramework.Core.Functional; +using WalletFramework.Oid4Vc.Oid4Vci.CredResponse.SdJwt.Errors; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredResponse.SdJwt; + +public record EncodedSdJwt +{ + private string Value { get; } + + public SdJwtDoc Decoded { get; } + + private EncodedSdJwt(string value, SdJwtDoc decoded) + { + Value = value; + Decoded = decoded; + } + + public override string ToString() => Value; + + public static implicit operator string(EncodedSdJwt encodedSdJwt) => encodedSdJwt.Value; + + public static Validation ValidEncodedSdJwt(JValue sdJwt) + { + var str = sdJwt.ToString(CultureInfo.InvariantCulture); + try + { + var sdJwtDoc = new SdJwtDoc(str); + return new EncodedSdJwt(str, sdJwtDoc); + } + catch (Exception e) + { + return new SdJwtParsingError(sdJwt, e); + } + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/SdJwt/Errors/SdJwtError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/SdJwt/Errors/SdJwtError.cs new file mode 100644 index 00000000..b7be1940 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/SdJwt/Errors/SdJwtError.cs @@ -0,0 +1,8 @@ +using System.Globalization; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.CredResponse.SdJwt.Errors; + +public record SdJwtParsingError(JValue Value, Exception E) + : Error($"The encoded SD-JWT could not be parsed. Value is: {Value.ToString(CultureInfo.InvariantCulture)}", E); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/TransactionId.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/TransactionId.cs new file mode 100644 index 00000000..49cc8002 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredResponse/TransactionId.cs @@ -0,0 +1,6 @@ +namespace WalletFramework.Oid4Vc.Oid4Vci.CredResponse; + +public struct TransactionId +{ + +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Extensions/Oid4VciHttpClientExtensions.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Extensions/Oid4VciHttpClientExtensions.cs index 3b2bb5df..2f044579 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Extensions/Oid4VciHttpClientExtensions.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Extensions/Oid4VciHttpClientExtensions.cs @@ -1,22 +1,24 @@ -using WalletFramework.Oid4Vc.Oid4Vci.Models.DPop; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; -namespace WalletFramework.Oid4Vc.Oid4Vci.Extensions +namespace WalletFramework.Oid4Vc.Oid4Vci.Extensions; + +internal static class Oid4VciHttpClientExtensions { - internal static class Oid4VciHttpClientExtensions + public static HttpClient WithDPopHeader(this HttpClient httpClient, string dPopProofJwt) { - public static void AddDPopHeader(this HttpClient httpClient, string dPopProofJwt) - { - httpClient.DefaultRequestHeaders.Remove("DPoP"); - httpClient.DefaultRequestHeaders.Add("DPoP", dPopProofJwt); - } - - public static void AddAuthorizationHeader(this HttpClient httpClient, OAuthToken oAuthToken) - { - httpClient.DefaultRequestHeaders.Remove("Authorization"); - httpClient.DefaultRequestHeaders.Add( - "Authorization", - $"{oAuthToken.TokenResponse.TokenType} {oAuthToken.TokenResponse.AccessToken}" - ); - } + httpClient.DefaultRequestHeaders.Remove("DPoP"); + httpClient.DefaultRequestHeaders.Add("DPoP", dPopProofJwt); + + return httpClient; + } + + public static HttpClient WithAuthorizationHeader(this HttpClient httpClient, OAuthToken oAuthToken) + { + httpClient.DefaultRequestHeaders.Remove("Authorization"); + httpClient.DefaultRequestHeaders.Add( + "Authorization", + $"{oAuthToken.TokenType} {oAuthToken.AccessToken}"); + + return httpClient; } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocFun.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocFun.cs new file mode 100644 index 00000000..5ed02a39 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocFun.cs @@ -0,0 +1,41 @@ +using LanguageExt; +using WalletFramework.MdocVc; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Implementations; + +public static class MdocFun +{ + public static Option> CreateMdocDisplays(MdocConfiguration configuration) + { + var claimsDisplays = + from claimsMetadata in configuration.Claims + from dict in claimsMetadata.ToClaimsDisplays() + select dict; + + var credentialConfigurationDisplay = configuration.CredentialConfiguration.Display; + var result = + from credentialDisplays in credentialConfigurationDisplay + let mdocDisplays = credentialDisplays.Select(credentialDisplay => + { + var logo = + from credentialLogo in credentialDisplay.Logo + from uri in credentialLogo.Uri + select new MdocLogo(uri); + + var mdocName = + from credentialName in credentialDisplay.Name + from name in MdocName.OptionMdocName(credentialName.ToString()) + select name; + + var backgroundColor = credentialDisplay.BackgroundColor; + var textColor = credentialDisplay.TextColor; + var locale = credentialDisplay.Locale; + + return new MdocDisplay(logo, mdocName, backgroundColor, textColor, locale, claimsDisplays); + }).ToList() + select mdocDisplays; + + return result; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocStorage.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocStorage.cs new file mode 100644 index 00000000..95cfc647 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocStorage.cs @@ -0,0 +1,68 @@ +using Hyperledger.Aries.Agents; +using Hyperledger.Aries.Storage; +using LanguageExt; +using WalletFramework.Core.Credentials; +using WalletFramework.Core.Functional; +using WalletFramework.MdocVc; +using WalletFramework.Oid4Vc.Oid4Vci.Abstractions; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Implementations; + +public class MdocStorage : IMdocStorage +{ + public MdocStorage(IAgentProvider agentProvider, IWalletRecordService recordService) + { + _agentProvider = agentProvider; + _recordService = recordService; + } + + private readonly IAgentProvider _agentProvider; + private readonly IWalletRecordService _recordService; + + public async Task Add(MdocRecord record) + { + var context = await _agentProvider.GetContextAsync(); + await _recordService.AddAsync(context.Wallet, record); + return Unit.Default; + } + + public async Task> Get(CredentialId id) + { + var context = await _agentProvider.GetContextAsync(); + return await _recordService.GetAsync(context.Wallet, id, MdocRecordFun.DecodeFromJson); + } + + public async Task>> List( + Option query, + int count = 100, + int skip = 0) + { + var context = await _agentProvider.GetContextAsync(); + var list = await _recordService.SearchAsync( + context.Wallet, + query.ToNullable(), + null, + count, + skip, + MdocRecordFun.DecodeFromJson); + + if (list.Count == 0) + return Option>.None; + + return list; + } + + public async Task Update(MdocRecord record) + { + var context = await _agentProvider.GetContextAsync(); + await _recordService.Update(context.Wallet, record); + return Unit.Default; + } + + public async Task Delete(MdocRecord record) + { + var context = await _agentProvider.GetContextAsync(); + await _recordService.Delete(context.Wallet, record); + return Unit.Default; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs new file mode 100644 index 00000000..834ecd54 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/Oid4VciClientService.cs @@ -0,0 +1,351 @@ +using System.Security.Cryptography; +using System.Text; +using Hyperledger.Aries.Agents; +using LanguageExt; +using Microsoft.IdentityModel.Tokens; +using WalletFramework.Oid4Vc.Oid4Vci.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; +using OneOf; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Localization; +using WalletFramework.MdocVc; +using WalletFramework.SdJwtVc.Models.Records; +using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; +using static Newtonsoft.Json.JsonConvert; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Implementations; + +/// +public class Oid4VciClientService : IOid4VciClientService +{ + private const string AuthorizationCodeGrantTypeIdentifier = "authorization_code"; + private const string PreAuthorizedCodeGrantTypeIdentifier = "urn:ietf:params:oauth:grant-type:pre-authorized_code"; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The credential request service. + /// + /// + /// The factory to create instances of . Used for making HTTP + /// requests. + /// + /// The authorization record service + /// + /// The token service. + /// + /// + public Oid4VciClientService( + IAgentProvider agentProvider, + ICredentialOfferService credentialOfferService, + ICredentialRequestService credentialRequestService, + IMdocStorage mdocStorage, + IIssuerMetadataService issuerMetadataService, + IHttpClientFactory httpClientFactory, + IAuthFlowSessionStorage authFlowSessionAuthFlowSessionStorage, + ISdJwtVcHolderService sdJwtService, + ITokenService tokenService) + { + _agentProvider = agentProvider; + _credentialOfferService = credentialOfferService; + _credentialRequestService = credentialRequestService; + _httpClient = httpClientFactory.CreateClient(); + _issuerMetadataService = issuerMetadataService; + _mdocStorage = mdocStorage; + _authFlowSessionStorage = authFlowSessionAuthFlowSessionStorage; + _sdJwtService = sdJwtService; + _tokenService = tokenService; + } + + private readonly HttpClient _httpClient; + private readonly IAgentProvider _agentProvider; + private readonly IAuthFlowSessionStorage _authFlowSessionStorage; + private readonly ICredentialOfferService _credentialOfferService; + private readonly ICredentialRequestService _credentialRequestService; + private readonly IIssuerMetadataService _issuerMetadataService; + private readonly IMdocStorage _mdocStorage; + private readonly ISdJwtVcHolderService _sdJwtService; + private readonly ITokenService _tokenService; + + /// + public async Task InitiateAuthFlow(CredentialOfferMetadata offer, ClientOptions clientOptions) + { + var authorizationCodeParameters = CreateAndStoreCodeChallenge(); + var sessionId = VciSessionId.CreateSessionId(); + var issuerMetadata = offer.IssuerMetadata; + + var scopes = offer + .CredentialOffer + .CredentialConfigurationIds + .Select(id => issuerMetadata.CredentialConfigurationsSupported[id]) + .Select(oneOf => oneOf.Match( + sdJwt => sdJwt.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()), + mdoc => mdoc.CredentialConfiguration.Scope.OnSome(scope => scope.ToString()) + )) + .Where(option => option.IsSome) + .Select(option => option.Fallback(string.Empty)); + + var scope = string.Join(" ", scopes); + + var authorizationDetails = issuerMetadata + .CredentialConfigurationsSupported + .Where(config => offer.CredentialOffer.CredentialConfigurationIds.Contains(config.Key)) + .Select(pair => pair.Value.Match( + sdJwt => new AuthorizationDetails( + null, + sdJwt.Vct.ToString(), + pair.Key.ToString(), + issuerMetadata.AuthorizationServers.ToNullable()?.Select(id => id.ToString()).ToArray(), + null + ), + mdoc => new AuthorizationDetails( + null, + null, + pair.Key.ToString(), + issuerMetadata.AuthorizationServers.ToNullable()?.Select(id => id.ToString()).ToArray(), + mdoc.DocType.ToString())) + ); + + var authCode = + from grants in offer.CredentialOffer.Grants + from code in grants.AuthorizationCode + select code; + + var issuerState = + from code in authCode + from issState in code.IssuerState + select issState; + + var par = new PushedAuthorizationRequest( + sessionId, + clientOptions, + authorizationCodeParameters, + authorizationDetails.ToArray(), + scope, + issuerState.ToNullable(), + null, + null); + + var authServerMetadata = + await FetchAuthorizationServerMetadataAsync(issuerMetadata); + + _httpClient.DefaultRequestHeaders.Clear(); + var response = await _httpClient.PostAsync( + authServerMetadata.PushedAuthorizationRequestEndpoint, + par.ToFormUrlEncoded() + ); + + var parResponse = DeserializeObject(await response.Content.ReadAsStringAsync()) + ?? throw new InvalidOperationException("Failed to deserialize the PAR response."); + + var authorizationRequestUri = new Uri(authServerMetadata.AuthorizationEndpoint + + "?client_id=" + par.ClientId + + "&request_uri=" + System.Net.WebUtility.UrlEncode(parResponse.RequestUri.ToString())); + + var authorizationData = new AuthorizationData( + clientOptions, + issuerMetadata, + authServerMetadata, + offer.CredentialOffer.CredentialConfigurationIds); + + var context = await _agentProvider.GetContextAsync(); + await _authFlowSessionStorage.StoreAsync( + context, + authorizationData, + authorizationCodeParameters, + sessionId); + + return authorizationRequestUri; + } + + public async Task>> AcceptOffer(CredentialOfferMetadata credentialOfferMetadata, string? transactionCode) + { + var issuerMetadata = credentialOfferMetadata.IssuerMetadata; + // TODO: Support multiple configs + var configId = credentialOfferMetadata.CredentialOffer.CredentialConfigurationIds.First(); + var configuration = issuerMetadata.CredentialConfigurationsSupported[configId]; + var preAuthorizedCode = + from grants in credentialOfferMetadata.CredentialOffer.Grants + from preAuthCode in grants.PreAuthorizedCode + select preAuthCode.Value; + + var tokenRequest = new TokenRequest + { + GrantType = PreAuthorizedCodeGrantTypeIdentifier, + PreAuthorizedCode = preAuthorizedCode.ToNullable(), + TransactionCode = transactionCode + }; + + var authorizationServerMetadata = await FetchAuthorizationServerMetadataAsync(issuerMetadata); + + var token = await _tokenService.RequestToken( + tokenRequest, + authorizationServerMetadata); + + var validResponse = await _credentialRequestService.RequestCredentials( + configuration, + issuerMetadata, + token, + Option.None); + + var result = + from response in validResponse + let credentialOrTransactionId = response.CredentialOrTransactionId + select credentialOrTransactionId.Match( + async credential => await credential.Value.Match>>( + async sdJwt => + { + var record = sdJwt.Decoded.ToRecord(configuration.AsT0, issuerMetadata, response.KeyId); + var context = await _agentProvider.GetContextAsync(); + await _sdJwtService.SaveAsync(context, record); + return record; + }, + async mdoc => + { + var displays = MdocFun.CreateMdocDisplays(configuration.AsT1); + var record = mdoc.Decoded.ToRecord(displays); + await _mdocStorage.Add(record); + return record; + }), + // ReSharper disable once UnusedParameter.Local + transactionId => throw new NotImplementedException()); + + return await result.OnSuccess(task => task); + } + + public async Task> ProcessOffer(Uri credentialOffer, Option language) + { + var locale = language.Match( + some => some, + () => Constants.DefaultLocale); + + var result = + from offer in _credentialOfferService.ProcessCredentialOffer(credentialOffer, locale) + from metadata in _issuerMetadataService.ProcessMetadata(offer.CredentialIssuer, locale) + select new CredentialOfferMetadata(offer, metadata); + + return await result; + } + + /// + public async Task>> RequestCredential(IssuanceSession issuanceSession) + { + var context = await _agentProvider.GetContextAsync(); + + var session = await _authFlowSessionStorage.GetAsync(context, issuanceSession.SessionId); + + var credConfiguration = session + .AuthorizationData + .IssuerMetadata + .CredentialConfigurationsSupported + .Where(config => session.AuthorizationData.CredentialConfigurationIds.Contains(config.Key)) + .Select(pair => pair.Value) + .First(); + + var tokenRequest = new TokenRequest + { + GrantType = AuthorizationCodeGrantTypeIdentifier, + RedirectUri = session.AuthorizationData.ClientOptions.RedirectUri + "?session=" + session.SessionId, + CodeVerifier = session.AuthorizationCodeParameters.Verifier, + Code = issuanceSession.Code, + ClientId = session.AuthorizationData.ClientOptions.ClientId + }; + + var token = await _tokenService.RequestToken( + tokenRequest, + session.AuthorizationData.AuthorizationServerMetadata); + + var validResponse = await _credentialRequestService.RequestCredentials( + credConfiguration, + session.AuthorizationData.IssuerMetadata, + token, + session.AuthorizationData.ClientOptions); + + await _authFlowSessionStorage.DeleteAsync(context, session.SessionId); + + var result = + from response in validResponse + let credentialOrTransactionId = response.CredentialOrTransactionId + select credentialOrTransactionId.Match( + async credential => await credential.Value.Match>>( + async sdJwt => + { + var record = sdJwt.Decoded.ToRecord(credConfiguration.AsT0, session.AuthorizationData.IssuerMetadata, response.KeyId); + await _sdJwtService.SaveAsync(context, record); + return record; + }, + async mdoc => + { + var displays = MdocFun.CreateMdocDisplays(credConfiguration.AsT1); + var record = mdoc.Decoded.ToRecord(displays); + await _mdocStorage.Add(record); + return record; + }), + // ReSharper disable once UnusedParameter.Local + transactionId => throw new NotImplementedException()); + + return await result.OnSuccess(task => task); + } + + private static AuthorizationCodeParameters CreateAndStoreCodeChallenge() + { + var rng = new RNGCryptoServiceProvider(); + var randomNumber = new byte[32]; + rng.GetBytes(randomNumber); + + var codeVerifier = Base64UrlEncoder.Encode(randomNumber); + + var sha256 = SHA256.Create(); + var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier)); + + var codeChallenge = Base64UrlEncoder.Encode(bytes); + + return new AuthorizationCodeParameters(codeChallenge, codeVerifier); + } + + private async Task FetchAuthorizationServerMetadataAsync(IssuerMetadata issuerMetadata) + { + Uri credentialIssuer = issuerMetadata.CredentialIssuer; + + var authServerUrl = issuerMetadata.AuthorizationServers.Match( + servers => + { + Uri first = servers.First(); + return first; + }, + () => + { + string result; + if (string.IsNullOrWhiteSpace(credentialIssuer.AbsolutePath) || credentialIssuer.AbsolutePath == "/") + result = $"{credentialIssuer.GetLeftPart(UriPartial.Authority)}/.well-known/oauth-authorization-server"; + else + result = $"{credentialIssuer.GetLeftPart(UriPartial.Authority)}/.well-known/oauth-authorization-server" + credentialIssuer.AbsolutePath.TrimEnd('/'); + + return new Uri(result); + }); + + var getAuthServerResponse = await _httpClient.GetAsync(authServerUrl); + + if (!getAuthServerResponse.IsSuccessStatusCode) + throw new HttpRequestException( + $"Failed to get authorization server metadata. Status Code is: {getAuthServerResponse.StatusCode}" + ); + + var content = await getAuthServerResponse.Content.ReadAsStringAsync(); + + var authServer = DeserializeObject(content) + ?? throw new InvalidOperationException( + "Failed to deserialize the authorization server metadata."); + + return authServer; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/SdJwtRecordExtensions.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/SdJwtRecordExtensions.cs new file mode 100644 index 00000000..52ed4bd4 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/SdJwtRecordExtensions.cs @@ -0,0 +1,65 @@ +using System.Drawing; +using SD_JWT.Models; +using WalletFramework.Core.Cryptography.Models; +using WalletFramework.Core.Functional; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.SdJwt; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; +using WalletFramework.SdJwtVc.Models.Credential; +using WalletFramework.SdJwtVc.Models.Records; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Implementations; + +public static class SdJwtRecordExtensions +{ + public static SdJwtRecord ToRecord( + this SdJwtDoc sdJwtDoc, + SdJwtConfiguration configuration, + IssuerMetadata issuerMetadata, + KeyId keyId) + { + var claims = configuration + .Claims? + .Select(pair => (pair.Key, pair.Value)) + .ToDictionary(pair => pair.Key, pair => pair.Value); + + var display = configuration + .CredentialConfiguration + .Display + .ToNullable()? + .Select(credentialDisplay => + { + var backgroundColor = credentialDisplay.BackgroundColor.ToNullable() ?? Color.White; + var textColor = credentialDisplay.TextColor.ToNullable() ?? Color.Black; + + return new SdJwtDisplay + { + Logo = new SdJwtDisplay.SdJwtLogo + { + AltText = credentialDisplay.Logo.ToNullable()?.AltText.ToNullable(), + Uri = credentialDisplay.Logo.ToNullable()?.Uri.ToNullable()! + }, + Name = credentialDisplay.Name.ToNullable(), + BackgroundColor = backgroundColor, + Locale = credentialDisplay.Locale.ToNullable(), + TextColor = textColor + }; + }) + .ToList(); + + var issuerName = issuerMetadata + .Display + .ToNullable()? + .ToDictionary( + issuerDisplay => issuerDisplay.Locale.ToNullable()?.ToString(), + issuerDisplay => issuerDisplay.Name.ToNullable()?.ToString()); + + var record = new SdJwtRecord( + sdJwtDoc, + claims!, + display!, + issuerName!, + keyId); + + return record; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Abstractions/IIssuerMetadataService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Abstractions/IIssuerMetadataService.cs new file mode 100644 index 00000000..15dc5b60 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Abstractions/IIssuerMetadataService.cs @@ -0,0 +1,11 @@ +using WalletFramework.Core.Functional; +using WalletFramework.Core.Localization; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Issuer.Abstractions; + +public interface IIssuerMetadataService +{ + public Task> ProcessMetadata(Uri issuerEndpoint, Locale language); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Errors/CredentialEndpointError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Errors/CredentialEndpointError.cs new file mode 100644 index 00000000..a82d831e --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Errors/CredentialEndpointError.cs @@ -0,0 +1,5 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Issuer.Errors; + +public record CredentialEndpointError(Exception E) : Error("The credential enpoint could not be processed", E); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Errors/CredentialIssuerIdError.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Errors/CredentialIssuerIdError.cs new file mode 100644 index 00000000..38cf29f9 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Errors/CredentialIssuerIdError.cs @@ -0,0 +1,5 @@ +using WalletFramework.Core.Functional; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Issuer.Errors; + +public record CredentialIssuerIdError(string Value, Exception E) : Error($"The CredentialIssuerId could not be parsed. Value is {Value}", E); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Implementations/IssuerMetadataService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Implementations/IssuerMetadataService.cs new file mode 100644 index 00000000..fb79327d --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Implementations/IssuerMetadataService.cs @@ -0,0 +1,42 @@ +using WalletFramework.Core.Functional; +using WalletFramework.Core.Localization; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; +using static WalletFramework.Core.Json.JsonFun; +using static WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models.IssuerMetadata; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Issuer.Implementations; + +public class IssuerMetadataService : IIssuerMetadataService +{ + public IssuerMetadataService(IHttpClientFactory httpClientFactory) + { + _httpClient = httpClientFactory.CreateClient(); + } + + private readonly HttpClient _httpClient; + + public async Task> ProcessMetadata(Uri issuerEndpoint, Locale language) + { + var baseEndpoint = issuerEndpoint + .AbsolutePath + .EndsWith("/") + ? issuerEndpoint + : new Uri(issuerEndpoint.OriginalString + "/"); + + var metadataUrl = new Uri(baseEndpoint, ".well-known/openid-credential-issuer"); + + _httpClient.DefaultRequestHeaders.Add("Accept-Language", language); + + var response = await _httpClient.GetAsync(metadataUrl); + if (response.IsSuccessStatusCode) + { + var str = await response.Content.ReadAsStringAsync(); + return ParseAsJObject(str).OnSuccess(ValidIssuerMetadata); + } + + throw new HttpRequestException($"Failed to get Issuer metadata. Status code is {response.StatusCode}"); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/CredentialIssuerId.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/CredentialIssuerId.cs new file mode 100644 index 00000000..12e1f3a3 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/CredentialIssuerId.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Uri; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Errors; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct CredentialIssuerId +{ + private Uri Value { get; } + + private CredentialIssuerId(Uri value) => Value = value; + + public override string ToString() => Value.ToStringWithoutTrail(); + + public static implicit operator Uri(CredentialIssuerId credentialIssuerId) => credentialIssuerId.Value; + + public static Validation ValidCredentialIssuerId(JToken credentialIssuer) => credentialIssuer.ToJValue().OnSuccess(value => + { + try + { + var str = value.ToString(CultureInfo.InvariantCulture); + var uri = new Uri(str); + return new CredentialIssuerId(uri); + } + catch (Exception e) + { + return new CredentialIssuerIdError(credentialIssuer.ToString(), e).ToInvalid(); + } + }); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerMetadata.cs new file mode 100644 index 00000000..082c0187 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerMetadata.cs @@ -0,0 +1,202 @@ +using System.Globalization; +using System.Runtime.Serialization.Formatters.Binary; +using Hyperledger.Aries.Extensions; +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.SdJwt; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Errors; +using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; +using static WalletFramework.Core.Functional.ValidationFun; +using static WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models.AuthorizationServerId; +using static WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models.CredentialIssuerId; +using static WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models.CredentialConfigurationId; +using static WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer.IssuerDisplay; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; + +/// +/// Represents the metadata of an OpenID4VCI Credential Issuer. +/// +[JsonConverter(typeof(IssuerMetadataJsonConverter))] +public record IssuerMetadata +{ + // Do not change the order of this property (must be last) otherwise the JSON serialization will not + // work properly... + /// + /// Gets a dictionary which maps a CredentialConfigurationId to its credential metadata. + /// + [JsonProperty(CredentialConfigsSupportedJsonKey)] + [JsonConverter(typeof(DictJsonConverter))] + public Dictionary CredentialConfigurationsSupported { get; } + + /// + /// Gets a list of display properties of a Credential Issuer for different languages. + /// + [JsonProperty(DisplayJsonKey)] + [JsonConverter(typeof(OptionJsonConverter>))] + public Option> Display { get; } + + /// + /// Gets the URL of the Credential Issuer's Credential Endpoint. + /// + [JsonProperty(CredentialEndpointJsonKey)] + public Uri CredentialEndpoint { get; } + + /// + /// Gets the identifier of the Credential Issuer. + /// + [JsonProperty(CredentialIssuerJsonKey)] + public CredentialIssuerId CredentialIssuer { get; } + + /// + /// Gets the identifier of the OAuth 2.0 Authorization Server that the Credential Issuer relies on for + /// authorization. If this property is omitted, it is assumed that the entity providing the Credential Issuer + /// is also acting as the Authorization Server. In such cases, the Credential Issuer's + /// identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server + /// metadata. + /// + [JsonProperty(AuthorizationServersJsonKey)] + [JsonConverter(typeof(OptionJsonConverter>))] + public Option> AuthorizationServers { get; } + + private IssuerMetadata( + Dictionary credentialConfigurationsSupported, + Option> display, + Uri credentialEndpoint, + CredentialIssuerId credentialIssuer, + Option> authorizationServers) + { + CredentialConfigurationsSupported = credentialConfigurationsSupported; + Display = display; + CredentialEndpoint = credentialEndpoint; + CredentialIssuer = credentialIssuer; + AuthorizationServers = authorizationServers; + } + + private static IssuerMetadata Create( + Dictionary credentialConfigurationsSupported, + Option> display, + Uri credentialEndpoint, + CredentialIssuerId credentialIssuer, + Option> authorizationServers) => new( + credentialConfigurationsSupported, + display, + credentialEndpoint, + credentialIssuer, + authorizationServers); + + public static Validation ValidIssuerMetadata(JObject json) + { + var credentialConfigurations = + from jToken in json.GetByKey(CredentialConfigsSupportedJsonKey) + from jObj in jToken.ToJObject() + from dict in jObj.ToValidDictionaryAny(ValidCredentialConfigurationId, token => + { + var sdJwt = SdJwtConfiguration.ValidSdJwtCredentialConfiguration(token); + + if (sdJwt.Value.IsSuccess) + { + return sdJwt.OnSuccess(configuration => + { + SupportedCredentialConfiguration oneOf = configuration; + return oneOf; + }); + } + else + { + var mdoc = token + .ToJObject() + .OnSuccess(MdocConfiguration.ValidMdocConfiguration); + + return mdoc.OnSuccess(configuration => + { + SupportedCredentialConfiguration oneOf = configuration; + return oneOf; + }); + } + }) + select dict; + + var display = + from jToken in json.GetByKey(DisplayJsonKey).ToOption() + from jArray in jToken.ToJArray().ToOption() + from result in jArray.TraverseAny(OptionalIssuerDisplay) + select result.ToList(); + + var credentialEndpoint = + from jToken in json.GetByKey(CredentialEndpointJsonKey) + from endpoint in jToken.ToJValue().OnSuccess(value => + { + try + { + var str = value.ToString(CultureInfo.InvariantCulture); + return new Uri(str); + } + catch (Exception e) + { + return new CredentialEndpointError(e).ToInvalid(); + } + }) + select endpoint; + + var credentialIssuerId = json + .GetByKey(CredentialIssuerJsonKey) + .OnSuccess(ValidCredentialIssuerId); + + var authServers = + from jToken in json.GetByKey(AuthorizationServersJsonKey).ToOption() + from jArray in jToken.ToJArray().ToOption() + from serverIds in jArray + .TraverseAny(token => ValidAuthorizationServerId(token).ToOption()) + select serverIds; + + return Valid(Create) + .Apply(credentialConfigurations) + .Apply(display) + .Apply(credentialEndpoint) + .Apply(credentialIssuerId) + .Apply(authServers); + } + + private const string CredentialConfigsSupportedJsonKey = "credential_configurations_supported"; + private const string DisplayJsonKey = "display"; + private const string CredentialEndpointJsonKey = "credential_endpoint"; + private const string CredentialIssuerJsonKey = "credential_issuer"; + private const string AuthorizationServersJsonKey = "authorization_servers"; +} + +public sealed class IssuerMetadataJsonConverter : JsonConverter +{ + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, IssuerMetadata? value, JsonSerializer serializer) => + throw new NotImplementedException(); + + public override IssuerMetadata ReadJson( + JsonReader reader, + Type objectType, + IssuerMetadata? existingValue, + bool hasExistingValue, + JsonSerializer serializer) + { + var json = JObject.Load(reader); + + var result = IssuerMetadata + .ValidIssuerMetadata(json) + .Match( + metadata => metadata, + errors => + throw new InvalidOperationException($"IssuerMetadata is corrupt. Errors: {errors}") + ); + + return result; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerName.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerName.cs new file mode 100644 index 00000000..86b59bf3 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerName.cs @@ -0,0 +1,26 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct IssuerName +{ + private string Value { get; } + + private IssuerName(string value) => Value = value; + + public override string ToString() => Value; + + public static implicit operator string(IssuerName issuerName) => issuerName.Value; + + public static Option OptionIssuerName(JToken issuerName) + { + var str = issuerName.ToString(); + return string.IsNullOrWhiteSpace(str) + ? Option.None + : new IssuerName(str); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationCodeParameters.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationCodeParameters.cs deleted file mode 100644 index b6e0ffd1..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationCodeParameters.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization -{ - /// - /// Represents the parameters required for the authorization during the VCI authorization code flow. - /// The code itself will be part of the Client redirect uri that is created by the authorization server upon - /// successful authorization. - /// - public record AuthorizationCodeParameters - { - /// - /// Gets the code challenge. - /// - public string Challenge { get; } - - /// - /// Gets the code challenge method. SHA-256 is the only supported method. - /// - public string CodeChallengeMethod => "S256"; - - /// - /// Gets the code verifier. - /// - public string Verifier { get; } - - [JsonConstructor] - internal AuthorizationCodeParameters(string challenge, string verifier) - { - if (string.IsNullOrWhiteSpace(challenge) || string.IsNullOrWhiteSpace(verifier)) - { - throw new ArgumentException("Authorization Code Parameters cannot be null or empty."); - } - - Challenge = challenge; - Verifier = verifier; - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationData.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationData.cs deleted file mode 100644 index b1073140..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationData.cs +++ /dev/null @@ -1,27 +0,0 @@ -using WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialOffer.GrantTypes; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization; - -public record AuthorizationData -{ - public ClientOptions ClientOptions { get; } - - public MetadataSet MetadataSet { get; } - - public string[] CredentialConfigurationIds { get; } - - public AuthorizationCode? AuthorizationCode { get; } - - public AuthorizationData( - ClientOptions clientOptions, - MetadataSet metadataSet, - string[] credentialConfigurationIds, - AuthorizationCode? authorizationCode) - { - ClientOptions = clientOptions; - MetadataSet = metadataSet; - CredentialConfigurationIds = credentialConfigurationIds; - AuthorizationCode = authorizationCode; - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationDetails.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationDetails.cs deleted file mode 100644 index 054993e0..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationDetails.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization -{ - /// - /// Represents the authorization details. - /// - internal record AuthorizationDetails - { - /// - /// Gets the type of the credential. - /// - [JsonProperty("type")] - public string Type { get; } = "openid_credential"; - - /// - /// Gets or Sets the credential configuration id. - /// - [JsonProperty("credential_configuration_id")] - public string CredentialConfigurationId { get; } - - /// - /// - /// - [JsonProperty("locations", NullValueHandling = NullValueHandling.Ignore)] - public string[]? Locations { get; } - - internal AuthorizationDetails( - string credentialConfigurationId, - string[]? locations) - { - if (string.IsNullOrWhiteSpace(credentialConfigurationId)) - { - throw new ArgumentException("CredentialConfigurationId must be provided."); - } - - CredentialConfigurationId = credentialConfigurationId; - Locations = locations; - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationServerMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationServerMetadata.cs deleted file mode 100644 index 3664eb96..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/AuthorizationServerMetadata.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization -{ - /// - /// Represents the metadata associated with an OAuth 2.0 Authorization Server. - /// - public class AuthorizationServerMetadata - { - /// - /// Gets or sets the issuer location for the OAuth 2.0 Authorization Server. - /// - [JsonProperty("issuer")] - public string Issuer { get; set; } - - /// - /// Gets or sets the URL of the OAuth 2.0 token endpoint. - /// Clients use this endpoint to obtain an access token by presenting its authorization grant or refresh token. - /// - [JsonProperty("token_endpoint")] - public string TokenEndpoint { get; set; } - - /// - /// Gets or sets the URL of the OAuth 2.0 JSON Web Key Set (JWKS) document. - /// Clients use this to verify the signatures from the Authorization Server. - /// - [JsonProperty("jwks_uri")] - public string JwksUri { get; set; } - - /// - /// Gets or sets the URL of the OAuth 2.0 authorization endpoint. - /// - [JsonProperty("authorization_endpoint")] - public string AuthorizationEndpoint { get; set; } - - /// - /// Gets or sets the response types that the OAuth 2.0 Authorization Server supports. - /// These types determine how the Authorization Server responds to client requests. - /// - [JsonProperty("response_types_supported", NullValueHandling = NullValueHandling.Ignore)] - public string[]? ResponseTypesSupported { get; set; } - - /// - /// Gets or sets the supported authentication methods the OAuth 2.0 Authorization Server supports - /// when calling the token endpoint. - /// - [JsonProperty("token_endpoint_auth_methods_supported")] - public string[] TokenEndpointAuthMethodsSupported { get; set; } - - /// - /// Gets or sets the supported token endpoint authentication signing algorithms. - /// This indicates which algorithms the Authorization Server supports when receiving requests - /// at the token endpoint. - /// - [JsonProperty("token_endpoint_auth_signing_alg_values_supported")] - public string[] TokenEndpointAuthSigningAlgValuesSupported { get; set; } - - /// - /// Gets or sets the supported DPoP signing algorithms. - /// This indicates which algorithms the Authorization Server supports for DPoP Proof JWTs. - /// - [JsonProperty("dpop_signing_alg_values_supported")] - public string[]? DPopSigningAlgValuesSupported { get; set; } - - /// - /// Gets or sets the URL of the endpoint where the wallet sends the Pushed Authorization Request (PAR) to. - /// - [JsonProperty("pushed_authorization_request_endpoint")] - public string? PushedAuthorizationRequestEndpoint { get; set; } - - /// - /// Gets or sets a value indicating whether the Authorization Server requires the use of Pushed Authorization Requests. - /// - [JsonProperty("require_pushed_authorization_requests")] - public bool? RequirePushedAuthorizationRequests { get; set; } - - internal bool IsDPoPSupported => DPopSigningAlgValuesSupported != null && DPopSigningAlgValuesSupported.Contains("ES256"); - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/ClientOptions.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/ClientOptions.cs deleted file mode 100644 index 5d2c8482..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/ClientOptions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization -{ - /// - /// Represents the client options that are used during the VCI Authorization Code Flow. Here the wallet acts as the client. - /// - public record ClientOptions - { - /// - /// Identifier of the client (wallet) - /// - public string ClientId { get; init; } - - /// - /// Identifier of the wallet issuer - /// - public string WalletIssuer { get; init; } - - /// - /// Redirect URI that the Authorization Server will use after the authorization was successful. - /// - public string RedirectUri { get; init; } - -#pragma warning disable CS8618 - /// - /// Parameterless Default Constructor. - /// - public ClientOptions() - { - } -#pragma warning restore CS8618 - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - [JsonConstructor] - public ClientOptions(string clientId, string walletIssuer, string redirectUri) - { - if (string.IsNullOrWhiteSpace(clientId) - || string.IsNullOrWhiteSpace(walletIssuer) - || !Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) - { - throw new ArgumentException("Invalid Client Options"); - } - - ClientId = clientId; - WalletIssuer = walletIssuer; - RedirectUri = redirectUri; - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/PushedAuthorizationRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/PushedAuthorizationRequest.cs deleted file mode 100644 index b9dac5b5..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/PushedAuthorizationRequest.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Newtonsoft.Json; -using static Newtonsoft.Json.JsonConvert; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization -{ - internal record PushedAuthorizationRequest - { - [JsonProperty("client_id")] - public string ClientId { get; } - - [JsonProperty("response_type")] - public string ResponseType { get; } = "code"; - - [JsonProperty("redirect_uri")] - public string RedirectUri { get; } - - [JsonProperty("code_challenge")] - public string CodeChallenge { get; } - - [JsonProperty("code_challenge_method")] - public string CodeChallengeMethod { get; } - - [JsonProperty("authorization_details", NullValueHandling = NullValueHandling.Ignore)] - public AuthorizationDetails[]? AuthorizationDetails { get; } - - [JsonProperty("issuer_state", NullValueHandling = NullValueHandling.Ignore)] - public string? IssuerState { get; } - - [JsonProperty("wallet_issuer", NullValueHandling = NullValueHandling.Ignore)] - public string? WalletIssuer { get; } - - [JsonProperty("user_hint", NullValueHandling = NullValueHandling.Ignore)] - public string? UserHint { get; } - - [JsonProperty("scope", NullValueHandling = NullValueHandling.Ignore)] - public string? Scope { get; } - - [JsonProperty("resource", NullValueHandling = NullValueHandling.Ignore)] - public string? Resource { get; } - - public PushedAuthorizationRequest( - VciSessionId sessionId, - ClientOptions clientOptions, - AuthorizationCodeParameters authorizationCodeParameters, - AuthorizationDetails[]? authorizationDetails, - string? scope, - string? issuerState, - string? userHint, - string? resource) - { - ClientId = clientOptions.ClientId; - RedirectUri = clientOptions.RedirectUri + "?session=" + sessionId; - WalletIssuer = clientOptions.WalletIssuer; - CodeChallenge = authorizationCodeParameters.Challenge; - CodeChallengeMethod = authorizationCodeParameters.CodeChallengeMethod; - AuthorizationDetails = authorizationDetails; - IssuerState = issuerState; - UserHint = userHint; - Scope = scope; - Resource = resource; - } - - public FormUrlEncodedContent ToFormUrlEncoded() - { - var keyValuePairs = new List>(); - - if (!string.IsNullOrEmpty(ClientId)) - keyValuePairs.Add(new KeyValuePair("client_id", ClientId)); - - if (!string.IsNullOrEmpty(ResponseType)) - keyValuePairs.Add(new KeyValuePair("response_type", ResponseType)); - - if (!string.IsNullOrEmpty(RedirectUri)) - keyValuePairs.Add(new KeyValuePair("redirect_uri", RedirectUri)); - - if (!string.IsNullOrEmpty(CodeChallenge)) - keyValuePairs.Add(new KeyValuePair("code_challenge", CodeChallenge)); - - if (!string.IsNullOrEmpty(CodeChallengeMethod)) - keyValuePairs.Add(new KeyValuePair("code_challenge_method", CodeChallengeMethod)); - - if (AuthorizationDetails != null) - keyValuePairs.Add(new KeyValuePair("authorization_details", SerializeObject(AuthorizationDetails))); - - if (!string.IsNullOrEmpty(IssuerState)) - keyValuePairs.Add(new KeyValuePair("issuer_state", IssuerState)); - - if (!string.IsNullOrEmpty(WalletIssuer)) - keyValuePairs.Add(new KeyValuePair("wallet_issuer", WalletIssuer)); - - if (!string.IsNullOrEmpty(UserHint)) - keyValuePairs.Add(new KeyValuePair("user_hint", UserHint)); - - if (!string.IsNullOrEmpty(Scope)) - keyValuePairs.Add(new KeyValuePair("scope", Scope)); - - return new FormUrlEncodedContent(keyValuePairs); - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/PushedAuthorizationRequestResponse.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/PushedAuthorizationRequestResponse.cs deleted file mode 100644 index fa1d8867..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/PushedAuthorizationRequestResponse.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization -{ - internal record PushedAuthorizationRequestResponse - { - [JsonProperty("request_uri")] - public Uri RequestUri { get; init; } - - [JsonProperty("expires_in")] - public string ExpiresIn { get; init; } - - [JsonConstructor] - private PushedAuthorizationRequestResponse(Uri requestUri, string expiresIn) - => (RequestUri, ExpiresIn) = (requestUri, expiresIn); - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/TokenRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/TokenRequest.cs deleted file mode 100644 index a5cfc33f..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/TokenRequest.cs +++ /dev/null @@ -1,91 +0,0 @@ -#nullable enable - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization -{ - /// - /// Represents a request for an access token from an OAuth 2.0 Authorization Server. - /// - public class TokenRequest - { - /// - /// Gets or sets the grant type of the request. Determines the type of token request being made. - /// - public string GrantType { get; set; } = null!; - - /// - /// Gets or sets the pre-authorized code. Represents the authorization to obtain specific credentials. - /// This is required if the grant type is urn:ietf:params:oauth:grant-type:pre-authorized_code. - /// - public string? PreAuthorizedCode { get; set; } - - /// - /// Gets or sets the pre-authorized code. Represents the authorization to obtain specific credentials. - /// This is required if the grant type is urn:ietf:params:oauth:grant-type:pre-authorized_code. - /// - public string? Code { get; set; } - - /// - /// Gets or sets the pre-authorized code. Represents the authorization to obtain specific credentials. - /// This is required if the grant type is urn:ietf:params:oauth:grant-type:pre-authorized_code. - /// - public string? CodeVerifier { get; set; } - - /// - /// Gets or sets the pre-authorized code. Represents the authorization to obtain specific credentials. - /// This is required if the grant type is urn:ietf:params:oauth:grant-type:pre-authorized_code. - /// - public string? ClientId { get; set; } - - /// - /// Gets or sets the pre-authorized code. Represents the authorization to obtain specific credentials. - /// This is required if the grant type is urn:ietf:params:oauth:grant-type:pre-authorized_code. - /// - public string? RedirectUri { get; set; } - - /// - /// Gets or sets the scope of the access request. Defines the permissions the client is asking for. - /// - public string? Scope { get; set; } - - /// - /// Gets or sets the transaction code. This value must be present if a transaction code was required in a previous step. - /// - public string? TransactionCode { get; set; } - - /// - /// Converts the properties of the TokenRequest instance into an FormUrlEncodedContent type suitable for HTTP POST - /// operations. - /// - /// Returns an instance of FormUrlEncodedContent containing the URL-encoded properties of the TokenRequest. - public FormUrlEncodedContent ToFormUrlEncoded() - { - var keyValuePairs = new List>(); - - if (!string.IsNullOrEmpty(GrantType)) - keyValuePairs.Add(new KeyValuePair("grant_type", GrantType)); - - if (!string.IsNullOrEmpty(PreAuthorizedCode)) - keyValuePairs.Add(new KeyValuePair("pre-authorized_code", PreAuthorizedCode)); - - if (!string.IsNullOrEmpty(Scope)) - keyValuePairs.Add(new KeyValuePair("scope", Scope)); - - if (!string.IsNullOrEmpty(TransactionCode)) - keyValuePairs.Add(new KeyValuePair("tx_code", TransactionCode)); - - if (!string.IsNullOrEmpty(Code)) - keyValuePairs.Add(new KeyValuePair("code", Code)); - - if (!string.IsNullOrEmpty(RedirectUri)) - keyValuePairs.Add(new KeyValuePair("redirect_uri", RedirectUri)); - - if (!string.IsNullOrEmpty(CodeVerifier)) - keyValuePairs.Add(new KeyValuePair("code_verifier", CodeVerifier)); - - if (!string.IsNullOrEmpty(ClientId)) - keyValuePairs.Add(new KeyValuePair("client_id", ClientId)); - - return new FormUrlEncodedContent(keyValuePairs); - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/TokenResponse.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/TokenResponse.cs deleted file mode 100644 index 4ecea30b..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/TokenResponse.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization -{ - /// - /// Represents a successful response from the OAuth 2.0 Authorization Server containing - /// the issued access token and related information. - /// - internal class TokenResponse - { - /// - /// Indicates if the Token Request is still pending as the Credential Issuer - /// is waiting for the End-User interaction to complete. - /// - [JsonProperty("authorization_pending")] - public bool? AuthorizationPending { get; set; } - - /// - /// Gets or sets the lifetime in seconds of the c_nonce. - /// - [JsonProperty("c_nonce_expires_in")] - public int? CNonceExpiresIn { get; set; } - - /// - /// Gets or sets the lifetime in seconds of the access token. - /// - [JsonProperty("expires_in")] - public int? ExpiresIn { get; set; } - - /// - /// Gets or sets the minimum amount of time in seconds that the client should wait - /// between polling requests to the Token Endpoint. - /// - [JsonProperty("interval")] - public int? Interval { get; set; } - - /// - /// Gets or sets the access token issued by the authorization server. - /// - [JsonProperty("access_token")] - public string AccessToken { get; set; } - - /// - /// Gets or sets the nonce to be used to create a proof of possession of key material - /// when requesting a Credential. - /// - [JsonProperty("c_nonce")] - public string CNonce { get; set; } - - /// - /// Gets or sets the refresh token, which can be used to obtain new access tokens. - /// - [JsonProperty("refresh_token")] - public string RefreshToken { get; set; } - - /// - /// Gets or sets the scope of the access token. - /// - [JsonProperty("scope")] - public string Scope { get; set; } - - /// - /// Gets or sets the type of the token issued. - /// - [JsonProperty("token_type")] - public string TokenType { get; set; } - - /// - /// Gets or sets the credential identifier. - /// - [JsonProperty("credential_identifiers")] - public AuthorizationDetails? CredentialIdentifier { get; set; } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/VciAuthorizationSessionRecord.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/VciAuthorizationSessionRecord.cs deleted file mode 100644 index d45f68fc..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/VciAuthorizationSessionRecord.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Hyperledger.Aries.Storage; -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization -{ - /// - /// Represents the authorization session record. Used during the VCI Authorization Code Flow to hold session relevant information. - /// - public sealed class VciAuthorizationSessionRecord : RecordBase - { - /// - /// The session specific id. - /// - [JsonIgnore] - public VciSessionId SessionId - { - get => VciSessionId.CreateSessionId(Get()); - set => Set((string)value, false); - } - - /// - /// The Authorization Code from the CredentialOffer associated with the session. Only needed within the Pre Authorization Code flow. - /// - public AuthorizationData AuthorizationData { get; } - - /// - /// The parameters for the 'authorization_code' grant type. - /// - public AuthorizationCodeParameters AuthorizationCodeParameters { get; } - - /// - /// Initializes a new instance of the class. - /// - public override string TypeName => "AF.VciAuthorizationSessionRecord"; - -#pragma warning disable CS8618 - /// - /// Initializes a new instance of the class. - /// - public VciAuthorizationSessionRecord() - { - } -#pragma warning restore CS8618 - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - [JsonConstructor] - public VciAuthorizationSessionRecord( - VciSessionId sessionId, - AuthorizationData authorizationData, - AuthorizationCodeParameters authorizationCodeParameters) - { - SessionId = sessionId; - Id = Guid.NewGuid().ToString(); - RecordVersion = 1; - AuthorizationCodeParameters = authorizationCodeParameters; - AuthorizationData = authorizationData; - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/VciSessionId.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/VciSessionId.cs deleted file mode 100644 index 414cdb3f..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Authorization/VciSessionId.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization -{ - /// - /// Identifier of the authorization session during the VCI Authorization Code Flow. - /// - public struct VciSessionId - { - /// - /// Gets the value of the session identifier. - /// - private string Value { get; } - - private VciSessionId(string value) => Value = value; - - /// - /// Returns the value of the session identifier. - /// - /// - /// - public static implicit operator string(VciSessionId sessionParameters) => sessionParameters.Value; - - public static VciSessionId CreateSessionId(string sessionId) - { - if (!Guid.TryParse(sessionId, out _)) - { - throw new ArgumentException("SessionId must not be a Guid"); - } - - return new VciSessionId(sessionId); - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/GrantTypes/AuthorizationCode.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/GrantTypes/AuthorizationCode.cs deleted file mode 100644 index c700a229..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/GrantTypes/AuthorizationCode.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialOffer.GrantTypes -{ - /// - /// Represents the parameters for the 'authorization_code' grant type. - /// - public class AuthorizationCode - { - /// - /// Gets or sets an optional string value created by the Credential Issuer, 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. - /// - [JsonProperty("issuer_state")] - public string? IssuerState { get; set; } - - /// - /// Gets or sets the URL of the Authorization Server that the Wallet should use to obtain the Authorization Code. - /// - [JsonProperty("authorization_server")] - public string? AuthorizationServer { get; set; } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/GrantTypes/PreAuthorizedCode.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/GrantTypes/PreAuthorizedCode.cs deleted file mode 100644 index 43f8003b..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/GrantTypes/PreAuthorizedCode.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialOffer.GrantTypes -{ - /// - /// Represents the parameters for the 'pre-authorized_code' grant type. - /// - public class PreAuthorizedCode - { - /// - /// Gets or sets the pre-authorized code representing the Credential Issuer's authorization for the Wallet to obtain - /// Credentials of a certain type. - /// - [JsonProperty("pre-authorized_code")] - public string Value { get; set; } = null!; - - /// - /// Specifying whether the user must send a Transaction Code along with the Token Request in a Pre-Authorized Code Flow. - /// - [JsonProperty("tx_code")] - public TransactionCode? TransactionCode { get; set; } - } - - /// - /// Represents the details of the expected Transaction Code. - /// - public class TransactionCode - { - /// - /// Gets or sets the length of the transaction code. - /// - [JsonProperty("length")] - public int? Length { get; set; } - - /// - /// Gets or sets a description of the transaction code. - /// - [JsonProperty("description")] - public string? Description { get; set; } - - /// - /// Gets or sets the input mode of the transaction code which specifies the valid character set. (Must be 'numeric' ot 'text') - /// - [JsonProperty("input_mode")] - public string? InputMode { get; set; } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/Grants.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/Grants.cs deleted file mode 100644 index ec557b98..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/Grants.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Newtonsoft.Json; -using WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialOffer.GrantTypes; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialOffer -{ - /// - /// Represents the grant types that the Credential Issuer's AS is prepared to process for the credential offer. - /// - public class Grants - { - /// - /// Gets or sets the authorization_code grant type parameters. This includes an optional issuer state that is used to - /// bind the subsequent Authorization Request with the Credential Issuer to a context set up during previous steps. - /// - [JsonProperty("authorization_code")] - public AuthorizationCode? AuthorizationCode { get; set; } - - /// - /// Gets or sets the pre-authorized_code grant type parameters. This includes a required pre-authorized code - /// representing the Credential Issuer's authorization for the Wallet to obtain Credentials of a certain type, and an - /// optional boolean specifying whether a user PIN is required along with the Token Request. - /// - [JsonProperty("urn:ietf:params:oauth:grant-type:pre-authorized_code")] - public PreAuthorizedCode? PreAuthorizedCode { get; set; } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/OidCredentialOffer.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/OidCredentialOffer.cs deleted file mode 100644 index c0662a52..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialOffer/OidCredentialOffer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialOffer -{ - /// - /// Represents an OpenID4VCI Credential Offer, which is used to obtain one or more credentials from a Credential - /// Issuer. - /// - public class OidCredentialOffer - { - /// - /// Gets or sets the JSON object indicating to the Wallet the Grant Types the Credential Issuer's AS is prepared to - /// process for this credential offer. If not present or empty, the Wallet must determine the Grant Types the - /// Credential Issuer's AS supports using the respective metadata. - /// - [JsonProperty("grants")] - public Grants? Grants { get; set; } - - /// - /// Gets or sets the list of credentials that the Wallet may request. The List contains CredentialMetadataIds - /// that must map to the keys in the credential_configurations_supported dictionary of the Issuer Metadata - /// - [JsonProperty("credential_configuration_ids")] - public List CredentialConfigurationIds { get; set; } = null!; - - /// - /// Gets or sets the URL of the Credential Issuer from where the Wallet is requested to obtain one or more Credentials - /// from. - /// - [JsonProperty("credential_issuer")] - public string CredentialIssuer { get; set; } = null!; - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialRequest/OidCredentialRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialRequest/OidCredentialRequest.cs deleted file mode 100644 index 41093c15..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialRequest/OidCredentialRequest.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Newtonsoft.Json; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential.Attributes; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialRequest -{ - /// - /// Represents a credential request made by a client to the Credential Endpoint. - /// This request contains the format of the credential, the type of credential, - /// and a proof of possession of the key material the issued credential shall be bound to. - /// - public class OidCredentialRequest - { - /// - /// Gets or sets the proof of possession of the key material the issued credential shall be bound to. - /// - [JsonProperty("proof")] - public OidProofOfPossession? Proof { get; set; } - - /// - /// Gets or sets the format of the credential to be issued. - /// - [JsonProperty("format")] - public string Format { get; set; } = null!; - - /// - /// Gets or sets the dictionary representing the attributes of the credential in different languages. - /// - [JsonProperty("claims")] - public Dictionary? Claims { get; set; } - - /// - /// Gets or sets the verifiable credential type (vct). - /// - [JsonProperty("vct")] - public string Vct { get; set; } = null!; - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialRequest/OidProofOfPossession.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialRequest/OidProofOfPossession.cs deleted file mode 100644 index 325d2716..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialRequest/OidProofOfPossession.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialRequest -{ - /// - /// Represents the proof of possession of the key material that the issued credential is bound to. - /// This contains the JWT that acts as the proof of possession, with the proof type being "jwt". - /// - public class OidProofOfPossession - { - /// - /// Gets or sets the JWT that acts as the proof of possession of the key material the issued credential is bound to. - /// - [JsonProperty("jwt")] - public string Jwt { get; set; } - - /// - /// Gets or sets the type of proof, expected to be "jwt". - /// - [JsonProperty("proof_type")] - public string ProofType { get; set; } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialResponse/OidCredentialResponse.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialResponse/OidCredentialResponse.cs deleted file mode 100644 index 79133497..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/CredentialResponse/OidCredentialResponse.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialResponse -{ - /// - /// Represents a Credential Response. The response can be either immediate or deferred. In the synchronous response, - /// the issued Credential is immediately returned to the client. In the deferred response, an acceptance token - /// is sent to the client, which will be used later to retrieve the Credential once it's ready. - /// - public class OidCredentialResponse - { - /// - /// OPTIONAL. JSON integer denoting the lifetime in seconds of the c_nonce. - /// - [JsonProperty("c_nonce_expires_in")] - public int? CNonceExpiresIn { get; set; } - - /// - /// OPTIONAL. A JSON string containing a security token subsequently used to obtain a Credential. - /// MUST be present when credential is not returned. - /// - [JsonProperty("acceptance_token")] - public string? AcceptanceToken { get; set; } - - /// - /// OPTIONAL. JSON string containing a nonce to be used to create a proof of possession of key material - /// when requesting a Credential. - /// - [JsonProperty("c_nonce")] - public string? CNonce { get; set; } - - /// - /// OPTIONAL. Contains issued Credential. MUST be present when acceptance_token is not returned. - /// MAY be a JSON string or a JSON object, depending on the Credential format. - /// - [JsonProperty("credential")] - public string? Credential { get; set; } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/DPop/OAuthToken.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/DPop/OAuthToken.cs deleted file mode 100644 index 3ee9d92e..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/DPop/OAuthToken.cs +++ /dev/null @@ -1,33 +0,0 @@ -using WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.DPop -{ - internal record OAuthToken - { - internal OAuthToken(TokenResponse tokenResponse, DPop? dPop = null) - { - TokenResponse = tokenResponse; - DPop = dPop; - } - - public TokenResponse TokenResponse { get; } - - public DPop? DPop { get; } - } - - internal record DPop(string KeyId, string? Nonce) - { - public string? Nonce { get; } = Nonce; - - public string KeyId { get; } = KeyId; - } - - internal static class OAuthTokenExtensions - { - internal static bool IsDPoPRequested(this OAuthToken oAuthToken) - { - return oAuthToken.TokenResponse.TokenType == "DPoP" - && oAuthToken.DPop != null; - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/IssuanceSessionParameters.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/IssuanceSessionParameters.cs deleted file mode 100644 index 8c784786..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/IssuanceSessionParameters.cs +++ /dev/null @@ -1,44 +0,0 @@ -using WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization; -using static System.Web.HttpUtility; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models -{ - /// - /// Represents the parameters of an VCI Authorization Code Flow issuance session. - /// - public record IssuanceSessionParameters - { - /// - /// Gets the session identifier. - /// - public VciSessionId SessionId { get; } - - /// - /// Gets the actual authorization code that is received from the authorization server upon succesful authorization. - /// - public string Code { get; } - - private IssuanceSessionParameters(VciSessionId sessionId, string code) => (SessionId, Code) = (sessionId, code); - - /// - /// Creates a new instance of from the given . - /// - /// - /// - /// - public static IssuanceSessionParameters FromUri(Uri uri) - { - var queryParams = ParseQueryString(uri.Query); - - var code = queryParams.Get("code"); - var sessionId = queryParams.Get("session"); - - if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(sessionId)) - { - throw new InvalidOperationException("Query parameter 'code' and/or 'session' are missing"); - } - - return new IssuanceSessionParameters(VciSessionId.CreateSessionId(sessionId), code); - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialDisplay.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialDisplay.cs deleted file mode 100644 index 0fb2a184..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialDisplay.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential -{ - /// - /// Represents the visual representations for the credential. - /// - public class OidCredentialDisplay - { - /// - /// Gets or sets the logo associated with this Credential. - /// - [JsonProperty("logo", NullValueHandling = NullValueHandling.Ignore)] - public OidCredentialLogo? Logo { get; set; } - - /// - /// Gets or sets the name of the Credential. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string? Name { get; set; } - - /// - /// Gets or sets the background color for the Credential. - /// - [JsonProperty("background_color", NullValueHandling = NullValueHandling.Ignore)] - public string? BackgroundColor { get; set; } - - /// - /// Gets or sets the locale, which represents the specific culture or region. - /// - [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] - public string? Locale { get; set; } - - /// - /// Gets or sets the text color for the Credential. - /// - [JsonProperty("text_color", NullValueHandling = NullValueHandling.Ignore)] - public string? TextColor { get; set; } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialLogo.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialLogo.cs deleted file mode 100644 index 489f68bd..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialLogo.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential -{ - /// - /// Represents the Logo for a Credential. - /// - public class OidCredentialLogo - { - /// - /// Gets or sets the alternate text that describes the logo image. This is typically used for accessibility purposes. - /// - [JsonProperty("alt_text")] - public string? AltText { get; set; } - - /// - /// Gets or sets the URL of the logo image. - /// - [JsonProperty("uri")] - public Uri Uri { get; set; } = null!; - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialMetadata.cs deleted file mode 100644 index 5c064696..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Credential/OidCredentialMetadata.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Newtonsoft.Json; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential.Attributes; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential -{ - /// - /// Represents the metadata of a specific type of credential that a Credential Issuer can issue. - /// - public class OidCredentialMetadata - { - /// - /// Gets or sets the verifiable credential type (vct). - /// - [JsonProperty("vct")] - public string Vct { get; set; } = null!; - - /// - /// Gets or sets the dictionary representing the attributes of the credential in different languages. - /// - [JsonProperty("claims")] - public Dictionary? Claims { get; set; } - - /// - /// Gets or sets a list of display properties of the supported credential for different languages. - /// - [JsonProperty("display", NullValueHandling = NullValueHandling.Ignore)] - public List? Display { get; set; } - - /// - /// Gets or sets a list of methods that identify how the Credential is bound to the identifier of the End-User who - /// possesses the Credential. - /// - [JsonProperty("cryptographic_binding_methods_supported", NullValueHandling = NullValueHandling.Ignore)] - public List? CryptographicBindingMethodsSupported { get; set; } - - /// - /// Gets or sets a list of identifiers for the signing algorithms that are supported by the issuer and used - /// to sign credentials. - /// - [JsonProperty("credential_signing_alg_values_supported", NullValueHandling = NullValueHandling.Ignore)] - public List? CredentialSigningAlgValuesSupported { get; set; } - - /// - /// A list of claim display names, arranged in the order in which they should be displayed by the Wallet. - /// - [JsonProperty("order", NullValueHandling = NullValueHandling.Ignore)] - public List? Order { get; set; } - - /// - /// Gets or sets the identifier for the format of the credential. - /// - [JsonProperty("format")] - public string Format { get; set; } = null!; - - /// - /// Gets or sets the unique identifier for the respective credential. - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public string? Id { get; set; } - - /// - /// Gets or sets a dictionary which maps a credential type to its supported signing algorithms for key proofs. - /// - [JsonProperty("proof_types_supported", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary? ProofTypesSupported { get; set; } - - /// - /// Gets or sets a string indicating the credential that can be issued. - /// - [JsonProperty("scope", NullValueHandling = NullValueHandling.Ignore)] - public string? Scope { get; set; } - } - - /// - /// Represents credential type specific signing algorithm information. - /// - public class OidCredentialProofType - { - /// - /// Gets or sets the available signing algorithms for the associated credential type. - /// - [JsonProperty("proof_signing_alg_values_supported")] - public string[] ProofSigningAlgValuesSupported { get; set; } = null!; - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerDisplay.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerDisplay.cs new file mode 100644 index 00000000..9c6ce986 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerDisplay.cs @@ -0,0 +1,62 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Localization; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; +using static WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models.IssuerName; +using static WalletFramework.Core.Localization.Locale; +using static WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer.IssuerLogo; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; + +/// +/// Represents the visual representations for the Issuer. +/// +public record IssuerDisplay +{ + /// + /// Gets the name of the Issuer + /// + [JsonProperty("name")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Name { get; } + + /// + /// Gets the locale which represents the specific culture or region + /// + [JsonProperty("locale")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Locale { get; } + + /// + /// Gets the logo of the Issuer + /// + [JsonProperty("logo")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Logo { get; } + + private IssuerDisplay( + Option name, + Option locale, + Option logo) + { + Name = name; + Locale = locale; + Logo = logo; + } + + public static Option OptionalIssuerDisplay(JToken display) => display.ToJObject().ToOption().OnSome(jObject => + { + var name = jObject.GetByKey("name").ToOption().OnSome(OptionIssuerName); + var locale = jObject.GetByKey("locale").OnSuccess(ValidLocale).ToOption(); + var logo = jObject.GetByKey("logo").ToOption().OnSome(OptionalIssuerLogo); + + if (name.IsNone && locale.IsNone && logo.IsNone) + return Option.None; + + return new IssuerDisplay(name, locale, logo); + }); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerLogo.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerLogo.cs new file mode 100644 index 00000000..fa4cf257 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerLogo.cs @@ -0,0 +1,67 @@ +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; + +/// +/// Represents the Logo of the Issuer. +/// +public record IssuerLogo +{ + /// + /// Gets the alternate text that describes the logo image. This is typically used for accessibility purposes. + /// + [JsonProperty("alt_text")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option AltText { get; } + + /// + /// Gets the URL of the logo image. + /// + [JsonProperty("uri")] + [JsonConverter(typeof(OptionJsonConverter))] + public Option Uri { get; } + + private IssuerLogo( + Option altText, + Option uri) + { + AltText = altText; + Uri = uri; + } + + public static Option OptionalIssuerLogo(JToken logo) => logo.ToJObject().ToOption().OnSome(jObject => + { + var altText = jObject.GetByKey("alt_text").ToOption().OnSome(text => + { + var str = text.ToString(); + if (string.IsNullOrWhiteSpace(str)) + return Option.None; + + return str; + }); + + var imageUri = jObject.GetByKey("uri").ToOption().OnSome(uri => + { + try + { + var str = uri.ToString(); + var result = new Uri(str); + return result; + } + catch (Exception) + { + return Option.None; + } + }); + + if (altText.IsNone && imageUri.IsNone) + return Option.None; + + return new IssuerLogo(altText, imageUri); + }); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerDisplay.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerDisplay.cs deleted file mode 100644 index 83499f59..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerDisplay.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer -{ - /// - /// Represents the visual representations for the Issuer. - /// - public class OidIssuerDisplay - { - /// - /// Gets or sets the name of the Issuer. - /// - [JsonProperty("name")] - public string? Name { get; set; } - - /// - /// Gets or sets the locale, which represents the specific culture or region. - /// - [JsonProperty("locale")] - public string? Locale { get; set; } - - /// - /// Gets or sets the logo, which represents the specific culture or region.. - /// - [JsonProperty("logo", NullValueHandling = NullValueHandling.Ignore)] - public OidIssuerLogo? Logo { get; set; } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerLogo.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerLogo.cs deleted file mode 100644 index 3fa0eda0..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerLogo.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer -{ - /// - /// Represents the Logo of the Issuer. - /// - public class OidIssuerLogo - { - /// - /// Gets or sets the alternate text that describes the logo image. This is typically used for accessibility purposes. - /// - [JsonProperty("alt_text")] - public string? AltText { get; set; } - - /// - /// Gets or sets the URL of the logo image. - /// - [JsonProperty("uri")] - public Uri Uri { get; set; } = null!; - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerMetadata.cs deleted file mode 100644 index 9dda5825..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/OidIssuerMetadata.cs +++ /dev/null @@ -1,130 +0,0 @@ -using Newtonsoft.Json; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential.Attributes; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer -{ - /// - /// Represents the metadata of an OpenID4VCI Credential Issuer. - /// - public class OidIssuerMetadata - { - /// - /// Gets or sets a dictionary which maps a CredentialMetadataId to its credential metadata. - /// - [JsonProperty("credential_configurations_supported")] - public Dictionary CredentialConfigurationsSupported { get; set; } = null!; - - /// - /// Gets or sets a list of display properties of a Credential Issuer for different languages. - /// - [JsonProperty("display", NullValueHandling = NullValueHandling.Ignore)] - public List? Display { get; set; } - - /// - /// Gets or sets the URL of the Credential Issuer's Credential Endpoint. - /// - [JsonProperty("credential_endpoint")] - public string CredentialEndpoint { get; set; } = null!; - - /// - /// Gets or sets the identifier of the Credential Issuer. - /// - [JsonProperty("credential_issuer")] - public string CredentialIssuer { get; set; } = null!; - - /// - /// Gets or sets the identifier of the OAuth 2.0 Authorization Server that the Credential Issuer relies on for - /// authorization. If this property is omitted, it is assumed that the entity providing the Credential Issuer - /// is also acting as the Authorization Server. In such cases, the Credential Issuer's - /// identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server - /// metadata. - /// - [JsonProperty("authorization_servers", NullValueHandling = NullValueHandling.Ignore)] - public string[]? AuthorizationServers { get; set; } - - [JsonConstructor] - public OidIssuerMetadata( - Dictionary credentialConfigurationsSupported, - List? display, - string credentialEndpoint, - string credentialIssuer, - string[]? authorizationServer - ) - { - CredentialConfigurationsSupported = credentialConfigurationsSupported ?? throw new ArgumentNullException(nameof(credentialConfigurationsSupported)); - Display = display; - CredentialEndpoint = credentialEndpoint ?? throw new ArgumentNullException(nameof(credentialEndpoint)); - CredentialIssuer = credentialIssuer ?? throw new ArgumentNullException(nameof(credentialIssuer)); - AuthorizationServers = authorizationServer; - } - - public OidIssuerMetadata() {} - - /// - /// Gets the display properties of a given Credential for different languages. - /// - /// The credentialMetadataId to retrieve the display properties for. - /// - /// A list of display properties for the specified Credential or null if the Credential is not found in the - /// metadata. - /// - public List? GetCredentialDisplay(string credentialMetadataId) - => CredentialConfigurationsSupported[credentialMetadataId].Display; - - /// - /// Gets the claim attributes of a given Credential. - /// - /// The credentialMetadataId to retrieve the claim attributes for. - /// - /// A dictionary of attribute names and their corresponding display properties for the specified Credential, or - /// null if the Credential is not found in the metadata. - /// - public Dictionary? GetCredentialClaims(string credentialMetadataId) => - CredentialConfigurationsSupported[credentialMetadataId].Claims; - - /// - /// Gets the localized attribute names of a given Credential for a specific locale. - /// - /// The credentialMetadataId to retrieve the localized attribute names for. - /// The locale to retrieve the attribute names in (e.g., "en-US"). - /// - /// A list of localized attribute names for the specified Credential and locale, or null if no matching attributes - /// are found. - /// - public List? GetLocalizedCredentialAttributeNames(string credentialMetadataId, string locale) - { - var displayNames = new List(); - - var matchingCredential = CredentialConfigurationsSupported[credentialMetadataId]; - - if (matchingCredential == null) - return null; - - var localeDisplayNames = matchingCredential.Claims - .SelectMany(subject => subject.Value.Display) - .Where(display => display.Locale == locale) - .Select(display => display.Name); - - displayNames.AddRange(localeDisplayNames!); - - return displayNames.Count > 0 ? displayNames : null; - } - - /// - /// Gets the localized alias name of the Credential Issuer for a specific locale. - /// - /// The locale to retrieve the issuer alias name in (e.g., "en-US"). - /// - /// The localized alias name for the specified locale, or null if no matching alias is found. - /// - public string? GetLocalizedIssuerAlias(string locale) - { - if (Display == null) - return string.Empty; - - var display = Display.FirstOrDefault(display => display.Locale == locale); - return display?.Name; - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/IssuerMetadataSet.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/IssuerMetadataSet.cs new file mode 100644 index 00000000..405f665f --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/IssuerMetadataSet.cs @@ -0,0 +1,32 @@ +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; + +namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata; + +/// +/// Represents the metadata set of an OIDC issuer and authorization server. +/// +public record IssuerMetadataSet +{ + /// + /// Gets the metadata of the OIDC issuer. + /// + public IssuerMetadata IssuerMetadata { get; } + + /// + /// Gets the metadata of the OIDC authorization server. + /// + public AuthorizationServerMetadata AuthorizationServerMetadata { get; } + + /// + /// Creates a new instance of . + /// + /// + /// + public IssuerMetadataSet(IssuerMetadata issuerMetadata, AuthorizationServerMetadata authorizationServerMetadata) + { + IssuerMetadata = issuerMetadata; + AuthorizationServerMetadata = authorizationServerMetadata; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/MetadataSet.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/MetadataSet.cs deleted file mode 100644 index bdfaa392..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/MetadataSet.cs +++ /dev/null @@ -1,29 +0,0 @@ -using WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata -{ - /// - /// Represents the metadata set of an OIDC issuer and authorization server. - /// - public record MetadataSet - { - /// - /// Gets the metadata of the OIDC issuer. - /// - public OidIssuerMetadata IssuerMetadata { get; } - - /// - /// Gets the metadata of the OIDC authorization server. - /// - public AuthorizationServerMetadata AuthorizationServerMetadata { get; } - - /// - /// Creates a new instance of . - /// - /// - /// - public MetadataSet(OidIssuerMetadata issuerMetadata, AuthorizationServerMetadata authorizationServerMetadata) => - (IssuerMetadata, AuthorizationServerMetadata) = (issuerMetadata, authorizationServerMetadata); - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Services/ISessionRecordService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Services/ISessionRecordService.cs deleted file mode 100644 index ed787a19..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Services/ISessionRecordService.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Hyperledger.Aries.Agents; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Services -{ - /// - /// Service for managing authorization records. They are used during the VCI Authorization Code Flow to hold session relevant inforation. - /// - public interface ISessionRecordService - { - /// - /// Stores the authorization session record. - /// - /// The Agent Context - /// Session Identifier of a Authorization Code Flow session - /// Options specified by the Client (Wallet) - /// Parameters required for the authorization during the VCI authorization code flow. - /// - Task StoreAsync( - IAgentContext agentContext, - VciSessionId sessionId, - AuthorizationData authorizationData, - AuthorizationCodeParameters authorizationCodeParameters); - - /// - /// Retrieves the authorization session record by the session identifier. - /// - /// Agent Context - /// Session Identifier of a Authorization Code Flow session - /// - Task GetAsync(IAgentContext context, VciSessionId sessionId); - - /// - /// Deletes the authorization session record by the session identifier. - /// - /// Agent Context - /// Session Identifier of a Authorization Code Flow session - /// - Task DeleteAsync(IAgentContext context, VciSessionId sessionId); - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Services/Oid4VciClientService/IOid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Services/Oid4VciClientService/IOid4VciClientService.cs deleted file mode 100644 index 12506e16..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Services/Oid4VciClientService/IOid4VciClientService.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Hyperledger.Aries.Agents; -using WalletFramework.Oid4Vc.Oid4Vci.Models; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization; -using WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialOffer; -using WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialResponse; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Services.Oid4VciClientService -{ - /// - /// Provides an interface for services related to OpenID for Verifiable Credential Issuance. - /// - public interface IOid4VciClientService - { - /// - /// Fetches the metadata related to the OID issuer from the specified endpoint. - /// - /// The Credential Offer. - /// The preferred language of the wallet in which it would like to retrieve the issuer metadata. The default is "en" - /// A task that represents the asynchronous operation. The task result contains the OID issuer metadata. - //Task FetchIssuerMetadataAsync(Uri endpoint, string preferredLanguage = "en"); - Task FetchMetadataAsync(OidCredentialOffer offer, string preferredLanguage = "en"); - - /// - /// Initiates the authorization process of the VCI authorization code flow. - /// - /// The Agent Context - /// Holds all the necessary data to initiate the authorization within the Oid4Vci authorization code flow - /// - Task InitiateAuthentication( - IAgentContext agentContext, - AuthorizationData authorizationData); - - /// - /// Requests a verifiable credential using the pre-authorized code flow. - /// - /// Holds the Issuer Metadata and Authorization Server Metadata - /// The credential metadata. - /// The pre-authorized code for token request. - /// /// The Transaction Code. - /// - /// A tuple containing the credential response and the key ID used during the signing of the Proof of Possession. - /// - Task<(OidCredentialResponse credentialResponse, string keyId)[]> RequestCredentialAsync( - MetadataSet metadataSet, - OidCredentialMetadata credentialMetadata, - string preAuthorizedCode, - string? transactionCode - ); - - /// - /// Requests a verifiable credential using the authorization code flow. - /// - /// The agent context. - /// Holds authorization session relevant information. - /// - /// A tuple containing the credential response and the key ID used during the signing of the Proof of Possession. - /// - Task<(OidCredentialResponse credentialResponse, string keyId)[]> RequestCredentialAsync( - IAgentContext context, - IssuanceSessionParameters issuanceSessionParameters - ); - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Services/Oid4VciClientService/Oid4VciClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Services/Oid4VciClientService/Oid4VciClientService.cs deleted file mode 100644 index b2f0694a..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Services/Oid4VciClientService/Oid4VciClientService.cs +++ /dev/null @@ -1,559 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using Hyperledger.Aries.Agents; -using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json.Linq; -using WalletFramework.Oid4Vc.Oid4Vci.Exceptions; -using WalletFramework.Oid4Vc.Oid4Vci.Extensions; -using WalletFramework.Oid4Vc.Oid4Vci.Models; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization; -using WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialOffer; -using WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialRequest; -using WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialResponse; -using WalletFramework.Oid4Vc.Oid4Vci.Models.DPop; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; -using WalletFramework.SdJwtVc.KeyStore.Services; -using static Newtonsoft.Json.JsonConvert; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Services.Oid4VciClientService -{ - /// - public class Oid4VciClientService : IOid4VciClientService - { - /// - /// Initializes a new instance of the class. - /// - /// - /// The factory to create instances of . Used for making HTTP - /// requests. - /// - /// The authorization record service - /// The key store. - public Oid4VciClientService( - IHttpClientFactory httpClientFactory, - ISessionRecordService sessionRecordService, - IKeyStore keyStore) - { - _httpClientFactory = httpClientFactory; - RecordService = sessionRecordService; - _keyStore = keyStore; - } - - private readonly IHttpClientFactory _httpClientFactory; - private readonly IKeyStore _keyStore; - - private const string ErrorCodeKey = "error"; - private const string InvalidGrantError = "invalid_grant"; - private const string UseDPopNonceError = "use_dpop_nonce"; - private const string AuthorizationCodeGrantTypeIdentifier = "authorization_code"; - private const string PreAuthorizedCodeGrantTypeIdentifier = "urn:ietf:params:oauth:grant-type:pre-authorized_code"; - - /// - /// The service responsible for wallet record operations. - /// - protected readonly ISessionRecordService RecordService; - - /// - public async Task FetchMetadataAsync(OidCredentialOffer offer, string preferredLanguage) - { - var issuerEndpoint = new Uri(offer.CredentialIssuer); - var issuerMetadata = await FetchIssuerMetadataAsync(issuerEndpoint, "en"); - - var authorizationServerMetadata = await FetchAuthorizationServerMetadataAsync(issuerMetadata, offer); - - return new MetadataSet(issuerMetadata, authorizationServerMetadata); - } - - /// - public async Task InitiateAuthentication( - IAgentContext agentContext, - AuthorizationData authorizationData) - { - var authorizationCodeParameters = CreateAndStoreCodeChallenge(); - var sessionId = Guid.NewGuid().ToString(); - - var credentialMetadatas = authorizationData.MetadataSet.IssuerMetadata.CredentialConfigurationsSupported - .Where(credentialConfiguration => authorizationData.CredentialConfigurationIds.Contains(credentialConfiguration.Key)) - .Select(y => y.Value).ToList(); - - var par = new PushedAuthorizationRequest( - VciSessionId.CreateSessionId(sessionId), - authorizationData.ClientOptions, - authorizationCodeParameters, - authorizationData.CredentialConfigurationIds - .Select(configurationId => new AuthorizationDetails( - configurationId, - new []{authorizationData.MetadataSet.IssuerMetadata.CredentialIssuer}) - ).ToArray(), - string.Join(" ", credentialMetadatas.Select(metadata => metadata.Scope)), - authorizationData.AuthorizationCode?.IssuerState, - null, - null - ); - - var client = _httpClientFactory.CreateClient(); - client.DefaultRequestHeaders.Clear(); - - var response = await client.PostAsync( - authorizationData.MetadataSet.AuthorizationServerMetadata.PushedAuthorizationRequestEndpoint, - par.ToFormUrlEncoded() - ); - - var parResponse = DeserializeObject(await response.Content.ReadAsStringAsync()) - ?? throw new InvalidOperationException("Failed to deserialize the PAR response."); - - var authorizationRequestUri = new Uri(authorizationData.MetadataSet.AuthorizationServerMetadata.AuthorizationEndpoint - + "?client_id=" + par.ClientId - + "&request_uri=" + System.Net.WebUtility.UrlEncode(parResponse.RequestUri.ToString())); - - await RecordService.StoreAsync( - agentContext, - VciSessionId.CreateSessionId(sessionId), - authorizationData, - authorizationCodeParameters); - - return authorizationRequestUri; - } - - /// - public async Task<(OidCredentialResponse credentialResponse, string keyId)[]> RequestCredentialAsync( - IAgentContext context, - IssuanceSessionParameters issuanceSessionParameters) - { - var record = await RecordService.GetAsync(context, issuanceSessionParameters.SessionId); - - var tokenRequest = new TokenRequest - { - GrantType = AuthorizationCodeGrantTypeIdentifier, - RedirectUri = record.AuthorizationData.ClientOptions.RedirectUri + "?session=" + record.SessionId, - CodeVerifier = record.AuthorizationCodeParameters.Verifier, - Code = issuanceSessionParameters.Code, - ClientId = record.AuthorizationData.ClientOptions.ClientId - }; - - var oAuthToken = record.AuthorizationData.MetadataSet.AuthorizationServerMetadata.IsDPoPSupported - ? await RequestTokenWithDPop( - new Uri(record.AuthorizationData.MetadataSet.AuthorizationServerMetadata.TokenEndpoint), - tokenRequest) - : await RequestTokenWithoutDPop( - new Uri(record.AuthorizationData.MetadataSet.AuthorizationServerMetadata.TokenEndpoint), - tokenRequest); - - var credentialMetadatas = record.AuthorizationData.MetadataSet.IssuerMetadata.CredentialConfigurationsSupported - .Where(credentialConfiguration => record.AuthorizationData.CredentialConfigurationIds.Contains(credentialConfiguration.Key)) - .Select(y => y.Value); - - var credential = oAuthToken.IsDPoPRequested() - ? await RequestCredentialWithDPoP( - credentialMetadatas.First(), - record.AuthorizationData.MetadataSet.IssuerMetadata, - oAuthToken, - record.AuthorizationData.ClientOptions) - : await RequestCredentialWithoutDPoP( - credentialMetadatas.First(), - record.AuthorizationData.MetadataSet.IssuerMetadata, - oAuthToken, - record.AuthorizationData.ClientOptions); - - await RecordService.DeleteAsync(context, record.SessionId); - - //TODO: Return multiple credentials - return new[] { credential }; - } - - /// - public async Task<(OidCredentialResponse credentialResponse, string keyId)[]> RequestCredentialAsync( - MetadataSet metadataSet, - OidCredentialMetadata credentialMetadata, - string preAuthorizedCode, - string? transactionCode) - { - var tokenRequest = new TokenRequest - { - GrantType = PreAuthorizedCodeGrantTypeIdentifier, - PreAuthorizedCode = preAuthorizedCode, - TransactionCode = transactionCode - }; - - var oAuthToken = metadataSet.AuthorizationServerMetadata.IsDPoPSupported - ? await RequestTokenWithDPop( - new Uri(metadataSet.AuthorizationServerMetadata.TokenEndpoint), - tokenRequest) - : await RequestTokenWithoutDPop( - new Uri(metadataSet.AuthorizationServerMetadata.TokenEndpoint), - tokenRequest); - - var credential = oAuthToken.IsDPoPRequested() - ? await RequestCredentialWithDPoP( - credentialMetadata, - metadataSet.IssuerMetadata, - oAuthToken) - : await RequestCredentialWithoutDPoP( - credentialMetadata, - metadataSet.IssuerMetadata, - oAuthToken); - - //TODO: Return multiple credentials - return new[] { credential }; - } - - private async Task FetchIssuerMetadataAsync(Uri endpoint, string preferredLanguage) - { - var baseEndpoint = endpoint - .AbsolutePath - .EndsWith("/") - ? endpoint - : new Uri(endpoint.OriginalString + "/"); - - var metadataUrl = new Uri(baseEndpoint, ".well-known/openid-credential-issuer"); - - var client = _httpClientFactory.CreateClient(); - client.DefaultRequestHeaders.Add("Accept-Language", preferredLanguage); - - var response = await client.GetAsync(metadataUrl); - - if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException( - $"Failed to get Issuer metadata. Status code is {response.StatusCode}" - ); - } - - return DeserializeObject( - await response.Content.ReadAsStringAsync() - ) ?? throw new InvalidOperationException("Failed to deserialize the issuer metadata."); - } - - private async Task FetchAuthorizationServerMetadataAsync(OidIssuerMetadata issuerMetadata, OidCredentialOffer offer) - { - if (!string.IsNullOrWhiteSpace(offer.Grants?.AuthorizationCode?.AuthorizationServer) - && (issuerMetadata.AuthorizationServers == null - || !issuerMetadata.AuthorizationServers.Contains(offer.Grants.AuthorizationCode.AuthorizationServer))) - { - throw new InvalidOperationException("The AuthorizationServer in the offer must be one of the Authorization Servers listed in the Issuer Metadata."); - } - - var authServerUrl = new Uri(offer.Grants?.AuthorizationCode?.AuthorizationServer - ?? issuerMetadata.AuthorizationServers?.First() - ?? issuerMetadata.CredentialIssuer); - - var getAuthServerUrl = string.IsNullOrEmpty(authServerUrl.AbsolutePath) || authServerUrl.AbsolutePath == "/" - ? $"{authServerUrl.GetLeftPart(UriPartial.Authority)}/.well-known/oauth-authorization-server" - : $"{authServerUrl.GetLeftPart(UriPartial.Authority)}/.well-known/oauth-authorization-server" - + authServerUrl.AbsolutePath.TrimEnd('/'); - - var httpClient = _httpClientFactory.CreateClient(); - - var getAuthServerResponse = await httpClient.GetAsync(getAuthServerUrl); - - if (!getAuthServerResponse.IsSuccessStatusCode) - { - throw new HttpRequestException( - $"Failed to get authorization server metadata. Status Code is: {getAuthServerResponse.StatusCode}" - ); - } - - var authServer = - DeserializeObject( - await getAuthServerResponse.Content.ReadAsStringAsync() - ) - ?? throw new InvalidOperationException( - "Failed to deserialize the authorization server metadata." - ); - - return authServer; - } - - private async Task RequestTokenWithDPop( - Uri authServerTokenEndpoint, - TokenRequest tokenRequest) - { - var dPopKey = await _keyStore.GenerateKey(); - var dPopProofJwt = await _keyStore.GenerateDPopProofOfPossessionAsync( - dPopKey, - authServerTokenEndpoint.ToString(), - null, - null - ); - - var httpClient = _httpClientFactory.CreateClient(); - httpClient.AddDPopHeader(dPopProofJwt); - - var response = await httpClient.PostAsync( - authServerTokenEndpoint, - tokenRequest.ToFormUrlEncoded() - ); - - await ThrowIfInvalidGrantError(response); - - var dPopNonce = await GetDPopNonce(response); - - if (!string.IsNullOrEmpty(dPopNonce)) - { - dPopProofJwt = await _keyStore.GenerateDPopProofOfPossessionAsync( - dPopKey, - authServerTokenEndpoint.ToString(), - dPopNonce, - null); - - httpClient.AddDPopHeader(dPopProofJwt); - response = await httpClient.PostAsync( - authServerTokenEndpoint, - tokenRequest.ToFormUrlEncoded()); - } - - await ThrowIfInvalidGrantError(response); - - if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException( - $"Failed to get token. Status Code is {response.StatusCode}" - ); - } - - var tokenResponse = DeserializeObject(await response.Content.ReadAsStringAsync()) - ?? throw new InvalidOperationException("Failed to deserialize the token response"); - - var dPop = new DPop(dPopKey, dPopNonce); - var oAuthToken = new OAuthToken(tokenResponse, dPop); - - return oAuthToken; - } - - private async Task RequestTokenWithoutDPop( - Uri authServerTokenEndpoint, - TokenRequest tokenRequest) - { - var httpClient = _httpClientFactory.CreateClient(); - - var response = await httpClient.PostAsync( - authServerTokenEndpoint, - tokenRequest.ToFormUrlEncoded()); - - await ThrowIfInvalidGrantError(response); - - if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException( - $"Failed to get token. Status Code is {response.StatusCode}" - ); - } - - var tokenResponse = DeserializeObject(await response.Content.ReadAsStringAsync()) - ?? throw new InvalidOperationException("Failed to deserialize the token response"); - - var oAuthToken = new OAuthToken(tokenResponse); - return oAuthToken; - } - - private async Task<(OidCredentialResponse credentialResponse, string keyId)> RequestCredentialWithDPoP( - OidCredentialMetadata credentialMetadata, - OidIssuerMetadata issuerMetadata, - OAuthToken oAuthToken, - ClientOptions? clientOptions = null) - { - if (oAuthToken.DPop == null) - { - throw new InvalidOperationException("The DPoP Flow requires the DPoP specific parameters."); - } - - var dPopProofJwt = await _keyStore.GenerateDPopProofOfPossessionAsync( - oAuthToken.DPop.KeyId, - issuerMetadata.CredentialEndpoint, - oAuthToken.DPop.Nonce, - oAuthToken.TokenResponse.AccessToken); - - var sdJwtKeyId = await _keyStore.GenerateKey(); - var keyBindingJwt = await _keyStore.GenerateKbProofOfPossessionAsync( - sdJwtKeyId, - issuerMetadata.CredentialIssuer, - oAuthToken.TokenResponse.CNonce, - "openid4vci-proof+jwt", - null, - clientOptions?.ClientId - ); - - var httpClient = _httpClientFactory.CreateClient(); - httpClient.AddAuthorizationHeader(oAuthToken); - httpClient.AddDPopHeader(dPopProofJwt); - - var response = await httpClient.PostAsync( - issuerMetadata.CredentialEndpoint, - new StringContent( - content: new OidCredentialRequest - { - Format = credentialMetadata.Format, - Vct = credentialMetadata.Vct, - Proof = new OidProofOfPossession - { - ProofType = "jwt", - Jwt = keyBindingJwt - } - }.ToJson(), - encoding: Encoding.UTF8, - mediaType: "application/json" - ) - ); - - var refreshedDPopNonce = await GetDPopNonce(response); - - if (!string.IsNullOrEmpty(refreshedDPopNonce)) - { - dPopProofJwt = await _keyStore.GenerateDPopProofOfPossessionAsync( - oAuthToken.DPop.KeyId, - issuerMetadata.CredentialEndpoint, - refreshedDPopNonce, - oAuthToken.TokenResponse.AccessToken); - httpClient.AddDPopHeader(dPopProofJwt); - - response = await httpClient.PostAsync( - issuerMetadata.CredentialEndpoint, - new StringContent( - content: new OidCredentialRequest - { - Format = credentialMetadata.Format, - Vct = credentialMetadata.Vct, - Proof = new OidProofOfPossession - { - ProofType = "jwt", - Jwt = keyBindingJwt - } - }.ToJson(), - encoding: Encoding.UTF8, - mediaType: "application/json" - ) - ); - } - - if (!string.IsNullOrEmpty(oAuthToken.DPop.KeyId)) - { - await _keyStore.DeleteKey(oAuthToken.DPop.KeyId); - } - - if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException( - $"Failed to request Credential. Status Code is {response.StatusCode}" - ); - } - - var credentialResponse = DeserializeObject( - await response.Content.ReadAsStringAsync() - ); - - if (credentialResponse?.Credential == null) - { - throw new InvalidOperationException("Credential response is null."); - } - - return (credentialResponse, sdJwtKeyId); - } - - private async Task<(OidCredentialResponse credentialResponse, string keyId)> RequestCredentialWithoutDPoP( - OidCredentialMetadata credentialMetadata, - OidIssuerMetadata issuerMetadata, - OAuthToken oAuthToken, - ClientOptions? clientOptions = null) - { - var sdJwtKeyId = await _keyStore.GenerateKey(); - var proofOfPossession = await _keyStore.GenerateKbProofOfPossessionAsync( - sdJwtKeyId, - issuerMetadata.CredentialIssuer, - oAuthToken.TokenResponse.CNonce, - "openid4vci-proof+jwt", - null, - clientOptions?.ClientId - ); - - var httpClient = _httpClientFactory.CreateClient(); - httpClient.AddAuthorizationHeader(oAuthToken); - - var response = await httpClient.PostAsync( - issuerMetadata.CredentialEndpoint, - new StringContent( - content: new OidCredentialRequest - { - Format = credentialMetadata.Format, - Vct = credentialMetadata.Vct, - Proof = new OidProofOfPossession - { - ProofType = "jwt", - Jwt = proofOfPossession - } - }.ToJson(), - encoding: Encoding.UTF8, - mediaType: "application/json" - ) - ); - - if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException( - $"Failed to request Credential. Status Code is {response.StatusCode}" - ); - } - - var credentialResponse = DeserializeObject( - await response.Content.ReadAsStringAsync() - ); - - if (credentialResponse?.Credential == null) - { - throw new InvalidOperationException("Credential response is null."); - } - - return (credentialResponse, sdJwtKeyId); - } - - private async Task ThrowIfInvalidGrantError(HttpResponseMessage response) - { - var content = await response.Content.ReadAsStringAsync(); - var errorReason = string.IsNullOrEmpty(content) - ? null - : JObject.Parse(content)[ErrorCodeKey]?.ToString(); - - if (response.StatusCode is System.Net.HttpStatusCode.BadRequest - && errorReason == InvalidGrantError) - { - throw new Oid4VciInvalidGrantException(response.StatusCode); - } - } - - private async Task GetDPopNonce(HttpResponseMessage response) - { - var content = await response.Content.ReadAsStringAsync(); - var errorReason = string.IsNullOrEmpty(content) - ? null - : JObject.Parse(content)[ErrorCodeKey]?.ToString(); - - if (response.StatusCode - is System.Net.HttpStatusCode.BadRequest - or System.Net.HttpStatusCode.Unauthorized - && errorReason == UseDPopNonceError - && response.Headers.TryGetValues("DPoP-Nonce", out var dPopNonce)) - { - return dPopNonce?.FirstOrDefault(); - } - - return null; - } - - private AuthorizationCodeParameters CreateAndStoreCodeChallenge() - { - var rng = new RNGCryptoServiceProvider(); - byte[] randomNumber = new byte[32]; - rng.GetBytes(randomNumber); - - var codeVerifier = Base64UrlEncoder.Encode(randomNumber); - - var sha256 = SHA256.Create(); - byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier)); - - var codeChallenge = Base64UrlEncoder.Encode(bytes); - - return new AuthorizationCodeParameters(codeChallenge, codeVerifier); - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Services/SessionRecordService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Services/SessionRecordService.cs deleted file mode 100644 index b415988d..00000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Services/SessionRecordService.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Hyperledger.Aries; -using Hyperledger.Aries.Agents; -using Hyperledger.Aries.Storage; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization; -using WalletFramework.Oid4Vc.Oid4Vp.Services; - -namespace WalletFramework.Oid4Vc.Oid4Vci.Services -{ - /// - public class SessionRecordService : ISessionRecordService - { - /// - /// The service responsible for wallet record operations. - /// - protected readonly IWalletRecordService RecordService; - - /// - /// Initializes a new instance of the class. - /// - /// The service responsible for wallet record operations. - public SessionRecordService(IWalletRecordService recordService) - { - RecordService = recordService; - } - - /// - public async Task StoreAsync( - IAgentContext agentContext, - VciSessionId sessionId, - AuthorizationData authorizationData, - AuthorizationCodeParameters authorizationCodeParameters) - { - var record = new VciAuthorizationSessionRecord( - sessionId, - authorizationData, - authorizationCodeParameters); - - await RecordService.AddAsync( - agentContext.Wallet, - record - ); - - return record.Id; - } - - /// - public async Task GetAsync(IAgentContext context, VciSessionId sessionId) - { - var record = (await RecordService.SearchAsync( - context.Wallet, - SearchQuery.Equal( - "~" + nameof(VciAuthorizationSessionRecord.SessionId), - sessionId - ))).First(); - if (record == null) - throw new AriesFrameworkException(ErrorCode.RecordNotFound, "VciAuthorizationSessionRecord record not found"); - - return record; - } - - /// - public async Task DeleteAsync(IAgentContext context, VciSessionId sessionId) - { - return await RecordService.DeleteAsync(context.Wallet, sessionId); - } - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/PexService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/PexService.cs index 48a14664..cf3ddccb 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/PexService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/PresentationExchange/Services/PexService.cs @@ -75,7 +75,7 @@ private static SdJwtRecord[] _filterMatchingCredentialsForFields(SdJwtRecord[] r foreach (var record in records) { var doc = _toSdJwtDoc(record); - bool isAMatch = fields.All(field => + var isAMatch = fields.All(field => { try { diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/IOid4VpHaipClient.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/IOid4VpHaipClient.cs index 92abbfea..1a60942f 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/IOid4VpHaipClient.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/IOid4VpHaipClient.cs @@ -1,29 +1,28 @@ using WalletFramework.Oid4Vc.Oid4Vp.Models; -namespace WalletFramework.Oid4Vc.Oid4Vp.Services +namespace WalletFramework.Oid4Vc.Oid4Vp.Services; + +/// +/// This Service offers methods to handle the OpenId4Vp protocol +/// +public interface IOid4VpHaipClient { /// - /// This Service offers methods to handle the OpenId4Vp protocol + /// Processes an OpenID4VP Authorization Request Url. /// - internal interface IOid4VpHaipClient - { - /// - /// Processes an OpenID4VP Authorization Request Url. - /// - /// - /// - /// A task representing the asynchronous operation. The task result contains the Authorization Response object associated with the OpenID4VP Authorization Request Url. - /// - Task ProcessAuthorizationRequestAsync(HaipAuthorizationRequestUri haipAuthorizationRequestUri); + /// + /// + /// A task representing the asynchronous operation. The task result contains the Authorization Response object associated with the OpenID4VP Authorization Request Url. + /// + Task ProcessAuthorizationRequestAsync(HaipAuthorizationRequestUri haipAuthorizationRequestUri); - /// - /// Creates the Parameters that are necessary to send an OpenId4VP Authorization Response. - /// - /// - /// /// - /// - /// A task representing the asynchronous operation. The task result contains the Presentation Submission and the VP Token. - /// - Task CreateAuthorizationResponseAsync(AuthorizationRequest authorizationRequest, (string inputDescriptorId, string presentation)[] presentationMap); - } + /// + /// Creates the Parameters that are necessary to send an OpenId4VP Authorization Response. + /// + /// + /// /// + /// + /// A task representing the asynchronous operation. The task result contains the Presentation Submission and the VP Token. + /// + Task CreateAuthorizationResponseAsync(AuthorizationRequest authorizationRequest, (string inputDescriptorId, string presentation)[] presentationMap); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs index a80e7064..22c6fc31 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs @@ -7,184 +7,183 @@ using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; using static Newtonsoft.Json.JsonConvert; -namespace WalletFramework.Oid4Vc.Oid4Vp.Services +namespace WalletFramework.Oid4Vc.Oid4Vp.Services; + +/// +public class Oid4VpClientService : IOid4VpClientService { - /// - internal class Oid4VpClientService : IOid4VpClientService + /// + /// Initializes a new instance of the class. + /// + /// The http client factory to create http clients. + /// The service responsible for SD-JWT related operations. + /// The Presentation Exchange service. + /// The service responsible for OpenId4VP related operations. + /// The ILogger. + /// The service responsible for OidPresentationRecord related operations. + public Oid4VpClientService( + IHttpClientFactory httpClientFactory, + ISdJwtVcHolderService sdJwtVcHolderService, + IPexService pexService, + IOid4VpHaipClient oid4VpHaipClient, + ILogger logger, + IOid4VpRecordService oid4VpRecordService) { - /// - /// Initializes a new instance of the class. - /// - /// The http client factory to create http clients. - /// The service responsible for SD-JWT related operations. - /// The Presentation Exchange service. - /// The service responsible for OpenId4VP related operations. - /// The ILogger. - /// The service responsible for OidPresentationRecord related operations. - public Oid4VpClientService( - IHttpClientFactory httpClientFactory, - ISdJwtVcHolderService sdJwtVcHolderService, - IPexService pexService, - IOid4VpHaipClient oid4VpHaipClient, - ILogger logger, - IOid4VpRecordService oid4VpRecordService) - { - _httpClientFactory = httpClientFactory; - _sdJwtVcHolderService = sdJwtVcHolderService; - _oid4VpHaipClient = oid4VpHaipClient; - _logger = logger; - _oid4VpRecordService = oid4VpRecordService; - _pexService = pexService; - } + _httpClientFactory = httpClientFactory; + _sdJwtVcHolderService = sdJwtVcHolderService; + _oid4VpHaipClient = oid4VpHaipClient; + _logger = logger; + _oid4VpRecordService = oid4VpRecordService; + _pexService = pexService; + } - private readonly IHttpClientFactory _httpClientFactory; - private readonly IOid4VpHaipClient _oid4VpHaipClient; - private readonly ILogger _logger; - private readonly IOid4VpRecordService _oid4VpRecordService; - private readonly ISdJwtVcHolderService _sdJwtVcHolderService; - private readonly IPexService _pexService; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IOid4VpHaipClient _oid4VpHaipClient; + private readonly ILogger _logger; + private readonly IOid4VpRecordService _oid4VpRecordService; + private readonly ISdJwtVcHolderService _sdJwtVcHolderService; + private readonly IPexService _pexService; - /// - public async Task<(AuthorizationRequest, CredentialCandidates[])> ProcessAuthorizationRequestAsync( - IAgentContext agentContext, Uri authorizationRequestUri) - { - var haipAuthorizationRequestUri = HaipAuthorizationRequestUri.FromUri(authorizationRequestUri); + /// + public async Task<(AuthorizationRequest, CredentialCandidates[])> ProcessAuthorizationRequestAsync( + IAgentContext agentContext, Uri authorizationRequestUri) + { + var haipAuthorizationRequestUri = HaipAuthorizationRequestUri.FromUri(authorizationRequestUri); - var authorizationRequest = - await _oid4VpHaipClient.ProcessAuthorizationRequestAsync(haipAuthorizationRequestUri); + var authorizationRequest = + await _oid4VpHaipClient.ProcessAuthorizationRequestAsync(haipAuthorizationRequestUri); - var credentials = await _sdJwtVcHolderService.ListAsync(agentContext); + var credentials = await _sdJwtVcHolderService.ListAsync(agentContext); - var credentialCandidates = await _pexService.FindCredentialCandidates( - credentials.ToArray(), - authorizationRequest.PresentationDefinition.InputDescriptors - ); + var credentialCandidates = await _pexService.FindCredentialCandidates( + credentials.ToArray(), + authorizationRequest.PresentationDefinition.InputDescriptors + ); - return (authorizationRequest, credentialCandidates); - } + return (authorizationRequest, credentialCandidates); + } + + /// + public async Task SendAuthorizationResponseAsync( + IAgentContext agentContext, + AuthorizationRequest authorizationRequest, + SelectedCredential[] selectedCredentials) + { + var createPresentationMaps = + from credential in selectedCredentials + from inputDescriptor in authorizationRequest.PresentationDefinition.InputDescriptors + where credential.InputDescriptorId == inputDescriptor.Id + let disclosedClaims = inputDescriptor + .Constraints + .Fields? + .SelectMany(field => field.Path.Select(path => path.TrimStart('$', '.'))) + let createPresentation = _sdJwtVcHolderService.CreatePresentation( + (SdJwtRecord)credential.Credential, + disclosedClaims.ToArray(), + authorizationRequest.ClientId, + authorizationRequest.Nonce + ) + select (inputDescriptor.Id, createPresentation); - /// - public async Task SendAuthorizationResponseAsync( - IAgentContext agentContext, - AuthorizationRequest authorizationRequest, - SelectedCredential[] selectedCredentials) + var presentationMaps = new List<(string, string)>(); + foreach (var (inputDescriptorId, createPresentation) in createPresentationMaps) { - var createPresentationMaps = - from credential in selectedCredentials - from inputDescriptor in authorizationRequest.PresentationDefinition.InputDescriptors - where credential.InputDescriptorId == inputDescriptor.Id - let disclosedClaims = inputDescriptor - .Constraints - .Fields? - .SelectMany(field => field.Path.Select(path => path.TrimStart('$', '.'))) - let createPresentation = _sdJwtVcHolderService.CreatePresentation( - (SdJwtRecord)credential.Credential, - disclosedClaims.ToArray(), - authorizationRequest.ClientId, - authorizationRequest.Nonce - ) - select (inputDescriptor.Id, createPresentation); - - var presentationMaps = new List<(string, string)>(); - foreach (var (inputDescriptorId, createPresentation) in createPresentationMaps) - { - presentationMaps.Add((inputDescriptorId, await createPresentation)); - } + presentationMaps.Add((inputDescriptorId, await createPresentation)); + } - var authorizationResponse = await _oid4VpHaipClient.CreateAuthorizationResponseAsync( - authorizationRequest, - presentationMaps.ToArray() - ); + var authorizationResponse = await _oid4VpHaipClient.CreateAuthorizationResponseAsync( + authorizationRequest, + presentationMaps.ToArray() + ); - var httpClient = _httpClientFactory.CreateClient(); - httpClient.DefaultRequestHeaders.Clear(); + var httpClient = _httpClientFactory.CreateClient(); + httpClient.DefaultRequestHeaders.Clear(); - var responseMessage = - await httpClient.SendAsync( - new HttpRequestMessage - { - RequestUri = new Uri(authorizationRequest.ResponseUri), - Method = HttpMethod.Post, - Content = new FormUrlEncodedContent( - DeserializeObject>( - SerializeObject(authorizationResponse) - )? - .ToList() - ?? throw new InvalidOperationException("Authorization Response could not be parsed") - ) - } - ); - - if (!responseMessage.IsSuccessStatusCode) - throw new InvalidOperationException("Authorization Response could not be sent"); - - var presentedCredentials = presentationMaps - .Join( - selectedCredentials, - presentation => presentation.Item1, - selectedCredential => selectedCredential.InputDescriptorId, - (presentation, selectedCredential) => new - { - inputDescriptorId = presentation.Item1, - presentationFormat = presentation.Item2, - credential = selectedCredential.Credential - } - ).Select(presentationMapItem => + var responseMessage = + await httpClient.SendAsync( + new HttpRequestMessage { - var credentialRecord = (SdJwtRecord)presentationMapItem.credential; - var issuanceSdJwtDoc = credentialRecord.ToSdJwtDoc(); - var presentationSdJwtDoc = new SdJwtDoc(presentationMapItem.presentationFormat); - - var nonDisclosedDisclosure = - from issuedDisclosures in issuanceSdJwtDoc.Disclosures - let base64Encoded = issuedDisclosures.Base64UrlEncoded - where presentationSdJwtDoc.Disclosures.All(itm => itm.Base64UrlEncoded != base64Encoded) - select issuedDisclosures; - - var presentedClaims = - from claim in credentialRecord.Claims - where !nonDisclosedDisclosure.Any(nd => claim.Key.StartsWith(nd.Path ?? string.Empty)) - select new - { - key = claim.Key, - value = new PresentedClaim { Value = claim.Value } - }; - - return new PresentedCredential - { - CredentialId = credentialRecord.Id, - PresentedClaims = presentedClaims.ToDictionary(itm => itm.key, itm => itm.value) - }; - }); - - await _oid4VpRecordService.StoreAsync( - agentContext, - authorizationRequest.ClientId, - authorizationRequest.ClientMetadata, - authorizationRequest.PresentationDefinition.Name, - presentedCredentials.ToArray() + RequestUri = new Uri(authorizationRequest.ResponseUri), + Method = HttpMethod.Post, + Content = new FormUrlEncodedContent( + DeserializeObject>( + SerializeObject(authorizationResponse) + )? + .ToList() + ?? throw new InvalidOperationException("Authorization Response could not be parsed") + ) + } ); - var redirectUriJson = await responseMessage.Content.ReadAsStringAsync(); + if (!responseMessage.IsSuccessStatusCode) + throw new InvalidOperationException("Authorization Response could not be sent"); - try + var presentedCredentials = presentationMaps + .Join( + selectedCredentials, + presentation => presentation.Item1, + selectedCredential => selectedCredential.InputDescriptorId, + (presentation, selectedCredential) => new + { + inputDescriptorId = presentation.Item1, + presentationFormat = presentation.Item2, + credential = selectedCredential.Credential + } + ).Select(presentationMapItem => { - return DeserializeObject(redirectUriJson); + var credentialRecord = (SdJwtRecord)presentationMapItem.credential; + var issuanceSdJwtDoc = credentialRecord.ToSdJwtDoc(); + var presentationSdJwtDoc = new SdJwtDoc(presentationMapItem.presentationFormat); + + var nonDisclosedDisclosure = + from issuedDisclosures in issuanceSdJwtDoc.Disclosures + let base64Encoded = issuedDisclosures.Base64UrlEncoded + where presentationSdJwtDoc.Disclosures.All(itm => itm.Base64UrlEncoded != base64Encoded) + select issuedDisclosures; + + var presentedClaims = + from claim in credentialRecord.Claims + where !nonDisclosedDisclosure.Any(nd => claim.Key.StartsWith(nd.Path ?? string.Empty)) + select new + { + key = claim.Key, + value = new PresentedClaim { Value = claim.Value } + }; - } - catch (Exception e) - { - _logger.LogWarning("Could not parse Redirect URI received from: {ResponseUri} due to exception: {Exception}", authorizationRequest.ResponseUri, e); - return null; - } + return new PresentedCredential + { + CredentialId = credentialRecord.Id, + PresentedClaims = presentedClaims.ToDictionary(itm => itm.key, itm => itm.value) + }; + }); + + await _oid4VpRecordService.StoreAsync( + agentContext, + authorizationRequest.ClientId, + authorizationRequest.ClientMetadata, + authorizationRequest.PresentationDefinition.Name, + presentedCredentials.ToArray() + ); + + var redirectUriJson = await responseMessage.Content.ReadAsStringAsync(); + + try + { + return DeserializeObject(redirectUriJson); + + } + catch (Exception e) + { + _logger.LogWarning("Could not parse Redirect URI received from: {ResponseUri} due to exception: {Exception}", authorizationRequest.ResponseUri, e); + return null; } } +} - internal static class SdJwtRecordExtensions +internal static class SdJwtRecordExtensions +{ + internal static SdJwtDoc ToSdJwtDoc(this SdJwtRecord record) { - internal static SdJwtDoc ToSdJwtDoc(this SdJwtRecord record) - { - return new SdJwtDoc(record.EncodedIssuerSignedJwt + "~" + string.Join("~", record.Disclosures) + "~"); - } + return new SdJwtDoc(record.EncodedIssuerSignedJwt + "~" + string.Join("~", record.Disclosures) + "~"); } } diff --git a/src/WalletFramework.Oid4Vc/SeviceCollectionExtensions.cs b/src/WalletFramework.Oid4Vc/SeviceCollectionExtensions.cs index a35055b3..63e9b2a1 100644 --- a/src/WalletFramework.Oid4Vc/SeviceCollectionExtensions.cs +++ b/src/WalletFramework.Oid4Vc/SeviceCollectionExtensions.cs @@ -1,8 +1,22 @@ using Microsoft.Extensions.DependencyInjection; -using WalletFramework.Oid4Vc.Oid4Vci.Services; -using WalletFramework.Oid4Vc.Oid4Vci.Services.Oid4VciClientService; +using WalletFramework.Oid4Vc.Oid4Vci.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Implementations; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Implementations; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Implementations; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Implementations; +using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Implementations; +using WalletFramework.Oid4Vc.Oid4Vci.Implementations; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Abstractions; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Implementations; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Services; using WalletFramework.Oid4Vc.Oid4Vp.Services; +using WalletFramework.SdJwtVc; +using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; namespace WalletFramework.Oid4Vc; @@ -12,15 +26,23 @@ public static class SeviceCollectionExtensions /// Adds the default OpenID services. /// /// The builder. - public static IServiceCollection AddOpenIdDefaultServices(this IServiceCollection builder) + public static IServiceCollection AddOpenIdServices(this IServiceCollection builder) { - builder.AddSingleton(); + builder.AddSingleton(); + builder.AddSingleton(); + builder.AddSingleton(); + builder.AddSingleton(); + builder.AddSingleton(); + builder.AddSingleton(); builder.AddSingleton(); builder.AddSingleton(); builder.AddSingleton(); builder.AddSingleton(); - builder.AddSingleton(); - + builder.AddSingleton(); + builder.AddSingleton(); + + builder.AddSdJwtVcServices(); + return builder; } diff --git a/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj b/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj index 69f2e6af..842e193b 100644 --- a/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj +++ b/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj @@ -6,10 +6,15 @@ enable + + + <_Parameter1>WalletFramework.Oid4Vc.Tests + + + - diff --git a/src/WalletFramework.SdJwtVc/KeyStore/Services/IKeyStore.cs b/src/WalletFramework.SdJwtVc/KeyStore/Services/IKeyStore.cs deleted file mode 100644 index fa8fee3c..00000000 --- a/src/WalletFramework.SdJwtVc/KeyStore/Services/IKeyStore.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace WalletFramework.SdJwtVc.KeyStore.Services -{ - /// - /// Represents a store for managing keys. - /// This interface is intended to be implemented outside of the framework on the device side, - /// allowing flexibility in key generation or retrieval mechanisms. - /// - public interface IKeyStore - { - /// - /// Asynchronously generates a key for the specified algorithm and returns the key identifier. - /// - /// The algorithm for key generation (default is "ES256"). - /// A representing the generated key's identifier as a string. - Task GenerateKey(string alg = "ES256"); - - /// - /// Asynchronously creates a proof of possession for a specific key, based on the provided audience and nonce. - /// - /// The identifier of the key to be used in creating the proof of possession. - /// The intended recipient of the proof. Typically represents the entity that will verify it. - /// - /// A unique token, typically used to prevent replay attacks by ensuring that the proof is only used once. - /// - /// The type of the proof. (For example "openid4vci-proof+jwt") - /// Base64url-encoded hash digest over the Issuer-signed JWT and the selected Disclosures for integrity protection - /// - /// A representing the asynchronous operation. When evaluated, the task's result contains - /// the proof. - /// - Task GenerateKbProofOfPossessionAsync(string keyId, string audience, string nonce, string type, string? sdHash = null, string? clientId = null); - - /// - /// Asynchronously creates a DPoP Proof JWT for a specific key, based on the provided audience, nonce and access token. - /// - /// The identifier of the key to be used in creating the proof of possession. - /// The intended recipient of the proof. Typically represents the entity that will verify it. - /// A unique token, typically used to prevent replay attacks by ensuring that the proof is only used once. - /// The access token, that the DPoP Proof JWT is bound to - /// - /// A representing the asynchronous operation. When evaluated, the task's result contains - /// the DPoP Proof JWT. - /// - Task GenerateDPopProofOfPossessionAsync(string keyId, string audience, string? nonce, string? accessToken); - - /// - /// Asynchronously loads a key by its identifier and returns it as a JSON Web Key (JWK) containing the public key - /// information. - /// - /// The identifier of the key to load. - /// A representing the loaded key as a JWK string. - Task LoadKey(string keyId); - - /// - /// Asynchronously signs the given payload using the key identified by the provided key ID. - /// - /// The identifier of the key to use for signing. - /// The payload to sign. - /// A representing the signed payload as a byte array. - Task Sign(string keyId, byte[] payload); - - /// - /// Asynchronously deletes the key associated with the provided key ID. - /// - /// The identifier of the key that should be deleted - /// A representing the asynchronous operation. - Task DeleteKey(string keyId); - } -} diff --git a/src/WalletFramework.SdJwtVc/Models/Credential/CredentialDisplayMetadata.cs b/src/WalletFramework.SdJwtVc/Models/Credential/CredentialDisplayMetadata.cs deleted file mode 100644 index 4ff43db0..00000000 --- a/src/WalletFramework.SdJwtVc/Models/Credential/CredentialDisplayMetadata.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.SdJwtVc.Models.Credential -{ - /// - /// Represents the visual representations for the credential. - /// - public class CredentialDisplayMetadata - { - /// - /// Gets or sets the logo associated with this Credential. - /// - [JsonProperty("logo", NullValueHandling = NullValueHandling.Ignore)] - public CredentialLogo? Logo { get; set; } - - /// - /// Gets or sets the name of the Credential. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string? Name { get; set; } - - /// - /// Gets or sets the background color for the Credential. - /// - [JsonProperty("background_color", NullValueHandling = NullValueHandling.Ignore)] - public string? BackgroundColor { get; set; } - - /// - /// Gets or sets the locale, which represents the specific culture or region. - /// - [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] - public string? Locale { get; set; } - - /// - /// Gets or sets the text color for the Credential. - /// - [JsonProperty("text_color", NullValueHandling = NullValueHandling.Ignore)] - public string? TextColor { get; set; } - - /// - /// Represents the Logo for a Credential. - /// - public class CredentialLogo - { - /// - /// Gets or sets the alternate text that describes the logo image. This is typically used for accessibility purposes. - /// - [JsonProperty("alt_text")] - public string? AltText { get; set; } - - /// - /// Gets or sets the URL of the logo image. - /// - [JsonProperty("uri")] - public Uri Uri { get; set; } = null!; - } - } -} diff --git a/src/WalletFramework.SdJwtVc/Models/Credential/SdJwtDisplay.cs b/src/WalletFramework.SdJwtVc/Models/Credential/SdJwtDisplay.cs new file mode 100644 index 00000000..03248476 --- /dev/null +++ b/src/WalletFramework.SdJwtVc/Models/Credential/SdJwtDisplay.cs @@ -0,0 +1,57 @@ +using Newtonsoft.Json; + +namespace WalletFramework.SdJwtVc.Models.Credential; + +/// +/// Represents the visual representations for the credential. +/// +public record SdJwtDisplay +{ + /// + /// Gets or sets the logo associated with this Credential. + /// + [JsonProperty("logo", NullValueHandling = NullValueHandling.Ignore)] + public SdJwtLogo? Logo { get; set; } + + /// + /// Gets or sets the name of the Credential. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; set; } + + /// + /// Gets or sets the background color for the Credential. + /// + [JsonProperty("background_color", NullValueHandling = NullValueHandling.Ignore)] + public string? BackgroundColor { get; set; } + + /// + /// Gets or sets the locale, which represents the specific culture or region. + /// + [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] + public string? Locale { get; set; } + + /// + /// Gets or sets the text color for the Credential. + /// + [JsonProperty("text_color", NullValueHandling = NullValueHandling.Ignore)] + public string? TextColor { get; set; } + + /// + /// Represents the Logo for a Credential. + /// + public class SdJwtLogo + { + /// + /// Gets or sets the alternate text that describes the logo image. This is typically used for accessibility purposes. + /// + [JsonProperty("alt_text")] + public string? AltText { get; set; } + + /// + /// Gets or sets the URL of the logo image. + /// + [JsonProperty("uri")] + public Uri Uri { get; set; } = null!; + } +} diff --git a/src/WalletFramework.SdJwtVc/Models/Credential/CredentialMetadata.cs b/src/WalletFramework.SdJwtVc/Models/Credential/SdJwtMetadata.cs similarity index 97% rename from src/WalletFramework.SdJwtVc/Models/Credential/CredentialMetadata.cs rename to src/WalletFramework.SdJwtVc/Models/Credential/SdJwtMetadata.cs index fb8044df..a6b9b7d5 100644 --- a/src/WalletFramework.SdJwtVc/Models/Credential/CredentialMetadata.cs +++ b/src/WalletFramework.SdJwtVc/Models/Credential/SdJwtMetadata.cs @@ -6,7 +6,7 @@ namespace WalletFramework.SdJwtVc.Models.Credential /// /// Represents the metadata of a specific type of credential that a Credential Issuer can issue. /// - public class CredentialMetadata + public class SdJwtMetadata { /// /// Gets or sets the verifiable credential type (vct). This is SD-JWT specific. @@ -36,7 +36,7 @@ public class CredentialMetadata /// Gets or sets a list of display properties of the supported credential for different languages. /// [JsonProperty("display", NullValueHandling = NullValueHandling.Ignore)] - public List? Display { get; set; } + public List? Display { get; set; } /// /// Gets or sets a list of methods that identify how the Credential is bound to the identifier of the End-User who diff --git a/src/WalletFramework.SdJwtVc/Models/Issuer/IssuerDisplay.cs b/src/WalletFramework.SdJwtVc/Models/Issuer/IssuerDisplay.cs deleted file mode 100644 index 66f2dafa..00000000 --- a/src/WalletFramework.SdJwtVc/Models/Issuer/IssuerDisplay.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Newtonsoft.Json; - -namespace WalletFramework.SdJwtVc.Models.Issuer -{ - /// - /// Represents the visual representations for the Issuer. - /// - public class IssuerDisplay - { - /// - /// Gets or sets the name of the Issuer. - /// - [JsonProperty("name")] - public string? Name { get; set; } - - /// - /// Gets or sets the locale, which represents the specific culture or region. - /// - [JsonProperty("locale")] - public string? Locale { get; set; } - - /// - /// Gets or sets the logo, which represents the specific culture or region.. - /// - [JsonProperty("logo", NullValueHandling = NullValueHandling.Ignore)] - public IssuerLogo? Logo { get; set; } - - /// - /// Represents the Logo of the Issuer. - /// - public class IssuerLogo - { - /// - /// Gets or sets the alternate text that describes the logo image. This is typically used for accessibility purposes. - /// - [JsonProperty("alt_text")] - public string? AltText { get; set; } - - /// - /// Gets or sets the URL of the logo image. - /// - [JsonProperty("uri")] - public Uri Uri { get; set; } = null!; - } - } -} diff --git a/src/WalletFramework.SdJwtVc/Models/Issuer/IssuerMetadata.cs b/src/WalletFramework.SdJwtVc/Models/Issuer/IssuerMetadata.cs deleted file mode 100644 index d2f9e4f8..00000000 --- a/src/WalletFramework.SdJwtVc/Models/Issuer/IssuerMetadata.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Newtonsoft.Json; -using WalletFramework.SdJwtVc.Models.Credential; -using WalletFramework.SdJwtVc.Models.Credential.Attributes; - -namespace WalletFramework.SdJwtVc.Models.Issuer -{ - // Todo: Clean up this class and remove the unnecessary fields - /// - /// Represents the metadata of an SdJwtVc issuer. - /// - public class IssuerMetadata - { - /// - /// Gets or sets a dictionary which maps a CredentialMetadataId to its credential metadata. - /// - [JsonProperty("credential_configurations_supported")] - public Dictionary CredentialConfigurationsSupported { get; set; } = null!; - - /// - /// Gets or sets a list of display properties of a Credential Issuer for different languages. - /// - [JsonProperty("display", NullValueHandling = NullValueHandling.Ignore)] - public List? Display { get; set; } - - /// - /// Gets or sets the URL of the Credential Issuer's Credential Endpoint. - /// - [JsonProperty("credential_endpoint")] - public string CredentialEndpoint { get; set; } = null!; - - /// - /// Gets or sets the identifier of the Credential Issuer. - /// - [JsonProperty("credential_issuer")] - public string CredentialIssuer { get; set; } = null!; - - /// - /// Gets or sets the identifier of the OAuth 2.0 Authorization Server that the Credential Issuer relies on for - /// authorization. If this property is omitted, it is assumed that the entity providing the Credential Issuer - /// is also acting as the Authorization Server. In such cases, the Credential Issuer's - /// identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server - /// metadata. - /// - [JsonProperty("authorization_servers", NullValueHandling = NullValueHandling.Ignore)] - public string[]? AuthorizationServers { get; set; } - - /// - /// Gets the display properties of a given Credential for different languages. - /// - /// The credentialMetadataId to retrieve the display properties for. - /// - /// A list of display properties for the specified Credential or null if the Credential is not found in the - /// metadata. - /// - public List? GetCredentialDisplay(string credentialMetadataId) - => CredentialConfigurationsSupported[credentialMetadataId].Display; - - /// - /// Gets the claim attributes of a given Credential. - /// - /// The credentialMetadataId to retrieve the claim attributes for. - /// - /// A dictionary of attribute names and their corresponding display properties for the specified Credential, or - /// null if the Credential is not found in the metadata. - /// - public Dictionary? GetCredentialClaims(string credentialMetadataId) => - CredentialConfigurationsSupported[credentialMetadataId].Claims; - - /// - /// Gets the localized attribute names of a given Credential for a specific locale. - /// - /// The credentialMetadataId to retrieve the localized attribute names for. - /// The locale to retrieve the attribute names in (e.g., "en-US"). - /// - /// A list of localized attribute names for the specified Credential and locale, or null if no matching attributes - /// are found. - /// - public List? GetLocalizedCredentialAttributeNames(string credentialMetadataId, string locale) - { - var displayNames = new List(); - - var matchingCredential = CredentialConfigurationsSupported[credentialMetadataId]; - - if (matchingCredential == null) - return null; - - var localeDisplayNames = matchingCredential.Claims - .SelectMany(subject => subject.Value.Display) - .Where(display => display.Locale == locale) - .Select(display => display.Name); - - displayNames.AddRange(localeDisplayNames!); - - return displayNames.Count > 0 ? displayNames : null; - } - - /// - /// Gets the localized alias name of the Credential Issuer for a specific locale. - /// - /// The locale to retrieve the issuer alias name in (e.g., "en-US"). - /// - /// The localized alias name for the specified locale, or null if no matching alias is found. - /// - public string? GetLocalizedIssuerAlias(string locale) - { - if (Display == null) - return string.Empty; - - var display = Display.FirstOrDefault(display => display.Locale == locale); - return display?.Name; - } - } -} diff --git a/src/WalletFramework.SdJwtVc/Models/Records/SdJwtRecord.cs b/src/WalletFramework.SdJwtVc/Models/Records/SdJwtRecord.cs index 715ba8a1..625034e8 100644 --- a/src/WalletFramework.SdJwtVc/Models/Records/SdJwtRecord.cs +++ b/src/WalletFramework.SdJwtVc/Models/Records/SdJwtRecord.cs @@ -4,237 +4,244 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SD_JWT.Models; +using WalletFramework.Core.Cryptography.Models; +using WalletFramework.Core.Functional; using WalletFramework.SdJwtVc.Models.Credential; using WalletFramework.SdJwtVc.Models.Credential.Attributes; -using WalletFramework.SdJwtVc.Models.Issuer; -namespace WalletFramework.SdJwtVc.Models.Records +namespace WalletFramework.SdJwtVc.Models.Records; + +/// +/// A record that represents a Selective Disclosure JSON Web Token (SD-JWT) Credential with additional properties. +/// Inherits from base class RecordBase. +/// +public sealed class SdJwtRecord : RecordBase, ICredential { /// - /// A record that represents a Selective Disclosure JSON Web Token (SD-JWT) Credential with additional properties. - /// Inherits from base class RecordBase. + /// Gets or sets the attributes that should be displayed. + /// + public Dictionary? DisplayedAttributes { get; set; } + + /// + /// Gets or sets the attributes that should be displayed. + /// + public List? AttributeOrder { get; set; } + + /// + /// Gets or sets the flattened structure of the claims in the credential. + /// The key is a JSON path to the claim value in the decoded SdJwtVc. + /// + public Dictionary Claims { get; set; } + + /// + /// Gets or sets the name of the issuer in different languages. + /// + public Dictionary? IssuerName { get; set; } + + /// + /// Gets the disclosures. + /// + public ImmutableArray Disclosures { get; set; } + + /// + /// Gets or sets the display of the credential. + /// + public List? Display { get; set; } + + /// + /// Gets the Issuer-signed JWT part of the SD-JWT. + /// + public string EncodedIssuerSignedJwt { get; set; } = null!; + + /// + /// Gets or sets the identifier for the issuer. + /// + [JsonIgnore] + public string IssuerId + { + get => Get(); + set => Set(value, false); + } + + /// + /// Gets the key record ID. /// - public sealed class SdJwtRecord : RecordBase, ICredential + [JsonIgnore] + public KeyId KeyId { - /// - /// Gets or sets the attributes that should be displayed. - /// - public Dictionary? DisplayedAttributes { get; set; } - - /// - /// Gets or sets the attributes that should be displayed. - /// - public List? AttributeOrder { get; set; } - - /// - /// Gets or sets the flattened structure of the claims in the credential. - /// The key is a JSON path to the claim value in the decoded SdJwtVc. - /// - public Dictionary Claims { get; set; } - - /// - /// Gets or sets the name of the issuer in different languages. - /// - public Dictionary? IssuerName { get; set; } - - /// - /// Gets the disclosures. - /// - public ImmutableArray Disclosures { get; set; } - - /// - /// Gets or sets the display of the credential. - /// - public List? Display { get; set; } - - /// - /// Gets the Issuer-signed JWT part of the SD-JWT. - /// - public string EncodedIssuerSignedJwt { get; set; } = null!; - - /// - /// Gets or sets the identifier for the issuer. - /// - [JsonIgnore] - public string IssuerId + get { - get => Get(); - set => Set(value, false); + var str = Get(); + return KeyId + .ValidKeyId(str) + .UnwrapOrThrow(new InvalidOperationException("Persisted Key-ID in SD-JWT Record is corrupt")); } - - /// - /// Gets or sets the key record ID. - /// - [JsonIgnore] - public string KeyId + private set { - get => Get(); - set => Set(value); + string keyId = value; + Set(keyId); } + } - /// - public override string TypeName => "AF.SdJwtRecord"; + /// + public override string TypeName => "AF.SdJwtRecord"; - /// - /// Gets the verifiable credential type. - /// - [JsonIgnore] - public string Vct - { - get => Get(); - set => Set(value, false); - } + /// + /// Gets the verifiable credential type. + /// + [JsonIgnore] + public string Vct + { + get => Get(); + set => Set(value, false); + } + #pragma warning disable CS8618 - /// - /// Parameterless Default Constructor. - /// - public SdJwtRecord() - { - } + /// + /// Parameterless Default Constructor. + /// + public SdJwtRecord() + { + } #pragma warning restore CS8618 - /// - /// Constructor for Serialization. - /// - /// The attributes that should be displayed. - /// The claims made. - /// The name of the issuer in different languages. - /// The disclosures. - /// The display of the credential. - /// The Issuer-signed JWT part of the SD-JWT. - /// The identifier. - /// The identifier for the issuer. - /// The key record ID. - /// The verifiable credential type. - [JsonConstructor] - public SdJwtRecord( - Dictionary displayedAttributes, - Dictionary claims, - Dictionary issuerName, - ImmutableArray disclosures, - List display, - string encodedIssuerSignedJwt, - string issuerId, - string keyId, - string vct) - { - Id = Guid.NewGuid().ToString(); + /// + /// Constructor for Serialization. + /// + /// The attributes that should be displayed. + /// The claims made. + /// The name of the issuer in different languages. + /// The disclosures. + /// The display of the credential. + /// + /// The Issuer-signed JWT part of the SD-JWT. + [JsonConstructor] + public SdJwtRecord( + Dictionary displayedAttributes, + Dictionary claims, + Dictionary issuerName, + ImmutableArray disclosures, + List display, + string issuerId, + string encodedIssuerSignedJwt) + { + Claims = claims; + Disclosures = disclosures; + + Display = display; + DisplayedAttributes = displayedAttributes; - Claims = claims; - Disclosures = disclosures; + EncodedIssuerSignedJwt = encodedIssuerSignedJwt; - Display = display; - DisplayedAttributes = displayedAttributes; + IssuerName = issuerName; + + IssuerId = issuerId; + } + + public SdJwtRecord( + string serializedSdJwtWithDisclosures, + Dictionary displayedAttributes, + List display, + Dictionary issuerName, + KeyId keyId) + { + Id = Guid.NewGuid().ToString(); - EncodedIssuerSignedJwt = encodedIssuerSignedJwt; + var sdJwtDoc = new SdJwtDoc(serializedSdJwtWithDisclosures); + EncodedIssuerSignedJwt = sdJwtDoc.IssuerSignedJwt; + Disclosures = sdJwtDoc.Disclosures.Select(x => x.Serialize()).ToImmutableArray(); + Claims = sdJwtDoc.GetAllSubjectClaims(); + Display = display; + DisplayedAttributes = displayedAttributes; - IssuerId = issuerId; - IssuerName = issuerName; + IssuerName = issuerName; - KeyId = keyId; - Vct = vct; - } - - public SdJwtRecord( - string serializedSdJwtWithDisclosures, - Dictionary displayedAttributes, - List display, - Dictionary issuerName, - string keyId - ) - { - Id = Guid.NewGuid().ToString(); + KeyId = keyId; + IssuerId = sdJwtDoc.UnsecuredPayload.SelectToken("iss")?.Value() + ?? throw new ArgumentNullException(nameof(IssuerId), "iss claim is missing or null"); + Vct = sdJwtDoc.UnsecuredPayload.SelectToken("vct")?.Value() + ?? throw new ArgumentNullException(nameof(Vct), "vct claim is missing or null"); + } + + public SdJwtRecord( + SdJwtDoc sdJwtDoc, + Dictionary displayedAttributes, + List display, + Dictionary issuerName, + KeyId keyId) + { + Id = Guid.NewGuid().ToString(); - SdJwtDoc sdJwtDoc = new SdJwtDoc(serializedSdJwtWithDisclosures); - EncodedIssuerSignedJwt = sdJwtDoc.IssuerSignedJwt; - Disclosures = sdJwtDoc.Disclosures.Select(x => x.Serialize()).ToImmutableArray(); - Claims = sdJwtDoc.GetAllSubjectClaims(); - Display = display; - DisplayedAttributes = displayedAttributes; + EncodedIssuerSignedJwt = sdJwtDoc.IssuerSignedJwt; + Disclosures = sdJwtDoc.Disclosures.Select(disclosure => disclosure.Serialize()).ToImmutableArray(); + Claims = sdJwtDoc.GetAllSubjectClaims(); + Display = display; + DisplayedAttributes = displayedAttributes; - IssuerName = issuerName; + IssuerName = issuerName; - KeyId = keyId; - IssuerId = sdJwtDoc.UnsecuredPayload.SelectToken("iss")?.Value() ?? - throw new ArgumentNullException(nameof(IssuerId), "iss claim is missing or null"); - Vct = sdJwtDoc.UnsecuredPayload.SelectToken("vct")?.Value() ?? - throw new ArgumentNullException(nameof(Vct), "vct claim is missing or null"); // Extract vct - } - - /// - /// Creates a dictionary of the issuer name in different languages based on the issuer metadata. - /// - /// The issuer metadata. - /// The dictionary of the issuer name in different languages. - private static Dictionary? CreateIssuerNameDictionary(IssuerMetadata issuerMetadata) - { - var issuerNameDictionary = new Dictionary(); - - foreach (var display in issuerMetadata.Display?.Where(d => d.Locale != null) ?? - Enumerable.Empty()) - { - issuerNameDictionary[display.Locale!] = display.Name!; - } - - return issuerNameDictionary.Count > 0 ? issuerNameDictionary : null; - } + KeyId = keyId; + IssuerId = sdJwtDoc.UnsecuredPayload.SelectToken("iss")?.Value() + ?? throw new ArgumentNullException(nameof(IssuerId), "iss claim is missing or null"); + Vct = sdJwtDoc.UnsecuredPayload.SelectToken("vct")?.Value() + ?? throw new ArgumentNullException(nameof(Vct), "vct claim is missing or null"); } +} - internal static class JsonExtensions +internal static class JsonExtensions +{ + internal static Dictionary GetAllSubjectClaims(this SdJwtDoc sdJwtDoc) { - internal static Dictionary GetAllSubjectClaims(this SdJwtDoc sdJwtDoc) - { - var unsecuredPayload = (JObject)sdJwtDoc.UnsecuredPayload.DeepClone(); + var unsecuredPayload = (JObject)sdJwtDoc.UnsecuredPayload.DeepClone(); - RemoveRegisteredClaims(unsecuredPayload); + RemoveRegisteredClaims(unsecuredPayload); - var allLeafClaims = GetLeafNodePaths(unsecuredPayload); + var allLeafClaims = GetLeafNodePaths(unsecuredPayload); - return allLeafClaims.ToDictionary(key => key, key => unsecuredPayload.SelectToken(key)?.ToString() ?? string.Empty); + return allLeafClaims.ToDictionary(key => key, key => unsecuredPayload.SelectToken(key)?.ToString() ?? string.Empty); - void RemoveRegisteredClaims(JObject jObject) + void RemoveRegisteredClaims(JObject jObject) + { + string[] registeredClaims = { "iss", "sub", "aud", "exp", "nbf", "iat", "jti", "vct", "cnf", "status" }; + foreach (var claim in registeredClaims) { - string[] registeredClaims = { "iss", "sub", "aud", "exp", "nbf", "iat", "jti", "vct", "cnf", "status" }; - foreach (var claim in registeredClaims) - { - jObject.Remove(claim); - } + jObject.Remove(claim); } } + } - private static List GetLeafNodePaths(JObject jObject) - { - var leafNodePaths = new List(); + private static List GetLeafNodePaths(JObject jObject) + { + var leafNodePaths = new List(); - TraverseJToken(jObject, "", leafNodePaths); + TraverseJToken(jObject, "", leafNodePaths); - return leafNodePaths; - } + return leafNodePaths; + } - private static void TraverseJToken(JToken token, string currentPath, List leafNodePaths) + private static void TraverseJToken(JToken token, string currentPath, List leafNodePaths) + { + switch (token.Type) { - switch (token.Type) - { - case JTokenType.Object: - foreach (var property in token.Children()) - { - TraverseJToken(property.Value, $"{currentPath}.{property.Name}", leafNodePaths); - } - break; - - case JTokenType.Array: - int index = 0; - foreach (var item in token.Children()) - { - TraverseJToken(item, $"{currentPath}[{index}]", leafNodePaths); - index++; - } - break; - - default: - leafNodePaths.Add(currentPath.TrimStart('.')); - break; - } + case JTokenType.Object: + foreach (var property in token.Children()) + { + TraverseJToken(property.Value, $"{currentPath}.{property.Name}", leafNodePaths); + } + break; + + case JTokenType.Array: + int index = 0; + foreach (var item in token.Children()) + { + TraverseJToken(item, $"{currentPath}[{index}]", leafNodePaths); + index++; + } + break; + + default: + leafNodePaths.Add(currentPath.TrimStart('.')); + break; } } } diff --git a/src/WalletFramework.SdJwtVc/Models/Vct.cs b/src/WalletFramework.SdJwtVc/Models/Vct.cs new file mode 100644 index 00000000..eee00c2b --- /dev/null +++ b/src/WalletFramework.SdJwtVc/Models/Vct.cs @@ -0,0 +1,30 @@ +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; +using WalletFramework.Core.Json; +using WalletFramework.Core.Json.Converters; + +namespace WalletFramework.SdJwtVc.Models; + +[JsonConverter(typeof(ValueTypeJsonConverter))] +public readonly struct Vct +{ + private string Value { get; } + + private Vct(string vct) => Value = vct; + + public override string ToString() => Value; + + public static implicit operator string(Vct vct) => vct.Value; + + public static Validation ValidVct(JToken vct) => vct.ToJValue().OnSuccess(value => + { + var str = value.ToString(CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(str)) + return new StringIsNullOrWhitespaceError().ToInvalid(); + + return new Vct(str); + }); +} diff --git a/src/WalletFramework.SdJwtVc/ServiceCollectionExtensions.cs b/src/WalletFramework.SdJwtVc/ServiceCollectionExtensions.cs index fa14b6b6..7dbfe4fe 100644 --- a/src/WalletFramework.SdJwtVc/ServiceCollectionExtensions.cs +++ b/src/WalletFramework.SdJwtVc/ServiceCollectionExtensions.cs @@ -7,28 +7,24 @@ namespace WalletFramework.SdJwtVc; public static class ServiceCollectionExtensions { - public static IServiceCollection AddSdJwtVcDefaultServices(this IServiceCollection builder) + public static IServiceCollection AddSdJwtVcServices(this IServiceCollection builder) { builder.AddSingleton(); - builder.AddSingleton(); + builder.AddSingleton(); return builder; } - /// /// Adds the extended Sd-JWT credential service. /// /// The extended SD-JWT credential service. /// Builder. - /// The 1st type parameter. /// The 2nd type parameter. - public static IServiceCollection AddExtendedSdJwtCredentialService(this IServiceCollection builder) - where TService : class, ISdJwtVcHolderService - where TImplementation : class, TService, ISdJwtVcHolderService + public static IServiceCollection AddExtendedSdJwtHolderService(this IServiceCollection builder) + where TImplementation : class, ISdJwtVcHolderService { builder.AddSingleton(); builder.AddSingleton(x => x.GetService()); - builder.AddSingleton(x => x.GetService()); return builder; } } diff --git a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/DefaultSdJwtVcHolderService.cs b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/DefaultSdJwtVcHolderService.cs deleted file mode 100644 index 6cc0d257..00000000 --- a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/DefaultSdJwtVcHolderService.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Hyperledger.Aries; -using Hyperledger.Aries.Agents; -using Hyperledger.Aries.Storage; -using Hyperledger.Indy.WalletApi; -using SD_JWT.Models; -using SD_JWT.Roles; -using WalletFramework.SdJwtVc.KeyStore.Services; -using WalletFramework.SdJwtVc.Models.Credential; -using WalletFramework.SdJwtVc.Models.Credential.Attributes; -using WalletFramework.SdJwtVc.Models.Issuer; -using WalletFramework.SdJwtVc.Models.Records; - -namespace WalletFramework.SdJwtVc.Services.SdJwtVcHolderService -{ - /// - public class DefaultSdJwtVcHolderService : ISdJwtVcHolderService - { - /// - /// The service responsible for holder operations. - /// - protected readonly IHolder Holder; - - /// - /// The key store responsible for key operations. - /// - protected readonly IKeyStore KeyStore; - - /// - /// The service responsible for wallet record operations. - /// - protected readonly IWalletRecordService RecordService; - - /// - /// Initializes a new instance of the class. - /// - /// The key store responsible for key operations. - /// The service responsible for wallet record operations. - /// The service responsible for holder operations. - public DefaultSdJwtVcHolderService( - IHolder holder, - IKeyStore keyStore, - IWalletRecordService recordService) - { - Holder = holder; - KeyStore = keyStore; - RecordService = recordService; - } - - /// - public async Task CreatePresentation(SdJwtRecord credential, string[] disclosedClaimPaths, - string? audience = null, - string? nonce = null) - { - var sdJwtDoc = credential.ToSdJwtDoc(); - var disclosures = new List(); - foreach (var disclosure in sdJwtDoc.Disclosures) - { - if (disclosedClaimPaths.Any(disclosedClaim => disclosedClaim.StartsWith(disclosure.Path ?? string.Empty))) - { - disclosures.Add(disclosure); - } - } - - var presentationFormat = Holder.CreatePresentationFormat(credential.EncodedIssuerSignedJwt, disclosures.ToArray()); - - if (!string.IsNullOrEmpty(credential.KeyId) && !string.IsNullOrEmpty(nonce) && - !string.IsNullOrEmpty(audience)) - { - var keybindingJwt = - await KeyStore.GenerateKbProofOfPossessionAsync(credential.KeyId, audience, nonce, "kb+jwt", presentationFormat.ToSdHash()); - return presentationFormat.AddKeyBindingJwt(keybindingJwt); - } - - return presentationFormat.Value; - } - - /// - public virtual async Task DeleteAsync(IAgentContext context, string recordId) - { - return await RecordService.DeleteAsync(context.Wallet, recordId); - } - - /// - public virtual async Task GetAsync(IAgentContext context, string credentialId) - { - var record = await RecordService.GetAsync(context.Wallet, credentialId); - if (record == null) - throw new AriesFrameworkException(ErrorCode.RecordNotFound, "SD-JWT Credential record not found"); - - return record; - } - - /// - public virtual Task> ListAsync(IAgentContext context, ISearchQuery query = null, - int count = 100, - int skip = 0) - { - return RecordService.SearchAsync(context.Wallet, query, null, count, skip); - } - - /// - [Obsolete("Use SaveAsync instead.")] - public virtual async Task StoreAsync( - IAgentContext context, - string combinedIssuance, - string keyId, - IssuerMetadata issuerMetadata, - List displayMetadata, - Dictionary claimMetadata, - Dictionary issuerName - ) - { - var record = new SdJwtRecord(combinedIssuance, claimMetadata, displayMetadata, issuerName, keyId); - - await SaveAsync(context, record); - return record.Id; - } - - /// - public virtual async Task SaveAsync(IAgentContext context, SdJwtRecord record) - { - try - { - await RecordService.AddAsync(context.Wallet, record); - } - catch (WalletItemAlreadyExistsException) - { - await RecordService.UpdateAsync(context.Wallet, record); - } - } - } - - internal static class SdJwtRecordExtensions - { - internal static SdJwtDoc ToSdJwtDoc(this SdJwtRecord record) - { - return new SdJwtDoc(record.EncodedIssuerSignedJwt + "~" + string.Join("~", record.Disclosures) + "~"); - } - } -} diff --git a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtVcHolderService.cs b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtVcHolderService.cs index 4e700b66..72e1ec9e 100644 --- a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtVcHolderService.cs +++ b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtVcHolderService.cs @@ -1,84 +1,75 @@ using Hyperledger.Aries.Agents; using Hyperledger.Aries.Storage; -using WalletFramework.SdJwtVc.Models.Credential; -using WalletFramework.SdJwtVc.Models.Credential.Attributes; -using WalletFramework.SdJwtVc.Models.Issuer; using WalletFramework.SdJwtVc.Models.Records; -namespace WalletFramework.SdJwtVc.Services.SdJwtVcHolderService +namespace WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; + +/// +/// Provides methods for handling SD-JWT credentials. +/// +public interface ISdJwtVcHolderService { /// - /// Provides methods for handling SD-JWT credentials. + /// Deletes a specific SD-JWT record by its ID. /// - public interface ISdJwtVcHolderService - { - /// - /// Deletes a specific SD-JWT record by its ID. - /// - /// The agent context. - /// The ID of the SD-JWT credential record to delete. - /// - /// A task representing the asynchronous operation. The task result indicates whether the deletion was successful. - /// - Task DeleteAsync(IAgentContext context, string recordId); + /// The agent context. + /// The ID of the SD-JWT credential record to delete. + /// + /// A task representing the asynchronous operation. The task result indicates whether the deletion was successful. + /// + Task DeleteAsync(IAgentContext context, string recordId); - /// - /// Creates a SD-JWT in presentation format where the provided claims are disclosed. - /// The key binding is optional and can be activated by providing an audience and a nonce. - /// - /// - /// The SD-JWT is created using the provided SD-JWT credential and the provided claims are disclosed - /// - /// The claims to disclose - /// The SD-JWT credential - /// The targeted audience - /// The nonce - /// The SD-JWT in presentation format - Task CreatePresentation(SdJwtRecord credential, string[] disclosedClaimPaths, string? audience = null, string? nonce = null); + /// + /// Creates a SD-JWT in presentation format where the provided claims are disclosed. + /// The key binding is optional and can be activated by providing an audience and a nonce. + /// + /// + /// The SD-JWT is created using the provided SD-JWT credential and the provided claims are disclosed + /// + /// The claims to disclose + /// The SD-JWT credential + /// The targeted audience + /// The nonce + /// The SD-JWT in presentation format + Task CreatePresentation( + SdJwtRecord credential, + string[] disclosedClaimPaths, + string? audience = null, + string? nonce = null); - /// - /// Retrieves a specific SD-JWT record by its ID. - /// - /// The agent context. - /// The ID of the SD-JWT credential record to retrieve. - /// - /// A task representing the asynchronous operation. The task result contains the - /// associated with the given ID. - /// - Task GetAsync(IAgentContext context, string credentialId); + /// + /// Retrieves a specific SD-JWT record by its ID. + /// + /// The agent context. + /// The ID of the SD-JWT credential record to retrieve. + /// + /// A task representing the asynchronous operation. The task result contains the + /// associated with the given ID. + /// + Task GetAsync(IAgentContext context, string credentialId); - /// - /// Lists SD-JWT records based on specified criteria. - /// - /// The agent context. - /// The search query to filter SD-JWT records. Default is null, meaning no filter. - /// The maximum number of records to retrieve. Default is 100. - /// The number of records to skip. Default is 0. - /// - /// A task representing the asynchronous operation. The task result contains a list of - /// that match the criteria. - /// - Task> ListAsync(IAgentContext context, ISearchQuery? query = null, int count = 100, - int skip = 0); + /// + /// Lists SD-JWT records based on specified criteria. + /// + /// The agent context. + /// The search query to filter SD-JWT records. Default is null, meaning no filter. + /// The maximum number of records to retrieve. Default is 100. + /// The number of records to skip. Default is 0. + /// + /// A task representing the asynchronous operation. The task result contains a list of + /// that match the criteria. + /// + Task> ListAsync( + IAgentContext context, + ISearchQuery? query = null, + int count = 100, + int skip = 0); - /// - /// Stores a new SD-JWT record. - /// - /// The agent context. - /// The combined issuance. - /// The key id. - /// The issuer metadata. - /// - /// - /// - /// A task representing the asynchronous operation. The task result contains the ID of the stored JWT record. - Task StoreAsync( - IAgentContext context, - string combinedIssuance, - string keyId, - IssuerMetadata issuerMetadata, - List displayMetadata, - Dictionary claimMetadata, - Dictionary issuerName); - } + /// + /// Stores or updates a SD-JWT record. + /// + /// The agent context. + /// The SD-JWT record to be saved + /// A task representing the asynchronous operation. The task result contains the ID of the stored JWT record. + Task SaveAsync(IAgentContext context, SdJwtRecord record); } diff --git a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs new file mode 100644 index 00000000..7597ed02 --- /dev/null +++ b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs @@ -0,0 +1,113 @@ +using Hyperledger.Aries; +using Hyperledger.Aries.Agents; +using Hyperledger.Aries.Storage; +using Hyperledger.Indy.WalletApi; +using SD_JWT.Models; +using SD_JWT.Roles; +using WalletFramework.Core.Cryptography.Abstractions; +using WalletFramework.SdJwtVc.Models.Records; + +namespace WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; + +/// +public class SdJwtVcHolderService : ISdJwtVcHolderService +{ + /// + /// Initializes a new instance of the class. + /// + /// The key store responsible for key operations. + /// The service responsible for wallet record operations. + /// The service responsible for holder operations. + public SdJwtVcHolderService( + IHolder holder, + IKeyStore keyStore, + IWalletRecordService recordService) + { + _holder = holder; + _keyStore = keyStore; + _recordService = recordService; + } + + private readonly IHolder _holder; + private readonly IKeyStore _keyStore; + private readonly IWalletRecordService _recordService; + + /// + public async Task CreatePresentation( + SdJwtRecord credential, + string[] disclosedClaimPaths, + string? audience = null, + string? nonce = null) + { + var sdJwtDoc = credential.ToSdJwtDoc(); + var disclosures = new List(); + foreach (var disclosure in sdJwtDoc.Disclosures) + { + if (disclosedClaimPaths.Any(disclosedClaim => disclosedClaim.StartsWith(disclosure.Path ?? string.Empty))) + { + disclosures.Add(disclosure); + } + } + + var presentationFormat = + _holder.CreatePresentationFormat(credential.EncodedIssuerSignedJwt, disclosures.ToArray()); + + if (!string.IsNullOrEmpty(credential.KeyId) + && !string.IsNullOrEmpty(nonce) + && !string.IsNullOrEmpty(audience)) + { + + + var keybindingJwt = await _keyStore.GenerateKbProofOfPossessionAsync( + credential.KeyId, + audience, + nonce, + "kb+jwt", + presentationFormat.ToSdHash()); + + return presentationFormat.AddKeyBindingJwt(keybindingJwt); + } + + return presentationFormat.Value; + } + + /// + public virtual async Task DeleteAsync(IAgentContext context, string recordId) => + await _recordService.DeleteAsync(context.Wallet, recordId); + + /// + public async Task GetAsync(IAgentContext context, string credentialId) + { + var record = await _recordService.GetAsync(context.Wallet, credentialId); + if (record == null) + throw new AriesFrameworkException(ErrorCode.RecordNotFound, "SD-JWT Credential record not found"); + + return record; + } + + /// + public Task> ListAsync( + IAgentContext context, + ISearchQuery? query = null, + int count = 100, + int skip = 0) => _recordService.SearchAsync(context.Wallet, query, null, count, skip); + + /// + public virtual async Task SaveAsync(IAgentContext context, SdJwtRecord record) + { + try + { + await _recordService.AddAsync(context.Wallet, record); + } + catch (WalletItemAlreadyExistsException) + { + await _recordService.UpdateAsync(context.Wallet, record); + } + } +} + +internal static class SdJwtRecordExtensions +{ + internal static SdJwtDoc ToSdJwtDoc(this SdJwtRecord record) => + new(record.EncodedIssuerSignedJwt + "~" + string.Join("~", record.Disclosures) + "~"); +} diff --git a/src/WalletFramework.SdJwtVc/WalletFramework.SdJwtVc.csproj b/src/WalletFramework.SdJwtVc/WalletFramework.SdJwtVc.csproj index 18890638..95c50643 100644 --- a/src/WalletFramework.SdJwtVc/WalletFramework.SdJwtVc.csproj +++ b/src/WalletFramework.SdJwtVc/WalletFramework.SdJwtVc.csproj @@ -8,10 +8,10 @@ + - diff --git a/src/WalletFramework.sln b/src/WalletFramework.sln index eb5211af..56766f62 100644 --- a/src/WalletFramework.sln +++ b/src/WalletFramework.sln @@ -37,13 +37,19 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletFramework.Oid4Vc.Test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletFramework.SdJwtVc.Tests", "..\test\WalletFramework.SdJwtVc.Tests\WalletFramework.SdJwtVc.Tests.csproj", "{16BE497A-1ED7-41CD-AD68-BEB0E2A0AC0B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletFramework.Mdoc", "WalletFramework.Mdoc\WalletFramework.Mdoc.csproj", "{0EDD27CB-967F-4451-81AE-309E7F534F1C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletFramework.MdocLib", "WalletFramework.MdocLib\WalletFramework.MdocLib.csproj", "{0EDD27CB-967F-4451-81AE-309E7F534F1C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletFramework.Mdoc.Tests", "..\test\WalletFramework.Mdoc.Tests\WalletFramework.Mdoc.Tests.csproj", "{15D8778B-F83C-4DDF-B30E-8F95F79ACAE8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletFramework.MdocLib.Tests", "..\test\WalletFramework.MdocLib.Tests\WalletFramework.MdocLib.Tests.csproj", "{15D8778B-F83C-4DDF-B30E-8F95F79ACAE8}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Functional", "Functional", "{F8F02219-0462-4031-843E-6F3DC8CD1638}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletFramework.MdocVc", "WalletFramework.MdocVc\WalletFramework.MdocVc.csproj", "{D1ABF6E9-AD2C-4068-A86F-D35E78C187B7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletFramework.Functional", "WalletFramework.Functional\WalletFramework.Functional.csproj", "{314D2A44-8A23-48CC-8848-7FBDC33847F5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletFramework.MdocVc.Tests", "..\test\WalletFramework.MdocVc.Tests\WalletFramework.MdocVc.Tests.csproj", "{A7DC0511-DE0A-4EC2-8CC1-197FB81D7A76}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletFramework.Core", "WalletFramework.Core\WalletFramework.Core.csproj", "{F6B3A24B-CDA2-4CC1-9F68-380203355099}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{873772C5-60B9-442B-B06E-C279919B963C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mdoc", "Mdoc", "{A1DD69B3-DC35-43CF-AE14-D751722F074A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -111,10 +117,18 @@ Global {15D8778B-F83C-4DDF-B30E-8F95F79ACAE8}.Debug|Any CPU.Build.0 = Debug|Any CPU {15D8778B-F83C-4DDF-B30E-8F95F79ACAE8}.Release|Any CPU.ActiveCfg = Release|Any CPU {15D8778B-F83C-4DDF-B30E-8F95F79ACAE8}.Release|Any CPU.Build.0 = Release|Any CPU - {314D2A44-8A23-48CC-8848-7FBDC33847F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {314D2A44-8A23-48CC-8848-7FBDC33847F5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {314D2A44-8A23-48CC-8848-7FBDC33847F5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {314D2A44-8A23-48CC-8848-7FBDC33847F5}.Release|Any CPU.Build.0 = Release|Any CPU + {D1ABF6E9-AD2C-4068-A86F-D35E78C187B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1ABF6E9-AD2C-4068-A86F-D35E78C187B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1ABF6E9-AD2C-4068-A86F-D35E78C187B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1ABF6E9-AD2C-4068-A86F-D35E78C187B7}.Release|Any CPU.Build.0 = Release|Any CPU + {A7DC0511-DE0A-4EC2-8CC1-197FB81D7A76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7DC0511-DE0A-4EC2-8CC1-197FB81D7A76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7DC0511-DE0A-4EC2-8CC1-197FB81D7A76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7DC0511-DE0A-4EC2-8CC1-197FB81D7A76}.Release|Any CPU.Build.0 = Release|Any CPU + {F6B3A24B-CDA2-4CC1-9F68-380203355099}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6B3A24B-CDA2-4CC1-9F68-380203355099}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6B3A24B-CDA2-4CC1-9F68-380203355099}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6B3A24B-CDA2-4CC1-9F68-380203355099}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -134,9 +148,11 @@ Global {FE43A9CD-1E5B-4F1F-BA08-A4A7E9A131FD} = {615A7979-79AD-4485-805F-3F4B772510CD} {8E8B8EFA-3AFE-4D17-B381-8C230A86DB88} = {02ADBA96-A50C-44F0-A9D9-FA0629AA2DF4} {16BE497A-1ED7-41CD-AD68-BEB0E2A0AC0B} = {02ADBA96-A50C-44F0-A9D9-FA0629AA2DF4} - {0EDD27CB-967F-4451-81AE-309E7F534F1C} = {615A7979-79AD-4485-805F-3F4B772510CD} {15D8778B-F83C-4DDF-B30E-8F95F79ACAE8} = {02ADBA96-A50C-44F0-A9D9-FA0629AA2DF4} - {314D2A44-8A23-48CC-8848-7FBDC33847F5} = {F8F02219-0462-4031-843E-6F3DC8CD1638} + {D1ABF6E9-AD2C-4068-A86F-D35E78C187B7} = {615A7979-79AD-4485-805F-3F4B772510CD} + {A7DC0511-DE0A-4EC2-8CC1-197FB81D7A76} = {02ADBA96-A50C-44F0-A9D9-FA0629AA2DF4} + {F6B3A24B-CDA2-4CC1-9F68-380203355099} = {873772C5-60B9-442B-B06E-C279919B963C} + {0EDD27CB-967F-4451-81AE-309E7F534F1C} = {A1DD69B3-DC35-43CF-AE14-D751722F074A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4FFA80F9-ADC6-40DB-BBD1-A522B8A68560} diff --git a/test/Hyperledger.Aries.Tests/Routing/RoutingInboxHandlerTests.cs b/test/Hyperledger.Aries.Tests/Routing/RoutingInboxHandlerTests.cs index ee6355ba..4f833337 100644 --- a/test/Hyperledger.Aries.Tests/Routing/RoutingInboxHandlerTests.cs +++ b/test/Hyperledger.Aries.Tests/Routing/RoutingInboxHandlerTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using Hyperledger.Aries.Agents; @@ -9,6 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; +using Newtonsoft.Json.Linq; using Xunit; namespace Hyperledger.Aries.Tests.Routing @@ -46,7 +48,7 @@ public async Task CreateInboxRecordAsync() CreateInboxResponseMessage agentMessage = (CreateInboxResponseMessage)await routingInboxHandler.ProcessAsync(agentContext.Object, unpackedMessage); walletService.Verify(w => w.CreateWalletAsync(It.Is(wc => wc.Id == agentMessage.InboxId), It.Is(wc => wc.Key == agentMessage.InboxKey))); - recordService.Verify(m => m.AddAsync(agentContext.Object.Wallet, It.Is(i => i.Tags.Count == 0)), Times.Once()); + recordService.Verify(m => m.AddAsync(agentContext.Object.Wallet, It.Is(i => i.Tags.Count == 0), It.IsAny?>()), Times.Once()); recordService.Verify(m => m.UpdateAsync(agentContext.Object.Wallet, It.Is(c => c.GetTag("InboxId") == agentMessage.InboxId))); } @@ -72,7 +74,7 @@ public async Task CreateInboxRecordWithMetadataAsync() agentMessage.InboxKey.Should().HaveLength(44); walletService.Verify(w => w.CreateWalletAsync(It.Is(wc => wc.Id == agentMessage.InboxId), It.Is(wc => wc.Key == agentMessage.InboxKey))); - recordService.Verify(m => m.AddAsync(agentContext.Object.Wallet, It.Is(i => i.GetTag(key) == value)), Times.Once()); + recordService.Verify(m => m.AddAsync(agentContext.Object.Wallet, It.Is(i => i.GetTag(key) == value), It.IsAny?>()), Times.Once()); recordService.Verify(m => m.UpdateAsync(agentContext.Object.Wallet, It.Is(c => c.GetTag("InboxId") == agentMessage.InboxId))); } diff --git a/test/WalletFramework.Mdoc.Tests/Samples.cs b/test/WalletFramework.Mdoc.Tests/Samples.cs deleted file mode 100644 index a846f88c..00000000 --- a/test/WalletFramework.Mdoc.Tests/Samples.cs +++ /dev/null @@ -1,68 +0,0 @@ -using PeterO.Cbor; -using WalletFramework.Functional; - -namespace WalletFramework.Mdoc.Tests; - -public static class Samples -{ - public const string DocType = "org.iso.18013.5.1.mDL"; - - public const string MdlNameSpace = "org.iso.18013.5.1"; - - public static string EncodedMdoc = - "omdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiam5hbWVTcGFjZXOhcW9yZy5pc28uMTgwMTMuNS4xiNgYWFukaGRpZ2VzdElEAWZyYW5kb21QcbnmTIHt0_17t-AcHkKZbHFlbGVtZW50SWRlbnRpZmllcmppc3N1ZV9kYXRlbGVsZW1lbnRWYWx1ZdkD7GoyMDI0LTAxLTEy2BhYXKRoZGlnZXN0SUQCZnJhbmRvbVBRwvzBVJYBc2plhd7vXZwTcWVsZW1lbnRJZGVudGlmaWVya2V4cGlyeV9kYXRlbGVsZW1lbnRWYWx1ZdkD7GoyMDI1LTAxLTEy2BhYWqRoZGlnZXN0SUQDZnJhbmRvbVDcuBh2xE6SqxDDECOY9H3CcWVsZW1lbnRJZGVudGlmaWVya2ZhbWlseV9uYW1lbGVsZW1lbnRWYWx1ZWtTaWx2ZXJzdG9uZdgYWFKkaGRpZ2VzdElEBGZyYW5kb21QHu5Fe96gJQH-NeOAvSuJdHFlbGVtZW50SWRlbnRpZmllcmpnaXZlbl9uYW1lbGVsZW1lbnRWYWx1ZWRJbmdh2BhYW6RoZGlnZXN0SUQFZnJhbmRvbVDI-4b03R-29ljFhUoZMHP0cWVsZW1lbnRJZGVudGlmaWVyamJpcnRoX2RhdGVsZWxlbWVudFZhbHVl2QPsajE5OTEtMTEtMDbYGFhVpGhkaWdlc3RJRAZmcmFuZG9tUCJlXpl0UAxhiiN9BwSnLeBxZWxlbWVudElkZW50aWZpZXJvaXNzdWluZ19jb3VudHJ5bGVsZW1lbnRWYWx1ZWJVU9gYWFukaGRpZ2VzdElEB2ZyYW5kb21QbWz_ggUxytSax7_FqCzoEHFlbGVtZW50SWRlbnRpZmllcm9kb2N1bWVudF9udW1iZXJsZWxlbWVudFZhbHVlaDEyMzQ1Njc42BhYoqRoZGlnZXN0SUQIZnJhbmRvbVBbSwOg91lMspu_ctBa2uqgcWVsZW1lbnRJZGVudGlmaWVycmRyaXZpbmdfcHJpdmlsZWdlc2xlbGVtZW50VmFsdWWBo3V2ZWhpY2xlX2NhdGVnb3J5X2NvZGVhQWppc3N1ZV9kYXRl2QPsajIwMjMtMDEtMDFrZXhwaXJ5X2RhdGXZA-xqMjA0My0wMS0wMWppc3N1ZXJBdXRohEOhASahGCFZAWEwggFdMIIBBKADAgECAgYBjJHZwhkwCgYIKoZIzj0EAwIwNjE0MDIGA1UEAwwrSjFGd0pQODdDNi1RTl9XU0lPbUpBUWM2bjVDUV9iWmRhRko1R0RuVzFSazAeFw0yMzEyMjIxNDA2NTZaFw0yNDEwMTcxNDA2NTZaMDYxNDAyBgNVBAMMK0oxRndKUDg3QzYtUU5fV1NJT21KQVFjNm41Q1FfYlpkYUZKNUdEblcxUmswWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQCilV5ugmlhHJzDVgqSRE5d8KkoQqX1jVg8WE4aPjFODZQ66fFPFIhWRP3ioVUi67WGQSgTY3F6Vmjf7JMVQ4MMAoGCCqGSM49BAMCA0cAMEQCIGcWNJwFy8RGV4uMwK7k1vEkqQ2xr-BCGRdN8OZur5PeAiBVrNuxV1C9mCW5z2clhDFaXNdP2Lp_7CBQrHQoJhuPcNgYWQHopWd2ZXJzaW9uYzEuMG9kaWdlc3RBbGdvcml0aG1nU0hBLTI1Nmx2YWx1ZURpZ2VzdHOhcW9yZy5pc28uMTgwMTMuNS4xqAFYIKuS8FCeCcvDMwZgEezuuVv-DYsUpdypJp9abJrqHAmXAlggu7D-3vr-NrLg3zigunUzEKFqYAyG5sA-ffvmDjRxZ24DWCC2OBnhoZFhqE7s8PRfdej8t5frp-HgF_2X4qMtzvEY6ARYIBF_rl93VR21umkIdSMiWqFmT5Jxs0n3H5SWonWrJoDrBVggKDvVyMU358Le0n6TkVb2c0BbhbSMJwpswtPLNiZrTR8GWCAFZzJwAmnC7QcMQwq72FDQlmPxk0434cZbh6_rt1VagQdYIHwBHQ3-sVPtco-RcUhuYYq6iivujjYyJmQBbQ_OdhFDCFggcjT2HYgkoxnwWP-9jqO_6-D-d69H9UW2xjpDWrknlvBnZG9jVHlwZXVvcmcuaXNvLjE4MDEzLjUuMS5tRExsdmFsaWRpdHlJbmZvo2ZzaWduZWTAdDIwMjQtMDEtMTJUMDA6MTA6MDVaaXZhbGlkRnJvbcB0MjAyNC0wMS0xMlQwMDoxMDowNVpqdmFsaWRVbnRpbMB0MjAyNS0wMS0xMlQwMDoxMDowNVpYQHFzEb09NFyFlj533FE_1B9I2rku90K52ar64Id1CyOUXWXzhINeVfoJU1cfxgCT2CX1369cGd_TQxSjhVx8bpY"; - - public static ElementIdentifier GivenName => - ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("given_name")).Match( - identifier => identifier, - _ => throw new InvalidOperationException() - ); - - public static ElementIdentifier FamilyName => - ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("family_name")).Match( - identifier => identifier, - _ => throw new InvalidOperationException() - ); - - public static ElementIdentifier DrivingPrivileges => - ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("driving_privileges")).Match( - identifier => identifier, - _ => throw new InvalidOperationException() - ); - - public static CoseLabel Es256CoseLabel => - CoseLabel.ValidCoseLabel(CBORObject.FromObject(1)).Match( - label => label, - _ => throw new InvalidOperationException() - ); - - public static CoseLabel X509ChainCoseLabel => - CoseLabel.ValidCoseLabel(CBORObject.FromObject(33)).Match( - label => label, - _ => throw new InvalidOperationException() - ); - - public static ElementIdentifier ExpiryDateIdentifier => - ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("expiry_date")).Match( - identifier => identifier, - _ => throw new InvalidOperationException() - ); - - public static ElementIdentifier IssueDateIdentifier => - ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("issue_date")).Match( - identifier => identifier, - _ => throw new InvalidOperationException() - ); - - public static ElementIdentifier VehicleCategoryCodeIdentifier => - ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("vehicle_category_code")).Match( - identifier => identifier, - _ => throw new InvalidOperationException() - ); - - public static NameSpace DocTypeNameSpace => - NameSpace.ValidNameSpace(CBORObject.FromObject(MdlNameSpace)).Match( - space => space, - _ => throw new InvalidOperationException() - ); -} diff --git a/test/WalletFramework.Mdoc.Tests/Helpers.cs b/test/WalletFramework.MdocLib.Tests/Helpers.cs similarity index 87% rename from test/WalletFramework.Mdoc.Tests/Helpers.cs rename to test/WalletFramework.MdocLib.Tests/Helpers.cs index 420dee2c..cab055f7 100644 --- a/test/WalletFramework.Mdoc.Tests/Helpers.cs +++ b/test/WalletFramework.MdocLib.Tests/Helpers.cs @@ -1,6 +1,6 @@ using System.Security.Cryptography; -namespace WalletFramework.Mdoc.Tests; +namespace WalletFramework.MdocLib.Tests; public static class Helpers { diff --git a/test/WalletFramework.Mdoc.Tests/MdocTests.cs b/test/WalletFramework.MdocLib.Tests/MdocTests.cs similarity index 80% rename from test/WalletFramework.Mdoc.Tests/MdocTests.cs rename to test/WalletFramework.MdocLib.Tests/MdocTests.cs index f9f64481..96a964e2 100644 --- a/test/WalletFramework.Mdoc.Tests/MdocTests.cs +++ b/test/WalletFramework.MdocLib.Tests/MdocTests.cs @@ -1,13 +1,13 @@ using FluentAssertions; using Microsoft.IdentityModel.Tokens; using PeterO.Cbor; -using WalletFramework.Functional; -using WalletFramework.Mdoc.Common; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib.Common; using Xunit; -using static WalletFramework.Mdoc.Mdoc; -using static WalletFramework.Mdoc.Common.Constants; +using static WalletFramework.MdocLib.Mdoc; +using static WalletFramework.MdocLib.Common.Constants; -namespace WalletFramework.Mdoc.Tests; +namespace WalletFramework.MdocLib.Tests; public class MdocTests { @@ -21,17 +21,8 @@ public void Can_Encode() ValidMdoc(encodedSample).Match( mdoc => { - var encoded = mdoc.Encode(); - ValidMdoc(encoded).Match( - sut => - { - // TODO: Assertion - // var equals = sut.Equals(mdoc); - // if (!equals) - // Assert.Fail("encoded mdoc is not equals to input mdoc"); - }, - _ => Assert.Fail("Encoded mdoc could not be decoded again") - ); + var sut = mdoc.Encode(); + sut.Should().Be(Samples.EncodedMdoc); }, _ => Assert.Fail("Validation of mDoc failed") ); @@ -53,9 +44,9 @@ public void Can_Parse_Mdoc() ValidMdoc(encoded).Match(sut => { // Assert - sut.DocType.Value.Should().Be(Samples.DocType); + sut.DocType.ToString().Should().Be(Samples.DocType); - var issuerSignedItems = sut.IssuerSigned.NameSpaces[Samples.DocTypeNameSpace]; + var issuerSignedItems = sut.IssuerSigned.NameSpaces[Samples.MdlIsoNameSpace]; issuerSignedItems[0].ElementId.Value.Should().Be("issue_date"); issuerSignedItems[0].ElementValue.Value.AsT0.Value.Should().Be("2024-01-12"); @@ -87,7 +78,7 @@ public void Can_Parse_Mdoc() issuerAuth.UnprotectedHeaders[Samples.X509ChainCoseLabel].Should().BeOfType(); issuerAuth.Payload.DigestAlgorithm.Value.Should().Be(DigestAlgorithmValue.Sha256); - issuerAuth.Payload.DocType.Value.Should().Be(Samples.DocType); + issuerAuth.Payload.DocType.ToString().Should().Be(Samples.DocType); issuerAuth.Payload.Version.Should().Be(new Version("1.0")); issuerAuth.Payload.ValidityInfo.ValidFrom.Should().Be(new DateTime(2024, 01, 12, 01, 10, 05)); @@ -115,10 +106,11 @@ public void Can_Selectively_Disclose() }; // Act - sut.SelectivelyDisclose(Samples.DocTypeNameSpace, disclosures); + sut.SelectivelyDisclose(Samples.MdlIsoNameSpace, disclosures); // Assert - var items = sut.IssuerSigned.NameSpaces.Value[Samples.DocTypeNameSpace]; + var items = sut.IssuerSigned.NameSpaces.Value[Samples.MdlIsoNameSpace]; + items.Count.Should().Be(disclosures.Count); items.Should().Contain(item => item.ElementId.Value == Samples.GivenName); items.Should().Contain(item => item.ElementId.Value == Samples.FamilyName); items.Should().Contain(item => item.ElementId.Value == Samples.DrivingPrivileges); @@ -144,20 +136,11 @@ public void Can_Selectively_Disclose_And_Encode() }; // Act - mdoc.SelectivelyDisclose(Samples.DocTypeNameSpace, disclosures); - var encoded = mdoc.Encode(); + mdoc.SelectivelyDisclose(Samples.MdlIsoNameSpace, disclosures); + var sut = mdoc.Encode(); // Assert - ValidMdoc(encoded).Match( - sut => - { - // TODO: Assertion - // var equals = mdoc.Equals(sampleMdoc); - // if (!equals) - // Assert.Fail("encoded mdoc is not equals to input mdoc"); - }, - _ => Assert.Fail("Encoded mdoc could not be decoded again") - ); + sut.Should().Be(Samples.SelectivelyDisclosedEncodedMdoc); }, _ => Assert.Fail("Validation of mDoc failed") ); @@ -171,7 +154,8 @@ public void Mdoc_With_Invalid_Digests_Is_Rejected() var base64 = Base64UrlEncoder.DecodeBytes(mdoc); var cbor = CBORObject.DecodeFromBytes(base64); - var msoBytes = cbor[IssuerSignedLabel][IssuerAuthLabel][2].GetByteString(); + var msoEncodedBytes = cbor[IssuerSignedLabel][IssuerAuthLabel][2].GetByteString(); + var msoBytes = CBORObject.DecodeFromBytes(msoEncodedBytes).GetByteString(); var mso = CBORObject.DecodeFromBytes(msoBytes); var valueDigests = mso["valueDigests"][Samples.MdlNameSpace]; @@ -182,7 +166,8 @@ public void Mdoc_With_Invalid_Digests_Is_Rejected() var corruptedDigestsBytes = CBORObject.FromObject(valueDigests); mso["valueDigests"][Samples.MdlNameSpace] = corruptedDigestsBytes; var encodedMso = CBORObject.FromObject(mso.EncodeToBytes()); - cbor[IssuerSignedLabel][IssuerAuthLabel][2] = encodedMso; + var encodedIssuerAuthPayload = CBORObject.FromObject(CBORObject.FromObject(encodedMso).EncodeToBytes()); + cbor[IssuerSignedLabel][IssuerAuthLabel][2] = encodedIssuerAuthPayload; var corruptedMdocBytes = cbor.EncodeToBytes(); var corruptedMdoc = Base64UrlEncoder.Encode(corruptedMdocBytes); @@ -190,7 +175,7 @@ public void Mdoc_With_Invalid_Digests_Is_Rejected() // Act ValidMdoc(corruptedMdoc).Match( // Assert - _ => Assert.Fail("mdoc must not be valid"), + _ => Assert.Fail("mdoc must not invalid"), sut => { sut.Should().Contain(error => ((InvalidDigestError)error).Id.Value == 2); @@ -218,7 +203,7 @@ public void Mdoc_With_Invalid_Structure_Is_Rejected() // Act ValidMdoc(encoded).Match( - _ => Assert.Fail("Mdoc must not be valid"), + _ => Assert.Fail("Mdoc must invalid"), sut => { sut.Should().ContainItemsAssignableTo(); diff --git a/test/WalletFramework.MdocLib.Tests/Samples.cs b/test/WalletFramework.MdocLib.Tests/Samples.cs new file mode 100644 index 00000000..33f63832 --- /dev/null +++ b/test/WalletFramework.MdocLib.Tests/Samples.cs @@ -0,0 +1,71 @@ +using PeterO.Cbor; +using WalletFramework.Core.Functional; + +namespace WalletFramework.MdocLib.Tests; + +public static class Samples +{ + public const string DocType = "org.iso.18013.5.1.mDL"; + + public const string MdlNameSpace = "org.iso.18013.5.1"; + + public static string EncodedMdoc = + "omdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiamlzc3VlckF1dGiEQ6EBJqEYIVkBYTCCAV0wggEEoAMCAQICBgGMkdnCGTAKBggqhkjOPQQDAjA2MTQwMgYDVQQDDCtKMUZ3SlA4N0M2LVFOX1dTSU9tSkFRYzZuNUNRX2JaZGFGSjVHRG5XMVJrMB4XDTIzMTIyMjE0MDY1NloXDTI0MTAxNzE0MDY1NlowNjE0MDIGA1UEAwwrSjFGd0pQODdDNi1RTl9XU0lPbUpBUWM2bjVDUV9iWmRhRko1R0RuVzFSazBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAKKVXm6CaWEcnMNWCpJETl3wqShCpfWNWDxYTho-MU4NlDrp8U8UiFZE_eKhVSLrtYZBKBNjcXpWaN_skxVDgwwCgYIKoZIzj0EAwIDRwAwRAIgZxY0nAXLxEZXi4zAruTW8SSpDbGv4EIZF03w5m6vk94CIFWs27FXUL2YJbnPZyWEMVpc10_Yun_sIFCsdCgmG49wWQHt2BhZAeilZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGoAVggq5LwUJ4Jy8MzBmAR7O65W_4NixSl3Kkmn1psmuocCZcCWCC7sP7e-v42suDfOKC6dTMQoWpgDIbmwD59--YONHFnbgNYILY4GeGhkWGoTuzw9F916Py3l-un4eAX_Zfioy3O8RjoBFggEX-uX3dVHbW6aQh1IyJaoWZPknGzSfcflJaidasmgOsFWCAoO9XIxTfnwt7SfpORVvZzQFuFtIwnCmzC08s2JmtNHwZYIAVnMnACacLtBwxDCrvYUNCWY_GTTjfhxluHr-u3VVqBB1ggfAEdDf6xU-1yj5FxSG5hirqKK-6ONjImZAFtD852EUMIWCByNPYdiCSjGfBY_72Oo7_r4P53r0f1RbbGOkNauSeW8Gdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGx2YWxpZGl0eUluZm-jZnNpZ25lZMB0MjAyNC0wMS0xMlQwMDoxMDowNVppdmFsaWRGcm9twHQyMDI0LTAxLTEyVDAwOjEwOjA1Wmp2YWxpZFVudGlswHQyMDI1LTAxLTEyVDAwOjEwOjA1WlhAcXMRvT00XIWWPnfcUT_UH0jauS73QrnZqvrgh3ULI5RdZfOEg15V-glTVx_GAJPYJfXfr1wZ39NDFKOFXHxulmpuYW1lU3BhY2VzoXFvcmcuaXNvLjE4MDEzLjUuMYjYGFhbpGhkaWdlc3RJRAFmcmFuZG9tUHG55kyB7dP9e7fgHB5CmWxxZWxlbWVudElkZW50aWZpZXJqaXNzdWVfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMjAyNC0wMS0xMtgYWFykaGRpZ2VzdElEAmZyYW5kb21QUcL8wVSWAXNqZYXe712cE3FlbGVtZW50SWRlbnRpZmllcmtleHBpcnlfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMjAyNS0wMS0xMtgYWFqkaGRpZ2VzdElEA2ZyYW5kb21Q3LgYdsROkqsQwxAjmPR9wnFlbGVtZW50SWRlbnRpZmllcmtmYW1pbHlfbmFtZWxlbGVtZW50VmFsdWVrU2lsdmVyc3RvbmXYGFhSpGhkaWdlc3RJRARmcmFuZG9tUB7uRXveoCUB_jXjgL0riXRxZWxlbWVudElkZW50aWZpZXJqZ2l2ZW5fbmFtZWxlbGVtZW50VmFsdWVkSW5nYdgYWFukaGRpZ2VzdElEBWZyYW5kb21QyPuG9N0ftvZYxYVKGTBz9HFlbGVtZW50SWRlbnRpZmllcmpiaXJ0aF9kYXRlbGVsZW1lbnRWYWx1ZdkD7GoxOTkxLTExLTA22BhYVaRoZGlnZXN0SUQGZnJhbmRvbVAiZV6ZdFAMYYojfQcEpy3gcWVsZW1lbnRJZGVudGlmaWVyb2lzc3VpbmdfY291bnRyeWxlbGVtZW50VmFsdWViVVPYGFhbpGhkaWdlc3RJRAdmcmFuZG9tUG1s_4IFMcrUmse_xags6BBxZWxlbWVudElkZW50aWZpZXJvZG9jdW1lbnRfbnVtYmVybGVsZW1lbnRWYWx1ZWgxMjM0NTY3ONgYWKKkaGRpZ2VzdElECGZyYW5kb21QW0sDoPdZTLKbv3LQWtrqoHFlbGVtZW50SWRlbnRpZmllcnJkcml2aW5nX3ByaXZpbGVnZXNsZWxlbWVudFZhbHVlgaN1dmVoaWNsZV9jYXRlZ29yeV9jb2RlYUFqaXNzdWVfZGF0ZdkD7GoyMDIzLTAxLTAxa2V4cGlyeV9kYXRl2QPsajIwNDMtMDEtMDE"; + + public static string SelectivelyDisclosedEncodedMdoc = + "omdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiamlzc3VlckF1dGiEQ6EBJqEYIVkBYTCCAV0wggEEoAMCAQICBgGMkdnCGTAKBggqhkjOPQQDAjA2MTQwMgYDVQQDDCtKMUZ3SlA4N0M2LVFOX1dTSU9tSkFRYzZuNUNRX2JaZGFGSjVHRG5XMVJrMB4XDTIzMTIyMjE0MDY1NloXDTI0MTAxNzE0MDY1NlowNjE0MDIGA1UEAwwrSjFGd0pQODdDNi1RTl9XU0lPbUpBUWM2bjVDUV9iWmRhRko1R0RuVzFSazBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAKKVXm6CaWEcnMNWCpJETl3wqShCpfWNWDxYTho-MU4NlDrp8U8UiFZE_eKhVSLrtYZBKBNjcXpWaN_skxVDgwwCgYIKoZIzj0EAwIDRwAwRAIgZxY0nAXLxEZXi4zAruTW8SSpDbGv4EIZF03w5m6vk94CIFWs27FXUL2YJbnPZyWEMVpc10_Yun_sIFCsdCgmG49wWQHt2BhZAeilZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGoAVggq5LwUJ4Jy8MzBmAR7O65W_4NixSl3Kkmn1psmuocCZcCWCC7sP7e-v42suDfOKC6dTMQoWpgDIbmwD59--YONHFnbgNYILY4GeGhkWGoTuzw9F916Py3l-un4eAX_Zfioy3O8RjoBFggEX-uX3dVHbW6aQh1IyJaoWZPknGzSfcflJaidasmgOsFWCAoO9XIxTfnwt7SfpORVvZzQFuFtIwnCmzC08s2JmtNHwZYIAVnMnACacLtBwxDCrvYUNCWY_GTTjfhxluHr-u3VVqBB1ggfAEdDf6xU-1yj5FxSG5hirqKK-6ONjImZAFtD852EUMIWCByNPYdiCSjGfBY_72Oo7_r4P53r0f1RbbGOkNauSeW8Gdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGx2YWxpZGl0eUluZm-jZnNpZ25lZMB0MjAyNC0wMS0xMlQwMDoxMDowNVppdmFsaWRGcm9twHQyMDI0LTAxLTEyVDAwOjEwOjA1Wmp2YWxpZFVudGlswHQyMDI1LTAxLTEyVDAwOjEwOjA1WlhAcXMRvT00XIWWPnfcUT_UH0jauS73QrnZqvrgh3ULI5RdZfOEg15V-glTVx_GAJPYJfXfr1wZ39NDFKOFXHxulmpuYW1lU3BhY2VzoXFvcmcuaXNvLjE4MDEzLjUuMYPYGFhapGhkaWdlc3RJRANmcmFuZG9tUNy4GHbETpKrEMMQI5j0fcJxZWxlbWVudElkZW50aWZpZXJrZmFtaWx5X25hbWVsZWxlbWVudFZhbHVla1NpbHZlcnN0b25l2BhYUqRoZGlnZXN0SUQEZnJhbmRvbVAe7kV73qAlAf4144C9K4l0cWVsZW1lbnRJZGVudGlmaWVyamdpdmVuX25hbWVsZWxlbWVudFZhbHVlZEluZ2HYGFiipGhkaWdlc3RJRAhmcmFuZG9tUFtLA6D3WUyym79y0Fra6qBxZWxlbWVudElkZW50aWZpZXJyZHJpdmluZ19wcml2aWxlZ2VzbGVsZW1lbnRWYWx1ZYGjdXZlaGljbGVfY2F0ZWdvcnlfY29kZWFBamlzc3VlX2RhdGXZA-xqMjAyMy0wMS0wMWtleHBpcnlfZGF0ZdkD7GoyMDQzLTAxLTAx"; + + public static ElementIdentifier GivenName => + ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("given_name")).Match( + identifier => identifier, + _ => throw new InvalidOperationException() + ); + + public static ElementIdentifier FamilyName => + ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("family_name")).Match( + identifier => identifier, + _ => throw new InvalidOperationException() + ); + + public static ElementIdentifier DrivingPrivileges => + ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("driving_privileges")).Match( + identifier => identifier, + _ => throw new InvalidOperationException() + ); + + public static CoseLabel Es256CoseLabel => + CoseLabel.ValidCoseLabel(CBORObject.FromObject(1)).Match( + label => label, + _ => throw new InvalidOperationException() + ); + + public static CoseLabel X509ChainCoseLabel => + CoseLabel.ValidCoseLabel(CBORObject.FromObject(33)).Match( + label => label, + _ => throw new InvalidOperationException() + ); + + public static ElementIdentifier ExpiryDateIdentifier => + ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("expiry_date")).Match( + identifier => identifier, + _ => throw new InvalidOperationException() + ); + + public static ElementIdentifier IssueDateIdentifier => + ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("issue_date")).Match( + identifier => identifier, + _ => throw new InvalidOperationException() + ); + + public static ElementIdentifier VehicleCategoryCodeIdentifier => + ElementIdentifier.ValidElementIdentifier(CBORObject.FromObject("vehicle_category_code")).Match( + identifier => identifier, + _ => throw new InvalidOperationException() + ); + + public static NameSpace MdlIsoNameSpace => + NameSpace.ValidNameSpace(CBORObject.FromObject(MdlNameSpace)).Match( + space => space, + _ => throw new InvalidOperationException() + ); +} diff --git a/test/WalletFramework.Mdoc.Tests/WalletFramework.Mdoc.Tests.csproj b/test/WalletFramework.MdocLib.Tests/WalletFramework.MdocLib.Tests.csproj similarity index 84% rename from test/WalletFramework.Mdoc.Tests/WalletFramework.Mdoc.Tests.csproj rename to test/WalletFramework.MdocLib.Tests/WalletFramework.MdocLib.Tests.csproj index 2dd8568c..3f30eb8a 100644 --- a/test/WalletFramework.Mdoc.Tests/WalletFramework.Mdoc.Tests.csproj +++ b/test/WalletFramework.MdocLib.Tests/WalletFramework.MdocLib.Tests.csproj @@ -7,16 +7,17 @@ false true + WalletFramework.MdocLib.Tests - + - + diff --git a/test/WalletFramework.MdocVc.Tests/MdocRecordTests.cs b/test/WalletFramework.MdocVc.Tests/MdocRecordTests.cs new file mode 100644 index 00000000..32c722f0 --- /dev/null +++ b/test/WalletFramework.MdocVc.Tests/MdocRecordTests.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using Hyperledger.Aries.Storage; +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib; +using Xunit; + +namespace WalletFramework.MdocVc.Tests; + +public class MdocRecordTests +{ + [Fact] + public void Can_Encode_To_Json() + { + var encodedMdoc = MdocLib.Tests.Samples.EncodedMdoc; + var mdoc = Mdoc.ValidMdoc(encodedMdoc).UnwrapOrThrow(new InvalidOperationException("Mdoc sample is corrupt")); + var record = mdoc.ToRecord(Option>.None); + + var sut = JObject.FromObject(record); + + sut[nameof(RecordBase.Id)]!.ToString().Should().Be(record.Id); + sut[MdocRecordJsonKeys.MdocJsonKey]!.ToString().Should().Be(encodedMdoc); + } + + [Fact] + public void Can_Decode_From_Json() + { + var json = MdocVcSamples.MdocRecordJson; + + var sut = JsonConvert.DeserializeObject(json.ToString())!; + + sut.Mdoc.DocType.ToString().Should().Be(MdocLib.Tests.Samples.DocType); + } +} diff --git a/test/WalletFramework.MdocVc.Tests/MdocVcSamples.cs b/test/WalletFramework.MdocVc.Tests/MdocVcSamples.cs new file mode 100644 index 00000000..5c29c387 --- /dev/null +++ b/test/WalletFramework.MdocVc.Tests/MdocVcSamples.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json.Linq; + +namespace WalletFramework.MdocVc.Tests; + +public static class MdocVcSamples +{ + public static JObject MdocRecordJson => new() + { + ["Id"] = "5c75f511-8ebb-4f3a-9ef2-cc63eaa65cc0", + ["mdoc"] = "omdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGxpc3N1ZXJTaWduZWSiamlzc3VlckF1dGiEQ6EBJqEYIVkBYTCCAV0wggEEoAMCAQICBgGMkdnCGTAKBggqhkjOPQQDAjA2MTQwMgYDVQQDDCtKMUZ3SlA4N0M2LVFOX1dTSU9tSkFRYzZuNUNRX2JaZGFGSjVHRG5XMVJrMB4XDTIzMTIyMjE0MDY1NloXDTI0MTAxNzE0MDY1NlowNjE0MDIGA1UEAwwrSjFGd0pQODdDNi1RTl9XU0lPbUpBUWM2bjVDUV9iWmRhRko1R0RuVzFSazBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAKKVXm6CaWEcnMNWCpJETl3wqShCpfWNWDxYTho-MU4NlDrp8U8UiFZE_eKhVSLrtYZBKBNjcXpWaN_skxVDgwwCgYIKoZIzj0EAwIDRwAwRAIgZxY0nAXLxEZXi4zAruTW8SSpDbGv4EIZF03w5m6vk94CIFWs27FXUL2YJbnPZyWEMVpc10_Yun_sIFCsdCgmG49wWQHt2BhZAeilZ3ZlcnNpb25jMS4wb2RpZ2VzdEFsZ29yaXRobWdTSEEtMjU2bHZhbHVlRGlnZXN0c6Fxb3JnLmlzby4xODAxMy41LjGoAVggq5LwUJ4Jy8MzBmAR7O65W_4NixSl3Kkmn1psmuocCZcCWCC7sP7e-v42suDfOKC6dTMQoWpgDIbmwD59--YONHFnbgNYILY4GeGhkWGoTuzw9F916Py3l-un4eAX_Zfioy3O8RjoBFggEX-uX3dVHbW6aQh1IyJaoWZPknGzSfcflJaidasmgOsFWCAoO9XIxTfnwt7SfpORVvZzQFuFtIwnCmzC08s2JmtNHwZYIAVnMnACacLtBwxDCrvYUNCWY_GTTjfhxluHr-u3VVqBB1ggfAEdDf6xU-1yj5FxSG5hirqKK-6ONjImZAFtD852EUMIWCByNPYdiCSjGfBY_72Oo7_r4P53r0f1RbbGOkNauSeW8Gdkb2NUeXBldW9yZy5pc28uMTgwMTMuNS4xLm1ETGx2YWxpZGl0eUluZm-jZnNpZ25lZMB0MjAyNC0wMS0xMlQwMDoxMDowNVppdmFsaWRGcm9twHQyMDI0LTAxLTEyVDAwOjEwOjA1Wmp2YWxpZFVudGlswHQyMDI1LTAxLTEyVDAwOjEwOjA1WlhAcXMRvT00XIWWPnfcUT_UH0jauS73QrnZqvrgh3ULI5RdZfOEg15V-glTVx_GAJPYJfXfr1wZ39NDFKOFXHxulmpuYW1lU3BhY2VzoXFvcmcuaXNvLjE4MDEzLjUuMYjYGFhbpGhkaWdlc3RJRAFmcmFuZG9tUHG55kyB7dP9e7fgHB5CmWxxZWxlbWVudElkZW50aWZpZXJqaXNzdWVfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMjAyNC0wMS0xMtgYWFykaGRpZ2VzdElEAmZyYW5kb21QUcL8wVSWAXNqZYXe712cE3FlbGVtZW50SWRlbnRpZmllcmtleHBpcnlfZGF0ZWxlbGVtZW50VmFsdWXZA-xqMjAyNS0wMS0xMtgYWFqkaGRpZ2VzdElEA2ZyYW5kb21Q3LgYdsROkqsQwxAjmPR9wnFlbGVtZW50SWRlbnRpZmllcmtmYW1pbHlfbmFtZWxlbGVtZW50VmFsdWVrU2lsdmVyc3RvbmXYGFhSpGhkaWdlc3RJRARmcmFuZG9tUB7uRXveoCUB_jXjgL0riXRxZWxlbWVudElkZW50aWZpZXJqZ2l2ZW5fbmFtZWxlbGVtZW50VmFsdWVkSW5nYdgYWFukaGRpZ2VzdElEBWZyYW5kb21QyPuG9N0ftvZYxYVKGTBz9HFlbGVtZW50SWRlbnRpZmllcmpiaXJ0aF9kYXRlbGVsZW1lbnRWYWx1ZdkD7GoxOTkxLTExLTA22BhYVaRoZGlnZXN0SUQGZnJhbmRvbVAiZV6ZdFAMYYojfQcEpy3gcWVsZW1lbnRJZGVudGlmaWVyb2lzc3VpbmdfY291bnRyeWxlbGVtZW50VmFsdWViVVPYGFhbpGhkaWdlc3RJRAdmcmFuZG9tUG1s_4IFMcrUmse_xags6BBxZWxlbWVudElkZW50aWZpZXJvZG9jdW1lbnRfbnVtYmVybGVsZW1lbnRWYWx1ZWgxMjM0NTY3ONgYWKKkaGRpZ2VzdElECGZyYW5kb21QW0sDoPdZTLKbv3LQWtrqoHFlbGVtZW50SWRlbnRpZmllcnJkcml2aW5nX3ByaXZpbGVnZXNsZWxlbWVudFZhbHVlgaN1dmVoaWNsZV9jYXRlZ29yeV9jb2RlYUFqaXNzdWVfZGF0ZdkD7GoyMDIzLTAxLTAxa2V4cGlyeV9kYXRl2QPsajIwNDMtMDEtMDE" + }; +} diff --git a/test/WalletFramework.MdocVc.Tests/WalletFramework.MdocVc.Tests.csproj b/test/WalletFramework.MdocVc.Tests/WalletFramework.MdocVc.Tests.csproj new file mode 100644 index 00000000..85de78e4 --- /dev/null +++ b/test/WalletFramework.MdocVc.Tests/WalletFramework.MdocVc.Tests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + diff --git a/test/WalletFramework.Oid4Vc.Tests/Extensions/ObjectExtensions.cs b/test/WalletFramework.Oid4Vc.Tests/Extensions/ObjectExtensions.cs index e7d623ee..e9addf63 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Extensions/ObjectExtensions.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Extensions/ObjectExtensions.cs @@ -1,18 +1,15 @@ -using System; using System.Linq.Expressions; -using System.Reflection; -namespace Hyperledger.Aries.Tests.Extensions +namespace WalletFramework.Oid4Vc.Tests.Extensions; + +public static class ObjectExtensions { - public static class ObjectExtensions + public static void PrivateSet(this T member, Expression> property, TProperty value) { - public static void PrivateSet(this T member, Expression> property, TProperty value) - { - var name = ((MemberExpression)property.Body).Member.Name; + var name = ((MemberExpression)property.Body).Member.Name; - PropertyInfo propertyInfo = typeof(T).GetProperty(name); - if (propertyInfo == null) return; - propertyInfo.SetValue(member, value); - } + var propertyInfo = typeof(T).GetProperty(name); + if (propertyInfo == null) return; + propertyInfo.SetValue(member, value); } } diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs new file mode 100644 index 00000000..b5bafb9a --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs @@ -0,0 +1,78 @@ +using FluentAssertions; +using Hyperledger.Aries.Storage; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Uri; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; +using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Records; +using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.AuthFlow.Samples; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.AuthFlow; + +public class AuthFlowSessionRecordTests +{ + [Fact] + public void Can_Encode_To_Json() + { + // Arrange + var clientOptions = new ClientOptions + { + ClientId = "i can write anything", + WalletIssuer = "i can write anything", + RedirectUri = "i can write anything" + }; + + var issuerMetadata = IssuerMetadataSample.Decoded; + + var authorizationServerMetadata = new AuthorizationServerMetadata + { + Issuer = "i can write anything", + TokenEndpoint = "i can write anything", + JwksUri = "i can write anything", + AuthorizationEndpoint = "i can write anything", + ResponseTypesSupported = new[] { "i can write anything" }, + }; + + var credentialConfigurationId = CredentialConfigurationId + .ValidCredentialConfigurationId(IssuerMetadataSample.MdocConfigurationId.ToString()) + .UnwrapOrThrow(new InvalidOperationException()); + + var authorizationData = new AuthorizationData( + clientOptions, + issuerMetadata, + authorizationServerMetadata, + new List { credentialConfigurationId }); + + var authorizationCodeParameters = new AuthorizationCodeParameters("hello", "world"); + + var sessionId = VciSessionId.CreateSessionId(); + var record = new AuthFlowSessionRecord(authorizationData, authorizationCodeParameters, sessionId); + + // Act + var recordSut = JObject.FromObject(record); + var tagsSut = JObject.FromObject(record.Tags); + + // Assert + recordSut[nameof(RecordBase.Id)]!.ToString().Should().Be(record.Id); + tagsSut[nameof(AuthFlowSessionRecord.SessionId)] = record.SessionId.ToString(); + } + + [Fact] + public void Can_Decode_From_Json() + { + // Arrange + var json = AuthFlowSamples.AuthFlowSessionRecordJson; + + // Act + var record = JsonConvert.DeserializeObject(json.ToString()); + + // Assert + record.Should().NotBeNull(); + record!.Id.Should().Be(json[nameof(RecordBase.Id)]!.ToString()); + record.AuthorizationData.IssuerMetadata.CredentialIssuer.ToString().Should().Be(IssuerMetadataSample.CredentialIssuer.ToStringWithoutTrail()); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs new file mode 100644 index 00000000..2bf810b1 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json.Linq; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.AuthFlow.Samples; + +public static class AuthFlowSamples +{ + public static JObject AuthFlowSessionRecordJson => new() + { + ["AuthorizationData"] = new JObject + { + ["ClientOptions"] = new JObject + { + ["ClientId"] = "https://test-issuer.com/redirect", + ["WalletIssuer"] = "i can write anything", + ["RedirectUri"] = "https://test-issuer.com/redirect" + }, + ["IssuerMetadata"] = IssuerMetadataSample.EncodedAsJson, + ["AuthorizationServerMetadata"] = new JObject + { + ["issuer"] = "i can write anything", + ["token_endpoint"] = "i can write anything", + ["jwks_uri"] = "i can write anything", + ["authorization_endpoint"] = "i can write anything", + ["response_types_supported"] = new JArray("i can write anything"), + }, + ["CredentialConfigurationIds"] = new JArray("org.iso.18013.5.1.mDL") + }, + ["AuthorizationCodeParameters"] = new JObject + { + ["Challenge"] = "hello", + ["CodeChallengeMethod"] = "S256", + ["Verifier"] = "world" + }, + ["RecordVersion"] = 1, + ["Id"] = "598e7661-95a8-4531-b707-3d256d3c1745" + }; +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/MdocConfigurationTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/MdocConfigurationTests.cs new file mode 100644 index 00000000..30916c93 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/MdocConfigurationTests.cs @@ -0,0 +1,78 @@ +using FluentAssertions; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json.Errors; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.Mdoc; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.MdocConfiguration; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration; + +public class MdocConfigurationTests +{ + [Fact] + public void Can_Parse() + { + // Arrange + var sample = MdocConfigurationSample.Valid; + + // Act + ValidMdocConfiguration(sample).Match( + // Assert + sut => + { + sut.Format.Should().Be(MdocConfigurationSample.Format); + sut.DocType.Should().Be(MdocConfigurationSample.DocType); + sut.Policy.Match( + policy => + { + policy.OneTimeUse.Should().Be(MdocConfigurationSample.OneTimeUse); + policy.BatchSize.Match( + batchSize => batchSize.Should().Be(MdocConfigurationSample.BatchSize), + () => Assert.Fail("BatchSize must be some") + ); + }, + () => Assert.Fail("Policy must be some")); + + sut.CryptographicSuitesSupported.Match( + list => list.Should().Contain(MdocConfigurationSample.CryptoSuite), + () => Assert.Fail("CryptographicSuitesSupported must be some")); + + sut.CryptographicCurvesSupported.Match( + list => list.Should().Contain(MdocConfigurationSample.CryptoCurve), + () => Assert.Fail("CryptographicCurvesSupported must be some")); + + sut.Claims.Match( + claims => + { + var dict = claims.Value[MdocConfigurationSample.NameSpace]; + dict[MdocConfigurationSample.GivenName].Display.Match( + list => + { + list.Should().Contain(MdocConfigurationSample.EnglishDisplay); + list.Should().Contain(MdocConfigurationSample.JapaneseDisplay); + }, + () => Assert.Fail("Display must be some")); + }, + () => Assert.Fail("Claims must be some")); + }, + _ => Assert.Fail("Must be valid") + ); + } + + [Fact] + public void Configuration_With_Invalid_Structure_Is_Rejected() + { + // Arrange + var sample = MdocConfigurationSample.Valid; + + sample["format"] = ""; + sample["doctype"] = null; + + ValidMdocConfiguration(sample).Match( + _ => Assert.Fail("MdocConfiguration must be invalid"), + errors => + { + errors.Count.Should().Be(2); + errors.Should().AllBeOfType(); + }); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwtConfigurationTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwtConfigurationTests.cs new file mode 100644 index 00000000..eccf508e --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwtConfigurationTests.cs @@ -0,0 +1,10 @@ +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration; + +public class SdJwtConfigurationTests +{ + [Fact] + public void Can_Parse() + { + + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredOffer/CredentialOfferTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredOffer/CredentialOfferTests.cs new file mode 100644 index 00000000..1d630c3d --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredOffer/CredentialOfferTests.cs @@ -0,0 +1,81 @@ +using FluentAssertions; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; +using static WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models.CredentialOffer; +using static WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.CredentialOfferSample; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredOffer; + +public class CredentialOfferTests +{ + [Fact] + public void Can_Parse_From_Json() + { + var sample = PreAuth; + + ValidCredentialOffer(sample).Match( + offer => + { + offer.CredentialIssuer.Should().Be(CredentialIssuer); + + var ids = offer.CredentialConfigurationIds.Select(id => (string)id).ToList(); + + ids.Length().Should().Be(2); + ids.Should().Contain(UniversityDegreeCredential); + ids.Should().Contain(OrgIso1801351Mdl); + + offer.Grants.Match( + grants => + { + grants.AuthorizationCode.IsNone.Should().BeTrue(); + grants.PreAuthorizedCode.Match( + preAuthCode => + { + preAuthCode.ToString().Should().Be(PreAuthorizedCode); + preAuthCode.TransactionCode.Match( + transactionCode => + { + transactionCode.Length.Match( + length => length.Should().Be(Length), + () => Assert.Fail("Length must be some")); + + transactionCode.Description.Match( + description => description.Should().Be(Description), + () => Assert.Fail("Description must be some")); + + transactionCode.InputMode.Match( + mode => mode.ToString().Should().Be("numeric"), + () => Assert.Fail("InputMode must be some")); + }, + () => Assert.Fail("TransactionCode must be some")); + }, + () => Assert.Fail("PreAuthorizedCode must be some")); + }, + () => Assert.Fail("Grants must be some")); + }, + _ => Assert.Fail("Offer must be valid")); + } + + [Fact] + public void Offer_With_Invalid_Structure_Is_Rejected() + { + var sample = PreAuth; + + sample["credential_issuer"] = "this is not a valid URI"; + sample["credential_configuration_ids"] = new JArray(); + + sample["grants"]!["urn:ietf:params:oauth:grant-type:pre-authorized_code"]!["pre-authorized_code"] = null; + + ValidCredentialOffer(sample).Match( + _ => Assert.Fail("Offer with invalid structure must be invalid"), + errors => + { + errors.Should().ContainSingle(error => error is CredentialIssuerError); + errors.Should().ContainSingle(error => error is EnumerableIsEmptyError); + } + ); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs new file mode 100644 index 00000000..64b4a8e4 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs @@ -0,0 +1,84 @@ +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Json; +using WalletFramework.Core.Uri; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.Mdoc; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.SdJwt; +using static WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models.IssuerMetadata; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Issuer; + +public class IssuerMetadataTests +{ + [Fact] + public void Can_Decode_From_Json() + { + // Arrange + var sample = IssuerMetadataSample.EncodedAsJson; + + // Act + ValidIssuerMetadata(sample).Match( + // Assert + sut => + { + new Uri(sut.CredentialIssuer.ToString()).Should().Be(IssuerMetadataSample.CredentialIssuer); + new Uri(sut.CredentialEndpoint.ToString()).Should().Be(IssuerMetadataSample.CredentialEndpoint); + + var mdocConfiguration = sut + .CredentialConfigurationsSupported[IssuerMetadataSample.MdocConfigurationId] + .AsT1; + + mdocConfiguration.Format.Should().Be(MdocConfigurationSample.Format); + mdocConfiguration.DocType.Should().Be(MdocConfigurationSample.DocType); + + var sdJwtConfiguration = sut + .CredentialConfigurationsSupported[IssuerMetadataSample.SdJwtConfigurationId] + .AsT0; + + sdJwtConfiguration.Format.Should().Be(SdJwtConfigurationSample.Format); + sdJwtConfiguration.Vct.Should().Be(SdJwtConfigurationSample.Vct); + }, + _ => Assert.Fail("IssuerMetadata must be valid")); + } + + [Fact] + public void Can_Encode_To_Json() + { + var decoded = IssuerMetadataSample.Decoded; + + var sut = JObject.FromObject(decoded).RemoveNulls(); + + sut.Should().BeEquivalentTo(IssuerMetadataSample.EncodedAsJson); + } + + [Fact] + public void Can_Decode_And_Encode_From_Json() + { + // Arrange + var sample = IssuerMetadataSample.EncodedAsJson; + + // Act + ValidIssuerMetadata(sample).Match( + // Assert + sut => + { + var encoded = JObject.FromObject(sut).RemoveNulls(); + encoded.Should().BeEquivalentTo(sample); + }, + _ => Assert.Fail("IssuerMetadata must be valid")); + } + + [Fact] + public void Can_Decode_From_Persisted_Json() + { + var sample = IssuerMetadataSample.EncodedAsJson; + + var sut = JsonConvert.DeserializeObject(sample.ToString())!; + + sut.CredentialIssuer.ToString().Should().Be(IssuerMetadataSample.CredentialIssuer.ToStringWithoutTrail()); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/IssuerMetadataTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/IssuerMetadataTests.cs index 1cb8bdaa..22d08d58 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/IssuerMetadataTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/IssuerMetadataTests.cs @@ -1,54 +1,54 @@ -using FluentAssertions; -using Hyperledger.Aries.Extensions; -using Newtonsoft.Json.Linq; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; -using static WalletFramework.Oid4Vc.Tests.Samples; - - -namespace WalletFramework.Oid4Vc.Tests.Oid4Vci -{ - public class IssuerMetadataTests - { - [Fact] - public void Additional_Or_Unrecognized_Fields_Are_Ignored_During_Deserialization() - { - var json = IssuerMetadataJson; - var jObject = JObject.Parse(json); - jObject["Additional_Field"] = "Additional_Field"; - - var sut = jObject.ToObject(); - - sut.Should().BeOfType(); - sut!.CredentialIssuer.Should().Be(CredentialIssuer); - sut.CredentialEndpoint.Should().Be(CredentialEndpoint); - } - - [Theory] - [InlineData("credential_issuer")] - [InlineData("credential_endpoint")] - [InlineData("credential_configurations_supported")] - public void Deserialization_Fails_When_Required_Fields_Are_Missing(string fieldName) - { - // Arrange - var json = IssuerMetadataJson; - - var jObject = JObject.Parse(json); - - jObject[fieldName] = null; - - Assert.Throws(() => jObject.ToObject()); - } - - [Fact] - public void Valid_Json_Deserializes_To_Model() - { - var json = IssuerMetadataJson; - - var sut = json.ToObject(); - - sut.Should().BeOfType(); - sut.CredentialIssuer.Should().Be(CredentialIssuer); - sut.CredentialEndpoint.Should().Be(CredentialEndpoint); - } - } -} +// using FluentAssertions; +// using Hyperledger.Aries.Extensions; +// using Newtonsoft.Json.Linq; +// using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; +// using static WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.IssuerMetadataSample; +// +// +// namespace WalletFramework.Oid4Vc.Tests.Oid4Vci +// { +// public class IssuerMetadataTests +// { +// [Fact] +// public void Additional_Or_Unrecognized_Fields_Are_Ignored_During_Deserialization() +// { +// var json = IssuerMetadataJson; +// var jObject = JObject.Parse(json); +// jObject["Additional_Field"] = "Additional_Field"; +// +// var sut = jObject.ToObject(); +// +// sut.Should().BeOfType(); +// sut!.CredentialIssuer.Should().Be(CredentialIssuer); +// sut.CredentialEndpoint.Should().Be(CredentialEndpoint); +// } +// +// [Theory] +// [InlineData("credential_issuer")] +// [InlineData("credential_endpoint")] +// [InlineData("credential_configurations_supported")] +// public void Deserialization_Fails_When_Required_Fields_Are_Missing(string fieldName) +// { +// // Arrange +// var json = IssuerMetadataJson; +// +// var jObject = JObject.Parse(json); +// +// jObject[fieldName] = null; +// +// Assert.Throws(() => jObject.ToObject()); +// } +// +// [Fact] +// public void Valid_Json_Deserializes_To_Model() +// { +// var json = IssuerMetadataJson; +// +// var sut = json.ToObject(); +// +// sut.Should().BeOfType(); +// sut.CredentialIssuer.Should().Be(CredentialIssuer); +// sut.CredentialEndpoint.Should().Be(CredentialEndpoint); +// } +// } +// } diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Localization/LocaleTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Localization/LocaleTests.cs new file mode 100644 index 00000000..59c157f1 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Localization/LocaleTests.cs @@ -0,0 +1,75 @@ +using FluentAssertions; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Localization; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Localization.Samples; +using static System.Collections.Immutable.ImmutableDictionary; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Localization; + +public class LocaleTests +{ + [Theory] + [InlineData("en-US", "en-US")] + [InlineData("en", "en-US")] + [InlineData("de-DE", "de-DE")] + [InlineData("de", "de-DE")] + public void Can_Create_From_Valid_Input(string input, string expected) => Locale.ValidLocale(input).Match( + sut => + { + sut.ToString().Should().Be(expected); + }, + _ => Assert.Fail("Locale is invalid")); + + [Theory] + [InlineData("Peter")] + [InlineData("")] + [InlineData("english")] + public void Invalid_Input_Is_Not_Allowed(string invalidInput) => Locale.ValidLocale(invalidInput).Match( + _ => Assert.Fail("Invalid input must not be able to create locale"), + _ => { }); + + [Fact] + public void Can_Find_Matching_Locale_In_A_Dictionary() + { + var english = LocaleSample.English; + var dictionary = CreateRange(new[] + { + KeyValuePair.Create(LocaleSample.German, 1), + KeyValuePair.Create(english, 0) + }); + + var sut = dictionary.FindOrDefault(english); + + sut.Should().Be(0); + } + + [Fact] + public void Get_English_As_Fallback_When_No_Matching_Locale_Is_Found_In_A_Dictionary() + { + var dictionary = CreateRange(new[] + { + KeyValuePair.Create(LocaleSample.German, 1), + KeyValuePair.Create(LocaleSample.English, 0) + } + ); + + var sut = dictionary.FindOrDefault(LocaleSample.Japanese); + + sut.Should().Be(0); + } + + [Fact] + public void Get_First_Entry_As_Fallback_When_No_Matching_Locale_And_No_English_Is_Found_In_A_Dictionary() + { + var dictionary = CreateRange(new[] + { + KeyValuePair.Create(LocaleSample.German, 1), + KeyValuePair.Create(LocaleSample.Japanese, 2) + } + ); + + var sut = dictionary.FindOrDefault(LocaleSample.Korean); + + sut.Should().BeGreaterOrEqualTo(1); + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Localization/Samples/LocaleSample.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Localization/Samples/LocaleSample.cs new file mode 100644 index 00000000..f5e2c642 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Localization/Samples/LocaleSample.cs @@ -0,0 +1,15 @@ +using WalletFramework.Core.Functional; +using WalletFramework.Core.Localization; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Localization.Samples; + +public static class LocaleSample +{ + public static Locale English => Locale.ValidLocale("en-US").UnwrapOrThrow(new InvalidOperationException("The default locale is corrupt.")); + + public static Locale German => Locale.ValidLocale("de-DE").UnwrapOrThrow(new InvalidOperationException("German locale is corrupt.")); + + public static Locale Japanese => Locale.ValidLocale("ja-JP").UnwrapOrThrow(new InvalidOperationException("Japanese locale is corrupt.")); + + public static Locale Korean => Locale.ValidLocale("ko-KR").UnwrapOrThrow(new InvalidOperationException("Korean locale is corrupt.")); +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/CredentialOfferSample.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/CredentialOfferSample.cs new file mode 100644 index 00000000..6bdd38b2 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/CredentialOfferSample.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json.Linq; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples; + +public static class CredentialOfferSample +{ + public const string CredentialIssuer = "https://credential-issuer.example.com"; + + public const string UniversityDegreeCredential = "UniversityDegreeCredential"; + + public const string OrgIso1801351Mdl = "org.iso.18013.5.1.mDL"; + + public const string PreAuthorizedCode = "oaKazRN8I0IbtZ0C7JuMn5"; + + public const int Length = 4; + + public const string Description = "Please provide the one-time code that was sent via e-mail"; + + public const string InputMode = "numeric"; + + public static JObject PreAuth => new() + { + ["credential_issuer"] = CredentialIssuer, + ["credential_configuration_ids"] = new JArray + { + UniversityDegreeCredential, + OrgIso1801351Mdl + }, + ["grants"] = new JObject + { + ["urn:ietf:params:oauth:grant-type:pre-authorized_code"] = new JObject + { + ["pre-authorized_code"] = PreAuthorizedCode, + ["tx_code"] = new JObject + { + ["length"] = Length, + ["input_mode"] = InputMode, + ["description"] = Description + } + } + } + }; +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/IssuerMetadataSample.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/IssuerMetadataSample.cs new file mode 100644 index 00000000..6c4ab17c --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/IssuerMetadataSample.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Core.Uri; +using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; +using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.Mdoc; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.SdJwt; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples; + +public static class IssuerMetadataSample +{ + public static Uri CredentialEndpoint => new(CredentialIssuer + "/credential"); + + public static Uri CredentialIssuer => new("https://test-issuer.de"); + + public static CredentialConfigurationId MdocConfigurationId => CredentialConfigurationId + .ValidCredentialConfigurationId(MdocConfigurationSample.DocType.ToString()) + .UnwrapOrThrow(new InvalidOperationException()); + + public static CredentialConfigurationId SdJwtConfigurationId => CredentialConfigurationId + .ValidCredentialConfigurationId(SdJwtConfigurationSample.Scope.ToString()) + .UnwrapOrThrow(new InvalidOperationException()); + + public static JObject EncodedAsJson => new() + { + ["credential_issuer"] = CredentialIssuer.ToStringWithoutTrail(), + ["credential_endpoint"] = CredentialEndpoint.ToStringWithoutTrail(), + ["display"] = new JArray + { + new JObject + { + ["name"] = "Test Company GmbH", + ["logo"] = new JObject + { + { "uri", "https://test-issuer.com/logo.png" } + }, + ["locale"] = "en-US" + }, + new JObject + { + ["name"] = "Test Company GmbH", + ["logo"] = new JObject + { + { "uri", "https://test-issuer.com/logo.png" } + }, + ["locale"] = "de-DE" + } + }, + ["authorization_servers"] = new JArray { "https://test-issuer.com/authorizationserver" }, + ["credential_configurations_supported"] = new JObject + { + [SdJwtConfigurationId] = SdJwtConfigurationSample.Valid, + [MdocConfigurationId] = MdocConfigurationSample.Valid + } + }; + + public static IssuerMetadata Decoded => + IssuerMetadata.ValidIssuerMetadata(EncodedAsJson).UnwrapOrThrow(new InvalidOperationException()); +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/Mdoc/MdocConfigurationSample.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/Mdoc/MdocConfigurationSample.cs new file mode 100644 index 00000000..4036c1f7 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/Mdoc/MdocConfigurationSample.cs @@ -0,0 +1,99 @@ +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.MdocLib; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Localization.Samples; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.CryptographicCurve; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.CryptographicSuite; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.ElementDisplay; +using static WalletFramework.MdocLib.ElementIdentifier; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.ElementName; +using static WalletFramework.MdocLib.DocType; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Format; +using static WalletFramework.MdocLib.NameSpace; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.Mdoc; + +public static class MdocConfigurationSample +{ + public const bool OneTimeUse = true; + + public const uint BatchSize = 10; + + public static CryptographicCurve CryptoCurve => + ValidCryptographicCurve(1).UnwrapOrThrow(new InvalidOperationException()); + + public static CryptographicSuite CryptoSuite => + ValidCryptographicSuite("ES256").UnwrapOrThrow(new InvalidOperationException()); + + public static DocType DocType => + ValidDoctype("org.iso.18013.5.1.mDL").UnwrapOrThrow(new InvalidOperationException()); + + public static ElementDisplay EnglishDisplay => OptionalElementDisplay(EnglishDisplayJson).ToNullable() ?? + throw new InvalidOperationException(); + + public static ElementDisplay JapaneseDisplay => OptionalElementDisplay(JapaneseDisplayJson).ToNullable() ?? + throw new InvalidOperationException(); + + public static ElementIdentifier GivenName => + ValidElementIdentifier("given_name").UnwrapOrThrow(new InvalidOperationException()); + + public static Format Format => ValidFormat("mso_mdoc").UnwrapOrThrow(new InvalidOperationException()); + + public static JObject Valid => new() + { + ["format"] = Format.ToString(), + ["doctype"] = DocType.ToString(), + ["policy"] = new JObject + { + ["one_time_use"] = OneTimeUse, + ["batch_size"] = BatchSize + }, + ["cryptographic_suites_supported"] = new JArray { CryptoSuite.ToString() }, + ["cryptographic_curves_supported"] = new JArray { CryptoCurve.ToString() }, + ["claims"] = new JObject + { + [NameSpace] = new JObject + { + [GivenName] = new JObject + { + ["display"] = new JArray + { + new JObject + { + ["name"] = EnglishName.ToString(), + ["locale"] = LocaleSample.English.ToString() + }, + new JObject + { + ["name"] = JapaneseName.ToString(), + ["locale"] = LocaleSample.Japanese.ToString() + } + } + } + } + } + }; + + public static NameSpace NameSpace => + ValidNameSpace("org.iso.18013.5.1").UnwrapOrThrow(new InvalidOperationException()); + + private static ElementName EnglishName => + OptionalElementName("Given Name").ToNullable() ?? throw new InvalidOperationException(); + + private static ElementName JapaneseName => + OptionalElementName("名前").ToNullable() ?? throw new InvalidOperationException(); + + private static JObject EnglishDisplayJson => new() + { + ["name"] = EnglishName.ToString(), + ["locale"] = LocaleSample.English.ToString() + }; + + private static JObject JapaneseDisplayJson => new() + { + ["name"] = JapaneseName.ToString(), + ["locale"] = LocaleSample.Japanese.ToString() + }; +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/SdJwt/SdJwtConfigurationSample.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/SdJwt/SdJwtConfigurationSample.cs new file mode 100644 index 00000000..c73b3b20 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/SdJwt/SdJwtConfigurationSample.cs @@ -0,0 +1,95 @@ +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; +using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; +using WalletFramework.SdJwtVc.Models; + +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.SdJwt; + +public static class SdJwtConfigurationSample +{ + public static Format Format => Format + .ValidFormat("vc+sd-jwt") + .UnwrapOrThrow(new InvalidOperationException()); + + public static Vct Vct => Vct + .ValidVct("https://test-issuer.com/VerifiedEMail") + .UnwrapOrThrow(new InvalidOperationException()); + + public static Scope Scope => Scope + .OptionalScope("VerifiedEMailSdJwtVc") + .ToNullable() ?? throw new InvalidOperationException(); + + public static JObject Valid => new() + { + ["format"] = Format.ToString(), + ["scope"] = Scope.ToString(), + ["cryptographic_binding_methods_supported"] = new JArray { "jwk" }, + ["credential_signing_alg_values_supported"] = new JArray { "ES256" }, + ["display"] = new JArray + { + new JObject + { + ["name"] = "Verified e-mail adress", + ["logo"] = new JObject + { + ["uri"] = "https://test-issuer.com/credential-logo.png" + }, + ["background_color"] = "#12107c", + ["text_color"] = "#FFFFFF", + ["locale"] = "en-US" + } + }, + ["vct"] = Vct.ToString(), + ["claims"] = new JObject + { + ["given_name"] = new JObject + { + ["display"] = new JArray + { + new JObject + { + ["locale"] = "de-DE", + ["name"] = "Vorname" + }, + new JObject + { + ["locale"] = "en-US", + ["name"] = "Given name" + } + } + }, + ["family_name"] = new JObject + { + ["display"] = new JArray + { + new JObject + { + ["locale"] = "de-DE", + ["name"] = "Nachname" + }, + new JObject + { + ["locale"] = "en-US", + ["name"] = "Surname" + } + } + }, + ["email"] = new JObject + { + ["display"] = new JArray + { + new JObject + { + ["locale"] = "de-DE", + ["name"] = "E-Mail Adresse" + }, + new JObject + { + ["locale"] = "en-US", + ["name"] = "e-Mail address" + } + } + } + } + }; +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Services/Oid4VciClientServiceTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Services/Oid4VciClientServiceTests.cs deleted file mode 100644 index 225f7663..00000000 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Services/Oid4VciClientServiceTests.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System.Net; -using FluentAssertions; -using Moq; -using Moq.Protected; -using Newtonsoft.Json; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Authorization; -using WalletFramework.Oid4Vc.Oid4Vci.Models.CredentialResponse; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential.Attributes; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; -using WalletFramework.Oid4Vc.Oid4Vci.Services; -using WalletFramework.Oid4Vc.Oid4Vci.Services.Oid4VciClientService; -using WalletFramework.SdJwtVc.KeyStore.Services; - -namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Services -{ - public class Oid4VciClientServiceTests - { - private const string AuthServerMetadataWithoutDpop = - "{\"issuer\":\"https://issuer.io\",\"token_endpoint\":\"https://issuer.io/token\",\"token_endpoint_auth_methods_supported\":[\"urn:ietf:params:oauth:client-assertion-type:verifiable-presentation\"],\"response_types_supported\":[\"urn:ietf:params:oauth:grant-type:pre-authorized_code\"]}\n"; - - private const string AuthServerMetadataWithDpop = - "{\"issuer\":\"https://issuer.io\",\"token_endpoint\":\"https://issuer.io/token\",\"token_endpoint_auth_methods_supported\":[\"urn:ietf:params:oauth:client-assertion-type:verifiable-presentation\"],\"response_types_supported\":[\"urn:ietf:params:oauth:grant-type:pre-authorized_code\"],\"dpop_signing_alg_values_supported\":[\"ES256\"]}\n"; - - private const string IssuerMetadata = - "{\"credential_configurations_supported\":{\"VerifiedEmail\":{\"vct\":\"VerifiedEmail\",\"claims\":{},\"format\":\"vc+sdjwt\"}},\"credential_endpoint\":\"https://issuer.io/credential\",\"credential_issuer\":\"https://issuer.io\"}"; - - private const string PreAuthorizedCode = "1234"; - - private const string TokenResponseWithoutDpopSupport = - "{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..sHQ\",\"token_type\":\"bearer\",\"expires_in\": 86400,\"c_nonce\": \"tZignsnFbp\",\"c_nonce_expires_in\":86400}"; - - private const string TokenResponseWithDpopSupport = - "{\"access_token\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..sHQ\",\"token_type\":\"DPoP\",\"expires_in\": 86400,\"c_nonce\": \"tZignsnFbp\",\"c_nonce_expires_in\":86400}"; - - private const string DPopBadRequestTokenResponse = "{\"error\":\"use_dpop_nonce\"}"; - - private const string Vct = "VerifiedEmail"; - - private const string DPopNonce = "someRadnomNonceStringFromIssuer"; - - private const string CredentialResponse = "{\"format\":\"vc+sd-jwt\",\"credential\":\"eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOlsiT0dfT2lCMk5ZS0JzTVhIOFVVb2luREhUT1h5VER1Z3JPdE94RFI2NF9ZcyIsIlQzbHRYQUFtODNJTXRUYkRTb1J2d1g2Tk10em1scV9ZWG9Vd1EwZDY0NEUiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuaW8vIiwiaWF0IjoxNTE2MjM5MDIyLCJ0eXBlIjoiVmVyaWZpZWRFbWFpbCIsImV4cCI6MTUxNjI0NzAyMiwiX3NkX2FsZyI6InNoYS0yNTYiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiVENBRVIxOVp2dTNPSEY0ajRXNHZmU1ZvSElQMUlMaWxEbHM3dkNlR2VtYyIsInkiOiJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19LCJhbGciOiJFUzI1NiJ9.OVSoCqHZLgAPaYK27gJx6J1ejwskP62xIHryqc1ZJYOR8yZdicSF4KXBk5qgocWZdiqEsri5Q3sW69xIfbmXSA~WyJseVMxN1ZzenNGb3doaFBnY3VuOTFRIiwgImV4cCIsIDE1NDE0OTQ3MjRd~WyJaRmNwSWxTNlJ5eWV2U3JTeFdJbDZRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJVSHVVVUNlOWZzNUdody1mZ0JJWi13IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJ3ZnR5YkpzYktzVWJDay1XaWpaQ3RRIiwgImVtYWlsIiwgInRlc3RAZXhhbXBsZS5jb20iXQ\"}"; - - private const string KeyBindingJwtKeyId = "someKbJwtKeyId"; - - private const string DPopJwtKeyId = "someDpopJwtKeyId"; - - private const string KbJwtMock = "someKeyBindingJwtMock"; - - private const string TransactionCode = "someTransactionCode"; - - private readonly HttpResponseMessage _credentialResponse = new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(CredentialResponse) - }; - - private readonly HttpResponseMessage _tokenResponseWithoutDpopSupport = new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(TokenResponseWithoutDpopSupport) - }; - - private readonly HttpResponseMessage _tokenResponseWithDpopSupport = new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(TokenResponseWithDpopSupport) - }; - - private readonly HttpResponseMessage _dPopBadRequestTokenResponse = new HttpResponseMessage() - { - StatusCode = HttpStatusCode.BadRequest, - Content = new StringContent(DPopBadRequestTokenResponse), - Headers = {{"DPoP-Nonce", DPopNonce}}, - }; - - private readonly Mock _httpMessageHandlerMock = new Mock(); - private readonly Mock _httpClientFactoryMock = new Mock(); - private readonly Mock _authorizationRecordService = new Mock(); - private readonly Mock _keyStoreMock = new Mock(); - private Oid4VciClientService _oid4VciClientService; - - private readonly OidIssuerMetadata _issuerMetadata = new( - credentialConfigurationsSupported: new Dictionary() - { - { - "VerifiedEmail", new OidCredentialMetadata - { - Format = "vc+sdjwt", - Vct = Vct, - Claims = new Dictionary() - } - } - }, - display: null, - credentialIssuer: "https://issuer.io", - credentialEndpoint: "https://issuer.io/credential", - authorizationServer: null - ); - - private readonly AuthorizationServerMetadata _authorizationServerMetadataWithDpop = - new AuthorizationServerMetadata() - { - Issuer = "https://issuer.io", - TokenEndpoint = "https://issuer.io/token", - TokenEndpointAuthMethodsSupported = new string[] {"urn:ietf:params:oauth:client-assertion-type:verifiable-presentation"}, - ResponseTypesSupported = new string[] {"urn:ietf:params:oauth:grant-type:pre-authorized_code"}, - DPopSigningAlgValuesSupported = new string[] {"ES256"} - }; - - private readonly AuthorizationServerMetadata _authorizationServerMetadataWithoutDpop = - new AuthorizationServerMetadata() - { - Issuer = "https://issuer.io", - TokenEndpoint = "https://issuer.io/token", - TokenEndpointAuthMethodsSupported = new string[] {"urn:ietf:params:oauth:client-assertion-type:verifiable-presentation"}, - ResponseTypesSupported = new string[] {"urn:ietf:params:oauth:grant-type:pre-authorized_code"} - }; - - //TODO: Add tests for Authorization Code Flow when Storage is replaced (indy-sdk) - - [Fact] - public async Task CanRequestCredentialWithoutDPopInPreAuthFlowAsync() - { - //Arrange - SetupKeyStoreGenerateKeySequence(KeyBindingJwtKeyId); - _keyStoreMock.Setup(j => - j.GenerateKbProofOfPossessionAsync(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(),It.IsAny(), It.IsAny())) - .ReturnsAsync(KbJwtMock); - - SetupHttpClientSequence(_tokenResponseWithoutDpopSupport, _credentialResponse); - - var expectedCredentialResponse = JsonConvert.DeserializeObject(CredentialResponse); - - var metadataSet = new MetadataSet(_issuerMetadata, _authorizationServerMetadataWithoutDpop); - - // Act - var actualCredentialResponse = - await _oid4VciClientService.RequestCredentialAsync( - metadataSet, - _issuerMetadata.CredentialConfigurationsSupported.First().Value, - PreAuthorizedCode, - TransactionCode - ); - - //Assert - actualCredentialResponse[0].Item1.Should().BeEquivalentTo(expectedCredentialResponse); - actualCredentialResponse[0].Item2.Should().BeEquivalentTo(KeyBindingJwtKeyId); - } - - [Fact] - public async Task CanRequestCredentialWithDPoPInPreAuthFlowAsync() - { - //Arrange - const string dPopJwtMock = "someDPopJwtMock"; - - SetupKeyStoreGenerateKeySequence(DPopJwtKeyId, KeyBindingJwtKeyId); - _keyStoreMock.Setup(j => - j.GenerateKbProofOfPossessionAsync(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(),It.IsAny(), It.IsAny())) - .ReturnsAsync(KbJwtMock); - _keyStoreMock.Setup(j => - j.GenerateDPopProofOfPossessionAsync(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny())) - .ReturnsAsync(dPopJwtMock); - - SetupHttpClientSequence( - _dPopBadRequestTokenResponse, - _tokenResponseWithDpopSupport, - _credentialResponse); - - var expectedCredentialResponse = JsonConvert.DeserializeObject(CredentialResponse); - - var metadataSet = new MetadataSet(_issuerMetadata, _authorizationServerMetadataWithDpop); - - //Act - var actualCredentialResponse = - await _oid4VciClientService.RequestCredentialAsync( - metadataSet, - _issuerMetadata.CredentialConfigurationsSupported.First().Value, - PreAuthorizedCode, - TransactionCode - ); - - //Assert - actualCredentialResponse[0].Item1.Should().BeEquivalentTo(expectedCredentialResponse); - actualCredentialResponse[0].Item2.Should().BeEquivalentTo(KeyBindingJwtKeyId); - } - - private void SetupHttpClientSequence(params HttpResponseMessage[] responses) - { - var responseQueue = new Queue(responses); - - _httpMessageHandlerMock.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), - ItExpr.IsAny()) - .ReturnsAsync(() => responseQueue.Dequeue()); - - var httpClient = new HttpClient(_httpMessageHandlerMock.Object); - _httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())).Returns(httpClient); - - _oid4VciClientService = - new Oid4VciClientService(_httpClientFactoryMock.Object, _authorizationRecordService.Object, _keyStoreMock.Object); - } - - private void SetupKeyStoreGenerateKeySequence(params string[] responses) - { - var responseQueue = new Queue(responses); - - _keyStoreMock.Setup(j => j.GenerateKey(It.IsAny())) - .ReturnsAsync(() => responseQueue.Dequeue()); - } - } -} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Services/Oid4VpClientServiceTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Services/Oid4VpClientServiceTests.cs index 169da62a..b85b7549 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Services/Oid4VpClientServiceTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Services/Oid4VpClientServiceTests.cs @@ -1,188 +1,165 @@ using System.Net; +using FluentAssertions; using Hyperledger.Aries.Storage; using Hyperledger.TestHarness.Mock; using Microsoft.Extensions.Logging; using Moq; using Moq.Protected; using SD_JWT.Roles.Implementation; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Credential.Attributes; -using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; +using WalletFramework.Core.Cryptography.Abstractions; +using WalletFramework.Core.Cryptography.Models; +using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Services; using WalletFramework.Oid4Vc.Oid4Vp.Services; -using WalletFramework.SdJwtVc.KeyStore.Services; +using WalletFramework.SdJwtVc.Models.Records; using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; -namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Services +namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Services; + +public class Oid4VpClientServiceTests : IAsyncLifetime { - public class Oid4VpClientServiceTests : IAsyncLifetime - { - private const string AuthRequestWithRequestUri = - "haip://?client_id=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Fcallback&request_uri=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Frequestobject%2F4ba20ad0cb08545830aa549ab4305c03"; + private const string AuthRequestWithRequestUri = + "haip://?client_id=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Fcallback&request_uri=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Frequestobject%2F4ba20ad0cb08545830aa549ab4305c03"; - private const string CombinedIssuance = - "eyJ4NWMiOlsiTUlJQ09qQ0NBZUdnQXdJQkFnSUJBekFLQmdncWhrak9QUVFEQWpCak1Rc3dDUVlEVlFRR0V3SkVSVEVQTUEwR0ExVUVCd3dHUW1WeWJHbHVNUjB3R3dZRFZRUUtEQlJDZFc1a1pYTmtjblZqYTJWeVpXa2dSMjFpU0RFS01BZ0dBMVVFQ3d3QlNURVlNQllHQTFVRUF3d1BTVVIxYm1sdmJpQlVaWE4wSUVOQk1CNFhEVEl6TURjeE9ERXlOVE16TlZvWERUSTRNRGN4TmpFeU5UTXpOVm93V1RFTE1Ba0dBMVVFQmhNQ1JFVXhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SOHdIUVlEVlFRRERCWldaWEpwWm1sbFpDQkZMVTFoYVd3Z1NYTnpkV1Z5TUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFOGoxOEsyZTRjZGRkdjRzaGRFUE84Z251MTJnM242RDFtRC9KU09TcEdDZlc5YUdoaU92bHpPck5icGRzTGVlWjVtclV3SXpra3BrMUhPVnZwSTNwVXFPQmp6Q0JqREFkQmdOVkhRNEVGZ1FVTFo4eWFCbDJJUVJWeCtrTGY4d3ZmRFpIY1pRd0RBWURWUjBUQVFIL0JBSXdBREFPQmdOVkhROEJBZjhFQkFNQ0I0QXdMQVlEVlIwUkJDVXdJNEloYVhOemRXVnlMVzl3Wlc1cFpEUjJZeTV6YzJrdWRHbHlMbUoxWkhKMUxtUmxNQjhHQTFVZEl3UVlNQmFBRkUrVzZ6N2FqVHVtZXgrWWNGYm9OclZlQzJ0Uk1Bb0dDQ3FHU000OUJBTUNBMGNBTUVRQ0lDZU5KYi85OENkV3RPdEtrREs0bm1WSGV4N0ZJclJQMlBRY3lmOVIzUGdPQWlCUHNkeENsakZXcTdxUGFOdUthUzhnTjRqZEkyVXUrNlNKaWZLZGp6SDdsQT09IiwiTUlJQ0xUQ0NBZFNnQXdJQkFnSVVNWVVIaEdEOWhVL2MwRW82bVc4cmpqZUordDB3Q2dZSUtvWkl6ajBFQXdJd1l6RUxNQWtHQTFVRUJoTUNSRVV4RHpBTkJnTlZCQWNNQmtKbGNteHBiakVkTUJzR0ExVUVDZ3dVUW5WdVpHVnpaSEoxWTJ0bGNtVnBJRWR0WWtneENqQUlCZ05WQkFzTUFVa3hHREFXQmdOVkJBTU1EMGxFZFc1cGIyNGdWR1Z6ZENCRFFUQWVGdzB5TXpBM01UTXdPVEkxTWpoYUZ3MHpNekEzTVRBd09USTFNamhhTUdNeEN6QUpCZ05WQkFZVEFrUkZNUTh3RFFZRFZRUUhEQVpDWlhKc2FXNHhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SZ3dGZ1lEVlFRRERBOUpSSFZ1YVc5dUlGUmxjM1FnUTBFd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFTRUh6OFlqckZ5VE5IR0x2TzE0RUF4bTl5aDhiS09na1V6WVdjQzFjdnJKbjVKZ0hZSE14WmJOTU8xM0VoMEVyMjczOFFRT2dlUm9aTUlUYW9ka2ZOU28yWXdaREFkQmdOVkhRNEVGZ1FVVDViclB0cU5PNlo3SDVod1Z1ZzJ0VjRMYTFFd0h3WURWUjBqQkJnd0ZvQVVUNWJyUHRxTk82WjdINWh3VnVnMnRWNExhMUV3RWdZRFZSMFRBUUgvQkFnd0JnRUIvd0lCQURBT0JnTlZIUThCQWY4RUJBTUNBWVl3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnWTBEZXJkQ3h0NHpHUFluOHlOckR4SVdDSkhwenE0QmRqZHNWTjJvMUdSVUNJQjBLQTdiRzFGVkIxSWlLOGQ1N1FBTCtQRzlYNWxkS0c3RWtvQW1oV1ZLZSJdLCJraWQiOiJNR3d3WjZSbE1HTXhDekFKQmdOVkJBWVRBa1JGTVE4d0RRWURWUVFIREFaQ1pYSnNhVzR4SFRBYkJnTlZCQW9NRkVKMWJtUmxjMlJ5ZFdOclpYSmxhU0JIYldKSU1Rb3dDQVlEVlFRTERBRkpNUmd3RmdZRFZRUUREQTlKUkhWdWFXOXVJRlJsYzNRZ1EwRUNBUU09IiwidHlwIjoidmMrc2Qtand0IiwiYWxnIjoiRVMyNTYifQ.eyJfc2QiOlsibmJkd2Z0NE9QdmcycFFlaVFYOHdoc2hnZ0VVTTdBY29mdHRWUE95ejJpdyIsIkxoQjF2dE9WM1ZHd3V6QmhKWnhoUUd5OUNSY0l0dC1QSmkydDRvRk83X28iLCJZNEl2Uk4yY2VDU2V6aXZKRjREMHFDc0JQNW81eUZVdDJiXy1YRkFXTGZjIiwieU9kNkRJbGFDUERXTG9xLUJfY2JQWTY4dFZmV18wU25NRGQzeU5qRDIxRSJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLW9wZW5pZDR2Yy5zc2kudGlyLmJ1ZHJ1LmRlIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjN1ZVRuN3VYLTZpSmxJYllOU2xrM0NSa3pwVDZGYldNMkxjdDZhdy1HZEEiLCJ5IjoiWlFlZnFJVnlzOG1MT05PZXBSclNsOWhXVGhGai1HTUVWR0pGUVk1TXQ2TSJ9fSwidHlwZSI6IlZlcmlmaWVkRU1haWwiLCJleHAiOjE2OTcyODIwOTgsImlhdCI6MTY5NjQxODA5OH0.Sbj1LaWpz45iqsdS8NFaLFgZ7G5hj1ofYLlO4rTI-jHELD6ORMGe1LVHe7IiOr_DNCDDde0ScGIEZKRiNCHEfA~WyJZTVVoZzh3Q2RHUGJjV245NW9MbUtBIiwiZW1haWwiLCJqb3RlbWVrMzYxQGZlc2dyaWQuY29tIl0"; + private const string CombinedIssuance = + "eyJ4NWMiOlsiTUlJQ09qQ0NBZUdnQXdJQkFnSUJBekFLQmdncWhrak9QUVFEQWpCak1Rc3dDUVlEVlFRR0V3SkVSVEVQTUEwR0ExVUVCd3dHUW1WeWJHbHVNUjB3R3dZRFZRUUtEQlJDZFc1a1pYTmtjblZqYTJWeVpXa2dSMjFpU0RFS01BZ0dBMVVFQ3d3QlNURVlNQllHQTFVRUF3d1BTVVIxYm1sdmJpQlVaWE4wSUVOQk1CNFhEVEl6TURjeE9ERXlOVE16TlZvWERUSTRNRGN4TmpFeU5UTXpOVm93V1RFTE1Ba0dBMVVFQmhNQ1JFVXhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SOHdIUVlEVlFRRERCWldaWEpwWm1sbFpDQkZMVTFoYVd3Z1NYTnpkV1Z5TUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFOGoxOEsyZTRjZGRkdjRzaGRFUE84Z251MTJnM242RDFtRC9KU09TcEdDZlc5YUdoaU92bHpPck5icGRzTGVlWjVtclV3SXpra3BrMUhPVnZwSTNwVXFPQmp6Q0JqREFkQmdOVkhRNEVGZ1FVTFo4eWFCbDJJUVJWeCtrTGY4d3ZmRFpIY1pRd0RBWURWUjBUQVFIL0JBSXdBREFPQmdOVkhROEJBZjhFQkFNQ0I0QXdMQVlEVlIwUkJDVXdJNEloYVhOemRXVnlMVzl3Wlc1cFpEUjJZeTV6YzJrdWRHbHlMbUoxWkhKMUxtUmxNQjhHQTFVZEl3UVlNQmFBRkUrVzZ6N2FqVHVtZXgrWWNGYm9OclZlQzJ0Uk1Bb0dDQ3FHU000OUJBTUNBMGNBTUVRQ0lDZU5KYi85OENkV3RPdEtrREs0bm1WSGV4N0ZJclJQMlBRY3lmOVIzUGdPQWlCUHNkeENsakZXcTdxUGFOdUthUzhnTjRqZEkyVXUrNlNKaWZLZGp6SDdsQT09IiwiTUlJQ0xUQ0NBZFNnQXdJQkFnSVVNWVVIaEdEOWhVL2MwRW82bVc4cmpqZUordDB3Q2dZSUtvWkl6ajBFQXdJd1l6RUxNQWtHQTFVRUJoTUNSRVV4RHpBTkJnTlZCQWNNQmtKbGNteHBiakVkTUJzR0ExVUVDZ3dVUW5WdVpHVnpaSEoxWTJ0bGNtVnBJRWR0WWtneENqQUlCZ05WQkFzTUFVa3hHREFXQmdOVkJBTU1EMGxFZFc1cGIyNGdWR1Z6ZENCRFFUQWVGdzB5TXpBM01UTXdPVEkxTWpoYUZ3MHpNekEzTVRBd09USTFNamhhTUdNeEN6QUpCZ05WQkFZVEFrUkZNUTh3RFFZRFZRUUhEQVpDWlhKc2FXNHhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SZ3dGZ1lEVlFRRERBOUpSSFZ1YVc5dUlGUmxjM1FnUTBFd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFTRUh6OFlqckZ5VE5IR0x2TzE0RUF4bTl5aDhiS09na1V6WVdjQzFjdnJKbjVKZ0hZSE14WmJOTU8xM0VoMEVyMjczOFFRT2dlUm9aTUlUYW9ka2ZOU28yWXdaREFkQmdOVkhRNEVGZ1FVVDViclB0cU5PNlo3SDVod1Z1ZzJ0VjRMYTFFd0h3WURWUjBqQkJnd0ZvQVVUNWJyUHRxTk82WjdINWh3VnVnMnRWNExhMUV3RWdZRFZSMFRBUUgvQkFnd0JnRUIvd0lCQURBT0JnTlZIUThCQWY4RUJBTUNBWVl3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnWTBEZXJkQ3h0NHpHUFluOHlOckR4SVdDSkhwenE0QmRqZHNWTjJvMUdSVUNJQjBLQTdiRzFGVkIxSWlLOGQ1N1FBTCtQRzlYNWxkS0c3RWtvQW1oV1ZLZSJdLCJraWQiOiJNR3d3WjZSbE1HTXhDekFKQmdOVkJBWVRBa1JGTVE4d0RRWURWUVFIREFaQ1pYSnNhVzR4SFRBYkJnTlZCQW9NRkVKMWJtUmxjMlJ5ZFdOclpYSmxhU0JIYldKSU1Rb3dDQVlEVlFRTERBRkpNUmd3RmdZRFZRUUREQTlKUkhWdWFXOXVJRlJsYzNRZ1EwRUNBUU09IiwidHlwIjoidmMrc2Qtand0IiwiYWxnIjoiRVMyNTYifQ.eyJfc2QiOlsibmJkd2Z0NE9QdmcycFFlaVFYOHdoc2hnZ0VVTTdBY29mdHRWUE95ejJpdyIsIkxoQjF2dE9WM1ZHd3V6QmhKWnhoUUd5OUNSY0l0dC1QSmkydDRvRk83X28iLCJZNEl2Uk4yY2VDU2V6aXZKRjREMHFDc0JQNW81eUZVdDJiXy1YRkFXTGZjIiwieU9kNkRJbGFDUERXTG9xLUJfY2JQWTY4dFZmV18wU25NRGQzeU5qRDIxRSJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLW9wZW5pZDR2Yy5zc2kudGlyLmJ1ZHJ1LmRlIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjN1ZVRuN3VYLTZpSmxJYllOU2xrM0NSa3pwVDZGYldNMkxjdDZhdy1HZEEiLCJ5IjoiWlFlZnFJVnlzOG1MT05PZXBSclNsOWhXVGhGai1HTUVWR0pGUVk1TXQ2TSJ9fSwidHlwZSI6IlZlcmlmaWVkRU1haWwiLCJleHAiOjE2OTcyODIwOTgsImlhdCI6MTY5NjQxODA5OH0.Sbj1LaWpz45iqsdS8NFaLFgZ7G5hj1ofYLlO4rTI-jHELD6ORMGe1LVHe7IiOr_DNCDDde0ScGIEZKRiNCHEfA~WyJZTVVoZzh3Q2RHUGJjV245NW9MbUtBIiwiZW1haWwiLCJqb3RlbWVrMzYxQGZlc2dyaWQuY29tIl0"; - private const string ExpectedRedirectUrl = - "https://client.example.org/cb#response_code=091535f699ea575c7937fa5f0f454aee"; + private const string ExpectedRedirectUrl = + "https://client.example.org/cb#response_code=091535f699ea575c7937fa5f0f454aee"; - private const string KeyBindingJwtMock = - "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiK2p3dCJ9.eyJhdWQiOiJodHRwczovL3ZlcmlmaWVyLnNzaS50aXIuYnVkcnUuZGUvcHJlc2VudGF0aW9uL2F1dGhvcml6YXRpb24tcmVzcG9uc2UiLCJub25jZSI6IkxIc1lGRnlpMnNXQzZIM3ZSWVFsNFQiLCJpYXQiOjE2OTY0MjcwNzR9.Kxj1e7ucZeAnFnfOjo05QnW-DYeEprciDqkOhe6fhXIWprEYd1NJ6a0gpZJ66oTJsv49ExvDOKTLOzt6R75gcg"; + private const string KeyBindingJwtMock = + "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiK2p3dCJ9.eyJhdWQiOiJodHRwczovL3ZlcmlmaWVyLnNzaS50aXIuYnVkcnUuZGUvcHJlc2VudGF0aW9uL2F1dGhvcml6YXRpb24tcmVzcG9uc2UiLCJub25jZSI6IkxIc1lGRnlpMnNXQzZIM3ZSWVFsNFQiLCJpYXQiOjE2OTY0MjcwNzR9.Kxj1e7ucZeAnFnfOjo05QnW-DYeEprciDqkOhe6fhXIWprEYd1NJ6a0gpZJ66oTJsv49ExvDOKTLOzt6R75gcg"; - private const string KeyId = "KeyId"; + private const string KeyId = "KeyId"; - private const string RequestUriResponse = - "eyJ4NWMiOlsiTUlJQ0x6Q0NBZFdnQXdJQkFnSUJCREFLQmdncWhrak9QUVFEQWpCak1Rc3dDUVlEVlFRR0V3SkVSVEVQTUEwR0ExVUVCd3dHUW1WeWJHbHVNUjB3R3dZRFZRUUtEQlJDZFc1a1pYTmtjblZqYTJWeVpXa2dSMjFpU0RFS01BZ0dBMVVFQ3d3QlNURVlNQllHQTFVRUF3d1BTVVIxYm1sdmJpQlVaWE4wSUVOQk1CNFhEVEl6TURnd016QTROREkwTkZvWERUSTRNRGd3TVRBNE5ESTBORm93VlRFTE1Ba0dBMVVFQmhNQ1JFVXhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1Sc3dHUVlEVlFRRERCSlBjR1Z1U1dRMFZsQWdWbVZ5YVdacFpYSXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUnNoUzVDaVBrSzVXRUN1RHpybmN0SXBwYm1nc1lkOURzT1lEcElFeFpFczFmUWNOeXZrQjVFZU5Xc2MwU0ExUU5xd3dHVzRndUZLZzBJZjFKR0R4VWZvNEdITUlHRU1CMEdBMVVkRGdRV0JCUmZMQVBzeG1Mc3AxblEvRk12RkkzN0MzQmxZREFNQmdOVkhSTUJBZjhFQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBa0JnTlZIUkVFSFRBYmdobDJaWEpwWm1sbGNpNXpjMmt1ZEdseUxtSjFaSEoxTG1SbE1COEdBMVVkSXdRWU1CYUFGRStXNno3YWpUdW1leCtZY0Zib05yVmVDMnRSTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUNWZURUMnNkZHhySEMrZ0ZJTUVmc3huc0lXRmdIdnZlZnBuWXZrb0RjbHdBaUVBMlFnRVRHV3hIWUVObWxsNDA2VUNwYnFRb1kzMzJPbE9qdDUwWjc2WHBtQT0iLCJNSUlDTFRDQ0FkU2dBd0lCQWdJVU1ZVUhoR0Q5aFUvYzBFbzZtVzhyamplSit0MHdDZ1lJS29aSXpqMEVBd0l3WXpFTE1Ba0dBMVVFQmhNQ1JFVXhEekFOQmdOVkJBY01Ca0psY214cGJqRWRNQnNHQTFVRUNnd1VRblZ1WkdWelpISjFZMnRsY21WcElFZHRZa2d4Q2pBSUJnTlZCQXNNQVVreEdEQVdCZ05WQkFNTUQwbEVkVzVwYjI0Z1ZHVnpkQ0JEUVRBZUZ3MHlNekEzTVRNd09USTFNamhhRncwek16QTNNVEF3T1RJMU1qaGFNR014Q3pBSkJnTlZCQVlUQWtSRk1ROHdEUVlEVlFRSERBWkNaWEpzYVc0eEhUQWJCZ05WQkFvTUZFSjFibVJsYzJSeWRXTnJaWEpsYVNCSGJXSklNUW93Q0FZRFZRUUxEQUZKTVJnd0ZnWURWUVFEREE5SlJIVnVhVzl1SUZSbGMzUWdRMEV3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVNFSHo4WWpyRnlUTkhHTHZPMTRFQXhtOXloOGJLT2drVXpZV2NDMWN2ckpuNUpnSFlITXhaYk5NTzEzRWgwRXIyNzM4UVFPZ2VSb1pNSVRhb2RrZk5TbzJZd1pEQWRCZ05WSFE0RUZnUVVUNWJyUHRxTk82WjdINWh3VnVnMnRWNExhMUV3SHdZRFZSMGpCQmd3Rm9BVVQ1YnJQdHFOTzZaN0g1aHdWdWcydFY0TGExRXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBREFPQmdOVkhROEJBZjhFQkFNQ0FZWXdDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWdZMERlcmRDeHQ0ekdQWW44eU5yRHhJV0NKSHB6cTRCZGpkc1ZOMm8xR1JVQ0lCMEtBN2JHMUZWQjFJaUs4ZDU3UUFMK1BHOVg1bGRLRzdFa29BbWhXVktlIl0sImtpZCI6Ik1Hd3daNlJsTUdNeEN6QUpCZ05WQkFZVEFrUkZNUTh3RFFZRFZRUUhEQVpDWlhKc2FXNHhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SZ3dGZ1lEVlFRRERBOUpSSFZ1YVc5dUlGUmxjM1FnUTBFQ0FRUT0iLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IjE1ZDQwNjU0LWM2NTgtNDkzOC1hYzA3LWVjYjQxYzlhZmIxMCIsImlucHV0X2Rlc2NyaXB0b3JzIjpbeyJpZCI6IjUwYjZlNGYzLTYyMmEtNDk3NC1iMzMwLTVlNzIwZWM5MjJiZiIsImZvcm1hdCI6eyJ2YytzZC1qd3QiOnsicHJvb2ZfdHlwZSI6WyJKc29uV2ViU2lnbmF0dXJlMjAyMCJdfX0sImNvbnN0cmFpbnRzIjp7ImxpbWl0X2Rpc2Nsb3N1cmUiOiJyZXF1aXJlZCIsImZpZWxkcyI6W3sicGF0aCI6WyIkLnR5cGUiXSwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJjb25zdCI6IlZlcmlmaWVkRU1haWwifX0seyJwYXRoIjpbIiQuZW1haWwiXX1dfX1dfSwicmVzcG9uc2VfdXJpIjoiaHR0cHM6Ly92ZXJpZmllci5zc2kudGlyLmJ1ZHJ1LmRlL3ByZXNlbnRhdGlvbi9hdXRob3JpemF0aW9uLXJlc3BvbnNlIiwibm9uY2UiOiJZZjg4dGRlZzhZTTkyM3E0aFFBRzlPIiwiY2xpZW50X2lkIjoiaHR0cHM6Ly92ZXJpZmllci5zc2kudGlyLmJ1ZHJ1LmRlL3ByZXNlbnRhdGlvbi9hdXRob3JpemF0aW9uLXJlc3BvbnNlIiwicmVzcG9uc2VfbW9kZSI6ImRpcmVjdF9wb3N0In0.sdeLcG6Ta4ozfbDuHBr2Vq-Ro2WpdUIhJWy3BgazyvrgkQw27uTFGioPWXNCruK5H5E5nvHS420u5tv0671tjg"; + private const string RequestUriResponse = + "eyJ4NWMiOlsiTUlJQ0x6Q0NBZFdnQXdJQkFnSUJCREFLQmdncWhrak9QUVFEQWpCak1Rc3dDUVlEVlFRR0V3SkVSVEVQTUEwR0ExVUVCd3dHUW1WeWJHbHVNUjB3R3dZRFZRUUtEQlJDZFc1a1pYTmtjblZqYTJWeVpXa2dSMjFpU0RFS01BZ0dBMVVFQ3d3QlNURVlNQllHQTFVRUF3d1BTVVIxYm1sdmJpQlVaWE4wSUVOQk1CNFhEVEl6TURnd016QTROREkwTkZvWERUSTRNRGd3TVRBNE5ESTBORm93VlRFTE1Ba0dBMVVFQmhNQ1JFVXhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1Sc3dHUVlEVlFRRERCSlBjR1Z1U1dRMFZsQWdWbVZ5YVdacFpYSXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUnNoUzVDaVBrSzVXRUN1RHpybmN0SXBwYm1nc1lkOURzT1lEcElFeFpFczFmUWNOeXZrQjVFZU5Xc2MwU0ExUU5xd3dHVzRndUZLZzBJZjFKR0R4VWZvNEdITUlHRU1CMEdBMVVkRGdRV0JCUmZMQVBzeG1Mc3AxblEvRk12RkkzN0MzQmxZREFNQmdOVkhSTUJBZjhFQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBa0JnTlZIUkVFSFRBYmdobDJaWEpwWm1sbGNpNXpjMmt1ZEdseUxtSjFaSEoxTG1SbE1COEdBMVVkSXdRWU1CYUFGRStXNno3YWpUdW1leCtZY0Zib05yVmVDMnRSTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUNWZURUMnNkZHhySEMrZ0ZJTUVmc3huc0lXRmdIdnZlZnBuWXZrb0RjbHdBaUVBMlFnRVRHV3hIWUVObWxsNDA2VUNwYnFRb1kzMzJPbE9qdDUwWjc2WHBtQT0iLCJNSUlDTFRDQ0FkU2dBd0lCQWdJVU1ZVUhoR0Q5aFUvYzBFbzZtVzhyamplSit0MHdDZ1lJS29aSXpqMEVBd0l3WXpFTE1Ba0dBMVVFQmhNQ1JFVXhEekFOQmdOVkJBY01Ca0psY214cGJqRWRNQnNHQTFVRUNnd1VRblZ1WkdWelpISjFZMnRsY21WcElFZHRZa2d4Q2pBSUJnTlZCQXNNQVVreEdEQVdCZ05WQkFNTUQwbEVkVzVwYjI0Z1ZHVnpkQ0JEUVRBZUZ3MHlNekEzTVRNd09USTFNamhhRncwek16QTNNVEF3T1RJMU1qaGFNR014Q3pBSkJnTlZCQVlUQWtSRk1ROHdEUVlEVlFRSERBWkNaWEpzYVc0eEhUQWJCZ05WQkFvTUZFSjFibVJsYzJSeWRXTnJaWEpsYVNCSGJXSklNUW93Q0FZRFZRUUxEQUZKTVJnd0ZnWURWUVFEREE5SlJIVnVhVzl1SUZSbGMzUWdRMEV3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVNFSHo4WWpyRnlUTkhHTHZPMTRFQXhtOXloOGJLT2drVXpZV2NDMWN2ckpuNUpnSFlITXhaYk5NTzEzRWgwRXIyNzM4UVFPZ2VSb1pNSVRhb2RrZk5TbzJZd1pEQWRCZ05WSFE0RUZnUVVUNWJyUHRxTk82WjdINWh3VnVnMnRWNExhMUV3SHdZRFZSMGpCQmd3Rm9BVVQ1YnJQdHFOTzZaN0g1aHdWdWcydFY0TGExRXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBREFPQmdOVkhROEJBZjhFQkFNQ0FZWXdDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWdZMERlcmRDeHQ0ekdQWW44eU5yRHhJV0NKSHB6cTRCZGpkc1ZOMm8xR1JVQ0lCMEtBN2JHMUZWQjFJaUs4ZDU3UUFMK1BHOVg1bGRLRzdFa29BbWhXVktlIl0sImtpZCI6Ik1Hd3daNlJsTUdNeEN6QUpCZ05WQkFZVEFrUkZNUTh3RFFZRFZRUUhEQVpDWlhKc2FXNHhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SZ3dGZ1lEVlFRRERBOUpSSFZ1YVc5dUlGUmxjM1FnUTBFQ0FRUT0iLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IjE1ZDQwNjU0LWM2NTgtNDkzOC1hYzA3LWVjYjQxYzlhZmIxMCIsImlucHV0X2Rlc2NyaXB0b3JzIjpbeyJpZCI6IjUwYjZlNGYzLTYyMmEtNDk3NC1iMzMwLTVlNzIwZWM5MjJiZiIsImZvcm1hdCI6eyJ2YytzZC1qd3QiOnsicHJvb2ZfdHlwZSI6WyJKc29uV2ViU2lnbmF0dXJlMjAyMCJdfX0sImNvbnN0cmFpbnRzIjp7ImxpbWl0X2Rpc2Nsb3N1cmUiOiJyZXF1aXJlZCIsImZpZWxkcyI6W3sicGF0aCI6WyIkLnR5cGUiXSwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJjb25zdCI6IlZlcmlmaWVkRU1haWwifX0seyJwYXRoIjpbIiQuZW1haWwiXX1dfX1dfSwicmVzcG9uc2VfdXJpIjoiaHR0cHM6Ly92ZXJpZmllci5zc2kudGlyLmJ1ZHJ1LmRlL3ByZXNlbnRhdGlvbi9hdXRob3JpemF0aW9uLXJlc3BvbnNlIiwibm9uY2UiOiJZZjg4dGRlZzhZTTkyM3E0aFFBRzlPIiwiY2xpZW50X2lkIjoiaHR0cHM6Ly92ZXJpZmllci5zc2kudGlyLmJ1ZHJ1LmRlL3ByZXNlbnRhdGlvbi9hdXRob3JpemF0aW9uLXJlc3BvbnNlIiwicmVzcG9uc2VfbW9kZSI6ImRpcmVjdF9wb3N0In0.sdeLcG6Ta4ozfbDuHBr2Vq-Ro2WpdUIhJWy3BgazyvrgkQw27uTFGioPWXNCruK5H5E5nvHS420u5tv0671tjg"; - private const string Vct = "VerifiedEmail"; + private const string Vct = "VerifiedEmail"; - public Oid4VpClientServiceTests() - { - var holder = new Holder(); - var walletRecordService = new DefaultWalletRecordService(); - var pexService = new PexService(); - _sdJwtVcHolderService = new DefaultSdJwtVcHolderService(holder, _keyStoreMock.Object, walletRecordService); - _oid4VpHaipClient = new Oid4VpHaipClient(_httpClientFactoryMock.Object, pexService); - _oid4VpRecordService = new Oid4VpRecordService(walletRecordService); - - _oid4VpClientService = new Oid4VpClientService( - _httpClientFactoryMock.Object, - _sdJwtVcHolderService, - pexService, - _oid4VpHaipClient, - _loggerMock.Object, - _oid4VpRecordService - ); + public Oid4VpClientServiceTests() + { + var holder = new Holder(); + var walletRecordService = new DefaultWalletRecordService(); + var pexService = new PexService(); + + _sdJwtVcHolderService = new SdJwtVcHolderService(holder, _keyStoreMock.Object, walletRecordService); + _oid4VpHaipClient = new Oid4VpHaipClient(_httpClientFactoryMock.Object, pexService); + _oid4VpRecordService = new Oid4VpRecordService(walletRecordService); + + _oid4VpClientService = new Oid4VpClientService( + _httpClientFactoryMock.Object, + _sdJwtVcHolderService, + pexService, + _oid4VpHaipClient, + _loggerMock.Object, + _oid4VpRecordService + ); - _keyStoreMock.Setup(keyStore => - keyStore.GenerateKbProofOfPossessionAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny() - ) + _keyStoreMock.Setup(keyStore => + keyStore.GenerateKbProofOfPossessionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() ) - .ReturnsAsync(KeyBindingJwtMock); - } - - private readonly DefaultSdJwtVcHolderService _sdJwtVcHolderService; - - private readonly Mock _httpMessageHandlerMock = new Mock(); - private readonly Mock _httpClientFactoryMock = new Mock(); - private readonly Mock> _loggerMock = new Mock>(); - private readonly Mock _keyStoreMock = new Mock(); - private MockAgent? _agent1; - private readonly MockAgentRouter _router = new MockAgentRouter(); - - private readonly Oid4VpClientService _oid4VpClientService; - private readonly Oid4VpHaipClient _oid4VpHaipClient; - private readonly Oid4VpRecordService _oid4VpRecordService; - - private readonly OidIssuerMetadata _oidIssuerMetadata = new( - credentialConfigurationsSupported: new Dictionary() - { - { - "VerifiedEmail", new OidCredentialMetadata - { - Format = "vc+sdjwt", - Vct = Vct, - Claims = new Dictionary() - } - } - }, - display: null, - credentialEndpoint: "https://issuer.io/credential", - credentialIssuer: "https://issuer.io", - authorizationServer: null - ); + ) + .ReturnsAsync(KeyBindingJwtMock); + } - private readonly WalletConfiguration _config1 = new WalletConfiguration() { Id = Guid.NewGuid().ToString() }; - private readonly WalletCredentials _cred = new WalletCredentials() { Key = "2" }; - - // Todo: Fix this test - // [Fact] - // public async Task CanExecuteOpenId4VpFlow() - // { - // //Arrange - // SetupHttpClient(RequestUriResponse); - // - // await _sdJwtVcHolderService.StoreAsync( - // _agent1.Context, - // CombinedIssuance, - // KeyId, - // _oidIssuerMetadata, - // "VerifiedEmail" - // ); - // - // //Act - // var (authorizationRequest, credentials) = - // await _oid4VpClientService.ProcessAuthorizationRequestAsync( - // _agent1.Context, - // new Uri(AuthRequestWithRequestUri) - // ); - // - // var selectedCandidates = new SelectedCredential - // { - // InputDescriptorId = credentials.First().InputDescriptorId, - // Credential = credentials.First().Credentials.First() - // }; - // - // SetupHttpClient( - // "{'redirect_uri':'https://client.example.org/cb#response_code=091535f699ea575c7937fa5f0f454aee'}" - // ); - // - // var response = await _oid4VpClientService.SendAuthorizationResponseAsync( - // _agent1.Context, - // authorizationRequest, - // new[] { selectedCandidates } - // ); - // - // // Assert - // credentials.Length.Should().Be(1); - // - // response.Should().BeEquivalentTo(new Uri(ExpectedRedirectUrl)); - // - // (await _oid4VpRecordService.ListAsync(_agent1.Context)).Count.Should().Be(1); - // } - - public async Task DisposeAsync() - { - await _agent1.Dispose(); - } + private readonly SdJwtVcHolderService _sdJwtVcHolderService; + + private readonly Mock _httpMessageHandlerMock = new(); + private readonly Mock _httpClientFactoryMock = new(); + private readonly Mock> _loggerMock = new(); + private readonly Mock _keyStoreMock = new(); + private readonly MockAgentRouter _router = new(); + private MockAgent? _agent1; + + private readonly Oid4VpClientService _oid4VpClientService; + private readonly Oid4VpHaipClient _oid4VpHaipClient; + private readonly Oid4VpRecordService _oid4VpRecordService; + private readonly WalletConfiguration _config1 = new() { Id = Guid.NewGuid().ToString() }; + private readonly WalletCredentials _cred = new() { Key = "2" }; + + [Fact] + public async Task CanExecuteOpenId4VpFlow() + { + //Arrange + SetupHttpClient(RequestUriResponse); - public async Task InitializeAsync() + var sdJwt = new SdJwtRecord(); + + await _sdJwtVcHolderService.SaveAsync(_agent1.Context, sdJwt); + + //Act + var (authorizationRequest, credentials) = + await _oid4VpClientService.ProcessAuthorizationRequestAsync( + _agent1.Context, + new Uri(AuthRequestWithRequestUri) + ); + + var selectedCandidates = new SelectedCredential { - _agent1 = - await MockUtils.CreateAsync( - "agent1", - _config1, - _cred, - new MockAgentHttpHandler( - cb => _router.RouteMessage(cb.name, cb.data) - ) - ); + InputDescriptorId = credentials.First().InputDescriptorId, + Credential = credentials.First().Credentials.First() + }; + + SetupHttpClient( + "{'redirect_uri':'https://client.example.org/cb#response_code=091535f699ea575c7937fa5f0f454aee'}" + ); + + var response = await _oid4VpClientService.SendAuthorizationResponseAsync( + _agent1.Context, + authorizationRequest, + new[] { selectedCandidates } + ); + + // Assert + credentials.Length.Should().Be(1); + + response.Should().BeEquivalentTo(new Uri(ExpectedRedirectUrl)); - _router.RegisterAgent(_agent1); - } + (await _oid4VpRecordService.ListAsync(_agent1.Context)).Count.Should().Be(1); + } + + public async Task DisposeAsync() + { + await _agent1.Dispose(); + } - private void SetupHttpClient(string response) + public async Task InitializeAsync() + { + _agent1 = + await MockUtils.CreateAsync( + "agent1", + _config1, + _cred, + new MockAgentHttpHandler( + cb => _router.RouteMessage(cb.name, cb.data) + ) + ); + + _router.RegisterAgent(_agent1); + } + + private void SetupHttpClient(string response) + { + var httpResponseMessage = new HttpResponseMessage { - var httpResponseMessage = new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(response) - }; - - _httpMessageHandlerMock.Protected() - .Setup>("SendAsync", ItExpr.IsAny(), - ItExpr.IsAny()) - .ReturnsAsync(() => httpResponseMessage); - - var httpClient = new HttpClient(_httpMessageHandlerMock.Object); - _httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())).Returns(httpClient); - } + StatusCode = HttpStatusCode.OK, + Content = new StringContent(response) + }; + + _httpMessageHandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(() => httpResponseMessage); + + var httpClient = new HttpClient(_httpMessageHandlerMock.Object); + _httpClientFactoryMock.Setup(f => f.CreateClient(It.IsAny())).Returns(httpClient); } } diff --git a/test/WalletFramework.Oid4Vc.Tests/PresentationExchange/Services/PexServiceTests.cs b/test/WalletFramework.Oid4Vc.Tests/PresentationExchange/Services/PexServiceTests.cs index 4a99affe..66978c9b 100644 --- a/test/WalletFramework.Oid4Vc.Tests/PresentationExchange/Services/PexServiceTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/PresentationExchange/Services/PexServiceTests.cs @@ -1,13 +1,14 @@ using FluentAssertions; using Hyperledger.Aries.Storage.Models.Interfaces; -using Hyperledger.Aries.Tests.Extensions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SD_JWT.Roles.Implementation; +using WalletFramework.Core.Cryptography.Models; using WalletFramework.Oid4Vc.Oid4Vp.Exceptions; using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Services; +using WalletFramework.Oid4Vc.Tests.Extensions; using WalletFramework.Oid4Vc.Tests.PresentationExchange.Models; using WalletFramework.SdJwtVc.Models.Credential; using WalletFramework.SdJwtVc.Models.Credential.Attributes; @@ -17,7 +18,7 @@ namespace WalletFramework.Oid4Vc.Tests.PresentationExchange.Services { public class PexServiceTests { - private readonly PexService _pexService = new PexService(); + private readonly PexService _pexService = new(); [Fact] public async Task Can_Create_Presentation_Submission() @@ -58,6 +59,7 @@ public async Task Can_Create_Presentation_Submission() [Fact] public async Task Can_Get_Credential_Candidates_For_Input_Descriptors() { + // Arrange var driverLicenseCredential = CreateCredential(CredentialExamples.DriverCredential); var driverLicenseCredentialClone = CreateCredential(CredentialExamples.DriverCredential); var universityCredential = CreateCredential(CredentialExamples.UniversityCredential); @@ -84,17 +86,19 @@ public async Task Can_Get_Credential_Candidates_For_Input_Descriptors() var expected = new List { - new CredentialCandidates( - driverLicenseInputDescriptor.Id, - new List { driverLicenseCredential, driverLicenseCredentialClone }), - new CredentialCandidates( - universityInputDescriptor.Id, new List { universityCredential }) + new(driverLicenseInputDescriptor.Id, + new List + { + driverLicenseCredential, + driverLicenseCredentialClone + }), + new(universityInputDescriptor.Id, new List { universityCredential }) }; - var sdJwtVcHolderService = CreatePexService(); + var pexService = CreatePexService(); // Act - var credentialCandidatesArray = await sdJwtVcHolderService.FindCredentialCandidates( + var credentialCandidatesArray = await pexService.FindCredentialCandidates( new[] { driverLicenseCredential, driverLicenseCredentialClone, universityCredential @@ -267,13 +271,18 @@ private static IPexService CreatePexService() private static SdJwtRecord CreateCredential(JObject payload) { + // Arrange const string jwk = "{\"kty\":\"EC\",\"d\":\"1_2Dagk1gvTIOX-DLPe7GHNsGLJMc7biySNA-so7TXE\",\"use\":\"sig\",\"crv\":\"P-256\",\"x\":\"X6sZhH_kFp_oKYiPXW-LvUyAv9mHp1xYcpAK3yy0wGY\",\"y\":\"p0URU7tgWbh42miznti0NVKM36fpJBbIfnF8ZCYGryE\",\"alg\":\"ES256\"}"; - var issuedSdJwt = new Issuer().IssueCredential(payload, jwk); + var keyId = KeyId.CreateKeyId(); + + var record = new SdJwtRecord( + issuedSdJwt.IssuanceFormat, + new Dictionary(), + new List(), + new Dictionary(), + keyId); - var record = new SdJwtRecord(issuedSdJwt.IssuanceFormat, new Dictionary(), - new List(), new Dictionary(), "0"); - return record; } diff --git a/test/WalletFramework.Oid4Vc.Tests/Samples.cs b/test/WalletFramework.Oid4Vc.Tests/Samples.cs deleted file mode 100644 index 2e3351ec..00000000 --- a/test/WalletFramework.Oid4Vc.Tests/Samples.cs +++ /dev/null @@ -1,166 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace WalletFramework.Oid4Vc.Tests -{ - public static class Samples - { - public const string CredentialEndpoint = CredentialIssuer + "/credential"; - public const string CredentialIssuer = "https://test-issuer.de"; - - public static string AuthorizationRequestJson => - new JObject - { - ["response_uri"] = "https://lissi-test-verifier.de/authorization-response", - ["client_id_scheme"] = "x509_san_dns", - ["response_type"] = "vp_token", - ["presentation_definition"] = new JObject - { - ["id"] = "e325164b-5699-4deb-b3ee-e7b2d75e5034", - ["input_descriptors"] = new JArray - { - new JObject - { - ["id"] = "64863101-5049-407f-97e6-f6eb2deed16d", - ["format"] = new JObject - { - ["vc+sd-jwt"] = new JObject() - }, - ["constraints"] = new JObject - { - ["fields"] = new JArray - { - new JObject - { - ["path"] = new JArray { "$.vct" }, - ["filter"] = new JObject - { - ["type"] = "string", - ["const"] = "https://lissi-test.de/VerifiedEMail" - } - }, - new JObject - { - ["path"] = new JArray { "$.email" } - } - }, - ["limit_disclosure"] = "required" - } - } - } - }, - ["state"] = "e325164b-5699-4deb-b3ee-e7b2d75e5034", - ["nonce"] = "kkxicbKxPSViBh97jqSB6r", - ["client_id"] = "lissi-test-verifier.de", - ["response_mode"] = "direct_post" - } - .ToString(); - - public static string IssuerMetadataJson => - new JObject - { - ["credential_issuer"] = CredentialIssuer, - ["credential_endpoint"] = CredentialEndpoint, - ["display"] = new JArray - { - new JObject - { - ["name"] = "Test Company GmbH", - ["logo"] = new JObject - { - {"uri", "https://test-issuer.com/logo.png"} - }, - ["locale"] = "en-US" - }, - new JObject - { - ["name"] = "Test Company GmbH", - ["logo"] = new JObject - { - {"uri", "https://test-issuer.com/logo.png"} - }, - ["locale"] = "de-DE" - } - }, - ["credential_configurations_supported"] = new JObject - { - ["VerifiedEMailSdJwtVc"] = new JObject - { - ["format"] = "vc+sd-jwt", - ["scope"] = "VerifiedEMailSdJwtVc", - ["cryptographic_binding_methods_supported"] = new JArray { "jwk" }, - ["cryptographic_suites_supported"] = new JArray { "ES256" }, - ["display"] = new JArray - { - new JObject - { - ["name"] = "Verified e-mail adress", - ["logo"] = new JObject - { - ["url"] = "https:/test-issuer.com/credential-logo.png" - }, - ["background_color"] = "#12107c", - ["text_color"] = "#FFFFFF", - ["locale"] = "en-US" - } - }, - ["credential_definition"] = new JObject - { - ["vct"] = "https://test-issuer.com/VerifiedEMail", - ["claims"] = new JObject - { - ["given_name"] = new JObject - { - ["display"] = new JArray - { - new JObject - { - ["locale"] = "de-DE", - ["name"] = "Vorname" - }, - new JObject - { - ["locale"] = "en-US", - ["name"] = "Given name" - } - } - }, - ["family_name"] = new JObject - { - ["display"] = new JArray - { - new JObject - { - ["locale"] = "de-DE", - ["name"] = "Nachname" - }, - new JObject - { - ["locale"] = "en-US", - ["name"] = "Surname" - } - } - }, - ["email"] = new JObject - { - ["display"] = new JArray - { - new JObject - { - ["locale"] = "de-DE", - ["name"] = "E-Mail Adresse" - }, - new JObject - { - ["locale"] = "en-US", - ["name"] = "e-Mail address" - } - } - } - } - } - } - } - } - .ToString(); - } -} diff --git a/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj b/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj index 522cbd71..a0c376ea 100644 --- a/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj +++ b/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj @@ -10,7 +10,7 @@ - + @@ -37,4 +37,9 @@ + + + + + diff --git a/test/WalletFramework.SdJwtVc.Tests/SdJwtRecordTests.cs b/test/WalletFramework.SdJwtVc.Tests/SdJwtRecordTests.cs index c7bc6392..0c0dbe43 100644 --- a/test/WalletFramework.SdJwtVc.Tests/SdJwtRecordTests.cs +++ b/test/WalletFramework.SdJwtVc.Tests/SdJwtRecordTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using WalletFramework.Core.Cryptography.Models; using WalletFramework.SdJwtVc.Models.Credential; using WalletFramework.SdJwtVc.Models.Credential.Attributes; using WalletFramework.SdJwtVc.Models.Records; @@ -10,8 +11,15 @@ public class SdJwtRecordTests [Fact] public void CanCreateSdJwtRecord() { - string encodedSdJwt = "eyJraWQiOiJiZmFmYjkzMy1iNzQ4LTQ3ODYtODc1Ny0zYzg0ZWFlNmUzZGUiLCJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiUVBEUFFCbEEzdk9QaU9qR0lRRXBOc1l5S2Zjd2M1T3dDUlV5eWY2QTlRbyIsIk9LMWJpZXUwR0RIZWVRc2lzRkxOcUdmX0Z4eW5HT0dTNHl5Q2dZeFVhTkEiLCJUSkJ4ajBGSmdTQlUxMzVDSDRacFJieTRfVG4tNWR4TFJBX0paRnNscXhjIiwiaFBjV0phVkRJdDlDZ1E3bWxzNmFSVFR6bHZ0NmlMYzlUWFRJZ2VuZDFWayIsIkNhZm9TdzRiMWdsV196ckdyN3lodFFyQ3RIYW51NG15MVBxTGtXQkx5aFkiXSwibmJmIjoxNzA2NTQyNjgxLCJ2Y3QiOiJJRC1DYXJkIiwiX3NkX2FsZyI6InNoYS0yNTYiLCJpc3MiOiJodHRwczovL2U4MGMtMjE3LTExMS0xMDgtMTc0Lm5ncm9rLWZyZWUuYXBwIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Img2VUtiVXQ1SW4yTzVwUzUxYXRWaERuTDl0SGR4S3lkMTZXTG94R2dFQzQiLCJ5IjoiMDdIX05RcmlxRmxSb0JjVk5ZVW5aS2wwQ1A0U0NiN3RxU0NWWFNDTWh0ayJ9fSwiZXhwIjoxNzM4MjUxNDgxLCJpYXQiOjE3MTY5OTA0MDAsInN0YXR1cyI6eyJpZHgiOjYsInVyaSI6Imh0dHBzOi8vZTgwYy0yMTctMTExLTEwOC0xNzQubmdyb2stZnJlZS5hcHAvc3RhdHVzLWxpc3RzP3JlZ2lzdHJ5SWQ9YmQ1MDllMzYtNTQzNy00Zjg4LTkzYTUtNDEzNDA3ZjZiZDhmIn19.-3GEPOjEn4bopEGyy8ho_kFSfQVmkkZiFKMebtiZE6EsyRnunJtA46M_SwHQjmSm-73zIeRX7L7Rpszm8dkFhQ~WyJfSU1WWFVtc052bm9YTDR3NVRPSFpnIiwiYWRkcmVzcyIseyJzdHJlZXRfYWRkcmVzcyI6IjQyIE1hcmtldCBTdHJlZXQiLCJwb3N0YWxfY29kZSI6IjEyMzQ1In1d~WyJ3RzkzbExRRFBDUVgxTUtCYW5mVkVRIiwibGFzdF9uYW1lIiwiRG9lIl0~WyJFa1h0a0JHZXd2dkthRXlzTWhyVGJnIiwibmF0aW9uYWxpdGllcyIsWyJCcml0aXNoIiwiQmV0ZWxnZXVzaWFuIl1d~WyJzQlh2dVQxRHhaN0NrMTdJUXQzWWd3IiwiZmlyc3RfbmFtZSIsIkpvaG4iXQ~WyJoRWphWTA2WmFsNUZTS0pXSm9kUjZnIiwiZGVncmVlcyIsW3sidW5pdmVyc2l0eSI6IlVuaXZlcnNpdHkgb2YgQmV0ZWxnZXVzZSIsInR5cGUiOiJCYWNoZWxvciBvZiBTY2llbmNlIn0seyJ1bml2ZXJzaXR5IjoiVW5pdmVyc2l0eSBvZiBCZXRlbGdldXNlIiwidHlwZSI6Ik1hc3RlciBvZiBTY2llbmNlIn1dXQ~"; - var record = new SdJwtRecord(encodedSdJwt, new Dictionary(), new List(), new Dictionary(), "0"); + const string encodedSdJwt = "eyJraWQiOiJiZmFmYjkzMy1iNzQ4LTQ3ODYtODc1Ny0zYzg0ZWFlNmUzZGUiLCJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiUVBEUFFCbEEzdk9QaU9qR0lRRXBOc1l5S2Zjd2M1T3dDUlV5eWY2QTlRbyIsIk9LMWJpZXUwR0RIZWVRc2lzRkxOcUdmX0Z4eW5HT0dTNHl5Q2dZeFVhTkEiLCJUSkJ4ajBGSmdTQlUxMzVDSDRacFJieTRfVG4tNWR4TFJBX0paRnNscXhjIiwiaFBjV0phVkRJdDlDZ1E3bWxzNmFSVFR6bHZ0NmlMYzlUWFRJZ2VuZDFWayIsIkNhZm9TdzRiMWdsV196ckdyN3lodFFyQ3RIYW51NG15MVBxTGtXQkx5aFkiXSwibmJmIjoxNzA2NTQyNjgxLCJ2Y3QiOiJJRC1DYXJkIiwiX3NkX2FsZyI6InNoYS0yNTYiLCJpc3MiOiJodHRwczovL2U4MGMtMjE3LTExMS0xMDgtMTc0Lm5ncm9rLWZyZWUuYXBwIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Img2VUtiVXQ1SW4yTzVwUzUxYXRWaERuTDl0SGR4S3lkMTZXTG94R2dFQzQiLCJ5IjoiMDdIX05RcmlxRmxSb0JjVk5ZVW5aS2wwQ1A0U0NiN3RxU0NWWFNDTWh0ayJ9fSwiZXhwIjoxNzM4MjUxNDgxLCJpYXQiOjE3MTY5OTA0MDAsInN0YXR1cyI6eyJpZHgiOjYsInVyaSI6Imh0dHBzOi8vZTgwYy0yMTctMTExLTEwOC0xNzQubmdyb2stZnJlZS5hcHAvc3RhdHVzLWxpc3RzP3JlZ2lzdHJ5SWQ9YmQ1MDllMzYtNTQzNy00Zjg4LTkzYTUtNDEzNDA3ZjZiZDhmIn19.-3GEPOjEn4bopEGyy8ho_kFSfQVmkkZiFKMebtiZE6EsyRnunJtA46M_SwHQjmSm-73zIeRX7L7Rpszm8dkFhQ~WyJfSU1WWFVtc052bm9YTDR3NVRPSFpnIiwiYWRkcmVzcyIseyJzdHJlZXRfYWRkcmVzcyI6IjQyIE1hcmtldCBTdHJlZXQiLCJwb3N0YWxfY29kZSI6IjEyMzQ1In1d~WyJ3RzkzbExRRFBDUVgxTUtCYW5mVkVRIiwibGFzdF9uYW1lIiwiRG9lIl0~WyJFa1h0a0JHZXd2dkthRXlzTWhyVGJnIiwibmF0aW9uYWxpdGllcyIsWyJCcml0aXNoIiwiQmV0ZWxnZXVzaWFuIl1d~WyJzQlh2dVQxRHhaN0NrMTdJUXQzWWd3IiwiZmlyc3RfbmFtZSIsIkpvaG4iXQ~WyJoRWphWTA2WmFsNUZTS0pXSm9kUjZnIiwiZGVncmVlcyIsW3sidW5pdmVyc2l0eSI6IlVuaXZlcnNpdHkgb2YgQmV0ZWxnZXVzZSIsInR5cGUiOiJCYWNoZWxvciBvZiBTY2llbmNlIn0seyJ1bml2ZXJzaXR5IjoiVW5pdmVyc2l0eSBvZiBCZXRlbGdldXNlIiwidHlwZSI6Ik1hc3RlciBvZiBTY2llbmNlIn1dXQ~"; + var keyId = KeyId.CreateKeyId(); + + var record = new SdJwtRecord( + encodedSdJwt, + new Dictionary(), + new List(), + new Dictionary(), + keyId); record.Claims.Count.Should().Be(10); } diff --git a/test/WalletFramework.SdJwtVc.Tests/SdJwtVcHolderServiceTests.cs b/test/WalletFramework.SdJwtVc.Tests/SdJwtVcHolderServiceTests.cs index 62ab1340..abf3b7e2 100644 --- a/test/WalletFramework.SdJwtVc.Tests/SdJwtVcHolderServiceTests.cs +++ b/test/WalletFramework.SdJwtVc.Tests/SdJwtVcHolderServiceTests.cs @@ -3,7 +3,8 @@ using SD_JWT.Models; using SD_JWT.Roles; using SD_JWT.Roles.Implementation; -using WalletFramework.SdJwtVc.KeyStore.Services; +using WalletFramework.Core.Cryptography.Abstractions; +using WalletFramework.Core.Cryptography.Models; using WalletFramework.SdJwtVc.Models.Credential; using WalletFramework.SdJwtVc.Models.Credential.Attributes; using WalletFramework.SdJwtVc.Models.Records; @@ -13,7 +14,7 @@ namespace WalletFramework.SdJwtVc.Tests; public class SdJwtVcHolderServiceTests { - private readonly DefaultSdJwtVcHolderService _service; + private readonly SdJwtVcHolderService _service; public SdJwtVcHolderServiceTests() { @@ -21,7 +22,7 @@ public SdJwtVcHolderServiceTests() IHolder holder = new Holder(); IKeyStore keyStoreMock = new Mock().Object; IWalletRecordService walletRecordServiceMock = new Mock().Object; - _service = new DefaultSdJwtVcHolderService(holder, keyStoreMock, walletRecordServiceMock); + _service = new SdJwtVcHolderService(holder, keyStoreMock, walletRecordServiceMock); } // https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-08.html#appendix-A.3-4 @@ -30,7 +31,8 @@ public async Task Can_Create_Presentation_For_Example_4A() { const string issuedSdJwt = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogInZjK3NkLWp3dCJ9.eyJfc2QiOiBbIjBIWm1uU0lQejMzN2tTV2U3QzM0bC0tODhnekppLWVCSjJWel9ISndBVGciLCAiOVpicGxDN1RkRVc3cWFsNkJCWmxNdHFKZG1lRU9pWGV2ZEpsb1hWSmRSUSIsICJJMDBmY0ZVb0RYQ3VjcDV5eTJ1anFQc3NEVkdhV05pVWxpTnpfYXdEMGdjIiwgIklFQllTSkdOaFhJbHJRbzU4eWtYbTJaeDN5bGw5WmxUdFRvUG8xN1FRaVkiLCAiTGFpNklVNmQ3R1FhZ1hSN0F2R1RyblhnU2xkM3o4RUlnX2Z2M2ZPWjFXZyIsICJodkRYaHdtR2NKUXNCQ0EyT3RqdUxBY3dBTXBEc2FVMG5rb3ZjS09xV05FIiwgImlrdXVyOFE0azhxM1ZjeUE3ZEMtbU5qWkJrUmVEVFUtQ0c0bmlURTdPVFUiLCAicXZ6TkxqMnZoOW80U0VYT2ZNaVlEdXZUeWtkc1dDTmcwd1RkbHIwQUVJTSIsICJ3elcxNWJoQ2t2a3N4VnZ1SjhSRjN4aThpNjRsbjFqb183NkJDMm9hMXVnIiwgInpPZUJYaHh2SVM0WnptUWNMbHhLdUVBT0dHQnlqT3FhMXoySW9WeF9ZRFEiXSwgImlzcyI6ICJodHRwczovL2lzc3Vlci5leGFtcGxlLmNvbSIsICJpYXQiOiAxNjgzMDAwMDAwLCAiZXhwIjogMTg4MzAwMDAwMCwgInZjdCI6ICJodHRwczovL2JtaS5idW5kLmV4YW1wbGUvY3JlZGVudGlhbC9waWQvMS4wIiwgImFnZV9lcXVhbF9vcl9vdmVyIjogeyJfc2QiOiBbIkZjOElfMDdMT2NnUHdyREpLUXlJR085N3dWc09wbE1Makh2UkM0UjQtV2ciLCAiWEx0TGphZFVXYzl6Tl85aE1KUm9xeTQ2VXNDS2IxSXNoWnV1cVVGS1NDQSIsICJhb0NDenNDN3A0cWhaSUFoX2lkUkNTQ2E2NDF1eWNuYzh6UGZOV3o4bngwIiwgImYxLVAwQTJkS1dhdnYxdUZuTVgyQTctRVh4dmhveHY1YUhodUVJTi1XNjQiLCAiazVoeTJyMDE4dnJzSmpvLVZqZDZnNnl0N0Fhb25Lb25uaXVKOXplbDNqbyIsICJxcDdaX0t5MVlpcDBzWWdETzN6VnVnMk1GdVBOakh4a3NCRG5KWjRhSS1jIl19LCAiX3NkX2FsZyI6ICJzaGEtMjU2IiwgImNuZiI6IHsiandrIjogeyJrdHkiOiAiRUMiLCAiY3J2IjogIlAtMjU2IiwgIngiOiAiVENBRVIxOVp2dTNPSEY0ajRXNHZmU1ZvSElQMUlMaWxEbHM3dkNlR2VtYyIsICJ5IjogIlp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.jeF9GjGbjCr0NND0SbkV4HeSpsysixALFScJl4bYkIykXhF6cRtqni64_d7X6Ef8Rx80rfsgXe0H7TdiSoIJOw~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiRXJpa2EiXQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIk11c3Rlcm1hbm4iXQ~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImJpcnRoZGF0ZSIsICIxOTYzLTA4LTEyIl0~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInNvdXJjZV9kb2N1bWVudF90eXBlIiwgImlkX2NhcmQiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInN0cmVldF9hZGRyZXNzIiwgIkhlaWRlc3RyYVx1MDBkZmUgMTciXQ~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImxvY2FsaXR5IiwgIktcdTAwZjZsbiJd~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgInBvc3RhbF9jb2RlIiwgIjUxMTQ3Il0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImNvdW50cnkiLCAiREUiXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImFkZHJlc3MiLCB7Il9zZCI6IFsiWEZjN3pYUG03enpWZE15d20yRXVCZmxrYTVISHF2ZjhVcF9zek5HcXZpZyIsICJiZDFFVnpnTm9wVWs0RVczX2VRMm4zX05VNGl1WE9IdjlYYkdITjNnMVRFIiwgImZfRlFZZ3ZRV3Z5VnFObklYc0FSbE55ZTdZR3A4RTc3Z1JBamFxLXd2bnciLCAidjRra2JfcFAxamx2VWJTanR5YzVicWNXeUEtaThYTHZoVllZN1pUMHRiMCJdfV0~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIm5hdGlvbmFsaXRpZXMiLCBbIkRFIl1d~WyI1YlBzMUlxdVpOYTBoa2FGenp6Wk53IiwgImdlbmRlciIsICJmZW1hbGUiXQ~WyI1YTJXMF9OcmxFWnpmcW1rXzdQcS13IiwgImJpcnRoX2ZhbWlseV9uYW1lIiwgIkdhYmxlciJd~WyJ5MXNWVTV3ZGZKYWhWZGd3UGdTN1JRIiwgImxvY2FsaXR5IiwgIkJlcmxpbiJd~WyJIYlE0WDhzclZXM1FEeG5JSmRxeU9BIiwgInBsYWNlX29mX2JpcnRoIiwgeyJfc2QiOiBbIldwaEhvSUR5b1diQXBEQzR6YnV3UjQweGwweExoRENfY3Y0dHNTNzFyRUEiXSwgImNvdW50cnkiOiAiREUifV0~WyJDOUdTb3VqdmlKcXVFZ1lmb2pDYjFBIiwgImFsc29fa25vd25fYXMiLCAiU2Nod2VzdGVyIEFnbmVzIl0~WyJreDVrRjE3Vi14MEptd1V4OXZndnR3IiwgIjEyIiwgdHJ1ZV0~WyJIM28xdXN3UDc2MEZpMnllR2RWQ0VRIiwgIjE0IiwgdHJ1ZV0~WyJPQktsVFZsdkxnLUFkd3FZR2JQOFpBIiwgIjE2IiwgdHJ1ZV0~WyJNMEpiNTd0NDF1YnJrU3V5ckRUM3hBIiwgIjE4IiwgdHJ1ZV0~WyJEc210S05ncFY0ZEFIcGpyY2Fvc0F3IiwgIjIxIiwgdHJ1ZV0~WyJlSzVvNXBIZmd1cFBwbHRqMXFoQUp3IiwgIjY1IiwgZmFsc2Vd~"; // Arrange - var sdJwtRecord = new SdJwtRecord(issuedSdJwt, new Dictionary(), new List(), new Dictionary(), "0"); + var keyId = KeyId.CreateKeyId(); + var sdJwtRecord = new SdJwtRecord(issuedSdJwt, new Dictionary(), new List(), new Dictionary(), keyId); var claimsToDisclose = new[] { "given_name", "address.street_address", "nationalities[0]" }; // Act diff --git a/test/WalletFramework.SdJwtVc.Tests/WalletFramework.SdJwtVc.Tests.csproj b/test/WalletFramework.SdJwtVc.Tests/WalletFramework.SdJwtVc.Tests.csproj index 13ba623b..abea3547 100644 --- a/test/WalletFramework.SdJwtVc.Tests/WalletFramework.SdJwtVc.Tests.csproj +++ b/test/WalletFramework.SdJwtVc.Tests/WalletFramework.SdJwtVc.Tests.csproj @@ -10,7 +10,7 @@ - + From ee4bcf3a552821dd3c71b3f098e0cfe815ece0d6 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 17 Jul 2024 09:20:03 +0200 Subject: [PATCH 3/8] separate integration tests Signed-off-by: Kevin --- .../WalletFramework.Oid4Vc.csproj | 3 ++ src/WalletFramework.sln | 7 +++++ .../Oid4Vp}/Oid4VpClientServiceTests.cs | 28 ++++++----------- .../Usings.cs | 1 + .../WalletFramework.Integration.Tests.csproj | 30 +++++++++++++++++++ .../WalletFramework.MdocLib.Tests.csproj | 7 +++-- .../WalletFramework.MdocVc.Tests.csproj | 2 +- .../Oid4Vci/Issuer/IssuerMetadataTests.cs | 6 ++-- .../WalletFramework.Oid4Vc.Tests.csproj | 6 ++-- .../WalletFramework.SdJwtVc.Tests.csproj | 6 ++-- 10 files changed, 65 insertions(+), 31 deletions(-) rename test/{WalletFramework.Oid4Vc.Tests/Oid4Vp/Services => WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp}/Oid4VpClientServiceTests.cs (71%) create mode 100644 test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Usings.cs create mode 100644 test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests.csproj diff --git a/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj b/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj index 842e193b..ecc08872 100644 --- a/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj +++ b/src/WalletFramework.Oid4Vc/WalletFramework.Oid4Vc.csproj @@ -10,6 +10,9 @@ <_Parameter1>WalletFramework.Oid4Vc.Tests + + <_Parameter1>WalletFramework.Integration.Tests + diff --git a/src/WalletFramework.sln b/src/WalletFramework.sln index 56766f62..42a3c01b 100644 --- a/src/WalletFramework.sln +++ b/src/WalletFramework.sln @@ -51,6 +51,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{873772C5-6 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mdoc", "Mdoc", "{A1DD69B3-DC35-43CF-AE14-D751722F074A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalletFramework.Integration.Tests", "..\test\WalletFramework.Integration.Tests\WalletFramework.Integration.Tests\WalletFramework.Integration.Tests.csproj", "{70DB749B-255A-4B71-8B76-BAD6B091DA7C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -129,6 +131,10 @@ Global {F6B3A24B-CDA2-4CC1-9F68-380203355099}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6B3A24B-CDA2-4CC1-9F68-380203355099}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6B3A24B-CDA2-4CC1-9F68-380203355099}.Release|Any CPU.Build.0 = Release|Any CPU + {70DB749B-255A-4B71-8B76-BAD6B091DA7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70DB749B-255A-4B71-8B76-BAD6B091DA7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70DB749B-255A-4B71-8B76-BAD6B091DA7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70DB749B-255A-4B71-8B76-BAD6B091DA7C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -153,6 +159,7 @@ Global {A7DC0511-DE0A-4EC2-8CC1-197FB81D7A76} = {02ADBA96-A50C-44F0-A9D9-FA0629AA2DF4} {F6B3A24B-CDA2-4CC1-9F68-380203355099} = {873772C5-60B9-442B-B06E-C279919B963C} {0EDD27CB-967F-4451-81AE-309E7F534F1C} = {A1DD69B3-DC35-43CF-AE14-D751722F074A} + {70DB749B-255A-4B71-8B76-BAD6B091DA7C} = {02ADBA96-A50C-44F0-A9D9-FA0629AA2DF4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4FFA80F9-ADC6-40DB-BBD1-A522B8A68560} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Services/Oid4VpClientServiceTests.cs b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs similarity index 71% rename from test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Services/Oid4VpClientServiceTests.cs rename to test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs index b85b7549..f2154033 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Services/Oid4VpClientServiceTests.cs +++ b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs @@ -14,30 +14,22 @@ using WalletFramework.SdJwtVc.Models.Records; using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; -namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Services; +namespace WalletFramework.Integration.Tests.Oid4Vp; public class Oid4VpClientServiceTests : IAsyncLifetime { private const string AuthRequestWithRequestUri = "haip://?client_id=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Fcallback&request_uri=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Frequestobject%2F4ba20ad0cb08545830aa549ab4305c03"; - private const string CombinedIssuance = - "eyJ4NWMiOlsiTUlJQ09qQ0NBZUdnQXdJQkFnSUJBekFLQmdncWhrak9QUVFEQWpCak1Rc3dDUVlEVlFRR0V3SkVSVEVQTUEwR0ExVUVCd3dHUW1WeWJHbHVNUjB3R3dZRFZRUUtEQlJDZFc1a1pYTmtjblZqYTJWeVpXa2dSMjFpU0RFS01BZ0dBMVVFQ3d3QlNURVlNQllHQTFVRUF3d1BTVVIxYm1sdmJpQlVaWE4wSUVOQk1CNFhEVEl6TURjeE9ERXlOVE16TlZvWERUSTRNRGN4TmpFeU5UTXpOVm93V1RFTE1Ba0dBMVVFQmhNQ1JFVXhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SOHdIUVlEVlFRRERCWldaWEpwWm1sbFpDQkZMVTFoYVd3Z1NYTnpkV1Z5TUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFOGoxOEsyZTRjZGRkdjRzaGRFUE84Z251MTJnM242RDFtRC9KU09TcEdDZlc5YUdoaU92bHpPck5icGRzTGVlWjVtclV3SXpra3BrMUhPVnZwSTNwVXFPQmp6Q0JqREFkQmdOVkhRNEVGZ1FVTFo4eWFCbDJJUVJWeCtrTGY4d3ZmRFpIY1pRd0RBWURWUjBUQVFIL0JBSXdBREFPQmdOVkhROEJBZjhFQkFNQ0I0QXdMQVlEVlIwUkJDVXdJNEloYVhOemRXVnlMVzl3Wlc1cFpEUjJZeTV6YzJrdWRHbHlMbUoxWkhKMUxtUmxNQjhHQTFVZEl3UVlNQmFBRkUrVzZ6N2FqVHVtZXgrWWNGYm9OclZlQzJ0Uk1Bb0dDQ3FHU000OUJBTUNBMGNBTUVRQ0lDZU5KYi85OENkV3RPdEtrREs0bm1WSGV4N0ZJclJQMlBRY3lmOVIzUGdPQWlCUHNkeENsakZXcTdxUGFOdUthUzhnTjRqZEkyVXUrNlNKaWZLZGp6SDdsQT09IiwiTUlJQ0xUQ0NBZFNnQXdJQkFnSVVNWVVIaEdEOWhVL2MwRW82bVc4cmpqZUordDB3Q2dZSUtvWkl6ajBFQXdJd1l6RUxNQWtHQTFVRUJoTUNSRVV4RHpBTkJnTlZCQWNNQmtKbGNteHBiakVkTUJzR0ExVUVDZ3dVUW5WdVpHVnpaSEoxWTJ0bGNtVnBJRWR0WWtneENqQUlCZ05WQkFzTUFVa3hHREFXQmdOVkJBTU1EMGxFZFc1cGIyNGdWR1Z6ZENCRFFUQWVGdzB5TXpBM01UTXdPVEkxTWpoYUZ3MHpNekEzTVRBd09USTFNamhhTUdNeEN6QUpCZ05WQkFZVEFrUkZNUTh3RFFZRFZRUUhEQVpDWlhKc2FXNHhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SZ3dGZ1lEVlFRRERBOUpSSFZ1YVc5dUlGUmxjM1FnUTBFd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFTRUh6OFlqckZ5VE5IR0x2TzE0RUF4bTl5aDhiS09na1V6WVdjQzFjdnJKbjVKZ0hZSE14WmJOTU8xM0VoMEVyMjczOFFRT2dlUm9aTUlUYW9ka2ZOU28yWXdaREFkQmdOVkhRNEVGZ1FVVDViclB0cU5PNlo3SDVod1Z1ZzJ0VjRMYTFFd0h3WURWUjBqQkJnd0ZvQVVUNWJyUHRxTk82WjdINWh3VnVnMnRWNExhMUV3RWdZRFZSMFRBUUgvQkFnd0JnRUIvd0lCQURBT0JnTlZIUThCQWY4RUJBTUNBWVl3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnWTBEZXJkQ3h0NHpHUFluOHlOckR4SVdDSkhwenE0QmRqZHNWTjJvMUdSVUNJQjBLQTdiRzFGVkIxSWlLOGQ1N1FBTCtQRzlYNWxkS0c3RWtvQW1oV1ZLZSJdLCJraWQiOiJNR3d3WjZSbE1HTXhDekFKQmdOVkJBWVRBa1JGTVE4d0RRWURWUVFIREFaQ1pYSnNhVzR4SFRBYkJnTlZCQW9NRkVKMWJtUmxjMlJ5ZFdOclpYSmxhU0JIYldKSU1Rb3dDQVlEVlFRTERBRkpNUmd3RmdZRFZRUUREQTlKUkhWdWFXOXVJRlJsYzNRZ1EwRUNBUU09IiwidHlwIjoidmMrc2Qtand0IiwiYWxnIjoiRVMyNTYifQ.eyJfc2QiOlsibmJkd2Z0NE9QdmcycFFlaVFYOHdoc2hnZ0VVTTdBY29mdHRWUE95ejJpdyIsIkxoQjF2dE9WM1ZHd3V6QmhKWnhoUUd5OUNSY0l0dC1QSmkydDRvRk83X28iLCJZNEl2Uk4yY2VDU2V6aXZKRjREMHFDc0JQNW81eUZVdDJiXy1YRkFXTGZjIiwieU9kNkRJbGFDUERXTG9xLUJfY2JQWTY4dFZmV18wU25NRGQzeU5qRDIxRSJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLW9wZW5pZDR2Yy5zc2kudGlyLmJ1ZHJ1LmRlIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjN1ZVRuN3VYLTZpSmxJYllOU2xrM0NSa3pwVDZGYldNMkxjdDZhdy1HZEEiLCJ5IjoiWlFlZnFJVnlzOG1MT05PZXBSclNsOWhXVGhGai1HTUVWR0pGUVk1TXQ2TSJ9fSwidHlwZSI6IlZlcmlmaWVkRU1haWwiLCJleHAiOjE2OTcyODIwOTgsImlhdCI6MTY5NjQxODA5OH0.Sbj1LaWpz45iqsdS8NFaLFgZ7G5hj1ofYLlO4rTI-jHELD6ORMGe1LVHe7IiOr_DNCDDde0ScGIEZKRiNCHEfA~WyJZTVVoZzh3Q2RHUGJjV245NW9MbUtBIiwiZW1haWwiLCJqb3RlbWVrMzYxQGZlc2dyaWQuY29tIl0"; - private const string ExpectedRedirectUrl = "https://client.example.org/cb#response_code=091535f699ea575c7937fa5f0f454aee"; private const string KeyBindingJwtMock = "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiK2p3dCJ9.eyJhdWQiOiJodHRwczovL3ZlcmlmaWVyLnNzaS50aXIuYnVkcnUuZGUvcHJlc2VudGF0aW9uL2F1dGhvcml6YXRpb24tcmVzcG9uc2UiLCJub25jZSI6IkxIc1lGRnlpMnNXQzZIM3ZSWVFsNFQiLCJpYXQiOjE2OTY0MjcwNzR9.Kxj1e7ucZeAnFnfOjo05QnW-DYeEprciDqkOhe6fhXIWprEYd1NJ6a0gpZJ66oTJsv49ExvDOKTLOzt6R75gcg"; - private const string KeyId = "KeyId"; - private const string RequestUriResponse = "eyJ4NWMiOlsiTUlJQ0x6Q0NBZFdnQXdJQkFnSUJCREFLQmdncWhrak9QUVFEQWpCak1Rc3dDUVlEVlFRR0V3SkVSVEVQTUEwR0ExVUVCd3dHUW1WeWJHbHVNUjB3R3dZRFZRUUtEQlJDZFc1a1pYTmtjblZqYTJWeVpXa2dSMjFpU0RFS01BZ0dBMVVFQ3d3QlNURVlNQllHQTFVRUF3d1BTVVIxYm1sdmJpQlVaWE4wSUVOQk1CNFhEVEl6TURnd016QTROREkwTkZvWERUSTRNRGd3TVRBNE5ESTBORm93VlRFTE1Ba0dBMVVFQmhNQ1JFVXhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1Sc3dHUVlEVlFRRERCSlBjR1Z1U1dRMFZsQWdWbVZ5YVdacFpYSXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUnNoUzVDaVBrSzVXRUN1RHpybmN0SXBwYm1nc1lkOURzT1lEcElFeFpFczFmUWNOeXZrQjVFZU5Xc2MwU0ExUU5xd3dHVzRndUZLZzBJZjFKR0R4VWZvNEdITUlHRU1CMEdBMVVkRGdRV0JCUmZMQVBzeG1Mc3AxblEvRk12RkkzN0MzQmxZREFNQmdOVkhSTUJBZjhFQWpBQU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBa0JnTlZIUkVFSFRBYmdobDJaWEpwWm1sbGNpNXpjMmt1ZEdseUxtSjFaSEoxTG1SbE1COEdBMVVkSXdRWU1CYUFGRStXNno3YWpUdW1leCtZY0Zib05yVmVDMnRSTUFvR0NDcUdTTTQ5QkFNQ0EwZ0FNRVVDSUNWZURUMnNkZHhySEMrZ0ZJTUVmc3huc0lXRmdIdnZlZnBuWXZrb0RjbHdBaUVBMlFnRVRHV3hIWUVObWxsNDA2VUNwYnFRb1kzMzJPbE9qdDUwWjc2WHBtQT0iLCJNSUlDTFRDQ0FkU2dBd0lCQWdJVU1ZVUhoR0Q5aFUvYzBFbzZtVzhyamplSit0MHdDZ1lJS29aSXpqMEVBd0l3WXpFTE1Ba0dBMVVFQmhNQ1JFVXhEekFOQmdOVkJBY01Ca0psY214cGJqRWRNQnNHQTFVRUNnd1VRblZ1WkdWelpISjFZMnRsY21WcElFZHRZa2d4Q2pBSUJnTlZCQXNNQVVreEdEQVdCZ05WQkFNTUQwbEVkVzVwYjI0Z1ZHVnpkQ0JEUVRBZUZ3MHlNekEzTVRNd09USTFNamhhRncwek16QTNNVEF3T1RJMU1qaGFNR014Q3pBSkJnTlZCQVlUQWtSRk1ROHdEUVlEVlFRSERBWkNaWEpzYVc0eEhUQWJCZ05WQkFvTUZFSjFibVJsYzJSeWRXTnJaWEpsYVNCSGJXSklNUW93Q0FZRFZRUUxEQUZKTVJnd0ZnWURWUVFEREE5SlJIVnVhVzl1SUZSbGMzUWdRMEV3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVNFSHo4WWpyRnlUTkhHTHZPMTRFQXhtOXloOGJLT2drVXpZV2NDMWN2ckpuNUpnSFlITXhaYk5NTzEzRWgwRXIyNzM4UVFPZ2VSb1pNSVRhb2RrZk5TbzJZd1pEQWRCZ05WSFE0RUZnUVVUNWJyUHRxTk82WjdINWh3VnVnMnRWNExhMUV3SHdZRFZSMGpCQmd3Rm9BVVQ1YnJQdHFOTzZaN0g1aHdWdWcydFY0TGExRXdFZ1lEVlIwVEFRSC9CQWd3QmdFQi93SUJBREFPQmdOVkhROEJBZjhFQkFNQ0FZWXdDZ1lJS29aSXpqMEVBd0lEUndBd1JBSWdZMERlcmRDeHQ0ekdQWW44eU5yRHhJV0NKSHB6cTRCZGpkc1ZOMm8xR1JVQ0lCMEtBN2JHMUZWQjFJaUs4ZDU3UUFMK1BHOVg1bGRLRzdFa29BbWhXVktlIl0sImtpZCI6Ik1Hd3daNlJsTUdNeEN6QUpCZ05WQkFZVEFrUkZNUTh3RFFZRFZRUUhEQVpDWlhKc2FXNHhIVEFiQmdOVkJBb01GRUoxYm1SbGMyUnlkV05yWlhKbGFTQkhiV0pJTVFvd0NBWURWUVFMREFGSk1SZ3dGZ1lEVlFRRERBOUpSSFZ1YVc5dUlGUmxjM1FnUTBFQ0FRUT0iLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IjE1ZDQwNjU0LWM2NTgtNDkzOC1hYzA3LWVjYjQxYzlhZmIxMCIsImlucHV0X2Rlc2NyaXB0b3JzIjpbeyJpZCI6IjUwYjZlNGYzLTYyMmEtNDk3NC1iMzMwLTVlNzIwZWM5MjJiZiIsImZvcm1hdCI6eyJ2YytzZC1qd3QiOnsicHJvb2ZfdHlwZSI6WyJKc29uV2ViU2lnbmF0dXJlMjAyMCJdfX0sImNvbnN0cmFpbnRzIjp7ImxpbWl0X2Rpc2Nsb3N1cmUiOiJyZXF1aXJlZCIsImZpZWxkcyI6W3sicGF0aCI6WyIkLnR5cGUiXSwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJjb25zdCI6IlZlcmlmaWVkRU1haWwifX0seyJwYXRoIjpbIiQuZW1haWwiXX1dfX1dfSwicmVzcG9uc2VfdXJpIjoiaHR0cHM6Ly92ZXJpZmllci5zc2kudGlyLmJ1ZHJ1LmRlL3ByZXNlbnRhdGlvbi9hdXRob3JpemF0aW9uLXJlc3BvbnNlIiwibm9uY2UiOiJZZjg4dGRlZzhZTTkyM3E0aFFBRzlPIiwiY2xpZW50X2lkIjoiaHR0cHM6Ly92ZXJpZmllci5zc2kudGlyLmJ1ZHJ1LmRlL3ByZXNlbnRhdGlvbi9hdXRob3JpemF0aW9uLXJlc3BvbnNlIiwicmVzcG9uc2VfbW9kZSI6ImRpcmVjdF9wb3N0In0.sdeLcG6Ta4ozfbDuHBr2Vq-Ro2WpdUIhJWy3BgazyvrgkQw27uTFGioPWXNCruK5H5E5nvHS420u5tv0671tjg"; - private const string Vct = "VerifiedEmail"; - - public Oid4VpClientServiceTests() { var holder = new Holder(); @@ -45,14 +37,14 @@ public Oid4VpClientServiceTests() var pexService = new PexService(); _sdJwtVcHolderService = new SdJwtVcHolderService(holder, _keyStoreMock.Object, walletRecordService); - _oid4VpHaipClient = new Oid4VpHaipClient(_httpClientFactoryMock.Object, pexService); + var oid4VpHaipClient = new Oid4VpHaipClient(_httpClientFactoryMock.Object, pexService); _oid4VpRecordService = new Oid4VpRecordService(walletRecordService); _oid4VpClientService = new Oid4VpClientService( _httpClientFactoryMock.Object, _sdJwtVcHolderService, pexService, - _oid4VpHaipClient, + oid4VpHaipClient, _loggerMock.Object, _oid4VpRecordService ); @@ -70,20 +62,18 @@ public Oid4VpClientServiceTests() .ReturnsAsync(KeyBindingJwtMock); } - private readonly SdJwtVcHolderService _sdJwtVcHolderService; - private readonly Mock _httpMessageHandlerMock = new(); private readonly Mock _httpClientFactoryMock = new(); - private readonly Mock> _loggerMock = new(); private readonly Mock _keyStoreMock = new(); + private readonly Mock> _loggerMock = new(); private readonly MockAgentRouter _router = new(); - private MockAgent? _agent1; - private readonly Oid4VpClientService _oid4VpClientService; - private readonly Oid4VpHaipClient _oid4VpHaipClient; private readonly Oid4VpRecordService _oid4VpRecordService; + private readonly SdJwtVcHolderService _sdJwtVcHolderService; private readonly WalletConfiguration _config1 = new() { Id = Guid.NewGuid().ToString() }; private readonly WalletCredentials _cred = new() { Key = "2" }; + + private MockAgent? _agent1; [Fact] public async Task CanExecuteOpenId4VpFlow() @@ -93,7 +83,7 @@ public async Task CanExecuteOpenId4VpFlow() var sdJwt = new SdJwtRecord(); - await _sdJwtVcHolderService.SaveAsync(_agent1.Context, sdJwt); + await _sdJwtVcHolderService.SaveAsync(_agent1!.Context, sdJwt); //Act var (authorizationRequest, credentials) = @@ -128,7 +118,7 @@ await _oid4VpClientService.ProcessAuthorizationRequestAsync( public async Task DisposeAsync() { - await _agent1.Dispose(); + await _agent1!.Dispose(); } public async Task InitializeAsync() diff --git a/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Usings.cs b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Usings.cs new file mode 100644 index 00000000..c802f448 --- /dev/null +++ b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests.csproj b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests.csproj new file mode 100644 index 00000000..339f782f --- /dev/null +++ b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests.csproj @@ -0,0 +1,30 @@ + + + net6.0 + enable + enable + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/test/WalletFramework.MdocLib.Tests/WalletFramework.MdocLib.Tests.csproj b/test/WalletFramework.MdocLib.Tests/WalletFramework.MdocLib.Tests.csproj index 3f30eb8a..bc9e9f46 100644 --- a/test/WalletFramework.MdocLib.Tests/WalletFramework.MdocLib.Tests.csproj +++ b/test/WalletFramework.MdocLib.Tests/WalletFramework.MdocLib.Tests.csproj @@ -13,8 +13,11 @@ - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/test/WalletFramework.MdocVc.Tests/WalletFramework.MdocVc.Tests.csproj b/test/WalletFramework.MdocVc.Tests/WalletFramework.MdocVc.Tests.csproj index 85de78e4..eb9936e4 100644 --- a/test/WalletFramework.MdocVc.Tests/WalletFramework.MdocVc.Tests.csproj +++ b/test/WalletFramework.MdocVc.Tests/WalletFramework.MdocVc.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs index 64b4a8e4..62e8cd54 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs @@ -50,8 +50,8 @@ public void Can_Encode_To_Json() { var decoded = IssuerMetadataSample.Decoded; - var sut = JObject.FromObject(decoded).RemoveNulls(); - + var sut = JObject.FromObject(decoded).RemoveNulls().ToObject(); + sut.Should().BeEquivalentTo(IssuerMetadataSample.EncodedAsJson); } @@ -66,7 +66,7 @@ public void Can_Decode_And_Encode_From_Json() // Assert sut => { - var encoded = JObject.FromObject(sut).RemoveNulls(); + var encoded = JObject.FromObject(sut).RemoveNulls().ToObject(); encoded.Should().BeEquivalentTo(sample); }, _ => Assert.Fail("IssuerMetadata must be valid")); diff --git a/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj b/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj index a0c376ea..c8e8b679 100644 --- a/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj +++ b/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj @@ -9,15 +9,15 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/WalletFramework.SdJwtVc.Tests/WalletFramework.SdJwtVc.Tests.csproj b/test/WalletFramework.SdJwtVc.Tests/WalletFramework.SdJwtVc.Tests.csproj index abea3547..c041938a 100644 --- a/test/WalletFramework.SdJwtVc.Tests/WalletFramework.SdJwtVc.Tests.csproj +++ b/test/WalletFramework.SdJwtVc.Tests/WalletFramework.SdJwtVc.Tests.csproj @@ -9,15 +9,15 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 0b04128cde3c505217672bf2bef35585b76bc4df Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 17 Jul 2024 16:05:57 +0200 Subject: [PATCH 4/8] move away from json converters Signed-off-by: Kevin --- src/WalletFramework.Core/Colors/Color.cs | 3 - .../Functional/Validation.cs | 3 + .../Json/Converters/DictJsonConverter.cs | 28 ------ .../Json/Converters/OneOfJsonConverter.cs | 29 ------- .../Json/Converters/OptionJsonConverter.cs | 28 ------ .../Json/Converters/ValueTypeJsonConverter.cs | 48 ---------- .../Localization/Locale.cs | 2 - src/WalletFramework.MdocLib/DocType.cs | 3 - .../IssuerSignedItem.cs | 3 - src/WalletFramework.MdocLib/NameSpace.cs | 3 - src/WalletFramework.MdocVc/ClaimDisplay.cs | 6 -- src/WalletFramework.MdocVc/ClaimName.cs | 3 - src/WalletFramework.MdocVc/MdocDisplay.cs | 85 +++++++++++------- src/WalletFramework.MdocVc/MdocLogo.cs | 3 - src/WalletFramework.MdocVc/MdocName.cs | 3 - src/WalletFramework.MdocVc/MdocRecord.cs | 76 ++++++---------- .../Implementations/AuthFlowSessionStorage.cs | 5 +- .../AuthFlow/Models/AuthorizationData.cs | 53 +++++++++++ .../AuthFlow/Records/AuthFlowSessionRecord.cs | 41 +++++---- .../Models/AuthorizationServerId.cs | 3 - .../Models/CredentialConfiguration.cs | 80 +++++++++++++---- .../Models/CredentialDisplay.cs | 66 ++++++++++---- .../Models/CredentialLogo.cs | 35 ++++++-- .../Models/CredentialName.cs | 3 - .../Models/CryptograhicSigningAlgValue.cs | 3 - .../Models/CryptographicBindingMethod.cs | 3 - .../CredConfiguration/Models/Format.cs | 3 - .../Models/Mdoc/ClaimsMetadata.cs | 32 +++---- .../Models/Mdoc/CryptoGraphicCurve.cs | 3 - .../Models/Mdoc/CryptographicSuite.cs | 3 - .../Models/Mdoc/ElementDisplay.cs | 27 ++++-- .../Models/Mdoc/ElementMetadata.cs | 42 ++++++--- .../Models/Mdoc/ElementName.cs | 3 - .../Models/Mdoc/MdocConfiguration.cs | 79 ++++++++--------- .../CredConfiguration/Models/Mdoc/Policy.cs | 32 +++++-- .../CredConfiguration/Models/ProofTypeId.cs | 3 - .../Models/ProofTypeMetadata.cs | 17 ++++ .../Oid4Vci/CredConfiguration/Models/Scope.cs | 3 - .../Models/SdJwt/SdJwtConfiguration.cs | 34 +++----- .../SupportedCredentialConfiguration.cs | 13 ++- .../Models/CredentialConfigurationId.cs | 12 --- .../CredRequest/Models/CredentialRequest.cs | 2 - .../Oid4Vci/Implementations/MdocStorage.cs | 4 +- .../Issuer/Models/CredentialIssuerId.cs | 3 - .../Oid4Vci/Issuer/Models/IssuerMetadata.cs | 87 ++++++++++--------- .../Oid4Vci/Issuer/Models/IssuerName.cs | 3 - .../Models/Metadata/Issuer/IssuerDisplay.cs | 31 ++++--- .../Models/Metadata/Issuer/IssuerLogo.cs | 26 ++++-- src/WalletFramework.SdJwtVc/Models/Vct.cs | 3 - .../MdocRecordTests.cs | 7 +- .../AuthFlow/AuthFlowSessionRecordTests.cs | 4 +- .../AuthFlow/Samples/AuthFlowSamples.cs | 12 +-- .../Oid4Vci/Issuer/IssuerMetadataTests.cs | 20 ++--- 53 files changed, 561 insertions(+), 565 deletions(-) delete mode 100644 src/WalletFramework.Core/Json/Converters/DictJsonConverter.cs delete mode 100644 src/WalletFramework.Core/Json/Converters/OneOfJsonConverter.cs delete mode 100644 src/WalletFramework.Core/Json/Converters/OptionJsonConverter.cs delete mode 100644 src/WalletFramework.Core/Json/Converters/ValueTypeJsonConverter.cs diff --git a/src/WalletFramework.Core/Colors/Color.cs b/src/WalletFramework.Core/Colors/Color.cs index 6d644a6c..607500e8 100644 --- a/src/WalletFramework.Core/Colors/Color.cs +++ b/src/WalletFramework.Core/Colors/Color.cs @@ -1,11 +1,8 @@ using System.Drawing; using LanguageExt; -using Newtonsoft.Json; -using WalletFramework.Core.Json.Converters; namespace WalletFramework.Core.Colors; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct Color { private System.Drawing.Color Value { get; } diff --git a/src/WalletFramework.Core/Functional/Validation.cs b/src/WalletFramework.Core/Functional/Validation.cs index e77dbae6..989059da 100644 --- a/src/WalletFramework.Core/Functional/Validation.cs +++ b/src/WalletFramework.Core/Functional/Validation.cs @@ -271,6 +271,9 @@ public static T UnwrapOrThrow(this Validation validation, Exception e) => t => t, _ => throw e); + public static T UnwrapOrThrow(this Validation validation) => + validation.UnwrapOrThrow(new InvalidOperationException($"Value of Type `{typeof(T)}` is corrupt")); + public static Validator AggregateValidators(this IEnumerable> validators) => t => { var errors = validators diff --git a/src/WalletFramework.Core/Json/Converters/DictJsonConverter.cs b/src/WalletFramework.Core/Json/Converters/DictJsonConverter.cs deleted file mode 100644 index 699ac9b6..00000000 --- a/src/WalletFramework.Core/Json/Converters/DictJsonConverter.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace WalletFramework.Core.Json.Converters; - -public sealed class DictJsonConverter : JsonConverter> -{ - public override bool CanRead => false; - - public override void WriteJson(JsonWriter writer, Dictionary? dict, JsonSerializer serializer) - { - var dictJson = new JObject(); - foreach (var (key, config) in dict!) - { - var x = JObject.FromObject(config!); - dictJson.Add(key!.ToString(), x); - } - serializer.Serialize(writer, dictJson); - } - - public override Dictionary ReadJson( - JsonReader reader, - Type objectType, - Dictionary? existingValue, - bool hasExistingValue, - JsonSerializer serializer) => - throw new NotImplementedException(); -} diff --git a/src/WalletFramework.Core/Json/Converters/OneOfJsonConverter.cs b/src/WalletFramework.Core/Json/Converters/OneOfJsonConverter.cs deleted file mode 100644 index 6ec2cba8..00000000 --- a/src/WalletFramework.Core/Json/Converters/OneOfJsonConverter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using LanguageExt; -using Newtonsoft.Json; -using OneOf; - -namespace WalletFramework.Core.Json.Converters; - -public class OneOfJsonConverter : JsonConverter where TOneOf : OneOfBase -{ - public override void WriteJson(JsonWriter writer, TOneOf? oneOf, JsonSerializer serializer) - { - oneOf!.Match( - t1 => - { - serializer.Serialize(writer, t1); - return Unit.Default; - }, - t2 => - { - serializer.Serialize(writer, t2); - return Unit.Default; - }); - } - - public override TOneOf ReadJson(JsonReader reader, Type objectType, TOneOf? existingValue, bool hasExistingValue, - JsonSerializer serializer) => - throw new NotImplementedException(); - - public override bool CanRead => false; -} diff --git a/src/WalletFramework.Core/Json/Converters/OptionJsonConverter.cs b/src/WalletFramework.Core/Json/Converters/OptionJsonConverter.cs deleted file mode 100644 index 0f5c50ac..00000000 --- a/src/WalletFramework.Core/Json/Converters/OptionJsonConverter.cs +++ /dev/null @@ -1,28 +0,0 @@ -using LanguageExt; -using Newtonsoft.Json; - -namespace WalletFramework.Core.Json.Converters; - -public sealed class OptionJsonConverter : JsonConverter> -{ - public override void WriteJson(JsonWriter writer, Option option, JsonSerializer serializer) - { - option.Match( - t => - { - serializer.Serialize(writer, t); - }, - () => serializer.Serialize(writer, null) - ); - } - - public override Option ReadJson( - JsonReader reader, - Type objectType, - Option existingValue, - bool hasExistingValue, - JsonSerializer serializer) => - throw new NotImplementedException(); - - public override bool CanRead => false; -} diff --git a/src/WalletFramework.Core/Json/Converters/ValueTypeJsonConverter.cs b/src/WalletFramework.Core/Json/Converters/ValueTypeJsonConverter.cs deleted file mode 100644 index 2289d809..00000000 --- a/src/WalletFramework.Core/Json/Converters/ValueTypeJsonConverter.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace WalletFramework.Core.Json.Converters; - -public interface IValueTypeDecoder -{ - public T Decode(JToken token); -} - -public sealed class ValueTypeJsonConverter : JsonConverter -{ - public override bool CanRead => false; - - public override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer) - { - var str = value!.ToString(); - writer.WriteValue(str); - } - - public override T ReadJson( - JsonReader reader, - Type objectType, - T? existingValue, - bool hasExistingValue, - JsonSerializer serializer) => throw new NotImplementedException(); -} - -public sealed class ValueTypeJsonConverter : JsonConverter where TDecoder : IValueTypeDecoder -{ - public override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer) - { - var str = value!.ToString(); - writer.WriteValue(str); - } - - public override T ReadJson( - JsonReader reader, - Type objectType, - T? existingValue, - bool hasExistingValue, - JsonSerializer serializer) - { - var token = JToken.Load(reader); - var decoder = (TDecoder)Activator.CreateInstance(typeof(TDecoder)); - return decoder.Decode(token); - } -} diff --git a/src/WalletFramework.Core/Localization/Locale.cs b/src/WalletFramework.Core/Localization/Locale.cs index 0af15b7e..c7b75b5f 100644 --- a/src/WalletFramework.Core/Localization/Locale.cs +++ b/src/WalletFramework.Core/Localization/Locale.cs @@ -5,7 +5,6 @@ using WalletFramework.Core.Functional; using WalletFramework.Core.Functional.Errors; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Oid4Vc.Oid4Vci.Errors; namespace WalletFramework.Core.Localization; @@ -15,7 +14,6 @@ namespace WalletFramework.Core.Localization; /// ("en-US"). These are based on RFC 4646: https://www.rfc-editor.org/rfc/rfc4646.html. /// Locales are case-sensitive. /// -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct Locale { private CultureInfo Value { get; } diff --git a/src/WalletFramework.MdocLib/DocType.cs b/src/WalletFramework.MdocLib/DocType.cs index 9d58f9e4..b55420ac 100644 --- a/src/WalletFramework.MdocLib/DocType.cs +++ b/src/WalletFramework.MdocLib/DocType.cs @@ -1,14 +1,11 @@ -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PeterO.Cbor; using WalletFramework.Core.Functional; -using WalletFramework.Core.Json.Converters; using WalletFramework.MdocLib.Common; using static WalletFramework.MdocLib.Common.Constants; namespace WalletFramework.MdocLib; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct DocType { private string Value { get; } diff --git a/src/WalletFramework.MdocLib/IssuerSignedItem.cs b/src/WalletFramework.MdocLib/IssuerSignedItem.cs index c28d9e7a..68242235 100644 --- a/src/WalletFramework.MdocLib/IssuerSignedItem.cs +++ b/src/WalletFramework.MdocLib/IssuerSignedItem.cs @@ -1,9 +1,7 @@ -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OneOf; using PeterO.Cbor; using WalletFramework.Core.Functional; -using WalletFramework.Core.Json.Converters; using WalletFramework.MdocLib.Common; using static WalletFramework.MdocLib.ElementArray; using static WalletFramework.MdocLib.ElementMap; @@ -65,7 +63,6 @@ internal static Validation ValidIssuerSignedItem(CBORObject is }); } -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct ElementIdentifier { public string Value { get; } diff --git a/src/WalletFramework.MdocLib/NameSpace.cs b/src/WalletFramework.MdocLib/NameSpace.cs index 3ceeb3c4..d107d6f2 100644 --- a/src/WalletFramework.MdocLib/NameSpace.cs +++ b/src/WalletFramework.MdocLib/NameSpace.cs @@ -1,13 +1,10 @@ -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PeterO.Cbor; using WalletFramework.Core.Functional; -using WalletFramework.Core.Json.Converters; using WalletFramework.MdocLib.Common; namespace WalletFramework.MdocLib; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct NameSpace { public string Value { get; } diff --git a/src/WalletFramework.MdocVc/ClaimDisplay.cs b/src/WalletFramework.MdocVc/ClaimDisplay.cs index c63f6830..2fdc797a 100644 --- a/src/WalletFramework.MdocVc/ClaimDisplay.cs +++ b/src/WalletFramework.MdocVc/ClaimDisplay.cs @@ -1,16 +1,10 @@ using LanguageExt; -using Newtonsoft.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Core.Localization; namespace WalletFramework.MdocVc; public record ClaimDisplay( - [property: JsonProperty(ClaimDisplayJsonKeys.ClaimName)] - [property: JsonConverter(typeof(OptionJsonConverter))] Option Name, - [property: JsonProperty(ClaimDisplayJsonKeys.Locale)] - [property: JsonConverter(typeof(OptionJsonConverter))] Option Locale); public static class ClaimDisplayJsonKeys diff --git a/src/WalletFramework.MdocVc/ClaimName.cs b/src/WalletFramework.MdocVc/ClaimName.cs index b83096d9..ad7cfa34 100644 --- a/src/WalletFramework.MdocVc/ClaimName.cs +++ b/src/WalletFramework.MdocVc/ClaimName.cs @@ -1,10 +1,7 @@ using LanguageExt; -using Newtonsoft.Json; -using WalletFramework.Core.Json.Converters; namespace WalletFramework.MdocVc; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct ClaimName { private string Value { get; } diff --git a/src/WalletFramework.MdocVc/MdocDisplay.cs b/src/WalletFramework.MdocVc/MdocDisplay.cs index 63b97ff6..e4106413 100644 --- a/src/WalletFramework.MdocVc/MdocDisplay.cs +++ b/src/WalletFramework.MdocVc/MdocDisplay.cs @@ -1,47 +1,30 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Colors; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Core.Localization; using WalletFramework.MdocLib; namespace WalletFramework.MdocVc; public record MdocDisplay( - [property: JsonProperty(MdocDisplayJsonKeys.Logo)] - [property: JsonConverter(typeof(OptionJsonConverter))] Option Logo, - [property: JsonProperty(MdocDisplayJsonKeys.Name)] - [property: JsonConverter(typeof(OptionJsonConverter))] Option Name, - [property: JsonProperty(MdocDisplayJsonKeys.BackgroundColor)] - [property: JsonConverter(typeof(OptionJsonConverter))] Option BackgroundColor, - [property: JsonProperty(MdocDisplayJsonKeys.TextColor)] - [property: JsonConverter(typeof(OptionJsonConverter))] Option TextColor, - [property: JsonProperty(MdocDisplayJsonKeys.Locale)] - [property: JsonConverter(typeof(OptionJsonConverter))] Option Locale, - [property: JsonProperty(MdocDisplayJsonKeys.ClaimsDisplays)] - [property: JsonConverter(typeof(OptionJsonConverter>>>))] Option>>> ClaimsDisplays); -public static class MdocDisplayJsonKeys -{ - public const string Logo = "logo"; - public const string Name = "name"; - public const string BackgroundColor = "background_color"; - public const string TextColor = "text_color"; - public const string Locale = "locale"; - public const string ClaimsDisplays = "claims_displays"; -} - public static class MdocDisplayFun { + private const string LogoJsonKey = "logo"; + private const string NameJsonKey = "name"; + private const string BackgroundColorJsonKey = "background_color"; + private const string TextColorJsonKey = "text_color"; + private const string LocaleJsonKey = "locale"; + private const string ClaimsDisplaysJsonKey = "claims_displays"; + public static Option GetByLocale(this List displays, Locale locale) { var dict = new Dictionary(); @@ -71,6 +54,48 @@ public static Option GetByLocale(this List displays, L return Option.None; } } + + public static JObject EncodeToJson(this MdocDisplay display) + { + var result = new JObject(); + + display.Logo.IfSome(logo => result.Add(LogoJsonKey, logo.ToString())); + display.Name.IfSome(name => result.Add(NameJsonKey, name.ToString())); + display.BackgroundColor.IfSome(color => result.Add(BackgroundColorJsonKey, color.ToString())); + display.TextColor.IfSome(color => result.Add(TextColorJsonKey, color.ToString())); + display.Locale.IfSome(locale => result.Add(LocaleJsonKey, locale.ToString())); + display.ClaimsDisplays.IfSome(claimsDisplays => + { + var claimsDict = new JObject(); + foreach (var (nameSpace, elementDict) in claimsDisplays) + { + var elements = new JObject(); + foreach (var (elementId, claimDisplays) in elementDict) + { + var displays = new JArray(); + foreach (var claimDisplay in claimDisplays) + { + var claimDisplayJson = new JObject(); + + claimDisplay.Name.IfSome( + name => claimDisplayJson.Add(ClaimDisplayJsonKeys.ClaimName, name.ToString()) + ); + + claimDisplay.Locale.IfSome( + locale => claimDisplayJson.Add(ClaimDisplayJsonKeys.Locale, locale.ToString()) + ); + + displays.Add(claimDisplayJson); + } + elements.Add(elementId.ToString(), displays); + } + claimsDict.Add(nameSpace.ToString(), elements); + } + result.Add(ClaimsDisplaysJsonKey, claimsDict); + }); + + return result; + } public static Option> DecodeFromJson(JArray array) { @@ -87,32 +112,32 @@ from displays in result private static MdocDisplay DecodeFromJson(JObject display) { var logo = - from jToken in display.GetByKey(MdocDisplayJsonKeys.Logo).ToOption() + from jToken in display.GetByKey(LogoJsonKey).ToOption() let uri = new Uri(jToken.ToString()) select new MdocLogo(uri); var mdocName = - from jToken in display.GetByKey(MdocDisplayJsonKeys.Name).ToOption() + from jToken in display.GetByKey(NameJsonKey).ToOption() from name in MdocName.OptionMdocName(jToken.ToString()) select name; var backgroundColor = - from jToken in display.GetByKey(MdocDisplayJsonKeys.BackgroundColor).ToOption() + from jToken in display.GetByKey(BackgroundColorJsonKey).ToOption() from color in Color.OptionColor(jToken.ToString()) select color; var textColor = - from jToken in display.GetByKey(MdocDisplayJsonKeys.TextColor).ToOption() + from jToken in display.GetByKey(TextColorJsonKey).ToOption() from color in Color.OptionColor(jToken.ToString()) select color; var locale = - from jToken in display.GetByKey(MdocDisplayJsonKeys.Locale).ToOption() + from jToken in display.GetByKey(LocaleJsonKey).ToOption() from l in Locale.OptionLocale(jToken.ToString()) select l; var claimsDisplays = - from jToken in display.GetByKey(MdocDisplayJsonKeys.ClaimsDisplays).ToOption() + from jToken in display.GetByKey(ClaimsDisplaysJsonKey).ToOption() from claimsJson in jToken.ToJObject().ToOption() from displays in DecodeClaimsDisplaysFromJson(claimsJson) select displays; diff --git a/src/WalletFramework.MdocVc/MdocLogo.cs b/src/WalletFramework.MdocVc/MdocLogo.cs index e17cb727..6d42f7c6 100644 --- a/src/WalletFramework.MdocVc/MdocLogo.cs +++ b/src/WalletFramework.MdocVc/MdocLogo.cs @@ -1,10 +1,7 @@ -using Newtonsoft.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Core.Uri; namespace WalletFramework.MdocVc; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct MdocLogo { public MdocLogo(Uri value) diff --git a/src/WalletFramework.MdocVc/MdocName.cs b/src/WalletFramework.MdocVc/MdocName.cs index f11ca172..07cd44ff 100644 --- a/src/WalletFramework.MdocVc/MdocName.cs +++ b/src/WalletFramework.MdocVc/MdocName.cs @@ -1,10 +1,7 @@ using LanguageExt; -using Newtonsoft.Json; -using WalletFramework.Core.Json.Converters; namespace WalletFramework.MdocVc; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct MdocName { private string Value { get; } diff --git a/src/WalletFramework.MdocVc/MdocRecord.cs b/src/WalletFramework.MdocVc/MdocRecord.cs index 0a24c47e..4c00f983 100644 --- a/src/WalletFramework.MdocVc/MdocRecord.cs +++ b/src/WalletFramework.MdocVc/MdocRecord.cs @@ -2,16 +2,15 @@ using Hyperledger.Aries.Storage.Models; using Hyperledger.Aries.Storage.Models.Interfaces; using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Credentials; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; using WalletFramework.MdocLib; +using static WalletFramework.MdocVc.MdocRecordFun; namespace WalletFramework.MdocVc; -[JsonConverter(typeof(MdocRecordJsonConverter))] public sealed class MdocRecord : RecordBase, ICredential { public CredentialId CredentialId @@ -47,25 +46,43 @@ public MdocRecord() public static implicit operator Mdoc(MdocRecord record) => record.Mdoc; } -public static class MdocRecordJsonKeys +public static class MdocRecordFun { public const string MdocJsonKey = "mdoc"; - public const string MdocDisplaysKey = "displays"; -} + private const string MdocDisplaysJsonKey = "displays"; + + public static JObject EncodeToJson(this MdocRecord record) + { + var result = new JObject + { + {nameof(RecordBase.Id), record.Id}, + {MdocJsonKey, record.Mdoc.Encode()} + }; -public static class MdocRecordFun -{ + record.Displays.IfSome(displays => + { + var displaysJson = new JArray(); + foreach (var display in displays) + { + displaysJson.Add(display.EncodeToJson()); + } + result.Add(MdocDisplaysJsonKey, displaysJson); + }); + + return result; + } + public static MdocRecord DecodeFromJson(JObject json) { var id = json[nameof(RecordBase.Id)]!.ToString(); - var mdocStr = json[MdocRecordJsonKeys.MdocJsonKey]!.ToString(); + var mdocStr = json[MdocJsonKey]!.ToString(); var mdoc = Mdoc .ValidMdoc(mdocStr) .UnwrapOrThrow(new InvalidOperationException($"The MdocRecord with ID: {id} is corrupt")); var displays = - from jToken in json.GetByKey(MdocRecordJsonKeys.MdocDisplaysKey).ToOption() + from jToken in json.GetByKey(MdocDisplaysJsonKey).ToOption() from jArray in jToken.ToJArray().ToOption() from mdocDisplays in MdocDisplayFun.DecodeFromJson(jArray) select mdocDisplays; @@ -80,44 +97,3 @@ from mdocDisplays in MdocDisplayFun.DecodeFromJson(jArray) public static MdocRecord ToRecord(this Mdoc mdoc, Option> displays) => new(mdoc, displays); } - -public sealed class MdocRecordJsonConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, MdocRecord? record, JsonSerializer serializer) - { - writer.WriteStartObject(); - - writer.WritePropertyName(nameof(RecordBase.Id)); - writer.WriteValue(record!.Id); - - writer.WritePropertyName(MdocRecordJsonKeys.MdocJsonKey); - writer.WriteValue(record.Mdoc.Encode()); - - writer.WritePropertyName(MdocRecordJsonKeys.MdocDisplaysKey); - record.Displays.Match( - list => - { - writer.WriteStartArray(); - foreach (var display in list) - { - serializer.Serialize(writer, display); - } - writer.WriteEndArray(); - }, - () => {} - ); - - writer.WriteEndObject(); - } - - public override MdocRecord ReadJson( - JsonReader reader, - Type objectType, - MdocRecord? existingValue, - bool hasExistingValue, - JsonSerializer serializer) - { - var json = JObject.Load(reader); - return MdocRecordFun.DecodeFromJson(json); - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs index ca00ebf8..e0cbaa08 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Implementations/AuthFlowSessionStorage.cs @@ -1,6 +1,5 @@ using Hyperledger.Aries.Agents; using Hyperledger.Aries.Storage; -using LanguageExt; using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Abstractions; using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; using WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Records; @@ -34,7 +33,7 @@ public async Task StoreAsync( authorizationCodeParameters, sessionId); - await _recordService.AddAsync(agentContext.Wallet, record); + await _recordService.AddAsync(agentContext.Wallet, record, AuthFlowSessionRecordFun.EncodeToJson); return record.Id; } @@ -42,7 +41,7 @@ public async Task StoreAsync( /// public async Task GetAsync(IAgentContext context, VciSessionId sessionId) { - var record = await _recordService.GetAsync(context.Wallet, sessionId); + var record = await _recordService.GetAsync(context.Wallet, sessionId, AuthFlowSessionRecordFun.DecodeFromJson); return record!; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationData.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationData.cs index 86017ddb..7ae073d5 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationData.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/AuthorizationData.cs @@ -1,3 +1,6 @@ +using System.Globalization; +using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; @@ -9,3 +12,53 @@ public record AuthorizationData( IssuerMetadata IssuerMetadata, AuthorizationServerMetadata AuthorizationServerMetadata, List CredentialConfigurationIds); + +public static class AuthorizationDataFun +{ + private const string ClientOptionsJsonKey = "client_options"; + private const string IssuerMetadataJsonKey = "issuer_metadata"; + private const string AuthorizationServerMetadataJsonKey = "authorization_server_metadata"; + private const string CredentialConfigurationIdsJsonKey = "credential_configuration_ids"; + + public static JObject EncodeToJson(this AuthorizationData data) + { + var clientOptions = JObject.FromObject(data.ClientOptions); + + var issuerMetadata = data.IssuerMetadata.EncodeToJson(); + + var authServerMetadata = JObject.FromObject(data.AuthorizationServerMetadata); + + var ids = data.CredentialConfigurationIds.Select(id => id.ToString()); + var idsArray = new JArray(ids); + + return new JObject + { + { ClientOptionsJsonKey, clientOptions }, + { IssuerMetadataJsonKey, issuerMetadata }, + { AuthorizationServerMetadataJsonKey, authServerMetadata }, + { CredentialConfigurationIdsJsonKey, idsArray } + }; + } + + public static AuthorizationData DecodeFromJson(JObject json) + { + var clientOptions = json[ClientOptionsJsonKey]!.ToObject()!; + + var issuerMetadata = IssuerMetadata + .ValidIssuerMetadata(json[IssuerMetadataJsonKey]!.ToObject()!) + .UnwrapOrThrow(); + + var authServerMetadata = json[AuthorizationServerMetadataJsonKey]!.ToObject()!; + + var configIds = json[CredentialConfigurationIdsJsonKey]!.Cast().Select(value => + { + var str = value.ToString(CultureInfo.InvariantCulture); + return CredentialConfigurationId + .ValidCredentialConfigurationId(str) + .UnwrapOrThrow(); + + }).ToList(); + + return new AuthorizationData(clientOptions, issuerMetadata, authServerMetadata, configIds); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs index 6c5c62f3..26fb9b46 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Records/AuthFlowSessionRecord.cs @@ -9,7 +9,6 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Records; /// /// Represents the authorization session record. Used during the VCI Authorization Code Flow to hold session relevant information. /// -[JsonConverter(typeof(AuthFlowSessionRecordJsonConverter))] public sealed class AuthFlowSessionRecord : RecordBase { /// @@ -70,31 +69,35 @@ public AuthFlowSessionRecord( } } -public sealed class AuthFlowSessionRecordJsonConverter : JsonConverter +public static class AuthFlowSessionRecordFun { - public override bool CanWrite => false; + private const string AuthorizationDataJsonKey = "authorization_data"; + private const string AuthorizationCodeParametersJsonKey = "authorization_code_parameters"; - public override void WriteJson(JsonWriter writer, AuthFlowSessionRecord? record, JsonSerializer serializer) - => throw new NotImplementedException(); + public static JObject EncodeToJson(this AuthFlowSessionRecord record) + { + var authorizationData = record.AuthorizationData.EncodeToJson(); + var authorizationCodeParameters = JObject.FromObject(record.AuthorizationCodeParameters); + + return new JObject + { + { nameof(RecordBase.Id), record.Id }, + { AuthorizationDataJsonKey, authorizationData }, + { AuthorizationCodeParametersJsonKey, authorizationCodeParameters } + }; + } - public override AuthFlowSessionRecord ReadJson( - JsonReader reader, - Type objectType, - AuthFlowSessionRecord? existingValue, - bool hasExistingValue, - JsonSerializer serializer) + public static AuthFlowSessionRecord DecodeFromJson(JObject json) { - var json = JObject.Load(reader); + var idJson = json[nameof(RecordBase.Id)]!.ToObject()!; + var id = VciSessionIdFun.DecodeFromJson(idJson); - var id = VciSessionIdFun.DecodeFromJson(json[nameof(RecordBase.Id)]!.ToObject()!); - var authCodeParameters = JsonConvert.DeserializeObject( - json[nameof(AuthorizationCodeParameters)]!.ToString() + json[AuthorizationCodeParametersJsonKey]!.ToString() ); - - var authorizationData = JsonConvert.DeserializeObject( - json[nameof(AuthorizationData)]!.ToString() - )!; + + var authorizationData = AuthorizationDataFun + .DecodeFromJson(json[AuthorizationDataJsonKey]!.ToObject()!); var result = new AuthFlowSessionRecord(authorizationData, authCodeParameters!, id); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerId.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerId.cs index acc7b37d..f25d7c75 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerId.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/Models/AuthorizationServerId.cs @@ -1,15 +1,12 @@ using System.Globalization; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Core.Uri; using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Errors; namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct AuthorizationServerId { private Uri Value { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialConfiguration.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialConfiguration.cs index ea7ad271..823873bd 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialConfiguration.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialConfiguration.cs @@ -1,9 +1,7 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using static WalletFramework.Core.Functional.ValidationFun; using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Format; using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Scope; @@ -12,6 +10,7 @@ using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.ProofTypeId; using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.ProofTypeMetadata; using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CredentialDisplay; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CredentialConfigurationFun; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; @@ -23,44 +22,33 @@ public record CredentialConfiguration /// /// Gets the identifier for the format of the credential. /// - [JsonProperty(FormatJsonKey)] public Format Format { get; } /// /// Gets a string indicating the credential that can be issued. /// - [JsonProperty(ScopeJsonKey)] - [JsonConverter(typeof(OptionJsonConverter))] public Option Scope { get; set; } /// /// Gets list of methods that identify how the Credential is bound to the identifier of the End-User who /// possesses the Credential. /// - [JsonProperty(CryptographicBindingMethodsSupportedJsonKey)] - [JsonConverter(typeof(OptionJsonConverter>))] public Option> CryptographicBindingMethodsSupported { get; } /// /// Gets a list of identifiers for the signing algorithms that are supported by the issuer and used /// to sign credentials. /// - [JsonProperty(CredentialSigningAlgValuesSupportedJsonKey)] - [JsonConverter(typeof(OptionJsonConverter>))] public Option> CredentialSigningAlgValuesSupported { get; } /// /// Gets a dictionary which maps a credential type to its supported signing algorithms for key proofs. /// - [JsonProperty(ProofTypesSupportedJsonKey)] - [JsonConverter(typeof(OptionJsonConverter>))] public Option> ProofTypesSupported { get; } /// /// Gets a list of display properties of the supported credential for different languages. /// - [JsonProperty(DisplayJsonKey)] - [JsonConverter(typeof(OptionJsonConverter>))] public Option> Display { get; } private CredentialConfiguration( @@ -122,11 +110,65 @@ from displays in array.TraverseAny(OptionalCredentialDisplay) .Apply(credentialMetadata.GetByKey(ProofTypesSupportedJsonKey).OnSuccess(validProofTypes).ToOption()) .Apply(credentialMetadata.GetByKey(DisplayJsonKey).ToOption().OnSome(optionalCredentialDisplays)); } +} + +public static class CredentialConfigurationFun +{ + public const string FormatJsonKey = "format"; + public const string ScopeJsonKey = "scope"; + public const string CryptographicBindingMethodsSupportedJsonKey = "cryptographic_binding_methods_supported"; + public const string CredentialSigningAlgValuesSupportedJsonKey = "credential_signing_alg_values_supported"; + public const string ProofTypesSupportedJsonKey = "proof_types_supported"; + public const string DisplayJsonKey = "display"; + + public static JObject EncodeToJson(this CredentialConfiguration config) + { + var result = new JObject(); + + result.Add(FormatJsonKey, config.Format.ToString()); + + config.Scope.IfSome(scope => result.Add(ScopeJsonKey, scope.ToString())); - private const string FormatJsonKey = "format"; - private const string ScopeJsonKey = "scope"; - private const string CryptographicBindingMethodsSupportedJsonKey = "cryptographic_binding_methods_supported"; - private const string CredentialSigningAlgValuesSupportedJsonKey = "credential_signing_alg_values_supported"; - private const string ProofTypesSupportedJsonKey = "proof_types_supported"; - private const string DisplayJsonKey = "display"; + config.CryptographicBindingMethodsSupported.IfSome(list => + { + var bindingMethods = new JArray(); + foreach (var method in list) + { + bindingMethods.Add(method.ToString()); + } + result.Add(CryptographicBindingMethodsSupportedJsonKey, bindingMethods); + }); + + config.CredentialSigningAlgValuesSupported.IfSome(list => + { + var signingAlgValues = new JArray(); + foreach (var value in list) + { + signingAlgValues.Add(value.ToString()); + } + result.Add(CredentialSigningAlgValuesSupportedJsonKey, signingAlgValues); + }); + + config.ProofTypesSupported.IfSome(dict => + { + var proofTypes = new JObject(); + foreach (var (key, value) in dict) + { + proofTypes.Add(key.ToString(), value.EncodeToJson()); + } + result.Add(ProofTypesSupportedJsonKey, proofTypes); + }); + + config.Display.IfSome(displays => + { + var displayArray = new JArray(); + foreach (var display in displays) + { + displayArray.Add(display.EncodeToJson()); + } + result.Add(DisplayJsonKey, displayArray); + }); + + return result; + } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs index 63580e48..20c51065 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialDisplay.cs @@ -1,16 +1,13 @@ -using System.Drawing; using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using WalletFramework.Core.Colors; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Core.Localization; using WalletFramework.SdJwtVc.Models.Credential; using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CredentialName; using static WalletFramework.Core.Localization.Locale; using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CredentialLogo; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CredentialDisplayJsonExtensions; using Color = WalletFramework.Core.Colors.Color; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; @@ -23,36 +20,26 @@ public record CredentialDisplay /// /// Gets the logo associated with this Credential. /// - [JsonProperty("logo")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Logo { get; } /// /// Gets the name of the Credential. /// - [JsonProperty("name")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Name { get; } /// /// Gets the background color for the Credential. /// - [JsonProperty("background_color")] - [JsonConverter(typeof(OptionJsonConverter))] public Option BackgroundColor { get; } /// /// Gets the locale, which represents the specific culture or region. /// - [JsonProperty("locale")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Locale { get; } /// /// Gets the text color for the Credential. /// - [JsonProperty("text_color")] - [JsonConverter(typeof(OptionJsonConverter))] public Option TextColor { get; } private CredentialDisplay( @@ -75,18 +62,18 @@ public static Option OptionalCredentialDisplay(JToken display .OnSome(jObject => { var backgroundColor = jObject - .GetByKey("background_color") + .GetByKey(BackgroundColorJsonKey) .ToOption() .OnSome(color => Color.OptionColor(color.ToString())); var textColor = jObject - .GetByKey("text_color") + .GetByKey(TextColorJsonKey) .ToOption() .OnSome(color => Color.OptionColor(color.ToString())); - var name = jObject.GetByKey("name").ToOption().OnSome(OptionalCredentialName); - var logo = jObject.GetByKey("logo").ToOption().OnSome(OptionalCredentialLogo); - var locale = jObject.GetByKey("locale").OnSuccess(ValidLocale).ToOption(); + var name = jObject.GetByKey(NameJsonKey).ToOption().OnSome(OptionalCredentialName); + var logo = jObject.GetByKey(LogoJsonKey).ToOption().OnSome(OptionalCredentialLogo); + var locale = jObject.GetByKey(LocaleJsonKey).OnSuccess(ValidLocale).ToOption(); if (name.IsNone && logo.IsNone && backgroundColor.IsNone && locale.IsNone && textColor.IsNone) return Option.None; @@ -116,3 +103,44 @@ public static SdJwtDisplay ToSdJwtDisplay(this CredentialDisplay credentialDispl }; } } + +public static class CredentialDisplayJsonExtensions +{ + public const string LogoJsonKey = "logo"; + public const string NameJsonKey = "name"; + public const string BackgroundColorJsonKey = "background_color"; + public const string LocaleJsonKey = "locale"; + public const string TextColorJsonKey = "text_color"; + + public static JObject EncodeToJson(this CredentialDisplay display) + { + var result = new JObject(); + + display.Logo.IfSome(logo => + { + result.Add(LogoJsonKey, logo.EncodeToJson()); + }); + + display.Name.IfSome(name => + { + result.Add(NameJsonKey, name.ToString()); + }); + + display.BackgroundColor.IfSome(color => + { + result.Add(BackgroundColorJsonKey, color.ToString()); + }); + + display.Locale.IfSome(locale => + { + result.Add(LocaleJsonKey, locale.ToString()); + }); + + display.TextColor.IfSome(color => + { + result.Add(TextColorJsonKey, color.ToString()); + }); + + return result; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs index 2a58ee40..99a9d079 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialLogo.cs @@ -1,9 +1,9 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Uri; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.CredentialLogoJsonExtensions; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; @@ -15,15 +15,11 @@ public record CredentialLogo /// /// Gets the alternate text that describes the logo image. This is typically used for accessibility purposes. /// - [JsonProperty("alt_text")] - [JsonConverter(typeof(OptionJsonConverter))] public Option AltText { get; } /// /// Gets the URL of the logo image. /// - [JsonProperty("uri")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Uri { get; } private CredentialLogo(Option altText, Option uri) @@ -34,7 +30,7 @@ private CredentialLogo(Option altText, Option uri) public static Option OptionalCredentialLogo(JToken logo) { - var altText = logo.GetByKey("alt_text").ToOption().OnSome(text => + var altText = logo.GetByKey(AltTextJsonKey).ToOption().OnSome(text => { var str = text.ToString(); if (string.IsNullOrWhiteSpace(str)) @@ -43,7 +39,7 @@ public static Option OptionalCredentialLogo(JToken logo) return str; }); - var imageUri = logo.GetByKey("uri").ToOption().OnSome(uri => + var imageUri = logo.GetByKey(UriJsonKey).ToOption().OnSome(uri => { try { @@ -63,3 +59,26 @@ public static Option OptionalCredentialLogo(JToken logo) return new CredentialLogo(altText, imageUri); } } + +public static class CredentialLogoJsonExtensions +{ + public const string AltTextJsonKey = "alt_text"; + public static string UriJsonKey => "uri"; + + public static JObject EncodeToJson(this CredentialLogo logo) + { + var result = new JObject(); + + logo.AltText.IfSome(altText => + { + result.Add(AltTextJsonKey, altText); + }); + + logo.Uri.IfSome(uri => + { + result.Add(UriJsonKey, uri.ToStringWithoutTrail()); + }); + + return result; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialName.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialName.cs index 8bfc5d0f..1a6f4ba0 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialName.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CredentialName.cs @@ -1,14 +1,11 @@ using System.Globalization; using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct CredentialName { private string Value { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptograhicSigningAlgValue.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptograhicSigningAlgValue.cs index ded1adeb..477ba37c 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptograhicSigningAlgValue.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptograhicSigningAlgValue.cs @@ -1,15 +1,12 @@ using System.Globalization; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Functional.Errors; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using static WalletFramework.Core.Functional.ValidationFun; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct CryptograhicSigningAlgValue { private string Value { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptographicBindingMethod.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptographicBindingMethod.cs index bf1b7f2c..f2516e10 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptographicBindingMethod.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/CryptographicBindingMethod.cs @@ -1,14 +1,11 @@ using System.Globalization; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Functional.Errors; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct CryptographicBindingMethod { private string Value { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Format.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Format.cs index 2ef65059..df136e21 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Format.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Format.cs @@ -1,14 +1,11 @@ using System.Globalization; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Errors; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct Format { private string Value { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ClaimsMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ClaimsMetadata.cs index c3b30a86..082b9fc8 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ClaimsMetadata.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ClaimsMetadata.cs @@ -1,5 +1,4 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; @@ -9,7 +8,6 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; -[JsonConverter(typeof(ClaimsMetadataJsonConverter))] public readonly struct ClaimsMetadata { public Dictionary> Value { get; } @@ -22,28 +20,26 @@ public static Validation ValidClaimsMetadata(JObject claims) => .OnSuccess(dictionary => new ClaimsMetadata(dictionary)); } -public class ClaimsMetadataJsonConverter : JsonConverter +public static class ClaimsMetadataFun { - public override bool CanRead => false; - - public override void WriteJson(JsonWriter writer, ClaimsMetadata claimsMetadata, JsonSerializer serializer) + public static JObject EncodeToJson(this ClaimsMetadata metadata) { - writer.WriteStartObject(); + var result = new JObject(); - var value = JObject.FromObject(claimsMetadata.Value, serializer); - foreach (var property in value.Properties()) + foreach (var (key, elementMetadatas) in metadata.Value) { - property.WriteTo(writer); + var elementsJson = new JObject(); + foreach (var (elementIdentifier, elementMetadata) in elementMetadatas) + { + elementsJson.Add(elementIdentifier, elementMetadata.EncodeToJson()); + } + + result.Add(key.ToString(), elementsJson); } - } - public override ClaimsMetadata ReadJson(JsonReader reader, Type objectType, ClaimsMetadata existingValue, bool hasExistingValue, - JsonSerializer serializer) => - throw new NotImplementedException(); -} - -public static class ClaimsMetadataFun -{ + return result; + } + public static Option>>> ToClaimsDisplays( this ClaimsMetadata claimsMetadata) { diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptoGraphicCurve.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptoGraphicCurve.cs index a076ee4e..6f157659 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptoGraphicCurve.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptoGraphicCurve.cs @@ -1,12 +1,9 @@ -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; -using WalletFramework.Core.Json.Converters; using WalletFramework.MdocLib; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct CryptographicCurve { public CoseLabel Value { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptographicSuite.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptographicSuite.cs index 00191e4b..bc697f22 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptographicSuite.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/CryptographicSuite.cs @@ -1,14 +1,11 @@ using System.Globalization; -using System.Text.Json.Serialization; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Functional.Errors; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct CryptographicSuite { // TODO: Validate if Value is part of IANA Registry diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementDisplay.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementDisplay.cs index 40a0b7e5..a43054b6 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementDisplay.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementDisplay.cs @@ -1,21 +1,16 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Core.Localization; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.ElementDisplayFun; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; public record ElementDisplay { - [JsonProperty("locale")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Locale { get; } - [JsonProperty("name")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Name { get; } private ElementDisplay(Option name, Option locale) @@ -26,11 +21,11 @@ private ElementDisplay(Option name, Option locale) public static Option OptionalElementDisplay(JObject display) { - var name = display.GetByKey("name").Match( + var name = display.GetByKey(NameJsonKey).Match( ElementName.OptionalElementName, _ => Option.None); - var locale = display.GetByKey("locale").Match( + var locale = display.GetByKey(LocaleJsonKey).Match( Core.Localization.Locale.OptionLocale, _ => Option.None); @@ -42,3 +37,19 @@ public static Option OptionalElementDisplay(JObject display) return new ElementDisplay(name, locale); } } + +public static class ElementDisplayFun +{ + public const string LocaleJsonKey = "locale"; + public const string NameJsonKey = "name"; + + public static JObject EncodeToJson(this ElementDisplay display) + { + var result = new JObject(); + + display.Locale.IfSome(locale => result.Add(LocaleJsonKey, locale.ToString())); + display.Name.IfSome(name => result.Add(NameJsonKey, name.ToString())); + + return result; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementMetadata.cs index fe90bb99..cb5c06ba 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementMetadata.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementMetadata.cs @@ -1,21 +1,16 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.MdocLib; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.ElementMetadataFun; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; public record ElementMetadata { - [JsonProperty("mandatory")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Mandatory { get; } - [JsonProperty("display")] - [JsonConverter(typeof(OptionJsonConverter>))] public Option> Display { get; } private ElementMetadata(Option mandatory, Option> display) @@ -24,9 +19,9 @@ private ElementMetadata(Option mandatory, Option> dis Display = display; } - public static ElementMetadata CreateElementMetadata(JToken metadata) + private static ElementMetadata CreateElementMetadata(JToken metadata) { - var mandatory = metadata.GetByKey("mandatory").Match( + var mandatory = metadata.GetByKey(MandatoryJsonKey).Match( jToken => { var str = jToken.ToString(); @@ -35,7 +30,7 @@ public static ElementMetadata CreateElementMetadata(JToken metadata) _ => Option.None); var validDisplay = - from token in metadata.GetByKey("display") + from token in metadata.GetByKey(DisplayJsonKey) from array in token.ToJArray() select array; @@ -59,5 +54,32 @@ public static Validation> ValidEl .ToJObject() .OnSuccess(o => o.ToValidDictionaryAll( ElementIdentifier.ValidElementIdentifier, - token => ValidationFun.Valid(ElementMetadata.CreateElementMetadata(token)))); + token => ValidationFun.Valid(CreateElementMetadata(token)))); +} + +public static class ElementMetadataFun +{ + public const string MandatoryJsonKey = "mandatory"; + public const string DisplayJsonKey = "display"; + + public static JObject EncodeToJson(this ElementMetadata metadata) + { + var result = new JObject(); + + metadata.Mandatory.IfSome( + mandatory => result.Add(MandatoryJsonKey, mandatory) + ); + + metadata.Display.IfSome(displays => + { + var jArray = new JArray(); + foreach (var display in displays) + { + jArray.Add(display.EncodeToJson()); + } + result.Add(DisplayJsonKey, jArray); + }); + + return result; + } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementName.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementName.cs index 2d3623a2..908b87ab 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementName.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/ElementName.cs @@ -1,11 +1,8 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using WalletFramework.Core.Json.Converters; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct ElementName { private string Value { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/MdocConfiguration.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/MdocConfiguration.cs index 759148d4..a63b44c3 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/MdocConfiguration.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/MdocConfiguration.cs @@ -1,19 +1,16 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.MdocLib; using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.CryptographicCurve; using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.CryptographicSuite; using static WalletFramework.MdocLib.DocType; using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.Policy; -using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.MdocConfiguration.MdocConfigurationJsonKeys; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.MdocConfigurationFun; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; -[JsonConverter(typeof(MdocConfigurationJsonConverter))] public record MdocConfiguration { public CredentialConfiguration CredentialConfiguration { get; } @@ -91,56 +88,50 @@ public static Validation ValidMdocConfiguration(JObject confi .Apply(cryptographicCurvesSupported) .Apply(claims); } - - public static class MdocConfigurationJsonKeys - { - public const string DocTypeJsonKey = "doctype"; - public const string PolicyJsonKey = "policy"; - public const string CryptographicSuitesSupportedJsonKey = "cryptographic_suites_supported"; - public const string CryptographicCurvesSupportedJsonKey = "cryptographic_curves_supported"; - public const string ClaimsJsonKey = "claims"; - } } -public class MdocConfigurationJsonConverter : JsonConverter +public static class MdocConfigurationFun { - public override bool CanRead => false; + public const string DocTypeJsonKey = "doctype"; + public const string PolicyJsonKey = "policy"; + public const string CryptographicSuitesSupportedJsonKey = "cryptographic_suites_supported"; + public const string CryptographicCurvesSupportedJsonKey = "cryptographic_curves_supported"; + public const string ClaimsJsonKey = "claims"; - public override void WriteJson(JsonWriter writer, MdocConfiguration? mdocConfig, JsonSerializer serializer) + public static JObject EncodeToJson(this MdocConfiguration mdocConfig) { - writer.WriteStartObject(); - - var credentialConfig = JObject.FromObject(mdocConfig!.CredentialConfiguration); - foreach (var property in credentialConfig.Properties()) - { - property.WriteTo(writer); - } + var configJson = mdocConfig.CredentialConfiguration.EncodeToJson(); - serializer.Converters.Add(new OptionJsonConverter()); - serializer.Converters.Add(new OptionJsonConverter>()); - serializer.Converters.Add(new OptionJsonConverter>()); - serializer.Converters.Add(new OptionJsonConverter()); - serializer.Converters.Add(new ValueTypeJsonConverter()); + configJson.Add(DocTypeJsonKey, mdocConfig.DocType.ToString()); - writer.WritePropertyName(DocTypeJsonKey); - serializer.Serialize(writer, mdocConfig.DocType); + mdocConfig.Policy.IfSome(policy => + configJson.Add(PolicyJsonKey, policy.EncodeToJson()) + ); - writer.WritePropertyName(PolicyJsonKey); - serializer.Serialize(writer, mdocConfig.Policy); - - writer.WritePropertyName(CryptographicSuitesSupportedJsonKey); - serializer.Serialize(writer, mdocConfig.CryptographicSuitesSupported); + mdocConfig.CryptographicSuitesSupported.IfSome(suites => + { + var suitesJson = new JArray(); + foreach (var suite in suites) + { + suitesJson.Add(suite.ToString()); + } + configJson.Add(CryptographicSuitesSupportedJsonKey, suitesJson); + }); - writer.WritePropertyName(CryptographicCurvesSupportedJsonKey); - serializer.Serialize(writer, mdocConfig.CryptographicCurvesSupported); + mdocConfig.CryptographicCurvesSupported.IfSome(curves => + { + var curvesJson = new JArray(); + foreach (var curve in curves) + { + curvesJson.Add(curve.ToString()); + } + configJson.Add(CryptographicCurvesSupportedJsonKey, curvesJson); + }); - writer.WritePropertyName(ClaimsJsonKey); - serializer.Serialize(writer, mdocConfig.Claims); + mdocConfig.Claims.IfSome(claims => + configJson.Add(ClaimsJsonKey, claims.EncodeToJson()) + ); - writer.WriteEndObject(); + return configJson; } - - public override MdocConfiguration ReadJson(JsonReader reader, Type objectType, MdocConfiguration? existingValue, - bool hasExistingValue, JsonSerializer serializer) => - throw new NotImplementedException(); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/Policy.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/Policy.cs index 565b7dd6..654e0521 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/Policy.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Mdoc/Policy.cs @@ -1,23 +1,19 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Core.Json.Errors; using WalletFramework.MdocLib.Common; using WalletFramework.MdocVc.Common; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Errors; +using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.PolicyFun; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; public record Policy { - [JsonProperty("one_time_use")] public bool OneTimeUse { get; } - [JsonProperty("batch_size")] - [JsonConverter(typeof(OptionJsonConverter))] public Option BatchSize { get; } private Policy(bool oneTimeUse, Option batchSize) @@ -41,13 +37,13 @@ public static Validation ValidPolicy(JToken policy) } var oneTimeUse = jObject - .GetByKey("one_time_use") + .GetByKey(OneTimeUseJsonKey) .OnSuccess(token => { var str = token.ToString(); if (string.IsNullOrWhiteSpace(str)) { - return new FieldValueIsNullOrEmptyError("one_time_use").ToInvalid(); + return new FieldValueIsNullOrEmptyError(OneTimeUseJsonKey).ToInvalid(); } else { @@ -63,7 +59,7 @@ public static Validation ValidPolicy(JToken policy) }); var batchSize = jObject - .GetByKey("batch_size") + .GetByKey(BatchSizeJsonKey) .OnSuccess(token => { try @@ -83,3 +79,23 @@ public static Validation ValidPolicy(JToken policy) .Apply(batchSize); } } + +public static class PolicyFun +{ + public const string OneTimeUseJsonKey = "one_time_use"; + public const string BatchSizeJsonKey = "batch_size"; + + public static JObject EncodeToJson(this Policy policy) + { + var policyJson = new JObject + { + { OneTimeUseJsonKey, policy.OneTimeUse } + }; + + policy.BatchSize.IfSome(batchSize => + policyJson.Add(BatchSizeJsonKey, batchSize) + ); + + return policyJson; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeId.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeId.cs index 7ffb7ff0..7bf34c70 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeId.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeId.cs @@ -1,14 +1,11 @@ using System.Globalization; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Errors; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct ProofTypeId { private string Value { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeMetadata.cs index 300ef7ac..3c50be38 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeMetadata.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/ProofTypeMetadata.cs @@ -29,3 +29,20 @@ from jArray in jToken.ToJArray() from algValues in jArray.TraverseAny(ValidCryptograhicSigningAlgValue) select new ProofTypeMetadata(algValues.ToList()); } + +public static class ProofTypeMetadataFun +{ + public static JObject EncodeToJson(this ProofTypeMetadata proofTypeMetadata) + { + var result = new JObject(); + + var jArray = new JArray(); + foreach (var algValue in proofTypeMetadata.ProofSigningAlgValuesSupported) + { + jArray.Add(algValue.ToString()); + } + result.Add("proof_signing_alg_values_supported", jArray); + + return result; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Scope.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Scope.cs index 3a6d371c..5466d8d3 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Scope.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/Scope.cs @@ -1,14 +1,11 @@ using System.Globalization; using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct Scope { private string Value { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs index 8b0c0668..3e87132e 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SdJwt/SdJwtConfiguration.cs @@ -9,7 +9,6 @@ namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.SdJwt; -[JsonConverter(typeof(SdJwtConfigurationJsonConverter))] public record SdJwtConfiguration { public CredentialConfiguration CredentialConfiguration { get; } @@ -65,33 +64,24 @@ public static class SdJwtConfigurationJsonKeys } } -public class SdJwtConfigurationJsonConverter : JsonConverter +public static class SdJwtConfigurationFun { - public override bool CanRead => false; - - public override void WriteJson(JsonWriter writer, SdJwtConfiguration? sdJwtConfig, JsonSerializer serializer) + public static JObject EncodeToJson(this SdJwtConfiguration config) { - writer.WriteStartObject(); + var credentialConfig = config.CredentialConfiguration.EncodeToJson(); + + credentialConfig.Add(VctJsonName, config.Vct.ToString()); - var credentialConfig = JObject.FromObject(sdJwtConfig!.CredentialConfiguration, serializer); - foreach (var property in credentialConfig.Properties()) + if (config.Claims is not null) { - property.WriteTo(writer); + credentialConfig.Add(ClaimsJsonName, JObject.FromObject(config.Claims)); } - writer.WritePropertyName(ClaimsJsonName); - serializer.Serialize(writer, sdJwtConfig.Claims); - - writer.WritePropertyName(OrderJsonName); - serializer.Serialize(writer, sdJwtConfig.Order); + if (config.Order is not null) + { + credentialConfig.Add(OrderJsonName, JArray.FromObject(config.Order)); + } - writer.WritePropertyName(VctJsonName); - serializer.Serialize(writer, sdJwtConfig.Vct); - - writer.WriteEndObject(); + return credentialConfig; } - - public override SdJwtConfiguration ReadJson(JsonReader reader, Type objectType, SdJwtConfiguration? existingValue, - bool hasExistingValue, JsonSerializer serializer) => - throw new NotImplementedException(); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SupportedCredentialConfiguration.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SupportedCredentialConfiguration.cs index 826cb54e..9ddcdf29 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SupportedCredentialConfiguration.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredConfiguration/Models/SupportedCredentialConfiguration.cs @@ -1,12 +1,10 @@ -using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OneOf; -using WalletFramework.Core.Json.Converters; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.SdJwt; namespace WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; -[JsonConverter(typeof(OneOfJsonConverter))] public sealed class SupportedCredentialConfiguration : OneOfBase { public static implicit operator OneOf( @@ -29,3 +27,12 @@ private SupportedCredentialConfiguration(OneOf + config.Match( + sdJwt => sdJwt.EncodeToJson(), + mdoc => mdoc.EncodeToJson() + ); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialConfigurationId.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialConfigurationId.cs index d0010ec4..db4ea1b6 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialConfigurationId.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredOffer/Models/CredentialConfigurationId.cs @@ -1,14 +1,11 @@ using System.Globalization; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; namespace WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct CredentialConfigurationId { private string Value { get; } @@ -36,12 +33,3 @@ public static Validation ValidCredentialConfiguration } }); } - -public class CredentialConfigurationIdDecoder : IValueTypeDecoder -{ - public CredentialConfigurationId Decode(JToken token) => - CredentialConfigurationId - .ValidCredentialConfigurationId(token) - .UnwrapOrThrow(new InvalidOperationException("CredentialConfigurationId is corrupt")); -} - diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs index a72c0652..fcb1d2a4 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs @@ -1,6 +1,5 @@ using LanguageExt; using Newtonsoft.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models; @@ -16,7 +15,6 @@ public record CredentialRequest(Option Proof, Format Format) /// Gets the proof of possession of the key material the issued credential shall be bound to. /// [JsonProperty("proof")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Proof { get; } = Proof; /// diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocStorage.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocStorage.cs index 95cfc647..c28fa2fa 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocStorage.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Implementations/MdocStorage.cs @@ -22,7 +22,7 @@ public MdocStorage(IAgentProvider agentProvider, IWalletRecordService recordServ public async Task Add(MdocRecord record) { var context = await _agentProvider.GetContextAsync(); - await _recordService.AddAsync(context.Wallet, record); + await _recordService.AddAsync(context.Wallet, record, MdocRecordFun.EncodeToJson); return Unit.Default; } @@ -55,7 +55,7 @@ public async Task>> List( public async Task Update(MdocRecord record) { var context = await _agentProvider.GetContextAsync(); - await _recordService.Update(context.Wallet, record); + await _recordService.Update(context.Wallet, record, MdocRecordFun.EncodeToJson); return Unit.Default; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/CredentialIssuerId.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/CredentialIssuerId.cs index 12e1f3a3..9c2cc152 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/CredentialIssuerId.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/CredentialIssuerId.cs @@ -1,15 +1,12 @@ using System.Globalization; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Core.Uri; using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Errors; namespace WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct CredentialIssuerId { private Uri Value { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerMetadata.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerMetadata.cs index 082c0187..3ffb366b 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerMetadata.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerMetadata.cs @@ -1,8 +1,5 @@ using System.Globalization; -using System.Runtime.Serialization.Formatters.Binary; -using Hyperledger.Aries.Extensions; using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc; @@ -12,48 +9,40 @@ using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Uri; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; using static WalletFramework.Core.Functional.ValidationFun; using static WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models.AuthorizationServerId; using static WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models.CredentialIssuerId; using static WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models.CredentialConfigurationId; using static WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer.IssuerDisplay; +using static WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models.IssuerMetadataJsonExtensions; namespace WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; /// /// Represents the metadata of an OpenID4VCI Credential Issuer. /// -[JsonConverter(typeof(IssuerMetadataJsonConverter))] public record IssuerMetadata { - // Do not change the order of this property (must be last) otherwise the JSON serialization will not - // work properly... /// /// Gets a dictionary which maps a CredentialConfigurationId to its credential metadata. /// - [JsonProperty(CredentialConfigsSupportedJsonKey)] - [JsonConverter(typeof(DictJsonConverter))] public Dictionary CredentialConfigurationsSupported { get; } /// /// Gets a list of display properties of a Credential Issuer for different languages. /// - [JsonProperty(DisplayJsonKey)] - [JsonConverter(typeof(OptionJsonConverter>))] public Option> Display { get; } /// /// Gets the URL of the Credential Issuer's Credential Endpoint. /// - [JsonProperty(CredentialEndpointJsonKey)] public Uri CredentialEndpoint { get; } /// /// Gets the identifier of the Credential Issuer. /// - [JsonProperty(CredentialIssuerJsonKey)] public CredentialIssuerId CredentialIssuer { get; } /// @@ -63,10 +52,8 @@ public record IssuerMetadata /// identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server /// metadata. /// - [JsonProperty(AuthorizationServersJsonKey)] - [JsonConverter(typeof(OptionJsonConverter>))] public Option> AuthorizationServers { get; } - + private IssuerMetadata( Dictionary credentialConfigurationsSupported, Option> display, @@ -165,37 +152,55 @@ from serverIds in jArray .Apply(credentialIssuerId) .Apply(authServers); } - - private const string CredentialConfigsSupportedJsonKey = "credential_configurations_supported"; - private const string DisplayJsonKey = "display"; - private const string CredentialEndpointJsonKey = "credential_endpoint"; - private const string CredentialIssuerJsonKey = "credential_issuer"; - private const string AuthorizationServersJsonKey = "authorization_servers"; } -public sealed class IssuerMetadataJsonConverter : JsonConverter +public static class IssuerMetadataJsonExtensions { - public override bool CanWrite => false; - - public override void WriteJson(JsonWriter writer, IssuerMetadata? value, JsonSerializer serializer) => - throw new NotImplementedException(); - - public override IssuerMetadata ReadJson( - JsonReader reader, - Type objectType, - IssuerMetadata? existingValue, - bool hasExistingValue, - JsonSerializer serializer) + public const string CredentialConfigsSupportedJsonKey = "credential_configurations_supported"; + public const string DisplayJsonKey = "display"; + public const string CredentialEndpointJsonKey = "credential_endpoint"; + public const string CredentialIssuerJsonKey = "credential_issuer"; + public const string AuthorizationServersJsonKey = "authorization_servers"; + + public static JObject EncodeToJson(this IssuerMetadata issuerMetadata) { - var json = JObject.Load(reader); + var result = new JObject(); - var result = IssuerMetadata - .ValidIssuerMetadata(json) - .Match( - metadata => metadata, - errors => - throw new InvalidOperationException($"IssuerMetadata is corrupt. Errors: {errors}") + var configsJson = new JObject(); + foreach (var (key, config) in issuerMetadata.CredentialConfigurationsSupported) + { + var configJson = config.Match( + sdJwt => sdJwt.EncodeToJson(), + mdoc => mdoc.EncodeToJson() ); + + configsJson.Add(key.ToString(), configJson); + } + result.Add(CredentialConfigsSupportedJsonKey, configsJson); + + issuerMetadata.Display.IfSome(displays => + { + var displaysJson = new JArray(); + foreach (var display in displays) + { + displaysJson.Add(display.EncodeToJson()); + } + result.Add(DisplayJsonKey, displaysJson); + }); + + // TODO: ValueTypeEncodeFunc? + result.Add(CredentialEndpointJsonKey, issuerMetadata.CredentialEndpoint.ToStringWithoutTrail()); + result.Add(CredentialIssuerJsonKey, issuerMetadata.CredentialIssuer.ToString()); + + var authServersJson = new JArray(); + issuerMetadata.AuthorizationServers.IfSome(servers => + { + foreach (var server in servers) + { + authServersJson.Add(server.ToString()); + } + }); + result.Add(AuthorizationServersJsonKey, authServersJson); return result; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerName.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerName.cs index 86b59bf3..3c531f0d 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerName.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Issuer/Models/IssuerName.cs @@ -1,11 +1,8 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using WalletFramework.Core.Json.Converters; namespace WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct IssuerName { private string Value { get; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerDisplay.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerDisplay.cs index 9c6ce986..a580a47d 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerDisplay.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerDisplay.cs @@ -1,14 +1,13 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; using WalletFramework.Core.Localization; using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; using static WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models.IssuerName; using static WalletFramework.Core.Localization.Locale; using static WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer.IssuerLogo; +using static WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer.IssuerDisplayFun; namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; @@ -20,22 +19,16 @@ public record IssuerDisplay /// /// Gets the name of the Issuer /// - [JsonProperty("name")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Name { get; } /// /// Gets the locale which represents the specific culture or region /// - [JsonProperty("locale")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Locale { get; } /// /// Gets the logo of the Issuer /// - [JsonProperty("logo")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Logo { get; } private IssuerDisplay( @@ -50,9 +43,9 @@ private IssuerDisplay( public static Option OptionalIssuerDisplay(JToken display) => display.ToJObject().ToOption().OnSome(jObject => { - var name = jObject.GetByKey("name").ToOption().OnSome(OptionIssuerName); - var locale = jObject.GetByKey("locale").OnSuccess(ValidLocale).ToOption(); - var logo = jObject.GetByKey("logo").ToOption().OnSome(OptionalIssuerLogo); + var name = jObject.GetByKey(NameJsonKey).ToOption().OnSome(OptionIssuerName); + var locale = jObject.GetByKey(LocaleJsonKey).OnSuccess(ValidLocale).ToOption(); + var logo = jObject.GetByKey(LogoJsonKey).ToOption().OnSome(OptionalIssuerLogo); if (name.IsNone && locale.IsNone && logo.IsNone) return Option.None; @@ -60,3 +53,19 @@ public static Option OptionalIssuerDisplay(JToken display) => dis return new IssuerDisplay(name, locale, logo); }); } + +public static class IssuerDisplayFun +{ + public const string NameJsonKey = "name"; + public const string LocaleJsonKey = "locale"; + public const string LogoJsonKey = "logo"; + + public static JObject EncodeToJson(this IssuerDisplay display) + { + var json = new JObject(); + display.Name.IfSome(name => json.Add(NameJsonKey, name.ToString())); + display.Locale.IfSome(locale => json.Add(LocaleJsonKey, locale.ToString())); + display.Logo.IfSome(logo => json.Add(LogoJsonKey, logo.EncodeToJson())); + return json; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerLogo.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerLogo.cs index fa4cf257..dc0c8438 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerLogo.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Models/Metadata/Issuer/IssuerLogo.cs @@ -1,9 +1,9 @@ using LanguageExt; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; +using WalletFramework.Core.Uri; +using static WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer.IssuerLogoFun; namespace WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; @@ -15,15 +15,11 @@ public record IssuerLogo /// /// Gets the alternate text that describes the logo image. This is typically used for accessibility purposes. /// - [JsonProperty("alt_text")] - [JsonConverter(typeof(OptionJsonConverter))] public Option AltText { get; } /// /// Gets the URL of the logo image. /// - [JsonProperty("uri")] - [JsonConverter(typeof(OptionJsonConverter))] public Option Uri { get; } private IssuerLogo( @@ -36,7 +32,7 @@ private IssuerLogo( public static Option OptionalIssuerLogo(JToken logo) => logo.ToJObject().ToOption().OnSome(jObject => { - var altText = jObject.GetByKey("alt_text").ToOption().OnSome(text => + var altText = jObject.GetByKey(AltTextJsonKey).ToOption().OnSome(text => { var str = text.ToString(); if (string.IsNullOrWhiteSpace(str)) @@ -45,7 +41,7 @@ public static Option OptionalIssuerLogo(JToken logo) => logo.ToJObje return str; }); - var imageUri = jObject.GetByKey("uri").ToOption().OnSome(uri => + var imageUri = jObject.GetByKey(UriJsonKey).ToOption().OnSome(uri => { try { @@ -65,3 +61,17 @@ public static Option OptionalIssuerLogo(JToken logo) => logo.ToJObje return new IssuerLogo(altText, imageUri); }); } + +public static class IssuerLogoFun +{ + public const string AltTextJsonKey = "alt_text"; + public const string UriJsonKey = "uri"; + + public static JObject EncodeToJson(this IssuerLogo logo) + { + var json = new JObject(); + logo.AltText.IfSome(altText => json.Add(AltTextJsonKey, altText)); + logo.Uri.IfSome(uri => json.Add(UriJsonKey, uri.ToStringWithoutTrail())); + return json; + } +} diff --git a/src/WalletFramework.SdJwtVc/Models/Vct.cs b/src/WalletFramework.SdJwtVc/Models/Vct.cs index eee00c2b..6c1a0370 100644 --- a/src/WalletFramework.SdJwtVc/Models/Vct.cs +++ b/src/WalletFramework.SdJwtVc/Models/Vct.cs @@ -1,14 +1,11 @@ using System.Globalization; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.Core.Functional.Errors; using WalletFramework.Core.Json; -using WalletFramework.Core.Json.Converters; namespace WalletFramework.SdJwtVc.Models; -[JsonConverter(typeof(ValueTypeJsonConverter))] public readonly struct Vct { private string Value { get; } diff --git a/test/WalletFramework.MdocVc.Tests/MdocRecordTests.cs b/test/WalletFramework.MdocVc.Tests/MdocRecordTests.cs index 32c722f0..cb988ae0 100644 --- a/test/WalletFramework.MdocVc.Tests/MdocRecordTests.cs +++ b/test/WalletFramework.MdocVc.Tests/MdocRecordTests.cs @@ -2,7 +2,6 @@ using Hyperledger.Aries.Storage; using LanguageExt; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using WalletFramework.Core.Functional; using WalletFramework.MdocLib; using Xunit; @@ -18,10 +17,10 @@ public void Can_Encode_To_Json() var mdoc = Mdoc.ValidMdoc(encodedMdoc).UnwrapOrThrow(new InvalidOperationException("Mdoc sample is corrupt")); var record = mdoc.ToRecord(Option>.None); - var sut = JObject.FromObject(record); + var sut = record.EncodeToJson(); sut[nameof(RecordBase.Id)]!.ToString().Should().Be(record.Id); - sut[MdocRecordJsonKeys.MdocJsonKey]!.ToString().Should().Be(encodedMdoc); + sut[MdocRecordFun.MdocJsonKey]!.ToString().Should().Be(encodedMdoc); } [Fact] @@ -29,7 +28,7 @@ public void Can_Decode_From_Json() { var json = MdocVcSamples.MdocRecordJson; - var sut = JsonConvert.DeserializeObject(json.ToString())!; + var sut = MdocRecordFun.DecodeFromJson(json); sut.Mdoc.DocType.ToString().Should().Be(MdocLib.Tests.Samples.DocType); } diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs index b5bafb9a..55b3a7f5 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs @@ -53,7 +53,7 @@ public void Can_Encode_To_Json() var record = new AuthFlowSessionRecord(authorizationData, authorizationCodeParameters, sessionId); // Act - var recordSut = JObject.FromObject(record); + var recordSut = record.EncodeToJson(); var tagsSut = JObject.FromObject(record.Tags); // Assert @@ -68,7 +68,7 @@ public void Can_Decode_From_Json() var json = AuthFlowSamples.AuthFlowSessionRecordJson; // Act - var record = JsonConvert.DeserializeObject(json.ToString()); + var record = AuthFlowSessionRecordFun.DecodeFromJson(json); // Assert record.Should().NotBeNull(); diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs index 2bf810b1..030ad6a4 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs @@ -7,16 +7,16 @@ public static class AuthFlowSamples { public static JObject AuthFlowSessionRecordJson => new() { - ["AuthorizationData"] = new JObject + ["authorization_data"] = new JObject { - ["ClientOptions"] = new JObject + ["client_options"] = new JObject { ["ClientId"] = "https://test-issuer.com/redirect", ["WalletIssuer"] = "i can write anything", ["RedirectUri"] = "https://test-issuer.com/redirect" }, - ["IssuerMetadata"] = IssuerMetadataSample.EncodedAsJson, - ["AuthorizationServerMetadata"] = new JObject + ["issuer_metadata"] = IssuerMetadataSample.EncodedAsJson, + ["authorization_server_metadata"] = new JObject { ["issuer"] = "i can write anything", ["token_endpoint"] = "i can write anything", @@ -24,9 +24,9 @@ public static class AuthFlowSamples ["authorization_endpoint"] = "i can write anything", ["response_types_supported"] = new JArray("i can write anything"), }, - ["CredentialConfigurationIds"] = new JArray("org.iso.18013.5.1.mDL") + ["credential_configuration_ids"] = new JArray("org.iso.18013.5.1.mDL") }, - ["AuthorizationCodeParameters"] = new JObject + ["authorization_code_parameters"] = new JObject { ["Challenge"] = "hello", ["CodeChallengeMethod"] = "S256", diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs index 62e8cd54..9f70e9a1 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs @@ -48,9 +48,9 @@ public void Can_Decode_From_Json() [Fact] public void Can_Encode_To_Json() { - var decoded = IssuerMetadataSample.Decoded; + var issuerMetadata = IssuerMetadataSample.Decoded; - var sut = JObject.FromObject(decoded).RemoveNulls().ToObject(); + var sut = issuerMetadata.EncodeToJson(); sut.Should().BeEquivalentTo(IssuerMetadataSample.EncodedAsJson); } @@ -64,21 +64,11 @@ public void Can_Decode_And_Encode_From_Json() // Act ValidIssuerMetadata(sample).Match( // Assert - sut => + issuerMetadata => { - var encoded = JObject.FromObject(sut).RemoveNulls().ToObject(); - encoded.Should().BeEquivalentTo(sample); + var sut = issuerMetadata.EncodeToJson(); + sut.Should().BeEquivalentTo(sample); }, _ => Assert.Fail("IssuerMetadata must be valid")); } - - [Fact] - public void Can_Decode_From_Persisted_Json() - { - var sample = IssuerMetadataSample.EncodedAsJson; - - var sut = JsonConvert.DeserializeObject(sample.ToString())!; - - sut.CredentialIssuer.ToString().Should().Be(IssuerMetadataSample.CredentialIssuer.ToStringWithoutTrail()); - } } From c70be2324c8412d8039b7c416b01086d48534025 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 18 Jul 2024 12:23:51 +0200 Subject: [PATCH 5/8] fix credential request json Signed-off-by: Kevin --- .../CredentialRequestService.cs | 2 +- .../CredRequest/Models/CredentialRequest.cs | 24 +++++++-- .../Models/Mdoc/MdocCredentialRequest.cs | 11 +--- .../Models/SdJwt/SdJwtCredentialRequest.cs | 15 ++---- .../AuthFlow/AuthFlowSessionRecordTests.cs | 2 +- .../AuthFlow/Samples/AuthFlowSamples.cs | 2 +- .../{ => Mdoc}/MdocConfigurationTests.cs | 4 +- .../Mdoc/Samples}/MdocConfigurationSample.cs | 2 +- .../Samples}/SdJwtConfigurationSample.cs | 2 +- .../{ => SdJwt}/SdJwtConfigurationTests.cs | 4 +- .../Oid4Vci/CredOffer/CredentialOfferTests.cs | 2 +- .../Samples/CredentialOfferSample.cs | 2 +- .../CredRequest/CredentialRequestTests.cs | 10 ++++ .../Oid4Vci/Issuer/IssuerMetadataTests.cs | 6 +-- .../Samples/IssuerMetadataSample.cs | 6 +-- .../Oid4Vci/IssuerMetadataTests.cs | 54 ------------------- .../WalletFramework.Oid4Vc.Tests.csproj | 5 -- 17 files changed, 53 insertions(+), 100 deletions(-) rename test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/{ => Mdoc}/MdocConfigurationTests.cs (97%) rename test/WalletFramework.Oid4Vc.Tests/Oid4Vci/{Samples/Mdoc => CredConfiguration/Mdoc/Samples}/MdocConfigurationSample.cs (98%) rename test/WalletFramework.Oid4Vc.Tests/Oid4Vci/{Samples/SdJwt => CredConfiguration/SdJwt/Samples}/SdJwtConfigurationSample.cs (97%) rename test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/{ => SdJwt}/SdJwtConfigurationTests.cs (81%) rename test/WalletFramework.Oid4Vc.Tests/Oid4Vci/{ => CredOffer}/Samples/CredentialOfferSample.cs (95%) create mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredRequest/CredentialRequestTests.cs rename test/WalletFramework.Oid4Vc.Tests/Oid4Vci/{ => Issuer}/Samples/IssuerMetadataSample.cs (91%) delete mode 100644 test/WalletFramework.Oid4Vc.Tests/Oid4Vci/IssuerMetadataTests.cs diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs index 733c7738..dd9be71c 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs @@ -86,7 +86,7 @@ async Task> ICredentialRequestService.RequestCred clientOptions); var result = new SdJwtCredentialRequest(vciRequest, sdJwt.Vct); - return result.AsJson(); + return result.EncodeToJson(); }, async mdoc => { diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs index fcb1d2a4..56afadd2 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/CredentialRequest.cs @@ -1,5 +1,5 @@ using LanguageExt; -using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models; @@ -14,12 +14,30 @@ public record CredentialRequest(Option Proof, Format Format) /// /// Gets the proof of possession of the key material the issued credential shall be bound to. /// - [JsonProperty("proof")] public Option Proof { get; } = Proof; /// /// Gets the format of the credential to be issued. /// - [JsonProperty("format")] public Format Format { get; } = Format; } + +public static class CredentialRequestFun +{ + private const string ProofJsonKey = "proof"; + private const string FormatJsonKey = "format"; + + public static JObject EncodeToJson(this CredentialRequest request) + { + var result = new JObject(); + + request.Proof.IfSome(proof => + { + result.Add(ProofJsonKey, JObject.FromObject(proof)); + }); + + result.Add(FormatJsonKey, request.Format.ToString()); + + return result; + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/Mdoc/MdocCredentialRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/Mdoc/MdocCredentialRequest.cs index 15b9f8cc..060fb544 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/Mdoc/MdocCredentialRequest.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/Mdoc/MdocCredentialRequest.cs @@ -27,16 +27,9 @@ public static class MdocCredentialRequestFun { public static string AsJson(this MdocCredentialRequest request) { - var json = new JObject(); - - var vciRequest = JObject.FromObject(request.VciRequest); - foreach (var property in vciRequest.Properties()) - { - json.Add(property); - } + var json = request.VciRequest.EncodeToJson(); - var vct = JToken.FromObject(request.DocType); - json.Add("doctype", vct); + json.Add("doctype", request.DocType.ToString()); return json.ToString(); } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/SdJwt/SdJwtCredentialRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/SdJwt/SdJwtCredentialRequest.cs index 088c48a9..693e3dca 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/SdJwt/SdJwtCredentialRequest.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Models/SdJwt/SdJwtCredentialRequest.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.SdJwtVc.Models; @@ -11,7 +10,6 @@ public record SdJwtCredentialRequest /// /// Gets the verifiable credential type (vct). /// - [JsonProperty("vct")] public Vct Vct { get; } internal SdJwtCredentialRequest(CredentialRequest vciRequest, Vct vct) @@ -23,18 +21,11 @@ internal SdJwtCredentialRequest(CredentialRequest vciRequest, Vct vct) public static class SdJwtCredentialRequestFun { - public static string AsJson(this SdJwtCredentialRequest request) + public static string EncodeToJson(this SdJwtCredentialRequest request) { - var json = new JObject(); - - var vciRequest = JObject.FromObject(request.VciRequest); - foreach (var property in vciRequest.Properties()) - { - json.Add(property); - } + var json = request.VciRequest.EncodeToJson(); - var vct = JToken.FromObject(request.Vct); - json.Add("vct", vct); + json.Add("vct", request.Vct.ToString()); return json.ToString(); } diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs index 55b3a7f5..e69c767a 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/AuthFlowSessionRecordTests.cs @@ -9,7 +9,7 @@ using WalletFramework.Oid4Vc.Oid4Vci.Authorization.Models; using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; using WalletFramework.Oid4Vc.Tests.Oid4Vci.AuthFlow.Samples; -using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Issuer.Samples; namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.AuthFlow; diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs index 030ad6a4..4cdc1406 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/AuthFlow/Samples/AuthFlowSamples.cs @@ -1,5 +1,5 @@ using Newtonsoft.Json.Linq; -using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Issuer.Samples; namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.AuthFlow.Samples; diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/MdocConfigurationTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/Mdoc/MdocConfigurationTests.cs similarity index 97% rename from test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/MdocConfigurationTests.cs rename to test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/Mdoc/MdocConfigurationTests.cs index 30916c93..51e09c4e 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/MdocConfigurationTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/Mdoc/MdocConfigurationTests.cs @@ -1,10 +1,10 @@ using FluentAssertions; using WalletFramework.Core.Functional; using WalletFramework.Core.Json.Errors; -using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.Mdoc; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration.Mdoc.Samples; using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Mdoc.MdocConfiguration; -namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration; +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration.Mdoc; public class MdocConfigurationTests { diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/Mdoc/MdocConfigurationSample.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/Mdoc/Samples/MdocConfigurationSample.cs similarity index 98% rename from test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/Mdoc/MdocConfigurationSample.cs rename to test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/Mdoc/Samples/MdocConfigurationSample.cs index 4036c1f7..bad19de3 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/Mdoc/MdocConfigurationSample.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/Mdoc/Samples/MdocConfigurationSample.cs @@ -13,7 +13,7 @@ using static WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Format; using static WalletFramework.MdocLib.NameSpace; -namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.Mdoc; +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration.Mdoc.Samples; public static class MdocConfigurationSample { diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/SdJwt/SdJwtConfigurationSample.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwt/Samples/SdJwtConfigurationSample.cs similarity index 97% rename from test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/SdJwt/SdJwtConfigurationSample.cs rename to test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwt/Samples/SdJwtConfigurationSample.cs index c73b3b20..0c66397a 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/SdJwt/SdJwtConfigurationSample.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwt/Samples/SdJwtConfigurationSample.cs @@ -3,7 +3,7 @@ using WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models; using WalletFramework.SdJwtVc.Models; -namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.SdJwt; +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration.SdJwt.Samples; public static class SdJwtConfigurationSample { diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwtConfigurationTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwt/SdJwtConfigurationTests.cs similarity index 81% rename from test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwtConfigurationTests.cs rename to test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwt/SdJwtConfigurationTests.cs index eccf508e..a0c88456 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwtConfigurationTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredConfiguration/SdJwt/SdJwtConfigurationTests.cs @@ -1,10 +1,10 @@ -namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration; +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration.SdJwt; public class SdJwtConfigurationTests { + // TODO: Implement this [Fact] public void Can_Parse() { - } } diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredOffer/CredentialOfferTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredOffer/CredentialOfferTests.cs index 1d630c3d..24514f10 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredOffer/CredentialOfferTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredOffer/CredentialOfferTests.cs @@ -5,7 +5,7 @@ using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Errors; using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; using static WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models.CredentialOffer; -using static WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.CredentialOfferSample; +using static WalletFramework.Oid4Vc.Tests.Oid4Vci.CredOffer.Samples.CredentialOfferSample; namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredOffer; diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/CredentialOfferSample.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredOffer/Samples/CredentialOfferSample.cs similarity index 95% rename from test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/CredentialOfferSample.cs rename to test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredOffer/Samples/CredentialOfferSample.cs index 6bdd38b2..b1ba9a77 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/CredentialOfferSample.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredOffer/Samples/CredentialOfferSample.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json.Linq; -namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples; +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredOffer.Samples; public static class CredentialOfferSample { diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredRequest/CredentialRequestTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredRequest/CredentialRequestTests.cs new file mode 100644 index 00000000..c0c45ee0 --- /dev/null +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/CredRequest/CredentialRequestTests.cs @@ -0,0 +1,10 @@ +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.CredRequest; + +public class CredentialRequestTests +{ + [Fact] + public void Can_Encode_To_Json() + { + + } +} diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs index 9f70e9a1..5c8ad5e9 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/IssuerMetadataTests.cs @@ -5,9 +5,9 @@ using WalletFramework.Core.Json; using WalletFramework.Core.Uri; using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; -using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples; -using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.Mdoc; -using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.SdJwt; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration.Mdoc.Samples; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration.SdJwt.Samples; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.Issuer.Samples; using static WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models.IssuerMetadata; namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Issuer; diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/IssuerMetadataSample.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/Samples/IssuerMetadataSample.cs similarity index 91% rename from test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/IssuerMetadataSample.cs rename to test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/Samples/IssuerMetadataSample.cs index 6c4ab17c..69442908 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Samples/IssuerMetadataSample.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/Issuer/Samples/IssuerMetadataSample.cs @@ -3,10 +3,10 @@ using WalletFramework.Core.Uri; using WalletFramework.Oid4Vc.Oid4Vci.CredOffer.Models; using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; -using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.Mdoc; -using WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.SdJwt; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration.Mdoc.Samples; +using WalletFramework.Oid4Vc.Tests.Oid4Vci.CredConfiguration.SdJwt.Samples; -namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples; +namespace WalletFramework.Oid4Vc.Tests.Oid4Vci.Issuer.Samples; public static class IssuerMetadataSample { diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/IssuerMetadataTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/IssuerMetadataTests.cs deleted file mode 100644 index 22d08d58..00000000 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vci/IssuerMetadataTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -// using FluentAssertions; -// using Hyperledger.Aries.Extensions; -// using Newtonsoft.Json.Linq; -// using WalletFramework.Oid4Vc.Oid4Vci.Models.Metadata.Issuer; -// using static WalletFramework.Oid4Vc.Tests.Oid4Vci.Samples.IssuerMetadataSample; -// -// -// namespace WalletFramework.Oid4Vc.Tests.Oid4Vci -// { -// public class IssuerMetadataTests -// { -// [Fact] -// public void Additional_Or_Unrecognized_Fields_Are_Ignored_During_Deserialization() -// { -// var json = IssuerMetadataJson; -// var jObject = JObject.Parse(json); -// jObject["Additional_Field"] = "Additional_Field"; -// -// var sut = jObject.ToObject(); -// -// sut.Should().BeOfType(); -// sut!.CredentialIssuer.Should().Be(CredentialIssuer); -// sut.CredentialEndpoint.Should().Be(CredentialEndpoint); -// } -// -// [Theory] -// [InlineData("credential_issuer")] -// [InlineData("credential_endpoint")] -// [InlineData("credential_configurations_supported")] -// public void Deserialization_Fails_When_Required_Fields_Are_Missing(string fieldName) -// { -// // Arrange -// var json = IssuerMetadataJson; -// -// var jObject = JObject.Parse(json); -// -// jObject[fieldName] = null; -// -// Assert.Throws(() => jObject.ToObject()); -// } -// -// [Fact] -// public void Valid_Json_Deserializes_To_Model() -// { -// var json = IssuerMetadataJson; -// -// var sut = json.ToObject(); -// -// sut.Should().BeOfType(); -// sut.CredentialIssuer.Should().Be(CredentialIssuer); -// sut.CredentialEndpoint.Should().Be(CredentialEndpoint); -// } -// } -// } diff --git a/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj b/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj index c8e8b679..9c99ced5 100644 --- a/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj +++ b/test/WalletFramework.Oid4Vc.Tests/WalletFramework.Oid4Vc.Tests.csproj @@ -37,9 +37,4 @@ - - - - - From 4b93c38ca90042d02f3a629897b39e4e15ac1170 Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Tue, 23 Jul 2024 13:28:46 +0200 Subject: [PATCH 6/8] refactor key store Signed-off-by: Johannes Tuerk --- .../Cryptography/Abstractions/IKeyStore.cs | 39 ---------- .../Models/ClientAttestationPopDetails.cs} | 16 +++-- .../Models/CombinedWalletAttestation.cs | 61 ++++++++++++++++ .../DPop/Implementations/DPopHttpClient.cs | 48 ++++++++++++- .../CredentialRequestService.cs | 8 ++- .../ISdJwtSignerService.cs | 10 +++ .../SdJwtSignerService.cs | 71 +++++++++++++++++++ .../SdJwtVcHolderService.cs | 16 ++--- .../Oid4Vp/Oid4VpClientServiceTests.cs | 6 +- .../SdJwtVcHolderServiceTests.cs | 4 +- 10 files changed, 215 insertions(+), 64 deletions(-) rename src/{WalletFramework.SdJwtVc/Models/ClientAttestationPopOptions.cs => WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/ClientAttestationPopDetails.cs} (56%) create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CombinedWalletAttestation.cs create mode 100644 src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtSignerService.cs create mode 100644 src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtSignerService.cs diff --git a/src/WalletFramework.Core/Cryptography/Abstractions/IKeyStore.cs b/src/WalletFramework.Core/Cryptography/Abstractions/IKeyStore.cs index 7459d6d2..903380ed 100644 --- a/src/WalletFramework.Core/Cryptography/Abstractions/IKeyStore.cs +++ b/src/WalletFramework.Core/Cryptography/Abstractions/IKeyStore.cs @@ -16,45 +16,6 @@ public interface IKeyStore /// A representing the generated key's identifier as a string. Task GenerateKey(string alg = "ES256"); - /// - /// Asynchronously creates a proof of possession for a specific key, based on the provided audience and nonce. - /// - /// The identifier of the key to be used in creating the proof of possession. - /// The intended recipient of the proof. Typically represents the entity that will verify it. - /// - /// A unique token, typically used to prevent replay attacks by ensuring that the proof is only used once. - /// - /// The type of the proof. (For example "openid4vci-proof+jwt") - /// Base64url-encoded hash digest over the Issuer-signed JWT and the selected Disclosures for integrity protection - /// - /// A representing the asynchronous operation. When evaluated, the task's result contains - /// the proof. - /// - Task GenerateKbProofOfPossessionAsync( - KeyId keyId, - string audience, - string nonce, - string type, - string? sdHash = null, - string? clientId = null); - - /// - /// Asynchronously creates a DPoP Proof JWT for a specific key, based on the provided audience, nonce and access token. - /// - /// The identifier of the key to be used in creating the proof of possession. - /// The intended recipient of the proof. Typically represents the entity that will verify it. - /// A unique token, typically used to prevent replay attacks by ensuring that the proof is only used once. - /// The access token, that the DPoP Proof JWT is bound to - /// - /// A representing the asynchronous operation. When evaluated, the task's result contains - /// the DPoP Proof JWT. - /// - Task GenerateDPopProofOfPossessionAsync( - KeyId keyId, - string audience, - string? nonce, - string? accessToken); - /// /// Asynchronously loads a key by its identifier and returns it as a JSON Web Key (JWK) containing the public key /// information. diff --git a/src/WalletFramework.SdJwtVc/Models/ClientAttestationPopOptions.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/ClientAttestationPopDetails.cs similarity index 56% rename from src/WalletFramework.SdJwtVc/Models/ClientAttestationPopOptions.cs rename to src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/ClientAttestationPopDetails.cs index 2fd9fc95..fe5c8597 100644 --- a/src/WalletFramework.SdJwtVc/Models/ClientAttestationPopOptions.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/ClientAttestationPopDetails.cs @@ -1,28 +1,30 @@ +using LanguageExt; using static System.String; -namespace WalletFramework.SdJwtVc.Models; +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; -public record ClientAttestationPopOptions +public record ClientAttestationPopDetails { public string Audience { get; } + public string Issuer { get; } - //TODO: Change nullable to Option<> when available - public string? Nonce { get; } - private ClientAttestationPopOptions(string audience, string issuer, string? nonce) + public Option Nonce { get; } + + private ClientAttestationPopDetails(string audience, string issuer, string? nonce) { Audience = audience; Issuer = issuer; Nonce = nonce; } - public static ClientAttestationPopOptions CreateClientAttestationPopOptions(string audience, string issuer, string? nonce) + public static ClientAttestationPopDetails CreateClientAttestationPopOptions(string audience, string issuer, string? nonce) { if (IsNullOrWhiteSpace(audience) || IsNullOrWhiteSpace(issuer)) { throw new ArgumentException("Audience and Issuer must be provided."); } - return new ClientAttestationPopOptions(audience, issuer, nonce); + return new ClientAttestationPopDetails(audience, issuer, nonce); } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CombinedWalletAttestation.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CombinedWalletAttestation.cs new file mode 100644 index 00000000..b81abc04 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CombinedWalletAttestation.cs @@ -0,0 +1,61 @@ +using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +public record CombinedWalletAttestation +{ + public WalletInstanceAttestationJwt WalletInstanceAttestationJwt { get; } + + public WalletInstanceAttestationPopJwt WalletInstanceAttestationPopJwt { get; } + + private CombinedWalletAttestation( + WalletInstanceAttestationJwt walletInstanceAttestationJwt, + WalletInstanceAttestationPopJwt walletInstanceAttestationPopJwt) + { + WalletInstanceAttestationJwt = walletInstanceAttestationJwt; + WalletInstanceAttestationPopJwt = walletInstanceAttestationPopJwt; + } + + public static CombinedWalletAttestation Create( + WalletInstanceAttestationJwt walletInstanceAttestationJwt, + WalletInstanceAttestationPopJwt walletInstanceAttestationPopJwt) + => new(walletInstanceAttestationJwt, walletInstanceAttestationPopJwt); +} + +public static class CombinedWalletAttestationExtensions +{ + public static string ToStringRepresentation(this CombinedWalletAttestation combinedWalletAttestation) + => combinedWalletAttestation.WalletInstanceAttestationJwt + "~" + + combinedWalletAttestation.WalletInstanceAttestationPopJwt; +} + +public struct WalletInstanceAttestationPopJwt +{ + public string Value { get; } + + public static implicit operator string(WalletInstanceAttestationPopJwt keyId) => keyId.Value; + + private WalletInstanceAttestationPopJwt(string value) => Value = value; + + public static WalletInstanceAttestationPopJwt CreateWalletInstanceAttestationPopJwt(string value) => new (value); +} + +public struct WalletInstanceAttestationJwt +{ + public string Value { get; } + + public static implicit operator string(WalletInstanceAttestationJwt keyId) => keyId.Value; + + private WalletInstanceAttestationJwt(string value) => Value = value; + + public static Validation ValidWalletInstanceAttestationJwt(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return new StringIsNullOrWhitespaceError(); + } + + return new WalletInstanceAttestationJwt(value); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs index 6e747ce9..1141675f 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/Authorization/DPop/Implementations/DPopHttpClient.cs @@ -1,11 +1,17 @@ +using System.Security.Cryptography; +using System.Text; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WalletFramework.Core.Cryptography.Abstractions; +using WalletFramework.Core.Cryptography.Models; using WalletFramework.Core.Functional; using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Abstractions; using WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Models; using WalletFramework.Oid4Vc.Oid4Vci.Exceptions; using WalletFramework.Oid4Vc.Oid4Vci.Extensions; +using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; namespace WalletFramework.Oid4Vc.Oid4Vci.Authorization.DPop.Implementations; @@ -18,14 +24,17 @@ public class DPopHttpClient : IDPopHttpClient public DPopHttpClient( IHttpClientFactory httpClientFactory, IKeyStore keyStore, + ISdJwtSignerService sdJwtSignerService, ILogger logger) { _keyStore = keyStore; + _sdJwtSignerService = sdJwtSignerService; _httpClient = httpClientFactory.CreateClient(); _logger = logger; } private readonly IKeyStore _keyStore; + private readonly ISdJwtSignerService _sdJwtSignerService; private readonly ILogger _logger; private readonly HttpClient _httpClient; @@ -34,7 +43,7 @@ public async Task Post( HttpContent content, DPopConfig config) { - var dPop = await _keyStore.GenerateDPopProofOfPossessionAsync( + var dPop = await GenerateDPopAsync( config.KeyId, config.Audience, config.Nonce.ToNullable(), @@ -55,7 +64,7 @@ public async Task Post( { config = config with { Nonce = new DPopNonce(nonceStr) }; - var newDpop = await _keyStore.GenerateDPopProofOfPossessionAsync( + var newDpop = await GenerateDPopAsync( config.KeyId, config.Audience, config.Nonce.ToNullable(), @@ -108,4 +117,39 @@ or System.Net.HttpStatusCode.Unauthorized return null; } + + private async Task GenerateDPopAsync(KeyId keyId, string audience, string? nonce, string? accessToken) + { + var header = new Dictionary + { + { "alg", "ES256" }, + { "typ", "dpop+jwt" } + }; + + var jwkSerialized = await _keyStore.LoadKey(keyId); + var jwkDeserialized = JsonConvert.DeserializeObject(jwkSerialized); + if (jwkDeserialized != null) + { + header["jwk"] = jwkDeserialized; + } + + string? ath = null; + if (!string.IsNullOrEmpty(accessToken)) + { + var sha256 = SHA256.Create(); + ath = Base64UrlEncoder.Encode(sha256.ComputeHash(Encoding.UTF8.GetBytes(accessToken))); + } + + var dPopPayload = new + { + jti = Guid.NewGuid().ToString(), + htm = "POST", + iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + htu = new Uri(audience).GetLeftPart(UriPartial.Path), + nonce, + ath + }; + + return await _sdJwtSignerService.CreateSignedJwt(header, dPopPayload, keyId); + } } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs index dd9be71c..743c30d1 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/CredRequest/Implementations/CredentialRequestService.cs @@ -20,6 +20,7 @@ using WalletFramework.Oid4Vc.Oid4Vci.Issuer.Models; using WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Models.SdJwt; using WalletFramework.Oid4Vc.Oid4Vci.CredResponse; +using WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; namespace WalletFramework.Oid4Vc.Oid4Vci.CredRequest.Implementations; @@ -28,15 +29,18 @@ public class CredentialRequestService : ICredentialRequestService public CredentialRequestService( HttpClient httpClient, IDPopHttpClient dPopHttpClient, + ISdJwtSignerService sdJwtSignerService, IKeyStore keyStore) { _dPopHttpClient = dPopHttpClient; - _httpClient = httpClient; + _sdJwtSignerService = sdJwtSignerService; _keyStore = keyStore; + _httpClient = httpClient; } private readonly HttpClient _httpClient; private readonly IDPopHttpClient _dPopHttpClient; + private readonly ISdJwtSignerService _sdJwtSignerService; private readonly IKeyStore _keyStore; private async Task CreateCredentialRequest( @@ -50,7 +54,7 @@ private async Task CreateCredentialRequest( oauthToken => oauthToken.CNonce, dPopToken => dPopToken.Token.CNonce); - var keyBindingJwt = await _keyStore.GenerateKbProofOfPossessionAsync( + var keyBindingJwt = await _sdJwtSignerService.GenerateKbProofOfPossessionAsync( keyId, issuerMetadata.CredentialIssuer.ToString(), cNonce, diff --git a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtSignerService.cs b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtSignerService.cs new file mode 100644 index 00000000..96d1176e --- /dev/null +++ b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/ISdJwtSignerService.cs @@ -0,0 +1,10 @@ +using WalletFramework.Core.Cryptography.Models; + +namespace WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; + +public interface ISdJwtSignerService +{ + Task CreateSignedJwt(object header, object payload, KeyId keyId); + + Task GenerateKbProofOfPossessionAsync(KeyId keyId, string audience, string nonce, string type, string? sdHash, string? clientId); +} diff --git a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtSignerService.cs b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtSignerService.cs new file mode 100644 index 00000000..3d7dbad2 --- /dev/null +++ b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtSignerService.cs @@ -0,0 +1,71 @@ +using System.Text; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using Org.BouncyCastle.Asn1; +using WalletFramework.Core.Cryptography.Abstractions; +using WalletFramework.Core.Cryptography.Models; + +namespace WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; + +public class SdJwtSignerService : ISdJwtSignerService +{ + private readonly IKeyStore _keyStore; + + public SdJwtSignerService(IKeyStore keyStore) => _keyStore = keyStore; + + public async Task GenerateKbProofOfPossessionAsync(KeyId keyId, string audience, string nonce, string type, string? sdHash, string? clientId) + { + var header = new Dictionary + { + { "alg", "ES256" }, + { "typ", type } + }; + + if (string.Equals(type, "openid4vci-proof+jwt", StringComparison.OrdinalIgnoreCase)) + { + var jwkSerialized = await _keyStore.LoadKey(keyId); + var jwkDeserialized = JsonConvert.DeserializeObject(jwkSerialized); + if (jwkDeserialized != null) + { + header["jwk"] = jwkDeserialized; + } + } + + var payload = new + { + aud = audience, + nonce, + iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + sd_hash = sdHash, + iss = clientId + }; + + return await CreateSignedJwt(header, payload, keyId); + } + + public async Task CreateSignedJwt(object header, object payload, KeyId keyId) + { + var encodedHeader = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(header)); + var encodedPayload = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(payload)); + + var dataToSign = encodedHeader + "." + encodedPayload; + var signedData = await _keyStore.Sign(keyId, Encoding.UTF8.GetBytes(dataToSign)); + var rawSignature = ConvertDerToRawFormat(signedData); + + var encodedSignature = Base64UrlEncoder.Encode(rawSignature); + return encodedHeader + "." + encodedPayload + "." + encodedSignature; + } + + private static byte[] ConvertDerToRawFormat(byte[]? derSignature) + { + var seq = (Asn1Sequence)Asn1Object.FromByteArray(derSignature); + var r = ((DerInteger)seq[0]).Value; + var s = ((DerInteger)seq[1]).Value; + var rawSignature = new byte[64]; + var rBytes = r.ToByteArrayUnsigned(); + var sBytes = s.ToByteArrayUnsigned(); + Array.Copy(rBytes, Math.Max(0, rBytes.Length - 32), rawSignature, 0, Math.Min(32, rBytes.Length)); + Array.Copy(sBytes, Math.Max(0, sBytes.Length - 32), rawSignature, 32, Math.Min(32, sBytes.Length)); + return rawSignature; + } +} diff --git a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs index 7597ed02..4b1e91cd 100644 --- a/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs +++ b/src/WalletFramework.SdJwtVc/Services/SdJwtVcHolderService/SdJwtVcHolderService.cs @@ -4,7 +4,6 @@ using Hyperledger.Indy.WalletApi; using SD_JWT.Models; using SD_JWT.Roles; -using WalletFramework.Core.Cryptography.Abstractions; using WalletFramework.SdJwtVc.Models.Records; namespace WalletFramework.SdJwtVc.Services.SdJwtVcHolderService; @@ -15,21 +14,21 @@ public class SdJwtVcHolderService : ISdJwtVcHolderService /// /// Initializes a new instance of the class. /// - /// The key store responsible for key operations. + /// The service responsible for SD-JWT signature related operations. /// The service responsible for wallet record operations. /// The service responsible for holder operations. public SdJwtVcHolderService( IHolder holder, - IKeyStore keyStore, + ISdJwtSignerService sdJwtSignerService, IWalletRecordService recordService) { _holder = holder; - _keyStore = keyStore; + _sdJwtSignerService = sdJwtSignerService; _recordService = recordService; } private readonly IHolder _holder; - private readonly IKeyStore _keyStore; + private readonly ISdJwtSignerService _sdJwtSignerService; private readonly IWalletRecordService _recordService; /// @@ -56,14 +55,13 @@ public async Task CreatePresentation( && !string.IsNullOrEmpty(nonce) && !string.IsNullOrEmpty(audience)) { - - - var keybindingJwt = await _keyStore.GenerateKbProofOfPossessionAsync( + var keybindingJwt = await _sdJwtSignerService.GenerateKbProofOfPossessionAsync( credential.KeyId, audience, nonce, "kb+jwt", - presentationFormat.ToSdHash()); + presentationFormat.ToSdHash(), + null); return presentationFormat.AddKeyBindingJwt(keybindingJwt); } diff --git a/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs index f2154033..cc1cc3e5 100644 --- a/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs +++ b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs @@ -36,7 +36,7 @@ public Oid4VpClientServiceTests() var walletRecordService = new DefaultWalletRecordService(); var pexService = new PexService(); - _sdJwtVcHolderService = new SdJwtVcHolderService(holder, _keyStoreMock.Object, walletRecordService); + _sdJwtVcHolderService = new SdJwtVcHolderService(holder, _sdJwtSignerService.Object, walletRecordService); var oid4VpHaipClient = new Oid4VpHaipClient(_httpClientFactoryMock.Object, pexService); _oid4VpRecordService = new Oid4VpRecordService(walletRecordService); @@ -49,7 +49,7 @@ public Oid4VpClientServiceTests() _oid4VpRecordService ); - _keyStoreMock.Setup(keyStore => + _sdJwtSignerService.Setup(keyStore => keyStore.GenerateKbProofOfPossessionAsync( It.IsAny(), It.IsAny(), @@ -64,7 +64,7 @@ public Oid4VpClientServiceTests() private readonly Mock _httpMessageHandlerMock = new(); private readonly Mock _httpClientFactoryMock = new(); - private readonly Mock _keyStoreMock = new(); + private readonly Mock _sdJwtSignerService = new(); private readonly Mock> _loggerMock = new(); private readonly MockAgentRouter _router = new(); private readonly Oid4VpClientService _oid4VpClientService; diff --git a/test/WalletFramework.SdJwtVc.Tests/SdJwtVcHolderServiceTests.cs b/test/WalletFramework.SdJwtVc.Tests/SdJwtVcHolderServiceTests.cs index abf3b7e2..c462c9cc 100644 --- a/test/WalletFramework.SdJwtVc.Tests/SdJwtVcHolderServiceTests.cs +++ b/test/WalletFramework.SdJwtVc.Tests/SdJwtVcHolderServiceTests.cs @@ -20,9 +20,9 @@ public SdJwtVcHolderServiceTests() { // Mock with moq IKestore and IWalletRecordService IHolder holder = new Holder(); - IKeyStore keyStoreMock = new Mock().Object; + ISdJwtSignerService sdJwtSignerServiceMock = new Mock().Object; IWalletRecordService walletRecordServiceMock = new Mock().Object; - _service = new SdJwtVcHolderService(holder, keyStoreMock, walletRecordServiceMock); + _service = new SdJwtVcHolderService(holder, sdJwtSignerServiceMock, walletRecordServiceMock); } // https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-08.html#appendix-A.3-4 From 98c0b437d056f9bcc7c2e589e7eb91f355617ac0 Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Tue, 23 Jul 2024 13:52:24 +0200 Subject: [PATCH 7/8] move structs to own file Signed-off-by: Johannes Tuerk --- .../Models/CombinedWalletAttestation.cs | 33 ------------------- .../Models/WalletInstanceAttestationJwt.cs | 23 +++++++++++++ .../Models/WalletInstanceAttestationPopJwt.cs | 12 +++++++ 3 files changed, 35 insertions(+), 33 deletions(-) create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/WalletInstanceAttestationJwt.cs create mode 100644 src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/WalletInstanceAttestationPopJwt.cs diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CombinedWalletAttestation.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CombinedWalletAttestation.cs index b81abc04..80b2d399 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CombinedWalletAttestation.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/CombinedWalletAttestation.cs @@ -1,6 +1,3 @@ -using WalletFramework.Core.Functional; -using WalletFramework.Core.Functional.Errors; - namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; public record CombinedWalletAttestation @@ -29,33 +26,3 @@ public static string ToStringRepresentation(this CombinedWalletAttestation combi => combinedWalletAttestation.WalletInstanceAttestationJwt + "~" + combinedWalletAttestation.WalletInstanceAttestationPopJwt; } - -public struct WalletInstanceAttestationPopJwt -{ - public string Value { get; } - - public static implicit operator string(WalletInstanceAttestationPopJwt keyId) => keyId.Value; - - private WalletInstanceAttestationPopJwt(string value) => Value = value; - - public static WalletInstanceAttestationPopJwt CreateWalletInstanceAttestationPopJwt(string value) => new (value); -} - -public struct WalletInstanceAttestationJwt -{ - public string Value { get; } - - public static implicit operator string(WalletInstanceAttestationJwt keyId) => keyId.Value; - - private WalletInstanceAttestationJwt(string value) => Value = value; - - public static Validation ValidWalletInstanceAttestationJwt(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return new StringIsNullOrWhitespaceError(); - } - - return new WalletInstanceAttestationJwt(value); - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/WalletInstanceAttestationJwt.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/WalletInstanceAttestationJwt.cs new file mode 100644 index 00000000..691f64e6 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/WalletInstanceAttestationJwt.cs @@ -0,0 +1,23 @@ +using WalletFramework.Core.Functional; +using WalletFramework.Core.Functional.Errors; + +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +public struct WalletInstanceAttestationJwt +{ + public string Value { get; } + + public static implicit operator string(WalletInstanceAttestationJwt keyId) => keyId.Value; + + private WalletInstanceAttestationJwt(string value) => Value = value; + + public static Validation ValidWalletInstanceAttestationJwt(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return new StringIsNullOrWhitespaceError(); + } + + return new WalletInstanceAttestationJwt(value); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/WalletInstanceAttestationPopJwt.cs b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/WalletInstanceAttestationPopJwt.cs new file mode 100644 index 00000000..1cd8e84e --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vci/AuthFlow/Models/WalletInstanceAttestationPopJwt.cs @@ -0,0 +1,12 @@ +namespace WalletFramework.Oid4Vc.Oid4Vci.AuthFlow.Models; + +public struct WalletInstanceAttestationPopJwt +{ + public string Value { get; } + + public static implicit operator string(WalletInstanceAttestationPopJwt keyId) => keyId.Value; + + private WalletInstanceAttestationPopJwt(string value) => Value = value; + + public static WalletInstanceAttestationPopJwt CreateWalletInstanceAttestationPopJwt(string value) => new (value); +} From c914fa4203e256871ebda55b017de6fcce080459 Mon Sep 17 00:00:00 2001 From: Johannes Tuerk Date: Tue, 23 Jul 2024 14:00:42 +0200 Subject: [PATCH 8/8] add singelton Signed-off-by: Johannes Tuerk --- src/WalletFramework.SdJwtVc/ServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/WalletFramework.SdJwtVc/ServiceCollectionExtensions.cs b/src/WalletFramework.SdJwtVc/ServiceCollectionExtensions.cs index 7dbfe4fe..69d1cae4 100644 --- a/src/WalletFramework.SdJwtVc/ServiceCollectionExtensions.cs +++ b/src/WalletFramework.SdJwtVc/ServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ public static IServiceCollection AddSdJwtVcServices(this IServiceCollection buil { builder.AddSingleton(); builder.AddSingleton(); + builder.AddSingleton(); return builder; }