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]>
171 lines
6.1 KiB
Plaintext
171 lines
6.1 KiB
Plaintext
// Pulsar's KMAC256 - SP 800-185 ยง4.
|
|
//
|
|
// KMAC256(K, X, L, S) := cSHAKE256(newX, L, "KMAC", S)
|
|
// where newX = bytepad(encode_string(K), 136) || X || right_encode(L*8)
|
|
//
|
|
// Pulsar's Go reference (ref/go/pkg/pulsar/transcript.go::kmac256) uses
|
|
// the customisation strings "PULSAR-SIGN-R1-MAC-V1", "PULSAR-SIGN-R2-V1"
|
|
// etc. The keccak rate is 136 bytes (SHAKE256), the key length is
|
|
// fixed to 32 bytes throughout Pulsar (the ML-KEM-768-established
|
|
// session key K_{i,j}), and the output length is fixed to 32 bytes
|
|
// (a MAC tag). This drastically simplifies the SP 800-185 encoder.
|
|
//
|
|
// We provide:
|
|
//
|
|
// kmac256_init_R1MAC(state, K) - absorb the prelude + key for the
|
|
// PULSAR-SIGN-R1-MAC-V1 customisation.
|
|
// kmac256_init_R2(state, K) - absorb the prelude + key for the
|
|
// PULSAR-SIGN-R2-V1 customisation
|
|
// (used by Round-2 transcript MAC).
|
|
//
|
|
// After init, the caller absorbs the message bytes via repeated calls
|
|
// to absorb_byte_kmac256 / cshake256_absorb_bytes (from transcript.jinc),
|
|
// then calls kmac256_finalize_32(state, tag_out) which appends
|
|
// right_encode(256) followed by the cSHAKE finaliser and squeezes the
|
|
// 32-byte MAC tag.
|
|
|
|
require "pulsar_params.jinc"
|
|
require "transcript.jinc"
|
|
from Jade require "crypto_sign/dilithium/common/amd64/fips202.jinc"
|
|
|
|
// Absorb the cSHAKE256 prelude bytes for KMAC256 with N="KMAC",
|
|
// S="PULSAR-SIGN-R1-MAC-V1" (21 bytes -> 168 bits = 0xa8).
|
|
//
|
|
// bytepad(encode_string("KMAC") || encode_string(S), 136):
|
|
// left_encode(136) = {0x01, 0x88} (2 bytes)
|
|
// || encode_string("KMAC") = {0x01, 0x20, K, M, A, C} (6 bytes)
|
|
// || encode_string(S) = {0x01, 0xa8, S[0..21)} (23 bytes)
|
|
// || zero-pad to 136 bytes (105 bytes)
|
|
//
|
|
// The first block (136 bytes) is exactly one SHAKE256 absorption block.
|
|
//
|
|
// We then absorb the SP 800-185 key prefix:
|
|
// bytepad(encode_string(K), 136) where |K| = 32 bytes:
|
|
// left_encode(136) = {0x01, 0x88} (2 bytes)
|
|
// || encode_string(K) [|K|=256] = {0x02, 0x01, 0x00, K[0..32)}
|
|
// (35 bytes)
|
|
// || zero-pad to 136 bytes (99 bytes)
|
|
//
|
|
// After this routine, the state is ready to absorb the message body X,
|
|
// starting at running offset 0 (the second 136-byte block having just
|
|
// been permuted).
|
|
//
|
|
// Stack-return call (not inlined): this routine is large (two keccak
|
|
// permutations plus 30+ state byte-writes). Inlining it into each of the
|
|
// several Round-2 KMAC sites multiplied that working set across the whole
|
|
// compilation unit until Jasmin's global register allocator ran dry. As
|
|
// a real call it is emitted once. Semantics/CT unchanged.
|
|
#[returnaddress="stack"]
|
|
fn kmac256_init_R1MAC(reg ptr u64[25] state, reg ptr u8[32] K)
|
|
-> reg ptr u64[25]
|
|
{
|
|
reg u64 i;
|
|
reg u8 c;
|
|
|
|
// ---- First block: prelude ----
|
|
state = st0(state);
|
|
state[:u8 0] = 0x01;
|
|
state[:u8 1] = 0x88;
|
|
state[:u8 2] = 0x01;
|
|
state[:u8 3] = 0x20;
|
|
state[:u8 4] = 0x4b; state[:u8 5] = 0x4d; state[:u8 6] = 0x41;
|
|
state[:u8 7] = 0x43;
|
|
state[:u8 8] = 0x01;
|
|
state[:u8 9] = 0xa8;
|
|
state[:u8 10] = 0x50; state[:u8 11] = 0x55; state[:u8 12] = 0x4c;
|
|
state[:u8 13] = 0x53; state[:u8 14] = 0x41; state[:u8 15] = 0x52;
|
|
state[:u8 16] = 0x2d; state[:u8 17] = 0x53; state[:u8 18] = 0x49;
|
|
state[:u8 19] = 0x47; state[:u8 20] = 0x4e; state[:u8 21] = 0x2d;
|
|
state[:u8 22] = 0x52; state[:u8 23] = 0x31; state[:u8 24] = 0x2d;
|
|
state[:u8 25] = 0x4d; state[:u8 26] = 0x41; state[:u8 27] = 0x43;
|
|
state[:u8 28] = 0x2d; state[:u8 29] = 0x56; state[:u8 30] = 0x31;
|
|
state = __keccakf1600_ref(state);
|
|
|
|
// ---- Second block: key prefix ----
|
|
// Start fresh - we are now at the next block, so bytes 0..135 must
|
|
// be xored with the key prefix. We use plain assignment because the
|
|
// state was just permuted from a fresh st0() and the prelude block:
|
|
// its rate-block bytes are non-zero now, so we MUST xor.
|
|
state[:u8 0] ^= 0x01;
|
|
state[:u8 1] ^= 0x88;
|
|
state[:u8 2] ^= 0x02;
|
|
state[:u8 3] ^= 0x01;
|
|
state[:u8 4] ^= 0x00;
|
|
?{}, i = #set0_64();
|
|
while (i < 32) {
|
|
c = K[i];
|
|
state[:u8 5 + i] ^= c;
|
|
i += 1;
|
|
}
|
|
state = __keccakf1600_ref(state);
|
|
|
|
return state;
|
|
}
|
|
|
|
// Append the KMAC256 trailer right_encode(L*8 = 256) = {0x01,0x00,0x02}
|
|
// followed by the cSHAKE256 finaliser (0x04 || ... || 0x80) and permute.
|
|
// Squeezes the 32-byte tag into `tag_out`. Caller passes the current
|
|
// absorption `offset` mod SHAKE256_RATE.
|
|
// Stack-return call (not inlined), same global-pressure rationale as
|
|
// kmac256_init_R1MAC. Semantics/CT unchanged.
|
|
#[returnaddress="stack"]
|
|
fn kmac256_finalize_32(
|
|
reg ptr u64[25] state,
|
|
reg u64 offset,
|
|
reg ptr u8[32] tag_out)
|
|
-> reg ptr u64[25], reg ptr u8[32]
|
|
{
|
|
reg u64 i;
|
|
reg u8 c;
|
|
|
|
// right_encode(256) = {0x01, 0x00, 0x02}
|
|
state[:u8 offset] ^= 0x01; offset += 1;
|
|
if (offset == SHAKE256_RATE) {
|
|
state = __keccakf1600_ref(state);
|
|
?{}, offset = #set0_64();
|
|
}
|
|
state[:u8 offset] ^= 0x00; offset += 1;
|
|
if (offset == SHAKE256_RATE) {
|
|
state = __keccakf1600_ref(state);
|
|
?{}, offset = #set0_64();
|
|
}
|
|
state[:u8 offset] ^= 0x02; offset += 1;
|
|
if (offset == SHAKE256_RATE) {
|
|
state = __keccakf1600_ref(state);
|
|
?{}, offset = #set0_64();
|
|
}
|
|
|
|
// cSHAKE finaliser.
|
|
state[:u8 offset] ^= 0x04;
|
|
state[:u8 SHAKE256_RATE - 1] ^= 0x80;
|
|
state = __keccakf1600_ref(state);
|
|
|
|
?{}, i = #set0_64();
|
|
while (i < 32) {
|
|
c = state[:u8 i];
|
|
tag_out[i] = c;
|
|
i += 1;
|
|
}
|
|
return state, tag_out;
|
|
}
|
|
|
|
// Constant-time 32-byte equality. Returns 0 on equal, non-zero on differ.
|
|
inline
|
|
fn ct_eq_32(reg ptr u8[32] a, reg ptr u8[32] b) -> reg u8
|
|
{
|
|
reg u64 i;
|
|
reg u8 acc, x, y, d;
|
|
|
|
?{}, acc = #set0_8();
|
|
?{}, i = #set0_64();
|
|
while (i < 32) {
|
|
x = a[i];
|
|
y = b[i];
|
|
d = x;
|
|
d ^= y;
|
|
acc |= d;
|
|
i += 1;
|
|
}
|
|
return acc;
|
|
}
|