File size: 3,285 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
/**
 * @file packages/rae1/src/dsse-pae.ts
 * @description Canonical DSSE v1 Pre-Authentication Encoding (PAE).
 *
 * Per https://github.com/secure-systems-lab/dsse/blob/master/protocol.md
 *   PAE = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body
 *   SP  = 0x20 (single ASCII space)
 *   LEN = ASCII decimal byte-length of the following field (no leading zeros,
 *         no negatives)
 *   type, body are UTF-8 bytes; body is the RAW envelope body, NOT base64.
 *
 * This is the single source of truth for PAE across all SZL consumers
 * (rae1/hmac.ts, vessels/pepr/*). Any change here is a wire-format change and
 * must be mirrored byte-for-byte in vessels/pepr/dsse-pae.ts until the module
 * is deduplicated via npm publish.
 *
 * Source of truth frozen against the DSSE spec example: for type
 * "http://example.com/HelloWorld" and body "hello world", the canonical PAE is
 *   "DSSEv1 29 http://example.com/HelloWorld 11 hello world"
 * (see protocol.md "Test vectors").
 */

/** Concatenate a list of byte arrays into a single Uint8Array. */
function concat(arrs: Uint8Array[]): Uint8Array {
  let total = 0;
  for (const a of arrs) total += a.length;
  const out = new Uint8Array(total);
  let off = 0;
  for (const a of arrs) {
    out.set(a, off);
    off += a.length;
  }
  return out;
}

/**
 * Compute the canonical DSSE v1 PAE over a payload type and a raw payload body.
 *
 * @param payloadType - the DSSE payloadType (e.g. "application/vnd.szl.receipt.v1+json")
 * @param payload     - the RAW body bytes (NOT base64)
 * @returns the PAE byte string
 */
export function dsseV1Pae(payloadType: string, payload: Uint8Array): Uint8Array {
  const enc = new TextEncoder();
  const typeBytes = enc.encode(payloadType);
  const sp = new Uint8Array([0x20]);
  const prefix = enc.encode("DSSEv1");
  const lenType = enc.encode(String(typeBytes.length));
  const lenBody = enc.encode(String(payload.length));
  return concat([prefix, sp, lenType, sp, typeBytes, sp, lenBody, sp, payload]);
}

/**
 * Convenience: compute the canonical DSSE v1 PAE when the body is supplied as a
 * base64 (standard or base64url) string, as it is in a DSSE envelope's
 * `payload` field. The PAE is computed over the RAW decoded body bytes, per the
 * spec — the base64 form is never fed into the PAE.
 *
 * @param payloadType - the DSSE payloadType
 * @param payloadB64  - the envelope `payload` field (base64 or base64url)
 * @returns the PAE byte string
 */
export function dsseV1PaeFromBase64Body(
  payloadType: string,
  payloadB64: string
): Uint8Array {
  const raw = base64ToBytes(payloadB64);
  return dsseV1Pae(payloadType, raw);
}

/**
 * Decode a base64 or base64url string to raw bytes.
 * Tolerates missing padding and the URL-safe alphabet.
 */
export function base64ToBytes(s: string): Uint8Array {
  const normalized = s.replace(/-/g, "+").replace(/_/g, "/");
  const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
  // Buffer is available in Node; fall back to atob in browser/edge runtimes.
  if (typeof Buffer !== "undefined") {
    return new Uint8Array(Buffer.from(padded, "base64"));
  }
  const bin = atob(padded);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}