mirror of
https://github.com/luxfi/magnetar.git
synced 2026-07-27 02:53:47 +00:00
magnetar v1.1: internal FIPS 205 sec 5-8 walk for SHAKE family
slhdsa_internal.go: Magnetar-internal reference implementation of
FIPS 205 sec 5 (WOTS+ chain), sec 6 (XMSS), sec 7 (Hypertree), and
sec 8 (FORS) for the SHAKE_{192s, 192f, 256s} parameter sets.
The engine exposes the FIPS 205 byte production as a sequence of
WRITES into an already-allocated signature buffer, driven by two
opaque PRF callbacks (prfOutFn for sec 5/6/7/8 PRF, prfMsgFn for
sec 11.2 PRF_msg). The callbacks are the only seam for the FIPS 205
master byte material; the engine itself touches only public bytes.
Byte-conformant to cloudflare/circl/sign/slhdsa.SignDeterministic
across all three SHAKE modes. The strict-atom Combine path will
install the v1.1 closures over Lagrange-reconstructed material at
the next commit.
This commit is contained in:
@@ -0,0 +1,943 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package magnetar
|
||||
|
||||
// slhdsa_internal.go — Magnetar-internal FIPS 205 SLH-DSA reference
|
||||
// implementation for the SHAKE-{192s, 192f, 256s} parameter sets.
|
||||
//
|
||||
// This file is the v1.1 substrate for the strict-atom-assembly Combine
|
||||
// path (thbsse_assemble.go). It implements FIPS 205 §5 (WOTS+ chain),
|
||||
// §6.2 (FORS sign), §7 (XMSS), and §8 (Hypertree) directly, exposing
|
||||
// the FIPS 205 byte production as a sequence of WRITES into an
|
||||
// already-allocated signature buffer driven by an opaque PRF-output
|
||||
// CALLBACK. The callback (called prfOutFn) takes a fully-prepared
|
||||
// address and returns the n-byte PRF output that FIPS 205 §11.2 specifies
|
||||
// for SHAKE families:
|
||||
//
|
||||
// PRF(PK.seed, ADRS, X) = SHAKE256(PK.seed || ADRS || X)[:n]
|
||||
//
|
||||
// The CALLBACK is the strict-atom path's seam: thbsse_assemble.go
|
||||
// installs a callback that composes the X portion of the SHAKE input
|
||||
// from per-party Shamir shares of the FIPS 205 derived material WITHOUT
|
||||
// ever materialising the derived bytes as a free-standing variable.
|
||||
//
|
||||
// Every primitive in this file is fed BYTES — pkSeed, the address bytes,
|
||||
// the FORS-leaf indices, the WOTS+ chain indices. None of them holds
|
||||
// the underlying byte-shares of SK.seed / SK.prf; those live inside
|
||||
// thbsse_assemble.go's atom bag and never escape its closures.
|
||||
//
|
||||
// This file is FIPS 205 byte-exact against cloudflare/circl 1.6.3:
|
||||
// the byte-equality test in slhdsa_internal_test.go drives a circl-
|
||||
// derived (skSeed, skPrf, pkSeed) tuple through the SAME PRF callback
|
||||
// and asserts the output matches circl's SignDeterministic for the
|
||||
// same inputs across all three SHAKE parameter sets.
|
||||
//
|
||||
// =====================================================================
|
||||
// Parameter sets supported
|
||||
// =====================================================================
|
||||
//
|
||||
// Only the SHAKE family (SHAKE_192s, SHAKE_192f, SHAKE_256s) is
|
||||
// implemented here. The Magnetar package as a whole supports only these
|
||||
// three modes (params.go). The SHA-2 family would require a separate
|
||||
// PRF/PRFMsg/HashMsg dispatch and is intentionally out of scope.
|
||||
//
|
||||
// =====================================================================
|
||||
// Address layout (FIPS 205 §4.2)
|
||||
// =====================================================================
|
||||
//
|
||||
// SHAKE addresses are 32 bytes (non-compressed); SHA-2 addresses are
|
||||
// 22 bytes (compressed). This file uses 32-byte addresses unconditionally
|
||||
// for SHAKE modes:
|
||||
//
|
||||
// bytes 0- 3 : layer address (uint32 BE)
|
||||
// bytes 4- 7 : tree address [2] (uint32 BE)
|
||||
// bytes 8-11 : tree address [1] (uint32 BE)
|
||||
// bytes 12-15 : tree address [0] (uint32 BE)
|
||||
// bytes 16-19 : type (uint32 BE)
|
||||
// bytes 20-23 : key-pair address (uint32 BE)
|
||||
// bytes 24-27 : chain address / tree height (uint32 BE)
|
||||
// bytes 28-31 : hash address / tree index (uint32 BE)
|
||||
//
|
||||
// The constants below name the type values.
|
||||
//
|
||||
// =====================================================================
|
||||
// Hash primitives (FIPS 205 §11.2 — SHAKE instantiation)
|
||||
// =====================================================================
|
||||
//
|
||||
// H_msg(R, PK.seed, PK.root, M) =
|
||||
// SHAKE256(R || PK.seed || PK.root || M)[:m]
|
||||
//
|
||||
// PRF_msg(SK.prf, optRand, M) =
|
||||
// SHAKE256(SK.prf || optRand || M)[:n]
|
||||
//
|
||||
// PRF(PK.seed, ADRS, SK.seed) =
|
||||
// SHAKE256(PK.seed || ADRS || SK.seed)[:n]
|
||||
//
|
||||
// F(PK.seed, ADRS, M_1) =
|
||||
// SHAKE256(PK.seed || ADRS || M_1)[:n]
|
||||
//
|
||||
// H(PK.seed, ADRS, M_1 || M_2) =
|
||||
// SHAKE256(PK.seed || ADRS || M_1 || M_2)[:n]
|
||||
//
|
||||
// T_l(PK.seed, ADRS, M) =
|
||||
// SHAKE256(PK.seed || ADRS || M)[:n]
|
||||
//
|
||||
// The implementation routes ALL of {F, H, T_l, H_msg, PRF_msg} through
|
||||
// the SAME absorbBuf helper. The PRF call alone is hosted by the
|
||||
// strict-atom callback (prfOutFn) because it is the ONE site where
|
||||
// secret material (SK.seed) enters the input. F/H/T_l/H_msg/PRF_msg
|
||||
// operate on either public bytes (msg, R, pkSeed, addr) or on outputs
|
||||
// of earlier PRF calls; their inputs do not contain seed bytes
|
||||
// directly.
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// Address types per FIPS 205 §4.2 Table 1.
|
||||
const (
|
||||
adrsTypeWotsHash uint32 = 0
|
||||
adrsTypeWotsPk uint32 = 1
|
||||
adrsTypeTree uint32 = 2
|
||||
adrsTypeForsTree uint32 = 3
|
||||
adrsTypeForsRoots uint32 = 4
|
||||
adrsTypeWotsPrf uint32 = 5
|
||||
adrsTypeForsPrf uint32 = 6
|
||||
)
|
||||
|
||||
// WOTS+ Winternitz parameter w = 16, lg(w) = 4.
|
||||
const (
|
||||
wotsW uint32 = 16
|
||||
wotsLen2 uint32 = 3
|
||||
)
|
||||
|
||||
// internalParams is the FIPS 205 §10.1 parameter tuple this file
|
||||
// dispatches on. Materialised from a public-facing Magnetar Mode via
|
||||
// internalParamsForMode.
|
||||
type internalParams struct {
|
||||
mode Mode
|
||||
n uint32 // WOTS+ message length in bytes
|
||||
hPrime uint32 // XMSS Merkle tree height
|
||||
h uint32 // total hypertree height
|
||||
d uint32 // hypertree layers
|
||||
a uint32 // FORS bits per leaf
|
||||
k uint32 // FORS trees
|
||||
m uint32 // HashMsg digest length
|
||||
|
||||
// addrSize is fixed at 32 for SHAKE; included as a parameter so
|
||||
// future SHA-2 dispatch lines up uniformly.
|
||||
addrSize uint32
|
||||
}
|
||||
|
||||
// FIPS 205 §10.1 Table 2 — SHAKE family only.
|
||||
var (
|
||||
internalParamsM192s = &internalParams{
|
||||
mode: ModeM192s, n: 24, hPrime: 9, h: 63, d: 7, a: 14, k: 17, m: 39, addrSize: 32,
|
||||
}
|
||||
internalParamsM192f = &internalParams{
|
||||
mode: ModeM192f, n: 24, hPrime: 3, h: 66, d: 22, a: 8, k: 33, m: 42, addrSize: 32,
|
||||
}
|
||||
internalParamsM256s = &internalParams{
|
||||
mode: ModeM256s, n: 32, hPrime: 8, h: 64, d: 8, a: 14, k: 22, m: 47, addrSize: 32,
|
||||
}
|
||||
)
|
||||
|
||||
func internalParamsForMode(mode Mode) (*internalParams, bool) {
|
||||
switch mode {
|
||||
case ModeM192s:
|
||||
return internalParamsM192s, true
|
||||
case ModeM192f:
|
||||
return internalParamsM192f, true
|
||||
case ModeM256s:
|
||||
return internalParamsM256s, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// wotsLen1 = 2n (per FIPS 205 §5).
|
||||
func (p *internalParams) wotsLen1() uint32 { return 2 * p.n }
|
||||
|
||||
// wotsLen = wotsLen1 + wotsLen2.
|
||||
func (p *internalParams) wotsLen() uint32 { return p.wotsLen1() + wotsLen2 }
|
||||
|
||||
// wotsSigSize = wotsLen * n.
|
||||
func (p *internalParams) wotsSigSize() uint32 { return p.wotsLen() * p.n }
|
||||
|
||||
// xmssAuthPathSize = hPrime * n.
|
||||
func (p *internalParams) xmssAuthPathSize() uint32 { return p.hPrime * p.n }
|
||||
|
||||
// xmssSigSize = wotsSigSize + xmssAuthPathSize.
|
||||
func (p *internalParams) xmssSigSize() uint32 {
|
||||
return p.wotsSigSize() + p.xmssAuthPathSize()
|
||||
}
|
||||
|
||||
// hyperTreeSigSize = d * xmssSigSize.
|
||||
func (p *internalParams) hyperTreeSigSize() uint32 {
|
||||
return p.d * p.xmssSigSize()
|
||||
}
|
||||
|
||||
// forsAuthPathSize = a * n per FORS tree.
|
||||
func (p *internalParams) forsAuthPathSize() uint32 { return p.a * p.n }
|
||||
|
||||
// forsPairSize = n (sk) + a*n (auth) per FORS tree.
|
||||
func (p *internalParams) forsPairSize() uint32 {
|
||||
return p.n + p.forsAuthPathSize()
|
||||
}
|
||||
|
||||
// forsSigSize = k * (n + a*n).
|
||||
func (p *internalParams) forsSigSize() uint32 {
|
||||
return p.k * p.forsPairSize()
|
||||
}
|
||||
|
||||
// signatureSize = n (rnd) + forsSigSize + hyperTreeSigSize.
|
||||
func (p *internalParams) signatureSize() uint32 {
|
||||
return p.n + p.forsSigSize() + p.hyperTreeSigSize()
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Address manipulation
|
||||
// =====================================================================
|
||||
//
|
||||
// Addresses for SHAKE modes are 32 bytes laid out per FIPS 205 §4.2.
|
||||
|
||||
// adrsBuf is a 32-byte address scratchpad. All field setters write at
|
||||
// fixed offsets; the buffer is reused across PRF/F/H calls.
|
||||
type adrsBuf [32]byte
|
||||
|
||||
// setLayerAddress writes the layer address (uint32 BE) at offset 0.
|
||||
func (a *adrsBuf) setLayerAddress(l uint32) {
|
||||
binary.BigEndian.PutUint32(a[0:], l)
|
||||
}
|
||||
|
||||
// setTreeAddress writes the 3-word tree address.
|
||||
// - t[2] → bytes [4:8]
|
||||
// - t[1] → bytes [8:12]
|
||||
// - t[0] → bytes [12:16]
|
||||
func (a *adrsBuf) setTreeAddress(t [3]uint32) {
|
||||
binary.BigEndian.PutUint32(a[4:], t[2])
|
||||
binary.BigEndian.PutUint32(a[8:], t[1])
|
||||
binary.BigEndian.PutUint32(a[12:], t[0])
|
||||
}
|
||||
|
||||
// setTypeAndClear writes the type word at offset 16 and zeros bytes
|
||||
// 20..32 (key-pair address, chain/height address, hash/tree-index
|
||||
// address).
|
||||
func (a *adrsBuf) setTypeAndClear(t uint32) {
|
||||
binary.BigEndian.PutUint32(a[16:], t)
|
||||
for i := 20; i < 32; i++ {
|
||||
a[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// setKeyPairAddress writes the keypair address at offset 20.
|
||||
func (a *adrsBuf) setKeyPairAddress(i uint32) {
|
||||
binary.BigEndian.PutUint32(a[20:], i)
|
||||
}
|
||||
|
||||
// setChainAddress writes the chain-address word at offset 24.
|
||||
func (a *adrsBuf) setChainAddress(i uint32) {
|
||||
binary.BigEndian.PutUint32(a[24:], i)
|
||||
}
|
||||
|
||||
// setTreeHeight writes the tree-height word at offset 24 (overlaps
|
||||
// chain address per FIPS 205 §4.2 — they are union fields per address
|
||||
// type).
|
||||
func (a *adrsBuf) setTreeHeight(i uint32) {
|
||||
binary.BigEndian.PutUint32(a[24:], i)
|
||||
}
|
||||
|
||||
// setHashAddress writes the hash-address word at offset 28.
|
||||
func (a *adrsBuf) setHashAddress(i uint32) {
|
||||
binary.BigEndian.PutUint32(a[28:], i)
|
||||
}
|
||||
|
||||
// setTreeIndex writes the tree-index word at offset 28 (overlaps
|
||||
// hash address by FIPS 205 §4.2 union).
|
||||
func (a *adrsBuf) setTreeIndex(i uint32) {
|
||||
binary.BigEndian.PutUint32(a[28:], i)
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// PRF callback (the strict-atom seam)
|
||||
// =====================================================================
|
||||
//
|
||||
// prfOutFn is the FIPS 205 §11.2 PRF for SHAKE:
|
||||
// PRF(PK.seed, ADRS, X) = SHAKE256(PK.seed || ADRS || X)[:n]
|
||||
// where X is the n-byte secret seed material per FIPS 205 §6.2 and §5.
|
||||
//
|
||||
// The strict-atom Combine path (thbsse_assemble.go) installs a closure
|
||||
// here that composes X from Lagrange-interpolated per-party shares
|
||||
// WITHOUT ever holding X as a free-standing variable. The closure
|
||||
// owns the only buffer that holds X bytes and zeroizes it before
|
||||
// returning.
|
||||
//
|
||||
// The callee MUST write exactly n bytes to out. Callees that mutate the
|
||||
// supplied addr argument are responsible for restoring it before
|
||||
// return (the implementation here passes a fresh address per call so
|
||||
// this is moot).
|
||||
type prfOutFn func(out []byte, addr *adrsBuf)
|
||||
|
||||
// prfMsgFn is the FIPS 205 §11.2 PRF_msg for SHAKE:
|
||||
// PRF_msg(SK.prf, optRand, M) = SHAKE256(SK.prf || optRand || M)[:n]
|
||||
// SK.prf is the message-randomizer key half of the FIPS 205 expanded
|
||||
// material; it is hosted by the strict-atom atom bag for the same
|
||||
// reason X is.
|
||||
type prfMsgFn func(out, optRand, msgPrime []byte)
|
||||
|
||||
// =====================================================================
|
||||
// Hash primitives — public-byte hashes
|
||||
// =====================================================================
|
||||
|
||||
// shakeIntoCat absorbs the concatenation of parts and writes outLen
|
||||
// bytes to out. The standard SHAKE256 squeeze.
|
||||
func shakeIntoCat(out []byte, parts ...[]byte) {
|
||||
h := sha3.NewShake256()
|
||||
for _, p := range parts {
|
||||
_, _ = h.Write(p)
|
||||
}
|
||||
_, _ = h.Read(out)
|
||||
}
|
||||
|
||||
// fPub computes F(PK.seed, ADRS, M_1) per FIPS 205 §11.2 for SHAKE.
|
||||
// M_1 is n bytes. Used in WOTS+ chain step and FORS leaf hashing.
|
||||
func fPub(out, pkSeed []byte, addr *adrsBuf, m1 []byte) {
|
||||
shakeIntoCat(out, pkSeed, addr[:], m1)
|
||||
}
|
||||
|
||||
// hPub computes H(PK.seed, ADRS, M_1 || M_2) per FIPS 205 §11.2 for
|
||||
// SHAKE. M_1 and M_2 are each n bytes. Used in XMSS / FORS tree
|
||||
// concatenation steps.
|
||||
func hPub(out, pkSeed []byte, addr *adrsBuf, m1, m2 []byte) {
|
||||
shakeIntoCat(out, pkSeed, addr[:], m1, m2)
|
||||
}
|
||||
|
||||
// tlPub computes T_l(PK.seed, ADRS, M) per FIPS 205 §11.2 for SHAKE.
|
||||
// M is the concatenation of the WOTS+ public key chains (T_len)
|
||||
// or FORS root chains (T_k). Variadic to match FIPS 205's variable-
|
||||
// length input.
|
||||
func tlPub(out, pkSeed []byte, addr *adrsBuf, parts ...[]byte) {
|
||||
h := sha3.NewShake256()
|
||||
_, _ = h.Write(pkSeed)
|
||||
_, _ = h.Write(addr[:])
|
||||
for _, p := range parts {
|
||||
_, _ = h.Write(p)
|
||||
}
|
||||
_, _ = h.Read(out)
|
||||
}
|
||||
|
||||
// hMsgPub computes H_msg(R, PK.seed, PK.root, M) per FIPS 205 §11.2
|
||||
// for SHAKE. R is n bytes, PK.seed is n, PK.root is n, M is the
|
||||
// concatenation (b, |ctx|, ctx, msg) per FIPS 205 §10.2 Algorithm 22.
|
||||
func hMsgPub(out, r, pkSeed, pkRoot, msgPrime []byte) {
|
||||
shakeIntoCat(out, r, pkSeed, pkRoot, msgPrime)
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Index parsing (FIPS 205 §9.2 Algorithm 19 lines 4-13)
|
||||
// =====================================================================
|
||||
|
||||
// parseDigest unpacks the FIPS 205 HashMsg output into the FORS index
|
||||
// stream md and the hypertree leaf coordinates (idxTree, idxLeaf).
|
||||
func parseDigest(p *internalParams, digest []byte) (md []byte, idxTree [3]uint32, idxLeaf uint32) {
|
||||
l1 := (p.k*p.a + 7) / 8
|
||||
l2 := (p.h - p.h/p.d + 7) / 8
|
||||
l3 := (p.h + 8*p.d - 1) / (8 * p.d)
|
||||
|
||||
md = make([]byte, l1)
|
||||
copy(md, digest[:l1])
|
||||
|
||||
s2 := digest[l1 : l1+l2]
|
||||
s3 := digest[l1+l2 : l1+l2+l3]
|
||||
|
||||
var b2 [12]byte
|
||||
copy(b2[12-len(s2):], s2)
|
||||
n2 := p.h - p.h/p.d
|
||||
idxTree[0] = binary.BigEndian.Uint32(b2[8:]) & ((1 << n2) - 1)
|
||||
n2 -= 32
|
||||
idxTree[1] = binary.BigEndian.Uint32(b2[4:]) & ((1 << n2) - 1)
|
||||
idxTree[2] = binary.BigEndian.Uint32(b2[0:])
|
||||
|
||||
var b3 [4]byte
|
||||
copy(b3[4-len(s3):], s3)
|
||||
mask32 := (uint32(1) << (p.h / p.d)) - 1
|
||||
idxLeaf = mask32 & binary.BigEndian.Uint32(b3[0:])
|
||||
return
|
||||
}
|
||||
|
||||
// nextHTIndex shifts the hypertree tree-address one XMSS layer and
|
||||
// returns the next idxLeaf. FIPS 205 §7.1 Algorithm 12 line 11.
|
||||
func nextHTIndex(idxTree *[3]uint32, n uint32) (idxLeaf uint32) {
|
||||
idxLeaf = idxTree[0] & ((1 << n) - 1)
|
||||
idxTree[0] = (idxTree[0] >> n) | (idxTree[1] << (32 - n))
|
||||
idxTree[1] = (idxTree[1] >> n) | (idxTree[2] << (32 - n))
|
||||
idxTree[2] = (idxTree[2] >> n)
|
||||
return
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// FIPS 205 §5 — WOTS+ chain compute
|
||||
// =====================================================================
|
||||
|
||||
// wotsChain implements the FIPS 205 §5 Algorithm 5 chain function:
|
||||
//
|
||||
// chain(X, i, s, PK.seed, ADRS):
|
||||
// tmp ← X
|
||||
// for j ∈ [i, i+s):
|
||||
// ADRS.SetHashAddress(j)
|
||||
// tmp ← F(PK.seed, ADRS, tmp)
|
||||
// return tmp
|
||||
//
|
||||
// X is the chain base (n bytes). The caller is responsible for the
|
||||
// outer address state; this routine mutates only the hash-address word.
|
||||
func wotsChain(out, pkSeed []byte, addr *adrsBuf, x []byte, i, s uint32) {
|
||||
scratch := make([]byte, len(x))
|
||||
copy(scratch, x)
|
||||
for j := i; j < i+s; j++ {
|
||||
addr.setHashAddress(j)
|
||||
fPub(scratch, pkSeed, addr, scratch)
|
||||
}
|
||||
copy(out, scratch)
|
||||
}
|
||||
|
||||
// wotsBaseW partitions n bytes of message into 2n base-w (w=16) digits
|
||||
// (big-endian nibble order). FIPS 205 §4.5 base_2b helper, b=4.
|
||||
func wotsBaseW(out []uint32, msg []byte) {
|
||||
for i := range out {
|
||||
out[i] = uint32((msg[i/2] >> ((1 - (i & 1)) << 2)) & 0xF)
|
||||
}
|
||||
}
|
||||
|
||||
// wotsChecksum computes the WOTS+ checksum digits. FIPS 205 §5.2
|
||||
// Algorithm 7 lines 7-9 + §4.5 base_2b unpack.
|
||||
func wotsChecksum(p *internalParams, baseWMsg []uint32) [wotsLen2]uint32 {
|
||||
wotsLen1 := p.wotsLen1()
|
||||
csum := wotsLen1 * (wotsW - 1)
|
||||
for i := uint32(0); i < wotsLen1; i++ {
|
||||
csum -= baseWMsg[i]
|
||||
}
|
||||
var out [wotsLen2]uint32
|
||||
for i := uint32(0); i < wotsLen2; i++ {
|
||||
out[i] = (csum >> (8 - 4*i)) & 0xF
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// wotsSign produces a WOTS+ signature on msg under the seed material
|
||||
// reachable via prfOut. The signature occupies wotsSigSize bytes of out.
|
||||
//
|
||||
// FIPS 205 §5.2 Algorithm 7 — but expressed with the secret-side PRF
|
||||
// as an opaque callback. addr's keypair address must be pre-set; this
|
||||
// routine sets the chain address, type, and hash address per chain
|
||||
// step.
|
||||
func wotsSign(
|
||||
p *internalParams,
|
||||
out []byte,
|
||||
pkSeed []byte,
|
||||
addr *adrsBuf,
|
||||
msg []byte,
|
||||
prfOut prfOutFn,
|
||||
) {
|
||||
wotsLen1 := p.wotsLen1()
|
||||
|
||||
baseW := make([]uint32, wotsLen1)
|
||||
wotsBaseW(baseW, msg)
|
||||
csum := wotsChecksum(p, baseW)
|
||||
|
||||
// Save outer address words so we can restore between PRF and F.
|
||||
keypair := binary.BigEndian.Uint32(addr[20:])
|
||||
|
||||
// PRF (chain-base derivation) address template.
|
||||
prfAddr := *addr
|
||||
prfAddr.setTypeAndClear(adrsTypeWotsPrf)
|
||||
prfAddr.setKeyPairAddress(keypair)
|
||||
|
||||
// F-chain (chain-step) address template.
|
||||
chainAddr := *addr
|
||||
chainAddr.setTypeAndClear(adrsTypeWotsHash)
|
||||
chainAddr.setKeyPairAddress(keypair)
|
||||
|
||||
for i := uint32(0); i < wotsLen1; i++ {
|
||||
prfAddr.setChainAddress(i)
|
||||
sk := make([]byte, p.n)
|
||||
prfOut(sk, &prfAddr)
|
||||
|
||||
chainAddr.setChainAddress(i)
|
||||
wotsChain(out[i*p.n:(i+1)*p.n], pkSeed, &chainAddr, sk, 0, baseW[i])
|
||||
// Best-effort wipe of the PRF-derived chain base; the chain-
|
||||
// step output is what enters the WOTS+ signature publicly.
|
||||
for j := range sk {
|
||||
sk[j] = 0
|
||||
}
|
||||
}
|
||||
for i := uint32(0); i < wotsLen2; i++ {
|
||||
prfAddr.setChainAddress(wotsLen1 + i)
|
||||
sk := make([]byte, p.n)
|
||||
prfOut(sk, &prfAddr)
|
||||
|
||||
chainAddr.setChainAddress(wotsLen1 + i)
|
||||
offset := (wotsLen1 + i) * p.n
|
||||
wotsChain(out[offset:offset+p.n], pkSeed, &chainAddr, sk, 0, csum[i])
|
||||
for j := range sk {
|
||||
sk[j] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wotsPkGen derives the WOTS+ public key (one n-byte node) under the
|
||||
// supplied addr template. FIPS 205 §5.1 Algorithm 6. Used by XMSS leaf
|
||||
// computation.
|
||||
func wotsPkGen(
|
||||
p *internalParams,
|
||||
out []byte,
|
||||
pkSeed []byte,
|
||||
addr *adrsBuf,
|
||||
prfOut prfOutFn,
|
||||
) {
|
||||
wotsLen := p.wotsLen()
|
||||
keypair := binary.BigEndian.Uint32(addr[20:])
|
||||
|
||||
prfAddr := *addr
|
||||
prfAddr.setTypeAndClear(adrsTypeWotsPrf)
|
||||
prfAddr.setKeyPairAddress(keypair)
|
||||
|
||||
chainAddr := *addr
|
||||
chainAddr.setTypeAndClear(adrsTypeWotsHash)
|
||||
chainAddr.setKeyPairAddress(keypair)
|
||||
|
||||
concat := make([]byte, wotsLen*p.n)
|
||||
for i := uint32(0); i < wotsLen; i++ {
|
||||
prfAddr.setChainAddress(i)
|
||||
sk := make([]byte, p.n)
|
||||
prfOut(sk, &prfAddr)
|
||||
|
||||
chainAddr.setChainAddress(i)
|
||||
wotsChain(concat[i*p.n:(i+1)*p.n], pkSeed, &chainAddr, sk, 0, wotsW-1)
|
||||
for j := range sk {
|
||||
sk[j] = 0
|
||||
}
|
||||
}
|
||||
|
||||
tAddr := *addr
|
||||
tAddr.setTypeAndClear(adrsTypeWotsPk)
|
||||
tAddr.setKeyPairAddress(keypair)
|
||||
tlPub(out, pkSeed, &tAddr, concat)
|
||||
for j := range concat {
|
||||
concat[j] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// FIPS 205 §6 — XMSS tree (sign + node compute)
|
||||
// =====================================================================
|
||||
|
||||
// xmssNodeCompute returns the node at (i, z) of the XMSS Merkle tree
|
||||
// for the supplied addr template. FIPS 205 §6.1 Algorithm 9 — iterative
|
||||
// stack-based reduction.
|
||||
func xmssNodeCompute(
|
||||
p *internalParams,
|
||||
out []byte,
|
||||
pkSeed []byte,
|
||||
addr *adrsBuf,
|
||||
i, z uint32,
|
||||
prfOut prfOutFn,
|
||||
) {
|
||||
if z == 0 {
|
||||
// Leaf: WOTS+ public key.
|
||||
a := *addr
|
||||
a.setTypeAndClear(adrsTypeWotsHash)
|
||||
a.setKeyPairAddress(i)
|
||||
wotsPkGen(p, out, pkSeed, &a, prfOut)
|
||||
return
|
||||
}
|
||||
// Internal node: hash left || right children.
|
||||
left := make([]byte, p.n)
|
||||
right := make([]byte, p.n)
|
||||
xmssNodeCompute(p, left, pkSeed, addr, 2*i, z-1, prfOut)
|
||||
xmssNodeCompute(p, right, pkSeed, addr, 2*i+1, z-1, prfOut)
|
||||
a := *addr
|
||||
a.setTypeAndClear(adrsTypeTree)
|
||||
a.setTreeHeight(z)
|
||||
a.setTreeIndex(i)
|
||||
hPub(out, pkSeed, &a, left, right)
|
||||
for j := range left {
|
||||
left[j] = 0
|
||||
}
|
||||
for j := range right {
|
||||
right[j] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// xmssSign produces an XMSS signature (WOTS+ sig + auth path) on msg
|
||||
// under the addr template at leaf idx. FIPS 205 §6.2 Algorithm 10.
|
||||
func xmssSign(
|
||||
p *internalParams,
|
||||
out []byte,
|
||||
pkSeed []byte,
|
||||
addr *adrsBuf,
|
||||
msg []byte,
|
||||
idx uint32,
|
||||
prfOut prfOutFn,
|
||||
) {
|
||||
// Auth path (hPrime sibling nodes).
|
||||
authOffset := p.wotsSigSize()
|
||||
for j := uint32(0); j < p.hPrime; j++ {
|
||||
k := (idx >> j) ^ 1
|
||||
xmssNodeCompute(p, out[authOffset+j*p.n:authOffset+(j+1)*p.n], pkSeed, addr, k, j, prfOut)
|
||||
}
|
||||
// WOTS+ signature on msg at leaf idx.
|
||||
a := *addr
|
||||
a.setTypeAndClear(adrsTypeWotsHash)
|
||||
a.setKeyPairAddress(idx)
|
||||
wotsSign(p, out[:p.wotsSigSize()], pkSeed, &a, msg, prfOut)
|
||||
}
|
||||
|
||||
// xmssPkFromSig reconstructs the XMSS public key (one n-byte node) from
|
||||
// an XMSS signature on msg at leaf idx. FIPS 205 §6.3 Algorithm 11.
|
||||
// Public — only operates on (pkSeed, sig bytes, msg, idx).
|
||||
func xmssPkFromSig(
|
||||
p *internalParams,
|
||||
out []byte,
|
||||
pkSeed []byte,
|
||||
addr *adrsBuf,
|
||||
sig []byte,
|
||||
msg []byte,
|
||||
idx uint32,
|
||||
) {
|
||||
wotsSig := sig[:p.wotsSigSize()]
|
||||
authPath := sig[p.wotsSigSize():]
|
||||
|
||||
a := *addr
|
||||
a.setTypeAndClear(adrsTypeWotsHash)
|
||||
a.setKeyPairAddress(idx)
|
||||
node := make([]byte, p.n)
|
||||
wotsPkFromSig(p, node, pkSeed, &a, wotsSig, msg)
|
||||
|
||||
treeIdx := idx
|
||||
tAddr := *addr
|
||||
tAddr.setTypeAndClear(adrsTypeTree)
|
||||
for k := uint32(0); k < p.hPrime; k++ {
|
||||
tAddr.setTreeHeight(k + 1)
|
||||
if (idx>>k)&1 == 0 {
|
||||
treeIdx >>= 1
|
||||
tAddr.setTreeIndex(treeIdx)
|
||||
hPub(node, pkSeed, &tAddr, node, authPath[k*p.n:(k+1)*p.n])
|
||||
} else {
|
||||
treeIdx = (treeIdx - 1) >> 1
|
||||
tAddr.setTreeIndex(treeIdx)
|
||||
hPub(node, pkSeed, &tAddr, authPath[k*p.n:(k+1)*p.n], node)
|
||||
}
|
||||
}
|
||||
copy(out, node)
|
||||
}
|
||||
|
||||
// wotsPkFromSig reconstructs the WOTS+ public key (one n-byte node)
|
||||
// from a WOTS+ signature on msg. FIPS 205 §5.3 Algorithm 8. Public.
|
||||
func wotsPkFromSig(
|
||||
p *internalParams,
|
||||
out []byte,
|
||||
pkSeed []byte,
|
||||
addr *adrsBuf,
|
||||
sig []byte,
|
||||
msg []byte,
|
||||
) {
|
||||
wotsLen1 := p.wotsLen1()
|
||||
baseW := make([]uint32, wotsLen1)
|
||||
wotsBaseW(baseW, msg)
|
||||
csum := wotsChecksum(p, baseW)
|
||||
|
||||
keypair := binary.BigEndian.Uint32(addr[20:])
|
||||
chainAddr := *addr
|
||||
chainAddr.setTypeAndClear(adrsTypeWotsHash)
|
||||
chainAddr.setKeyPairAddress(keypair)
|
||||
|
||||
concat := make([]byte, p.wotsLen()*p.n)
|
||||
for i := uint32(0); i < wotsLen1; i++ {
|
||||
chainAddr.setChainAddress(i)
|
||||
wotsChain(concat[i*p.n:(i+1)*p.n], pkSeed, &chainAddr, sig[i*p.n:(i+1)*p.n], baseW[i], wotsW-1-baseW[i])
|
||||
}
|
||||
for i := uint32(0); i < wotsLen2; i++ {
|
||||
chainAddr.setChainAddress(wotsLen1 + i)
|
||||
offset := (wotsLen1 + i) * p.n
|
||||
wotsChain(concat[offset:offset+p.n], pkSeed, &chainAddr, sig[offset:offset+p.n], csum[i], wotsW-1-csum[i])
|
||||
}
|
||||
tAddr := *addr
|
||||
tAddr.setTypeAndClear(adrsTypeWotsPk)
|
||||
tAddr.setKeyPairAddress(keypair)
|
||||
tlPub(out, pkSeed, &tAddr, concat)
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// FIPS 205 §7 — Hypertree (sign + verify)
|
||||
// =====================================================================
|
||||
|
||||
// htSign produces a hypertree signature: d XMSS layers each signing
|
||||
// the previous root (or pkFors at layer 0). FIPS 205 §7.1 Algorithm 12.
|
||||
func htSign(
|
||||
p *internalParams,
|
||||
out []byte,
|
||||
pkSeed []byte,
|
||||
pkFors []byte,
|
||||
idxTree [3]uint32,
|
||||
idxLeaf uint32,
|
||||
prfOut prfOutFn,
|
||||
) {
|
||||
addr := adrsBuf{}
|
||||
addr.setLayerAddress(0)
|
||||
addr.setTreeAddress(idxTree)
|
||||
|
||||
msg := make([]byte, p.n)
|
||||
copy(msg, pkFors)
|
||||
|
||||
for j := uint32(0); j < p.d; j++ {
|
||||
xmssSig := out[j*p.xmssSigSize() : (j+1)*p.xmssSigSize()]
|
||||
xmssSign(p, xmssSig, pkSeed, &addr, msg, idxLeaf, prfOut)
|
||||
if j < p.d-1 {
|
||||
xmssPkFromSig(p, msg, pkSeed, &addr, xmssSig, msg, idxLeaf)
|
||||
idxLeaf = nextHTIndex(&idxTree, p.hPrime)
|
||||
addr.setLayerAddress(j + 1)
|
||||
addr.setTreeAddress(idxTree)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// htRootCompute computes the hypertree root (pkRoot) given pkSeed and
|
||||
// the PRF callback. Used at setup to derive the public key root from
|
||||
// the same Lagrange-reconstructed material the signer uses. FIPS 205
|
||||
// §9.1 Algorithm 18 (line 5).
|
||||
func htRootCompute(
|
||||
p *internalParams,
|
||||
out []byte,
|
||||
pkSeed []byte,
|
||||
prfOut prfOutFn,
|
||||
) {
|
||||
addr := adrsBuf{}
|
||||
addr.setLayerAddress(p.d - 1)
|
||||
addr.setTreeAddress([3]uint32{0, 0, 0})
|
||||
xmssNodeCompute(p, out, pkSeed, &addr, 0, p.hPrime, prfOut)
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// FIPS 205 §8 — FORS sign + public-key reconstruction
|
||||
// =====================================================================
|
||||
|
||||
// forsNodeCompute returns the node at (i, z) of the i-th FORS subtree
|
||||
// rooted in addr.keyPairAddress. FIPS 205 §8.2 Algorithm 15.
|
||||
func forsNodeCompute(
|
||||
p *internalParams,
|
||||
out []byte,
|
||||
pkSeed []byte,
|
||||
addr *adrsBuf,
|
||||
i, z uint32,
|
||||
prfOut prfOutFn,
|
||||
) {
|
||||
if z == 0 {
|
||||
// Leaf: F(PK.seed, ADRS, PRF(PK.seed, ADRS_prf, SK.seed))
|
||||
prfAddr := *addr
|
||||
prfAddr.setTypeAndClear(adrsTypeForsPrf)
|
||||
prfAddr.setKeyPairAddress(binary.BigEndian.Uint32(addr[20:]))
|
||||
prfAddr.setTreeIndex(i)
|
||||
sk := make([]byte, p.n)
|
||||
prfOut(sk, &prfAddr)
|
||||
|
||||
fAddr := *addr
|
||||
fAddr.setTreeHeight(0)
|
||||
fAddr.setTreeIndex(i)
|
||||
fPub(out, pkSeed, &fAddr, sk)
|
||||
for j := range sk {
|
||||
sk[j] = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
left := make([]byte, p.n)
|
||||
right := make([]byte, p.n)
|
||||
forsNodeCompute(p, left, pkSeed, addr, 2*i, z-1, prfOut)
|
||||
forsNodeCompute(p, right, pkSeed, addr, 2*i+1, z-1, prfOut)
|
||||
a := *addr
|
||||
a.setTreeHeight(z)
|
||||
a.setTreeIndex(i)
|
||||
hPub(out, pkSeed, &a, left, right)
|
||||
for j := range left {
|
||||
left[j] = 0
|
||||
}
|
||||
for j := range right {
|
||||
right[j] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// forsSign produces a FORS signature (k pairs of (sk, auth path))
|
||||
// over the digest md under the supplied addr template. FIPS 205 §8.3
|
||||
// Algorithm 16.
|
||||
func forsSign(
|
||||
p *internalParams,
|
||||
out []byte,
|
||||
pkSeed []byte,
|
||||
addr *adrsBuf,
|
||||
digest []byte,
|
||||
prfOut prfOutFn,
|
||||
) {
|
||||
pairSize := p.forsPairSize()
|
||||
in, bits, total := 0, uint32(0), uint32(0)
|
||||
maskA := (uint32(1) << p.a) - 1
|
||||
|
||||
for i := uint32(0); i < p.k; i++ {
|
||||
for bits < p.a {
|
||||
total = (total << 8) + uint32(digest[in])
|
||||
in++
|
||||
bits += 8
|
||||
}
|
||||
bits -= p.a
|
||||
indicesI := (total >> bits) & maskA
|
||||
|
||||
// sk = PRF(PK.seed, ADRS_forsPrf, SK.seed) at tree index (i << a) + indicesI
|
||||
treeIdx := (i << p.a) + indicesI
|
||||
prfAddr := *addr
|
||||
prfAddr.setTypeAndClear(adrsTypeForsPrf)
|
||||
prfAddr.setKeyPairAddress(binary.BigEndian.Uint32(addr[20:]))
|
||||
prfAddr.setTreeIndex(treeIdx)
|
||||
|
||||
skOffset := i * pairSize
|
||||
prfOut(out[skOffset:skOffset+p.n], &prfAddr)
|
||||
|
||||
// Auth path: a sibling nodes at heights 0..a-1.
|
||||
authOffset := skOffset + p.n
|
||||
for j := uint32(0); j < p.a; j++ {
|
||||
s := (indicesI >> j) ^ 1
|
||||
nodeIdx := (i << (p.a - j)) + s
|
||||
forsNodeCompute(p, out[authOffset+j*p.n:authOffset+(j+1)*p.n], pkSeed, addr, nodeIdx, j, prfOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// forsPkFromSig reconstructs the FORS public key (one n-byte T_k root)
|
||||
// from the FORS signature + message digest md. FIPS 205 §8.4 Algorithm
|
||||
// 17. Public.
|
||||
func forsPkFromSig(
|
||||
p *internalParams,
|
||||
out []byte,
|
||||
pkSeed []byte,
|
||||
addr *adrsBuf,
|
||||
digest []byte,
|
||||
sig []byte,
|
||||
) {
|
||||
pairSize := p.forsPairSize()
|
||||
in, bits, total := 0, uint32(0), uint32(0)
|
||||
maskA := (uint32(1) << p.a) - 1
|
||||
|
||||
rootsConcat := make([]byte, p.k*p.n)
|
||||
|
||||
for i := uint32(0); i < p.k; i++ {
|
||||
for bits < p.a {
|
||||
total = (total << 8) + uint32(digest[in])
|
||||
in++
|
||||
bits += 8
|
||||
}
|
||||
bits -= p.a
|
||||
indicesI := (total >> bits) & maskA
|
||||
|
||||
treeIdx := (i << p.a) + indicesI
|
||||
skOffset := i * pairSize
|
||||
authOffset := skOffset + p.n
|
||||
|
||||
fAddr := *addr
|
||||
fAddr.setTreeHeight(0)
|
||||
fAddr.setTreeIndex(treeIdx)
|
||||
node := make([]byte, p.n)
|
||||
fPub(node, pkSeed, &fAddr, sig[skOffset:skOffset+p.n])
|
||||
|
||||
for j := uint32(0); j < p.a; j++ {
|
||||
hAddr := *addr
|
||||
hAddr.setTreeHeight(j + 1)
|
||||
if (indicesI>>j)&1 == 0 {
|
||||
treeIdx >>= 1
|
||||
hAddr.setTreeIndex(treeIdx)
|
||||
hPub(node, pkSeed, &hAddr, node, sig[authOffset+j*p.n:authOffset+(j+1)*p.n])
|
||||
} else {
|
||||
treeIdx = (treeIdx - 1) >> 1
|
||||
hAddr.setTreeIndex(treeIdx)
|
||||
hPub(node, pkSeed, &hAddr, sig[authOffset+j*p.n:authOffset+(j+1)*p.n], node)
|
||||
}
|
||||
}
|
||||
copy(rootsConcat[i*p.n:(i+1)*p.n], node)
|
||||
}
|
||||
|
||||
tAddr := *addr
|
||||
tAddr.setTypeAndClear(adrsTypeForsRoots)
|
||||
tAddr.setKeyPairAddress(binary.BigEndian.Uint32(addr[20:]))
|
||||
tlPub(out, pkSeed, &tAddr, rootsConcat)
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// FIPS 205 §9 / §10 — top-level Sign (deterministic, ctx-binding)
|
||||
// =====================================================================
|
||||
|
||||
// slhSignAtom is the top-level Magnetar-internal SLH-DSA Sign. It
|
||||
// implements FIPS 205 §10.2 Algorithm 23 (Sign) ∘ §9.2 Algorithm 19
|
||||
// (slh_sign_internal) for SHAKE deterministic signing.
|
||||
//
|
||||
// Inputs:
|
||||
// - p: the parameter set
|
||||
// - pkSeed: the public seed (n bytes)
|
||||
// - pkRoot: the public root (n bytes)
|
||||
// - message: the user message bytes
|
||||
// - ctx: the FIPS 205 §10.2 context string (<= 255 bytes)
|
||||
// - prfOut: the secret-side PRF (FIPS 205 §11.2) — the only seam
|
||||
// for SK.seed access
|
||||
// - prfMsg: the message-randomizer PRF — the only seam for SK.prf
|
||||
// access
|
||||
//
|
||||
// Output: the signatureSize-byte FIPS 205 wire signature.
|
||||
func slhSignAtom(
|
||||
p *internalParams,
|
||||
pkSeed, pkRoot []byte,
|
||||
message, ctx []byte,
|
||||
prfOut prfOutFn,
|
||||
prfMsg prfMsgFn,
|
||||
) ([]byte, error) {
|
||||
if len(ctx) > 255 {
|
||||
return nil, ErrCtxTooLong
|
||||
}
|
||||
|
||||
// FIPS 205 §10.2 Algorithm 23 — bind ctx to msgPrime.
|
||||
msgPrime := make([]byte, 0, 2+len(ctx)+len(message))
|
||||
msgPrime = append(msgPrime, 0) // domain separator: pure
|
||||
msgPrime = append(msgPrime, byte(len(ctx))) // ctx length
|
||||
msgPrime = append(msgPrime, ctx...)
|
||||
msgPrime = append(msgPrime, message...)
|
||||
|
||||
sigBytes := make([]byte, p.signatureSize())
|
||||
|
||||
// Step 1: randomizer R = PRF_msg(SK.prf, addRand=pkSeed, msgPrime).
|
||||
// SignDeterministic uses addRand = pkSeed per FIPS 205 §10.2 line 5.
|
||||
rBuf := sigBytes[:p.n]
|
||||
prfMsg(rBuf, pkSeed, msgPrime)
|
||||
|
||||
// Step 2: digest = H_msg(R, PK.seed, PK.root, M').
|
||||
digest := make([]byte, p.m)
|
||||
hMsgPub(digest, rBuf, pkSeed, pkRoot, msgPrime)
|
||||
|
||||
md, idxTree, idxLeaf := parseDigest(p, digest)
|
||||
|
||||
// Step 3: FORS sign on md at (idxTree, idxLeaf).
|
||||
addr := adrsBuf{}
|
||||
addr.setLayerAddress(0)
|
||||
addr.setTreeAddress(idxTree)
|
||||
addr.setTypeAndClear(adrsTypeForsTree)
|
||||
addr.setKeyPairAddress(idxLeaf)
|
||||
|
||||
forsOffset := p.n
|
||||
forsEnd := forsOffset + p.forsSigSize()
|
||||
forsSign(p, sigBytes[forsOffset:forsEnd], pkSeed, &addr, md, prfOut)
|
||||
|
||||
// Step 4: pkFors = forsPkFromSig (deterministic from FORS sig + md).
|
||||
pkFors := make([]byte, p.n)
|
||||
forsPkFromSig(p, pkFors, pkSeed, &addr, md, sigBytes[forsOffset:forsEnd])
|
||||
|
||||
// Step 5: htSign on pkFors at (idxTree, idxLeaf).
|
||||
htSign(p, sigBytes[forsEnd:], pkSeed, pkFors, idxTree, idxLeaf, prfOut)
|
||||
|
||||
return sigBytes, nil
|
||||
}
|
||||
Reference in New Issue
Block a user