UNPKG

@kya-os/mcp-i

Version:

The TypeScript MCP framework with identity features built-in

440 lines (439 loc) 18.2 kB
"use strict"; /** * Delegation Credential Verifier * * Progressive enhancement verification for W3C Delegation Credentials. * Follows the Edge-Delegation-Verification.md pattern: * * Stage 1: Fast basic checks (no network, early rejection) * Stage 2: Parallel advanced checks (signature, status) * Stage 3: Combined results * * Related Spec: MCP-I §4.3, W3C VC Data Model 1.1 * Python Reference: Edge-Delegation-Verification.md */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DelegationCredentialVerifier = void 0; exports.createDelegationVerifier = createDelegationVerifier; const crypto_1 = require("crypto"); const json_canonicalize_1 = require("json-canonicalize"); const delegation_1 = require("@kya-os/contracts/delegation"); /** * Delegation Credential Verifier * * Implements progressive enhancement pattern from Edge-Delegation-Verification.md: * 1. Fast basic checks (no network) - early rejection * 2. Parallel advanced checks (signature + status) * 3. Combined results */ class DelegationCredentialVerifier { didResolver; statusListResolver; cache = new Map(); cacheTtl; constructor(options) { this.didResolver = options?.didResolver; this.statusListResolver = options?.statusListResolver; this.cacheTtl = options?.cacheTtl || 60_000; // 1 minute default } /** * Verify a delegation credential with progressive enhancement * * Per Edge-Delegation-Verification.md:41-102 * * @param vc - The delegation credential to verify * @param options - Verification options * @returns Verification result */ async verifyDelegationCredential(vc, options = {}) { const startTime = Date.now(); // Check cache first if (!options.skipCache) { const cached = this.getFromCache(vc.id || ''); if (cached) { return { ...cached, cached: true }; } } // =================================================================== // STAGE 1: Fast Basic Checks (no network calls) // Per Edge-Delegation-Verification.md:152-186 // =================================================================== const basicCheckStart = Date.now(); const basicValidation = this.validateBasicProperties(vc); const basicCheckMs = Date.now() - basicCheckStart; if (!basicValidation.valid) { const result = { valid: false, reason: basicValidation.reason, stage: 'basic', metrics: { basicCheckMs, totalMs: Date.now() - startTime, }, checks: { basicValid: false, }, }; return result; } // =================================================================== // STAGE 2: Parallel Advanced Checks // Per Edge-Delegation-Verification.md:281-301 // =================================================================== // Start signature verification (if not skipped) const signaturePromise = !options.skipSignature ? this.verifySignature(vc, options.didResolver || this.didResolver) : Promise.resolve({ valid: true, durationMs: 0, }); // Start status checking (if not skipped) const statusPromise = !options.skipStatus && vc.credentialStatus ? this.checkCredentialStatus(vc.credentialStatus, options.statusListResolver || this.statusListResolver) : Promise.resolve({ valid: true, durationMs: 0, }); // Wait for both checks in parallel const [signatureResult, statusResult] = await Promise.all([ signaturePromise, statusPromise, ]); const signatureCheckMs = signatureResult.durationMs || 0; const statusCheckMs = statusResult.durationMs || 0; // =================================================================== // STAGE 3: Combined Results // Per Edge-Delegation-Verification.md:82-94 // =================================================================== const allValid = basicValidation.valid && signatureResult.valid && statusResult.valid; const result = { valid: allValid, reason: !allValid ? signatureResult.reason || statusResult.reason || 'Unknown failure' : undefined, stage: 'complete', metrics: { basicCheckMs, signatureCheckMs, statusCheckMs, totalMs: Date.now() - startTime, }, checks: { basicValid: basicValidation.valid, signatureValid: signatureResult.valid, statusValid: statusResult.valid, }, }; // Cache successful verifications if (result.valid && vc.id) { this.setInCache(vc.id, result); } return result; } /** * Stage 1: Validate basic properties (no network calls) * * Fast path for early rejection of invalid delegations. * Per Edge-Delegation-Verification.md:155-186 * * @param vc - The delegation credential * @returns Validation result */ validateBasicProperties(vc) { // 1. Validate schema const schemaValidation = (0, delegation_1.validateDelegationCredential)(vc); if (!schemaValidation.success) { return { valid: false, reason: `Schema validation failed: ${schemaValidation.error.message}`, }; } // 2. Check expiration if ((0, delegation_1.isDelegationCredentialExpired)(vc)) { return { valid: false, reason: 'Delegation credential expired' }; } // 3. Check not yet valid if ((0, delegation_1.isDelegationCredentialNotYetValid)(vc)) { return { valid: false, reason: 'Delegation credential not yet valid' }; } // 4. Check delegation status const delegation = vc.credentialSubject.delegation; if (delegation.status === 'revoked') { return { valid: false, reason: 'Delegation status is revoked' }; } if (delegation.status === 'expired') { return { valid: false, reason: 'Delegation status is expired' }; } // 5. Check required fields if (!delegation.issuerDid || !delegation.subjectDid) { return { valid: false, reason: 'Missing issuer or subject DID' }; } // 6. Check proof exists (we'll verify it later) if (!vc.proof) { return { valid: false, reason: 'Missing proof' }; } return { valid: true }; } /** * Stage 2a: Verify signature * * Per Edge-Delegation-Verification.md:191-234 * * @param vc - The delegation credential * @param didResolver - Optional DID resolver * @returns Verification result */ async verifySignature(vc, didResolver) { const startTime = Date.now(); try { // Get issuer DID const issuerDid = typeof vc.issuer === 'string' ? vc.issuer : vc.issuer.id; // If no DID resolver, we can't verify the signature if (!didResolver) { return { valid: true, // Trust but don't verify (no resolver available) reason: 'No DID resolver available, skipping signature verification', durationMs: Date.now() - startTime, }; } // Resolve issuer DID to get public key const didDoc = await didResolver.resolve(issuerDid); if (!didDoc) { return { valid: false, reason: `Could not resolve issuer DID: ${issuerDid}`, durationMs: Date.now() - startTime, }; } // Find verification method from proof if (!vc.proof) { return { valid: false, reason: 'Proof is missing', durationMs: Date.now() - startTime, }; } const verificationMethodId = vc.proof.verificationMethod; if (!verificationMethodId) { return { valid: false, reason: 'Proof missing verificationMethod', durationMs: Date.now() - startTime, }; } const verificationMethod = this.findVerificationMethod(didDoc, verificationMethodId); if (!verificationMethod) { return { valid: false, reason: `Verification method ${verificationMethodId} not found`, durationMs: Date.now() - startTime, }; } // Extract public key const publicKeyJwk = verificationMethod.publicKeyJwk; if (!publicKeyJwk) { return { valid: false, reason: 'Verification method missing publicKeyJwk', durationMs: Date.now() - startTime, }; } // Verify Ed25519 signature // For Ed25519Signature2020: // 1. The signing input is the canonicalized VC (without proofValue) // 2. The proofValue is the base64url-encoded Ed25519 signature const vcWithoutProof = { ...vc }; delete vcWithoutProof.proof; // The proof.proofValue is base64url-encoded Ed25519 signature // The proof.jws is an alternative JWS-based signature format (header.payload.signature) const rawProofValue = vc.proof?.proofValue || vc.proof?.jws; if (!rawProofValue) { return { valid: false, reason: 'Proof missing proofValue or jws', durationMs: Date.now() - startTime, }; } // Extract signature bytes and construct signing input // Handle both JWS format (header.payload.signature) and proofValue format let signatureBase64url; let signingInput; if (rawProofValue.includes('.')) { // JWS format with b64:false (standard for Ed25519Signature2020) // The signing input is: base64url(header) + "." + payload_bytes const jwsParts = rawProofValue.split('.'); if (jwsParts.length !== 3) { return { valid: false, reason: `Invalid JWS format: expected 3 parts, got ${jwsParts.length}`, durationMs: Date.now() - startTime, }; } const [headerB64, , signatureB64] = jwsParts; signatureBase64url = signatureB64; // For b64:false JWS, the signing input is the detached payload format: // ASCII(BASE64URL(UTF8(JWS Protected Header))) || '.' || JWS Payload // The payload is the hash of the document (for Ed25519Signature2020) // Per RFC 7797 (JWS Unencoded Payload Option) // Construct proof options for hashing (everything in proof except signature fields) const proofOptions = { ...vc.proof }; delete proofOptions.proofValue; delete proofOptions.jws; delete proofOptions.signatureValue; const canonicalDoc = (0, json_canonicalize_1.canonicalize)(vcWithoutProof); const canonicalProof = (0, json_canonicalize_1.canonicalize)(proofOptions); // Hash both and concatenate (payload for JWS) const docHash = (0, crypto_1.createHash)('sha256').update(canonicalDoc, 'utf8').digest(); const proofHash = (0, crypto_1.createHash)('sha256').update(canonicalProof, 'utf8').digest(); const payloadBytes = Buffer.concat([proofHash, docHash]); // JWS signing input: header + "." + payload_bytes const headerBytes = Buffer.from(headerB64, 'utf8'); const separator = Buffer.from('.', 'utf8'); signingInput = Buffer.concat([headerBytes, separator, payloadBytes]); } else { // proofValue is raw base64url signature (Ed25519Signature2020 native format) signatureBase64url = rawProofValue; // Construct proof options (everything in proof except proofValue) const proofOptions = { ...vc.proof }; delete proofOptions.proofValue; delete proofOptions.jws; delete proofOptions.signatureValue; // Create signing input: hash(canonicalize(proofOptions)) || hash(canonicalize(document)) const canonicalDoc = (0, json_canonicalize_1.canonicalize)(vcWithoutProof); const canonicalProof = (0, json_canonicalize_1.canonicalize)(proofOptions); const docHash = (0, crypto_1.createHash)('sha256').update(canonicalDoc, 'utf8').digest(); const proofHash = (0, crypto_1.createHash)('sha256').update(canonicalProof, 'utf8').digest(); signingInput = Buffer.concat([proofHash, docHash]); } try { // Decode the base64url signature const signatureBytes = Buffer.from(signatureBase64url, 'base64url'); // Use subtle crypto for Ed25519 verification const cryptoKey = await globalThis.crypto.subtle.importKey('jwk', publicKeyJwk, { name: 'Ed25519' }, false, ['verify']); const isValid = await globalThis.crypto.subtle.verify({ name: 'Ed25519' }, cryptoKey, new Uint8Array(signatureBytes), new Uint8Array(signingInput)); if (!isValid) { return { valid: false, reason: 'Ed25519 signature verification failed', durationMs: Date.now() - startTime, }; } return { valid: true, durationMs: Date.now() - startTime, }; } catch (error) { return { valid: false, reason: `Signature verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`, durationMs: Date.now() - startTime, }; } } catch (error) { return { valid: false, reason: `Signature verification error: ${error instanceof Error ? error.message : 'Unknown error'}`, durationMs: Date.now() - startTime, }; } } /** * Stage 2b: Check credential status via StatusList2021 * * @param status - The credential status entry * @param statusListResolver - Optional status list resolver * @returns Status check result */ async checkCredentialStatus(status, statusListResolver) { const startTime = Date.now(); try { // If no status list resolver, we can't check the status if (!statusListResolver) { return { valid: true, // Trust but don't verify (no resolver available) reason: 'No status list resolver available, skipping status check', durationMs: Date.now() - startTime, }; } // Check if credential is revoked/suspended const isRevoked = await statusListResolver.checkStatus(status); if (isRevoked) { return { valid: false, reason: `Credential revoked via StatusList2021 (${status.statusPurpose})`, durationMs: Date.now() - startTime, }; } return { valid: true, durationMs: Date.now() - startTime, }; } catch (error) { return { valid: false, reason: `Status check error: ${error instanceof Error ? error.message : 'Unknown error'}`, durationMs: Date.now() - startTime, }; } } /** * Find verification method in DID document * * @param didDoc - The DID document * @param verificationMethodId - The verification method ID * @returns Verification method or undefined */ findVerificationMethod(didDoc, verificationMethodId) { return didDoc.verificationMethod?.find((vm) => vm.id === verificationMethodId); } /** * Get from cache */ getFromCache(id) { const entry = this.cache.get(id); if (!entry) return null; if (Date.now() > entry.expiresAt) { this.cache.delete(id); return null; } return entry.result; } /** * Set in cache */ setInCache(id, result) { this.cache.set(id, { result, expiresAt: Date.now() + this.cacheTtl, }); } /** * Clear cache */ clearCache() { this.cache.clear(); } /** * Clear cache entry for specific VC */ clearCacheEntry(id) { this.cache.delete(id); } } exports.DelegationCredentialVerifier = DelegationCredentialVerifier; /** * Create a delegation credential verifier * * Convenience factory function. * * @param options - Verifier options * @returns DelegationCredentialVerifier instance */ function createDelegationVerifier(options) { return new DelegationCredentialVerifier(options); }