Skip to content
This repository was archived by the owner on Oct 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 111 additions & 46 deletions lib/crypto.rce
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,17 @@ fn seal(key: List[i64], nonce: List[i64], ad: List[i64], pt: List[i64]) -> text
let tag = slice(hmac(slice(copy(ks), 32, 64), cat(cat(copy(nonce), ad), copy(ct))), 0, 16)
return tohex(cat(ct, tag))

# Equality of two byte lists with no early exit: the OR of all byte XORs is
# tested once at the end. Only the lengths are public (a length mismatch
# answers at once). Constant fuel for any contents of equal length.
fn ct_eq(a: List[i64], b: List[i64]) -> bool
if a.len() != b.len()
return false
var acc: i64 = 0
for i in 0..a.len()
acc = acc | (a[i] ^ b[i])
return acc == 0

# The plaintext bytes, or [-1] when the tag does not verify.
fn open(key: List[i64], nonce: List[i64], ad: List[i64], sealed: text) -> List[i64]
let b = unhex(sealed)
Expand All @@ -191,19 +202,19 @@ fn open(key: List[i64], nonce: List[i64], ad: List[i64], sealed: text) -> List[i
let tag = slice(copy(b), b.len() - 16, b.len())
let ks = hkdf([], copy(key), tobytes("rce-aead-v0"), 64)
let want = slice(hmac(slice(copy(ks), 32, 64), cat(cat(copy(nonce), ad), copy(ct))), 0, 16)
if tohex(want) != tohex(tag)
if not ct_eq(want, tag)
return [-1]
return chacha20(slice(copy(ks), 0, 32), nonce, ct)

# Value of one lowercase hex digit (0 for any other character, as before),
# by arithmetic only: no digit table scan and no branch on the digit. The
# range tests use the sign of a difference: ((c - lo) | (hi - c)) >> 63 is
# all ones exactly when c is outside lo..hi.
fn hexval(c: text) -> i64
let ds = "0123456789abcdef".chars()
var v: i64 = 0
var k: i64 = 0
for d in ds
if d == c
v = k
k += 1
return v
let b = c[0] as i64
let dm = (((b - 48) | (57 - b)) >> 63) ^ (0 - 1)
let am = (((b - 97) | (102 - b)) >> 63) ^ (0 - 1)
return (dm & (b - 48)) | (am & (b - 87))

fn unhex(h: text) -> List[i64]
let cs = h.chars()
Expand All @@ -214,12 +225,24 @@ fn unhex(h: text) -> List[i64]
i += 2
return out

# ASCII code of the lowercase hex digit of n in 0..15: 48 + n, plus 39 when
# n > 9 (mask from the sign of 9 - n). No table index.
fn hexdig(n: i64) -> i64 = 48 + n + (39 & ((9 - n) >> 63))

# Hex text of a byte list. Elements must be bytes: anything outside 0..255
# traps, as an out-of-range table index did before. The trap comes from
# utf8() of a guard element that is 256 exactly when the OR of all elements
# has a bit outside 0..255, so there is no branch on the data.
fn tohex(b: List[i64]) -> text
let ds = "0123456789abcdef".chars()
var s = ""
var codes: List[i64] = []
var acc: i64 = 0
for x in b
s = s + copy(ds[(x >> 4) as usize]) + copy(ds[(x & 15) as usize])
return s
acc = acc | x
codes.push(hexdig(x >> 4))
codes.push(hexdig(x & 15))
let t = acc >> 8
let g = [((t | (0 - t)) >> 63 & 1) << 8].utf8()
return codes.utf8()

# GF(2^255 - 19) in ten 26-bit limbs (radix 2^26, 2^260 = 608 mod p) in
# i64: limb products stay below 2^58 before carrying.
Expand Down Expand Up @@ -313,6 +336,21 @@ fn unpack(b: List[i64]) -> List[i64]
o[li] = o[li] | (bit << (k % 26))
return o

# Conditional swap of RFC 7748 5.1 (cswap) without a branch: for the mask m
# (0 or all ones, m = 0 - bit) the swap of a and b is a ^= d, b ^= d with
# d = m & (a ^ b). Every limb is processed whatever the bit.
fn cdiff(m: i64, a: List[i64], b: List[i64]) -> List[i64]
var d: List[i64] = []
for i in 0..10
d.push(m & (a[i] ^ b[i]))
return d

fn fxor(a: List[i64], d: List[i64]) -> List[i64]
var o: List[i64] = []
for i in 0..10
o.push(a[i] ^ d[i])
return o

# X25519(k, u) of RFC 7748 over 32-byte hex strings.
fn x25519(kh: text, uh: text) -> text
var z = unhex(kh)
Expand All @@ -327,13 +365,13 @@ fn x25519(kh: text, uh: text) -> text
for s in 0..255
let i = 254 - s
let r = (z[(i >> 3) as usize] >> (i & 7)) & 1
if r == 1
let t1 = copy(a)
a = copy(b)
b = t1
let t2 = copy(c)
c = copy(d)
d = t2
let m = 0 - r
let sab = cdiff(m, copy(a), copy(b))
let scd = cdiff(m, copy(c), copy(d))
a = fxor(a, copy(sab))
b = fxor(b, sab)
c = fxor(c, copy(scd))
d = fxor(d, scd)
var e = fadd(copy(a), copy(c))
a = fsub(copy(a), copy(c))
c = fadd(copy(b), copy(d))
Expand All @@ -352,13 +390,13 @@ fn x25519(kh: text, uh: text) -> text
a = fmul(copy(d), copy(f))
d = fmul(copy(b), copy(x))
b = fmul(copy(e), copy(e))
if r == 1
let t1 = copy(a)
a = copy(b)
b = t1
let t2 = copy(c)
c = copy(d)
d = t2
let m2 = 0 - r
let sab2 = cdiff(m2, copy(a), copy(b))
let scd2 = cdiff(m2, copy(c), copy(d))
a = fxor(a, copy(sab2))
b = fxor(b, sab2)
c = fxor(c, copy(scd2))
d = fxor(d, scd2)
let ci = finv(copy(c))
return tohex(pack(fmul(copy(a), ci)))

Expand Down Expand Up @@ -731,7 +769,27 @@ fn pt_table(p: Pt) -> List[Pt]

fn nib(s: List[i64], i: i64) -> usize = ((s[(i >> 1) as usize] >> (4 * (i & 1))) & 15) as usize

# s * p, s a 32-byte little-endian scalar, 4-bit fixed window
# tb[n] for n in 0..15 by a full scan: every entry is read and masked, the
# entry whose index equals n (mask all ones: (j ^ n) - 1 is negative only
# when j ^ n = 0) is the one kept. No index depends on n.
fn pt_select(tb: List[Pt], n: i64) -> Pt
var x = fe(0)
var y = fe(0)
var z = fe(0)
var t = fe(0)
for j in 0..16
let m = ((j as i64 ^ n) - 1) >> 63
for l in 0..10
x[l] = x[l] | (m & tb[j].x[l])
y[l] = y[l] | (m & tb[j].y[l])
z[l] = z[l] | (m & tb[j].z[l])
t[l] = t[l] | (m & tb[j].t[l])
return Pt(x: x, y: y, z: z, t: t)

# s * p, s a 32-byte little-endian scalar, 4-bit fixed window. The window
# entry is chosen by pt_select (full scan), so no table index depends on s;
# s is secret in key generation and signing. (pt_mul2 below is verification
# only: both scalars are public there, and it keeps the direct index.)
fn pt_mul(s: List[i64], p: Pt) -> Pt
let tb = pt_table(p)
var r = pt_zero()
Expand All @@ -740,7 +798,7 @@ fn pt_mul(s: List[i64], p: Pt) -> Pt
if k > 0
for _ in 0..4
r = pt_dbl(copy(r))
r = pt_add(copy(r), copy(tb[nib(copy(s), i)]))
r = pt_add(copy(r), pt_select(copy(tb), nib(copy(s), i) as i64))
return r

# s * p + t * q (Straus, shared doublings)
Expand Down Expand Up @@ -799,15 +857,28 @@ fn pt_dec(b: List[i64]) -> Dec

fn ed_l() -> List[i64] = [237, 211, 245, 92, 26, 99, 18, 88, 214, 156, 247, 162, 222, 249, 222, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0]

# a >= b on equal-length little-endian byte lists
# a - b on equal-length little-endian byte lists, as the difference modulo
# 256^n and the final borrow (1 when a < b). The borrow is the sign bit of
# each limb difference: no branch, same work for all values.
fn sub_borrow(a: List[i64], b: List[i64]) -> List[i64]
var o: List[i64] = []
var br: i64 = 0
for j in 0..a.len()
let v = a[j] - b[j] - br
br = (v >> 63) & 1
o.push(v & 255)
o.push(br)
return o

# a >= b on equal-length little-endian byte lists, with no early exit
fn bge(a: List[i64], b: List[i64]) -> bool
for k in 0..a.len()
let i = a.len() - 1 - k
if a[i] != b[i]
return a[i] > b[i]
return true
let d = sub_borrow(a, b)
return d[d.len() - 1] == 0

# x mod L for a little-endian byte list x of any length, 32 bytes out
# x mod L for a little-endian byte list x of any length, 32 bytes out.
# Shift-and-subtract over the bits of x (a public count); each step
# computes r - L with its borrow and keeps r or r - L by a mask, so the
# work is the same whether or not the subtraction applies.
fn sc_reduce(x: List[i64]) -> List[i64]
let l = ed_l()
var r: List[i64] = []
Expand All @@ -822,16 +893,10 @@ fn sc_reduce(x: List[i64]) -> List[i64]
let v = r[j] * 2 + c
r[j] = v & 255
c = v >> 8
if bge(copy(r), copy(l))
var br: i64 = 0
for j in 0..33
let v = r[j] - l[j] - br
if v < 0
r[j] = v + 256
br = 1
else
r[j] = v
br = 0
let d = sub_borrow(copy(r), copy(l))
let keep = 0 - d[33]
for j in 0..33
r[j] = d[j] ^ (keep & (r[j] ^ d[j]))
k += 1
return slice(r, 0, 32)

Expand Down
83 changes: 69 additions & 14 deletions lib/hpke.rce
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,54 @@ module hpke
# A.1 (base mode) and the RFC 9420 crypto-basics.json EncryptWithLabel
# vector for suite 1 as host steps in tests/hpke_rce.rs.

fn aes_sbox() -> List[i64] = [99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22]
# AES S-box by arithmetic: the inverse in GF(2^8) (a^254, 15 multiplications
# whose bit pattern is the public constant 254) followed by the FIPS 197
# affine map. No table is indexed by the (secret) byte and nothing branches
# on it. gmul is the shift-and-add product with masks in place of branches.
fn gmul(a: i64, b: i64) -> i64
var p: i64 = 0
var x = a
var y = b
for _ in 0..8
p = p ^ (x & (0 - (y & 1)))
x = ((x << 1) ^ (27 & (0 - (x >> 7)))) & 255
y = y >> 1
return p

fn ginv(a: i64) -> i64
var r: i64 = 1
for k in 0..8
r = gmul(r, r)
if ((254 >> (7 - k as i64)) & 1) == 1
r = gmul(r, a)
return r

fn rotl8(v: i64, n: i64) -> i64 = ((v << n) | (v >> (8 - n))) & 255

fn sbox(x: i64) -> i64
let i = ginv(x)
return i ^ rotl8(i, 1) ^ rotl8(i, 2) ^ rotl8(i, 3) ^ rotl8(i, 4) ^ 99

fn xt(b: i64) -> i64 = ((b << 1) ^ (if b >= 128 then 27 else 0)) & 255

# multiply by x in GF(2^8): the reduction is a mask from the top bit, no branch
fn xt(b: i64) -> i64 = ((b << 1) ^ (27 & (0 - (b >> 7)))) & 255

# the 176-byte expanded key of a 16-byte AES-128 key
fn aes_expand(key: List[i64]) -> List[i64]
let sb = aes_sbox()
var w = copy(key)
var rc: i64 = 1
var i: usize = 16
while i < 176 bound 160
var t = [w[i - 4], w[i - 3], w[i - 2], w[i - 1]]
if i % 16 == 0
t = [sb[t[1] as usize] ^ rc, sb[t[2] as usize], sb[t[3] as usize], sb[t[0] as usize]]
t = [sbox(t[1]) ^ rc, sbox(t[2]), sbox(t[3]), sbox(t[0])]
rc = xt(rc)
for j in 0..4
w.push(w[i - 16 + j] ^ t[j])
i += 4
return w

fn aes_block(rk: List[i64], inp: List[i64]) -> List[i64]
let sb = aes_sbox()
var s: List[i64] = []
for i in 0..16
s.push(inp[i] ^ rk[i])
Expand All @@ -40,7 +66,7 @@ fn aes_block(rk: List[i64], inp: List[i64]) -> List[i64]
var t: List[i64] = []
for c in 0..4
for row in 0..4
t.push(sb[s[((c + row) % 4) * 4 + row] as usize])
t.push(sbox(s[((c + row) % 4) * 4 + row]))
# MixColumns, except in the last round
if r < 10
var m: List[i64] = []
Expand All @@ -66,17 +92,16 @@ fn gf_mul(x: List[i64], y: List[i64]) -> List[i64]
z.push(0)
var v = copy(y)
for i in 0..128
if ((x[i / 8] >> (7 - (i % 8) as i64)) & 1) == 1
for j in 0..16
z[j] = z[j] ^ v[j]
let xm = 0 - ((x[i / 8] >> (7 - (i % 8) as i64)) & 1)
for j in 0..16
z[j] = z[j] ^ (v[j] & xm)
let lsb = v[15] & 1
var j: usize = 15
while j > 0 bound 16
v[j] = (v[j] >> 1) | ((v[j - 1] & 1) << 7)
j -= 1
v[0] = v[0] >> 1
if lsb == 1
v[0] = v[0] ^ 225
v[0] = v[0] ^ (225 & (0 - lsb))
return z

fn ghash(h: List[i64], aad: List[i64], ct: List[i64]) -> List[i64]
Expand Down Expand Up @@ -133,12 +158,23 @@ fn gcm_seal(key: List[i64], iv: List[i64], aad: List[i64], pt: List[i64]) -> Lis
tag.push(s[j] ^ ek[j])
return cat(ct, tag)

# byte-list equality with no early exit (OR of the XORs, tested once); a
# copy of crypto.ct_eq so this file also checks against an embedded crypto
# that predates it. Only the lengths are public.
fn hp_ct_eq(a: List[i64], b: List[i64]) -> bool
if a.len() != b.len()
return false
var acc: i64 = 0
for i in 0..a.len()
acc = acc | (a[i] ^ b[i])
return acc == 0

# the plaintext, or [-1] when the tag does not verify
fn gcm_open(key: List[i64], iv: List[i64], aad: List[i64], sealed: List[i64]) -> List[i64]
if sealed.len() < 16
return [-1]
let pt = gcm_ctr(aes_expand(copy(key)), copy(iv), slice(copy(sealed), 0, sealed.len() - 16))
if tohex(gcm_seal(key, iv, aad, copy(pt))) == tohex(sealed)
if hp_ct_eq(gcm_seal(key, iv, aad, copy(pt)), sealed)
return pt
return [-1]

Expand Down Expand Up @@ -184,20 +220,39 @@ fn hp_nonce(base: List[i64], seq: i64) -> List[i64]
n[11 - k] = n[11 - k] ^ ((seq >> (8 * k as i64)) & 255)
return n

# SetupBaseS + Seal: "enc.ciphertext" (hex) to recipient public key pkr,
# RFC 9180 7.1.4: the DH output must not be the all-zero value (the peer sent
# a low-order point); the OR of all bytes is tested once at the end, so the
# test does not branch per byte. Only the abort (a public event) branches.
fn hp_allzero(b: List[i64]) -> bool
var acc: i64 = 0
for x in b
acc = acc | x
return acc == 0

# SetupBaseS + Seal: "enc.ciphertext" (hex) to recipient public key pkr (""
# when pkr is not 32 bytes or the DH output is all zero, RFC 9180 7.1.4),
# with the ephemeral private key ske (from logged random bytes), message
# number seq of the context
fn hpke_seal(pkr: text, ske: text, info: text, aad: text, pt: text, seq: i64) -> text
if unhex(copy(pkr)).len() != 32
return ""
let enc = pubkey(copy(ske))
let dh = unhex(x25519(ske, copy(pkr)))
if hp_allzero(copy(dh))
return ""
let kn = hp_key_nonce(hp_shared(dh, cat(unhex(copy(enc)), unhex(pkr))), unhex(info))
let ct = gcm_seal(slice(copy(kn), 0, 16), hp_nonce(slice(kn, 16, 28), seq), unhex(aad), unhex(pt))
return enc + "." + tohex(ct)

# SetupBaseR + Open: the plaintext (hex), or "" when it does not open
# SetupBaseR + Open: the plaintext (hex), or "" when it does not open (also for
# an enc that is not 32 bytes or gives an all-zero DH output, RFC 9180 7.1.4)
fn hpke_open(skr: text, enc: text, info: text, aad: text, ct: text, seq: i64) -> text
if unhex(copy(enc)).len() != 32
return ""
let pkr = pubkey(copy(skr))
let dh = unhex(x25519(skr, copy(enc)))
if hp_allzero(copy(dh))
return ""
let kn = hp_key_nonce(hp_shared(dh, cat(unhex(enc), unhex(pkr))), unhex(info))
let pt = gcm_open(slice(copy(kn), 0, 16), hp_nonce(slice(kn, 16, 28), seq), unhex(aad), unhex(ct))
if pt.len() == 1 and pt[0] == -1
Expand Down
Loading
Loading