Spaces:
Running
Running
File size: 6,233 Bytes
518343a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | /**
* @file packages/rae1/src/hmac.ts
* @description RAE-1 DSSE HMAC-SHA-256 signature gate.
*
* Implements HMAC-SHA-256 verification over the canonical DSSE v1
* Pre-Authentication Encoding (PAE) for DSSE envelopes, per RAE_1_PROTOCOL.md
* §5.3 and the DSSE spec
* (https://github.com/secure-systems-lab/dsse/blob/master/protocol.md).
*
* Canonical PAE formula (the ONLY one used across SZL — see ./dsse-pae.ts):
* PAE = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body
* SP = 0x20, LEN = ASCII decimal byte length, body = RAW envelope body.
*
* The DSSE envelope `payload` field is base64url of the raw body, so the PAE is
* computed over the base64-DECODED body (dsseV1PaeFromBase64Body).
*
* The signature in each DSSEEnvelope.signatures[i].sig is:
* base64url(HMAC-SHA-256(key, dsseV1PaeFromBase64Body(payloadType, payload)))
*
* NOTE: prior to PR fix(rae1) this file used the old in-toto LE64 binary PAE,
* which has no "DSSEv1" prefix and is not DSSEv1-compatible
* (PhD_CRYPTO_VERDICT.md Finding A1, hmac.ts:53-76).
*
* Lean ref: SZL.AGI.PACBayes.capability_improvement_rate_bound
* Lean file: Lutar/PACBayes/CapabilityImprovementRate.lean
* Lean commit: c4d1379568
*
* Doctrine v6 — no fake green, real HMAC-SHA-256.
* Signed-off-by: SZL Engineering <eng@szl-holdings.com>
*/
import { createHmac, timingSafeEqual } from "crypto";
import { dsseV1Pae, dsseV1PaeFromBase64Body } from "./dsse-pae.js";
// ─── PAE (Pre-Authentication Encoding) ───────────────────────────────────────
/**
* Canonical DSSE v1 PAE for an [payloadType, base64Body] item pair.
*
* Backward-compatible call shape: callers historically invoked
* pae([payloadType, base64Payload])
* where the second item is the envelope's base64url `payload` field. The PAE is
* now computed over the RAW (base64-decoded) body, per the DSSE spec.
*
* @param items - exactly [payloadType, base64Body]
* @returns Buffer containing the canonical DSSE v1 PAE
*
* @example
* ```typescript
* const encoded = pae(["application/vnd.szl.rae1+json", base64urlPayload]);
* const sig = createHmac("sha256", key).update(encoded).digest("base64url");
* ```
*/
export function pae(items: string[]): Buffer {
if (items.length !== 2) {
throw new Error(
`DSSE v1 PAE requires exactly [payloadType, base64Body]; got ${items.length} items`
);
}
const [payloadType, base64Body] = items;
return Buffer.from(dsseV1PaeFromBase64Body(payloadType, base64Body));
}
/**
* Canonical DSSE v1 PAE over a payload type and RAW body bytes.
* Re-exported convenience for callers that already hold raw bytes.
*/
export function paeRaw(payloadType: string, body: Uint8Array): Buffer {
return Buffer.from(dsseV1Pae(payloadType, body));
}
// ─── HMAC Verification ───────────────────────────────────────────────────────
/**
* Verifies the DSSE HMAC-SHA-256 signature on a single RAE-1 receipt envelope.
*
* Checks all signatures in the envelope; returns true if any signature matches
* the provided key (following the DSSE multi-signer model).
*
* Algorithm:
* 1. Compute PAE([envelope.payloadType, envelope.payload])
* 2. Compute expected = HMAC-SHA-256(key, PAE_result) → base64url
* 3. Return true if any signature in envelope.signatures has sig === expected
*
* Lean ref: SZL.AGI.PACBayes.capability_improvement_rate_bound
* file: Lutar/PACBayes/CapabilityImprovementRate.lean
* commit: c4d1379568
*
* @param envelope - DSSE envelope with payloadType, payload, and signatures
* @param key - Raw HMAC key as a Buffer
* @returns True if at least one signature is valid for the provided key
*
* @example
* ```typescript
* const key = Buffer.from(process.env.RAE1_HMAC_KEY!, "base64url");
* const valid = verifyHMAC(envelope, key);
* if (!valid) throw new Error("Receipt signature verification failed");
* ```
*/
export function verifyHMAC(
envelope: {
payloadType: string;
payload: string;
signatures: Array<{ keyid: string; sig: string }>;
},
key: Buffer
): boolean {
const message = pae([envelope.payloadType, envelope.payload]);
const expected = createHmac("sha256", key).update(message).digest();
// Timing-safe comparison over the raw MAC bytes (PhD_CRYPTO_VERDICT.md
// "Positive" nit: hmac.ts:116 previously used `===` string compare on the
// public tag). Decode each candidate signature and compare in constant time.
return envelope.signatures.some((s) => {
let actual: Buffer;
try {
actual = Buffer.from(s.sig, "base64url");
} catch {
return false;
}
if (actual.length !== expected.length) return false;
return timingSafeEqual(actual, expected);
});
}
/**
* Signs a DSSE envelope with HMAC-SHA-256, adding a signature entry.
*
* Returns a new envelope with the signature appended; does not mutate the input.
*
* @param envelope - Existing envelope (with or without prior signatures)
* @param key - Raw HMAC key as a Buffer
* @param keyid - Key identifier in format "hmac-sha256:<sha256-of-key-material>"
* @returns New envelope with the HMAC signature added to signatures array
*/
export function signEnvelope<T extends {
payloadType: string;
payload: string;
signatures: Array<{ keyid: string; sig: string }>;
}>(
envelope: T,
key: Buffer,
keyid: string
): T {
const message = pae([envelope.payloadType, envelope.payload]);
const sig = createHmac("sha256", key).update(message).digest("base64url");
return {
...envelope,
signatures: [...envelope.signatures, { keyid, sig }],
};
}
/**
* Generates a key ID string for an HMAC key.
*
* Format per RAE-1 §2.1: "hmac-sha256:<sha256-of-key-material>"
* where the sha256 is the hex digest of the raw key bytes.
*
* @param key - Raw HMAC key as a Buffer
* @returns keyid string
*/
export function makeKeyId(key: Buffer): string {
const { createHash } = require("crypto");
const keyHash = createHash("sha256").update(key).digest("hex");
return `hmac-sha256:${keyHash}`;
}
|