genesis Ed25519 verification — working against live mainnet + preprod
- verify package: ComputeProtocolMessageHash mirrors Rust's BTreeMap-ordered SHA256 over key||value concatenation (enum declaration order, not alpha) - DecodeGenesisVerifyKey unpacks Mithril's 'hex of ASCII [b0,b1,...,b31]' wrapping convention; also accepts plain 64-char hex - Genesis() verifies the Ed25519 signature over the ASCII BYTES of the signed_message HEX string (critical subtlety from upstream) - networks: real genesis vkeys for mainnet + preprod + preview from mithril-infra/configuration/*/genesis.vkey - cmd: 'verify genesis' walks head→genesis chain, verifies the terminal cert; 'verify head' and 'verify <hash>' also wired; JSON output supported - exit codes honored: 3 network, 4 integrity, 5 bad sig verified: mainnet genesis cert 25acfcfe… epoch 539 Ed25519 ✓ preprod genesis cert 69bc3bdf… epoch 196 Ed25519 ✓ next: STM BLS12-381 aggregate verification (the big one)
This commit is contained in:
parent
326d75d91a
commit
97a9434106
3 changed files with 273 additions and 34 deletions
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/aggregator"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/aggregator"
|
||||||
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/artifact"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/artifact"
|
||||||
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/networks"
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/networks"
|
||||||
|
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/verify"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "0.0.3-dev"
|
const version = "0.0.3-dev"
|
||||||
|
|
@ -264,8 +265,116 @@ func cmdDownload(ctx context.Context, args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdVerify(ctx context.Context, args []string) int {
|
func cmdVerify(ctx context.Context, args []string) int {
|
||||||
fmt.Fprintln(os.Stderr, "verify: not yet implemented (STM BLS sprint pending)")
|
fs := flag.NewFlagSet("verify", flag.ExitOnError)
|
||||||
return 1
|
asJSON := fs.Bool("json", false, "emit structured JSON")
|
||||||
|
n, rest, err := resolveNetwork(fs, args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
return exitUsage
|
||||||
|
}
|
||||||
|
if len(rest) == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "verify: cert hash required (or 'head' / 'genesis')")
|
||||||
|
return exitUsage
|
||||||
|
}
|
||||||
|
mode := rest[0] // "head" = verify head cert (STM, not yet), "genesis" = walk chain + verify genesis, or a specific hash
|
||||||
|
c := aggregator.New(n.AggregatorURL)
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case "genesis":
|
||||||
|
return runVerifyGenesis(ctx, c, n, *asJSON)
|
||||||
|
case "head":
|
||||||
|
return runVerifyHead(ctx, c, n, *asJSON)
|
||||||
|
default:
|
||||||
|
// Treat as a literal cert hash: fetch + verify
|
||||||
|
return runVerifySingle(ctx, c, n, mode, *asJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVerifyGenesis(ctx context.Context, c *aggregator.Client, n networks.Network, asJSON bool) int {
|
||||||
|
// Find the head snapshot's cert, walk to genesis, verify Ed25519 on the genesis cert.
|
||||||
|
snap, err := resolveSnapshot(ctx, c, "latest")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "resolve:", err)
|
||||||
|
return exitNetwork
|
||||||
|
}
|
||||||
|
chain, err := c.CertChain(ctx, snap.CertificateHash, 2048)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "chain:", err)
|
||||||
|
return exitNetwork
|
||||||
|
}
|
||||||
|
if len(chain) == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "empty chain")
|
||||||
|
return exitGeneric
|
||||||
|
}
|
||||||
|
gen := chain[len(chain)-1]
|
||||||
|
if gen.GenesisSignature == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "tail of chain is not a genesis certificate")
|
||||||
|
return exitGeneric
|
||||||
|
}
|
||||||
|
return verifyGenesisCert(n, gen, asJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVerifyHead(ctx context.Context, c *aggregator.Client, n networks.Network, asJSON bool) int {
|
||||||
|
snap, err := resolveSnapshot(ctx, c, "latest")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "resolve:", err)
|
||||||
|
return exitNetwork
|
||||||
|
}
|
||||||
|
return runVerifySingle(ctx, c, n, snap.CertificateHash, asJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runVerifySingle(ctx context.Context, c *aggregator.Client, n networks.Network, hash string, asJSON bool) int {
|
||||||
|
cert, err := c.GetCertificate(ctx, hash)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "cert:", err)
|
||||||
|
return exitNetwork
|
||||||
|
}
|
||||||
|
if cert.GenesisSignature != "" {
|
||||||
|
return verifyGenesisCert(n, cert, asJSON)
|
||||||
|
}
|
||||||
|
// STM cert
|
||||||
|
if asJSON {
|
||||||
|
return emitJSON(map[string]any{
|
||||||
|
"cert_hash": cert.Hash,
|
||||||
|
"kind": "stm",
|
||||||
|
"verified": false,
|
||||||
|
"error": "STM BLS verification not yet implemented",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, "cert is STM-signed; STM verification not yet implemented")
|
||||||
|
return exitGeneric
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyGenesisCert(n networks.Network, cert *aggregator.Certificate, asJSON bool) int {
|
||||||
|
vk, err := verify.DecodeGenesisVerifyKey(n.GenesisVerifyKey)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "decode genesis key:", err)
|
||||||
|
return exitGeneric
|
||||||
|
}
|
||||||
|
err = verify.GenesisFromJSON(vk, cert.SignedMessage, cert.GenesisSignature, cert.ProtocolMessage)
|
||||||
|
if asJSON {
|
||||||
|
result := map[string]any{
|
||||||
|
"cert_hash": cert.Hash,
|
||||||
|
"kind": "genesis",
|
||||||
|
"signed_message": cert.SignedMessage,
|
||||||
|
"epoch": cert.Epoch,
|
||||||
|
"verified": err == nil,
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
result["error"] = err.Error()
|
||||||
|
}
|
||||||
|
code := emitJSON(result)
|
||||||
|
if err != nil {
|
||||||
|
return exitBadSig
|
||||||
|
}
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "verify genesis:", err)
|
||||||
|
return exitBadSig
|
||||||
|
}
|
||||||
|
fmt.Printf("genesis cert %s epoch=%d Ed25519 ✓\n", cert.Hash, cert.Epoch)
|
||||||
|
return exitOK
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdCert(ctx context.Context, args []string) int {
|
func cmdCert(ctx context.Context, args []string) int {
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,13 @@ var (
|
||||||
Preprod = Network{
|
Preprod = Network{
|
||||||
Name: "preprod",
|
Name: "preprod",
|
||||||
AggregatorURL: "https://aggregator.release-preprod.api.mithril.network/aggregator",
|
AggregatorURL: "https://aggregator.release-preprod.api.mithril.network/aggregator",
|
||||||
GenesisVerifyKey: "5b3132372c37332c3132342c3136312c31362c38372c3133332c3136372c3135352c3138362c3138372c36372c3231322c37382c3131372c3230352c3234362c35322c35312c31372c3138302c38372c3130342c3139362c3131332c3130332c3239355d", // placeholder — replace with known-good key at implementation time
|
GenesisVerifyKey: "5b3132372c37332c3132342c3136312c362c3133372c3133312c3231332c3230372c3131372c3139382c38352c3137362c3139392c3136322c3234312c36382c3132332c3131392c3134352c31332c3233322c3234332c34392c3232392c322c3234392c3230352c3230352c33392c3233352c34345d",
|
||||||
}
|
}
|
||||||
Preview = Network{
|
Preview = Network{
|
||||||
Name: "preview",
|
Name: "preview",
|
||||||
AggregatorURL: "https://aggregator.pre-release-preview.api.mithril.network/aggregator",
|
AggregatorURL: "https://aggregator.pre-release-preview.api.mithril.network/aggregator",
|
||||||
GenesisVerifyKey: "5b3132372c37332c3132342c3136312c31362c38372c3133332c3136372c3135352c3138362c3138372c36372c3231322c37382c3131372c3230352c3234362c35322c35312c31372c3138302c38372c3130342c3139362c3131332c3130332c3239355d", // placeholder
|
// Pre-release/preview uses the same genesis key as preprod.
|
||||||
|
GenesisVerifyKey: "5b3132372c37332c3132342c3136312c362c3133372c3133312c3231332c3230372c3131372c3139382c38352c3137362c3139392c3136322c3234312c36382c3132332c3131392c3134352c31332c3233322c3234332c34392c3232392c322c3234392c3230352c3230352c33392c3233352c34345d",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,60 +2,189 @@
|
||||||
//
|
//
|
||||||
// Two layers of verification exist in Mithril:
|
// Two layers of verification exist in Mithril:
|
||||||
//
|
//
|
||||||
// 1. The genesis certificate is signed by a static Ed25519 key baked into
|
// 1. The genesis certificate is signed by a static Ed25519 key baked into
|
||||||
// the client (per-network). This bootstraps trust into the STM protocol.
|
// the client (per-network). This bootstraps trust into the STM protocol.
|
||||||
// 2. Subsequent certificates carry an STM (Stake-based Threshold Multi-
|
// 2. Subsequent certificates carry an STM (Stake-based Threshold Multi-
|
||||||
// signature) aggregate signature over BLS12-381. Verification requires
|
// signature) aggregate signature over BLS12-381.
|
||||||
// the stake distribution snapshot plus the signers' verification keys
|
|
||||||
// and their individual signature shares.
|
|
||||||
//
|
//
|
||||||
// v1 scope: genesis Ed25519 verification only. STM/BLS verification is a
|
// Ed25519 (genesis) verification is fully implemented here. STM verification
|
||||||
// separate follow-on milestone — it is the bulk of the cryptographic work
|
// is stubbed pending the BLS crypto sprint.
|
||||||
// in this project.
|
|
||||||
package verify
|
package verify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotGenesis = errors.New("certificate is not a genesis certificate")
|
ErrNotGenesis = errors.New("certificate is not a genesis certificate")
|
||||||
ErrBadSignature = errors.New("genesis signature verification failed")
|
ErrBadSignature = errors.New("genesis signature verification failed")
|
||||||
|
ErrSignedMessageHash = errors.New("signed_message does not match SHA256(protocol_message)")
|
||||||
ErrSTMNotImplemented = errors.New("STM signature verification not implemented yet")
|
ErrSTMNotImplemented = errors.New("STM signature verification not implemented yet")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Genesis verifies that the certificate was signed by the network's genesis
|
// The Mithril enum order on ProtocolMessagePartKey — BTreeMap iteration
|
||||||
// verification key. signedPayload is the exact bytes the aggregator stated
|
// follows variant declaration order, which is what compute_hash relies on.
|
||||||
// were signed (derived from the certificate's protocol_message, not this
|
// Keep this aligned with mithril-common/src/entities/protocol_message.rs.
|
||||||
// function's job to construct).
|
var messagePartOrder = []string{
|
||||||
func Genesis(verifyKeyHex, genesisSignatureHex string, signedPayload []byte) error {
|
"snapshot_digest",
|
||||||
pkHex, err := hex.DecodeString(verifyKeyHex)
|
"cardano_transactions_merkle_root",
|
||||||
if err != nil {
|
"cardano_blocks_transactions_merkle_root",
|
||||||
return fmt.Errorf("decode verify key: %w", err)
|
"next_aggregate_verification_key",
|
||||||
|
"next_protocol_parameters",
|
||||||
|
"current_epoch",
|
||||||
|
"latest_block_number",
|
||||||
|
"cardano_blocks_transactions_block_number_offset",
|
||||||
|
"cardano_stake_distribution_epoch",
|
||||||
|
"cardano_stake_distribution_merkle_root",
|
||||||
|
"cardano_database_merkle_root",
|
||||||
|
"next_aggregate_verification_key_snark",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComputeProtocolMessageHash mirrors Rust's ProtocolMessage::compute_hash:
|
||||||
|
// SHA256 over each (key_string || value_string) concatenated in
|
||||||
|
// BTreeMap order (Rust enum declaration order, not alphabetical).
|
||||||
|
// Returns the lowercase-hex form so it can be compared to the
|
||||||
|
// signed_message field directly.
|
||||||
|
func ComputeProtocolMessageHash(messageParts map[string]string) string {
|
||||||
|
h := sha256.New()
|
||||||
|
// iterate in canonical order
|
||||||
|
ordinal := make(map[string]int, len(messagePartOrder))
|
||||||
|
for i, k := range messagePartOrder {
|
||||||
|
ordinal[k] = i
|
||||||
}
|
}
|
||||||
// Mithril genesis keys are serialized as hex(ascii-of-byte-array-literal),
|
keys := make([]string, 0, len(messageParts))
|
||||||
// e.g. "[191,66,140,...]" → outer hex → inner ASCII → parse. The real decoder
|
for k := range messageParts {
|
||||||
// will unpack this; for now accept a raw 32-byte hex as well.
|
if _, known := ordinal[k]; known {
|
||||||
pk := ed25519.PublicKey(pkHex)
|
keys = append(keys, k)
|
||||||
if len(pk) != ed25519.PublicKeySize {
|
} else {
|
||||||
return fmt.Errorf("verify key wrong size: got %d, want %d", len(pk), ed25519.PublicKeySize)
|
// Unknown key — placed at end, alphabetical. Safer than
|
||||||
|
// silently dropping; any future key we haven't mirrored yet
|
||||||
|
// still feeds into the digest, matching Rust's forward-
|
||||||
|
// compatibility.
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
|
oi, iOK := ordinal[keys[i]]
|
||||||
|
oj, jOK := ordinal[keys[j]]
|
||||||
|
switch {
|
||||||
|
case iOK && jOK:
|
||||||
|
return oi < oj
|
||||||
|
case iOK:
|
||||||
|
return true
|
||||||
|
case jOK:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return keys[i] < keys[j]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
for _, k := range keys {
|
||||||
|
h.Write([]byte(k))
|
||||||
|
h.Write([]byte(messageParts[k]))
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtocolMessage is the subset of the Mithril protocol_message JSON shape
|
||||||
|
// that verification consumes.
|
||||||
|
type ProtocolMessage struct {
|
||||||
|
MessageParts map[string]string `json:"message_parts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeGenesisVerifyKey unpacks the Mithril genesis verification key from
|
||||||
|
// its serialized form. The key is stored as:
|
||||||
|
//
|
||||||
|
// hex( ascii( "[b0,b1,...,b31]" ) )
|
||||||
|
//
|
||||||
|
// i.e. the Ed25519 public key's 32 raw bytes are written as a Rust-debug
|
||||||
|
// byte-slice literal, converted to ASCII bytes, then hex-encoded. We
|
||||||
|
// strip both layers and return the 32-byte public key.
|
||||||
|
//
|
||||||
|
// Plain 64-char hex of the raw key is also accepted (some distributions
|
||||||
|
// ship the key that way).
|
||||||
|
func DecodeGenesisVerifyKey(encoded string) (ed25519.PublicKey, error) {
|
||||||
|
// Fast-path: plain 64-char hex of 32 raw bytes.
|
||||||
|
if len(encoded) == 2*ed25519.PublicKeySize {
|
||||||
|
if b, err := hex.DecodeString(encoded); err == nil && len(b) == ed25519.PublicKeySize {
|
||||||
|
return ed25519.PublicKey(b), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Standard path: hex-of-ASCII-of-"[a,b,c,...]".
|
||||||
|
inner, err := hex.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode outer hex: %w", err)
|
||||||
|
}
|
||||||
|
s := strings.TrimSpace(string(inner))
|
||||||
|
s = strings.TrimPrefix(s, "[")
|
||||||
|
s = strings.TrimSuffix(s, "]")
|
||||||
|
parts := strings.Split(s, ",")
|
||||||
|
if len(parts) != ed25519.PublicKeySize {
|
||||||
|
return nil, fmt.Errorf("genesis key literal has %d parts, want %d", len(parts), ed25519.PublicKeySize)
|
||||||
|
}
|
||||||
|
out := make([]byte, ed25519.PublicKeySize)
|
||||||
|
for i, p := range parts {
|
||||||
|
n, err := strconv.Atoi(strings.TrimSpace(p))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse byte %d: %w", i, err)
|
||||||
|
}
|
||||||
|
if n < 0 || n > 255 {
|
||||||
|
return nil, fmt.Errorf("byte %d out of range: %d", i, n)
|
||||||
|
}
|
||||||
|
out[i] = byte(n)
|
||||||
|
}
|
||||||
|
return ed25519.PublicKey(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Genesis verifies a genesis certificate:
|
||||||
|
// - signedMessageHex must equal SHA256(protocolMessage) in hex
|
||||||
|
// - the Ed25519 signature (64 bytes hex) over the ASCII bytes of
|
||||||
|
// signedMessageHex must verify against verifyKey
|
||||||
|
//
|
||||||
|
// Returns nil on success, a typed error on any failure. Matches the
|
||||||
|
// upstream `verify_genesis_certificate` semantics (integrity →
|
||||||
|
// signed_message → signature → epoch-match; the epoch check is the
|
||||||
|
// caller's job since it needs the full cert context).
|
||||||
|
func Genesis(verifyKey ed25519.PublicKey, signedMessageHex, genesisSignatureHex string, pm ProtocolMessage) error {
|
||||||
|
computed := ComputeProtocolMessageHash(pm.MessageParts)
|
||||||
|
if computed != signedMessageHex {
|
||||||
|
return fmt.Errorf("%w: got %s, want %s", ErrSignedMessageHash, computed, signedMessageHex)
|
||||||
}
|
}
|
||||||
sig, err := hex.DecodeString(genesisSignatureHex)
|
sig, err := hex.DecodeString(genesisSignatureHex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("decode signature: %w", err)
|
return fmt.Errorf("decode signature: %w", err)
|
||||||
}
|
}
|
||||||
if !ed25519.Verify(pk, signedPayload, sig) {
|
if len(sig) != ed25519.SignatureSize {
|
||||||
|
return fmt.Errorf("signature wrong size: got %d, want %d", len(sig), ed25519.SignatureSize)
|
||||||
|
}
|
||||||
|
// Per upstream: verify over signed_message.as_bytes() — the ASCII
|
||||||
|
// bytes of the hex string, not the 32 raw digest bytes.
|
||||||
|
if !ed25519.Verify(verifyKey, []byte(signedMessageHex), sig) {
|
||||||
return ErrBadSignature
|
return ErrBadSignature
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// STM verifies a non-genesis certificate's aggregate BLS signature against
|
// GenesisFromJSON is a convenience wrapper when the caller has the raw
|
||||||
// the stake distribution. Stub — implementation target: Mithril STM paper
|
// JSON protocol_message (as the aggregator returns).
|
||||||
// §5 (signing protocol) + §6 (aggregation) using a BLS12-381 library.
|
func GenesisFromJSON(verifyKey ed25519.PublicKey, signedMessageHex, genesisSignatureHex string, protocolMessageJSON []byte) error {
|
||||||
func STM(protocolMessage, multiSignature []byte, stakeDistribution any) error {
|
var pm ProtocolMessage
|
||||||
|
if err := json.Unmarshal(protocolMessageJSON, &pm); err != nil {
|
||||||
|
return fmt.Errorf("parse protocol_message: %w", err)
|
||||||
|
}
|
||||||
|
return Genesis(verifyKey, signedMessageHex, genesisSignatureHex, pm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// STM verifies a non-genesis certificate's aggregate BLS signature.
|
||||||
|
// Stub — target is Mithril STM paper §5 (signing) + §6 (aggregation)
|
||||||
|
// using gnark-crypto's bls12-381 primitives.
|
||||||
|
func STM(protocolMessageJSON, multiSignature []byte, avk any) error {
|
||||||
return ErrSTMNotImplemented
|
return ErrSTMNotImplemented
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue