mirror of
https://github.com/luxfi/pulsar.git
synced 2026-09-24 00:25:52 +00:00
These 3 threshold-signature sources type-checked but failed in the Jasmin backend. Fixed with codegen-only restructuring — NO algorithm, value, wire layout, or constant-time change: - combine: decomposed an over-blended x86 effective address into a base reg; split a shifted-OR source into shift-into-temp then OR. - round1: relieved register pressure (spill output ptrs, extract the expandA+matvec+decompose phase into a helper frame, pack derive_rho_prime inputs into one preimage buffer, u32 stores for y_state). - round2: an export fn had 13 pointer params (Jasmin caps at 6) — marshalled them into a caller-supplied table; gave polyvecl_scalar_mont distinct in/out ptrs; routed per-peer MAC tables through one buffer; stack-return digest helpers; spilled keccak-crossing loop-invariants. Shared lib tweaks (consumed by these files): mac/transcript/lagrange/seed .jinc stack-return + u64-index + BE-via-temp. All #[ct] contracts and #declassify pragmas preserved (CT type-checked at compile). Full Jasmin corpus: 23/23 compile (was 20/23). Co-authored-by: Hanzo Dev <[email protected]>
566 lines
21 KiB
Plaintext
566 lines
21 KiB
Plaintext
// Pulsar Round-1 - per-party commit message.
|
|
//
|
|
// Reference: spec/pulsar-m.tex Algorithm "Sign Round 1 (party i, attempt
|
|
// kappa)" (Algorithm alg:sign-r1). Mirrors ref/go/pkg/pulsar/threshold.go
|
|
// ThresholdSigner.Round1 semantics.
|
|
//
|
|
// -----------------------------------------------------------------------------
|
|
// Algorithm
|
|
// -----------------------------------------------------------------------------
|
|
//
|
|
// Inputs (in order):
|
|
// share - this party's polyvecl secret share s_i (secret).
|
|
// Layout: Li2_l polynomials, each Li2_polydeg u32
|
|
// coefficients in F_q canonical form.
|
|
// blind - this party's polyvecl Pedersen-blinding share u_i
|
|
// (secret). Same layout as share.
|
|
// randomness - 32 bytes of PRNG key material K_i^prng (secret).
|
|
// In production: a KMS-backed long-lived key. For KAT
|
|
// runs: a deterministic seed.
|
|
// session_id - 16-byte session id (public).
|
|
// attempt - u32 attempt counter kappa (public).
|
|
// my_node_id - 32-byte node id of this party (public).
|
|
// pk_seed - 32-byte ML-DSA-65 public-key seed rho (public).
|
|
// mu - 64-byte FIPS 204 message hash binder (public).
|
|
//
|
|
// Outputs:
|
|
// commit_out (PULSARM_COMMIT_BYTES = 1952 bytes):
|
|
// [0 .. 32) D_i = cSHAKE256(pack_w1(w1_i), "PULSAR-SIGN-R1-V1")
|
|
// [32 .. 800) pack_w1(w1_i) (Li2_pack_w1len = 768)
|
|
// [800 .. 1472) per-peer MAC set (zero-initialised here; the
|
|
// wrapper fills slot j with KMAC(K_{i,j}, D_i||tau,
|
|
// "PULSAR-SIGN-R1-MAC-V1") via pulsar_round1_mac)
|
|
// [1472 .. 1504) tau1_digest (32 bytes)
|
|
// [1504 .. 1952) reserved zero (448 bytes)
|
|
//
|
|
// y_state_out (PULSARM_Y_STATE_BYTES = 17408 bytes):
|
|
// [0 .. 5120) y_i (Li2_l polys, 256 u32 coeffs each)
|
|
// [5120 .. 11264) w_i (Li2_k polys, 256 u32 coeffs each)
|
|
// [11264 .. 17408) w1_i (Li2_k polys, 256 u32 coeffs each)
|
|
//
|
|
// Constant-time obligations:
|
|
// - Time and memory-access patterns independent of share, blind,
|
|
// randomness. (Round-1 itself only touches `randomness`; share /
|
|
// blind are passed through unchanged for symmetry with Round-2.)
|
|
// - Polynomial sampling uses libjade's CT-proven expandMask.
|
|
// - MACs use libjade's keccakf1600 + this layer's CT KMAC256.
|
|
//
|
|
// Note on scaffold-vs-real signatures:
|
|
// The original scaffold's Round-1 signature was incomplete (no
|
|
// pk_seed, no mu, no node id, no Pedersen-blinding share). We extend
|
|
// the input set with the additional PUBLIC values required to
|
|
// actually compute the round. The high-level (commit, y_state)
|
|
// return shape is preserved.
|
|
|
|
require "../lib/pulsar_params.jinc"
|
|
require "../lib/seed.jinc"
|
|
require "../lib/transcript.jinc"
|
|
require "../lib/mac.jinc"
|
|
|
|
// libjade Dilithium3-ref single-party kernel.
|
|
from Jade require "crypto_sign/dilithium/dilithium3/amd64/ref/params.jinc"
|
|
from Jade require "crypto_sign/dilithium/dilithium3/amd64/ref/common.jinc"
|
|
from Jade require "crypto_sign/dilithium/dilithium3/amd64/ref/packing.jinc"
|
|
|
|
// Compute D_i = cSHAKE256(pack_w1(w1_i), 32, "Pulsar",
|
|
// "PULSAR-SIGN-R1-V1") - the Round-1 commit digest.
|
|
inline
|
|
fn round1_commit_digest(
|
|
reg ptr u8[Li2_pack_w1len] w1_packed,
|
|
reg ptr u8[32] D_out)
|
|
-> reg ptr u8[32]
|
|
{
|
|
stack u64[25] state;
|
|
reg ptr u64[25] state_rsp;
|
|
reg u64 offset;
|
|
reg u64 i;
|
|
reg u8 c;
|
|
|
|
state_rsp = state;
|
|
state_rsp, offset = cshake256_absorb_prelude_R1(state_rsp);
|
|
|
|
?{}, i = #set0_64();
|
|
while (i < Li2_pack_w1len) {
|
|
c = w1_packed[i];
|
|
state_rsp, offset = absorb_one_byte(state_rsp, offset, c);
|
|
i += 1;
|
|
}
|
|
|
|
state_rsp = cshake256_finalize(state_rsp, offset);
|
|
|
|
?{}, i = #set0_64();
|
|
while (i < 32) {
|
|
c = state_rsp[:u8 i];
|
|
D_out[i] = c;
|
|
i += 1;
|
|
}
|
|
return D_out;
|
|
}
|
|
|
|
// Expand A from pk_seed and compute the commitment polynomials:
|
|
// w_i = invNTT( A . NTT(y_i) ) (k polys, canonical form)
|
|
// w1_i = HighBits(w_i, 2*gamma2) (k polys)
|
|
// w1_packed = pack_w1(w1_i) (Li2_pack_w1len bytes)
|
|
//
|
|
// Pulled out of pulsar_round1_commit into its own (non-inline) function
|
|
// so the very large inlined working set of expandA + mult_mat_vec +
|
|
// decompose_vec gets an independent register frame. When this lived in
|
|
// the caller, the entire round-1 body — every libjade primitive is
|
|
// `inline` — flattened into one frame whose peak exceeded the 15 GP
|
|
// registers (the allocator fails on the expandA keccak state colliding
|
|
// with the still-live hashing pointers). Splitting at this boundary
|
|
// leaves the heavy matrix expansion with only its own locals live.
|
|
//
|
|
// reg-ptr arguments alias the caller's stack arrays and are written
|
|
// in place / returned by reference, exactly as libjade's own
|
|
// primitives do. The arithmetic is byte-identical to the original
|
|
// inlined sequence; no value, ordering, or CT property changes (all
|
|
// inputs here — pk_seed public, y_i the party's masked nonce — keep
|
|
// their original sensitivity and there are no secret-dependent
|
|
// branches or indices).
|
|
fn round1_w_from_y(
|
|
reg ptr u8[32] pk_seed,
|
|
reg ptr u32[Li2_l * Li2_polydeg] y_i,
|
|
reg ptr u32[Li2_k * Li2_polydeg] w_i_out,
|
|
reg ptr u32[Li2_k * Li2_polydeg] w1_i_out,
|
|
reg ptr u8[Li2_pack_w1len] w1_packed_out)
|
|
-> reg ptr u32[Li2_k * Li2_polydeg],
|
|
reg ptr u32[Li2_k * Li2_polydeg],
|
|
reg ptr u8[Li2_pack_w1len]
|
|
{
|
|
stack u8[32] pk_seed_local;
|
|
stack u8[Li2_pack_w1len] w1_packed_local;
|
|
stack u32[Li2_l * Li2_polydeg] fft_y_i;
|
|
stack u32[Li2_k * Li2_l * Li2_polydeg] fft_matA;
|
|
stack u32[Li2_k * Li2_polydeg] w_i;
|
|
stack u32[Li2_k * Li2_polydeg] w0_i;
|
|
stack u32[Li2_k * Li2_polydeg] w1_i;
|
|
reg ptr u8[Li2_pack_w1len] w1_rsp;
|
|
reg u64 i;
|
|
reg u32 r32;
|
|
reg u8 cb;
|
|
|
|
// expandA consumes its seed as a by-value `stack u8[32]`; copy the
|
|
// incoming reg-ptr seed into a local stack array so the allocator
|
|
// does not have to place a reg-ptr argument onto the stack.
|
|
?{}, i = #set0_64();
|
|
while (i < 32) { cb = pk_seed[i]; pk_seed_local[i] = cb; i += 1; }
|
|
|
|
// ---- Expand public matrix A from pk_seed ----
|
|
fft_matA = expandA(pk_seed_local);
|
|
|
|
// ---- w_i = A * y_i in NTT domain, then bring back ----
|
|
fft_y_i = deep_copy_vecl(y_i);
|
|
fft_y_i = fft_vec(fft_y_i);
|
|
w_i = mult_mat_vec(fft_matA, fft_y_i);
|
|
w_i = reduce32_veck(w_i);
|
|
w_i = ifft_to_mont_vec(w_i);
|
|
w_i = polyveck_caddq(w_i);
|
|
|
|
// ---- Decompose w_i = w1_i * 2*gamma2 + w0_i ----
|
|
w0_i, w1_i = decompose_vec(w_i);
|
|
// pack_w1 returns a fresh local stack buffer; capture it locally then
|
|
// copy into the caller-provided w1_packed_out (a pointer into a local
|
|
// stack region cannot be returned across the call boundary).
|
|
w1_rsp = w1_packed_local;
|
|
w1_rsp = pack_w1(w1_i);
|
|
w1_packed_local = w1_rsp;
|
|
|
|
// Copy w_i / w1_i into the caller-provided buffers.
|
|
?{}, i = #set0_64();
|
|
while (i < Li2_k * Li2_polydeg) {
|
|
r32 = w_i[i];
|
|
w_i_out[i] = r32;
|
|
r32 = w1_i[i];
|
|
w1_i_out[i] = r32;
|
|
i += 1;
|
|
}
|
|
// Copy packed w1 into the caller buffer.
|
|
?{}, i = #set0_64();
|
|
while (i < Li2_pack_w1len) {
|
|
cb = w1_packed_local[i];
|
|
w1_packed_out[i] = cb;
|
|
i += 1;
|
|
}
|
|
|
|
return w_i_out, w1_i_out, w1_packed_out;
|
|
}
|
|
|
|
// Compute tau_1 = cSHAKE256("PULSAR-SIGN-R1-V1") over:
|
|
// session_id || attempt(BE32) || my_node_id || pk_seed || mu
|
|
// The preimage is pre-assembled by the caller (shared with
|
|
// derive_rho_prime). Absorbed byte stream is identical to the previous
|
|
// five-operand version.
|
|
inline
|
|
fn round1_tau1_digest(
|
|
reg ptr u8[PULSARM_TAU1_PREIMAGE_LEN] tau1_preimage,
|
|
reg ptr u8[32] tau_out)
|
|
-> reg ptr u8[32]
|
|
{
|
|
stack u64[25] state;
|
|
reg ptr u64[25] state_rsp;
|
|
reg u64 offset;
|
|
reg u64 i;
|
|
reg u8 c;
|
|
|
|
state_rsp = state;
|
|
state_rsp, offset = cshake256_absorb_prelude_R1(state_rsp);
|
|
|
|
?{}, i = #set0_64();
|
|
while (i < PULSARM_TAU1_PREIMAGE_LEN) {
|
|
c = tau1_preimage[i];
|
|
state_rsp, offset = absorb_one_byte(state_rsp, offset, c);
|
|
i += 1;
|
|
}
|
|
|
|
state_rsp = cshake256_finalize(state_rsp, offset);
|
|
|
|
?{}, i = #set0_64();
|
|
while (i < 32) {
|
|
c = state_rsp[:u8 i];
|
|
tau_out[i] = c;
|
|
i += 1;
|
|
}
|
|
return tau_out;
|
|
}
|
|
|
|
// Pulsar Round-1 entry point. The function signature extends the
|
|
// original scaffold with the additional public inputs (blind share,
|
|
// my_node_id, pk_seed, mu) required to actually compute Round-1.
|
|
// The CT discipline is preserved over (share, blind, randomness).
|
|
//
|
|
// Returns 0 on success. The output buffer is laid out as
|
|
// [PULSARM_COMMIT_BYTES commit][PULSARM_Y_STATE_BYTES y_state] starting
|
|
// at `outputs_ptr`. Public inputs are packed into a single 144-byte
|
|
// blob at `public_inputs_ptr` with layout [sid:16][my_node_id:32]
|
|
// [pk_seed:32][mu:64]. Secrets stay as separate pointers so the
|
|
// caller does not have to memcpy them.
|
|
//
|
|
// Six register parameters total, fitting the x86_64 SysV ABI.
|
|
//
|
|
// CT signature: pointer values themselves are PUBLIC (they're just
|
|
// addresses used for memory loads/stores — leaking an address is OK
|
|
// here, the secrets are the *contents*). The attempt counter is
|
|
// PUBLIC. The return is a PUBLIC status code.
|
|
//
|
|
// The pointed-to data carries its own secrecy level: share/blind/
|
|
// randomness are SECRET in the cryptographic-content sense, but the
|
|
// loads from those buffers go into stack arrays which carry the
|
|
// secrecy forward. The public_inputs blob (sid, my_node_id, pk_seed,
|
|
// mu) is loaded into stack arrays that flow into expandA / KMAC and
|
|
// must be PUBLIC by the FIPS 204 + Pulsar-protocol spec; we mark
|
|
// those explicitly via #public on the stack arrays where needed.
|
|
#[ct = "public * public * public * public * public * public -> public"]
|
|
export fn pulsar_round1_commit(
|
|
reg u64 share_ptr,
|
|
reg u64 blind_ptr,
|
|
reg u64 randomness_ptr,
|
|
reg u64 public_inputs_ptr,
|
|
reg u32 attempt,
|
|
reg u64 outputs_ptr)
|
|
-> reg u64
|
|
{
|
|
// Output base address. We keep it in a stack slot and only
|
|
// materialise the two derived output pointers (commit/y_state) into
|
|
// registers in the final write phase (steps 9-10). The middle of
|
|
// this function inlines several keccak permutations whose register
|
|
// demand is high; pinning two GP registers to long-lived output
|
|
// pointers across that region exhausts the allocator. Spilling the
|
|
// base and re-deriving late keeps the same addresses with no extra
|
|
// live registers during the hash-heavy section.
|
|
stack u64 outputs_base;
|
|
reg u64 commit_out_ptr;
|
|
reg u64 y_state_out_ptr;
|
|
outputs_base = outputs_ptr;
|
|
|
|
// Stack workspace
|
|
stack u8[16] sid;
|
|
stack u8[32] node_id;
|
|
stack u8[32] pk_seed;
|
|
stack u8[64] mu;
|
|
stack u8[32] randomness;
|
|
stack u8[64] rho_prime;
|
|
stack u8[32] tau1_digest;
|
|
stack u8[Li2_pack_w1len] w1_packed;
|
|
stack u8[32] D_i;
|
|
// tau_1 preimage (sid||attempt_be||node_id||pk_seed||mu), assembled
|
|
// once and shared by derive_rho_prime and round1_tau1_digest.
|
|
stack u8[PULSARM_TAU1_PREIMAGE_LEN] tau1_preimage;
|
|
stack u32[Li2_l * Li2_polydeg] y_i;
|
|
// fft_y_i / fft_matA / w0_i moved into round1_w_from_y (their own
|
|
// register frame); only w_i and w1_i remain here as they feed the
|
|
// y_state output below.
|
|
stack u32[Li2_k * Li2_polydeg] w_i;
|
|
stack u32[Li2_k * Li2_polydeg] w1_i;
|
|
stack u16 nonce_local;
|
|
reg u16 nonce_reg;
|
|
reg u64 i;
|
|
reg u64 w_base;
|
|
// Separate scratch bytes for the public-input copy loops (sid,
|
|
// node_id, pk_seed, mu) versus the secret randomness copy loop.
|
|
// Sharing `c` across all five would make jasmin-ct join its level
|
|
// to `secret` (from the randomness load), tainting pk_seed et al.
|
|
reg u8 c;
|
|
reg u8 c_sec;
|
|
reg u32 r32;
|
|
|
|
reg ptr u8[16] sid_rsp;
|
|
reg ptr u8[32] node_id_rsp;
|
|
reg ptr u8[32] pk_seed_rsp;
|
|
reg ptr u8[64] mu_rsp;
|
|
reg ptr u8[32] randomness_rsp;
|
|
reg ptr u8[64] rho_prime_rsp;
|
|
reg ptr u8[32] tau1_rsp;
|
|
reg ptr u8[32] D_rsp;
|
|
reg ptr u8[Li2_pack_w1len] w1_rsp;
|
|
reg ptr u32[Li2_l * Li2_polydeg] yi_rsp;
|
|
reg ptr u32[Li2_k * Li2_polydeg] wi_rsp;
|
|
reg ptr u32[Li2_k * Li2_polydeg] w1i_rsp;
|
|
reg ptr u8[PULSARM_TAU1_PREIMAGE_LEN] preimage_rsp;
|
|
reg u32 attempt_be;
|
|
|
|
// ---- 1. Copy inputs into stack workspace ----
|
|
// Public inputs blob layout: [sid:16][node_id:32][pk_seed:32][mu:64]
|
|
?{}, i = #set0_64();
|
|
while (i < 16) { c = [:u8 public_inputs_ptr + i]; sid[i] = c; i += 1; }
|
|
?{}, i = #set0_64();
|
|
while (i < 32) { c = [:u8 public_inputs_ptr + 16 + i]; node_id[i] = c; i += 1; }
|
|
?{}, i = #set0_64();
|
|
while (i < 32) { c = [:u8 public_inputs_ptr + 48 + i]; pk_seed[i] = c; i += 1; }
|
|
?{}, i = #set0_64();
|
|
while (i < 64) { c = [:u8 public_inputs_ptr + 80 + i]; mu[i] = c; i += 1; }
|
|
?{}, i = #set0_64();
|
|
while (i < 32) { c_sec = [:u8 randomness_ptr + i]; randomness[i] = c_sec; i += 1; }
|
|
|
|
// The four public-spec arrays loaded above (sid, node_id,
|
|
// pk_seed, mu) flow into expandA / KMAC absorb contexts that the
|
|
// FIPS 204 + Pulsar protocol treats as public. Mark them public
|
|
// explicitly so jasmin-ct's flow inference does not back-propagate
|
|
// secrecy from subsequent (intentionally-declassified) uses.
|
|
#declassify(sid);
|
|
#declassify(node_id);
|
|
#declassify(pk_seed);
|
|
#declassify(mu);
|
|
|
|
// ---- 1b. Assemble the tau_1 preimage once ----
|
|
// Layout: sid(16) || attempt(BE32) || node_id(32) || pk_seed(32) || mu(64).
|
|
// This matches the byte stream the previous five-operand absorb
|
|
// produced; both derive_rho_prime and round1_tau1_digest consume it.
|
|
?{}, i = #set0_64();
|
|
while (i < 16) { c = sid[i]; tau1_preimage[i] = c; i += 1; }
|
|
// Big-endian attempt bytes. Compute each byte into a register first;
|
|
// a store of a shifted-then-truncated value is not a single x86
|
|
// instruction. Byte order is unchanged (matches absorb_u32_be).
|
|
attempt_be = attempt; attempt_be >>= 24; c = (8u) attempt_be; tau1_preimage[16] = c;
|
|
attempt_be = attempt; attempt_be >>= 16; c = (8u) attempt_be; tau1_preimage[17] = c;
|
|
attempt_be = attempt; attempt_be >>= 8; c = (8u) attempt_be; tau1_preimage[18] = c;
|
|
attempt_be = attempt; c = (8u) attempt_be; tau1_preimage[19] = c;
|
|
?{}, i = #set0_64();
|
|
while (i < 32) { c = node_id[i]; tau1_preimage[20 + i] = c; i += 1; }
|
|
?{}, i = #set0_64();
|
|
while (i < 32) { c = pk_seed[i]; tau1_preimage[52 + i] = c; i += 1; }
|
|
?{}, i = #set0_64();
|
|
while (i < 64) { c = mu[i]; tau1_preimage[84 + i] = c; i += 1; }
|
|
#declassify(tau1_preimage);
|
|
|
|
// ---- 2. Derive rho_prime_i via KMAC(K, tau_1, "PULSAR-SIGN-PRNG-V1") ----
|
|
randomness_rsp = randomness;
|
|
preimage_rsp = tau1_preimage;
|
|
rho_prime_rsp = rho_prime;
|
|
rho_prime_rsp = derive_rho_prime(randomness_rsp, preimage_rsp, rho_prime_rsp);
|
|
rho_prime = rho_prime_rsp;
|
|
|
|
// ---- 3. Sample y_i = expandMask(rho_prime, nonce = attempt) ----
|
|
rho_prime_rsp = rho_prime;
|
|
nonce_local = (16u) attempt;
|
|
nonce_reg = nonce_local;
|
|
y_i = expandMask(rho_prime_rsp, nonce_reg);
|
|
|
|
// ---- 4-6. Expand A, compute w_i = A*y_i, decompose, pack w1 ----
|
|
// Delegated to the non-inline round1_w_from_y so that expandA's large
|
|
// inlined working set occupies its own register frame (see that
|
|
// function's header). Results (w_i, w1_i, w1_packed) are written back
|
|
// into the local stack buffers by reference.
|
|
pk_seed_rsp = pk_seed;
|
|
yi_rsp = y_i;
|
|
wi_rsp = w_i;
|
|
w1i_rsp = w1_i;
|
|
w1_rsp = w1_packed;
|
|
wi_rsp, w1i_rsp, w1_rsp =
|
|
round1_w_from_y(pk_seed_rsp, yi_rsp, wi_rsp, w1i_rsp, w1_rsp);
|
|
w_i = wi_rsp;
|
|
w1_i = w1i_rsp;
|
|
w1_packed = w1_rsp;
|
|
|
|
// ---- 7. Compute tau1_digest ----
|
|
preimage_rsp = tau1_preimage;
|
|
tau1_rsp = tau1_digest;
|
|
tau1_rsp = round1_tau1_digest(preimage_rsp, tau1_rsp);
|
|
tau1_digest = tau1_rsp;
|
|
|
|
// ---- 8. Compute D_i = cSHAKE256(pack_w1(w1_i)) ----
|
|
w1_rsp = w1_packed;
|
|
D_rsp = D_i;
|
|
D_rsp = round1_commit_digest(w1_rsp, D_rsp);
|
|
D_i = D_rsp;
|
|
|
|
// ---- 9. Write commit_out ----
|
|
// Re-derive the output pointers now (see outputs_base comment above).
|
|
commit_out_ptr = outputs_base;
|
|
y_state_out_ptr = outputs_base;
|
|
y_state_out_ptr += PULSARM_COMMIT_BYTES;
|
|
// [0..32)
|
|
?{}, i = #set0_64();
|
|
while (i < 32) {
|
|
c = D_i[i];
|
|
[:u8 commit_out_ptr + i] = c;
|
|
i += 1;
|
|
}
|
|
// [32..800)
|
|
?{}, i = #set0_64();
|
|
while (i < Li2_pack_w1len) {
|
|
c = w1_packed[i];
|
|
[:u8 commit_out_ptr + PULSARM_R1_W1_OFFSET + i] = c;
|
|
i += 1;
|
|
}
|
|
// [800..1472) MAC slots - zero. The wrapper fills these via
|
|
// pulsar_round1_mac once per peer.
|
|
?{}, i = #set0_64();
|
|
while (i < PULSARM_R1_MAC_LEN) {
|
|
[:u8 commit_out_ptr + PULSARM_R1_MAC_OFFSET + i] = 0;
|
|
i += 1;
|
|
}
|
|
// [1472..1504) tau1_digest
|
|
?{}, i = #set0_64();
|
|
while (i < 32) {
|
|
c = tau1_digest[i];
|
|
[:u8 commit_out_ptr + PULSARM_R1_TAU_OFFSET + i] = c;
|
|
i += 1;
|
|
}
|
|
// [1504..1952) reserved zero
|
|
?{}, i = #set0_64();
|
|
while (i < PULSARM_R1_RESERVED) {
|
|
[:u8 commit_out_ptr + PULSARM_R1_TAU_OFFSET + PULSARM_R1_TAU_LEN + i] = 0;
|
|
i += 1;
|
|
}
|
|
|
|
// ---- 10. Write y_state_out ----
|
|
// Each coefficient is emitted as one little-endian u32 store. On x86
|
|
// a u32 store produces exactly the four LE bytes the previous
|
|
// explicit byte-shift sequence wrote, so the output is byte-identical;
|
|
// it also avoids the unencodable shifted-byte-store-with-compound-
|
|
// address pattern the byte form produced after offset folding. The
|
|
// per-poly base offset is folded into a base register (w_base) so the
|
|
// address stays a simple [base + index*4] mode.
|
|
// y_i (l polys)
|
|
?{}, i = #set0_64();
|
|
while (i < Li2_l * Li2_polydeg) {
|
|
r32 = y_i[i];
|
|
[:u32 y_state_out_ptr + 4*i] = r32;
|
|
i += 1;
|
|
}
|
|
// w_i (k polys)
|
|
w_base = y_state_out_ptr;
|
|
w_base += 4*(Li2_l*Li2_polydeg);
|
|
?{}, i = #set0_64();
|
|
while (i < Li2_k * Li2_polydeg) {
|
|
r32 = w_i[i];
|
|
[:u32 w_base + 4*i] = r32;
|
|
i += 1;
|
|
}
|
|
// w1_i (k polys)
|
|
w_base = y_state_out_ptr;
|
|
w_base += 4*((Li2_l + Li2_k)*Li2_polydeg);
|
|
?{}, i = #set0_64();
|
|
while (i < Li2_k * Li2_polydeg) {
|
|
r32 = w1_i[i];
|
|
[:u32 w_base + 4*i] = r32;
|
|
i += 1;
|
|
}
|
|
|
|
// Touch share/blind to satisfy the linker (they're passed through
|
|
// for the higher-level driver; Round-1 itself does not consume
|
|
// them).
|
|
c = [:u8 share_ptr];
|
|
c = [:u8 blind_ptr];
|
|
|
|
reg u64 status;
|
|
?{}, status = #set0_64();
|
|
return status;
|
|
}
|
|
|
|
// Per-peer MAC emission.
|
|
//
|
|
// Computes MAC_{i->j} = KMAC256(K_{i,j}, D_i || tau_1, 32,
|
|
// "PULSAR-SIGN-R1-MAC-V1") and writes the 32-byte tag into slot
|
|
// peer_index of commit_buf's per-peer MAC region.
|
|
//
|
|
// The caller is expected to call this once per peer j != i after
|
|
// pulsar_round1_commit has populated commit_buf[0..32) (= D_i) and
|
|
// commit_buf[1472..1504) (= tau1_digest).
|
|
// CT signature: pointer values are public addresses (no
|
|
// secret-address branching). The pointed-to session key K_{i,j} is
|
|
// SECRET; the commit_buf contents (D_i + tau1) are public-by-Round-1-
|
|
// protocol (they're already on the wire). peer_index is public.
|
|
#[ct = "public * public * public -> public"]
|
|
export fn pulsar_round1_mac(
|
|
reg u64 session_key_ptr, // *u8[32] K_{i,j} (secret contents)
|
|
reg u64 commit_buf_ptr, // *u8[PULSARM_COMMIT_BYTES] (public contents)
|
|
reg u32 peer_index)
|
|
-> reg u64
|
|
{
|
|
stack u8[32] K;
|
|
stack u8[32] D_i;
|
|
stack u8[32] tau1;
|
|
stack u8[32] tag;
|
|
stack u64[25] state;
|
|
reg ptr u64[25] state_rsp;
|
|
reg ptr u8[32] K_rsp, D_rsp, tau_rsp, tag_rsp;
|
|
reg u64 i, slot_offset, offset;
|
|
reg u8 c;
|
|
|
|
// Load session key, D_i, tau1.
|
|
?{}, i = #set0_64();
|
|
while (i < 32) { c = [:u8 session_key_ptr + i]; K[i] = c; i += 1; }
|
|
?{}, i = #set0_64();
|
|
while (i < 32) { c = [:u8 commit_buf_ptr + PULSARM_R1_D_OFFSET + i]; D_i[i] = c; i += 1; }
|
|
?{}, i = #set0_64();
|
|
while (i < 32) { c = [:u8 commit_buf_ptr + PULSARM_R1_TAU_OFFSET + i]; tau1[i]= c; i += 1; }
|
|
|
|
// KMAC256 init for PULSAR-SIGN-R1-MAC-V1 + absorb K.
|
|
state_rsp = state;
|
|
K_rsp = K;
|
|
state_rsp = kmac256_init_R1MAC(state_rsp, K_rsp);
|
|
|
|
// Absorb message X = D_i || tau1 (64 bytes total).
|
|
?{}, offset = #set0_64();
|
|
D_rsp = D_i;
|
|
tau_rsp = tau1;
|
|
state_rsp, offset = absorb_array32(state_rsp, offset, D_rsp);
|
|
state_rsp, offset = absorb_array32(state_rsp, offset, tau_rsp);
|
|
|
|
// Finalise + squeeze 32 bytes.
|
|
tag_rsp = tag;
|
|
state_rsp, tag_rsp = kmac256_finalize_32(state_rsp, offset, tag_rsp);
|
|
tag = tag_rsp;
|
|
|
|
// Write tag into commit_buf at peer_index slot. Fold the buffer base
|
|
// and the slot offset into a single register so the per-byte store is
|
|
// an encodable [base + index] mode (base + slot + index would be
|
|
// three address registers, which x86 cannot encode).
|
|
slot_offset = (64u) peer_index;
|
|
slot_offset *= 32;
|
|
slot_offset += PULSARM_R1_MAC_OFFSET;
|
|
slot_offset += commit_buf_ptr;
|
|
?{}, i = #set0_64();
|
|
while (i < 32) {
|
|
c = tag[i];
|
|
[:u8 slot_offset + i] = c;
|
|
i += 1;
|
|
}
|
|
|
|
reg u64 status;
|
|
?{}, status = #set0_64();
|
|
return status;
|
|
}
|