@cardanowall/crypto-core 0.1.0 → 0.3.0
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 +9 -9
- package/dist/canonical-DHeJLYDR.d.cts +7 -0
- package/dist/canonical-DHeJLYDR.d.ts +7 -0
- package/dist/cbor.cjs +1 -1
- package/dist/cbor.cjs.map +1 -1
- package/dist/cbor.d.cts +2 -6
- package/dist/cbor.d.ts +2 -6
- package/dist/cbor.js +1 -1
- package/dist/cbor.js.map +1 -1
- package/dist/cose.cjs +11 -11
- package/dist/cose.cjs.map +1 -1
- package/dist/cose.d.cts +7 -7
- package/dist/cose.d.ts +7 -7
- package/dist/cose.js +9 -9
- package/dist/cose.js.map +1 -1
- package/dist/index.cjs +291 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +270 -118
- package/dist/index.js.map +1 -1
- package/dist/kem.cjs.map +1 -1
- package/dist/kem.js.map +1 -1
- package/dist/merkle.cjs +1 -1
- package/dist/merkle.cjs.map +1 -1
- package/dist/merkle.js +1 -1
- package/dist/merkle.js.map +1 -1
- package/dist/sealed-poe.cjs +299 -118
- package/dist/sealed-poe.cjs.map +1 -1
- package/dist/sealed-poe.d.cts +55 -3
- package/dist/sealed-poe.d.ts +55 -3
- package/dist/sealed-poe.js +281 -118
- package/dist/sealed-poe.js.map +1 -1
- package/dist/seed-derive.cjs.map +1 -1
- package/dist/seed-derive.js.map +1 -1
- package/package.json +5 -5
package/dist/seed-derive.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/kdf/hkdf.ts","../src/kem/mlkem768x25519.ts","../src/kem/x25519.ts","../src/sig/ed25519.ts","../src/seed-derive/errors.ts","../src/seed-derive/derive.ts"],"names":["hkdf","sha256","XWing","x25519","ed","sha512"],"mappings":"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","file":"seed-derive.cjs","sourcesContent":["import { hkdf } from '@noble/hashes/hkdf.js';\nimport { sha256 } from '@noble/hashes/sha2.js';\n\nexport interface HkdfSha256Opts {\n readonly ikm: Uint8Array;\n readonly salt: Uint8Array;\n readonly info: Uint8Array;\n readonly length: number;\n}\n\nexport function hkdfSha256(opts: HkdfSha256Opts): Uint8Array {\n return hkdf(sha256, opts.ikm, opts.salt, opts.info, opts.length);\n}\n","import { XWing } from '@noble/post-quantum/hybrid.js';\n\n// X-Wing (ML-KEM-768 + X25519) hybrid KEM per draft-connolly-cfrg-xwing-kem-06.\n// `XWing` is @noble/post-quantum's alias for `ml_kem768_x25519`. We expose it\n// through opts-object wrappers that pin the wire lengths and map noble's field\n// names onto the project's vocabulary.\n//\n// Unlike the bare X25519 KEM, there is no contributory-behaviour rejection to\n// translate: X-Wing combines the ML-KEM and X25519 shared secrets through a\n// SHA3-256 combiner that also binds the X25519 ephemeral and recipient public\n// keys, and ML-KEM's implicit rejection already yields a constant-work\n// pseudorandom secret on a malformed ciphertext. Decapsulation therefore never\n// throws on attacker-supplied wire data — a wrong shared secret is the correct,\n// indistinguishable failure mode, and callers MUST treat it as a non-match\n// rather than expecting an exception.\n\nexport const MLKEM768X25519_PUBLIC_KEY_LENGTH = 1216 as const;\nexport const MLKEM768X25519_ENC_LENGTH = 1120 as const;\nexport const MLKEM768X25519_SHARED_SECRET_LENGTH = 32 as const;\nexport const MLKEM768X25519_SEED_LENGTH = 32 as const;\nexport const MLKEM768X25519_ESEED_LENGTH = 64 as const;\n\nexport interface Mlkem768X25519KeyPair {\n // The 32-byte root seed IS the secret key in draft-06: the ML-KEM coins and\n // the X25519 scalar are re-expanded from it via SHAKE-256 at decapsulation.\n readonly secretSeed: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface Mlkem768X25519EncapsulateOpts {\n readonly publicKey: Uint8Array;\n // Optional 64-byte encapsulation randomness (msgRand). When supplied the\n // ciphertext and shared secret are fully deterministic; a 32-byte value is\n // rejected by noble, so we pin the length here too.\n readonly eseed?: Uint8Array;\n}\n\nexport interface Mlkem768X25519Encapsulation {\n readonly enc: Uint8Array;\n readonly ss: Uint8Array;\n}\n\nexport interface Mlkem768X25519DecapsulateOpts {\n readonly secretSeed: Uint8Array;\n readonly enc: Uint8Array;\n}\n\nexport function mlkem768x25519Keygen(seed: Uint8Array): Mlkem768X25519KeyPair {\n if (seed.length !== MLKEM768X25519_SEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 seed must be ${MLKEM768X25519_SEED_LENGTH} bytes, got ${seed.length}`,\n );\n }\n const { secretKey, publicKey } = XWing.keygen(seed);\n return { secretSeed: secretKey, publicKey };\n}\n\nexport function mlkem768x25519Encapsulate(\n opts: Mlkem768X25519EncapsulateOpts,\n): Mlkem768X25519Encapsulation {\n if (opts.publicKey.length !== MLKEM768X25519_PUBLIC_KEY_LENGTH) {\n throw new Error(\n `mlkem768x25519 public key must be ${MLKEM768X25519_PUBLIC_KEY_LENGTH} bytes, got ${opts.publicKey.length}`,\n );\n }\n if (opts.eseed !== undefined && opts.eseed.length !== MLKEM768X25519_ESEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 eseed must be ${MLKEM768X25519_ESEED_LENGTH} bytes, got ${opts.eseed.length}`,\n );\n }\n const { cipherText, sharedSecret } = XWing.encapsulate(opts.publicKey, opts.eseed);\n return { enc: cipherText, ss: sharedSecret };\n}\n\nexport function mlkem768x25519Decapsulate(opts: Mlkem768X25519DecapsulateOpts): Uint8Array {\n // Pre-check both lengths before calling noble: decapsulation must perform a\n // constant amount of work for any caller-supplied ciphertext (implicit\n // rejection), which requires the inputs to be the exact expected sizes.\n if (opts.secretSeed.length !== MLKEM768X25519_SEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 secret seed must be ${MLKEM768X25519_SEED_LENGTH} bytes, got ${opts.secretSeed.length}`,\n );\n }\n if (opts.enc.length !== MLKEM768X25519_ENC_LENGTH) {\n throw new Error(\n `mlkem768x25519 enc must be ${MLKEM768X25519_ENC_LENGTH} bytes, got ${opts.enc.length}`,\n );\n }\n // noble's signature is decapsulate(cipherText, secretKey) — ciphertext first.\n return XWing.decapsulate(opts.enc, opts.secretSeed);\n}\n","import { x25519 } from '@noble/curves/ed25519.js';\n\n// RFC 7748 §6.1 contributory-behaviour rejection: a small-order (low-order)\n// Montgomery `u` coordinate makes the X25519 shared secret all-zero, which\n// @noble/curves refuses with `Error: invalid private or public key received`.\n// We rethrow that as a *typed* error so callers can distinguish a structurally\n// valid-but-malicious peer public key (a property of attacker-supplied wire\n// data — trial-decrypt MUST treat the slot as a non-match, not crash) from\n// genuine caller misuse such as a wrong-length key (which @noble raises as a\n// RangeError and which we deliberately let propagate untouched).\nexport class X25519LowOrderPointError extends Error {\n readonly code = 'X25519_LOW_ORDER_POINT' as const;\n constructor(options?: { cause?: unknown }) {\n super('x25519 ECDH rejected: peer public key is a small-order point', options);\n this.name = 'X25519LowOrderPointError';\n }\n}\n\n// @noble/curves v2 signals a small-order/all-zero shared secret with this exact\n// message. Matching on it (rather than the broad Error class) keeps unrelated\n// failures — e.g. a future internal assertion — surfacing as themselves.\nconst NOBLE_LOW_ORDER_MESSAGE = 'invalid private or public key received';\n\nexport interface X25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface X25519PublicKeyOpts {\n readonly secretKey: Uint8Array;\n}\n\nexport interface X25519EcdhOpts {\n readonly secretKey: Uint8Array;\n readonly theirPublicKey: Uint8Array;\n}\n\nexport function x25519Keygen(): X25519KeyPair {\n return x25519.keygen();\n}\n\nexport function x25519PublicKey(opts: X25519PublicKeyOpts): Uint8Array {\n return x25519.getPublicKey(opts.secretKey);\n}\n\nexport function x25519Ecdh(opts: X25519EcdhOpts): Uint8Array {\n try {\n return x25519.getSharedSecret(opts.secretKey, opts.theirPublicKey);\n } catch (e) {\n // Translate ONLY the contributory-check rejection into our typed error.\n // A wrong-length key throws a RangeError from @noble's length assertion;\n // that is caller misuse, not malicious wire data, so it must propagate.\n if (e instanceof Error && e.message === NOBLE_LOW_ORDER_MESSAGE) {\n throw new X25519LowOrderPointError({ cause: e });\n }\n throw e;\n }\n}\n","import * as ed from '@noble/ed25519';\nimport { sha512 } from '@noble/hashes/sha2.js';\n\ned.hashes.sha512 = sha512;\n\n// Ed25519 group order L (= 2^252 + 27742317777372353535851937790883648493).\nconst L = ed.Point.CURVE().n;\n\nexport interface SignEd25519Opts {\n readonly seed: Uint8Array;\n readonly message: Uint8Array;\n}\n\nexport interface VerifyEd25519Opts {\n readonly publicKey: Uint8Array;\n readonly message: Uint8Array;\n readonly signature: Uint8Array;\n}\n\nexport interface GetPublicKeyEd25519Opts {\n readonly seed: Uint8Array;\n}\n\nexport function signEd25519(opts: SignEd25519Opts): Uint8Array {\n return ed.sign(opts.message, opts.seed);\n}\n\n// Little-endian 32-byte scalar → bigint.\nfunction leBytesToBigInt(bytes: Uint8Array): bigint {\n let value = 0n;\n for (let i = bytes.length - 1; i >= 0; i--) {\n value = (value << 8n) | BigInt(bytes[i]!);\n }\n return value;\n}\n\n// Strict (non-cofactored) Ed25519 verification per RFC 8032 §5.1.7, matching\n// libsodium/PyNaCl `crypto_sign_verify_detached` and ed25519-dalek\n// `verify_strict`. The cofactor-less check rejects every small-order /\n// torsion-component edge case in the C2SP/CCTV corpus, which noble's\n// `{ zip215: false }` mode does NOT (it remains cofactored: it checks\n// `[8]([S]B - [k]A - R) == 0`, accepting torsion components).\n//\n// The verification equation is the unscaled `[S]B == R + [k]A`, rewritten as\n// `[S]B - [k]A - R == identity`. We reject S >= L (non-canonical scalar) and\n// any small-order A or R up front, so a torsion component can never be smuggled\n// through the cofactor multiplication the cofactored variant performs.\nexport function verifyEd25519(opts: VerifyEd25519Opts): boolean {\n const { signature, message, publicKey } = opts;\n if (signature.length !== 64 || publicKey.length !== 32) return false;\n\n // S = LE(sig[32..64]); reject if not a canonical scalar (S >= L).\n const S = leBytesToBigInt(signature.subarray(32, 64));\n if (S >= L) return false;\n\n // Decode A (public key) and R (sig[0..32]) with the canonical (non-zip215)\n // point encoding; a non-canonical encoding throws and rejects.\n let A: ed.Point;\n let R: ed.Point;\n try {\n A = ed.Point.fromBytes(publicKey);\n R = ed.Point.fromBytes(signature.subarray(0, 32));\n } catch {\n return false;\n }\n\n // Reject small-order (cofactor-torsion) A or R: this is exactly the strictness\n // that distinguishes verify_strict from the cofactored check.\n if (A.isSmallOrder() || R.isSmallOrder()) return false;\n\n // k = SHA-512(R || A || M) reduced mod L.\n const k =\n leBytesToBigInt(ed.hash(concatBytes(signature.subarray(0, 32), publicKey, message))) % L;\n\n // Accept iff [S]B - [k]A - R == identity. `multiplyUnsafe` returns the\n // identity for a 0 scalar, but guard explicitly to avoid relying on that.\n const sB = S === 0n ? ed.Point.ZERO : ed.Point.BASE.multiplyUnsafe(S);\n const kA = k === 0n ? ed.Point.ZERO : A.multiplyUnsafe(k);\n return sB.subtract(kA).subtract(R).is0();\n}\n\nfunction concatBytes(...parts: Uint8Array[]): Uint8Array {\n let total = 0;\n for (const p of parts) total += p.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const p of parts) {\n out.set(p, offset);\n offset += p.length;\n }\n return out;\n}\n\nexport function getPublicKeyEd25519(opts: GetPublicKeyEd25519Opts): Uint8Array {\n return ed.getPublicKey(opts.seed);\n}\n","export type SeedDeriveErrorCode = 'INVALID_SEED_LENGTH';\n\nexport class SeedDeriveError extends Error {\n readonly code: SeedDeriveErrorCode;\n\n constructor(code: SeedDeriveErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'SeedDeriveError';\n this.code = code;\n }\n}\n","import { hkdfSha256 } from '../kdf/hkdf';\nimport { mlkem768x25519Keygen } from '../kem/mlkem768x25519';\nimport { x25519PublicKey } from '../kem/x25519';\nimport { getPublicKeyEd25519 } from '../sig/ed25519';\n\nimport { SeedDeriveError } from './errors';\n\n// HKDF info constants for the long-term identity keypairs.\n// These literal byte sequences are part of the on-wire protocol; every\n// conformant implementation MUST hash against these exact ASCII bytes (the\n// Python parity twin pins the identical labels).\nexport const INFO_ED25519: Uint8Array = new TextEncoder().encode('cardano-poe-ed25519-v1');\nexport const INFO_X25519: Uint8Array = new TextEncoder().encode('cardano-poe-x25519-v1');\nexport const INFO_MLKEM768X25519: Uint8Array = new TextEncoder().encode(\n 'cardano-poe-mlkem768x25519-v1',\n);\n\nif (INFO_ED25519.length !== 22) {\n throw new Error('INFO_ED25519 byte-length invariant violated (expected 22)');\n}\nif (INFO_X25519.length !== 21) {\n throw new Error('INFO_X25519 byte-length invariant violated (expected 21)');\n}\nif (INFO_MLKEM768X25519.length !== 29) {\n throw new Error('INFO_MLKEM768X25519 byte-length invariant violated (expected 29)');\n}\n\nconst EMPTY_SALT: Uint8Array = new Uint8Array(0);\nconst SEED_LENGTH = 32;\nconst DERIVED_LENGTH = 32;\n\nexport interface DerivedEd25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface DerivedX25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface DerivedMlKem768X25519KeyPair {\n readonly secretSeed: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nfunction assertSeedLength(seed: Uint8Array): void {\n if (seed.length !== SEED_LENGTH) {\n throw new SeedDeriveError(\n 'INVALID_SEED_LENGTH',\n `seed must be exactly 32 bytes, got ${seed.length}`,\n );\n }\n}\n\nexport function deriveEd25519KeypairFromSeed(seed: Uint8Array): DerivedEd25519KeyPair {\n assertSeedLength(seed);\n const secretKey = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_ED25519,\n length: DERIVED_LENGTH,\n });\n const publicKey = getPublicKeyEd25519({ seed: secretKey });\n return { secretKey, publicKey };\n}\n\nexport function deriveX25519KeypairFromSeed(seed: Uint8Array): DerivedX25519KeyPair {\n assertSeedLength(seed);\n const secretKey = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_X25519,\n length: DERIVED_LENGTH,\n });\n const publicKey = x25519PublicKey({ secretKey });\n return { secretKey, publicKey };\n}\n\nexport function deriveMlKem768X25519KeypairFromSeed(\n seed: Uint8Array,\n): DerivedMlKem768X25519KeyPair {\n assertSeedLength(seed);\n // The 32-byte HKDF output IS the X-Wing root seed: keygen re-expands the\n // ML-KEM coins and the X25519 scalar from it, so the derived keypair's\n // secretSeed equals this value.\n const xwingSeed = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_MLKEM768X25519,\n length: DERIVED_LENGTH,\n });\n return mlkem768x25519Keygen(xwingSeed);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/kdf/hkdf.ts","../src/kem/mlkem768x25519.ts","../src/kem/x25519.ts","../src/sig/ed25519.ts","../src/seed-derive/errors.ts","../src/seed-derive/derive.ts"],"names":["hkdf","sha256","XWing","x25519","ed","sha512"],"mappings":"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","file":"seed-derive.cjs","sourcesContent":["import { hkdf } from '@noble/hashes/hkdf.js';\nimport { sha256 } from '@noble/hashes/sha2.js';\n\nexport interface HkdfSha256Opts {\n readonly ikm: Uint8Array;\n readonly salt: Uint8Array;\n readonly info: Uint8Array;\n readonly length: number;\n}\n\nexport function hkdfSha256(opts: HkdfSha256Opts): Uint8Array {\n return hkdf(sha256, opts.ikm, opts.salt, opts.info, opts.length);\n}\n","import { XWing } from '@noble/post-quantum/hybrid.js';\n\n// X-Wing (ML-KEM-768 + X25519) hybrid KEM per draft-connolly-cfrg-xwing-kem-10.\n// `XWing` is @noble/post-quantum's alias for `ml_kem768_x25519`. We expose it\n// through opts-object wrappers that pin the wire lengths and map noble's field\n// names onto the project's vocabulary.\n//\n// Unlike the bare X25519 KEM, there is no contributory-behaviour rejection to\n// translate: X-Wing combines the ML-KEM and X25519 shared secrets through a\n// SHA3-256 combiner that also binds the X25519 ephemeral and recipient public\n// keys, and ML-KEM's implicit rejection already yields a constant-work\n// pseudorandom secret on a malformed ciphertext. Decapsulation therefore never\n// throws on attacker-supplied wire data — a wrong shared secret is the correct,\n// indistinguishable failure mode, and callers MUST treat it as a non-match\n// rather than expecting an exception.\n\nexport const MLKEM768X25519_PUBLIC_KEY_LENGTH = 1216 as const;\nexport const MLKEM768X25519_ENC_LENGTH = 1120 as const;\nexport const MLKEM768X25519_SHARED_SECRET_LENGTH = 32 as const;\nexport const MLKEM768X25519_SEED_LENGTH = 32 as const;\nexport const MLKEM768X25519_ESEED_LENGTH = 64 as const;\n\nexport interface Mlkem768X25519KeyPair {\n // The 32-byte root seed IS the secret key: the ML-KEM coins and the X25519\n // scalar are re-expanded from it via SHAKE-256 at decapsulation. (Later X-Wing\n // drafts also define an optional expanded decapsulation-key form; we keep the\n // seed-only key, which the draft-10 Appendix C vectors still pin.)\n readonly secretSeed: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface Mlkem768X25519EncapsulateOpts {\n readonly publicKey: Uint8Array;\n // Optional 64-byte encapsulation randomness (msgRand). When supplied the\n // ciphertext and shared secret are fully deterministic; a 32-byte value is\n // rejected by noble, so we pin the length here too.\n readonly eseed?: Uint8Array;\n}\n\nexport interface Mlkem768X25519Encapsulation {\n readonly enc: Uint8Array;\n readonly ss: Uint8Array;\n}\n\nexport interface Mlkem768X25519DecapsulateOpts {\n readonly secretSeed: Uint8Array;\n readonly enc: Uint8Array;\n}\n\nexport function mlkem768x25519Keygen(seed: Uint8Array): Mlkem768X25519KeyPair {\n if (seed.length !== MLKEM768X25519_SEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 seed must be ${MLKEM768X25519_SEED_LENGTH} bytes, got ${seed.length}`,\n );\n }\n const { secretKey, publicKey } = XWing.keygen(seed);\n return { secretSeed: secretKey, publicKey };\n}\n\nexport function mlkem768x25519Encapsulate(\n opts: Mlkem768X25519EncapsulateOpts,\n): Mlkem768X25519Encapsulation {\n if (opts.publicKey.length !== MLKEM768X25519_PUBLIC_KEY_LENGTH) {\n throw new Error(\n `mlkem768x25519 public key must be ${MLKEM768X25519_PUBLIC_KEY_LENGTH} bytes, got ${opts.publicKey.length}`,\n );\n }\n if (opts.eseed !== undefined && opts.eseed.length !== MLKEM768X25519_ESEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 eseed must be ${MLKEM768X25519_ESEED_LENGTH} bytes, got ${opts.eseed.length}`,\n );\n }\n const { cipherText, sharedSecret } = XWing.encapsulate(opts.publicKey, opts.eseed);\n return { enc: cipherText, ss: sharedSecret };\n}\n\nexport function mlkem768x25519Decapsulate(opts: Mlkem768X25519DecapsulateOpts): Uint8Array {\n // Pre-check both lengths before calling noble: decapsulation must perform a\n // constant amount of work for any caller-supplied ciphertext (implicit\n // rejection), which requires the inputs to be the exact expected sizes.\n if (opts.secretSeed.length !== MLKEM768X25519_SEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 secret seed must be ${MLKEM768X25519_SEED_LENGTH} bytes, got ${opts.secretSeed.length}`,\n );\n }\n if (opts.enc.length !== MLKEM768X25519_ENC_LENGTH) {\n throw new Error(\n `mlkem768x25519 enc must be ${MLKEM768X25519_ENC_LENGTH} bytes, got ${opts.enc.length}`,\n );\n }\n // noble's signature is decapsulate(cipherText, secretKey) — ciphertext first.\n return XWing.decapsulate(opts.enc, opts.secretSeed);\n}\n","import { x25519 } from '@noble/curves/ed25519.js';\n\n// RFC 7748 §6.1 contributory-behaviour rejection: a small-order (low-order)\n// Montgomery `u` coordinate makes the X25519 shared secret all-zero, which\n// @noble/curves refuses with `Error: invalid private or public key received`.\n// We rethrow that as a *typed* error so callers can distinguish a structurally\n// valid-but-malicious peer public key (a property of attacker-supplied wire\n// data — trial-decrypt MUST treat the slot as a non-match, not crash) from\n// genuine caller misuse such as a wrong-length key (which @noble raises as a\n// RangeError and which we deliberately let propagate untouched).\nexport class X25519LowOrderPointError extends Error {\n readonly code = 'X25519_LOW_ORDER_POINT' as const;\n constructor(options?: { cause?: unknown }) {\n super('x25519 ECDH rejected: peer public key is a small-order point', options);\n this.name = 'X25519LowOrderPointError';\n }\n}\n\n// @noble/curves v2 signals a small-order/all-zero shared secret with this exact\n// message. Matching on it (rather than the broad Error class) keeps unrelated\n// failures — e.g. a future internal assertion — surfacing as themselves.\nconst NOBLE_LOW_ORDER_MESSAGE = 'invalid private or public key received';\n\nexport interface X25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface X25519PublicKeyOpts {\n readonly secretKey: Uint8Array;\n}\n\nexport interface X25519EcdhOpts {\n readonly secretKey: Uint8Array;\n readonly theirPublicKey: Uint8Array;\n}\n\nexport function x25519Keygen(): X25519KeyPair {\n return x25519.keygen();\n}\n\nexport function x25519PublicKey(opts: X25519PublicKeyOpts): Uint8Array {\n return x25519.getPublicKey(opts.secretKey);\n}\n\nexport function x25519Ecdh(opts: X25519EcdhOpts): Uint8Array {\n try {\n return x25519.getSharedSecret(opts.secretKey, opts.theirPublicKey);\n } catch (e) {\n // Translate ONLY the contributory-check rejection into our typed error.\n // A wrong-length key throws a RangeError from @noble's length assertion;\n // that is caller misuse, not malicious wire data, so it must propagate.\n if (e instanceof Error && e.message === NOBLE_LOW_ORDER_MESSAGE) {\n throw new X25519LowOrderPointError({ cause: e });\n }\n throw e;\n }\n}\n","import * as ed from '@noble/ed25519';\nimport { sha512 } from '@noble/hashes/sha2.js';\n\ned.hashes.sha512 = sha512;\n\n// Ed25519 group order L (= 2^252 + 27742317777372353535851937790883648493).\nconst L = ed.Point.CURVE().n;\n\nexport interface SignEd25519Opts {\n readonly seed: Uint8Array;\n readonly message: Uint8Array;\n}\n\nexport interface VerifyEd25519Opts {\n readonly publicKey: Uint8Array;\n readonly message: Uint8Array;\n readonly signature: Uint8Array;\n}\n\nexport interface GetPublicKeyEd25519Opts {\n readonly seed: Uint8Array;\n}\n\nexport function signEd25519(opts: SignEd25519Opts): Uint8Array {\n return ed.sign(opts.message, opts.seed);\n}\n\n// Little-endian 32-byte scalar → bigint.\nfunction leBytesToBigInt(bytes: Uint8Array): bigint {\n let value = 0n;\n for (let i = bytes.length - 1; i >= 0; i--) {\n value = (value << 8n) | BigInt(bytes[i]!);\n }\n return value;\n}\n\n// Strict (non-cofactored) Ed25519 verification per RFC 8032 §5.1.7, matching\n// libsodium/PyNaCl `crypto_sign_verify_detached` and ed25519-dalek\n// `verify_strict`. The cofactor-less check rejects every small-order /\n// torsion-component edge case in the C2SP/CCTV corpus, which noble's\n// `{ zip215: false }` mode does NOT (it remains cofactored: it checks\n// `[8]([S]B - [k]A - R) == 0`, accepting torsion components).\n//\n// The verification equation is the unscaled `[S]B == R + [k]A`, rewritten as\n// `[S]B - [k]A - R == identity`. We reject S >= L (non-canonical scalar) and\n// any small-order A or R up front, so a torsion component can never be smuggled\n// through the cofactor multiplication the cofactored variant performs.\nexport function verifyEd25519(opts: VerifyEd25519Opts): boolean {\n const { signature, message, publicKey } = opts;\n if (signature.length !== 64 || publicKey.length !== 32) return false;\n\n // S = LE(sig[32..64]); reject if not a canonical scalar (S >= L).\n const S = leBytesToBigInt(signature.subarray(32, 64));\n if (S >= L) return false;\n\n // Decode A (public key) and R (sig[0..32]) with the canonical (non-zip215)\n // point encoding; a non-canonical encoding throws and rejects.\n let A: ed.Point;\n let R: ed.Point;\n try {\n A = ed.Point.fromBytes(publicKey);\n R = ed.Point.fromBytes(signature.subarray(0, 32));\n } catch {\n return false;\n }\n\n // Reject small-order (cofactor-torsion) A or R: this is exactly the strictness\n // that distinguishes verify_strict from the cofactored check.\n if (A.isSmallOrder() || R.isSmallOrder()) return false;\n\n // k = SHA-512(R || A || M) reduced mod L.\n const k =\n leBytesToBigInt(ed.hash(concatBytes(signature.subarray(0, 32), publicKey, message))) % L;\n\n // Accept iff [S]B - [k]A - R == identity. `multiplyUnsafe` returns the\n // identity for a 0 scalar, but guard explicitly to avoid relying on that.\n const sB = S === 0n ? ed.Point.ZERO : ed.Point.BASE.multiplyUnsafe(S);\n const kA = k === 0n ? ed.Point.ZERO : A.multiplyUnsafe(k);\n return sB.subtract(kA).subtract(R).is0();\n}\n\nfunction concatBytes(...parts: Uint8Array[]): Uint8Array {\n let total = 0;\n for (const p of parts) total += p.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const p of parts) {\n out.set(p, offset);\n offset += p.length;\n }\n return out;\n}\n\nexport function getPublicKeyEd25519(opts: GetPublicKeyEd25519Opts): Uint8Array {\n return ed.getPublicKey(opts.seed);\n}\n","export type SeedDeriveErrorCode = 'INVALID_SEED_LENGTH';\n\nexport class SeedDeriveError extends Error {\n readonly code: SeedDeriveErrorCode;\n\n constructor(code: SeedDeriveErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'SeedDeriveError';\n this.code = code;\n }\n}\n","import { hkdfSha256 } from '../kdf/hkdf';\nimport { mlkem768x25519Keygen } from '../kem/mlkem768x25519';\nimport { x25519PublicKey } from '../kem/x25519';\nimport { getPublicKeyEd25519 } from '../sig/ed25519';\n\nimport { SeedDeriveError } from './errors';\n\n// HKDF info constants for the long-term identity keypairs.\n// These literal byte sequences are part of the on-wire protocol; every\n// conformant implementation MUST hash against these exact ASCII bytes (the\n// Python parity twin pins the identical labels).\nexport const INFO_ED25519: Uint8Array = new TextEncoder().encode('cardano-poe-ed25519-v1');\nexport const INFO_X25519: Uint8Array = new TextEncoder().encode('cardano-poe-x25519-v1');\nexport const INFO_MLKEM768X25519: Uint8Array = new TextEncoder().encode(\n 'cardano-poe-mlkem768x25519-v1',\n);\n\nif (INFO_ED25519.length !== 22) {\n throw new Error('INFO_ED25519 byte-length invariant violated (expected 22)');\n}\nif (INFO_X25519.length !== 21) {\n throw new Error('INFO_X25519 byte-length invariant violated (expected 21)');\n}\nif (INFO_MLKEM768X25519.length !== 29) {\n throw new Error('INFO_MLKEM768X25519 byte-length invariant violated (expected 29)');\n}\n\nconst EMPTY_SALT: Uint8Array = new Uint8Array(0);\nconst SEED_LENGTH = 32;\nconst DERIVED_LENGTH = 32;\n\nexport interface DerivedEd25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface DerivedX25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface DerivedMlKem768X25519KeyPair {\n readonly secretSeed: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nfunction assertSeedLength(seed: Uint8Array): void {\n if (seed.length !== SEED_LENGTH) {\n throw new SeedDeriveError(\n 'INVALID_SEED_LENGTH',\n `seed must be exactly 32 bytes, got ${seed.length}`,\n );\n }\n}\n\nexport function deriveEd25519KeypairFromSeed(seed: Uint8Array): DerivedEd25519KeyPair {\n assertSeedLength(seed);\n const secretKey = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_ED25519,\n length: DERIVED_LENGTH,\n });\n const publicKey = getPublicKeyEd25519({ seed: secretKey });\n return { secretKey, publicKey };\n}\n\nexport function deriveX25519KeypairFromSeed(seed: Uint8Array): DerivedX25519KeyPair {\n assertSeedLength(seed);\n const secretKey = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_X25519,\n length: DERIVED_LENGTH,\n });\n const publicKey = x25519PublicKey({ secretKey });\n return { secretKey, publicKey };\n}\n\nexport function deriveMlKem768X25519KeypairFromSeed(\n seed: Uint8Array,\n): DerivedMlKem768X25519KeyPair {\n assertSeedLength(seed);\n // The 32-byte HKDF output IS the X-Wing root seed: keygen re-expands the\n // ML-KEM coins and the X25519 scalar from it, so the derived keypair's\n // secretSeed equals this value.\n const xwingSeed = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_MLKEM768X25519,\n length: DERIVED_LENGTH,\n });\n return mlkem768x25519Keygen(xwingSeed);\n}\n"]}
|
package/dist/seed-derive.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/kdf/hkdf.ts","../src/kem/mlkem768x25519.ts","../src/kem/x25519.ts","../src/sig/ed25519.ts","../src/seed-derive/errors.ts","../src/seed-derive/derive.ts"],"names":[],"mappings":"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","file":"seed-derive.js","sourcesContent":["import { hkdf } from '@noble/hashes/hkdf.js';\nimport { sha256 } from '@noble/hashes/sha2.js';\n\nexport interface HkdfSha256Opts {\n readonly ikm: Uint8Array;\n readonly salt: Uint8Array;\n readonly info: Uint8Array;\n readonly length: number;\n}\n\nexport function hkdfSha256(opts: HkdfSha256Opts): Uint8Array {\n return hkdf(sha256, opts.ikm, opts.salt, opts.info, opts.length);\n}\n","import { XWing } from '@noble/post-quantum/hybrid.js';\n\n// X-Wing (ML-KEM-768 + X25519) hybrid KEM per draft-connolly-cfrg-xwing-kem-06.\n// `XWing` is @noble/post-quantum's alias for `ml_kem768_x25519`. We expose it\n// through opts-object wrappers that pin the wire lengths and map noble's field\n// names onto the project's vocabulary.\n//\n// Unlike the bare X25519 KEM, there is no contributory-behaviour rejection to\n// translate: X-Wing combines the ML-KEM and X25519 shared secrets through a\n// SHA3-256 combiner that also binds the X25519 ephemeral and recipient public\n// keys, and ML-KEM's implicit rejection already yields a constant-work\n// pseudorandom secret on a malformed ciphertext. Decapsulation therefore never\n// throws on attacker-supplied wire data — a wrong shared secret is the correct,\n// indistinguishable failure mode, and callers MUST treat it as a non-match\n// rather than expecting an exception.\n\nexport const MLKEM768X25519_PUBLIC_KEY_LENGTH = 1216 as const;\nexport const MLKEM768X25519_ENC_LENGTH = 1120 as const;\nexport const MLKEM768X25519_SHARED_SECRET_LENGTH = 32 as const;\nexport const MLKEM768X25519_SEED_LENGTH = 32 as const;\nexport const MLKEM768X25519_ESEED_LENGTH = 64 as const;\n\nexport interface Mlkem768X25519KeyPair {\n // The 32-byte root seed IS the secret key in draft-06: the ML-KEM coins and\n // the X25519 scalar are re-expanded from it via SHAKE-256 at decapsulation.\n readonly secretSeed: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface Mlkem768X25519EncapsulateOpts {\n readonly publicKey: Uint8Array;\n // Optional 64-byte encapsulation randomness (msgRand). When supplied the\n // ciphertext and shared secret are fully deterministic; a 32-byte value is\n // rejected by noble, so we pin the length here too.\n readonly eseed?: Uint8Array;\n}\n\nexport interface Mlkem768X25519Encapsulation {\n readonly enc: Uint8Array;\n readonly ss: Uint8Array;\n}\n\nexport interface Mlkem768X25519DecapsulateOpts {\n readonly secretSeed: Uint8Array;\n readonly enc: Uint8Array;\n}\n\nexport function mlkem768x25519Keygen(seed: Uint8Array): Mlkem768X25519KeyPair {\n if (seed.length !== MLKEM768X25519_SEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 seed must be ${MLKEM768X25519_SEED_LENGTH} bytes, got ${seed.length}`,\n );\n }\n const { secretKey, publicKey } = XWing.keygen(seed);\n return { secretSeed: secretKey, publicKey };\n}\n\nexport function mlkem768x25519Encapsulate(\n opts: Mlkem768X25519EncapsulateOpts,\n): Mlkem768X25519Encapsulation {\n if (opts.publicKey.length !== MLKEM768X25519_PUBLIC_KEY_LENGTH) {\n throw new Error(\n `mlkem768x25519 public key must be ${MLKEM768X25519_PUBLIC_KEY_LENGTH} bytes, got ${opts.publicKey.length}`,\n );\n }\n if (opts.eseed !== undefined && opts.eseed.length !== MLKEM768X25519_ESEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 eseed must be ${MLKEM768X25519_ESEED_LENGTH} bytes, got ${opts.eseed.length}`,\n );\n }\n const { cipherText, sharedSecret } = XWing.encapsulate(opts.publicKey, opts.eseed);\n return { enc: cipherText, ss: sharedSecret };\n}\n\nexport function mlkem768x25519Decapsulate(opts: Mlkem768X25519DecapsulateOpts): Uint8Array {\n // Pre-check both lengths before calling noble: decapsulation must perform a\n // constant amount of work for any caller-supplied ciphertext (implicit\n // rejection), which requires the inputs to be the exact expected sizes.\n if (opts.secretSeed.length !== MLKEM768X25519_SEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 secret seed must be ${MLKEM768X25519_SEED_LENGTH} bytes, got ${opts.secretSeed.length}`,\n );\n }\n if (opts.enc.length !== MLKEM768X25519_ENC_LENGTH) {\n throw new Error(\n `mlkem768x25519 enc must be ${MLKEM768X25519_ENC_LENGTH} bytes, got ${opts.enc.length}`,\n );\n }\n // noble's signature is decapsulate(cipherText, secretKey) — ciphertext first.\n return XWing.decapsulate(opts.enc, opts.secretSeed);\n}\n","import { x25519 } from '@noble/curves/ed25519.js';\n\n// RFC 7748 §6.1 contributory-behaviour rejection: a small-order (low-order)\n// Montgomery `u` coordinate makes the X25519 shared secret all-zero, which\n// @noble/curves refuses with `Error: invalid private or public key received`.\n// We rethrow that as a *typed* error so callers can distinguish a structurally\n// valid-but-malicious peer public key (a property of attacker-supplied wire\n// data — trial-decrypt MUST treat the slot as a non-match, not crash) from\n// genuine caller misuse such as a wrong-length key (which @noble raises as a\n// RangeError and which we deliberately let propagate untouched).\nexport class X25519LowOrderPointError extends Error {\n readonly code = 'X25519_LOW_ORDER_POINT' as const;\n constructor(options?: { cause?: unknown }) {\n super('x25519 ECDH rejected: peer public key is a small-order point', options);\n this.name = 'X25519LowOrderPointError';\n }\n}\n\n// @noble/curves v2 signals a small-order/all-zero shared secret with this exact\n// message. Matching on it (rather than the broad Error class) keeps unrelated\n// failures — e.g. a future internal assertion — surfacing as themselves.\nconst NOBLE_LOW_ORDER_MESSAGE = 'invalid private or public key received';\n\nexport interface X25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface X25519PublicKeyOpts {\n readonly secretKey: Uint8Array;\n}\n\nexport interface X25519EcdhOpts {\n readonly secretKey: Uint8Array;\n readonly theirPublicKey: Uint8Array;\n}\n\nexport function x25519Keygen(): X25519KeyPair {\n return x25519.keygen();\n}\n\nexport function x25519PublicKey(opts: X25519PublicKeyOpts): Uint8Array {\n return x25519.getPublicKey(opts.secretKey);\n}\n\nexport function x25519Ecdh(opts: X25519EcdhOpts): Uint8Array {\n try {\n return x25519.getSharedSecret(opts.secretKey, opts.theirPublicKey);\n } catch (e) {\n // Translate ONLY the contributory-check rejection into our typed error.\n // A wrong-length key throws a RangeError from @noble's length assertion;\n // that is caller misuse, not malicious wire data, so it must propagate.\n if (e instanceof Error && e.message === NOBLE_LOW_ORDER_MESSAGE) {\n throw new X25519LowOrderPointError({ cause: e });\n }\n throw e;\n }\n}\n","import * as ed from '@noble/ed25519';\nimport { sha512 } from '@noble/hashes/sha2.js';\n\ned.hashes.sha512 = sha512;\n\n// Ed25519 group order L (= 2^252 + 27742317777372353535851937790883648493).\nconst L = ed.Point.CURVE().n;\n\nexport interface SignEd25519Opts {\n readonly seed: Uint8Array;\n readonly message: Uint8Array;\n}\n\nexport interface VerifyEd25519Opts {\n readonly publicKey: Uint8Array;\n readonly message: Uint8Array;\n readonly signature: Uint8Array;\n}\n\nexport interface GetPublicKeyEd25519Opts {\n readonly seed: Uint8Array;\n}\n\nexport function signEd25519(opts: SignEd25519Opts): Uint8Array {\n return ed.sign(opts.message, opts.seed);\n}\n\n// Little-endian 32-byte scalar → bigint.\nfunction leBytesToBigInt(bytes: Uint8Array): bigint {\n let value = 0n;\n for (let i = bytes.length - 1; i >= 0; i--) {\n value = (value << 8n) | BigInt(bytes[i]!);\n }\n return value;\n}\n\n// Strict (non-cofactored) Ed25519 verification per RFC 8032 §5.1.7, matching\n// libsodium/PyNaCl `crypto_sign_verify_detached` and ed25519-dalek\n// `verify_strict`. The cofactor-less check rejects every small-order /\n// torsion-component edge case in the C2SP/CCTV corpus, which noble's\n// `{ zip215: false }` mode does NOT (it remains cofactored: it checks\n// `[8]([S]B - [k]A - R) == 0`, accepting torsion components).\n//\n// The verification equation is the unscaled `[S]B == R + [k]A`, rewritten as\n// `[S]B - [k]A - R == identity`. We reject S >= L (non-canonical scalar) and\n// any small-order A or R up front, so a torsion component can never be smuggled\n// through the cofactor multiplication the cofactored variant performs.\nexport function verifyEd25519(opts: VerifyEd25519Opts): boolean {\n const { signature, message, publicKey } = opts;\n if (signature.length !== 64 || publicKey.length !== 32) return false;\n\n // S = LE(sig[32..64]); reject if not a canonical scalar (S >= L).\n const S = leBytesToBigInt(signature.subarray(32, 64));\n if (S >= L) return false;\n\n // Decode A (public key) and R (sig[0..32]) with the canonical (non-zip215)\n // point encoding; a non-canonical encoding throws and rejects.\n let A: ed.Point;\n let R: ed.Point;\n try {\n A = ed.Point.fromBytes(publicKey);\n R = ed.Point.fromBytes(signature.subarray(0, 32));\n } catch {\n return false;\n }\n\n // Reject small-order (cofactor-torsion) A or R: this is exactly the strictness\n // that distinguishes verify_strict from the cofactored check.\n if (A.isSmallOrder() || R.isSmallOrder()) return false;\n\n // k = SHA-512(R || A || M) reduced mod L.\n const k =\n leBytesToBigInt(ed.hash(concatBytes(signature.subarray(0, 32), publicKey, message))) % L;\n\n // Accept iff [S]B - [k]A - R == identity. `multiplyUnsafe` returns the\n // identity for a 0 scalar, but guard explicitly to avoid relying on that.\n const sB = S === 0n ? ed.Point.ZERO : ed.Point.BASE.multiplyUnsafe(S);\n const kA = k === 0n ? ed.Point.ZERO : A.multiplyUnsafe(k);\n return sB.subtract(kA).subtract(R).is0();\n}\n\nfunction concatBytes(...parts: Uint8Array[]): Uint8Array {\n let total = 0;\n for (const p of parts) total += p.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const p of parts) {\n out.set(p, offset);\n offset += p.length;\n }\n return out;\n}\n\nexport function getPublicKeyEd25519(opts: GetPublicKeyEd25519Opts): Uint8Array {\n return ed.getPublicKey(opts.seed);\n}\n","export type SeedDeriveErrorCode = 'INVALID_SEED_LENGTH';\n\nexport class SeedDeriveError extends Error {\n readonly code: SeedDeriveErrorCode;\n\n constructor(code: SeedDeriveErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'SeedDeriveError';\n this.code = code;\n }\n}\n","import { hkdfSha256 } from '../kdf/hkdf';\nimport { mlkem768x25519Keygen } from '../kem/mlkem768x25519';\nimport { x25519PublicKey } from '../kem/x25519';\nimport { getPublicKeyEd25519 } from '../sig/ed25519';\n\nimport { SeedDeriveError } from './errors';\n\n// HKDF info constants for the long-term identity keypairs.\n// These literal byte sequences are part of the on-wire protocol; every\n// conformant implementation MUST hash against these exact ASCII bytes (the\n// Python parity twin pins the identical labels).\nexport const INFO_ED25519: Uint8Array = new TextEncoder().encode('cardano-poe-ed25519-v1');\nexport const INFO_X25519: Uint8Array = new TextEncoder().encode('cardano-poe-x25519-v1');\nexport const INFO_MLKEM768X25519: Uint8Array = new TextEncoder().encode(\n 'cardano-poe-mlkem768x25519-v1',\n);\n\nif (INFO_ED25519.length !== 22) {\n throw new Error('INFO_ED25519 byte-length invariant violated (expected 22)');\n}\nif (INFO_X25519.length !== 21) {\n throw new Error('INFO_X25519 byte-length invariant violated (expected 21)');\n}\nif (INFO_MLKEM768X25519.length !== 29) {\n throw new Error('INFO_MLKEM768X25519 byte-length invariant violated (expected 29)');\n}\n\nconst EMPTY_SALT: Uint8Array = new Uint8Array(0);\nconst SEED_LENGTH = 32;\nconst DERIVED_LENGTH = 32;\n\nexport interface DerivedEd25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface DerivedX25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface DerivedMlKem768X25519KeyPair {\n readonly secretSeed: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nfunction assertSeedLength(seed: Uint8Array): void {\n if (seed.length !== SEED_LENGTH) {\n throw new SeedDeriveError(\n 'INVALID_SEED_LENGTH',\n `seed must be exactly 32 bytes, got ${seed.length}`,\n );\n }\n}\n\nexport function deriveEd25519KeypairFromSeed(seed: Uint8Array): DerivedEd25519KeyPair {\n assertSeedLength(seed);\n const secretKey = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_ED25519,\n length: DERIVED_LENGTH,\n });\n const publicKey = getPublicKeyEd25519({ seed: secretKey });\n return { secretKey, publicKey };\n}\n\nexport function deriveX25519KeypairFromSeed(seed: Uint8Array): DerivedX25519KeyPair {\n assertSeedLength(seed);\n const secretKey = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_X25519,\n length: DERIVED_LENGTH,\n });\n const publicKey = x25519PublicKey({ secretKey });\n return { secretKey, publicKey };\n}\n\nexport function deriveMlKem768X25519KeypairFromSeed(\n seed: Uint8Array,\n): DerivedMlKem768X25519KeyPair {\n assertSeedLength(seed);\n // The 32-byte HKDF output IS the X-Wing root seed: keygen re-expands the\n // ML-KEM coins and the X25519 scalar from it, so the derived keypair's\n // secretSeed equals this value.\n const xwingSeed = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_MLKEM768X25519,\n length: DERIVED_LENGTH,\n });\n return mlkem768x25519Keygen(xwingSeed);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/kdf/hkdf.ts","../src/kem/mlkem768x25519.ts","../src/kem/x25519.ts","../src/sig/ed25519.ts","../src/seed-derive/errors.ts","../src/seed-derive/derive.ts"],"names":[],"mappings":"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","file":"seed-derive.js","sourcesContent":["import { hkdf } from '@noble/hashes/hkdf.js';\nimport { sha256 } from '@noble/hashes/sha2.js';\n\nexport interface HkdfSha256Opts {\n readonly ikm: Uint8Array;\n readonly salt: Uint8Array;\n readonly info: Uint8Array;\n readonly length: number;\n}\n\nexport function hkdfSha256(opts: HkdfSha256Opts): Uint8Array {\n return hkdf(sha256, opts.ikm, opts.salt, opts.info, opts.length);\n}\n","import { XWing } from '@noble/post-quantum/hybrid.js';\n\n// X-Wing (ML-KEM-768 + X25519) hybrid KEM per draft-connolly-cfrg-xwing-kem-10.\n// `XWing` is @noble/post-quantum's alias for `ml_kem768_x25519`. We expose it\n// through opts-object wrappers that pin the wire lengths and map noble's field\n// names onto the project's vocabulary.\n//\n// Unlike the bare X25519 KEM, there is no contributory-behaviour rejection to\n// translate: X-Wing combines the ML-KEM and X25519 shared secrets through a\n// SHA3-256 combiner that also binds the X25519 ephemeral and recipient public\n// keys, and ML-KEM's implicit rejection already yields a constant-work\n// pseudorandom secret on a malformed ciphertext. Decapsulation therefore never\n// throws on attacker-supplied wire data — a wrong shared secret is the correct,\n// indistinguishable failure mode, and callers MUST treat it as a non-match\n// rather than expecting an exception.\n\nexport const MLKEM768X25519_PUBLIC_KEY_LENGTH = 1216 as const;\nexport const MLKEM768X25519_ENC_LENGTH = 1120 as const;\nexport const MLKEM768X25519_SHARED_SECRET_LENGTH = 32 as const;\nexport const MLKEM768X25519_SEED_LENGTH = 32 as const;\nexport const MLKEM768X25519_ESEED_LENGTH = 64 as const;\n\nexport interface Mlkem768X25519KeyPair {\n // The 32-byte root seed IS the secret key: the ML-KEM coins and the X25519\n // scalar are re-expanded from it via SHAKE-256 at decapsulation. (Later X-Wing\n // drafts also define an optional expanded decapsulation-key form; we keep the\n // seed-only key, which the draft-10 Appendix C vectors still pin.)\n readonly secretSeed: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface Mlkem768X25519EncapsulateOpts {\n readonly publicKey: Uint8Array;\n // Optional 64-byte encapsulation randomness (msgRand). When supplied the\n // ciphertext and shared secret are fully deterministic; a 32-byte value is\n // rejected by noble, so we pin the length here too.\n readonly eseed?: Uint8Array;\n}\n\nexport interface Mlkem768X25519Encapsulation {\n readonly enc: Uint8Array;\n readonly ss: Uint8Array;\n}\n\nexport interface Mlkem768X25519DecapsulateOpts {\n readonly secretSeed: Uint8Array;\n readonly enc: Uint8Array;\n}\n\nexport function mlkem768x25519Keygen(seed: Uint8Array): Mlkem768X25519KeyPair {\n if (seed.length !== MLKEM768X25519_SEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 seed must be ${MLKEM768X25519_SEED_LENGTH} bytes, got ${seed.length}`,\n );\n }\n const { secretKey, publicKey } = XWing.keygen(seed);\n return { secretSeed: secretKey, publicKey };\n}\n\nexport function mlkem768x25519Encapsulate(\n opts: Mlkem768X25519EncapsulateOpts,\n): Mlkem768X25519Encapsulation {\n if (opts.publicKey.length !== MLKEM768X25519_PUBLIC_KEY_LENGTH) {\n throw new Error(\n `mlkem768x25519 public key must be ${MLKEM768X25519_PUBLIC_KEY_LENGTH} bytes, got ${opts.publicKey.length}`,\n );\n }\n if (opts.eseed !== undefined && opts.eseed.length !== MLKEM768X25519_ESEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 eseed must be ${MLKEM768X25519_ESEED_LENGTH} bytes, got ${opts.eseed.length}`,\n );\n }\n const { cipherText, sharedSecret } = XWing.encapsulate(opts.publicKey, opts.eseed);\n return { enc: cipherText, ss: sharedSecret };\n}\n\nexport function mlkem768x25519Decapsulate(opts: Mlkem768X25519DecapsulateOpts): Uint8Array {\n // Pre-check both lengths before calling noble: decapsulation must perform a\n // constant amount of work for any caller-supplied ciphertext (implicit\n // rejection), which requires the inputs to be the exact expected sizes.\n if (opts.secretSeed.length !== MLKEM768X25519_SEED_LENGTH) {\n throw new Error(\n `mlkem768x25519 secret seed must be ${MLKEM768X25519_SEED_LENGTH} bytes, got ${opts.secretSeed.length}`,\n );\n }\n if (opts.enc.length !== MLKEM768X25519_ENC_LENGTH) {\n throw new Error(\n `mlkem768x25519 enc must be ${MLKEM768X25519_ENC_LENGTH} bytes, got ${opts.enc.length}`,\n );\n }\n // noble's signature is decapsulate(cipherText, secretKey) — ciphertext first.\n return XWing.decapsulate(opts.enc, opts.secretSeed);\n}\n","import { x25519 } from '@noble/curves/ed25519.js';\n\n// RFC 7748 §6.1 contributory-behaviour rejection: a small-order (low-order)\n// Montgomery `u` coordinate makes the X25519 shared secret all-zero, which\n// @noble/curves refuses with `Error: invalid private or public key received`.\n// We rethrow that as a *typed* error so callers can distinguish a structurally\n// valid-but-malicious peer public key (a property of attacker-supplied wire\n// data — trial-decrypt MUST treat the slot as a non-match, not crash) from\n// genuine caller misuse such as a wrong-length key (which @noble raises as a\n// RangeError and which we deliberately let propagate untouched).\nexport class X25519LowOrderPointError extends Error {\n readonly code = 'X25519_LOW_ORDER_POINT' as const;\n constructor(options?: { cause?: unknown }) {\n super('x25519 ECDH rejected: peer public key is a small-order point', options);\n this.name = 'X25519LowOrderPointError';\n }\n}\n\n// @noble/curves v2 signals a small-order/all-zero shared secret with this exact\n// message. Matching on it (rather than the broad Error class) keeps unrelated\n// failures — e.g. a future internal assertion — surfacing as themselves.\nconst NOBLE_LOW_ORDER_MESSAGE = 'invalid private or public key received';\n\nexport interface X25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface X25519PublicKeyOpts {\n readonly secretKey: Uint8Array;\n}\n\nexport interface X25519EcdhOpts {\n readonly secretKey: Uint8Array;\n readonly theirPublicKey: Uint8Array;\n}\n\nexport function x25519Keygen(): X25519KeyPair {\n return x25519.keygen();\n}\n\nexport function x25519PublicKey(opts: X25519PublicKeyOpts): Uint8Array {\n return x25519.getPublicKey(opts.secretKey);\n}\n\nexport function x25519Ecdh(opts: X25519EcdhOpts): Uint8Array {\n try {\n return x25519.getSharedSecret(opts.secretKey, opts.theirPublicKey);\n } catch (e) {\n // Translate ONLY the contributory-check rejection into our typed error.\n // A wrong-length key throws a RangeError from @noble's length assertion;\n // that is caller misuse, not malicious wire data, so it must propagate.\n if (e instanceof Error && e.message === NOBLE_LOW_ORDER_MESSAGE) {\n throw new X25519LowOrderPointError({ cause: e });\n }\n throw e;\n }\n}\n","import * as ed from '@noble/ed25519';\nimport { sha512 } from '@noble/hashes/sha2.js';\n\ned.hashes.sha512 = sha512;\n\n// Ed25519 group order L (= 2^252 + 27742317777372353535851937790883648493).\nconst L = ed.Point.CURVE().n;\n\nexport interface SignEd25519Opts {\n readonly seed: Uint8Array;\n readonly message: Uint8Array;\n}\n\nexport interface VerifyEd25519Opts {\n readonly publicKey: Uint8Array;\n readonly message: Uint8Array;\n readonly signature: Uint8Array;\n}\n\nexport interface GetPublicKeyEd25519Opts {\n readonly seed: Uint8Array;\n}\n\nexport function signEd25519(opts: SignEd25519Opts): Uint8Array {\n return ed.sign(opts.message, opts.seed);\n}\n\n// Little-endian 32-byte scalar → bigint.\nfunction leBytesToBigInt(bytes: Uint8Array): bigint {\n let value = 0n;\n for (let i = bytes.length - 1; i >= 0; i--) {\n value = (value << 8n) | BigInt(bytes[i]!);\n }\n return value;\n}\n\n// Strict (non-cofactored) Ed25519 verification per RFC 8032 §5.1.7, matching\n// libsodium/PyNaCl `crypto_sign_verify_detached` and ed25519-dalek\n// `verify_strict`. The cofactor-less check rejects every small-order /\n// torsion-component edge case in the C2SP/CCTV corpus, which noble's\n// `{ zip215: false }` mode does NOT (it remains cofactored: it checks\n// `[8]([S]B - [k]A - R) == 0`, accepting torsion components).\n//\n// The verification equation is the unscaled `[S]B == R + [k]A`, rewritten as\n// `[S]B - [k]A - R == identity`. We reject S >= L (non-canonical scalar) and\n// any small-order A or R up front, so a torsion component can never be smuggled\n// through the cofactor multiplication the cofactored variant performs.\nexport function verifyEd25519(opts: VerifyEd25519Opts): boolean {\n const { signature, message, publicKey } = opts;\n if (signature.length !== 64 || publicKey.length !== 32) return false;\n\n // S = LE(sig[32..64]); reject if not a canonical scalar (S >= L).\n const S = leBytesToBigInt(signature.subarray(32, 64));\n if (S >= L) return false;\n\n // Decode A (public key) and R (sig[0..32]) with the canonical (non-zip215)\n // point encoding; a non-canonical encoding throws and rejects.\n let A: ed.Point;\n let R: ed.Point;\n try {\n A = ed.Point.fromBytes(publicKey);\n R = ed.Point.fromBytes(signature.subarray(0, 32));\n } catch {\n return false;\n }\n\n // Reject small-order (cofactor-torsion) A or R: this is exactly the strictness\n // that distinguishes verify_strict from the cofactored check.\n if (A.isSmallOrder() || R.isSmallOrder()) return false;\n\n // k = SHA-512(R || A || M) reduced mod L.\n const k =\n leBytesToBigInt(ed.hash(concatBytes(signature.subarray(0, 32), publicKey, message))) % L;\n\n // Accept iff [S]B - [k]A - R == identity. `multiplyUnsafe` returns the\n // identity for a 0 scalar, but guard explicitly to avoid relying on that.\n const sB = S === 0n ? ed.Point.ZERO : ed.Point.BASE.multiplyUnsafe(S);\n const kA = k === 0n ? ed.Point.ZERO : A.multiplyUnsafe(k);\n return sB.subtract(kA).subtract(R).is0();\n}\n\nfunction concatBytes(...parts: Uint8Array[]): Uint8Array {\n let total = 0;\n for (const p of parts) total += p.length;\n const out = new Uint8Array(total);\n let offset = 0;\n for (const p of parts) {\n out.set(p, offset);\n offset += p.length;\n }\n return out;\n}\n\nexport function getPublicKeyEd25519(opts: GetPublicKeyEd25519Opts): Uint8Array {\n return ed.getPublicKey(opts.seed);\n}\n","export type SeedDeriveErrorCode = 'INVALID_SEED_LENGTH';\n\nexport class SeedDeriveError extends Error {\n readonly code: SeedDeriveErrorCode;\n\n constructor(code: SeedDeriveErrorCode, message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'SeedDeriveError';\n this.code = code;\n }\n}\n","import { hkdfSha256 } from '../kdf/hkdf';\nimport { mlkem768x25519Keygen } from '../kem/mlkem768x25519';\nimport { x25519PublicKey } from '../kem/x25519';\nimport { getPublicKeyEd25519 } from '../sig/ed25519';\n\nimport { SeedDeriveError } from './errors';\n\n// HKDF info constants for the long-term identity keypairs.\n// These literal byte sequences are part of the on-wire protocol; every\n// conformant implementation MUST hash against these exact ASCII bytes (the\n// Python parity twin pins the identical labels).\nexport const INFO_ED25519: Uint8Array = new TextEncoder().encode('cardano-poe-ed25519-v1');\nexport const INFO_X25519: Uint8Array = new TextEncoder().encode('cardano-poe-x25519-v1');\nexport const INFO_MLKEM768X25519: Uint8Array = new TextEncoder().encode(\n 'cardano-poe-mlkem768x25519-v1',\n);\n\nif (INFO_ED25519.length !== 22) {\n throw new Error('INFO_ED25519 byte-length invariant violated (expected 22)');\n}\nif (INFO_X25519.length !== 21) {\n throw new Error('INFO_X25519 byte-length invariant violated (expected 21)');\n}\nif (INFO_MLKEM768X25519.length !== 29) {\n throw new Error('INFO_MLKEM768X25519 byte-length invariant violated (expected 29)');\n}\n\nconst EMPTY_SALT: Uint8Array = new Uint8Array(0);\nconst SEED_LENGTH = 32;\nconst DERIVED_LENGTH = 32;\n\nexport interface DerivedEd25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface DerivedX25519KeyPair {\n readonly secretKey: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nexport interface DerivedMlKem768X25519KeyPair {\n readonly secretSeed: Uint8Array;\n readonly publicKey: Uint8Array;\n}\n\nfunction assertSeedLength(seed: Uint8Array): void {\n if (seed.length !== SEED_LENGTH) {\n throw new SeedDeriveError(\n 'INVALID_SEED_LENGTH',\n `seed must be exactly 32 bytes, got ${seed.length}`,\n );\n }\n}\n\nexport function deriveEd25519KeypairFromSeed(seed: Uint8Array): DerivedEd25519KeyPair {\n assertSeedLength(seed);\n const secretKey = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_ED25519,\n length: DERIVED_LENGTH,\n });\n const publicKey = getPublicKeyEd25519({ seed: secretKey });\n return { secretKey, publicKey };\n}\n\nexport function deriveX25519KeypairFromSeed(seed: Uint8Array): DerivedX25519KeyPair {\n assertSeedLength(seed);\n const secretKey = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_X25519,\n length: DERIVED_LENGTH,\n });\n const publicKey = x25519PublicKey({ secretKey });\n return { secretKey, publicKey };\n}\n\nexport function deriveMlKem768X25519KeypairFromSeed(\n seed: Uint8Array,\n): DerivedMlKem768X25519KeyPair {\n assertSeedLength(seed);\n // The 32-byte HKDF output IS the X-Wing root seed: keygen re-expands the\n // ML-KEM coins and the X25519 scalar from it, so the derived keypair's\n // secretSeed equals this value.\n const xwingSeed = hkdfSha256({\n ikm: seed,\n salt: EMPTY_SALT,\n info: INFO_MLKEM768X25519,\n length: DERIVED_LENGTH,\n });\n return mlkem768x25519Keygen(xwingSeed);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardanowall/crypto-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Closed-catalogue cryptographic primitives for
|
|
5
|
+
"description": "Closed-catalogue cryptographic primitives for Label 309 Proof-of-Existence (TypeScript reference implementation; byte-identical Python parity twin).",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "CardanoWall <hello@cardanowall.com>",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/cardanowall/
|
|
10
|
+
"url": "git+https://github.com/cardanowall/label-309-ts.git",
|
|
11
11
|
"directory": "packages/crypto-core"
|
|
12
12
|
},
|
|
13
|
-
"homepage": "https://github.com/cardanowall/
|
|
13
|
+
"homepage": "https://github.com/cardanowall/label-309-ts#readme",
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/cardanowall/
|
|
15
|
+
"url": "https://github.com/cardanowall/label-309-ts/issues"
|
|
16
16
|
},
|
|
17
17
|
"main": "./dist/index.cjs",
|
|
18
18
|
"module": "./dist/index.js",
|