Repository: scionproto/scion
Pull request #3467 (2019-12-06 15:59:53)

TrustStore: Implement sqlite database
Add sqlite database that implements the trust.DB

Additionally, add a test suite that can test multiple implementations
of the trust database.

Fixes https://github.com/scionproto/scion/issues/3112.

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/3467)
<!-- Reviewable:end -->



Patch:
diff --git a/go/lib/infra/modules/trust/v2/db.go b/go/lib/infra/modules/trust/v2/db.go
index 634a22daf..938d821fe 100644
--- a/go/lib/infra/modules/trust/v2/db.go
+++ b/go/lib/infra/modules/trust/v2/db.go
@@ -106,7 +106,7 @@ type ChainRead interface {
 	// ChainExists returns whether the certificate chain is found in the
 	// database and the content matches. ErrContentMismatch is returned if any
 	// of the two certificates exist in the database with differing contents.
-	ChainExists(ctx context.Context, d decoded.TRC) (bool, error)
+	ChainExists(ctx context.Context, d decoded.Chain) (bool, error)
 }
 
 // ChainWrite defines the certificate chain write operations.
diff --git a/go/lib/infra/modules/trust/v2/mock_v2/v2.go b/go/lib/infra/modules/trust/v2/mock_v2/v2.go
index 42f40da0f..6d0e15fe0 100644
--- a/go/lib/infra/modules/trust/v2/mock_v2/v2.go
+++ b/go/lib/infra/modules/trust/v2/mock_v2/v2.go
@@ -125,7 +125,7 @@ func (mr *MockDBMockRecorder) BeginTransaction(arg0, arg1 interface{}) *gomock.C
 }
 
 // ChainExists mocks base method
-func (m *MockDB) ChainExists(arg0 context.Context, arg1 decoded.TRC) (bool, error) {
+func (m *MockDB) ChainExists(arg0 context.Context, arg1 decoded.Chain) (bool, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "ChainExists", arg0, arg1)
 	ret0, _ := ret[0].(bool)
diff --git a/go/lib/infra/modules/trust/v2/trustdbsqlite/BUILD.bazel b/go/lib/infra/modules/trust/v2/trustdbsqlite/BUILD.bazel
new file mode 100644
index 000000000..4250f60a0
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbsqlite/BUILD.bazel
@@ -0,0 +1,33 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "db.go",
+        "schema.go",
+    ],
+    importpath = "github.com/scionproto/scion/go/lib/infra/modules/trust/v2/trustdbsqlite",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//go/lib/addr:go_default_library",
+        "//go/lib/infra/modules/db:go_default_library",
+        "//go/lib/infra/modules/trust/v2:go_default_library",
+        "//go/lib/infra/modules/trust/v2/internal/decoded:go_default_library",
+        "//go/lib/scrypto:go_default_library",
+        "//go/lib/scrypto/cert/v2:go_default_library",
+        "//go/lib/scrypto/trc/v2:go_default_library",
+        "//go/lib/serrors:go_default_library",
+        "//go/lib/util:go_default_library",
+        "@com_github_mattn_go_sqlite3//:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["db_test.go"],
+    embed = [":go_default_library"],
+    deps = [
+        "//go/lib/infra/modules/trust/v2/trustdbtest:go_default_library",
+        "@com_github_stretchr_testify//require:go_default_library",
+    ],
+)
diff --git a/go/lib/infra/modules/trust/v2/trustdbsqlite/db.go b/go/lib/infra/modules/trust/v2/trustdbsqlite/db.go
new file mode 100644
index 000000000..c99bed802
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbsqlite/db.go
@@ -0,0 +1,358 @@
+// Copyright 2019 Anapaya Systems
+//
+// 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 trustdbsqlite
+
+import (
+	"bytes"
+	"context"
+	"crypto/sha256"
+	"database/sql"
+	"sync"
+	"time"
+
+	_ "github.com/mattn/go-sqlite3"
+
+	"github.com/scionproto/scion/go/lib/addr"
+	"github.com/scionproto/scion/go/lib/infra/modules/db"
+	"github.com/scionproto/scion/go/lib/infra/modules/trust/v2"
+	"github.com/scionproto/scion/go/lib/infra/modules/trust/v2/internal/decoded"
+	"github.com/scionproto/scion/go/lib/scrypto"
+	"github.com/scionproto/scion/go/lib/scrypto/cert/v2"
+	"github.com/scionproto/scion/go/lib/scrypto/trc/v2"
+	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/lib/util"
+)
+
+// Backend implements the trust DB with an SQLite backend.
+type Backend struct {
+	db *sql.DB
+	*executor
+}
+
+// New returns a new SQLite backend opening a database at the given path. If
+// no database exists a new database is be created. If the schema version of the
+// stored database is different from the one in schema.go, an error is returned.
+func New(path string) (*Backend, error) {
+	db, err := db.NewSqlite(path, Schema, SchemaVersion)
+	if err != nil {
+		return nil, err
+	}
+	return &Backend{
+		executor: &executor{
+			db: db,
+		},
+		db: db,
+	}, nil
+}
+
+// SetMaxOpenConns sets the maximum number of open connections.
+func (b *Backend) SetMaxOpenConns(maxOpenConns int) {
+	b.db.SetMaxOpenConns(maxOpenConns)
+}
+
+// SetMaxIdleConns sets the maximum number of idle connections.
+func (b *Backend) SetMaxIdleConns(maxIdleConns int) {
+	b.db.SetMaxIdleConns(maxIdleConns)
+}
+
+// BeginTransaction begins a transaction on the database.
+func (b *Backend) BeginTransaction(ctx context.Context,
+	opts *sql.TxOptions) (trust.Transaction, error) {
+
+	b.Lock()
+	defer b.Unlock()
+	tx, err := b.db.BeginTx(ctx, opts)
+	if err != nil {
+		return nil, db.NewTxError("create tx", err)
+	}
+	return &transaction{
+		executor: &executor{
+			db: tx,
+		},
+		tx: tx,
+	}, nil
+}
+
+// Close closes the database.
+func (b *Backend) Close() error {
+	return b.db.Close()
+}
+
+var _ (trust.Transaction) = (*transaction)(nil)
+
+type transaction struct {
+	*executor
+	tx *sql.Tx
+}
+
+func (tx *transaction) Commit() error {
+	tx.Lock()
+	defer tx.Unlock()
+	return tx.tx.Commit()
+}
+
+func (tx *transaction) Rollback() error {
+	tx.Lock()
+	defer tx.Unlock()
+	return tx.tx.Rollback()
+}
+
+type executor struct {
+	sync.RWMutex
+	db db.Sqler
+}
+
+func (e *executor) TRCExists(ctx context.Context, d decoded.TRC) (bool, error) {
+	e.RLock()
+	defer e.RUnlock()
+	return trcExists(ctx, e.db, d)
+}
+
+func (e *executor) GetTRC(ctx context.Context, isd addr.ISD,
+	version scrypto.Version) (*trc.TRC, error) {
+
+	e.RLock()
+	defer e.RUnlock()
+	var pld []byte
+	query := `SELECT pld FROM trcs WHERE isd_id=? AND version=?`
+	if version.IsLatest() {
+		query = `SELECT pld FROM (SELECT pld, max(version) FROM trcs WHERE isd_id=?)
+		         WHERE pld IS NOT NULL`
+	}
+	err := e.db.QueryRowContext(ctx, query, isd, version).Scan(&pld)
+	switch {
+	case err == sql.ErrNoRows:
+		return nil, trust.ErrNotFound
+	case err != nil:
+		return nil, err
+	}
+	return trc.Encoded(pld).Decode()
+}
+
+func (e *executor) GetRawTRC(ctx context.Context, isd addr.ISD,
+	version scrypto.Version) ([]byte, error) {
+
+	e.RLock()
+	defer e.RUnlock()
+	query := `SELECT raw FROM trcs WHERE isd_id=? AND version=?`
+	if version.IsLatest() {
+		query = `SELECT raw FROM (SELECT raw, max(version) FROM trcs WHERE isd_id=?)
+		         WHERE raw IS NOT NULL`
+	}
+	var raw []byte
+	err := e.db.QueryRowContext(ctx, query, isd, version).Scan(&raw)
+	if err == sql.ErrNoRows {
+		return nil, trust.ErrNotFound
+	}
+	return raw, err
+}
+
+func (e *executor) GetTRCInfo(ctx context.Context, isd addr.ISD,
+	version scrypto.Version) (trust.TRCInfo, error) {
+
+	e.RLock()
+	defer e.RUnlock()
+	query := `SELECT version, not_before, not_after, grace_period from trcs
+	          WHERE isd_id=? AND version=?`
+	if version.IsLatest() {
+		query = `SELECT max(version), not_before, not_after, grace_period from trcs WHERE isd_id=?`
+	}
+	var ver scrypto.Version
+	var grace int
+	var notBefore, notAfter uint32
+	err := e.db.QueryRowContext(ctx, query, isd, version).Scan(&ver, &notBefore, &notAfter, &grace)
+	switch {
+	case err == sql.ErrNoRows:
+		return trust.TRCInfo{}, trust.ErrNotFound
+	case err != nil:
+		return trust.TRCInfo{}, trust.ErrNotFound
+	}
+	info := trust.TRCInfo{
+		Version: ver,
+		Validity: scrypto.Validity{
+			NotBefore: util.UnixTime{Time: util.SecsToTime(notBefore)},
+			NotAfter:  util.UnixTime{Time: util.SecsToTime(notAfter)},
+		},
+		GracePeriod: time.Duration(grace) * time.Second,
+	}
+	return info, nil
+}
+
+func (e *executor) InsertTRC(ctx context.Context, d decoded.TRC) (bool, error) {
+	e.Lock()
+	defer e.Unlock()
+
+	h := hash(d.Signed.EncodedTRC)
+	query := `INSERT INTO trcs (isd_id, version, raw, pld, pld_hash, not_before,
+	          not_after, grace_period) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`
+	var inserted bool
+	err := db.DoInTx(ctx, e.db, func(ctx context.Context, tx *sql.Tx) error {
+		exists, err := trcExists(ctx, tx, d)
+		switch {
+		case err != nil:
+			return err
+		case exists:
+			return nil
+		}
+		_, err = tx.ExecContext(ctx, query, d.TRC.ISD, d.TRC.Version, d.Raw,
+			d.Signed.EncodedTRC, h, util.TimeToSecs(d.TRC.Validity.NotBefore.Time),
+			util.TimeToSecs(d.TRC.Validity.NotAfter.Time),
+			int(d.TRC.GracePeriod.Duration/time.Second))
+		if err != nil {
+			return err
+		}
+		inserted = true
+		return nil
+	})
+	if err != nil {
+		return false, err
+	}
+	return inserted, nil
+}
+
+func (e *executor) GetRawChain(ctx context.Context, ia addr.IA,
+	version scrypto.Version) ([]byte, error) {
+
+	e.RLock()
+	defer e.RUnlock()
+	query := `SELECT raw FROM chains WHERE isd_id=? AND as_id=? AND version=?`
+	if version.IsLatest() {
+		query = `SELECT raw FROM (SELECT raw, max(version) FROM chains WHERE isd_id=? AND as_id=?)
+		         WHERE raw IS NOT NULL`
+	}
+	var raw []byte
+	err := e.db.QueryRowContext(ctx, query, ia.I, ia.A, version).Scan(&raw)
+	if err == sql.ErrNoRows {
+		return nil, trust.ErrNotFound
+	}
+	return raw, err
+}
+
+func (e *executor) ChainExists(ctx context.Context, d decoded.Chain) (bool, error) {
+	e.RLock()
+	defer e.RUnlock()
+	return chainExists(ctx, e.db, d)
+}
+
+func (e *executor) InsertChain(ctx context.Context, d decoded.Chain) (bool, bool, error) {
+	e.Lock()
+	defer e.Unlock()
+
+	asHash, issHash := hash(d.Chain.AS.Encoded), hash(d.Chain.Issuer.Encoded)
+	var asInserted, issInserted bool
+	err := db.DoInTx(ctx, e.db, func(ctx context.Context, tx *sql.Tx) error {
+		exists, err := chainExists(ctx, tx, d)
+		switch {
+		case err != nil:
+			return err
+		case exists:
+			return nil
+		}
+		if issInserted, err = insertIssuer(ctx, tx, d.Issuer, d.Chain.Issuer); err != nil {
+			return serrors.WrapStr("unable to insert issuer certificate", err)
+		}
+		query := `INSERT INTO chains (isd_id, as_id, version, raw, as_hash, issuer_hash)
+		          VALUES ($1, $2, $3, $4, $5, $6)`
+		_, err = tx.ExecContext(ctx, query, d.AS.Subject.I, d.AS.Subject.A, d.AS.Version,
+			d.Raw, asHash, issHash)
+		if err != nil {
+			return err
+		}
+		asInserted = true
+		return nil
+	})
+	if err != nil {
+		return false, false, err
+	}
+	return asInserted, issInserted, nil
+}
+
+func insertIssuer(ctx context.Context, db db.Sqler, iss *cert.Issuer,
+	signed cert.SignedIssuer) (bool, error) {
+
+	query := `INSERT INTO issuer_certs (isd_id, as_id, version, pld, pld_hash, protected, signature)
+	          VALUES ($1, $2, $3, $4, $5, $6, $7)`
+	exists, err := issuerExists(ctx, db, iss, signed)
+	switch {
+	case err != nil:
+		return false, err
+	case exists:
+		return false, nil
+	}
+	_, err = db.ExecContext(ctx, query, iss.Subject.I, iss.Subject.A, iss.Version, signed.Encoded,
+		hash(signed.Encoded), signed.EncodedProtected, signed.Signature)
+	if err != nil {
+		return false, err
+	}
+	return true, nil
+}
+
+func trcExists(ctx context.Context, db db.Sqler, d decoded.TRC) (bool, error) {
+	var dbHash []byte
+	query := `SELECT pld_hash FROM trcs WHERE isd_id=? AND version=?`
+	err := db.QueryRowContext(ctx, query, d.TRC.ISD, d.TRC.Version).Scan(&dbHash)
+	switch {
+	case err == sql.ErrNoRows:
+		return false, nil
+	case err != nil:
+		return false, err
+	case !bytes.Equal(hash(d.Signed.EncodedTRC), dbHash):
+		return true, trust.ErrContentMismatch
+	default:
+		return true, nil
+	}
+}
+
+func chainExists(ctx context.Context, db db.Sqler, d decoded.Chain) (bool, error) {
+	var asHash, issHash []byte
+	query := `SELECT as_hash, issuer_hash FROM chains WHERE isd_id=? AND as_id=? AND version=?`
+	err := db.QueryRowContext(ctx, query, d.AS.Subject.I, d.AS.Subject.A, d.AS.Version).Scan(
+		&asHash, &issHash)
+	switch {
+	case err == sql.ErrNoRows:
+		return false, nil
+	case err != nil:
+		return false, err
+	case !bytes.Equal(hash(d.Chain.AS.Encoded), asHash):
+		return false, serrors.WithCtx(trust.ErrContentMismatch, "part", "as")
+	case !bytes.Equal(hash(d.Chain.Issuer.Encoded), issHash):
+		return false, serrors.WithCtx(trust.ErrContentMismatch, "part", "issuer")
+	default:
+		return true, nil
+	}
+}
+
+func issuerExists(ctx context.Context, db db.Sqler, iss *cert.Issuer,
+	signed cert.SignedIssuer) (bool, error) {
+
+	var dbHash []byte
+	query := `SELECT pld_hash FROM issuer_certs Where isd_id=? AND as_id=? AND version=?`
+	err := db.QueryRowContext(ctx, query, iss.Subject.I, iss.Subject.A, iss.Version).Scan(&dbHash)
+	switch {
+	case err == sql.ErrNoRows:
+		return false, nil
+	case err != nil:
+		return false, err
+	case !bytes.Equal(hash(signed.Encoded), dbHash):
+		return false, trust.ErrContentMismatch
+	default:
+		return true, nil
+	}
+}
+
+func hash(input []byte) []byte {
+	h := sha256.Sum256(input)
+	return h[:]
+}
diff --git a/go/lib/infra/modules/trust/v2/trustdbsqlite/schema.go b/go/lib/infra/modules/trust/v2/trustdbsqlite/schema.go
new file mode 100644
index 000000000..870e33336
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbsqlite/schema.go
@@ -0,0 +1,57 @@
+// Copyright 2019 Anapaya Systems
+//
+// 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.
+
+// This file contains an SQLite backend for the trust database.
+
+package trustdbsqlite
+
+const (
+	// SchemaVersion is the version of the SQLite schema understood by this backend.
+	// Whenever changes to the schema are made, this version number should be increased
+	// to prevent data corruption between incompatible database schemas.
+	SchemaVersion = 1
+	// Schema is the SQLite database layout.
+	Schema = `
+	CREATE TABLE trcs(
+		isd_id INTEGER NOT NULL,
+		version INTEGER NOT NULL,
+		raw DATA NOT NULL,
+		pld DATA NOT NULL,
+		pld_hash DATA NOT NULL,
+		not_before INTEGER NOT NULL,
+		not_after INTEGER NOT NULL,
+		grace_period INTEGER NOT NULL,
+		PRIMARY KEY (isd_id, version)
+	);
+	CREATE TABLE chains(
+		isd_id INTEGER NOT NULL,
+		as_id INTEGER NOT NULL,
+		version INTEGER NOT NULL,
+		raw DATA NOT NULL,
+		as_hash DATA NOT NULL,
+		issuer_hash DATA NOT NULL,
+		PRIMARY KEY (isd_id, as_id, version)
+	);
+	CREATE TABLE issuer_certs(
+		isd_id INTEGER NOT NULL,
+		as_id INTEGER NOT NULL,
+		version INTEGER NOT NULL,
+		pld DATA NOT NULL,
+		pld_hash DATA NOT NULL,
+		protected DATA NOT NULL,
+		signature DATA NOT NULL,
+		PRIMARY KEY (isd_id, as_id, version)
+	);
+	`
+)


Test patch:
diff --git a/go/lib/infra/modules/trust/v2/trustdbsqlite/db_test.go b/go/lib/infra/modules/trust/v2/trustdbsqlite/db_test.go
new file mode 100644
index 000000000..1189be2b5
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbsqlite/db_test.go
@@ -0,0 +1,44 @@
+// Copyright 2019 Anapaya Systems
+//
+// 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 trustdbsqlite
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/scionproto/scion/go/lib/infra/modules/trust/v2/trustdbtest"
+)
+
+var _ trustdbtest.TestableDB = (*TestDB)(nil)
+
+type TestDB struct {
+	*Backend
+}
+
+func (b *TestDB) Prepare(t *testing.T, _ context.Context) {
+	b.Backend = newDatabase(t)
+}
+
+func TestTrustDBSuite(t *testing.T) {
+	trustdbtest.TestDB(t, &TestDB{}, trustdbtest.Config{})
+}
+
+func newDatabase(t *testing.T) *Backend {
+	db, err := New(":memory:")
+	require.NoError(t, err)
+	return db
+}
diff --git a/go/lib/infra/modules/trust/v2/trustdbtest/BUILD.bazel b/go/lib/infra/modules/trust/v2/trustdbtest/BUILD.bazel
new file mode 100644
index 000000000..656ab04ff
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbtest/BUILD.bazel
@@ -0,0 +1,17 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["trustdbtest.go"],
+    data = glob(["testdata/*"]),
+    importpath = "github.com/scionproto/scion/go/lib/infra/modules/trust/v2/trustdbtest",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//go/lib/infra/modules/trust/v2:go_default_library",
+        "//go/lib/infra/modules/trust/v2/internal/decoded:go_default_library",
+        "//go/lib/scrypto:go_default_library",
+        "//go/lib/xtest:go_default_library",
+        "@com_github_stretchr_testify//assert:go_default_library",
+        "@com_github_stretchr_testify//require:go_default_library",
+    ],
+)
diff --git a/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-ASff00_0_110-V1.crt b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-ASff00_0_110-V1.crt
new file mode 100644
index 000000000..3bb44fb05
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-ASff00_0_110-V1.crt
@@ -0,0 +1,1 @@
+[{"payload":"ZXlKemRXSnFaV04wSWpvaU1TMW1aakF3T2pBNk1URXdJaXdpZG1WeWMybHZiaUk2TVN3aVptOXliV0YwWDNabGNuTnBiMjRpT2pFc0ltUmxjMk55YVhCMGFXOXVJam9pU1hOemRXVnlJR05sY25ScFptbGpZWFJsSURFdFptWXdNRG93T2pFeE1DSXNJbTl3ZEdsdmJtRnNYMlJwYzNSeWFXSjFkR2x2Ymw5d2IybHVkSE1pT2x0ZExDSjJZV3hwWkdsMGVTSTZleUp1YjNSZlltVm1iM0psSWpveE5UYzFOVE0yTnpRMkxDSnViM1JmWVdaMFpYSWlPakUyTURjd056STNORFo5TENKclpYbHpJanA3SW1semMzVnBibWNpT25zaWEyVjVYM1psY25OcGIyNGlPakVzSW1Gc1oyOXlhWFJvYlNJNkltVmtNalUxTVRraUxDSnJaWGtpT2lJekswbFNhR3AwVkVkTE5rbzJUbkJIYzNGNk0yOWFieXMwTWtsM04wWTNUM2RtSzJ4alV6VndUa2huUFNKOWZTd2lhWE56ZFdWeUlqcDdJblJ5WTE5MlpYSnphVzl1SWpveGZTd2lZMlZ5ZEdsbWFXTmhkR1ZmZEhsd1pTSTZJbWx6YzNWbGNpSjk=","protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJblJ5WXlJc0luUnlZMTkyWlhKemFXOXVJam94TENKamNtbDBJanBiSW5SNWNHVWlMQ0owY21OZmRtVnljMmx2YmlKZGZR","signature":"zmjtrge2J3PHtPOkf7sLQEkM5O+leN0gbi8rvJRYp9wIFUuh2Y4Ku5LLzGe/awrO9A51Kuza+vK+DDFH3Zv7BA=="},{"payload":"ZXlKemRXSnFaV04wSWpvaU1TMW1aakF3T2pBNk1URXdJaXdpZG1WeWMybHZiaUk2TVN3aVptOXliV0YwWDNabGNuTnBiMjRpT2pFc0ltUmxjMk55YVhCMGFXOXVJam9pU1hOemRXVnlJR05sY25ScFptbGpZWFJsSURFdFptWXdNRG93T2pFeE1DSXNJbTl3ZEdsdmJtRnNYMlJwYzNSeWFXSjFkR2x2Ymw5d2IybHVkSE1pT2x0ZExDSjJZV3hwWkdsMGVTSTZleUp1YjNSZlltVm1iM0psSWpveE5UYzFOVE0yTnpRMkxDSnViM1JmWVdaMFpYSWlPakUyTURjd056STNORFo5TENKclpYbHpJanA3SW1WdVkzSjVjSFJwYjI0aU9uc2lhMlY1WDNabGNuTnBiMjRpT2pFc0ltRnNaMjl5YVhSb2JTSTZJbU4xY25abE1qVTFNVGw0YzJGc2MyRXlNSEJ2YkhreE16QTFJaXdpYTJWNUlqb2lOV2hDYVdoNlRIRTFVRU5ZWmtGRFlUQlZNV1ZTUVdkeFpVTjZhMVUyWW13MU1UQjVXSFl5WkZSc2N6MGlmU3dpY21WMmIyTmhkR2x2YmlJNmV5SnJaWGxmZG1WeWMybHZiaUk2TVN3aVlXeG5iM0pwZEdodElqb2laV1F5TlRVeE9TSXNJbXRsZVNJNklrOVJVVEZFU0ZwcWJsQjRSR3gxU1ROck1tOU9PVVEwYVNzMVFtZGxibTVoZFdwQlRWcFRXbmhyY0c4OUluMHNJbk5wWjI1cGJtY2lPbnNpYTJWNVgzWmxjbk5wYjI0aU9qRXNJbUZzWjI5eWFYUm9iU0k2SW1Wa01qVTFNVGtpTENKclpYa2lPaUpVVkdoYWQxaG9ibkF2Y21keGEzWmtaVzU2YjJOR1RIZENVVTF0UkZGaWNFa3ZVMnhzVFV0eU5FSjNQU0o5ZlN3aWFYTnpkV1Z5SWpwN0ltbGhJam9pTVMxbVpqQXdPakE2TVRFd0lpd2lZMlZ5ZEdsbWFXTmhkR1ZmZG1WeWMybHZiaUk2TVgwc0ltTmxjblJwWm1sallYUmxYM1I1Y0dVaU9pSmhjeUo5","protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpWTNKcGRDSTZXeUowZVhCbElpd2lZMlZ5ZEdsbWFXTmhkR1ZmZG1WeWMybHZiaUlzSW1saElsMHNJblI1Y0dVaU9pSmpaWEowYVdacFkyRjBaU0lzSW1ObGNuUnBabWxqWVhSbFgzWmxjbk5wYjI0aU9qRXNJbWxoSWpvaU1TMW1aakF3T2pBNk1URXdJbjA=","signature":"8ho+ewCeCReyfiJYJapORS87ajMUB3Jd/fXrYiR0XFdqUycA/gqE4Npd+cQwfup4jWTony4NWf14XRc0qm/PAg=="}]
diff --git a/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-ASff00_0_110-V10.crt b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-ASff00_0_110-V10.crt
new file mode 100644
index 000000000..c72bad56b
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-ASff00_0_110-V10.crt
@@ -0,0 +1,1 @@
+[{"payload":"ZXlKemRXSnFaV04wSWpvaU1TMW1aakF3T2pBNk1URXdJaXdpZG1WeWMybHZiaUk2TVN3aVptOXliV0YwWDNabGNuTnBiMjRpT2pFc0ltUmxjMk55YVhCMGFXOXVJam9pU1hOemRXVnlJR05sY25ScFptbGpZWFJsSURFdFptWXdNRG93T2pFeE1DSXNJbTl3ZEdsdmJtRnNYMlJwYzNSeWFXSjFkR2x2Ymw5d2IybHVkSE1pT2x0ZExDSjJZV3hwWkdsMGVTSTZleUp1YjNSZlltVm1iM0psSWpveE5UYzFOVE0yTnpRMkxDSnViM1JmWVdaMFpYSWlPakUyTURjd056STNORFo5TENKclpYbHpJanA3SW1semMzVnBibWNpT25zaWEyVjVYM1psY25OcGIyNGlPakVzSW1Gc1oyOXlhWFJvYlNJNkltVmtNalUxTVRraUxDSnJaWGtpT2lJekswbFNhR3AwVkVkTE5rbzJUbkJIYzNGNk0yOWFieXMwTWtsM04wWTNUM2RtSzJ4alV6VndUa2huUFNKOWZTd2lhWE56ZFdWeUlqcDdJblJ5WTE5MlpYSnphVzl1SWpveGZTd2lZMlZ5ZEdsbWFXTmhkR1ZmZEhsd1pTSTZJbWx6YzNWbGNpSjk=","protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJblJ5WXlJc0luUnlZMTkyWlhKemFXOXVJam94TENKamNtbDBJanBiSW5SNWNHVWlMQ0owY21OZmRtVnljMmx2YmlKZGZR","signature":"zmjtrge2J3PHtPOkf7sLQEkM5O+leN0gbi8rvJRYp9wIFUuh2Y4Ku5LLzGe/awrO9A51Kuza+vK+DDFH3Zv7BA=="},{"payload":"ZXlKemRXSnFaV04wSWpvaU1TMW1aakF3T2pBNk1URXdJaXdpZG1WeWMybHZiaUk2TVRBc0ltWnZjbTFoZEY5MlpYSnphVzl1SWpveExDSmtaWE5qY21sd2RHbHZiaUk2SWtsemMzVmxjaUJqWlhKMGFXWnBZMkYwWlNBeExXWm1NREE2TURveE1UQWlMQ0p2Y0hScGIyNWhiRjlrYVhOMGNtbGlkWFJwYjI1ZmNHOXBiblJ6SWpwYlhTd2lkbUZzYVdScGRIa2lPbnNpYm05MFgySmxabTl5WlNJNk1UVTNOVFV6TmpjME5pd2libTkwWDJGbWRHVnlJam94TmpBM01EY3lOelEyZlN3aWEyVjVjeUk2ZXlKbGJtTnllWEIwYVc5dUlqcDdJbXRsZVY5MlpYSnphVzl1SWpveExDSmhiR2R2Y21sMGFHMGlPaUpqZFhKMlpUSTFOVEU1ZUhOaGJITmhNakJ3YjJ4NU1UTXdOU0lzSW10bGVTSTZJalZvUW1sb2VreHhOVkJEV0daQlEyRXdWVEZsVWtGbmNXVkRlbXRWTm1Kc05URXdlVmgyTW1SVWJITTlJbjBzSW5KbGRtOWpZWFJwYjI0aU9uc2lhMlY1WDNabGNuTnBiMjRpT2pFc0ltRnNaMjl5YVhSb2JTSTZJbVZrTWpVMU1Ua2lMQ0pyWlhraU9pSlBVVkV4UkVoYWFtNVFlRVJzZFVremF6SnZUamxFTkdrck5VSm5aVzV1WVhWcVFVMWFVMXA0YTNCdlBTSjlMQ0p6YVdkdWFXNW5JanA3SW10bGVWOTJaWEp6YVc5dUlqb3hMQ0poYkdkdmNtbDBhRzBpT2lKbFpESTFOVEU1SWl3aWEyVjVJam9pVkZSb1duZFlhRzV3TDNKbmNXdDJaR1Z1ZW05alJreDNRbEZOYlVSUlluQkpMMU5zYkUxTGNqUkNkejBpZlgwc0ltbHpjM1ZsY2lJNmV5SnBZU0k2SWpFdFptWXdNRG93T2pFeE1DSXNJbU5sY25ScFptbGpZWFJsWDNabGNuTnBiMjRpT2pGOUxDSmpaWEowYVdacFkyRjBaVjkwZVhCbElqb2lZWE1pZlE=","protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpWTNKcGRDSTZXeUowZVhCbElpd2lZMlZ5ZEdsbWFXTmhkR1ZmZG1WeWMybHZiaUlzSW1saElsMHNJblI1Y0dVaU9pSmpaWEowYVdacFkyRjBaU0lzSW1ObGNuUnBabWxqWVhSbFgzWmxjbk5wYjI0aU9qRXNJbWxoSWpvaU1TMW1aakF3T2pBNk1URXdJbjA=","signature":"vP5dsgWOLA8H1ZpNXtCyyDfkiHrNUsBlXjpZ4e83buFo9VR4rW5dUwLFz9BNOeCSNI9FG9walOgqqGK99ci4Cw=="}]
diff --git a/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-ASff00_0_111-V1.crt b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-ASff00_0_111-V1.crt
new file mode 100644
index 000000000..c83fe020b
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-ASff00_0_111-V1.crt
@@ -0,0 +1,1 @@
+[{"payload":"ZXlKemRXSnFaV04wSWpvaU1TMW1aakF3T2pBNk1URXdJaXdpZG1WeWMybHZiaUk2TVN3aVptOXliV0YwWDNabGNuTnBiMjRpT2pFc0ltUmxjMk55YVhCMGFXOXVJam9pU1hOemRXVnlJR05sY25ScFptbGpZWFJsSURFdFptWXdNRG93T2pFeE1DSXNJbTl3ZEdsdmJtRnNYMlJwYzNSeWFXSjFkR2x2Ymw5d2IybHVkSE1pT2x0ZExDSjJZV3hwWkdsMGVTSTZleUp1YjNSZlltVm1iM0psSWpveE5UYzFOVE0yTnpRMkxDSnViM1JmWVdaMFpYSWlPakUyTURjd056STNORFo5TENKclpYbHpJanA3SW1semMzVnBibWNpT25zaWEyVjVYM1psY25OcGIyNGlPakVzSW1Gc1oyOXlhWFJvYlNJNkltVmtNalUxTVRraUxDSnJaWGtpT2lJekswbFNhR3AwVkVkTE5rbzJUbkJIYzNGNk0yOWFieXMwTWtsM04wWTNUM2RtSzJ4alV6VndUa2huUFNKOWZTd2lhWE56ZFdWeUlqcDdJblJ5WTE5MlpYSnphVzl1SWpveGZTd2lZMlZ5ZEdsbWFXTmhkR1ZmZEhsd1pTSTZJbWx6YzNWbGNpSjk=","protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJblJ5WXlJc0luUnlZMTkyWlhKemFXOXVJam94TENKamNtbDBJanBiSW5SNWNHVWlMQ0owY21OZmRtVnljMmx2YmlKZGZR","signature":"zmjtrge2J3PHtPOkf7sLQEkM5O+leN0gbi8rvJRYp9wIFUuh2Y4Ku5LLzGe/awrO9A51Kuza+vK+DDFH3Zv7BA=="},{"payload":"ZXlKemRXSnFaV04wSWpvaU1TMW1aakF3T2pBNk1URXhJaXdpZG1WeWMybHZiaUk2TVN3aVptOXliV0YwWDNabGNuTnBiMjRpT2pFc0ltUmxjMk55YVhCMGFXOXVJam9pU1hOemRXVnlJR05sY25ScFptbGpZWFJsSURFdFptWXdNRG93T2pFeE1TSXNJbTl3ZEdsdmJtRnNYMlJwYzNSeWFXSjFkR2x2Ymw5d2IybHVkSE1pT2x0ZExDSjJZV3hwWkdsMGVTSTZleUp1YjNSZlltVm1iM0psSWpveE5UYzFOVE0yTnpRMkxDSnViM1JmWVdaMFpYSWlPakUyTURjd056STNORFo5TENKclpYbHpJanA3SW1WdVkzSjVjSFJwYjI0aU9uc2lhMlY1WDNabGNuTnBiMjRpT2pFc0ltRnNaMjl5YVhSb2JTSTZJbU4xY25abE1qVTFNVGw0YzJGc2MyRXlNSEJ2YkhreE16QTFJaXdpYTJWNUlqb2lObVpxT1RONU5FOVdVR2xwZW5CaVEwYzFlRmhsYUVodVNrcElka0ZFTmtWb1p6SklhME12V0c1SFJUMGlmU3dpY21WMmIyTmhkR2x2YmlJNmV5SnJaWGxmZG1WeWMybHZiaUk2TVN3aVlXeG5iM0pwZEdodElqb2laV1F5TlRVeE9TSXNJbXRsZVNJNklubEdaMmxuVkdwNk1HSTJVamhHYTNaV00wVlBkeTh2WXprMVZ6VkthblptUkdReE5WUmtOeTltWkVVOUluMHNJbk5wWjI1cGJtY2lPbnNpYTJWNVgzWmxjbk5wYjI0aU9qRXNJbUZzWjI5eWFYUm9iU0k2SW1Wa01qVTFNVGtpTENKclpYa2lPaUpuVFhGMU9IZFlhVm81TVRSdFFsRjVaVWxJWnpZM1ZHODBhbkJyVUc1elNGRmhWemxEU2t4d2RWSlJQU0o5ZlN3aWFYTnpkV1Z5SWpwN0ltbGhJam9pTVMxbVpqQXdPakE2TVRFd0lpd2lZMlZ5ZEdsbWFXTmhkR1ZmZG1WeWMybHZiaUk2TVgwc0ltTmxjblJwWm1sallYUmxYM1I1Y0dVaU9pSmhjeUo5","protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpWTNKcGRDSTZXeUowZVhCbElpd2lZMlZ5ZEdsbWFXTmhkR1ZmZG1WeWMybHZiaUlzSW1saElsMHNJblI1Y0dVaU9pSmpaWEowYVdacFkyRjBaU0lzSW1ObGNuUnBabWxqWVhSbFgzWmxjbk5wYjI0aU9qRXNJbWxoSWpvaU1TMW1aakF3T2pBNk1URXdJbjA=","signature":"1XDgwu17C7z6TMK3yIqyhVKYBgks0o0I1Kkn5hK29os86xLHAkVlW11VxtTwhdISpp2ieHy8Dp23TaQrLGONDA=="}]
diff --git a/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-V1.trc b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-V1.trc
new file mode 100644
index 000000000..d27889dc3
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-V1.trc
@@ -0,0 +1,1 @@
+{"payload":"ZXlKcGMyUWlPakVzSW5SeVkxOTJaWEp6YVc5dUlqb3hMQ0ppWVhObFgzWmxjbk5wYjI0aU9qRXNJbVJsYzJOeWFYQjBhVzl1SWpvaVNWTkVJREVpTENKMmIzUnBibWRmY1hWdmNuVnRJam95TENKbWIzSnRZWFJmZG1WeWMybHZiaUk2TVN3aVozSmhZMlZmY0dWeWFXOWtJam93TENKMGNuVnpkRjl5WlhObGRGOWhiR3h2ZDJWa0lqcDBjblZsTENKMllXeHBaR2wwZVNJNmV5SnViM1JmWW1WbWIzSmxJam94TlRjMU5UTTJOelEyTENKdWIzUmZZV1owWlhJaU9qRTJNRGN3TnpJM05EWjlMQ0p3Y21sdFlYSjVYMkZ6WlhNaU9uc2labVl3TURvd09qRXhNQ0k2ZXlKaGRIUnlhV0oxZEdWeklqcGJJbUYxZEdodmNtbDBZWFJwZG1VaUxDSmpiM0psSWl3aWFYTnpkV2x1WnlJc0luWnZkR2x1WnlKZExDSnJaWGx6SWpwN0ltbHpjM1ZwYm1jaU9uc2lhMlY1WDNabGNuTnBiMjRpT2pFc0ltRnNaMjl5YVhSb2JTSTZJbVZrTWpVMU1Ua2lMQ0pyWlhraU9pSktabkpUY0RGYVUwVnZaWHBuWW04d05YQkxVRzR3Um5WdlExQmFVMHhGUVhoNVkySjJiU3RNTkdwclBTSjlMQ0p2Wm1ac2FXNWxJanA3SW10bGVWOTJaWEp6YVc5dUlqb3hMQ0poYkdkdmNtbDBhRzBpT2lKbFpESTFOVEU1SWl3aWEyVjVJam9pVnpacVMyMW1PVEI1V1doT2JERlRia3hRZEZJek9XWkpVMEZQWTBGNFYzUnBNa1kyUVU1SVZVNDRORDBpZlN3aWIyNXNhVzVsSWpwN0ltdGxlVjkyWlhKemFXOXVJam94TENKaGJHZHZjbWwwYUcwaU9pSmxaREkxTlRFNUlpd2lhMlY1SWpvaU9EVm9jMFZvVkRabWFsVnRSV1F5Y0RaclJGQTJhM0V3VjBsd1QwYzNZVU5hZEVZNE5IZDFjakZCWXowaWZYMTlMQ0ptWmpBd09qQTZNVEl3SWpwN0ltRjBkSEpwWW5WMFpYTWlPbHNpWVhWMGFHOXlhWFJoZEdsMlpTSXNJbU52Y21VaUxDSnBjM04xYVc1bklpd2lkbTkwYVc1bklsMHNJbXRsZVhNaU9uc2lhWE56ZFdsdVp5STZleUpyWlhsZmRtVnljMmx2YmlJNk1Td2lZV3huYjNKcGRHaHRJam9pWldReU5UVXhPU0lzSW10bGVTSTZJblZxUXpkbFpHc3ZZM1JRZVZKQlltbDZLM1ZtV0U5d1JrUTRiakZSUlhwUVNsRjZZV2hQVmpOVlJFazlJbjBzSW05bVpteHBibVVpT25zaWEyVjVYM1psY25OcGIyNGlPakVzSW1Gc1oyOXlhWFJvYlNJNkltVmtNalUxTVRraUxDSnJaWGtpT2lKdk1EVm9MMGhXTW5vek5GbDBSMWszVW5WSk4wZFlNMjFOV1ZOc1JEQktibEJrUm5KeWVrSnJWRkpCUFNKOUxDSnZibXhwYm1VaU9uc2lhMlY1WDNabGNuTnBiMjRpT2pFc0ltRnNaMjl5YVhSb2JTSTZJbVZrTWpVMU1Ua2lMQ0pyWlhraU9pSk9kMkZ5WWpWMFIyZzRXVlp6Vm1NcmNrdE5WRkpsVWtSUVdEZExUVkpJWWtOWFZHZHRkVWd2UWtKSlBTSjlmWDBzSW1abU1EQTZNRG94TXpBaU9uc2lZWFIwY21saWRYUmxjeUk2V3lKaGRYUm9iM0pwZEdGMGFYWmxJaXdpWTI5eVpTSXNJbWx6YzNWcGJtY2lMQ0oyYjNScGJtY2lYU3dpYTJWNWN5STZleUpwYzNOMWFXNW5JanA3SW10bGVWOTJaWEp6YVc5dUlqb3hMQ0poYkdkdmNtbDBhRzBpT2lKbFpESTFOVEU1SWl3aWEyVjVJam9pTkZGWlRqaGFSakZ3WldGV1pYcG5PSEZEV1dWQ2RWVjVOR295VkRjdlYyeG9UMGhhVW1kd1l6SnNVVDBpZlN3aWIyWm1iR2x1WlNJNmV5SnJaWGxmZG1WeWMybHZiaUk2TVN3aVlXeG5iM0pwZEdodElqb2laV1F5TlRVeE9TSXNJbXRsZVNJNkltMW9kRlpCVFZwWWRUQktWa2xIZVd4a2FrbEhiVzFWU0VWNmRDODVZMWRWWlVOVloyVXhka1V5V21zOUluMHNJbTl1YkdsdVpTSTZleUpyWlhsZmRtVnljMmx2YmlJNk1Td2lZV3huYjNKcGRHaHRJam9pWldReU5UVXhPU0lzSW10bGVTSTZJa1pLTTJaSWFVczBOV0ZWVlVOdVFscHJibkI1TldZMk9HOTNSbE5uV1V0RGJIaE1aMmc0TDNsQ1ZGRTlJbjE5Zlgwc0luWnZkR1Z6SWpwN2ZTd2ljSEp2YjJaZmIyWmZjRzl6YzJWemMybHZiaUk2ZXlKbVpqQXdPakE2TVRFd0lqcGJJbWx6YzNWcGJtY2lMQ0p2Ym14cGJtVWlMQ0p2Wm1ac2FXNWxJbDBzSW1abU1EQTZNRG94TWpBaU9sc2lhWE56ZFdsdVp5SXNJbTl1YkdsdVpTSXNJbTltWm14cGJtVWlYU3dpWm1Zd01Eb3dPakV6TUNJNld5SnBjM04xYVc1bklpd2liMjVzYVc1bElpd2liMlptYkdsdVpTSmRmWDA=","signatures":[{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltbHpjM1ZwYm1jaUxDSnJaWGxmZG1WeWMybHZiaUk2TVN3aVlYTWlPaUptWmpBd09qQTZNVEV3SWl3aVkzSnBkQ0k2V3lKMGVYQmxJaXdpYTJWNVgzUjVjR1VpTENKclpYbGZkbVZ5YzJsdmJpSXNJbUZ6SWwxOQ==","signature":"5iASl4Te3Mz7wj5P3KecRwT64tbKvciw0c2oVx2glJaU0fCfsGdzPSZu+Z+C3qwpRyqxYiwTwW86BAdIfdakBg=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltOXViR2x1WlNJc0ltdGxlVjkyWlhKemFXOXVJam94TENKaGN5STZJbVptTURBNk1Eb3hNVEFpTENKamNtbDBJanBiSW5SNWNHVWlMQ0pyWlhsZmRIbHdaU0lzSW10bGVWOTJaWEp6YVc5dUlpd2lZWE1pWFgw","signature":"bOQ3Icqopkf2z85i4ER0SmuAwJ+Ft0j78AUnZ04gtgYo8b9WXXyL8/NELCraPJUadxPtIReRp0rHBpyaDvAPCQ=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltOW1abXhwYm1VaUxDSnJaWGxmZG1WeWMybHZiaUk2TVN3aVlYTWlPaUptWmpBd09qQTZNVEV3SWl3aVkzSnBkQ0k2V3lKMGVYQmxJaXdpYTJWNVgzUjVjR1VpTENKclpYbGZkbVZ5YzJsdmJpSXNJbUZ6SWwxOQ==","signature":"ZNBfMUbp4AS5BtfDretYp4FXqNSO3AeRJ/hx4ATmN7JUJSADq4ELB8ZHkT9vGVNj2zAwPCZ4peGP5RtYBq/QBA=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltbHpjM1ZwYm1jaUxDSnJaWGxmZG1WeWMybHZiaUk2TVN3aVlYTWlPaUptWmpBd09qQTZNVEl3SWl3aVkzSnBkQ0k2V3lKMGVYQmxJaXdpYTJWNVgzUjVjR1VpTENKclpYbGZkbVZ5YzJsdmJpSXNJbUZ6SWwxOQ==","signature":"LDabLwPra3ixKDOoSiXN82CbWWld1uPV0h0SFJ+h4GgRw2MyH8robfzSOLg7Vhjc5PPC6jy5K5c7ivV5jiI4Cw=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltOXViR2x1WlNJc0ltdGxlVjkyWlhKemFXOXVJam94TENKaGN5STZJbVptTURBNk1Eb3hNakFpTENKamNtbDBJanBiSW5SNWNHVWlMQ0pyWlhsZmRIbHdaU0lzSW10bGVWOTJaWEp6YVc5dUlpd2lZWE1pWFgw","signature":"/H9BihWKl9B3opBSm+Fo8J3CvDNVmBvc2z9VwgJKUcsAUXl4Ie6KgzBmw0YJPhlyogp5Xlr1GQDBfwNskFp9Ag=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltOW1abXhwYm1VaUxDSnJaWGxmZG1WeWMybHZiaUk2TVN3aVlYTWlPaUptWmpBd09qQTZNVEl3SWl3aVkzSnBkQ0k2V3lKMGVYQmxJaXdpYTJWNVgzUjVjR1VpTENKclpYbGZkbVZ5YzJsdmJpSXNJbUZ6SWwxOQ==","signature":"V2Q7hCgzfA7N5cI6jjGrzuXPdgPo54XIoU592SuLQUNI6CjcDCUUb0a38LP7M461RJZOSQ1R93qSXzDsiOiGAg=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltbHpjM1ZwYm1jaUxDSnJaWGxmZG1WeWMybHZiaUk2TVN3aVlYTWlPaUptWmpBd09qQTZNVE13SWl3aVkzSnBkQ0k2V3lKMGVYQmxJaXdpYTJWNVgzUjVjR1VpTENKclpYbGZkbVZ5YzJsdmJpSXNJbUZ6SWwxOQ==","signature":"p6DBICCylnejN5WLupnJhdjBVx2mJs4oQewSJ3zjf40nDtEcbh1KNIutLmnSzH0C5r/+DtOqrKnSi+oT5D0NDQ=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltOXViR2x1WlNJc0ltdGxlVjkyWlhKemFXOXVJam94TENKaGN5STZJbVptTURBNk1Eb3hNekFpTENKamNtbDBJanBiSW5SNWNHVWlMQ0pyWlhsZmRIbHdaU0lzSW10bGVWOTJaWEp6YVc5dUlpd2lZWE1pWFgw","signature":"pp96jvCcBR0AXx673lP8QHEGSDBRdVrqoOECluBfD32ArQeHi0wGJNorOvoSQWycu4z35NphhLlzUy+KV7tsCQ=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltOW1abXhwYm1VaUxDSnJaWGxmZG1WeWMybHZiaUk2TVN3aVlYTWlPaUptWmpBd09qQTZNVE13SWl3aVkzSnBkQ0k2V3lKMGVYQmxJaXdpYTJWNVgzUjVjR1VpTENKclpYbGZkbVZ5YzJsdmJpSXNJbUZ6SWwxOQ==","signature":"1sk37ECQFSQeNlAgs7sG13jIcduKxflDkf13TkNiXh65x4KYAwSPghr2A6VZIFjo4XPorK8ScBFK/e8eTnpDDQ=="}]}
diff --git a/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-V7.trc b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-V7.trc
new file mode 100644
index 000000000..8627c5832
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD1-V7.trc
@@ -0,0 +1,1 @@
+{"payload":"ZXlKcGMyUWlPakVzSW5SeVkxOTJaWEp6YVc5dUlqbzNMQ0ppWVhObFgzWmxjbk5wYjI0aU9qRXNJbVJsYzJOeWFYQjBhVzl1SWpvaVNWTkVJREVpTENKMmIzUnBibWRmY1hWdmNuVnRJam95TENKbWIzSnRZWFJmZG1WeWMybHZiaUk2TVN3aVozSmhZMlZmY0dWeWFXOWtJam96TmpBd0xDSjBjblZ6ZEY5eVpYTmxkRjloYkd4dmQyVmtJanAwY25WbExDSjJZV3hwWkdsMGVTSTZleUp1YjNSZlltVm1iM0psSWpveE5UYzFOVE0yTnpRMkxDSnViM1JmWVdaMFpYSWlPakUyTURjd056STNORFo5TENKd2NtbHRZWEo1WDJGelpYTWlPbnNpWm1Zd01Eb3dPakV4TUNJNmV5SmhkSFJ5YVdKMWRHVnpJanBiSW1GMWRHaHZjbWwwWVhScGRtVWlMQ0pqYjNKbElpd2lhWE56ZFdsdVp5SXNJblp2ZEdsdVp5SmRMQ0pyWlhseklqcDdJbWx6YzNWcGJtY2lPbnNpYTJWNVgzWmxjbk5wYjI0aU9qRXNJbUZzWjI5eWFYUm9iU0k2SW1Wa01qVTFNVGtpTENKclpYa2lPaUpLWm5KVGNERmFVMFZ2WlhwblltOHdOWEJMVUc0d1JuVnZRMUJhVTB4RlFYaDVZMkoyYlN0TU5HcHJQU0o5TENKdlptWnNhVzVsSWpwN0ltdGxlVjkyWlhKemFXOXVJam94TENKaGJHZHZjbWwwYUcwaU9pSmxaREkxTlRFNUlpd2lhMlY1SWpvaVZ6WnFTMjFtT1RCNVdXaE9iREZUYmt4UWRGSXpPV1pKVTBGUFkwRjRWM1JwTWtZMlFVNUlWVTQ0TkQwaWZTd2liMjVzYVc1bElqcDdJbXRsZVY5MlpYSnphVzl1SWpveExDSmhiR2R2Y21sMGFHMGlPaUpsWkRJMU5URTVJaXdpYTJWNUlqb2lPRFZvYzBWb1ZEWm1hbFZ0UldReWNEWnJSRkEyYTNFd1YwbHdUMGMzWVVOYWRFWTROSGQxY2pGQll6MGlmWDE5TENKbVpqQXdPakE2TVRJd0lqcDdJbUYwZEhKcFluVjBaWE1pT2xzaVlYVjBhRzl5YVhSaGRHbDJaU0lzSW1OdmNtVWlMQ0pwYzNOMWFXNW5JaXdpZG05MGFXNW5JbDBzSW10bGVYTWlPbnNpYVhOemRXbHVaeUk2ZXlKclpYbGZkbVZ5YzJsdmJpSTZNU3dpWVd4bmIzSnBkR2h0SWpvaVpXUXlOVFV4T1NJc0ltdGxlU0k2SW5WcVF6ZGxaR3N2WTNSUWVWSkJZbWw2SzNWbVdFOXdSa1E0YmpGUlJYcFFTbEY2WVdoUFZqTlZSRWs5SW4wc0ltOW1abXhwYm1VaU9uc2lhMlY1WDNabGNuTnBiMjRpT2pFc0ltRnNaMjl5YVhSb2JTSTZJbVZrTWpVMU1Ua2lMQ0pyWlhraU9pSnZNRFZvTDBoV01ub3pORmwwUjFrM1VuVkpOMGRZTTIxTldWTnNSREJLYmxCa1JuSnlla0pyVkZKQlBTSjlMQ0p2Ym14cGJtVWlPbnNpYTJWNVgzWmxjbk5wYjI0aU9qRXNJbUZzWjI5eWFYUm9iU0k2SW1Wa01qVTFNVGtpTENKclpYa2lPaUpPZDJGeVlqVjBSMmc0V1ZaelZtTXJja3ROVkZKbFVrUlFXRGRMVFZKSVlrTlhWR2R0ZFVndlFrSkpQU0o5Zlgwc0ltWm1NREE2TURveE16QWlPbnNpWVhSMGNtbGlkWFJsY3lJNld5SmhkWFJvYjNKcGRHRjBhWFpsSWl3aVkyOXlaU0lzSW1semMzVnBibWNpTENKMmIzUnBibWNpWFN3aWEyVjVjeUk2ZXlKcGMzTjFhVzVuSWpwN0ltdGxlVjkyWlhKemFXOXVJam94TENKaGJHZHZjbWwwYUcwaU9pSmxaREkxTlRFNUlpd2lhMlY1SWpvaU5GRlpUamhhUmpGd1pXRldaWHBuT0hGRFdXVkNkVlY1TkdveVZEY3ZWMnhvVDBoYVVtZHdZekpzVVQwaWZTd2liMlptYkdsdVpTSTZleUpyWlhsZmRtVnljMmx2YmlJNk1Td2lZV3huYjNKcGRHaHRJam9pWldReU5UVXhPU0lzSW10bGVTSTZJbTFvZEZaQlRWcFlkVEJLVmtsSGVXeGtha2xIYlcxVlNFVjZkQzg1WTFkVlpVTlZaMlV4ZGtVeVdtczlJbjBzSW05dWJHbHVaU0k2ZXlKclpYbGZkbVZ5YzJsdmJpSTZNU3dpWVd4bmIzSnBkR2h0SWpvaVpXUXlOVFV4T1NJc0ltdGxlU0k2SWtaS00yWklhVXMwTldGVlZVTnVRbHByYm5CNU5XWTJPRzkzUmxObldVdERiSGhNWjJnNEwzbENWRkU5SW4xOWZYMHNJblp2ZEdWeklqcDdJbVptTURBNk1Eb3hNVEFpT25zaWEyVjVYM1I1Y0dVaU9pSnZibXhwYm1VaUxDSnJaWGxmZG1WeWMybHZiaUk2TVgwc0ltWm1NREE2TURveE1qQWlPbnNpYTJWNVgzUjVjR1VpT2lKdmJteHBibVVpTENKclpYbGZkbVZ5YzJsdmJpSTZNWDBzSW1abU1EQTZNRG94TXpBaU9uc2lhMlY1WDNSNWNHVWlPaUp2Ym14cGJtVWlMQ0pyWlhsZmRtVnljMmx2YmlJNk1YMTlMQ0p3Y205dlpsOXZabDl3YjNOelpYTnphVzl1SWpwN2ZYMA==","signatures":[{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJblp2ZEdVaUxDSnJaWGxmZEhsd1pTSTZJbTl1YkdsdVpTSXNJbXRsZVY5MlpYSnphVzl1SWpveExDSmhjeUk2SW1abU1EQTZNRG94TVRBaUxDSmpjbWwwSWpwYkluUjVjR1VpTENKclpYbGZkSGx3WlNJc0ltdGxlVjkyWlhKemFXOXVJaXdpWVhNaVhYMA==","signature":"wTpW/SP68f+/V/gb/f5O7OLB1A/Ckk8YuwTWhimJtbVM3IVM+7yvNV5D+TwK6WQpqK6HYl1Zcaz5kviRH5JQBA=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJblp2ZEdVaUxDSnJaWGxmZEhsd1pTSTZJbTl1YkdsdVpTSXNJbXRsZVY5MlpYSnphVzl1SWpveExDSmhjeUk2SW1abU1EQTZNRG94TWpBaUxDSmpjbWwwSWpwYkluUjVjR1VpTENKclpYbGZkSGx3WlNJc0ltdGxlVjkyWlhKemFXOXVJaXdpWVhNaVhYMA==","signature":"RwMy2bWOFd0xVzREHjwO1TNSaX7PeWg3Vsduu9b0cjJrfplJah7QRLV3qejfHdQknJ84lLdWEflVuFEGhvw7CQ=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJblp2ZEdVaUxDSnJaWGxmZEhsd1pTSTZJbTl1YkdsdVpTSXNJbXRsZVY5MlpYSnphVzl1SWpveExDSmhjeUk2SW1abU1EQTZNRG94TXpBaUxDSmpjbWwwSWpwYkluUjVjR1VpTENKclpYbGZkSGx3WlNJc0ltdGxlVjkyWlhKemFXOXVJaXdpWVhNaVhYMA==","signature":"qpcdsnOX9XNtGg4fdZwBVsL8acyyOH58aNWdwIzWYux/wgl525Ewf0YDyQOCZMo2q73mDxmKJnd7PWp481n7Dw=="}]}
diff --git a/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD2-V1.trc b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD2-V1.trc
new file mode 100644
index 000000000..f0f7d95e0
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbtest/testdata/ISD2-V1.trc
@@ -0,0 +1,1 @@
+{"payload":"ZXlKcGMyUWlPaklzSW5SeVkxOTJaWEp6YVc5dUlqb3hMQ0ppWVhObFgzWmxjbk5wYjI0aU9qRXNJbVJsYzJOeWFYQjBhVzl1SWpvaVNWTkVJRElpTENKMmIzUnBibWRmY1hWdmNuVnRJam95TENKbWIzSnRZWFJmZG1WeWMybHZiaUk2TVN3aVozSmhZMlZmY0dWeWFXOWtJam93TENKMGNuVnpkRjl5WlhObGRGOWhiR3h2ZDJWa0lqcDBjblZsTENKMllXeHBaR2wwZVNJNmV5SnViM1JmWW1WbWIzSmxJam94TlRjMU5UTTJOelEyTENKdWIzUmZZV1owWlhJaU9qRTJNRGN3TnpJM05EWjlMQ0p3Y21sdFlYSjVYMkZ6WlhNaU9uc2labVl3TURvd09qSXhNQ0k2ZXlKaGRIUnlhV0oxZEdWeklqcGJJbUYxZEdodmNtbDBZWFJwZG1VaUxDSmpiM0psSWl3aWFYTnpkV2x1WnlJc0luWnZkR2x1WnlKZExDSnJaWGx6SWpwN0ltbHpjM1ZwYm1jaU9uc2lhMlY1WDNabGNuTnBiMjRpT2pFc0ltRnNaMjl5YVhSb2JTSTZJbVZrTWpVMU1Ua2lMQ0pyWlhraU9pSjJXa3RaWlhWTlVGaEVLMHRWTmlzMEt5OHdlbVpSUVZOcGNFWklVVzB5UVVsSGRsUnpkMVJsYWs1blBTSjlMQ0p2Wm1ac2FXNWxJanA3SW10bGVWOTJaWEp6YVc5dUlqb3hMQ0poYkdkdmNtbDBhRzBpT2lKbFpESTFOVEU1SWl3aWEyVjVJam9pV2xSMGJUUjFkbFoyYW14b1dEQm5RbEppZFZkWGJFOXlZM0JZWVc5SWQxbFVPWEJGVTJSUGRVUjFNRDBpZlN3aWIyNXNhVzVsSWpwN0ltdGxlVjkyWlhKemFXOXVJam94TENKaGJHZHZjbWwwYUcwaU9pSmxaREkxTlRFNUlpd2lhMlY1SWpvaVFWTTRTelUyU0d4aU5HSXZNbmxDWjNGUlZpdENTMmhLTVM5VFQwcG9XVTlRVDNaclJqSTVNMGxPTUQwaWZYMTlMQ0ptWmpBd09qQTZNakl3SWpwN0ltRjBkSEpwWW5WMFpYTWlPbHNpWVhWMGFHOXlhWFJoZEdsMlpTSXNJbU52Y21VaUxDSnBjM04xYVc1bklpd2lkbTkwYVc1bklsMHNJbXRsZVhNaU9uc2lhWE56ZFdsdVp5STZleUpyWlhsZmRtVnljMmx2YmlJNk1Td2lZV3huYjNKcGRHaHRJam9pWldReU5UVXhPU0lzSW10bGVTSTZJak4yTXl0V1FWaDJObU5pU0c5V2JsUklOMmN4UVhkUlV6QlRXVFZqTTAxek1taEVOM1kwU1c1dGIyczlJbjBzSW05bVpteHBibVVpT25zaWEyVjVYM1psY25OcGIyNGlPakVzSW1Gc1oyOXlhWFJvYlNJNkltVmtNalUxTVRraUxDSnJaWGtpT2lKWkwwOXBVbk1yTUVsMVVWQXZSMncyWmxwVlJFbHVZMlZOTW05YU1rZHhkbWxsZW0xbFJuQjBRVU5qUFNKOUxDSnZibXhwYm1VaU9uc2lhMlY1WDNabGNuTnBiMjRpT2pFc0ltRnNaMjl5YVhSb2JTSTZJbVZrTWpVMU1Ua2lMQ0pyWlhraU9pSktUMEZaU1dkVWJrZHdUV1EyTW14R1JqSkZNVXN3UW04NVJHWXpSV2M1U0VFMWRqRmFRMjAyU201TlBTSjlmWDE5TENKMmIzUmxjeUk2ZTMwc0luQnliMjltWDI5bVgzQnZjM05sYzNOcGIyNGlPbnNpWm1Zd01Eb3dPakl4TUNJNld5SnBjM04xYVc1bklpd2liMjVzYVc1bElpd2liMlptYkdsdVpTSmRMQ0ptWmpBd09qQTZNakl3SWpwYkltbHpjM1ZwYm1jaUxDSnZibXhwYm1VaUxDSnZabVpzYVc1bElsMTlmUQ==","signatures":[{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltbHpjM1ZwYm1jaUxDSnJaWGxmZG1WeWMybHZiaUk2TVN3aVlYTWlPaUptWmpBd09qQTZNakV3SWl3aVkzSnBkQ0k2V3lKMGVYQmxJaXdpYTJWNVgzUjVjR1VpTENKclpYbGZkbVZ5YzJsdmJpSXNJbUZ6SWwxOQ==","signature":"Zd42tW63mqcLBcB52jy1Vbm18vo0ESPK0SFGuG35KLrECtJuZwJsyJj3PW/0gfutoBPw0J3swdlrDYcHBB9/Cw=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltOXViR2x1WlNJc0ltdGxlVjkyWlhKemFXOXVJam94TENKaGN5STZJbVptTURBNk1Eb3lNVEFpTENKamNtbDBJanBiSW5SNWNHVWlMQ0pyWlhsZmRIbHdaU0lzSW10bGVWOTJaWEp6YVc5dUlpd2lZWE1pWFgw","signature":"oJDWnrW/gUGBKzg2frlGw6Slb4k+N4q7CF7L3L3mGuM/LHfpMmPCR8z2HbL27YRT/QC7foM7OpQQR7UUXDivBg=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltOW1abXhwYm1VaUxDSnJaWGxmZG1WeWMybHZiaUk2TVN3aVlYTWlPaUptWmpBd09qQTZNakV3SWl3aVkzSnBkQ0k2V3lKMGVYQmxJaXdpYTJWNVgzUjVjR1VpTENKclpYbGZkbVZ5YzJsdmJpSXNJbUZ6SWwxOQ==","signature":"ho8Mm1f06y3cvvzkiZ2G3lPIJ0DbibA0rbK5OMMjSgE0A8OcUiA7/HXdS3VWh7rXuXUy51dSxQCsJ6NsY7HjDA=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltbHpjM1ZwYm1jaUxDSnJaWGxmZG1WeWMybHZiaUk2TVN3aVlYTWlPaUptWmpBd09qQTZNakl3SWl3aVkzSnBkQ0k2V3lKMGVYQmxJaXdpYTJWNVgzUjVjR1VpTENKclpYbGZkbVZ5YzJsdmJpSXNJbUZ6SWwxOQ==","signature":"3Jn9F1Y0hiTjn/NI5B17bsYSgtkCJDLIOikMHq9OvCg3oUHWgIQCeMrg0fkndBaQxJrTQQqJmdRMSJauuBaGBw=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltOXViR2x1WlNJc0ltdGxlVjkyWlhKemFXOXVJam94TENKaGN5STZJbVptTURBNk1Eb3lNakFpTENKamNtbDBJanBiSW5SNWNHVWlMQ0pyWlhsZmRIbHdaU0lzSW10bGVWOTJaWEp6YVc5dUlpd2lZWE1pWFgw","signature":"XXbTa3nGOPakjx+sAkmma8lwrfGfgUlKXNarP7Yo7vU6E/+ReQPHfyQYZVacvMvQwPo9i2e3rngQOQ3azVLFCA=="},{"protected":"ZXlKaGJHY2lPaUpsWkRJMU5URTVJaXdpZEhsd1pTSTZJbkJ5YjI5bVgyOW1YM0J2YzNObGMzTnBiMjRpTENKclpYbGZkSGx3WlNJNkltOW1abXhwYm1VaUxDSnJaWGxmZG1WeWMybHZiaUk2TVN3aVlYTWlPaUptWmpBd09qQTZNakl3SWl3aVkzSnBkQ0k2V3lKMGVYQmxJaXdpYTJWNVgzUjVjR1VpTENKclpYbGZkbVZ5YzJsdmJpSXNJbUZ6SWwxOQ==","signature":"1CGRAcGEkNTvEhcIH3CtQLaoTXfMQZQvxAesLabjKAnry/NOxZX0o6BdWNaGZzAYtuE0s+OpmS6IrtUi1C3GDA=="}]}
diff --git a/go/lib/infra/modules/trust/v2/trustdbtest/trustdbtest.go b/go/lib/infra/modules/trust/v2/trustdbtest/trustdbtest.go
new file mode 100644
index 000000000..84202c6da
--- /dev/null
+++ b/go/lib/infra/modules/trust/v2/trustdbtest/trustdbtest.go
@@ -0,0 +1,306 @@
+// Copyright 2019 Anapaya Systems
+//
+// 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 trustdbtest
+
+import (
+	"context"
+	"io/ioutil"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/scionproto/scion/go/lib/infra/modules/trust/v2"
+	"github.com/scionproto/scion/go/lib/infra/modules/trust/v2/internal/decoded"
+	"github.com/scionproto/scion/go/lib/scrypto"
+	"github.com/scionproto/scion/go/lib/xtest"
+)
+
+var (
+	// DefaultTimeout is the default timeout for running the test harness.
+	DefaultTimeout = time.Second
+	// DefaultRelPath is the default relative path to the test data.
+	DefaultRelPath = "../trustdbtest/testdata"
+)
+
+// Config holds the configuration for the trust database testing harness.
+type Config struct {
+	Timeout time.Duration
+	RelPath string
+}
+
+// InitDefaults initializes the default values for the config.
+func (cfg *Config) InitDefaults() {
+	if cfg.Timeout == 0 {
+		cfg.Timeout = DefaultTimeout
+	}
+	if cfg.RelPath == "" {
+		cfg.RelPath = DefaultRelPath
+	}
+}
+
+func (cfg *Config) filePath(name string) string {
+	return filepath.Join(cfg.RelPath, name)
+}
+
+// TestableDB extends the trust db interface with methods that are needed for testing.
+type TestableDB interface {
+	trust.DB
+	// Prepare should reset the internal state so that the db is empty and is ready to be tested.
+	Prepare(*testing.T, context.Context)
+}
+
+// TestDB should be used to test any implementation of the trust.DB interface.
+// An implementation interface should at least have one test method that calls
+// this test-suite.
+func TestDB(t *testing.T, db TestableDB, cfg Config) {
+	cfg.InitDefaults()
+	tests := map[string]func(*testing.T, trust.ReadWrite, Config){
+		"test TRC":   testTRC,
+		"test chain": testChain,
+	}
+	// Run test suite on DB directly.
+	for name, test := range tests {
+		t.Run("DB: "+name, func(t *testing.T) {
+			ctx, cancelF := context.WithTimeout(context.Background(), cfg.Timeout)
+			defer cancelF()
+			db.Prepare(t, ctx)
+			test(t, db, cfg)
+		})
+	}
+	t.Run("DB: test rollback", func(t *testing.T) {
+		ctx, cancelF := context.WithTimeout(context.Background(), cfg.Timeout)
+		defer cancelF()
+		db.Prepare(t, ctx)
+		testRollback(t, db, cfg)
+	})
+	// Run test suite on transaction.
+	for name, test := range tests {
+		t.Run("TX: "+name, func(t *testing.T) {
+			ctx, cancelF := context.WithTimeout(context.Background(), cfg.Timeout)
+			defer cancelF()
+			db.Prepare(t, ctx)
+			tx, err := db.BeginTransaction(ctx, nil)
+			require.NoError(t, err)
+			test(t, tx, cfg)
+			err = tx.Commit()
+			require.NoError(t, err)
+		})
+	}
+}
+
+func testTRC(t *testing.T, db trust.ReadWrite, cfg Config) {
+	ctx, cancelF := context.WithTimeout(context.Background(), cfg.Timeout)
+	defer cancelF()
+	insert := func(name string) decoded.TRC {
+		dec, err := decoded.DecodeTRC(loadFile(t, cfg.filePath(name)))
+		require.NoError(t, err)
+		inserted, err := db.InsertTRC(ctx, dec)
+		assert.NoError(t, err)
+		assert.True(t, inserted)
+		return dec
+	}
+	v1 := insert("ISD1-V1.trc")
+	v7 := insert("ISD1-V7.trc")
+
+	t.Run("TRCExists", func(t *testing.T) {
+		// Check existing TRC.
+		exists, err := db.TRCExists(ctx, v1)
+		assert.NoError(t, err)
+		assert.True(t, exists)
+		// Check inexistent TRC.
+		other, err := decoded.DecodeTRC(loadFile(t, cfg.filePath("ISD2-V1.trc")))
+		require.NoError(t, err)
+		exists, err = db.TRCExists(ctx, other)
+		assert.NoError(t, err)
+		assert.False(t, exists)
+		// Check existing TRC with different content.
+		mismatch := v1
+		mismatch.Signed.EncodedTRC = []byte("some garbage")
+		exists, err = db.TRCExists(ctx, mismatch)
+		xtest.AssertErrorsIs(t, err, trust.ErrContentMismatch)
+		assert.True(t, exists)
+	})
+	t.Run("GetTRC", func(t *testing.T) {
+		// Fetch existing TRC.
+		fetched, err := db.GetTRC(ctx, v1.TRC.ISD, v1.TRC.Version)
+		assert.NoError(t, err)
+		assert.Equal(t, v1.TRC, fetched)
+		// Fetch max of existing TRC.
+		max, err := db.GetTRC(ctx, v1.TRC.ISD, scrypto.LatestVer)
+		assert.NoError(t, err)
+		assert.Equal(t, v7.TRC, max)
+		// Fetch inexistent TRC.
+		_, err = db.GetTRC(ctx, 42, scrypto.LatestVer)
+		xtest.AssertErrorsIs(t, err, trust.ErrNotFound)
+	})
+	t.Run("GetRawTRC", func(t *testing.T) {
+		// Fetch existing TRC.
+		fetched, err := db.GetRawTRC(ctx, v1.TRC.ISD, v1.TRC.Version)
+		assert.NoError(t, err)
+		assert.Equal(t, v1.Raw, fetched)
+		// Fetch max of existing TRC.
+		max, err := db.GetRawTRC(ctx, v1.TRC.ISD, scrypto.LatestVer)
+		assert.NoError(t, err)
+		assert.Equal(t, v7.Raw, max)
+		// Fetch inexistent TRC.
+		_, err = db.GetRawTRC(ctx, 42, scrypto.LatestVer)
+		xtest.AssertErrorsIs(t, err, trust.ErrNotFound)
+	})
+	t.Run("GetTRCInfo", func(t *testing.T) {
+		// Fetch existing TRC.
+		info := trust.TRCInfo{
+			Validity:    *v1.TRC.Validity,
+			GracePeriod: v1.TRC.GracePeriod.Duration,
+			Version:     v1.TRC.Version,
+		}
+		fetched, err := db.GetTRCInfo(ctx, v1.TRC.ISD, v1.TRC.Version)
+		assert.NoError(t, err)
+		assert.Equal(t, info, fetched)
+		// Fetch max of existing TRC.
+		info = trust.TRCInfo{
+			Validity:    *v7.TRC.Validity,
+			GracePeriod: v7.TRC.GracePeriod.Duration,
+			Version:     v7.TRC.Version,
+		}
+		max, err := db.GetTRCInfo(ctx, v1.TRC.ISD, scrypto.LatestVer)
+		assert.NoError(t, err)
+		assert.Equal(t, info, max)
+		// Fetch inexistent TRC.
+		_, err = db.GetTRCInfo(ctx, 42, scrypto.LatestVer)
+		xtest.AssertErrorsIs(t, err, trust.ErrNotFound)
+	})
+	t.Run("InsertTRC", func(t *testing.T) {
+		// Insert existing TRC.
+		inserted, err := db.InsertTRC(ctx, v1)
+		assert.NoError(t, err)
+		assert.False(t, inserted)
+		// Insert existing TRC with different contents.
+		mismatch := v1
+		mismatch.Signed.EncodedTRC = []byte("some garbage")
+		inserted, err = db.InsertTRC(ctx, mismatch)
+		xtest.AssertErrorsIs(t, err, trust.ErrContentMismatch)
+		assert.False(t, inserted)
+	})
+}
+
+func testChain(t *testing.T, db trust.ReadWrite, cfg Config) {
+	ctx, cancelF := context.WithTimeout(context.Background(), cfg.Timeout)
+	defer cancelF()
+	insert := func(name string, freshIssuer bool) decoded.Chain {
+		dec, err := decoded.DecodeChain(loadFile(t, cfg.filePath(name)))
+		require.NoError(t, err)
+		asInserted, issInserted, err := db.InsertChain(ctx, dec)
+		assert.NoError(t, err)
+		assert.True(t, asInserted)
+		assert.Equal(t, freshIssuer, issInserted)
+		return dec
+	}
+	v1 := insert("ISD1-ASff00_0_110-V1.crt", true)
+	v7 := insert("ISD1-ASff00_0_110-V10.crt", false)
+
+	t.Run("ChainExists", func(t *testing.T) {
+		// Check existing certificate chain.
+		exists, err := db.ChainExists(ctx, v1)
+		assert.NoError(t, err)
+		assert.True(t, exists)
+		// Check inexistent certificate chain.
+		other, err := decoded.DecodeChain(loadFile(t, cfg.filePath("ISD1-ASff00_0_111-V1.crt")))
+		require.NoError(t, err)
+		exists, err = db.ChainExists(ctx, other)
+		assert.NoError(t, err)
+		assert.False(t, exists)
+		// Check existing certificate chain with different content in AS certificate.
+		mismatch := v1
+		mismatch.Chain.AS.Encoded = []byte("some garbage")
+		exists, err = db.ChainExists(ctx, mismatch)
+		xtest.AssertErrorsIs(t, err, trust.ErrContentMismatch)
+		assert.False(t, exists)
+		// Check existing certificate chain with different content in issuer certificate.
+		mismatch = v1
+		mismatch.Chain.Issuer.Encoded = []byte("some garbage")
+		exists, err = db.ChainExists(ctx, mismatch)
+		xtest.AssertErrorsIs(t, err, trust.ErrContentMismatch)
+		assert.False(t, exists)
+	})
+	t.Run("GetRawChain", func(t *testing.T) {
+		// Check existing certificate chain.
+		fetched, err := db.GetRawChain(ctx, v1.AS.Subject, v1.AS.Version)
+		assert.NoError(t, err)
+		assert.Equal(t, v1.Raw, fetched)
+		// Check max of existing certificate chain.
+		max, err := db.GetRawChain(ctx, v1.AS.Subject, scrypto.LatestVer)
+		assert.NoError(t, err)
+		assert.Equal(t, v7.Raw, max)
+		// Check inexistent certificate chain.
+		_, err = db.GetRawChain(ctx, xtest.MustParseIA("42-ff00:0:142"), scrypto.LatestVer)
+		xtest.AssertErrorsIs(t, err, trust.ErrNotFound)
+	})
+	t.Run("InsertChain", func(t *testing.T) {
+		// Check inexistent certificate chain with existing issuer certifcate.
+		other, err := decoded.DecodeChain(loadFile(t, cfg.filePath("ISD1-ASff00_0_111-V1.crt")))
+		require.NoError(t, err)
+		asInserted, issInserted, err := db.InsertChain(ctx, other)
+		assert.NoError(t, err)
+		assert.True(t, asInserted)
+		assert.False(t, issInserted)
+		// Check existing certificate chain.
+		asInserted, issInserted, err = db.InsertChain(ctx, v1)
+		assert.NoError(t, err)
+		assert.False(t, asInserted)
+		assert.False(t, issInserted)
+		// Check existing certificate chain with different content in AS certificate.
+		mismatch := v1
+		mismatch.Chain.AS.Encoded = []byte("some garbage")
+		asInserted, issInserted, err = db.InsertChain(ctx, mismatch)
+		xtest.AssertErrorsIs(t, err, trust.ErrContentMismatch)
+		assert.False(t, asInserted)
+		assert.False(t, issInserted)
+		// Check existing certificate chain with different content in AS certificate.
+		mismatch = v1
+		mismatch.Chain.Issuer.Encoded = []byte("some garbage")
+		asInserted, issInserted, err = db.InsertChain(ctx, mismatch)
+		xtest.AssertErrorsIs(t, err, trust.ErrContentMismatch)
+		assert.False(t, asInserted)
+		assert.False(t, issInserted)
+	})
+}
+
+func testRollback(t *testing.T, db trust.DB, cfg Config) {
+	ctx, cancelF := context.WithTimeout(context.Background(), time.Second)
+	defer cancelF()
+	tx, err := db.BeginTransaction(ctx, nil)
+	require.NoError(t, err)
+
+	trcobj, err := decoded.DecodeTRC(loadFile(t, cfg.filePath("ISD1-V1.trc")))
+	require.NoError(t, err)
+	inserted, err := tx.InsertTRC(ctx, trcobj)
+	assert.NoError(t, err)
+	assert.True(t, inserted)
+	err = tx.Rollback()
+	assert.NoError(t, err)
+	// Check that TRC is not in database after rollback.
+	_, err = db.GetTRCInfo(ctx, 1, scrypto.LatestVer)
+	xtest.AssertErrorsIs(t, err, trust.ErrNotFound)
+}
+
+func loadFile(t *testing.T, name string) []byte {
+	t.Helper()
+	raw, err := ioutil.ReadFile(name)
+	require.NoError(t, err)
+	return raw
+}