{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Implement A PKCS#11 HSM Signer","siteUrl":"https://opensource.ripple.com/","meta":[{"name":"google-site-verification","content":"bLwyBi1imklcIuQxZ7JeI_kRF5Mg7yfr6arpEQV2nsE"}],"llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]},"description":"Implement the ExternalSignerPort seam against a PKCS#11 HSM — you provide the public key and digest signing; the SDK owns the XRPL crypto."},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"implement-a-pkcs11-hsm-signer","__idx":0},"children":["Implement A PKCS#11 HSM Signer"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For an HSM, you implement the same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ExternalSignerPort"]}," seam against your device: you provide only \"give me the public key\" and \"sign this digest,\" and the SDK owns the XRPL crypto. HSM setups vary, so this is a reference to adapt rather than a drop-in."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"ts","header":{"controls":{"copy":{}}},"source":"/**\n * Bring-your-own HSM signer (PKCS#11).\n *\n * simpleXRPL ships an AWS KMS adapter (`simplexrpl/aws-kms`); for an HSM you\n * implement the same `ExternalSignerPort` seam against your device. The SDK owns\n * the XRPL crypto (SHA-512Half digest, low-S normalization, DER encoding); your\n * port only provides \"give me the public key\" and \"sign this digest\".\n *\n * As shipped this file is illustrative: the PKCS#11 adapter and its in-process\n * demo HSM at the bottom are commented out. Uncomment them (or wire the `Hsm`\n * interface to your real PKCS#11 binding, e.g. `pkcs11js`) for the snippet to\n * run. Everything it needs is in this file — nothing else to copy.\n */\nimport { ExternalSigner, SimpleXRPL } from 'simplexrpl'\nimport type {\n  LedgerPort,\n  Secp256k1SignerPort,\n  SubmitResponse,\n  Transaction,\n  TxResponse,\n} from 'simplexrpl'\n\n// === What you write with simpleXRPL ===\n// `signer` is your Secp256k1SignerPort backed by the HSM (see the adapter\n// below). `client.xrp`, `client.iou`, etc. now sign through the HSM — the\n// private key never leaves the device. Build → sign (in the HSM) → submit.\nasync function transferWithHsm(signer: Secp256k1SignerPort): Promise<void> {\n  const custody = await ExternalSigner.create({ signer })\n  const client = await SimpleXRPL.init({\n    xrpldUrl: 'wss://s.altnet.rippletest.net:51233', // XRPL Testnet\n    signers: [custody],\n    ledger: inMemoryLedger(), // omit in production to use the live XRPL connection\n  })\n  const result = await client.xrp.transfer({\n    to: client.account.create().address,\n    amount: '10',\n  })\n  console.log(\n    `HSM account ${custody.primary.address} signed & submitted ` +\n      `(source=${result.source}, hash=${result.txHash})`,\n  )\n  await client.disconnect()\n}\n\n// === Test scaffolding — NOT production code ===\n// In a real app you omit `ledger` from `SimpleXRPL.init` and the SDK uses the\n// live XRPL connection. This in-memory stand-in lets the example run offline:\n// it fills the network fields and reports a successful submission without\n// touching a network.\n\n/** An in-memory `LedgerPort`: accepts any signed blob and reports success. */\nfunction inMemoryLedger(): LedgerPort {\n  return {\n    autofill: async (tx: Transaction): Promise<Transaction> => ({\n      ...tx,\n      Sequence: 1,\n      Fee: '12',\n      LastLedgerSequence: 100,\n    }),\n    submit: async (): Promise<SubmitResponse> =>\n      ({ result: {} }) as unknown as SubmitResponse,\n    submitAndWait: async (): Promise<TxResponse> =>\n      ({\n        result: { hash: 'MOCKHASH', meta: { TransactionResult: 'tesSUCCESS' } },\n      }) as unknown as TxResponse,\n    request: async <T>(): Promise<T> => ({}) as T,\n  }\n}\n\n// === Bring-your-own HSM (PKCS#11) — uncomment to run, or wire your device ===\n// HSM setups vary (slot, PIN, key label, vendor library), so this is a\n// reference to adapt. `demoHsm()` below is an in-process stand-in so the file\n// runs offline; swap it for a real PKCS#11 binding. The SDK owns low-S\n// normalization + DER encoding; your port returns the raw `r‖s` scalars.\n//\n// import { secp256k1 } from '@noble/curves/secp256k1'\n// import type { EcdsaSignature } from 'simplexrpl'\n//\n// // secp256k1 sizes: 32-byte scalars, 65-byte uncompressed point (0x04‖X‖Y).\n// const SCALAR_BYTES = 32\n// const POINT_BYTES = 65\n// const COMPRESSED_EVEN = 0x02\n// const COMPRESSED_ODD = 0x03\n// const EVEN = 2\n//\n// /** The narrow slice of your HSM the signer needs (PKCS#11, ECDSA secp256k1). */\n// interface Hsm {\n//   // CKA_EC_POINT — DER OCTET STRING wrapping the uncompressed point 0x04‖X‖Y.\n//   readonly ecPoint: () => Promise<Uint8Array>\n//   // C_Sign with CKM_ECDSA (NOT CKM_ECDSA_SHA256 — the digest is pre-hashed);\n//   // returns the raw 64-byte r‖s.\n//   readonly signDigest: (digest: Uint8Array) => Promise<Uint8Array>\n// }\n//\n// /** Strip the DER wrapper; the uncompressed point is the trailing 65 bytes. */\n// function uncompressedPoint(ecPoint: Uint8Array): Buffer {\n//   return Buffer.from(ecPoint).subarray(-POINT_BYTES)\n// }\n//\n// /** An ExternalSignerPort backed by a PKCS#11 HSM. */\n// class Pkcs11Signer implements Secp256k1SignerPort {\n//   public readonly algorithm = 'secp256k1'\n//   public constructor(private readonly hsm: Hsm) {}\n//\n//   public async publicKey(): Promise<string> {\n//     const point = uncompressedPoint(await this.hsm.ecPoint())\n//     const x = point.subarray(1, 1 + SCALAR_BYTES)\n//     const y = point.subarray(1 + SCALAR_BYTES)\n//     const prefix =\n//       y[y.length - 1] % EVEN === 0 ? COMPRESSED_EVEN : COMPRESSED_ODD\n//     return Buffer.concat([Buffer.from([prefix]), x])\n//       .toString('hex')\n//       .toUpperCase()\n//   }\n//\n//   public async signDigest(digest: Uint8Array): Promise<EcdsaSignature> {\n//     const raw = Buffer.from(await this.hsm.signDigest(digest))\n//     return {\n//       r: BigInt(`0x${raw.subarray(0, SCALAR_BYTES).toString('hex')}`),\n//       s: BigInt(`0x${raw.subarray(SCALAR_BYTES).toString('hex')}`),\n//     }\n//   }\n// }\n//\n// /**\n//  * DEMO ONLY: an in-process secp256k1 key that stands in for a real HSM so\n//  * this example runs end to end offline. It returns exactly the shapes a\n//  * PKCS#11 binding would — an uncompressed EC point and a raw `r‖s`\n//  * signature — so `Pkcs11Signer` is identical against this stub or a real\n//  * device. In production you delete this and wire the adapter to your device.\n//  */\n// function demoHsm(): Hsm {\n//   const priv = Buffer.from(\n//     'c9537c5a2f3f7e1d4b6a8c0e2f4d6b8a1c3e5f7091b3d5f7a9c1e3050709b0d0f',\n//     'hex',\n//   )\n//   return {\n//     ecPoint: async (): Promise<Uint8Array> =>\n//       secp256k1.getPublicKey(priv, false),\n//     signDigest: async (digest: Uint8Array): Promise<Uint8Array> =>\n//       secp256k1.sign(digest, priv).toCompactRawBytes(),\n//   }\n// }\n//\n// await transferWithHsm(new Pkcs11Signer(demoHsm()))\n","lang":"ts"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"see-also","__idx":1},"children":["See Also"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"a","attributes":{"href":"/docs/simplexrpl/references/verticals/account/create"},"children":["account.create()"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"a","attributes":{"href":"/docs/simplexrpl/references/verticals/xrp/transfer"},"children":["xrp.transfer()"]}]}]}]},"headings":[{"value":"Implement A PKCS#11 HSM Signer","id":"implement-a-pkcs11-hsm-signer","depth":1},{"value":"See Also","id":"see-also","depth":2}],"frontmatter":{"seo":{"description":"Implement the ExternalSignerPort seam against a PKCS#11 HSM — you provide the public key and digest signing; the SDK owns the XRPL crypto.","title":"Implement A PKCS#11 HSM Signer"},"labels":["simpleXRPL","SDK"]},"editPage":{"to":"https://github.com/ripple/opensource.ripple.com/tree/main/docs/simpleXRPL/tutorials/implement-pkcs11-signer.md"},"lastModified":"2026-08-21T20:52:47.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/docs/simplexrpl/tutorials/implement-pkcs11-signer","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}