Skip to content

Commit

Permalink
refactor: Initial db table and api for identity keys (#2771)
Browse files Browse the repository at this point in the history
A WIP for #2595
  • Loading branch information
gak authored Sep 25, 2024
1 parent 9fa19e7 commit 9a4944d
Show file tree
Hide file tree
Showing 16 changed files with 596 additions and 0 deletions.
6 changes: 6 additions & 0 deletions backend/controller/encryption/api/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func (e *EncryptedColumn[SK]) Scan(src interface{}) error {

type EncryptedTimelineColumn = EncryptedColumn[TimelineSubKey]
type EncryptedAsyncColumn = EncryptedColumn[AsyncSubKey]
type EncryptedIdentityColumn = EncryptedColumn[IdentityKeySubKey]

type OptionalEncryptedTimelineColumn = optional.Option[EncryptedTimelineColumn]
type OptionalEncryptedAsyncColumn = optional.Option[EncryptedAsyncColumn]
Expand All @@ -53,3 +54,8 @@ func (TimelineSubKey) SubKey() string { return "timeline" }
type AsyncSubKey struct{}

func (AsyncSubKey) SubKey() string { return "async" }

// IdentityKeySubKey is a type that represents the subkey for identity keys.
type IdentityKeySubKey struct{}

func (IdentityKeySubKey) SubKey() string { return "identity" }
12 changes: 12 additions & 0 deletions backend/controller/encryption/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"

"github.com/alecthomas/types/optional"
"github.com/tink-crypto/tink-go/v2/tink"

"github.com/TBD54566975/ftl/backend/controller/encryption/api"
"github.com/TBD54566975/ftl/backend/controller/encryption/internal/dal"
Expand All @@ -31,6 +32,17 @@ func New(ctx context.Context, conn libdal.Connection, encryptionBuilder Builder)
return &Service{encryptor: encryptor}, nil
}

// AEAD returns the AEAD instance used by the encryptor.
// TODO: Remove this method once we have a better way to handle this.
func (s *Service) AEAD() (tink.AEAD, error) {
kmsEncryptor, ok := s.encryptor.(*KMSEncryptor)
if !ok {
return nil, fmt.Errorf("encryptor is not of type *KMSEncryptor")
}

return kmsEncryptor.kekAEAD, nil
}

// EncryptJSON encrypts the given JSON object and stores it in the provided destination.
func (s *Service) EncryptJSON(v any, dest api.Encrypted) error {
serialized, err := json.Marshal(v)
Expand Down
48 changes: 48 additions & 0 deletions backend/controller/identity/dal/dal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dal

import (
"context"
"fmt"

"github.com/TBD54566975/ftl/backend/controller/identity/dal/internal/sql"
"github.com/TBD54566975/ftl/backend/libdal"
)

type DAL struct {
*libdal.Handle[DAL]
db sql.Querier
}

func New(conn libdal.Connection) *DAL {
return &DAL{
db: sql.New(conn),
Handle: libdal.New(conn, func(h *libdal.Handle[DAL]) *DAL {
return &DAL{Handle: h, db: sql.New(h.Connection)}
}),
}
}

type EncryptedIdentity = sql.GetIdentityKeysRow

func (d *DAL) GetOnlyIdentityKey(ctx context.Context) (EncryptedIdentity, error) {
rows, err := d.db.GetIdentityKeys(ctx)
if err != nil {
return EncryptedIdentity{}, fmt.Errorf("failed to get only identity key: %w", err)
}
if len(rows) == 0 {
return EncryptedIdentity{}, libdal.ErrNotFound
}
if len(rows) > 1 {
return EncryptedIdentity{}, fmt.Errorf("too many identity keys found: %d", len(rows))
}

return rows[0], nil
}

func (d *DAL) CreateOnlyIdentityKey(ctx context.Context, e EncryptedIdentity) error {
if err := d.db.CreateOnlyIdentityKey(ctx, e.Private, e.Public, e.VerifySignature); err != nil {
return fmt.Errorf("failed to create only identity key: %w", err)
}

return nil
}
31 changes: 31 additions & 0 deletions backend/controller/identity/dal/internal/sql/db.go

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

5 changes: 5 additions & 0 deletions backend/controller/identity/dal/internal/sql/models.go

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

18 changes: 18 additions & 0 deletions backend/controller/identity/dal/internal/sql/querier.go

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

8 changes: 8 additions & 0 deletions backend/controller/identity/dal/internal/sql/queries.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- name: GetIdentityKeys :many
SELECT private, public, verify_signature
FROM identity_keys
LIMIT 2;

-- name: CreateOnlyIdentityKey :exec
INSERT INTO identity_keys (private, public, verify_signature)
VALUES ($1, $2, $3);
57 changes: 57 additions & 0 deletions backend/controller/identity/dal/internal/sql/queries.sql.go

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

183 changes: 183 additions & 0 deletions backend/controller/identity/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package identity

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

"github.com/tink-crypto/tink-go/v2/keyset"

encryptionsvc "github.com/TBD54566975/ftl/backend/controller/encryption"
"github.com/TBD54566975/ftl/backend/controller/encryption/api"
"github.com/TBD54566975/ftl/backend/controller/identity/dal"
"github.com/TBD54566975/ftl/backend/libdal"
internalidentity "github.com/TBD54566975/ftl/internal/identity"
"github.com/TBD54566975/ftl/internal/log"
)

type Service struct {
dal dal.DAL
encryption *encryptionsvc.Service
signer internalidentity.Signer
verifier internalidentity.Verifier
}

func New(ctx context.Context, encryption *encryptionsvc.Service, conn *sql.DB) (*Service, error) {
svc := &Service{
dal: *dal.New(conn),
encryption: encryption,
}

err := svc.ensureIdentity(ctx)
if err != nil {
return nil, fmt.Errorf("failed to ensure identity: %w", err)
}

keyPair, err := svc.getKeyPair(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get key pair: %w", err)
}

signer, err := keyPair.Signer()
if err != nil {
return nil, fmt.Errorf("failed to create signer: %w", err)
}
svc.signer = signer

verifier, err := keyPair.Verifier()
if err != nil {
return nil, fmt.Errorf("failed to create verifier: %w", err)
}
svc.verifier = verifier

return svc, nil
}

func (s Service) Sign(data []byte) (*internalidentity.SignedData, error) {
signedData, err := s.signer.Sign(data)
if err != nil {
return nil, fmt.Errorf("failed to sign data: %w", err)
}

return signedData, nil
}

func (s Service) Verify(signedData internalidentity.SignedData) error {
err := s.verifier.Verify(signedData)
if err != nil {
return fmt.Errorf("failed to verify data: %w", err)
}

return nil
}

func (s Service) getKeyPair(ctx context.Context) (internalidentity.KeyPair, error) {
identity, err := s.dal.GetOnlyIdentityKey(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get only identity key: %w", err)
}

reader := keyset.NewBinaryReader(bytes.NewReader(identity.Private.Bytes()))
aead, err := s.encryption.AEAD()
if err != nil {
return nil, fmt.Errorf("failed to get AEAD: %w", err)
}

handle, err := keyset.Read(reader, aead)
if err != nil {
return nil, fmt.Errorf("failed to read keyset: %w", err)
}

keyPair := internalidentity.NewTinkKeyPair(*handle)
return keyPair, nil
}

const verificationText = "My voice is my passport, verify me."

func (s Service) ensureIdentity(ctx context.Context) (err error) {
logger := log.FromContext(ctx)
tx, err := s.dal.Begin(ctx)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.CommitOrRollback(ctx, &err)

_, err = s.dal.GetOnlyIdentityKey(ctx)
if err != nil {
if !errors.Is(err, libdal.ErrNotFound) {
return fmt.Errorf("failed to get only identity key: %w", err)
}

logger.Debugf("Generating identity key")
err = s.generateAndSaveIdentity(ctx, tx)
if err != nil {
return fmt.Errorf("failed to generate and save identity: %w", err)
}
} else {
logger.Debugf("Identity key already exists")
}

return nil
}

func (s Service) generateAndSaveIdentity(ctx context.Context, tx *dal.DAL) error {
pair, err := internalidentity.GenerateTinkKeyPair()
if err != nil {
return fmt.Errorf("failed to generate key pair: %w", err)
}

signer, err := pair.Signer()
if err != nil {
return fmt.Errorf("failed to create signer: %w", err)
}

signed, err := signer.Sign([]byte(verificationText))
if err != nil {
return fmt.Errorf("failed to sign verification: %w", err)
}

verifier, err := pair.Verifier()
if err != nil {
return fmt.Errorf("failed to create verifier: %w", err)
}

// For total sanity, verify immediately
if err = verifier.Verify(*signed); err != nil {
return fmt.Errorf("failed to verify signed verification: %w", err)
}

// TODO: Make this support different encryptors.
// Might need to refactor internal/identity to access controller encryption types.
// It's a bit tricky because you can't take out the private key from the keyset without
// encrypting it with the AEAD.
handle := pair.Handle()
buf := new(bytes.Buffer)
writer := keyset.NewBinaryWriter(buf)
aead, err := s.encryption.AEAD()
if err != nil {
return fmt.Errorf("failed to get AEAD: %w", err)
}
if err := handle.Write(writer, aead); err != nil {
return fmt.Errorf("failed to write keyset: %w", err)
}
encryptedIdentityColumn := api.EncryptedIdentityColumn{}
encryptedIdentityColumn.Set(buf.Bytes())

public, err := pair.Public()
if err != nil {
return fmt.Errorf("failed to get public key: %w", err)
}

encryptedIdentity := &dal.EncryptedIdentity{
Private: encryptedIdentityColumn,
Public: public,
VerifySignature: signed.Signature,
}
if err := tx.CreateOnlyIdentityKey(ctx, *encryptedIdentity); err != nil {
return fmt.Errorf("failed to create only identity key: %w", err)
}

return nil
}
Loading

0 comments on commit 9a4944d

Please sign in to comment.