diff --git a/.changelog/3674.feature.md b/.changelog/3674.feature.md new file mode 100644 index 00000000000..1dfc4ce5244 --- /dev/null +++ b/.changelog/3674.feature.md @@ -0,0 +1,6 @@ +go/beacon: Backport the beacon key generation/registration + +To prepare for the future migration to a PVSS based beacon scheme, every +node should generate and register a elliptic curve point in advance of +the migration. This commit selectively backports the required logic for +a hopefully smooth transition. diff --git a/go/common/crypto/pvss/pvss.go b/go/common/crypto/pvss/pvss.go new file mode 100644 index 00000000000..4d2a94b3253 --- /dev/null +++ b/go/common/crypto/pvss/pvss.go @@ -0,0 +1,528 @@ +// Package pvss implements a PVSS backed commit-reveal scheme loosely +// based on the Scalable Randomness Attested by Public Entities +// protocol by Casudo and David. +// +// In practice this implementation omits the things that make SCRAPE +// scalable/fast, and is just a consensus backed PVSS based beacon. +// The differences are as follows: +// +// * The coding theory based share verification mechanism is not +// implemented. The check is as in Schoenmakers' paper. This +// could be added at a future date for a performance gain. +// +// * The commit/reveal based fast path that skips having to recover +// each participant's secret if they submitted a protocol level +// reveal is omitted. It is possible to game the system by +// publishing shares for one secret and a commitment for another +// secret, and then choosing to reveal or not after everyone else +// has revealed. While this behavior is detectable, it either +// involves having to recover the secret from the shares anyway +// rendering the optimization moot, or having a userbase that +// understands that slashing is integral to the security of the +// system. +// +// See: https://eprint.iacr.org/2017/216.pdf +package pvss + +import ( + "fmt" + "math" + "sort" + + "go.dedis.ch/kyber/v3" + "go.dedis.ch/kyber/v3/group/nist" + "go.dedis.ch/kyber/v3/share" + "go.dedis.ch/kyber/v3/share/pvss" +) + +const maxRetries = 3 + +var ( + // Yes, this uses the NIST P-256 curve, due to vastly superior + // performance compared to kyber's Ed25519 implementation. In + // theory Ed25519 should be faster, but the runtime library's + // P-256 scalar multiply is optimized, and kyber's Ed25519 + // is basically ref10. + suite = nist.NewBlakeSHA256P256() + + basePoint = suite.Point().Pick(suite.XOF([]byte("oasis-core/pvss: base point"))) + errVerifyOnly = fmt.Errorf("pvss: instance is verify only") +) + +type dealerState struct { + scalar kyber.Scalar + point kyber.Point + index int +} + +// Config is the configuration for an execution of the PVSS protocol. +type Config struct { + // PrivateKey is the scalar to use as the private key. + PrivateKey *Scalar + + // Participants is the vector of public keys of all participants + // in the protocol. + // + // Note: This must be in consistent order across all participants, + // and include the public point generated from `PrivateKey`, if + // this config is for a participant. + Participants []Point + + // Threshold is the threshold to use for specifying the + // minimum number of commits and reveals required for the + // protocol to proceed. This value is also used as the threshold + // for the underling PVSS scheme. + Threshold int +} + +// Instance is an instance of the PVSS protocol. +type Instance struct { + Participants []Point `json:"participants"` + Commits map[int]*CommitState `json:"commits"` + Reveals map[int]*Reveal `json:"reveals"` + DecryptedShares map[int]map[int]*PubVerShare `json:"decrypted_shares"` + Threshold int `json:"threshold"` + + dealerState *dealerState + cachedParticipants []kyber.Point +} + +// SetScalar sets the private scalar belonging to an instance. Under +// most circumstances this will be handled by the constructor. +func (inst *Instance) SetScalar(privateKey *Scalar) error { + if privateKey == nil { + return fmt.Errorf("pvss: privateKey is nil") + } + if err := privateKey.isWellFormed(); err != nil { + return fmt.Errorf("pvss: invalid private key: %w", err) + } + if inst.dealerState != nil { + return fmt.Errorf("pvss: private key for dealer already set") + } + + var ( + scalar = privateKey.Inner() + point = suite.Point().Mul(scalar, nil) + selfIdx = -1 + ) + for idx, publicKey := range inst.participants() { + if publicKey.Equal(point) { + selfIdx = idx + break + } + } + if selfIdx < 0 { + return fmt.Errorf("pvss: privateKey's point not in participant list") + } + + inst.dealerState = &dealerState{ + scalar: scalar, + point: point, + index: selfIdx, + } + + return nil +} + +// Commit executes the commit phase of the protocol, generating a commitment +// message to be broadcasted to all participants. +func (inst *Instance) Commit() (*Commit, error) { + if inst.isVerifyOnly() { + return nil, errVerifyOnly + } + + // Generate the secret scalar. + secret, _, err := NewKeyPair() + if err != nil { + return nil, fmt.Errorf("pvss/commit: failed to generate secret: %w", err) + } + + // Generate the encrypted shares for the PVSS commitment. + encShares, pubPoly, err := pvss.EncShares( + suite, + basePoint, + inst.participants(), + secret.Inner(), + inst.Threshold, + ) + if err != nil { + return nil, fmt.Errorf("pvss/commit: failed to encrypt shares: %w", err) + } + + commit := &Commit{ + Index: inst.dealerState.index, + Shares: commitSharesFromKyber( + pubPoly.Shares(len(inst.Participants)), + encShares, + ), + } + + // Handle our own encrypted share. + if err = inst.OnCommit(commit); err != nil { + return nil, fmt.Errorf("pvss/commit: failed to process own commit: %w", err) + } + + return commit, nil +} + +// OnCommit processes a commitment message received from a participant. +// +// Note: This assumes that the commit is authentic and attributable. +func (inst *Instance) OnCommit(commit *Commit) error { + onlyVerify := inst.isVerifyOnly() + + numParticipants := len(inst.Participants) + if numS := len(commit.Shares); numS != numParticipants { + return fmt.Errorf("pvss/commit: invalid number of shares: %d", numS) + } + + commitIdx := commit.Index + if commitIdx < 0 || commitIdx >= numParticipants { + return fmt.Errorf("pvss/commit: invalid commit index: %d", commitIdx) + } + if inst.Commits[commitIdx] != nil { + return fmt.Errorf("pvss/commit: received multiple commits for participant: %d", commitIdx) + } + + var ( + polyShares []*share.PubShare + encShares []*pvss.PubVerShare + ) + for idx, share := range commit.Shares { + if share == nil { + return fmt.Errorf("pvss/commit: missing share: %d", idx) + } + if err := share.isWellFormed(); err != nil { + return fmt.Errorf("pvss/commit: share %d malformed: %w", idx, err) + } + + polyShare, encShare := share.toKyber(idx) + polyShares = append(polyShares, polyShare) + encShares = append(encShares, encShare) + } + + publicPoly, err := share.RecoverPubPoly(suite, polyShares, inst.Threshold, numParticipants) + if err != nil { + return fmt.Errorf("pvss/commit: failed to recover public polynomial: %w", err) + } + + // Note: In theory this is overly strict since the PVSS algorithm + // can recover the secret even if shares are malformed. But the + // assumption is that anything that submits a commit that has + // malformed entries is evil. + // + // The coding theory checks would go here, if we ever decide to + // implement such a thing. + cs := &CommitState{ + Commit: commit, + } + for idx, share := range encShares { + sH := publicPoly.Eval(idx).V + switch { + case !onlyVerify && idx == inst.dealerState.index: + // Verify and decrypt the share that is intended for us. + ds, err := pvss.DecShare( + suite, + basePoint, + inst.dealerState.point, + sH, + inst.dealerState.scalar, + share, + ) + if err != nil { + return fmt.Errorf("pvss/commit: failed to decrypt share: %w", err) + } + + // Only store this if the all of the shares check out. + cs.DecryptedShare = pubVerShareFromKyber(ds) + default: + // Verify the encrypted share that is intended for another. + if err := pvss.VerifyEncShare( + suite, + basePoint, + inst.participants()[idx], + sH, + share, + ); err != nil { + return fmt.Errorf("pvss/commit: failed to verify encrypted share (%d:%d): %w", commitIdx, idx, err) + } + } + } + + inst.Commits[commitIdx] = cs + + return nil +} + +// MayReveal returns true iff it is possible to proceed to the reveal +// step, and the total number of distinct valid commitments received. +func (inst *Instance) MayReveal() (bool, int) { + totalCommits := len(inst.Commits) + return totalCommits >= inst.Threshold, totalCommits +} + +// Reveal executes the reveal phase of the protocol, generating a reveal +// message to be broadcasted to all participants. +func (inst *Instance) Reveal() (*Reveal, error) { + if inst.isVerifyOnly() { + return nil, errVerifyOnly + } + + if ok, _ := inst.MayReveal(); !ok { + return nil, fmt.Errorf("pvss/reveal: insufficient valid commits") + } + + reveal := &Reveal{ + Index: inst.dealerState.index, + DecryptedShares: make(map[int]*PubVerShare), + } + for idx, cs := range inst.Commits { + reveal.DecryptedShares[idx] = cs.DecryptedShare + } + + // Handle our own reveal. + if err := inst.OnReveal(reveal); err != nil { + return nil, fmt.Errorf("pvss/reveal: failed to process own reveal: %w", err) + } + + return reveal, nil +} + +// OnReveal processes a reveal message received from a participant. +// +// Note: This assumes that the reveal is authentic and attributable. +func (inst *Instance) OnReveal(reveal *Reveal) error { + revealIdx := reveal.Index + + if revealIdx < 0 || revealIdx >= len(inst.Participants) { + return fmt.Errorf("pvss/reveal: invalid reveal index: %d", revealIdx) + } + if inst.Reveals[revealIdx] != nil { + return fmt.Errorf("pvss/reveal: received multiple reveals for participant: %d", revealIdx) + } + if numDs := len(reveal.DecryptedShares); numDs != len(inst.Commits) { + return fmt.Errorf("pvss/reveal: invalid number of decrypted shares: %d", numDs) + } + + decShares := make(map[int]*pvss.PubVerShare) + for idx, share := range reveal.DecryptedShares { + if share == nil { + return fmt.Errorf("pvss/reveal: missing share: %d", idx) + } + if err := share.isWellFormed(); err != nil { + return fmt.Errorf("pvss/reveal: share %d malformed: %w", idx, err) + } + + decShares[idx] = share.toKyber(revealIdx) + } + + g := suite.Point().Base() + for idx, ds := range decShares { + cs := inst.Commits[idx] + if cs == nil { + return fmt.Errorf("pvss/reveal: reveal for missing commit: %d", idx) + } + + _, es := cs.Commit.Shares[revealIdx].toKyber(idx) + + if err := pvss.VerifyDecShare( + suite, + g, + inst.participants()[revealIdx], + es, + ds, + ); err != nil { + return fmt.Errorf("pvss/reveal: failed to verify decrypted share (%d:%d): %w", revealIdx, idx, err) + } + } + + // Store the reveal and all of the decrypted shares. + inst.Reveals[revealIdx] = reveal + for idx := range decShares { + m := inst.DecryptedShares[idx] + if m == nil { + m = make(map[int]*PubVerShare) + inst.DecryptedShares[idx] = m + } + + // This works because decShares and reveal.DecryptedShares + // have the same reveals in the same places. + m[revealIdx] = reveal.DecryptedShares[idx] + } + + return nil +} + +// MayRecover returns true iff it is possible to proceed to the recovery +// step, and the total number of distinct valid reveals received. +func (inst *Instance) MayRecover() (bool, int) { + var goodInstances int + for i := 0; i < len(inst.Participants); i++ { + decShares := inst.DecryptedShares[i] + if len(decShares) >= inst.Threshold { + goodInstances++ + } + } + + return goodInstances >= inst.Threshold, len(inst.Reveals) +} + +// Recover executes the recovery phase of the protocol, returning the resulting +// composite entropy and the indexes of the participants that contributed fully. +func (inst *Instance) Recover() ([]byte, []int, error) { + if ok, _ := inst.MayRecover(); !ok { + return nil, nil, fmt.Errorf("pvss/recover: insufficient valid reveals") + } + + // Iterate over each participant's PVSS instance. + points := make([]kyber.Point, 0, len(inst.DecryptedShares)) + for i := 0; i < len(inst.Participants); i++ { + // All of the shares in `inst.DecryptedShares` are valid. + decShares := inst.DecryptedShares[i] + if len(decShares) < inst.Threshold { + continue + } + + // Note: This uses `share.RecoverCommit` instead of + // `pvss.RecoverSecret` because `Instance.OnReveal` calls + // `pvss.VerifyDecShare`. + shares := make([]*share.PubShare, 0, len(decShares)) + for idx, decShare := range decShares { + ds := decShare.toKyber(idx) + shares = append(shares, &ds.S) + } + point, err := share.RecoverCommit( + suite, + shares, + inst.Threshold, + len(inst.Participants), + ) + if err != nil { + return nil, nil, fmt.Errorf("pvss/recover: failed to recover secret (%d): %w", i, err) + } + + points = append(points, point) + } + + // This should NEVER happen, but check anyway. + if numPoints := len(points); numPoints < inst.Threshold { + return nil, nil, fmt.Errorf("pvss/recover: insufficient recovered points: %d", numPoints) + } + + // Produce the final entropy based on all of the points. + h := suite.Hash() + _, _ = h.Write([]byte("oasis-core/pvss: Hash points")) + for _, point := range points { + b, err := point.MarshalBinary() + if err != nil { + return nil, nil, fmt.Errorf("pvss/recover: failed to marshal point: %w", err) + } + _, _ = h.Write(b) + } + + // Credit everyone that submitted a valid reveal. + contributors := make([]int, 0, len(inst.DecryptedShares)) + for idx := range inst.Reveals { + contributors = append(contributors, idx) + } + sort.Ints(contributors) + + return h.Sum(nil), contributors, nil +} + +// New creates a new protocol instance with the provided configuration. +func New(cfg *Config) (*Instance, error) { + numParticipants := len(cfg.Participants) + if numParticipants < 2 { + return nil, fmt.Errorf("pvss/new: insufficient participants: %d", numParticipants) + } + if numParticipants > math.MaxInt32 { + // Enforcing this constraint, in combination with explicit + // checks in `OnCommit` and `OnReveal`, ensures that using + // signed integers in the serialized types will not cause + // issues regardless of the underlying architecture. + // + // That said, 32 bit platforms aren't supported by + // tendermint anyway, and our use of this uses CBOR (all + // integers are 64 bits), so it is a bit of a moot point. + return nil, fmt.Errorf("pvss/new: too many participants: %d", numParticipants) + } + if cfg.Threshold <= 0 || cfg.Threshold > numParticipants { + return nil, fmt.Errorf("pvss/new: insufficient threshold: %d", cfg.Threshold) + } + + var participants []kyber.Point + for idx, participant := range cfg.Participants { + if err := participant.isWellFormed(); err != nil { + return nil, fmt.Errorf("pvss/new: invalid point for participant %d: %w", idx, err) + } + participants = append(participants, participant.Inner()) + } + + inst := &Instance{ + Participants: cfg.Participants, + Commits: make(map[int]*CommitState), + Reveals: make(map[int]*Reveal), + DecryptedShares: make(map[int]map[int]*PubVerShare), + Threshold: cfg.Threshold, + cachedParticipants: participants, + } + + if cfg.PrivateKey != nil { + if err := inst.SetScalar(cfg.PrivateKey); err != nil { + return nil, err + } + } + + return inst, nil +} + +func (inst *Instance) participants() []kyber.Point { + if len(inst.cachedParticipants) == 0 { + var participants []kyber.Point + for _, participant := range inst.Participants { + participants = append(participants, participant.Inner()) + } + inst.cachedParticipants = participants + } + + return inst.cachedParticipants +} + +func (inst *Instance) isVerifyOnly() bool { + return inst.dealerState == nil +} + +// NewKeyPair creates a new scalar/point pair for use with a PVSS instance. +func NewKeyPair() (*Scalar, *Point, error) { + for i := 0; i < maxRetries; i++ { + scalarInner := suite.Scalar().Pick(suite.RandomStream()) + scalar := scalarFromKyber(scalarInner) + point := scalar.Point() + + if pointIsValid(point.Inner()) { + return &scalar, &point, nil + } + } + + return nil, nil, fmt.Errorf("pvss: failed to generate scalar") +} + +// Commit is a PVSS commit. +type Commit struct { + Index int `json:"index"` + Shares []*CommitShare `json:"shares"` +} + +// Reveal is a PVSS reveal. +type Reveal struct { + Index int `json:"index"` + DecryptedShares map[int]*PubVerShare `json:"decrypted_shares"` +} + +// CommitState is a PVSS commit and the corresponding decrypted share, +// if any. +type CommitState struct { + Commit *Commit `json:"commit"` + DecryptedShare *PubVerShare `json:"decrypted_share,omitempty"` +} diff --git a/go/common/crypto/pvss/pvss_test.go b/go/common/crypto/pvss/pvss_test.go new file mode 100644 index 00000000000..96256f46169 --- /dev/null +++ b/go/common/crypto/pvss/pvss_test.go @@ -0,0 +1,252 @@ +package pvss + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/oasisprotocol/oasis-core/go/common/cbor" +) + +const ( + numNodes = 5 + threshold = 3 +) + +func TestPVSS(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + doTestPVSS(t, &testPVSSCfg{ + s11nChecks: true, + }) + }) + t.Run("MissingCommits", func(t *testing.T) { + doTestPVSS(t, &testPVSSCfg{ + numDiscardCommits: 2, + }) + }) + t.Run("MissingReveals", func(t *testing.T) { + doTestPVSS(t, &testPVSSCfg{ + numDiscardReveals: 2, + }) + }) + t.Run("MissingVerifierReveals", func(t *testing.T) { + doTestPVSS(t, &testPVSSCfg{ + numDiscardVerReveals: 2, + }) + }) +} + +func BenchmarkPVSS(b *testing.B) { + benchNodes := []int{3, 10, 20} + + doBench := func(name string, fn func(*testing.B, int)) { + for _, n := range benchNodes { + b.Run(fmt.Sprintf("%s/%d", name, n), func(b *testing.B) { + fn(b, n) + }) + } + } + + // Note: The Commit number will always be larger than OnCommit, + // because it calls OnCommit for the node's own share. + doBench("Commit", doBenchCommit) + doBench("OnCommit", doBenchOnCommit) +} + +type testPVSSCfg struct { + numDiscardCommits int + numDiscardReveals int + numDiscardVerReveals int + + s11nChecks bool +} + +func doTestPVSS(t *testing.T, cfg *testPVSSCfg) { + require := require.New(t) + + instances, publicKeys := initInstances(require, numNodes, threshold) + + verifier, err := New(&Config{ + Participants: publicKeys, + Threshold: threshold, + }) + require.NoError(err, "New - verifier") + + var commits []*Commit + for i, inst := range instances { + var commit *Commit + commit, err = inst.Commit() + require.NoError(err, "inst[%d].Commit()", i) + commits = append(commits, commit) + } + + if n := cfg.numDiscardCommits; n > 0 { + // Truncate from the head to catch dumb indexing errors. + instances = instances[n:] + commits = commits[n:] + t.Logf("Commits: Discarding down to %d", len(commits)) + } + + if cfg.s11nChecks { + commit := commits[0] + b := cbor.Marshal(commit) + var commit2 Commit + err = cbor.Unmarshal(b, &commit2) + require.NoError(err, "cbor.Unmarshal: Commit") + require.EqualValues(commit, &commit2, "Commit s11n round-trips") + } + + for i, commit := range commits { + for ii, inst := range instances { + err = inst.OnCommit(commit) + switch ii { + case i: + // Own commit is handled when generating the commit. + require.Error(err, "inst[%d].OnCommit(commit[%d])", ii, i) + default: + require.NoError(err, "inst[%d].OnCommit(commit[%d])", ii, i) + } + } + err = verifier.OnCommit(commit) + require.NoError(err, "verifier.OnCommit(commit[%d])", i) + } + + var reveals []*Reveal + for i, inst := range instances { + var reveal *Reveal + reveal, err = inst.Reveal() + require.NoError(err, "inst[%d].Reveal()", i) + reveals = append(reveals, reveal) + } + + if cfg.s11nChecks { + reveal := reveals[0] + b := cbor.Marshal(reveal) + var reveal2 Reveal + err = cbor.Unmarshal(b, &reveal2) + require.NoError(err, "cbor.Unmarshal: Reveal") + require.EqualValues(reveal, &reveal2, "Reveal s11n round-trips") + } + + if n := cfg.numDiscardReveals; n > 0 { + n = numNodes - n + instances = instances[:n] + reveals = reveals[:n] + t.Logf("Reveals: Discarding down to %d", len(reveals)) + } + + for i, reveal := range reveals { + for ii, inst := range instances { + err = inst.OnReveal(reveal) + switch ii { + case i: + // Own reveal is handled when generating the reveal. + require.Error(err, "inst[%d].OnReveal(reveal[%d])", i, ii) + default: + require.NoError(err, "inst[%d].OnReveal(reveal[%d])", i, ii) + } + } + if n := cfg.numDiscardVerReveals; n > 0 { + if i < numNodes-n { + err = verifier.OnReveal(reveal) + require.NoError(err, "verifier.OnReveal(reveal[%d]) - truncated", i) + } else { + t.Logf("Verifier: Skipping reveal[%d]", i) + } + } else { + err = verifier.OnReveal(reveal) + require.NoError(err, "verifier.OnReveal(reveal[%d])", i) + } + } + + resultMap := make(map[string]bool) + for i, inst := range instances { + var b []byte + b, _, err = inst.Recover() + require.NoError(err, "inst[%d].Recover()", i) + + resultMap[string(b)] = true + } + require.Len(resultMap, 1, "All nodes agree on the output") + + b, contributors, err := verifier.Recover() + require.NoError(err, "verifier.Recover()") + require.True(resultMap[string(b)], "Verifier agrees on the output") + + t.Logf("Entropy: %x", b) + t.Logf("Contributors: %+v", contributors) + + if cfg.s11nChecks { + // Use the verifier for this since it doesn't have a scalar. + instance := verifier + b = cbor.Marshal(instance) + var instance2 Instance + err = cbor.Unmarshal(b, &instance2) + require.NoError(err, "cbor.Unmarshal: Instance") + _ = instance2.participants() // But it does have this, so re-generate. + require.EqualValues(instance, &instance2, "Instance s11n round-trips") + } +} + +func doBenchCommit(b *testing.B, n int) { + require := require.New(b) + + for i := 0; i < b.N; i++ { + b.StopTimer() + instances, _ := initInstances(require, n, n) + b.StartTimer() + + _, err := instances[0].Commit() + require.NoError(err, "Commit") + } +} + +func doBenchOnCommit(b *testing.B, n int) { + require := require.New(b) + + for i := 0; i < b.N; i++ { + b.StopTimer() + instances, publicKeys := initInstances(require, n, n) + verifier, err := New(&Config{ + Participants: publicKeys, + Threshold: n, + }) + require.NoError(err, "New - verifier") + + commit, err := instances[0].Commit() + require.NoError(err, "Commit") + b.StartTimer() + + err = verifier.OnCommit(commit) + require.NoError(err, "OnCommit") + } +} + +func initInstances(require *require.Assertions, n, t int) ([]*Instance, []Point) { + var ( + instances []*Instance + privateKeys []*Scalar + publicKeys []Point + ) + + // Initialize the long term key pairs. + for i := 0; i < n; i++ { + scalar, point, err := NewKeyPair() + require.NoError(err, "NewKeyPair") + + privateKeys = append(privateKeys, scalar) + publicKeys = append(publicKeys, *point) + } + for i, privateKey := range privateKeys { + inst, err := New(&Config{ + PrivateKey: privateKey, + Participants: publicKeys, + Threshold: t, + }) + require.NoError(err, "New(states[%d])", i) + instances = append(instances, inst) + } + + return instances, publicKeys +} diff --git a/go/common/crypto/pvss/s11n.go b/go/common/crypto/pvss/s11n.go new file mode 100644 index 00000000000..c9bd7898d59 --- /dev/null +++ b/go/common/crypto/pvss/s11n.go @@ -0,0 +1,426 @@ +package pvss + +import ( + "crypto/elliptic" + "fmt" + "io/ioutil" + "os" + + "go.dedis.ch/kyber/v3" + "go.dedis.ch/kyber/v3/proof/dleq" + "go.dedis.ch/kyber/v3/share" + "go.dedis.ch/kyber/v3/share/pvss" + + "github.com/oasisprotocol/oasis-core/go/common/pem" +) + +const ( + pointPEMType = "EC PUBLIC KEY" + scalarPEMType = "EC PRIVATE KEY" + filePerm = 0o600 +) + +// As convenient as it is to use kyber's PVSS implementation, scalars and +// points being interfaces makes s11n a huge pain, and mandates using +// wrapper types so that this can play nice with CBOR/JSON etc. +// +// Aut viam inveniam aut faciam. + +// Point is an elliptic curve point. +type Point struct { + inner kyber.Point +} + +// Inner returns the actual kyber.Point. +func (p *Point) Inner() kyber.Point { + return p.inner +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (p *Point) UnmarshalBinary(data []byte) error { + inner := suite.Point() + if err := inner.UnmarshalBinary(data); err != nil { + return fmt.Errorf("pvss/s11n: failed to deserialize point: %w", err) + } + + checkPoint := Point{inner: inner} + if err := checkPoint.isWellFormed(); err != nil { + return fmt.Errorf("pvss/s11n: deserialized point is invalid: %w", err) + } + if checkPoint2, ok := inner.(isCanonicalAble); ok { + // edwards25519.Point.IsCanonical takes a buffer, since points + // that get serialized are always in canonical form. + if !checkPoint2.IsCanonical(data) { + return fmt.Errorf("pvss/s11n: point is not in canonical form") + } + } + + p.inner = inner + + return nil +} + +// UnmarshalPEM decodes a PEM marshaled point. +func (p *Point) UnmarshalPEM(data []byte) error { + b, err := pem.Unmarshal(pointPEMType, data) + if err != nil { + return fmt.Errorf("pvss/s11n: failed to deserialize PEM encoded point: %w", err) + } + + return p.UnmarshalBinary(b) +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (p Point) MarshalBinary() ([]byte, error) { + if err := p.isWellFormed(); err != nil { + return nil, fmt.Errorf("pvss/s11n: refusing to serialize invalid point: %w", err) + } + + data, err := p.inner.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("pvss/s11n: failed to serialize point: %w", err) + } + + return data, nil +} + +// MarshalPEM encodes a point into PEM form. +func (p Point) MarshalPEM() ([]byte, error) { + b, err := p.MarshalBinary() + if err != nil { + return nil, err + } + + return pem.Marshal(pointPEMType, b) +} + +// LoadPEM loads a point from a PEM file on disk. Iff the point is missing +// and a Scalar is provided, the Scalar's corresponding point will be written +// and loaded. +func (p *Point) LoadPEM(fn string, scalar *Scalar) error { + f, err := os.Open(fn) //nolint: gosec + if err != nil { + if os.IsNotExist(err) && scalar != nil { + if err = scalar.isWellFormed(); err != nil { + return fmt.Errorf("pvss/s11n: refusing to use invalid scalar to generate point: %w", err) + } + pointInner := scalar.Point() + p.inner = pointInner.Inner() + + var buf []byte + if buf, err = p.MarshalPEM(); err != nil { + return err + } + + return ioutil.WriteFile(fn, buf, filePerm) + } + return err + } + defer f.Close() //nolint: errcheck + + buf, err := ioutil.ReadAll(f) + if err != nil { + return fmt.Errorf("pvss/s11n: failed to read PEM serialized point: %w", err) + } + + if err = p.UnmarshalPEM(buf); err != nil { + return fmt.Errorf("pvss/s11n: failed to parse PEM serialized point: %w", err) + } + + if scalar != nil { + if err = scalar.isWellFormed(); err != nil { + return fmt.Errorf("pvss/s11n: invalid scalar provided for verification: %w", err) + } + + checkPoint := scalar.Point() + if !p.Inner().Equal(checkPoint.Inner()) { + return fmt.Errorf("pvss/s11n: point PEM is not for scalar") + } + } + + return nil +} + +func (p *Point) isWellFormed() error { + // Can never happen(?), but check anyway. + if p.inner == nil { + return fmt.Errorf("pvss/s11n: point is missing") + } + + if !pointIsValid(p.inner) { + return fmt.Errorf("pvss/s11n: point is invalid") + } + + return nil +} + +func pointFromKyber(p kyber.Point) Point { + return Point{ + inner: p, + } +} + +type validAble interface { + Valid() bool +} + +type hasSmallOrderAble interface { + HasSmallOrder() bool +} + +type isCanonicalAble interface { + IsCanonical([]byte) bool +} + +func pointIsValid(point kyber.Point) bool { + switch validator := point.(type) { + case validAble: + // P-256 point validation (ensures point is on curve) + // + // Note: Kyber's idea of a valid point includes the point at + // infinity, which does not ensure contributory behavior when + // doing ECDH. + + // We write out the point to binary data, and unmarshal + // it with elliptic.Unmarshal, which checks to see if the + // point is on the curve (while rejecting the point at + // infinity). + // + // In theory, we could just examine the x/y coordinates, but + // there's no way to get at those without reflection hacks. + // + // WARNING: If this ever needs to support NIST curves other + // than P-256, this will need to get significantly more + // involved. + b, err := point.MarshalBinary() + if err != nil { + return false + } + if x, _ := elliptic.Unmarshal(elliptic.P256(), b); x == nil { + return false + } + return true + case hasSmallOrderAble: + // Ed25519 point validation (rejects small-order points) + return !validator.HasSmallOrder() + default: + return false + } +} + +// Scalar is a scalar. +type Scalar struct { + inner kyber.Scalar +} + +// Inner returns the actual kyber.Scalar. +func (s *Scalar) Inner() kyber.Scalar { + return s.inner +} + +// Point returns the corresponding point. +func (s *Scalar) Point() Point { + if err := s.isWellFormed(); err != nil { + panic(fmt.Errorf("pvss/s11n: malformed scalar for basepoint multiply: %w", err)) + } + return pointFromKyber(suite.Point().Mul(s.Inner(), nil)) +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (s *Scalar) UnmarshalBinary(data []byte) error { + inner := suite.Scalar() + if err := inner.UnmarshalBinary(data); err != nil { + return fmt.Errorf("pvss/s11n: failed to deserialize scalar: %w", err) + } + + s.inner = inner + + return nil +} + +// UnmarshalPEM decodes a PEM marshaled scalar. +func (s *Scalar) UnmarshalPEM(data []byte) error { + b, err := pem.Unmarshal(scalarPEMType, data) + if err != nil { + return fmt.Errorf("pvss/s11n: failed to deserialize PEM encoded scalar: %w", err) + } + + return s.UnmarshalBinary(b) +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (s Scalar) MarshalBinary() ([]byte, error) { + data, err := s.inner.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("pvss/s11n: failed to serialize scalar: %w", err) + } + + return data, nil +} + +// MarshalPEM encodes a scalar into PEM form. +func (s Scalar) MarshalPEM() ([]byte, error) { + b, err := s.MarshalBinary() + if err != nil { + return nil, err + } + + return pem.Marshal(scalarPEMType, b) +} + +// LoadOrGeneratePEM loads a scalar from a PEM file on disk. Iff the +// scalar is missing, a new one will be generated, written, and loaded. +func (s *Scalar) LoadOrGeneratePEM(fn string) error { + f, err := os.Open(fn) //nolint: gosec + if err != nil { + if os.IsNotExist(err) { + var newScalar *Scalar + if newScalar, _, err = NewKeyPair(); err != nil { + return fmt.Errorf("pvss/s11n: failed to generate new scalar: %w", err) + } + s.inner = newScalar.inner + + var buf []byte + if buf, err = s.MarshalPEM(); err != nil { + return err + } + + return ioutil.WriteFile(fn, buf, filePerm) + } + return err + } + defer f.Close() //nolint: errcheck + + buf, err := ioutil.ReadAll(f) + if err != nil { + return fmt.Errorf("pvss/s11n: failed to read PEM serialized scalar: %w", err) + } + + if err = s.UnmarshalPEM(buf); err != nil { + return fmt.Errorf("pvss/s11n: failed to parse PEM serialized scalar: %w", err) + } + if err = s.isWellFormed(); err != nil { + return fmt.Errorf("pvss/s11n: deserialized scalar is invalid: %w", err) + } + + return nil +} + +func (s *Scalar) isWellFormed() error { + // Can never happen(?), but check anyway. + if s.inner == nil { + return fmt.Errorf("pvss/s11n: scalar is missing") + } + + return nil +} + +func scalarFromKyber(s kyber.Scalar) Scalar { + return Scalar{ + inner: s, + } +} + +// PubVerShare is a public verifiable share (`pvss.PubVerShare`) +type PubVerShare struct { + V Point `json:"v"` // Encrypted/decrypted share + + C Scalar `json:"c"` // Challenge + R Scalar `json:"r"` // Response + VG Point `json:"vg"` // Public commitment with respect to base point G + VH Point `json:"vh"` // Public commitment with respect to base point H +} + +func (pvs *PubVerShare) isWellFormed() error { + if err := pvs.V.isWellFormed(); err != nil { + return fmt.Errorf("pvss/s11n: invalid PubVerShare V: %w", err) + } + if err := pvs.C.isWellFormed(); err != nil { + return fmt.Errorf("pvss/s11n: invalid PubVerShare C: %w", err) + } + if err := pvs.R.isWellFormed(); err != nil { + return fmt.Errorf("pvss/s11n: invalid PubVerShare R: %w", err) + } + if err := pvs.VG.isWellFormed(); err != nil { + return fmt.Errorf("pvss/s11n: invalid PubVerShare VG: %w", err) + } + if err := pvs.VH.isWellFormed(); err != nil { + return fmt.Errorf("pvss/s11n: invalid PubVerShare VH: %w", err) + } + + return nil +} + +func (pvs *PubVerShare) toKyber(index int) *pvss.PubVerShare { + return &pvss.PubVerShare{ + S: share.PubShare{ + I: index, + V: pvs.V.Inner(), + }, + P: dleq.Proof{ + C: pvs.C.Inner(), + R: pvs.R.Inner(), + VG: pvs.VG.Inner(), + VH: pvs.VH.Inner(), + }, + } +} + +func pubVerShareFromKyber(pvs *pvss.PubVerShare) *PubVerShare { + return &PubVerShare{ + V: pointFromKyber(pvs.S.V), + C: scalarFromKyber(pvs.P.C), + R: scalarFromKyber(pvs.P.R), + VG: pointFromKyber(pvs.P.VG), + VH: pointFromKyber(pvs.P.VH), + } +} + +// CommitShare is a commit share. +type CommitShare struct { + PolyV Point `json:"poly_v"` // Share of the public commitment polynomial + PubVerShare +} + +func (cs *CommitShare) isWellFormed() error { + if err := cs.PolyV.isWellFormed(); err != nil { + return fmt.Errorf("pvss/s11n: invalid CommitShare PolyV: %w", err) + } + if err := cs.PubVerShare.isWellFormed(); err != nil { + return fmt.Errorf("pvss/s11n: invalid CommitShare PubVerShare: %w", err) + } + + return nil +} + +func (cs *CommitShare) toKyber(index int) (*share.PubShare, *pvss.PubVerShare) { + pubShare := &share.PubShare{ + I: index, + V: cs.PolyV.Inner(), + } + return pubShare, cs.PubVerShare.toKyber(index) +} + +func commitShareFromKyber(pubPolyShare *share.PubShare, encShare *pvss.PubVerShare) *CommitShare { + return &CommitShare{ + PolyV: pointFromKyber(pubPolyShare.V), + PubVerShare: *pubVerShareFromKyber(encShare), + } +} + +func commitSharesFromKyber(pubPolyShares []*share.PubShare, encShares []*pvss.PubVerShare) []*CommitShare { + if len(pubPolyShares) != len(encShares) { + panic("pvss/s11n: BUG: len(pubPolyShares != len(encShares)") + } + + var shares []*CommitShare + for i, pubPolyShare := range pubPolyShares { + encShare := encShares[i] + if pubPolyShare.I != encShare.S.I { + panic("pvss/s11n: BUG: pubPolyShare.I != encShare.I") + } + shares = append(shares, commitShareFromKyber(pubPolyShare, encShare)) + } + + return shares +} diff --git a/go/common/identity/identity.go b/go/common/identity/identity.go index 23f5698bd43..e33e7f1a6a2 100644 --- a/go/common/identity/identity.go +++ b/go/common/identity/identity.go @@ -8,6 +8,7 @@ import ( "path/filepath" "sync" + "github.com/oasisprotocol/oasis-core/go/common/crypto/pvss" "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" "github.com/oasisprotocol/oasis-core/go/common/crypto/signature/signers/memory" tlsCert "github.com/oasisprotocol/oasis-core/go/common/crypto/tls" @@ -29,6 +30,8 @@ const ( // CommonName is the CommonName to use when generating TLS certificates. CommonName = "oasis-node" + beaconScalarFilename = "beacon.pem" + tlsKeyFilename = "tls_identity.pem" tlsCertFilename = "tls_identity_cert.pem" @@ -53,6 +56,8 @@ type Identity struct { P2PSigner signature.Signer // ConsensusSigner is a node consensus key signer. ConsensusSigner signature.Signer + // BeaconScalar is a node beacon scalar. + BeaconScalar pvss.Scalar // TLSSentryClientCertificate is the client certificate used for // connecting to the sentry node's control connection. It is never rotated. @@ -272,10 +277,18 @@ func doLoadOrGenerate(dataDir string, signerFactory signature.SignerFactory, sho } } + // Load or generate the beacon scalar for this node. + beaconScalarPath := filepath.Join(dataDir, beaconScalarFilename) + var beaconScalar pvss.Scalar + if err := beaconScalar.LoadOrGeneratePEM(beaconScalarPath); err != nil { + return nil, err + } + return &Identity{ NodeSigner: signers[0], P2PSigner: signers[1], ConsensusSigner: signers[2], + BeaconScalar: beaconScalar, tlsSigner: memory.NewFromRuntime(cert.PrivateKey.(ed25519.PrivateKey)), tlsCertificate: cert, nextTLSSigner: nextSigner, diff --git a/go/common/node/node.go b/go/common/node/node.go index 798c6a07679..e906616b487 100644 --- a/go/common/node/node.go +++ b/go/common/node/node.go @@ -14,6 +14,7 @@ import ( "github.com/oasisprotocol/oasis-core/go/common" "github.com/oasisprotocol/oasis-core/go/common/cbor" "github.com/oasisprotocol/oasis-core/go/common/crypto/hash" + "github.com/oasisprotocol/oasis-core/go/common/crypto/pvss" "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" "github.com/oasisprotocol/oasis-core/go/common/prettyprint" "github.com/oasisprotocol/oasis-core/go/common/sgx/ias" @@ -71,7 +72,8 @@ type Node struct { // nolint: maligned // Beacon contains information for this node's participation // in the random beacon protocol. // - // NOTE: This is reserved for future use. + // TODO: This is optional for now, make mandatory once enough + // nodes provide this field. Beacon cbor.RawMessage `json:"beacon,omitempty"` // Runtimes are the node's runtimes. @@ -283,6 +285,19 @@ type ConsensusInfo struct { Addresses []ConsensusAddress `json:"addresses"` } +// BeaconInfo contains information for this node's participation in +// the random beacon protocol. +type BeaconInfo struct { + // Point is the elliptic curve point used for the PVSS algorithm. + Point pvss.Point `json:"point"` +} + +// MustRawCBOR returns the BeaconInfo as a CBOR RawMessage. +func (bi BeaconInfo) MustRawCBOR() cbor.RawMessage { + b := cbor.Marshal(bi) + return cbor.RawMessage(b) +} + // Capabilities represents a node's capabilities. type Capabilities struct { // TEE is the capability of a node executing batches in a TEE. diff --git a/go/go.mod b/go/go.mod index 04b7db25eb5..fd3137211f1 100644 --- a/go/go.mod +++ b/go/go.mod @@ -53,8 +53,9 @@ require ( github.com/uber/jaeger-lib v2.2.0+incompatible // indirect github.com/whyrusleeping/go-logging v0.0.1 gitlab.com/yawning/dynlib.git v0.0.0-20200603163025-35fe007b0761 - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 - golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc + go.dedis.ch/kyber/v3 v3.0.13 + golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 + golang.org/x/net v0.0.0-20201021035429-f5854403a974 google.golang.org/genproto v0.0.0-20200624020401-64a14ca9d1ad google.golang.org/grpc v1.32.0 google.golang.org/grpc/security/advancedtls v0.0.0-20200902210233-8630cac324bf diff --git a/go/go.sum b/go/go.sum index 5bef6f0401b..09d3703f3db 100644 --- a/go/go.sum +++ b/go/go.sum @@ -999,6 +999,15 @@ gitlab.com/yawning/dynlib.git v0.0.0-20200603163025-35fe007b0761 h1:27Qf3BkBLzRQ gitlab.com/yawning/dynlib.git v0.0.0-20200603163025-35fe007b0761/go.mod h1:U41r+zgpFRTlkSzMhBjUqbupvVBafgokFFkKn0j+874= gitlab.com/yawning/slice.git v0.0.0-20190714152416-bc4ae2510529 h1:GeSIG/kLmenUveo0XvlLXXtcKDeeItKA8iFnf0osNfg= gitlab.com/yawning/slice.git v0.0.0-20190714152416-bc4ae2510529/go.mod h1:sgaKGjNNjAAVrZvQQhE3oYIbnFZVaCBE2T7PmbpKJ4U= +go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs= +go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw= +go.dedis.ch/kyber/v3 v3.0.4/go.mod h1:OzvaEnPvKlyrWyp3kGXlFdp7ap1VC6RkZDTaPikqhsQ= +go.dedis.ch/kyber/v3 v3.0.9/go.mod h1:rhNjUUg6ahf8HEg5HUvVBYoWY4boAafX8tYxX+PS+qg= +go.dedis.ch/kyber/v3 v3.0.13 h1:s5Lm8p2/CsTMueQHCN24gPpZ4couBBeKU7r2Yl6r32o= +go.dedis.ch/kyber/v3 v3.0.13/go.mod h1:kXy7p3STAurkADD+/aZcsznZGKVHEqbtmdIzvPfrs1U= +go.dedis.ch/protobuf v1.0.5/go.mod h1:eIV4wicvi6JK0q/QnfIEGeSFNG0ZeB24kzut5+HaRLo= +go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI4= +go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -1039,6 +1048,7 @@ golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190225124518-7f87c0fbb88b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -1060,6 +1070,8 @@ golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 h1:phUcVbl53swtrUN8kQEXFhUxPlIlWyBfKmidCu7P95o= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1112,6 +1124,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1134,6 +1148,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1158,6 +1173,7 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1172,6 +1188,9 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= diff --git a/go/oasis-node/cmd/registry/node/node.go b/go/oasis-node/cmd/registry/node/node.go index 836b4419035..98eeb7af43a 100644 --- a/go/oasis-node/cmd/registry/node/node.go +++ b/go/oasis-node/cmd/registry/node/node.go @@ -197,6 +197,9 @@ func doInit(cmd *cobra.Command, args []string) { // nolint: gocyclo Consensus: node.ConsensusInfo{ ID: nodeIdentity.ConsensusSigner.Public(), }, + Beacon: (&node.BeaconInfo{ + Point: nodeIdentity.BeaconScalar.Point(), + }).MustRawCBOR(), } if n.Roles, err = argsToRolesMask(); err != nil { logger.Error("failed to parse node roles mask", diff --git a/go/oasis-test-runner/scenario/e2e/registry_cli.go b/go/oasis-test-runner/scenario/e2e/registry_cli.go index d5133ed4389..b40e97cac30 100644 --- a/go/oasis-test-runner/scenario/e2e/registry_cli.go +++ b/go/oasis-test-runner/scenario/e2e/registry_cli.go @@ -508,6 +508,7 @@ func (sc *registryCLIImpl) initNode(childEnv *env.Env, ent *entity.Entity, entDi testNode.TLS.NextPubKey = n.TLS.NextPubKey testNode.P2P.ID = n.P2P.ID testNode.Consensus.ID = n.Consensus.ID + testNode.Beacon = n.Beacon for idx := range testNode.TLS.Addresses { testNode.TLS.Addresses[idx].PubKey = n.TLS.PubKey } diff --git a/go/registry/tests/tester.go b/go/registry/tests/tester.go index dc2725cff07..f566fc7bd06 100644 --- a/go/registry/tests/tester.go +++ b/go/registry/tests/tester.go @@ -15,6 +15,7 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/cbor" "github.com/oasisprotocol/oasis-core/go/common/crypto/drbg" "github.com/oasisprotocol/oasis-core/go/common/crypto/hash" + "github.com/oasisprotocol/oasis-core/go/common/crypto/pvss" "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" memorySigner "github.com/oasisprotocol/oasis-core/go/common/crypto/signature/signers/memory" "github.com/oasisprotocol/oasis-core/go/common/crypto/tls" @@ -876,11 +877,20 @@ func randomIdentity(rng *drbg.Drbg) *identity.Identity { } return signer } + mustGenerateScalar := func() pvss.Scalar { + // Note: This is non-deterministic, but that's ok for now. + scalar, _, err := pvss.NewKeyPair() + if err != nil { + panic(err) + } + return *scalar + } ident := &identity.Identity{ NodeSigner: mustGenerateSigner(), P2PSigner: mustGenerateSigner(), ConsensusSigner: mustGenerateSigner(), + BeaconScalar: mustGenerateScalar(), } cert, err := tls.Generate(identity.CommonName) @@ -933,8 +943,11 @@ func (ent *TestEntity) NewTestNodes(nCompute, nStorage int, idNonce []byte, runt ID: nod.Signer.Public(), EntityID: ent.Entity.ID, Expiration: uint64(expiration), - Runtimes: runtimes, - Roles: role, + Beacon: (&node.BeaconInfo{ + Point: nodeIdentity.BeaconScalar.Point(), + }).MustRawCBOR(), + Runtimes: runtimes, + Roles: role, } addr := node.Address{ TCPAddr: net.TCPAddr{ diff --git a/go/worker/registration/worker.go b/go/worker/registration/worker.go index 6117ce7334a..f8a3d4876b8 100644 --- a/go/worker/registration/worker.go +++ b/go/worker/registration/worker.go @@ -676,6 +676,9 @@ func (w *Worker) registerNode(epoch epochtime.EpochTime, hook RegisterNodeHook) Consensus: node.ConsensusInfo{ ID: w.identity.ConsensusSigner.Public(), }, + Beacon: (&node.BeaconInfo{ + Point: w.identity.BeaconScalar.Point(), + }).MustRawCBOR(), } if err := hook(&nodeDesc); err != nil {