Skip to content

Commit

Permalink
Pass a signer to Tessera (#126)
Browse files Browse the repository at this point in the history
* read validated config instead of full config

* pass a callback through instance options to create storage

* add a custom CT signer

* pass a signer option to tessera

* raise an error if the createStorage function doesn't exist or fails

* rename a few things

* fix ifelse conditions

* simplify hashing calls

* more docstrings

* fix last test
  • Loading branch information
phbnf authored Aug 9, 2024
1 parent d2a7cc7 commit e6136d5
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 23 deletions.
14 changes: 7 additions & 7 deletions personalities/sctfe/ct_server_gcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/cors"
"github.com/tomasen/realip"
tessera "github.com/transparency-dev/trillian-tessera"
"github.com/transparency-dev/trillian-tessera/personalities/sctfe"
"github.com/transparency-dev/trillian-tessera/personalities/sctfe/configpb"
"github.com/transparency-dev/trillian-tessera/storage/gcp"
"golang.org/x/mod/sumdb/note"
"google.golang.org/protobuf/proto"
"k8s.io/klog/v2"
)
Expand Down Expand Up @@ -267,11 +269,8 @@ func setupAndRegister(ctx context.Context, deadline time.Duration, vCfg *sctfe.V

switch vCfg.Config.StorageConfig.(type) {
case *configpb.LogConfig_Gcp:
storage, err := newGCPStorage(ctx, vCfg.Config.GetGcp())
if err != nil {
return nil, fmt.Errorf("failed to initialize GCP storage: %v", err)
}
opts.Storage = storage
klog.Info("Found GCP storage config, will set up GCP tessera storage")
opts.CreateStorage = newGCPStorage
default:
return nil, fmt.Errorf("unrecognized storage config")
}
Expand All @@ -286,13 +285,14 @@ func setupAndRegister(ctx context.Context, deadline time.Duration, vCfg *sctfe.V
return inst, nil
}

func newGCPStorage(ctx context.Context, cfg *configpb.GCPConfig) (*sctfe.CTStorage, error) {
func newGCPStorage(ctx context.Context, vCfg *sctfe.ValidatedLogConfig, signer note.Signer) (*sctfe.CTStorage, error) {
cfg := vCfg.Config.GetGcp()
gcpCfg := gcp.Config{
ProjectID: cfg.ProjectId,
Bucket: cfg.Bucket,
Spanner: cfg.SpannerDbPath,
}
storage, err := gcp.New(ctx, gcpCfg)
storage, err := gcp.New(ctx, gcpCfg, tessera.WithCheckpointSignerVerifier(signer, nil))
if err != nil {
return nil, fmt.Errorf("Failed to initialize GCP storage: %v", err)
}
Expand Down
3 changes: 2 additions & 1 deletion personalities/sctfe/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,13 +217,14 @@ func newLogInfo(
validationOpts CertValidationOpts,
signer crypto.Signer,
timeSource TimeSource,
storage Storage,
) *logInfo {
vCfg := instanceOpts.Validated
cfg := vCfg.Config

li := &logInfo{
LogOrigin: cfg.Origin,
storage: instanceOpts.Storage,
storage: storage,
signer: signer,
TimeSource: timeSource,
instanceOpts: instanceOpts,
Expand Down
4 changes: 2 additions & 2 deletions personalities/sctfe/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ func setupTest(t *testing.T, pemRoots []string, signer crypto.Signer) handlerTes

cfg := &configpb.LogConfig{Origin: "example.com"}
vCfg := &ValidatedLogConfig{Config: cfg}
iOpts := InstanceOptions{Validated: vCfg, Storage: info.storage, Deadline: time.Millisecond * 500, MetricFactory: monitoring.InertMetricFactory{}, RequestLog: new(DefaultRequestLog)}
info.li = newLogInfo(iOpts, vOpts, signer, fakeTimeSource)
iOpts := InstanceOptions{Validated: vCfg, Deadline: time.Millisecond * 500, MetricFactory: monitoring.InertMetricFactory{}, RequestLog: new(DefaultRequestLog)}
info.li = newLogInfo(iOpts, vOpts, signer, fakeTimeSource, info.storage)

for _, pemRoot := range pemRoots {
if !info.roots.AppendCertsFromPEM([]byte(pemRoot)) {
Expand Down
22 changes: 19 additions & 3 deletions personalities/sctfe/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ import (
"github.com/google/certificate-transparency-go/x509util"
"github.com/google/trillian/crypto/keys"
"github.com/google/trillian/monitoring"
"golang.org/x/mod/sumdb/note"
)

// InstanceOptions describes the options for a log instance.
type InstanceOptions struct {
// Validated holds the original configuration options for the log, and some
// of its fields parsed as a result of validating it.
Validated *ValidatedLogConfig
// Storage is a corresponding Tessera storage implementation.
Storage Storage
// CreateStorage instantiates a Tessera storage implementation with a signer option.
CreateStorage func(context.Context, *ValidatedLogConfig, note.Signer) (*CTStorage, error)
// Deadline is a timeout for Tessera requests.
Deadline time.Duration
// MetricFactory allows creating metrics.
Expand Down Expand Up @@ -141,7 +142,22 @@ func setUpLogInfo(ctx context.Context, opts InstanceOptions) (*logInfo, error) {
return nil, fmt.Errorf("failed to parse RejectExtensions: %v", err)
}

logInfo := newLogInfo(opts, validationOpts, signer, new(SystemTimeSource))
logID, err := GetCTLogID(signer.Public())
if err != nil {
return nil, fmt.Errorf("failed to get logID for signing: %v", err)
}
timeSource := new(SystemTimeSource)
ctSigner := NewCpSigner(signer, vCfg.Config.Origin, logID, timeSource)

if opts.CreateStorage == nil {
return nil, fmt.Errorf("failed to initiate storage backend: nil createStorage")
}
storage, err := opts.CreateStorage(ctx, opts.Validated, ctSigner)
if err != nil {
return nil, fmt.Errorf("failed to initiate storage backend: %v", err)
}

logInfo := newLogInfo(opts, validationOpts, signer, timeSource, storage)
return logInfo, nil
}

Expand Down
58 changes: 48 additions & 10 deletions personalities/sctfe/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/google/trillian/crypto/keyspb"
"github.com/google/trillian/monitoring"
"github.com/transparency-dev/trillian-tessera/personalities/sctfe/configpb"
"golang.org/x/mod/sumdb/note"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/timestamppb"
)
Expand All @@ -37,6 +38,10 @@ func init() {
keys.RegisterHandler(&keyspb.PEMKeyFile{}, pem.FromProto)
}

func fakeCTStorage(_ context.Context, _ *ValidatedLogConfig, _ note.Signer) (*CTStorage, error) {
return &CTStorage{}, nil
}

func TestSetUpInstance(t *testing.T) {
ctx := context.Background()

Expand All @@ -45,9 +50,10 @@ func TestSetUpInstance(t *testing.T) {
wrongPassPrivKey := mustMarshalAny(&keyspb.PEMKeyFile{Path: "./testdata/ct-http-server.privkey.pem", Password: "dirkly"})

var tests = []struct {
desc string
cfg *configpb.LogConfig
wantErr string
desc string
cfg *configpb.LogConfig
ctStorage func(context.Context, *ValidatedLogConfig, note.Signer) (*CTStorage, error)
wantErr string
}{
{
desc: "valid",
Expand All @@ -57,6 +63,7 @@ func TestSetUpInstance(t *testing.T) {
PrivateKey: privKey,
StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}},
},
ctStorage: fakeCTStorage,
},
{
desc: "no-roots",
Expand All @@ -65,7 +72,8 @@ func TestSetUpInstance(t *testing.T) {
PrivateKey: privKey,
StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}},
},
wantErr: "specify RootsPemFile",
ctStorage: fakeCTStorage,
wantErr: "specify RootsPemFile",
},
{
desc: "missing-root-cert",
Expand All @@ -75,7 +83,8 @@ func TestSetUpInstance(t *testing.T) {
PrivateKey: privKey,
StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}},
},
wantErr: "failed to read trusted roots",
ctStorage: fakeCTStorage,
wantErr: "failed to read trusted roots",
},
{
desc: "missing-privkey",
Expand All @@ -85,7 +94,8 @@ func TestSetUpInstance(t *testing.T) {
PrivateKey: missingPrivKey,
StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}},
},
wantErr: "failed to load private key",
ctStorage: fakeCTStorage,
wantErr: "failed to load private key",
},
{
desc: "privkey-wrong-password",
Expand All @@ -95,7 +105,8 @@ func TestSetUpInstance(t *testing.T) {
PrivateKey: wrongPassPrivKey,
StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}},
},
wantErr: "failed to load private key",
ctStorage: fakeCTStorage,
wantErr: "failed to load private key",
},
{
desc: "valid-ekus-1",
Expand All @@ -106,6 +117,7 @@ func TestSetUpInstance(t *testing.T) {
ExtKeyUsages: []string{"Any"},
StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}},
},
ctStorage: fakeCTStorage,
},
{
desc: "valid-ekus-2",
Expand All @@ -116,6 +128,7 @@ func TestSetUpInstance(t *testing.T) {
ExtKeyUsages: []string{"Any", "ServerAuth", "TimeStamping"},
StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}},
},
ctStorage: fakeCTStorage,
},
{
desc: "valid-reject-ext",
Expand All @@ -126,6 +139,7 @@ func TestSetUpInstance(t *testing.T) {
RejectExtensions: []string{"1.2.3.4", "5.6.7.8"},
StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}},
},
ctStorage: fakeCTStorage,
},
{
desc: "invalid-reject-ext",
Expand All @@ -136,7 +150,31 @@ func TestSetUpInstance(t *testing.T) {
RejectExtensions: []string{"1.2.3.4", "one.banana.two.bananas"},
StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}},
},
wantErr: "one",
ctStorage: fakeCTStorage,
wantErr: "one",
},
{
desc: "missing-create-storage",
cfg: &configpb.LogConfig{
Origin: "log",
RootsPemFile: []string{"./testdata/fake-ca.cert"},
PrivateKey: privKey,
StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}},
},
wantErr: "failed to initiate storage backend",
},
{
desc: "failing-create-storage",
cfg: &configpb.LogConfig{
Origin: "log",
RootsPemFile: []string{"./testdata/fake-ca.cert"},
PrivateKey: privKey,
StorageConfig: &configpb.LogConfig_Gcp{Gcp: &configpb.GCPConfig{Bucket: "bucket", SpannerDbPath: "spanner"}},
},
ctStorage: func(_ context.Context, _ *ValidatedLogConfig, _ note.Signer) (*CTStorage, error) {
return nil, fmt.Errorf("I failed")
},
wantErr: "failed to initiate storage backend",
},
}

Expand All @@ -146,7 +184,7 @@ func TestSetUpInstance(t *testing.T) {
if err != nil {
t.Fatalf("ValidateLogConfig(): %v", err)
}
opts := InstanceOptions{Validated: vCfg, Deadline: time.Second, MetricFactory: monitoring.InertMetricFactory{}}
opts := InstanceOptions{Validated: vCfg, Deadline: time.Second, MetricFactory: monitoring.InertMetricFactory{}, CreateStorage: test.ctStorage}

if _, err := SetUpInstance(ctx, opts); err != nil {
if test.wantErr == "" {
Expand Down Expand Up @@ -237,7 +275,7 @@ func TestSetUpInstanceSetsValidationOpts(t *testing.T) {
if err != nil {
t.Fatalf("ValidateLogConfig(): %v", err)
}
opts := InstanceOptions{Validated: vCfg, Deadline: time.Second, MetricFactory: monitoring.InertMetricFactory{}}
opts := InstanceOptions{Validated: vCfg, Deadline: time.Second, MetricFactory: monitoring.InertMetricFactory{}, CreateStorage: fakeCTStorage}

inst, err := SetUpInstance(ctx, opts)
if err != nil {
Expand Down
108 changes: 108 additions & 0 deletions personalities/sctfe/serialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import (
"crypto"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"fmt"

"github.com/google/certificate-transparency-go/tls"
"github.com/transparency-dev/formats/log"
"golang.org/x/mod/sumdb/note"

ct "github.com/google/certificate-transparency-go"
)
Expand Down Expand Up @@ -64,3 +67,108 @@ func buildV1SCT(signer crypto.Signer, leaf *ct.MerkleTreeLeaf) (*ct.SignedCertif
Signature: digitallySigned,
}, nil
}

type RFC6962NoteSignature struct {
timestamp uint64
signature ct.DigitallySigned
}

// buildCp builds a https://c2sp.org/static-ct-api checkpoint.
// TODO(phboneff): add tests
func buildCp(signer crypto.Signer, size uint64, timeMilli uint64, hash []byte) ([]byte, error) {
sth := ct.SignedTreeHead{
Version: ct.V1,
TreeSize: size,
Timestamp: timeMilli,
}
copy(sth.SHA256RootHash[:], hash)

sthBytes, err := ct.SerializeSTHSignatureInput(sth)
if err != nil {
return nil, fmt.Errorf("ct.SerializeSTHSignatureInput(): %v", err)
}

h := sha256.Sum256(sthBytes)
signature, err := signer.Sign(rand.Reader, h[:], crypto.SHA256)
if err != nil {
return nil, err
}

rfc6962Note := RFC6962NoteSignature{
timestamp: sth.Timestamp,
signature: ct.DigitallySigned{
Algorithm: tls.SignatureAndHashAlgorithm{
Hash: tls.SHA256,
Signature: tls.SignatureAlgorithmFromPubKey(signer.Public()),
},
Signature: signature,
},
}

sig, err := tls.Marshal(rfc6962Note)
if err != nil {
return nil, fmt.Errorf("couldn't encode RFC6962NoteSignature: %w", err)
}

return sig, nil
}

// CpSigner implements note.Signer. It can generate https://c2sp.org/static-ct-api checkpoints.
type CpSigner struct {
sthSigner crypto.Signer
origin string
keyHash uint32
timeSource TimeSource
}

// Sign takes an unsigned checkpoint, and signs it with a https://c2sp.org/static-ct-api signature.
// Returns an error if the message doesn't parse as a checkpoint, or if the
// checkpoint origin doesn't match with the Signer's origin.
// TODO(phboneff): add tests
func (cts *CpSigner) Sign(msg []byte) ([]byte, error) {
ckpt := &log.Checkpoint{}
rest, err := ckpt.Unmarshal(msg)

if len(rest) != 0 {
return nil, fmt.Errorf("checkpoint contains trailing data: %s", string(rest))
} else if err != nil {
return nil, fmt.Errorf("ckpt.Unmarshal: %v", err)
} else if ckpt.Origin != cts.origin {
return nil, fmt.Errorf("checkpoint's origin %s doesn't match signer's origin %s", ckpt.Origin, cts.origin)
}

// TODO(phboneff): make sure that it's ok to generate the timestamp here
t := uint64(cts.timeSource.Now().UnixMilli())
sig, err := buildCp(cts.sthSigner, ckpt.Size, t, ckpt.Hash[:])
if err != nil {
return nil, fmt.Errorf("coudn't sign CT checkpoint: %v", err)
}
return sig, nil
}

func (cts *CpSigner) Name() string {
return cts.origin
}

func (cts *CpSigner) KeyHash() uint32 {
return cts.keyHash
}

// NewCpSigner returns a new note signer that can sign https://c2sp.org/static-ct-api checkpoints.
// TODO(phboneff): add tests
func NewCpSigner(signer crypto.Signer, origin string, logID [32]byte, timeSource TimeSource) note.Signer {
h := sha256.New()
h.Write([]byte(origin))
h.Write([]byte{0x0A}) // newline
h.Write([]byte{0x05}) // signature type
h.Write(logID[:])
sum := h.Sum(nil)

ctSigner := &CpSigner{
sthSigner: signer,
origin: origin,
keyHash: binary.BigEndian.Uint32(sum),
timeSource: timeSource,
}
return ctSigner
}

0 comments on commit e6136d5

Please sign in to comment.