@coordination-network/identity 0.0.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ const deviceSecret = generateDeviceSecret()
22
22
  // Deterministically re-derive the full key bundle from it, any time.
23
23
  const keys = deriveKeys(deviceSecret)
24
24
 
25
- console.log(keys.networkId) // 0x-prefixed bytes32, ends with the account's Ethereum address
25
+ console.log(keys.networkId) // 0x-prefixed bytes32: a domain-separated hash of the account public key
26
26
  console.log(keys.account.publicKey) // 33-byte compressed secp256k1 public key
27
27
  ```
28
28
 
@@ -34,6 +34,8 @@ If a caller only needs the `networkId` (e.g. to look up or announce a device wit
34
34
  import { deriveNetworkId } from "@coordination-network/identity"
35
35
 
36
36
  const networkId = deriveNetworkId(deviceSecret) // === deriveKeys(deviceSecret).networkId
37
+
38
+ // Network members use `keys.memberNetworkId`, whose last 20 bytes are the member's auth address.
37
39
  ```
38
40
 
39
41
  ### Credential binding (dual proof-of-possession)
@@ -47,10 +49,14 @@ import { buildBindingMessages, signAccountBinding } from "@coordination-network/
47
49
 
48
50
  // `nonce` comes from the issuer (e.g. a fresh 32-byte session challenge).
49
51
  // `confirmationKey` is the device's P-256 public key (any of 65B/33B/64B encodings).
52
+ // `holderKeyCommitment` is the identity key's commitment as 32 bytes. `requestCommitment` is 32 opaque
53
+ // bytes naming the issuer and scope the user consented to; the credential package computes it.
50
54
  const { deviceBindingMessage, accountBindingMessage } = buildBindingMessages(
51
55
  nonce,
52
56
  keys.account.uncompressedPublicKey,
53
- confirmationKey
57
+ confirmationKey,
58
+ holderKeyCommitment,
59
+ requestCommitment
54
60
  )
55
61
 
56
62
  // The platform signs deviceBindingMessage with the hardware-backed P-256 key
@@ -73,6 +79,8 @@ const { valid, networkId } = verifyCredentialBinding({
73
79
  nonce,
74
80
  accountPublicKey, // the account's public key, as claimed by the wallet
75
81
  confirmationKey,
82
+ holderKeyCommitment,
83
+ requestCommitment, // recomputed by the issuer from its own id and the scope it will sign
76
84
  accountSignature: sigA,
77
85
  deviceSignature: sigB
78
86
  })
@@ -84,7 +92,9 @@ if (!valid) {
84
92
 
85
93
  ### Baby Jubjub identity key and Semaphore commitment
86
94
 
87
- `deriveEddsaKeypair` derives a stable, device-level Baby Jubjub EdDSA keypair and Semaphore-v4-compatible identity commitment — a standalone derivation, additive to `deriveKeys`. Unlike the secp256k1 core, this key is **hardware-rooted**, not derived from `deviceSecret`: its input is an ECDH shared secret computed by the platform's hardware-backed P-256 key (the same key used for credential-binding's `confirmationKey`) against a fixed, NUMS peer point this package pins. See [`DERIVATION.md` §12](./DERIVATION.md#12-baby-jubjub-identity-key-and-semaphore-commitment) for the full normative scheme and the rationale for hardware-rooting it this way.
95
+ `deriveEddsaKeypair` derives a stable, device-level Baby Jubjub EdDSA keypair and Semaphore-v4-compatible identity commitment — a standalone derivation, additive to `deriveKeys`. Unlike the secp256k1 core, this key is **hardware-rooted**, not derived from `deviceSecret`: its input is an ECDH shared secret computed by the platform's hardware-backed P-256 device agreement key (a separate key from the device signing key that is credential-binding's `confirmationKey`) against a fixed, NUMS peer point this package pins. See [`DERIVATION.md` §12](./DERIVATION.md#12-baby-jubjub-identity-key-and-semaphore-commitment) for the full normative scheme and the rationale for hardware-rooting it this way.
96
+
97
+ `derivePaymasterKeypair` accepts the same ECDH shared secret but uses a separate domain tag. Its commitment may be published as a paymaster group leaf; the credential holder commitment returned by `deriveEddsaKeypair` must not be published there.
88
98
 
89
99
  ```typescript
90
100
  import { deriveEddsaKeypair, eddsaPeerPointBytes } from "@coordination-network/identity"
@@ -92,7 +102,7 @@ import { deriveEddsaKeypair, eddsaPeerPointBytes } from "@coordination-network/i
92
102
  // The platform layer computes this natively, inside the secure element —
93
103
  // e.g. Android Keystore KeyAgreement("ECDH") against the pinned peer point:
94
104
  // ecdhSharedSecret = await keystore.ecdh(alias, base64(eddsaPeerPointBytes()))
95
- const ecdhSharedSecret = await platformEcdh(confirmationKeyAlias, eddsaPeerPointBytes())
105
+ const ecdhSharedSecret = await platformEcdh(agreementKeyAlias, eddsaPeerPointBytes())
96
106
 
97
107
  const eddsa = deriveEddsaKeypair(ecdhSharedSecret)
98
108
 
@@ -100,7 +110,7 @@ console.log(eddsa.commitment) // bigint — the Semaphore v4 identity commitment
100
110
  console.log(eddsa.publicKey) // [bigint, bigint] — the Baby Jubjub public key
101
111
  ```
102
112
 
103
- **This key is not recoverable from `deviceSecret` alone.** If the hardware confirmation key is lost (device reset, Keystore wipe, replaced secure element), this device's Semaphore commitment cannot be recomputed — a new hardware key derives a new, unrelated identity key. This is a deliberate trade-off of rooting the key in hardware at all; see [`DERIVATION.md` §12.1](./DERIVATION.md#121-the-formula).
113
+ **This key is not recoverable from `deviceSecret` alone.** If the hardware device agreement key is lost (device reset, Keystore wipe, removal of the secure lock screen, replaced secure element), this device's Semaphore commitment cannot be recomputed — a new hardware key derives a new, unrelated identity key. This is a deliberate trade-off of rooting the key in hardware at all; see [`DERIVATION.md` §12.1](./DERIVATION.md#121-the-formula).
104
114
 
105
115
  **`eddsa.seed` is `Hex` — decode it to raw bytes before handing it to Semaphore.** `@semaphore-protocol/identity`'s `Identity` constructor accepts a hex _string_ as raw UTF-8 text, not as hex to decode, so passing `eddsa.seed` directly derives a different, unrelated identity — and it will not throw, it will just silently be wrong. Decode the hex to bytes first with whatever hex utility your project already uses (e.g. viem's/ethers' `hexToBytes`, or `Buffer.from(seed.slice(2), "hex")` in Node):
106
116
 
@@ -176,76 +186,78 @@ const stealthPrivateKey = generateStealthPrivateKey({
176
186
 
177
187
  All hex values are lowercase, `0x`-prefixed strings (type `Hex`). Every function documented below is exported from the package root. See [`DERIVATION.md`](./DERIVATION.md) for the exact formulas behind each one (stealth-address formulas: [`specs/05-stealth-addresses.md`](../../specs/05-stealth-addresses.md)).
178
188
 
179
- | Export | Kind | Signature | Description |
180
- | -------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
181
- | `generateDeviceSecret` | function | `() => Hex` | Generates a fresh 32-byte device secret via `globalThis.crypto.getRandomValues`. |
182
- | `deriveKeys` | function | `(deviceSecret: Uint8Array \| Hex) => DerivedKeys` | Derives the full `v1` identity (`networkId` + account/spending/viewing/auth keypairs) from a device secret. |
183
- | `deriveNetworkId` | function | `(deviceSecret: Uint8Array \| Hex) => Hex` | Cheap path: derives only the account key and `networkId`, skipping the signature and stealth/auth derivation. |
184
- | `deriveAccountKeypair` | function | `(deviceSecret: Uint8Array \| Hex) => Secp256k1Keypair` | Derives the root account keypair (`hashToScalar(DOMAIN_ACCOUNT, deviceSecret)`). |
185
- | `signDerivationMessage` | function | `(accountPrivateKey: Hex \| Uint8Array) => Hex` | Deterministic EIP-191 personal-sign signature over the fixed domain message, using the account private key. |
186
- | `deriveStealthKeys` | function | `(sig64: Uint8Array \| Hex) => { spending: Secp256k1Keypair; viewing: Secp256k1Keypair }` | Derives spending/viewing keypairs from the derivation-message signature (FluidKey `generateKeysFromSignature` semantics). |
187
- | `deriveAuthKeypair` | function | `(sig64: Uint8Array \| Hex) => Secp256k1Keypair` | Derives the auth keypair (`hashToScalar(DOMAIN_AUTH, sig64)`). |
188
- | `computeSharedSecret` | function | `({ privateKey: Hex; publicKey: Hex }) => Uint8Array` | ERC-5564 hashed shared secret: `keccak256` of the 33-byte compressed ECDH point. Symmetric — sender passes `(ephemeralPriv, viewingPub)`, recipient `(viewingPriv, ephemeralPub)`. |
189
- | `getViewTag` | function | `(hashedSharedSecret: Uint8Array) => number` | The one-byte view tag: first byte of the hashed shared secret, published in announcements. |
190
- | `getStealthPublicKey` | function | `({ spendingPublicKey: Hex; hashedSharedSecret: Uint8Array }) => Uint8Array` | Scheme 1 point math: `P_spend + (s_h mod n)·G`, returned as a 65-byte uncompressed key. |
191
- | `publicKeyToAddress` | function | `(uncompressedPublicKey: Uint8Array) => Address` | Standard Ethereum address derivation: `keccak256(X \|\| Y)`, last 20 bytes. |
192
- | `generateEphemeralKey` | function | `() => Secp256k1Keypair` | Fresh CSPRNG keypair for one load. MUST be single-use — reuse links the payments it was used for. |
193
- | `generateStealthAddress` | function | `({ spendingPublicKey, viewingPublicKey, ephemeralPrivateKey }) => Address` | Sender side: derives the fresh stealth address for a load. |
194
- | `generateStealthPrivateKey` | function | `({ ephemeralPublicKey, viewingPrivateKey, spendingPrivateKey }) => Hex` | Recipient side: recovers the private key controlling a stealth address — `(p_spend + s_h) mod n`. |
195
- | `checkStealthAddress` | function | `({ stealthAddress, ephemeralPublicKey, viewTag, viewingPrivateKey, spendingPublicKey }) => boolean` | Recipient side: view-tag filter then full recompute; `true` when the announcement is addressed to these keys. Case-insensitive on `stealthAddress`. Throws on malformed announcement data — scan loops should try/catch per announcement. |
196
- | `validateMetaStealthAddress` | function | `(metaStealthAddress: string) => boolean` | Shape + curve check for a 66-byte meta-address: `0x` + 132 hex chars, both halves valid compressed points. |
197
- | `encodeMetaStealthAddress` | function | `({ spendingPublicKey, viewingPublicKey }) => Hex` | The 66-byte compressed `spending \|\| viewing` concatenation `IdentityRegistry` stores. Validates before emitting (the registry is write-once) — throws `InvalidMetaStealthAddressError`. |
198
- | `encodeMetaStealthAddressUri` | function | `({ spendingPublicKey, viewingPublicKey }) => string` | The `st:eth:0x…` display form (ERC-5564 URI). Never stored onchain. |
199
- | `decodeMetaStealthAddress` | function | `(metaStealthAddress: string) => { spendingPublicKey: Hex; viewingPublicKey: Hex }` | Splits a 66-byte meta-address (raw or `st:eth:` form) into its two keys. No validation — pair with `validateMetaStealthAddress` for untrusted input. |
200
- | `buildAnnouncement` | function | `({ metaStealthAddress: Hex }) => { schemeId: 1; stealthAddress: Address; ephemeralPublicKey: Hex; metadata: Hex }` | Builds the ERC-5564 announcement payload for a fresh stealth address from a recipient's meta-address. Generates the single-use ephemeral key internally; `metadata` is the one-byte view tag. |
201
- | `deriveEddsaKeypair` | function | `(ecdhSharedSecret: Uint8Array \| Hex) => EddsaKeypair` | Derives the Baby Jubjub identity keypair and its Semaphore v4 identity commitment from a hardware-computed ECDH shared secret against `EDDSA_PEER_POINT_HEX`. |
202
- | `eddsaPeerPointBytes` | function | `() => Uint8Array` | Returns a fresh 65-byte SEC1 copy of the fixed EdDSA peer point `Q`, for platform ECDH calls. |
203
- | `computeEddsaPeerPoint` | function | `() => Uint8Array` | Recomputes `Q` via RFC 9380 hash-to-curve at runtime — reproducibility evidence that `EDDSA_PEER_POINT_HEX` is not hand-picked. |
204
- | `normalizeEcdhSharedSecret` | function | `(secret: Uint8Array \| Hex \| string) => Uint8Array` | Validates/normalizes an ECDH shared secret to exactly 32 bytes. |
205
- | `computeNetworkId` | function | `(uncompressedPublicKey: Uint8Array \| Hex) => Hex` | Computes the bytes32 `networkId` from an uncompressed secp256k1 public key. |
206
- | `hashToScalar` | function | `(tag: string, input: Uint8Array) => Uint8Array` | The normative hash-to-secp256k1-scalar primitive (keccak256, reject-and-retry counter). |
207
- | `hashToScalarWith` | function | `(hashFn: HashFn, tag: string, input: Uint8Array) => Uint8Array` | Same algorithm as `hashToScalar` with an injectable hash function (used to test the retry branch). |
208
- | `isValidScalar` | function | `(scalar: Uint8Array) => boolean` | Tests whether `scalar` is a valid secp256k1 private key (32 bytes, integer in `[1, n)`). |
209
- | `scalarToKeypair` | function | `(scalar: Uint8Array) => Secp256k1Keypair` | Expands a validated 32-byte scalar into a full keypair (compressed + uncompressed public keys). |
210
- | `normalizeDeviceSecret` | function | `(secret: Uint8Array \| Hex \| string) => Uint8Array` | Validates/normalizes a device secret to exactly 32 bytes. |
211
- | `normalizeConfirmationKey` | function | `(confirmationKey: Uint8Array \| Hex) => Uint8Array` | Normalizes a P-256 `confirmationKey` (65B/33B/64B) to its raw 64-byte `X \|\| Y`, validating it is on-curve. |
212
- | `computeConfirmationKeyHash` | function | `(confirmationKey: Uint8Array \| Hex) => Hex` | Computes `confirmationKeyHash`, the RFC 7638 JWK thumbprint of a P-256 `confirmationKey`. |
213
- | `buildDeviceBindingMessage` | function | `(nonce: Uint8Array \| Hex, networkId: Uint8Array \| Hex) => Uint8Array` | Low-level: builds the 64-byte `nonce \|\| networkId` message signed by the P-256 confirmation key. |
214
- | `buildAccountBindingMessage` | function | `(nonce, confirmationKey, networkId) => Uint8Array` | Low-level: builds the 96-byte `nonce \|\| confirmationKeyHash \|\| networkId` message signed by the account key. |
215
- | `buildBindingMessages` | function | `(nonce, accountUncompressedPublicKey, confirmationKey) => { networkId: Hex; deviceBindingMessage: Uint8Array; accountBindingMessage: Uint8Array }` | Wallet-side: builds both binding messages with `networkId` derived internally from the account public key. |
216
- | `signAccountBinding` | function | `(accountPrivateKey: Hex \| Uint8Array, nonce, confirmationKey) => { signature: Hex; networkId: Hex }` | Produces `sig_A`: the deterministic EIP-191 signature over `accountBindingMessage`, with `networkId` derived internally. |
217
- | `verifyAccountBindingSignature` | function | `({ nonce, confirmationKey, accountPublicKey, signature }) => boolean` | Issuer-side: verifies `sig_A` against `accountPublicKey`, with `networkId` derived internally. |
218
- | `verifyDeviceBindingSignature` | function | `({ nonce, networkId, confirmationKey, signature }) => boolean` | Issuer-side: verifies `sig_B` (raw-compact or DER, `lowS` not enforced). `networkId` is a parameter here — the one place it cannot be derived internally; see [`DERIVATION.md` §11.5](./DERIVATION.md#115-mutual-binding-and-the-locally-derived-networkid-must). |
219
- | `verifyCredentialBinding` | function | `({ nonce, accountPublicKey, confirmationKey, accountSignature, deviceSignature }) => { valid: boolean; networkId: Hex }` | The full issuer gate: derives one `networkId` and checks both `sig_A` and `sig_B` against it. |
220
- | `KEY_DERIVATION_VERSION` | constant | `"v1"` | The current derivation scheme version, also the value of `DerivedKeys.version`. |
221
- | `FIXED_DOMAIN_MSG` | constant | `"coordination-network/identity/v1"` | Fixed message signed by `signDerivationMessage`. |
222
- | `DOMAIN_ACCOUNT` | constant | `"coordination-network/identity/v1/account"` | Domain-separation tag for the account key. |
223
- | `DOMAIN_AUTH` | constant | `"coordination-network/identity/v1/auth"` | Domain-separation tag for the auth key. |
224
- | `DOMAIN_EDDSA_PEER` | constant | `"coordination-network/identity/v1/eddsa/peer"` | Domain-separation tag hashed to produce the fixed EdDSA peer point `Q` (`EDDSA_PEER_POINT_HEX`). |
225
- | `EDDSA_PEER_POINT_HEX` | constant | `Hex` | The fixed, NUMS P-256 peer point ECDH'd against the hardware confirmation key to produce this device's `deriveEddsaKeypair` input. |
226
- | `DOMAIN_EDDSA` | constant | `"coordination-network/identity/v1/eddsa"` | Domain-separation tag for the Baby Jubjub EdDSA seed, applied to the ECDH shared secret. |
227
- | `BINDING_NONCE_LENGTH` | constant | `32` | Required byte length of `nonce` and `networkId` in the credential-binding messages. |
228
- | `COMPRESSED_PUBLIC_KEY_LENGTH` | constant | `33` | SEC1-compressed secp256k1 public key length in bytes. |
229
- | `META_STEALTH_ADDRESS_LENGTH` | constant | `66` | ERC-6538 scheme 1 meta-stealth address length: compressed spending ‖ viewing keys. |
230
- | `META_STEALTH_URI_PREFIX` | constant | `"st:eth:"` | ERC-5564 offchain display prefix — never stored onchain. |
231
- | `ERC_5564_SCHEME_ID` | constant | `1` | ERC-5564 scheme id for secp256k1 with view tags. |
232
- | `KeygenError` | class | extends `Error`, has `code: KeygenErrorCode` | Base class for every error this package throws. |
233
- | `InvalidDeviceSecretError` | class | extends `KeygenError` (`code: "INVALID_DEVICE_SECRET"`) | Thrown when a device secret is not exactly 32 bytes. |
234
- | `InvalidEcdhSecretError` | class | extends `KeygenError` (`code: "INVALID_ECDH_SECRET"`) | Thrown when an ECDH shared secret passed to `deriveEddsaKeypair` is not exactly 32 bytes. |
235
- | `DerivationFailureError` | class | extends `KeygenError` (`code: "DERIVATION_FAILURE"`) | Thrown on an (unreachable in practice, ~2⁻¹²⁸) scalar-derivation failure. |
236
- | `InvalidBindingInputError` | class | extends `KeygenError` (`code: "INVALID_BINDING_INPUT"`) | Thrown when a credential-binding input is malformed. A well-formed signature that fails to verify returns `false` instead — see [`DERIVATION.md` §11](./DERIVATION.md#11-credential-binding-messages-dual-proof-of-possession). |
237
- | `InvalidMetaStealthAddressError` | class | extends `KeygenError` (`code: "INVALID_META_STEALTH_ADDRESS"`) | Thrown by `encodeMetaStealthAddress` when a public key fails validation before the write-once registry encode. |
238
- | `Hex` | type | `` `0x${string}` `` | A `0x`-prefixed lowercase hex string — the wire representation of every byte value in this package. |
239
- | `Address` | type | `Hex` | An Ethereum address (20 bytes as lowercase hex). Alias of `Hex` — documentation, not enforcement. |
240
- | `Secp256k1Keypair` | type | `{ privateKey: Hex; publicKey: Hex; uncompressedPublicKey: Hex }` | Shape of every secp256k1 keypair in the secp256k1 core. |
241
- | `DerivedKeys` | type | `{ version: "v1"; networkId: Hex; account, spending, viewing, auth: Secp256k1Keypair }` | Shape returned by `deriveKeys`. |
242
- | `EddsaKeypair` | type | `{ seed: Hex; secretScalar: bigint; publicKey: [bigint, bigint]; commitment: bigint }` | Shape returned by `deriveEddsaKeypair`, the identity key. `seed` is `Hex` for storage/transport only — decode to raw bytes before re-feeding Semaphore. |
243
- | `KeygenErrorCode` | type | `"INVALID_DEVICE_SECRET" \| "INVALID_ECDH_SECRET" \| "DERIVATION_FAILURE" \| "INVALID_BINDING_INPUT" \| "INVALID_META_STEALTH_ADDRESS"` | Discriminant for branching on `KeygenError#code`. |
244
- | `HashFn` | type | `(input: Uint8Array) => Uint8Array` | Shape of an injectable 32-byte-output hash function, as accepted by `hashToScalarWith`. |
189
+ | Export | Kind | Signature | Description |
190
+ | -------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
191
+ | `generateDeviceSecret` | function | `() => Hex` | Generates a fresh 32-byte device secret via `globalThis.crypto.getRandomValues`. |
192
+ | `deriveKeys` | function | `(deviceSecret: Uint8Array \| Hex) => DerivedKeys` | Derives the full `v2` identity (`networkId`, `memberNetworkId` + account/spending/viewing/auth keypairs) from a device secret. |
193
+ | `deriveNetworkId` | function | `(deviceSecret: Uint8Array \| Hex) => Hex` | Cheap path: derives only the account key and `networkId`, skipping the signature and stealth/auth derivation. |
194
+ | `deriveAccountKeypair` | function | `(deviceSecret: Uint8Array \| Hex) => Secp256k1Keypair` | Derives the root account keypair (`hashToScalar(DOMAIN_ACCOUNT, deviceSecret)`). |
195
+ | `signDerivationMessage` | function | `(accountPrivateKey: Hex \| Uint8Array) => Hex` | Deterministic EIP-191 personal-sign signature over the fixed domain message, using the account private key. |
196
+ | `deriveStealthKeys` | function | `(sig64: Uint8Array \| Hex) => { spending: Secp256k1Keypair; viewing: Secp256k1Keypair }` | Derives spending/viewing keypairs from the derivation-message signature (FluidKey `generateKeysFromSignature` semantics). |
197
+ | `deriveAuthKeypair` | function | `(sig64: Uint8Array \| Hex) => Secp256k1Keypair` | Derives the auth keypair (`hashToScalar(DOMAIN_AUTH, sig64)`). |
198
+ | `computeSharedSecret` | function | `({ privateKey: Hex; publicKey: Hex }) => Uint8Array` | ERC-5564 hashed shared secret: `keccak256` of the 33-byte compressed ECDH point. Symmetric — sender passes `(ephemeralPriv, viewingPub)`, recipient `(viewingPriv, ephemeralPub)`. |
199
+ | `getViewTag` | function | `(hashedSharedSecret: Uint8Array) => number` | The one-byte view tag: first byte of the hashed shared secret, published in announcements. |
200
+ | `getStealthPublicKey` | function | `({ spendingPublicKey: Hex; hashedSharedSecret: Uint8Array }) => Uint8Array` | Scheme 1 point math: `P_spend + (s_h mod n)·G`, returned as a 65-byte uncompressed key. |
201
+ | `publicKeyToAddress` | function | `(uncompressedPublicKey: Uint8Array) => Address` | Standard Ethereum address derivation: `keccak256(X \|\| Y)`, last 20 bytes. |
202
+ | `generateEphemeralKey` | function | `() => Secp256k1Keypair` | Fresh CSPRNG keypair for one load. MUST be single-use — reuse links the payments it was used for. |
203
+ | `generateStealthAddress` | function | `({ spendingPublicKey, viewingPublicKey, ephemeralPrivateKey }) => Address` | Sender side: derives the fresh stealth address for a load. |
204
+ | `generateStealthPrivateKey` | function | `({ ephemeralPublicKey, viewingPrivateKey, spendingPrivateKey }) => Hex` | Recipient side: recovers the private key controlling a stealth address — `(p_spend + s_h) mod n`. |
205
+ | `checkStealthAddress` | function | `({ stealthAddress, ephemeralPublicKey, viewTag, viewingPrivateKey, spendingPublicKey }) => boolean` | Recipient side: view-tag filter then full recompute; `true` when the announcement is addressed to these keys. Case-insensitive on `stealthAddress`. Throws on malformed announcement data — scan loops should try/catch per announcement. |
206
+ | `validateMetaStealthAddress` | function | `(metaStealthAddress: string) => boolean` | Shape + curve check for a 66-byte meta-address: `0x` + 132 hex chars, both halves valid compressed points. |
207
+ | `encodeMetaStealthAddress` | function | `({ spendingPublicKey, viewingPublicKey }) => Hex` | The 66-byte compressed `spending \|\| viewing` concatenation `IdentityRegistry` stores. Validates before emitting (the registry is write-once) — throws `InvalidMetaStealthAddressError`. |
208
+ | `encodeMetaStealthAddressUri` | function | `({ spendingPublicKey, viewingPublicKey }) => string` | The `st:eth:0x…` display form (ERC-5564 URI). Never stored onchain. |
209
+ | `decodeMetaStealthAddress` | function | `(metaStealthAddress: string) => { spendingPublicKey: Hex; viewingPublicKey: Hex }` | Splits a 66-byte meta-address (raw or `st:eth:` form) into its two keys. No validation — pair with `validateMetaStealthAddress` for untrusted input. |
210
+ | `buildAnnouncement` | function | `({ metaStealthAddress: Hex }) => { schemeId: 1; stealthAddress: Address; ephemeralPublicKey: Hex; metadata: Hex }` | Builds the ERC-5564 announcement payload for a fresh stealth address from a recipient's meta-address. Generates the single-use ephemeral key internally; `metadata` is the one-byte view tag. |
211
+ | `deriveEddsaKeypair` | function | `(ecdhSharedSecret: Uint8Array \| Hex) => EddsaKeypair` | Derives the Baby Jubjub identity keypair and its Semaphore v4 identity commitment from a hardware-computed ECDH shared secret against `EDDSA_PEER_POINT_HEX`. |
212
+ | `eddsaPeerPointBytes` | function | `() => Uint8Array` | Returns a fresh 65-byte SEC1 copy of the fixed EdDSA peer point `Q`, for platform ECDH calls. |
213
+ | `computeEddsaPeerPoint` | function | `() => Uint8Array` | Recomputes `Q` via RFC 9380 hash-to-curve at runtime — reproducibility evidence that `EDDSA_PEER_POINT_HEX` is not hand-picked. |
214
+ | `normalizeEcdhSharedSecret` | function | `(secret: Uint8Array \| Hex \| string) => Uint8Array` | Validates/normalizes an ECDH shared secret to exactly 32 bytes. |
215
+ | `computeNetworkId` | function | `(uncompressedPublicKey: Uint8Array \| Hex) => Hex` | Computes a user's bytes32 `networkId`, `keccak256(utf8(DOMAIN_NETWORK_ID) \|\| X \|\| Y)`, from an uncompressed secp256k1 public key. |
216
+ | `computeMemberNetworkId` | function | `(uncompressedPublicKey: Uint8Array \| Hex) => Hex` | Computes a member's bytes32 id, `keccak256(X \|\| Y)`; its last 20 bytes are the member's auth address. |
217
+ | `hashToScalar` | function | `(tag: string, input: Uint8Array) => Uint8Array` | The normative hash-to-secp256k1-scalar primitive (keccak256, reject-and-retry counter). |
218
+ | `hashToScalarWith` | function | `(hashFn: HashFn, tag: string, input: Uint8Array) => Uint8Array` | Same algorithm as `hashToScalar` with an injectable hash function (used to test the retry branch). |
219
+ | `isValidScalar` | function | `(scalar: Uint8Array) => boolean` | Tests whether `scalar` is a valid secp256k1 private key (32 bytes, integer in `[1, n)`). |
220
+ | `scalarToKeypair` | function | `(scalar: Uint8Array) => Secp256k1Keypair` | Expands a validated 32-byte scalar into a full keypair (compressed + uncompressed public keys). |
221
+ | `normalizeDeviceSecret` | function | `(secret: Uint8Array \| Hex \| string) => Uint8Array` | Validates/normalizes a device secret to exactly 32 bytes. |
222
+ | `normalizeConfirmationKey` | function | `(confirmationKey: Uint8Array \| Hex) => Uint8Array` | Normalizes a P-256 `confirmationKey` (65B/33B/64B) to its raw 64-byte `X \|\| Y`, validating it is on-curve. |
223
+ | `computeConfirmationKeyHash` | function | `(confirmationKey: Uint8Array \| Hex) => Hex` | Computes `confirmationKeyHash`, the RFC 7638 JWK thumbprint of a P-256 `confirmationKey`. |
224
+ | `buildDeviceBindingMessage` | function | `(nonce, networkId, holderKeyCommitment, requestCommitment) => Uint8Array` | Low-level: builds the 128-byte `nonce \|\| networkId \|\| holderKeyCommitment \|\| requestCommitment` message signed by the P-256 confirmation key. |
225
+ | `buildAccountBindingMessage` | function | `(nonce, confirmationKey, networkId) => Uint8Array` | Low-level: builds the 96-byte `nonce \|\| confirmationKeyHash \|\| networkId` message signed by the account key. |
226
+ | `buildBindingMessages` | function | `(nonce, accountUncompressedPublicKey, confirmationKey, holderKeyCommitment, requestCommitment) => { networkId: Hex; deviceBindingMessage: Uint8Array; accountBindingMessage: Uint8Array }` | Wallet-side: builds both binding messages with `networkId` derived internally from the account public key. |
227
+ | `signAccountBinding` | function | `(accountPrivateKey: Hex \| Uint8Array, nonce, confirmationKey) => { signature: Hex; networkId: Hex }` | Produces `sig_A`: the deterministic EIP-191 signature over `accountBindingMessage`, with `networkId` derived internally. |
228
+ | `verifyAccountBindingSignature` | function | `({ nonce, confirmationKey, accountPublicKey, signature }) => boolean` | Issuer-side: verifies `sig_A` against `accountPublicKey`, with `networkId` derived internally. |
229
+ | `verifyDeviceBindingSignature` | function | `({ nonce, networkId, confirmationKey, holderKeyCommitment, requestCommitment, signature }) => boolean` | Issuer-side: verifies `sig_B` (raw-compact or DER, `lowS` not enforced). `networkId` is a parameter here — the one place it cannot be derived internally; see [`DERIVATION.md` §11.5](./DERIVATION.md#115-mutual-binding-and-the-locally-derived-networkid-must). |
230
+ | `verifyCredentialBinding` | function | `({ nonce, accountPublicKey, confirmationKey, holderKeyCommitment, requestCommitment, accountSignature, deviceSignature }) => { valid: boolean; networkId: Hex }` | The full issuer gate: derives one `networkId` and checks both `sig_A` and `sig_B` against it. |
231
+ | `KEY_DERIVATION_VERSION` | constant | `"v2"` | The current derivation scheme version, also the value of `DerivedKeys.version`. |
232
+ | `FIXED_DOMAIN_MSG` | constant | `"coordination-network/identity/v1"` | Fixed message signed by `signDerivationMessage`. |
233
+ | `DOMAIN_ACCOUNT` | constant | `"coordination-network/identity/v1/account"` | Domain-separation tag for the account key. |
234
+ | `DOMAIN_AUTH` | constant | `"coordination-network/identity/v1/auth"` | Domain-separation tag for the auth key. |
235
+ | `DOMAIN_NETWORK_ID` | constant | `"coordination-network/identity/v2/network-id"` | Domain-separation tag prefixed to `X \|\| Y` for a user's `networkId`. |
236
+ | `DOMAIN_EDDSA_PEER` | constant | `"coordination-network/identity/v1/eddsa/peer"` | Domain-separation tag hashed to produce the fixed EdDSA peer point `Q` (`EDDSA_PEER_POINT_HEX`). |
237
+ | `EDDSA_PEER_POINT_HEX` | constant | `Hex` | The fixed, NUMS P-256 peer point ECDH'd against the hardware device agreement key to produce this device's `deriveEddsaKeypair` input. |
238
+ | `DOMAIN_EDDSA` | constant | `"coordination-network/identity/v1/eddsa"` | Domain-separation tag for the Baby Jubjub EdDSA seed, applied to the ECDH shared secret. |
239
+ | `BINDING_NONCE_LENGTH` | constant | `32` | Required byte length of `nonce` and `networkId` in the credential-binding messages. |
240
+ | `COMPRESSED_PUBLIC_KEY_LENGTH` | constant | `33` | SEC1-compressed secp256k1 public key length in bytes. |
241
+ | `META_STEALTH_ADDRESS_LENGTH` | constant | `66` | ERC-6538 scheme 1 meta-stealth address length: compressed spending ‖ viewing keys. |
242
+ | `META_STEALTH_URI_PREFIX` | constant | `"st:eth:"` | ERC-5564 offchain display prefix — never stored onchain. |
243
+ | `ERC_5564_SCHEME_ID` | constant | `1` | ERC-5564 scheme id for secp256k1 with view tags. |
244
+ | `KeygenError` | class | extends `Error`, has `code: KeygenErrorCode` | Base class for every error this package throws. |
245
+ | `InvalidDeviceSecretError` | class | extends `KeygenError` (`code: "INVALID_DEVICE_SECRET"`) | Thrown when a device secret is not exactly 32 bytes. |
246
+ | `InvalidEcdhSecretError` | class | extends `KeygenError` (`code: "INVALID_ECDH_SECRET"`) | Thrown when an ECDH shared secret passed to `deriveEddsaKeypair` is not exactly 32 bytes. |
247
+ | `DerivationFailureError` | class | extends `KeygenError` (`code: "DERIVATION_FAILURE"`) | Thrown on an (unreachable in practice, ~2⁻¹²⁸) scalar-derivation failure. |
248
+ | `InvalidBindingInputError` | class | extends `KeygenError` (`code: "INVALID_BINDING_INPUT"`) | Thrown when a credential-binding input is malformed. A well-formed signature that fails to verify returns `false` instead — see [`DERIVATION.md` §11](./DERIVATION.md#11-credential-binding-messages-dual-proof-of-possession). |
249
+ | `InvalidMetaStealthAddressError` | class | extends `KeygenError` (`code: "INVALID_META_STEALTH_ADDRESS"`) | Thrown by `encodeMetaStealthAddress` when a public key fails validation before the write-once registry encode. |
250
+ | `Hex` | type | `` `0x${string}` `` | A `0x`-prefixed lowercase hex string — the wire representation of every byte value in this package. |
251
+ | `Address` | type | `Hex` | An Ethereum address (20 bytes as lowercase hex). Alias of `Hex` — documentation, not enforcement. |
252
+ | `Secp256k1Keypair` | type | `{ privateKey: Hex; publicKey: Hex; uncompressedPublicKey: Hex }` | Shape of every secp256k1 keypair in the secp256k1 core. |
253
+ | `DerivedKeys` | type | `{ version: "v2"; networkId: Hex; memberNetworkId: Hex; account, spending, viewing, auth: Secp256k1Keypair }` | Shape returned by `deriveKeys`. |
254
+ | `EddsaKeypair` | type | `{ seed: Hex; secretScalar: bigint; publicKey: [bigint, bigint]; commitment: bigint }` | Shape returned by `deriveEddsaKeypair`, the identity key. `seed` is `Hex` for storage/transport only — decode to raw bytes before re-feeding Semaphore. |
255
+ | `KeygenErrorCode` | type | `"INVALID_DEVICE_SECRET" \| "INVALID_ECDH_SECRET" \| "DERIVATION_FAILURE" \| "INVALID_BINDING_INPUT" \| "INVALID_META_STEALTH_ADDRESS"` | Discriminant for branching on `KeygenError#code`. |
256
+ | `HashFn` | type | `(input: Uint8Array) => Uint8Array` | Shape of an injectable 32-byte-output hash function, as accepted by `hashToScalarWith`. |
245
257
 
246
258
  ## Security notes
247
259
 
248
- - **The device secret is not the only persistent root this library depends on.** It is the sole root of `deriveKeys`'s secp256k1 core (`account`, `spending`, `viewing`, `auth`) and the `networkId` — re-derive those with `deriveKeys` whenever you need them instead of storing them separately. The identity key (`deriveEddsaKeypair`) is different: it is rooted in the platform's non-exportable hardware confirmation key, so that key is a second persistent root this device's identity depends on — and, unlike the device secret, it is not recoverable if lost. See [`DERIVATION.md` §12.1](./DERIVATION.md#121-the-formula).
260
+ - **The device secret is not the only persistent root this library depends on.** It is the sole root of `deriveKeys`'s secp256k1 core (`account`, `spending`, `viewing`, `auth`) and the `networkId` — re-derive those with `deriveKeys` whenever you need them instead of storing them separately. The identity key (`deriveEddsaKeypair`) is different: it is rooted in the platform's non-exportable hardware device agreement key, so that key is a second persistent root this device's identity depends on — and, unlike the device secret, it is not recoverable if lost. See [`DERIVATION.md` §12.1](./DERIVATION.md#121-the-formula).
249
261
  - **Storage is out of scope for this package** — see issue E3-3. Store the device secret in the platform's secure keystore (e.g. iOS Keychain, Android Keystore, an OS-level secret store, or a hardware-backed enclave), not in plain application storage. This package's model is _store one secret, re-derive on demand_, not _derive once and persist the results_.
250
262
  - **No PII is required or accepted anywhere in this derivation chain.** The device secret is opaque random bytes; nothing in `deriveKeys` takes or produces personally identifying input.
251
263
  - **JavaScript offers no memory-hygiene guarantees, and this library does not pretend otherwise.** It never logs secret material, and it keeps no module-level state (no key, signature, or secret survives past the call that produced it) — but it cannot zero out memory or JS engine-internal copies after use. Wiping sensitive buffers, if required, is the responsibility of the platform layer that owns the device secret's storage, not of this library.