260 lines
9.7 KiB
JavaScript
260 lines
9.7 KiB
JavaScript
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,
|
|
}
|