Files
zooqueen 861db0f621 a request nobody hears is not a quorum problem, and a secret needs a way in
Two halves of opening a secret were missing, and both showed up the first time
this ran against the real ring rather than an in-process one.

The request reached no party. Parties subscribed to "mpc.reveal_request.*" and
the requester published to "mpc.reveal_request.<session>". This transport
delivers by exact map lookup on the topic — it has no wildcards — so the
subscription matched nothing, every node stayed silent, and the opening failed
as "0 of 2 answered" with all three shares present and nobody having been asked.
That reads like a quorum fault and is a routing one. Both sides name one literal
now; the session rides in the body, where a reader wants it anyway. The pattern
came from the NATS-era topics next door, where wildcards do work.

And nothing could seal. mpcrek requires a sealed root to bring and no tool in
the estate produced one, so the protocol had no beginning. reveal.Seal takes the
published group key and a secret — no share, no node, no round, because writing
to the committee needs none of that — and `mpcd seal` is that from a shell, with
the secret on stdin so it never becomes an argument other processes can read.

Sealing to the identity is refused. It decodes as a valid point, so the decoder
lets it through, and every answer verifies against it: a secret sealed there is
readable by anyone. A test asked for that and the first version did not do it.
2026-08-21 00:57:48 -07:00

60 lines
2.1 KiB
Go

// Copyright © 2026 Lux Industries Inc. All rights reserved.
package reveal
import (
"crypto/rand"
"errors"
"fmt"
"github.com/luxfi/threshold/pkg/math/curve"
tr "github.com/luxfi/threshold/protocols/reveal"
)
// PublicKeySize is the length of the group key a ring publishes for a wallet:
// one compressed Ed25519 point.
const PublicKeySize = 32
// ErrBadPublicKey reports a group key that is not a point on the curve.
var ErrBadPublicKey = errors.New("reveal: not a group key")
// Seal writes a secret only a quorum can read.
//
// It is the half of this protocol that needs no ring. Enrolling a secret takes
// the group key and nothing else — no share, no node, no round — so whoever
// holds a secret can seal it where it already lives and hand over ciphertext.
// Reading it back is the part that needs the committee, and that asymmetry is
// the whole point: a secret can be given to the ring without ever being shown
// to it.
//
// publicKey is the 32 bytes the keygen published for the wallet (eddsa_pub_key,
// and what PublicKey returns on a node). The result is opened by Combine from
// enough Answers, and by nothing else.
func Seal(publicKey, secret []byte) ([]byte, error) {
if len(publicKey) != PublicKeySize {
return nil, fmt.Errorf("%w: %d bytes, want %d", ErrBadPublicKey, len(publicKey), PublicKeySize)
}
if len(secret) == 0 {
return nil, errors.New("reveal: sealing nothing")
}
point := curve.Ed25519{}.NewPoint()
if err := point.UnmarshalBinary(publicKey); err != nil {
return nil, fmt.Errorf("%w: %v", ErrBadPublicKey, err)
}
// The identity is a VALID encoding, so the decoder lets it through. It is
// the one point that must not be sealed to: every answer verifies against
// it, so anyone could open the result. Refused here, under the same error
// as any other key that cannot be sealed to, because a caller deciding what
// to do about a bad key should not have to tell the two apart.
if point.IsIdentity() {
return nil, fmt.Errorf("%w: the identity", ErrBadPublicKey)
}
ct, err := tr.Encrypt(rand.Reader, point, secret)
if err != nil {
return nil, fmt.Errorf("reveal: seal: %w", err)
}
return ct.MarshalBinary()
}