wire: canonical Signature + GroupKey codec, stateless VerifyBytes

Mirror of corona@42838bd. Magic PULS / PULG, version byte, bounded
length-prefixed fields. VerifyBytes is stateless; wraps the FIPS 204
ML-DSA-44/65/87 verify (cloudflare/circl) over the unwrapped payload.
The threshold-combined signature is byte-identical to a single-party
FIPS 204 ML-DSA signature on the same message + group public key,
verified by TestPulsar_Wire_FIPS204Verifiable across all three modes.

Wire layout (big-endian):
  Signature: magic(4)='PULS' || version(2) || mode(1) || len(4) || FIPS-204-sig
  GroupKey:  magic(4)='PULG' || version(2) || mode(1) || len(4) || FIPS-204-pk

Bounded reads check declared length against the canonical FIPS 204
size for the declared mode BEFORE allocation, then against the
remaining buffer — a malformed length header cannot trigger an
oversized allocation. Trailing bytes after the frame are rejected.
Distinct magic from corona's CORS/CORG defeats cross-protocol replay.

orchestrate.go adds OrchestrateV03Sign — an in-process driver of
the full Round1 / Round2W / Round2Sign / AlgebraicAggregate cycle
with FIPS 204 rejection-restart, plus QuorumSessionKeys helper for
the pairwise ML-KEM-768 + ML-DSA-65 identity layer. This is the
surface luxfi/threshold/pkg/thresholdd consumes; lets the dispatcher
publish a Signature in one call without leaking the package-private
polyVec type out of pulsar.

Tests:
- TestPulsar_Wire_SigRoundtrip / GroupKeyRoundtrip across ModeP44/65/87
- TestPulsar_Wire_FIPS204Verifiable — frame round-trip then verify
  via cloudflare/circl directly with zero pulsar code on the verifier
  side. Headline cryptographic claim.
- TestPulsar_Wire_RejectMalformed — empty, header-only, wrong-magic,
  group-key-magic-into-sig, wrong-version, unknown-mode, length-
  mismatch, length-oversized, trailing-bytes, truncated-payload.
- TestPulsar_Wire_GroupKeyRejectMalformed — symmetric for GroupKey.
- TestPulsar_Wire_VerifyBytes_RejectsCrossSlot — domain-separation
  between PULS and PULG is effective at the VerifyBytes surface.
- TestPulsar_Wire_ModeMismatch_Rejected — gk.Mode != sig.Mode fails
  verify with no allocation surprise.
- TestPulsar_Wire_MarshalSafetyChecks — nil receivers / unknown mode
  / wrong-length payloads refuse to emit malformed frames.

Full pulsar/ref/go/pkg/pulsar test suite (87 tests, -race) green.
This commit is contained in:
2026-05-31 12:39:56 -07:00
parent 111755fbeb
commit 3a2bc3fdc6
3 changed files with 1247 additions and 0 deletions
+219
View File
@@ -0,0 +1,219 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package pulsar
// orchestrate.go — in-process orchestration helper that drives a
// complete v0.3 algebraic-aggregate threshold sign across t parties
// AND runs the FIPS 204 rejection-restart loop. This is the surface
// the luxfi/threshold JSON-RPC dispatcher consumes — it lets the
// dispatcher publish a Signature from a single function call
// without leaking pulsar's package-private polyVec type into the
// dispatcher.
//
// Trust model is the same as the test-side stageAlgebraic helper:
// all t parties' state lives in this process. This is appropriate
// for the JSON-RPC dispatcher (off-chain integration test harness,
// MPC bus dev tooling, SDK fixtures), NOT for chain-genesis
// ceremonies. Chain-genesis runs the no-trusted-dealer DKG via
// pulsar.NewDKGSession across the messaging layer.
import (
"crypto/rand"
"errors"
"fmt"
"io"
)
// OrchestrateV03Sign drives the full Round1 → Round2W → Round2Sign
// → AlgebraicAggregate cycle for the t signers in `quorum`, looping
// over FIPS 204 rejection-restart attempts up to maxAttempts. The
// output Signature verifies under unmodified FIPS 204 ML-DSA
// (Class N1 manifesto).
//
// Arguments:
//
// - params: the canonical Pulsar Params for setup.Mode.
// - setup: the v0.3 algebraic-aggregate setup produced by
// DealAlgebraicV03Shares (carries the group public
// key + ρ + tr + A). No master sk inside.
// - msg: the FIPS 204 message bytes.
// - sessionID: a fresh per-Sign session identifier.
// - quorum: the t-element committee (sorted by NodeID).
// - quorumShares: each quorum member's AlgebraicKeyShare in the
// same order as quorum.
// - evalPoints: the Shamir x-coordinates for each quorum
// member; precomputed via V03QuorumEvalPoints.
// - sessionKeys: the pairwise ML-KEM-768 session keys (see
// SymmetricSession) for every pair in the quorum.
// - maxAttempts: cap on FIPS 204 rejection-restart attempts. Pass
// params.MaxRestart for the production default.
// - rng: per-party PRNG. nil falls back to crypto/rand.
//
// Returns the aggregated Signature on success, or an error if no
// attempt within maxAttempts produced a signature that passes the
// FIPS 204 norm checks (||w_0 - c·s_2||, ||z||, ||c·t_0||, hint
// population). The probability of needing > k attempts under
// honest sampling is ~exp(-k/5) per FIPS 204; maxAttempts=256
// gives an abort probability < 2^-512.
//
// This helper does NOT compute sessionKeys for the caller — the
// caller owns the identity layer. For a self-contained one-shot
// API, see OrchestrateV03SignWithKeys below.
func OrchestrateV03Sign(
params *Params,
setup *AlgebraicSetup,
msg []byte,
sessionID [16]byte,
quorum []NodeID,
quorumShares []*AlgebraicKeyShare,
evalPoints []uint32,
sessionKeys map[NodeID]map[NodeID][32]byte,
maxAttempts uint32,
rng io.Reader,
) (*Signature, error) {
if err := params.Validate(); err != nil {
return nil, err
}
if setup == nil {
return nil, ErrAlgebraicNoSetup
}
if setup.Mode != params.Mode {
return nil, ErrModeMismatch
}
threshold := len(quorum)
if threshold == 0 {
return nil, ErrEmptyQuorum
}
if len(quorumShares) != threshold {
return nil, fmt.Errorf("pulsar: quorumShares length %d != quorum length %d",
len(quorumShares), threshold)
}
if len(evalPoints) != threshold {
return nil, fmt.Errorf("pulsar: evalPoints length %d != quorum length %d",
len(evalPoints), threshold)
}
if maxAttempts == 0 {
maxAttempts = params.MaxRestart
}
if rng == nil {
rng = rand.Reader
}
K, _, _ := modeShape(setup.Mode)
for attempt := uint32(0); attempt < maxAttempts; attempt++ {
signers := make([]*AlgebraicThresholdSigner, threshold)
for i := 0; i < threshold; i++ {
signer, err := NewAlgebraicThresholdSigner(
params, setup, sessionID, attempt, quorum, quorumShares[i],
sessionKeys[quorum[i]], msg, rng)
if err != nil {
return nil, fmt.Errorf("pulsar: NewAlgebraicThresholdSigner party %d: %w", i, err)
}
if err := signer.SetQuorumEvalPoints(evalPoints); err != nil {
return nil, fmt.Errorf("pulsar: SetQuorumEvalPoints party %d: %w", i, err)
}
signers[i] = signer
}
r1 := make([]*AlgebraicRound1Message, threshold)
for i, signer := range signers {
m, err := signer.Round1()
if err != nil {
return nil, fmt.Errorf("pulsar: Round1 party %d attempt %d: %w", i, attempt, err)
}
r1[i] = m
}
r2W := make([]*AlgebraicRound2Message, threshold)
for i, signer := range signers {
m, _, err := signer.Round2W(r1)
if err != nil {
return nil, fmt.Errorf("pulsar: Round2W party %d attempt %d: %w", i, attempt, err)
}
r2W[i] = m
}
peerWByParty := make([]map[NodeID]polyVec, threshold)
for i := 0; i < threshold; i++ {
peerW := make(map[NodeID]polyVec, threshold-1)
for j := 0; j < threshold; j++ {
if i == j {
continue
}
peerW[r2W[j].NodeID] = unpackPolyVec(r2W[j].W, K)
}
peerWByParty[i] = peerW
}
r2 := make([]*AlgebraicRound2Message, threshold)
for i, signer := range signers {
m, _, err := signer.Round2Sign(r1, peerWByParty[i])
if err != nil {
return nil, fmt.Errorf("pulsar: Round2Sign party %d attempt %d: %w", i, attempt, err)
}
r2[i] = m
}
sig, err := AlgebraicAggregate(params, setup, msg, sessionID, attempt,
quorum, evalPoints, threshold, r1, r2, sessionKeys)
if err == nil {
return sig, nil
}
if !errors.Is(err, ErrAlgebraicRestart) {
return nil, fmt.Errorf("pulsar: AlgebraicAggregate attempt %d: %w", attempt, err)
}
// Restart with attempt+1.
}
return nil, fmt.Errorf("pulsar: no acceptance within %d FIPS 204 rejection-restart attempts", maxAttempts)
}
// QuorumSessionKeys computes every pairwise ML-KEM-768-derived
// session key for the quorum and returns each party's local view
// (peer -> session key). Required by OrchestrateV03Sign so the
// per-pair Round-1 MAC keys are ephemeral and authenticated under
// each party's long-term ML-DSA-65 identity.
//
// For a quorum of t parties this is t*(t-1)/2 SymmetricSession
// calls. Each SymmetricSession is deterministic from (sid,
// transcript) so two parties agreeing on those inputs derive the
// same key independently in production.
//
// identities MUST cover every NodeID in quorum; missing keys
// produce ErrIdentityKeyMissing.
func QuorumSessionKeys(
quorum []NodeID,
identities map[NodeID]*IdentityKey,
sid [16]byte,
transcript []byte,
) (map[NodeID]map[NodeID][32]byte, error) {
if len(quorum) == 0 {
return nil, ErrEmptyQuorum
}
out := make(map[NodeID]map[NodeID][32]byte, len(quorum))
for _, id := range quorum {
out[id] = make(map[NodeID][32]byte, len(quorum)-1)
}
for i := 0; i < len(quorum); i++ {
aKey, ok := identities[quorum[i]]
if !ok {
return nil, fmt.Errorf("%w: party %x", ErrIdentityKeyMissing, quorum[i][:4])
}
for j := i + 1; j < len(quorum); j++ {
bKey, ok := identities[quorum[j]]
if !ok {
return nil, fmt.Errorf("%w: party %x", ErrIdentityKeyMissing, quorum[j][:4])
}
key, err := SymmetricSession(quorum[i], aKey, quorum[j], bKey, sid, transcript)
if err != nil {
return nil, fmt.Errorf("SymmetricSession %x↔%x: %w",
quorum[i][:4], quorum[j][:4], err)
}
out[quorum[i]][quorum[j]] = key
out[quorum[j]][quorum[i]] = key
}
}
return out, nil
}
+425
View File
@@ -0,0 +1,425 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package pulsar
// wire.go — canonical wire codec for Signature and PublicKey (the
// group public key on the wire) plus a stateless VerifyBytes helper
// that any independent peer can call given only:
//
// - the wire bytes of the group public key (PULG-framed),
// - the bytes of the message that was signed,
// - the wire bytes of the signature (PULS-framed),
//
// and obtain a bool answer with no additional state. This is the
// surface luxfi/threshold/pkg/thresholdd consumes to publish Pulsar
// outputs over a JSON-RPC bus — and the surface independent
// verifiers (other mpcd, bridge nodes, L1 verifier contracts) must
// satisfy.
//
// The Class N1 manifesto of Pulsar (see verify.go, spec/nist-mptc-
// category.tex) is that a Pulsar Signature is bit-identical to a
// single-party FIPS 204 ML-DSA signature on the same (message,
// group public key) tuple. The wire codec preserves this property
// by transporting the FIPS 204 bytes verbatim inside a small
// domain-separated frame. A relying party that strips the PULS /
// PULG frame obtains the FIPS 204 wire bytes that
// cloudflare/circl's mldsa{44,65,87}.Verify accepts unmodified —
// this is what TestPulsar_Wire_FIPS204Verifiable asserts.
//
// Wire frame layout (big-endian throughout):
//
// Signature:
// magic(4) = 'P' 'U' 'L' 'S' = 0x50554C53
// version(2) = 0x0001
// mode(1) = 0x2C | 0x41 | 0x57 (44 | 65 | 87, the FIPS 204 level)
// len(4) = FIPS 204 SignatureSize for mode (2420 | 3309 | 4627)
// payload = len bytes — the FIPS 204 sigEncode(c̃, z, h) output
//
// GroupKey:
// magic(4) = 'P' 'U' 'L' 'G' = 0x50554C47
// version(2) = 0x0001
// mode(1) = 0x2C | 0x41 | 0x57
// len(4) = FIPS 204 PublicKeySize for mode (1312 | 1952 | 2592)
// payload = len bytes — the FIPS 204 (ρ || t_1) encoding
//
// Domain separation: the four-byte magic is distinct from corona's
// CORS / CORG (0x434F5253 / 0x434F5247), so a corona-shaped frame
// fed into pulsar's parser is rejected at the first dispatch and
// vice versa. Distinct magic per type within pulsar 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 204 length to the
// canonical value for the Mode — a malformed frame that claims a
// 4 GiB signature for ModeP65 is rejected without allocating any
// buffer. Any wire-format change (additional field, version flip)
// bumps versionV1.
//
// Trailing garbage policy: STRICT — UnmarshalBinary returns an
// error if any byte remains after the declared frame. Mirrors
// corona/wire.go.
//
// Version-bump rule: any byte-level change to the layout (new
// field, reordered fields, bigger length prefix, additional FIPS
// 204 mode) requires bumping versionV1. The Mode byte is
// independent — adding ModeP44/65/87 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/mldsa/mldsa44"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/cloudflare/circl/sign/mldsa/mldsa87"
)
// Wire-format identifiers. Distinct from 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 (
wireMagicPulsarSig uint32 = 0x50554C53 // "PULS" Pulsar Signature
wireMagicPulsarGroupKey uint32 = 0x50554C47 // "PULG" Pulsar 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("pulsar/wire: magic mismatch")
ErrWireVersionMismatch = errors.New("pulsar/wire: version mismatch")
ErrWireFrameTooShort = errors.New("pulsar/wire: frame too short")
ErrWireFrameRejected = errors.New("pulsar/wire: frame rejected by bounded reader")
ErrWireModeUnknown = errors.New("pulsar/wire: unknown mode byte")
ErrWireLengthMismatch = errors.New("pulsar/wire: declared length does not match FIPS 204 size for mode")
ErrWireTrailingBytes = errors.New("pulsar/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 PULS frame.
//
// The body bytes are the FIPS 204 sigEncode(c̃, z, h) output
// verbatim — Pulsar adds NO envelope around them. Stripping the
// 11-byte header recovers the unmodified FIPS 204 bytes that
// cloudflare/circl's mldsa{44,65,87}.Verify accepts. This is the
// load-bearing property TestPulsar_Wire_FIPS204Verifiable pins.
//
// Returns an error if the Signature's Mode is unknown, its Bytes
// field does not match the FIPS 204 size for the Mode, or the
// Signature receiver is nil.
func (s *Signature) MarshalBinary() ([]byte, error) {
if s == nil {
return nil, errors.New("pulsar: nil Signature")
}
sigSize, err := sigSizeForMode(s.Mode)
if err != nil {
return nil, fmt.Errorf("pulsar/wire: %w", err)
}
if len(s.Bytes) != sigSize {
return nil, fmt.Errorf("pulsar/wire: Signature.Bytes length %d != FIPS 204 size %d for %s",
len(s.Bytes), sigSize, s.Mode)
}
out := make([]byte, 0, wireFixedSigHeader+sigSize)
out = binary.BigEndian.AppendUint32(out, wireMagicPulsarSig)
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 PULS frame.
//
// Validation order is strict: magic, version, mode, length-against-
// FIPS 204-size are all checked BEFORE any payload bytes are read.
// The codec never allocates a payload buffer larger than the canonical
// FIPS 204 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("pulsar: 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("pulsar/wire: read magic: %w", err)
}
if magic != wireMagicPulsarSig {
return fmt.Errorf("%w: got 0x%08x, want 0x%08x", ErrWireMagicMismatch, magic, wireMagicPulsarSig)
}
var version uint16
if err := binary.Read(r, binary.BigEndian, &version); err != nil {
return fmt.Errorf("pulsar/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("pulsar/wire: read mode: %w", err)
}
mode := Mode(modeByte)
sigSize, err := sigSizeForMode(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("pulsar/wire: read length: %w", err)
}
// Bounded read: declared length must equal the canonical FIPS 204
// 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 204 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("pulsar/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
}
// MarshalBinary serialises a PublicKey (used on the wire as the
// group public key) into the canonical PULG frame.
//
// The body bytes are the FIPS 204 (ρ || t_1) public-key encoding
// verbatim — Pulsar adds NO envelope around them. Stripping the
// 11-byte header recovers the unmodified FIPS 204 public-key bytes
// that cloudflare/circl's mldsa{44,65,87}.PublicKey.Unpack accepts.
//
// Returns an error if the PublicKey's Mode is unknown, its Bytes
// field does not match the FIPS 204 size for the Mode, or the
// PublicKey receiver is nil.
func (p *PublicKey) MarshalBinary() ([]byte, error) {
if p == nil {
return nil, errors.New("pulsar: nil PublicKey")
}
pkSize, err := pubKeySizeForMode(p.Mode)
if err != nil {
return nil, fmt.Errorf("pulsar/wire: %w", err)
}
if len(p.Bytes) != pkSize {
return nil, fmt.Errorf("pulsar/wire: PublicKey.Bytes length %d != FIPS 204 size %d for %s",
len(p.Bytes), pkSize, p.Mode)
}
out := make([]byte, 0, wireFixedGroupKeyHeader+pkSize)
out = binary.BigEndian.AppendUint32(out, wireMagicPulsarGroupKey)
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
}
// UnmarshalBinary parses a PublicKey from canonical PULG frame.
//
// Validation order matches Signature.UnmarshalBinary: magic,
// version, mode, length-against-FIPS 204-size are all checked
// BEFORE any payload bytes are read. Trailing bytes after the
// frame are rejected.
func (p *PublicKey) UnmarshalBinary(b []byte) error {
if p == nil {
return errors.New("pulsar: nil PublicKey receiver")
}
if len(b) < wireFixedGroupKeyHeader {
return ErrWireFrameTooShort
}
r := bytes.NewReader(b)
var magic uint32
if err := binary.Read(r, binary.BigEndian, &magic); err != nil {
return fmt.Errorf("pulsar/wire: read magic: %w", err)
}
if magic != wireMagicPulsarGroupKey {
return fmt.Errorf("%w: got 0x%08x, want 0x%08x", ErrWireMagicMismatch, magic, wireMagicPulsarGroupKey)
}
var version uint16
if err := binary.Read(r, binary.BigEndian, &version); err != nil {
return fmt.Errorf("pulsar/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("pulsar/wire: read mode: %w", err)
}
mode := Mode(modeByte)
pkSize, err := pubKeySizeForMode(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("pulsar/wire: read length: %w", err)
}
if int(declared) != pkSize {
return fmt.Errorf("%w: declared %d, FIPS 204 size for %s is %d",
ErrWireLengthMismatch, declared, mode, pkSize)
}
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("pulsar/wire: read payload: %w", err)
}
if r.Len() != 0 {
return fmt.Errorf("%w: %d bytes remaining", ErrWireTrailingBytes, r.Len())
}
p.Mode = mode
p.Bytes = payload
return 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 (the group public key, in a PULG frame) and
// a Signature (in a PULS 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 204 verifier verbatim (no Pulsar envelope) — this
// matches the Class N1 manifesto.
//
// 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 204 Verify with an empty
// context string. Callers needing the FIPS 204 §6.3 context binding
// must transport the context bytes by including them in the
// message argument (canonical convention: ctx || 0x00 || msg). The
// wire codec stays narrow so the dispatcher remains a thin proxy.
func VerifyBytes(gpkBytes, message, sigBytes []byte) bool {
var gk PublicKey
if err := gk.UnmarshalBinary(gpkBytes); err != nil {
return false
}
var sig Signature
if err := sig.UnmarshalBinary(sigBytes); err != nil {
return false
}
if gk.Mode != sig.Mode {
return false
}
return mldsaVerifyMode(gk.Mode, gk.Bytes, message, nil, sig.Bytes)
}
// mldsaVerifyMode is the constant-time FIPS 204 Verify dispatch.
// Kept distinct from sign.go's mldsaVerify so this file is
// self-contained for the wire layer; both paths call into the
// same circl primitives.
func mldsaVerifyMode(mode Mode, packedPk, message, ctx, sig []byte) bool {
switch mode {
case ModeP44:
if len(packedPk) != mldsa44.PublicKeySize || len(sig) != mldsa44.SignatureSize {
return false
}
var pk mldsa44.PublicKey
var pkBuf [mldsa44.PublicKeySize]byte
copy(pkBuf[:], packedPk)
pk.Unpack(&pkBuf)
return mldsa44.Verify(&pk, message, ctx, sig)
case ModeP65:
if len(packedPk) != mldsa65.PublicKeySize || len(sig) != mldsa65.SignatureSize {
return false
}
var pk mldsa65.PublicKey
var pkBuf [mldsa65.PublicKeySize]byte
copy(pkBuf[:], packedPk)
pk.Unpack(&pkBuf)
return mldsa65.Verify(&pk, message, ctx, sig)
case ModeP87:
if len(packedPk) != mldsa87.PublicKeySize || len(sig) != mldsa87.SignatureSize {
return false
}
var pk mldsa87.PublicKey
var pkBuf [mldsa87.PublicKeySize]byte
copy(pkBuf[:], packedPk)
pk.Unpack(&pkBuf)
return mldsa87.Verify(&pk, message, ctx, sig)
default:
return false
}
}
// sigSizeForMode returns the FIPS 204 signature size for the
// declared mode, or ErrWireModeUnknown 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 sigSizeForMode(mode Mode) (int, error) {
switch mode {
case ModeP44:
return mldsa44.SignatureSize, nil
case ModeP65:
return mldsa65.SignatureSize, nil
case ModeP87:
return mldsa87.SignatureSize, nil
default:
return 0, fmt.Errorf("mode 0x%02x", byte(mode))
}
}
// pubKeySizeForMode returns the FIPS 204 public-key size for the
// declared mode.
func pubKeySizeForMode(mode Mode) (int, error) {
switch mode {
case ModeP44:
return mldsa44.PublicKeySize, nil
case ModeP65:
return mldsa65.PublicKeySize, nil
case ModeP87:
return mldsa87.PublicKeySize, nil
default:
return 0, fmt.Errorf("mode 0x%02x", byte(mode))
}
}
+603
View File
@@ -0,0 +1,603 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package pulsar
import (
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"testing"
"github.com/cloudflare/circl/sign/mldsa/mldsa44"
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
"github.com/cloudflare/circl/sign/mldsa/mldsa87"
)
// TestPulsar_Wire_SigRoundtrip checks the Signature wire codec
// round-trips byte-equal for every 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).
func TestPulsar_Wire_SigRoundtrip(t *testing.T) {
for _, mode := range []Mode{ModeP44, ModeP65, ModeP87} {
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))
}
})
}
}
// TestPulsar_Wire_GroupKeyRoundtrip mirrors TestPulsar_Wire_SigRoundtrip
// for PublicKey (the group public key on the wire).
func TestPulsar_Wire_GroupKeyRoundtrip(t *testing.T) {
for _, mode := range []Mode{ModeP44, ModeP65, ModeP87} {
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 := pk.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary: %v", err)
}
if got := len(wire1); got != wireFixedGroupKeyHeader+params.PublicKeySize {
t.Fatalf("wire length %d != header(%d) + payload(%d)",
got, wireFixedGroupKeyHeader, params.PublicKeySize)
}
var parsed PublicKey
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))
}
})
}
}
// TestPulsar_Wire_FIPS204Verifiable is the headline cryptographic
// claim of the wire codec: a Pulsar Signature, after MarshalBinary +
// payload-extraction, verifies under cloudflare/circl's FIPS 204
// mldsa{44,65,87}.Verify with NO Pulsar code path involved on the
// verifier side.
//
// This is the Class N1 manifesto in test form: the threshold
// orchestration layer can hand wire bytes to a relying party that
// has never heard of Pulsar, and that party — with only a FIPS 204
// ML-DSA verifier and the documented PULS / PULG frame — verifies
// the signature.
//
// For every mode we run the single-party deterministic Sign path
// (the production threshold path emits a bit-identical signature,
// proved by TestAlgebraic_FullCycle_n5_t3 and TestAlgebraic_ByteValid;
// the relevant property here is that the WIRE FORMAT preserves the
// FIPS 204 bytes verbatim, which is mode-agnostic and orthogonal to
// whether the signer was single-party or threshold).
func TestPulsar_Wire_FIPS204Verifiable(t *testing.T) {
for _, mode := range []Mode{ModeP44, ModeP65, ModeP87} {
mode := mode
t.Run(mode.String(), func(t *testing.T) {
params := MustParamsFor(mode)
var seed [SeedSize]byte
copy(seed[:], "pulsar-wire-test-deterministic!!")
seed[0] ^= byte(mode) // distinct seed per mode
sk, err := KeyFromSeed(params, seed)
if err != nil {
t.Fatalf("KeyFromSeed: %v", err)
}
msg := []byte("pulsar wire codec — FIPS 204 byte-identity claim")
// Deterministic signing so the test is repeatable.
sig, err := Sign(params, sk, msg, nil, false, nil)
if err != nil {
t.Fatalf("Sign: %v", err)
}
// 1. Sanity: pulsar's own Verify accepts it.
if err := Verify(params, sk.Pub, msg, sig); err != nil {
t.Fatalf("baseline Verify failed before wire: %v", err)
}
// 2. Frame both via the wire codec.
gkWire, err := sk.Pub.MarshalBinary()
if err != nil {
t.Fatalf("Pub.MarshalBinary: %v", err)
}
sigWire, err := sig.MarshalBinary()
if err != nil {
t.Fatalf("sig.MarshalBinary: %v", err)
}
// 3. Extract the FIPS 204 payload from each frame by skipping
// the magic(4) + version(2) + mode(1) + len(4) header. NO
// pulsar package code touches the payload — this is exactly
// what an external relying party with a FIPS 204 verifier
// would do.
fipsPK := extractFIPSPayload(t, gkWire, wireMagicPulsarGroupKey)
fipsSig := extractFIPSPayload(t, sigWire, wireMagicPulsarSig)
// 4. Verify under cloudflare/circl directly. No pulsar code
// path is hit on the verify side.
if !verifyDirectCirc(mode, fipsPK, msg, fipsSig) {
t.Fatalf("cloudflare/circl FIPS 204 Verify rejected the unwrapped Pulsar bytes — wire codec broke byte-identity")
}
// 5. Stateless VerifyBytes path used by thresholdd accepts
// the same bytes.
if !VerifyBytes(gkWire, msg, sigWire) {
t.Fatalf("VerifyBytes rejected a valid signature")
}
// 6. 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 verifyDirectCirc(mode, fipsPK, msg, tamperedFips) {
t.Fatalf("circl FIPS 204 Verify accepted a tampered signature")
}
})
}
}
// extractFIPSPayload strips the wire header from a PULS or PULG
// frame and returns the FIPS 204 payload bytes. Asserts the magic
// matches the expected one. Test-only — production wire callers go
// through (Signature|PublicKey).UnmarshalBinary.
func extractFIPSPayload(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:]
}
// verifyDirectCirc bypasses every Pulsar code path and calls
// cloudflare/circl's FIPS 204 Verify directly. This proves the
// wire codec's body bytes are valid FIPS 204 bytes.
func verifyDirectCirc(mode Mode, pkBytes, msg, sigBytes []byte) bool {
switch mode {
case ModeP44:
if len(pkBytes) != mldsa44.PublicKeySize || len(sigBytes) != mldsa44.SignatureSize {
return false
}
var pk mldsa44.PublicKey
var pkBuf [mldsa44.PublicKeySize]byte
copy(pkBuf[:], pkBytes)
pk.Unpack(&pkBuf)
return mldsa44.Verify(&pk, msg, nil, sigBytes)
case ModeP65:
if len(pkBytes) != mldsa65.PublicKeySize || len(sigBytes) != mldsa65.SignatureSize {
return false
}
var pk mldsa65.PublicKey
var pkBuf [mldsa65.PublicKeySize]byte
copy(pkBuf[:], pkBytes)
pk.Unpack(&pkBuf)
return mldsa65.Verify(&pk, msg, nil, sigBytes)
case ModeP87:
if len(pkBytes) != mldsa87.PublicKeySize || len(sigBytes) != mldsa87.SignatureSize {
return false
}
var pk mldsa87.PublicKey
var pkBuf [mldsa87.PublicKeySize]byte
copy(pkBuf[:], pkBytes)
pk.Unpack(&pkBuf)
return mldsa87.Verify(&pk, msg, nil, sigBytes)
default:
return false
}
}
// TestPulsar_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 TestPulsar_Wire_RejectMalformed(t *testing.T) {
validSig := func(t *testing.T) []byte {
t.Helper()
mode := ModeP65
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",
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 PULS with PULG.
binary.BigEndian.PutUint32(w[0:4], wireMagicPulsarGroupKey)
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 44, 65, or 87
return w
},
want: ErrWireModeUnknown,
},
{
name: "length-mismatch-mode-65",
build: func(t *testing.T) []byte {
w := validSig(t)
// Declared length 999 for ModeP65 — not the canonical 3309.
binary.BigEndian.PutUint32(w[7:11], 999)
return w
},
want: ErrWireLengthMismatch,
},
{
name: "length-oversized-vs-buffer",
build: func(t *testing.T) []byte {
// Manually craft: PULS, v1, mode=44, declared=4 GiB.
w := make([]byte, wireFixedSigHeader)
binary.BigEndian.PutUint32(w[0:4], wireMagicPulsarSig)
binary.BigEndian.PutUint16(w[4:6], wireVersionV1)
w[6] = byte(ModeP44)
binary.BigEndian.PutUint32(w[7:11], 0xFFFFFFFF)
return w
},
// 4 GiB is not equal to FIPS 204 mldsa44 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 204 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)
}
})
}
}
// TestPulsar_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 PULS and PULG must be effective.
func TestPulsar_Wire_GroupKeyRejectMalformed(t *testing.T) {
validGk := func(t *testing.T) []byte {
t.Helper()
mode := ModeP65
params := MustParamsFor(mode)
body := make([]byte, params.PublicKeySize)
p := &PublicKey{Mode: mode, Bytes: body}
w, err := p.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: "sig-magic-into-groupkey",
build: func(t *testing.T) []byte {
w := validGk(t)
binary.BigEndian.PutUint32(w[0:4], wireMagicPulsarSig)
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) {
var gk PublicKey
err := gk.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)
}
})
}
}
// TestPulsar_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 TestPulsar_Wire_VerifyBytes_RejectsCrossSlot(t *testing.T) {
mode := ModeP65
params := MustParamsFor(mode)
var seed [SeedSize]byte
copy(seed[:], "pulsar-wire-cross-slot-test-32!!")
sk, err := KeyFromSeed(params, seed)
if err != nil {
t.Fatalf("KeyFromSeed: %v", err)
}
msg := []byte("cross-slot rejection")
sig, err := Sign(params, sk, msg, nil, false, nil)
if err != nil {
t.Fatalf("Sign: %v", err)
}
gkWire, err := sk.Pub.MarshalBinary()
if err != nil {
t.Fatalf("gk MarshalBinary: %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")
}
}
// TestPulsar_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
// ModeP65 signature verifying under a ModeP44 group key.
func TestPulsar_Wire_ModeMismatch_Rejected(t *testing.T) {
// Build a valid sig (ModeP65) and a valid gk (ModeP44).
skP65, err := KeyFromSeed(MustParamsFor(ModeP65), [SeedSize]byte{1})
if err != nil {
t.Fatalf("KeyFromSeed P65: %v", err)
}
skP44, err := KeyFromSeed(MustParamsFor(ModeP44), [SeedSize]byte{2})
if err != nil {
t.Fatalf("KeyFromSeed P44: %v", err)
}
msg := []byte("mode mismatch test")
sigP65, err := Sign(MustParamsFor(ModeP65), skP65, msg, nil, false, nil)
if err != nil {
t.Fatalf("Sign P65: %v", err)
}
gkP44Wire, err := skP44.Pub.MarshalBinary()
if err != nil {
t.Fatalf("gk P44 MarshalBinary: %v", err)
}
sigP65Wire, err := sigP65.MarshalBinary()
if err != nil {
t.Fatalf("sig P65 MarshalBinary: %v", err)
}
if VerifyBytes(gkP44Wire, msg, sigP65Wire) {
t.Fatalf("VerifyBytes accepted cross-mode (gk=P44, sig=P65)")
}
}
// TestPulsar_Wire_MarshalSafetyChecks ensures Marshal refuses
// inputs that would emit malformed frames: nil receiver, unknown
// mode, length-vs-mode mismatch.
func TestPulsar_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) {
var p *PublicKey
if _, err := p.MarshalBinary(); err == nil {
t.Fatal("nil PublicKey.MarshalBinary did not return error")
}
})
t.Run("sig-unknown-mode", func(t *testing.T) {
s := &Signature{Mode: Mode(0xAA), Bytes: make([]byte, 3309)}
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: ModeP65, 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: ModeP65, Bytes: make([]byte, 99)}
if _, err := p.MarshalBinary(); err == nil {
t.Fatal("wrong-length PublicKey.MarshalBinary did not return error")
}
})
}