You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys-App/src/lib/dkc/header.ts

170 lines
6.3 KiB

// PUBLIC_HEADER (spec §24, §27), as DecodeHeader and EncodeHeader of the Go
// package capsule at 3820066.
import { goQuote, toHex, utf8Length } from './bytes.ts';
import { checkSchema, type Decoder, Encoder, MAX_SAFE_UINT, unmarshal } from './cbor.ts';
import { compactDateKey, type DateKey, parseDateKey } from './datekey.ts';
import { DateKeysError, withContext } from './errors.ts';
import { MAX_PUBLIC_HEADER_LEN } from './framing.ts';
import { canonicalExtensions, checkDisjoint, decodeArray, type Extension } from './extension.ts';
import { encodeExtensionArrays, fieldOf, presence, requireKeys } from './schema.ts';
export const HEADER_TYPE_TAG = 'datekeycap';
export const HEADER_VERSION = 1;
export const CAPSULE_ID_SIZE = 16;
/** The declared access policy (spec §25): 0 time_only, 1 time_and_key. */
export type Policy = number;
export const TIME_ONLY: Policy = 0;
export const TIME_AND_KEY: Policy = 1;
/** "time_only", "time_and_key" or "policy(n)", as Go's Policy.String. */
export function policyName(p: Policy): string {
if (p === TIME_ONLY) return 'time_only';
if (p === TIME_AND_KEY) return 'time_and_key';
return `policy(${p})`;
}
/** Parses "time_only" or "time_and_key". */
export function parsePolicy(s: string): Policy {
if (s === 'time_only') return TIME_ONLY;
if (s === 'time_and_key') return TIME_AND_KEY;
throw new Error(`capsule: unknown access policy ${goQuote(s)}`);
}
/** PUBLIC_HEADER (spec §24). The profile comes from the DateKey only. */
export interface Header {
/** key 2, 16 bytes. */
readonly capsuleId: Uint8Array;
/** key 3, canonical dk1_. */
readonly dateKey: DateKey;
/** key 4, access_policy. */
readonly policy: Policy;
/** key 5. */
readonly critical: readonly Extension[];
/** key 6. */
readonly noncritical: readonly Extension[];
}
/** The capsule_id in hexadecimal. */
export function capsuleIdHex(h: Header): string {
return toHex(h.capsuleId);
}
// PUBLIC_HEADER as it is encoded: keys 2 to 6, keys 0 and 1 being the
// constants HEADER_TYPE_TAG and HEADER_VERSION.
interface HeaderWire {
capsuleId: Uint8Array;
dateKey: string;
policy: number;
critical: Extension[] | undefined;
noncritical: Extension[] | undefined;
}
// Reads PUBLIC_HEADER with every CDDL rule whose violation is
// ERR_NON_CANONICAL_CBOR, the extension arrays included (layer 3 of spec
// §69.1); the DateKey, which has codes of its own (spec §57), is parsed
// afterwards.
function decodeWire(d: Decoder): HeaderWire {
const w: HeaderWire = { capsuleId: new Uint8Array(0), dateKey: '', policy: 0, critical: undefined, noncritical: undefined };
const pairs = d.map(7);
const seen = new Set<number>();
for (let i = 0; i < pairs; i++) {
const k = d.key();
const field = fieldOf(k);
switch (k) {
case 0:
field(() => d.text(utf8Length(HEADER_TYPE_TAG)));
break;
case 1:
field(() => d.uint(HEADER_VERSION));
break;
case 2:
w.capsuleId = field(() => d.bstr(CAPSULE_ID_SIZE, CAPSULE_ID_SIZE));
break;
case 3:
w.dateKey = field(() => d.text(MAX_PUBLIC_HEADER_LEN));
break;
case 4:
// Compared as read: 256, 257 or 2^32 are not a V1 policy.
w.policy = field(() => {
const p = d.uint(MAX_SAFE_UINT);
if (p > TIME_AND_KEY) throw new DateKeysError('ERR_NON_CANONICAL_CBOR', `access_policy ${p} is not defined in V1`);
return p;
});
break;
case 5:
w.critical = field(() => decodeArray(d));
break;
case 6:
w.noncritical = field(() => decodeArray(d));
break;
default:
throw new DateKeysError('ERR_NON_CANONICAL_CBOR', `key ${k} is not defined`);
}
seen.add(Number(k));
}
requireKeys(seen, 5);
d.endMap();
return w;
}
function encodeWire(e: Encoder, w: HeaderWire): void {
e.map(5 + presence(w.critical) + presence(w.noncritical));
e.uint(0);
e.text(HEADER_TYPE_TAG);
e.uint(1);
e.uint(HEADER_VERSION);
e.uint(2);
e.bstr(w.capsuleId);
e.uint(3);
e.text(w.dateKey);
e.uint(4);
e.uint(w.policy);
encodeExtensionArrays(e, 5, w.critical, w.noncritical);
}
/**
* Validates and decodes PUBLIC_HEADER bytes (spec §24, §27, §63 step 4) in
* the layers of spec §69.1: the §57 limit (layer 1, ERR_INTEGRITY); the type
* tag and the schema version (layer 2); canonical CBOR and the CDDL, with a
* 16-byte capsule_id, a V1 access policy and well-formed extension arrays in
* strictly ascending order, and no identifier in both arrays (layer 3,
* ERR_NON_CANONICAL_CBOR); and only then a canonical DateKey (layer 4).
* Whether the profile is pinned and the critical extensions known is decided
* by the caller, still in layer 4.
*/
export function decodeHeader(b: Uint8Array): Header {
if (b.length > MAX_PUBLIC_HEADER_LEN) {
throw new DateKeysError('ERR_INTEGRITY', `capsule: PUBLIC_HEADER of ${b.length} bytes exceeds ${MAX_PUBLIC_HEADER_LEN}`);
}
return withContext('capsule: PUBLIC_HEADER', () => {
checkSchema(b, HEADER_TYPE_TAG, HEADER_VERSION);
const w = unmarshal(b, decodeWire, encodeWire);
checkDisjoint(w.critical ?? [], w.noncritical ?? []);
const dateKey = parseDateKey(w.dateKey);
return { capsuleId: w.capsuleId, dateKey, policy: w.policy, critical: w.critical ?? [], noncritical: w.noncritical ?? [] };
});
}
/** The Deterministic CBOR bytes of `h`, at most MAX_PUBLIC_HEADER_LEN. */
export function encodeHeader(h: Header): Uint8Array {
const compact = compactDateKey(h.dateKey);
if (compact === '') throw new DateKeysError('ERR_DATEKEY_INVALID', 'capsule: invalid DateKey');
if (h.policy !== TIME_ONLY && h.policy !== TIME_AND_KEY) throw new Error(`capsule: unknown access policy ${h.policy}`);
if (h.capsuleId.length !== CAPSULE_ID_SIZE) {
throw new DateKeysError('ERR_NON_CANONICAL_CBOR', `capsule: capsule_id is ${h.capsuleId.length} bytes, want ${CAPSULE_ID_SIZE}`);
}
const critical = canonicalExtensions(h.critical);
const noncritical = canonicalExtensions(h.noncritical);
checkDisjoint(h.critical, h.noncritical);
const e = new Encoder();
encodeWire(e, { capsuleId: h.capsuleId, dateKey: compact, policy: h.policy, critical, noncritical });
const b = e.out();
if (b.length > MAX_PUBLIC_HEADER_LEN) {
throw new DateKeysError('ERR_INTEGRITY', `capsule: PUBLIC_HEADER of ${b.length} bytes exceeds ${MAX_PUBLIC_HEADER_LEN}`);
}
return b;
}

Powered by TurnKey Linux.