@calimero-network/mero-js 26.11.0 → 27.0.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 +73 -288
- package/dist/cloud-v2/attestation.d.ts +91 -0
- package/dist/cloud-v2/attestation.js +135 -0
- package/dist/cloud-v2/capabilities.d.ts +30 -0
- package/dist/cloud-v2/capabilities.js +31 -0
- package/dist/cloud-v2/client.d.ts +426 -0
- package/dist/cloud-v2/client.js +646 -0
- package/dist/cloud-v2/errors.d.ts +98 -0
- package/dist/cloud-v2/errors.js +165 -0
- package/dist/cloud-v2/index.d.ts +10 -0
- package/dist/cloud-v2/index.js +5 -0
- package/dist/cloud-v2/passkey.d.ts +27 -0
- package/dist/cloud-v2/passkey.js +73 -0
- package/dist/crypto/internal.d.ts +3 -37
- package/dist/crypto/internal.js +3 -72
- package/dist/index.browser.mjs +4 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +1750 -12772
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +3 -56
- package/dist/index.js +14 -75
- package/dist/index.mjs +1750 -12772
- package/dist/index.mjs.map +4 -4
- package/dist/sealed/index.d.ts +4 -4
- package/dist/sealed/index.js +2 -2
- package/dist/sealed/release.d.ts +21 -76
- package/dist/sealed/release.js +24 -108
- package/dist/sealed/sealed.d.ts +8 -56
- package/dist/sealed/sealed.js +11 -89
- package/dist/sealed/verify.d.ts +7 -13
- package/dist/sealed/verify.js +3 -7
- package/package.json +8 -13
- package/dist/account/account-admin.d.ts +0 -80
- package/dist/account/account-admin.js +0 -346
- package/dist/account/account.d.ts +0 -196
- package/dist/account/account.js +0 -269
- package/dist/account/application-id.d.ts +0 -50
- package/dist/account/application-id.js +0 -83
- package/dist/account/bootstrap-from-invitation.d.ts +0 -110
- package/dist/account/bootstrap-from-invitation.js +0 -80
- package/dist/account/create-context.d.ts +0 -103
- package/dist/account/create-context.js +0 -267
- package/dist/account/govern.d.ts +0 -24
- package/dist/account/govern.js +0 -50
- package/dist/account/index.d.ts +0 -26
- package/dist/account/index.js +0 -15
- package/dist/account/join-as-account.d.ts +0 -23
- package/dist/account/join-as-account.js +0 -34
- package/dist/account/join-with-node.d.ts +0 -115
- package/dist/account/join-with-node.js +0 -161
- package/dist/account/node-admin.d.ts +0 -20
- package/dist/account/node-admin.js +0 -33
- package/dist/account/relay-from-invitation.d.ts +0 -137
- package/dist/account/relay-from-invitation.js +0 -180
- package/dist/account/relay-login-error.d.ts +0 -32
- package/dist/account/relay-login-error.js +0 -34
- package/dist/account/session.d.ts +0 -266
- package/dist/account/session.js +0 -915
- package/dist/account/wordlist.d.ts +0 -16
- package/dist/account/wordlist.js +0 -273
- package/dist/admin-api/admin-client.d.ts +0 -688
- package/dist/admin-api/admin-client.js +0 -1230
- package/dist/admin-api/admin-factory.d.ts +0 -14
- package/dist/admin-api/admin-factory.js +0 -22
- package/dist/admin-api/admin-types.d.ts +0 -2046
- package/dist/admin-api/admin-types.js +0 -12
- package/dist/admin-api/index.d.ts +0 -4
- package/dist/admin-api/index.js +0 -5
- package/dist/admin-api/redeem-invitation.d.ts +0 -101
- package/dist/admin-api/redeem-invitation.js +0 -200
- package/dist/auth/index.d.ts +0 -34
- package/dist/auth/index.js +0 -67
- package/dist/auth-api/auth-client.d.ts +0 -31
- package/dist/auth-api/auth-client.js +0 -117
- package/dist/auth-api/auth-factory.d.ts +0 -7
- package/dist/auth-api/auth-factory.js +0 -41
- package/dist/auth-api/auth-types.d.ts +0 -130
- package/dist/auth-api/auth-types.js +0 -2
- package/dist/auth-api/index.d.ts +0 -3
- package/dist/auth-api/index.js +0 -4
- package/dist/capabilities.d.ts +0 -84
- package/dist/capabilities.js +0 -117
- package/dist/cloud/account-ownership.d.ts +0 -63
- package/dist/cloud/account-ownership.js +0 -87
- package/dist/cloud/cloud-client.d.ts +0 -949
- package/dist/cloud/cloud-client.js +0 -1068
- package/dist/cloud/connect.d.ts +0 -213
- package/dist/cloud/connect.js +0 -260
- package/dist/cloud/enrol-redirect.d.ts +0 -181
- package/dist/cloud/enrol-redirect.js +0 -244
- package/dist/cloud/index.d.ts +0 -12
- package/dist/cloud/index.js +0 -10
- package/dist/cloud/link-redirect.d.ts +0 -95
- package/dist/cloud/link-redirect.js +0 -121
- package/dist/cloud/link-state.d.ts +0 -36
- package/dist/cloud/link-state.js +0 -104
- package/dist/cloud/routing-proof.d.ts +0 -113
- package/dist/cloud/routing-proof.js +0 -96
- package/dist/device-cert/device-cert.d.ts +0 -189
- package/dist/device-cert/device-cert.js +0 -239
- package/dist/device-cert/device-scope.d.ts +0 -58
- package/dist/device-cert/device-scope.js +0 -76
- package/dist/device-cert/index.d.ts +0 -4
- package/dist/device-cert/index.js +0 -3
- package/dist/ephemeral/codec.d.ts +0 -4
- package/dist/ephemeral/codec.js +0 -12
- package/dist/ephemeral/index.d.ts +0 -57
- package/dist/ephemeral/index.js +0 -101
- package/dist/ephemeral/types.d.ts +0 -27
- package/dist/ephemeral/types.js +0 -1
- package/dist/errors/classify.d.ts +0 -65
- package/dist/errors/classify.js +0 -117
- package/dist/errors/index.d.ts +0 -2
- package/dist/errors/index.js +0 -1
- package/dist/events/group.d.ts +0 -95
- package/dist/events/group.js +0 -10
- package/dist/events/index.d.ts +0 -5
- package/dist/events/index.js +0 -2
- package/dist/events/sse.d.ts +0 -127
- package/dist/events/sse.js +0 -469
- package/dist/events/ws.d.ts +0 -57
- package/dist/events/ws.js +0 -212
- package/dist/http-client/api-response.d.ts +0 -15
- package/dist/http-client/api-response.js +0 -1
- package/dist/http-client/http-factory.d.ts +0 -71
- package/dist/http-client/http-factory.js +0 -68
- package/dist/http-client/http-types.d.ts +0 -87
- package/dist/http-client/http-types.js +0 -1
- package/dist/http-client/index.d.ts +0 -7
- package/dist/http-client/index.js +0 -11
- package/dist/http-client/path-segment.d.ts +0 -1
- package/dist/http-client/path-segment.js +0 -8
- package/dist/http-client/retry.d.ts +0 -5
- package/dist/http-client/retry.js +0 -82
- package/dist/http-client/signal-utils.d.ts +0 -2
- package/dist/http-client/signal-utils.js +0 -41
- package/dist/http-client/web-client.d.ts +0 -80
- package/dist/http-client/web-client.js +0 -557
- package/dist/invitation/index.d.ts +0 -2
- package/dist/invitation/index.js +0 -2
- package/dist/invitation/invitation.d.ts +0 -106
- package/dist/invitation/invitation.js +0 -104
- package/dist/login/index.d.ts +0 -4
- package/dist/login/index.js +0 -3
- package/dist/login/login.d.ts +0 -111
- package/dist/login/login.js +0 -110
- package/dist/login/session.d.ts +0 -124
- package/dist/login/session.js +0 -141
- package/dist/mero-js.d.ts +0 -231
- package/dist/mero-js.js +0 -469
- package/dist/namespace-op/index.d.ts +0 -2
- package/dist/namespace-op/index.js +0 -3
- package/dist/namespace-op/namespace-op.d.ts +0 -150
- package/dist/namespace-op/namespace-op.js +0 -191
- package/dist/nodeDiscovery.d.ts +0 -47
- package/dist/nodeDiscovery.js +0 -101
- package/dist/owner-op/index.d.ts +0 -2
- package/dist/owner-op/index.js +0 -3
- package/dist/owner-op/owner-op.d.ts +0 -119
- package/dist/owner-op/owner-op.js +0 -214
- package/dist/presence/relay-presence.d.ts +0 -56
- package/dist/presence/relay-presence.js +0 -107
- package/dist/presence/statement.d.ts +0 -12
- package/dist/presence/statement.js +0 -17
- package/dist/relay/index.d.ts +0 -4
- package/dist/relay/index.js +0 -2
- package/dist/relay/nonce-source.d.ts +0 -72
- package/dist/relay/nonce-source.js +0 -111
- package/dist/relay/relay-client.d.ts +0 -472
- package/dist/relay/relay-client.js +0 -518
- package/dist/relay-attestation/attest-node-key.d.ts +0 -66
- package/dist/relay-attestation/attest-node-key.js +0 -110
- package/dist/relay-attestation/index.d.ts +0 -1
- package/dist/relay-attestation/index.js +0 -1
- package/dist/request/caller-proof.d.ts +0 -43
- package/dist/request/caller-proof.js +0 -36
- package/dist/request/index.d.ts +0 -3
- package/dist/request/index.js +0 -3
- package/dist/request/proof-signer.d.ts +0 -49
- package/dist/request/proof-signer.js +0 -59
- package/dist/request/request-sig.d.ts +0 -99
- package/dist/request/request-sig.js +0 -95
- package/dist/request-proof/authorizer.d.ts +0 -41
- package/dist/request-proof/authorizer.js +0 -60
- package/dist/request-proof/index.d.ts +0 -4
- package/dist/request-proof/index.js +0 -2
- package/dist/request-proof/request-proof.d.ts +0 -53
- package/dist/request-proof/request-proof.js +0 -95
- package/dist/rpc/index.d.ts +0 -64
- package/dist/rpc/index.js +0 -78
- package/dist/signer/index.d.ts +0 -2
- package/dist/signer/index.js +0 -3
- package/dist/signer/signer.d.ts +0 -92
- package/dist/signer/signer.js +0 -147
- package/dist/token-store/index.d.ts +0 -27
- package/dist/token-store/index.js +0 -62
- package/dist/transport/index.d.ts +0 -6
- package/dist/transport/index.js +0 -3
- package/dist/transport/mero-client.d.ts +0 -294
- package/dist/transport/mero-client.js +0 -386
- package/dist/transport/relay-observer.d.ts +0 -152
- package/dist/transport/relay-observer.js +0 -173
- package/dist/transport/relay-transport.d.ts +0 -73
- package/dist/transport/relay-transport.js +0 -94
- package/dist/transport/types.d.ts +0 -100
- package/dist/transport/types.js +0 -32
- package/dist/types/api-response.d.ts +0 -15
- package/dist/types/api-response.js +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/warrant/creation-warrant.d.ts +0 -163
- package/dist/warrant/creation-warrant.js +0 -194
- package/dist/warrant/governance-op.d.ts +0 -202
- package/dist/warrant/governance-op.js +0 -355
- package/dist/warrant/governance-warrant.d.ts +0 -124
- package/dist/warrant/governance-warrant.js +0 -164
- package/dist/warrant/index.d.ts +0 -8
- package/dist/warrant/index.js +0 -5
- package/dist/warrant/warrant.d.ts +0 -188
- package/dist/warrant/warrant.js +0 -202
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Proving an account to the cloud for the one read a joiner cannot avoid.
|
|
3
|
-
*
|
|
4
|
-
* `GET /api/cloud/namespaces/{ns}/admitters` answers where to send a signed
|
|
5
|
-
* membership op. A joiner needs it and *cannot* hold a cloud login: it is by
|
|
6
|
-
* construction not the namespace owner, and not needing an account on the cloud
|
|
7
|
-
* is the entire point of the path — a keyholder mints a root, certifies a
|
|
8
|
-
* device and signs its own join offline, and this read is the only thing that
|
|
9
|
-
* tells it where to post the result.
|
|
10
|
-
*
|
|
11
|
-
* So the caller proves possession of the credential it already holds. The cloud
|
|
12
|
-
* mints a sealed, namespace-bound nonce; the device signs it; the read carries
|
|
13
|
-
* the certificate and that signature.
|
|
14
|
-
*
|
|
15
|
-
* ## What this establishes, and what it does not
|
|
16
|
-
*
|
|
17
|
-
* It establishes that the caller holds a device key certified by some account
|
|
18
|
-
* root, and *which* account that is. It does **not** establish that the account
|
|
19
|
-
* was invited to the namespace, nor that it is a member — the cloud cannot
|
|
20
|
-
* check either, because membership is governance state held by the nodes.
|
|
21
|
-
* Anyone can mint an account root offline, so this is not a wall. It buys
|
|
22
|
-
* attribution (every read names an account and can be rate-limited) and it ends
|
|
23
|
-
* anonymous bulk discovery. Authorization stays where it always was: at the
|
|
24
|
-
* node, on the signed op.
|
|
25
|
-
*
|
|
26
|
-
* ## Why the DEVICE signs, not the root
|
|
27
|
-
*
|
|
28
|
-
* The opposite choice from {@link signAccountLink}, deliberately. That one
|
|
29
|
-
* exists for the device-loss case, so only the root will do. This one runs on
|
|
30
|
-
* every routing read from a browser that discards the root immediately after
|
|
31
|
-
* certifying its device — requiring the root would mean re-entering a 24-word
|
|
32
|
-
* phrase to look up a URL.
|
|
33
|
-
*
|
|
34
|
-
* The certificate alone is not enough and the signature is not redundant with
|
|
35
|
-
* it: a certificate travels in the clear inside every device-link op, so anyone
|
|
36
|
-
* who has seen one can replay it. Only the nonce signature binds the presenter
|
|
37
|
-
* to the device.
|
|
38
|
-
*/
|
|
39
|
-
/**
|
|
40
|
-
* The credential a routing read presents: a certificate, and the key to sign
|
|
41
|
-
* the challenge with.
|
|
42
|
-
*
|
|
43
|
-
* `credential` is the 237-byte `AccountProof<DeviceCert>` as hex — exactly what
|
|
44
|
-
* {@link signDeviceCert} returns, and the same string a login statement and a
|
|
45
|
-
* warrant carry. It is public by construction.
|
|
46
|
-
*
|
|
47
|
-
* `deviceSecret` never leaves the process. It signs the challenge locally and
|
|
48
|
-
* only the signature travels.
|
|
49
|
-
*/
|
|
50
|
-
export interface RoutingCredential {
|
|
51
|
-
/** The `AccountProof<DeviceCert>`, hex borsh, as `signDeviceCert` returns it. */
|
|
52
|
-
credential: string;
|
|
53
|
-
/** The certified device's ed25519 signing secret, hex (32 bytes). */
|
|
54
|
-
deviceSecret: string;
|
|
55
|
-
}
|
|
56
|
-
/** A challenge to sign, as the cloud minted it. */
|
|
57
|
-
export interface RoutingChallenge {
|
|
58
|
-
namespaceId: string;
|
|
59
|
-
/** Opaque and sealed — the client neither parses nor constructs this. */
|
|
60
|
-
nonce: string;
|
|
61
|
-
/** Epoch **milliseconds**, as MDMA reports it. Nonces live about two minutes. */
|
|
62
|
-
expiresAtMs: number;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* A challenge bound to an ACCOUNT rather than a namespace, for asking which
|
|
66
|
-
* relays serve that account.
|
|
67
|
-
*
|
|
68
|
-
* Separate from {@link RoutingChallenge} because the binding is the security
|
|
69
|
-
* property, not a field name: MDMA seals the account into this one and the
|
|
70
|
-
* namespace into that one, and refuses each at the other's route. Keeping the
|
|
71
|
-
* TypeScript types distinct is what stops a caller passing the wrong one and
|
|
72
|
-
* learning about it as a 403.
|
|
73
|
-
*/
|
|
74
|
-
export interface DiscoveryChallenge {
|
|
75
|
-
accountId: string;
|
|
76
|
-
/** Opaque and sealed — the client neither parses nor constructs this. */
|
|
77
|
-
nonce: string;
|
|
78
|
-
/** Epoch **milliseconds**, as MDMA reports it. Nonces live about two minutes. */
|
|
79
|
-
expiresAtMs: number;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* The three header values a proven routing read carries.
|
|
83
|
-
*
|
|
84
|
-
* Intersected with `Record<string, string>` rather than declared as a plain
|
|
85
|
-
* interface so it drops straight into a headers bag: an interface has no index
|
|
86
|
-
* signature, so TypeScript refuses it where `Record<string, string>` is wanted
|
|
87
|
-
* even though every field is a string.
|
|
88
|
-
*/
|
|
89
|
-
export type RoutingProofHeaders = Record<string, string> & {
|
|
90
|
-
'X-Calimero-Credential': string;
|
|
91
|
-
'X-Calimero-Nonce': string;
|
|
92
|
-
'X-Calimero-Signature': string;
|
|
93
|
-
};
|
|
94
|
-
/**
|
|
95
|
-
* Sign a routing challenge with the device key.
|
|
96
|
-
*
|
|
97
|
-
* Returns base64, which is what the header carries and what MDMA's verifier
|
|
98
|
-
* decodes — the one place this SDK does not spell a signature as hex, because
|
|
99
|
-
* the wire format is not ours to choose.
|
|
100
|
-
*/
|
|
101
|
-
export declare function signRoutingChallenge(nonce: string, deviceSecret: string): Promise<string>;
|
|
102
|
-
/**
|
|
103
|
-
* Build the headers for one routing read, from a challenge the cloud minted.
|
|
104
|
-
*
|
|
105
|
-
* Separated from the request so a caller that fetches its own challenge — a
|
|
106
|
-
* test, or a client batching several reads against one nonce — can use the same
|
|
107
|
-
* encoding rather than a second spelling of it.
|
|
108
|
-
*
|
|
109
|
-
* Takes either challenge kind: only the nonce is signed and both are signed
|
|
110
|
-
* over the same domain, so one encoder serves both. What separates them is what
|
|
111
|
-
* MDMA sealed inside, which is checked server-side.
|
|
112
|
-
*/
|
|
113
|
-
export declare function routingProofHeaders(challenge: RoutingChallenge | DiscoveryChallenge, credential: RoutingCredential): Promise<RoutingProofHeaders>;
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Proving an account to the cloud for the one read a joiner cannot avoid.
|
|
3
|
-
*
|
|
4
|
-
* `GET /api/cloud/namespaces/{ns}/admitters` answers where to send a signed
|
|
5
|
-
* membership op. A joiner needs it and *cannot* hold a cloud login: it is by
|
|
6
|
-
* construction not the namespace owner, and not needing an account on the cloud
|
|
7
|
-
* is the entire point of the path — a keyholder mints a root, certifies a
|
|
8
|
-
* device and signs its own join offline, and this read is the only thing that
|
|
9
|
-
* tells it where to post the result.
|
|
10
|
-
*
|
|
11
|
-
* So the caller proves possession of the credential it already holds. The cloud
|
|
12
|
-
* mints a sealed, namespace-bound nonce; the device signs it; the read carries
|
|
13
|
-
* the certificate and that signature.
|
|
14
|
-
*
|
|
15
|
-
* ## What this establishes, and what it does not
|
|
16
|
-
*
|
|
17
|
-
* It establishes that the caller holds a device key certified by some account
|
|
18
|
-
* root, and *which* account that is. It does **not** establish that the account
|
|
19
|
-
* was invited to the namespace, nor that it is a member — the cloud cannot
|
|
20
|
-
* check either, because membership is governance state held by the nodes.
|
|
21
|
-
* Anyone can mint an account root offline, so this is not a wall. It buys
|
|
22
|
-
* attribution (every read names an account and can be rate-limited) and it ends
|
|
23
|
-
* anonymous bulk discovery. Authorization stays where it always was: at the
|
|
24
|
-
* node, on the signed op.
|
|
25
|
-
*
|
|
26
|
-
* ## Why the DEVICE signs, not the root
|
|
27
|
-
*
|
|
28
|
-
* The opposite choice from {@link signAccountLink}, deliberately. That one
|
|
29
|
-
* exists for the device-loss case, so only the root will do. This one runs on
|
|
30
|
-
* every routing read from a browser that discards the root immediately after
|
|
31
|
-
* certifying its device — requiring the root would mean re-entering a 24-word
|
|
32
|
-
* phrase to look up a URL.
|
|
33
|
-
*
|
|
34
|
-
* The certificate alone is not enough and the signature is not redundant with
|
|
35
|
-
* it: a certificate travels in the clear inside every device-link op, so anyone
|
|
36
|
-
* who has seen one can replay it. Only the nonce signature binds the presenter
|
|
37
|
-
* to the device.
|
|
38
|
-
*/
|
|
39
|
-
import { concat, fromHex, importSigningKey } from '../crypto/internal.js';
|
|
40
|
-
/**
|
|
41
|
-
* The signing domain, followed immediately by the nonce as UTF-8.
|
|
42
|
-
*
|
|
43
|
-
* A flat prefix rather than this SDK's usual length-prefixed `domainHash`,
|
|
44
|
-
* because the verifier is MDMA's `verify_routing_proof` and it checks
|
|
45
|
-
* `DOMAIN ‖ nonce` against the raw message. The trailing NUL is part of the
|
|
46
|
-
* domain string on both sides; it is what stops a domain that is a prefix of
|
|
47
|
-
* some future one from colliding with it.
|
|
48
|
-
*/
|
|
49
|
-
const ROUTING_PROOF_DOMAIN = new TextEncoder().encode('calimero.mdma.routing-read.v1\0');
|
|
50
|
-
/**
|
|
51
|
-
* Sign a routing challenge with the device key.
|
|
52
|
-
*
|
|
53
|
-
* Returns base64, which is what the header carries and what MDMA's verifier
|
|
54
|
-
* decodes — the one place this SDK does not spell a signature as hex, because
|
|
55
|
-
* the wire format is not ours to choose.
|
|
56
|
-
*/
|
|
57
|
-
export async function signRoutingChallenge(nonce, deviceSecret) {
|
|
58
|
-
// Validate the secret the same way every other signing path does, so a
|
|
59
|
-
// truncated or mis-cased key fails here rather than as a 403 from the cloud.
|
|
60
|
-
fromHex(deviceSecret, 'deviceSecret', 32);
|
|
61
|
-
const key = await importSigningKey(deviceSecret);
|
|
62
|
-
const message = concat(ROUTING_PROOF_DOMAIN, new TextEncoder().encode(nonce));
|
|
63
|
-
const signature = new Uint8Array(await crypto.subtle.sign({ name: 'Ed25519' }, key, message));
|
|
64
|
-
return base64(signature);
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Build the headers for one routing read, from a challenge the cloud minted.
|
|
68
|
-
*
|
|
69
|
-
* Separated from the request so a caller that fetches its own challenge — a
|
|
70
|
-
* test, or a client batching several reads against one nonce — can use the same
|
|
71
|
-
* encoding rather than a second spelling of it.
|
|
72
|
-
*
|
|
73
|
-
* Takes either challenge kind: only the nonce is signed and both are signed
|
|
74
|
-
* over the same domain, so one encoder serves both. What separates them is what
|
|
75
|
-
* MDMA sealed inside, which is checked server-side.
|
|
76
|
-
*/
|
|
77
|
-
export async function routingProofHeaders(challenge, credential) {
|
|
78
|
-
return {
|
|
79
|
-
'X-Calimero-Credential': credential.credential,
|
|
80
|
-
'X-Calimero-Nonce': challenge.nonce,
|
|
81
|
-
'X-Calimero-Signature': await signRoutingChallenge(challenge.nonce, credential.deviceSecret),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* base64, the encoding the header carries.
|
|
86
|
-
*
|
|
87
|
-
* `btoa` is global in every browser and in Node since 16, and this package
|
|
88
|
-
* requires Node 18 — so there is no runtime here that needs a `Buffer`
|
|
89
|
-
* fallback, and adding one would pull a Node-only global into browser code.
|
|
90
|
-
*/
|
|
91
|
-
function base64(bytes) {
|
|
92
|
-
let binary = '';
|
|
93
|
-
for (const byte of bytes)
|
|
94
|
-
binary += String.fromCharCode(byte);
|
|
95
|
-
return btoa(binary);
|
|
96
|
-
}
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { type Signer } from '../signer/signer.js';
|
|
2
|
-
/**
|
|
3
|
-
* Core's `ACCOUNT_GENESIS_VERSION`. It is part of the `AccountId` preimage, so
|
|
4
|
-
* this value decides which account a root key names — a mismatch does not fail
|
|
5
|
-
* as a version error at the edge, it silently derives a different account and
|
|
6
|
-
* core refuses the credential as unverifiable.
|
|
7
|
-
*
|
|
8
|
-
* `2` since the genesis dropped its per-namespace nonce. This was `1`, which
|
|
9
|
-
* produced credentials current core rejects outright; the shape was already v2,
|
|
10
|
-
* only the tag was stale.
|
|
11
|
-
*/
|
|
12
|
-
export declare const ACCOUNT_GENESIS_VERSION = 2;
|
|
13
|
-
/** What a root certifies about one device. */
|
|
14
|
-
export interface DeviceCertInput {
|
|
15
|
-
/**
|
|
16
|
-
* The account root's signing secret, 64 hex. Signs the certificate.
|
|
17
|
-
*
|
|
18
|
-
* Mutually exclusive with {@link DeviceCertInput.signer}, and the weaker of
|
|
19
|
-
* the two in a browser: this is the one key in the system with nothing above
|
|
20
|
-
* it to revoke it, so a page that can read it can take the account for good.
|
|
21
|
-
* Prefer `signer` anywhere the root is held rather than loaded.
|
|
22
|
-
*/
|
|
23
|
-
rootSecret?: string;
|
|
24
|
-
/**
|
|
25
|
-
* The account root's signer — use instead of {@link DeviceCertInput.rootSecret}
|
|
26
|
-
* when the root is a key this page may use and cannot read.
|
|
27
|
-
*
|
|
28
|
-
* This is what lets the certifying page keep its root as a non-extractable
|
|
29
|
-
* `CryptoKey`: {@link accountRootSignerFromPhrase} hands one back, and script
|
|
30
|
-
* injected into the page can then mint certificates only while the page is
|
|
31
|
-
* open, rather than walking off with the account.
|
|
32
|
-
*/
|
|
33
|
-
signer?: Signer;
|
|
34
|
-
/** The device being certified, 64 hex — see {@link mintDeviceId}. */
|
|
35
|
-
device: string;
|
|
36
|
-
/** The key that device signs with, 64 hex. */
|
|
37
|
-
signPublicKey: string;
|
|
38
|
-
/** The key the group key is delivered to, 64 hex (X25519). */
|
|
39
|
-
kemPublicKey: string;
|
|
40
|
-
/**
|
|
41
|
-
* Must strictly exceed any epoch already folded for this device.
|
|
42
|
-
*
|
|
43
|
-
* The projection refuses a link that does not advance it, so a re-issued
|
|
44
|
-
* certificate reusing an epoch is inert rather than a rollback.
|
|
45
|
-
*/
|
|
46
|
-
deviceEpoch: number;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Mint a device id: `nonce ‖ H(DEVICE_ID_DOMAIN, account ‖ nonce)[..16]`.
|
|
50
|
-
*
|
|
51
|
-
* Derived from the account and a fresh nonce rather than from the device's keys,
|
|
52
|
-
* so rotating a keypair keeps the replica identity - and with it the counter
|
|
53
|
-
* slots and HLC lineage - intact. The hash half binds the id to its account:
|
|
54
|
-
* core refuses a certificate naming an id minted for another account.
|
|
55
|
-
*/
|
|
56
|
-
export declare function mintDeviceId(account: string, nonce: Uint8Array): Promise<string>;
|
|
57
|
-
/**
|
|
58
|
-
* The 32 bytes a root signs to certify a device.
|
|
59
|
-
*
|
|
60
|
-
* Both keys are covered, so neither the signing key nor the delivery key can be
|
|
61
|
-
* substituted into a certificate that otherwise verifies — which is what stops a
|
|
62
|
-
* relay carrying someone's credential from making itself the reader.
|
|
63
|
-
*/
|
|
64
|
-
export declare function deviceCertPayload(input: {
|
|
65
|
-
account: string;
|
|
66
|
-
device: string;
|
|
67
|
-
signPublicKey: string;
|
|
68
|
-
kemPublicKey: string;
|
|
69
|
-
keyEpoch: number;
|
|
70
|
-
deviceEpoch: number;
|
|
71
|
-
}): Promise<Uint8Array>;
|
|
72
|
-
/**
|
|
73
|
-
* Borsh-encode an `AccountProof<DeviceCert>` — what core calls a
|
|
74
|
-
* `JoinAccountCredential`.
|
|
75
|
-
*
|
|
76
|
-
* Extracted so the device-cert path and the namespace-op signer encode this from
|
|
77
|
-
* one implementation. Two copies of a borsh layout drift silently: the encoding
|
|
78
|
-
* is only checked by a peer rejecting a signature, which points at the signature
|
|
79
|
-
* rather than at whichever copy went stale.
|
|
80
|
-
*
|
|
81
|
-
* Layout is `AccountProof { genesis, chain, statement }`:
|
|
82
|
-
* `AccountGenesis` (version u8 + root pk 32) + `chain` (empty Vec, u32 len) +
|
|
83
|
-
* `DeviceCert` (account 32 + device 32 + sign pk 32 + kem pk 32 + key_epoch u32
|
|
84
|
-
* + device_epoch u32 + signature 64) = 237 bytes with an empty chain.
|
|
85
|
-
*/
|
|
86
|
-
export declare function accountProofBytes(input: {
|
|
87
|
-
rootPublicKey: Uint8Array;
|
|
88
|
-
account: string;
|
|
89
|
-
device: string;
|
|
90
|
-
signPublicKey: string;
|
|
91
|
-
kemPublicKey: string;
|
|
92
|
-
keyEpoch: number;
|
|
93
|
-
deviceEpoch: number;
|
|
94
|
-
signature: Uint8Array;
|
|
95
|
-
}): Uint8Array;
|
|
96
|
-
/**
|
|
97
|
-
* Certify a device, returning the hex credential `merod account sign-cert`
|
|
98
|
-
* prints — a borsh-encoded `AccountProof<DeviceCert>`.
|
|
99
|
-
*
|
|
100
|
-
* The layout, field for field, is core's:
|
|
101
|
-
*
|
|
102
|
-
* ```
|
|
103
|
-
* AccountProof { genesis: AccountGenesis, chain: Vec<RootKeyHandoff>, statement: DeviceCert }
|
|
104
|
-
* AccountGenesis { version: u8, root_sign_pk: [u8; 32] }
|
|
105
|
-
* chain → u32-LE count, then that many handoffs
|
|
106
|
-
* DeviceCert { account, device, sign_pk, kem_pk: [u8; 32] x4,
|
|
107
|
-
* key_epoch: u32, device_epoch: u32, signature: [u8; 64] }
|
|
108
|
-
* ```
|
|
109
|
-
*
|
|
110
|
-
* Borsh for plain fixed-width data is concatenation; the only variable part is
|
|
111
|
-
* the chain's length prefix. `chain` is empty here for the same reason it is in
|
|
112
|
-
* `sign-cert`: a root that has never been handed off signs directly, and a
|
|
113
|
-
* handoff chain is only needed once it has.
|
|
114
|
-
*
|
|
115
|
-
* `keyEpoch` is 0 for the same reason — it names the root epoch that signed, and
|
|
116
|
-
* an un-rotated root is at 0. Re-signing after a handoff is a separate flow that
|
|
117
|
-
* needs the chain, so this deliberately does not take it as an argument rather
|
|
118
|
-
* than accepting a value it would then have to ignore.
|
|
119
|
-
*/
|
|
120
|
-
export declare function signDeviceCert(input: DeviceCertInput): Promise<string>;
|
|
121
|
-
/** A credential read back apart, field for field. */
|
|
122
|
-
export interface DeviceCredential {
|
|
123
|
-
/** The account root that signed, 64 hex — `AccountGenesis::root_sign_pk`. */
|
|
124
|
-
rootPublicKey: string;
|
|
125
|
-
/** The account the certificate names, 64 hex. */
|
|
126
|
-
account: string;
|
|
127
|
-
/** The device it certifies, 64 hex. */
|
|
128
|
-
device: string;
|
|
129
|
-
/** That device's ed25519 signing key, 64 hex. */
|
|
130
|
-
signPublicKey: string;
|
|
131
|
-
/** That device's X25519 key-delivery key, 64 hex. */
|
|
132
|
-
kemPublicKey: string;
|
|
133
|
-
keyEpoch: number;
|
|
134
|
-
deviceEpoch: number;
|
|
135
|
-
/** The root's signature over {@link deviceCertPayload}, 128 hex. */
|
|
136
|
-
signature: string;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Read an `AccountProof<DeviceCert>` back apart — the inverse of
|
|
140
|
-
* {@link accountProofBytes}, and deliberately in the same file as it.
|
|
141
|
-
*
|
|
142
|
-
* A decoder that lives away from its encoder drifts: the layout is only checked
|
|
143
|
-
* by a peer rejecting a signature, which points at the signature rather than at
|
|
144
|
-
* whichever copy went stale.
|
|
145
|
-
*
|
|
146
|
-
* This checks the *shape* and nothing else — it says what the bytes claim, not
|
|
147
|
-
* whether the claim is true. {@link verifyDeviceCredential} is the one that
|
|
148
|
-
* answers that, and it is the one a caller acting on a credential from another
|
|
149
|
-
* origin wants.
|
|
150
|
-
*
|
|
151
|
-
* @throws on a wrong genesis version, a non-empty handoff chain, or trailing
|
|
152
|
-
* bytes. A non-empty chain is refused rather than skipped: verifying a handed-off
|
|
153
|
-
* root means walking the chain, and a decoder that returned the statement while
|
|
154
|
-
* ignoring who was entitled to sign it would hand a verifier the wrong root.
|
|
155
|
-
*/
|
|
156
|
-
export declare function parseDeviceCredential(credential: string): DeviceCredential;
|
|
157
|
-
/**
|
|
158
|
-
* Check that a credential says what it claims: the root signed it, the
|
|
159
|
-
* account it names is the one that root derives, and the device id was minted
|
|
160
|
-
* for that account.
|
|
161
|
-
*
|
|
162
|
-
* The first two matter and neither implies the other. The signature says the
|
|
163
|
-
* genesis root consented to these exact keys; the account derivation says the
|
|
164
|
-
* certificate cannot have been re-pointed at a different account while keeping a
|
|
165
|
-
* signature that verifies. A caller that checked only the first would accept a
|
|
166
|
-
* certificate naming an account whose root never signed anything. The third
|
|
167
|
-
* stops a root claiming a device id minted for another account.
|
|
168
|
-
*
|
|
169
|
-
* What it does **not** establish is that the account is one the caller wanted.
|
|
170
|
-
* Anyone can mint a root offline and certify any public key with it, so a
|
|
171
|
-
* credential arriving from elsewhere proves a *consistent* account, not *your*
|
|
172
|
-
* account — see `cloud/enrol-redirect.ts` for the binding that closes that.
|
|
173
|
-
*
|
|
174
|
-
* @throws with which check failed, rather than returning false — every failure
|
|
175
|
-
* here is a credential that must not be stored, and a boolean gets ignored.
|
|
176
|
-
*/
|
|
177
|
-
export declare function verifyDeviceCredential(credential: string): Promise<DeviceCredential>;
|
|
178
|
-
/** The account this root owns — the content address of its genesis. */
|
|
179
|
-
export declare function accountForRoot(rootSecret: string): Promise<string>;
|
|
180
|
-
/**
|
|
181
|
-
* The same account id, from the root's **public** half.
|
|
182
|
-
*
|
|
183
|
-
* The form a root that cannot be exported needs. It is also the honest shape of
|
|
184
|
-
* the derivation: an account id is a function of the public key, and asking for
|
|
185
|
-
* the secret only ever meant "let me derive the public key first".
|
|
186
|
-
*
|
|
187
|
-
* @param rootPublicKey the root's ed25519 public key, 64 hex
|
|
188
|
-
*/
|
|
189
|
-
export declare function accountForRootPublicKey(rootPublicKey: string): Promise<string>;
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minting and certifying a device offline, with a key that never reaches a node.
|
|
3
|
-
*
|
|
4
|
-
* This is the second half of the keyholder story. `signWarrant` lets an account
|
|
5
|
-
* with no node *write*; this lets one exist in the first place — an account root
|
|
6
|
-
* certifies a device, and the resulting credential is what a joiner presents.
|
|
7
|
-
*
|
|
8
|
-
* As with the warrant signer, the risk is that this reproduces core's encoding
|
|
9
|
-
* and has to keep doing so. The answer is the same: a golden vector here, and an
|
|
10
|
-
* e2e that diffs this output against `merod account sign-cert` byte for byte.
|
|
11
|
-
*
|
|
12
|
-
* Everything below is derived from core:
|
|
13
|
-
*
|
|
14
|
-
* - `DeviceCert::signing_payload` — `crates/account/src/device.rs`
|
|
15
|
-
* - `DeviceId::mint` — `crates/primitives/src/identity.rs`
|
|
16
|
-
* - `AccountProof` / `AccountGenesis` borsh layout — `crates/account/src/{signed,account}.rs`
|
|
17
|
-
*/
|
|
18
|
-
import { concat, derivePublicKey, domainHash, fromHex, hex, u32le, verifySignature, } from '../crypto/internal.js';
|
|
19
|
-
import { resolveSigner } from '../signer/signer.js';
|
|
20
|
-
const CERT_DOMAIN = new TextEncoder().encode('calimero.device.cert.v1');
|
|
21
|
-
const ACCOUNT_ID_DOMAIN = new TextEncoder().encode('calimero.account.genesis.v1');
|
|
22
|
-
const DEVICE_ID_DOMAIN = new TextEncoder().encode('calimero.device.id.v1');
|
|
23
|
-
/**
|
|
24
|
-
* Core's `ACCOUNT_GENESIS_VERSION`. It is part of the `AccountId` preimage, so
|
|
25
|
-
* this value decides which account a root key names — a mismatch does not fail
|
|
26
|
-
* as a version error at the edge, it silently derives a different account and
|
|
27
|
-
* core refuses the credential as unverifiable.
|
|
28
|
-
*
|
|
29
|
-
* `2` since the genesis dropped its per-namespace nonce. This was `1`, which
|
|
30
|
-
* produced credentials current core rejects outright; the shape was already v2,
|
|
31
|
-
* only the tag was stale.
|
|
32
|
-
*/
|
|
33
|
-
export const ACCOUNT_GENESIS_VERSION = 2;
|
|
34
|
-
/** Byte length of an `AccountProof<DeviceCert>` with an empty handoff chain. */
|
|
35
|
-
const ACCOUNT_PROOF_BYTES = 237;
|
|
36
|
-
/**
|
|
37
|
-
* Mint a device id: `nonce ‖ H(DEVICE_ID_DOMAIN, account ‖ nonce)[..16]`.
|
|
38
|
-
*
|
|
39
|
-
* Derived from the account and a fresh nonce rather than from the device's keys,
|
|
40
|
-
* so rotating a keypair keeps the replica identity - and with it the counter
|
|
41
|
-
* slots and HLC lineage - intact. The hash half binds the id to its account:
|
|
42
|
-
* core refuses a certificate naming an id minted for another account.
|
|
43
|
-
*/
|
|
44
|
-
export async function mintDeviceId(account, nonce) {
|
|
45
|
-
if (nonce.length !== 16) {
|
|
46
|
-
throw new Error(`nonce must be 16 bytes, got ${nonce.length}`);
|
|
47
|
-
}
|
|
48
|
-
const accountBytes = fromHex(account, 'account', 32);
|
|
49
|
-
const binding = await domainHash(DEVICE_ID_DOMAIN, [accountBytes, nonce]);
|
|
50
|
-
return hex(concat(nonce, binding.subarray(0, 16)));
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* The 32 bytes a root signs to certify a device.
|
|
54
|
-
*
|
|
55
|
-
* Both keys are covered, so neither the signing key nor the delivery key can be
|
|
56
|
-
* substituted into a certificate that otherwise verifies — which is what stops a
|
|
57
|
-
* relay carrying someone's credential from making itself the reader.
|
|
58
|
-
*/
|
|
59
|
-
export async function deviceCertPayload(input) {
|
|
60
|
-
return domainHash(CERT_DOMAIN, [
|
|
61
|
-
fromHex(input.account, 'account', 32),
|
|
62
|
-
fromHex(input.device, 'device', 32),
|
|
63
|
-
fromHex(input.signPublicKey, 'signPublicKey', 32),
|
|
64
|
-
fromHex(input.kemPublicKey, 'kemPublicKey', 32),
|
|
65
|
-
u32le(input.keyEpoch),
|
|
66
|
-
u32le(input.deviceEpoch),
|
|
67
|
-
]);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Borsh-encode an `AccountProof<DeviceCert>` — what core calls a
|
|
71
|
-
* `JoinAccountCredential`.
|
|
72
|
-
*
|
|
73
|
-
* Extracted so the device-cert path and the namespace-op signer encode this from
|
|
74
|
-
* one implementation. Two copies of a borsh layout drift silently: the encoding
|
|
75
|
-
* is only checked by a peer rejecting a signature, which points at the signature
|
|
76
|
-
* rather than at whichever copy went stale.
|
|
77
|
-
*
|
|
78
|
-
* Layout is `AccountProof { genesis, chain, statement }`:
|
|
79
|
-
* `AccountGenesis` (version u8 + root pk 32) + `chain` (empty Vec, u32 len) +
|
|
80
|
-
* `DeviceCert` (account 32 + device 32 + sign pk 32 + kem pk 32 + key_epoch u32
|
|
81
|
-
* + device_epoch u32 + signature 64) = 237 bytes with an empty chain.
|
|
82
|
-
*/
|
|
83
|
-
export function accountProofBytes(input) {
|
|
84
|
-
return concat(new Uint8Array([ACCOUNT_GENESIS_VERSION]), input.rootPublicKey, u32le(0), fromHex(input.account, 'account', 32), fromHex(input.device, 'device', 32), fromHex(input.signPublicKey, 'signPublicKey', 32), fromHex(input.kemPublicKey, 'kemPublicKey', 32), u32le(input.keyEpoch), u32le(input.deviceEpoch), input.signature);
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Certify a device, returning the hex credential `merod account sign-cert`
|
|
88
|
-
* prints — a borsh-encoded `AccountProof<DeviceCert>`.
|
|
89
|
-
*
|
|
90
|
-
* The layout, field for field, is core's:
|
|
91
|
-
*
|
|
92
|
-
* ```
|
|
93
|
-
* AccountProof { genesis: AccountGenesis, chain: Vec<RootKeyHandoff>, statement: DeviceCert }
|
|
94
|
-
* AccountGenesis { version: u8, root_sign_pk: [u8; 32] }
|
|
95
|
-
* chain → u32-LE count, then that many handoffs
|
|
96
|
-
* DeviceCert { account, device, sign_pk, kem_pk: [u8; 32] x4,
|
|
97
|
-
* key_epoch: u32, device_epoch: u32, signature: [u8; 64] }
|
|
98
|
-
* ```
|
|
99
|
-
*
|
|
100
|
-
* Borsh for plain fixed-width data is concatenation; the only variable part is
|
|
101
|
-
* the chain's length prefix. `chain` is empty here for the same reason it is in
|
|
102
|
-
* `sign-cert`: a root that has never been handed off signs directly, and a
|
|
103
|
-
* handoff chain is only needed once it has.
|
|
104
|
-
*
|
|
105
|
-
* `keyEpoch` is 0 for the same reason — it names the root epoch that signed, and
|
|
106
|
-
* an un-rotated root is at 0. Re-signing after a handoff is a separate flow that
|
|
107
|
-
* needs the chain, so this deliberately does not take it as an argument rather
|
|
108
|
-
* than accepting a value it would then have to ignore.
|
|
109
|
-
*/
|
|
110
|
-
export async function signDeviceCert(input) {
|
|
111
|
-
const signer = await resolveSigner(input.rootSecret, input.signer, 'rootSecret');
|
|
112
|
-
// The signer names its own key, so the account is read off it rather than
|
|
113
|
-
// derived from material this function may not have.
|
|
114
|
-
const rootPublicKey = fromHex(signer.publicKey, 'signer.publicKey', 32);
|
|
115
|
-
const account = await accountForRootPublicKey(signer.publicKey);
|
|
116
|
-
const keyEpoch = 0;
|
|
117
|
-
const payload = await deviceCertPayload({
|
|
118
|
-
account,
|
|
119
|
-
device: input.device,
|
|
120
|
-
signPublicKey: input.signPublicKey,
|
|
121
|
-
kemPublicKey: input.kemPublicKey,
|
|
122
|
-
keyEpoch,
|
|
123
|
-
deviceEpoch: input.deviceEpoch,
|
|
124
|
-
});
|
|
125
|
-
const signature = await signer.sign(payload);
|
|
126
|
-
const credential = accountProofBytes({
|
|
127
|
-
rootPublicKey,
|
|
128
|
-
account,
|
|
129
|
-
device: input.device,
|
|
130
|
-
signPublicKey: input.signPublicKey,
|
|
131
|
-
kemPublicKey: input.kemPublicKey,
|
|
132
|
-
keyEpoch,
|
|
133
|
-
deviceEpoch: input.deviceEpoch,
|
|
134
|
-
signature,
|
|
135
|
-
});
|
|
136
|
-
return hex(credential);
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Read an `AccountProof<DeviceCert>` back apart — the inverse of
|
|
140
|
-
* {@link accountProofBytes}, and deliberately in the same file as it.
|
|
141
|
-
*
|
|
142
|
-
* A decoder that lives away from its encoder drifts: the layout is only checked
|
|
143
|
-
* by a peer rejecting a signature, which points at the signature rather than at
|
|
144
|
-
* whichever copy went stale.
|
|
145
|
-
*
|
|
146
|
-
* This checks the *shape* and nothing else — it says what the bytes claim, not
|
|
147
|
-
* whether the claim is true. {@link verifyDeviceCredential} is the one that
|
|
148
|
-
* answers that, and it is the one a caller acting on a credential from another
|
|
149
|
-
* origin wants.
|
|
150
|
-
*
|
|
151
|
-
* @throws on a wrong genesis version, a non-empty handoff chain, or trailing
|
|
152
|
-
* bytes. A non-empty chain is refused rather than skipped: verifying a handed-off
|
|
153
|
-
* root means walking the chain, and a decoder that returned the statement while
|
|
154
|
-
* ignoring who was entitled to sign it would hand a verifier the wrong root.
|
|
155
|
-
*/
|
|
156
|
-
export function parseDeviceCredential(credential) {
|
|
157
|
-
const bytes = fromHex(credential, 'credential',
|
|
158
|
-
// Fixed-width with an empty chain: 1 + 32 + 4 + 32*4 + 4 + 4 + 64.
|
|
159
|
-
ACCOUNT_PROOF_BYTES);
|
|
160
|
-
if (bytes[0] !== ACCOUNT_GENESIS_VERSION) {
|
|
161
|
-
throw new Error(`this credential has account genesis version ${bytes[0]}, not ` +
|
|
162
|
-
`${ACCOUNT_GENESIS_VERSION} — it names a different account than its root ` +
|
|
163
|
-
'key derives here, and core would refuse it');
|
|
164
|
-
}
|
|
165
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
166
|
-
const chainLength = view.getUint32(33, true);
|
|
167
|
-
if (chainLength !== 0) {
|
|
168
|
-
throw new Error(`this credential carries a ${chainLength}-handoff root chain, which this ` +
|
|
169
|
-
'SDK cannot verify yet — the signing root is not the genesis root, so ' +
|
|
170
|
-
'checking it means walking the chain');
|
|
171
|
-
}
|
|
172
|
-
const at = (offset, length) => hex(bytes.subarray(offset, offset + length));
|
|
173
|
-
return {
|
|
174
|
-
rootPublicKey: at(1, 32),
|
|
175
|
-
account: at(37, 32),
|
|
176
|
-
device: at(69, 32),
|
|
177
|
-
signPublicKey: at(101, 32),
|
|
178
|
-
kemPublicKey: at(133, 32),
|
|
179
|
-
keyEpoch: view.getUint32(165, true),
|
|
180
|
-
deviceEpoch: view.getUint32(169, true),
|
|
181
|
-
signature: at(173, 64),
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Check that a credential says what it claims: the root signed it, the
|
|
186
|
-
* account it names is the one that root derives, and the device id was minted
|
|
187
|
-
* for that account.
|
|
188
|
-
*
|
|
189
|
-
* The first two matter and neither implies the other. The signature says the
|
|
190
|
-
* genesis root consented to these exact keys; the account derivation says the
|
|
191
|
-
* certificate cannot have been re-pointed at a different account while keeping a
|
|
192
|
-
* signature that verifies. A caller that checked only the first would accept a
|
|
193
|
-
* certificate naming an account whose root never signed anything. The third
|
|
194
|
-
* stops a root claiming a device id minted for another account.
|
|
195
|
-
*
|
|
196
|
-
* What it does **not** establish is that the account is one the caller wanted.
|
|
197
|
-
* Anyone can mint a root offline and certify any public key with it, so a
|
|
198
|
-
* credential arriving from elsewhere proves a *consistent* account, not *your*
|
|
199
|
-
* account — see `cloud/enrol-redirect.ts` for the binding that closes that.
|
|
200
|
-
*
|
|
201
|
-
* @throws with which check failed, rather than returning false — every failure
|
|
202
|
-
* here is a credential that must not be stored, and a boolean gets ignored.
|
|
203
|
-
*/
|
|
204
|
-
export async function verifyDeviceCredential(credential) {
|
|
205
|
-
const parsed = parseDeviceCredential(credential);
|
|
206
|
-
const derived = await accountForRootPublicKey(parsed.rootPublicKey);
|
|
207
|
-
if (derived !== parsed.account) {
|
|
208
|
-
throw new Error(`this credential names account ${parsed.account}, but its root key ` +
|
|
209
|
-
`derives ${derived} — the certificate was re-pointed at another account`);
|
|
210
|
-
}
|
|
211
|
-
const nonce = fromHex(parsed.device, 'device', 32).subarray(0, 16);
|
|
212
|
-
if ((await mintDeviceId(parsed.account, nonce)) !== parsed.device) {
|
|
213
|
-
throw new Error(`this credential names device ${parsed.device}, which was not minted for ` +
|
|
214
|
-
`account ${parsed.account}; core refuses it`);
|
|
215
|
-
}
|
|
216
|
-
const signed = await verifySignature(parsed.rootPublicKey, fromHex(parsed.signature, 'signature', 64), await deviceCertPayload(parsed));
|
|
217
|
-
if (!signed) {
|
|
218
|
-
throw new Error(`the root key of account ${parsed.account} did not sign this certificate`);
|
|
219
|
-
}
|
|
220
|
-
return parsed;
|
|
221
|
-
}
|
|
222
|
-
/** The account this root owns — the content address of its genesis. */
|
|
223
|
-
export async function accountForRoot(rootSecret) {
|
|
224
|
-
return accountForRootPublicKey(hex(await derivePublicKey(rootSecret)));
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* The same account id, from the root's **public** half.
|
|
228
|
-
*
|
|
229
|
-
* The form a root that cannot be exported needs. It is also the honest shape of
|
|
230
|
-
* the derivation: an account id is a function of the public key, and asking for
|
|
231
|
-
* the secret only ever meant "let me derive the public key first".
|
|
232
|
-
*
|
|
233
|
-
* @param rootPublicKey the root's ed25519 public key, 64 hex
|
|
234
|
-
*/
|
|
235
|
-
export async function accountForRootPublicKey(rootPublicKey) {
|
|
236
|
-
return hex(await domainHash(ACCOUNT_ID_DOMAIN, [
|
|
237
|
-
concat(new Uint8Array([ACCOUNT_GENESIS_VERSION]), fromHex(rootPublicKey, 'rootPublicKey', 32)),
|
|
238
|
-
]));
|
|
239
|
-
}
|