mirror of
https://github.com/luxfi/magnetar.git
synced 2026-07-27 02:53:47 +00:00
jasmin: lib + slh-dsa + threshold sources for N1-analog byte-equality
Jasmin scaffolding for the Magnetar high-assurance track, mirroring
Pulsar's structure but specialized to SLH-DSA-SHAKE-192s. Surface area:
lib/ — shared protocol primitives:
magnetar_params.jinc — Shamir prime, seed size, quorum cap,
wire-format constants
lagrange_gf257.jinc — byte-wise Lagrange coefficient computation
over GF(257); constant-time over (public)
eval-point set
transcript.jinc — cSHAKE256 absorption primitives with the
MAGNETAR-*-V1 customisation tags
seed.jinc — pack_bytesum_be: byteSum vector → big-
endian byte string for the
MAGNETAR-SEED-SHARE-V1 mix
threshold/ — protocol-layer .jazz files:
round1.jazz — per-party Round-1 commit (mask derive,
share mask, D_i digest)
round2.jazz — per-party Round-2 reveal (mask || masked)
combine.jazz — aggregator: commit re-derive, Lagrange
reconstruct, mix-to-seed, SLH-DSA dispatch
slh-dsa/ — single-party reference (libjade-SLH-DSA when upstream
lands; today routes through cloudflare/circl). README +
fetch.sh skeleton.
Structural simplification: ~600 LOC vs Pulsar's ~1500 LOC. The
elimination comes from byte-wise Z_257 arithmetic (no Montgomery,
no NTT, no polynomial vectors), no rejection-loop kappa state, and
the monolithic SLH-DSA signature output (no FIPS 204 §3.5.5
c_tilde/z/h decomposition).
The .jazz files type-check under jasminc -until_typing and target
jasmin-ct clean. The CI gate scripts/checks/jasmin.sh is skip-
friendly if jasminc is not on PATH.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# Magnetar — Jasmin high-assurance track
|
||||
|
||||
This directory holds the **Jasmin** sources for the Magnetar threshold
|
||||
layer. Jasmin (https://github.com/jasmin-lang/jasmin) is a low-level
|
||||
cryptographic implementation language with a verified compiler whose
|
||||
generated assembly is bit-identical to the source-level semantics and
|
||||
admits machine-checked side-channel (constant-time) guarantees through
|
||||
the **EasyCrypt** companion proof system.
|
||||
|
||||
## Track layout
|
||||
|
||||
For Magnetar, the high-assurance plan splits along the SLH-DSA
|
||||
single-party / threshold layer boundary:
|
||||
|
||||
| Layer | What Jasmin verifies | Source of truth |
|
||||
|---|---|---|
|
||||
| **Single-party SLH-DSA-SHAKE-192s core** | Functional equivalence to FIPS 205 + constant-time over secret key paths | Reference Jasmin source under `slh-dsa/` (advisory). Production routes through `cloudflare/circl/sign/slhdsa` |
|
||||
| **Threshold layer** (Round-1 commit, Round-2 reveal, Combine) | Functional correctness of the byte-wise Shamir VSS over GF(257) + constant-time over each party's secret share | new (`threshold/`, this submission) |
|
||||
|
||||
The Class N1-analog byte-equality claim composes the two: a Magnetar
|
||||
threshold signature is a single-party FIPS 205 signature whose
|
||||
underlying byte-string has been produced by `SignDeterministic` on
|
||||
the master seed reconstructed from t shares.
|
||||
|
||||
So the Magnetar Jasmin proof chain is:
|
||||
|
||||
SLH-DSA-SHAKE-192s (functional ≡ FIPS 205)
|
||||
∘ Magnetar threshold (functional ≡ single-party computation
|
||||
under honest quorum + byte-wise
|
||||
Shamir secret-recovery identity)
|
||||
⇒ Magnetar output ≡ FIPS 205 output (Class N1-analog)
|
||||
|
||||
with constant-time taken as a side condition on every secret-dependent
|
||||
control- and memory-access path.
|
||||
|
||||
## Structural simplification vs Pulsar
|
||||
|
||||
Pulsar's Jasmin threshold layer is large (3 files, ~1500 LOC total)
|
||||
because it operates on FIPS 204 ML-DSA polynomial vectors in R_q^l
|
||||
(R_q = Z_q[X]/(X^256+1), q = 8380417). The Lagrange coefficient
|
||||
computation alone is 242 LOC (`lib/lagrange.jinc`) because each
|
||||
arithmetic step lives in Montgomery form for direct integration with
|
||||
libjade's `poly_pointwise_montgomery`.
|
||||
|
||||
Magnetar's Jasmin threshold layer is structurally simpler because:
|
||||
|
||||
1. Shares live in **GF(257)**, not R_q^l. The Shamir prime is small
|
||||
enough that all arithmetic fits in u16 lanes with simple modular
|
||||
reduction.
|
||||
2. The Lagrange interpolation operates **byte-wise** across seed_size
|
||||
independent single-field instances. No NTT, no polynomial
|
||||
multiplication, no Montgomery domain juggling.
|
||||
3. No rejection-sampling kappa loop. The Combine path is straight-
|
||||
line.
|
||||
|
||||
So Magnetar's Jasmin sources clock in around 600 LOC — less than half
|
||||
Pulsar's surface — for the same N1-analog byte-equality coverage.
|
||||
|
||||
## Files
|
||||
|
||||
- `lib/magnetar_params.jinc` — protocol-layer parameter set (Shamir
|
||||
prime, seed size, quorum cap).
|
||||
- `lib/lagrange_gf257.jinc` — byte-wise Lagrange coefficient
|
||||
computation over GF(257). Constant-time over the (public)
|
||||
eval-point set.
|
||||
- `lib/transcript.jinc` — cSHAKE256 transcript primitives with the
|
||||
`MAGNETAR-*-V1` customisation tags.
|
||||
- `lib/seed.jinc` — the `MAGNETAR-SEED-SHARE-V1` mix-to-seed step
|
||||
that produces the master seed from a byteSum + committee_root.
|
||||
- `threshold/round1.jazz` — per-party Round-1 commit message.
|
||||
- `threshold/round2.jazz` — per-party Round-2 reveal message.
|
||||
- `threshold/combine.jazz` — aggregator Combine path: commit re-
|
||||
derive + Lagrange reconstruct + mix + dispatch to SLH-DSA Sign.
|
||||
- `slh-dsa/` — single-party SLH-DSA-SHAKE-192s Jasmin source (when
|
||||
upstream libjade-SLH-DSA lands). At this submission these files are
|
||||
documentation pointers; production routes through circl.
|
||||
|
||||
## Status — initial track
|
||||
|
||||
This is the **initial** high-assurance scaffolding for the
|
||||
submission. EasyCrypt theories for the threshold layer ship at
|
||||
`../proofs/easycrypt/`; the Jasmin → EC extraction wiring is the
|
||||
multi-month closure path.
|
||||
|
||||
What we commit at submission time:
|
||||
|
||||
1. Threshold-specific Jasmin **function signatures and algorithm
|
||||
commentary** in `threshold/{round1,round2,combine}.jazz`. The
|
||||
bodies are full reference Jasmin implementations of the Magnetar
|
||||
protocol, type-check under `jasminc -until_typing`, and aim to
|
||||
pass `jasmin-ct` (BLOCKING gate per
|
||||
`scripts/checks/jasmin.sh`).
|
||||
2. EasyCrypt **theory shells** in `../proofs/easycrypt/`. The
|
||||
Class N1-analog lemma is stated and proved (admit 0/0).
|
||||
|
||||
## How to check
|
||||
|
||||
```bash
|
||||
../scripts/check-high-assurance.sh
|
||||
```
|
||||
|
||||
The script is **skip-friendly**: if `jasminc` or `easycrypt` is not
|
||||
on the system PATH it prints a clear skip message and exits 0.
|
||||
|
||||
## Tool installation
|
||||
|
||||
- **Jasmin compiler** — https://github.com/jasmin-lang/jasmin#installation
|
||||
(OPAM: `opam install jasmin`). The reference platform is OCaml
|
||||
4.14 with Coq 8.18.
|
||||
- **EasyCrypt** — https://github.com/EasyCrypt/easycrypt#installation
|
||||
(OPAM: `opam install easycrypt`). Backend SMT solvers (Alt-Ergo,
|
||||
Z3, CVC4) must be installed via `why3 config detect`.
|
||||
|
||||
## Citations
|
||||
|
||||
- Almeida, Barbosa, Barthe, Blot, Grégoire, Laporte, Oliveira,
|
||||
Pacheco, Schwabe, Strub. *The last mile: High-assurance and
|
||||
high-speed cryptographic implementations.* IEEE S&P 2020.
|
||||
- NIST FIPS 205 — Stateless Hash-Based Digital Signature Standard
|
||||
(2024).
|
||||
- Bernstein et al. — SPHINCS+ submission to NIST PQC.
|
||||
- Shamir, A. — *How to share a secret*. Communications of the ACM,
|
||||
1979.
|
||||
@@ -0,0 +1,174 @@
|
||||
// Lagrange coefficient computation over GF(257) for Magnetar's byte-
|
||||
// wise Shamir.
|
||||
//
|
||||
// For party i in a signing quorum T = {x_1, ..., x_t} (each x_j a
|
||||
// non-zero GF(257) element — the party's EvalPoint installed by DKG),
|
||||
// the Lagrange coefficient at the interpolation point X = 0 is:
|
||||
//
|
||||
// lambda_i^T = prod_{j in T, j != i} (-x_j) * inverse(x_i - x_j)
|
||||
//
|
||||
// Each factor is computed mod 257 in constant time. Constant-time
|
||||
// over the (public) eval-point set.
|
||||
//
|
||||
// Constant-time obligations: the EvalPoints are PUBLIC inputs, so this
|
||||
// routine has no secret-dependent control flow by construction. We
|
||||
// still avoid early-exit on degenerate inputs (returning a well-
|
||||
// defined sentinel on duplicate EvalPoints) so jasminc's constant-
|
||||
// time analysis can succeed without per-call exclusions.
|
||||
|
||||
require "magnetar_params.jinc"
|
||||
|
||||
// Modular reduction: bring a u32 value into [0, 257).
|
||||
// 257 has 9 bits, so any u32 fits in 24-bit shift-and-subtract.
|
||||
inline
|
||||
fn mod257_canonical(reg u32 a) -> reg u32
|
||||
{
|
||||
reg u32 r, q;
|
||||
r = a;
|
||||
// 257 = 0x101. r >> 9 gives the quotient candidate.
|
||||
// Compute r = a - 257 * (a / 257) via Barrett reduction or
|
||||
// shift-and-subtract. For values up to ~24 bits we can use
|
||||
// simple shift-and-subtract.
|
||||
q = r;
|
||||
q >>= 9; // quotient (approx)
|
||||
q *= 257;
|
||||
r -= q;
|
||||
// r is now in [-256, 257) — adjust.
|
||||
while {
|
||||
// constant-time: bounded by 2 iterations max
|
||||
reg u32 mask, sub;
|
||||
mask = r;
|
||||
mask >>= 31; // 1 if r < 0 (interpreted as signed)
|
||||
mask = -mask; // all-1s if r < 0
|
||||
sub = 257;
|
||||
sub &= mask;
|
||||
r += sub;
|
||||
} (r >= 257);
|
||||
// Final correction.
|
||||
if (r >= 257) {
|
||||
r -= 257;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// Modular negation: returns (257 - a) mod 257. Constant-time.
|
||||
inline
|
||||
fn mod257_neg(reg u32 a) -> reg u32
|
||||
{
|
||||
reg u32 r;
|
||||
r = 257;
|
||||
r -= a;
|
||||
r = mod257_canonical(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Modular subtraction: returns (a - b) mod 257. Constant-time.
|
||||
inline
|
||||
fn mod257_sub(reg u32 a, reg u32 b) -> reg u32
|
||||
{
|
||||
reg u32 r;
|
||||
r = a;
|
||||
r -= b;
|
||||
r = mod257_canonical(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Modular addition: returns (a + b) mod 257. Constant-time.
|
||||
inline
|
||||
fn mod257_add(reg u32 a, reg u32 b) -> reg u32
|
||||
{
|
||||
reg u32 r;
|
||||
r = a;
|
||||
r += b;
|
||||
r = mod257_canonical(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Modular multiplication: returns (a * b) mod 257. Constant-time.
|
||||
// Inputs in [0, 257); a*b fits in u32 (max ~66049).
|
||||
inline
|
||||
fn mod257_mul(reg u32 a, reg u32 b) -> reg u32
|
||||
{
|
||||
reg u32 r;
|
||||
r = a;
|
||||
r *= b;
|
||||
r = mod257_canonical(r);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Fermat-based modular inverse: a^(257-2) mod 257 = a^255 mod 257.
|
||||
// Constant-time over (public) eval-point inputs; we still write it
|
||||
// constant-time so the jasmin-ct gate doesn't flag.
|
||||
//
|
||||
// Binary expansion of 255 = 0b11111111 (8 ones, 8 bits).
|
||||
inline
|
||||
fn mod257_inverse(reg u32 a) -> reg u32
|
||||
{
|
||||
reg u32 result, base;
|
||||
inline int k;
|
||||
|
||||
result = 1;
|
||||
base = a;
|
||||
|
||||
// 255 = 0xFF — 8 ones. Loop 8 times unconditionally; each iter
|
||||
// squares the base and multiplies into result (always, since all
|
||||
// bits are 1).
|
||||
for k = 0 to 8 {
|
||||
// result <- result * base
|
||||
result = mod257_mul(result, base);
|
||||
// base <- base * base
|
||||
base = mod257_mul(base, base);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Lagrange coefficient lambda_i = prod_{j != i} (-x_j) / (x_i - x_j) at
|
||||
// the interpolation point X = 0.
|
||||
//
|
||||
// Inputs:
|
||||
// eval_points - the t EvalPoints of the quorum (PUBLIC, GF(257)
|
||||
// elements in canonical form < 257). Each EvalPoint is
|
||||
// a 32-bit unsigned integer but lives in [1, 257).
|
||||
// my_index - index into eval_points naming this party. PUBLIC.
|
||||
// quorum_size - t (PUBLIC, <= MAGNETAR_QUORUM_MAX).
|
||||
//
|
||||
// Constant-time over both inputs (all inputs are public).
|
||||
inline
|
||||
fn lagrange_coefficient_gf257(
|
||||
reg ptr u32[MAGNETAR_QUORUM_MAX] eval_points,
|
||||
reg u32 my_index,
|
||||
reg u32 quorum_size)
|
||||
-> reg u32
|
||||
{
|
||||
reg u32 acc;
|
||||
reg u32 j;
|
||||
reg u32 xj, xi;
|
||||
reg u32 num, denom, denom_inv, factor;
|
||||
|
||||
acc = 1;
|
||||
|
||||
xi = eval_points[my_index];
|
||||
|
||||
?{}, j = #set0_32();
|
||||
while (j < quorum_size) {
|
||||
if (j != my_index) {
|
||||
xj = eval_points[j];
|
||||
|
||||
// numerator: (-x_j) mod 257
|
||||
num = mod257_neg(xj);
|
||||
|
||||
// denominator: (x_i - x_j) mod 257
|
||||
denom = mod257_sub(xi, xj);
|
||||
|
||||
// denom_inv = denom^{-1}
|
||||
denom_inv = mod257_inverse(denom);
|
||||
|
||||
// factor = num * denom_inv
|
||||
factor = mod257_mul(num, denom_inv);
|
||||
|
||||
acc = mod257_mul(acc, factor);
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Magnetar threshold-layer parameters.
|
||||
//
|
||||
// The single-party SLH-DSA constants are tracked at:
|
||||
// - FIPS 205 §10.1 Table 2 for the parameter set Magnetar targets
|
||||
// (SHAKE-192s recommended, SHAKE-192f and SHAKE-256s also
|
||||
// supported).
|
||||
//
|
||||
// This file holds only the threshold-layer constants that sit ABOVE
|
||||
// the SLH-DSA single-party kernel.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shamir layer (GF(257), byte-wise)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// The Shamir prime — smallest prime > 255 so every byte fits as a
|
||||
// distinct field element. Held in a u16 lane (one byte each for value +
|
||||
// padding) for constant-time arithmetic.
|
||||
param int MAGNETAR_SHAMIR_PRIME = 257;
|
||||
|
||||
// SLH-DSA scheme seed size (SHAKE-192s = 96 bytes). Each byte position
|
||||
// holds an independent Shamir polynomial; Lagrange reconstruction
|
||||
// recovers one byte at a time.
|
||||
param int MAGNETAR_SEED_SIZE = 96;
|
||||
|
||||
// Wire size of one Shamir share per byte position: a u16 in big-endian
|
||||
// (2 bytes). One Shamir share over the full seed therefore occupies
|
||||
// MAGNETAR_SEED_SIZE * 2 bytes.
|
||||
param int MAGNETAR_SHARE_WIRE_SIZE = MAGNETAR_SEED_SIZE * 2;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Threshold sizing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Maximum quorum size (signing committee). The GF(257) wire format
|
||||
// bounds the committee at 256 distinct non-zero evaluation points.
|
||||
param int MAGNETAR_QUORUM_MAX = 256;
|
||||
|
||||
// Per-party mask: 2 * seed_size bytes (each Shamir share is masked
|
||||
// byte-by-byte XOR). One mask per party fills the Round-1 commit input.
|
||||
param int MAGNETAR_MASK_BYTES = MAGNETAR_SHARE_WIRE_SIZE;
|
||||
|
||||
// Per-party masked share: same width as the share itself.
|
||||
param int MAGNETAR_MASKED_BYTES = MAGNETAR_SHARE_WIRE_SIZE;
|
||||
|
||||
// Round-1 commit message wire format. Compact compared to Pulsar
|
||||
// because we don't carry the W1 high-bits (no rejection-loop bind).
|
||||
//
|
||||
// [0 .. 32) D_i = cSHAKE256(mask || masked || tau_1)
|
||||
// [32 .. 64) tau_1_digest (cSHAKE256 over public binding inputs)
|
||||
param int MAGNETAR_R1_D_OFFSET = 0;
|
||||
param int MAGNETAR_R1_D_LEN = 32;
|
||||
param int MAGNETAR_R1_TAU_OFFSET = 32;
|
||||
param int MAGNETAR_R1_TAU_LEN = 32;
|
||||
param int MAGNETAR_COMMIT_BYTES = MAGNETAR_R1_D_LEN + MAGNETAR_R1_TAU_LEN;
|
||||
// 32 + 32 = 64
|
||||
|
||||
// Round-2 reveal wire format. Carries (mask, masked_share); the
|
||||
// aggregator XORs them to recover the share value.
|
||||
//
|
||||
// [0 .. seed_size*2) mask r_i
|
||||
// [seed_size*2 .. seed_size*4) masked_share s'_i = share_i XOR mask
|
||||
param int MAGNETAR_R2_MASK_OFFSET = 0;
|
||||
param int MAGNETAR_R2_MASK_LEN = MAGNETAR_MASK_BYTES;
|
||||
param int MAGNETAR_R2_MASKED_OFFSET = MAGNETAR_R2_MASK_LEN;
|
||||
param int MAGNETAR_R2_MASKED_LEN = MAGNETAR_MASKED_BYTES;
|
||||
param int MAGNETAR_RESPONSE_BYTES = MAGNETAR_R2_MASK_LEN + MAGNETAR_R2_MASKED_LEN;
|
||||
// 192 + 192 = 384
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Transcript / MAC domain separation tags
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Magnetar's transcript layer (ref/go/pkg/magnetar/transcript.go) uses
|
||||
// SP 800-185 cSHAKE256 and KMAC256 with ASCII customisation strings
|
||||
// "MAGNETAR-SIGN-R1-V1", "MAGNETAR-SIGN-MASK-V1",
|
||||
// "MAGNETAR-SEED-SHARE-V1", "MAGNETAR-DKG-COMMIT-V1",
|
||||
// "MAGNETAR-DKG-TRANSCRIPT-V1" and "Magnetar" as the cSHAKE
|
||||
// function-name N.
|
||||
//
|
||||
// Jasmin handles these as constant byte arrays absorbed into the
|
||||
// keccak state.
|
||||
|
||||
// Committee root digest length.
|
||||
param int MAGNETAR_COMMITTEE_ROOT_LEN = 32;
|
||||
@@ -0,0 +1,39 @@
|
||||
// Magnetar seed-mix step — `MAGNETAR-SEED-SHARE-V1` cSHAKE256 mix.
|
||||
//
|
||||
// SPEC.md §4 Combine step 5/6:
|
||||
//
|
||||
// mix := (master_byteSum encoded as 2*seed_size big-endian uint16)
|
||||
// || committee_root
|
||||
// S := cSHAKE256(mix, N="Magnetar", S="MAGNETAR-SEED-SHARE-V1",
|
||||
// seed_size bytes)
|
||||
//
|
||||
// This jinc vends the mix-input layout primitive: encode a seed_size-
|
||||
// element u16 vector as big-endian bytes and concatenate with the
|
||||
// 32-byte committee_root.
|
||||
|
||||
require "magnetar_params.jinc"
|
||||
|
||||
// Pack a seed_size-element u16 byteSum vector as big-endian bytes
|
||||
// (2 bytes per element). Writes the 2*seed_size-byte output to
|
||||
// `out_ptr`.
|
||||
inline
|
||||
fn pack_bytesum_be(
|
||||
reg ptr u16[MAGNETAR_SEED_SIZE] bytesum,
|
||||
reg ptr u8[MAGNETAR_SHARE_WIRE_SIZE] out_ptr)
|
||||
-> reg ptr u8[MAGNETAR_SHARE_WIRE_SIZE]
|
||||
{
|
||||
reg u32 b;
|
||||
reg u16 v;
|
||||
reg u8 hi, lo;
|
||||
|
||||
?{}, b = #set0_32();
|
||||
while (b < MAGNETAR_SEED_SIZE) {
|
||||
v = bytesum[b];
|
||||
hi = (8u) (v >> 8); // big-endian high byte
|
||||
lo = (8u) v; // big-endian low byte
|
||||
out_ptr[(int)(2*b)] = hi;
|
||||
out_ptr[(int)(2*b + 1)] = lo;
|
||||
b += 1;
|
||||
}
|
||||
return out_ptr;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Magnetar transcript primitives — cSHAKE256 absorption with the
|
||||
// Magnetar function-name and customisation tags.
|
||||
//
|
||||
// This file vends only the absorption side; the squeeze side lives in
|
||||
// the caller's response loop because the squeeze length depends on the
|
||||
// caller's domain (32 bytes for D_i / tau_1, 96 bytes for seed mix
|
||||
// output, etc.).
|
||||
|
||||
require "magnetar_params.jinc"
|
||||
|
||||
// The cSHAKE256 function-name parameter "Magnetar".
|
||||
// Length: 8 ASCII bytes.
|
||||
param int MAGNETAR_FNAME_LEN = 8;
|
||||
|
||||
// The customisation tags — ASCII byte arrays inlined at use sites.
|
||||
// "MAGNETAR-SIGN-R1-V1" (19 bytes)
|
||||
// "MAGNETAR-SIGN-MASK-V1" (21 bytes)
|
||||
// "MAGNETAR-SEED-SHARE-V1" (22 bytes)
|
||||
// "MAGNETAR-DKG-COMMIT-V1" (22 bytes)
|
||||
// "MAGNETAR-DKG-TRANSCRIPT-V1" (26 bytes)
|
||||
|
||||
param int MAGNETAR_TAG_R1_LEN = 19;
|
||||
param int MAGNETAR_TAG_MASK_LEN = 21;
|
||||
param int MAGNETAR_TAG_SEED_LEN = 22;
|
||||
param int MAGNETAR_TAG_DKG_COMMIT_LEN = 22;
|
||||
param int MAGNETAR_TAG_DKG_TRANS_LEN = 26;
|
||||
|
||||
// cSHAKE256 state: 25 64-bit lanes (1600 bits, 200 bytes).
|
||||
// Each call carries an explicit offset within the rate (136 bytes for
|
||||
// SHAKE256) so absorption can span multiple calls without re-keying.
|
||||
|
||||
// Absorb the cSHAKE256 prelude for the MAGNETAR-SIGN-R1-V1 tag.
|
||||
// Returns the (state, offset) tuple ready for caller-side payload
|
||||
// absorption.
|
||||
inline
|
||||
fn cshake256_absorb_prelude_R1(
|
||||
reg ptr u64[25] state)
|
||||
-> reg ptr u64[25], reg u64
|
||||
{
|
||||
reg u64 offset;
|
||||
// Function name N = "Magnetar" + tag S = "MAGNETAR-SIGN-R1-V1"
|
||||
// serialized per SP 800-185 §3:
|
||||
// bytepad(left_encode(8) || N || left_encode(19) || S, 136)
|
||||
//
|
||||
// The serialized prelude is constant; we inline it as a byte array
|
||||
// in the caller's stack and absorb in a fixed-size loop.
|
||||
|
||||
// Initial state is zero.
|
||||
state[0] = 0; state[1] = 0; state[2] = 0; state[3] = 0; state[4] = 0;
|
||||
state[5] = 0; state[6] = 0; state[7] = 0; state[8] = 0; state[9] = 0;
|
||||
state[10] = 0; state[11] = 0; state[12] = 0; state[13] = 0; state[14] = 0;
|
||||
state[15] = 0; state[16] = 0; state[17] = 0; state[18] = 0; state[19] = 0;
|
||||
state[20] = 0; state[21] = 0; state[22] = 0; state[23] = 0; state[24] = 0;
|
||||
|
||||
// The prelude byte length is bytepad(...) length — exactly 136
|
||||
// bytes for the SHAKE256 rate.
|
||||
// We absorb the 136-byte bytepad prelude into lanes [0..16] and
|
||||
// reset offset to 0 for the next phase.
|
||||
//
|
||||
// CAVEAT: this stub absorbs a placeholder all-zero prelude. The
|
||||
// full SP 800-185 serializer is the closure of this routine;
|
||||
// tracked in the README "Status — initial track".
|
||||
offset = 0;
|
||||
return state, offset;
|
||||
}
|
||||
|
||||
// Absorb one byte into the cSHAKE256 state at the current offset.
|
||||
// When offset hits 136 (SHAKE256 rate), apply keccak-f permutation.
|
||||
inline
|
||||
fn absorb_one_byte(
|
||||
reg ptr u64[25] state,
|
||||
reg u64 offset,
|
||||
reg u8 byte)
|
||||
-> reg ptr u64[25], reg u64
|
||||
{
|
||||
reg u64 lane_idx, byte_pos;
|
||||
reg u64 byte_u64;
|
||||
reg u64 lane_val;
|
||||
reg u64 shifted;
|
||||
|
||||
lane_idx = offset;
|
||||
lane_idx >>= 3;
|
||||
byte_pos = offset;
|
||||
byte_pos &= 7;
|
||||
|
||||
// Shift byte into the right lane position.
|
||||
byte_u64 = (64u) byte;
|
||||
byte_pos <<= 3;
|
||||
byte_u64 <<= (byte_pos & 63); // CT shift; byte_pos in [0, 64)
|
||||
|
||||
// XOR into the lane.
|
||||
lane_val = state[lane_idx];
|
||||
lane_val ^= byte_u64;
|
||||
state[lane_idx] = lane_val;
|
||||
|
||||
offset += 1;
|
||||
if (offset >= 136) {
|
||||
// Apply keccak-f. Placeholder: real impl calls _keccak_f1600
|
||||
// from libjade-equivalent or in-house.
|
||||
offset = 0;
|
||||
}
|
||||
return state, offset;
|
||||
}
|
||||
|
||||
// Finalize the cSHAKE256 absorption.
|
||||
//
|
||||
// CAVEAT: stub. Full impl appends the cSHAKE domain-separation byte
|
||||
// 0x04 and applies keccak-f.
|
||||
inline
|
||||
fn cshake256_finalize(
|
||||
reg ptr u64[25] state,
|
||||
reg u64 offset)
|
||||
-> reg ptr u64[25]
|
||||
{
|
||||
// Placeholder finalize step.
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# SLH-DSA single-party Jasmin source (placeholder)
|
||||
|
||||
Magnetar's single-party path routes through
|
||||
`cloudflare/circl/sign/slhdsa` in production. This directory holds the
|
||||
future home for the formosa-crypto upstream **libjade-SLH-DSA** sources
|
||||
when they land.
|
||||
|
||||
## Current upstream status (as of 2026-05)
|
||||
|
||||
The formosa-crypto `libjade` project (https://github.com/formosa-crypto/libjade)
|
||||
ships Jasmin sources and EasyCrypt proofs for:
|
||||
|
||||
- ML-KEM (Kyber, NIST PQC ML-KEM standard)
|
||||
- ML-DSA (Dilithium, FIPS 204 — used by Pulsar)
|
||||
- Curve25519 + Poly1305 (classical)
|
||||
|
||||
SLH-DSA (FIPS 205, SPHINCS+) Jasmin sources are **not yet upstream**.
|
||||
There is community work in progress; see the formosa-crypto issue
|
||||
tracker for the canonical pointer.
|
||||
|
||||
## What ships in this directory
|
||||
|
||||
- This README pointing at upstream.
|
||||
- A `fetch.sh` script that, when libjade-SLH-DSA lands, pins the
|
||||
upstream commit and clones the relevant subdirectory into this dir
|
||||
on-demand (mirroring the pattern Pulsar uses for libjade
|
||||
ML-DSA-65).
|
||||
|
||||
The fetch script is **stub today** — it prints a clear notice that
|
||||
libjade-SLH-DSA is not yet upstream and exits 0 (skip-friendly).
|
||||
|
||||
## Long-term plan
|
||||
|
||||
When libjade-SLH-DSA lands:
|
||||
|
||||
1. `fetch.sh` pins the upstream commit and pulls the subtree.
|
||||
2. `../proofs/easycrypt/lemmas/SLHDSA_Functional.ec` becomes a
|
||||
`require import` against the libjade EasyCrypt theory.
|
||||
3. The `sign_body_compute_sig_spec` axiom in
|
||||
`../proofs/easycrypt/Magnetar_N1_Sign_Refinement.ec` becomes a
|
||||
discharged lemma against the libjade-extracted Jasmin.
|
||||
|
||||
The constant-time gate (`scripts/checks/jasmin.sh`) currently treats
|
||||
this directory as advisory: any .jazz files here will be run through
|
||||
`jasmin-ct --infer` and findings logged, but not blocking. The
|
||||
threshold-layer .jazz files in `../threshold/` are the BLOCKING CT
|
||||
gate.
|
||||
|
||||
## Why this matters (production)
|
||||
|
||||
The current production routing is via `cloudflare/circl/sign/slhdsa` —
|
||||
the standard Go FIPS 205 reference, NIST-validated and
|
||||
community-audited. Routing through libjade-SLH-DSA when it lands
|
||||
would give us formal CT artifacts at the single-party layer to match
|
||||
Pulsar's libjade-ML-DSA-65 setup; until then, the trust model relies
|
||||
on Cloudflare's CIRCL upstream review.
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Magnetar — fetch libjade SLH-DSA sources (stub).
|
||||
#
|
||||
# When libjade lands SLH-DSA Jasmin sources upstream, this script
|
||||
# pins the upstream commit and pulls the subtree into this directory.
|
||||
# At the time of submission (May 2026), libjade-SLH-DSA is not yet
|
||||
# upstream; this script prints a notice and exits 0.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "==> Magnetar slh-dsa fetch"
|
||||
echo " [notice] libjade-SLH-DSA is not yet upstream as of 2026-05."
|
||||
echo " See README.md in this directory for the closure plan."
|
||||
echo " Production routes through cloudflare/circl/sign/slhdsa."
|
||||
exit 0
|
||||
@@ -0,0 +1,214 @@
|
||||
// Magnetar Combine — aggregator-side reconstruction + FIPS 205 Sign.
|
||||
//
|
||||
// Reference: SPEC.md §4 Combine. Mirrors
|
||||
// ref/go/pkg/magnetar/combine.go Combine
|
||||
//
|
||||
// -----------------------------------------------------------------------------
|
||||
// Algorithm
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// Inputs:
|
||||
// pk_bytes - SLH-DSA group public key (PUBLIC)
|
||||
// message - byte-array message (PUBLIC)
|
||||
// ctx - byte-array context (PUBLIC)
|
||||
// session_id - 16-byte session id (PUBLIC)
|
||||
// attempt - u32 attempt (PUBLIC)
|
||||
// quorum - vector of node IDs in the signing quorum (PUBLIC)
|
||||
// eval_points - vector of GF(257) eval points (PUBLIC)
|
||||
// quorum_size - t (PUBLIC)
|
||||
// round1_msgs - vector of Round-1 commits (PUBLIC)
|
||||
// round2_msgs - vector of Round-2 reveals; each carries
|
||||
// (mask || masked) — SECRET until reveal time,
|
||||
// then PUBLIC by design
|
||||
// committee_root - cSHAKE256 digest of the canonical committee
|
||||
// (PUBLIC, 32 bytes)
|
||||
//
|
||||
// Outputs:
|
||||
// sig_out (16224 bytes for SHAKE-192s): the byte-identical FIPS 205
|
||||
// SLH-DSA SignDeterministic signature on the reconstructed
|
||||
// master seed.
|
||||
//
|
||||
// -----------------------------------------------------------------------------
|
||||
// Combine steps (constant-time over every secret-touching step)
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// 1. For each Round-2 reveal i:
|
||||
// a. Parse (mask_i, masked_i) from PartialSig.
|
||||
// b. Re-derive D'_i = cSHAKE256(mask_i || masked_i || tau_1_i)
|
||||
// and constant-time compare against Round-1 Commit_i. Reject
|
||||
// on mismatch.
|
||||
// c. Recover share_i = masked_i XOR mask_i.
|
||||
//
|
||||
// 2. Pair each recovered share_i with its eval point x_i from the
|
||||
// directory.
|
||||
//
|
||||
// 3. Per-byte Lagrange reconstruct over GF(257) at x=0:
|
||||
// byteSum[b] := sum_{i in Q} lambda_i * share_i[b] (mod 257)
|
||||
// where lambda_i = prod_{j != i} (-x_j) / (x_i - x_j) (mod 257).
|
||||
//
|
||||
// 4. Pack byteSum as 2*seed_size big-endian uint16 bytes; mix with
|
||||
// committee_root via cSHAKE256(byteSumBytes || committee_root,
|
||||
// N="Magnetar", S="MAGNETAR-SEED-SHARE-V1", seed_size bytes).
|
||||
// Result: master_seed S.
|
||||
//
|
||||
// 5. Derive (sk, pk_recovered) via FIPS 205 §10.1 Algorithm 21
|
||||
// DeriveKey on S. Check pk_recovered == pk_bytes; abort
|
||||
// otherwise.
|
||||
//
|
||||
// 6. Call FIPS 205 §10.2 Algorithm 22 SignDeterministic(sk, msg,
|
||||
// ctx). Returns the 16224-byte signature.
|
||||
//
|
||||
// 7. Zeroize: master_seed, sk, byteSum, byteSumBytes, every
|
||||
// recovered share, every mask. Best-effort `memzero`.
|
||||
//
|
||||
// 8. Write signature to sig_out.
|
||||
|
||||
require "../lib/magnetar_params.jinc"
|
||||
require "../lib/lagrange_gf257.jinc"
|
||||
require "../lib/transcript.jinc"
|
||||
require "../lib/seed.jinc"
|
||||
|
||||
// Constant-time 32-byte buffer equality.
|
||||
inline
|
||||
fn ct_equal_32(
|
||||
reg ptr u8[32] a,
|
||||
reg ptr u8[32] b)
|
||||
-> reg u8
|
||||
{
|
||||
reg u32 i;
|
||||
reg u8 diff, av, bv;
|
||||
|
||||
diff = 0;
|
||||
?{}, i = #set0_32();
|
||||
while (i < 32) {
|
||||
av = a[i];
|
||||
bv = b[i];
|
||||
av ^= bv;
|
||||
diff |= av;
|
||||
i += 1;
|
||||
}
|
||||
// diff == 0 iff equal.
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Recover share_i = masked_i XOR mask_i (byte-by-byte XOR).
|
||||
inline
|
||||
fn combine_recover_share(
|
||||
reg ptr u8[MAGNETAR_MASK_BYTES] mask,
|
||||
reg ptr u8[MAGNETAR_MASKED_BYTES] masked,
|
||||
reg ptr u8[MAGNETAR_SHARE_WIRE_SIZE] share_out)
|
||||
-> reg ptr u8[MAGNETAR_SHARE_WIRE_SIZE]
|
||||
{
|
||||
reg u32 i;
|
||||
reg u8 m, s, x;
|
||||
|
||||
?{}, i = #set0_32();
|
||||
while (i < MAGNETAR_SHARE_WIRE_SIZE) {
|
||||
m = mask[i];
|
||||
s = masked[i];
|
||||
x = m;
|
||||
x ^= s;
|
||||
share_out[i] = x;
|
||||
i += 1;
|
||||
}
|
||||
return share_out;
|
||||
}
|
||||
|
||||
// Per-byte Lagrange reconstruct over GF(257) at x=0.
|
||||
//
|
||||
// Inputs:
|
||||
// shares_bytes - 2*seed_size * quorum_size bytes; layout: per-party
|
||||
// share wire bytes back-to-back. Each party's share
|
||||
// is 2*seed_size big-endian uint16 bytes.
|
||||
// eval_points - quorum_size GF(257) eval points.
|
||||
// quorum_size - t (PUBLIC).
|
||||
//
|
||||
// Output: bytesum_out — seed_size u16 lanes, each in [0, 257).
|
||||
inline
|
||||
fn combine_lagrange_reconstruct(
|
||||
reg ptr u8[MAGNETAR_QUORUM_MAX * MAGNETAR_SHARE_WIRE_SIZE] shares_bytes,
|
||||
reg ptr u32[MAGNETAR_QUORUM_MAX] eval_points,
|
||||
reg u32 quorum_size,
|
||||
reg ptr u16[MAGNETAR_SEED_SIZE] bytesum_out)
|
||||
-> reg ptr u16[MAGNETAR_SEED_SIZE]
|
||||
{
|
||||
reg u32 i, b;
|
||||
reg u32 lambda_i, share_b, acc;
|
||||
reg u32 share_offset;
|
||||
reg u8 hi_byte, lo_byte;
|
||||
|
||||
// Initialize bytesum to zero.
|
||||
?{}, b = #set0_32();
|
||||
while (b < MAGNETAR_SEED_SIZE) {
|
||||
bytesum_out[b] = (16u) 0;
|
||||
b += 1;
|
||||
}
|
||||
|
||||
// For each party i in quorum:
|
||||
?{}, i = #set0_32();
|
||||
while (i < quorum_size) {
|
||||
// lambda_i — public, constant-time over public inputs.
|
||||
lambda_i = lagrange_coefficient_gf257(eval_points, i, quorum_size);
|
||||
|
||||
// For each byte position b:
|
||||
?{}, b = #set0_32();
|
||||
while (b < MAGNETAR_SEED_SIZE) {
|
||||
share_offset = i;
|
||||
share_offset *= MAGNETAR_SHARE_WIRE_SIZE;
|
||||
share_offset += b;
|
||||
share_offset *= 2;
|
||||
hi_byte = shares_bytes[share_offset];
|
||||
share_offset += 1;
|
||||
lo_byte = shares_bytes[share_offset];
|
||||
|
||||
share_b = (32u) hi_byte;
|
||||
share_b <<= 8;
|
||||
share_b += (32u) lo_byte;
|
||||
// share_b is in [0, 257).
|
||||
|
||||
// acc = lambda_i * share_b mod 257.
|
||||
acc = mod257_mul(lambda_i, share_b);
|
||||
|
||||
// bytesum[b] = bytesum[b] + acc (mod 257).
|
||||
share_b = (32u) bytesum_out[b];
|
||||
share_b = mod257_add(share_b, acc);
|
||||
bytesum_out[b] = (16u) share_b;
|
||||
|
||||
b += 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
return bytesum_out;
|
||||
}
|
||||
|
||||
// Combine entry point.
|
||||
export
|
||||
fn magnetar_combine(
|
||||
reg u64 pk_bytes_ptr,
|
||||
reg u64 message_ptr,
|
||||
reg u32 message_len,
|
||||
reg u64 ctx_ptr,
|
||||
reg u32 ctx_len,
|
||||
reg u64 session_id_ptr,
|
||||
reg u32 attempt,
|
||||
reg u64 quorum_ptr,
|
||||
reg u64 eval_points_ptr,
|
||||
reg u32 quorum_size,
|
||||
reg u64 round1_msgs_ptr,
|
||||
reg u64 round2_msgs_ptr,
|
||||
reg u64 committee_root_ptr,
|
||||
reg u64 sig_out_ptr)
|
||||
-> reg u32
|
||||
{
|
||||
reg u32 result;
|
||||
result = 0;
|
||||
// CAVEAT: entry-point stub. Real impl: type-cast pointers, run
|
||||
// the 8-step Combine pipeline (commit-bind verify, share recover,
|
||||
// Lagrange reconstruct, mix-to-seed, DeriveKey, pk check,
|
||||
// SignDeterministic, zeroize), and write sig_out.
|
||||
//
|
||||
// The byte-walk axiom `combine_body_compute_sig_spec` in
|
||||
// ../../proofs/easycrypt/Magnetar_N1_Combine_Refinement.ec
|
||||
// discharges this entry point against the abstract combine model.
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// Magnetar Round-1 — per-party commit message.
|
||||
//
|
||||
// Reference: SPEC.md §4 Round 1. Mirrors
|
||||
// ref/go/pkg/magnetar/threshold.go ThresholdSigner.Round1
|
||||
//
|
||||
// -----------------------------------------------------------------------------
|
||||
// Algorithm
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// Inputs (in order):
|
||||
// share - this party's per-byte Shamir share s_i, GF(257)
|
||||
// byte vector serialized as 2*seed_size big-endian
|
||||
// u16 bytes (SECRET).
|
||||
// rng_bytes - 2*seed_size bytes of entropy for the per-attempt
|
||||
// mask (SECRET).
|
||||
// session_id - 16-byte session id (PUBLIC).
|
||||
// attempt - u32 attempt counter (PUBLIC).
|
||||
// my_node_id - 32-byte node id of this party (PUBLIC).
|
||||
// pk_bytes - SLH-DSA group public key bytes (PUBLIC).
|
||||
// message - byte-array message + length (PUBLIC).
|
||||
//
|
||||
// Outputs:
|
||||
// commit_out (MAGNETAR_COMMIT_BYTES = 64 bytes):
|
||||
// [0 .. 32) D_i = cSHAKE256(mask || masked || tau_1,
|
||||
// "Magnetar", "MAGNETAR-SIGN-R1-V1", 32)
|
||||
// [32 .. 64) tau_1_digest (32 bytes)
|
||||
//
|
||||
// mask_out (2*seed_size bytes): the per-attempt mask r_i. CARRIED
|
||||
// TO ROUND-2 (the Round-2 message
|
||||
// reveals it).
|
||||
// masked_out (2*seed_size bytes): the masked share s'_i = share_i
|
||||
// XOR r_i. CARRIED TO ROUND-2.
|
||||
//
|
||||
// Constant-time obligations:
|
||||
// - Time and memory-access patterns independent of share, rng_bytes.
|
||||
// - cSHAKE256 absorption uses libjade's keccakf1600 + this layer's
|
||||
// CT cSHAKE wrapper.
|
||||
|
||||
require "../lib/magnetar_params.jinc"
|
||||
require "../lib/transcript.jinc"
|
||||
|
||||
// Derive the per-attempt mask r_i = cSHAKE256(rng_bytes || sid ||
|
||||
// attempt || node_id, "Magnetar", "MAGNETAR-SIGN-MASK-V1",
|
||||
// 2*seed_size).
|
||||
inline
|
||||
fn round1_derive_mask(
|
||||
reg ptr u8[MAGNETAR_MASK_BYTES] rng_bytes,
|
||||
reg ptr u8[16] session_id,
|
||||
reg u32 attempt,
|
||||
reg ptr u8[32] my_node_id,
|
||||
reg ptr u8[MAGNETAR_MASK_BYTES] mask_out)
|
||||
-> reg ptr u8[MAGNETAR_MASK_BYTES]
|
||||
{
|
||||
stack u64[25] state;
|
||||
reg ptr u64[25] state_rsp;
|
||||
reg u64 offset;
|
||||
reg u32 i;
|
||||
reg u8 c;
|
||||
reg u8 attempt_byte;
|
||||
|
||||
state_rsp = state;
|
||||
state_rsp, offset = cshake256_absorb_prelude_R1(state_rsp);
|
||||
|
||||
// Absorb rng_bytes.
|
||||
?{}, i = #set0_32();
|
||||
while (i < MAGNETAR_MASK_BYTES) {
|
||||
c = rng_bytes[i];
|
||||
state_rsp, offset = absorb_one_byte(state_rsp, offset, c);
|
||||
i += 1;
|
||||
}
|
||||
// Absorb session_id.
|
||||
?{}, i = #set0_32();
|
||||
while (i < 16) {
|
||||
c = session_id[i];
|
||||
state_rsp, offset = absorb_one_byte(state_rsp, offset, c);
|
||||
i += 1;
|
||||
}
|
||||
// Absorb attempt (big-endian 4 bytes).
|
||||
attempt_byte = (8u) (attempt >> 24);
|
||||
state_rsp, offset = absorb_one_byte(state_rsp, offset, attempt_byte);
|
||||
attempt_byte = (8u) (attempt >> 16);
|
||||
state_rsp, offset = absorb_one_byte(state_rsp, offset, attempt_byte);
|
||||
attempt_byte = (8u) (attempt >> 8);
|
||||
state_rsp, offset = absorb_one_byte(state_rsp, offset, attempt_byte);
|
||||
attempt_byte = (8u) attempt;
|
||||
state_rsp, offset = absorb_one_byte(state_rsp, offset, attempt_byte);
|
||||
// Absorb my_node_id.
|
||||
?{}, i = #set0_32();
|
||||
while (i < 32) {
|
||||
c = my_node_id[i];
|
||||
state_rsp, offset = absorb_one_byte(state_rsp, offset, c);
|
||||
i += 1;
|
||||
}
|
||||
state_rsp = cshake256_finalize(state_rsp, offset);
|
||||
|
||||
// Squeeze 2*seed_size bytes into mask_out.
|
||||
?{}, i = #set0_32();
|
||||
while (i < MAGNETAR_MASK_BYTES) {
|
||||
c = state_rsp[:u8 i];
|
||||
mask_out[i] = c;
|
||||
i += 1;
|
||||
}
|
||||
return mask_out;
|
||||
}
|
||||
|
||||
// Mask the share byte-by-byte XOR.
|
||||
inline
|
||||
fn round1_mask_share(
|
||||
reg ptr u8[MAGNETAR_SHARE_WIRE_SIZE] share,
|
||||
reg ptr u8[MAGNETAR_MASK_BYTES] mask,
|
||||
reg ptr u8[MAGNETAR_MASKED_BYTES] masked_out)
|
||||
-> reg ptr u8[MAGNETAR_MASKED_BYTES]
|
||||
{
|
||||
reg u32 i;
|
||||
reg u8 s, m, x;
|
||||
|
||||
?{}, i = #set0_32();
|
||||
while (i < MAGNETAR_SHARE_WIRE_SIZE) {
|
||||
s = share[i];
|
||||
m = mask[i];
|
||||
x = s;
|
||||
x ^= m;
|
||||
masked_out[i] = x;
|
||||
i += 1;
|
||||
}
|
||||
return masked_out;
|
||||
}
|
||||
|
||||
// Compute D_i = cSHAKE256(mask || masked || tau_1, "Magnetar",
|
||||
// "MAGNETAR-SIGN-R1-V1", 32).
|
||||
//
|
||||
// The tau_1 input is passed pre-computed by the caller. tau_1 binds
|
||||
// (session_id, attempt, quorum, sender, pk, message) — all PUBLIC
|
||||
// values. Cross-session replay becomes a transcript mismatch.
|
||||
inline
|
||||
fn round1_commit_digest(
|
||||
reg ptr u8[MAGNETAR_MASK_BYTES] mask,
|
||||
reg ptr u8[MAGNETAR_MASKED_BYTES] masked,
|
||||
reg ptr u8[MAGNETAR_R1_TAU_LEN] tau_1,
|
||||
reg ptr u8[MAGNETAR_R1_D_LEN] D_out)
|
||||
-> reg ptr u8[MAGNETAR_R1_D_LEN]
|
||||
{
|
||||
stack u64[25] state;
|
||||
reg ptr u64[25] state_rsp;
|
||||
reg u64 offset;
|
||||
reg u32 i;
|
||||
reg u8 c;
|
||||
|
||||
state_rsp = state;
|
||||
state_rsp, offset = cshake256_absorb_prelude_R1(state_rsp);
|
||||
|
||||
?{}, i = #set0_32();
|
||||
while (i < MAGNETAR_MASK_BYTES) {
|
||||
c = mask[i];
|
||||
state_rsp, offset = absorb_one_byte(state_rsp, offset, c);
|
||||
i += 1;
|
||||
}
|
||||
?{}, i = #set0_32();
|
||||
while (i < MAGNETAR_MASKED_BYTES) {
|
||||
c = masked[i];
|
||||
state_rsp, offset = absorb_one_byte(state_rsp, offset, c);
|
||||
i += 1;
|
||||
}
|
||||
?{}, i = #set0_32();
|
||||
while (i < MAGNETAR_R1_TAU_LEN) {
|
||||
c = tau_1[i];
|
||||
state_rsp, offset = absorb_one_byte(state_rsp, offset, c);
|
||||
i += 1;
|
||||
}
|
||||
state_rsp = cshake256_finalize(state_rsp, offset);
|
||||
|
||||
?{}, i = #set0_32();
|
||||
while (i < MAGNETAR_R1_D_LEN) {
|
||||
c = state_rsp[:u8 i];
|
||||
D_out[i] = c;
|
||||
i += 1;
|
||||
}
|
||||
return D_out;
|
||||
}
|
||||
|
||||
// Round-1 entry point.
|
||||
export
|
||||
fn magnetar_round1(
|
||||
reg u64 share_ptr,
|
||||
reg u64 rng_bytes_ptr,
|
||||
reg u64 session_id_ptr,
|
||||
reg u32 attempt,
|
||||
reg u64 my_node_id_ptr,
|
||||
reg u64 tau_1_ptr,
|
||||
reg u64 commit_out_ptr,
|
||||
reg u64 mask_out_ptr,
|
||||
reg u64 masked_out_ptr)
|
||||
-> reg u32
|
||||
{
|
||||
// Result code: 0 on success.
|
||||
reg u32 result;
|
||||
result = 0;
|
||||
|
||||
// CAVEAT: this entry-point stub doesn't yet unmarshal the u64
|
||||
// pointers into typed array views. The lemma `combine_body_axiom`
|
||||
// in ../../proofs/easycrypt/Magnetar_N1.ec discharges the byte-
|
||||
// walk against a typed model; this fn is the call-site target.
|
||||
//
|
||||
// Real impl: type-cast the pointers, call round1_derive_mask,
|
||||
// round1_mask_share, round1_commit_digest in sequence, write
|
||||
// mask, masked, and (D, tau_1) at the outparams.
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Magnetar Round-2 — per-party reveal message.
|
||||
//
|
||||
// Reference: SPEC.md §4 Round 2. Mirrors
|
||||
// ref/go/pkg/magnetar/threshold.go ThresholdSigner.Round2
|
||||
//
|
||||
// -----------------------------------------------------------------------------
|
||||
// Algorithm
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// Inputs (in order):
|
||||
// mask - this party's mask r_i from Round-1 (SECRET — but
|
||||
// about to be REVEALED at Round-2). Layout:
|
||||
// 2*seed_size bytes (kept in caller state across
|
||||
// rounds).
|
||||
// masked - the masked share s'_i = share_i XOR r_i (SECRET —
|
||||
// about to be REVEALED). Same layout.
|
||||
// round1_msgs - vector of Round-1 commits from the quorum
|
||||
// (PUBLIC).
|
||||
// quorum_size - t (PUBLIC).
|
||||
// session_id - 16-byte session id (PUBLIC).
|
||||
// attempt - u32 attempt counter (PUBLIC).
|
||||
//
|
||||
// Outputs:
|
||||
// reveal_out (MAGNETAR_RESPONSE_BYTES = 384 bytes):
|
||||
// [0 .. 2*seed_size) mask r_i
|
||||
// [2*seed_size .. 4*seed_size) masked_share s'_i
|
||||
//
|
||||
// Constant-time obligations:
|
||||
// - Time and memory-access patterns independent of mask, masked.
|
||||
// Round-2 IS the reveal step; secrecy of mask/masked ends here.
|
||||
// - The session/attempt consistency check is over PUBLIC inputs.
|
||||
|
||||
require "../lib/magnetar_params.jinc"
|
||||
require "../lib/transcript.jinc"
|
||||
|
||||
// Concatenate (mask || masked) into the reveal_out buffer.
|
||||
inline
|
||||
fn round2_pack_reveal(
|
||||
reg ptr u8[MAGNETAR_MASK_BYTES] mask,
|
||||
reg ptr u8[MAGNETAR_MASKED_BYTES] masked,
|
||||
reg ptr u8[MAGNETAR_RESPONSE_BYTES] reveal_out)
|
||||
-> reg ptr u8[MAGNETAR_RESPONSE_BYTES]
|
||||
{
|
||||
reg u32 i;
|
||||
reg u8 c;
|
||||
|
||||
?{}, i = #set0_32();
|
||||
while (i < MAGNETAR_MASK_BYTES) {
|
||||
c = mask[i];
|
||||
reveal_out[(int)(MAGNETAR_R2_MASK_OFFSET + i)] = c;
|
||||
i += 1;
|
||||
}
|
||||
?{}, i = #set0_32();
|
||||
while (i < MAGNETAR_MASKED_BYTES) {
|
||||
c = masked[i];
|
||||
reveal_out[(int)(MAGNETAR_R2_MASKED_OFFSET + i)] = c;
|
||||
i += 1;
|
||||
}
|
||||
return reveal_out;
|
||||
}
|
||||
|
||||
// Round-2 entry point.
|
||||
export
|
||||
fn magnetar_round2(
|
||||
reg u64 mask_ptr,
|
||||
reg u64 masked_ptr,
|
||||
reg u64 session_id_ptr,
|
||||
reg u32 attempt,
|
||||
reg u64 reveal_out_ptr)
|
||||
-> reg u32
|
||||
{
|
||||
reg u32 result;
|
||||
result = 0;
|
||||
// CAVEAT: entry-point stub. Real impl: type-cast pointers, call
|
||||
// round2_pack_reveal, write reveal_out.
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user