@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/dist/index.cjs CHANGED
@@ -10,11 +10,11 @@ var p256 = require('@noble/curves/p256');
10
10
  var sha2 = require('@noble/hashes/sha2');
11
11
 
12
12
  /**
13
- * Normative constants for the `v1` key derivation scheme. Changing any of
13
+ * Normative constants for the `v2` key derivation scheme. Changing any of
14
14
  * these changes every derived key and `networkId`, so they MUST NOT change
15
15
  * without a version bump (new `KEY_DERIVATION_VERSION` and domain strings).
16
16
  */
17
- const KEY_DERIVATION_VERSION = "v1";
17
+ const KEY_DERIVATION_VERSION = "v2";
18
18
  /** Fixed domain-separation message signed by {@link signDerivationMessage}. */
19
19
  const FIXED_DOMAIN_MSG = "coordination-network/identity/v1";
20
20
  /** Domain-separation tag for deriving the account keypair from a device secret. */
@@ -23,11 +23,15 @@ const DOMAIN_ACCOUNT = "coordination-network/identity/v1/account";
23
23
  const DOMAIN_AUTH = "coordination-network/identity/v1/auth";
24
24
  /** Domain-separation tag for deriving the Baby Jubjub EdDSA seed (the "fifth key") from the hardware ECDH shared secret. */
25
25
  const DOMAIN_EDDSA = "coordination-network/identity/v1/eddsa";
26
+ /** Distinct Baby Jubjub seed for public paymaster membership; never use the credential holder key as a group leaf. */
27
+ const DOMAIN_PAYMASTER = "coordination-network/identity/v1/paymaster";
26
28
  /** Domain-separation message hashed to the fixed NUMS P-256 peer point `Q` for the fifth-key ECDH (`eddsa-peer.ts`). */
27
29
  const DOMAIN_EDDSA_PEER = "coordination-network/identity/v1/eddsa/peer";
30
+ /** Domain-separation tag prefixed to `X || Y` when hashing a user `networkId`. */
31
+ const DOMAIN_NETWORK_ID = "coordination-network/identity/v2/network-id";
28
32
  /**
29
33
  * Byte length of `nonce` and `networkId` in the binding messages — fixed at
30
- * 32 so the 64- vs 96-byte messages are injective.
34
+ * 32 so the 128-byte device and 96-byte account messages are injective.
31
35
  */
32
36
  const BINDING_NONCE_LENGTH = 32;
33
37
  /** SEC1-compressed secp256k1 public key length. */
@@ -548,6 +552,19 @@ function deriveAuthKeypair(sig64) {
548
552
  }
549
553
 
550
554
  /** Baby Jubjub EdDSA "fifth key" derivation — see `DERIVATION.md` §12. */
555
+ function deriveKeypair(ecdhSharedSecret, domain) {
556
+ const secret = normalizeEcdhSharedSecret(ecdhSharedSecret);
557
+ const seed = sha3.keccak_256(utils.concatBytes(utils.utf8ToBytes(domain), secret));
558
+ const secretScalar = eddsaPoseidon.deriveSecretScalar(seed);
559
+ const publicKey = eddsaPoseidon.derivePublicKey(seed);
560
+ const commitment = poseidon2.poseidon2(publicKey);
561
+ return {
562
+ seed: bytesToHex(seed),
563
+ secretScalar,
564
+ publicKey,
565
+ commitment
566
+ };
567
+ }
551
568
  /**
552
569
  * Derives the Baby Jubjub EdDSA keypair and Semaphore-v4 identity commitment
553
570
  * (the "fifth key"), rooted in a hardware ECDH shared secret.
@@ -562,17 +579,11 @@ function deriveAuthKeypair(sig64) {
562
579
  * @throws {InvalidEcdhSecretError} if `ecdhSharedSecret` is not exactly 32 bytes
563
580
  */
564
581
  function deriveEddsaKeypair(ecdhSharedSecret) {
565
- const secret = normalizeEcdhSharedSecret(ecdhSharedSecret);
566
- const seed = sha3.keccak_256(utils.concatBytes(utils.utf8ToBytes(DOMAIN_EDDSA), secret));
567
- const secretScalar = eddsaPoseidon.deriveSecretScalar(seed);
568
- const publicKey = eddsaPoseidon.derivePublicKey(seed);
569
- const commitment = poseidon2.poseidon2(publicKey);
570
- return {
571
- seed: bytesToHex(seed),
572
- secretScalar,
573
- publicKey,
574
- commitment
575
- };
582
+ return deriveKeypair(ecdhSharedSecret, DOMAIN_EDDSA);
583
+ }
584
+ /** Derives a separate Semaphore identity whose public commitment may join the paymaster group. */
585
+ function derivePaymasterKeypair(ecdhSharedSecret) {
586
+ return deriveKeypair(ecdhSharedSecret, DOMAIN_PAYMASTER);
576
587
  }
577
588
 
578
589
  /** Fixed NUMS P-256 peer point `Q` for the fifth-key ECDH — see `DERIVATION.md` §12. */
@@ -599,35 +610,46 @@ function computeEddsaPeerPoint() {
599
610
  return p256.p256.ProjectivePoint.fromAffine(point.toAffine()).toRawBytes(false);
600
611
  }
601
612
 
602
- /**
603
- * Computes the bytes32 `networkId` from an uncompressed secp256k1 public key
604
- * (65-byte SEC1 `0x04`-prefixed or 64-byte raw `X || Y`).
605
- *
606
- * NORMATIVE: `networkId = keccak256(X || Y)` — the SEC1 prefix is stripped.
607
- * The last 20 bytes are the account's Ethereum address.
608
- */
609
- function computeNetworkId(uncompressedPublicKey) {
613
+ function publicKeyCoordinates(uncompressedPublicKey, caller) {
610
614
  const bytes = toBytes(uncompressedPublicKey);
611
- let coordinates;
612
615
  if (bytes.length === 65 && bytes[0] === 0x04) {
613
- coordinates = bytes.slice(1);
616
+ return bytes.slice(1);
614
617
  }
615
- else if (bytes.length === 64) {
616
- coordinates = bytes;
617
- }
618
- else {
619
- throw new TypeError("computeNetworkId: expected a 65-byte 0x04-prefixed or 64-byte raw uncompressed public key");
618
+ if (bytes.length === 64) {
619
+ return bytes;
620
620
  }
621
+ throw new TypeError(`${caller}: expected a 65-byte 0x04-prefixed or 64-byte raw uncompressed public key`);
622
+ }
623
+ /**
624
+ * Computes a user's bytes32 `networkId` from an uncompressed secp256k1 public
625
+ * key (65-byte SEC1 `0x04`-prefixed or 64-byte raw `X || Y`).
626
+ *
627
+ * NORMATIVE: `networkId = keccak256(utf8(DOMAIN_NETWORK_ID) || X || Y)` — the
628
+ * SEC1 prefix is stripped.
629
+ */
630
+ function computeNetworkId(uncompressedPublicKey) {
631
+ const coordinates = publicKeyCoordinates(uncompressedPublicKey, "computeNetworkId");
632
+ return bytesToHex(sha3.keccak_256(utils.concatBytes(utils.utf8ToBytes(DOMAIN_NETWORK_ID), coordinates)));
633
+ }
634
+ /**
635
+ * Computes a member's bytes32 id from the same public key forms.
636
+ *
637
+ * NORMATIVE: `memberNetworkId = keccak256(X || Y)`. The last 20 bytes are the
638
+ * member's auth address, which `NetworkMemberRegistry` reads.
639
+ */
640
+ function computeMemberNetworkId(uncompressedPublicKey) {
641
+ const coordinates = publicKeyCoordinates(uncompressedPublicKey, "computeMemberNetworkId");
621
642
  return bytesToHex(sha3.keccak_256(coordinates));
622
643
  }
623
644
 
624
645
  /**
625
- * Derives the full `v1` device identity — `networkId` plus the four-key
626
- * all-secp256k1 model — from a single device secret.
646
+ * Derives the full `v2` device identity — `networkId`, `memberNetworkId` and the account,
647
+ * spending, viewing and auth secp256k1 keypairs — from a single device secret.
627
648
  *
628
649
  * Pipeline (NORMATIVE):
629
650
  * 1. `account = deriveAccountKeypair(deviceSecret)`
630
- * 2. `networkId = computeNetworkId(account.uncompressedPublicKey)`
651
+ * 2. `networkId = computeNetworkId(account.uncompressedPublicKey)` and
652
+ * `memberNetworkId = computeMemberNetworkId(account.uncompressedPublicKey)`
631
653
  * 3. `sig = signDerivationMessage(account.privateKey)` (EIP-191, deterministic)
632
654
  * 4. `{ spending, viewing } = deriveStealthKeys(sig)` (FluidKey semantics)
633
655
  * 5. `auth = deriveAuthKeypair(sig)`
@@ -635,12 +657,14 @@ function computeNetworkId(uncompressedPublicKey) {
635
657
  function deriveKeys(deviceSecret) {
636
658
  const account = deriveAccountKeypair(deviceSecret);
637
659
  const networkId = computeNetworkId(account.uncompressedPublicKey);
660
+ const memberNetworkId = computeMemberNetworkId(account.uncompressedPublicKey);
638
661
  const signature = signDerivationMessage(account.privateKey);
639
662
  const { spending, viewing } = deriveStealthKeys(signature);
640
663
  const auth = deriveAuthKeypair(signature);
641
664
  return {
642
665
  version: KEY_DERIVATION_VERSION,
643
666
  networkId,
667
+ memberNetworkId,
644
668
  account,
645
669
  spending,
646
670
  viewing,
@@ -695,10 +719,15 @@ function computeConfirmationKeyHash(confirmationKey) {
695
719
  *
696
720
  * NORMATIVE layouts:
697
721
  * ```
698
- * deviceBindingMessage = nonce(32) || networkId(32) || holderKeyCommitment(32) // credential issuance
699
- * accountBindingMessage = nonce(32) || confirmationKeyHash(32) || networkId(32) // 96B, signed by A (secp256k1)
722
+ * deviceBindingMessage = nonce(32) || networkId(32) || holderKeyCommitment(32) || requestCommitment(32)
723
+ * accountBindingMessage = nonce(32) || confirmationKeyHash(32) || networkId(32)
700
724
  * ```
701
725
  *
726
+ * B (P-256) signs the 128-byte device message; A (secp256k1) signs the 96-byte account message.
727
+ *
728
+ * `requestCommitment` is opaque here: the credential layer defines it and
729
+ * binds the issuer and scope shown at consent into it.
730
+ *
702
731
  * NORMATIVE MUST: the device signs a locally-derived `networkId`, never one
703
732
  * supplied by the issuance request. Every function here except the two
704
733
  * low-level builders and {@link verifyDeviceBindingSignature} enforces this
@@ -712,6 +741,12 @@ function toBindingBytes(value, label) {
712
741
  throw new InvalidBindingInputError(`${label}: expected raw bytes or a well-formed 0x-prefixed hex string`);
713
742
  }
714
743
  }
744
+ function toBindingHash32(value, label) {
745
+ const bytes = toBindingBytes(value, label);
746
+ if (bytes.length !== 32)
747
+ throw new InvalidBindingInputError(`${label}: expected exactly 32 bytes`);
748
+ return bytes;
749
+ }
715
750
  function decodeAndValidateNonceAndNetworkId(nonce, networkId) {
716
751
  const nonceBytes = toBindingBytes(nonce, "nonce");
717
752
  const networkIdBytes = toBindingBytes(networkId, "networkId");
@@ -776,19 +811,17 @@ function parseDeviceSignature(signature) {
776
811
  throw new InvalidBindingInputError("deviceSignature: expected a 64-byte compact (r || s) or DER-encoded P-256 signature");
777
812
  }
778
813
  /**
779
- * Builds `nonce(32) || networkId(32)`, optionally followed by a 32-byte holder commitment.
814
+ * Builds the 128-byte `deviceBindingMessage`:
815
+ * `nonce(32) || networkId(32) || holderKeyCommitment(32) || requestCommitment(32)`.
780
816
  *
781
817
  * Low-level, spec-literal builder — prefer {@link buildBindingMessages},
782
818
  * which enforces the locally-derived-`networkId` MUST structurally.
783
819
  */
784
- function buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment) {
820
+ function buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment, requestCommitment) {
785
821
  const { nonceBytes, networkIdBytes } = decodeAndValidateNonceAndNetworkId(nonce, networkId);
786
- if (holderKeyCommitment === undefined)
787
- return utils.concatBytes(nonceBytes, networkIdBytes);
788
- const holderBytes = toBindingBytes(holderKeyCommitment, "holderKeyCommitment");
789
- if (holderBytes.length !== 32)
790
- throw new InvalidBindingInputError("holderKeyCommitment: expected exactly 32 bytes");
791
- return utils.concatBytes(nonceBytes, networkIdBytes, holderBytes);
822
+ const holderBytes = toBindingHash32(holderKeyCommitment, "holderKeyCommitment");
823
+ const requestBytes = toBindingHash32(requestCommitment, "requestCommitment");
824
+ return utils.concatBytes(nonceBytes, networkIdBytes, holderBytes, requestBytes);
792
825
  }
793
826
  /**
794
827
  * Builds the 96-byte `accountBindingMessage`:
@@ -807,7 +840,7 @@ function buildAccountBindingMessage(nonce, confirmationKey, networkId) {
807
840
  * derived internally from the account's own public key, so a caller cannot
808
841
  * supply a `networkId` from elsewhere even by mistake.
809
842
  */
810
- function buildBindingMessages(nonce, accountUncompressedPublicKey, confirmationKey, holderKeyCommitment) {
843
+ function buildBindingMessages(nonce, accountUncompressedPublicKey, confirmationKey, holderKeyCommitment, requestCommitment) {
811
844
  let networkId;
812
845
  try {
813
846
  networkId = computeNetworkId(accountUncompressedPublicKey);
@@ -815,7 +848,7 @@ function buildBindingMessages(nonce, accountUncompressedPublicKey, confirmationK
815
848
  catch {
816
849
  throw new InvalidBindingInputError("accountUncompressedPublicKey: expected a 65-byte 0x04-prefixed or 64-byte raw uncompressed secp256k1 public key");
817
850
  }
818
- const deviceBindingMessage = buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment);
851
+ const deviceBindingMessage = buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment, requestCommitment);
819
852
  const accountBindingMessage = buildAccountBindingMessage(nonce, confirmationKey, networkId);
820
853
  return { networkId, deviceBindingMessage, accountBindingMessage };
821
854
  }
@@ -858,8 +891,8 @@ function verifyAccountBindingSignature({ nonce, confirmationKey, accountPublicKe
858
891
  * `prehash: true` (signers hash SHA-256 internally); `lowS: false` (hardware
859
892
  * signatures aren't s-normalized — `DERIVATION.md` §11.4).
860
893
  */
861
- function verifyDeviceBindingSignature({ nonce, networkId, confirmationKey, holderKeyCommitment, signature }) {
862
- const message = buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment);
894
+ function verifyDeviceBindingSignature({ nonce, networkId, confirmationKey, holderKeyCommitment, requestCommitment, signature }) {
895
+ const message = buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment, requestCommitment);
863
896
  const publicKeyBytes = prependUncompressedPrefix(normalizeConfirmationKey(confirmationKey));
864
897
  const parsedSignature = parseDeviceSignature(signature);
865
898
  return p256.p256.verify(parsedSignature.toCompactRawBytes(), message, publicKeyBytes, {
@@ -873,7 +906,7 @@ function verifyDeviceBindingSignature({ nonce, networkId, confirmationKey, holde
873
906
  * with an unrelated account signature in either direction. `networkId` is
874
907
  * returned even when `valid` is false, for issuer-side logging/rate-limiting.
875
908
  */
876
- function verifyCredentialBinding({ nonce, accountPublicKey, confirmationKey, holderKeyCommitment, accountSignature, deviceSignature }) {
909
+ function verifyCredentialBinding({ nonce, accountPublicKey, confirmationKey, holderKeyCommitment, requestCommitment, accountSignature, deviceSignature }) {
877
910
  const publicKeyBytes = normalizeAccountPublicKey(accountPublicKey);
878
911
  const networkId = computeNetworkId(publicKeyBytes);
879
912
  const accountValid = verifyAccountBindingSignature({
@@ -887,6 +920,7 @@ function verifyCredentialBinding({ nonce, accountPublicKey, confirmationKey, hol
887
920
  networkId,
888
921
  confirmationKey,
889
922
  holderKeyCommitment,
923
+ requestCommitment,
890
924
  signature: deviceSignature
891
925
  });
892
926
  return { valid: accountValid && deviceValid, networkId };
@@ -898,6 +932,8 @@ exports.DOMAIN_ACCOUNT = DOMAIN_ACCOUNT;
898
932
  exports.DOMAIN_AUTH = DOMAIN_AUTH;
899
933
  exports.DOMAIN_EDDSA = DOMAIN_EDDSA;
900
934
  exports.DOMAIN_EDDSA_PEER = DOMAIN_EDDSA_PEER;
935
+ exports.DOMAIN_NETWORK_ID = DOMAIN_NETWORK_ID;
936
+ exports.DOMAIN_PAYMASTER = DOMAIN_PAYMASTER;
901
937
  exports.DerivationFailureError = DerivationFailureError;
902
938
  exports.EDDSA_PEER_POINT_HEX = EDDSA_PEER_POINT_HEX;
903
939
  exports.ERC_5564_SCHEME_ID = ERC_5564_SCHEME_ID;
@@ -921,6 +957,7 @@ exports.bytesToHex = bytesToHex;
921
957
  exports.checkStealthAddress = checkStealthAddress;
922
958
  exports.computeConfirmationKeyHash = computeConfirmationKeyHash;
923
959
  exports.computeEddsaPeerPoint = computeEddsaPeerPoint;
960
+ exports.computeMemberNetworkId = computeMemberNetworkId;
924
961
  exports.computeNetworkId = computeNetworkId;
925
962
  exports.computeSharedSecret = computeSharedSecret;
926
963
  exports.decodeMetaStealthAddress = decodeMetaStealthAddress;
@@ -929,6 +966,7 @@ exports.deriveAuthKeypair = deriveAuthKeypair;
929
966
  exports.deriveEddsaKeypair = deriveEddsaKeypair;
930
967
  exports.deriveKeys = deriveKeys;
931
968
  exports.deriveNetworkId = deriveNetworkId;
969
+ exports.derivePaymasterKeypair = derivePaymasterKeypair;
932
970
  exports.deriveStealthKeys = deriveStealthKeys;
933
971
  exports.eddsaPeerPointBytes = eddsaPeerPointBytes;
934
972
  exports.encodeMetaStealthAddress = encodeMetaStealthAddress;
package/dist/index.js CHANGED
@@ -8,11 +8,11 @@ import { hashToCurve, p256 } from '@noble/curves/p256';
8
8
  import { sha256 } from '@noble/hashes/sha2';
9
9
 
10
10
  /**
11
- * Normative constants for the `v1` key derivation scheme. Changing any of
11
+ * Normative constants for the `v2` key derivation scheme. Changing any of
12
12
  * these changes every derived key and `networkId`, so they MUST NOT change
13
13
  * without a version bump (new `KEY_DERIVATION_VERSION` and domain strings).
14
14
  */
15
- const KEY_DERIVATION_VERSION = "v1";
15
+ const KEY_DERIVATION_VERSION = "v2";
16
16
  /** Fixed domain-separation message signed by {@link signDerivationMessage}. */
17
17
  const FIXED_DOMAIN_MSG = "coordination-network/identity/v1";
18
18
  /** Domain-separation tag for deriving the account keypair from a device secret. */
@@ -21,11 +21,15 @@ const DOMAIN_ACCOUNT = "coordination-network/identity/v1/account";
21
21
  const DOMAIN_AUTH = "coordination-network/identity/v1/auth";
22
22
  /** Domain-separation tag for deriving the Baby Jubjub EdDSA seed (the "fifth key") from the hardware ECDH shared secret. */
23
23
  const DOMAIN_EDDSA = "coordination-network/identity/v1/eddsa";
24
+ /** Distinct Baby Jubjub seed for public paymaster membership; never use the credential holder key as a group leaf. */
25
+ const DOMAIN_PAYMASTER = "coordination-network/identity/v1/paymaster";
24
26
  /** Domain-separation message hashed to the fixed NUMS P-256 peer point `Q` for the fifth-key ECDH (`eddsa-peer.ts`). */
25
27
  const DOMAIN_EDDSA_PEER = "coordination-network/identity/v1/eddsa/peer";
28
+ /** Domain-separation tag prefixed to `X || Y` when hashing a user `networkId`. */
29
+ const DOMAIN_NETWORK_ID = "coordination-network/identity/v2/network-id";
26
30
  /**
27
31
  * Byte length of `nonce` and `networkId` in the binding messages — fixed at
28
- * 32 so the 64- vs 96-byte messages are injective.
32
+ * 32 so the 128-byte device and 96-byte account messages are injective.
29
33
  */
30
34
  const BINDING_NONCE_LENGTH = 32;
31
35
  /** SEC1-compressed secp256k1 public key length. */
@@ -546,6 +550,19 @@ function deriveAuthKeypair(sig64) {
546
550
  }
547
551
 
548
552
  /** Baby Jubjub EdDSA "fifth key" derivation — see `DERIVATION.md` §12. */
553
+ function deriveKeypair(ecdhSharedSecret, domain) {
554
+ const secret = normalizeEcdhSharedSecret(ecdhSharedSecret);
555
+ const seed = keccak_256(concatBytes(utf8ToBytes(domain), secret));
556
+ const secretScalar = deriveSecretScalar(seed);
557
+ const publicKey = derivePublicKey(seed);
558
+ const commitment = poseidon2(publicKey);
559
+ return {
560
+ seed: bytesToHex(seed),
561
+ secretScalar,
562
+ publicKey,
563
+ commitment
564
+ };
565
+ }
549
566
  /**
550
567
  * Derives the Baby Jubjub EdDSA keypair and Semaphore-v4 identity commitment
551
568
  * (the "fifth key"), rooted in a hardware ECDH shared secret.
@@ -560,17 +577,11 @@ function deriveAuthKeypair(sig64) {
560
577
  * @throws {InvalidEcdhSecretError} if `ecdhSharedSecret` is not exactly 32 bytes
561
578
  */
562
579
  function deriveEddsaKeypair(ecdhSharedSecret) {
563
- const secret = normalizeEcdhSharedSecret(ecdhSharedSecret);
564
- const seed = keccak_256(concatBytes(utf8ToBytes(DOMAIN_EDDSA), secret));
565
- const secretScalar = deriveSecretScalar(seed);
566
- const publicKey = derivePublicKey(seed);
567
- const commitment = poseidon2(publicKey);
568
- return {
569
- seed: bytesToHex(seed),
570
- secretScalar,
571
- publicKey,
572
- commitment
573
- };
580
+ return deriveKeypair(ecdhSharedSecret, DOMAIN_EDDSA);
581
+ }
582
+ /** Derives a separate Semaphore identity whose public commitment may join the paymaster group. */
583
+ function derivePaymasterKeypair(ecdhSharedSecret) {
584
+ return deriveKeypair(ecdhSharedSecret, DOMAIN_PAYMASTER);
574
585
  }
575
586
 
576
587
  /** Fixed NUMS P-256 peer point `Q` for the fifth-key ECDH — see `DERIVATION.md` §12. */
@@ -597,35 +608,46 @@ function computeEddsaPeerPoint() {
597
608
  return p256.ProjectivePoint.fromAffine(point.toAffine()).toRawBytes(false);
598
609
  }
599
610
 
600
- /**
601
- * Computes the bytes32 `networkId` from an uncompressed secp256k1 public key
602
- * (65-byte SEC1 `0x04`-prefixed or 64-byte raw `X || Y`).
603
- *
604
- * NORMATIVE: `networkId = keccak256(X || Y)` — the SEC1 prefix is stripped.
605
- * The last 20 bytes are the account's Ethereum address.
606
- */
607
- function computeNetworkId(uncompressedPublicKey) {
611
+ function publicKeyCoordinates(uncompressedPublicKey, caller) {
608
612
  const bytes = toBytes(uncompressedPublicKey);
609
- let coordinates;
610
613
  if (bytes.length === 65 && bytes[0] === 0x04) {
611
- coordinates = bytes.slice(1);
614
+ return bytes.slice(1);
612
615
  }
613
- else if (bytes.length === 64) {
614
- coordinates = bytes;
615
- }
616
- else {
617
- throw new TypeError("computeNetworkId: expected a 65-byte 0x04-prefixed or 64-byte raw uncompressed public key");
616
+ if (bytes.length === 64) {
617
+ return bytes;
618
618
  }
619
+ throw new TypeError(`${caller}: expected a 65-byte 0x04-prefixed or 64-byte raw uncompressed public key`);
620
+ }
621
+ /**
622
+ * Computes a user's bytes32 `networkId` from an uncompressed secp256k1 public
623
+ * key (65-byte SEC1 `0x04`-prefixed or 64-byte raw `X || Y`).
624
+ *
625
+ * NORMATIVE: `networkId = keccak256(utf8(DOMAIN_NETWORK_ID) || X || Y)` — the
626
+ * SEC1 prefix is stripped.
627
+ */
628
+ function computeNetworkId(uncompressedPublicKey) {
629
+ const coordinates = publicKeyCoordinates(uncompressedPublicKey, "computeNetworkId");
630
+ return bytesToHex(keccak_256(concatBytes(utf8ToBytes(DOMAIN_NETWORK_ID), coordinates)));
631
+ }
632
+ /**
633
+ * Computes a member's bytes32 id from the same public key forms.
634
+ *
635
+ * NORMATIVE: `memberNetworkId = keccak256(X || Y)`. The last 20 bytes are the
636
+ * member's auth address, which `NetworkMemberRegistry` reads.
637
+ */
638
+ function computeMemberNetworkId(uncompressedPublicKey) {
639
+ const coordinates = publicKeyCoordinates(uncompressedPublicKey, "computeMemberNetworkId");
619
640
  return bytesToHex(keccak_256(coordinates));
620
641
  }
621
642
 
622
643
  /**
623
- * Derives the full `v1` device identity — `networkId` plus the four-key
624
- * all-secp256k1 model — from a single device secret.
644
+ * Derives the full `v2` device identity — `networkId`, `memberNetworkId` and the account,
645
+ * spending, viewing and auth secp256k1 keypairs — from a single device secret.
625
646
  *
626
647
  * Pipeline (NORMATIVE):
627
648
  * 1. `account = deriveAccountKeypair(deviceSecret)`
628
- * 2. `networkId = computeNetworkId(account.uncompressedPublicKey)`
649
+ * 2. `networkId = computeNetworkId(account.uncompressedPublicKey)` and
650
+ * `memberNetworkId = computeMemberNetworkId(account.uncompressedPublicKey)`
629
651
  * 3. `sig = signDerivationMessage(account.privateKey)` (EIP-191, deterministic)
630
652
  * 4. `{ spending, viewing } = deriveStealthKeys(sig)` (FluidKey semantics)
631
653
  * 5. `auth = deriveAuthKeypair(sig)`
@@ -633,12 +655,14 @@ function computeNetworkId(uncompressedPublicKey) {
633
655
  function deriveKeys(deviceSecret) {
634
656
  const account = deriveAccountKeypair(deviceSecret);
635
657
  const networkId = computeNetworkId(account.uncompressedPublicKey);
658
+ const memberNetworkId = computeMemberNetworkId(account.uncompressedPublicKey);
636
659
  const signature = signDerivationMessage(account.privateKey);
637
660
  const { spending, viewing } = deriveStealthKeys(signature);
638
661
  const auth = deriveAuthKeypair(signature);
639
662
  return {
640
663
  version: KEY_DERIVATION_VERSION,
641
664
  networkId,
665
+ memberNetworkId,
642
666
  account,
643
667
  spending,
644
668
  viewing,
@@ -693,10 +717,15 @@ function computeConfirmationKeyHash(confirmationKey) {
693
717
  *
694
718
  * NORMATIVE layouts:
695
719
  * ```
696
- * deviceBindingMessage = nonce(32) || networkId(32) || holderKeyCommitment(32) // credential issuance
697
- * accountBindingMessage = nonce(32) || confirmationKeyHash(32) || networkId(32) // 96B, signed by A (secp256k1)
720
+ * deviceBindingMessage = nonce(32) || networkId(32) || holderKeyCommitment(32) || requestCommitment(32)
721
+ * accountBindingMessage = nonce(32) || confirmationKeyHash(32) || networkId(32)
698
722
  * ```
699
723
  *
724
+ * B (P-256) signs the 128-byte device message; A (secp256k1) signs the 96-byte account message.
725
+ *
726
+ * `requestCommitment` is opaque here: the credential layer defines it and
727
+ * binds the issuer and scope shown at consent into it.
728
+ *
700
729
  * NORMATIVE MUST: the device signs a locally-derived `networkId`, never one
701
730
  * supplied by the issuance request. Every function here except the two
702
731
  * low-level builders and {@link verifyDeviceBindingSignature} enforces this
@@ -710,6 +739,12 @@ function toBindingBytes(value, label) {
710
739
  throw new InvalidBindingInputError(`${label}: expected raw bytes or a well-formed 0x-prefixed hex string`);
711
740
  }
712
741
  }
742
+ function toBindingHash32(value, label) {
743
+ const bytes = toBindingBytes(value, label);
744
+ if (bytes.length !== 32)
745
+ throw new InvalidBindingInputError(`${label}: expected exactly 32 bytes`);
746
+ return bytes;
747
+ }
713
748
  function decodeAndValidateNonceAndNetworkId(nonce, networkId) {
714
749
  const nonceBytes = toBindingBytes(nonce, "nonce");
715
750
  const networkIdBytes = toBindingBytes(networkId, "networkId");
@@ -774,19 +809,17 @@ function parseDeviceSignature(signature) {
774
809
  throw new InvalidBindingInputError("deviceSignature: expected a 64-byte compact (r || s) or DER-encoded P-256 signature");
775
810
  }
776
811
  /**
777
- * Builds `nonce(32) || networkId(32)`, optionally followed by a 32-byte holder commitment.
812
+ * Builds the 128-byte `deviceBindingMessage`:
813
+ * `nonce(32) || networkId(32) || holderKeyCommitment(32) || requestCommitment(32)`.
778
814
  *
779
815
  * Low-level, spec-literal builder — prefer {@link buildBindingMessages},
780
816
  * which enforces the locally-derived-`networkId` MUST structurally.
781
817
  */
782
- function buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment) {
818
+ function buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment, requestCommitment) {
783
819
  const { nonceBytes, networkIdBytes } = decodeAndValidateNonceAndNetworkId(nonce, networkId);
784
- if (holderKeyCommitment === undefined)
785
- return concatBytes(nonceBytes, networkIdBytes);
786
- const holderBytes = toBindingBytes(holderKeyCommitment, "holderKeyCommitment");
787
- if (holderBytes.length !== 32)
788
- throw new InvalidBindingInputError("holderKeyCommitment: expected exactly 32 bytes");
789
- return concatBytes(nonceBytes, networkIdBytes, holderBytes);
820
+ const holderBytes = toBindingHash32(holderKeyCommitment, "holderKeyCommitment");
821
+ const requestBytes = toBindingHash32(requestCommitment, "requestCommitment");
822
+ return concatBytes(nonceBytes, networkIdBytes, holderBytes, requestBytes);
790
823
  }
791
824
  /**
792
825
  * Builds the 96-byte `accountBindingMessage`:
@@ -805,7 +838,7 @@ function buildAccountBindingMessage(nonce, confirmationKey, networkId) {
805
838
  * derived internally from the account's own public key, so a caller cannot
806
839
  * supply a `networkId` from elsewhere even by mistake.
807
840
  */
808
- function buildBindingMessages(nonce, accountUncompressedPublicKey, confirmationKey, holderKeyCommitment) {
841
+ function buildBindingMessages(nonce, accountUncompressedPublicKey, confirmationKey, holderKeyCommitment, requestCommitment) {
809
842
  let networkId;
810
843
  try {
811
844
  networkId = computeNetworkId(accountUncompressedPublicKey);
@@ -813,7 +846,7 @@ function buildBindingMessages(nonce, accountUncompressedPublicKey, confirmationK
813
846
  catch {
814
847
  throw new InvalidBindingInputError("accountUncompressedPublicKey: expected a 65-byte 0x04-prefixed or 64-byte raw uncompressed secp256k1 public key");
815
848
  }
816
- const deviceBindingMessage = buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment);
849
+ const deviceBindingMessage = buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment, requestCommitment);
817
850
  const accountBindingMessage = buildAccountBindingMessage(nonce, confirmationKey, networkId);
818
851
  return { networkId, deviceBindingMessage, accountBindingMessage };
819
852
  }
@@ -856,8 +889,8 @@ function verifyAccountBindingSignature({ nonce, confirmationKey, accountPublicKe
856
889
  * `prehash: true` (signers hash SHA-256 internally); `lowS: false` (hardware
857
890
  * signatures aren't s-normalized — `DERIVATION.md` §11.4).
858
891
  */
859
- function verifyDeviceBindingSignature({ nonce, networkId, confirmationKey, holderKeyCommitment, signature }) {
860
- const message = buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment);
892
+ function verifyDeviceBindingSignature({ nonce, networkId, confirmationKey, holderKeyCommitment, requestCommitment, signature }) {
893
+ const message = buildDeviceBindingMessage(nonce, networkId, holderKeyCommitment, requestCommitment);
861
894
  const publicKeyBytes = prependUncompressedPrefix(normalizeConfirmationKey(confirmationKey));
862
895
  const parsedSignature = parseDeviceSignature(signature);
863
896
  return p256.verify(parsedSignature.toCompactRawBytes(), message, publicKeyBytes, {
@@ -871,7 +904,7 @@ function verifyDeviceBindingSignature({ nonce, networkId, confirmationKey, holde
871
904
  * with an unrelated account signature in either direction. `networkId` is
872
905
  * returned even when `valid` is false, for issuer-side logging/rate-limiting.
873
906
  */
874
- function verifyCredentialBinding({ nonce, accountPublicKey, confirmationKey, holderKeyCommitment, accountSignature, deviceSignature }) {
907
+ function verifyCredentialBinding({ nonce, accountPublicKey, confirmationKey, holderKeyCommitment, requestCommitment, accountSignature, deviceSignature }) {
875
908
  const publicKeyBytes = normalizeAccountPublicKey(accountPublicKey);
876
909
  const networkId = computeNetworkId(publicKeyBytes);
877
910
  const accountValid = verifyAccountBindingSignature({
@@ -885,9 +918,10 @@ function verifyCredentialBinding({ nonce, accountPublicKey, confirmationKey, hol
885
918
  networkId,
886
919
  confirmationKey,
887
920
  holderKeyCommitment,
921
+ requestCommitment,
888
922
  signature: deviceSignature
889
923
  });
890
924
  return { valid: accountValid && deviceValid, networkId };
891
925
  }
892
926
 
893
- export { BINDING_NONCE_LENGTH, COMPRESSED_PUBLIC_KEY_LENGTH, DOMAIN_ACCOUNT, DOMAIN_AUTH, DOMAIN_EDDSA, DOMAIN_EDDSA_PEER, DerivationFailureError, EDDSA_PEER_POINT_HEX, ERC_5564_SCHEME_ID, FIXED_DOMAIN_MSG, InvalidBindingInputError, InvalidDeviceSecretError, InvalidEcdhSecretError, InvalidMetaStealthAddressError, InvalidPublicKeyLengthError, KEY_DERIVATION_VERSION, KeygenError, META_STEALTH_ADDRESS_LENGTH, META_STEALTH_URI_PREFIX, base64urlToBytes, buildAccountBindingMessage, buildAnnouncement, buildBindingMessages, buildDeviceBindingMessage, bytesToBase64url, bytesToHex, checkStealthAddress, computeConfirmationKeyHash, computeEddsaPeerPoint, computeNetworkId, computeSharedSecret, decodeMetaStealthAddress, deriveAccountKeypair, deriveAuthKeypair, deriveEddsaKeypair, deriveKeys, deriveNetworkId, deriveStealthKeys, eddsaPeerPointBytes, encodeMetaStealthAddress, encodeMetaStealthAddressUri, generateDeviceSecret, generateEphemeralKey, generateStealthAddress, generateStealthPrivateKey, getStealthPublicKey, getViewTag, hashToScalar, hashToScalarWith, hexToBytes, isValidScalar, metaStealthAddressUri, normalizeConfirmationKey, normalizeDeviceSecret, normalizeEcdhSharedSecret, publicKeyToAddress, scalarToKeypair, signAccountBinding, signDerivationMessage, validateMetaStealthAddress, verifyAccountBindingSignature, verifyCredentialBinding, verifyDeviceBindingSignature };
927
+ export { BINDING_NONCE_LENGTH, COMPRESSED_PUBLIC_KEY_LENGTH, DOMAIN_ACCOUNT, DOMAIN_AUTH, DOMAIN_EDDSA, DOMAIN_EDDSA_PEER, DOMAIN_NETWORK_ID, DOMAIN_PAYMASTER, DerivationFailureError, EDDSA_PEER_POINT_HEX, ERC_5564_SCHEME_ID, FIXED_DOMAIN_MSG, InvalidBindingInputError, InvalidDeviceSecretError, InvalidEcdhSecretError, InvalidMetaStealthAddressError, InvalidPublicKeyLengthError, KEY_DERIVATION_VERSION, KeygenError, META_STEALTH_ADDRESS_LENGTH, META_STEALTH_URI_PREFIX, base64urlToBytes, buildAccountBindingMessage, buildAnnouncement, buildBindingMessages, buildDeviceBindingMessage, bytesToBase64url, bytesToHex, checkStealthAddress, computeConfirmationKeyHash, computeEddsaPeerPoint, computeMemberNetworkId, computeNetworkId, computeSharedSecret, decodeMetaStealthAddress, deriveAccountKeypair, deriveAuthKeypair, deriveEddsaKeypair, deriveKeys, deriveNetworkId, derivePaymasterKeypair, deriveStealthKeys, eddsaPeerPointBytes, encodeMetaStealthAddress, encodeMetaStealthAddressUri, generateDeviceSecret, generateEphemeralKey, generateStealthAddress, generateStealthPrivateKey, getStealthPublicKey, getViewTag, hashToScalar, hashToScalarWith, hexToBytes, isValidScalar, metaStealthAddressUri, normalizeConfirmationKey, normalizeDeviceSecret, normalizeEcdhSharedSecret, publicKeyToAddress, scalarToKeypair, signAccountBinding, signDerivationMessage, validateMetaStealthAddress, verifyAccountBindingSignature, verifyCredentialBinding, verifyDeviceBindingSignature };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coordination-network/identity",
3
- "version": "0.0.2",
3
+ "version": "0.1.1",
4
4
  "description": "Deterministic device key derivation for the coordination network: networkId and the four-key secp256k1 model (account, spending, viewing, auth) from one device secret, plus a hardware-rooted Baby Jubjub EdDSA/Semaphore-commitment fifth key.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,7 +46,7 @@
46
46
  "@fluidkey/stealth-account-kit": "1.1.1",
47
47
  "@rollup/plugin-typescript": "^12.3.0",
48
48
  "@scopelift/stealth-address-sdk": "^1.0.0-beta.5",
49
- "@semaphore-protocol/identity": "4.14.3",
49
+ "@semaphore-protocol/identity": "4.14.5",
50
50
  "rimraf": "^6.1.3",
51
51
  "rollup": "^4.62.2",
52
52
  "rollup-plugin-cleanup": "^3.2.1",