UNPKG

@zipwire/proofpack-ethereum

Version:

Ethereum integration for ProofPack with ES256K signatures, EAS attestations, and multi-network blockchain verification

242 lines (207 loc) 8.32 kB
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js'; import { sha256 } from 'ethereum-cryptography/sha256.js'; import { keccak256 } from 'ethereum-cryptography/keccak.js'; import { Base64Url } from '../../base/src/Base64Url.js'; /** * ES256K (Ethereum secp256k1) JWS verifier * Verifies JWS signatures using Ethereum's secp256k1 curve * Future-ready for attestation and timestamp validation */ class ES256KVerifier { /** * Create an ES256K verifier * @param {string} expectedSignerAddress - Expected Ethereum address of the signer */ constructor(expectedSignerAddress) { if (!expectedSignerAddress || typeof expectedSignerAddress !== 'string') { throw new Error('Invalid Ethereum address: must be a non-empty string'); } // Validate Ethereum address format if (!this._isValidEthereumAddress(expectedSignerAddress)) { throw new Error('Invalid Ethereum address format'); } this.algorithm = 'ES256K'; this.expectedSignerAddress = expectedSignerAddress.toLowerCase(); } /** * Verify a JWS token signature * @param {object} jwsToken - JWS token with header, payload, signature * @returns {Promise<object>} Verification result with structured flags */ async verify(jwsToken) { try { // Validate input structure if (!jwsToken || typeof jwsToken !== 'object') { return this._createFailureResult(['Invalid JWS token structure']); } const { header, payload: payloadBase64, signature } = jwsToken; if (!header || !payloadBase64 || !signature) { return this._createFailureResult(['Missing required JWS token fields']); } // Parse and validate header const headerResult = this._validateHeader(header); if (!headerResult.valid) { return this._createFailureResult(headerResult.errors); } // Verify signature const signatureResult = await this._verifySignature(header, payloadBase64, signature); return { isValid: signatureResult.valid, errors: signatureResult.errors }; } catch (error) { return this._createFailureResult(['Verification error: ' + error.message]); } } /** * Validate JWS header * @param {string} headerBase64 - Base64URL encoded header * @returns {object} Validation result * @private */ _validateHeader(headerBase64) { try { const headerJson = Base64Url.decode(headerBase64); const header = JSON.parse(headerJson); if (header.alg !== this.algorithm) { return { valid: false, errors: [`Unsupported algorithm: ${header.alg}, expected: ${this.algorithm}`] }; } return { valid: true, errors: [] }; } catch (error) { return { valid: false, errors: ['Invalid JWS header: ' + error.message] }; } } /** * Verify ES256K signature * @param {string} headerBase64 - Base64URL encoded header * @param {string} payloadBase64 - Base64URL encoded payload * @param {string} signatureBase64 - Base64URL encoded signature * @returns {Promise<object>} Verification result * @private */ async _verifySignature(headerBase64, payloadBase64, signatureBase64) { try { // Create signing input (header.payload) const signingInput = `${headerBase64}.${payloadBase64}`; const signingInputBytes = new TextEncoder().encode(signingInput); const messageHash = sha256(signingInputBytes); // Decode signature const signatureBytes = Base64Url.decodeToBytes(signatureBase64); if (signatureBytes.length !== 64) { return { valid: false, errors: [`Invalid signature length: ${signatureBytes.length}, expected: 64 bytes`] }; } // Extract r and s from compact signature const r = signatureBytes.subarray(0, 32); const s = signatureBytes.subarray(32, 64); // Recover signer address from signature const recoveredAddress = await this._recoverSignerAddress(messageHash, r, s); if (!recoveredAddress) { return { valid: false, errors: ['Failed to recover signer address from signature'] }; } // Compare with expected address const addressMatch = recoveredAddress.toLowerCase() === this.expectedSignerAddress.toLowerCase(); if (!addressMatch) { return { valid: false, errors: [ `Signer address mismatch. Expected: ${this.expectedSignerAddress}, ` + `Recovered: ${recoveredAddress}` ] }; } return { valid: true, errors: [] }; } catch (error) { return { valid: false, errors: ['Signature verification failed: ' + error.message] }; } } /** * Recover Ethereum address from secp256k1 signature * @param {Uint8Array} messageHash - 32-byte message hash * @param {Uint8Array} r - 32-byte signature r value * @param {Uint8Array} s - 32-byte signature s value * @returns {Promise<string|null>} Recovered Ethereum address or null * @private */ async _recoverSignerAddress(messageHash, r, s) { try { // Try recovery IDs 0 and 1 (2 and 3 are invalid for secp256k1) for (let recoveryId = 0; recoveryId < 2; recoveryId++) { try { // Create signature object with recovery ID const signature = new secp256k1.Signature( this._bytesToBigInt(r), this._bytesToBigInt(s), recoveryId ); // Recover public key const recoveredPublicKey = signature.recoverPublicKey(messageHash); const publicKeyBytes = recoveredPublicKey.toRawBytes(false); // uncompressed // Derive Ethereum address from public key const publicKeyHash = keccak256(publicKeyBytes.slice(1)); // Remove 0x04 prefix const addressBytes = publicKeyHash.slice(-20); const address = '0x' + Array.from(addressBytes, b => b.toString(16).padStart(2, '0')).join(''); // Check if this recovered address matches the expected address if (address.toLowerCase() === this.expectedSignerAddress.toLowerCase()) { return address; } } catch (recoveryError) { // Try next recovery ID continue; } } return null; } catch (error) { return null; } } /** * Convert byte array to BigInt * @param {Uint8Array} bytes - Byte array * @returns {bigint} BigInt representation * @private */ _bytesToBigInt(bytes) { let result = 0n; for (let i = 0; i < bytes.length; i++) { result = (result << 8n) + BigInt(bytes[i]); } return result; } /** * Validate Ethereum address format * @param {string} address - Address to validate * @returns {boolean} True if valid format * @private */ _isValidEthereumAddress(address) { return /^0x[a-fA-F0-9]{40}$/.test(address); } /** * Create a failure result object * @param {string[]} errors - Array of error messages * @returns {object} Failure result * @private */ _createFailureResult(errors) { return { isValid: false, errors: errors || [] }; } } export { ES256KVerifier };