@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/DERIVATION.md +46 -30
- package/README.md +84 -72
- package/dist/index.cjs +85 -47
- package/dist/index.js +82 -48
- package/package.json +2 -2
- package/src/binding.ts +31 -13
- package/src/constants.ts +9 -3
- package/src/derive.ts +7 -4
- package/src/eddsa.ts +22 -13
- package/src/index.ts +2 -2
- package/src/network-id.ts +31 -14
- package/src/types.ts +5 -3
- package/types/index.d.ts +47 -23
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 `
|
|
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 = "
|
|
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
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
616
|
+
return bytes.slice(1);
|
|
614
617
|
}
|
|
615
|
-
|
|
616
|
-
|
|
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 `
|
|
626
|
-
*
|
|
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)
|
|
699
|
-
* accountBindingMessage = nonce(32) || confirmationKeyHash(32) || networkId(32)
|
|
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
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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 `
|
|
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 = "
|
|
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
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
614
|
+
return bytes.slice(1);
|
|
612
615
|
}
|
|
613
|
-
|
|
614
|
-
|
|
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 `
|
|
624
|
-
*
|
|
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)
|
|
697
|
-
* accountBindingMessage = nonce(32) || confirmationKeyHash(32) || networkId(32)
|
|
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
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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.
|
|
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.
|
|
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",
|