Skip to content

Commit

Permalink
cmd/sunlight,internal/ctlog: add Ed25519 signature to checkpoints
Browse files Browse the repository at this point in the history
For witness ecosystem compatibility.
  • Loading branch information
FiloSottile committed Aug 7, 2024
1 parent a7c2b87 commit 83e7f51
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 36 deletions.
62 changes: 62 additions & 0 deletions cmd/sunlight-keygen/keygen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

import (
"crypto/ed25519"
"crypto/elliptic"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"fmt"
"io"
"log"
"os"

"filippo.io/keygen"
"golang.org/x/crypto/hkdf"
"golang.org/x/mod/sumdb/note"
)

func main() {
if len(os.Args) != 3 {
log.Fatal("usage: sunlight-keygen <name> <seed file>")
}

seed, err := os.ReadFile(os.Args[2])
if err != nil {
log.Fatal("failed to load seed:", err)
}

ecdsaSecret := make([]byte, 32)
if _, err := io.ReadFull(hkdf.New(sha256.New, seed, []byte("sunlight"), []byte("ECDSA P-256 log key")), ecdsaSecret); err != nil {
log.Fatal("failed to derive ECDSA secret:", err)
}
k, err := keygen.ECDSA(elliptic.P256(), ecdsaSecret)
if err != nil {
log.Fatal("failed to generate ECDSA key:", err)
}

spki, err := x509.MarshalPKIXPublicKey(&k.PublicKey)
if err != nil {
log.Fatal("failed to marshal public key from private key for display:", err)
}

logID := sha256.Sum256(spki)

publicKey := base64.StdEncoding.EncodeToString(spki)

ed25519Secret := make([]byte, ed25519.SeedSize)
if _, err := io.ReadFull(hkdf.New(sha256.New, seed, []byte("sunlight"), []byte("Ed25519 log key")), ed25519Secret); err != nil {
log.Fatal("failed to derive Ed25519 key:", err)
}
wk := ed25519.NewKeyFromSeed(ed25519Secret).Public().(ed25519.PublicKey)

v, err := note.NewEd25519VerifierKey(os.Args[1], wk)
if err != nil {
log.Fatal("failed to create verifier key:", err)
}

fmt.Printf("Log ID: %s\n", base64.StdEncoding.EncodeToString(logID[:]))
fmt.Printf("ECDSA public key: %s\n", publicKey)
fmt.Printf("Ed25519 public key: %s\n", base64.StdEncoding.EncodeToString(wk))
fmt.Printf("Witness verifier key: %s\n", v)
}
50 changes: 30 additions & 20 deletions cmd/sunlight/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ package main

import (
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"flag"
"io"
"log/slog"
"net"
"net/http"
Expand All @@ -31,13 +33,15 @@ import (
"strings"
"time"

"filippo.io/keygen"
"filippo.io/sunlight/internal/ctlog"
"github.com/google/certificate-transparency-go/x509util"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/crypto/hkdf"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -137,21 +141,22 @@ type LogConfig struct {
// Roots is the path to the accepted roots as a PEM file.
Roots string

// Key is the path to the private key as a PKCS#8 PEM file.
// Seed is the path to a file containing a secret seed from which the log's
// private keys are derived. The whole file is used as HKDF input.
//
// To generate a new key, run:
// To generate a new seed, run:
//
// $ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -outform PEM -out key.pem
// $ head -c 32 /dev/urandom > seed.bin
//
Key string
Seed string

// PublicKey is the SubjectPublicKeyInfo for this log, base64 encoded.
//
// This is the same format as used in Google and Apple's log list JSON files.
//
// To generate from a private key, run:
// To generate this from a seed, run:
//
// $ openssl pkey -in key.pem -pubout -outform DER | base64 -w0
// $ sunlight-keygen log.example/logA seed.bin
//
// If provided, the loaded private Key is required to match it. Optional.
PublicKey string
Expand Down Expand Up @@ -308,21 +313,25 @@ func main() {
fatalError(logger, "failed to load roots", "err", err)
}

keyPEM, err := os.ReadFile(lc.Key)
seed, err := os.ReadFile(lc.Seed)
if err != nil {
fatalError(logger, "failed to load key", "err", err)
fatalError(logger, "failed to load seed", "err", err)
}
block, _ := pem.Decode(keyPEM)
if block == nil || block.Type != "PRIVATE KEY" {
fatalError(logger, "failed to parse key PEM")

ecdsaSecret := make([]byte, 32)
if _, err := io.ReadFull(hkdf.New(sha256.New, seed, []byte("sunlight"), []byte("ECDSA P-256 log key")), ecdsaSecret); err != nil {
fatalError(logger, "failed to derive ECDSA secret", "err", err)
}
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
k, err := keygen.ECDSA(elliptic.P256(), ecdsaSecret)
if err != nil {
fatalError(logger, "failed to parse key", "err", err)
fatalError(logger, "failed to generate ECDSA key", "err", err)
}
if _, ok := k.(*ecdsa.PrivateKey); !ok {
fatalError(logger, "key is not an ECDSA private key")

ed25519Secret := make([]byte, ed25519.SeedSize)
if _, err := io.ReadFull(hkdf.New(sha256.New, seed, []byte("sunlight"), []byte("Ed25519 log key")), ed25519Secret); err != nil {
fatalError(logger, "failed to derive Ed25519 key", "err", err)
}
wk := ed25519.NewKeyFromSeed(ed25519Secret)

if lc.PublicKey != "" {
cfgPubKey, err := base64.StdEncoding.DecodeString(lc.PublicKey)
Expand All @@ -335,8 +344,8 @@ func main() {
fatalError(logger, "failed to parse public key", "err", err)
}

if !k.(*ecdsa.PrivateKey).PublicKey.Equal(parsedPubKey) {
spki, err := x509.MarshalPKIXPublicKey(&k.(*ecdsa.PrivateKey).PublicKey)
if !k.PublicKey.Equal(parsedPubKey) {
spki, err := x509.MarshalPKIXPublicKey(&k.PublicKey)
if err != nil {
fatalError(logger, "failed to marshal public key from private key for display", "err", err)
}
Expand All @@ -357,7 +366,8 @@ func main() {

cc := &ctlog.Config{
Name: lc.Name,
Key: k.(*ecdsa.PrivateKey),
Key: k,
WitnessKey: wk,
Cache: lc.Cache,
PoolSize: lc.PoolSize,
Backend: b,
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.4
require (
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c
filippo.io/bigmod v0.0.3
filippo.io/keygen v0.0.0-20240718133620-7f162efbbd87
filippo.io/nistec v0.0.3
github.com/aws/aws-sdk-go-v2 v1.24.1
github.com/aws/aws-sdk-go-v2/config v1.26.6
Expand Down Expand Up @@ -46,7 +47,7 @@ require (
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.46.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 // indirect
google.golang.org/grpc v1.61.0 // indirect
Expand Down
10 changes: 8 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c h1:wvzox0eLO6CKQAMcOqz7o
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
filippo.io/bigmod v0.0.3 h1:qmdCFHmEMS+PRwzrW6eUrgA4Q3T8D6bRcjsypDMtWHM=
filippo.io/bigmod v0.0.3/go.mod h1:WxGvOYE0OUaBC2N112Dflb3CjOnMBuNRA2UWZc2UbPE=
filippo.io/keygen v0.0.0-20240718133620-7f162efbbd87 h1:HlcHAMbI9Xvw3aWnhPngghMl5AKE2GOvjmvSGOKzCcI=
filippo.io/keygen v0.0.0-20240718133620-7f162efbbd87/go.mod h1:nAs0+DyACEQGudhkTwlPC9atyqDYC7ZotgZR7D8OwXM=
filippo.io/nistec v0.0.3 h1:h336Je2jRDZdBCLy2fLDUd9E2unG32JLwcJi0JQE9Cw=
filippo.io/nistec v0.0.3/go.mod h1:84fxC9mi+MhC2AERXI4LSa8cmSVOzrFikg6hZ4IfCyw=
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
Expand Down Expand Up @@ -48,6 +50,8 @@ github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw=
github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
Expand Down Expand Up @@ -99,11 +103,13 @@ golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 h1:FSL3lRCkhaPFxqi0s9o+V4UI2WTzAVOvkgbd4kVV4Wg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014/go.mod h1:SaPjaZGWb0lPqs6Ittu0spdfrOArqji4ZdeP5IC/9N4=
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
Expand Down
101 changes: 88 additions & 13 deletions internal/ctlog/ctlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"log/slog"
"maps"
"math/rand"
mathrand "math/rand/v2"
"sync"
"time"

Expand Down Expand Up @@ -81,10 +84,11 @@ func (t tileWithBytes) String() string {
}

type Config struct {
Name string
Key *ecdsa.PrivateKey
PoolSize int
Cache string
Name string
Key *ecdsa.PrivateKey
WitnessKey ed25519.PrivateKey
PoolSize int
Cache string

Backend Backend
Lock LockBackend
Expand Down Expand Up @@ -286,17 +290,38 @@ func LoadLog(ctx context.Context, config *Config) (*Log, error) {
}

func openCheckpoint(config *Config, b []byte) (sunlight.Checkpoint, int64, error) {
v, err := sunlight.NewRFC6962Verifier(config.Name, config.Key.Public())
v1, err := sunlight.NewRFC6962Verifier(config.Name, config.Key.Public())
if err != nil {
return sunlight.Checkpoint{}, 0, fmt.Errorf("couldn't construct verifier: %w", err)
}
n, err := note.Open(b, note.VerifierList(v))
vk, err := note.NewEd25519VerifierKey(config.Name, config.WitnessKey.Public().(ed25519.PublicKey))
if err != nil {
return sunlight.Checkpoint{}, 0, fmt.Errorf("couldn't verify checkpoint signature: %w", err)
return sunlight.Checkpoint{}, 0, fmt.Errorf("couldn't construct verifier key: %w", err)
}
v2, err := note.NewVerifier(vk)
if err != nil {
return sunlight.Checkpoint{}, 0, fmt.Errorf("couldn't construct Ed25519 verifier: %w", err)
}
timestamp, err := sunlight.RFC6962SignatureTimestamp(n.Sigs[0])
n, err := note.Open(b, note.VerifierList(v1, v2))
if err != nil {
return sunlight.Checkpoint{}, 0, fmt.Errorf("couldn't extract timestamp: %w", err)
return sunlight.Checkpoint{}, 0, fmt.Errorf("couldn't verify checkpoint signature: %w", err)
}
var timestamp int64
var v1Found, v2Found bool
for _, sig := range n.Sigs {
switch sig.Hash {
case v1.KeyHash():
v1Found = true
timestamp, err = sunlight.RFC6962SignatureTimestamp(sig)
if err != nil {
return sunlight.Checkpoint{}, 0, fmt.Errorf("couldn't extract timestamp: %w", err)
}
case v2.KeyHash():
v2Found = true
}
}
if !v1Found || !v2Found {
return sunlight.Checkpoint{}, 0, errors.New("missing verifier signature")
}
c, err := sunlight.ParseCheckpoint(n.Text)
if err != nil {
Expand Down Expand Up @@ -596,7 +621,7 @@ func (l *Log) RunSequencer(ctx context.Context, period time.Duration) (err error
}()

// Randomly stagger the sequencers to avoid conflicting for resources.
time.Sleep(time.Duration(rand.Int63n(int64(period))))
time.Sleep(time.Duration(mathrand.Int64N(int64(period))))

t := time.NewTicker(period)
defer t.Stop()
Expand Down Expand Up @@ -845,13 +870,24 @@ func signTreeHead(c *Config, tree treeWithTimestamp) (checkpoint []byte, err err
if err != nil {
return nil, fmtErrorf("couldn't construct verifier: %w", err)
}
signer := &injectedSigner{v, sig}
rs := &injectedSigner{v, sig}

ws, err := newEd25519Signer(c.Name, c.WitnessKey)
if err != nil {
return nil, fmtErrorf("couldn't construct Ed25519 signer: %w", err)
}

signers := []note.Signer{rs, ws}
// Randomize the order to enforce forward-compatible client behavior.
mathrand.Shuffle(len(signers), func(i, j int) { signers[i], signers[j] = signers[j], signers[i] })

signedNote, err := note.Sign(&note.Note{
Text: sunlight.FormatCheckpoint(sunlight.Checkpoint{
Origin: c.Name,
Tree: tlog.Tree{N: tree.N, Hash: tree.Hash},
}),
}, signer)
UnverifiedSigs: greaseSignatures(c.Name),
}, signers...)
if err != nil {
return nil, fmtErrorf("couldn't sign note: %w", err)
}
Expand Down Expand Up @@ -893,6 +929,45 @@ func digitallySign(k *ecdsa.PrivateKey, msg []byte) ([]byte, error) {
return b.Bytes()
}

// greaseSignatures produces unverifiable but otherwise correct signatures.
// Clients MUST ignore unknown signatures, and including some "grease" ones
// ensures they do.
func greaseSignatures(name string) []note.Signature {
g1 := make([]byte, 5+mathrand.IntN(100))
rand.Read(g1)
g2 := make([]byte, 5+mathrand.IntN(100))
rand.Read(g2)
signatures := []note.Signature{
{Name: "grease.invalid", Hash: binary.BigEndian.Uint32(g1), Base64: base64.StdEncoding.EncodeToString(g1)},
{Name: name, Hash: binary.BigEndian.Uint32(g2), Base64: base64.StdEncoding.EncodeToString(g2)},
}
mathrand.Shuffle(len(signatures), func(i, j int) { signatures[i], signatures[j] = signatures[j], signatures[i] })
return signatures
}

// newEd25519Signer can be removed once note.NewEd25519SignerKey is added.
func newEd25519Signer(name string, key ed25519.PrivateKey) (note.Signer, error) {
vk, err := note.NewEd25519VerifierKey(name, key.Public().(ed25519.PublicKey))
if err != nil {
return nil, err
}
v, err := note.NewVerifier(vk)
if err != nil {
return nil, err
}
return &ed25519Signer{v, key}, nil
}

type ed25519Signer struct {
v note.Verifier
k ed25519.PrivateKey
}

func (s *ed25519Signer) Sign(msg []byte) ([]byte, error) { return ed25519.Sign(s.k, msg), nil }
func (s *ed25519Signer) Name() string { return s.v.Name() }
func (s *ed25519Signer) KeyHash() uint32 { return s.v.KeyHash() }
func (s *ed25519Signer) Verifier() note.Verifier { return s.v }

// hashReader returns hashes from l.edgeTiles and from overlay.
func (l *Log) hashReader(overlay map[int64]tlog.Hash) tlog.HashReaderFunc {
return func(indexes []int64) ([]tlog.Hash, error) {
Expand Down
Loading

0 comments on commit 83e7f51

Please sign in to comment.