addition of new dashboard
This commit is contained in:
259
backend/modules/core/data/audit.js
Normal file
259
backend/modules/core/data/audit.js
Normal file
@@ -0,0 +1,259 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user