@coordination-network/identity 0.0.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/DERIVATION.md +313 -0
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/index.cjs +957 -0
- package/dist/index.js +893 -0
- package/package.json +57 -0
- package/src/account.ts +61 -0
- package/src/auth.ts +20 -0
- package/src/binding.ts +298 -0
- package/src/bytes.ts +142 -0
- package/src/confirmation-key.ts +50 -0
- package/src/constants.ts +40 -0
- package/src/derive.ts +41 -0
- package/src/device-secret.ts +24 -0
- package/src/eddsa-peer.ts +32 -0
- package/src/eddsa.ts +37 -0
- package/src/errors.ts +76 -0
- package/src/hash-to-scalar.ts +35 -0
- package/src/index.ts +53 -0
- package/src/keypair.ts +38 -0
- package/src/network-id.ts +26 -0
- package/src/stealthAddresses/assertCompressedPublicKey.ts +25 -0
- package/src/stealthAddresses/buildAnnouncement.ts +37 -0
- package/src/stealthAddresses/checkStealthAddress.ts +37 -0
- package/src/stealthAddresses/computeSharedSecret.ts +16 -0
- package/src/stealthAddresses/decodeMetaStealthAddress.ts +27 -0
- package/src/stealthAddresses/deriveStealthKeys.ts +30 -0
- package/src/stealthAddresses/encodeMetaStealthAddress.ts +19 -0
- package/src/stealthAddresses/encodeMetaStealthAddressUri.ts +8 -0
- package/src/stealthAddresses/generateEphemeralKey.ts +12 -0
- package/src/stealthAddresses/generateStealthAddress.ts +27 -0
- package/src/stealthAddresses/generateStealthPrivateKey.ts +29 -0
- package/src/stealthAddresses/getStealthPublicKey.ts +21 -0
- package/src/stealthAddresses/getViewTag.ts +7 -0
- package/src/stealthAddresses/metaStealthAddressUri.ts +7 -0
- package/src/stealthAddresses/publicKeyToAddress.ts +17 -0
- package/src/stealthAddresses/validateMetaStealthAddress.ts +19 -0
- package/src/types.ts +51 -0
- package/types/index.d.ts +499 -0
package/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# `@coordination-network/identity`
|
|
2
|
+
|
|
3
|
+
Deterministic device key derivation for the coordination network: a `networkId` and a secp256k1 core (account, spending, viewing, auth) derived from a single 32-byte device secret, plus a standalone, hardware-rooted Baby Jubjub identity key with its Semaphore commitment, and ERC-5564 scheme 1 stealth-address derivation over the spending/viewing keys.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add @coordination-network/identity
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This pulls in two runtime dependencies beyond `@noble/curves`/`@noble/hashes`: `@zk-kit/eddsa-poseidon` and `poseidon-lite`, used only by `deriveEddsaKeypair` (see below). Both are exact-pinned to the versions `@semaphore-protocol/identity` itself depends on, so that a project depending on both packages dedupes to a single copy of each — see [`DERIVATION.md` §12.6](./DERIVATION.md#126-dependencies-exact-pinned).
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { generateDeviceSecret, deriveKeys } from "@coordination-network/identity"
|
|
17
|
+
|
|
18
|
+
// Generate a fresh device secret (32 random bytes, from the platform CSPRNG).
|
|
19
|
+
// Persist ONLY this value — see "Security notes" below.
|
|
20
|
+
const deviceSecret = generateDeviceSecret()
|
|
21
|
+
|
|
22
|
+
// Deterministically re-derive the full key bundle from it, any time.
|
|
23
|
+
const keys = deriveKeys(deviceSecret)
|
|
24
|
+
|
|
25
|
+
console.log(keys.networkId) // 0x-prefixed bytes32, ends with the account's Ethereum address
|
|
26
|
+
console.log(keys.account.publicKey) // 33-byte compressed secp256k1 public key
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Calling `deriveKeys` twice with the same `deviceSecret` always returns byte-for-byte identical keys — nothing else needs to be stored.
|
|
30
|
+
|
|
31
|
+
If a caller only needs the `networkId` (e.g. to look up or announce a device without touching its private keys), `deriveNetworkId` skips the signature and stealth/auth derivation steps:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { deriveNetworkId } from "@coordination-network/identity"
|
|
35
|
+
|
|
36
|
+
const networkId = deriveNetworkId(deviceSecret) // === deriveKeys(deviceSecret).networkId
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Credential binding (dual proof-of-possession)
|
|
40
|
+
|
|
41
|
+
Credential issuance binds two keys to one session: the device's secp256k1 account key and a hardware-backed P-256 `confirmationKey` (Secure Enclave / Android Keystore / WebCrypto). See [`DERIVATION.md` §11](./DERIVATION.md#11-credential-binding-messages-dual-proof-of-possession) for the full normative scheme.
|
|
42
|
+
|
|
43
|
+
**Wallet side** — build the messages, get the hardware key to sign its message, then sign the account message:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { buildBindingMessages, signAccountBinding } from "@coordination-network/identity"
|
|
47
|
+
|
|
48
|
+
// `nonce` comes from the issuer (e.g. a fresh 32-byte session challenge).
|
|
49
|
+
// `confirmationKey` is the device's P-256 public key (any of 65B/33B/64B encodings).
|
|
50
|
+
const { deviceBindingMessage, accountBindingMessage } = buildBindingMessages(
|
|
51
|
+
nonce,
|
|
52
|
+
keys.account.uncompressedPublicKey,
|
|
53
|
+
confirmationKey
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// The platform signs deviceBindingMessage with the hardware-backed P-256 key
|
|
57
|
+
// (SecKeyCreateSignature / Android Keystore / crypto.subtle.sign) — this
|
|
58
|
+
// library never signs with the P-256 key itself.
|
|
59
|
+
const sigB = await platformSignWithSecureEnclave(deviceBindingMessage)
|
|
60
|
+
|
|
61
|
+
// The account key signs accountBindingMessage (EIP-191, deterministic).
|
|
62
|
+
const { signature: sigA } = signAccountBinding(keys.account.privateKey, nonce, confirmationKey)
|
|
63
|
+
|
|
64
|
+
// Send `nonce`, `confirmationKey`, `sigA`, `sigB` to the issuer.
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Issuer side** — a single call verifies both proofs against one derived `networkId`:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { verifyCredentialBinding } from "@coordination-network/identity"
|
|
71
|
+
|
|
72
|
+
const { valid, networkId } = verifyCredentialBinding({
|
|
73
|
+
nonce,
|
|
74
|
+
accountPublicKey, // the account's public key, as claimed by the wallet
|
|
75
|
+
confirmationKey,
|
|
76
|
+
accountSignature: sigA,
|
|
77
|
+
deviceSignature: sigB
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (!valid) {
|
|
81
|
+
throw new Error("credential binding failed verification")
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Baby Jubjub identity key and Semaphore commitment
|
|
86
|
+
|
|
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.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { deriveEddsaKeypair, eddsaPeerPointBytes } from "@coordination-network/identity"
|
|
91
|
+
|
|
92
|
+
// The platform layer computes this natively, inside the secure element —
|
|
93
|
+
// e.g. Android Keystore KeyAgreement("ECDH") against the pinned peer point:
|
|
94
|
+
// ecdhSharedSecret = await keystore.ecdh(alias, base64(eddsaPeerPointBytes()))
|
|
95
|
+
const ecdhSharedSecret = await platformEcdh(confirmationKeyAlias, eddsaPeerPointBytes())
|
|
96
|
+
|
|
97
|
+
const eddsa = deriveEddsaKeypair(ecdhSharedSecret)
|
|
98
|
+
|
|
99
|
+
console.log(eddsa.commitment) // bigint — the Semaphore v4 identity commitment
|
|
100
|
+
console.log(eddsa.publicKey) // [bigint, bigint] — the Baby Jubjub public key
|
|
101
|
+
```
|
|
102
|
+
|
|
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).
|
|
104
|
+
|
|
105
|
+
**`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
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { Identity } from "@semaphore-protocol/identity"
|
|
109
|
+
|
|
110
|
+
function hexToBytes(hex: `0x${string}`): Uint8Array {
|
|
111
|
+
const body = hex.slice(2)
|
|
112
|
+
return Uint8Array.from(Buffer.from(body, "hex"))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Correct: decode the hex seed to raw bytes first.
|
|
116
|
+
const identity = new Identity(hexToBytes(eddsa.seed))
|
|
117
|
+
identity.commitment === eddsa.commitment // true
|
|
118
|
+
|
|
119
|
+
// WRONG — silently derives a different identity, does not throw:
|
|
120
|
+
// new Identity(eddsa.seed).commitment !== eddsa.commitment
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Stealth addresses (ERC-5564 scheme 1)
|
|
124
|
+
|
|
125
|
+
A sender derives a fresh, unlinkable Ethereum address for every payment from the recipient's published meta-address. Only the recipient's viewing key can detect it, and only the spending key controls it. Formulas and byte-level choices are specified in [`specs/05-stealth-addresses.md`](../../specs/05-stealth-addresses.md), and conformance is cross-checked against ScopeLift's `stealth-address-sdk` in `tests/stealth-crosschecks.test.ts`.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import {
|
|
129
|
+
encodeMetaStealthAddress,
|
|
130
|
+
buildAnnouncement,
|
|
131
|
+
generateEphemeralKey,
|
|
132
|
+
generateStealthAddress,
|
|
133
|
+
checkStealthAddress,
|
|
134
|
+
computeSharedSecret,
|
|
135
|
+
getViewTag,
|
|
136
|
+
generateStealthPrivateKey
|
|
137
|
+
} from "@coordination-network/identity"
|
|
138
|
+
|
|
139
|
+
// Recipient can publish the 66-byte meta-address (this is what IdentityRegistry stores).
|
|
140
|
+
const metaStealthAddress = encodeMetaStealthAddress({
|
|
141
|
+
spendingPublicKey: keys.spending.publicKey,
|
|
142
|
+
viewingPublicKey: keys.viewing.publicKey
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Fresh ephemeral key per load, derive the stealth address, announce.
|
|
146
|
+
const ephemeral = generateEphemeralKey()
|
|
147
|
+
const stealthAddress = generateStealthAddress({
|
|
148
|
+
spendingPublicKey: keys.spending.publicKey,
|
|
149
|
+
viewingPublicKey: keys.viewing.publicKey,
|
|
150
|
+
ephemeralPrivateKey: ephemeral.privateKey
|
|
151
|
+
})
|
|
152
|
+
const viewTag = getViewTag(computeSharedSecret({ privateKey: ephemeral.privateKey, publicKey: keys.viewing.publicKey }))
|
|
153
|
+
// announce (stealthAddress, ephemeral.publicKey, viewTag) via the ERC-5564 Announcer
|
|
154
|
+
|
|
155
|
+
// Or build the whole announcement payload in one call (ephemeral key generated internally):
|
|
156
|
+
const announcement = buildAnnouncement({ metaStealthAddress })
|
|
157
|
+
|
|
158
|
+
// Receiver can scan announcements with the viewing key
|
|
159
|
+
const isOwned = checkStealthAddress({
|
|
160
|
+
stealthAddress,
|
|
161
|
+
ephemeralPublicKey: ephemeral.publicKey,
|
|
162
|
+
viewTag,
|
|
163
|
+
viewingPrivateKey: keys.viewing.privateKey,
|
|
164
|
+
spendingPublicKey: keys.spending.publicKey
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Recover the private key controlling the stealth address.
|
|
168
|
+
const stealthPrivateKey = generateStealthPrivateKey({
|
|
169
|
+
ephemeralPublicKey: ephemeral.publicKey,
|
|
170
|
+
viewingPrivateKey: keys.viewing.privateKey,
|
|
171
|
+
spendingPrivateKey: keys.spending.privateKey
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## API reference
|
|
176
|
+
|
|
177
|
+
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
|
+
|
|
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`. |
|
|
245
|
+
|
|
246
|
+
## Security notes
|
|
247
|
+
|
|
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).
|
|
249
|
+
- **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
|
+
- **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
|
+
- **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.
|
|
252
|
+
- **Ephemeral keys MUST be fresh per load.** Nothing in the API prevents reusing an `ephemeralPrivateKey` across `generateStealthAddress` calls, but reuse derives the same stealth address twice, publicly linking those loads.
|
|
253
|
+
|
|
254
|
+
See [`DERIVATION.md`](./DERIVATION.md) for the full normative derivation spec, including rationale for each design choice and the conformance test vectors.
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT, see [`LICENSE`](./LICENSE).
|