From 8f20aa0626ff93f0310a48eb326c7de03e7219d8 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Mon, 29 Jul 2024 15:20:19 -0400 Subject: [PATCH] feat: Use only confirmed keys (#257) * Added tests * Use only confirmed keys * Fixed encrypt and decrypt and unit tests * Fixed full workflow * Refactored fetchKeyPair * Prevent key rotation when versions unconfirmed * Fixed verifyUpdate --- src/gatekeeper-lib.js | 6 +- src/gatekeeper.test.js | 3 +- src/keymaster-app/src/keymaster-lib.js | 56 +++++++++++------- src/keymaster-lib.js | 61 +++++++++++++------- src/keymaster.test.js | 79 +++++++++++++++++++------- src/workflow.js | 29 +++++----- 6 files changed, 154 insertions(+), 80 deletions(-) diff --git a/src/gatekeeper-lib.js b/src/gatekeeper-lib.js index 80091257..fcc3c9ec 100644 --- a/src/gatekeeper-lib.js +++ b/src/gatekeeper-lib.js @@ -105,10 +105,10 @@ async function verifyCreateAsset(operation) { throw exceptions.INVALID_OPERATION; } - const doc = await resolveDID(operation.signature.signer, { atTime: operation.signature.signed }); + const doc = await resolveDID(operation.signature.signer, { confirm: true, atTime: operation.signature.signed }); if (doc.mdip.registry === 'local' && operation.mdip.registry !== 'local') { - throw exceptions.INVALID_OPERATION; + throw exceptions.INVALID_REGISTRY; } const operationCopy = JSON.parse(JSON.stringify(operation)); @@ -265,7 +265,7 @@ async function verifyUpdate(operation, doc) { } if (doc.didDocument.controller) { - const controllerDoc = await resolveDID(doc.didDocument.controller, { atTime: operation.signature.signed }); + const controllerDoc = await resolveDID(doc.didDocument.controller, { confirm: true, atTime: operation.signature.signed }); return verifyUpdate(operation, controllerDoc); } diff --git a/src/gatekeeper.test.js b/src/gatekeeper.test.js index 1ad4ff47..c3dddc47 100644 --- a/src/gatekeeper.test.js +++ b/src/gatekeeper.test.js @@ -281,7 +281,8 @@ describe('createDID', () => { await gatekeeper.createDID(assetOp); throw exceptions.EXPECTED_EXCEPTION; } catch (error) { - expect(error).toBe(exceptions.INVALID_OPERATION); + // Can't let local IDs create assets on other registries + expect(error).toBe(exceptions.INVALID_REGISTRY); } try { diff --git a/src/keymaster-app/src/keymaster-lib.js b/src/keymaster-app/src/keymaster-lib.js index cc312c91..42578722 100644 --- a/src/keymaster-app/src/keymaster-lib.js +++ b/src/keymaster-app/src/keymaster-lib.js @@ -394,23 +394,36 @@ function hdKeyPair() { return cipher.generateJwk(hdkey.privateKey); } -function fetchKeyPair(name = null) { +async function fetchKeyPair(name = null) { const wallet = loadWallet(); const id = fetchId(name); const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey); - const path = `m/44'/0'/${id.account}'/0/${id.index}`; - const didkey = hdkey.derive(path); + const doc = await resolveDID(id.did, { confirm: true }); + const confirmedPublicKeyJwk = doc.didDocument.verificationMethod[0].publicKeyJwk; + + for (let i = id.index; i >= 0; i--) { + const path = `m/44'/0'/${id.account}'/0/${i}`; + const didkey = hdkey.derive(path); + const keypair = cipher.generateJwk(didkey.privateKey); + + if (keypair.publicJwk.x === confirmedPublicKeyJwk.x && + keypair.publicJwk.y === confirmedPublicKeyJwk.y + ) + { + return keypair; + } + } - return cipher.generateJwk(didkey.privateKey); + return null; } export async function encrypt(msg, did, encryptForSender = true, registry = defaultRegistry) { const id = fetchId(); - const keypair = fetchKeyPair(); - const doc = await resolveDID(did); - const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk; - const cipher_sender = encryptForSender ? cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg) : null; - const cipher_receiver = cipher.encryptMessage(publicJwk, keypair.privateJwk, msg); + const senderKeypair = await fetchKeyPair(); + const doc = await resolveDID(did, { confirm: true }); + const receivePublicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk; + const cipher_sender = encryptForSender ? cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg) : null; + const cipher_receiver = cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg); const msgHash = cipher.hashMessage(msg); return await createAsset({ @@ -431,18 +444,19 @@ export async function decrypt(did) { throw exceptions.INVALID_PARAMETER; } - const doc = await resolveDID(crypt.sender, { atTime: crypt.created }); - const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk; + const doc = await resolveDID(crypt.sender, { confirm: true, atTime: crypt.created }); + const senderPublicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk; const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey); const ciphertext = (crypt.sender === id.did && crypt.cipher_sender) ? crypt.cipher_sender : crypt.cipher_receiver; + // Try all private keys for this ID, starting with the most recent and working backward let index = id.index; while (index >= 0) { const path = `m/44'/0'/${id.account}'/0/${index}`; const didkey = hdkey.derive(path); - const keypair = cipher.generateJwk(didkey.privateKey); + const receiverKeypair = cipher.generateJwk(didkey.privateKey); try { - return cipher.decryptMessage(publicJwk, keypair.privateJwk, ciphertext); + return cipher.decryptMessage(senderPublicJwk, receiverKeypair.privateJwk, ciphertext); } catch (error) { index -= 1; @@ -465,7 +479,7 @@ export async function decryptJSON(did) { export async function addSignature(obj, controller = null) { // Fetches current ID if name is missing const id = fetchId(controller); - const keypair = fetchKeyPair(controller); + const keypair = await fetchKeyPair(controller); try { const msgHash = cipher.hashJSON(obj); @@ -861,9 +875,9 @@ export async function testAgent(id) { return doc?.mdip?.type === 'agent'; } -export async function createCredential(schema) { +export async function createCredential(schema, registry) { // TBD validate schema - return createAsset(schema); + return createAsset(schema, registry); } export async function bindCredential(schemaId, subjectId, validUntil = null) { @@ -1010,7 +1024,7 @@ export async function unpublishCredential(did) { throw exceptions.INVALID_PARAMETER; } -export async function createChallenge(challenge) { +export async function createChallenge(challenge, registry = ephemeralRegistry) { if (!challenge) { challenge = { credentials: [] }; @@ -1030,7 +1044,7 @@ export async function createChallenge(challenge) { throw exceptions.INVALID_PARAMETER; } - return createAsset(challenge, ephemeralRegistry); + return createAsset(challenge, registry); } async function findMatchingCredential(credential) { @@ -1075,7 +1089,7 @@ async function findMatchingCredential(credential) { } } -export async function createResponse(did) { +export async function createResponse(did, registry = ephemeralRegistry) { const challenge = lookupDID(did); if (!challenge) { @@ -1108,7 +1122,7 @@ export async function createResponse(did) { for (let vcDid of matches) { const plaintext = await decrypt(vcDid); - const vpDid = await encrypt(plaintext, requestor); + const vpDid = await encrypt(plaintext, requestor, true, registry); pairs.push({ vc: vcDid, vp: vpDid }); } @@ -1127,7 +1141,7 @@ export async function createResponse(did) { ephemeral: { validUntil: expires.toISOString() } }; - return await encryptJSON(response, requestor, true, ephemeralRegistry); + return await encryptJSON(response, requestor, true, registry); } export async function verifyResponse(responseDID, challengeDID) { diff --git a/src/keymaster-lib.js b/src/keymaster-lib.js index cc312c91..1d3984df 100644 --- a/src/keymaster-lib.js +++ b/src/keymaster-lib.js @@ -394,23 +394,36 @@ function hdKeyPair() { return cipher.generateJwk(hdkey.privateKey); } -function fetchKeyPair(name = null) { +async function fetchKeyPair(name = null) { const wallet = loadWallet(); const id = fetchId(name); const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey); - const path = `m/44'/0'/${id.account}'/0/${id.index}`; - const didkey = hdkey.derive(path); + const doc = await resolveDID(id.did, { confirm: true }); + const confirmedPublicKeyJwk = doc.didDocument.verificationMethod[0].publicKeyJwk; - return cipher.generateJwk(didkey.privateKey); + for (let i = id.index; i >= 0; i--) { + const path = `m/44'/0'/${id.account}'/0/${i}`; + const didkey = hdkey.derive(path); + const keypair = cipher.generateJwk(didkey.privateKey); + + if (keypair.publicJwk.x === confirmedPublicKeyJwk.x && + keypair.publicJwk.y === confirmedPublicKeyJwk.y + ) + { + return keypair; + } + } + + return null; } export async function encrypt(msg, did, encryptForSender = true, registry = defaultRegistry) { const id = fetchId(); - const keypair = fetchKeyPair(); - const doc = await resolveDID(did); - const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk; - const cipher_sender = encryptForSender ? cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg) : null; - const cipher_receiver = cipher.encryptMessage(publicJwk, keypair.privateJwk, msg); + const senderKeypair = await fetchKeyPair(); + const doc = await resolveDID(did, { confirm: true }); + const receivePublicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk; + const cipher_sender = encryptForSender ? cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg) : null; + const cipher_receiver = cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg); const msgHash = cipher.hashMessage(msg); return await createAsset({ @@ -431,18 +444,19 @@ export async function decrypt(did) { throw exceptions.INVALID_PARAMETER; } - const doc = await resolveDID(crypt.sender, { atTime: crypt.created }); - const publicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk; + const doc = await resolveDID(crypt.sender, { confirm: true, atTime: crypt.created }); + const senderPublicJwk = doc.didDocument.verificationMethod[0].publicKeyJwk; const hdkey = cipher.generateHDKeyJSON(wallet.seed.hdkey); const ciphertext = (crypt.sender === id.did && crypt.cipher_sender) ? crypt.cipher_sender : crypt.cipher_receiver; + // Try all private keys for this ID, starting with the most recent and working backward let index = id.index; while (index >= 0) { const path = `m/44'/0'/${id.account}'/0/${index}`; const didkey = hdkey.derive(path); - const keypair = cipher.generateJwk(didkey.privateKey); + const receiverKeypair = cipher.generateJwk(didkey.privateKey); try { - return cipher.decryptMessage(publicJwk, keypair.privateJwk, ciphertext); + return cipher.decryptMessage(senderPublicJwk, receiverKeypair.privateJwk, ciphertext); } catch (error) { index -= 1; @@ -465,7 +479,7 @@ export async function decryptJSON(did) { export async function addSignature(obj, controller = null) { // Fetches current ID if name is missing const id = fetchId(controller); - const keypair = fetchKeyPair(controller); + const keypair = await fetchKeyPair(controller); try { const msgHash = cipher.hashJSON(obj); @@ -738,6 +752,11 @@ export async function rotateKeys() { const didkey = hdkey.derive(path); const keypair = cipher.generateJwk(didkey.privateKey); const doc = await resolveDID(id.did); + + if (!doc.didDocumentMetadata.confirmed) { + throw new Error('Cannot rotate keys'); + } + const vmethod = doc.didDocument.verificationMethod[0]; vmethod.id = `#key-${nextIndex + 1}`; @@ -861,9 +880,9 @@ export async function testAgent(id) { return doc?.mdip?.type === 'agent'; } -export async function createCredential(schema) { +export async function createCredential(schema, registry) { // TBD validate schema - return createAsset(schema); + return createAsset(schema, registry); } export async function bindCredential(schemaId, subjectId, validUntil = null) { @@ -1010,7 +1029,7 @@ export async function unpublishCredential(did) { throw exceptions.INVALID_PARAMETER; } -export async function createChallenge(challenge) { +export async function createChallenge(challenge, registry = ephemeralRegistry) { if (!challenge) { challenge = { credentials: [] }; @@ -1030,7 +1049,7 @@ export async function createChallenge(challenge) { throw exceptions.INVALID_PARAMETER; } - return createAsset(challenge, ephemeralRegistry); + return createAsset(challenge, registry); } async function findMatchingCredential(credential) { @@ -1075,7 +1094,7 @@ async function findMatchingCredential(credential) { } } -export async function createResponse(did) { +export async function createResponse(did, registry = ephemeralRegistry) { const challenge = lookupDID(did); if (!challenge) { @@ -1108,7 +1127,7 @@ export async function createResponse(did) { for (let vcDid of matches) { const plaintext = await decrypt(vcDid); - const vpDid = await encrypt(plaintext, requestor); + const vpDid = await encrypt(plaintext, requestor, true, registry); pairs.push({ vc: vcDid, vp: vpDid }); } @@ -1127,7 +1146,7 @@ export async function createResponse(did) { ephemeral: { validUntil: expires.toISOString() } }; - return await encryptJSON(response, requestor, true, ephemeralRegistry); + return await encryptJSON(response, requestor, true, registry); } export async function verifyResponse(responseDID, challengeDID) { diff --git a/src/keymaster.test.js b/src/keymaster.test.js index 14019039..4dd279c7 100644 --- a/src/keymaster.test.js +++ b/src/keymaster.test.js @@ -492,7 +492,7 @@ describe('rotateKeys', () => { it('should update DID doc with new keys', async () => { mockFs({}); - const alice = await keymaster.createId('Alice'); + const alice = await keymaster.createId('Alice', 'local'); let doc = await keymaster.resolveDID(alice); let vm = doc.didDocument.verificationMethod[0]; let pubkey = vm.publicKeyJwk; @@ -513,15 +513,15 @@ describe('rotateKeys', () => { it('should decrypt messages encrypted with rotating keys', async () => { mockFs({}); - await keymaster.createId('Alice'); - const bob = await keymaster.createId('Bob'); + await keymaster.createId('Alice', 'local'); + const bob = await keymaster.createId('Bob', 'local'); const secrets = []; const msg = "Hi Bob!"; for (let i = 0; i < 3; i++) { keymaster.setCurrentId('Alice'); - const did = await keymaster.encrypt(msg, bob); + const did = await keymaster.encrypt(msg, bob, true, 'local'); secrets.push(did); await keymaster.rotateKeys(); @@ -546,7 +546,7 @@ describe('rotateKeys', () => { it('should import DID with multiple key rotations', async () => { mockFs({}); - const alice = await keymaster.createId('Alice'); + const alice = await keymaster.createId('Alice', 'local'); const rotations = 10; for (let i = 0; i < rotations; i++) { @@ -561,6 +561,22 @@ describe('rotateKeys', () => { expect(updated).toBe(rotations + 1); }); + + + it('should raise an exception if latest version is not confirmed', async () => { + mockFs({}); + + await keymaster.createId('Alice', 'TESS'); + await keymaster.rotateKeys(); + + try { + await keymaster.rotateKeys(); + throw exceptions.EXPECTED_EXCEPTION;; + } + catch (error) { + expect(error.message).toBe('Cannot rotate keys'); + } + }); }); describe('addName', () => { @@ -1002,6 +1018,31 @@ describe('decrypt', () => { expect(decipher).toBe(msg); }); + it('should decrypt a short message after rotating keys (confirmed)', async () => { + mockFs({}); + + const did = await keymaster.createId('Bob', 'local'); + const msg = 'Hi Bob!'; + await keymaster.rotateKeys(); + const encryptDid = await keymaster.encrypt(msg, did, true, 'local'); + await keymaster.rotateKeys(); + const decipher = await keymaster.decrypt(encryptDid); + + expect(decipher).toBe(msg); + }); + + it('should decrypt a short message after rotating keys (unconfirmed)', async () => { + mockFs({}); + + const did = await keymaster.createId('Bob', 'hyperswarm'); + const msg = 'Hi Bob!'; + await keymaster.rotateKeys(); + const encryptDid = await keymaster.encrypt(msg, did, true, 'hyperswarm'); + const decipher = await keymaster.decrypt(encryptDid); + + expect(decipher).toBe(msg); + }); + it('should decrypt a short message encrypted by another ID', async () => { mockFs({}); @@ -1792,32 +1833,32 @@ describe('verifyResponse', () => { it('should demonstrate full workflow with credential revocations', async () => { mockFs({}); - const alice = await keymaster.createId('Alice'); - const bob = await keymaster.createId('Bob'); - const carol = await keymaster.createId('Carol'); - await keymaster.createId('Victor'); + const alice = await keymaster.createId('Alice', 'local'); + const bob = await keymaster.createId('Bob', 'local'); + const carol = await keymaster.createId('Carol', 'local'); + await keymaster.createId('Victor', 'local'); keymaster.setCurrentId('Alice'); - const credential1 = await keymaster.createCredential(mockSchema); - const credential2 = await keymaster.createCredential(mockSchema); + const credential1 = await keymaster.createCredential(mockSchema, 'local'); + const credential2 = await keymaster.createCredential(mockSchema, 'local'); const bc1 = await keymaster.bindCredential(credential1, carol); const bc2 = await keymaster.bindCredential(credential2, carol); - const vc1 = await keymaster.issueCredential(bc1); - const vc2 = await keymaster.issueCredential(bc2); + const vc1 = await keymaster.issueCredential(bc1, 'local'); + const vc2 = await keymaster.issueCredential(bc2, 'local'); keymaster.setCurrentId('Bob'); - const credential3 = await keymaster.createCredential(mockSchema); - const credential4 = await keymaster.createCredential(mockSchema); + const credential3 = await keymaster.createCredential(mockSchema, 'local'); + const credential4 = await keymaster.createCredential(mockSchema, 'local'); const bc3 = await keymaster.bindCredential(credential3, carol); const bc4 = await keymaster.bindCredential(credential4, carol); - const vc3 = await keymaster.issueCredential(bc3); - const vc4 = await keymaster.issueCredential(bc4); + const vc3 = await keymaster.issueCredential(bc3, 'local'); + const vc4 = await keymaster.issueCredential(bc4, 'local'); keymaster.setCurrentId('Carol'); @@ -1848,10 +1889,10 @@ describe('verifyResponse', () => { }, ] }; - const challengeDid = await keymaster.createChallenge(challenge); + const challengeDid = await keymaster.createChallenge(challenge, 'local'); keymaster.setCurrentId('Carol'); - const vpDid = await keymaster.createResponse(challengeDid); + const vpDid = await keymaster.createResponse(challengeDid, 'local'); const data = await keymaster.decryptJSON(vpDid); expect(data.challenge).toBe(challengeDid); diff --git a/src/workflow.js b/src/workflow.js index d6af63e8..3a799db9 100644 --- a/src/workflow.js +++ b/src/workflow.js @@ -24,10 +24,10 @@ async function runWorkflow() { await gatekeeper.start(db_json); await keymaster.start(gatekeeper, db_wallet); - const alice = await keymaster.createId('Alice'); - const bob = await keymaster.createId('Bob'); - const carol = await keymaster.createId('Carol'); - const victor = await keymaster.createId('Victor'); + const alice = await keymaster.createId('Alice', 'local'); + const bob = await keymaster.createId('Bob', 'local'); + const carol = await keymaster.createId('Carol', 'local'); + const victor = await keymaster.createId('Victor', 'local'); console.log(`Created Alice ${alice}`); console.log(`Created Bob ${bob}`); @@ -36,8 +36,8 @@ async function runWorkflow() { keymaster.setCurrentId('Alice'); - const credential1 = await keymaster.createCredential(mockSchema); - const credential2 = await keymaster.createCredential(mockSchema); + const credential1 = await keymaster.createCredential(mockSchema, 'local'); + const credential2 = await keymaster.createCredential(mockSchema, 'local'); console.log(`Alice created credential1 ${credential1}`); console.log(`Alice created credential2 ${credential2}`); @@ -45,16 +45,16 @@ async function runWorkflow() { const bc1 = await keymaster.bindCredential(credential1, carol); const bc2 = await keymaster.bindCredential(credential2, carol); - const vc1 = await keymaster.issueCredential(bc1); - const vc2 = await keymaster.issueCredential(bc2); + const vc1 = await keymaster.issueCredential(bc1, 'local'); + const vc2 = await keymaster.issueCredential(bc2, 'local'); console.log(`Alice issued vc1 for Carol ${vc1}`); console.log(`Alice issued vc2 for Carol ${vc2}`); keymaster.setCurrentId('Bob'); - const credential3 = await keymaster.createCredential(mockSchema); - const credential4 = await keymaster.createCredential(mockSchema); + const credential3 = await keymaster.createCredential(mockSchema, 'local'); + const credential4 = await keymaster.createCredential(mockSchema, 'local'); console.log(`Bob created credential3 ${credential3}`); console.log(`Bob created credential4 ${credential4}`); @@ -62,8 +62,8 @@ async function runWorkflow() { const bc3 = await keymaster.bindCredential(credential3, carol); const bc4 = await keymaster.bindCredential(credential4, carol); - const vc3 = await keymaster.issueCredential(bc3); - const vc4 = await keymaster.issueCredential(bc4); + const vc3 = await keymaster.issueCredential(bc3, 'local'); + const vc4 = await keymaster.issueCredential(bc4, 'local'); console.log(`Bob issued vc3 for Carol ${vc3}`); console.log(`Bob issued vc4 for Carol ${vc4}`); @@ -99,11 +99,11 @@ async function runWorkflow() { }, ] }; - const challengeDid = await keymaster.createChallenge(mockChallenge); + const challengeDid = await keymaster.createChallenge(mockChallenge, 'local'); console.log(`Victor created challenge ${challengeDid}`); keymaster.setCurrentId('Carol'); - const vpDid = await keymaster.createResponse(challengeDid); + const vpDid = await keymaster.createResponse(challengeDid, 'local'); console.log(`Carol created response for Victor ${vpDid}`); keymaster.setCurrentId('Victor'); @@ -145,7 +145,6 @@ async function runWorkflow() { console.log(`Victor verified response ${verify4.vps.length} valid credentials`); keymaster.stop(); - } async function main() {