From d1de722367c1bd13b29ff573496ebdb66748fa76 Mon Sep 17 00:00:00 2001 From: Teddy Newell Date: Tue, 20 Dec 2022 17:25:38 -0800 Subject: [PATCH 1/5] Add test resources for multiple-subject credentials and presentations --- .../input.multisubject.vc.jsonld | 35 ++++++++++ .../input.multisubject.vp.jsonld | 59 +++++++++++++++++ .../signed.good.multisubject.vc.jsonld | 45 +++++++++++++ .../signed.good.multisubject.vp.jsonld | 65 +++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 src/test/resources/com/danubetech/verifiablecredentials/input.multisubject.vc.jsonld create mode 100644 src/test/resources/com/danubetech/verifiablecredentials/input.multisubject.vp.jsonld create mode 100644 src/test/resources/com/danubetech/verifiablecredentials/signed.good.multisubject.vc.jsonld create mode 100644 src/test/resources/com/danubetech/verifiablecredentials/signed.good.multisubject.vp.jsonld diff --git a/src/test/resources/com/danubetech/verifiablecredentials/input.multisubject.vc.jsonld b/src/test/resources/com/danubetech/verifiablecredentials/input.multisubject.vc.jsonld new file mode 100644 index 0000000..bc42677 --- /dev/null +++ b/src/test/resources/com/danubetech/verifiablecredentials/input.multisubject.vc.jsonld @@ -0,0 +1,35 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "schema": "http://schema.org/", + "ex": "https://example.org/examples#", + "givenName": "schema:givenName", + "familyName": "schema:familyName", + "college": "ex:college", + "UniversityDegreeCredential": "ex:UniversityDegreeCredential" + } + ], + "id": "http://example.edu/credentials/1872", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": "https://example.edu/issuers/565049", + "issuanceDate": "2010-01-01T19:73:24Z", + "credentialSubject": [ + { + "id" : "did:example:ebfeb1f712ebc6f1c276e12ec21", + "givenName" : "Manu", + "familyName" : "Sporny", + "college" : { + "id" : "did:example:c276e12ec21ebfeb1f712ebc6f1" + } + }, + { + "id" : "did:example:ebfeb1f712ebc6f1c276e12ec22", + "givenName" : "Manu", + "familyName" : "Sporny", + "college" : { + "id" : "did:example:c276e12ec21ebfeb1f712ebc6f2" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/com/danubetech/verifiablecredentials/input.multisubject.vp.jsonld b/src/test/resources/com/danubetech/verifiablecredentials/input.multisubject.vp.jsonld new file mode 100644 index 0000000..61ffa79 --- /dev/null +++ b/src/test/resources/com/danubetech/verifiablecredentials/input.multisubject.vp.jsonld @@ -0,0 +1,59 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "holder": "did:web:vc.transmute.world", + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "ex": "https://example.org/examples#", + "schema": "http://schema.org/", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "BachelorDegree": "ex:BachelorDegree", + "UniversityDegreeCredential": "ex:UniversityDegreeCredential", + "degree": "ex:degree", + "name": { + "@id": "schema:name", + "@type": "rdf:HTML" + } + } + ], + "id": "http://example.gov/credentials/3732", + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "issuer": "did:v1:test:nym:z6MkhdmzFu659ZJ4XKj31vtEDmjvsi5yDZG5L7Caz63oP39k", + "issuanceDate": "2020-03-10T04:24:12.164Z", + "credentialSubject": [ + { + "id": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts" + } + }, + { + "id": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHe", + "degree": { + "type": "GraduateDegree", + "name": "Master of Arts" + } + } + ], + "proof": { + "type": "RsaSignature2018", + "created": "2018-01-01T21:19:10Z", + "nonce": "c0ae1c8e-c7e7-469f-b252-86e6a0e7387e", + "verificationMethod": "did:sov:1yvXbmgPoUm4dl66D7KhyD#keys-1", + "proofPurpose": "assertionMethod", + "jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJSUzI1NiJ9..adMKtZU8XZhl4f4iQzSxa-5KwU6CiPCwz9U091y7e8eMxqAJpjtmr25N4fmcqkN886aNMyN561Lo1_F2k7bnHof1eibtp36-PFzW-dKrQhHEii2YB5Un4vu_Z0ZAHQl0oXwiDqnOmNdeVUnp2aU_uFaTrpVPUMrQpHMGJgiDuLk-VdVun3R4M4ANg4V14fK52auLoztRM2vxzzG15IeKmB6tJwpmvadlmQAJSZG05kALVF5TmrhESrJQIgZd2TTRgE7sVKbTbn6EGHWVa552oH7ZWvBzgzQH33PlZcx4bo1Y_7ErEKo1lps2Kh1hAY_Wxh2PiN5vIr8g2i7gmclV8Q" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/com/danubetech/verifiablecredentials/signed.good.multisubject.vc.jsonld b/src/test/resources/com/danubetech/verifiablecredentials/signed.good.multisubject.vc.jsonld new file mode 100644 index 0000000..e3f4a5f --- /dev/null +++ b/src/test/resources/com/danubetech/verifiablecredentials/signed.good.multisubject.vc.jsonld @@ -0,0 +1,45 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "schema": "http://schema.org/", + "ex": "https://example.org/examples#", + "givenName": "schema:givenName", + "familyName": "schema:familyName", + "college": "ex:college", + "UniversityDegreeCredential": "ex:UniversityDegreeCredential" + } + ], + "id": "http://example.edu/credentials/1872", + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "issuer": "https://example.edu/issuers/565049", + "issuanceDate": "2010-01-01T19:73:24Z", + "credentialSubject": [ + { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "givenName": "Manu", + "familyName": "Sporny", + "college": { + "id": "did:example:c276e12ec21ebfeb1f712ebc6f1" + } + }, + { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec22", + "givenName": "Manu", + "familyName": "Sporny", + "college": { + "id": "did:example:c276e12ec21ebfeb1f712ebc6f2" + } + } + ], + "proof": { + "type": "RsaSignature2018", + "created": "2018-01-01T21:19:10Z", + "nonce": "c0ae1c8e-c7e7-469f-b252-86e6a0e7387e", + "verificationMethod": "did:sov:1yvXbmgPoUm4dl66D7KhyD#keys-1", + "jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJSUzI1NiJ9..ftxWowm3x1roIwnkdOK8ARWtPzYh9WPmR4QAxnZy2kBA_NgePpqtlIek1lMshx4Syv6N9CJqEXDFeURRNsQWAhhiGYAts_PphIZtrpeXuEh_dnMcyRhO2iFNQby5hwhLRwo_HeuNop73oiJYqFm_P_3_F0NJaKrONQ5dsiM3M2SNwInKVhuZ_x-vOqEX3BDcajBJ0wHn9CCbysynhWmxzLiZ4HxQW3aBWG94GmCQBJVc9pP8H4B58-jwDhz_L74rl5iywH5NiaokBggrBbJcz2QJBMYuYEjsAO6UQAm-T5EJcVzDoy0qxlmxgSaDovbr-xHGU8Wjc-cROt8Kqc1lqw" + } +} \ No newline at end of file diff --git a/src/test/resources/com/danubetech/verifiablecredentials/signed.good.multisubject.vp.jsonld b/src/test/resources/com/danubetech/verifiablecredentials/signed.good.multisubject.vp.jsonld new file mode 100644 index 0000000..e479f3c --- /dev/null +++ b/src/test/resources/com/danubetech/verifiablecredentials/signed.good.multisubject.vp.jsonld @@ -0,0 +1,65 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "holder": "did:web:vc.transmute.world", + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "ex": "https://example.org/examples#", + "schema": "http://schema.org/", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "BachelorDegree": "ex:BachelorDegree", + "UniversityDegreeCredential": "ex:UniversityDegreeCredential", + "degree": "ex:degree", + "name": { + "@id": "schema:name", + "@type": "rdf:HTML" + } + } + ], + "id": "http://example.gov/credentials/3732", + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "issuer": "did:v1:test:nym:z6MkhdmzFu659ZJ4XKj31vtEDmjvsi5yDZG5L7Caz63oP39k", + "issuanceDate": "2020-03-10T04:24:12.164Z", + "credentialSubject": [ + { + "id": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts" + } + }, + { + "id": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHe", + "degree": { + "type": "GraduateDegree", + "name": "Master of Arts" + } + } + ], + "proof": { + "type": "RsaSignature2018", + "created": "2018-01-01T21:19:10Z", + "nonce": "c0ae1c8e-c7e7-469f-b252-86e6a0e7387e", + "verificationMethod": "did:sov:1yvXbmgPoUm4dl66D7KhyD#keys-1", + "jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJSUzI1NiJ9..OfsKRFq63GbZmLEekSN9VV6fcyF7SMs91rIa_GffsPqKhTCbN3wL9hazri-PxGYJYzHswG_0U_aMa5Fkye8hF6jxNJweYcnGTL5N5ipSb6hu6S2zRdiPWnP_L17zIlZJQdXJlqDGiXdi7oroExfrOPsww_cPfylRQAdh7jFF_rNwQLT_XMxriKi8ColXZlItZ4HrxeL4_DssTZPvjfTwQUQ4cNCHQ5Mmri06ghUzvamzPl1_Tp0Gz4D73aXRd-A4eoREK5nPX2BTDzVsZA0X1-9AN5MThdmNOKk3ZwF3IILSSWOmLhDX07YkvtkzTE32g6J9PXjBplShle-QKa8IUg" + } + } + ], + "proof": { + "type": "RsaSignature2018", + "created": "2018-01-01T21:19:10Z", + "nonce": "c0ae1c8e-c7e7-469f-b252-86e6a0e7387e", + "verificationMethod": "did:sov:1yvXbmgPoUm4dl66D7KhyD#keys-1", + "jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJSUzI1NiJ9..lhT2Lezru6k6kW3Fu4X6r30DRvLZpKRMvN6QzYzQS62NsAKdInHx16R1-sn0d6ZAM2NP9fHll_NqcRyKoOfGTHOZQ57aobaCmiRgmNZgl3eWXGwl_X9o47jDHoiOvlj14z4732lo1fgcR0LKqjWXCKXlVyoqkLO0LtHCcc05WdMAznuikXR2qzbkOFONBBq81PpNkX18qKjNCKoiOP6jDyABA8M2z7RidaHXHgyRVFuHt2iFF9MG0av-SxGaGUceRXbhNIo-pla686iMANDBDEqNKmCafBln5FdYW_lvxYqSBREd1J1rGT666GUMpwpgdDx42tJzUACNWBfwGzh4Mg" + } +} \ No newline at end of file From fc12b54efc0cae365ed27464e2eced99f7abaa75 Mon Sep 17 00:00:00 2001 From: Teddy Newell Date: Tue, 20 Dec 2022 17:26:22 -0800 Subject: [PATCH 2/5] Add tests for desired multi-subject behavior --- .../verifiablecredentials/JwtTest.java | 42 +++++++++++++++++++ .../SignCredentialTest.java | 31 ++++++++++++++ .../SignPresentationTest.java | 31 ++++++++++++++ .../VerifyCredentialTest.java | 35 ++++++++++++++++ .../VerifyPresentationTest.java | 36 +++++++++++++++- 5 files changed, 174 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/danubetech/verifiablecredentials/JwtTest.java b/src/test/java/com/danubetech/verifiablecredentials/JwtTest.java index 03f209b..afeed9d 100644 --- a/src/test/java/com/danubetech/verifiablecredentials/JwtTest.java +++ b/src/test/java/com/danubetech/verifiablecredentials/JwtTest.java @@ -10,6 +10,7 @@ import java.net.URI; import java.security.GeneralSecurityException; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -59,6 +60,47 @@ void testSign() throws Exception { assertEquals(TestUtil.read(VerifyCredentialTest.class.getResourceAsStream("jwt.payload.vc.jsonld")).trim(), jwtPayload.trim()); } + @Test + void testSignMultiSubjectUnsupported() throws Exception { + + Map firstClaims = new LinkedHashMap<>(); + Map firstDriversLicense = new LinkedHashMap(); + firstDriversLicense.put("licenseClass", "trucks"); + firstDriversLicense.put("suspended", Boolean.FALSE); + firstClaims.put("name", "M S"); + firstClaims.put("driversLicense", firstDriversLicense); + + CredentialSubject firstCredentialSubject = CredentialSubject.builder() + .id(URI.create("did:sov:21tDAKCERh95uGgKbJNHYp")) + .claims(firstClaims) + .build(); + + Map secondClaims = new LinkedHashMap<>(); + Map secondDriversLicense = new LinkedHashMap(); + secondDriversLicense.put("licenseClass", "motorcycles"); + secondDriversLicense.put("suspended", Boolean.TRUE); + secondClaims.put("name", "M S"); + secondClaims.put("driversLicense", secondDriversLicense); + + CredentialSubject secondCredentialSubject = CredentialSubject.builder() + .id(URI.create("did:sov:21tDAKCERh95uGgKbJNHYq")) + .claims(secondClaims) + .build(); + + VerifiableCredential verifiableCredential = VerifiableCredential.builder() + .context(URI.create("https://trafi.fi/credentials/v1")) + .type("DriversLicenseCredential") + .id(URI.create("urn:uuid:a87bdfb8-a7df-4bd9-ae0d-d883133538fe")) + .issuer(URI.create("did:sov:1yvXbmgPoUm4dl66D7KhyD")) + .issuanceDate(JsonLDUtils.stringToDate("2019-06-16T18:56:59Z")) + .expirationDate(JsonLDUtils.stringToDate("2019-06-17T18:56:59Z")) + .credentialSubjects(List.of(firstCredentialSubject, secondCredentialSubject)) + .build(); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { ToJwtConverter.toJwtVerifiableCredential(verifiableCredential); }); + assertEquals("JWTs are not capable of encoding multiple subjects and are thus not capable of encoding a verifiable credential with more than one subject.", exception.getMessage()); + } + @Test void testVerify() throws Exception { diff --git a/src/test/java/com/danubetech/verifiablecredentials/SignCredentialTest.java b/src/test/java/com/danubetech/verifiablecredentials/SignCredentialTest.java index 6b5d0a1..f752eb6 100644 --- a/src/test/java/com/danubetech/verifiablecredentials/SignCredentialTest.java +++ b/src/test/java/com/danubetech/verifiablecredentials/SignCredentialTest.java @@ -47,4 +47,35 @@ void testSign() throws Throwable { boolean verify = verifier.verify(verifiableCredential); assertTrue(verify); } + + @Test + void testSignMultiSubject() throws Throwable { + + VerifiableCredential verifiableCredential = VerifiableCredential.fromJson(new InputStreamReader(VerifyCredentialTest.class.getResourceAsStream("input.multisubject.vc.jsonld"))); + + URI verificationMethod = URI.create("did:sov:1yvXbmgPoUm4dl66D7KhyD#keys-1"); + Date created = JsonLDUtils.DATE_FORMAT.parse("2018-01-01T21:19:10Z"); + String domain = null; + String nonce = "c0ae1c8e-c7e7-469f-b252-86e6a0e7387e"; + + RsaSignature2018LdSigner signer = new RsaSignature2018LdSigner(TestUtil.testRSAPrivateKey); + signer.setVerificationMethod(verificationMethod); + signer.setCreated(created); + signer.setDomain(domain); + signer.setNonce(nonce); + LdProof ldProof = signer.sign(verifiableCredential, true, false); + + assertEquals(SignatureSuites.SIGNATURE_SUITE_RSASIGNATURE2018.getTerm(), ldProof.getType()); + assertEquals(verificationMethod, ldProof.getVerificationMethod()); + assertEquals(created, ldProof.getCreated()); + assertEquals(domain, ldProof.getDomain()); + assertEquals(nonce, ldProof.getNonce()); + assertEquals("eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJSUzI1NiJ9..ftxWowm3x1roIwnkdOK8ARWtPzYh9WPmR4QAxnZy2kBA_NgePpqtlIek1lMshx4Syv6N9CJqEXDFeURRNsQWAhhiGYAts_PphIZtrpeXuEh_dnMcyRhO2iFNQby5hwhLRwo_HeuNop73oiJYqFm_P_3_F0NJaKrONQ5dsiM3M2SNwInKVhuZ_x-vOqEX3BDcajBJ0wHn9CCbysynhWmxzLiZ4HxQW3aBWG94GmCQBJVc9pP8H4B58-jwDhz_L74rl5iywH5NiaokBggrBbJcz2QJBMYuYEjsAO6UQAm-T5EJcVzDoy0qxlmxgSaDovbr-xHGU8Wjc-cROt8Kqc1lqw", ldProof.getJws()); + + Validation.validate(verifiableCredential); + + RsaSignature2018LdVerifier verifier = new RsaSignature2018LdVerifier(TestUtil.testRSAPublicKey); + boolean verify = verifier.verify(verifiableCredential); + assertTrue(verify); + } } diff --git a/src/test/java/com/danubetech/verifiablecredentials/SignPresentationTest.java b/src/test/java/com/danubetech/verifiablecredentials/SignPresentationTest.java index 5af2f33..b9a5af4 100644 --- a/src/test/java/com/danubetech/verifiablecredentials/SignPresentationTest.java +++ b/src/test/java/com/danubetech/verifiablecredentials/SignPresentationTest.java @@ -47,4 +47,35 @@ void testSign() throws Throwable { boolean verify = verifier.verify(verifiablePresentation); assertTrue(verify); } + + @Test + void testMultiSubjectSign() throws Throwable { + + VerifiablePresentation verifiablePresentation = VerifiablePresentation.fromJson(new InputStreamReader(VerifyCredentialTest.class.getResourceAsStream("input.multisubject.vp.jsonld"))); + + URI verificationMethod = URI.create("did:sov:1yvXbmgPoUm4dl66D7KhyD#keys-1"); + Date created = JsonLDUtils.DATE_FORMAT.parse("2018-01-01T21:19:10Z"); + String domain = null; + String nonce = "c0ae1c8e-c7e7-469f-b252-86e6a0e7387e"; + + RsaSignature2018LdSigner signer = new RsaSignature2018LdSigner(TestUtil.testRSAPrivateKey); + signer.setVerificationMethod(verificationMethod); + signer.setCreated(created); + signer.setDomain(domain); + signer.setNonce(nonce); + LdProof ldSignature = signer.sign(verifiablePresentation, true, false); + + assertEquals(SignatureSuites.SIGNATURE_SUITE_RSASIGNATURE2018.getTerm(), ldSignature.getType()); + assertEquals(verificationMethod, ldSignature.getVerificationMethod()); + assertEquals(created, ldSignature.getCreated()); + assertEquals(domain, ldSignature.getDomain()); + assertEquals(nonce, ldSignature.getNonce()); + assertEquals("eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJSUzI1NiJ9..RNCMCgkZDI0gAQVXjG2LlJuAIFMv5ULpLE0u8dIryRTaqmLgx6gKXOTmr44qqIT2DmuMt_6guUXckXwKnRhBxHd0UP31QjCIK-DEJkNxOlccXqQUAxOj8pbS2IIrBh_a0lWeOxDrLRnLriqeLFvc_ihOOCkh45dTdCE7yhcf1QYX2Bn3-iZJlfLqHShI7tKC1n0S9-2f_KbOOLzfghY5pSEEPn-6QKQPhVSx0_G6t0zQXU6ti1eTx0Mx1E3qQ92eCrnxYkrLpDdhzvUCdh_vl8DwYzPnFYECZAkQxLKdeRmoISoiXhxQekhPPhz4JqJajBhpabTof1wv7qGwyw6qow", ldSignature.getJws()); + + Validation.validate(verifiablePresentation); + + RsaSignature2018LdVerifier verifier = new RsaSignature2018LdVerifier(TestUtil.testRSAPublicKey); + boolean verify = verifier.verify(verifiablePresentation); + assertTrue(verify); + } } diff --git a/src/test/java/com/danubetech/verifiablecredentials/VerifyCredentialTest.java b/src/test/java/com/danubetech/verifiablecredentials/VerifyCredentialTest.java index 92c371b..747b88c 100644 --- a/src/test/java/com/danubetech/verifiablecredentials/VerifyCredentialTest.java +++ b/src/test/java/com/danubetech/verifiablecredentials/VerifyCredentialTest.java @@ -1,10 +1,15 @@ package com.danubetech.verifiablecredentials; import com.danubetech.verifiablecredentials.validation.Validation; +import foundation.identity.jsonld.JsonLDException; import info.weboftrust.ldsignatures.verifier.RsaSignature2018LdVerifier; import org.junit.jupiter.api.Test; +import java.io.IOException; import java.io.InputStreamReader; +import java.security.GeneralSecurityException; +import java.util.List; +import java.util.Set; import static org.junit.jupiter.api.Assertions.*; @@ -26,6 +31,36 @@ void testVerify() throws Throwable { String givenName = credentialSubject == null ? null : (String) credentialSubject.getClaims().get("givenName"); assertEquals("Manu", givenName); + + // Credential subject as array has one entry + List credentialSubjects = verifiableCredential.getCredentialSubjects(); + + assertEquals(1, credentialSubjects.size()); + assertEquals(credentialSubject, credentialSubjects.get(0)); + } + + @Test + void testMultiSubjectVerify() throws Throwable { + + VerifiableCredential verifiableCredential = VerifiableCredential.fromJson(new InputStreamReader(VerifyCredentialTest.class.getResourceAsStream("signed.good.multisubject.vc.jsonld"))); + + Validation.validate(verifiableCredential); + + RsaSignature2018LdVerifier verifier = new RsaSignature2018LdVerifier(TestUtil.testRSAPublicKey); + boolean verify = verifier.verify(verifiableCredential); + + assertTrue(verify); + + // First credential subject still available + CredentialSubject credentialSubject = verifiableCredential.getCredentialSubject(); + String givenName = credentialSubject == null ? null : (String) credentialSubject.getClaims().get("givenName"); + + assertEquals("Manu", givenName); + + // Multiple credential subjects also available + List credentialSubjects = verifiableCredential.getCredentialSubjects(); + + assertEquals(2, credentialSubjects.size()); } @Test diff --git a/src/test/java/com/danubetech/verifiablecredentials/VerifyPresentationTest.java b/src/test/java/com/danubetech/verifiablecredentials/VerifyPresentationTest.java index 7276c1d..ceac698 100644 --- a/src/test/java/com/danubetech/verifiablecredentials/VerifyPresentationTest.java +++ b/src/test/java/com/danubetech/verifiablecredentials/VerifyPresentationTest.java @@ -9,12 +9,19 @@ import com.danubetech.keyformats.crypto.provider.impl.JavaSHA256Provider; import com.danubetech.keyformats.crypto.provider.impl.TinkEd25519Provider; import com.danubetech.verifiablecredentials.validation.Validation; +import foundation.identity.jsonld.JsonLDUtils; +import info.weboftrust.ldsignatures.LdProof; +import info.weboftrust.ldsignatures.signer.RsaSignature2018LdSigner; +import info.weboftrust.ldsignatures.suites.SignatureSuites; import info.weboftrust.ldsignatures.verifier.Ed25519Signature2018LdVerifier; +import info.weboftrust.ldsignatures.verifier.RsaSignature2018LdVerifier; import org.bitcoinj.core.Base58; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.InputStreamReader; +import java.net.URI; +import java.util.Date; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -28,10 +35,12 @@ public class VerifyPresentationTest { final static VerifiablePresentation verifiablePresentationGood1; final static VerifiablePresentation verifiablePresentationGood2; + final static VerifiablePresentation verifiablePresentationGoodMultiSubject; final static VerifiablePresentation verifiablePresentationBad1; final static VerifiablePresentation verifiablePresentationBad2; final static VerifiableCredential verifiableCredentialGood1; final static VerifiableCredential verifiableCredentialGood2; + final static VerifiableCredential verifiableCredentialGoodMultiSubject; final static VerifiableCredential verifiableCredentialBad1; final static VerifiableCredential verifiableCredentialBad2; @@ -46,10 +55,12 @@ public class VerifyPresentationTest { verifiablePresentationGood1 = VerifiablePresentation.fromJson(new InputStreamReader(VerifyPresentationTest.class.getResourceAsStream("signed.good.vp1.jsonld"))); verifiablePresentationGood2 = VerifiablePresentation.fromJson(new InputStreamReader(VerifyPresentationTest.class.getResourceAsStream("signed.good.vp2.jsonld"))); + verifiablePresentationGoodMultiSubject = VerifiablePresentation.fromJson(new InputStreamReader(VerifyPresentationTest.class.getResourceAsStream("signed.good.multisubject.vp.jsonld"))); verifiablePresentationBad1 = VerifiablePresentation.fromJson(new InputStreamReader(VerifyPresentationTest.class.getResourceAsStream("signed.bad.vp1.jsonld"))); verifiablePresentationBad2 = VerifiablePresentation.fromJson(new InputStreamReader(VerifyPresentationTest.class.getResourceAsStream("signed.bad.vp2.jsonld"))); verifiableCredentialGood1 = verifiablePresentationGood1.getVerifiableCredential(); verifiableCredentialGood2 = verifiablePresentationGood2.getVerifiableCredential(); + verifiableCredentialGoodMultiSubject = verifiablePresentationGoodMultiSubject.getVerifiableCredential(); verifiableCredentialBad1 = verifiablePresentationBad1.getVerifiableCredential(); verifiableCredentialBad2 = verifiablePresentationBad2.getVerifiableCredential(); } catch (Exception ex) { @@ -71,10 +82,12 @@ void testValidity() { Validation.validate(verifiablePresentationGood1); Validation.validate(verifiablePresentationGood2); + Validation.validate(verifiablePresentationGoodMultiSubject); Validation.validate(verifiablePresentationBad1); Validation.validate(verifiablePresentationBad2); Validation.validate(verifiableCredentialGood1); Validation.validate(verifiableCredentialGood2); + Validation.validate(verifiableCredentialGoodMultiSubject); Validation.validate(verifiableCredentialBad1); Validation.validate(verifiableCredentialBad2); } @@ -100,9 +113,21 @@ void testVerifyGoodCredential2() throws Exception { boolean verify = verifier.verify(verifiableCredentialGood2); assertTrue(verify); - assertEquals("Bachelor of Science and Arts", ((Map) verifiableCredentialGood1.getCredentialSubject().getClaims().get("degree")).get("name")); + assertEquals("Bachelor of Science and Arts", ((Map) verifiableCredentialGood2.getCredentialSubject().getClaims().get("degree")).get("name")); + } + + @Test + void testVerifyGoodCredentialMultiSubject() throws Exception { + + RsaSignature2018LdVerifier verifier = new RsaSignature2018LdVerifier(TestUtil.testRSAPublicKey); + boolean verify = verifier.verify(verifiableCredentialGoodMultiSubject); + + assertTrue(verify); + assertEquals("Bachelor of Science and Arts", ((Map) verifiableCredentialGoodMultiSubject.getCredentialSubject().getClaims().get("degree")).get("name")); + assertEquals("Master of Arts", ((Map) verifiableCredentialGoodMultiSubject.getCredentialSubjects().get(1).getClaims().get("degree")).get("name")); } + /* * BAD CREDENTIAL */ @@ -149,6 +174,15 @@ void testVerifyGoodPresentation2() throws Exception { assertTrue(verify); } + @Test + void testVerifyGoodPresentationMultiSubject() throws Exception { + + RsaSignature2018LdVerifier verifier = new RsaSignature2018LdVerifier(TestUtil.testRSAPublicKey); + boolean verify = verifier.verify(verifiablePresentationGoodMultiSubject); + + assertTrue(verify); + } + /* * BAD PRESENTATION */ From 2bf5e3d04fe108a18d15adb0e0164a95a2bbbcb4 Mon Sep 17 00:00:00 2001 From: Teddy Newell Date: Tue, 20 Dec 2022 17:38:24 -0800 Subject: [PATCH 3/5] Add plural credential subject access and builder to VerifiableCredential --- .../VerifiableCredential.java | 36 ++++++++++++++++--- .../jwt/ToJwtConverter.java | 12 +++++++ .../validation/Validation.java | 1 + 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/danubetech/verifiablecredentials/VerifiableCredential.java b/src/main/java/com/danubetech/verifiablecredentials/VerifiableCredential.java index 90da4ce..0cd973a 100644 --- a/src/main/java/com/danubetech/verifiablecredentials/VerifiableCredential.java +++ b/src/main/java/com/danubetech/verifiablecredentials/VerifiableCredential.java @@ -11,7 +11,9 @@ import java.io.Reader; import java.net.URI; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Map; public class VerifiableCredential extends JsonLDObject { @@ -39,12 +41,13 @@ public static class Builder> extends JsonLDObject.Builder credentialSubjects; private CredentialStatus credentialStatus; private LdProof ldProof; public Builder(VerifiableCredential jsonLdObject) { super(jsonLdObject); + this.credentialSubjects = new ArrayList<>(); this.forceContextsArray(true); this.forceTypesArray(true); this.defaultContexts(true); @@ -60,7 +63,13 @@ public VerifiableCredential build() { if (this.issuer != null) JsonLDUtils.jsonLdAdd(this.jsonLdObject, VerifiableCredentialKeywords.JSONLD_TERM_ISSUER, JsonLDUtils.uriToString(this.issuer)); if (this.issuanceDate != null) JsonLDUtils.jsonLdAdd(this.jsonLdObject, VerifiableCredentialKeywords.JSONLD_TERM_ISSUANCEDATE, JsonLDUtils.dateToString(this.issuanceDate)); if (this.expirationDate != null) JsonLDUtils.jsonLdAdd(this.jsonLdObject, VerifiableCredentialKeywords.JSONLD_TERM_EXPIRATIONDATE, JsonLDUtils.dateToString(this.expirationDate)); - if (this.credentialSubject != null) this.credentialSubject.addToJsonLDObject(this.jsonLdObject); + if (this.credentialSubjects != null) { + if (this.credentialSubjects.size() == 1) { + this.credentialSubjects.get(0).addToJsonLDObject(this.jsonLdObject); + } else { + JsonLDUtils.jsonLdAddAsJsonArray(this.jsonLdObject, VerifiableCredentialKeywords.JSONLD_TERM_CREDENTIALSUBJECT, this.credentialSubjects); + } + } if (this.credentialStatus != null) this.credentialStatus.addToJsonLDObject(this.jsonLdObject); if (this.ldProof != null) this.ldProof.addToJsonLDObject(this.jsonLdObject); @@ -83,7 +92,12 @@ public B expirationDate(Date expirationDate) { } public B credentialSubject(CredentialSubject credentialSubject) { - this.credentialSubject = credentialSubject; + this.credentialSubjects.add(credentialSubject); + return (B) this; + } + + public B credentialSubjects(List credentialSubjects) { + this.credentialSubjects = credentialSubjects; return (B) this; } @@ -149,7 +163,21 @@ public Date getExpirationDate() { } public CredentialSubject getCredentialSubject() { - return CredentialSubject.getFromJsonLDObject(this); + return getCredentialSubjects().get(0); + } + + /** + * Get a list of all subjects contained in the receiver. + * + * @return A list of each subject in the "credentialSubject" property as `CredentialSubject` objects. + */ + public List getCredentialSubjects() { + /* + The "credentialSubject" node may contain an array of objects as permissible in the VC spec: + https://www.w3.org/TR/vc-data-model/#credential-subject + */ + return new ArrayList(); + } public LdProof getLdProof() { diff --git a/src/main/java/com/danubetech/verifiablecredentials/jwt/ToJwtConverter.java b/src/main/java/com/danubetech/verifiablecredentials/jwt/ToJwtConverter.java index 8da8001..2b3efc5 100644 --- a/src/main/java/com/danubetech/verifiablecredentials/jwt/ToJwtConverter.java +++ b/src/main/java/com/danubetech/verifiablecredentials/jwt/ToJwtConverter.java @@ -38,6 +38,18 @@ public static JwtVerifiableCredential toJwtVerifiableCredential(VerifiableCreden CredentialSubject credentialSubject = verifiableCredential.getCredentialSubject(); if (credentialSubject != null) { + if (verifiableCredential.getCredentialSubjects().size() > 1) { + /* + Per https://www.w3.org/TR/vc-data-model/#jwt-encoding: + + Implementers are warned that JWTs are not capable of encoding multiple subjects and are thus not capable of + encoding a verifiable credential with more than one subject. JWTs might support multiple subjects in the + future and implementers are advised to refer to the JSON Web Token Claim Registry for multi-subject JWT + claim names or the Nested JSON Web Token specification. + */ + throw new IllegalArgumentException("JWTs are not capable of encoding multiple subjects and are thus not capable of encoding a verifiable credential with more than one subject."); + } + if (credentialSubject.getId() != null) { jwtPayloadBuilder.subject(credentialSubject.getId().toString()); } diff --git a/src/main/java/com/danubetech/verifiablecredentials/validation/Validation.java b/src/main/java/com/danubetech/verifiablecredentials/validation/Validation.java index 63135f0..f3efec0 100644 --- a/src/main/java/com/danubetech/verifiablecredentials/validation/Validation.java +++ b/src/main/java/com/danubetech/verifiablecredentials/validation/Validation.java @@ -53,6 +53,7 @@ public static void validate(VerifiableCredential verifiableCredential) throws Il validateRun(() -> { validateTrue(verifiableCredential.getIssuanceDate() != null); }, "Bad or missing 'issuanceDate'."); validateRun(() -> { verifiableCredential.getExpirationDate(); }, "Bad 'expirationDate'."); validateRun(() -> { verifiableCredential.getCredentialSubject(); }, "Bad 'credentialSubject'."); + validateRun(() -> { validateTrue(verifiableCredential.getCredentialSubjects().size() > 0); }, "Bad or missing 'credentialSubject'"); validateRun(() -> { validateTrue(verifiableCredential.getCredentialSubject() != null); }, "Bad or missing 'credentialSubject'."); } From 136aff966b3209fda5248133a57e12a9fd57503a Mon Sep 17 00:00:00 2001 From: Teddy Newell Date: Wed, 21 Dec 2022 15:01:05 -0800 Subject: [PATCH 4/5] Use ArrayList instead of List.of in JwtTest --- .../java/com/danubetech/verifiablecredentials/JwtTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/danubetech/verifiablecredentials/JwtTest.java b/src/test/java/com/danubetech/verifiablecredentials/JwtTest.java index afeed9d..282bded 100644 --- a/src/test/java/com/danubetech/verifiablecredentials/JwtTest.java +++ b/src/test/java/com/danubetech/verifiablecredentials/JwtTest.java @@ -9,6 +9,7 @@ import java.net.URI; import java.security.GeneralSecurityException; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -94,7 +95,10 @@ void testSignMultiSubjectUnsupported() throws Exception { .issuer(URI.create("did:sov:1yvXbmgPoUm4dl66D7KhyD")) .issuanceDate(JsonLDUtils.stringToDate("2019-06-16T18:56:59Z")) .expirationDate(JsonLDUtils.stringToDate("2019-06-17T18:56:59Z")) - .credentialSubjects(List.of(firstCredentialSubject, secondCredentialSubject)) + .credentialSubjects(new ArrayList() {{ + add(firstCredentialSubject); + add(secondCredentialSubject); + }}) .build(); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { ToJwtConverter.toJwtVerifiableCredential(verifiableCredential); }); From 86ff55a22670fc298affac1dbf9cca98da775bf3 Mon Sep 17 00:00:00 2001 From: Teddy Newell Date: Wed, 21 Dec 2022 15:02:19 -0800 Subject: [PATCH 5/5] Implement subject extraction from array or object --- .../VerifiableCredential.java | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/danubetech/verifiablecredentials/VerifiableCredential.java b/src/main/java/com/danubetech/verifiablecredentials/VerifiableCredential.java index 0cd973a..b74c588 100644 --- a/src/main/java/com/danubetech/verifiablecredentials/VerifiableCredential.java +++ b/src/main/java/com/danubetech/verifiablecredentials/VerifiableCredential.java @@ -5,16 +5,24 @@ import com.danubetech.verifiablecredentials.jsonld.VerifiableCredentialContexts; import com.danubetech.verifiablecredentials.jsonld.VerifiableCredentialKeywords; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import foundation.identity.jsonld.ConfigurableDocumentLoader; import foundation.identity.jsonld.JsonLDObject; import foundation.identity.jsonld.JsonLDUtils; import info.weboftrust.ldsignatures.LdProof; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import javax.security.auth.login.CredentialException; import java.io.Reader; import java.net.URI; import java.util.ArrayList; import java.util.Date; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public class VerifiableCredential extends JsonLDObject { @@ -23,6 +31,8 @@ public class VerifiableCredential extends JsonLDObject { public static final String DEFAULT_JSONLD_PREDICATE = VerifiableCredentialKeywords.JSONLD_TERM_VERIFIABLECREDENTIAL; public static final DocumentLoader DEFAULT_DOCUMENT_LOADER = VerifiableCredentialContexts.DOCUMENT_LOADER; + private static final ObjectMapper objectMapper = new ObjectMapper(); + @JsonCreator public VerifiableCredential() { super(); @@ -176,8 +186,26 @@ public List getCredentialSubjects() { The "credentialSubject" node may contain an array of objects as permissible in the VC spec: https://www.w3.org/TR/vc-data-model/#credential-subject */ - return new ArrayList(); - + Object entry = this.getJsonObject().get(VerifiableCredentialKeywords.JSONLD_TERM_CREDENTIALSUBJECT); + if (entry == null) { + return new ArrayList(); // empty list + } else if (entry instanceof List) { + final TypeReference> stringMapTypeReference = new TypeReference>() {}; + + final List subjectArray = JsonLDUtils.jsonLdGetJsonArray(this.getJsonObject(), VerifiableCredentialKeywords.JSONLD_TERM_CREDENTIALSUBJECT); + + // Convert each object in the list to a CredentialSubject + return subjectArray.stream() + .map(subjectObject -> objectMapper.convertValue(subjectObject, stringMapTypeReference)) + .map(subjectMap -> CredentialSubject.fromJsonObject(subjectMap)) + .collect(Collectors.toList()); + } else { + final Map subjectObject = JsonLDUtils.jsonLdGetJsonObject(this.getJsonObject(), VerifiableCredentialKeywords.JSONLD_TERM_CREDENTIALSUBJECT); + final CredentialSubject singleSubject = CredentialSubject.fromJsonObject(subjectObject); + return new ArrayList() {{ + add(singleSubject); + }}; + } } public LdProof getLdProof() {