Files
zeekay 8cf453d4b0 chains: a signature of the wrong width is refused, not reshaped
warpmsg is the one place bridgevm, mpcvm and zkvm turn a payload into a signed
cross-chain envelope. It copied whatever the signer returned into a fixed
96-byte array without checking the length. Signer is an interface, so that
width is a runtime fact: a short signature was silently zero-padded and a long
one silently truncated, and either way the envelope came out well-formed and
unverifiable -- a message the sender believes it sent and no receiver can ever
accept, with no error anywhere.

oraclevm's accel host documented an error for a nil VM and never checked,
deferring the failure to whichever method the operator called first.
2026-08-28 10:56:44 -07:00

66 lines
2.5 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package warpmsg is the single, canonical place in chains that builds,
// signs, and wraps a single-signer Warp message.
//
// Every VM that emits a cross-chain Warp message — bridgevm reshare gossip,
// zkvm FHE task callbacks, mpcvm decryption fulfillments — calls
// BuildSigned. The build→sign→wrap sequence lives here and nowhere else, so
// there is exactly one way to turn (networkID, sourceChainID, payload) into a
// transmittable signed Warp envelope.
package warpmsg
import (
"fmt"
"github.com/luxfi/ids"
"github.com/luxfi/warp"
)
// BuildSigned builds a Warp content Message for (networkID, sourceChainID,
// payload), signs it with the node's BLS key over the Beam domain, and wraps
// the signature into a single-signer Envelope.
//
// The signer is the local node's warp.Signer (BLS); it signs
// BeamSigningBytes(message.ID()). The resulting signature is placed at
// validator bit index 0 — the single-signer convention every chains VM uses
// before the receiver aggregates and verifies the Beam BitSetSignature
// against the canonical validator set (warp.VerifyEnvelope). The returned
// Envelope is therefore the complete, transmittable, verifiable signed
// message: env.Bytes() for the wire, env.Message for the authenticated
// content.
func BuildSigned(signer warp.Signer, networkID uint32, sourceChainID ids.ID, payload []byte) (*warp.Envelope, error) {
msg, err := warp.NewMessage(networkID, sourceChainID, payload)
if err != nil {
return nil, fmt.Errorf("build warp message: %w", err)
}
sigBytes, err := signer.Sign(msg)
if err != nil {
return nil, fmt.Errorf("sign warp message: %w", err)
}
// Signer is an interface, so the width of what it returns is not a
// compile-time fact. Copying it into a fixed array without checking pads a
// short signature with zeros and silently truncates a long one, and either
// way the envelope is well-formed and unverifiable — a cross-chain message
// the sender believes it sent and no receiver can accept. Refuse instead.
if len(sigBytes) != warp.SignatureLen {
return nil, fmt.Errorf("sign warp message: signature is %d bytes, want %d",
len(sigBytes), warp.SignatureLen)
}
var sig [warp.SignatureLen]byte
copy(sig[:], sigBytes)
signers := warp.NewBitSet()
signers.Add(0)
env, err := warp.NewEnvelope(msg, warp.NewBitSetSignature(signers, sig), nil, nil)
if err != nil {
return nil, fmt.Errorf("wrap warp envelope: %w", err)
}
return env, nil
}