import { getPool } from "../../../db/index.js"; import { agreement } from "./agreement.js"; import { event } from "./event.js"; import { stringify, configure } from "safe-stable-stringify"; import { createHash } from "crypto"; import { entity } from "./entity.js"; import sodium from "sodium-native"; function splitJws(jws) { const sections = jws.split('.'); if (sections.length !== 3) { throw ('Input must be a JWS.'); } const jwt = JSON.parse(Buffer.from(sections[0], 'base64url').toString()); if (jwt.alg !== 'EdDSA') { throw ('JWT does not indicate EdDSA'); } const payload = JSON.parse(Buffer.from(sections[1], 'base64url').toString()); const wasSigned = Buffer.from(sections[0] + '.' + sections[1]); const signature = Buffer.from(sections[2], 'base64url'); return { jwt, payload, wasSigned, signature, } } function verifyJws(input) { let ret = false; try { if (!input.jws) { throw ('No JWS provided.'); } if (!input.publicKey) { throw ('No publicKey provided.'); } const publicKey = Buffer.from(input.publicKey, 'base64url'); if (publicKey.length !== sodium.crypto_sign_PUBLICKEYBYTES) { throw ('publicKey length must be crypto_sign_PUBLICKEYBYTES (32).'); } const providedPublicKey = Buffer.from(input.jws.jwt.jwk.x, 'base64url'); if (publicKey.compare(providedPublicKey) !== 0) { ret = false; } else { ret = sodium.crypto_sign_verify_detached(input.jws.signature, input.jws.wasSigned, Buffer.from(input.publicKey, 'base64url')); } } catch (e) { console.error(e); } return ret; }; function validateSignatures(item, signatures, didDocs) { let res = false; try { for (const signature of signatures) { const didDoc = didDocs.find((didDoc) => didDoc.id === signature.id); if (!didDoc) throw ('DID Document not provided'); const vmCreatedDate = new Date(didDoc.verificationMethod[0].created); const signDate = new Date(signature.signedOn) const vmDeactivatedDate = didDoc.verificationMethod[0].deactivated ? new Date(didDoc.verificationMethod[0].deactivated) : null; const validDate = signDate >= vmCreatedDate && (!vmDeactivatedDate || vmDeactivatedDate >= signDate); if (!validDate) throw ('Signature is not valid due to key dates'); const split = splitJws(signature.jws); if (stringify(item) !== stringify(split.payload)) throw ('Payload does not match'); const validJws = verifyJws({ jws: split, publicKey: didDoc.verificationMethod[0].key, }); if (!validJws) throw ('Signature is not valid'); res = true; } } catch (e) { console.error(e); } return res; } function validateSignedBefore(item, signatures) { let res = false; try { let issue = false; for (const signature of signatures) { if (signature.signedOn >= item.created) { issue = true; break; } } res = !issue; } catch (e) { console.error(e); } return res; } function validateDidsMatch(auditSigs, targetSigs) { let match = true; for (const asig of auditSigs) { let found = false; for (const tsig of targetSigs) { if (tsig.id === asig.id) { found = true; break; } } if (!found) { match = false; } } return match; } function generateDigest(content, length) { if (typeof content === 'object') { content = stringify(content); } const hash = createHash('sha256') .update(content) .digest('hex') .slice(0, length); return hash; } async function verify(input, userId) { let response = { success: false, error: 'Unknown error', }; const client = await getPool(); try { const data = { valid: [], invalid: [], }; if (!input.didDocs) { input.didDocs = []; for (const shortName of input.shortNames || []) { input.didDocs.push((await entity.getEntity(client, userId, shortName)).didDoc) } } delete input.shortNames; const organizedById = []; for (const item of input.audits) { const found = organizedById.find((obi) => item.audit.agreementId && obi.agreementId === item.audit.agreementId || item.audit.eventId && obi.eventId === item.audit.eventId ); if (!found) { const newItem = item.audit.agreementId ? { agreementId: item.audit.agreementId, auditRecords: [item], } : { eventId: item.audit.eventId, auditRecords: [item], } organizedById.push(newItem) } else { found.auditRecords.push(item); } } for (const item of organizedById) { const existingItem = item.eventId ? await event.getEvent(client, userId, item.eventId, true) : await agreement.getAgreement(client, userId, item.agreementId) const existingSignatures = item.eventId ? await event.getSignatures(client, userId, item.eventId) : await agreement.getSignatures(client, userId, item.agreementId) // Does the agreement/event signature verify? let validSignature = false; if (validateSignatures(existingItem, existingSignatures, input.didDocs)) { validSignature = true; } for (const auditRecord of item.auditRecords) { const res = { audit: auditRecord, results: { validId: false, validSignature, validAuditHash: false, validAuditSignature: false, } } // Do the agreement/event IDs match? if ( (item.agreementId !== null && auditRecord.audit.agreementId === item.agreementId) || (item.eventId !== null && auditRecord.audit.eventId === item.eventId) ) res.results.validId = true; // Do DID IDs match between audit and target object? res.results.validMatchingDids = validateDidsMatch(auditRecord.signatures, existingSignatures); // Does the audit hash match? // The digest was created from whichever signatures this audit record has const signatures = []; for (const s of auditRecord.signatures) { const existingSignature = existingSignatures.find((es) => es.id === s.id); if (existingSignature) { signatures.push(existingSignature) } } const check = item.eventId ? { event: existingItem, signatures, } : { agreement: existingItem, signatures, } const digest = generateDigest(check); if (digest === auditRecord.audit.digest) res.results.validAuditHash = true; // Does the audit signature verify? if (validateSignatures(auditRecord.audit, auditRecord.signatures, input.didDocs)) { res.results.validAuditSignature = true; } const isValid = res.results.validId === true && res.results.validSignature === true && res.results.validAuditHash === true && res.results.validAuditSignature === true; if (isValid) { data.valid.push(res); } else { data.invalid.push(res); } // If an event, has the DID signed the agreement and is that signature valid? if (existingItem.eventId !== null && existingItem.agreementId !== '00000000-0000-0000-0000-000000000000') { const existingAgreement = await agreement.getAgreement(client, userId, existingItem.agreementId); const existingAgreementSignatures = await agreement.getSignatures(client, userId, existingItem.agreementId); res.results.validEventAgreement = validateDidsMatch(auditRecord.signatures, existingAgreementSignatures); res.results.validEventAgreementSignature = false; if (validateSignatures(existingAgreement, existingAgreementSignatures, input.didDocs) && validateSignedBefore(existingItem, existingAgreementSignatures)) { res.results.validEventAgreementSignature = true; } } else { res.results.validEventAgreement = true; res.results.validEventAgreementSignature = true; } } } response = { message: 'validation complete', data, }; } catch (e) { console.error(e) } finally { await client.release(); } return response; } export const audit = { verify, }