mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
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.
60 lines
2.1 KiB
Go
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()
|
|
}
|