mirror of
https://github.com/luxfi/magnetar.git
synced 2026-07-27 02:53:47 +00:00
magnetar v0.5.1 wire codec: MAGS/MAGG + stateless VerifyBytes
Canonical wire codec for Signature and PublicKey (group public key on
the wire) plus stateless VerifyBytes / VerifyBytesCtx, mirroring the
pulsar PULS/PULG and corona CORS/CORG patterns. Closes the audit gap
that left magnetar without a stable wire surface that independent
verifiers (other mpcd, bridge nodes, L1 verifier contracts) could
consume.
Wire frame layout (big-endian throughout):
Signature: 'M' 'A' 'G' 'S' || ver(2) || mode(1) || len(4) || FIPS-205 sig
GroupKey: 'M' 'A' 'G' 'G' || ver(2) || mode(1) || len(4) || FIPS-205 pk
Magic distinct from PULS/PULG (0x50554C53/47) and CORS/CORG
(0x434F5253/47). 11-byte fixed header. Bounded length-prefix:
declared length is pinned to the canonical FIPS 205 size for the mode
BEFORE allocation, so a malformed header can never trigger an
oversized allocation. Trailing-bytes policy is STRICT — exactly one
well-formed encoding of any (mode, bytes) pair exists.
Class N1 analog property: the MAGS payload IS the FIPS 205 sigEncode
output verbatim; the MAGG payload IS the FIPS 205 (PK.seed || PK.root)
bytes verbatim. Stripping the 11-byte header recovers bytes that
cloudflare/circl's slhdsa.Verify accepts with no magnetar code path
on the verifier side. TestMagnetar_Wire_FIPS205Verifiable pins this
across all three magnetar modes (SHAKE-192s / -192f / -256s) by
signing via the v0.5 per-validator standalone primary primitive
(PerValidatorKeypair + ValidatorSign with rng=nil → FIPS 205
SignDeterministic) and verifying via cloudflare/circl directly.
Tests added (wire_test.go, 47 sub-cases total):
- TestMagnetar_Wire_Roundtrip — byte-equal sig roundtrip x3 modes
- TestMagnetar_Wire_GroupKeyRoundtrip — symmetric pk roundtrip
- TestMagnetar_Wire_FIPS205Verifiable — HEADLINE byte-identity claim
- TestMagnetar_Wire_RejectMalformed — 12 negative paths including
cross-protocol PULS / CORS magic rejection
- TestMagnetar_Wire_RejectTrailingBytes — strict canonical
- TestMagnetar_Wire_GroupKeyRejectMalformed — symmetric GK negatives
including cross-protocol PULG / CORG magic rejection
- TestMagnetar_Wire_RejectCrossWire — feeds full-shape pulsar/corona
frames into magnetar parser, asserts ErrWireMagicMismatch
- TestMagnetar_Wire_VerifyBytes_RejectsCrossSlot — sig-slot vs
gk-slot domain separation in the stateless verifier
- TestMagnetar_Wire_ModeMismatch_Rejected — gk Mode != sig Mode
must produce false (M256s gk + M192s sig combination)
- TestMagnetar_Wire_MarshalSafetyChecks — nil + bad-length emit
- TestMagnetar_Wire_VerifyBytesCtx_RoundTrip — ctx-aware path used
by the EVM precompile slot binding
Suite is GREEN under -count=1 (10.5 s) and -race (4 min). No
backwards compatibility — wire format is canonical from v0.5.1
forward. Version-bump rule documented in wire.go header.
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package magnetar
|
||||
|
||||
// wire.go — canonical wire codec for Signature and PublicKey (the
|
||||
// group public key, in a MAGG frame) plus a stateless VerifyBytes
|
||||
// helper that any independent peer can call given only:
|
||||
//
|
||||
// - the wire bytes of the group public key (MAGG-framed),
|
||||
// - the bytes of the message that was signed,
|
||||
// - the wire bytes of the signature (MAGS-framed),
|
||||
//
|
||||
// and obtain a bool answer with no additional state. This is the
|
||||
// surface luxfi/threshold/pkg/thresholdd consumes to publish
|
||||
// Magnetar outputs over a JSON-RPC bus — and the surface independent
|
||||
// verifiers (other mpcd, bridge nodes, L1 verifier contracts) must
|
||||
// satisfy.
|
||||
//
|
||||
// The Class N1 analog of Magnetar (see verify.go, README §"FIPS 205
|
||||
// byte-identity") is that a Magnetar Signature is bit-identical to a
|
||||
// single-party FIPS 205 SLH-DSA signature on the same (message,
|
||||
// group public key) tuple. The wire codec preserves this property
|
||||
// by transporting the FIPS 205 bytes verbatim inside a small
|
||||
// domain-separated frame. A relying party that strips the MAGS /
|
||||
// MAGG frame obtains the FIPS 205 wire bytes that
|
||||
// cloudflare/circl's slhdsa.Verify accepts unmodified — this is
|
||||
// what TestMagnetar_Wire_FIPS205Verifiable asserts.
|
||||
//
|
||||
// Wire frame layout (big-endian throughout):
|
||||
//
|
||||
// Signature:
|
||||
// magic(4) = 'M' 'A' 'G' 'S' = 0x4D414753
|
||||
// version(2) = 0x0001
|
||||
// mode(1) = 0x01 | 0x02 | 0x03 (M192s | M192f | M256s)
|
||||
// len(4) = FIPS 205 SignatureSize for mode
|
||||
// (16224 | 35664 | 29792)
|
||||
// payload = len bytes — the FIPS 205 sigEncode output
|
||||
//
|
||||
// GroupKey:
|
||||
// magic(4) = 'M' 'A' 'G' 'G' = 0x4D414747
|
||||
// version(2) = 0x0001
|
||||
// mode(1) = 0x01 | 0x02 | 0x03
|
||||
// len(4) = FIPS 205 PublicKeySize for mode (48 | 48 | 64)
|
||||
// payload = len bytes — the FIPS 205 (PK.seed || PK.root) bytes
|
||||
//
|
||||
// Domain separation: the four-byte magic is distinct from pulsar's
|
||||
// PULS / PULG (0x50554C53 / 0x50554C47) and corona's CORS / CORG
|
||||
// (0x434F5253 / 0x434F5247), so a frame from any sibling protocol
|
||||
// fed into the magnetar parser is rejected at the first dispatch
|
||||
// and vice versa. Distinct magic per type within magnetar means a
|
||||
// GroupKey frame fed into the signature slot is rejected before any
|
||||
// length-decode is attempted.
|
||||
//
|
||||
// Bounded reads: every length-prefix read is checked against the
|
||||
// remaining bytes in the reader BEFORE any allocation. Beyond that,
|
||||
// the wire codec also pins the declared FIPS 205 length to the
|
||||
// canonical value for the Mode — a malformed frame that claims a
|
||||
// 4 GiB signature for ModeM192s is rejected without allocating any
|
||||
// buffer. Any wire-format change (additional field, version flip)
|
||||
// bumps wireVersionV1.
|
||||
//
|
||||
// Trailing garbage policy: STRICT — UnmarshalBinary returns an
|
||||
// error if any byte remains after the declared frame. Mirrors
|
||||
// pulsar/wire.go and corona/wire.go.
|
||||
//
|
||||
// Version-bump rule: any byte-level change to the layout (new
|
||||
// field, reordered fields, bigger length prefix, additional FIPS
|
||||
// 205 mode beyond the magnetar canonical set) requires bumping
|
||||
// wireVersionV1. The Mode byte is independent — adding ModeM192s /
|
||||
// ModeM192f / ModeM256s alternates does NOT bump the version
|
||||
// because Mode is a structural payload selector, not a format
|
||||
// change.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudflare/circl/sign/slhdsa"
|
||||
)
|
||||
|
||||
// Wire-format identifiers. Distinct from pulsar's PULS / PULG and
|
||||
// corona's CORS / CORG so the parsers cannot cross-accept and a
|
||||
// passive observer can tell from the first four bytes which
|
||||
// protocol the payload belongs to.
|
||||
const (
|
||||
wireMagicMagnetarSig uint32 = 0x4D414753 // "MAGS" Magnetar Signature
|
||||
wireMagicMagnetarGroupKey uint32 = 0x4D414747 // "MAGG" Magnetar GroupKey
|
||||
wireVersionV1 uint16 = 1
|
||||
)
|
||||
|
||||
// Errors returned by the wire codec. Typed for errors.Is matching
|
||||
// in callers (the dispatcher, downstream relying parties, fuzzers).
|
||||
var (
|
||||
ErrWireMagicMismatch = errors.New("magnetar/wire: magic mismatch")
|
||||
ErrWireVersionMismatch = errors.New("magnetar/wire: version mismatch")
|
||||
ErrWireFrameTooShort = errors.New("magnetar/wire: frame too short")
|
||||
ErrWireFrameRejected = errors.New("magnetar/wire: frame rejected by bounded reader")
|
||||
ErrWireModeUnknown = errors.New("magnetar/wire: unknown mode byte")
|
||||
ErrWireLengthMismatch = errors.New("magnetar/wire: declared length does not match FIPS 205 size for mode")
|
||||
ErrWireTrailingBytes = errors.New("magnetar/wire: trailing bytes after frame")
|
||||
)
|
||||
|
||||
// wireFixedSigHeader is magic(4) + version(2) + mode(1) + len(4).
|
||||
const wireFixedSigHeader = 4 + 2 + 1 + 4
|
||||
|
||||
// wireFixedGroupKeyHeader has the same shape as wireFixedSigHeader.
|
||||
const wireFixedGroupKeyHeader = wireFixedSigHeader
|
||||
|
||||
// MarshalBinary serialises a Signature into the canonical MAGS frame.
|
||||
//
|
||||
// The body bytes are the FIPS 205 sigEncode output verbatim —
|
||||
// Magnetar adds NO envelope around them. Stripping the 11-byte
|
||||
// header recovers the unmodified FIPS 205 bytes that
|
||||
// cloudflare/circl's slhdsa.Verify accepts. This is the
|
||||
// load-bearing property TestMagnetar_Wire_FIPS205Verifiable pins.
|
||||
//
|
||||
// Returns an error if the Signature's Mode is unknown, its Bytes
|
||||
// field does not match the FIPS 205 size for the Mode, or the
|
||||
// Signature receiver is nil.
|
||||
func (s *Signature) MarshalBinary() ([]byte, error) {
|
||||
if s == nil {
|
||||
return nil, errors.New("magnetar: nil Signature")
|
||||
}
|
||||
sigSize, err := wireSigSizeForMode(s.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("magnetar/wire: %w", err)
|
||||
}
|
||||
if len(s.Bytes) != sigSize {
|
||||
return nil, fmt.Errorf("magnetar/wire: Signature.Bytes length %d != FIPS 205 size %d for %s",
|
||||
len(s.Bytes), sigSize, s.Mode)
|
||||
}
|
||||
|
||||
out := make([]byte, 0, wireFixedSigHeader+sigSize)
|
||||
out = binary.BigEndian.AppendUint32(out, wireMagicMagnetarSig)
|
||||
out = binary.BigEndian.AppendUint16(out, wireVersionV1)
|
||||
out = append(out, byte(s.Mode))
|
||||
out = binary.BigEndian.AppendUint32(out, uint32(sigSize))
|
||||
out = append(out, s.Bytes...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary parses a Signature from canonical MAGS frame.
|
||||
//
|
||||
// Validation order is strict: magic, version, mode, length-against-
|
||||
// FIPS 205-size are all checked BEFORE any payload bytes are read.
|
||||
// The codec never allocates a payload buffer larger than the canonical
|
||||
// FIPS 205 signature size for the declared mode, so a malformed length
|
||||
// header cannot trigger an oversized allocation.
|
||||
//
|
||||
// Trailing bytes after the frame are rejected (ErrWireTrailingBytes)
|
||||
// to keep the format strictly canonical — there is exactly one
|
||||
// well-formed encoding of any (mode, bytes) pair.
|
||||
func (s *Signature) UnmarshalBinary(b []byte) error {
|
||||
if s == nil {
|
||||
return errors.New("magnetar: nil Signature receiver")
|
||||
}
|
||||
if len(b) < wireFixedSigHeader {
|
||||
return ErrWireFrameTooShort
|
||||
}
|
||||
r := bytes.NewReader(b)
|
||||
|
||||
var magic uint32
|
||||
if err := binary.Read(r, binary.BigEndian, &magic); err != nil {
|
||||
return fmt.Errorf("magnetar/wire: read magic: %w", err)
|
||||
}
|
||||
if magic != wireMagicMagnetarSig {
|
||||
return fmt.Errorf("%w: got 0x%08x, want 0x%08x", ErrWireMagicMismatch, magic, wireMagicMagnetarSig)
|
||||
}
|
||||
|
||||
var version uint16
|
||||
if err := binary.Read(r, binary.BigEndian, &version); err != nil {
|
||||
return fmt.Errorf("magnetar/wire: read version: %w", err)
|
||||
}
|
||||
if version != wireVersionV1 {
|
||||
return fmt.Errorf("%w: got %d, want %d", ErrWireVersionMismatch, version, wireVersionV1)
|
||||
}
|
||||
|
||||
modeByte, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return fmt.Errorf("magnetar/wire: read mode: %w", err)
|
||||
}
|
||||
mode := Mode(modeByte)
|
||||
sigSize, err := wireSigSizeForMode(mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrWireModeUnknown, err)
|
||||
}
|
||||
|
||||
var declared uint32
|
||||
if err := binary.Read(r, binary.BigEndian, &declared); err != nil {
|
||||
return fmt.Errorf("magnetar/wire: read length: %w", err)
|
||||
}
|
||||
// Bounded read: declared length must equal the canonical FIPS 205
|
||||
// size for the mode AND must not exceed the remaining buffer.
|
||||
// Both checks happen BEFORE the payload allocation.
|
||||
if int(declared) != sigSize {
|
||||
return fmt.Errorf("%w: declared %d, FIPS 205 size for %s is %d",
|
||||
ErrWireLengthMismatch, declared, mode, sigSize)
|
||||
}
|
||||
if int(declared) > r.Len() {
|
||||
return fmt.Errorf("%w: declared length %d exceeds remaining %d",
|
||||
ErrWireFrameRejected, declared, r.Len())
|
||||
}
|
||||
|
||||
payload := make([]byte, declared)
|
||||
if _, err := r.Read(payload); err != nil {
|
||||
return fmt.Errorf("magnetar/wire: read payload: %w", err)
|
||||
}
|
||||
if r.Len() != 0 {
|
||||
return fmt.Errorf("%w: %d bytes remaining", ErrWireTrailingBytes, r.Len())
|
||||
}
|
||||
|
||||
s.Mode = mode
|
||||
s.Bytes = payload
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalGroupKey serialises a PublicKey (used on the wire as the
|
||||
// group public key) into the canonical MAGG frame.
|
||||
//
|
||||
// The body bytes are the FIPS 205 public-key encoding (PK.seed ||
|
||||
// PK.root, per §10.1) verbatim — Magnetar adds NO envelope around
|
||||
// them. Stripping the 11-byte header recovers the unmodified FIPS
|
||||
// 205 public-key bytes that cloudflare/circl's
|
||||
// slhdsa.PublicKey.UnmarshalBinary accepts.
|
||||
//
|
||||
// A method on *PublicKey would collide with potential SDK-side
|
||||
// MarshalBinary on the same type, so the canonical group-key wire
|
||||
// emitter is a package-level function. This mirrors how the
|
||||
// distinction between PublicKey-as-validator-pk and
|
||||
// PublicKey-as-group-pk is documentary only (types.go: GroupKey =
|
||||
// PublicKey alias) — the wire emitter encodes the intent.
|
||||
//
|
||||
// Returns an error if the PublicKey's Mode is unknown, its Bytes
|
||||
// field does not match the FIPS 205 size for the Mode, or the
|
||||
// PublicKey is nil.
|
||||
func MarshalGroupKey(p *PublicKey) ([]byte, error) {
|
||||
if p == nil {
|
||||
return nil, errors.New("magnetar: nil GroupKey")
|
||||
}
|
||||
pkSize, err := wirePubKeySizeForMode(p.Mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("magnetar/wire: %w", err)
|
||||
}
|
||||
if len(p.Bytes) != pkSize {
|
||||
return nil, fmt.Errorf("magnetar/wire: GroupKey.Bytes length %d != FIPS 205 size %d for %s",
|
||||
len(p.Bytes), pkSize, p.Mode)
|
||||
}
|
||||
|
||||
out := make([]byte, 0, wireFixedGroupKeyHeader+pkSize)
|
||||
out = binary.BigEndian.AppendUint32(out, wireMagicMagnetarGroupKey)
|
||||
out = binary.BigEndian.AppendUint16(out, wireVersionV1)
|
||||
out = append(out, byte(p.Mode))
|
||||
out = binary.BigEndian.AppendUint32(out, uint32(pkSize))
|
||||
out = append(out, p.Bytes...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UnmarshalGroupKey parses a PublicKey from canonical MAGG frame.
|
||||
//
|
||||
// Validation order matches Signature.UnmarshalBinary: magic,
|
||||
// version, mode, length-against-FIPS 205-size are all checked
|
||||
// BEFORE any payload bytes are read. Trailing bytes after the
|
||||
// frame are rejected.
|
||||
func UnmarshalGroupKey(b []byte) (*PublicKey, error) {
|
||||
if len(b) < wireFixedGroupKeyHeader {
|
||||
return nil, ErrWireFrameTooShort
|
||||
}
|
||||
r := bytes.NewReader(b)
|
||||
|
||||
var magic uint32
|
||||
if err := binary.Read(r, binary.BigEndian, &magic); err != nil {
|
||||
return nil, fmt.Errorf("magnetar/wire: read magic: %w", err)
|
||||
}
|
||||
if magic != wireMagicMagnetarGroupKey {
|
||||
return nil, fmt.Errorf("%w: got 0x%08x, want 0x%08x", ErrWireMagicMismatch, magic, wireMagicMagnetarGroupKey)
|
||||
}
|
||||
|
||||
var version uint16
|
||||
if err := binary.Read(r, binary.BigEndian, &version); err != nil {
|
||||
return nil, fmt.Errorf("magnetar/wire: read version: %w", err)
|
||||
}
|
||||
if version != wireVersionV1 {
|
||||
return nil, fmt.Errorf("%w: got %d, want %d", ErrWireVersionMismatch, version, wireVersionV1)
|
||||
}
|
||||
|
||||
modeByte, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("magnetar/wire: read mode: %w", err)
|
||||
}
|
||||
mode := Mode(modeByte)
|
||||
pkSize, err := wirePubKeySizeForMode(mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrWireModeUnknown, err)
|
||||
}
|
||||
|
||||
var declared uint32
|
||||
if err := binary.Read(r, binary.BigEndian, &declared); err != nil {
|
||||
return nil, fmt.Errorf("magnetar/wire: read length: %w", err)
|
||||
}
|
||||
if int(declared) != pkSize {
|
||||
return nil, fmt.Errorf("%w: declared %d, FIPS 205 size for %s is %d",
|
||||
ErrWireLengthMismatch, declared, mode, pkSize)
|
||||
}
|
||||
if int(declared) > r.Len() {
|
||||
return nil, fmt.Errorf("%w: declared length %d exceeds remaining %d",
|
||||
ErrWireFrameRejected, declared, r.Len())
|
||||
}
|
||||
|
||||
payload := make([]byte, declared)
|
||||
if _, err := r.Read(payload); err != nil {
|
||||
return nil, fmt.Errorf("magnetar/wire: read payload: %w", err)
|
||||
}
|
||||
if r.Len() != 0 {
|
||||
return nil, fmt.Errorf("%w: %d bytes remaining", ErrWireTrailingBytes, r.Len())
|
||||
}
|
||||
|
||||
return &PublicKey{Mode: mode, Bytes: payload}, nil
|
||||
}
|
||||
|
||||
// VerifyBytes is the stateless verifier the threshold orchestration
|
||||
// layer (luxfi/threshold/pkg/thresholdd) needs to publish a
|
||||
// signature over a JSON-RPC bus: it accepts the canonical wire
|
||||
// bytes of a GroupKey (MAGG frame) and a Signature (MAGS frame)
|
||||
// plus the message that was signed, and returns true iff the
|
||||
// signature verifies under the group key.
|
||||
//
|
||||
// Rejection of any malformed input returns false (NOT an error) —
|
||||
// the dispatcher distinguishes "no valid signature" from
|
||||
// "infrastructure error" via its JSON-RPC envelope; bytes-in,
|
||||
// bool-out keeps this helper pure. The signature MUST verify under
|
||||
// the FIPS 205 verifier verbatim (no Magnetar envelope) — this
|
||||
// matches the Class N1 analog.
|
||||
//
|
||||
// Mode mismatch between the parsed Signature and parsed GroupKey
|
||||
// is treated as verification failure (returns false). This keeps
|
||||
// the helper allocation-free of any cross-mode dispatch surprises.
|
||||
//
|
||||
// Empty context: VerifyBytes calls FIPS 205 Verify with an empty
|
||||
// context string. Callers needing the FIPS 205 §10.3 context
|
||||
// binding (e.g. the EVM precompile's
|
||||
// "lux-evm-precompile-magnetar-v1" tag) must use VerifyBytesCtx.
|
||||
// The unbound helper stays narrow so the dispatcher remains a thin
|
||||
// proxy.
|
||||
func VerifyBytes(gpkBytes, message, sigBytes []byte) bool {
|
||||
return VerifyBytesCtx(gpkBytes, message, nil, sigBytes)
|
||||
}
|
||||
|
||||
// VerifyBytesCtx is the context-aware variant of VerifyBytes. The
|
||||
// FIPS 205 context string ctx (≤255 bytes) is included in the
|
||||
// verification transcript per FIPS 205 §10.3. Callers MUST pass the
|
||||
// SAME ctx that was supplied at Sign time; a different ctx (or nil
|
||||
// where the signer used non-nil) returns false.
|
||||
//
|
||||
// This is the path the on-chain precompile and any contextual
|
||||
// verifier (consensus block-binding tags, validator-set epoch tags)
|
||||
// take.
|
||||
func VerifyBytesCtx(gpkBytes, message, ctx, sigBytes []byte) bool {
|
||||
gk, err := UnmarshalGroupKey(gpkBytes)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var sig Signature
|
||||
if err := sig.UnmarshalBinary(sigBytes); err != nil {
|
||||
return false
|
||||
}
|
||||
if gk.Mode != sig.Mode {
|
||||
return false
|
||||
}
|
||||
if len(ctx) > 255 {
|
||||
return false
|
||||
}
|
||||
return slhVerify(slhdsaIDForMode(gk.Mode), gk.Bytes, message, ctx, sig.Bytes)
|
||||
}
|
||||
|
||||
// slhdsaIDForMode returns the circl/slhdsa.ID for a magnetar Mode.
|
||||
// Centralised here so the wire codec does not transitively depend
|
||||
// on the params.go table — the codec carries its own canonical
|
||||
// mapping so a corrupted Params can never confuse the parser.
|
||||
func slhdsaIDForMode(mode Mode) slhdsa.ID {
|
||||
switch mode {
|
||||
case ModeM192s:
|
||||
return slhdsa.SHAKE_192s
|
||||
case ModeM192f:
|
||||
return slhdsa.SHAKE_192f
|
||||
case ModeM256s:
|
||||
return slhdsa.SHAKE_256s
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// wireSigSizeForMode returns the FIPS 205 signature size for the
|
||||
// declared mode, or an error if the mode is unrecognised.
|
||||
// Centralising this here means the wire codec does not depend on
|
||||
// params.go's table — the codec carries its own canonical numbers
|
||||
// so a corrupted Params can never confuse the parser.
|
||||
func wireSigSizeForMode(mode Mode) (int, error) {
|
||||
switch mode {
|
||||
case ModeM192s:
|
||||
return 16224, nil
|
||||
case ModeM192f:
|
||||
return 35664, nil
|
||||
case ModeM256s:
|
||||
return 29792, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("mode 0x%02x", byte(mode))
|
||||
}
|
||||
}
|
||||
|
||||
// wirePubKeySizeForMode returns the FIPS 205 public-key size for
|
||||
// the declared mode.
|
||||
func wirePubKeySizeForMode(mode Mode) (int, error) {
|
||||
switch mode {
|
||||
case ModeM192s:
|
||||
return 48, nil
|
||||
case ModeM192f:
|
||||
return 48, nil
|
||||
case ModeM256s:
|
||||
return 64, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("mode 0x%02x", byte(mode))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,819 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package magnetar
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/circl/sign/slhdsa"
|
||||
)
|
||||
|
||||
// TestMagnetar_Wire_Roundtrip checks the Signature wire codec
|
||||
// round-trips byte-equal for every magnetar mode and that the
|
||||
// parsed Mode + Bytes match the input exactly. A second marshal
|
||||
// MUST produce the same bytes as the first (strict canonical
|
||||
// encoding). Mirrors TestPulsar_Wire_SigRoundtrip in scope.
|
||||
func TestMagnetar_Wire_Roundtrip(t *testing.T) {
|
||||
for _, mode := range []Mode{ModeM192s, ModeM192f, ModeM256s} {
|
||||
mode := mode
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
params := MustParamsFor(mode)
|
||||
body := make([]byte, params.SignatureSize)
|
||||
if _, err := rand.Read(body); err != nil {
|
||||
t.Fatalf("rand.Read: %v", err)
|
||||
}
|
||||
sig := &Signature{Mode: mode, Bytes: append([]byte{}, body...)}
|
||||
|
||||
wire1, err := sig.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalBinary: %v", err)
|
||||
}
|
||||
if got := len(wire1); got != wireFixedSigHeader+params.SignatureSize {
|
||||
t.Fatalf("wire length %d != header(%d) + payload(%d)",
|
||||
got, wireFixedSigHeader, params.SignatureSize)
|
||||
}
|
||||
|
||||
var parsed Signature
|
||||
if err := parsed.UnmarshalBinary(wire1); err != nil {
|
||||
t.Fatalf("UnmarshalBinary: %v", err)
|
||||
}
|
||||
if parsed.Mode != mode {
|
||||
t.Fatalf("parsed.Mode = %v want %v", parsed.Mode, mode)
|
||||
}
|
||||
if !bytes.Equal(parsed.Bytes, body) {
|
||||
t.Fatalf("parsed.Bytes diverged from input")
|
||||
}
|
||||
|
||||
wire2, err := parsed.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("re-MarshalBinary: %v", err)
|
||||
}
|
||||
if !bytes.Equal(wire1, wire2) {
|
||||
t.Fatalf("re-marshal not byte-equal: %d vs %d", len(wire1), len(wire2))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMagnetar_Wire_GroupKeyRoundtrip mirrors the signature
|
||||
// round-trip for the group-key (MAGG) frame.
|
||||
func TestMagnetar_Wire_GroupKeyRoundtrip(t *testing.T) {
|
||||
for _, mode := range []Mode{ModeM192s, ModeM192f, ModeM256s} {
|
||||
mode := mode
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
params := MustParamsFor(mode)
|
||||
body := make([]byte, params.PublicKeySize)
|
||||
if _, err := rand.Read(body); err != nil {
|
||||
t.Fatalf("rand.Read: %v", err)
|
||||
}
|
||||
pk := &PublicKey{Mode: mode, Bytes: append([]byte{}, body...)}
|
||||
|
||||
wire1, err := MarshalGroupKey(pk)
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalGroupKey: %v", err)
|
||||
}
|
||||
if got := len(wire1); got != wireFixedGroupKeyHeader+params.PublicKeySize {
|
||||
t.Fatalf("wire length %d != header(%d) + payload(%d)",
|
||||
got, wireFixedGroupKeyHeader, params.PublicKeySize)
|
||||
}
|
||||
|
||||
parsed, err := UnmarshalGroupKey(wire1)
|
||||
if err != nil {
|
||||
t.Fatalf("UnmarshalGroupKey: %v", err)
|
||||
}
|
||||
if parsed.Mode != mode {
|
||||
t.Fatalf("parsed.Mode = %v want %v", parsed.Mode, mode)
|
||||
}
|
||||
if !bytes.Equal(parsed.Bytes, body) {
|
||||
t.Fatalf("parsed.Bytes diverged from input")
|
||||
}
|
||||
|
||||
wire2, err := MarshalGroupKey(parsed)
|
||||
if err != nil {
|
||||
t.Fatalf("re-MarshalGroupKey: %v", err)
|
||||
}
|
||||
if !bytes.Equal(wire1, wire2) {
|
||||
t.Fatalf("re-marshal not byte-equal: %d vs %d", len(wire1), len(wire2))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMagnetar_Wire_FIPS205Verifiable is THE HEADLINE
|
||||
// cryptographic claim of the wire codec: a Magnetar Signature
|
||||
// produced by the v0.5 per-validator standalone primary path
|
||||
// (ValidatorSign), after MarshalBinary + payload extraction,
|
||||
// verifies under cloudflare/circl's FIPS 205 slhdsa.Verify with NO
|
||||
// magnetar code path involved on the verifier side.
|
||||
//
|
||||
// This is the Class N1 analog in test form: the threshold
|
||||
// orchestration layer can hand wire bytes to a relying party that
|
||||
// has never heard of Magnetar, and that party — with only a FIPS
|
||||
// 205 SLH-DSA verifier and the documented MAGS / MAGG frame —
|
||||
// verifies the signature.
|
||||
//
|
||||
// For every magnetar mode we run the v0.5 per-validator standalone
|
||||
// primary path: PerValidatorKeypair + ValidatorSign. The output is
|
||||
// FIPS 205 SignDeterministic when rng is nil (sign.go:91; see
|
||||
// magnetar README §"FIPS 205 byte-identity").
|
||||
//
|
||||
// We DO NOT silently fall back to slhdsa.GenerateKey + priv.SignCtx
|
||||
// here — if magnetar's v0.5 primary path failed to produce a
|
||||
// byte-identical FIPS 205 signature, that would be a real finding
|
||||
// for the cryptographer, not something to mask.
|
||||
func TestMagnetar_Wire_FIPS205Verifiable(t *testing.T) {
|
||||
skipUnderRace(t)
|
||||
for _, mode := range []Mode{ModeM192s, ModeM192f, ModeM256s} {
|
||||
mode := mode
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
params := MustParamsFor(mode)
|
||||
|
||||
// 1. Generate via the v0.5 per-validator standalone primary
|
||||
// path — this is the MPC code path the magnetar audit
|
||||
// cares about, even though "MPC" here is the public-BFT
|
||||
// per-validator-standalone shape (each validator runs
|
||||
// PerValidatorKeypair independently; the consensus layer
|
||||
// aggregates N signatures separately via
|
||||
// ValidatorAggregateCert).
|
||||
seed := []byte("magnetar-wire-fips205-test")
|
||||
seed = append(seed, byte(mode))
|
||||
sk, pk, err := PerValidatorKeypair(params, newDetReader(seed))
|
||||
if err != nil {
|
||||
t.Fatalf("PerValidatorKeypair: %v", err)
|
||||
}
|
||||
|
||||
msg := []byte("magnetar wire codec — FIPS 205 byte-identity claim")
|
||||
|
||||
// 2. Sign via the v0.5 ValidatorSign primary primitive.
|
||||
// rng=nil → SignDeterministic (deterministic, byte-
|
||||
// identical across calls). This is the magnetar entry
|
||||
// point the consensus layer hits.
|
||||
sigBytes, err := ValidatorSign(sk, nil, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidatorSign: %v", err)
|
||||
}
|
||||
if len(sigBytes) != params.SignatureSize {
|
||||
t.Fatalf("sig length = %d, want %d", len(sigBytes), params.SignatureSize)
|
||||
}
|
||||
|
||||
sig := &Signature{Mode: mode, Bytes: sigBytes}
|
||||
|
||||
// 3. Sanity: magnetar's own Verify accepts it (the same
|
||||
// FIPS 205 verifier, but routed through magnetar's
|
||||
// dispatch).
|
||||
if err := Verify(params, pk, msg, sig); err != nil {
|
||||
t.Fatalf("baseline magnetar Verify failed before wire: %v", err)
|
||||
}
|
||||
|
||||
// 4. Frame both via the wire codec.
|
||||
gkWire, err := MarshalGroupKey(pk)
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalGroupKey: %v", err)
|
||||
}
|
||||
sigWire, err := sig.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("sig.MarshalBinary: %v", err)
|
||||
}
|
||||
|
||||
// 5. Extract the FIPS 205 payload from each frame by
|
||||
// skipping the 11-byte header. NO magnetar package code
|
||||
// touches the payload — this is exactly what an external
|
||||
// relying party with a FIPS 205 verifier would do.
|
||||
fipsPK := extractFIPS205Payload(t, gkWire, wireMagicMagnetarGroupKey)
|
||||
fipsSig := extractFIPS205Payload(t, sigWire, wireMagicMagnetarSig)
|
||||
|
||||
if !bytes.Equal(fipsPK, pk.Bytes) {
|
||||
t.Fatalf("MAGG payload != raw FIPS 205 pk bytes — wire codec injected envelope")
|
||||
}
|
||||
if !bytes.Equal(fipsSig, sigBytes) {
|
||||
t.Fatalf("MAGS payload != raw FIPS 205 sig bytes — wire codec injected envelope")
|
||||
}
|
||||
|
||||
// 6. Verify under cloudflare/circl directly. No magnetar
|
||||
// code path is hit on the verify side. This is the
|
||||
// load-bearing assertion of byte-identity.
|
||||
if !verifyDirectCircl(mode, fipsPK, msg, fipsSig) {
|
||||
t.Fatalf("cloudflare/circl FIPS 205 Verify rejected the unwrapped Magnetar bytes — wire codec broke byte-identity")
|
||||
}
|
||||
|
||||
// 7. Stateless VerifyBytes path used by thresholdd accepts
|
||||
// the same wire bytes.
|
||||
if !VerifyBytes(gkWire, msg, sigWire) {
|
||||
t.Fatalf("VerifyBytes rejected a valid signature")
|
||||
}
|
||||
|
||||
// 8. Tamper resistance: flip a byte in the sig payload —
|
||||
// direct circl Verify MUST reject, and VerifyBytes must
|
||||
// reject.
|
||||
tamperedSig := append([]byte{}, sigWire...)
|
||||
tamperedSig[len(tamperedSig)-1] ^= 0x01
|
||||
if VerifyBytes(gkWire, msg, tamperedSig) {
|
||||
t.Fatalf("VerifyBytes accepted a tampered signature")
|
||||
}
|
||||
tamperedFIPS := append([]byte{}, fipsSig...)
|
||||
tamperedFIPS[len(tamperedFIPS)-1] ^= 0x01
|
||||
if verifyDirectCircl(mode, fipsPK, msg, tamperedFIPS) {
|
||||
t.Fatalf("circl FIPS 205 Verify accepted a tampered signature")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// extractFIPS205Payload strips the wire header from a MAGS or MAGG
|
||||
// frame and returns the FIPS 205 payload bytes. Asserts the magic
|
||||
// matches the expected one. Test-only — production wire callers go
|
||||
// through Signature.UnmarshalBinary / UnmarshalGroupKey.
|
||||
func extractFIPS205Payload(t *testing.T, wire []byte, wantMagic uint32) []byte {
|
||||
t.Helper()
|
||||
if len(wire) < wireFixedSigHeader {
|
||||
t.Fatalf("wire frame too short: %d", len(wire))
|
||||
}
|
||||
gotMagic := binary.BigEndian.Uint32(wire[0:4])
|
||||
if gotMagic != wantMagic {
|
||||
t.Fatalf("magic mismatch: got 0x%08x want 0x%08x", gotMagic, wantMagic)
|
||||
}
|
||||
version := binary.BigEndian.Uint16(wire[4:6])
|
||||
if version != wireVersionV1 {
|
||||
t.Fatalf("version mismatch: got %d want %d", version, wireVersionV1)
|
||||
}
|
||||
declared := binary.BigEndian.Uint32(wire[7:11])
|
||||
if int(declared) != len(wire)-wireFixedSigHeader {
|
||||
t.Fatalf("declared length %d != payload length %d",
|
||||
declared, len(wire)-wireFixedSigHeader)
|
||||
}
|
||||
return wire[wireFixedSigHeader:]
|
||||
}
|
||||
|
||||
// verifyDirectCircl bypasses every magnetar code path and calls
|
||||
// cloudflare/circl's FIPS 205 Verify directly. This proves the
|
||||
// wire codec's body bytes are valid FIPS 205 bytes.
|
||||
func verifyDirectCircl(mode Mode, pkBytes, msg, sigBytes []byte) bool {
|
||||
var id slhdsa.ID
|
||||
switch mode {
|
||||
case ModeM192s:
|
||||
id = slhdsa.SHAKE_192s
|
||||
case ModeM192f:
|
||||
id = slhdsa.SHAKE_192f
|
||||
case ModeM256s:
|
||||
id = slhdsa.SHAKE_256s
|
||||
default:
|
||||
return false
|
||||
}
|
||||
pk := slhdsa.PublicKey{ID: id}
|
||||
if err := pk.UnmarshalBinary(pkBytes); err != nil {
|
||||
return false
|
||||
}
|
||||
return slhdsa.Verify(&pk, slhdsa.NewMessage(msg), sigBytes, nil)
|
||||
}
|
||||
|
||||
// TestMagnetar_Wire_RejectMalformed exercises every negative path
|
||||
// the signature parser must catch. Each malformed input must
|
||||
// surface a typed error from the wire package; no panic, no
|
||||
// oversized allocation, no payload buffer for invalid magic /
|
||||
// version / mode / length.
|
||||
func TestMagnetar_Wire_RejectMalformed(t *testing.T) {
|
||||
validSig := func(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
mode := ModeM192s
|
||||
params := MustParamsFor(mode)
|
||||
body := make([]byte, params.SignatureSize)
|
||||
s := &Signature{Mode: mode, Bytes: body}
|
||||
w, err := s.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("setup MarshalBinary: %v", err)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
build func(t *testing.T) []byte
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
build: func(t *testing.T) []byte { return nil },
|
||||
want: ErrWireFrameTooShort,
|
||||
},
|
||||
{
|
||||
name: "header-only-minus-one",
|
||||
build: func(t *testing.T) []byte {
|
||||
return make([]byte, wireFixedSigHeader-1)
|
||||
},
|
||||
want: ErrWireFrameTooShort,
|
||||
},
|
||||
{
|
||||
name: "wrong-magic",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validSig(t)
|
||||
w[0] = 0xDE
|
||||
w[1] = 0xAD
|
||||
w[2] = 0xBE
|
||||
w[3] = 0xEF
|
||||
return w
|
||||
},
|
||||
want: ErrWireMagicMismatch,
|
||||
},
|
||||
{
|
||||
name: "group-key-magic-into-sig",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validSig(t)
|
||||
// Replace MAGS with MAGG.
|
||||
binary.BigEndian.PutUint32(w[0:4], wireMagicMagnetarGroupKey)
|
||||
return w
|
||||
},
|
||||
want: ErrWireMagicMismatch,
|
||||
},
|
||||
{
|
||||
name: "pulsar-sig-magic-rejected",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validSig(t)
|
||||
// PULS = 0x50554C53 — pulsar's signature magic.
|
||||
binary.BigEndian.PutUint32(w[0:4], 0x50554C53)
|
||||
return w
|
||||
},
|
||||
want: ErrWireMagicMismatch,
|
||||
},
|
||||
{
|
||||
name: "corona-sig-magic-rejected",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validSig(t)
|
||||
// CORS = 0x434F5253 — corona's signature magic.
|
||||
binary.BigEndian.PutUint32(w[0:4], 0x434F5253)
|
||||
return w
|
||||
},
|
||||
want: ErrWireMagicMismatch,
|
||||
},
|
||||
{
|
||||
name: "wrong-version",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validSig(t)
|
||||
binary.BigEndian.PutUint16(w[4:6], 0xFFFF)
|
||||
return w
|
||||
},
|
||||
want: ErrWireVersionMismatch,
|
||||
},
|
||||
{
|
||||
name: "unknown-mode",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validSig(t)
|
||||
w[6] = 0xAA // not in {ModeM192s, ModeM192f, ModeM256s}
|
||||
return w
|
||||
},
|
||||
want: ErrWireModeUnknown,
|
||||
},
|
||||
{
|
||||
name: "length-mismatch-mode-192s",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validSig(t)
|
||||
// Declared length 999 for ModeM192s — not the canonical 16224.
|
||||
binary.BigEndian.PutUint32(w[7:11], 999)
|
||||
return w
|
||||
},
|
||||
want: ErrWireLengthMismatch,
|
||||
},
|
||||
{
|
||||
name: "length-oversized-vs-buffer",
|
||||
build: func(t *testing.T) []byte {
|
||||
// Manually craft: MAGS, v1, mode=M192s, declared=4 GiB.
|
||||
w := make([]byte, wireFixedSigHeader)
|
||||
binary.BigEndian.PutUint32(w[0:4], wireMagicMagnetarSig)
|
||||
binary.BigEndian.PutUint16(w[4:6], wireVersionV1)
|
||||
w[6] = byte(ModeM192s)
|
||||
binary.BigEndian.PutUint32(w[7:11], 0xFFFFFFFF)
|
||||
return w
|
||||
},
|
||||
// 4 GiB is not equal to FIPS 205 ModeM192s size; the length
|
||||
// mismatch fires before the buffer-bounds check.
|
||||
want: ErrWireLengthMismatch,
|
||||
},
|
||||
{
|
||||
name: "trailing-bytes",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validSig(t)
|
||||
return append(w, 0x00, 0x00, 0x00)
|
||||
},
|
||||
want: ErrWireTrailingBytes,
|
||||
},
|
||||
{
|
||||
name: "truncated-payload",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validSig(t)
|
||||
// Drop the trailing 3 bytes of the payload. The declared
|
||||
// length still matches the canonical FIPS 205 size so
|
||||
// length-against-mode check passes; the bounded-reader
|
||||
// check fires because declared > remaining buffer.
|
||||
return w[:len(w)-3]
|
||||
},
|
||||
want: ErrWireFrameRejected,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var sig Signature
|
||||
err := sig.UnmarshalBinary(tc.build(t))
|
||||
if err == nil {
|
||||
t.Fatalf("expected error %v, got nil", tc.want)
|
||||
}
|
||||
if !errors.Is(err, tc.want) {
|
||||
t.Fatalf("expected %v, got %v", tc.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMagnetar_Wire_RejectTrailingBytes pins the strict-canonical
|
||||
// property as a standalone test: exactly one well-formed encoding
|
||||
// of any (mode, bytes) pair exists.
|
||||
func TestMagnetar_Wire_RejectTrailingBytes(t *testing.T) {
|
||||
params := MustParamsFor(ModeM192s)
|
||||
body := make([]byte, params.SignatureSize)
|
||||
s := &Signature{Mode: ModeM192s, Bytes: body}
|
||||
w, err := s.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalBinary: %v", err)
|
||||
}
|
||||
// One trailing zero byte must be rejected.
|
||||
withTrailing := append(append([]byte{}, w...), 0x00)
|
||||
var parsed Signature
|
||||
if err := parsed.UnmarshalBinary(withTrailing); !errors.Is(err, ErrWireTrailingBytes) {
|
||||
t.Fatalf("expected ErrWireTrailingBytes, got %v", err)
|
||||
}
|
||||
|
||||
// Same for GroupKey.
|
||||
pkBody := make([]byte, params.PublicKeySize)
|
||||
pk := &PublicKey{Mode: ModeM192s, Bytes: pkBody}
|
||||
gw, err := MarshalGroupKey(pk)
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalGroupKey: %v", err)
|
||||
}
|
||||
gwTrailing := append(append([]byte{}, gw...), 0xFF)
|
||||
if _, err := UnmarshalGroupKey(gwTrailing); !errors.Is(err, ErrWireTrailingBytes) {
|
||||
t.Fatalf("expected ErrWireTrailingBytes on GroupKey, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMagnetar_Wire_GroupKeyRejectMalformed mirrors the signature
|
||||
// rejection suite for GroupKey. Crucially, a SIGNATURE frame fed
|
||||
// into the GroupKey parser must be rejected at the magic check —
|
||||
// domain separation between MAGS and MAGG must be effective.
|
||||
func TestMagnetar_Wire_GroupKeyRejectMalformed(t *testing.T) {
|
||||
validGk := func(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
mode := ModeM192s
|
||||
params := MustParamsFor(mode)
|
||||
body := make([]byte, params.PublicKeySize)
|
||||
p := &PublicKey{Mode: mode, Bytes: body}
|
||||
w, err := MarshalGroupKey(p)
|
||||
if err != nil {
|
||||
t.Fatalf("setup MarshalGroupKey: %v", err)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
build func(t *testing.T) []byte
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
build: func(t *testing.T) []byte { return nil },
|
||||
want: ErrWireFrameTooShort,
|
||||
},
|
||||
{
|
||||
name: "sig-magic-into-groupkey",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validGk(t)
|
||||
binary.BigEndian.PutUint32(w[0:4], wireMagicMagnetarSig)
|
||||
return w
|
||||
},
|
||||
want: ErrWireMagicMismatch,
|
||||
},
|
||||
{
|
||||
name: "pulsar-groupkey-magic-rejected",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validGk(t)
|
||||
// PULG = 0x50554C47 — pulsar's GroupKey magic.
|
||||
binary.BigEndian.PutUint32(w[0:4], 0x50554C47)
|
||||
return w
|
||||
},
|
||||
want: ErrWireMagicMismatch,
|
||||
},
|
||||
{
|
||||
name: "corona-groupkey-magic-rejected",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validGk(t)
|
||||
// CORG = 0x434F5247 — corona's GroupKey magic.
|
||||
binary.BigEndian.PutUint32(w[0:4], 0x434F5247)
|
||||
return w
|
||||
},
|
||||
want: ErrWireMagicMismatch,
|
||||
},
|
||||
{
|
||||
name: "wrong-version",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validGk(t)
|
||||
binary.BigEndian.PutUint16(w[4:6], 0xFFFF)
|
||||
return w
|
||||
},
|
||||
want: ErrWireVersionMismatch,
|
||||
},
|
||||
{
|
||||
name: "unknown-mode",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validGk(t)
|
||||
w[6] = 0xAA
|
||||
return w
|
||||
},
|
||||
want: ErrWireModeUnknown,
|
||||
},
|
||||
{
|
||||
name: "length-mismatch",
|
||||
build: func(t *testing.T) []byte {
|
||||
w := validGk(t)
|
||||
binary.BigEndian.PutUint32(w[7:11], 7)
|
||||
return w
|
||||
},
|
||||
want: ErrWireLengthMismatch,
|
||||
},
|
||||
{
|
||||
name: "trailing-bytes",
|
||||
build: func(t *testing.T) []byte {
|
||||
return append(validGk(t), 0x00)
|
||||
},
|
||||
want: ErrWireTrailingBytes,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := UnmarshalGroupKey(tc.build(t))
|
||||
if err == nil {
|
||||
t.Fatalf("expected error %v, got nil", tc.want)
|
||||
}
|
||||
if !errors.Is(err, tc.want) {
|
||||
t.Fatalf("expected %v, got %v", tc.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMagnetar_Wire_RejectCrossWire pins the cross-protocol
|
||||
// rejection contract: feeding pulsar's PULS / PULG bytes or
|
||||
// corona's CORS / CORG bytes into magnetar's parser must fail at
|
||||
// the first magic check. This is the audit-load-bearing property
|
||||
// that the four-byte magic acts as a protocol-family domain
|
||||
// separator.
|
||||
func TestMagnetar_Wire_RejectCrossWire(t *testing.T) {
|
||||
t.Run("pulsar-PULS-frame-into-magnetar-sig", func(t *testing.T) {
|
||||
// Synthesise the smallest valid-looking PULS frame: PULS
|
||||
// magic + version=1 + mode=0x41 (P65) + declared FIPS 204
|
||||
// ML-DSA-65 sig size 3309 + 3309 zero bytes.
|
||||
const pulsarP65SigSize = 3309
|
||||
buf := make([]byte, 0, wireFixedSigHeader+pulsarP65SigSize)
|
||||
buf = binary.BigEndian.AppendUint32(buf, 0x50554C53) // PULS
|
||||
buf = binary.BigEndian.AppendUint16(buf, 1)
|
||||
buf = append(buf, 0x41) // pulsar ModeP65
|
||||
buf = binary.BigEndian.AppendUint32(buf, pulsarP65SigSize)
|
||||
buf = append(buf, make([]byte, pulsarP65SigSize)...)
|
||||
|
||||
var sig Signature
|
||||
err := sig.UnmarshalBinary(buf)
|
||||
if !errors.Is(err, ErrWireMagicMismatch) {
|
||||
t.Fatalf("expected ErrWireMagicMismatch on PULS frame, got %v", err)
|
||||
}
|
||||
// VerifyBytes must also reject in the signature slot.
|
||||
if VerifyBytes(nil, []byte("msg"), buf) {
|
||||
t.Fatalf("VerifyBytes accepted PULS frame in sig slot")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("corona-CORS-frame-into-magnetar-sig", func(t *testing.T) {
|
||||
// Corona's signature has nested length-prefixed polynomial
|
||||
// fields; we don't need a perfectly-formed corona frame, just
|
||||
// the canonical four-byte magic + version. Magnetar must
|
||||
// reject at the first byte check.
|
||||
buf := make([]byte, 0, wireFixedSigHeader+32)
|
||||
buf = binary.BigEndian.AppendUint32(buf, 0x434F5253) // CORS
|
||||
buf = binary.BigEndian.AppendUint16(buf, 1)
|
||||
buf = append(buf, 0x00)
|
||||
buf = binary.BigEndian.AppendUint32(buf, 32)
|
||||
buf = append(buf, make([]byte, 32)...)
|
||||
|
||||
var sig Signature
|
||||
err := sig.UnmarshalBinary(buf)
|
||||
if !errors.Is(err, ErrWireMagicMismatch) {
|
||||
t.Fatalf("expected ErrWireMagicMismatch on CORS frame, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pulsar-PULG-frame-into-magnetar-groupkey", func(t *testing.T) {
|
||||
const pulsarP65PkSize = 1952
|
||||
buf := make([]byte, 0, wireFixedGroupKeyHeader+pulsarP65PkSize)
|
||||
buf = binary.BigEndian.AppendUint32(buf, 0x50554C47) // PULG
|
||||
buf = binary.BigEndian.AppendUint16(buf, 1)
|
||||
buf = append(buf, 0x41)
|
||||
buf = binary.BigEndian.AppendUint32(buf, pulsarP65PkSize)
|
||||
buf = append(buf, make([]byte, pulsarP65PkSize)...)
|
||||
|
||||
_, err := UnmarshalGroupKey(buf)
|
||||
if !errors.Is(err, ErrWireMagicMismatch) {
|
||||
t.Fatalf("expected ErrWireMagicMismatch on PULG frame, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("corona-CORG-frame-into-magnetar-groupkey", func(t *testing.T) {
|
||||
buf := make([]byte, 0, wireFixedGroupKeyHeader+32)
|
||||
buf = binary.BigEndian.AppendUint32(buf, 0x434F5247) // CORG
|
||||
buf = binary.BigEndian.AppendUint16(buf, 1)
|
||||
buf = append(buf, 0x00)
|
||||
buf = binary.BigEndian.AppendUint32(buf, 32)
|
||||
buf = append(buf, make([]byte, 32)...)
|
||||
|
||||
_, err := UnmarshalGroupKey(buf)
|
||||
if !errors.Is(err, ErrWireMagicMismatch) {
|
||||
t.Fatalf("expected ErrWireMagicMismatch on CORG frame, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMagnetar_Wire_VerifyBytes_RejectsCrossSlot exercises the
|
||||
// dispatcher's contract that wire bytes cannot be swapped between
|
||||
// slots: a GroupKey-magic frame fed into the signature slot of
|
||||
// VerifyBytes must produce false. Same for a Signature-magic frame
|
||||
// fed into the GroupKey slot.
|
||||
func TestMagnetar_Wire_VerifyBytes_RejectsCrossSlot(t *testing.T) {
|
||||
skipUnderRace(t)
|
||||
params := MustParamsFor(ModeM192s)
|
||||
sk, pk, err := PerValidatorKeypair(params, newDetReader([]byte("magnetar-wire-cross-slot")))
|
||||
if err != nil {
|
||||
t.Fatalf("PerValidatorKeypair: %v", err)
|
||||
}
|
||||
msg := []byte("cross-slot rejection")
|
||||
sigBytes, err := ValidatorSign(sk, nil, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidatorSign: %v", err)
|
||||
}
|
||||
sig := &Signature{Mode: params.Mode, Bytes: sigBytes}
|
||||
|
||||
gkWire, err := MarshalGroupKey(pk)
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalGroupKey: %v", err)
|
||||
}
|
||||
sigWire, err := sig.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("sig.MarshalBinary: %v", err)
|
||||
}
|
||||
|
||||
if !VerifyBytes(gkWire, msg, sigWire) {
|
||||
t.Fatalf("baseline VerifyBytes failed")
|
||||
}
|
||||
// Swap: gkWire in the signature slot. Must reject.
|
||||
if VerifyBytes(gkWire, msg, gkWire) {
|
||||
t.Fatalf("VerifyBytes accepted GroupKey-magic bytes in signature slot")
|
||||
}
|
||||
// Swap: sigWire in the group-key slot. Must reject.
|
||||
if VerifyBytes(sigWire, msg, sigWire) {
|
||||
t.Fatalf("VerifyBytes accepted Signature-magic bytes in group-key slot")
|
||||
}
|
||||
// nil bytes — must reject without panic.
|
||||
if VerifyBytes(nil, msg, sigWire) {
|
||||
t.Fatalf("VerifyBytes accepted nil group-key bytes")
|
||||
}
|
||||
if VerifyBytes(gkWire, msg, nil) {
|
||||
t.Fatalf("VerifyBytes accepted nil signature bytes")
|
||||
}
|
||||
// Wrong message — must reject.
|
||||
if VerifyBytes(gkWire, []byte("different message"), sigWire) {
|
||||
t.Fatalf("VerifyBytes accepted a wrong message")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMagnetar_Wire_ModeMismatch_Rejected ensures the dispatcher's
|
||||
// VerifyBytes returns false when the parsed GroupKey and Signature
|
||||
// declare different modes — a relying party must never witness a
|
||||
// ModeM256s signature verifying under a ModeM192s group key.
|
||||
func TestMagnetar_Wire_ModeMismatch_Rejected(t *testing.T) {
|
||||
skipUnderRace(t)
|
||||
// Build a valid sig (ModeM192s) and a valid gk (ModeM256s).
|
||||
params192 := MustParamsFor(ModeM192s)
|
||||
params256 := MustParamsFor(ModeM256s)
|
||||
|
||||
sk192, _, err := PerValidatorKeypair(params192, newDetReader([]byte("mm-192")))
|
||||
if err != nil {
|
||||
t.Fatalf("PerValidatorKeypair 192: %v", err)
|
||||
}
|
||||
_, pk256, err := PerValidatorKeypair(params256, newDetReader([]byte("mm-256")))
|
||||
if err != nil {
|
||||
t.Fatalf("PerValidatorKeypair 256: %v", err)
|
||||
}
|
||||
|
||||
msg := []byte("mode mismatch test")
|
||||
sig192Bytes, err := ValidatorSign(sk192, nil, msg)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidatorSign 192: %v", err)
|
||||
}
|
||||
sig192 := &Signature{Mode: params192.Mode, Bytes: sig192Bytes}
|
||||
|
||||
gk256Wire, err := MarshalGroupKey(pk256)
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalGroupKey 256: %v", err)
|
||||
}
|
||||
sig192Wire, err := sig192.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("sig192.MarshalBinary: %v", err)
|
||||
}
|
||||
if VerifyBytes(gk256Wire, msg, sig192Wire) {
|
||||
t.Fatalf("VerifyBytes accepted cross-mode (gk=M256s, sig=M192s)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMagnetar_Wire_MarshalSafetyChecks ensures Marshal refuses
|
||||
// inputs that would emit malformed frames: nil receiver, unknown
|
||||
// mode, length-vs-mode mismatch.
|
||||
func TestMagnetar_Wire_MarshalSafetyChecks(t *testing.T) {
|
||||
t.Run("nil-signature", func(t *testing.T) {
|
||||
var s *Signature
|
||||
if _, err := s.MarshalBinary(); err == nil {
|
||||
t.Fatal("nil Signature.MarshalBinary did not return error")
|
||||
}
|
||||
})
|
||||
t.Run("nil-publickey", func(t *testing.T) {
|
||||
if _, err := MarshalGroupKey(nil); err == nil {
|
||||
t.Fatal("nil MarshalGroupKey did not return error")
|
||||
}
|
||||
})
|
||||
t.Run("sig-unknown-mode", func(t *testing.T) {
|
||||
s := &Signature{Mode: Mode(0xAA), Bytes: make([]byte, 16224)}
|
||||
if _, err := s.MarshalBinary(); err == nil {
|
||||
t.Fatal("unknown-mode Signature.MarshalBinary did not return error")
|
||||
}
|
||||
})
|
||||
t.Run("sig-wrong-length", func(t *testing.T) {
|
||||
s := &Signature{Mode: ModeM192s, Bytes: make([]byte, 999)}
|
||||
if _, err := s.MarshalBinary(); err == nil {
|
||||
t.Fatal("wrong-length Signature.MarshalBinary did not return error")
|
||||
}
|
||||
})
|
||||
t.Run("gk-wrong-length", func(t *testing.T) {
|
||||
p := &PublicKey{Mode: ModeM192s, Bytes: make([]byte, 99)}
|
||||
if _, err := MarshalGroupKey(p); err == nil {
|
||||
t.Fatal("wrong-length MarshalGroupKey did not return error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMagnetar_Wire_VerifyBytesCtx_RoundTrip exercises the
|
||||
// context-aware verifier with a non-nil ctx, the path the EVM
|
||||
// precompile takes. Same sign-vs-verify ctx → accept; different
|
||||
// ctx → reject.
|
||||
func TestMagnetar_Wire_VerifyBytesCtx_RoundTrip(t *testing.T) {
|
||||
skipUnderRace(t)
|
||||
params := MustParamsFor(ModeM192s)
|
||||
sk, pk, err := PerValidatorKeypair(params, newDetReader([]byte("magnetar-wire-ctx")))
|
||||
if err != nil {
|
||||
t.Fatalf("PerValidatorKeypair: %v", err)
|
||||
}
|
||||
msg := []byte("ctx-aware verify")
|
||||
ctx := []byte("lux-evm-precompile-magnetar-v1")
|
||||
|
||||
// Sign with explicit ctx via the package-level Sign (the entry
|
||||
// point ValidatorSign documents for the ctx case).
|
||||
sig, err := Sign(params, sk, msg, ctx, false, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign with ctx: %v", err)
|
||||
}
|
||||
|
||||
gkWire, err := MarshalGroupKey(pk)
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalGroupKey: %v", err)
|
||||
}
|
||||
sigWire, err := sig.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("sig.MarshalBinary: %v", err)
|
||||
}
|
||||
|
||||
if !VerifyBytesCtx(gkWire, msg, ctx, sigWire) {
|
||||
t.Fatalf("VerifyBytesCtx rejected a valid ctx-bound signature")
|
||||
}
|
||||
// Empty ctx must NOT verify (the sign used a non-empty ctx).
|
||||
if VerifyBytes(gkWire, msg, sigWire) {
|
||||
t.Fatalf("VerifyBytes (empty ctx) accepted a ctx-bound signature")
|
||||
}
|
||||
// Different ctx must NOT verify.
|
||||
if VerifyBytesCtx(gkWire, msg, []byte("different ctx"), sigWire) {
|
||||
t.Fatalf("VerifyBytesCtx accepted a wrong ctx")
|
||||
}
|
||||
// Over-long ctx must NOT verify (≤255 bytes per FIPS 205).
|
||||
if VerifyBytesCtx(gkWire, msg, make([]byte, 256), sigWire) {
|
||||
t.Fatalf("VerifyBytesCtx accepted an over-long ctx")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user