Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store issuer chains #132

Merged
merged 15 commits into from
Aug 22, 2024
16 changes: 11 additions & 5 deletions personalities/sctfe/ct_server_gcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ import (
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"
gcpMap "github.com/transparency-dev/trillian-tessera/personalities/sctfe/storage/gcp"
gcpTessera "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 @@ -268,14 +269,19 @@ func setupAndRegister(ctx context.Context, deadline time.Duration, vCfg *sctfe.V

func newGCPStorage(ctx context.Context, vCfg *sctfe.ValidatedLogConfig, signer note.Signer) (*sctfe.CTStorage, error) {
cfg := vCfg.Config.GetGcp()
gcpCfg := gcp.Config{
gcpCfg := gcpTessera.Config{
ProjectID: cfg.ProjectId,
Bucket: cfg.Bucket,
Spanner: cfg.SpannerDbPath,
}
storage, err := gcp.New(ctx, gcpCfg, tessera.WithCheckpointSignerVerifier(signer, nil))
tesseraStorage, err := gcpTessera.New(ctx, gcpCfg, tessera.WithCheckpointSignerVerifier(signer, nil))
if err != nil {
return nil, fmt.Errorf("Failed to initialize GCP storage: %v", err)
return nil, fmt.Errorf("Failed to initialize GCP Tessera storage: %v", err)
}
return sctfe.NewCTSTorage(storage)

issuerStorage, err := gcpMap.NewIssuerStorage(ctx, cfg.ProjectId, cfg.Bucket, "fingerprints/", "application/pkix-cert")
if err != nil {
return nil, fmt.Errorf("Failed to initialize GCP issuer storage: %v", err)
}
return sctfe.NewCTSTorage(tesseraStorage, issuerStorage)
}
7 changes: 7 additions & 0 deletions personalities/sctfe/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,13 @@ func addChainInternal(ctx context.Context, li *logInfo, w http.ResponseWriter, r
return http.StatusBadRequest, fmt.Errorf("failed to build MerkleTreeLeaf: %s", err)
}

// TODO(phboneff): refactor entryFromChain to avoid recomputing hashes in AddIssuerChain
if len(chain) > 1 {
if err := li.storage.AddIssuerChain(ctx, chain[1:]); err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to store issuer chain: %s", err)
}
}

klog.V(2).Infof("%s: %s => storage.Add", li.LogOrigin, method)
idx, err := li.storage.Add(ctx, entry)
if err != nil {
Expand Down
10 changes: 6 additions & 4 deletions personalities/sctfe/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func TestAddChainWhitespace(t *testing.T) {
chunk2 := "\"MIIDnTCCAoWgAwIBAgIIQoIqW4Zvv+swDQYJKoZIhvcNAQELBQAwcTELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9uZG9uMQ8wDQYDVQQKDAZHb29nbGUxDDAKBgNVBAsMA0VuZzEhMB8GA1UEAwwYRmFrZUNlcnRpZmljYXRlQXV0aG9yaXR5MB4XDTE2MDUxMzE0MjY0NFoXDTE5MDcxMjE0MjY0NFowcjELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9uZG9uMQ8wDQYDVQQKDAZHb29nbGUxDDAKBgNVBAsMA0VuZzEiMCAGA1UEAwwZRmFrZUludGVybWVkaWF0ZUF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMqkDHpt6SYi1GcZyClAxr3LRDnn+oQBHbMEFUg3+lXVmEsq/xQO1s4naynV6I05676XvlMh0qPyJ+9GaBxvhHeFtGh4etQ9UEmJj55rSs50wA/IaDh+roKukQxthyTESPPgjqg+DPjh6H+h3Sn00Os6sjh3DxpOphTEsdtb7fmk8J0e2KjQQCjW/GlECzc359b9KbBwNkcAiYFayVHPLaCAdvzYVyiHgXHkEEs5FlHyhe2gNEG/81Io8c3E3DH5JhT9tmVRL3bpgpT8Kr4aoFhU2LXe45YIB1A9DjUm5TrHZ+iNtvE0YfYMR9L9C1HPppmX1CahEhTdog7laE1198UCAwEAAaM4MDYwDwYDVR0jBAgwBoAEAQIDBDASBgNVHRMBAf8ECDAGAQH/AgEAMA8GA1UdDwEB/wQFAwMH/4AwDQYJKoZIhvcNAQELBQADggEBAAHiOgwAvEzhrNMQVAz8a+SsyMIABXQ5P8WbJeHjkIipE4+5ZpkrZVXq9p8wOdkYnOHx4WNi9PVGQbLG9Iufh9fpk8cyyRWDi+V20/CNNtawMq3ClV3dWC98Tj4WX/BXDCeY2jK4jYGV+ds43HYV0ToBmvvrccq/U7zYMGFcQiKBClz5bTE+GMvrZWcO5A/Lh38i2YSF1i8SfDVnAOBlAgZmllcheHpGsWfSnduIllUvTsRvEIsaaqfVLl5QpRXBOq8tbjK85/2g6ear1oxPhJ1w9hds+WTFXkmHkWvKJebY13t3OfSjAyhaRSt8hdzDzHTFwjPjHT8h6dU7/hMdkUg=\""
epilog := "]}\n"

req, _ := parseChain(t, false, pemChain, info.roots.RawCertificates()[0])
req, leafChain := parseChain(t, false, pemChain, info.roots.RawCertificates()[0])
rsp := uint64(0)

var tests = []struct {
Expand Down Expand Up @@ -266,6 +266,7 @@ func TestAddChainWhitespace(t *testing.T) {
for _, test := range tests {
t.Run(test.descr, func(t *testing.T) {
if test.want == http.StatusOK {
info.storage.EXPECT().AddIssuerChain(deadlineMatcher(), cmpMatcher{leafChain[1:]}).Return(nil)
info.storage.EXPECT().Add(deadlineMatcher(), cmpMatcher{req}).Return(rsp, nil)
}

Expand Down Expand Up @@ -337,8 +338,9 @@ func TestAddChain(t *testing.T) {
pool := loadCertsIntoPoolOrDie(t, test.chain)
chain := createJSONChain(t, *pool)
if len(test.toSign) > 0 {
req, _ := parseChain(t, false, test.chain, info.roots.RawCertificates()[0])
req, leafChain := parseChain(t, false, test.chain, info.roots.RawCertificates()[0])
rsp := uint64(0)
info.storage.EXPECT().AddIssuerChain(deadlineMatcher(), cmpMatcher{leafChain[1:]}).Return(nil)
info.storage.EXPECT().Add(deadlineMatcher(), cmpMatcher{req}).Return(rsp, test.err)
}

Expand Down Expand Up @@ -425,9 +427,9 @@ func TestAddPrechain(t *testing.T) {
pool := loadCertsIntoPoolOrDie(t, test.chain)
chain := createJSONChain(t, *pool)
if len(test.toSign) > 0 {
req, _ := parseChain(t, true, test.chain, info.roots.RawCertificates()[0])
req, leafChain := parseChain(t, true, test.chain, info.roots.RawCertificates()[0])
rsp := uint64(0)

info.storage.EXPECT().AddIssuerChain(deadlineMatcher(), cmpMatcher{leafChain[1:]}).Return(nil)
info.storage.EXPECT().Add(deadlineMatcher(), cmpMatcher{req}).Return(rsp, test.err)
}

Expand Down
15 changes: 15 additions & 0 deletions personalities/sctfe/mockstorage/mock_ct_storage.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 36 additions & 5 deletions personalities/sctfe/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,65 @@ package sctfe

import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"

"github.com/google/certificate-transparency-go/x509"
tessera "github.com/transparency-dev/trillian-tessera"
"github.com/transparency-dev/trillian-tessera/ctonly"
)

// Storage provides all the storage primitives necessary to write to a ct-static-api log.
type Storage interface {
// Add assign an index to the provided Entry, stages the entry for integration, and return it the assigned index.
// Add assigns an index to the provided Entry, stages the entry for integration, and return it the assigned index.
Add(context.Context, *ctonly.Entry) (uint64, error)
// AddIssuerChain stores every the chain certificate in a content-addressable store under their sha256 hash.
AddIssuerChain(context.Context, []*x509.Certificate) error
}

type KV struct {
K []byte
V []byte
}

type IssuerStorage interface {
Exists(ctx context.Context, key []byte) (bool, error)
AddIssuers(ctx context.Context, kv []KV) error
}

// CTStorage implements Storage.
type CTStorage struct {
storeData func(context.Context, *ctonly.Entry) (uint64, error)
// TODO(phboneff): add storeExtraData
// TODO(phboneff): add dedupe
issuers IssuerStorage
}

// NewCTStorage instantiates a CTStorage object.
func NewCTSTorage(logStorage tessera.Storage) (*CTStorage, error) {
func NewCTSTorage(logStorage tessera.Storage, issuerStorage IssuerStorage) (*CTStorage, error) {
ctStorage := &CTStorage{
storeData: tessera.NewCertificateTransparencySequencedWriter(logStorage),
issuers: issuerStorage,
}
return ctStorage, nil
}

// Add stores CT entries.
func (cts CTStorage) Add(ctx context.Context, entry *ctonly.Entry) (uint64, error) {
func (cts *CTStorage) Add(ctx context.Context, entry *ctonly.Entry) (uint64, error) {
// TODO(phboneff): add deduplication and chain storage
return cts.storeData(ctx, entry)
}

// AddIssuerChain stores every chain certificate under its sha256.
// If an object is already stored under this hash, continues.
func (cts *CTStorage) AddIssuerChain(ctx context.Context, chain []*x509.Certificate) error {
kvs := []KV{}
for _, c := range chain {
id := sha256.Sum256(c.Raw)
key := []byte(hex.EncodeToString(id[:]))
kvs = append(kvs, KV{K: key, V: c.Raw})
}
if err := cts.issuers.AddIssuers(ctx, kvs); err != nil {
return fmt.Errorf("error storing intermediates: %v", err)
}
return nil
}
133 changes: 133 additions & 0 deletions personalities/sctfe/storage/gcp/issuers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright 2024 The Tessera authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package gcp implements SCTFE storage systems for issuers.
//
// The interfaces are defined in sctfe/storage.go
package gcp

import (
"context"
"fmt"
"net/http"
"path"

gcs "cloud.google.com/go/storage"
"github.com/transparency-dev/trillian-tessera/personalities/sctfe"
"google.golang.org/api/googleapi"
"google.golang.org/api/iterator"
"k8s.io/klog/v2"
)

// IssuersStorage is a key value store backed by GCS on GCP to store issuer chains.
type IssuersStorage struct {
bucket *gcs.BucketHandle
prefix string
contentType string
}

// NewIssuerStorage creates a new GCSStorage.
//
// The specified bucket must exist or an error will be returned.
func NewIssuerStorage(ctx context.Context, projectID string, bucket string, prefix string, contentType string) (*IssuersStorage, error) {
c, err := gcs.NewClient(ctx, gcs.WithJSONReads())
if err != nil {
return nil, fmt.Errorf("failed to create GCS client: %v", err)
}

it := c.Buckets(ctx, projectID)
for {
bAttrs, err := it.Next()
if err == iterator.Done {
return nil, fmt.Errorf("bucket %q does not exist, please create it", bucket)
}
if err != nil {
return nil, fmt.Errorf("error scanning buckets: %v", err)
}
if bAttrs.Name == bucket {
break
}
}
r := &IssuersStorage{
bucket: c.Bucket(bucket),
prefix: prefix,
contentType: contentType,
}

return r, nil
}

// keyToObjName converts bytes to a GCS object name.
func (s *IssuersStorage) keyToObjName(key []byte) string {
return path.Join(s.prefix, string(key))
}

// Exists checks whether a value is stored under key.
func (s *IssuersStorage) Exists(ctx context.Context, key []byte) (bool, error) {
objName := s.keyToObjName(key)
obj := s.bucket.Object(objName)
_, err := obj.Attrs(ctx)
if err == gcs.ErrObjectNotExist {
return false, nil
}
if err != nil {
return false, fmt.Errorf("error fetching attributes for %q :%v", objName, err)
}
klog.V(2).Infof("Exists: object %q already exists in bucket %q", objName, s.bucket.BucketName())
return true, nil
}

// AddIssuers stores all Issuers values under their Key.
//
// If there is already an object under a given key, it does not override it.
func (s *IssuersStorage) AddIssuers(ctx context.Context, kv []sctfe.KV) error {
// We first try and see if this issuer cert has already been stored since reads
// are cheaper than writes.
// TODO(phboneff): monitor usage, eventually write directly depending on usage patterns
toStore := []sctfe.KV{}
for _, kv := range kv {
ok, err := s.Exists(ctx, kv.K)
if err != nil {
return fmt.Errorf("error checking if issuer %q exists: %s", string(kv.K), err)
}
if !ok {
toStore = append(toStore, kv)
}
}
// TODO(phboneff): add parallel writes
for _, kv := range toStore {
objName := s.keyToObjName(kv.K)
obj := s.bucket.Object(objName)

// Don't overwrite if it already exists
// TODO(phboneff): consider reading the object to make sure it's identical
w := obj.If(gcs.Conditions{DoesNotExist: true}).NewWriter(ctx)
w.ObjectAttrs.ContentType = s.contentType

if _, err := w.Write(kv.V); err != nil {
return fmt.Errorf("failed to write object %q to bucket %q: %w", objName, s.bucket.BucketName(), err)
}

if err := w.Close(); err != nil {
// If we run into a precondition failure error, it means that the object already exists.
if ee, ok := err.(*googleapi.Error); ok && ee.Code == http.StatusPreconditionFailed {
klog.V(2).Infof("Add: object %q already exists in bucket %q, continuing", objName, s.bucket.BucketName())
return nil
}

return fmt.Errorf("failed to close write on %q: %v", objName, err)
}
}
return nil
}
2 changes: 1 addition & 1 deletion storage/gcp/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -669,13 +669,13 @@ func (s *gcsStorage) setObject(ctx context.Context, objName string, data []byte,
obj := bkt.Object(objName)

var w *gcs.Writer
w.ObjectAttrs.ContentType = contType
if cond == nil {
w = obj.NewWriter(ctx)

} else {
w = obj.If(*cond).NewWriter(ctx)
}
w.ObjectAttrs.ContentType = contType
if _, err := w.Write(data); err != nil {
return fmt.Errorf("failed to write object %q to bucket %q: %w", objName, s.bucket, err)
}
Expand Down
Loading