diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml
index 671bfd9..f5a1923 100644
--- a/inspector-vc/pom.xml
+++ b/inspector-vc/pom.xml
@@ -5,7 +5,7 @@
org.1edtech
inspector
- 0.9.11
+ 0.9.12
inspector-vc
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java
index 5d402f1..fda8378 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java
@@ -12,6 +12,7 @@ import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.vc.VerifiableCredential;
import org.oneedtech.inspect.vc.W3CVCHolder;
+import org.oneedtech.inspect.vc.verification.Ed25519Signature2022LdVerifier;
import com.apicatalog.jsonld.StringUtils;
import com.apicatalog.jsonld.document.Document;
@@ -22,6 +23,7 @@ import com.apicatalog.multicodec.Multicodec.Codec;
import info.weboftrust.ldsignatures.LdProof;
import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier;
+import info.weboftrust.ldsignatures.verifier.LdVerifier;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
@@ -55,23 +57,18 @@ public class EmbeddedProofProbe extends Probe {
}
// get proof of standard type and purpose
- Optional selectedProof = proofs.stream().filter(
- proof -> proof.isType("Ed25519Signature2020") && proof.getProofPurpose().equals("assertionMethod"))
- .findFirst();
+ Optional selectedProof = proofs.stream()
+ .filter(proof -> proof.getProofPurpose().equals("assertionMethod"))
+ .filter(proof -> proof.isType("Ed25519Signature2020") ||
+ (proof.isType("DataIntegrityProof") && proof.getJsonObject().containsKey("cryptosuite") && proof.getJsonObject().get("cryptosuite").equals("eddsa-2022")))
+ .findFirst();
if (!selectedProof.isPresent()) {
- return error("No proof with type \"Ed25519Signature2020\" or proof purpose \"assertionMethod\" found", ctx);
+ return error("No proof with type any of (\"Ed25519Signature2020\", \"DataIntegrityProof\" with cryptosuite attr of \"eddsa-2022\") or proof purpose \"assertionMethod\" found", ctx);
}
LdProof proof = selectedProof.get();
- if (!proof.isType("Ed25519Signature2020")) {
- return error("Unknown proof type: " + proof.getType(), ctx);
- }
- if (!proof.getProofPurpose().equals("assertionMethod")) {
- return error("Invalid proof purpose: " + proof.getProofPurpose(), ctx);
- }
-
URI method = proof.getVerificationMethod();
// The verification method must dereference to an Ed25519VerificationKey2020.
@@ -166,7 +163,7 @@ public class EmbeddedProofProbe extends Probe {
}
if (!anyMatch) {
return error("Assertion method " + method + " not found in DID document.", ctx);
- }
+ }
}
// get keys from "verificationMethod"
@@ -245,7 +242,8 @@ public class EmbeddedProofProbe extends Probe {
// Extract the publicKey bytes from the Multicodec
byte[] publicKey = Multicodec.decode(Codec.Ed25519PublicKey, publicKeyMulticodec);
- Ed25519Signature2020LdVerifier verifier = new Ed25519Signature2020LdVerifier(publicKey);
+ // choose verifier
+ LdVerifier> verifier = getVerifier(proof, publicKey);
try {
boolean verify = verifier.verify(credentialHolder.getCredential(), proof);
@@ -259,6 +257,12 @@ public class EmbeddedProofProbe extends Probe {
return success(ctx);
}
+ private LdVerifier> getVerifier(LdProof proof, byte[] publicKey) {
+ return proof.isType("Ed25519Signature2020")
+ ? new Ed25519Signature2020LdVerifier(publicKey)
+ : new Ed25519Signature2022LdVerifier(publicKey);
+ }
+
private Boolean IsValidPublicKeyMultibase(String publicKeyMultibase) {
try {
byte[] publicKeyMulticodec = Multibase.decode(publicKeyMultibase);
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java
index c969f79..22f05fe 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java
@@ -114,6 +114,7 @@ public class CachingDocumentLoader extends ConfigurableDocumentLoader {
.put("https://www.w3.org/ns/did/v1", Resources.getResource("contexts/did-v1.jsonld"))
.put("https://www.w3.org/ns/odrl.jsonld", Resources.getResource("contexts/odrl.jsonld"))
.put("https://w3id.org/security/suites/ed25519-2020/v1",Resources.getResource("contexts/security-suites-ed25519-2020-v1.jsonld"))
+ .put("https://w3id.org/security/data-integrity/v1",Resources.getResource("contexts/data-integrity-v1.jsonld"))
.put("https://www.w3.org/2018/credentials/v1", Resources.getResource("contexts/2018-credentials-v1.jsonld"))
.put("https://w3id.org/security/v1", Resources.getResource("contexts/security-v1.jsonld"))
.put("https://w3id.org/security/v2", Resources.getResource("contexts/security-v2.jsonld"))
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/Ed25519Signature2022LdVerifier.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/Ed25519Signature2022LdVerifier.java
new file mode 100644
index 0000000..2f2fce0
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/Ed25519Signature2022LdVerifier.java
@@ -0,0 +1,52 @@
+package org.oneedtech.inspect.vc.verification;
+
+import com.danubetech.keyformats.crypto.ByteVerifier;
+import com.danubetech.keyformats.crypto.impl.Ed25519_EdDSA_PublicKeyVerifier;
+import com.danubetech.keyformats.jose.JWSAlgorithm;
+import info.weboftrust.ldsignatures.LdProof;
+import info.weboftrust.ldsignatures.verifier.LdVerifier;
+import io.ipfs.multibase.Multibase;
+
+import java.security.GeneralSecurityException;
+
+public class Ed25519Signature2022LdVerifier extends LdVerifier {
+
+ public Ed25519Signature2022LdVerifier(ByteVerifier verifier) {
+
+ super(SignatureSuites.SIGNATURE_SUITE_ED25519SIGNATURE2022, verifier, new URDNA2015Canonicalizer());
+ }
+
+ public Ed25519Signature2022LdVerifier(byte[] publicKey) {
+
+ this(new Ed25519_EdDSA_PublicKeyVerifier(publicKey));
+ }
+
+ public Ed25519Signature2022LdVerifier() {
+
+ this((ByteVerifier) null);
+ }
+
+ public static boolean verify(byte[] signingInput, LdProof ldProof, ByteVerifier verifier) throws GeneralSecurityException {
+
+ // verify
+
+ String proofValue = ldProof.getProofValue();
+ if (proofValue == null) throw new GeneralSecurityException("No 'proofValue' in proof.");
+
+ boolean verify;
+
+ byte[] bytes = Multibase.decode(proofValue);
+ verify = verifier.verify(signingInput, bytes, JWSAlgorithm.EdDSA);
+
+ // done
+
+ return verify;
+ }
+
+ @Override
+ public boolean verify(byte[] signingInput, LdProof ldProof) throws GeneralSecurityException {
+
+ return verify(signingInput, ldProof, this.getVerifier());
+ }
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/Ed25519Signature2022SignatureSuite.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/Ed25519Signature2022SignatureSuite.java
new file mode 100644
index 0000000..33790e9
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/Ed25519Signature2022SignatureSuite.java
@@ -0,0 +1,28 @@
+package org.oneedtech.inspect.vc.verification;
+
+import com.danubetech.keyformats.jose.JWSAlgorithm;
+import com.danubetech.keyformats.jose.KeyTypeName;
+import info.weboftrust.ldsignatures.suites.SignatureSuite;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public class Ed25519Signature2022SignatureSuite extends SignatureSuite {
+
+ Ed25519Signature2022SignatureSuite() {
+
+ super(
+ "DataIntegrityProof",
+ URI.create("https://www.w3.org/TR/vc-di-eddsa"),
+ URI.create("https://w3id.org/security#URDNA2015"),
+ URI.create("http://w3id.org/digests#sha256"),
+ URI.create("http://w3id.org/security#ed25519"),
+ List.of(KeyTypeName.Ed25519),
+ Map.of(KeyTypeName.Ed25519, List.of(JWSAlgorithm.EdDSA)),
+ Arrays.asList(LDSecurityContexts.JSONLD_CONTEXT_W3ID_SUITES_ED25519_2022_V1,
+ info.weboftrust.ldsignatures.jsonld.LDSecurityContexts.JSONLD_CONTEXT_W3ID_SECURITY_V3));
+ }
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/Eddsa2022LdProof.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/Eddsa2022LdProof.java
new file mode 100644
index 0000000..b163d3e
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/Eddsa2022LdProof.java
@@ -0,0 +1,49 @@
+package org.oneedtech.inspect.vc.verification;
+
+import java.net.URI;
+
+import com.apicatalog.jsonld.loader.DocumentLoader;
+
+import foundation.identity.jsonld.JsonLDObject;
+import foundation.identity.jsonld.JsonLDUtils;
+import info.weboftrust.ldsignatures.LdProof;
+
+public class Eddsa2022LdProof extends LdProof {
+ public static final URI[] DEFAULT_JSONLD_CONTEXTS = { LDSecurityContexts.JSONLD_CONTEXT_W3ID_SUITES_ED25519_2022_V1 };
+ public static final DocumentLoader DEFAULT_DOCUMENT_LOADER = LDSecurityContexts.DOCUMENT_LOADER;
+
+ public static Builder extends Builder>> builder() {
+ return new Builder(new Eddsa2022LdProof());
+ }
+
+ /*
+ * Factory methods
+ */
+
+ public static class Builder> extends LdProof.Builder {
+
+ private boolean addCryptosuite = true;
+
+ public Builder(LdProof jsonLdObject) {
+ super(jsonLdObject);
+ }
+
+ @Override
+ public B base(JsonLDObject base) {
+ addCryptosuite = false;
+ return super.base(base);
+ }
+
+ @Override
+ public LdProof build() {
+ super.build();
+
+ if (addCryptosuite) {
+ JsonLDUtils.jsonLdAdd(this.jsonLdObject, "cryptosuite", "eddsa-2022");
+ }
+
+ return (LdProof) this.jsonLdObject;
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/LDSecurityContexts.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/LDSecurityContexts.java
new file mode 100644
index 0000000..f8652e1
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/LDSecurityContexts.java
@@ -0,0 +1,43 @@
+package org.oneedtech.inspect.vc.verification;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.apicatalog.jsonld.JsonLdError;
+import com.apicatalog.jsonld.document.JsonDocument;
+import com.apicatalog.jsonld.http.media.MediaType;
+import com.apicatalog.jsonld.loader.DocumentLoader;
+import com.google.common.io.Resources;
+
+import foundation.identity.jsonld.ConfigurableDocumentLoader;
+
+public class LDSecurityContexts {
+ public static final URI JSONLD_CONTEXT_W3ID_SUITES_ED25519_2022_V1 = URI.create("https://w3id.org/security/data-integrity/v1");
+
+ public static final Map CONTEXTS;
+ public static final DocumentLoader DOCUMENT_LOADER;
+
+ static {
+
+ try {
+
+ CONTEXTS = new HashMap<>();
+
+ CONTEXTS.putAll(info.weboftrust.ldsignatures.jsonld.LDSecurityContexts.CONTEXTS);
+
+ CONTEXTS.put(JSONLD_CONTEXT_W3ID_SUITES_ED25519_2022_V1,
+ JsonDocument.of(MediaType.JSON_LD, Resources.getResource("contexts/data-integrity-v1.jsonld").openStream()));
+
+ for (Map.Entry context : CONTEXTS.entrySet()) {
+ context.getValue().setDocumentUrl(context.getKey());
+ }
+ } catch (JsonLdError | IOException ex) {
+
+ throw new ExceptionInInitializerError(ex);
+ }
+
+ DOCUMENT_LOADER = new ConfigurableDocumentLoader(CONTEXTS);
+ }
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/SignatureSuites.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/SignatureSuites.java
new file mode 100644
index 0000000..4301065
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/SignatureSuites.java
@@ -0,0 +1,5 @@
+package org.oneedtech.inspect.vc.verification;
+
+public class SignatureSuites {
+ public static final Ed25519Signature2022SignatureSuite SIGNATURE_SUITE_ED25519SIGNATURE2022 = new Ed25519Signature2022SignatureSuite();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/URDNA2015Canonicalizer.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/URDNA2015Canonicalizer.java
new file mode 100644
index 0000000..0ed3a9f
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/verification/URDNA2015Canonicalizer.java
@@ -0,0 +1,52 @@
+package org.oneedtech.inspect.vc.verification;
+
+import foundation.identity.jsonld.JsonLDException;
+import foundation.identity.jsonld.JsonLDObject;
+import info.weboftrust.ldsignatures.LdProof;
+import info.weboftrust.ldsignatures.canonicalizer.Canonicalizer;
+import info.weboftrust.ldsignatures.util.SHAUtil;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.List;
+
+public class URDNA2015Canonicalizer extends Canonicalizer {
+
+ public URDNA2015Canonicalizer() {
+
+ super(List.of("urdna2015"));
+ }
+
+ @Override
+ public byte[] canonicalize(LdProof ldProof, JsonLDObject jsonLdObject) throws IOException, GeneralSecurityException, JsonLDException {
+
+ // construct the LD proof without proof values
+
+ LdProof ldProofWithoutProofValues = Eddsa2022LdProof.builder()
+ .base(ldProof)
+ .defaultContexts(true)
+ .build();
+ LdProof.removeLdProofValues(ldProofWithoutProofValues);
+
+ // construct the LD object without proof
+
+ JsonLDObject jsonLdObjectWithoutProof = JsonLDObject.builder()
+ .base(jsonLdObject)
+ .build();
+ jsonLdObjectWithoutProof.setDocumentLoader(jsonLdObject.getDocumentLoader());
+ LdProof.removeFromJsonLdObject(jsonLdObjectWithoutProof);
+
+ // canonicalize the LD proof and LD object
+
+ String canonicalizedLdProofWithoutProofValues = ldProofWithoutProofValues.normalize("urdna2015");
+ String canonicalizedJsonLdObjectWithoutProof = jsonLdObjectWithoutProof.normalize("urdna2015");
+
+ // construct the canonicalization result
+
+ byte[] canonicalizationResult = new byte[64];
+ System.arraycopy(SHAUtil.sha256(canonicalizedLdProofWithoutProofValues), 0, canonicalizationResult, 0, 32);
+ System.arraycopy(SHAUtil.sha256(canonicalizedJsonLdObjectWithoutProof), 0, canonicalizationResult, 32, 32);
+
+ return canonicalizationResult;
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/data-integrity-v1.jsonld b/inspector-vc/src/main/resources/contexts/data-integrity-v1.jsonld
new file mode 100644
index 0000000..a44c053
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/data-integrity-v1.jsonld
@@ -0,0 +1,74 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "@protected": true,
+ "proof": {
+ "@id": "https://w3id.org/security#proof",
+ "@type": "@id",
+ "@container": "@graph"
+ },
+ "DataIntegrityProof": {
+ "@id": "https://w3id.org/security#DataIntegrityProof",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "cryptosuite": "https://w3id.org/security#cryptosuite",
+ "proofValue": {
+ "@id": "https://w3id.org/security#proofValue",
+ "@type": "https://w3id.org/security#multibase"
+ },
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ }
+ }
+ }
\ No newline at end of file
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java
index 4e316b6..277788f 100644
--- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java
@@ -379,4 +379,14 @@ public class OB30Tests {
assertHasProbeID(report, RevocationListProbe.ID, true);
});
}
+
+ @Test
+ void testEddsa2022Valid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.SIMPLE_EDDSA_20222_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+
+ }
}
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java
index ab68d9a..8a96657 100644
--- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java
@@ -11,6 +11,7 @@ public class Samples {
public static final class JSON {
public final static Sample COMPLETE_JSON = new Sample("ob30/complete.json", false);
public final static Sample SIMPLE_JSON = new Sample("ob30/simple.json", true);
+ public final static Sample SIMPLE_EDDSA_20222_JSON = new Sample("ob30/simple-eddsa-2022.json", true);
public final static Sample SIMPLE_DID_KEY_METHOD_JSON = new Sample("ob30/simple-did-key-method.json", true);
public final static Sample SIMPLE_DID_WEB_METHOD_JSON = new Sample("ob30/simple-did-web-method.json", true);
public final static Sample SIMPLE_MULTIPLE_PROOF_JSON = new Sample("ob30/simple-multiple-proofs.json", true);
diff --git a/inspector-vc/src/test/resources/ob30/simple-eddsa-2022.json b/inspector-vc/src/test/resources/ob30/simple-eddsa-2022.json
new file mode 100644
index 0000000..bf699a5
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-eddsa-2022.json
@@ -0,0 +1,31 @@
+{
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.1.json",
+ "https://w3id.org/security/data-integrity/v1"
+ ],
+ "type": ["VerifiableCredential", "AchievementCredential"],
+ "id": "http://www.1edtech.org/ob/1",
+ "issuanceDate": "2022-11-27T00:00:00Z",
+ "credentialSubject": {
+ "type": "AchievementSubject",
+ "id": "did:1edtech:1",
+ "achievement": {
+ "id": "did:1edtech:achievement:1",
+ "type": ["Achievement"],
+ "criteria": { "narrative": "some narrative" },
+ "description": "some description",
+ "name": "some name"
+ }
+ },
+ "name": "Some name",
+ "issuer": { "id": "https://example.com/issuers/876543", "type": ["Profile"] },
+ "proof": {
+ "type": "DataIntegrityProof",
+ "created": "2023-05-29T08:42:26Z",
+ "proofPurpose": "assertionMethod",
+ "verificationMethod": "https://example.com/issuers/876543#z6MkjZRZv3aez3r18pB1RBFJR1kwUVJ5jHt92JmQwXbd5hwi",
+ "cryptosuite": "eddsa-2022",
+ "proofValue": "z2H1miRAGKPRCPUSudNTPrsWyvpRZzgtgjPBmnMkbcwR8StwEv8dr3NigviBgvZwBu8yA7wAyF5qLjyo3zN1UqLR2"
+ }
+}
\ No newline at end of file