diff --git a/SECURITY.md b/SECURITY.md index c9597a8..c587c78 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,62 +1,42 @@ # EU Digital Identity Wallet Vulnerability Disclosure Policy (VDP) -At the European Commission, we treat the security of our Communication and Information Systems as a -top priority, in line with Commission Decision EC 2017/46. However, vulnerabilities can never be -completely eliminated, despite all efforts. If exploited, such vulnerabilities can harm the -confidentiality, integrity or availability of the Commission's systems and of the information -processed therein. To identify and remediate vulnerabilities as soon as possible, we value the input -of external entities acting in good faith, and we encourage responsible vulnerability research and -disclosure. This document sets out our definition of good faith in the context of finding and -reporting vulnerabilities, as well as what you can expect from us in return. +At the European Commission, we treat the security of our Communication and Information Systems as a top priority, in line with Commission Decision EC 2017/46. However, vulnerabilities can never be completely eliminated, despite all efforts. If exploited, such vulnerabilities can harm the confidentiality, integrity or availability of the Commission's systems and of the information processed therein. To identify and remediate vulnerabilities as soon as possible, we value the input of external entities acting in good faith, and we encourage responsible vulnerability research and disclosure. This document sets out our definition of good faith in the context of finding and reporting vulnerabilities, as well as what you can expect from us in return. ## Scope - Architecture and Reference Framework -- Source code in [eu-digital-identity-wallet](https://github.com/eu-digital-identity-wallet) public - repositories - -## If you have identified a vulnerability, please do the following: - -* E-mail your findings to EC-VULNERABILITY-DISCLOSURE@ec.europa.eu, specifying whether or not you - agree to your name or pseudonym being made publicly available as the discoverer of the problem. -* Encrypt your findings using - our [PGP key](https://sks.hnet.se/pks/lookup?search=EC-VULNERABILITY-DISCLOSURE%40ec.europa.eu&fingerprint=on&op=index) - to prevent this critical information from falling into the wrong hands. -* Provide us sufficient information to reproduce the problem so that we can resolve it as quickly as - possible. Usually, the IP address or the URL of the affected system and a description of the - vulnerability will be sufficient, but complex vulnerabilities may require further explanation in - terms of technical information or potential proof-of-concept code. -* Provide your report in English, preferably, or in any other official language of the European - Union. -* Inform us if you agree to make your name/pseudonym publicly available as the discoverer of the - vulnerability. +- Source code in [eu-digital-identity-wallet](https://github.com/eu-digital-identity-wallet) public repositories + +## If you have identified a vulnerability, please do the following + +- E-mail your findings to , specifying whether or not you agree to your name or pseudonym being made publicly available as the discoverer of the problem. +- Encrypt your findings using our [PGP key](https://pgp.mit.edu/pks/lookup?op=get&search=0x6773AACDF09F6628) to prevent this critical information from falling into the wrong hands. +- Provide us with sufficient information to reproduce the problem so that we can resolve it as quickly as possible. Usually, the IP address or the URL of the affected system and a description of the vulnerability will be sufficient, but complex vulnerabilities may require further explanation in terms of technical information or potential proof-of-concept code. +- Provide your report in English, preferably, or in any other official language of the European Union. +- Inform us if you agree to make your name/pseudonym publicly available as the discoverer of the vulnerability. ## Please do not do the following -* Do not take advantage of the vulnerability or problem you have discovered, for example by - downloading more data than necessary to demonstrate the vulnerability, deleting, or modifying - other people’s data. -* Do not reveal any data downloaded during the discovery to any other parties. -* Do not reveal the problem to others until it has been resolved. -* Do not perform the following actions: - * Placing malware (virus, worm, Trojan horse, etc.) within the system. - * Reading, copying, modifying or deleting data from the system. - * Making changes to the system. - * Repeatedly accessing the system or sharing access with others. - * Using any access obtained to attempt to access other systems. - * Changing access rights for any other users. - * Using automated scanning tools. - * Using the so-called "brute force" of access to the system. - * Using denial-of-service or social engineering (phishing, vishing, spam etc.). -* Do not use attacks on physical security. - -## What we promise: - -* We will respond to your report within three business days with our evaluation of the report. -* We will handle your report with strict confidentiality. -* Where possible, we will inform you when the vulnerability has been remedied. -* We will process the personal data that you provide (such as your e-mail address and name) in - accordance with the applicable data protection legislation and will not pass on your personal - details to third parties without your permission. -* In the public information concerning the problem reported, we will publish your name as the - discoverer of the problem if you have agreed to this in your initial e-mail \ No newline at end of file +- Do not take advantage of the vulnerability or problem you have discovered, for example, by downloading more data than necessary to demonstrate the vulnerability, deleting, or modifying other people’s data. +- Do not reveal any data downloaded during the discovery to any other parties. +- Do not reveal the problem to others until it has been resolved. +- Do not perform the following actions: + - Placing malware (virus, worm, Trojan horse, etc.) within the system. + - Reading, copying, modifying or deleting data from the system. + - Making changes to the system. + - Repeatedly accessing the system or sharing access with others. + - Using any access obtained to attempt to access other systems. + - Changing access rights for any other users. + - Using automated scanning tools. + - Using the so-called "brute force" of access to the system. + - Using denial-of-service or social engineering (phishing, vishing, spam, etc.). +- Do not use attacks on physical security. + +## What we promise + +- We will respond to your report within three business days with our evaluation of the report. + +- We will handle your report with strict confidentiality. +- Where possible, we will inform you when the vulnerability has been remedied. +- We will process the personal data that you provide (such as your e-mail address and name) in accordance with the applicable data protection legislation and will not pass on your personal details to third parties without your permission. +- In the public information concerning the problem reported, we will publish your name as the discoverer of the problem if you have agreed to this in your initial e-mail \ No newline at end of file diff --git a/Sources/Entities/CredentialSupported/SupportedCredential.swift b/Sources/Entities/CredentialSupported/SupportedCredential.swift index e793a8f..1545257 100644 --- a/Sources/Entities/CredentialSupported/SupportedCredential.swift +++ b/Sources/Entities/CredentialSupported/SupportedCredential.swift @@ -120,7 +120,7 @@ public extension SupportedCredential { proof: proof ) default: - throw ValidationError.error(reason: "Unsupported profile for issueance request") + throw ValidationError.error(reason: "Unsupported profile for issuance request") } } } diff --git a/Sources/Entities/Errors/JOSEError.swift b/Sources/Entities/Errors/JOSEError.swift index 6adbac9..30bea18 100644 --- a/Sources/Entities/Errors/JOSEError.swift +++ b/Sources/Entities/Errors/JOSEError.swift @@ -14,6 +14,7 @@ * limitations under the License. */ import Foundation +import JOSESwift /* This enum represents a set of JOSE (Javascript Object Signing and Encryption) errors. @@ -52,3 +53,65 @@ public enum JOSEError: LocalizedError { } } } + +extension JOSESwiftError: LocalizedError { + + public var errorDescription: String? { + switch self { + case .signingFailed(let description): + return ".signingFailed: \(description)" + case .verifyingFailed(let description): + return ".verifyingFailed: \(description)" + case .signatureInvalid: + return ".signatureInvalid" + case .encryptingFailed(let description): + return ".encryptingFailed: \(description)" + case .decryptingFailed: + return ".decryptingFailed" + case .wrongDataEncoding: + return ".wrongDataEncoding" + case .invalidCompactSerializationComponentCount(let count): + return ".invalidCompactSerializationComponentCount: \(count)" + case .componentNotValidBase64URL(let component): + return ".componentNotValidBase64URL: \(component)" + case .componentCouldNotBeInitializedFromData: + return ".componentCouldNotBeInitializedFromData" + case .couldNotConstructJWK: + return ".couldNotConstructJWK" + case .modulusNotBase64URLUIntEncoded: + return ".modulusNotBase64URLUIntEncoded" + case .exponentNotBase64URLUIntEncoded: + return ".exponentNotBase64URLUIntEncoded" + case .privateExponentNotBase64URLUIntEncoded: + return "" + case .symmetricKeyNotBase64URLEncoded: + return ".symmetricKeyNotBase64URLEncoded" + case .xNotBase64URLUIntEncoded: + return ".xNotBase64URLUIntEncoded" + case .yNotBase64URLUIntEncoded: + return ".yNotBase64URLUIntEncoded" + case .privateKeyNotBase64URLUIntEncoded: + return ".privateKeyNotBase64URLUIntEncoded" + case .invalidCurveType: + return ".invalidCurveType" + case .compressedCurvePointsUnsupported: + return ".compressedCurvePointsUnsupported" + case .invalidCurvePointOctetLength: + return ".invalidCurvePointOctetLength" + case .localAuthenticationFailed(let errorCode): + return ".localAuthenticationFailed: \(errorCode)" + case .compressionFailed: + return ".compressionFailed" + case .decompressionFailed: + return ".decompressionFailed" + case .compressionAlgorithmNotSupported: + return ".compressionAlgorithmNotSupported" + case .rawDataMustBeGreaterThanZero: + return ".rawDataMustBeGreaterThanZero" + case .compressedDataMustBeGreaterThanZero: + return ".compressedDataMustBeGreaterThanZero" + case .thumbprintSerialization: + return ".thumbprintSerialization" + } + } +} diff --git a/Sources/Issuers/IssuanceRequester.swift b/Sources/Issuers/IssuanceRequester.swift index 39291a2..54994cb 100644 --- a/Sources/Issuers/IssuanceRequester.swift +++ b/Sources/Issuers/IssuanceRequester.swift @@ -46,7 +46,7 @@ public actor IssuanceRequester: IssuanceRequesterType { public init( issuerMetadata: CredentialIssuerMetadata, service: AuthorisationServiceType = AuthorisationService(), - poster: PostingType = Poster() + poster: PostingType ) { self.issuerMetadata = issuerMetadata self.service = service @@ -80,6 +80,7 @@ public actor IssuanceRequester: IssuanceRequesterType { case .msoMdoc(let credential): switch issuerMetadata.credentialResponseEncryption { case .notRequired: + print(string) guard let response = SingleIssuanceSuccessResponse.fromJSONString(string) else { return .failure(ValidationError.todo(reason: "Cannot decode .notRequired response")) } @@ -104,7 +105,7 @@ public actor IssuanceRequester: IssuanceRequesterType { let keyManagementAlgorithm = KeyManagementAlgorithm(algorithm: responseEncryptionAlg), let contentEncryptionAlgorithm = ContentEncryptionAlgorithm(encryptionMethod: responseEncryptionMethod) else { - return .failure(ValidationError.error(reason: "Unsupported encryption algorithms")) + return .failure(ValidationError.error(reason: "Unsupported encryption algorithms: \(responseEncryptionAlg.name), \(responseEncryptionMethod.name)")) } let jwe = try JWE(compactSerialization: string) @@ -121,7 +122,7 @@ public actor IssuanceRequester: IssuanceRequesterType { } } catch { - return .failure(ValidationError.error(reason: error.localizedDescription)) + return .failure(error) } } case .sdJwtVc(let credential): diff --git a/Sources/Issuers/Issuer.swift b/Sources/Issuers/Issuer.swift index c166827..1939c6c 100644 --- a/Sources/Issuers/Issuer.swift +++ b/Sources/Issuers/Issuer.swift @@ -65,24 +65,38 @@ public actor Issuer: IssuerType { let config: WalletOpenId4VCIConfig private let authorizer: IssuanceAuthorizerType - private let requester: IssuanceRequesterType + + private let issuanceRequester: IssuanceRequesterType + private let deferredIssuanceRequester: IssuanceRequesterType public init( authorizationServerMetadata: IdentityAndAccessManagementMetadata, issuerMetadata: CredentialIssuerMetadata, - config: WalletOpenId4VCIConfig + config: WalletOpenId4VCIConfig, + parPoster: PostingType = Poster(), + tokenPoster: PostingType = Poster(), + requesterPoster: PostingType = Poster(), + deferredRequesterPoster: PostingType = Poster() ) throws { self.authorizationServerMetadata = authorizationServerMetadata self.issuerMetadata = issuerMetadata self.config = config authorizer = try IssuanceAuthorizer( + parPoster: parPoster, + tokenPoster: tokenPoster, config: config, authorizationServerMetadata: authorizationServerMetadata ) - requester = IssuanceRequester( - issuerMetadata: issuerMetadata + issuanceRequester = IssuanceRequester( + issuerMetadata: issuerMetadata, + poster: requesterPoster + ) + + deferredIssuanceRequester = IssuanceRequester( + issuerMetadata: issuerMetadata, + poster: deferredRequesterPoster ) } @@ -287,7 +301,7 @@ public actor Issuer: IssuerType { case .noProofRequired(let token): return try await requestIssuance(token: token) { return try supportedCredential.toIssuanceRequest( - requester: requester, + requester: issuanceRequester, claimSet: claimSet, responseEncryptionSpecProvider: responseEncryptionSpecProvider ) @@ -316,10 +330,10 @@ public actor Issuer: IssuerType { let cNonce = cNonce(from: proofRequest) return try await requestIssuance(token: accessToken(from: proofRequest)) { return try supportedCredential.toIssuanceRequest( - requester: requester, + requester: issuanceRequester, claimSet: claimSet, proof: bindingKey.toSupportedProof( - issuanceRequester: requester, + issuanceRequester: issuanceRequester, credentialSpec: supportedCredential, cNonce: cNonce?.value ), @@ -338,7 +352,7 @@ private extension Issuer { let credentialRequest = try issuanceRequestSupplier() switch credentialRequest { case .single(let single): - let result = try await requester.placeIssuanceRequest( + let result = try await issuanceRequester.placeIssuanceRequest( accessToken: token, request: single ) @@ -349,7 +363,7 @@ private extension Issuer { return handleIssuanceError(error) } case .batch(let credentials): - let result = try await requester.placeBatchIssuanceRequest( + let result = try await issuanceRequester.placeBatchIssuanceRequest( accessToken: token, request: credentials ) @@ -484,7 +498,7 @@ public extension Issuer { throw ValidationError.error(reason: "Invalid access token") } - return try await requester.placeDeferredCredentialRequest( + return try await deferredIssuanceRequester.placeDeferredCredentialRequest( accessToken: token, transactionId: transactionId ) diff --git a/Sources/Main/Authorisers/IssuanceAuthorizer.swift b/Sources/Main/Authorisers/IssuanceAuthorizer.swift index 82b882e..fefd615 100644 --- a/Sources/Main/Authorisers/IssuanceAuthorizer.swift +++ b/Sources/Main/Authorisers/IssuanceAuthorizer.swift @@ -167,7 +167,7 @@ public actor IssuanceAuthorizer: IssuanceAuthorizerType { ) } } catch { - return .failure(ValidationError.error(reason: error.localizedDescription)) + return .failure(error) } } diff --git a/Sources/Resources/europa/credential_issuer_metadata_valid_algos.json b/Sources/Resources/europa/credential_issuer_metadata_valid_algos.json new file mode 100644 index 0000000..47bad5f --- /dev/null +++ b/Sources/Resources/europa/credential_issuer_metadata_valid_algos.json @@ -0,0 +1,264 @@ +{ + "credential_issuer": "https://credential-issuer.example.com", + "authorization_servers": ["https://keycloak-eudi.netcompany-intrasoft.com/realms/pid-issuer-realm"], + "credential_endpoint": "https://credential-issuer.example.com/credentials", + "batch_credential_endpoint": "https://credential-issuer.example.com/credentials/batch", + "deferred_credential_endpoint": "https://credential-issuer.example.com/credentials/deferred", + "credential_response_encryption_alg_values_supported": [ + "PBES2-HS512+A256KW", + "PBES2-HS384+A192KW", + "PBES2-HS256+A128KW", + "RSA-OAEP-256" + ], + "credential_response_encryption_enc_values_supported": [ + "XC20P", + "A128CBC-HS256" + ], + "require_credential_response_encryption": true, + "credential_identifiers_supported": true, + "credentials_supported": { + "UniversityDegree_JWT": { + "format": "jwt_vc_json", + "scope": "UniversityDegree_JWT", + "cryptographic_binding_methods_supported": [ + "did:example" + ], + "cryptographic_suites_supported": [ + "ES256K" + ], + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "name", + "locale": "GPA" + } + ] + } + } + }, + "proof_types_supported": [ + "jwt" + ], + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + }, + "MobileDrivingLicense_msoMdoc": { + "format": "mso_mdoc", + "scope": "MobileDrivingLicense_msoMdoc", + "doctype": "org.iso.18013.5.1.mDL", + "cryptographic_binding_methods_supported": [ + "mso" + ], + "cryptographic_suites_supported": [ + "ES256", + "ES384", + "ES512" + ], + "display": [ + { + "name": "Mobile Driving License", + "locale": "en-US", + "logo": { + "url": "https://examplestate.com/public/mdl.png", + "alt_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "claims": { + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } + }, + "UniversityDegree_LDP_VC": { + "format": "ldp_vc", + "scope": "UniversityDegree_LDP_VC", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential_LDP_VC", + "UniversityDegreeCredential_LDP_VC" + ], + "cryptographic_binding_methods_supported": [ + "did:example" + ], + "cryptographic_suites_supported": [ + "Ed25519Signature2018" + ], + "credential_definition": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential_LDP_VC", + "UniversityDegreeCredential_LDP_VC" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "name", + "locale": "GPA" + } + ] + } + } + }, + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + }, + "UniversityDegree_JWT_VC_JSON-LD": { + "format": "jwt_vc_json-ld", + "scope": "UniversityDegree_JWT_VC_JSON-LD", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "cryptographic_binding_methods_supported": [ + "did:example" + ], + "cryptographic_suites_supported": [ + "Ed25519Signature2018" + ], + "credential_definition": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential_JWT_VC_JSON-LD", + "UniversityDegreeCredential_JWT_VC_JSON-LD" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "name", + "locale": "GPA" + } + ] + } + } + }, + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + } + }, + "display": [ + { + "name": "credential-issuer.example.com", + "locale": "en-US" + } + ] +} diff --git a/Sources/Resources/responses/access_token_request_response.json b/Sources/Resources/responses/access_token_request_response.json new file mode 100644 index 0000000..f438f96 --- /dev/null +++ b/Sources/Resources/responses/access_token_request_response.json @@ -0,0 +1,7 @@ +{ + "access_token": "my.access.toekn", + "expires_in": 3600, + "c_nonce": "c_nonce", + "c_nonce_expires_in": 7200, + "scope": "a.b.c" +} diff --git a/Sources/Resources/responses/access_token_request_response_no_proof.json b/Sources/Resources/responses/access_token_request_response_no_proof.json new file mode 100644 index 0000000..ca05ae3 --- /dev/null +++ b/Sources/Resources/responses/access_token_request_response_no_proof.json @@ -0,0 +1,5 @@ +{ + "access_token": "my.access.toekn", + "expires_in": 3600, + "scope": "a.b.c" +} diff --git a/Sources/Resources/responses/no_proof_generic_error_response.json b/Sources/Resources/responses/no_proof_generic_error_response.json new file mode 100644 index 0000000..26d93b8 --- /dev/null +++ b/Sources/Resources/responses/no_proof_generic_error_response.json @@ -0,0 +1,5 @@ +{ + "error": "invalid_proof", + "c_nonce": "ERE%@^TGWYEYWEY", + "c_nonce_expires_in": 34 +} diff --git a/Sources/Resources/responses/pushed_authorization_request_response.json b/Sources/Resources/responses/pushed_authorization_request_response.json new file mode 100644 index 0000000..7d9bf69 --- /dev/null +++ b/Sources/Resources/responses/pushed_authorization_request_response.json @@ -0,0 +1,4 @@ +{ + "request_uri": "https://request_uri.example.com", + "expires_in": 3600 +} diff --git a/Sources/Resources/responses/single_issuance_success_response_credential.json b/Sources/Resources/responses/single_issuance_success_response_credential.json new file mode 100644 index 0000000..0fc251a --- /dev/null +++ b/Sources/Resources/responses/single_issuance_success_response_credential.json @@ -0,0 +1,6 @@ +{ + "format": "vc+sd-jwt", + "credential": "1234565768122", + "c_nonce": "wlbQc6pCJp", + "c_nonce_expires_in": 86400 +} diff --git a/Sources/Resources/responses/single_issuance_success_response_deffered.json b/Sources/Resources/responses/single_issuance_success_response_deffered.json new file mode 100644 index 0000000..a672ecd --- /dev/null +++ b/Sources/Resources/responses/single_issuance_success_response_deffered.json @@ -0,0 +1,6 @@ +{ + "format": "vc+sd-jwt", + "transaction_id": "1234565768122", + "c_nonce": "wlbQc6pCJp", + "c_nonce_expires_in": 86400 +} diff --git a/Sources/Resources/responses/web_signature.jws b/Sources/Resources/responses/web_signature.jws new file mode 100644 index 0000000..628d782 --- /dev/null +++ b/Sources/Resources/responses/web_signature.jws @@ -0,0 +1 @@ +eyJraWQiOiIyMTQwMTRFMC05QjAyLTRBOEUtODVENy0wMEQ2MzU0RTI5OUUiLCJ0eXAiOiJKV1QiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiUlNBLU9BRVAtMjU2IiwiandrIjp7Imt0eSI6IlJTQSIsImUiOiJBUUFCIiwidXNlIjoiZW5jIiwia2lkIjoiMjE0MDE0RTAtOUIwMi00QThFLTg1RDctMDBENjM1NEUyOTlFIiwiYWxnIjoiUlNBLU9BRVAtMjU2IiwibiI6IjRKb19HX2hpdHN0azlKLXZJYnlta082QzQxR1BMY1VZOE5FaHRpcllhMkNWcy1rVVNRbzVNb3Z2a3FMWUtBdjBWUkFpeG9jN2x0Z2hFaFpuamxfcVFwTFlUWlRlT3RZLUVKRGRZRzVzNXM1MGVfcGV6ZXhQdE4zVlVpcVFLV3FZNExaR21zTmY5S0hTUUdIZ3ZkcXNWWkp1MVpEVXJaTmx0MVNQdjd5V2NPX2YtZ0g1ZEhHdEYxd1pyMjNRYk93NTRub0k1ODk5T2d1eWdHTnhZSDJHSFhYM2k5aUdxU1E2V0d0dmhNX0oyNkpPSzdMcVFoZW1MMUhjdU5GcTBjZVlSWXBKcmNIX0Z1MFpRSzBUd1ptci1oMFRkQ1diNnY1bjREeDRZZFJOQW1JLU10Nk9rNXl6Z2J0WG15YmpqTHRoS05LOS1RaTY3dncwaWV2VW1Wdk5sUSJ9fQ.eMTE3x1Br4Yy2nIfcGFfDICihprPgkcwAB8fIcDcenYIHxHAt7CFQX3RE0kJWc3n6jc2EPxlmnd13aJFR94F5ZnTFEnWiKX07P3CHezmf6PpsIw_gOS-29zjDIyywBhh8lNIgqVB893utnVYCZgCmR6SQKmtbUCqna0pJd0WrkeNTUdD-TuFeuMj2JkzFWN67DekRLHfN9w2A7FssEuj8U45ap4squX22A-eQApJk7ZiezY40-N3PPZFFyCkW67iPLXxNFLquN7BCJEE5ytOq6aSjgh-Jf6R2v6e3Uwgn_xbbG10HkTtEwBtOZjGMrwKE5ltHjWeEnb_d2uPVFTbWA.hoi-uTdH9KhT21SbImiPug.yDL-3UeoJeU0hgaBPAIkelA4nJWlQRlCzXUaixaemR4oItn58y3mW5a_BxKybTQ_06H4Fb66aMIgALShwQ9s00ByDwAehtRPW6TbKm877yTd9qJFcr9AhLlmod5nIGO32Z5MAKa_L3xibsiwjCX-6eE-Y7-rBxLoSBY16JxvorMzmVaoRCV6CpzVM38Sa1sr-Mh8TQNLSfH1P8yrDdwYM7dfRArtFfQXG9F2UrL4MZSmWKtx2jSa0GEPLwZhbyZ9h-BLMa9SZwGF01wry1H6CBGXZUHUHJFIwtSTJgAeQtVGlgG-ECkfudCUpJc9RDr1f40SMgLneMpRgdU_MzDRCVDgw0j3mmJLXmvMvF4hzyv6b48-KnWTkbZVhIIm_MQsFpb8NfEUHcWE-KD-o6KntYaXiBeplTx0vYg42cMYstpVZ7leE9FhaCmdN_qYHxg_ajCCE9z-d3TGglWsfR5KXwIS7Z6l7Bb3gxA-m7nnAkgr-zxiRad0zafikqwKwzMCQqCrcJZtBAR72OP69K00a1TIw76VoDJ8daOBBoMqjiWz1IUREhGlMTYw29RA0a4RFaqUvApdly85mG9VlbsiQwYijKQc_omUbud71GVx64aT4zl8pSJMz2OoCRn3CSLBbYycOluf7no7h1wmcQ1dMXygg43oJ8XitqveRAMBIxxFIXwpq0SHgP3Ogv3oabgUviGfcGIeZx6Vx4goY2Yppqo8GkRL-4q8zCr7-xsl73FSnWn6-qppWhrZax3V_fCyAfS05A64nesuHzCJhB4f9SyH3-Bui0kJwO8EovSooBQRFCjDiFpYkt1NNCZwMiwxw_c-OkDZjBSVeaXwDEg9HUy3uOIT6Ejx8bZp57HQSFU31FcfmVUiPQB_0ro6nNu834BbOjJfhgetxlsCdTsNSRbKp3bcnqIbo-TmkdZ9knhClPsa5RirK9Sx9TMlJOBbXUpIOew8odW5NMx9M93lveJlfdUaQjHprJTfTMaCycTSAfh0-qPB_G--xKsMIKf2mIo5e7k4GKNQwbvVisGLoktL99DIxIVMMBhhvswns2Ia3591wG_IS91foEbgFaYRA2ZJFeDqyORgsXrmnwPlZVdKqQD7i_ZXZltox_07GbAJa52JDLtNiY1-KZDa2Ax_MRF9ZNwT5gpGaQYc9Vb7YAYHJFz8AM-fdD7r1FdVCsmB_4JGsWxjuBmAsqn0PJz2uRcAcFLeUmViqOMWrtkcxgI6to3qWOKwEd__-wfPrGmgLXi4lSe9k5rZWvl8evGHmXowfKIEEd0FO9r3nd4OKbF88_h4g6IIVaY1IKt54p7pMSZHtQTCJies9wxj0xYEzUWBMoY27MIqrhzDGG8eXZYYfIDDr7hZnByEqxKmFnLm3Xa293rioWyVSN3w7tNKJ8yqdzFxlxYDQKVX4Wj0RZLC2KvvkRwtE98g6v4Bdxaj6Aw8Ct9owJZhuV21ZpPBeMBLDQ8-Hiy9hifQO14DgUeaKVkR3m1GQS8DereBUmaW5Lyb6QHv-pt5tCOwUx-dcanP6GYsNMg_9HIuX3Epd8h-6WMtObqzACdv8fvZ8igmfl0lWQCv8wdYmu-KlPAkMOUfQoQZUdP96h9S1M9DXlFu0TAr5SXtORJTFRZcjxtvr3I_yPrBavxrvKxIV5_OzKpgqYNEC32xMf8ATJDeqszOiZbLXecbXDDifftuvEFO4n9pMEbhLTXbhYhkyCZvS21Jh-KlGEriKBdztAurUAxZ41YWVnCskWtXZjnQgSYtOlEES1v-T9F2P8IxBYX2MM3S9M7UnJpk3uMAyi-U1nfhsXVPHJAnA8CeARbZ7Ufr63m2upuS8MSTpYTrjiqhvum0Vw2IZq63-VjG2FvZFmWTOGgcAbnPf73wAgzf48zJxSVvLpnASPqFjsk6mhsMD6Qg0j9PDaVLZk71rdf2yKCS-Ev9mKxXpYpfvxoZVP7I7ptMK2s8j7oNZwe1iGd1WsM7zhQs63IVpX9tyhFUnDOJsEiohlKQH5wxPx04ketOA7C0___xAAAribVPDmfBuPuO4pj3Bb994yyVou2Ukv7hUJ6o1HjYxxuK6g0r2A3xPYUw9zHBnT_SsUFoqRaeWWtR2wz-kj3KGEaKf_Mgu19dbIDpkWs9jGteDhacIA60zu7MQw8SpuNJrVe8wTgk1RlN4zhGv1QvSJUrCYoA40dcF0e10zzMpSIIhM3xsQg0WqodoJjO6Fy4DfcCKu65Z-EQ4F8fGwFupVUSf3q38XevidYe0fLsV37UJZqgeUtCIPM7ekS2yvZtm8P6N2c2ZEMsyIHBY1pRpZHtdAoh5YHB3IZFGVAyczPET9MN3MPYl7UfmE9MoRCKJiCycIMDF0GU6_YxedhbStydptsspUOnPTPqS7ozqfBTdwRHQgPQZbgNAkSdH67jkYlKRnS8eVICwvPVaCY4e_T5acpBqayNsit9VRUf0hR0wczM4GI7eqawz4xhD7GLnXcxrWPPKQFmRkBwGVWEyTjhV5G7P2Cl4_SYuWHZ46RxIZUYXth4B4p8ivfa7tT9isgp3TPJwKef44FKZ7vEdIeRGF753dERnh4a0HLb70xSRXOxj7FfmZioatj9MqfDpCS_WbLxKEAPrlT7kRj5uFr6NCLGHE3OSYsgwfbqOToEIfziOxXdrIzpaqZzz8pMAOsszWPI96Z7cWsMo-xNev8aBaFnBxBHYyq545BI4Ugl1zOA7XiXIwqW3MReKjRICkpSgXwYY3ayhE-pufRATz9NjN6o3MIK2Sj4MiHcbxiVRW81iuxosd5_w43WUDt8AdC9dPpeiRhDlgVlhC6sJRMMO7cSxsR4Cj5wlyplnuZxqHEg7kGI6bzKP3mk3bsJwxMAu6kDZGH__2QnSPvsXnYD49Rk772Bj6j5NiplaoV0NLDLupk5JEwQrIr6cLnCr6-YDiV9amiYu8tQABt1xGsM8a6IUE7SGyymrbkH2Q5CXMZGzm0cLyUvP2AGBXdq02bzXcedNq_vU0hP9lnt5WDcAhND_x1RDd5cAcbaZOpaqTTgqMFraFLD8kgfrSmiDSB8Ilp-ImTEJr22v91i9Hct30WGTU247D1lKHn8MEWSEGoycCTNw5mKHfMHPS1gzS3uqt9Fa6Ap-NrXcxuROTum6K5z_Kqdd2xLYrOghYOI_v_9n9PtMEAvTmmbOq0z4qGbtcU_GFz9wEu4sKOZHrDYqJ5ECvE7YqW1UC-E4F6Umz9mH2h3wLEtZuotJWvRaocBCxR9LKst0BTG_LsPGDgAe81Vgp6-gm2997NWLcBC7Yz1yHlq91kw3kKz2eE6QuZb_yYEdl4lvxJawEXmv3oNr0RvveXCZfICwRhLJnyJ_sJCLuZuTaSqRKMmF6QNiBPZ_PyRTFuZC2BgxaE4gzL4ZGmousjcg93675-fZJtkxgBPPQl7MOwaw5QsLJcXFdUR_jkUJO1LXQT25Tu5-JxPjbAR--_wEEXgVi3ufVd0krihiluGq4vQPFFmkHOrvmhWKM0sfYjFBU2RUP7ciWNd_lXdktkjQuMTMixiNVYBcV3tNq9batDHJLpTzkyzh4L_oR7yyeMfSohJ7kwsfDOGp-73Lp_rZLc4TidgXc7xG0DKUhEA8KA7dtzrShmzORucSzTyvest1I8Y16IC7Jtz37t2bwWd2nNVv3baA2txRgWwOgVOrDcSDG9SACqr_7FI0KQQq-ayu9gcRk4NE48tS4ufAvWaoJFe0Qf0SC9bX5_Gxx6uEhZk6w25mh3lukMGKrkMwFn_MiMbXR1MyIW6UBH5hokrXfgnf625AFjiFt1Nykb0VKRFYJAdyfB0REowqSi0OeSF0fhMifGQP7DDCqg5g5SOaoxJqoe9Z2kxTrjK6YV2in8s7vHk3HA4630GCcGK_mI2kPADjKOWyjRHkD0mKegZ6rImopLSUGsexiMWjldwbxE8yx63-OzcFQpZ02kq6VMyTbrFXxTTAauoEn1kFxRV7wOhNVqOBWLHNv7t4z_RxU3V-yX-dvYKubAT15XbiXidxWjydS20AvUeVBl83-8HNZHGAkEB4syFll4sdiWvAmk0VtI47_PQw1aTPeLrQfDkiwpiv1zl-QmZPbOi4wY1JM-nwLObpjEUtHbjGucARrDz-I-sGXMj7Ed77fMcTy9qwkFcDr8Gst4sCXqgFkt96OJ8csX3GDTR3FCsnpE2OdP5O8pqG_uDlJsq4F_S_qEazvm_EYn0-8BRZ7-5C2PeGqE7uCeFqK72kYExXgka23fqgvuCAhpVmPtDsFfajhx6FZwgY11MXXMVqwZE3OTO72Sw040zdFN4VRy0xLQs7UZnlfMwJ1QZ3n1F78LxMZOur4fFp2Pn6d-gzyjwDfWxsKxrTDLT1O4z2MePQ4RGJc9IaWIq1MLpo-BjurSevbkb_HqCeoort6frPlNibWAG08H4ergbEsfeqNp48vfS2StJko2hOW73r26S9TuRaQPlaFUOkD19t0zjyTdU6qhZMRXIediL0DA0SapD1SWfNOOFvGXQg4e1VRyNhxTkJzHaGrEJQ7cEpFbOTzJnFAMNdEm1EoX13JqSGwzyLQpXaZiMWbhB7fwDYKiKBdWe7H_s1CNOQ1KzIAI6UmXQl5QZdJ-HXwpwBDgpnMDlh-sSgGojznk5mhWC7IQmwc3VVYZpBb10wR9lgccjoTvuX9I95E79YtXm3WxllGHCh_tgMQ1n_Cj0MsxaVYkRjy4IbCh-0UzcudrNvH-Hy1IYv7cPPU_kOn2HJ39BKy-slGZ1UwahpYs731F60pXCpAQt-2rz2-681m_SurxZiZpEnw.Pe42Ps5zRw0gHqWIAZJ80w \ No newline at end of file diff --git a/Sources/Resources/well-known/openid-configuration.json b/Sources/Resources/well-known/openid-configuration.json new file mode 100644 index 0000000..4fb8a69 --- /dev/null +++ b/Sources/Resources/well-known/openid-configuration.json @@ -0,0 +1,303 @@ +{ + "issuer": "https://auth-server.example.com", + "authorization_endpoint": "https://auth-server.example.com/auth", + "token_endpoint": "https://auth-server.example.com/token", + "registration_endpoint": "https://auth-server.example.com/clients-registrations/openid-connect", + "introspection_endpoint": "https://auth-server.example.com/token/introspect", + "revocation_endpoint": "https://auth-server.example.com/revoke", + "pushed_authorization_request_endpoint": "https://auth-server.example.com/ext/par/request", + "device_authorization_endpoint": "https://auth-server.example.com/auth/device", + "backchannel_authentication_endpoint": "https://auth-server.example.com/ext/ciba/auth", + "jwks_uri": "https://auth-server.example.com/certs", + "scopes_supported": [ + "openid", + "roles", + "pid-mso-mdoc-scope", + "pid-sdjwt-vc-scope", + "profile", + "acr", + "address", + "email", + "offline_access", + "phone", + "microprofile-jwt", + "web-origins" + ], + "response_types_supported": [ + "code", + "none", + "id_token", + "token", + "id_token token", + "code id_token", + "code token", + "code id_token token" + ], + "response_modes_supported": [ + "query", + "fragment", + "form_post", + "query.jwt", + "fragment.jwt", + "form_post.jwt", + "jwt" + ], + "grant_types_supported": [ + "authorization_code", + "implicit", + "refresh_token", + "password", + "client_credentials", + "urn:ietf:params:oauth:grant-type:device_code", + "urn:openid:params:grant-type:ciba" + ], + "code_challenge_methods_supported": [ + "plain", + "S256" + ], + "token_endpoint_auth_methods_supported": [ + "private_key_jwt", + "client_secret_basic", + "client_secret_post", + "tls_client_auth", + "client_secret_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "introspection_endpoint_auth_methods_supported": [ + "private_key_jwt", + "client_secret_basic", + "client_secret_post", + "tls_client_auth", + "client_secret_jwt" + ], + "introspection_endpoint_auth_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "revocation_endpoint_auth_methods_supported": [ + "private_key_jwt", + "client_secret_basic", + "client_secret_post", + "tls_client_auth", + "client_secret_jwt" + ], + "revocation_endpoint_auth_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "request_object_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512", + "none" + ], + "request_object_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" + ], + "request_object_encryption_enc_values_supported": [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "request_parameter_supported": true, + "request_uri_parameter_supported": true, + "require_request_uri_registration": true, + "authorization_response_iss_parameter_supported": true, + "mtls_endpoint_aliases": { + "token_endpoint": "https://auth-server.example.com/token", + "registration_endpoint": "https://keycloak.local/realms/eudiw/clients-registrations/openid-connect", + "introspection_endpoint": "https://auth-server.example.com/token/introspect", + "revocation_endpoint": "https://auth-server.example.com/revoke", + "pushed_authorization_request_endpoint": "https://auth-server.example.com/ext/par/request", + "device_authorization_endpoint": "https://auth-server.example.com/auth/device", + "backchannel_authentication_endpoint": "https://auth-server.example.com/ext/ciba/auth", + "userinfo_endpoint": "https://auth-server.example.com/userinfo" + }, + "tls_client_certificate_bound_access_tokens": true, + "dpop_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "ES256", + "RS256", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "authorization_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "authorization_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" + ], + "authorization_encryption_enc_values_supported": [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "backchannel_token_delivery_modes_supported": [ + "poll", + "ping" + ], + "backchannel_authentication_request_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "ES256", + "RS256", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "subject_types_supported": [ + "public", + "pairwise" + ], + "userinfo_endpoint": "https://auth-server.example.com/userinfo", + "check_session_iframe": "https://auth-server.example.com/login-status-iframe.html", + "end_session_endpoint": "https://auth-server.example.com/logout", + "acr_values_supported": [ + "0", + "1" + ], + "id_token_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "id_token_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" + ], + "id_token_encryption_enc_values_supported": [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "userinfo_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512", + "none" + ], + "userinfo_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" + ], + "userinfo_encryption_enc_values_supported": [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "claim_types_supported": [ + "normal" + ], + "claims_supported": [ + "aud", + "sub", + "iss", + "auth_time", + "name", + "given_name", + "family_name", + "preferred_username", + "email", + "acr" + ], + "claims_parameter_supported": true, + "frontchannel_logout_supported": true, + "frontchannel_logout_session_supported": true, + "backchannel_logout_supported": true, + "backchannel_logout_session_supported": true +} \ No newline at end of file diff --git a/Sources/Resources/well-known/openid-credential-issuer_encrypted_responses.json b/Sources/Resources/well-known/openid-credential-issuer_encrypted_responses.json new file mode 100644 index 0000000..df0d9a5 --- /dev/null +++ b/Sources/Resources/well-known/openid-credential-issuer_encrypted_responses.json @@ -0,0 +1,165 @@ +{ + "credential_issuer": "https://credential-issuer.example.com", + "authorization_servers": ["https://auth-server.example.com"], + "credential_endpoint": "https://credential-issuer.example.com/credentials", + "batch_credential_endpoint": "https://credential-issuer.example.com/credentials/batch", + "deferred_credential_endpoint": "https://credential-issuer.example.com/credentials/deferred", + "credential_response_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256" + ], + "credential_response_encryption_enc_values_supported": [ + "A128CBC-HS256" + ], + "require_credential_response_encryption": true, + "credential_identifiers_supported": true, + "credentials_supported": { + "eu.europa.ec.eudiw.pid_vc_sd_jwt": { + "format": "vc+sd-jwt", + "scope": "eu.europa.ec.eudiw.pid_vc_sd_jwt", + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "cryptographic_suites_supported": [ + "RS256" + ], + "proof_types_supported": [ + "jwt" + ], + "credential_definition": { + "type": "eu.europa.ec.eudiw.pid.1", + "claims": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + } + }, + "display": [ + { + "name": "Personal Identification Data ", + "locale": "en-US", + "logo": { + "url": "https://examplestate.com/public/pid.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + }, + "eu.europa.ec.eudiw.pid_mso_mdoc": { + "format": "mso_mdoc", + "scope": "eu.europa.ec.eudiw.pid_mso_mdoc", + "doctype": "org.iso.18013.5.1.PID", + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "cryptographic_suites_supported": [ + "RS256" + ], + "display": [ + { + "name": "Personal Identification Data", + "locale": "en-US", + "logo": { + "url": "https://examplestate.com/public/pid.png", + "alt_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "claims": { + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } + }, + "UniversityDegree_mso_mdoc": { + "format": "mso_mdoc", + "scope": "UniversityDegree", + "doctype": "org.iso.18013.5.1.Degree", + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "cryptographic_suites_supported": [ + "RS256" + ], + "display": [ + { + "name": "Mobile Driving License", + "locale": "en-US", + "logo": { + "url": "https://examplestate.com/public/mdl.png", + "alt_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "claims": { + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } + } + }, + "display": [ + { + "name": "credential-issuer.example.com", + "locale": "en-US" + } + ] +} \ No newline at end of file diff --git a/Sources/Resources/well-known/openid-credential-issuer_no_encryption.json b/Sources/Resources/well-known/openid-credential-issuer_no_encryption.json new file mode 100644 index 0000000..c29c400 --- /dev/null +++ b/Sources/Resources/well-known/openid-credential-issuer_no_encryption.json @@ -0,0 +1,158 @@ +{ + "credential_issuer": "https://credential-issuer.example.com", + "authorization_servers": ["https://auth-server.example.com"], + "credential_endpoint": "https://credential-issuer.example.com/credentials", + "batch_credential_endpoint": "https://credential-issuer.example.com/credentials/batch", + "deferred_credential_endpoint": "https://credential-issuer.example.com/credentials/deferred", + "require_credential_response_encryption": false, + "credential_identifiers_supported": true, + "credentials_supported": { + "eu.europa.ec.eudiw.pid_vc_sd_jwt": { + "format": "vc+sd-jwt", + "scope": "eu.europa.ec.eudiw.pid_vc_sd_jwt", + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "cryptographic_suites_supported": [ + "RS256" + ], + "proof_types_supported": [ + "jwt" + ], + "credential_definition": { + "type": "eu.europa.ec.eudiw.pid.1", + "claims": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + } + }, + "display": [ + { + "name": "Personal Identification Data ", + "locale": "en-US", + "logo": { + "url": "https://examplestate.com/public/pid.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + }, + "eu.europa.ec.eudiw.pid_mso_mdoc": { + "format": "mso_mdoc", + "scope": "eu.europa.ec.eudiw.pid_mso_mdoc", + "doctype": "org.iso.18013.5.1.PID", + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "cryptographic_suites_supported": [ + "RS256" + ], + "display": [ + { + "name": "Personal Identification Data", + "locale": "en-US", + "logo": { + "url": "https://examplestate.com/public/pid.png", + "alt_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "claims": { + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } + }, + "UniversityDegree_mso_mdoc": { + "format": "mso_mdoc", + "scope": "UniversityDegree", + "doctype": "org.iso.18013.5.1.Degree", + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "cryptographic_suites_supported": [ + "RS256" + ], + "display": [ + { + "name": "Mobile Driving License", + "locale": "en-US", + "logo": { + "url": "https://examplestate.com/public/mdl.png", + "alt_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "claims": { + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } + } + }, + "display": [ + { + "name": "credential-issuer.example.com", + "locale": "en-US" + } + ] +} diff --git a/Sources/Utilities/RemoteDataAccess/Networking.swift b/Sources/Utilities/RemoteDataAccess/Networking.swift index 14f998c..aff93f6 100644 --- a/Sources/Utilities/RemoteDataAccess/Networking.swift +++ b/Sources/Utilities/RemoteDataAccess/Networking.swift @@ -15,6 +15,8 @@ */ import Foundation +extension URLSession: Networking {} + public protocol Networking { func data( from url: URL @@ -33,5 +35,3 @@ public extension Networking { try await data(for: request) } } - -extension URLSession: Networking {} diff --git a/Sources/Utilities/RemoteDataAccess/Poster.swift b/Sources/Utilities/RemoteDataAccess/Poster.swift index 15fbdea..e5c84c1 100644 --- a/Sources/Utilities/RemoteDataAccess/Poster.swift +++ b/Sources/Utilities/RemoteDataAccess/Poster.swift @@ -15,7 +15,7 @@ */ import Foundation -public enum PostError: Error { +public enum PostError: LocalizedError { case invalidUrl case networkError(Error) case response(GenericErrorResponse) @@ -26,7 +26,7 @@ public enum PostError: Error { - Returns: A string describing the post error. */ - public var localizedDescription: String { + public var errorDescription: String? { switch self { case .invalidUrl: return "Invalid URL" diff --git a/Tests/Constants/TestsConstants.swift b/Tests/Constants/TestsConstants.swift index ca81a4d..2aacd67 100644 --- a/Tests/Constants/TestsConstants.swift +++ b/Tests/Constants/TestsConstants.swift @@ -119,4 +119,81 @@ struct TestsConstants { } } """ + + static let unAuthorizedRequest: UnauthorizedRequest = .par( + .init( + credentials: (try? [.init(value: "UniversityDegree_JWT")]) ?? [], + getAuthorizationCodeURL: (try? .init(urlString: "https://example.com?client_id=wallet-dev&request_uri=https://request_uri.example.com&state=5A201471-D088-4544-B1E9-5476E5935A95"))!, + pkceVerifier: (try? .init( + codeVerifier: "GVaOE~J~xQmkE4aCKm4RNYviYW5QaFiFOxVv-8enIDL", + codeVerifierMethod: "S256"))!, + state: "5A201471-D088-4544-B1E9-5476E5935A95" + ) + ) + + static func createMockCredentialOffer() async -> CredentialOffer? { + let credentialIssuerMetadataResolver = CredentialIssuerMetadataResolver( + fetcher: Fetcher(session: NetworkingMock( + path: "credential_issuer_metadata", + extension: "json" + ) + )) + + let authorizationServerMetadataResolver = AuthorizationServerMetadataResolver( + oidcFetcher: Fetcher(session: NetworkingMock( + path: "oidc_authorization_server_metadata", + extension: "json" + )), + oauthFetcher: Fetcher(session: NetworkingMock( + path: "test", + extension: "json" + )) + ) + + let credentialOfferRequestResolver = CredentialOfferRequestResolver( + fetcher: Fetcher(session: NetworkingMock( + path: "credential_offer_with_blank_pre_authorized_code", + extension: "json" + )), + credentialIssuerMetadataResolver: credentialIssuerMetadataResolver, + authorizationServerMetadataResolver: authorizationServerMetadataResolver + ) + + return try? await credentialOfferRequestResolver.resolve( + source: .fetchByReference(url: .stub()) + ).get() + } + + static func createMockCredentialOfferValidEncryption() async -> CredentialOffer? { + let credentialIssuerMetadataResolver = CredentialIssuerMetadataResolver( + fetcher: Fetcher(session: NetworkingMock( + path: "openid-credential-issuer_no_encryption", + extension: "json" + ) + )) + + let authorizationServerMetadataResolver = AuthorizationServerMetadataResolver( + oidcFetcher: Fetcher(session: NetworkingMock( + path: "oidc_authorization_server_metadata", + extension: "json" + )), + oauthFetcher: Fetcher(session: NetworkingMock( + path: "test", + extension: "json" + )) + ) + + let credentialOfferRequestResolver = CredentialOfferRequestResolver( + fetcher: Fetcher(session: NetworkingMock( + path: "credential_offer_with_blank_pre_authorized_code", + extension: "json" + )), + credentialIssuerMetadataResolver: credentialIssuerMetadataResolver, + authorizationServerMetadataResolver: authorizationServerMetadataResolver + ) + + return try? await credentialOfferRequestResolver.resolve( + source: .fetchByReference(url: .stub()) + ).get() + } } diff --git a/Tests/Issuance/IssuanceAuthorizationTest.swift b/Tests/Issuance/IssuanceAuthorizationTest.swift new file mode 100644 index 0000000..b2b503d --- /dev/null +++ b/Tests/Issuance/IssuanceAuthorizationTest.swift @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation +import XCTest + +@testable import OpenID4VCI + +class IssuanceAuthorizationTest: XCTestCase { + + let config: WalletOpenId4VCIConfig = .init( + clientId: "wallet-dev", + authFlowRedirectionURI: URL(string: "urn:ietf:wg:oauth:2.0:oob")! + ) + + override func setUp() async throws { + try await super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testPushAuthorizationCodeRequestPlacementSuccesful() async throws { + + // Given + guard let offer = await TestsConstants.createMockCredentialOffer() else { + XCTAssert(false, "Unable to resolve credential offer") + return + } + + // When + let issuer = try Issuer( + authorizationServerMetadata: offer.authorizationServerMetadata, + issuerMetadata: offer.credentialIssuerMetadata, + config: config, + parPoster: Poster( + session: NetworkingMock( + path: "pushed_authorization_request_response", + extension: "json" + ) + ) + ) + + // Then + let parPlaced = await issuer.pushAuthorizationCodeRequest( + credentials: offer.credentials + ) + + if case let .success(request) = parPlaced, + case let .par(parRequested) = request { + let requestUrl = parRequested.getAuthorizationCodeURL.url.queryParameters["request_uri"] + XCTAssertNotNil(requestUrl) + } else { + XCTAssert(false, "parRequested failed") + } + } + + func testPushAuthorizationCodeRequestPlacementFailed() async throws { + + // Given + guard let offer = await TestsConstants.createMockCredentialOffer() else { + XCTAssert(false, "Unable to resolve credential offer") + return + } + + // When + let issuer = try Issuer( + authorizationServerMetadata: offer.authorizationServerMetadata, + issuerMetadata: offer.credentialIssuerMetadata, + config: config, + parPoster: Poster( + session: NetworkingMock( + path: "test", + extension: "json" + ) + ) + ) + + // Then + let parPlaced = await issuer.pushAuthorizationCodeRequest( + credentials: offer.credentials + ) + + switch parPlaced { + case .success: + XCTAssert(false, "Expected failure") + case .failure(let error): + XCTAssertTrue(true, error.localizedDescription) + } + } + + func testAccessTokenAquisitionNoProofRequiredSuccess() async throws { + + // Given + guard let offer = await TestsConstants.createMockCredentialOffer() else { + XCTAssert(false, "Unable to resolve credential offer") + return + } + + // When + let issuer = try Issuer( + authorizationServerMetadata: offer.authorizationServerMetadata, + issuerMetadata: offer.credentialIssuerMetadata, + config: config, + parPoster: Poster( + session: NetworkingMock( + path: "pushed_authorization_request_response", + extension: "json" + ) + ), + tokenPoster: Poster( + session: NetworkingMock( + path: "access_token_request_response_no_proof", + extension: "json" + ) + ) + ) + + let authorizationCode = "MZqG9bsQ8UALhsGNlY39Yw==" + let request: UnauthorizedRequest = TestsConstants.unAuthorizedRequest + + let issuanceAuthorization: IssuanceAuthorization = .authorizationCode(authorizationCode: authorizationCode) + let unAuthorized = await issuer.handleAuthorizationCode( + parRequested: request, + authorizationCode: issuanceAuthorization + ) + + switch unAuthorized { + case .success(let authorizationCode): + let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + + if case let .success(authorized) = authorizedRequest, + case let .noProofRequired(token) = authorized { + XCTAssert(true, "Got access token: \(token)") + return + } + + case .failure(let error): + XCTAssert(false, error.localizedDescription) + } + + XCTAssert(false, "Unable to get access token") + } + + func testAccessTokenAquisitionProofRequiredSuccess() async throws { + + // Given + guard let offer = await TestsConstants.createMockCredentialOffer() else { + XCTAssert(false, "Unable to resolve credential offer") + return + } + + // When + let issuer = try Issuer( + authorizationServerMetadata: offer.authorizationServerMetadata, + issuerMetadata: offer.credentialIssuerMetadata, + config: config, + parPoster: Poster( + session: NetworkingMock( + path: "pushed_authorization_request_response", + extension: "json" + ) + ), + tokenPoster: Poster( + session: NetworkingMock( + path: "access_token_request_response", + extension: "json" + ) + ) + ) + + let request: UnauthorizedRequest = TestsConstants.unAuthorizedRequest + + let authorizationCode = "MZqG9bsQ8UALhsGNlY39Yw==" + let issuanceAuthorization: IssuanceAuthorization = .authorizationCode(authorizationCode: authorizationCode) + let unAuthorized = await issuer.handleAuthorizationCode( + parRequested: request, + authorizationCode: issuanceAuthorization + ) + + switch unAuthorized { + case .success(let authorizationCode): + let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + + if case let .success(authorized) = authorizedRequest, + case let .proofRequired(token, _) = authorized { + XCTAssert(true, "Got access token: \(token)") + return + } + + case .failure(let error): + XCTAssert(false, error.localizedDescription) + } + + XCTAssert(false, "Unable to get access token") + } + + func testAccessTokenAquisitionProofRequiredFailure() async throws { + + // Given + guard let offer = await TestsConstants.createMockCredentialOffer() else { + XCTAssert(false, "Unable to resolve credential offer") + return + } + + // When + let issuer = try Issuer( + authorizationServerMetadata: offer.authorizationServerMetadata, + issuerMetadata: offer.credentialIssuerMetadata, + config: config, + parPoster: Poster( + session: NetworkingMock( + path: "pushed_authorization_request_response", + extension: "json" + ) + ), + tokenPoster: Poster( + session: NetworkingMock( + path: "test", + extension: "json" + ) + ) + ) + + let request: UnauthorizedRequest = TestsConstants.unAuthorizedRequest + + let authorizationCode = "MZqG9bsQ8UALhsGNlY39Yw==" + let issuanceAuthorization: IssuanceAuthorization = .authorizationCode(authorizationCode: authorizationCode) + let unAuthorized = await issuer.handleAuthorizationCode( + parRequested: request, + authorizationCode: issuanceAuthorization + ) + + switch unAuthorized { + case .success(let authorizationCode): + let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + + switch authorizedRequest { + case .success: + XCTAssert(false, "Did not expect success") + case .failure(let error): + XCTAssert(true, "Got expected failure: \(error.localizedDescription)") + } + return + + case .failure(let error): + XCTAssert(false, error.localizedDescription) + } + + XCTAssert(false, "Unable to get access token") + } +} + diff --git a/Tests/Issuance/IssuanceDeferredRequestTest.swift b/Tests/Issuance/IssuanceDeferredRequestTest.swift new file mode 100644 index 0000000..690405c --- /dev/null +++ b/Tests/Issuance/IssuanceDeferredRequestTest.swift @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation +import XCTest +import JOSESwift + +@testable import OpenID4VCI + +class IssuanceDeferredRequestTest: XCTestCase { + + let config: WalletOpenId4VCIConfig = .init( + clientId: "wallet-dev", + authFlowRedirectionURI: URL(string: "urn:ietf:wg:oauth:2.0:oob")! + ) + + override func setUp() async throws { + try await super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testWhenIssuerRespondsDefferredThenTransactionIdExists() async throws { + + // Given + guard let offer = await TestsConstants.createMockCredentialOfferValidEncryption() else { + XCTAssert(false, "Unable to resolve credential offer") + return + } + + let privateKey = try KeyController.generateRSAPrivateKey() + let publicKey = try KeyController.generateRSAPublicKey(from: privateKey) + + let alg = JWSAlgorithm(.RS256) + let publicKeyJWK = try RSAPublicKey( + publicKey: publicKey, + additionalParameters: [ + "alg": alg.name, + "use": "enc", + "kid": UUID().uuidString + ]) + + let spec = IssuanceResponseEncryptionSpec( + jwk: publicKeyJWK, + privateKey: privateKey, + algorithm: .init(.RSA_OAEP_256), + encryptionMethod: .init(.A128CBC_HS256) + ) + + // When + let issuer = try Issuer( + authorizationServerMetadata: offer.authorizationServerMetadata, + issuerMetadata: offer.credentialIssuerMetadata, + config: config, + parPoster: Poster( + session: NetworkingMock( + path: "pushed_authorization_request_response", + extension: "json" + ) + ), + tokenPoster: Poster( + session: NetworkingMock( + path: "access_token_request_response_no_proof", + extension: "json" + ) + ), + requesterPoster: Poster( + session: NetworkingMock( + path: "single_issuance_success_response_deffered", + extension: "json" + ) + ) + ) + + let authorizationCode = "MZqG9bsQ8UALhsGNlY39Yw==" + let request: UnauthorizedRequest = TestsConstants.unAuthorizedRequest + + let issuanceAuthorization: IssuanceAuthorization = .authorizationCode(authorizationCode: authorizationCode) + let unAuthorized = await issuer.handleAuthorizationCode( + parRequested: request, + authorizationCode: issuanceAuthorization + ) + + switch unAuthorized { + case .success(let authorizationCode): + let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + + if case let .success(authorized) = authorizedRequest, + case let .noProofRequired(token) = authorized { + XCTAssert(true, "Got access token: \(token)") + XCTAssert(true, "Is no proof required") + + do { + let result = try await issuer.requestSingle( + noProofRequest: authorized, + credentialIdentifier: .init(value: "eu.europa.ec.eudiw.pid_mso_mdoc"), + responseEncryptionSpecProvider: { _ in + spec + }) + + switch result { + case .success(let request): + switch request { + case .success(let response): + if let result = response.credentialResponses.first { + switch result { + case .deferred(let transactionId): + XCTAssert(true, "transaction_id: \(transactionId)") + return + case .issued(_, let credential): + XCTAssert(false, "credential: \(credential)") + } + } else { + XCTAssert(false, "No response") + } + case .failed(let error): + XCTAssert(false, "failed: \(error.localizedDescription)") + + case .invalidProof: + XCTAssert(false, ".invalidProof") + } + XCTAssert(false, "Unexpected request") + case .failure(let error): + XCTAssert(false, error.localizedDescription) + } + } catch { + XCTAssert(false, error.localizedDescription) + } + } + case .failure(let error): + XCTAssert(false, error.localizedDescription) + } + } +} diff --git a/Tests/Issuance/IssuanceEncryptionTest.swift b/Tests/Issuance/IssuanceEncryptionTest.swift new file mode 100644 index 0000000..0d52515 --- /dev/null +++ b/Tests/Issuance/IssuanceEncryptionTest.swift @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation +import XCTest +import JOSESwift + +@testable import OpenID4VCI + +class IssuanceEncryptionTest: XCTestCase { + + let config: WalletOpenId4VCIConfig = .init( + clientId: "wallet-dev", + authFlowRedirectionURI: URL(string: "urn:ietf:wg:oauth:2.0:oob")! + ) + + override func setUp() async throws { + try await super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testWhenEncryptionAlgorithmNotSupportedByIssuerThenThrowResponseEncryptionAlgorithmNotSupportedByIssuer() async throws { + + // Given + let privateKey = try KeyController.generateRSAPrivateKey() + let publicKey = try KeyController.generateRSAPublicKey(from: privateKey) + + let alg = JWSAlgorithm(.RS256) + let publicKeyJWK = try RSAPublicKey( + publicKey: publicKey, + additionalParameters: [ + "alg": alg.name, + "use": "sig", + "kid": UUID().uuidString + ]) + + let spec = IssuanceResponseEncryptionSpec( + jwk: publicKeyJWK, + privateKey: privateKey, + algorithm: .init(name: alg.name), + encryptionMethod: .init(.A128CBC_HS256) + ) + + // When + guard let (authorizedRequest, issuer) = await (try? initIssuerWithOfferAndAuthorize(issuanceResponseEncryptionSpec: spec)) else { + XCTAssert(false, "Unable to create tuple") + return + } + + // Then + do { + _ = try await issuer.requestSingle( + noProofRequest: authorizedRequest, + credentialIdentifier: .init(value: "MobileDrivingLicense_msoMdoc"), + responseEncryptionSpecProvider: { _ in + return spec + } + ) + + XCTAssert(false) + } catch CredentialIssuanceError.responseEncryptionAlgorithmNotSupportedByIssuer { + XCTAssert(true) + + } catch { + XCTAssert(false, error.localizedDescription) + } + } + + func testWhenIssuanceRequestEncryptionMethodNotSupportedByIssuerThrowResponseEncryptionMethodNotSupportedByIssuer() async throws { + + // Given + let privateKey = try KeyController.generateRSAPrivateKey() + let publicKey = try KeyController.generateRSAPublicKey(from: privateKey) + + let alg = JWSAlgorithm.init(name: "PBES2-HS512+A256KW") + let publicKeyJWK = try RSAPublicKey( + publicKey: publicKey, + additionalParameters: [ + "alg": alg.name, + "use": "sig", + "kid": UUID().uuidString + ]) + + let spec = IssuanceResponseEncryptionSpec( + jwk: publicKeyJWK, + privateKey: privateKey, + algorithm: .init(name: alg.name), + encryptionMethod: .init(.A128GCM) + ) + + // When + guard let (authorizedRequest, issuer) = await (try? initIssuerWithOfferAndAuthorize(issuanceResponseEncryptionSpec: spec)) else { + XCTAssert(false, "Unable to create tuple") + return + } + + // Then + do { + _ = try await issuer.requestSingle( + noProofRequest: authorizedRequest, + credentialIdentifier: .init(value: "MobileDrivingLicense_msoMdoc"), + responseEncryptionSpecProvider: { _ in + return spec + } + ) + + XCTAssert(false) + } catch CredentialIssuanceError.responseEncryptionMethodNotSupportedByIssuer { + XCTAssert(true) + + } catch { + XCTAssert(false, error.localizedDescription) + } + } + + func testWhenIssuanceRequestEncryptionAlgorithmNotSupportedByIssuerThrowResponseEncryptionMethodNotSupportedByIssuer() async throws { + + // Given + let privateKey = try KeyController.generateRSAPrivateKey() + let publicKey = try KeyController.generateRSAPublicKey(from: privateKey) + + let alg = JWSAlgorithm(.RS256) + + guard let spec = Issuer.createResponseEncryptionSpecFrom(algorithmsSupported: [.init(.RSA_OAEP_256)], encryptionMethodsSupported: [.init(.A128CBC_HS256)]) else { + XCTAssert(false, "Could not create encryption spec") + return + } + + // When + guard let (authorizedRequest, issuer) = await (try? initIssuerWithOfferAndAuthorizeRequesterGenericError(issuanceResponseEncryptionSpec: spec)) else { + XCTAssert(false, "Unable to create tuple") + return + } + + // Then + do { + _ = try await issuer.requestSingle( + noProofRequest: authorizedRequest, + credentialIdentifier: .init(value: "MobileDrivingLicense_msoMdoc"), + responseEncryptionSpecProvider: { _ in + return spec + } + ) + + } catch CredentialIssuanceError.responseEncryptionAlgorithmNotSupportedByIssuer { + XCTAssert(true) + + } catch { + print(error.localizedDescription) + XCTAssert(false, error.localizedDescription) + } + } +} + +extension IssuanceEncryptionTest { + + private func initIssuerWithOfferAndAuthorize( + issuanceResponseEncryptionSpec: IssuanceResponseEncryptionSpec + ) async throws -> (AuthorizedRequest, Issuer)? { + + guard let offer = await TestsConstants.createMockCredentialOffer() else { + XCTAssert(false, "Unable to resolve credential offer") + return nil + } + + let issuer = try Issuer( + authorizationServerMetadata: offer.authorizationServerMetadata, + issuerMetadata: offer.credentialIssuerMetadata, + config: config, + parPoster: Poster( + session: NetworkingMock( + path: "pushed_authorization_request_response", + extension: "json" + ) + ), + tokenPoster: Poster( + session: NetworkingMock( + path: "access_token_request_response_no_proof", + extension: "json" + ) + ) + ) + + guard let parRequested = try? await issuer.pushAuthorizationCodeRequest(credentials: offer.credentials).get() else { + XCTAssert(false, "Unable to create request") + return nil + } + + guard let unAuthorized = try? await issuer.handleAuthorizationCode( + parRequested: parRequested, + authorizationCode: .init(authorizationCode: UUID().uuidString) + ).get() else { + XCTAssert(false, "Unable to create request") + return nil + } + + if case .authorizationCode = unAuthorized { + guard let authorizedRequest = try? await issuer.requestAccessToken(authorizationCode: unAuthorized).get() else { + XCTAssert(false, "Could not get authorized request") + return nil + } + return (authorizedRequest, issuer) + + } else { + + XCTAssert(false, "Did not expect .par") + return nil + } + } + + private func initIssuerWithOfferAndAuthorizeRequesterGenericError( + issuanceResponseEncryptionSpec: IssuanceResponseEncryptionSpec + ) async throws -> (AuthorizedRequest, Issuer)? { + + guard let offer = await TestsConstants.createMockCredentialOffer() else { + XCTAssert(false, "Unable to resolve credential offer") + return nil + } + + let issuer = try Issuer( + authorizationServerMetadata: offer.authorizationServerMetadata, + issuerMetadata: offer.credentialIssuerMetadata, + config: config, + parPoster: Poster( + session: NetworkingMock( + path: "pushed_authorization_request_response", + extension: "json" + ) + ), + tokenPoster: Poster( + session: NetworkingMock( + path: "access_token_request_response_no_proof", + extension: "json" + ) + ), + requesterPoster: Poster( + session: NetworkingMock( + path: "no_proof_generic_error_response", + extension: "json" + ) + ) + ) + + guard let parRequested = try? await issuer.pushAuthorizationCodeRequest(credentials: offer.credentials).get() else { + XCTAssert(false, "Unable to create request") + return nil + } + + guard let unAuthorized = try? await issuer.handleAuthorizationCode( + parRequested: parRequested, + authorizationCode: .init(authorizationCode: UUID().uuidString) + ).get() else { + XCTAssert(false, "Unable to create request") + return nil + } + + if case .authorizationCode = unAuthorized { + guard let authorizedRequest = try? await issuer.requestAccessToken(authorizationCode: unAuthorized).get() else { + XCTAssert(false, "Could not get authorized request") + return nil + } + return (authorizedRequest, issuer) + + } else { + + XCTAssert(false, "Did not expect .par") + return nil + } + } +} diff --git a/Tests/Issuance/IssuanceSingleRequestTest.swift b/Tests/Issuance/IssuanceSingleRequestTest.swift new file mode 100644 index 0000000..2b11549 --- /dev/null +++ b/Tests/Issuance/IssuanceSingleRequestTest.swift @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation +import XCTest +import JOSESwift + +@testable import OpenID4VCI + +class IssuanceSingleRequestTest: XCTestCase { + + let config: WalletOpenId4VCIConfig = .init( + clientId: "wallet-dev", + authFlowRedirectionURI: URL(string: "urn:ietf:wg:oauth:2.0:oob")! + ) + + override func setUp() async throws { + try await super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testWhenIssuerRespondsSingleCredentialThenTCredentialExists() async throws { + + // Given + guard let offer = await TestsConstants.createMockCredentialOfferValidEncryption() else { + XCTAssert(false, "Unable to resolve credential offer") + return + } + + // Given + let privateKey = try KeyController.generateRSAPrivateKey() + let publicKey = try KeyController.generateRSAPublicKey(from: privateKey) + + let alg = JWSAlgorithm(.RS256) + let publicKeyJWK = try RSAPublicKey( + publicKey: publicKey, + additionalParameters: [ + "alg": alg.name, + "use": "enc", + "kid": UUID().uuidString + ]) + + let spec = IssuanceResponseEncryptionSpec( + jwk: publicKeyJWK, + privateKey: privateKey, + algorithm: .init(.RSA_OAEP_256), + encryptionMethod: .init(.A128CBC_HS256) + ) + + // When + let issuer = try Issuer( + authorizationServerMetadata: offer.authorizationServerMetadata, + issuerMetadata: offer.credentialIssuerMetadata, + config: config, + parPoster: Poster( + session: NetworkingMock( + path: "pushed_authorization_request_response", + extension: "json" + ) + ), + tokenPoster: Poster( + session: NetworkingMock( + path: "access_token_request_response_no_proof", + extension: "json" + ) + ), + requesterPoster: Poster( + session: NetworkingMock( + path: "single_issuance_success_response_credential", + extension: "json" + ) + ) + ) + + let authorizationCode = "MZqG9bsQ8UALhsGNlY39Yw==" + let request: UnauthorizedRequest = TestsConstants.unAuthorizedRequest + + let issuanceAuthorization: IssuanceAuthorization = .authorizationCode(authorizationCode: authorizationCode) + let unAuthorized = await issuer.handleAuthorizationCode( + parRequested: request, + authorizationCode: issuanceAuthorization + ) + + switch unAuthorized { + case .success(let authorizationCode): + let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + + if case let .success(authorized) = authorizedRequest, + case let .noProofRequired(token) = authorized { + XCTAssert(true, "Got access token: \(token)") + XCTAssert(true, "Is no proof required") + + do { + let result = try await issuer.requestSingle( + noProofRequest: authorized, + credentialIdentifier: .init(value: "eu.europa.ec.eudiw.pid_mso_mdoc"), + responseEncryptionSpecProvider: { _ in + spec + }) + + switch result { + case .success(let request): + switch request { + case .success(let response): + if let result = response.credentialResponses.first { + switch result { + case .deferred: + XCTAssert(false, "Unexpected deferred") + case .issued(_, let credential): + XCTAssert(true, "credential: \(credential)") + return + } + } else { + break + } + case .failed(let error): + XCTAssert(false, error.localizedDescription) + + case .invalidProof(_, let errorDescription): + XCTAssert(false, errorDescription!) + } + XCTAssert(false, "Unexpected request") + case .failure(let error): + XCTAssert(false, error.localizedDescription) + } + } catch { + XCTAssert(false, error.localizedDescription) + } + + return + } + + case .failure(let error): + XCTAssert(false, error.localizedDescription) + } + + XCTAssert(false, "Unable to get access token") + } +} diff --git a/Tests/Mocks/NetworkingMock.swift b/Tests/Mocks/NetworkingMock.swift index 3e1067d..441c496 100644 --- a/Tests/Mocks/NetworkingMock.swift +++ b/Tests/Mocks/NetworkingMock.swift @@ -41,4 +41,10 @@ class NetworkingMock: Networking { let response = HTTPURLResponse(url: .stub(), statusCode: 200, httpVersion: nil, headerFields: [:]) return try (result.get(), response!) } + + func data( + for request: URLRequest + ) async throws -> (Data, URLResponse) { + return try await data(from: URL(string: "https://www.example.com")!) + } }