@kya-os/mcp-i
Version:
The TypeScript MCP framework with identity features built-in
244 lines (243 loc) • 9.04 kB
JavaScript
;
/**
* Proof Generation for XMCP-I Runtime
*
* Handles JCS canonicalization, SHA-256 digest generation, and Ed25519 JWS signing (compact format)
* according to requirements 5.1, 5.2, 5.3, 5.6.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProofGenerator = void 0;
exports.createProofResponse = createProofResponse;
exports.extractCanonicalData = extractCanonicalData;
const crypto_1 = require("crypto");
const jose_1 = require("jose");
const json_canonicalize_1 = require("json-canonicalize");
const mcp_i_core_1 = require("@kya-os/mcp-i-core");
const node_providers_1 = require("../providers/node-providers");
/**
* Proof generator class
*/
class ProofGenerator {
identity;
constructor(identity) {
this.identity = identity;
}
/**
* Generate proof for tool request/response
* Requirements: 5.1, 5.2, 5.3, 5.6
*/
async generateProof(request, response, session, options = {}) {
// Generate canonical hashes
const hashes = this.generateCanonicalHashes(request, response);
// Create proof metadata
const meta = {
did: this.identity.did,
kid: this.identity.kid,
ts: Math.floor(Date.now() / 1000),
nonce: session.nonce,
audience: session.audience,
sessionId: session.sessionId,
requestHash: hashes.requestHash,
responseHash: hashes.responseHash,
...options, // Include scopeId, delegationRef, and clientDid if provided
};
// Generate JWS (compact format)
const jws = await this.generateJWS(meta);
return {
jws,
meta,
};
}
/**
* Generate canonical hashes for request and response
* Requirement: 5.1
*/
generateCanonicalHashes(request, response) {
// Canonicalize request (exclude transport metadata, include method and params)
const canonicalRequest = {
method: request.method,
...(request.params && { params: request.params }),
};
// Canonicalize response (only the data part, exclude meta)
const canonicalResponse = response.data;
// Generate SHA-256 hashes with JCS canonicalization
const requestHash = this.generateSHA256Hash(canonicalRequest);
const responseHash = this.generateSHA256Hash(canonicalResponse);
return {
requestHash,
responseHash,
};
}
/**
* Generate SHA-256 hash with JCS canonicalization
* Requirement: 5.2
*/
generateSHA256Hash(data) {
// JCS (JSON Canonicalization Scheme) canonicalization
const canonicalJson = this.canonicalizeJSON(data);
// Generate SHA-256 hash
const hash = (0, crypto_1.createHash)("sha256")
.update(canonicalJson, "utf8")
.digest("hex");
return `sha256:${hash}`;
}
/**
* JCS canonicalization implementation (RFC 8785)
*/
canonicalizeJSON(obj) {
return (0, json_canonicalize_1.canonicalize)(obj);
}
/**
* Generate Ed25519 JWS in compact format (header.payload.signature)
* Requirement: 5.3
*
* Uses standard JWT claims (aud, sub, iss) in addition to custom claims
*/
async generateJWS(meta) {
try {
// Import the private key
const privateKeyPem = this.formatPrivateKeyAsPEM(this.identity.privateKey);
const privateKey = await (0, jose_1.importPKCS8)(privateKeyPem, "EdDSA");
// Create JWT payload with standard claims + custom proof data
// Standard JWT claims: aud (audience), sub (subject), iss (issuer)
// Custom claims: requestHash, responseHash, nonce, sessionId, etc.
const payload = {
// Standard JWT claims (RFC 7519)
aud: meta.audience, // Audience (who the token is for)
sub: meta.did, // Subject (agent DID)
iss: meta.did, // Issuer (agent DID - self-issued)
// Custom MCP-I proof claims
requestHash: meta.requestHash,
responseHash: meta.responseHash,
ts: meta.ts,
nonce: meta.nonce,
sessionId: meta.sessionId,
// Optional claims
...(meta.scopeId && { scopeId: meta.scopeId }),
...(meta.delegationRef && { delegationRef: meta.delegationRef }),
...(meta.clientDid && { clientDid: meta.clientDid }),
};
// Create and sign JWT (compact format: header.payload.signature)
const jwt = await new jose_1.SignJWT(payload)
.setProtectedHeader({
alg: "EdDSA",
kid: this.identity.kid,
})
.sign(privateKey);
// Return full compact JWS (NOT detached)
return jwt;
}
catch (error) {
throw new Error(`Failed to generate JWS: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Format base64 private key as PKCS#8 PEM for JOSE library
*/
formatPrivateKeyAsPEM(base64PrivateKey) {
const keyData = Buffer.from(base64PrivateKey, "base64");
// Ed25519 PKCS#8 header and footer
const header = "-----BEGIN PRIVATE KEY-----\n";
const footer = "\n-----END PRIVATE KEY-----";
// Wrap Ed25519 raw key in PKCS#8 structure (ASN.1 encoding)
const pkcs8Header = Buffer.from([
0x30,
0x2e, // SEQUENCE, length 46
0x02,
0x01,
0x00, // INTEGER version 0
0x30,
0x05, // SEQUENCE, length 5
0x06,
0x03,
0x2b,
0x65,
0x70, // OID for Ed25519
0x04,
0x22, // OCTET STRING, length 34
0x04,
0x20, // OCTET STRING, length 32 (the actual key)
]);
const fullKey = Buffer.concat([pkcs8Header, keyData.subarray(0, 32)]);
const base64Key = fullKey.toString("base64");
// Format as PEM with line breaks every 64 characters
const formattedKey = base64Key.match(/.{1,64}/g)?.join("\n") || base64Key;
return header + formattedKey + footer;
}
/**
* Verify a proof (for testing/validation)
*/
async verifyProof(proof, request, response) {
try {
// Regenerate hashes
const expectedHashes = this.generateCanonicalHashes(request, response);
// Check if hashes match
if (proof.meta.requestHash !== expectedHashes.requestHash ||
proof.meta.responseHash !== expectedHashes.responseHash) {
return false;
}
// Verify JWS signature using CryptoService
const publicKeyJwk = this.base64PublicKeyToJWK(this.identity.publicKey);
const cryptoProvider = new node_providers_1.NodeCryptoProvider();
const cryptoService = new mcp_i_core_1.CryptoService(cryptoProvider);
const isValid = await cryptoService.verifyJWS(proof.jws, publicKeyJwk, {
expectedKid: this.identity.kid,
alg: "EdDSA",
});
return isValid;
}
catch (error) {
console.error("[ProofGenerator] Proof verification error:", error);
return false;
}
}
/**
* Convert base64 public key to Ed25519 JWK format
*/
base64PublicKeyToJWK(publicKeyBase64) {
// Decode base64 to bytes
const publicKeyBytes = Buffer.from(publicKeyBase64, "base64");
// Verify key length (Ed25519 public keys are 32 bytes)
if (publicKeyBytes.length !== 32) {
throw new Error(`Invalid Ed25519 public key length: ${publicKeyBytes.length}`);
}
// Convert to base64url encoding
const base64url = Buffer.from(publicKeyBytes)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
return {
kty: "OKP",
crv: "Ed25519",
x: base64url,
kid: this.identity.kid,
};
}
}
exports.ProofGenerator = ProofGenerator;
/**
* Utility functions
*/
/**
* Create a tool response with proof
*/
async function createProofResponse(request, data, identity, session, options = {}) {
const response = { data };
const proofGenerator = new ProofGenerator(identity);
const proof = await proofGenerator.generateProof(request, response, session, options);
response.meta = { proof };
return response;
}
/**
* Extract canonical data for hashing (utility for testing)
*/
function extractCanonicalData(request, response) {
return {
request: {
method: request.method,
...(request.params && { params: request.params }),
},
response: response.data,
};
}