2.7 KiB
canonical
Purpose
Implements canonicalSerialize, the deterministic byte-level encoding that gates every server signature on a DriveTemplate (DTP-03). Two implementations of the function MUST produce byte-identical output for identical input regardless of in-memory key order. JSON is deliberately rejected because object-key ordering is not portable across runtimes.
Features
- Hand-rolled big-endian binary writer (
ByteWriter) withuint8/uint16/uint32/uint64/bool/string/bytesprimitives plus length-prefixedoptional*helpers. - Fixed field order across the entire template tree —
schemaVersion,driveId,issuer,issuedAt,expiresAt,nonce,policy,cdnFallback,rootFolder. - Recursive folder/file walker that tags each node with
TAG_FOLDER(0x01) /TAG_FILE(0x02) so the decoder can reconstruct the tree without a separate manifest. - Encryption-aware:
inlineandretroactiveKeyDistributionvariants are tagged 1 / 2 respectively; thewrappedKeyblob andkeyAuthoritystring follow. - Excludes
serverSignature— the bytes the issuer signs MUST not include the signature itself.
Usage: Basic Example
import { canonicalSerialize } from "@suaveplan/drive-template";
const bytes = canonicalSerialize({
schemaVersion: 1,
driveId: "drive-1",
rootFolder: { name: "/", children: [] },
policy: { defaultPolicy: { autoDownload: false, meshAllowed: true, cdnAllowed: true } },
issuer: "00".repeat(32),
issuedAt: Date.now(),
expiresAt: Date.now() + 60_000,
nonce: new Uint8Array(16).fill(7),
});
Usage: Advanced Example
import { canonicalSerialize, signTemplate } from "@suaveplan/drive-template";
// canonicalSerialize is the inputs-side; signTemplate runs Ed25519 over it.
const tpl = signTemplate(buildTemplate(), serverSecretKey);
const recanon = canonicalSerialize({ ...tpl, serverSignature: undefined as never });
console.log(`signed bytes: ${recanon.length}`);
API Reference
canonicalSerialize(template: Omit<DriveTemplate, "serverSignature">): Uint8Array.- Internal-only
ByteWriterclass with explicit primitives — not exported.
Implementation Notes
The ByteWriter accumulates Uint8Array parts and concatenates once at finish() to avoid quadratic growth. optionalUint64 / optionalUint32 / optionalString write a discriminator byte first so receivers know whether the field follows; this trades two bytes per optional for unambiguous parsing. The retention policy is encoded as a tag (0 = unset, 1 = permanent, 2 = lru, 3 = { maxAgeMs }) so future variants can extend the union without breaking older readers — they will see an unknown tag and reject the template through the schema-version pin.