UNPKG

@zipwire/proofpack

Version:

Core ProofPack verifiable data exchange format with JWS, Merkle trees, selective disclosure, and blockchain attestation support

208 lines (180 loc) 9.08 kB
import { JwsReader } from './JwsReader.js'; import { MerkleTree } from './MerkleTree.js'; /** * The requirement for the presence of a signature in the JWS envelope. */ export const JwsSignatureRequirement = { /** * The reader will throw an exception if no signature is present. */ AtLeastOne: 'AtLeastOne', /** * The reader will throw an exception if no signature is present. */ All: 'All', /** * The reader will skip the signature verification. * This is useful when the signature is not required, but the reader should still verify the envelope, * or when the JWS is not signed. */ Skip: 'Skip' }; /** * Creates a result of reading an attested Merkle exchange. * @param {Object} document - The attested Merkle exchange document * @param {string} message - The result message * @param {boolean} isValid - Whether the result is valid * @returns {Object} The read result */ export const createAttestedMerkleExchangeReadResult = (document, message, isValid) => ({ document, message, isValid }); /** * Creates a verification context for verifying an attested Merkle proof. * @param {number} maxAge - Maximum age in milliseconds * @param {Function} resolveJwsVerifier - Function that takes (algorithm, signerAddresses) and returns a verifier * @param {string} signatureRequirement - Signature requirement from JwsSignatureRequirement * @param {Function} hasValidNonce - Function to check if a nonce is valid * @param {Function} verifyAttestation - Function to verify attestation and return AttestationResult * @returns {Object} The verification context */ export const createAttestedMerkleExchangeVerificationContext = (maxAge, resolveJwsVerifier, signatureRequirement, hasValidNonce, verifyAttestation) => ({ maxAge, resolveJwsVerifier, signatureRequirement, hasValidNonce, verifyAttestation }); /** * Creates a verification context using an attestation verifier factory. * @param {number} maxAge - Maximum age in milliseconds * @param {Function} resolveJwsVerifier - Function that takes (algorithm, signerAddresses) and returns a verifier * @param {string} signatureRequirement - Signature requirement from JwsSignatureRequirement * @param {Function} hasValidNonce - Function to check if a nonce is valid * @param {Object} attestationVerifierFactory - Factory for creating attestation verifiers * @returns {Object} The verification context */ export const createVerificationContextWithAttestationVerifierFactory = (maxAge, resolveJwsVerifier, signatureRequirement, hasValidNonce, attestationVerifierFactory) => { const verifyAttestation = async (attestedDocument) => { if (!attestedDocument?.attestation?.eas || !attestedDocument.merkleTree) { return { isValid: false, message: 'Attestation or Merkle tree is null', attester: null }; } try { const serviceId = getServiceIdFromAttestation(attestedDocument.attestation); if (!attestationVerifierFactory.hasVerifier(serviceId)) { return { isValid: false, message: `No verifier available for service '${serviceId}'`, attester: null }; } const verifier = attestationVerifierFactory.getVerifier(serviceId); const merkleRoot = attestedDocument.merkleTree.root; return await verifier.verifyAsync(attestedDocument.attestation, merkleRoot); } catch (error) { return { isValid: false, message: `Attestation verification failed: ${error.message}`, attester: null }; } }; return createAttestedMerkleExchangeVerificationContext( maxAge, resolveJwsVerifier, signatureRequirement, hasValidNonce, verifyAttestation ); }; /** * Gets the service ID from an attestation. * @param {Object} attestation - The attestation object * @returns {string} The service ID * @private */ const getServiceIdFromAttestation = (attestation) => { // For now, we only support EAS attestations // In the future, this could be extended to support other attestation services return attestation.eas != null ? 'eas' : 'unknown'; }; /** * The reader for attested Merkle proofs. */ export class AttestedMerkleExchangeReader { /** * Creates a new instance of the AttestedMerkleExchangeReader class. */ constructor() { } /** * Reads an attested Merkle proof from a JWS envelope. * @param {string} jwsEnvelopeJson - The JWS envelope as a JSON string * @param {Object} verificationContext - The context for verifying the attested Merkle proof * @returns {Promise<Object>} The read result */ async readAsync(jwsEnvelopeJson, verificationContext) { const jwsReader = new JwsReader(); try { const jwsEnvelope = await jwsReader.read(jwsEnvelopeJson); const attestedMerkleExchangeDoc = jwsEnvelope.payload; if (!attestedMerkleExchangeDoc) { return createAttestedMerkleExchangeReadResult(null, 'Attested Merkle exchange has no payload', false); } // Validate nonce if present if (attestedMerkleExchangeDoc.nonce) { const hasValidNonce = await verificationContext.hasValidNonce(attestedMerkleExchangeDoc.nonce); if (!hasValidNonce) { return createAttestedMerkleExchangeReadResult(null, 'Attested Merkle exchange has an invalid nonce', false); } } // Validate timestamp const timestamp = new Date(attestedMerkleExchangeDoc.timestamp); const maxAge = verificationContext.maxAge; const now = new Date(); if (timestamp.getTime() + maxAge < now.getTime()) { return createAttestedMerkleExchangeReadResult(null, 'Attested Merkle exchange is too old', false); } // Validate Merkle tree if (!attestedMerkleExchangeDoc.merkleTree) { return createAttestedMerkleExchangeReadResult(null, 'Attested Merkle exchange has no Merkle tree', false); } // Verify Merkle tree root const merkleTree = MerkleTree.parse(JSON.stringify(attestedMerkleExchangeDoc.merkleTree)); if (!merkleTree.verifyRoot()) { return createAttestedMerkleExchangeReadResult(null, 'Attested Merkle exchange has an invalid root hash', false); } // Verify attestation FIRST to get the attester address const attestationValidation = await verificationContext.verifyAttestation(attestedMerkleExchangeDoc); if (!attestationValidation.isValid) { return createAttestedMerkleExchangeReadResult(null, `Attested Merkle exchange has an invalid attestation: ${attestationValidation.message}`, false); } // Now verify JWS signatures using the attester address from attestation if (verificationContext.signatureRequirement !== JwsSignatureRequirement.Skip) { // Create resolver that uses the attester address from attestation const resolveVerifier = (algorithm) => { // Pass the attester address as potential signer address const signerAddresses = attestationValidation.attester ? [attestationValidation.attester] : []; return verificationContext.resolveJwsVerifier(algorithm, signerAddresses); }; // Use the envelope from read() to avoid re-parsing const verificationResult = await jwsReader.verify(jwsEnvelope, resolveVerifier); // Check signature requirements switch (verificationContext.signatureRequirement) { case JwsSignatureRequirement.AtLeastOne: if (verificationResult.verifiedSignatureCount === 0) { return createAttestedMerkleExchangeReadResult(null, 'Attested Merkle exchange has no verified signatures', false); } break; case JwsSignatureRequirement.All: if (verificationResult.verifiedSignatureCount !== verificationResult.signatureCount) { return createAttestedMerkleExchangeReadResult(null, 'Attested Merkle exchange has unverified signatures', false); } break; default: return createAttestedMerkleExchangeReadResult(null, `Unknown signature requirement: ${verificationContext.signatureRequirement}`, false); } } return createAttestedMerkleExchangeReadResult( attestedMerkleExchangeDoc, 'OK', true ); } catch (error) { return createAttestedMerkleExchangeReadResult(null, `Failed to read attested Merkle exchange: ${error.message}`, false); } } }