/** * Device proof envelope and canonical signing bytes (§12.6.3, sprint §S6.2). * * Three things live here and nothing else: * * 1. **The protected-claims schema.** Every field a signature must cover so the * proof binds a *specific request*: tenant, product, device, key + epoch, * request id, operation, resource, the request line (method+path or a stable * operation id), and the body's hash *and* size. Signing only the body is * forbidden — it would let a valid proof be replayed against a different * operation or resource. * 2. **A dependency-free deterministic canonicalizer.** RFC 8785 (JCS) in the * narrow subset the envelope allows. Node and Workers must produce * byte-identical output, so the accepted value space is deliberately small: * strings, booleans, null, safe integers, plain objects, arrays. Floats, * non-safe integers, `NaN`, `Infinity`, `undefined`, `bigint`, dates and * class instances throw instead of being coerced — a signature over a value * whose serialization is implementation-defined is not a signature. * 3. **The verify port.** An interface, never an implementation: core is * Node-free and Workers-safe, and `node:crypto` and WebCrypto disagree about * key handling. The composition brings its own verifier. * * Claims are **untrusted input**. `tenantId` here is a plain string, not a * branded `TenantId`, because a device asserting a tenant proves nothing: the * claim is a lookup key used to load the device row, and the row is the * authority (§12.6.2 layer 5). Branding happens after that lookup, not here. */ import { z } from 'zod'; /** Envelope schema id, self-consistent with the domain prefix below. */ export declare const DEVICE_PROOF_SCHEMA_ID = "byok-device-proof-v1"; /** * Domain separation prefix (§12.6.3). Prepended to the canonical claim bytes * before signing so a device-proof signature can never be replayed as a nonce * signature (`byok-nonce-v1\n`) or a record attestation. */ export declare const DEVICE_PROOF_DOMAIN_PREFIX = "byok-device-proof-v1\n"; export declare const DEVICE_PROOF_VERSION = 1; /** Signature algorithms this envelope version admits. */ export declare const DEVICE_PROOF_ALGORITHMS: readonly ['ed25519']; export type DeviceProofAlgorithm = (typeof DEVICE_PROOF_ALGORITHMS)[number]; /** * HTTP header carrying the base64url device-proof envelope on a truth-route * request (§12.6.3). Both the daemon that mints the proof and the host that * decodes it must name the exact same header, so it is a single wire constant * here rather than a string re-spelled on each side. */ export declare const DEVICE_PROOF_HEADER = "x-byok-device-proof"; export type JsonPrimitive = string | number | boolean | null; export type JsonValue = JsonPrimitive | readonly JsonValue[] | { readonly [key: string]: JsonValue; }; export type JsonObject = { readonly [key: string]: JsonValue; }; /** * Canonical JSON text for `value`, per RFC 8785 restricted to the value space * above. Key insertion order never affects the output; anything outside the * accepted space throws `proof_canonicalization_failed`. */ export declare function canonicalizeJson(value: JsonValue): string; /** UTF-8 bytes of {@link canonicalizeJson}. `TextEncoder` is a platform global on Node and Workers. */ export declare function canonicalizeJsonBytes(value: JsonValue): Uint8Array; /** * The signed claim set (§S6.2). * * The request line is expressible two ways — `method` + `path`, or a stable * `operationId` — and exactly one form must be present. Allowing both at once * would create two different canonical byte strings for one request; allowing * neither would drop the operation binding the whole design rests on. */ export declare const DeviceProofProtectedClaimsSchema: z.ZodObject<{ version: z.ZodLiteral<1>; tenantId: z.ZodString; productId: z.ZodString; deviceId: z.ZodString; keyId: z.ZodString; keyEpoch: z.ZodNumber; requestId: z.ZodString; operation: z.ZodString; resource: z.ZodString; method: z.ZodOptional; path: z.ZodOptional; operationId: z.ZodOptional; bodySha256: z.ZodString; bodySize: z.ZodNumber; issuedAt: z.ZodISODateTime; expiresAt: z.ZodOptional; nonce: z.ZodOptional; }, z.core.$strict>; export type DeviceProofProtectedClaims = z.infer; export declare const DeviceProofEnvelopeV1Schema: z.ZodObject<{ schema: z.ZodLiteral<"byok-device-proof-v1">; algorithm: z.ZodEnum<{ ed25519: "ed25519"; }>; protected: z.ZodObject<{ version: z.ZodLiteral<1>; tenantId: z.ZodString; productId: z.ZodString; deviceId: z.ZodString; keyId: z.ZodString; keyEpoch: z.ZodNumber; requestId: z.ZodString; operation: z.ZodString; resource: z.ZodString; method: z.ZodOptional; path: z.ZodOptional; operationId: z.ZodOptional; bodySha256: z.ZodString; bodySize: z.ZodNumber; issuedAt: z.ZodISODateTime; expiresAt: z.ZodOptional; nonce: z.ZodOptional; }, z.core.$strict>; signature: z.ZodString; }, z.core.$strict>; export type DeviceProofEnvelopeV1 = z.infer; /** * Parses an envelope fail-closed. * * @throws {ByokCoreError} code `proof_envelope_invalid`. */ export declare function parseDeviceProofEnvelope(input: unknown): DeviceProofEnvelopeV1; /** * Projects claims into the exact JSON object that gets canonicalized. * * Built field by field rather than by spreading the parsed object: an absent * optional must be an *absent key*, never a key holding `undefined`, or * `{nonce: undefined}` and `{}` would be the same request with two different * signatures. The canonicalizer refuses `undefined` outright, so this * projection is where absence is decided. */ export declare function deviceProofCanonicalClaims(claims: DeviceProofProtectedClaims): JsonObject; /** Canonical JSON text of the protected claim set, without the domain prefix. */ export declare function deviceProofCanonicalJson(claims: DeviceProofProtectedClaims): string; /** * The exact bytes a device signs and a verifier reconstructs: * `byok-device-proof-v1\n` followed by the canonical claim JSON, UTF-8 encoded. * * Frozen by `src/__tests__/golden/device-proof-v1.canonical.json`. */ export declare function deviceProofSigningInput(claims: DeviceProofProtectedClaims): Uint8Array; export interface DeviceProofVerifyInput { readonly algorithm: DeviceProofAlgorithm; /** Raw public key, base64url — the JWK `x` encoding the device registry stores. */ readonly publicKey: string; readonly signature: string; readonly signingInput: Uint8Array; } /** * Injected signature verification. * * Not a store port, so it has no tenant parameter: it answers a pure * cryptographic question about bytes. Deciding *which* key to check against is * the caller's tenant-scoped device-row lookup, and keeping that out of here is * what stops a verifier from becoming a second authority on device identity. */ export interface DeviceProofVerifier { verify(input: DeviceProofVerifyInput): Promise; }