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]>
414 lines
14 KiB
Plaintext
414 lines
14 KiB
Plaintext
// Pulsar transcript layer - SP 800-185 cSHAKE256 / KMAC256.
|
|
//
|
|
// The Pulsar Go reference (ref/go/pkg/pulsar/transcript.go) routes
|
|
// every transcript hash through cSHAKE256 with function-name N="Pulsar"
|
|
// and a per-purpose customisation string S in {PULSAR-SIGN-R1-V1,
|
|
// PULSAR-SIGN-R1-MAC-V1, PULSAR-SIGN-R2-V1, PULSAR-SEED-SHARE-V1, ...}.
|
|
//
|
|
// This file ports the bytes-on-the-wire computation of cSHAKE256 from
|
|
// SP 800-185 §3 into Jasmin so a quorum running Jasmin and a quorum
|
|
// running Go agree byte-for-byte on every transcript binding.
|
|
//
|
|
// cSHAKE256(X, L, N, S) = KECCAK[512]( bytepad(encode_string(N) ||
|
|
// encode_string(S), 136)
|
|
// || X || 00 ),
|
|
// truncated to L bits.
|
|
//
|
|
// When N=S=empty, cSHAKE256 degenerates to plain SHAKE256 (suffix 0x1f).
|
|
// Otherwise the domain-separation byte is 0x04 (SP 800-185 §3.3).
|
|
// libjade's hash_mu_w1 / hash_tr_m use the plain-SHAKE form (FIPS 204
|
|
// has no customisation); we use the cSHAKE form here.
|
|
//
|
|
// Per-call PULSAR domain strings are baked in as inline byte arrays so
|
|
// the compiler emits straight-line code with constant memory access.
|
|
|
|
from Jade require "crypto_sign/dilithium/common/amd64/fips202.jinc"
|
|
|
|
// SP 800-185 left_encode(x) - returns a byte string whose first byte is
|
|
// the number of subsequent bytes, followed by x in big-endian.
|
|
//
|
|
// Pulsar's transcripts only ever left_encode small integers:
|
|
// - left_encode(8 * 6) for "Pulsar" = {0x01, 0x30}
|
|
// - left_encode(8 * <S_len_bytes>) - one byte for S lengths up
|
|
// to 31 bytes (which covers
|
|
// every Pulsar tag).
|
|
// - left_encode(8 * |key|) - one byte for 32-byte keys.
|
|
// - left_encode(<number_of_parts>) - one byte for small N.
|
|
//
|
|
// We provide inline helpers below for the common cases used by Pulsar.
|
|
|
|
// Length-prefix encoder for ASCII strings up to 31 bytes.
|
|
// Emits left_encode(bit_length) = {0x01, byte(bit_length)} as two bytes,
|
|
// always. Returns the offset advanced past the prefix.
|
|
inline
|
|
fn left_encode_byte(reg ptr u8[200] buf, reg u64 offset, inline int bit_length)
|
|
-> reg ptr u8[200], reg u64
|
|
{
|
|
buf[offset] = 0x01;
|
|
offset += 1;
|
|
buf[offset] = bit_length;
|
|
offset += 1;
|
|
return buf, offset;
|
|
}
|
|
|
|
// Absorb a constant-size 6-byte ASCII "Pulsar" function-name with its
|
|
// SP 800-185 encode_string framing into a 200-byte staging buffer at the
|
|
// running offset. Returns the new offset.
|
|
inline
|
|
fn absorb_N_Pulsar(reg ptr u8[200] buf, reg u64 offset)
|
|
-> reg ptr u8[200], reg u64
|
|
{
|
|
// encode_string("Pulsar") = left_encode(48) || "Pulsar"
|
|
// = {0x01, 0x30} || "Pulsar"
|
|
buf[offset] = 0x01; offset += 1;
|
|
buf[offset] = 0x30; offset += 1; // 6 bytes * 8 bits = 48
|
|
buf[offset] = 0x50; offset += 1; // 'P'
|
|
buf[offset] = 0x75; offset += 1; // 'u'
|
|
buf[offset] = 0x6c; offset += 1; // 'l'
|
|
buf[offset] = 0x73; offset += 1; // 's'
|
|
buf[offset] = 0x61; offset += 1; // 'a'
|
|
buf[offset] = 0x72; offset += 1; // 'r'
|
|
return buf, offset;
|
|
}
|
|
|
|
// Build the cSHAKE256 prelude bytes (bytepad(encode_string(N) ||
|
|
// encode_string(S), 136)) into the supplied 200-byte staging buffer.
|
|
// 200 bytes is the keccak-1600 state size; SHAKE256's rate (136 bytes)
|
|
// is the bytepad width. Pulsar's longest customisation S is 23 bytes
|
|
// ("PULSAR-RESHARE-TRANSCRIPT-V1"), so the prelude always fits inside
|
|
// a single 136-byte block:
|
|
// 2 (encode_string framing) + 6 ("Pulsar") = 8
|
|
// + 2 (encode_string framing) + <=23 ASCII <= 33
|
|
// + 2 (bytepad left_encode(136)) = 2
|
|
// Total <= 43, plus zero-pad to 136.
|
|
//
|
|
// We never call cSHAKE256 with a per-message-variable S length, so the
|
|
// prelude is statically known at every call site. The caller supplies S
|
|
// as an inline byte string and its length, and absorbs the prelude
|
|
// directly into the keccak state.
|
|
|
|
// Absorb a full cSHAKE256 prelude into a fresh keccak state, ready to
|
|
// be followed by the message bytes X. Caller passes:
|
|
// state - 25 u64 words, will be zeroed and absorbed into
|
|
// S_bytes - the customisation ASCII array (e.g. b"PULSAR-SIGN-R1-V1")
|
|
// S_len - its byte length (compile-time constant)
|
|
//
|
|
// After this routine returns, `offset` indexes into the state at the
|
|
// position where X absorption should continue. Caller must NOT call
|
|
// keccakf1600 between prelude absorption and X absorption unless the
|
|
// running offset would overflow the rate.
|
|
inline
|
|
fn cshake256_absorb_prelude_R1(reg ptr u64[25] state)
|
|
-> reg ptr u64[25], reg u64
|
|
{
|
|
// Prelude for cSHAKE256 with N="Pulsar", S="PULSAR-SIGN-R1-V1".
|
|
//
|
|
// bytepad(encode_string("Pulsar") || encode_string("PULSAR-SIGN-R1-V1"), 136)
|
|
// = left_encode(136) = {0x01, 0x88} (2 bytes)
|
|
// || left_encode(48) "Pulsar"= {0x01, 0x30}"Pulsar" (8 bytes)
|
|
// || left_encode(136) "PULSAR-SIGN-R1-V1"
|
|
// = {0x01, 0x88}"PULSAR..."(19 bytes)
|
|
// || zero-pad to 136 bytes (107 bytes)
|
|
//
|
|
// Total: 136 bytes, exactly one SHAKE256 absorption block.
|
|
//
|
|
// 0x88 = 136 = 17 bytes * 8 bits/byte (the bit length of "PULSAR-SIGN-R1-V1")
|
|
reg u64 i;
|
|
state = st0(state);
|
|
state[:u8 0] = 0x01;
|
|
state[:u8 1] = 0x88;
|
|
state[:u8 2] = 0x01;
|
|
state[:u8 3] = 0x30;
|
|
state[:u8 4] = 0x50;
|
|
state[:u8 5] = 0x75;
|
|
state[:u8 6] = 0x6c;
|
|
state[:u8 7] = 0x73;
|
|
state[:u8 8] = 0x61;
|
|
state[:u8 9] = 0x72;
|
|
state[:u8 10] = 0x01;
|
|
state[:u8 11] = 0x88;
|
|
// "PULSAR-SIGN-R1-V1" - 17 bytes
|
|
state[:u8 12] = 0x50; // P
|
|
state[:u8 13] = 0x55; // U
|
|
state[:u8 14] = 0x4c; // L
|
|
state[:u8 15] = 0x53; // S
|
|
state[:u8 16] = 0x41; // A
|
|
state[:u8 17] = 0x52; // R
|
|
state[:u8 18] = 0x2d; // -
|
|
state[:u8 19] = 0x53; // S
|
|
state[:u8 20] = 0x49; // I
|
|
state[:u8 21] = 0x47; // G
|
|
state[:u8 22] = 0x4e; // N
|
|
state[:u8 23] = 0x2d; // -
|
|
state[:u8 24] = 0x52; // R
|
|
state[:u8 25] = 0x31; // 1
|
|
state[:u8 26] = 0x2d; // -
|
|
state[:u8 27] = 0x56; // V
|
|
state[:u8 28] = 0x31; // 1
|
|
// bytes 29..135 are already zero from st0().
|
|
// Permute - the prelude block is now consumed.
|
|
state = __keccakf1600_ref(state);
|
|
?{}, i = #set0_64();
|
|
return state, i;
|
|
}
|
|
|
|
// cSHAKE256 prelude with N="Pulsar", S="PULSAR-SIGN-R1-MAC-V1" (21 bytes).
|
|
inline
|
|
fn cshake256_absorb_prelude_R1MAC(reg ptr u64[25] state)
|
|
-> reg ptr u64[25], reg u64
|
|
{
|
|
reg u64 i;
|
|
state = st0(state);
|
|
state[:u8 0] = 0x01;
|
|
state[:u8 1] = 0x88;
|
|
state[:u8 2] = 0x01;
|
|
state[:u8 3] = 0x30;
|
|
state[:u8 4] = 0x50; state[:u8 5] = 0x75; state[:u8 6] = 0x6c;
|
|
state[:u8 7] = 0x73; state[:u8 8] = 0x61; state[:u8 9] = 0x72;
|
|
state[:u8 10] = 0x01;
|
|
state[:u8 11] = 0xa8; // 21 * 8 = 168 -> 0xa8
|
|
// "PULSAR-SIGN-R1-MAC-V1" - 21 bytes
|
|
state[:u8 12] = 0x50; state[:u8 13] = 0x55; state[:u8 14] = 0x4c;
|
|
state[:u8 15] = 0x53; state[:u8 16] = 0x41; state[:u8 17] = 0x52;
|
|
state[:u8 18] = 0x2d; state[:u8 19] = 0x53; state[:u8 20] = 0x49;
|
|
state[:u8 21] = 0x47; state[:u8 22] = 0x4e; state[:u8 23] = 0x2d;
|
|
state[:u8 24] = 0x52; state[:u8 25] = 0x31; state[:u8 26] = 0x2d;
|
|
state[:u8 27] = 0x4d; state[:u8 28] = 0x41; state[:u8 29] = 0x43;
|
|
state[:u8 30] = 0x2d; state[:u8 31] = 0x56; state[:u8 32] = 0x31;
|
|
state = __keccakf1600_ref(state);
|
|
?{}, i = #set0_64();
|
|
return state, i;
|
|
}
|
|
|
|
// cSHAKE256 prelude with N="Pulsar", S="PULSAR-SIGN-R2-V1" (17 bytes).
|
|
inline
|
|
fn cshake256_absorb_prelude_R2(reg ptr u64[25] state)
|
|
-> reg ptr u64[25], reg u64
|
|
{
|
|
reg u64 i;
|
|
state = st0(state);
|
|
state[:u8 0] = 0x01;
|
|
state[:u8 1] = 0x88;
|
|
state[:u8 2] = 0x01;
|
|
state[:u8 3] = 0x30;
|
|
state[:u8 4] = 0x50; state[:u8 5] = 0x75; state[:u8 6] = 0x6c;
|
|
state[:u8 7] = 0x73; state[:u8 8] = 0x61; state[:u8 9] = 0x72;
|
|
state[:u8 10] = 0x01;
|
|
state[:u8 11] = 0x88;
|
|
state[:u8 12] = 0x50; state[:u8 13] = 0x55; state[:u8 14] = 0x4c;
|
|
state[:u8 15] = 0x53; state[:u8 16] = 0x41; state[:u8 17] = 0x52;
|
|
state[:u8 18] = 0x2d; state[:u8 19] = 0x53; state[:u8 20] = 0x49;
|
|
state[:u8 21] = 0x47; state[:u8 22] = 0x4e; state[:u8 23] = 0x2d;
|
|
state[:u8 24] = 0x52; state[:u8 25] = 0x32; state[:u8 26] = 0x2d;
|
|
state[:u8 27] = 0x56; state[:u8 28] = 0x31;
|
|
state = __keccakf1600_ref(state);
|
|
?{}, i = #set0_64();
|
|
return state, i;
|
|
}
|
|
|
|
// cSHAKE256 prelude with N="Pulsar", S="PULSAR-SEED-SHARE-V1" (20 bytes).
|
|
inline
|
|
fn cshake256_absorb_prelude_SeedShare(reg ptr u64[25] state)
|
|
-> reg ptr u64[25], reg u64
|
|
{
|
|
reg u64 i;
|
|
state = st0(state);
|
|
state[:u8 0] = 0x01;
|
|
state[:u8 1] = 0x88;
|
|
state[:u8 2] = 0x01;
|
|
state[:u8 3] = 0x30;
|
|
state[:u8 4] = 0x50; state[:u8 5] = 0x75; state[:u8 6] = 0x6c;
|
|
state[:u8 7] = 0x73; state[:u8 8] = 0x61; state[:u8 9] = 0x72;
|
|
state[:u8 10] = 0x01;
|
|
state[:u8 11] = 0xa0; // 20 * 8 = 160 -> 0xa0
|
|
state[:u8 12] = 0x50; state[:u8 13] = 0x55; state[:u8 14] = 0x4c;
|
|
state[:u8 15] = 0x53; state[:u8 16] = 0x41; state[:u8 17] = 0x52;
|
|
state[:u8 18] = 0x2d; state[:u8 19] = 0x53; state[:u8 20] = 0x45;
|
|
state[:u8 21] = 0x45; state[:u8 22] = 0x44; state[:u8 23] = 0x2d;
|
|
state[:u8 24] = 0x53; state[:u8 25] = 0x48; state[:u8 26] = 0x41;
|
|
state[:u8 27] = 0x52; state[:u8 28] = 0x45; state[:u8 29] = 0x2d;
|
|
state[:u8 30] = 0x56; state[:u8 31] = 0x31;
|
|
state = __keccakf1600_ref(state);
|
|
?{}, i = #set0_64();
|
|
return state, i;
|
|
}
|
|
|
|
// Absorb `n` bytes from a raw memory address `src` into `state`
|
|
// starting at `offset`, calling keccakf1600 whenever the rate boundary
|
|
// is reached. Returns the running offset modulo the rate.
|
|
//
|
|
// Constant-time over the input bytes (no early exit, no
|
|
// data-dependent permute). Permute count is determined by n and offset
|
|
// only, both of which are PUBLIC in every Pulsar call site.
|
|
inline
|
|
fn cshake256_absorb_bytes(
|
|
reg ptr u64[25] state,
|
|
reg u64 offset,
|
|
reg u64 src,
|
|
reg u64 n)
|
|
-> reg ptr u64[25], reg u64
|
|
{
|
|
reg u64 i;
|
|
reg u8 c;
|
|
|
|
?{}, i = #set0_64();
|
|
while (i < n) {
|
|
c = [:u8 src + i];
|
|
state[:u8 offset] ^= c;
|
|
offset += 1;
|
|
i += 1;
|
|
if (offset == SHAKE256_RATE) {
|
|
state = __keccakf1600_ref(state);
|
|
?{}, offset = #set0_64();
|
|
}
|
|
}
|
|
return state, offset;
|
|
}
|
|
|
|
// Absorb a single byte, permuting if the rate is crossed.
|
|
inline
|
|
fn absorb_one_byte(
|
|
reg ptr u64[25] state,
|
|
reg u64 offset,
|
|
reg u8 c)
|
|
-> reg ptr u64[25], reg u64
|
|
{
|
|
state[:u8 offset] ^= c;
|
|
offset += 1;
|
|
if (offset == SHAKE256_RATE) {
|
|
state = __keccakf1600_ref(state);
|
|
?{}, offset = #set0_64();
|
|
}
|
|
return state, offset;
|
|
}
|
|
|
|
// Absorb a big-endian 32-bit integer (4 bytes) as a sequence of
|
|
// single-byte absorptions. Used everywhere we bind an attempt counter
|
|
// kappa or a byte-length into a transcript.
|
|
inline
|
|
fn absorb_u32_be(
|
|
reg ptr u64[25] state,
|
|
reg u64 offset,
|
|
reg u32 v)
|
|
-> reg ptr u64[25], reg u64
|
|
{
|
|
reg u8 c;
|
|
reg u32 t;
|
|
// Compute each big-endian byte via an explicit u32 shift into a temp
|
|
// before the u8 truncation; a fused shift-and-truncate is not always a
|
|
// single x86 instruction (linearization rejects the blended form in
|
|
// some register assignments). Byte values are unchanged.
|
|
t = v; t >>= 24; c = (8u) t;
|
|
state, offset = absorb_one_byte(state, offset, c);
|
|
t = v; t >>= 16; c = (8u) t;
|
|
state, offset = absorb_one_byte(state, offset, c);
|
|
t = v; t >>= 8; c = (8u) t;
|
|
state, offset = absorb_one_byte(state, offset, c);
|
|
c = (8u) v;
|
|
state, offset = absorb_one_byte(state, offset, c);
|
|
return state, offset;
|
|
}
|
|
|
|
// Absorb a fixed-size byte array from a stack/register array reference.
|
|
// `len` MUST be a compile-time constant matching the array size used at
|
|
// the call site; we expose only the lengths we need.
|
|
inline
|
|
fn absorb_array16(
|
|
reg ptr u64[25] state,
|
|
reg u64 offset,
|
|
reg ptr u8[16] src)
|
|
-> reg ptr u64[25], reg u64
|
|
{
|
|
reg u64 i;
|
|
reg u8 c;
|
|
?{}, i = #set0_64();
|
|
while (i < 16) {
|
|
c = src[i];
|
|
state, offset = absorb_one_byte(state, offset, c);
|
|
i += 1;
|
|
}
|
|
return state, offset;
|
|
}
|
|
|
|
inline
|
|
fn absorb_array32(
|
|
reg ptr u64[25] state,
|
|
reg u64 offset,
|
|
reg ptr u8[32] src)
|
|
-> reg ptr u64[25], reg u64
|
|
{
|
|
reg u64 i;
|
|
reg u8 c;
|
|
?{}, i = #set0_64();
|
|
while (i < 32) {
|
|
c = src[i];
|
|
state, offset = absorb_one_byte(state, offset, c);
|
|
i += 1;
|
|
}
|
|
return state, offset;
|
|
}
|
|
|
|
inline
|
|
fn absorb_array64(
|
|
reg ptr u64[25] state,
|
|
reg u64 offset,
|
|
reg ptr u8[64] src)
|
|
-> reg ptr u64[25], reg u64
|
|
{
|
|
reg u64 i;
|
|
reg u8 c;
|
|
?{}, i = #set0_64();
|
|
while (i < 64) {
|
|
c = src[i];
|
|
state, offset = absorb_one_byte(state, offset, c);
|
|
i += 1;
|
|
}
|
|
return state, offset;
|
|
}
|
|
|
|
// Absorb the SP 800-185 right_encode(L) for L = outLen*8 bits (KMAC256
|
|
// finaliser) - we don't need this for cSHAKE since the standard X || 00
|
|
// suffix is folded into the suffix byte 0x04 below.
|
|
|
|
// Finalise a cSHAKE256 absorption: xor the SP 800-185 §3.3 domain
|
|
// separation byte (0x04) at the current offset and the high padding bit
|
|
// at the last rate byte, then permute. Caller squeezes via state[:u8 j]
|
|
// for j < SHAKE256_RATE; for outputs > rate they should call
|
|
// __keccakf1600_ref + repeat.
|
|
inline
|
|
fn cshake256_finalize(reg ptr u64[25] state, reg u64 offset)
|
|
-> reg ptr u64[25]
|
|
{
|
|
state[:u8 offset] ^= 0x04;
|
|
state[:u8 SHAKE256_RATE - 1] ^= 0x80;
|
|
state = __keccakf1600_ref(state);
|
|
return state;
|
|
}
|
|
|
|
// Squeeze `n` bytes out of a fully-absorbed cSHAKE256 state into `dst`.
|
|
inline
|
|
fn cshake256_squeeze(
|
|
reg ptr u64[25] state,
|
|
reg u64 dst,
|
|
reg u64 n)
|
|
-> reg ptr u64[25]
|
|
{
|
|
reg u64 i;
|
|
reg u64 offset;
|
|
reg u8 c;
|
|
|
|
?{}, i = #set0_64();
|
|
?{}, offset = #set0_64();
|
|
while (i < n) {
|
|
if (offset == SHAKE256_RATE) {
|
|
state = __keccakf1600_ref(state);
|
|
?{}, offset = #set0_64();
|
|
}
|
|
c = state[:u8 offset];
|
|
[:u8 dst + i] = c;
|
|
offset += 1;
|
|
i += 1;
|
|
}
|
|
return state;
|
|
}
|