From 0505a844f95ac62ff58243fc5935d4016c349e97 Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Thu, 25 Apr 2024 18:31:04 +0100 Subject: [PATCH 1/3] Support Ed255192020 keys, open badges example --- example/open-badges.js | 73 +++++++++++++++++++ package.json | 1 + src/utils/vc/credentials.js | 2 + src/utils/vc/crypto/Ed25519Signature2020.js | 41 +++++++++++ .../vc/crypto/Ed25519VerificationKey2020.js | 29 ++++++++ src/utils/vc/crypto/constants.js | 2 + src/utils/vc/custom_crypto.js | 6 ++ src/utils/vc/helpers.js | 6 +- src/utils/vc/presentations.js | 2 + tests/integration/issuing.test.js | 28 ++++++- 10 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 example/open-badges.js create mode 100644 src/utils/vc/crypto/Ed25519Signature2020.js create mode 100644 src/utils/vc/crypto/Ed25519VerificationKey2020.js diff --git a/example/open-badges.js b/example/open-badges.js new file mode 100644 index 000000000..7840b6241 --- /dev/null +++ b/example/open-badges.js @@ -0,0 +1,73 @@ +import VerifiableCredential from '../src/verifiable-credential'; +import { DIDKeyResolver } from '../src/resolver'; + +// Sample credential data from https://gist.githubusercontent.com/ottonomy/6f72f5055220cfa8c6926e1a753f1870/raw/e7882e4a6eebb503359cce4bdc8978331d47544c/asu-tln-unconference-example-credential.json +const credentialJSON = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "type": "Profile", + "id": "did:key:z6MkoowWdLogChc6mRp18YcKBd2yYTNnQLeHdiT73wjL1h6z", + "name": "Trusted Learner Network (TLN) Unconference Issuer", + "url": "https://tln.asu.edu/", + "image": { + "id": "https://plugfest3-assets-20230928.s3.amazonaws.com/TLN+Gold+Circle.png", + "type": "Image" + } + }, + "issuanceDate": "2024-04-04T16:43:31.485Z", + "name": "2024 TLN Unconference Change Agent", + "credentialSubject": { + "type": "AchievementSubject", + "id": "did:key:7af28a8b2b9684073a0884aacd8c31eb5908baf4a1ba7e2ca60582bf585c68ad", + "achievement": { + "id": "https://tln.asu.edu/achievement/369435906932948", + "type": "Achievement", + "name": "2024 TLN Unconference Change Agent", + "description": "This credential certifies attendance, participation, and knowledge-sharing at the 2024 Trusted Learner Network (TLN) Unconference.", + "criteria": { + "type": "Criteria", + "narrative": "* Demonstrates initiative and passion for digital credentialing\n* Shares knowledge, skills and experience to broaden and deepen the community's collective understanding and competency\n* Engages in complex problems by collaborating with others\n* Creates connections and builds coalition to advance the ecosystem" + } + } + }, + "id": "https://tln.asu.edu/achievement/369435906932948", + "proof": { + "type": "Ed25519Signature2020", + "created": "2024-04-04T16:43:31Z", + "verificationMethod": "did:key:z6MkoowWdLogChc6mRp18YcKBd2yYTNnQLeHdiT73wjL1h6z#z6MkoowWdLogChc6mRp18YcKBd2yYTNnQLeHdiT73wjL1h6z", + "proofPurpose": "assertionMethod", + "proofValue": "z23JQwSmJKnWXw1HWDMBv1yoZDVyfUsRWihQFsrSLpb8cENqbuqpdnaSY72VmCkY3WQ4GovpNRZPNLRaatXeDJE8G" + } +}; + +const resolver = new DIDKeyResolver(); + +async function main() { + // Incrementally build a verifiable credential + const credential = VerifiableCredential.fromJSON(credentialJSON); + + // Verify the credential + const verifyResult = await credential.verify({ + resolver, + compactProof: true, + }); + if (verifyResult.verified) { + console.log('Credential has been verified! Result:', verifyResult); + } else { + console.error('Credential could not be verified!. Got error', verifyResult.error); + process.exit(1); + } + + // Exit + process.exit(0); +} + +main(); diff --git a/package.json b/package.json index 25e3d3ccb..ce4ed6657 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "did-resolver-example": "npx babel-node example/resolver.js", "revocation-example": "npx babel-node example/revocation.js", "vcdm-example": "npx babel-node example/vcdm.js", + "open-badges-example": "npx babel-node example/open-badges.js", "standard-schemas-example": "npx babel-node example/standard_schemas.js", "schema-example": "npx babel-node example/schema.js", "schema-validation-example": "npx babel-node example/schema-validation.js", diff --git a/src/utils/vc/credentials.js b/src/utils/vc/credentials.js index ad76c99ed..3f8b96bf7 100644 --- a/src/utils/vc/credentials.js +++ b/src/utils/vc/credentials.js @@ -32,6 +32,7 @@ import { ensureValidDatetime } from '../type-helpers'; import { EcdsaSecp256k1Signature2019, Ed25519Signature2018, + Ed25519Signature2020, Sr25519Signature2020, Bls12381PSSignatureDock2023, Bls12381PSSignatureProofDock2023, @@ -342,6 +343,7 @@ export async function verifyCredential( }; const fullSuite = [ new Ed25519Signature2018(), + new Ed25519Signature2020(), new EcdsaSecp256k1Signature2019(), new Sr25519Signature2020(), new JsonWebSignature2020(), diff --git a/src/utils/vc/crypto/Ed25519Signature2020.js b/src/utils/vc/crypto/Ed25519Signature2020.js new file mode 100644 index 000000000..946dede67 --- /dev/null +++ b/src/utils/vc/crypto/Ed25519Signature2020.js @@ -0,0 +1,41 @@ +import { Ed255192020SigName, Ed255192020VerKeyName } from './constants'; +import Ed25519VerificationKey2020 from './Ed25519VerificationKey2020'; +import CustomLinkedDataSignature from './common/CustomLinkedDataSignature'; + +const SUITE_CONTEXT_URL = 'https://w3id.org/security/suites/ed25519-2020/v1'; + +export default class Ed25519Signature2020 extends CustomLinkedDataSignature { + /** + * Creates a new Ed25519Signature2020 instance + * @constructor + * @param {object} config - Configuration options + */ + constructor({ + keypair, verificationMethod, verifier, signer, + } = {}) { + super({ + type: Ed255192020SigName, + LDKeyClass: Ed25519VerificationKey2020, + contextUrl: SUITE_CONTEXT_URL, + alg: 'EdDSA', + signer: signer || Ed25519Signature2020.signerFactory(keypair, verificationMethod), + verifier, + }); + this.requiredKeyType = Ed255192020VerKeyName; + } + + /** + * Generate object with `sign` method + * @param keypair + * @param verificationMethod + * @returns {object} + */ + static signerFactory(keypair, verificationMethod) { + return { + id: verificationMethod, + async sign({ data }) { + return keypair.sign(data); + }, + }; + } +} diff --git a/src/utils/vc/crypto/Ed25519VerificationKey2020.js b/src/utils/vc/crypto/Ed25519VerificationKey2020.js new file mode 100644 index 000000000..0bf54a4ae --- /dev/null +++ b/src/utils/vc/crypto/Ed25519VerificationKey2020.js @@ -0,0 +1,29 @@ +import b58 from 'bs58'; +import * as base64 from '@juanelas/base64'; +import { Ed25519VerKeyName, Ed255192020VerKeyName } from './constants'; +import Ed25519VerificationKey2018 from './Ed25519VerificationKey2018'; + +export default class Ed25519VerificationKey2020 extends Ed25519VerificationKey2018 { + /** + * Construct the public key object from the verification method + * @param verificationMethod + * @returns {Ed25519VerificationKey2020} + */ + static from(verificationMethod) { + const isEd25519Type = verificationMethod.type.indexOf(Ed255192020VerKeyName) !== -1 + || verificationMethod.type.indexOf(Ed25519VerKeyName) !== -1; + if (!verificationMethod.type || !isEd25519Type) { + throw new Error(`verification method should have type ${Ed255192020VerKeyName} - got: ${verificationMethod.type}`); + } + + if (verificationMethod.publicKeyBase58) { + return new this(b58.decode(verificationMethod.publicKeyBase58)); + } + + if (verificationMethod.publicKeyBase64) { + return new this(base64.decode(verificationMethod.publicKeyBase64)); + } + + throw new Error(`Unsupported signature encoding for ${Ed255192020VerKeyName}`); + } +} diff --git a/src/utils/vc/crypto/constants.js b/src/utils/vc/crypto/constants.js index 0a0d0e1fc..221da06e8 100644 --- a/src/utils/vc/crypto/constants.js +++ b/src/utils/vc/crypto/constants.js @@ -1,7 +1,9 @@ export const EcdsaSecp256k1VerKeyName = 'EcdsaSecp256k1VerificationKey2019'; export const EcdsaSecp256k1SigName = 'EcdsaSecp256k1Signature2019'; export const Ed25519VerKeyName = 'Ed25519VerificationKey2018'; +export const Ed255192020VerKeyName = 'Ed25519VerificationKey2020'; export const Ed25519SigName = 'Ed25519Signature2018'; +export const Ed255192020SigName = 'Ed25519Signature2020'; export const Sr25519VerKeyName = 'Sr25519VerificationKey2020'; export const Sr25519SigName = 'Sr25519Signature2020'; export const Bls12381BBSSigDockSigName = 'Bls12381BBS+SignatureDock2022'; diff --git a/src/utils/vc/custom_crypto.js b/src/utils/vc/custom_crypto.js index a7feb8f73..45793def6 100644 --- a/src/utils/vc/custom_crypto.js +++ b/src/utils/vc/custom_crypto.js @@ -3,6 +3,8 @@ import { EcdsaSecp256k1SigName, Ed25519VerKeyName, Ed25519SigName, + Ed255192020VerKeyName, + Ed255192020SigName, Sr25519VerKeyName, Sr25519SigName, Bls12381BBSDockVerKeyName, @@ -20,6 +22,7 @@ import EcdsaSecp256k1VerificationKey2019 from './crypto/EcdsaSecp256k1Verificati import EcdsaSecp256k1Signature2019 from './crypto/EcdsaSecp256k1Signature2019'; import Ed25519VerificationKey2018 from './crypto/Ed25519VerificationKey2018'; import Ed25519Signature2018 from './crypto/Ed25519Signature2018'; +import Ed25519Signature2020 from './crypto/Ed25519Signature2020'; import Sr25519VerificationKey2020 from './crypto/Sr25519VerificationKey2020'; import Sr25519Signature2020 from './crypto/Sr25519Signature2020'; import Bls12381BBSSignatureDock2022 from './crypto/Bls12381BBSSignatureDock2022'; @@ -35,12 +38,15 @@ export { EcdsaSecp256k1SigName, Ed25519VerKeyName, Ed25519SigName, + Ed255192020VerKeyName, + Ed255192020SigName, Sr25519VerKeyName, Sr25519SigName, EcdsaSecp256k1VerificationKey2019, EcdsaSecp256k1Signature2019, Ed25519VerificationKey2018, Ed25519Signature2018, + Ed25519Signature2020, Sr25519VerificationKey2020, Sr25519Signature2020, Bls12381BBSSignatureDock2022, diff --git a/src/utils/vc/helpers.js b/src/utils/vc/helpers.js index 550d20f85..319765340 100644 --- a/src/utils/vc/helpers.js +++ b/src/utils/vc/helpers.js @@ -16,8 +16,9 @@ import { Bls12381PSSignatureDock2023, Bls12381PSDockVerKeyName, JsonWebSignature2020, + Ed25519Signature2020, } from './custom_crypto'; -import { Bls12381BDDT16DockVerKeyName, Bls12381BDDT16MacDockName } from './crypto/constants'; +import { Bls12381BDDT16DockVerKeyName, Bls12381BDDT16MacDockName, Ed255192020VerKeyName } from './crypto/constants'; import Bls12381BDDT16MACDock2024 from './crypto/Bls12381BDDT16MACDock2024'; /** @@ -67,6 +68,9 @@ export async function getSuiteFromKeyDoc(keyDoc, useProofValue, options) { case Ed25519VerKeyName: Cls = Ed25519Signature2018; break; + case Ed255192020VerKeyName: + Cls = Ed25519Signature2020; + break; case Sr25519VerKeyName: Cls = Sr25519Signature2020; break; diff --git a/src/utils/vc/presentations.js b/src/utils/vc/presentations.js index 9e954c0bc..2de6eb672 100644 --- a/src/utils/vc/presentations.js +++ b/src/utils/vc/presentations.js @@ -27,6 +27,7 @@ import { DEFAULT_CONTEXT_V1_URL } from './constants'; import { EcdsaSecp256k1Signature2019, Ed25519Signature2018, + Ed25519Signature2020, Sr25519Signature2020, JsonWebSignature2020, Bls12381BBSSignatureDock2022, @@ -156,6 +157,7 @@ export async function verifyPresentation(presentation, options = {}) { resolver: null, suite: [ new Ed25519Signature2018(), + new Ed25519Signature2020(), new EcdsaSecp256k1Signature2019(), new Sr25519Signature2020(), new JsonWebSignature2020(), diff --git a/tests/integration/issuing.test.js b/tests/integration/issuing.test.js index 0de5e7378..10e34c6dc 100644 --- a/tests/integration/issuing.test.js +++ b/tests/integration/issuing.test.js @@ -70,7 +70,7 @@ describe('Verifiable Credential issuance where issuer has a Dock DID', () => { await dock.disconnect(); }, 10000); - test('Issue a verifiable credential with ed25519 key and verify it', async () => { + test('Issue a verifiable credential with ed25519 key and verify it (2018)', async () => { const issuerKey = getKeyDoc(issuer1DID, dock.keyring.addFromUri(issuer1KeySeed, null, 'ed25519'), 'Ed25519VerificationKey2018'); const credential = await issueCredential(issuerKey, unsignedCred); expect(credential).toMatchObject( @@ -87,6 +87,23 @@ describe('Verifiable Credential issuance where issuer has a Dock DID', () => { ); }, 40000); + test('Issue a verifiable credential with ed25519 key and verify it (2020)', async () => { + const issuerKey = getKeyDoc(issuer1DID, dock.keyring.addFromUri(issuer1KeySeed, null, 'ed25519'), 'Ed25519VerificationKey2020'); + const credential = await issueCredential(issuerKey, unsignedCred); + expect(credential).toMatchObject( + expect.objectContaining( + getCredMatcherDoc(unsignedCred, issuer1DID, issuerKey.id, 'Ed25519Signature2020'), + ), + ); + + const result = await verifyCredential(credential, { resolver }); + expect(result).toMatchObject( + expect.objectContaining( + getProofMatcherDoc(), + ), + ); + }, 40000); + test('Issue a verifiable credential with secp256k1 key and verify it', async () => { const issuerKey = getKeyDoc(issuer2DID, generateEcdsaSecp256k1Keypair(issuer2KeyEntropy), 'EcdsaSecp256k1VerificationKey2019'); const credential = await issueCredential(issuerKey, unsignedCred); @@ -122,13 +139,20 @@ describe('Verifiable Credential issuance where issuer has a Dock DID', () => { ); }, 40000); - test('(JWT) Issue a verifiable credential with ed25519 key and verify it', async () => { + test('(JWT) Issue a verifiable credential with ed25519 key and verify it (2018)', async () => { const issuerKey = getKeyDoc(issuer1DID, dock.keyring.addFromUri(issuer1KeySeed, null, 'ed25519'), 'Ed25519VerificationKey2018'); const credential = await issueCredential(issuerKey, unsignedCred, true, null, null, null, null, false, 'jwt'); const result = await verifyCredential(credential, { resolver }); expect(result.verified).toBeTruthy(); }, 40000); + test('(JWT) Issue a verifiable credential with ed25519 key and verify it (2020)', async () => { + const issuerKey = getKeyDoc(issuer1DID, dock.keyring.addFromUri(issuer1KeySeed, null, 'ed25519'), 'Ed25519VerificationKey2020'); + const credential = await issueCredential(issuerKey, unsignedCred, true, null, null, null, null, false, 'jwt'); + const result = await verifyCredential(credential, { resolver }); + expect(result.verified).toBeTruthy(); + }, 40000); + test('(JWT) Issue a verifiable credential with secp256k1 key and verify it', async () => { const issuerKey = getKeyDoc(issuer2DID, generateEcdsaSecp256k1Keypair(issuer2KeyEntropy), 'EcdsaSecp256k1VerificationKey2019'); const credential = await issueCredential(issuerKey, unsignedCred, true, null, null, null, null, false, 'jwt'); From d97bf1a7b5b6c5e430e950dd8b99928f1f9a77af Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Thu, 25 Apr 2024 19:36:55 +0100 Subject: [PATCH 2/3] Ed25519Signature2020 presenting test --- tests/integration/presenting.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/presenting.test.js b/tests/integration/presenting.test.js index bda206a9f..84fec5293 100644 --- a/tests/integration/presenting.test.js +++ b/tests/integration/presenting.test.js @@ -101,11 +101,13 @@ describe('Verifiable Presentation where both issuer and holder have a Dock DID', const holder1Key = getKeyDoc(holder1DID, dock.keyring.addFromUri(holder1KeySeed, null, 'ed25519'), 'Ed25519VerificationKey2018'); const holder2Key = getKeyDoc(holder2DID, generateEcdsaSecp256k1Keypair(holder2KeyEntropy), 'EcdsaSecp256k1VerificationKey2019'); const holder3Key = getKeyDoc(holder3DID, dock.keyring.addFromUri(holder3KeySeed, null, 'sr25519'), 'Sr25519VerificationKey2020'); + const holder4Key = getKeyDoc(holder1DID, dock.keyring.addFromUri(holder1KeySeed, null, 'ed25519'), 'Ed25519VerificationKey2020'); for (const elem of [ [cred1, 'Ed25519Signature2018', holder1Key], [cred2, 'EcdsaSecp256k1Signature2019', holder2Key], [cred3, 'Sr25519Signature2020', holder3Key], + [cred4, 'Ed25519Signature2020', holder4Key], ]) { const cred = elem[0]; const sigType = elem[1]; From fb526d792482ae1d38662e61a32ab44a3afc9341 Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Thu, 25 Apr 2024 21:05:40 +0100 Subject: [PATCH 3/3] comment --- src/utils/vc/crypto/Ed25519VerificationKey2020.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/vc/crypto/Ed25519VerificationKey2020.js b/src/utils/vc/crypto/Ed25519VerificationKey2020.js index 0bf54a4ae..68d2cb35e 100644 --- a/src/utils/vc/crypto/Ed25519VerificationKey2020.js +++ b/src/utils/vc/crypto/Ed25519VerificationKey2020.js @@ -26,4 +26,6 @@ export default class Ed25519VerificationKey2020 extends Ed25519VerificationKey20 throw new Error(`Unsupported signature encoding for ${Ed255192020VerKeyName}`); } + + // NOTE: Ed255192020 has the same cryptography as Ed255192018, so we inherit the verifier methods }