burnledger 0.8.1 → 0.9.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 +139 -2
- package/dist/cjs/anchor.d.ts +5 -3
- package/dist/cjs/anchor.d.ts.map +1 -1
- package/dist/cjs/anchor.js +10 -4
- package/dist/cjs/anchor.js.map +1 -1
- package/dist/cjs/client.d.ts +95 -0
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +133 -2
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/customer-keys.d.ts +73 -1
- package/dist/cjs/customer-keys.d.ts.map +1 -1
- package/dist/cjs/customer-keys.js +329 -3
- package/dist/cjs/customer-keys.js.map +1 -1
- package/dist/cjs/enclave-registration.d.ts +153 -0
- package/dist/cjs/enclave-registration.d.ts.map +1 -0
- package/dist/cjs/enclave-registration.js +275 -0
- package/dist/cjs/enclave-registration.js.map +1 -0
- package/dist/cjs/enclave-seal.d.ts +43 -0
- package/dist/cjs/enclave-seal.d.ts.map +1 -1
- package/dist/cjs/enclave-seal.js +62 -1
- package/dist/cjs/enclave-seal.js.map +1 -1
- package/dist/cjs/errors.d.ts +10 -0
- package/dist/cjs/errors.d.ts.map +1 -1
- package/dist/cjs/errors.js +11 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/index.browser.d.ts +7 -3
- package/dist/cjs/index.browser.d.ts.map +1 -1
- package/dist/cjs/index.browser.js +6 -3
- package/dist/cjs/index.browser.js.map +1 -1
- package/dist/cjs/index.d.ts +22 -10
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +30 -6
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/key-group.d.ts +80 -0
- package/dist/cjs/key-group.d.ts.map +1 -0
- package/dist/cjs/key-group.js +136 -0
- package/dist/cjs/key-group.js.map +1 -0
- package/dist/cjs/models.d.ts +60 -0
- package/dist/cjs/models.d.ts.map +1 -1
- package/dist/cjs/models.js +63 -0
- package/dist/cjs/models.js.map +1 -1
- package/dist/cjs/status-document.d.ts +25 -0
- package/dist/cjs/status-document.d.ts.map +1 -0
- package/dist/cjs/status-document.js +62 -0
- package/dist/cjs/status-document.js.map +1 -0
- package/dist/cjs/verify.d.ts +108 -3
- package/dist/cjs/verify.d.ts.map +1 -1
- package/dist/cjs/verify.js +389 -146
- package/dist/cjs/verify.js.map +1 -1
- package/dist/cjs/web-verifier.d.ts +23 -3
- package/dist/cjs/web-verifier.d.ts.map +1 -1
- package/dist/cjs/web-verifier.js +31 -5
- package/dist/cjs/web-verifier.js.map +1 -1
- package/dist/esm/anchor.d.ts +5 -3
- package/dist/esm/anchor.d.ts.map +1 -1
- package/dist/esm/anchor.js +10 -4
- package/dist/esm/anchor.js.map +1 -1
- package/dist/esm/cli.d.ts +13 -19
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +88 -81
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/client.d.ts +95 -0
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +133 -2
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/customer-keys.d.ts +73 -1
- package/dist/esm/customer-keys.d.ts.map +1 -1
- package/dist/esm/customer-keys.js +323 -3
- package/dist/esm/customer-keys.js.map +1 -1
- package/dist/esm/enclave-registration.d.ts +153 -0
- package/dist/esm/enclave-registration.d.ts.map +1 -0
- package/dist/esm/enclave-registration.js +265 -0
- package/dist/esm/enclave-registration.js.map +1 -0
- package/dist/esm/enclave-seal.d.ts +43 -0
- package/dist/esm/enclave-seal.d.ts.map +1 -1
- package/dist/esm/enclave-seal.js +61 -1
- package/dist/esm/enclave-seal.js.map +1 -1
- package/dist/esm/errors.d.ts +10 -0
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +10 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.browser.d.ts +7 -3
- package/dist/esm/index.browser.d.ts.map +1 -1
- package/dist/esm/index.browser.js +6 -3
- package/dist/esm/index.browser.js.map +1 -1
- package/dist/esm/index.d.ts +22 -10
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +18 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/key-group.d.ts +80 -0
- package/dist/esm/key-group.d.ts.map +1 -0
- package/dist/esm/key-group.js +130 -0
- package/dist/esm/key-group.js.map +1 -0
- package/dist/esm/models.d.ts +60 -0
- package/dist/esm/models.d.ts.map +1 -1
- package/dist/esm/models.js +59 -0
- package/dist/esm/models.js.map +1 -1
- package/dist/esm/status-document.d.ts +25 -0
- package/dist/esm/status-document.d.ts.map +1 -0
- package/dist/esm/status-document.js +59 -0
- package/dist/esm/status-document.js.map +1 -0
- package/dist/esm/verify.d.ts +108 -3
- package/dist/esm/verify.d.ts.map +1 -1
- package/dist/esm/verify.js +386 -146
- package/dist/esm/verify.js.map +1 -1
- package/dist/esm/web-verifier.d.ts +23 -3
- package/dist/esm/web-verifier.d.ts.map +1 -1
- package/dist/esm/web-verifier.js +27 -5
- package/dist/esm/web-verifier.js.map +1 -1
- package/package.json +1 -1
- package/src/anchor.ts +10 -4
- package/src/cli.ts +96 -79
- package/src/client.ts +198 -6
- package/src/customer-keys.ts +373 -3
- package/src/enclave-registration.ts +390 -0
- package/src/enclave-seal.ts +108 -1
- package/src/errors.ts +11 -0
- package/src/index.browser.ts +9 -3
- package/src/index.ts +35 -6
- package/src/key-group.ts +181 -0
- package/src/models.ts +131 -0
- package/src/status-document.ts +59 -0
- package/src/verify.ts +499 -151
- package/src/web-verifier.ts +36 -3
package/src/key-group.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members of a customer key group as signers (ADR-025 §2–3).
|
|
3
|
+
*
|
|
4
|
+
* Node only, like enclave-seal.ts: ADR-025 §2 makes a client the customer runs
|
|
5
|
+
* locally the only place enrolment can happen, because whoever serves a page
|
|
6
|
+
* controls the check the page would make.
|
|
7
|
+
*
|
|
8
|
+
* WHY A SIGNER AND NOT A PRIVATE KEY. Everything in this SDK that needs a
|
|
9
|
+
* member's signature takes a {@link KeyGroupSigner} — a public key and a
|
|
10
|
+
* function that signs bytes. The SDK never needs, holds or transmits a private
|
|
11
|
+
* key, so a member can live in an HSM, a cloud KMS, a hardware token or another
|
|
12
|
+
* process, and the SDK's only job is to hand it the exact canonical bytes and
|
|
13
|
+
* check what comes back. {@link InMemorySigner} is the convenience for tests and
|
|
14
|
+
* for customers who keep keys on the machine running the SDK.
|
|
15
|
+
*
|
|
16
|
+
* CUSTODY, stated plainly because nothing recovers from getting it wrong. The
|
|
17
|
+
* private halves are the customer's alone; BurnLedger never sees them and cannot
|
|
18
|
+
* reissue them. Lose enough members that the group can no longer meet its
|
|
19
|
+
* threshold and it can neither sign a registration nor authorise its own
|
|
20
|
+
* rotation — and an operator who could re-enrol on the customer's behalf is an
|
|
21
|
+
* operator who could mint, so there is no recovery path (ADR-025 §3 "Key loss").
|
|
22
|
+
* The only way back is to enrol a NEW group and re-register every system, and
|
|
23
|
+
* records issued before and after will name different `customer_key_id`s,
|
|
24
|
+
* visibly. Hedge before it happens: enrol more members than the threshold and
|
|
25
|
+
* keep at least one offline (the simplest is two members, threshold one).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
createPrivateKey,
|
|
30
|
+
createPublicKey,
|
|
31
|
+
generateKeyPairSync,
|
|
32
|
+
sign as nodeSign,
|
|
33
|
+
type KeyObject,
|
|
34
|
+
} from "node:crypto";
|
|
35
|
+
import { nodeCrypto } from "./crypto-node.js";
|
|
36
|
+
import {
|
|
37
|
+
InvalidKeyGroupError,
|
|
38
|
+
memberFingerprint,
|
|
39
|
+
validateKeyGroup,
|
|
40
|
+
type CustomerKeyGroup,
|
|
41
|
+
} from "./customer-keys.js";
|
|
42
|
+
|
|
43
|
+
/** One member of a key group, able to sign. */
|
|
44
|
+
export interface KeyGroupSigner {
|
|
45
|
+
/** The member's raw 32-byte Ed25519 public key. */
|
|
46
|
+
readonly publicKey: Uint8Array;
|
|
47
|
+
/** Sign exactly `payload` with the member's Ed25519 key; return the 64-byte signature. */
|
|
48
|
+
sign(payload: Uint8Array): Promise<Uint8Array>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// RFC 8410: an Ed25519 private key as PKCS#8 is this fixed prefix and the seed.
|
|
52
|
+
const PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
53
|
+
const SEED_SIZE = 32;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A member whose private key is held in this process.
|
|
57
|
+
*
|
|
58
|
+
* Convenient, and exactly as safe as the memory and disk of the machine running
|
|
59
|
+
* it. `exportSeed()` is the one way the key leaves: store the seed as you would
|
|
60
|
+
* any credential that cannot be reissued.
|
|
61
|
+
*/
|
|
62
|
+
export class InMemorySigner implements KeyGroupSigner {
|
|
63
|
+
readonly publicKey: Uint8Array;
|
|
64
|
+
readonly #privateKey: KeyObject;
|
|
65
|
+
|
|
66
|
+
private constructor(privateKey: KeyObject) {
|
|
67
|
+
this.#privateKey = privateKey;
|
|
68
|
+
const spki = createPublicKey(privateKey).export({ format: "der", type: "spki" });
|
|
69
|
+
this.publicKey = new Uint8Array(spki.subarray(spki.length - 32));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A fresh member, from the operating system's CSPRNG. */
|
|
73
|
+
static generate(): InMemorySigner {
|
|
74
|
+
return new InMemorySigner(generateKeyPairSync("ed25519").privateKey);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The member a 32-byte seed (as returned by `exportSeed`) describes. */
|
|
78
|
+
static fromSeed(seed: Uint8Array): InMemorySigner {
|
|
79
|
+
if (!(seed instanceof Uint8Array) || seed.length !== SEED_SIZE) {
|
|
80
|
+
throw new Error(`an Ed25519 seed is ${SEED_SIZE} bytes`);
|
|
81
|
+
}
|
|
82
|
+
return new InMemorySigner(
|
|
83
|
+
createPrivateKey({
|
|
84
|
+
key: Buffer.concat([PKCS8_ED25519_PREFIX, Buffer.from(seed)]),
|
|
85
|
+
format: "der",
|
|
86
|
+
type: "pkcs8",
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The 32-byte seed. Whoever holds it IS this member. */
|
|
92
|
+
exportSeed(): Uint8Array {
|
|
93
|
+
const der = this.#privateKey.export({ format: "der", type: "pkcs8" });
|
|
94
|
+
return new Uint8Array(der.subarray(der.length - SEED_SIZE));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async sign(payload: Uint8Array): Promise<Uint8Array> {
|
|
98
|
+
return new Uint8Array(nodeSign(null, payload, this.#privateKey));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A freshly generated group and the signers that hold its members. */
|
|
103
|
+
export interface GeneratedKeyGroup {
|
|
104
|
+
readonly group: CustomerKeyGroup;
|
|
105
|
+
readonly signers: readonly InMemorySigner[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generate `members` Ed25519 keys and the group requiring `threshold` of them.
|
|
110
|
+
*
|
|
111
|
+
* The private halves are returned to the caller and go nowhere else. Read the
|
|
112
|
+
* custody note at the top of this module before choosing `members` and
|
|
113
|
+
* `threshold`: a group whose members fall below its threshold is unrecoverable.
|
|
114
|
+
*/
|
|
115
|
+
export async function generateKeyGroup(opts: {
|
|
116
|
+
members: number;
|
|
117
|
+
threshold: number;
|
|
118
|
+
}): Promise<GeneratedKeyGroup> {
|
|
119
|
+
if (!Number.isInteger(opts.members) || opts.members < 1) {
|
|
120
|
+
throw new InvalidKeyGroupError(`a key group needs at least one member, got ${opts.members}`);
|
|
121
|
+
}
|
|
122
|
+
const signers = Array.from({ length: opts.members }, () => InMemorySigner.generate());
|
|
123
|
+
const group: CustomerKeyGroup = {
|
|
124
|
+
threshold: opts.threshold,
|
|
125
|
+
members: signers.map((s) => s.publicKey),
|
|
126
|
+
};
|
|
127
|
+
await validateKeyGroup(nodeCrypto, group);
|
|
128
|
+
return { group, signers };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function toHex(bytes: Uint8Array): string {
|
|
132
|
+
return Buffer.from(bytes).toString("hex");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Collect member signatures over `payload`, keyed by member fingerprint — the
|
|
137
|
+
* wire form every ADR-025 request carries.
|
|
138
|
+
*
|
|
139
|
+
* Refuses before anything is sent, with a reason, where the enclave would refuse
|
|
140
|
+
* after with a bare 401: a signer that is not a member of `group`, the same
|
|
141
|
+
* member twice, a signature that does not verify under the signer's own public
|
|
142
|
+
* key, or fewer distinct members than `required`.
|
|
143
|
+
*/
|
|
144
|
+
export async function signWithGroup(
|
|
145
|
+
payload: Uint8Array,
|
|
146
|
+
group: CustomerKeyGroup,
|
|
147
|
+
signers: readonly KeyGroupSigner[],
|
|
148
|
+
required: number,
|
|
149
|
+
): Promise<Record<string, string>> {
|
|
150
|
+
const members = new Set<string>();
|
|
151
|
+
for (const member of group.members) members.add(await memberFingerprint(nodeCrypto, member));
|
|
152
|
+
|
|
153
|
+
const signatures: Record<string, string> = {};
|
|
154
|
+
for (const signer of signers) {
|
|
155
|
+
const fingerprint = await memberFingerprint(nodeCrypto, signer.publicKey);
|
|
156
|
+
if (!members.has(fingerprint)) {
|
|
157
|
+
throw new InvalidKeyGroupError(`signer ${fingerprint} is not a member of this key group`);
|
|
158
|
+
}
|
|
159
|
+
if (fingerprint in signatures) {
|
|
160
|
+
throw new InvalidKeyGroupError(`member ${fingerprint} is listed twice among the signers`);
|
|
161
|
+
}
|
|
162
|
+
const signature = await signer.sign(payload);
|
|
163
|
+
if (
|
|
164
|
+
!(signature instanceof Uint8Array) ||
|
|
165
|
+
signature.length !== 64 ||
|
|
166
|
+
!(await nodeCrypto.ed25519Verify(signer.publicKey, payload, signature))
|
|
167
|
+
) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`the signer for member ${fingerprint} returned a signature that does not verify over the request`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
signatures[fingerprint] = toHex(signature);
|
|
173
|
+
}
|
|
174
|
+
const count = Object.keys(signatures).length;
|
|
175
|
+
if (count < required) {
|
|
176
|
+
throw new InvalidKeyGroupError(
|
|
177
|
+
`this request needs signatures from ${required} distinct members, got ${count}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return signatures;
|
|
181
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -153,6 +153,17 @@ export interface CertificateResponse {
|
|
|
153
153
|
* it can be handed straight to verifyCertificateWithStatus().
|
|
154
154
|
*/
|
|
155
155
|
readonly statusStatement: Record<string, unknown> | undefined;
|
|
156
|
+
/**
|
|
157
|
+
* Base64 AWS Nitro NSM attestation document produced while this record was
|
|
158
|
+
* signed, exactly as the server sent it. Its PCRs identify the enclave image
|
|
159
|
+
* that held the signing key and its user_data binds this record's canonical
|
|
160
|
+
* payload. Carried so it can be archived beside the record; this SDK does NOT
|
|
161
|
+
* verify it (that needs COSE ES384 and an x509 chain to the Nitro root, which
|
|
162
|
+
* only the Go verifier implements). undefined for records issued before it
|
|
163
|
+
* was recorded and for dev-mode issuance; absence is not a verification
|
|
164
|
+
* result.
|
|
165
|
+
*/
|
|
166
|
+
readonly nsmAttestation: string | undefined;
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
export interface RevocationStatus {
|
|
@@ -440,6 +451,7 @@ export function parseCertificateResponse(d: Raw): CertificateResponse {
|
|
|
440
451
|
revocationReason: d.revocation_reason ?? undefined,
|
|
441
452
|
certificate: d.certificate,
|
|
442
453
|
statusStatement: d.status_statement ?? undefined,
|
|
454
|
+
nsmAttestation: d.nsm_attestation ?? undefined,
|
|
443
455
|
};
|
|
444
456
|
}
|
|
445
457
|
|
|
@@ -624,3 +636,122 @@ export function parseSystemHealth(d: Raw): SystemHealth {
|
|
|
624
636
|
healthError: d.health_error ?? undefined,
|
|
625
637
|
};
|
|
626
638
|
}
|
|
639
|
+
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
// ADR-025 documents: enrolment, team authorization, system registration
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
|
|
644
|
+
/** The enclave's statement of which key group it was told to trust for a team
|
|
645
|
+
* (core.KeyEnrollmentStatement). Trust on first use: compare `customerKeyId` —
|
|
646
|
+
* a hash over the members and threshold — against the group you hold. The SDK's
|
|
647
|
+
* enrol and rotate calls do, after verifying the enclave's signature. */
|
|
648
|
+
export interface KeyEnrollmentStatement {
|
|
649
|
+
readonly assertedTeamId: string;
|
|
650
|
+
readonly customerKeyId: string;
|
|
651
|
+
readonly threshold: number;
|
|
652
|
+
readonly members: readonly Uint8Array[];
|
|
653
|
+
readonly enrolledAt: Date;
|
|
654
|
+
readonly enclavePcr0: string;
|
|
655
|
+
readonly signature: Uint8Array;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** The bounded authority that makes an enrolled group usable
|
|
659
|
+
* (core.TeamAuthorizationCertificate). It expires rather than being revoked;
|
|
660
|
+
* renew it by enrolling the same group again before `notAfter`. */
|
|
661
|
+
export interface TeamAuthorizationCertificate {
|
|
662
|
+
readonly assertedTeamId: string;
|
|
663
|
+
readonly customerKeyId: string;
|
|
664
|
+
readonly notBefore: Date;
|
|
665
|
+
readonly notAfter: Date;
|
|
666
|
+
/** The key this one replaced; undefined on a first enrolment. */
|
|
667
|
+
readonly prevKeyId: string | undefined;
|
|
668
|
+
readonly signature: Uint8Array;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Binds one system to the config and query it may be measured with, under a
|
|
672
|
+
* customer key (core.SystemRegistrationCertificate). No expiry: it states a
|
|
673
|
+
* fact about bytes. */
|
|
674
|
+
export interface SystemRegistrationCertificate {
|
|
675
|
+
readonly systemId: string;
|
|
676
|
+
/** SHA-256 of the exact config plaintext submitted for the system. */
|
|
677
|
+
readonly configDigest: Uint8Array;
|
|
678
|
+
/** HashQuery over the NORMALIZED subject query — not the raw-template hash
|
|
679
|
+
* the registration request signs. */
|
|
680
|
+
readonly queryTemplateHash: Uint8Array;
|
|
681
|
+
readonly connectorType: string;
|
|
682
|
+
readonly customerKeyId: string;
|
|
683
|
+
readonly registeredAt: Date;
|
|
684
|
+
readonly signature: Uint8Array;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** What enrolling or rotating a key group returns. */
|
|
688
|
+
export interface KeyEnrollmentResult {
|
|
689
|
+
readonly enrollment: KeyEnrollmentStatement;
|
|
690
|
+
readonly authorization: TeamAuthorizationCertificate;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Go encodes [N]byte as an array of numbers; anything else here is a response
|
|
694
|
+
* this SDK cannot read, and is refused rather than coerced. */
|
|
695
|
+
function bytesFromInts(v: unknown, field: string, length: number): Uint8Array {
|
|
696
|
+
if (
|
|
697
|
+
!Array.isArray(v) ||
|
|
698
|
+
v.length !== length ||
|
|
699
|
+
!v.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)
|
|
700
|
+
) {
|
|
701
|
+
throw new Error(`${field} is not ${length} bytes`);
|
|
702
|
+
}
|
|
703
|
+
return new Uint8Array(v as number[]);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function bytesFromBase64(v: unknown, field: string): Uint8Array {
|
|
707
|
+
if (typeof v !== "string") throw new Error(`${field} is not base64`);
|
|
708
|
+
const bin = atob(v);
|
|
709
|
+
const out = new Uint8Array(bin.length);
|
|
710
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
711
|
+
return out;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function parseKeyEnrollmentStatement(d: Raw): KeyEnrollmentStatement {
|
|
715
|
+
const group = (d.group ?? {}) as Raw;
|
|
716
|
+
return {
|
|
717
|
+
assertedTeamId: d.asserted_team_id,
|
|
718
|
+
customerKeyId: d.customer_key_id,
|
|
719
|
+
threshold: group.threshold,
|
|
720
|
+
members: ((group.members ?? []) as unknown[]).map((m, i) =>
|
|
721
|
+
bytesFromBase64(m, `enrollment.group.members[${i}]`),
|
|
722
|
+
),
|
|
723
|
+
enrolledAt: parseDt(d.enrolled_at),
|
|
724
|
+
enclavePcr0: d.enclave_pcr0,
|
|
725
|
+
signature: bytesFromInts(d.signature, "enrollment.signature", 64),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function parseTeamAuthorizationCertificate(d: Raw): TeamAuthorizationCertificate {
|
|
730
|
+
return {
|
|
731
|
+
assertedTeamId: d.asserted_team_id,
|
|
732
|
+
customerKeyId: d.customer_key_id,
|
|
733
|
+
notBefore: parseDt(d.not_before),
|
|
734
|
+
notAfter: parseDt(d.not_after),
|
|
735
|
+
prevKeyId: d.prev_key_id || undefined,
|
|
736
|
+
signature: bytesFromInts(d.signature, "authorization.signature", 64),
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export function parseSystemRegistrationCertificate(d: Raw): SystemRegistrationCertificate {
|
|
741
|
+
return {
|
|
742
|
+
systemId: d.system_id,
|
|
743
|
+
configDigest: bytesFromInts(d.config_digest, "registration.config_digest", 32),
|
|
744
|
+
queryTemplateHash: bytesFromInts(d.query_template_hash, "registration.query_template_hash", 32),
|
|
745
|
+
connectorType: d.connector_type,
|
|
746
|
+
customerKeyId: d.customer_key_id,
|
|
747
|
+
registeredAt: parseDt(d.registered_at),
|
|
748
|
+
signature: bytesFromInts(d.signature, "registration.signature", 64),
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export function parseKeyEnrollmentResult(d: Raw): KeyEnrollmentResult {
|
|
753
|
+
return {
|
|
754
|
+
enrollment: parseKeyEnrollmentStatement(d.enrollment ?? {}),
|
|
755
|
+
authorization: parseTeamAuthorizationCertificate(d.authorization ?? {}),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Reading a status statement as the status endpoint serves it, before anything
|
|
2
|
+
* verifies it. Shared by the CLI and the /verify page's status button, so the two
|
|
3
|
+
* agree on what counts as a statement: an answer that is not one is silence
|
|
4
|
+
* about revocation, while a statement that is one and fails is a refusal. */
|
|
5
|
+
|
|
6
|
+
type StatusStatement = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
/** The API wraps every response in {"data": ...}; a caller may hand over either
|
|
9
|
+
* the envelope or the document inside it. Mirrors
|
|
10
|
+
* core.ParseCertificateStatusDocument, so this CLI and the Go one cannot
|
|
11
|
+
* disagree about which shape the endpoint returns. Exported for testing.
|
|
12
|
+
*
|
|
13
|
+
* A document that does not parse THROWS, and the caller turns that into a
|
|
14
|
+
* warning and a VALID_REVOCATION_UNKNOWN verdict. That is deliberate: an answer
|
|
15
|
+
* this build cannot read is silence about revocation, not a finding about the
|
|
16
|
+
* certificate. Handing a half-read document to the verifier instead would
|
|
17
|
+
* report "status statement signature is invalid" — a claim about the issuer —
|
|
18
|
+
* when what actually happened is that something returned nonsense.
|
|
19
|
+
*
|
|
20
|
+
* `status` itself is NOT validated against a known set. A statement saying
|
|
21
|
+
* SUSPENDED is a statement this build cannot read, and the verifier already has
|
|
22
|
+
* the right answer for that: it authenticates the signature first, then reports
|
|
23
|
+
* VALID_REVOCATION_UNKNOWN. Refusing it here would discard the statement before
|
|
24
|
+
* anyone authenticated it. */
|
|
25
|
+
export function unwrapStatusStatement(parsed: unknown): StatusStatement {
|
|
26
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
27
|
+
throw new Error("status statement: response is not a JSON object");
|
|
28
|
+
}
|
|
29
|
+
let doc = parsed as StatusStatement;
|
|
30
|
+
if (doc.certificate_id === undefined && doc.data !== null && typeof doc.data === "object") {
|
|
31
|
+
doc = doc.data as StatusStatement;
|
|
32
|
+
}
|
|
33
|
+
for (const f of ["certificate_id", "status", "key_id"]) {
|
|
34
|
+
if (typeof doc[f] !== "string" || doc[f] === "") throw new Error(`status statement: ${f} is missing`);
|
|
35
|
+
}
|
|
36
|
+
for (const f of ["statement_issued_at", "statement_expires_at"]) requireRfc3339(doc, f);
|
|
37
|
+
if (doc.revoked_at != null) requireRfc3339(doc, "revoked_at");
|
|
38
|
+
if (typeof doc.sth_tree_size !== "number") throw new Error("status statement: sth_tree_size is not a number");
|
|
39
|
+
// Refuse a short or long value rather than letting it through: a truncated
|
|
40
|
+
// signature or root hash that reaches the verifier is checked against bytes
|
|
41
|
+
// nobody sent.
|
|
42
|
+
requireHex(doc, "sth_root_hash", 32);
|
|
43
|
+
requireHex(doc, "signature", 64);
|
|
44
|
+
return doc;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function requireRfc3339(doc: StatusStatement, field: string): void {
|
|
48
|
+
const v = doc[field];
|
|
49
|
+
if (typeof v !== "string" || Number.isNaN(Date.parse(v))) {
|
|
50
|
+
throw new Error(`status statement: ${field} is not RFC 3339`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function requireHex(doc: StatusStatement, field: string, bytes: number): void {
|
|
55
|
+
const v = doc[field];
|
|
56
|
+
if (typeof v !== "string" || !new RegExp(`^[0-9a-fA-F]{${bytes * 2}}$`).test(v)) {
|
|
57
|
+
throw new Error(`status statement: ${field} is not ${bytes} bytes of hex`);
|
|
58
|
+
}
|
|
59
|
+
}
|