Skip to content

Commit

Permalink
Add IssuanceChainStorage MySQL implementation (#1462)
Browse files Browse the repository at this point in the history
* Add IssuanceChainStorage MySQL implementation

* Add err check for issuanceChainStorage.Add in test

* Add strict SQL mode comment

* Panic when the MySQL database cannot be opened

* Add a separate error message for mysql data source name prefix check

* Replace `panic` with `klog.Exitf`
  • Loading branch information
roger2hk authored May 7, 2024
1 parent a8de77a commit 9302d5f
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ require (
cloud.google.com/go/monitoring v1.18.0 // indirect
cloud.google.com/go/trace v1.10.5 // indirect
contrib.go.opencensus.io/exporter/stackdriver v0.13.14 // indirect
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
github.com/aws/aws-sdk-go v1.46.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ contrib.go.opencensus.io/exporter/stackdriver v0.13.14 h1:zBakwHardp9Jcb8sQHcHpX
contrib.go.opencensus.io/exporter/stackdriver v0.13.14/go.mod h1:5pSSGY0Bhuk7waTHuDf4aQ8D2DrhgETRo9fy6k3Xlzc=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/aws/aws-sdk-go v1.46.4 h1:48tKgtm9VMPkb6y7HuYlsfhQmoIRAsTEXTsWLVlty4M=
github.com/aws/aws-sdk-go v1.46.4/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
Expand Down Expand Up @@ -156,6 +158,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down
106 changes: 106 additions & 0 deletions trillian/ctfe/storage/mysql/mysql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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 mysql defines the IssuanceChainStorage type, which implements IssuanceChainStorage interface with FindByKey and Add methods.
package mysql

import (
"context"
"database/sql"
"errors"
"fmt"
"strings"

"k8s.io/klog/v2"

"github.com/go-sql-driver/mysql"
)

const (
selectIssuanceChainByKeySQL = "SELECT c.ChainValue FROM IssuanceChain AS c WHERE c.IdentityHash = ?"
insertIssuanceChainSQL = "INSERT INTO IssuanceChain(IdentityHash, ChainValue) VALUES (?, ?)"
)

type IssuanceChainStorage struct {
db *sql.DB
}

// NewIssuanceChainStorage takes the database connection string as the input and return the IssuanceChainStorage.
func NewIssuanceChainStorage(ctx context.Context, dbConn string) *IssuanceChainStorage {
db, err := open(ctx, dbConn)
if err != nil {
klog.Exitf(fmt.Sprintf("failed to open database: %v", err))
}

return &IssuanceChainStorage{
db: db,
}
}

// FindByKey returns the key-value pair of issuance chain by the key.
func (s *IssuanceChainStorage) FindByKey(ctx context.Context, key []byte) ([]byte, error) {
row := s.db.QueryRowContext(ctx, selectIssuanceChainByKeySQL, key)
if err := row.Err(); err != nil {
return nil, err
}

var chain []byte
if err := row.Scan(&chain); err != nil {
return nil, err
}

return chain, nil
}

// Add inserts the key-value pair of issuance chain.
func (s *IssuanceChainStorage) Add(ctx context.Context, key []byte, chain []byte) error {
_, err := s.db.ExecContext(ctx, insertIssuanceChainSQL, key, chain)
if err != nil {
// Ignore duplicated key error.
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
return nil
}
return err
}

return nil
}

// open takes the data source name and returns the sql.DB object.
func open(ctx context.Context, dataSourceName string) (*sql.DB, error) {
// Verify data source name format.
conn := strings.Split(dataSourceName, "://")
if len(conn) != 2 {
return nil, errors.New("could not parse MySQL data source name")
}
if conn[0] != "mysql" {
return nil, errors.New("expect data source name to start with mysql")
}

db, err := sql.Open("mysql", conn[1])
if err != nil {
// Don't log data source name as it could contain credentials.
klog.Errorf("could not open MySQL database, check config: %s", err)
return nil, err
}

// Enable strict SQL mode to ensure consistent behaviour among different storage engines when handling invalid or missing values in data-change statements.
if _, err := db.ExecContext(ctx, "SET sql_mode = 'STRICT_ALL_TABLES'"); err != nil {
klog.Warningf("failed to set strict mode on mysql db: %s", err)
return nil, err
}

return db, nil
}
110 changes: 110 additions & 0 deletions trillian/ctfe/storage/mysql/mysql_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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 mysql

import (
"bytes"
"context"
"crypto/sha256"
"database/sql"
"os"
"testing"

"github.com/DATA-DOG/go-sqlmock"
)

func TestIssuanceChainFindByKeySuccess(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()

Check failure on line 33 in trillian/ctfe/storage/mysql/mysql_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `db.Close` is not checked (errcheck)

testVal := readTestData(t, "leaf00.chain")
testKey := sha256.Sum256(testVal)

issuanceChainMockRows := sqlmock.NewRows([]string{"ChainValue"}).AddRow(testVal)
mock.ExpectQuery(selectIssuanceChainByKeySQL).WillReturnRows(issuanceChainMockRows)

storage := mockIssuanceChainStorage(db)
got, err := storage.FindByKey(context.Background(), testKey[:])
if err != nil {
t.Errorf("issuanceChainStorage.FindByKey: %v", err)
}
if !bytes.Equal(got, testVal) {
t.Errorf("got: %v, want: %v", got, testVal)
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}

func TestIssuanceChainAddSuccess(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()

Check failure on line 60 in trillian/ctfe/storage/mysql/mysql_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `db.Close` is not checked (errcheck)

tests := setupTestData(t,
"leaf00.chain",
"leaf01.chain",
"leaf02.chain",
)

storage := mockIssuanceChainStorage(db)
for k, v := range tests {
mock.ExpectExec("INSERT INTO IssuanceChain").WithArgs([]byte(k), v).WillReturnResult(sqlmock.NewResult(1, 1))
if err := storage.Add(context.Background(), []byte(k), v); err != nil {
t.Errorf("issuanceChainStorage.Add: %v", err)
}
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}

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

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

return data
}

func setupTestData(t *testing.T, filenames ...string) map[string][]byte {
t.Helper()

data := make(map[string][]byte, len(filenames))

for _, filename := range filenames {
val := readTestData(t, filename)
key := sha256.Sum256(val)
data[string(key[:])] = val
}

return data
}

func mockIssuanceChainStorage(db *sql.DB) *IssuanceChainStorage {
return &IssuanceChainStorage{
db: db,
}
}
24 changes: 24 additions & 0 deletions trillian/ctfe/storage/mysql/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- 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.

-- MySQL / MariaDB version of the CTFE database schema

-- "IssuanceChain" table contains the hash and value pairs of the issuance chain.
CREATE TABLE IF NOT EXISTS `IssuanceChain` (
-- Hash of the chain of intermediate certificates and root certificates.
`IdentityHash` VARBINARY(255) NOT NULL,
-- Chain data of intermediate certificates and root certificates.
`ChainValue` LONGBLOB NOT NULL,
PRIMARY KEY (`IdentityHash`)
);

0 comments on commit 9302d5f

Please sign in to comment.