UNPKG

@synet/credential

Version:

VC Credentials - Simple, Robust, Unit-based Verifiable Credentials service

360 lines (355 loc) • 15.3 kB
"use strict"; /** * @synet/credential - Unit-based W3C Verifiable Credential operations * * This unit provides W3C-compatible verifiable credential operations * using the learning pattern to acquire crypto capabilities from * other units (@synet/signer, @synet/keys, etc.) * * Key features: * - Learning-based architecture (no tight coupling) * - Progressive capability acquisition * - W3C-compatible credential formats using existing types * - Composable with crypto units * - Uses existing issueVC/verifyVC functions internally * * Usage pattern: * ```typescript * const signer = new Signer(); * const credential = new Credential(); * * // Learn crypto capabilities * credential.learn([signer.teach()]); * * // Issue credential * const vc = await credential.execute('issue', subject, type, issuer); * ``` * * @author Synet Team */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Credential = void 0; const unit_1 = require("@synet/unit"); const utils_1 = require("./utils"); const result_1 = require("./result"); const DEFAULT_CONTEXT = ["https://www.w3.org/2018/credentials/v1"]; class Credential extends unit_1.Unit { constructor(props) { super(props); } static create(config) { const props = { dna: (0, unit_1.createUnitSchema)({ id: "credential", version: "1.0.0" }), created: new Date(), metadata: config?.metadata || {} }; return new Credential(props); } whoami() { return `${this.props.dna.id}@${this.props.dna.version} - W3C Verifiable Credential operations`; } capabilities() { return this._getAllCapabilities(); } help() { console.log(` šŸŽ“ Credential - W3C Verifiable Credential Operations Native Methods: - issueCredential(subject, type, issuerDid, options?): Create W3C Verifiable Credentials - verifyCredential(credential, options?): Verify credential signatures and structure - validateStructure(credential): Validate credential structure without crypto Required Learning: - Learn from @synet/keys Key unit to get: getPublicKey, sign, verify Usage Pattern: 1. Create unit: const credential = new Credential() 2. Learn from key: credential.learn([key.teach()]) 3. Issue VC: await credential.issueCredential(subject, type, issuer) Example: const signer = Signer.generate('ed25519'); const key = signer.createKey(); const credential = new Credential(); credential.learn([key.teach()]); const result = await credential.issueCredential( { id: 'did:example:123', name: 'Alice' }, 'UniversityDegree', 'did:example:university' ); `); } teach() { return { unitId: this.props.dna.id, capabilities: { issueCredential: (...args) => this.issueCredential(args[0], args[1], args[2], args[3]), verifyCredential: (...args) => this.verifyCredential(args[0], args[1]), validateStructure: (...args) => this.validateStructure(args[0]), // Deprecated methods for backwards compatibility }, }; } // ========================================== // CORE CREDENTIAL OPERATIONS // ========================================== /** * Issue a verifiable credential using Result pattern * Requires learned getPublicKey and sign capabilities from Key */ async issueCredential(subject, type, issuerDid, options) { try { // Check capabilities first if (!this.can("getPublicKey") || !this.can("sign")) { return result_1.Result.fail("Cannot issue credential: missing getPublicKey or sign capability. Learn from a crypto unit."); } // Create credential payload const payload = this.createCredentialPayload(subject, type, issuerDid, options); // Create proof based on format const proofFormat = options?.proofFormat || "jwt"; let proof; if (proofFormat === "jwt") { const proofResult = await this.createJWTProof(payload, issuerDid); if (!proofResult.isSuccess) { return result_1.Result.fail(`Failed to create JWT proof: ${proofResult.errorMessage}`); } proof = proofResult.value; } else { // Unsupported proof format return result_1.Result.fail(`Unsupported proof format: ${proofFormat}`); } const credential = { ...payload, proof: proof, }; return result_1.Result.success(credential); } catch (error) { return result_1.Result.fail(`Failed to issue credential: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined); } } /** * Verify a verifiable credential using Result pattern * * Verifies a W3C verifiable credential using the learned capabilities. */ async verifyCredential(credential, options) { try { // Check capabilities first if (!this.can("getPublicKey") || !this.can("verify")) { return result_1.Result.fail("Missing getPublicKey or verify capability"); } // Validate credential structure if (!credential || !credential.proof) { return result_1.Result.fail("Invalid credential: missing proof"); } const { proof } = credential; // Verify based on proof type if (proof.type === "JwtProof2020" && proof.jwt) { return await this.verifyJWTProof(credential, options); } return result_1.Result.fail(`Unsupported proof type: ${proof.type}`); } catch (error) { return result_1.Result.fail(`Failed to verify credential: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined); } } createCredentialPayload(subject, type, issuerDid, options) { const typeArray = Array.isArray(type) ? type : [type]; const appType = typeArray.find((t) => t !== "VerifiableCredential") || "Generic"; const context = options?.context ? [...DEFAULT_CONTEXT, ...options.context] : DEFAULT_CONTEXT; return { "@context": context, id: options?.vcId || this.generateCredentialId(appType), type: ["VerifiableCredential", ...typeArray], issuer: { id: issuerDid }, issuanceDate: options?.issuanceDate || new Date().toISOString(), expirationDate: options?.expirationDate, credentialSubject: subject, meta: options?.meta, }; } generateCredentialId(type) { const cuid = (0, utils_1.createId)(); return `urn:synet:${type}:${cuid}`; } /** * Create a JWT proof for a credential * Follows JWT standard (RFC 7515) - uses base64url encoding for signature */ async createJWTProof(payload, issuerDid) { try { const jwtHeader = { alg: "EdDSA", typ: "JWT", }; const jwtPayload = { vc: payload, jti: payload.id, nbf: Math.floor(new Date(payload.issuanceDate).getTime() / 1000), iss: issuerDid || payload.issuer.id, ...(payload.expirationDate && { exp: Math.floor(new Date(payload.expirationDate).getTime() / 1000), }), }; const headerB64 = (0, utils_1.base64urlEncode)(JSON.stringify(jwtHeader)); const payloadB64 = (0, utils_1.base64urlEncode)(JSON.stringify(jwtPayload)); const signingInput = `${headerB64}.${payloadB64}`; // Get signature from signer (returns base64) const base64Signature = (await this.execute("sign", signingInput)); if (!base64Signature) { return result_1.Result.fail("Failed to sign JWT proof"); } // The signature is already in base64url format from the Signer const jwt = `${signingInput}.${base64Signature}`; const proof = { type: "JwtProof2020", jwt, verificationMethod: issuerDid, }; return result_1.Result.success(proof); } catch (error) { return result_1.Result.fail(`Failed to create JWT proof: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined); } } /** * Verify a JWT proof using Result pattern * Converts base64url signature back to base64 for verification */ async verifyJWTProof(credential, options) { try { const { proof } = credential; if (!proof.jwt) { return result_1.Result.fail("No JWT found in proof"); } const jwtParts = proof.jwt.split("."); if (jwtParts.length !== 3) { return result_1.Result.fail("Invalid JWT format"); } const [headerB64, payloadB64, base64urlSignature] = jwtParts; const signingInput = `${headerB64}.${payloadB64}`; try { const jwtPayload = JSON.parse((0, utils_1.base64urlDecode)(payloadB64)); const issuerDid = jwtPayload.iss; if (!this.can("getPublicKey") || !this.can("verify")) { return result_1.Result.fail("Missing getPublicKey or verify capability"); } // The signature is already in base64url format - our verify function handles both formats // Verify signature const isValidSignature = await this.execute("verify", signingInput, base64urlSignature); if (!isValidSignature) { return result_1.Result.fail("Invalid signature"); } // Verify that the JWT payload matches the credential payload const expectedVc = jwtPayload.vc; if (!expectedVc) { return result_1.Result.fail("Missing vc claim in JWT payload"); } // Create a copy of the credential without the proof for comparison const { proof: _proof, ...credentialWithoutProof } = credential; // Veramo-style verification: JWT vc claim contains subset of credential fields // We verify that all fields in JWT vc exist and match in the credential for (const [key, value] of Object.entries(expectedVc)) { const credentialValue = credentialWithoutProof[key]; if (JSON.stringify(credentialValue) !== JSON.stringify(value)) { return result_1.Result.fail(`JWT payload field "${key}" does not match credential data`); } } // Additional verification: ensure required fields are present in credential if (!credentialWithoutProof.id || !credentialWithoutProof.issuer || !credentialWithoutProof.issuanceDate) { return result_1.Result.fail("Missing required credential fields"); } // Check expiration if requested if (options?.checkExpiration !== false && jwtPayload.exp) { const now = Math.floor(Date.now() / 1000); if (now > jwtPayload.exp) { return result_1.Result.fail("Credential has expired"); } } // Check issuer if requested if (options?.expectedIssuer && issuerDid !== options.expectedIssuer) { return result_1.Result.fail(`Unexpected issuer: ${issuerDid}`); } const verificationResult = { verified: true, issuer: issuerDid, subject: credential.credentialSubject.holder.id, issuanceDate: credential.issuanceDate, expirationDate: credential.expirationDate, }; return result_1.Result.success(verificationResult); } catch (parseError) { return result_1.Result.fail(`Failed to parse JWT payload: ${parseError instanceof Error ? parseError.message : String(parseError)}`, parseError instanceof Error ? parseError : undefined); } } catch (error) { return result_1.Result.fail(`Failed to verify JWT proof: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined); } } /** * Validate credential structure without cryptographic verification */ async validateStructure(credential) { // Check required fields if (!credential || typeof credential !== "object") { return { valid: false, reason: "Invalid credential structure" }; } if (!credential["@context"] || !Array.isArray(credential["@context"])) { return { valid: false, reason: "Missing or invalid @context" }; } if (!credential.type || !Array.isArray(credential.type) || !credential.type.includes("VerifiableCredential")) { return { valid: false, reason: "Missing or invalid type" }; } if (!credential.issuer) { return { valid: false, reason: "Missing issuer" }; } if (!credential.issuanceDate) { return { valid: false, reason: "Missing issuanceDate" }; } if (!credential.credentialSubject || !credential.credentialSubject.holder) { return { valid: false, reason: "Missing or invalid credentialSubject" }; } if (!credential.proof) { return { valid: false, reason: "Missing proof" }; } return { valid: true }; } /** * Check if this unit can perform a capability */ can(capability) { return this.capabilities().includes(capability); } /** * Learn capabilities from other units (especially @synet/keys Key) */ learn(contracts) { for (const contract of contracts) { for (const [cap, impl] of Object.entries(contract.capabilities)) { // Learn key capabilities that we need for credential operations if (cap === "getPublicKey" || cap === "sign" || cap === "verify") { this._addCapability(cap, impl); /* console.debug( `Credential learned: ${cap} from ${contract.unitId}`, ); */ } } } // Call parent learn for any other capabilities super.learn(contracts); } } exports.Credential = Credential; // ========================================== // EXPORTS // ========================================== exports.default = Credential;