diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 00000000..95ea7047 --- /dev/null +++ b/examples/package.json @@ -0,0 +1,22 @@ +{ + "name": "signify-ts-extern-kms-example", + "version": "1.0.0", + "description": "Example project for Extern (Incept & Rotate)", + "type": "module", + "scripts": { + "create": "tsx src/extern_createAccount.ts", + "rotate": "tsx src/extern_rotation.ts" + }, + "dependencies": { + "@aws-sdk/client-kms": "^3.998.0", + "signify-ts": "^0.3.0-rc2" + }, + "overrides": { + "libsodium-wrappers-sumo": "0.7.15" + }, + "devDependencies": { + "@types/node": "^25.0.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/examples/src/AwsKmsModule.ts b/examples/src/AwsKmsModule.ts new file mode 100644 index 00000000..8a49c6e8 --- /dev/null +++ b/examples/src/AwsKmsModule.ts @@ -0,0 +1,63 @@ +import { Algos, Matter, MtrDex, Diger, Siger, Signer } from "signify-ts"; +import { KMSClient, GetPublicKeyCommand, SignCommand, SignCommandInput } from "@aws-sdk/client-kms"; + +/* ================================================= + * AWS KMS External Module Implementation + * ------------------------------------------------- + * This strictly implements the IdentifierManager interface. + * You can swap the inner logic with `@google-cloud/kms` to test on GCP. + * ================================================= */ +export class AwsKmsModule { + algo: Algos = Algos.extern; + signers: Signer[] = []; // KMS handles signing, so local signers remain empty + private kms: KMSClient; + private keyId: string; + + constructor(pidx: number, args: any, region: string, keyId: string) { + this.kms = new KMSClient({ region }); + this.keyId = keyId; + } + + // This data is passed to KERIA and stored in the DB (fixes the 500 error) + params(): any { + return { extern_type: "aws_kms" }; + } + + // Fetches the public key from AWS KMS and formats it to qb64 + async getPubQb64(): Promise { + const cmd = new GetPublicKeyCommand({ KeyId: this.keyId }); + const res = await this.kms.send(cmd); + if (!res.PublicKey) throw new Error("PublicKey not found in KMS"); + + // Extract raw 32 bytes from AWS Ed25519 DER format + const raw32 = Buffer.from(res.PublicKey).slice(-32); + return new Matter({ raw: raw32, code: MtrDex.Ed25519 }).qb64; + } + + async incept(transferable: boolean): Promise<[string[], string[]]> { + const pubQb64 = await this.getPubQb64(); + const nextDigQb64 = new Diger({ code: MtrDex.Blake3_256 }, new Matter({ qb64: pubQb64 }).qb64b).qb64; + + return [[pubQb64], [nextDigQb64]]; + } + + async rotate(ncodes: string[], transferable: boolean): Promise<[string[], string[]]> { + return this.incept(transferable); + } + + // Delegates the payload signing to AWS KMS + async sign(ser: Uint8Array, indexed: boolean = true): Promise { + const input: SignCommandInput = { + KeyId: this.keyId, + Message: ser, + MessageType: "RAW", + SigningAlgorithm: "ED25519_SHA_512", // Required for Ed25519 in AWS SDK v3 + }; + const command = new SignCommand(input); + const response = await this.kms.send(command); + if (!response.Signature) throw new Error("Signature generation failed"); + + const sigBytes = Buffer.from(response.Signature); + return [new Siger({ raw: sigBytes, index: 0 }).qb64]; + } +} \ No newline at end of file diff --git a/examples/src/extern_createAccount.ts b/examples/src/extern_createAccount.ts new file mode 100644 index 00000000..0b80357d --- /dev/null +++ b/examples/src/extern_createAccount.ts @@ -0,0 +1,139 @@ +import dotenv from 'dotenv'; +import { Tier, Algos, ready, SignifyClient, ExternalModule } from 'signify-ts'; +import { AwsKmsModule } from './AwsKmsModule.js'; + +dotenv.config(); + +/* ================================================= + * Minimal Helpers for Standalone Execution + * ================================================= */ +async function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + +async function waitOperation(client: SignifyClient, op: any): Promise { + let cur = op; + for (let i = 0; i < 30; i++) { + if (cur?.done) { + try { + await client.operations().delete(op.name); + } catch {} + return cur.response; + } + await sleep(1000); + cur = await client.operations().get(op.name); + } + throw new Error(`Operation timed out: ${op.name}`); +} + +async function connectClient( + url: string, + bootUrl: string, + bran: string, + modules: any[] +) { + await ready(); + const client = new SignifyClient(url, bran, Tier.low, bootUrl, modules); + + try { + await client.connect(); + } catch { + await client.boot(); + await sleep(500); + await client.connect(); + } + return client; +} + +/* ================================================= + * Main Execution Script (Testing PR #415 FIX) + * ================================================= */ +async function main() { + console.log('\nšŸš€ EXTERN AID STANDALONE DEMO (CREATE ACCOUNT)'); + + const KERIA_URL = process.env.KERIA_URL || ''; + const KERIA_PORT = process.env.KERIA_PORT || '3901'; + const BOOT_PORT = process.env.BOOT_PORT || '3903'; + const BRAN = process.env.TEST_BRAN || ''; + const ALIAS = process.env.TEST_ALIAS || ''; + const AWS_REGION = process.env.AWS_REGION || ''; + const AWS_KEY_ID = process.env.AWS_KMS_KEY_ID || ''; + + await ready(); + + const modules: ExternalModule[] = [ + { + type: 'aws_kms', + name: 'AwsKmsModule', + module: class { + constructor(pidx: number, args: any) { + return new AwsKmsModule( + pidx, + args, + AWS_REGION, + AWS_KEY_ID + ) as any; + } + } as any, + }, + ]; + + console.log('\n[SETUP] Connecting to KERIA...'); + const client = await connectClient( + `${KERIA_URL}:${KERIA_PORT}`, + `${KERIA_URL}:${BOOT_PORT}`, + BRAN, + modules + ); + console.log('āœ… Client connected.'); + + const identifiers = client.identifiers(); + let prefix: string | undefined; + + console.log(`\n[STEP 1] Ensure extern AID with alias: ${ALIAS}`); + try { + const hab = await identifiers.get(ALIAS); + prefix = hab.prefix || hab.state?.i; + console.log(`ā„¹ļø AID already exists: ${prefix}`); + } catch { + console.log('āž”ļø Creating new extern AID...'); + const createRes = await identifiers.create(ALIAS, { + algo: Algos.extern, + extern_type: 'aws_kms', + transferable: true, + wits: [], + toad: 0, + }); + + const op = await createRes.op(); + const result = await waitOperation(client, op); + prefix = result?.i || result?.pre; + console.log(`āœ… AWS KMS AID created successfully: ${prefix}`); + } + + console.log( + `\n[STEP 2] Verifying metadata persistence (The 500 Error Fix)` + ); + const aidInfo = await identifiers.get(ALIAS); + + console.log('-----------------------------------------'); + console.log(JSON.stringify(aidInfo, null, 2)); + console.log('-----------------------------------------'); + + if (aidInfo.extern && aidInfo.extern.extern_type === 'aws_kms') { + console.log( + "āœ… SUCCESS: The 'extern' metadata is correctly persisted and retrieved without throwing a 500 error!" + ); + } else { + console.error( + "āŒ FAILED: The 'extern' metadata is missing from the DB response." + ); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((e) => { + console.error('\nšŸ’„ FATAL ERROR:', e); + process.exit(1); + }); +} diff --git a/examples/src/extern_rotation.ts b/examples/src/extern_rotation.ts new file mode 100644 index 00000000..cd4f253a --- /dev/null +++ b/examples/src/extern_rotation.ts @@ -0,0 +1,214 @@ +import dotenv from 'dotenv'; +import { + Tier, + Algos, + ready, + SignifyClient, + Saider, + Serder, + ExternalModule, + Diger, + Matter, + MtrDex, +} from 'signify-ts'; +import { AwsKmsModule } from './AwsKmsModule.js'; + +dotenv.config(); + +/* ================================================= + * Minimal Helpers for Standalone Execution + * ================================================= */ +async function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + +async function connectClient( + url: string, + bootUrl: string, + bran: string, + modules: any[] +) { + await ready(); + const client = new SignifyClient(url, bran, Tier.low, bootUrl, modules); + + try { + await client.connect(); + } catch { + await client.boot(); + await sleep(500); + await client.connect(); + } + return client; +} + +async function postRotEvent( + client: SignifyClient, + alias: string, + rotKed: any, + sigerQb64: string, + externMeta: any +) { + const path = `/identifiers/${encodeURIComponent(alias)}/events`; + + const body: any = { + rot: rotKed, + sigs: [sigerQb64], + smids: [], + rmids: [], + }; + + if (externMeta) body.extern = externMeta; + + const res = await (client as any).fetch(path, 'POST', body); + if (res && typeof res.json === 'function') return await res.json(); + return res; +} + +/* ================================================= + * Main Execution Script (Testing Rotation) + * ================================================= */ +async function main() { + console.log('\n================================================='); + console.log('šŸš€ EXTERN AID STANDALONE DEMO (ROTATION)'); + console.log('================================================='); + + const KERIA_URL = process.env.KERIA_URL || ''; + const KERIA_PORT = process.env.KERIA_PORT || '3901'; + const BOOT_PORT = process.env.BOOT_PORT || '3903'; + const BRAN = process.env.TEST_BRAN || ''; + const ALIAS = process.env.TEST_ALIAS || ''; + + const AWS_REGION = process.env.AWS_REGION || ''; + const AWS_KEY_ID = process.env.AWS_KMS_KEY_ID || ''; + + await ready(); + + const modules: ExternalModule[] = [ + { + type: 'aws_kms', + name: 'AwsKmsModule', + module: class { + constructor(pidx: number, args: any) { + return new AwsKmsModule( + pidx, + args, + AWS_REGION, + AWS_KEY_ID + ) as any; + } + } as any, + }, + ]; + + console.log('\n[SETUP] Connecting to KERIA...'); + const client = await connectClient( + `${KERIA_URL}:${KERIA_PORT}`, + `${KERIA_URL}:${BOOT_PORT}`, + BRAN, + modules + ); + console.log('āœ… Client connected.'); + + const identifiers = client.identifiers(); + + console.log(`\n[STEP 1] Fetching Identifier Info for ${ALIAS}...`); + let aidInfo: any; + try { + aidInfo = await identifiers.get(ALIAS); + } catch (e: any) { + throw new Error( + `āŒ Failed to fetch identifier ${ALIAS}. Run createAccount script first.` + ); + } + + const prefix = aidInfo.prefix || aidInfo.state?.i; + + console.log('\n[STEP 2] Fetching Current KeyState...'); + const statesRes = await client.keyStates().get(prefix); + const beforeState = Array.isArray(statesRes) ? statesRes[0] : statesRes; + const beforeS = parseInt(beforeState.s, 16); + const beforeD = beforeState.d; + + console.log(` - Current Sequence (s): ${beforeS}`); + console.log(` - Current Event Dig(d): ${beforeD}`); + + console.log('\n[STEP 3] Constructing ROT Event...'); + const awsSigner = new AwsKmsModule(0, {}, AWS_REGION, AWS_KEY_ID); + const pubQb64 = await awsSigner.getPubQb64(); + const nextDigQb64 = new Diger( + { code: MtrDex.Blake3_256 }, + new Matter({ qb64: pubQb64 }).qb64b + ).qb64; + + const nextS_hex = (beforeS + 1).toString(16); + + const rotKed0: any = { + v: 'KERI10JSON000000_', + t: 'rot', + d: '', + i: prefix, + s: nextS_hex, + p: beforeD, + kt: '1', + k: [pubQb64], + nt: '1', + n: [nextDigQb64], + bt: '0', + br: [], + ba: [], + a: [], + }; + + const [, rotKed1] = (Saider as any).saidify(rotKed0); + const serder = new Serder(rotKed1); + + const rotKedFinal = (serder as any).ked ?? (serder as any).sad ?? rotKed1; + const rawSer: Uint8Array = + (serder as any).raw instanceof Uint8Array + ? (serder as any).raw + : new TextEncoder().encode((serder as any).raw); + + console.log('\n[STEP 4] Signing ROT Event with AWS KMS...'); + const sigs = await awsSigner.sign(rawSer, true); + console.log(` - Generated KERI Siger: ${sigs[0]}`); + + console.log('\n[STEP 5] Submitting ROT Event to KERIA...'); + const externMeta = aidInfo.extern; + + await postRotEvent(client, ALIAS, rotKedFinal, sigs[0], externMeta); + console.log(' - Submit Response OK.'); + + console.log('\n[STEP 6] Verifying State & Key Event Log (KEL)...'); + await sleep(2000); + + const finalStates = await client.keyStates().get(prefix); + const finalState = Array.isArray(finalStates) + ? finalStates[0] + : finalStates; + const finalS = parseInt(finalState.s, 16); + + const fullKel = await client.keyEvents().get(prefix); + const lastEvent = fullKel[fullKel.length - 1]; + + console.log('---------------------------------------------------'); + console.log(`šŸ“Š Current State: Sequence ${finalS}`); + console.log(`šŸ“œ Full KEL Length: ${fullKel.length} events`); + console.log(`šŸ†• Latest Event Type: ${lastEvent.ked.t}`); + console.log(`šŸ†• Latest Event SAID: ${lastEvent.ked.d}`); + console.log('---------------------------------------------------'); + + if (finalS === beforeS + 1 && lastEvent.ked.t === 'rot') { + } else { + console.error( + '\nāŒ FAILED: Sequence number did not increment or ROT event missing.' + ); + process.exit(1); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((e) => { + console.error('\nšŸ’„ FATAL ERROR:', e); + process.exit(1); + }); +} diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 00000000..7a922699 --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "baseUrl": ".", + "paths": {} + }, + "include": ["src"] +} \ No newline at end of file