Skip to content

Commit

Permalink
Add issuance chain service (#1452)
Browse files Browse the repository at this point in the history
* Add issuance chain service

* Remove IsStorageBackendEnabled methods

* Remove IssuanceChainStorageBackend and its deps

* Add ChainHash function comment

* Add issuance chain service tests

* Remove context from NewIssuanceChainService

* Unexport `ChainHash` in IssuanceChainService

* Set issuance chain service to be unexported

* Set `newIssuanceChainService` to unexported function

* Convert `issuanceChainService.chainHash` to static `issuanceChainHash` method

* Add go routine for issuance chain cache set

* Fix race condition for concurrent read/write on fake storage and cache
  • Loading branch information
roger2hk authored May 2, 2024
1 parent 3926d60 commit 8d12fb6
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 0 deletions.
89 changes: 89 additions & 0 deletions trillian/ctfe/services.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2024 Google LLC
//
// 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 ctfe

import (
"context"
"crypto/sha256"

"github.com/google/certificate-transparency-go/trillian/ctfe/cache"
"github.com/google/certificate-transparency-go/trillian/ctfe/storage"
"k8s.io/klog/v2"
)

type issuanceChainService struct {
storage storage.IssuanceChainStorage
cache cache.IssuanceChainCache
}

func newIssuanceChainService(s storage.IssuanceChainStorage, c cache.IssuanceChainCache) *issuanceChainService {
service := &issuanceChainService{
storage: s,
cache: c,
}

return service
}

// GetByHash returns the issuance chain with hash as the input.
func (s *issuanceChainService) GetByHash(ctx context.Context, hash []byte) ([]byte, error) {
// Return if found in cache.
chain, err := s.cache.Get(ctx, hash)
if chain != nil || err != nil {
return chain, err
}

// Find in storage if cache miss.
chain, err = s.storage.FindByKey(ctx, hash)
if err != nil {
return nil, err
}

// If there is any error from cache set, do not return the error because
// the chain is still available for read.
go func(ctx context.Context, hash, chain []byte) {
if err := s.cache.Set(ctx, hash, chain); err != nil {
klog.Errorf("failed to set hash and chain into cache: %v", err)
}
}(ctx, hash, chain)

return chain, nil
}

// Add adds the issuance chain into the storage and cache and returns the hash
// of the chain.
func (s *issuanceChainService) Add(ctx context.Context, chain []byte) ([]byte, error) {
hash := issuanceChainHash(chain)

if err := s.storage.Add(ctx, hash, chain); err != nil {
return nil, err
}

// If there is any error from cache set, do not return the error because
// the chain is already stored.
go func(ctx context.Context, hash, chain []byte) {
if err := s.cache.Set(ctx, hash, chain); err != nil {
klog.Errorf("failed to set hash and chain into cache: %v", err)
}
}(ctx, hash, chain)

return hash, nil
}

// issuanceChainHash returns the SHA-256 hash of the chain.
func issuanceChainHash(chain []byte) []byte {
checksum := sha256.Sum256(chain)
return checksum[:]
}
116 changes: 116 additions & 0 deletions trillian/ctfe/services_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2024 Google LLC
//
// 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 ctfe

import (
"bytes"
"context"
"crypto/sha256"
"os"
"sync"
"testing"
)

func TestIssuanceChainServiceAddAndGet(t *testing.T) {
tests := []struct {
chain []byte
}{
{readTestData(t, "leaf00.chain")},
{readTestData(t, "leaf01.chain")},
{readTestData(t, "leaf02.chain")},
{nil},
}

ctx := context.Background()
storage := &fakeIssuanceChainStorage{}
cache := &fakeIssuanceChainCache{}
issuanceChainService := newIssuanceChainService(storage, cache)

for _, test := range tests {
hash, err := issuanceChainService.Add(ctx, test.chain)
if err != nil {
t.Errorf("IssuanceChainService.Add(): %v", err)
}

got, err := issuanceChainService.GetByHash(ctx, hash)
if err != nil {
t.Errorf("IssuanceChainService.GetByHash(): %v", err)
}

if !bytes.Equal(got, test.chain) {
t.Errorf("GetByHash = %v, want %v", got, test.chain)
}
}
}

func TestIssuanceChainHashLen(t *testing.T) {
want := sha256.Size
tests := []struct {
chain []byte
}{
{readTestData(t, "leaf00.chain")},
{readTestData(t, "leaf01.chain")},
{readTestData(t, "leaf02.chain")},
{nil},
}

for _, test := range tests {
got := len(issuanceChainHash(test.chain))
if got != want {
t.Errorf("len(issuanceChainHash(%v)) = %d, want %d", test.chain, got, want)
}
}
}

func readTestData(t *testing.T, filename string) []byte {
t.Helper()

data, err := os.ReadFile("../testdata/" + filename)
if err != nil {
t.Fatal(err)
}

return data
}

type fakeIssuanceChainStorage struct {
chains sync.Map
}

func (s *fakeIssuanceChainStorage) FindByKey(_ context.Context, key []byte) ([]byte, error) {
val, _ := s.chains.Load(string(key))
chain, _ := val.([]byte)
return chain, nil
}

func (s *fakeIssuanceChainStorage) Add(_ context.Context, key []byte, chain []byte) error {
s.chains.Store(string(key), chain)
return nil
}

type fakeIssuanceChainCache struct {
chains sync.Map
}

func (c *fakeIssuanceChainCache) Get(_ context.Context, key []byte) ([]byte, error) {
val, _ := c.chains.Load(string(key))
chain, _ := val.([]byte)
return chain, nil
}

func (c *fakeIssuanceChainCache) Set(_ context.Context, key []byte, chain []byte) error {
c.chains.Store(string(key), chain)
return nil
}

0 comments on commit 8d12fb6

Please sign in to comment.