@byok-sdk/core 0.29.0 → 0.30.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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The member key bundle (#654 design §7.1, §7.2, E2b; owner decision 10a).
3
+ *
4
+ * A bundle is the public half of one member: its id, its ECDSA P-256 signing
5
+ * key, its P-256 KEM key, the KEM epoch, and `kemSig`. A device member also
6
+ * names its SDK device id. A client member does not.
7
+ *
8
+ * `kemSig` is the member's own signature over its KEM key and epoch. The
9
+ * fingerprint covers only the signing key, so a KEM rotation keeps the member
10
+ * id and the fingerprint. The self-signature binds the KEM key to the member:
11
+ * a Host that swaps the KEM key breaks the signature.
12
+ *
13
+ * An `add` chain entry carries the bundle. The pairing MACs bind it. In a
14
+ * later slice, the device registers it with device proof.
15
+ */
16
+ import { z } from 'zod';
17
+ import type { WebCryptoKey } from '../webcrypto';
18
+ import { type Bytes, type P256PublicJwk } from './primitives';
19
+ export declare const MEMBER_KEY_BUNDLE_VERSION = 1;
20
+ /** Prepended to the canonical KEM statement, so `kemSig` never verifies as another signed object. */
21
+ export declare const MEMBER_KEM_DOMAIN_PREFIX = "byok-member-kem-v1\n";
22
+ export interface MemberKeyBundleV1 {
23
+ readonly v: typeof MEMBER_KEY_BUNDLE_VERSION;
24
+ readonly memberId: string;
25
+ readonly signingPublicJwk: P256PublicJwk;
26
+ readonly kemPublicJwk: P256PublicJwk;
27
+ readonly kemEpoch: number;
28
+ /** ECDSA P-256 / SHA-256 by the signing key over the KEM statement, base64url of the 64-byte P1363 form. */
29
+ readonly kemSig: string;
30
+ /** The SDK device id of a device member. A client member has none. */
31
+ readonly deviceId?: string;
32
+ }
33
+ export declare const KemEpochSchema: z.ZodNumber;
34
+ export declare const KemSigSchema: z.ZodString;
35
+ /** `MEMBER_KEM_DOMAIN_PREFIX || canonicalJson({ memberId, kemPublicKey: { x, y }, kemEpoch })`. */
36
+ export declare function memberKemSigningInput(memberId: string, kemPublicJwk: P256PublicJwk, kemEpoch: number): Bytes;
37
+ export interface CreateMemberKeyBundleInput {
38
+ readonly signingPublicJwk: P256PublicJwk;
39
+ readonly signingPrivateKey: WebCryptoKey;
40
+ readonly kemPublicJwk: P256PublicJwk;
41
+ readonly kemEpoch: number;
42
+ readonly deviceId?: string;
43
+ }
44
+ /** Build a bundle: derive the member id and sign the KEM statement. Bad input throws `sealed_secret_invalid`. */
45
+ export declare function createMemberKeyBundle(input: CreateMemberKeyBundleInput): Promise<MemberKeyBundleV1>;
46
+ /** Both keys are points on P-256, and the KEM key is not the signing key. */
47
+ export declare function isValidMemberKemKey(kemPublicJwk: P256PublicJwk, signingPublicJwk: P256PublicJwk): Promise<boolean>;
48
+ /** The KEM key is valid for the member, and `kemSig` is the member's signature over it. Never throws. */
49
+ export declare function verifyMemberKem(member: {
50
+ readonly memberId: string;
51
+ readonly signingPublicJwk: P256PublicJwk;
52
+ }, kem: {
53
+ readonly kemPublicJwk: P256PublicJwk;
54
+ readonly kemEpoch: number;
55
+ readonly kemSig: string;
56
+ }): Promise<boolean>;
57
+ /**
58
+ * Parse a received bundle. It is valid when its shape is strict, `memberId`
59
+ * is the id of `signingPublicJwk` (which also checks that key is on the
60
+ * curve), the KEM key is a different point on the curve, and `kemSig`
61
+ * verifies. Returns `undefined` for anything else; it never throws for
62
+ * hostile input.
63
+ */
64
+ export declare function parseMemberKeyBundle(value: unknown): Promise<MemberKeyBundleV1 | undefined>;
@@ -0,0 +1,77 @@
1
+ import { type MemberKeyBundleV1 } from './member-bundle';
2
+ import { type P256PublicJwk } from './primitives';
3
+ import { type SealedCategory } from './sealed-content';
4
+ export interface MemberChainScope {
5
+ readonly categories: readonly SealedCategory[];
6
+ /** Member ids from the latest `scope-policy` entry, sorted. Some may be removed since. */
7
+ readonly members: readonly string[];
8
+ /** `seq` of that entry. */
9
+ readonly policySeq: number;
10
+ }
11
+ export interface MemberChainHead {
12
+ readonly seq: number;
13
+ readonly hash: string;
14
+ }
15
+ export interface MemberChainState {
16
+ readonly tenantId: string;
17
+ /** `null` before the genesis entry. */
18
+ readonly head: MemberChainHead | null;
19
+ /** Current members by member id, with their current KEM key. */
20
+ readonly members: ReadonlyMap<string, MemberKeyBundleV1>;
21
+ readonly removed: ReadonlySet<string>;
22
+ readonly scopes: ReadonlyMap<string, MemberChainScope>;
23
+ readonly shredded: ReadonlySet<string>;
24
+ }
25
+ export type MemberChainWalkFailure = 'member_chain_entry_invalid' | 'member_chain_link_invalid' | 'member_chain_signer_invalid' | 'member_chain_signature_invalid' | 'member_chain_subject_invalid';
26
+ export type MemberChainWalkResult = {
27
+ readonly ok: true;
28
+ readonly state: MemberChainState;
29
+ } | {
30
+ readonly ok: false;
31
+ readonly reason: MemberChainWalkFailure;
32
+ readonly index: number;
33
+ };
34
+ /** The state before the genesis entry of `tenantId`'s chain. */
35
+ export declare function emptyMemberChainState(tenantId: string): MemberChainState;
36
+ /** The latest policy's members that are still current: the members that get key packages for the scope. */
37
+ export declare function memberChainScopeRecipients(state: MemberChainState, scopeId: string): readonly MemberKeyBundleV1[];
38
+ /** Apply `entries` in order after `state`. Never throws for hostile entries. */
39
+ export declare function applyMemberChainEntries(state: MemberChainState, entries: readonly unknown[]): Promise<MemberChainWalkResult>;
40
+ export type MemberChainAnchorFailure = MemberChainWalkFailure | 'member_chain_anchor_missing' | 'member_chain_head_mismatch';
41
+ export type MemberChainAnchorResult = {
42
+ readonly ok: true;
43
+ readonly state: MemberChainState;
44
+ /** `seq` of the anchor entry: the `add` of this member, signed by the pinned approver. */
45
+ readonly anchorSeq: number;
46
+ } | {
47
+ readonly ok: false;
48
+ readonly reason: MemberChainAnchorFailure;
49
+ readonly index?: number;
50
+ };
51
+ export interface VerifyMemberChainFromAnchorInput {
52
+ readonly tenantId: string;
53
+ /** The whole chain, from the genesis entry (seq 1). */
54
+ readonly entries: readonly unknown[];
55
+ /** This member's own bundle, exactly as it was approved. */
56
+ readonly self: MemberKeyBundleV1;
57
+ /** The approver's signing key, pinned out of band: by the pv MAC or by the two-way fingerprint (10a). */
58
+ readonly approverSigningPublicJwk: P256PublicJwk;
59
+ /**
60
+ * A chain head the approver authenticated out of band, for example the
61
+ * head in the client pairing MAC. When given, it must be the anchor entry.
62
+ */
63
+ readonly authenticatedHead?: MemberChainHead;
64
+ }
65
+ /**
66
+ * Verify a chain as a member that joined after the genesis entry (11b):
67
+ *
68
+ * 1. The member pins the approver's signing key K out of band.
69
+ * 2. Entry E is an `add` whose `member` is byte-identical to `self` (KEM key
70
+ * included) and whose signer is `memberId(K)`, verified with K.
71
+ * 3. The walk from genesis reaches E, so E is on the chain path.
72
+ * 4. The entries after E verify forward by signature.
73
+ *
74
+ * The first E counts; `self` can be added only once. Never throws for
75
+ * hostile entries.
76
+ */
77
+ export declare function verifyMemberChainFromAnchor(input: VerifyMemberChainFromAnchorInput): Promise<MemberChainAnchorResult>;
@@ -0,0 +1,14 @@
1
+ import { type P256PublicJwk } from './primitives';
2
+ export declare const MEMBER_FINGERPRINT_DOMAIN = "byok-member-fingerprint-v1\n";
3
+ export interface MemberFingerprintInput {
4
+ readonly tenantId: string;
5
+ readonly signingPublicJwk: P256PublicJwk;
6
+ }
7
+ /** The 25-digit fingerprint of one member, as 5 groups of 5 digits joined by single spaces. */
8
+ export declare function memberFingerprint(input: MemberFingerprintInput): Promise<string>;
9
+ /**
10
+ * The 50-digit two-way approval code of two members of one tenant: their
11
+ * fingerprints ordered by member id, as 10 groups of 5 digits joined by
12
+ * single spaces. The order of the arguments does not matter.
13
+ */
14
+ export declare function memberApprovalCode(tenantId: string, first: P256PublicJwk, second: P256PublicJwk): Promise<string>;
@@ -0,0 +1,51 @@
1
+ import type { MemberKeyBundleV1 } from './member-bundle';
2
+ import { type Bytes } from './primitives';
3
+ export declare const PAIRING_VERIFICATION_SECRET_BYTES = 16;
4
+ export declare const PAIRING_VERIFICATION_TEXT_LENGTH = 26;
5
+ /** 128 fresh CSPRNG bits. */
6
+ export declare function generatePairingVerificationSecret(): Bytes;
7
+ export declare function encodePairingVerificationSecret(pv: Uint8Array): string;
8
+ export declare function decodePairingVerificationSecret(text: string): Bytes;
9
+ /** `<pairingCode>.<pv>`, the string the client shows and the user gives to the device. */
10
+ export declare function formatPairingInput(pairingCode: string, pv: Uint8Array): string;
11
+ export interface PairingInput {
12
+ /** The only part the device sends to the Host. */
13
+ readonly pairingCode: string;
14
+ /** Kept on the device; never sent to the Host. */
15
+ readonly pv: Bytes;
16
+ }
17
+ /** Split `<pairingCode>.<pv>` on the last `.`. Both parts are required. */
18
+ export declare function parsePairingInput(text: string): PairingInput;
19
+ /** Domain tag of the MAC the device sends: step 3, `HMAC(pv, device bundle, tenantId, deviceId)`. */
20
+ export declare const DEVICE_PAIRING_MAC_DOMAIN = "byok-pairing-device-mac-v1\n";
21
+ /** Domain tag of the MAC the client sends: step 4, `HMAC(pv, client bundle, chain head)`. */
22
+ export declare const CLIENT_PAIRING_MAC_DOMAIN = "byok-pairing-client-mac-v1\n";
23
+ export interface DevicePairingMacInput {
24
+ readonly tenantId: string;
25
+ readonly deviceId: string;
26
+ /** The device's own bundle. Its `deviceId` must equal `deviceId`. */
27
+ readonly member: MemberKeyBundleV1;
28
+ }
29
+ export interface ClientPairingMacInput {
30
+ /** The client's own bundle. A client bundle has no `deviceId`. */
31
+ readonly member: MemberKeyBundleV1;
32
+ /** The chain head after the client signed `add(device)`. */
33
+ readonly chainHead: {
34
+ readonly seq: number;
35
+ readonly hash: string;
36
+ };
37
+ }
38
+ export declare function computeDevicePairingMac(pv: Uint8Array, input: DevicePairingMacInput): Promise<string>;
39
+ export declare function computeClientPairingMac(pv: Uint8Array, input: ClientPairingMacInput): Promise<string>;
40
+ /**
41
+ * Check a received device MAC. Parse the received bundle with
42
+ * `parseMemberKeyBundle` first. Returns `false` for a wrong or malformed MAC,
43
+ * or a bundle that does not name `deviceId`. Only a malformed `pv` throws.
44
+ */
45
+ export declare function verifyDevicePairingMac(pv: Uint8Array, input: DevicePairingMacInput, mac: unknown): Promise<boolean>;
46
+ /**
47
+ * Check a received client MAC. Parse the received bundle with
48
+ * `parseMemberKeyBundle` first. Returns `false` for a wrong or malformed MAC,
49
+ * a device bundle, or a malformed chain head. Only a malformed `pv` throws.
50
+ */
51
+ export declare function verifyClientPairingMac(pv: Uint8Array, input: ClientPairingMacInput, mac: unknown): Promise<boolean>;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Sealed blob bytes v1 (#654 design §8.1, E1c).
3
+ *
4
+ * A protocol field blob (`instruction.blobRef`, a progress spill, an artifact)
5
+ * is sealed before its outer content, by the same frame, key and AEAD rule.
6
+ * Its AAD is the 16 outer AAD members that do not depend on the outer
7
+ * plaintext, the blob's own `paddedLength`, and `fieldPath`: the RFC 6901
8
+ * JSON Pointer from the moved-fields object to its BlobRef. The blob's salt,
9
+ * nonce and padded length travel next to the BlobRef inside the outer
10
+ * plaintext, so the outer AEAD and content MAC cover them.
11
+ *
12
+ * Internal: the subpath index does not import this module, so it adds no
13
+ * bytes to the published closure.
14
+ */
15
+ import { z } from 'zod';
16
+ import { type SealedBinding, type SealedCategory, type SealedContentHeaderV1, type SealedContentOpenFailure, type SealedContentRandomness } from './sealed-content';
17
+ /** What the outer plaintext carries next to a sealed blob's BlobRef. */
18
+ export declare const SealedBlobParamsV1Schema: z.ZodObject<{
19
+ salt: z.ZodString;
20
+ nonce: z.ZodString;
21
+ paddedLength: z.ZodNumber;
22
+ }, z.core.$strict>;
23
+ export type SealedBlobParamsV1 = z.infer<typeof SealedBlobParamsV1Schema>;
24
+ /**
25
+ * The canonical AAD bytes of a blob sealed under an outer header (§8.1).
26
+ */
27
+ export declare function sealedBlobAad(header: SealedContentHeaderV1, binding: SealedBinding, paddedLength: number, fieldPath: string): Uint8Array;
28
+ /**
29
+ * The input of one blob seal. Scope, chain head, sender, category and binding
30
+ * must be the ones of the outer content that will carry the BlobRef.
31
+ */
32
+ export interface SealBlobInput {
33
+ readonly scopeKey: Uint8Array;
34
+ readonly scope: {
35
+ readonly id: string;
36
+ readonly epoch: number;
37
+ };
38
+ readonly chainHead: {
39
+ readonly seq: number;
40
+ readonly hash: string;
41
+ };
42
+ readonly sender: string;
43
+ readonly category: SealedCategory;
44
+ readonly binding: SealedBinding;
45
+ /** The pointer to the BlobRef node in the moved-fields object. */
46
+ readonly fieldPath: string;
47
+ /** The raw blob bytes. */
48
+ readonly bytes: Uint8Array;
49
+ /** The plaintext limit of this field, in bytes. */
50
+ readonly maxContentLength: number;
51
+ }
52
+ export interface SealedBlob {
53
+ readonly params: SealedBlobParamsV1;
54
+ /** AES-256-GCM output: the padded frame followed by the 16-byte tag. Upload these bytes. */
55
+ readonly ciphertext: Uint8Array;
56
+ }
57
+ /**
58
+ * Seal one blob once, before the outer content. A retry must reuse the
59
+ * uploaded blob, never seal again (§8.6).
60
+ */
61
+ export declare function sealBlob(input: SealBlobInput): Promise<SealedBlob>;
62
+ /**
63
+ * {@link sealBlob} with a caller-chosen salt and nonce, for test vectors only.
64
+ */
65
+ export declare function sealBlobWithRandomness(input: SealBlobInput, randomness: SealedContentRandomness): Promise<SealedBlob>;
66
+ export interface OpenSealedBlobInput {
67
+ /** The outer header that `openSealedContent` authenticated. */
68
+ readonly header: SealedContentHeaderV1;
69
+ /** The binding the outer content opened under. */
70
+ readonly binding: SealedBinding;
71
+ readonly scopeKey: Uint8Array;
72
+ /** The receiver derives it from where it found the BlobRef, never from a table. */
73
+ readonly fieldPath: string;
74
+ /** From the opened outer plaintext. Its shape is still checked here. */
75
+ readonly params: unknown;
76
+ /** The downloaded blob bytes. The caller checks the BlobRef `contentHash` against them (§9.3). */
77
+ readonly ciphertext: Uint8Array;
78
+ readonly maxContentLength: number;
79
+ }
80
+ export type SealedBlobOpenResult = {
81
+ readonly ok: true;
82
+ readonly bytes: Uint8Array;
83
+ } | {
84
+ readonly ok: false;
85
+ readonly reason: SealedContentOpenFailure;
86
+ };
87
+ /**
88
+ * Open one blob, fail closed. In order: binding and category
89
+ * (`sealing_policy_mismatch`); header, field path and params shape
90
+ * (`sealed_open_failed`); key id (`sealed_key_unavailable`); ciphertext
91
+ * length, AEAD under the rebuilt blob AAD and the five frame checks (all
92
+ * `sealed_open_failed`). Nothing here throws for hostile input.
93
+ */
94
+ export declare function openSealedBlob(input: OpenSealedBlobInput): Promise<SealedBlobOpenResult>;
@@ -22,8 +22,9 @@
22
22
  * `contentMac` is a keyed MAC over category, family, identity and plaintext. The
23
23
  * Host compares two submissions of one identity by it, without a key.
24
24
  *
25
- * Blob fields are not sealed here yet; their AAD rule comes with the blob wire
26
- * rules.
25
+ * A blob field is sealed by the same frame and key rule, before its outer
26
+ * content. Its AAD is the outer AAD without `paddedLength` and `contentMac`,
27
+ * plus its own `paddedLength` and the JSON Pointer `fieldPath` to its BlobRef.
27
28
  */
28
29
  import { z } from 'zod';
29
30
  import { type JsonObject } from '../attestation';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Canonical JSON of a sealed plaintext (#786): RFC 8785 with its full number
3
+ * rule.
4
+ *
5
+ * Keys sort by UTF-16 code unit and strings use the ECMAScript serialization,
6
+ * the same as `canonicalizeJson`. Numbers differ. A finite number serializes
7
+ * as `Number.prototype.toString` (RFC 8785 §3.2.2.3), so a decimal, an
8
+ * exponent and an integer above 2^53 are sealable. `-0` serializes as `0`.
9
+ * NaN and the infinities have no JSON form and throw.
10
+ *
11
+ * Only the sealed plaintext uses this rule. The AAD, the MAC identity, member
12
+ * chain entries and key packages keep `canonicalizeJson`, whose safe-integer
13
+ * rule belongs to the attestation contract.
14
+ */
15
+ import type { JsonValue } from '../attestation';
16
+ /** The canonical JSON text of a sealed plaintext value. Throws `sealed_secret_invalid` outside JSON. */
17
+ export declare function canonicalizeSealedJson(value: JsonValue): string;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A zod parse that never throws.
3
+ *
4
+ * `safeParse` returns a failure for invalid data, but it does not catch an
5
+ * error that a getter or a proxy trap throws while zod reads the value, and a
6
+ * revoked proxy throws on any read. A receiver that promises a typed result
7
+ * for hostile input, or a typed error for bad caller input, parses through
8
+ * this. Wire input from `JSON.parse` cannot hold getters or proxies; this
9
+ * closes the same gap for in-process callers.
10
+ */
11
+ import type { z } from 'zod';
12
+ export type TotalParseResult<T> = {
13
+ readonly success: true;
14
+ readonly data: T;
15
+ } | {
16
+ readonly success: false;
17
+ readonly error?: z.ZodError;
18
+ };
19
+ /** `schema.safeParse(value)`, with a throw while reading `value` as a failure without issues. */
20
+ export declare function safeParseTotal<S extends z.ZodType>(schema: S, value: unknown): TotalParseResult<z.output<S>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byok-sdk/core",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "BYOK SDK platform contracts: branded tenant identity, tenant-first store ports, device proof canonicalization, and the composition conformance reference",
5
5
  "type": "module",
6
6
  "license": "MIT",