diff --git a/tools/walletextension/storage/cert_storage.go b/tools/walletextension/storage/cert_storage.go new file mode 100644 index 000000000..c8f1199a9 --- /dev/null +++ b/tools/walletextension/storage/cert_storage.go @@ -0,0 +1,22 @@ +package storage + +import ( + gethlog "github.com/ethereum/go-ethereum/log" + "github.com/ten-protocol/go-ten/tools/walletextension/storage/database/cosmosdb" + "golang.org/x/crypto/acme/autocert" +) + +// CertStorage defines the interface for certificate storage +type CertStorage interface { + autocert.Cache +} + +// NewCertStorage creates a new certificate storage instance based on the database type +func NewCertStorage(dbType, dbConnectionURL string, randomKey []byte, logger gethlog.Logger) (CertStorage, error) { + switch dbType { + case "cosmosDB": + return cosmosdb.NewCertStorageCosmosDB(dbConnectionURL, randomKey) + default: + return autocert.DirCache("/data/certs"), nil + } +} diff --git a/tools/walletextension/storage/database/cosmosdb/cert_storage_cosmos.go b/tools/walletextension/storage/database/cosmosdb/cert_storage_cosmos.go new file mode 100644 index 000000000..cb0b831d4 --- /dev/null +++ b/tools/walletextension/storage/database/cosmosdb/cert_storage_cosmos.go @@ -0,0 +1,128 @@ +package cosmosdb + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" + "github.com/ten-protocol/go-ten/tools/walletextension/encryption" + "golang.org/x/crypto/acme/autocert" +) + +const ( + CERT_CONTAINER_NAME = "certificates" +) + +// CertStorageCosmosDB implements autocert.Cache interface using CosmosDB +type CertStorageCosmosDB struct { + client *azcosmos.Client + certsContainer *azcosmos.ContainerClient + encryptor encryption.Encryptor +} + +// EncryptedCertDocument represents the structure of a certificate document in CosmosDB +type EncryptedCertDocument struct { + ID string `json:"id"` + Data []byte `json:"data"` +} + +// NewCertStorageCosmosDB creates a new CosmosDB-based certificate storage +func NewCertStorageCosmosDB(connectionString string, encryptionKey []byte) (*CertStorageCosmosDB, error) { + encryptor, err := encryption.NewEncryptor(encryptionKey) + if err != nil { + return nil, fmt.Errorf("failed to create encryptor: %w", err) + } + + client, err := azcosmos.NewClientFromConnectionString(connectionString, nil) + if err != nil { + return nil, fmt.Errorf("failed to create CosmosDB client: %w", err) + } + + // Ensure database exists + ctx := context.Background() + _, err = client.CreateDatabase(ctx, azcosmos.DatabaseProperties{ID: DATABASE_NAME}, nil) + if err != nil && !strings.Contains(err.Error(), "Conflict") { + return nil, fmt.Errorf("failed to create database: %w", err) + } + + // Create container for certificates + certsContainer, err := client.NewContainer(DATABASE_NAME, CERT_CONTAINER_NAME) + if err != nil { + return nil, fmt.Errorf("failed to create certificates container: %w", err) + } + + return &CertStorageCosmosDB{ + client: client, + certsContainer: certsContainer, + encryptor: *encryptor, + }, nil +} + +// Get retrieves a certificate data for the given key +func (c *CertStorageCosmosDB) Get(ctx context.Context, key string) ([]byte, error) { + keyString, partitionKey := c.dbKey([]byte(key)) + + itemResponse, err := c.certsContainer.ReadItem(ctx, partitionKey, keyString, nil) + if err != nil { + if strings.Contains(err.Error(), "NotFound") { + return nil, autocert.ErrCacheMiss + } + return nil, err + } + + var doc EncryptedCertDocument + err = json.Unmarshal(itemResponse.Value, &doc) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal document: %w", err) + } + + return c.encryptor.Decrypt(doc.Data) +} + +// Put stores certificate data with the given key +func (c *CertStorageCosmosDB) Put(ctx context.Context, key string, data []byte) error { + keyString, partitionKey := c.dbKey([]byte(key)) + + encryptedData, err := c.encryptor.Encrypt(data) + if err != nil { + return fmt.Errorf("failed to encrypt certificate data: %w", err) + } + + doc := EncryptedCertDocument{ + ID: keyString, + Data: encryptedData, + } + + docJSON, err := json.Marshal(doc) + if err != nil { + return fmt.Errorf("failed to marshal document: %w", err) + } + + _, err = c.certsContainer.UpsertItem(ctx, partitionKey, docJSON, nil) + if err != nil { + return fmt.Errorf("failed to upsert certificate: %w", err) + } + + return nil +} + +// Delete removes certificate data for the given key +func (c *CertStorageCosmosDB) Delete(ctx context.Context, key string) error { + keyString, partitionKey := c.dbKey([]byte(key)) + + _, err := c.certsContainer.DeleteItem(ctx, partitionKey, keyString, nil) + if err != nil && !strings.Contains(err.Error(), "NotFound") { + return fmt.Errorf("failed to delete certificate: %w", err) + } + + return nil +} + +// dbKey generates a consistent key for CosmosDB storage +func (c *CertStorageCosmosDB) dbKey(key []byte) (string, azcosmos.PartitionKey) { + keyString := string(key) + partitionKey := azcosmos.NewPartitionKeyString(keyString) + return keyString, partitionKey +} diff --git a/tools/walletextension/walletextension_container.go b/tools/walletextension/walletextension_container.go index b9cbbc2a5..d95447f87 100644 --- a/tools/walletextension/walletextension_container.go +++ b/tools/walletextension/walletextension_container.go @@ -80,12 +80,17 @@ func NewContainerFromConfig(config wecommon.Config, logger gethlog.Logger) *Cont // Certificate Signing Request (CRS) is generated // CRS is sent to CA (Let's Encrypt) via ACME (automated certificate management environment) client // CA verifies CRS and issues a certificate - // we store store certificate and private key (in memory and also in on a mounted volume attached to docker container - /data/certs/) + // Store certificate and private key in certificate storage based on the database type + certStorage, err := storage.NewCertStorage(config.DBType, config.DBConnectionURL, encryptionKey, logger) + if err != nil { + logger.Crit("unable to create certificate storage", log.ErrKey, err) + os.Exit(1) + } certManager := &autocert.Manager{ Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist(config.TLSDomain), - Cache: autocert.DirCache("/data/certs"), + Cache: certStorage, } // Create HTTP-01 challenge handler