From 99d1990cadfffb48ca8b32f603f84060fb465bc2 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Mon, 30 Dec 2024 15:51:24 -0500 Subject: [PATCH 1/9] chore(core): Refactor Crypto Providers - New service crypto package, recrypto, which provides a shared interface for necessary TDF operations, both for ZTDF (Wrap) and Nano (Derive) - This includes a shift to yet another configuration for the crypto layer. Notably, the keys can now be completely configured in the `services.kas.keyring`, without needing the `server.cryptoprovider` field --- Makefile | 2 +- docs/configuration.md | 3 +- examples/cmd/benchmark.go | 9 + lib/ocrypto/asym_decryption.go | 2 +- lib/ocrypto/asym_encryption.go | 4 +- lib/ocrypto/ec_key_pair.go | 10 +- opentdf-dev.yaml | 24 +- opentdf-kas-mode.yaml | 22 +- service/internal/security/crypto_provider.go | 29 - service/internal/security/errors.go | 2 - service/internal/security/standard_crypto.go | 435 ------------ service/internal/security/standard_only.go | 24 - service/internal/server/server.go | 11 - service/kas/access/provider.go | 83 ++- service/kas/access/publicKey.go | 96 +-- service/kas/access/publicKey_test.go | 188 +++-- service/kas/access/rewrap.go | 139 ++-- service/kas/access/rewrap_test.go | 7 +- service/kas/kas.go | 65 +- service/kas/kidless_test.go | 58 -- service/kas/recrypt/recrypt.go | 110 +++ service/kas/recrypt/standard.go | 650 ++++++++++++++++++ service/kas/recrypt/standard_test.go | 122 ++++ .../pkg/server/testdata/all-no-config.yaml | 12 - test/tdf-roundtrips.bats | 41 +- 25 files changed, 1181 insertions(+), 967 deletions(-) delete mode 100644 service/internal/security/crypto_provider.go delete mode 100644 service/internal/security/standard_crypto.go delete mode 100644 service/internal/security/standard_only.go delete mode 100644 service/kas/kidless_test.go create mode 100644 service/kas/recrypt/recrypt.go create mode 100644 service/kas/recrypt/standard.go create mode 100644 service/kas/recrypt/standard_test.go diff --git a/Makefile b/Makefile index d4bc7b530..327db3a54 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ toolcheck: @which buf > /dev/null || (echo "buf not found, please install it from https://docs.buf.build/installation" && exit 1) @which golangci-lint > /dev/null || (echo "golangci-lint not found, run 'go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0'" && exit 1) @which protoc-gen-doc > /dev/null || (echo "protoc-gen-doc not found, run 'go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@v1.5.1'" && exit 1) - @golangci-lint --version | grep "version v\?1.6[123]" > /dev/null || (echo "golangci-lint version must be v1.61 or later [$$(golangci-lint --version)]" && exit 1) + @golangci-lint --version | grep "version v\?1.6[1234]" > /dev/null || (echo "golangci-lint version must be v1.61 or later [$$(golangci-lint --version)]" && exit 1) @which goimports >/dev/null || (echo "goimports not found, run 'go install golang.org/x/tools/cmd/goimports@latest'") fix: tidy fmt diff --git a/docs/configuration.md b/docs/configuration.md index 3810e4245..95da77f4d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -183,7 +183,8 @@ Environment Variable: `OPENTDF_SERVICES_KAS_KEYRING='[{"kid":"k1","alg":"rsa:204 | ------------------ | ------------------------------------------------------------------------------- | -------- | | `keyring.*.kid` | Which key id this is binding | | | `keyring.*.alg` | (Optional) Associated algorithm. (Allows reusing KID with different algorithms) | | -| `keyring.*.legacy` | Indicates this may be used for TDFs with no key ID; default if all unspecified. | inferred | +| `keyring.*.active` | Marks the current public key for new TDFs with the specific algorithm; please specify exactly 1 for each currently recommended mechanism | false | +| `keyring.*.legacy` | Indicates this may be used for TDFs with no key ID; default if all unspecified. | false | Example: diff --git a/examples/cmd/benchmark.go b/examples/cmd/benchmark.go index 3909cb1ff..eb8d99996 100644 --- a/examples/cmd/benchmark.go +++ b/examples/cmd/benchmark.go @@ -202,6 +202,15 @@ func runBenchmark(cmd *cobra.Command, args []string) error { successCount++ totalDuration += result } + if successCount == 0 { + if errorCount > 0 { + cmd.Printf("\nError Summary:\n") + for errMsg, count := range errorMsgs { + cmd.Printf("%s: %d occurrences\n", errMsg, count) + } + } + return fmt.Errorf("no successful requests") + } totalTime := time.Since(startTime) averageLatency := totalDuration / time.Duration(successCount) diff --git a/lib/ocrypto/asym_decryption.go b/lib/ocrypto/asym_decryption.go index 1cf2eba9d..570835040 100644 --- a/lib/ocrypto/asym_decryption.go +++ b/lib/ocrypto/asym_decryption.go @@ -18,7 +18,7 @@ type AsymDecryption struct { func NewAsymDecryption(privateKeyInPem string) (AsymDecryption, error) { block, _ := pem.Decode([]byte(privateKeyInPem)) if block == nil { - return AsymDecryption{}, errors.New("failed to parse PEM formatted private key") + return AsymDecryption{}, errors.New("failed to parse PEM formatted RSA private key (decode failed)") } priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) diff --git a/lib/ocrypto/asym_encryption.go b/lib/ocrypto/asym_encryption.go index 65ef5a115..94df93c63 100644 --- a/lib/ocrypto/asym_encryption.go +++ b/lib/ocrypto/asym_encryption.go @@ -19,7 +19,7 @@ type AsymEncryption struct { func NewAsymEncryption(publicKeyInPem string) (AsymEncryption, error) { block, _ := pem.Decode([]byte(publicKeyInPem)) if block == nil { - return AsymEncryption{}, errors.New("failed to parse PEM formatted public key") + return AsymEncryption{}, errors.New("failed to parse PEM formatted public key (decode fail)") } var pub any @@ -31,7 +31,7 @@ func NewAsymEncryption(publicKeyInPem string) (AsymEncryption, error) { var ok bool if pub, ok = cert.PublicKey.(*rsa.PublicKey); !ok { - return AsymEncryption{}, errors.New("failed to parse PEM formatted public key") + return AsymEncryption{}, errors.New("failed to parse PEM formatted public key (incorrect type)") } } else { var err error diff --git a/lib/ocrypto/ec_key_pair.go b/lib/ocrypto/ec_key_pair.go index 6b9d0a960..6d2a691f6 100644 --- a/lib/ocrypto/ec_key_pair.go +++ b/lib/ocrypto/ec_key_pair.go @@ -45,7 +45,7 @@ func GetECCurveFromECCMode(mode ECCMode) (elliptic.Curve, error) { // TODO FIXME - unsupported? return nil, errors.New("unsupported nanoTDF ecc mode") default: - return nil, fmt.Errorf("unsupported nanoTDF ecc mode %d", mode) + return nil, fmt.Errorf("unsupported nanoTDF ecc mode [%d]", mode) } return c, nil @@ -211,7 +211,7 @@ func VerifyECDSASig(digest, r, s []byte, pubKey *ecdsa.PublicKey) bool { func ECPubKeyFromPem(pemECPubKey []byte) (*ecdh.PublicKey, error) { block, _ := pem.Decode(pemECPubKey) if block == nil { - return nil, fmt.Errorf("failed to parse PEM formatted public key") + return nil, fmt.Errorf("failed to parse PEM formatted public key (decode fail)") } var pub any @@ -223,7 +223,7 @@ func ECPubKeyFromPem(pemECPubKey []byte) (*ecdh.PublicKey, error) { var ok bool if pub, ok = cert.PublicKey.(*ecdsa.PublicKey); !ok { - return nil, fmt.Errorf("failed to parse PEM formatted public key") + return nil, fmt.Errorf("failed to parse PEM formatted public key (incorrect cert type)") } } else { var err error @@ -247,7 +247,7 @@ func ECPubKeyFromPem(pemECPubKey []byte) (*ecdh.PublicKey, error) { func ECPrivateKeyFromPem(privateECKeyInPem []byte) (*ecdh.PrivateKey, error) { block, _ := pem.Decode(privateECKeyInPem) if block == nil { - return nil, fmt.Errorf("failed to parse PEM formatted private key") + return nil, fmt.Errorf("failed to parse PEM formatted EC private key (decode failed)") } priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) @@ -322,7 +322,7 @@ func UncompressECPubKey(curve elliptic.Curve, compressedPubKey []byte) (*ecdsa.P } // Creating ecdsa.PublicKey from *big.Int ephemeralECDSAPublicKey := &ecdsa.PublicKey{ - Curve: elliptic.P256(), + Curve: curve, X: x, Y: y, } diff --git a/opentdf-dev.yaml b/opentdf-dev.yaml index f93d7d630..901a16c22 100644 --- a/opentdf-dev.yaml +++ b/opentdf-dev.yaml @@ -13,14 +13,14 @@ services: keyring: - kid: e1 alg: ec:secp256r1 - - kid: e1 - alg: ec:secp256r1 - legacy: true - - kid: r1 - alg: rsa:2048 + cert: kas-ec-cert.pem + private: kas-ec-private.pem + active: true - kid: r1 alg: rsa:2048 - legacy: true + cert: kas-cert.pem + private: kas-private.pem + active: true entityresolution: log_level: info url: http://localhost:8888/auth @@ -105,16 +105,4 @@ server: maxage: 3600 grpc: reflectionEnabled: true # Default is false - cryptoProvider: - type: standard - standard: - keys: - - kid: r1 - alg: rsa:2048 - private: kas-private.pem - cert: kas-cert.pem - - kid: e1 - alg: ec:secp256r1 - private: kas-ec-private.pem - cert: kas-ec-cert.pem port: 8080 diff --git a/opentdf-kas-mode.yaml b/opentdf-kas-mode.yaml index 9340ea1c1..58b3393e0 100644 --- a/opentdf-kas-mode.yaml +++ b/opentdf-kas-mode.yaml @@ -16,13 +16,15 @@ services: keyring: - kid: e1 alg: ec:secp256r1 - - kid: e1 - alg: ec:secp256r1 + private: kas-ec-private.pem + cert: kas-ec-cert.pem + active: true legacy: true - kid: r1 alg: rsa:2048 - - kid: r1 - alg: rsa:2048 + private: kas-private.pem + cert: kas-cert.pem + active: true legacy: true server: tls: @@ -95,16 +97,4 @@ server: maxage: 3600 grpc: reflectionEnabled: true # Default is false - cryptoProvider: - type: standard - standard: - keys: - - kid: r1 - alg: rsa:2048 - private: kas-private.pem - cert: kas-cert.pem - - kid: e1 - alg: ec:secp256r1 - private: kas-ec-private.pem - cert: kas-ec-cert.pem port: 8181 diff --git a/service/internal/security/crypto_provider.go b/service/internal/security/crypto_provider.go deleted file mode 100644 index 9538d3d6a..000000000 --- a/service/internal/security/crypto_provider.go +++ /dev/null @@ -1,29 +0,0 @@ -package security - -import ( - "crypto" - "crypto/elliptic" -) - -const ( - // Key agreement along P-256 - AlgorithmECP256R1 = "ec:secp256r1" - // Used for encryption with RSA of the KAO - AlgorithmRSA2048 = "rsa:2048" -) - -type CryptoProvider interface { - // Gets some KID associated with a given algorithm. - // Returns empty string if none are found. - FindKID(alg string) string - RSAPublicKey(keyID string) (string, error) - RSAPublicKeyAsJSON(keyID string) (string, error) - RSADecrypt(hash crypto.Hash, keyID string, keyLabel string, ciphertext []byte) ([]byte, error) - - ECPublicKey(keyID string) (string, error) - ECCertificate(keyID string) (string, error) - GenerateNanoTDFSymmetricKey(kasKID string, ephemeralPublicKeyBytes []byte, curve elliptic.Curve) ([]byte, error) - GenerateEphemeralKasKeys() (any, []byte, error) - GenerateNanoTDFSessionKey(privateKeyHandle any, ephemeralPublicKey []byte) ([]byte, error) - Close() -} diff --git a/service/internal/security/errors.go b/service/internal/security/errors.go index 73f429376..f8328fed7 100644 --- a/service/internal/security/errors.go +++ b/service/internal/security/errors.go @@ -7,8 +7,6 @@ const ( ErrKeyPairInfoMalformed = Error("key pair info malformed") ErrCertificateEncode = Error("certificate encode error") ErrPublicKeyMarshal = Error("public key marshal error") - ErrHSMUnexpected = Error("hsm unexpected") - ErrHSMDecrypt = Error("hsm decrypt error") ErrHSMNotFound = Error("hsm unavailable") ErrKeyConfig = Error("key configuration error") ErrUnknownHashFunction = Error("unknown hash function") diff --git a/service/internal/security/standard_crypto.go b/service/internal/security/standard_crypto.go deleted file mode 100644 index a180e7751..000000000 --- a/service/internal/security/standard_crypto.go +++ /dev/null @@ -1,435 +0,0 @@ -package security - -import ( - "crypto" - "crypto/elliptic" - "crypto/sha256" - "crypto/x509" - "encoding/json" - "encoding/pem" - "errors" - "fmt" - "log/slog" - "os" - - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/opentdf/platform/lib/ocrypto" -) - -const ( - kNanoTDFMagicStringAndVersion = "L1L" -) - -type StandardConfig struct { - Keys []KeyPairInfo `mapstructure:"keys" json:"keys"` - // Deprecated - RSAKeys map[string]StandardKeyInfo `mapstructure:"rsa,omitempty" json:"rsa,omitempty"` - // Deprecated - ECKeys map[string]StandardKeyInfo `mapstructure:"ec,omitempty" json:"ec,omitempty"` -} - -type KeyPairInfo struct { - // Valid algorithm. May be able to be derived from Private but it is better to just say it. - Algorithm string `mapstructure:"alg" json:"alg"` - // Key identifier. Should be short - KID string `mapstructure:"kid" json:"kid"` - // Implementation specific locator for private key; - // for 'standard' crypto service this is the path to a PEM file - Private string `mapstructure:"private" json:"private"` - // Optional locator for the corresponding certificate. - // If not found, only public key (derivable from Private) is available. - Certificate string `mapstructure:"cert" json:"cert"` - // Optional enumeration of intended usages of keypair - Usage string `mapstructure:"usage" json:"usage"` - // Optional long form description of key pair including purpose and life cycle information - Purpose string `mapstructure:"purpose" json:"purpose"` -} - -type StandardKeyInfo struct { - PrivateKeyPath string `mapstructure:"private_key_path" json:"private_key_path"` - PublicKeyPath string `mapstructure:"public_key_path" json:"public_key_path"` -} - -type StandardRSACrypto struct { - KeyPairInfo - asymDecryption ocrypto.AsymDecryption - asymEncryption ocrypto.AsymEncryption -} - -type StandardECCrypto struct { - KeyPairInfo - ecPrivateKeyPem string - ecCertificatePEM string -} - -// List of keys by identifier -type keylist map[string]any - -type StandardCrypto struct { - // Lists of keys first sorted by algorithm - keys map[string]keylist -} - -// NewStandardCrypto Create a new instance of standard crypto -func NewStandardCrypto(cfg StandardConfig) (*StandardCrypto, error) { - switch { - case len(cfg.Keys) > 0 && len(cfg.RSAKeys)+len(cfg.ECKeys) > 0: - return nil, errors.New("please specify `keys` only; remove deprecated `rsa` and `ec` fields from cfg") - case len(cfg.Keys) > 0: - return loadKeys(cfg.Keys) - default: - return loadDeprecatedKeys(cfg.RSAKeys, cfg.ECKeys) - } -} - -func loadKeys(ks []KeyPairInfo) (*StandardCrypto, error) { - keys := make(map[string]keylist) - for _, k := range ks { - slog.Info("crypto cfg loading", "id", k.KID, "alg", k.Algorithm) - if _, ok := keys[k.Algorithm]; !ok { - keys[k.Algorithm] = make(map[string]any) - } - loadedKey, err := loadKey(k) - if err != nil { - return nil, err - } - keys[k.Algorithm][k.KID] = loadedKey - } - return &StandardCrypto{ - keys: keys, - }, nil -} - -func loadKey(k KeyPairInfo) (any, error) { - privatePEM, err := os.ReadFile(k.Private) - if err != nil { - return nil, fmt.Errorf("failed to read private key file [%s]: %w", k.Private, err) - } - var certPEM []byte - if k.Certificate != "" { - certPEM, err = os.ReadFile(k.Certificate) - if err != nil { - return nil, fmt.Errorf("failed to read certificate file [%s]: %w", k.Certificate, err) - } - } - switch k.Algorithm { - case AlgorithmECP256R1: - return StandardECCrypto{ - KeyPairInfo: k, - ecPrivateKeyPem: string(privatePEM), - ecCertificatePEM: string(certPEM), - }, nil - case AlgorithmRSA2048: - asymDecryption, err := ocrypto.NewAsymDecryption(string(privatePEM)) - if err != nil { - return nil, fmt.Errorf("ocrypto.NewAsymDecryption failed: %w", err) - } - asymEncryption, err := ocrypto.NewAsymEncryption(string(certPEM)) - if err != nil { - return nil, fmt.Errorf("ocrypto.NewAsymEncryption failed: %w", err) - } - return StandardRSACrypto{ - KeyPairInfo: k, - asymDecryption: asymDecryption, - asymEncryption: asymEncryption, - }, nil - default: - return nil, errors.New("unsupported algorithm [" + k.Algorithm + "]") - } -} - -func loadDeprecatedKeys(rsaKeys map[string]StandardKeyInfo, ecKeys map[string]StandardKeyInfo) (*StandardCrypto, error) { - keys := make(map[string]keylist) - - if len(ecKeys) > 0 { - keys[AlgorithmECP256R1] = make(map[string]any) - } - if len(rsaKeys) > 0 { - keys[AlgorithmRSA2048] = make(map[string]any) - } - - for id, kasInfo := range rsaKeys { - privatePemData, err := os.ReadFile(kasInfo.PrivateKeyPath) - if err != nil { - return nil, fmt.Errorf("failed to rsa private key file: %w", err) - } - - asymDecryption, err := ocrypto.NewAsymDecryption(string(privatePemData)) - if err != nil { - return nil, fmt.Errorf("ocrypto.NewAsymDecryption failed: %w", err) - } - - publicPemData, err := os.ReadFile(kasInfo.PublicKeyPath) - if err != nil { - return nil, fmt.Errorf("failed to rsa public key file: %w", err) - } - - asymEncryption, err := ocrypto.NewAsymEncryption(string(publicPemData)) - if err != nil { - return nil, fmt.Errorf("ocrypto.NewAsymEncryption failed: %w", err) - } - - keys[AlgorithmRSA2048][id] = StandardRSACrypto{ - KeyPairInfo: KeyPairInfo{ - Algorithm: AlgorithmRSA2048, - KID: id, - Private: kasInfo.PrivateKeyPath, - Certificate: kasInfo.PublicKeyPath, - }, - asymDecryption: asymDecryption, - asymEncryption: asymEncryption, - } - } - for id, kasInfo := range ecKeys { - slog.Info("cfg.ECKeys", "id", id, "kasInfo", kasInfo) - // private and public EC KAS key - privatePemData, err := os.ReadFile(kasInfo.PrivateKeyPath) - if err != nil { - return nil, fmt.Errorf("failed to EC private key file: %w", err) - } - // certificate EC KAS key - ecCertificatePEM, err := os.ReadFile(kasInfo.PublicKeyPath) - if err != nil { - return nil, fmt.Errorf("failed to EC certificate file: %w", err) - } - keys[AlgorithmECP256R1][id] = StandardECCrypto{ - KeyPairInfo: KeyPairInfo{ - Algorithm: AlgorithmRSA2048, - KID: id, - Private: kasInfo.PrivateKeyPath, - Certificate: kasInfo.PublicKeyPath, - }, - ecPrivateKeyPem: string(privatePemData), - ecCertificatePEM: string(ecCertificatePEM), - } - } - - return &StandardCrypto{ - keys: keys, - }, nil -} - -func (s StandardCrypto) FindKID(alg string) string { - if ks, ok := s.keys[alg]; ok && len(ks) > 0 { - for kid := range ks { - return kid - } - } - return "" -} - -func (s StandardCrypto) RSAPublicKey(kid string) (string, error) { - rsaKeys, ok := s.keys[AlgorithmRSA2048] - if !ok || len(rsaKeys) == 0 { - return "", ErrCertNotFound - } - k, ok := rsaKeys[kid] - if !ok { - return "", ErrCertNotFound - } - rsa, ok := k.(StandardRSACrypto) - if !ok { - return "", ErrCertNotFound - } - - pem, err := rsa.asymEncryption.PublicKeyInPemFormat() - if err != nil { - return "", fmt.Errorf("failed to retrieve rsa public key file: %w", err) - } - - return pem, nil -} - -func (s StandardCrypto) ECCertificate(kid string) (string, error) { - ecKeys, ok := s.keys[AlgorithmECP256R1] - if !ok || len(ecKeys) == 0 { - return "", ErrCertNotFound - } - k, ok := ecKeys[kid] - if !ok { - return "", ErrCertNotFound - } - ec, ok := k.(StandardECCrypto) - if !ok { - return "", ErrCertNotFound - } - return ec.ecCertificatePEM, nil -} - -func (s StandardCrypto) ECPublicKey(kid string) (string, error) { - ecKeys, ok := s.keys[AlgorithmECP256R1] - if !ok || len(ecKeys) == 0 { - return "", ErrCertNotFound - } - k, ok := ecKeys[kid] - if !ok { - return "", ErrCertNotFound - } - ec, ok := k.(StandardECCrypto) - if !ok { - return "", ErrCertNotFound - } - - ecPrivateKey, err := ocrypto.ECPrivateKeyFromPem([]byte(ec.ecPrivateKeyPem)) - if err != nil { - return "", fmt.Errorf("ECPrivateKeyFromPem failed: %s %w", kid, err) - } - - ecPublicKey := ecPrivateKey.PublicKey() - derBytes, err := x509.MarshalPKIXPublicKey(ecPublicKey) - if err != nil { - return "", fmt.Errorf("failed to marshal public key: %s %w", kid, err) - } - - pemBlock := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: derBytes, - } - pemBytes := pem.EncodeToMemory(pemBlock) - if pemBytes == nil { - return "", fmt.Errorf("failed to encode public key to PEM: %s", kid) - } - return string(pemBytes), nil -} - -func (s StandardCrypto) RSADecrypt(_ crypto.Hash, kid string, _ string, ciphertext []byte) ([]byte, error) { - rsaKeys, ok := s.keys[AlgorithmRSA2048] - if !ok || len(rsaKeys) == 0 { - return nil, ErrCertNotFound - } - k, ok := rsaKeys[kid] - if !ok { - return nil, ErrCertNotFound - } - rsa, ok := k.(StandardRSACrypto) - if !ok { - return nil, ErrCertNotFound - } - - data, err := rsa.asymDecryption.Decrypt(ciphertext) - if err != nil { - return nil, fmt.Errorf("error decrypting data: %w", err) - } - - return data, nil -} - -func (s StandardCrypto) RSAPublicKeyAsJSON(kid string) (string, error) { - rsaKeys, ok := s.keys[AlgorithmRSA2048] - if !ok || len(rsaKeys) == 0 { - return "", ErrCertNotFound - } - k, ok := rsaKeys[kid] - if !ok { - return "", ErrCertNotFound - } - rsa, ok := k.(StandardRSACrypto) - if !ok { - return "", ErrCertNotFound - } - - rsaPublicKeyJwk, err := jwk.FromRaw(rsa.asymEncryption.PublicKey) - if err != nil { - return "", fmt.Errorf("jwk.FromRaw: %w", err) - } - - jsonPublicKey, err := json.Marshal(rsaPublicKeyJwk) - if err != nil { - return "", fmt.Errorf("jwk.FromRaw: %w", err) - } - - return string(jsonPublicKey), nil -} - -func (s StandardCrypto) GenerateNanoTDFSymmetricKey(kasKID string, ephemeralPublicKeyBytes []byte, curve elliptic.Curve) ([]byte, error) { - ephemeralECDSAPublicKey, err := ocrypto.UncompressECPubKey(curve, ephemeralPublicKeyBytes) - if err != nil { - return nil, err - } - - derBytes, err := x509.MarshalPKIXPublicKey(ephemeralECDSAPublicKey) - if err != nil { - return nil, fmt.Errorf("failed to marshal ECDSA public key: %w", err) - } - pemBlock := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: derBytes, - } - ephemeralECDSAPublicKeyPEM := pem.EncodeToMemory(pemBlock) - - ecKeys, ok := s.keys[AlgorithmECP256R1] - if !ok || len(ecKeys) == 0 { - return nil, ErrNoKeys - } - k, ok := ecKeys[kasKID] - if !ok { - return nil, ErrKeyPairInfoNotFound - } - ec, ok := k.(StandardECCrypto) - if !ok { - return nil, ErrKeyPairInfoMalformed - } - - symmetricKey, err := ocrypto.ComputeECDHKey([]byte(ec.ecPrivateKeyPem), ephemeralECDSAPublicKeyPEM) - if err != nil { - return nil, fmt.Errorf("ocrypto.ComputeECDHKey failed: %w", err) - } - - key, err := ocrypto.CalculateHKDF(versionSalt(), symmetricKey) - if err != nil { - return nil, fmt.Errorf("ocrypto.CalculateHKDF failed:%w", err) - } - - return key, nil -} - -func (s StandardCrypto) GenerateEphemeralKasKeys() (any, []byte, error) { - ephemeralKeyPair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1) - if err != nil { - return nil, nil, fmt.Errorf("ocrypto.NewECKeyPair failed: %w", err) - } - - pubKeyInPem, err := ephemeralKeyPair.PublicKeyInPemFormat() - if err != nil { - return nil, nil, fmt.Errorf("failed to get public key in PEM format: %w", err) - } - pubKeyBytes := []byte(pubKeyInPem) - - privKey, err := ocrypto.ConvertToECDHPrivateKey(ephemeralKeyPair.PrivateKey) - if err != nil { - return nil, nil, fmt.Errorf("failed to convert provate key to ECDH: %w", err) - } - return privKey, pubKeyBytes, nil -} - -func (s StandardCrypto) GenerateNanoTDFSessionKey(privateKey any, ephemeralPublicKeyPEM []byte) ([]byte, error) { - ecdhKey, err := ocrypto.ConvertToECDHPrivateKey(privateKey) - if err != nil { - return nil, fmt.Errorf("GenerateNanoTDFSessionKey failed to ConvertToECDHPrivateKey: %w", err) - } - ephemeralECDHPublicKey, err := ocrypto.ECPubKeyFromPem(ephemeralPublicKeyPEM) - if err != nil { - return nil, fmt.Errorf("GenerateNanoTDFSessionKey failed to ocrypto.ECPubKeyFromPem: %w", err) - } - // shared secret - sessionKey, err := ecdhKey.ECDH(ephemeralECDHPublicKey) - if err != nil { - return nil, fmt.Errorf("GenerateNanoTDFSessionKey failed to ecdhKey.ECDH: %w", err) - } - - salt := versionSalt() - derivedKey, err := ocrypto.CalculateHKDF(salt, sessionKey) - if err != nil { - return nil, fmt.Errorf("ocrypto.CalculateHKDF failed:%w", err) - } - return derivedKey, nil -} - -func (s StandardCrypto) Close() { -} - -func versionSalt() []byte { - digest := sha256.New() - digest.Write([]byte(kNanoTDFMagicStringAndVersion)) - return digest.Sum(nil) -} diff --git a/service/internal/security/standard_only.go b/service/internal/security/standard_only.go deleted file mode 100644 index a4b49b3b2..000000000 --- a/service/internal/security/standard_only.go +++ /dev/null @@ -1,24 +0,0 @@ -package security - -import "log/slog" - -type Config struct { - Type string `mapstructure:"type" json:"type" default:"standard"` - // StandardConfig is the configuration for the standard key provider - StandardConfig StandardConfig `mapstructure:"standard" json:"standard"` -} - -func NewCryptoProvider(cfg Config) (CryptoProvider, error) { - switch cfg.Type { - case "hsm": - slog.Error("opentdf hsm mode has been removed") - return nil, ErrHSMNotFound - case "standard": - return NewStandardCrypto(cfg.StandardConfig) - default: - if cfg.Type != "" { - slog.Warn("unsupported crypto type", "crypto.type", cfg.Type) - } - return NewStandardCrypto(cfg.StandardConfig) - } -} diff --git a/service/internal/server/server.go b/service/internal/server/server.go index 8dac4f47d..adf44de0d 100644 --- a/service/internal/server/server.go +++ b/service/internal/server/server.go @@ -21,7 +21,6 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" sdkAudit "github.com/opentdf/platform/sdk/audit" "github.com/opentdf/platform/service/internal/auth" - "github.com/opentdf/platform/service/internal/security" "github.com/opentdf/platform/service/internal/server/memhttp" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/logger/audit" @@ -48,7 +47,6 @@ func (e Error) Error() string { type Config struct { Auth auth.Config `mapstructure:"auth" json:"auth"` GRPC GRPCConfig `mapstructure:"grpc" json:"grpc"` - CryptoProvider security.Config `mapstructure:"cryptoProvider" json:"cryptoProvider"` TLS TLSConfig `mapstructure:"tls" json:"tls"` CORS CORSConfig `mapstructure:"cors" json:"cors"` WellKnownConfigRegister func(namespace string, config any) error `mapstructure:"-" json:"-"` @@ -63,7 +61,6 @@ func (c Config) LogValue() slog.Value { return slog.GroupValue( slog.Any("auth", c.Auth), slog.Any("grpc", c.GRPC), - slog.Any("cryptoProvider", c.CryptoProvider), slog.Any("tls", c.TLS), slog.Any("cors", c.CORS), slog.Int("port", c.Port), @@ -116,7 +113,6 @@ type OpenTDFServer struct { HTTPServer *http.Server ConnectRPCInProcess *inProcessServer ConnectRPC *ConnectRPC - CryptoProvider security.CryptoProvider logger *logger.Logger } @@ -212,13 +208,6 @@ func NewOpenTDFServer(config Config, logger *logger.Logger) (*OpenTDFServer, err logger: logger, } - // Create crypto provider - logger.Info("creating crypto provider", slog.String("type", config.CryptoProvider.Type)) - o.CryptoProvider, err = security.NewCryptoProvider(config.CryptoProvider) - if err != nil { - return nil, fmt.Errorf("security.NewCryptoProvider: %w", err) - } - return &o, nil } diff --git a/service/kas/access/provider.go b/service/kas/access/provider.go index 24bc6c004..8c0515ba1 100644 --- a/service/kas/access/provider.go +++ b/service/kas/access/provider.go @@ -2,29 +2,32 @@ package access import ( "context" + "crypto" + "fmt" "net/url" + "os" + "github.com/opentdf/platform/lib/ocrypto" kaspb "github.com/opentdf/platform/protocol/go/kas" otdf "github.com/opentdf/platform/sdk" - "github.com/opentdf/platform/service/internal/security" + "github.com/opentdf/platform/service/kas/recrypt" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/pkg/serviceregistry" "go.opentelemetry.io/otel/trace" ) const ( - ErrHSM = Error("hsm unexpected") ErrConfig = Error("invalid config") ) type Provider struct { kaspb.AccessServiceServer - URI url.URL `json:"uri"` - SDK *otdf.SDK - AttributeSvc *url.URL - CryptoProvider security.CryptoProvider - Logger *logger.Logger - Config *serviceregistry.ServiceConfig + URI url.URL `json:"uri"` + SDK *otdf.SDK + AttributeSvc *url.URL + recrypt.CryptoProvider + Logger *logger.Logger + Config *serviceregistry.ServiceConfig KASConfig trace.Tracer } @@ -32,19 +35,63 @@ type Provider struct { type KASConfig struct { // Which keys are currently the default. Keyring []CurrentKeyFor `mapstructure:"keyring" json:"keyring"` - // Deprecated - ECCertID string `mapstructure:"eccertid" json:"eccertid"` - // Deprecated - RSACertID string `mapstructure:"rsacertid" json:"rsacertid"` } -// Specifies the preferred/default key for a given algorithm type. +func (p *Provider) LoadStandardCryptoProvider() (*recrypt.Standard, error) { + var opts []recrypt.StandardOption + for _, key := range p.Keyring { + privatePemData, err := os.ReadFile(key.Private) + if err != nil { + return nil, fmt.Errorf("failure to read rsa private key file [%s]: %w", key.Private, err) + } + + var secret crypto.PrivateKey + // FIXME will this work for EC keys? It seems to be rsa only. + if key.Algorithm == recrypt.AlgorithmRSA2048 { + asymDecryption, err := ocrypto.NewAsymDecryption(string(privatePemData)) + if err != nil { + return nil, fmt.Errorf("ocrypto.NewAsymDecryption failed: %w", err) + } + secret = asymDecryption.PrivateKey + } else { + ecPrivateKey, err := ocrypto.ECPrivateKeyFromPem(privatePemData) + if err != nil { + return nil, fmt.Errorf("ocrypto.ECPrivateKeyFromPem failed: %w", err) + } + secret = ecPrivateKey + } + + var publicPemData []byte + if key.Certificate != "" { + publicPemData, err = os.ReadFile(key.Certificate) + if err != nil { + return nil, fmt.Errorf("failure to read rsa public key file [%s]: %w", key.Certificate, err) + } + } + opts = append(opts, recrypt.WithKey(key.KID, key.Algorithm, secret, publicPemData, key.Active, key.Legacy)) + } + c, err := recrypt.NewStandardWithOptions(opts...) + if err != nil { + return nil, fmt.Errorf("recrypt.NewStandardWithOptions failed: %w", err) + } + p.CryptoProvider = c + return c, nil +} + +// Details about a key. type CurrentKeyFor struct { - Algorithm string `mapstructure:"alg" json:"alg"` - KID string `mapstructure:"kid" json:"kid"` - // Indicates that the key should not be serves by default, - // but instead is allowed for legacy reasons on decrypt (rewrap) only - Legacy bool `mapstructure:"legacy" json:"legacy"` + // Valid algorithm. May be able to be derived from Private but it is better to just say it. + recrypt.Algorithm `mapstructure:"alg"` + // Key identifier. Should be short + KID recrypt.KeyIdentifier `mapstructure:"kid"` + // Implementation specific locator for private key; + // for 'standard' crypto service this is the path to a PEM file + Private string `mapstructure:"private"` + // Optional locator for the corresponding certificate. + // If not found, only public key (derivable from Private) is available. + Certificate string `mapstructure:"cert"` + Active bool `mapstructure:"active"` + Legacy bool `mapstructure:"legacy"` } func (p *Provider) IsReady(ctx context.Context) error { diff --git a/service/kas/access/publicKey.go b/service/kas/access/publicKey.go index 4213bd243..1c2f1adc7 100644 --- a/service/kas/access/publicKey.go +++ b/service/kas/access/publicKey.go @@ -11,6 +11,7 @@ import ( "connectrpc.com/connect" kaspb "github.com/opentdf/platform/protocol/go/kas" "github.com/opentdf/platform/service/internal/security" + "github.com/opentdf/platform/service/kas/recrypt" "go.opentelemetry.io/otel/trace" wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -20,53 +21,20 @@ const ( ErrPublicKeyMarshal = Error("public key marshal error") ) -func (p Provider) lookupKid(ctx context.Context, algorithm string) (string, error) { - if len(p.KASConfig.Keyring) == 0 { - p.Logger.WarnContext(ctx, "no default keys found", "algorithm", algorithm) - return "", connect.NewError(connect.CodeNotFound, errors.Join(ErrConfig, errors.New("no default keys configured"))) - } - - for _, k := range p.KASConfig.Keyring { - if k.Algorithm == algorithm && !k.Legacy { - return k.KID, nil - } - } - p.Logger.WarnContext(ctx, "no (non-legacy) key for requested algorithm", "algorithm", algorithm) - return "", connect.NewError(connect.CodeNotFound, errors.Join(ErrConfig, errors.New("no default key for algorithm"))) -} - func (p Provider) LegacyPublicKey(ctx context.Context, req *connect.Request[kaspb.LegacyPublicKeyRequest]) (*connect.Response[wrapperspb.StringValue], error) { - algorithm := req.Msg.GetAlgorithm() - if algorithm == "" { - algorithm = security.AlgorithmRSA2048 - } - var pem string - var err error - if p.CryptoProvider == nil { - return nil, connect.NewError(connect.CodeInternal, errors.Join(ErrConfig, errors.New("configuration error"))) + algorithm, err := p.CryptoProvider.ParseAlgorithm(req.Msg.GetAlgorithm()) + if err != nil { + return nil, err } - kid, err := p.lookupKid(ctx, algorithm) + kid, err := p.CryptoProvider.CurrentKID(algorithm) if err != nil { return nil, err } - - switch algorithm { - case security.AlgorithmECP256R1: - pem, err = p.CryptoProvider.ECCertificate(kid) - if err != nil { - p.Logger.ErrorContext(ctx, "CryptoProvider.ECPublicKey failed", "err", err) - return nil, connect.NewError(connect.CodeInternal, errors.Join(ErrConfig, errors.New("configuration error"))) - } - case security.AlgorithmRSA2048: - fallthrough - case "": - pem, err = p.CryptoProvider.RSAPublicKey(kid) - if err != nil { - p.Logger.ErrorContext(ctx, "CryptoProvider.RSAPublicKey failed", "err", err) - return nil, connect.NewError(connect.CodeInternal, errors.Join(ErrConfig, errors.New("configuration error"))) - } - default: - return nil, connect.NewError(connect.CodeNotFound, errors.Join(ErrConfig, errors.New("invalid algorithm"))) + fmt := recrypt.KeyFormatPEM + pem, err := p.CryptoProvider.PublicKey(algorithm, kid, fmt) + if err != nil { + p.Logger.ErrorContext(ctx, "CryptoProvider.ECPublicKey failed", "err", err) + return nil, connect.NewError(connect.CodeInternal, errors.Join(ErrConfig, errors.New("configuration error"))) } return connect.NewResponse(&wrapperspb.StringValue{Value: pem}), nil } @@ -78,17 +46,24 @@ func (p Provider) PublicKey(ctx context.Context, req *connect.Request[kaspb.Publ defer span.End() } - algorithm := req.Msg.GetAlgorithm() - if algorithm == "" { - algorithm = security.AlgorithmRSA2048 + algorithm, err := p.CryptoProvider.ParseAlgorithm(req.Msg.GetAlgorithm()) + if err != nil { + return nil, connect.NewError(connect.CodeNotFound, err) + } + if algorithm == recrypt.AlgorithmUndefined { + algorithm = recrypt.AlgorithmRSA2048 } - fmt := req.Msg.GetFmt() - kid, err := p.lookupKid(ctx, algorithm) + + kid, err := p.CryptoProvider.CurrentKID(algorithm) if err != nil { - return nil, err + return nil, connect.NewError(connect.CodeNotFound, err) + } + fmt, err := p.CryptoProvider.ParseKeyFormat(req.Msg.GetFmt()) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) } - r := func(value, kid string, err error) (*connect.Response[kaspb.PublicKeyResponse], error) { + r := func(value string, kid []recrypt.KeyIdentifier, err error) (*connect.Response[kaspb.PublicKeyResponse], error) { if errors.Is(err, security.ErrCertNotFound) { p.Logger.ErrorContext(ctx, "no key found for", "err", err, "kid", kid, "algorithm", algorithm, "fmt", fmt) return nil, connect.NewError(connect.CodeNotFound, err) @@ -100,28 +75,11 @@ func (p Provider) PublicKey(ctx context.Context, req *connect.Request[kaspb.Publ p.Logger.WarnContext(ctx, "hiding kid in public key response for legacy client", "kid", kid, "v", req.Msg.GetV()) return connect.NewResponse(&kaspb.PublicKeyResponse{PublicKey: value}), nil } - return connect.NewResponse(&kaspb.PublicKeyResponse{PublicKey: value, Kid: kid}), nil + return connect.NewResponse(&kaspb.PublicKeyResponse{PublicKey: value, Kid: string(kid[0])}), nil } - switch algorithm { - case security.AlgorithmECP256R1: - ecPublicKeyPem, err := p.CryptoProvider.ECPublicKey(kid) - return r(ecPublicKeyPem, kid, err) - case security.AlgorithmRSA2048: - fallthrough - case "": - switch fmt { - case "jwk": - rsaPublicKeyPem, err := p.CryptoProvider.RSAPublicKeyAsJSON(kid) - return r(rsaPublicKeyPem, kid, err) - case "pkcs8": - fallthrough - case "": - rsaPublicKeyPem, err := p.CryptoProvider.RSAPublicKey(kid) - return r(rsaPublicKeyPem, kid, err) - } - } - return nil, connect.NewError(connect.CodeNotFound, errors.New("invalid algorithm or format")) + v, err := p.CryptoProvider.PublicKey(algorithm, kid, fmt) + return r(v, kid, err) } func exportRsaPublicKeyAsPemStr(pubkey *rsa.PublicKey) (string, error) { diff --git a/service/kas/access/publicKey_test.go b/service/kas/access/publicKey_test.go index a01e74a7c..e6d626855 100644 --- a/service/kas/access/publicKey_test.go +++ b/service/kas/access/publicKey_test.go @@ -15,7 +15,7 @@ import ( "connectrpc.com/connect" kaspb "github.com/opentdf/platform/protocol/go/kas" - "github.com/opentdf/platform/service/internal/security" + "github.com/opentdf/platform/service/kas/recrypt" "github.com/opentdf/platform/service/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -87,26 +87,21 @@ func TestError(t *testing.T) { const hostname = "localhost" func TestStandardCertificateHandlerEmpty(t *testing.T) { - configStandard := security.Config{ - Type: "standard", - } - c := mustNewCryptoProvider(t, configStandard) - defer c.Close() kasURI := urlHost(t) - kas := Provider{ - URI: *kasURI, - CryptoProvider: c, - Logger: logger.CreateTestLogger(), + URI: *kasURI, + Logger: logger.CreateTestLogger(), } + c := mustNewCryptoProvider(t, &kas) + defer c.Close() result, err := kas.PublicKey(context.Background(), &connect.Request[kaspb.PublicKeyRequest]{Msg: &kaspb.PublicKeyRequest{Fmt: "pkcs8"}}) require.Error(t, err, "not found") assert.Nil(t, result) } -func mustNewCryptoProvider(t *testing.T, configStandard security.Config) security.CryptoProvider { - c, err := security.NewCryptoProvider(configStandard) +func mustNewCryptoProvider(t *testing.T, p *Provider) recrypt.Closeable { + c, err := p.LoadStandardCryptoProvider() require.NoError(t, err) require.NotNil(t, c) return c @@ -119,32 +114,23 @@ func urlHost(t *testing.T) *url.URL { } func TestStandardPublicKeyHandlerV2(t *testing.T) { - configStandard := security.Config{ - Type: "standard", - StandardConfig: security.StandardConfig{ - RSAKeys: map[string]security.StandardKeyInfo{ - "rsa": { - PrivateKeyPath: "./testdata/access-provider-000-private.pem", - PublicKeyPath: "./testdata/access-provider-000-certificate.pem", - }, - }, - }, - } - c := mustNewCryptoProvider(t, configStandard) - defer c.Close() kasURI := urlHost(t) kas := Provider{ - URI: *kasURI, - CryptoProvider: c, + URI: *kasURI, KASConfig: KASConfig{ Keyring: []CurrentKeyFor{ { - Algorithm: security.AlgorithmRSA2048, - KID: "rsa", + Algorithm: recrypt.AlgorithmRSA2048, + KID: "rsa", + Private: "./testdata/access-provider-000-private.pem", + Certificate: "./testdata/access-provider-000-certificate.pem", + Active: true, }, }, }, } + c := mustNewCryptoProvider(t, &kas) + defer c.Close() result, err := kas.PublicKey(context.Background(), &connect.Request[kaspb.PublicKeyRequest]{Msg: &kaspb.PublicKeyRequest{}}) require.NoError(t, err) @@ -153,17 +139,13 @@ func TestStandardPublicKeyHandlerV2(t *testing.T) { } func TestStandardPublicKeyHandlerV2Failure(t *testing.T) { - configStandard := security.Config{ - Type: "standard", - } - c := mustNewCryptoProvider(t, configStandard) - defer c.Close() kasURI := urlHost(t) kas := Provider{ - URI: *kasURI, - CryptoProvider: c, - Logger: logger.CreateTestLogger(), + URI: *kasURI, + Logger: logger.CreateTestLogger(), } + c := mustNewCryptoProvider(t, &kas) + defer c.Close() k, err := kas.PublicKey(context.Background(), &connect.Request[kaspb.PublicKeyRequest]{Msg: &kaspb.PublicKeyRequest{}}) assert.Nil(t, k) @@ -171,25 +153,24 @@ func TestStandardPublicKeyHandlerV2Failure(t *testing.T) { } func TestStandardPublicKeyHandlerV2NotFound(t *testing.T) { - configStandard := security.Config{ - Type: "standard", - StandardConfig: security.StandardConfig{ - RSAKeys: map[string]security.StandardKeyInfo{ - "rsa": { - PrivateKeyPath: "./testdata/access-provider-000-private.pem", - PublicKeyPath: "./testdata/access-provider-000-certificate.pem", + kasURI := urlHost(t) + kas := Provider{ + URI: *kasURI, + Logger: logger.CreateTestLogger(), + KASConfig: KASConfig{ + Keyring: []CurrentKeyFor{ + { + Algorithm: recrypt.AlgorithmRSA2048, + KID: "rsa", + Private: "./testdata/access-provider-000-private.pem", + Certificate: "./testdata/access-provider-000-certificate.pem", + Active: true, }, }, }, } - c := mustNewCryptoProvider(t, configStandard) + c := mustNewCryptoProvider(t, &kas) defer c.Close() - kasURI := urlHost(t) - kas := Provider{ - URI: *kasURI, - CryptoProvider: c, - Logger: logger.CreateTestLogger(), - } k, err := kas.PublicKey(context.Background(), &connect.Request[kaspb.PublicKeyRequest]{ Msg: &kaspb.PublicKeyRequest{ @@ -204,32 +185,24 @@ func TestStandardPublicKeyHandlerV2NotFound(t *testing.T) { } func TestStandardPublicKeyHandlerV2WithJwk(t *testing.T) { - configStandard := security.Config{ - Type: "standard", - StandardConfig: security.StandardConfig{ - RSAKeys: map[string]security.StandardKeyInfo{ - "rsa": { - PrivateKeyPath: "./testdata/access-provider-000-private.pem", - PublicKeyPath: "./testdata/access-provider-000-certificate.pem", - }, - }, - }, - } - c := mustNewCryptoProvider(t, configStandard) - defer c.Close() kasURI := urlHost(t) kas := Provider{ - URI: *kasURI, - CryptoProvider: c, + URI: *kasURI, + Logger: logger.CreateTestLogger(), KASConfig: KASConfig{ Keyring: []CurrentKeyFor{ { - Algorithm: security.AlgorithmRSA2048, - KID: "rsa", + Algorithm: recrypt.AlgorithmRSA2048, + KID: "rsa", + Private: "./testdata/access-provider-000-private.pem", + Certificate: "./testdata/access-provider-000-certificate.pem", + Active: true, }, }, }, } + c := mustNewCryptoProvider(t, &kas) + defer c.Close() result, err := kas.PublicKey(context.Background(), &connect.Request[kaspb.PublicKeyRequest]{ Msg: &kaspb.PublicKeyRequest{ @@ -245,24 +218,23 @@ func TestStandardPublicKeyHandlerV2WithJwk(t *testing.T) { func TestStandardCertificateHandlerWithEc256(t *testing.T) { t.Skip("EC Not yet implemented") - configStandard := security.Config{ - Type: "standard", - StandardConfig: security.StandardConfig{ - ECKeys: map[string]security.StandardKeyInfo{ - "ec": { - PrivateKeyPath: "./testdata/access-provider-ec-private.pem", - PublicKeyPath: "./testdata/access-provider-ec-certificate.pem", + kasURI := urlHost(t) + kas := Provider{ + URI: *kasURI, + Logger: logger.CreateTestLogger(), + KASConfig: KASConfig{ + Keyring: []CurrentKeyFor{ + { + Algorithm: recrypt.AlgorithmECP256R1, + KID: "rsa", + Private: "./testdata/access-provider-ec-private.pem", + Certificate: "./testdata/access-provider-ec-certificate.pem", }, }, }, } - c := mustNewCryptoProvider(t, configStandard) + c := mustNewCryptoProvider(t, &kas) defer c.Close() - kasURI := urlHost(t) - kas := Provider{ - URI: *kasURI, - CryptoProvider: c, - } result, err := kas.LegacyPublicKey(context.Background(), &connect.Request[kaspb.LegacyPublicKeyRequest]{ Msg: &kaspb.LegacyPublicKeyRequest{ @@ -275,25 +247,24 @@ func TestStandardCertificateHandlerWithEc256(t *testing.T) { } func TestStandardPublicKeyHandlerWithEc256(t *testing.T) { - t.Skip("EC Not yet implemented") - configStandard := security.Config{ - Type: "standard", - StandardConfig: security.StandardConfig{ - ECKeys: map[string]security.StandardKeyInfo{ - "ec": { - PrivateKeyPath: "./testdata/access-provider-ec-private.pem", - PublicKeyPath: "./testdata/access-provider-ec-certificate.pem", + kasURI := urlHost(t) + kas := Provider{ + URI: *kasURI, + Logger: logger.CreateTestLogger(), + KASConfig: KASConfig{ + Keyring: []CurrentKeyFor{ + { + Algorithm: recrypt.AlgorithmECP256R1, + KID: "rsa", + Private: "./testdata/access-provider-ec-private.pem", + Certificate: "./testdata/access-provider-ec-certificate.pem", + Active: true, }, }, }, } - c := mustNewCryptoProvider(t, configStandard) + c := mustNewCryptoProvider(t, &kas) defer c.Close() - kasURI := urlHost(t) - kas := Provider{ - URI: *kasURI, - CryptoProvider: c, - } result, err := kas.PublicKey(context.Background(), &connect.Request[kaspb.PublicKeyRequest]{ Msg: &kaspb.PublicKeyRequest{ @@ -306,25 +277,24 @@ func TestStandardPublicKeyHandlerWithEc256(t *testing.T) { } func TestStandardPublicKeyHandlerV2WithEc256(t *testing.T) { - t.Skip("EC Not yet implemented") - configStandard := security.Config{ - Type: "standard", - StandardConfig: security.StandardConfig{ - ECKeys: map[string]security.StandardKeyInfo{ - "ec": { - PrivateKeyPath: "./testdata/access-provider-ec-private.pem", - PublicKeyPath: "./testdata/access-provider-ec-certificate.pem", + kasURI := urlHost(t) + kas := Provider{ + URI: *kasURI, + Logger: logger.CreateTestLogger(), + KASConfig: KASConfig{ + Keyring: []CurrentKeyFor{ + { + Algorithm: recrypt.AlgorithmECP256R1, + KID: "rsa", + Private: "./testdata/access-provider-ec-private.pem", + Certificate: "./testdata/access-provider-ec-certificate.pem", + Active: true, }, }, }, } - c := mustNewCryptoProvider(t, configStandard) + c := mustNewCryptoProvider(t, &kas) defer c.Close() - kasURI := urlHost(t) - kas := Provider{ - URI: *kasURI, - CryptoProvider: c, - } result, err := kas.PublicKey(context.Background(), &connect.Request[kaspb.PublicKeyRequest]{ Msg: &kaspb.PublicKeyRequest{ diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index ea4e53821..214ce7c40 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -3,11 +3,10 @@ package access import ( "bytes" "context" - "crypto" "crypto/ecdsa" + "crypto/elliptic" "crypto/hmac" "crypto/rsa" - "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/hex" @@ -30,7 +29,7 @@ import ( kaspb "github.com/opentdf/platform/protocol/go/kas" "github.com/opentdf/platform/sdk" - "github.com/opentdf/platform/service/internal/security" + "github.com/opentdf/platform/service/kas/recrypt" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/logger/audit" ctxAuth "github.com/opentdf/platform/service/pkg/auth" @@ -76,16 +75,6 @@ func err403(s string) error { return connect.NewError(connect.CodePermissionDenied, errors.Join(ErrUser, status.Error(codes.PermissionDenied, s))) } -func generateHMACDigest(ctx context.Context, msg, key []byte, logger logger.Logger) ([]byte, error) { - mac := hmac.New(sha256.New, key) - _, err := mac.Write(msg) - if err != nil { - logger.WarnContext(ctx, "failed to compute hmac") - return nil, errors.Join(ErrUser, status.Error(codes.InvalidArgument, "policy hmac")) - } - return mac.Sum(nil), nil -} - var acceptableSkew = 30 * time.Second func verifySRT(ctx context.Context, srt string, dpopJWK jwk.Key, logger logger.Logger) (string, error) { @@ -202,8 +191,8 @@ func extractPolicyBinding(policyBinding interface{}) (string, error) { } } -func verifyAndParsePolicy(ctx context.Context, requestBody *RequestBody, k []byte, logger logger.Logger) (*Policy, error) { - actualHMAC, err := generateHMACDigest(ctx, []byte(requestBody.Policy), k, logger) +func verifyAndParsePolicy(ctx context.Context, requestBody *RequestBody, k recrypt.UnwrappedKey, logger logger.Logger) (*Policy, error) { + actualHMAC, err := k.Digest([]byte(requestBody.Policy)) if err != nil { logger.WarnContext(ctx, "unable to generate policy hmac", "err", err) return nil, err400("bad request") @@ -311,13 +300,13 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, body *RequestBody, entity *en defer span.End() } - var kidsToCheck []string + var kidsToCheck []recrypt.KeyIdentifier if body.KeyAccess.KID != "" { - kidsToCheck = []string{body.KeyAccess.KID} + kidsToCheck = []recrypt.KeyIdentifier{recrypt.KeyIdentifier(body.KeyAccess.KID)} } else { p.Logger.InfoContext(ctx, "kid free kao") for _, k := range p.KASConfig.Keyring { - if k.Algorithm == security.AlgorithmRSA2048 && k.Legacy { + if k.Algorithm == recrypt.AlgorithmRSA2048 { kidsToCheck = append(kidsToCheck, k.KID) } } @@ -326,19 +315,18 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, body *RequestBody, entity *en return nil, err400("bad request") } } - symmetricKey, err := p.CryptoProvider.RSADecrypt(crypto.SHA1, kidsToCheck[0], "", body.KeyAccess.WrappedKey) + symmetricKey, err := p.CryptoProvider.Unwrap(kidsToCheck[0], body.KeyAccess.WrappedKey) for _, kid := range kidsToCheck[1:] { - p.Logger.WarnContext(ctx, "continue paging through legacy KIDs for kid free kao", "err", err) if err == nil { break } - symmetricKey, err = p.CryptoProvider.RSADecrypt(crypto.SHA1, kid, "", body.KeyAccess.WrappedKey) + p.Logger.DebugContext(ctx, "continue paging through legacy KIDs for kid free kao", "err", err) + symmetricKey, err = p.CryptoProvider.Unwrap(kid, body.KeyAccess.WrappedKey) } if err != nil { p.Logger.WarnContext(ctx, "failure to decrypt dek", "err", err) return nil, err400("bad request") } - p.Logger.DebugContext(ctx, "verifying policy binding", "requestBody.policy", body.Policy) policy, err := verifyAndParsePolicy(ctx, body, symmetricKey, *p.Logger) if err != nil { @@ -383,7 +371,7 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, body *RequestBody, entity *en p.Logger.WarnContext(ctx, "ocrypto.NewAsymEncryption:", "err", err) } - rewrappedKey, err := asymEncrypt.Encrypt(symmetricKey) + rewrappedKey, err := symmetricKey.Wrap(asymEncrypt) if err != nil { p.Logger.WarnContext(ctx, "rewrap: ocrypto.AsymEncryption.encrypt failed", "err", err, "clientPublicKey", &body.ClientPublicKey) p.Logger.Audit.RewrapFailure(ctx, auditEventParams) @@ -411,25 +399,31 @@ func (p *Provider) nanoTDFRewrap(ctx context.Context, body *RequestBody, entity if err != nil { return nil, fmt.Errorf("failed to parse NanoTDF header: %w", err) } - // Lookup KID from nano header - kid, err := header.GetKasURL().GetIdentifier() - if err != nil { - p.Logger.DebugContext(ctx, "nanoTDFRewrap GetIdentifier", "kid", kid, "err", err) - // legacy nano with KID - kid, err = p.lookupKid(ctx, security.AlgorithmECP256R1) - if err != nil { - p.Logger.ErrorContext(ctx, "failure to find default kid for ec", "err", err) - return nil, err400("bad request") - } - p.Logger.DebugContext(ctx, "nanoTDFRewrap lookupKid", "kid", kid) - } - p.Logger.DebugContext(ctx, "nanoTDFRewrap", "kid", kid) ecCurve, err := header.ECCurve() if err != nil { return nil, fmt.Errorf("ECCurve failed: %w", err) } - symmetricKey, err := p.CryptoProvider.GenerateNanoTDFSymmetricKey(kid, header.EphemeralKey, ecCurve) + var a recrypt.Algorithm + switch ecCurve { + case elliptic.P256(): + a = recrypt.AlgorithmECP256R1 + case elliptic.P384(): + a = recrypt.AlgorithmECP384R1 + case elliptic.P521(): + a = recrypt.AlgorithmECP521R1 + default: + return nil, fmt.Errorf("unsupported curve: %s", ecCurve) + } + + // Lookup KID from nano header, or infer if not found + kid, err := p.extractKasKID(ctx, header, a) + if err != nil { + return nil, err + } + p.Logger.DebugContext(ctx, "nanoTDFRewrap", "kid", kid) + + symmetricKey, err := p.CryptoProvider.Derive(kid, header.EphemeralKey) if err != nil { return nil, fmt.Errorf("failed to generate symmetric key: %w", err) } @@ -478,48 +472,55 @@ func (p *Provider) nanoTDFRewrap(ctx context.Context, body *RequestBody, entity return nil, err403("forbidden") } - privateKeyHandle, publicKeyHandle, err := p.CryptoProvider.GenerateEphemeralKasKeys() + wrappedKey, err := symmetricKey.NanoWrap([]byte(body.ClientPublicKey)) if err != nil { p.Logger.Audit.RewrapFailure(ctx, auditEventParams) - return nil, fmt.Errorf("failed to generate keypair: %w", err) + return nil, fmt.Errorf("failed to wrap key: %w", err) } - sessionKey, err := p.CryptoProvider.GenerateNanoTDFSessionKey(privateKeyHandle, []byte(body.ClientPublicKey)) - if err != nil { - p.Logger.Audit.RewrapFailure(ctx, auditEventParams) - return nil, fmt.Errorf("failed to generate session key: %w", err) - } - - cipherText, err := wrapKeyAES(sessionKey, symmetricKey) - if err != nil { - p.Logger.Audit.RewrapFailure(ctx, auditEventParams) - return nil, fmt.Errorf("failed to encrypt key: %w", err) - } - p.Logger.Audit.RewrapSuccess(ctx, auditEventParams) return &kaspb.RewrapResponse{ - EntityWrappedKey: cipherText, - SessionPublicKey: string(publicKeyHandle), + EntityWrappedKey: wrappedKey.EntityWrappedKey, + SessionPublicKey: wrappedKey.SessionPublicKey, SchemaVersion: schemaVersion, }, nil } -func extractNanoPolicy(symmetricKey []byte, header sdk.NanoTDFHeader) (*Policy, error) { - gcm, err := ocrypto.NewAESGcm(symmetricKey) - if err != nil { - return nil, fmt.Errorf("crypto.NewAESGcm:%w", err) +func (p *Provider) extractKasKID(ctx context.Context, header sdk.NanoTDFHeader, a recrypt.Algorithm) (recrypt.KeyIdentifier, error) { + kidStr, err := header.GetKasURL().GetIdentifier() + if err == nil { + kid, err := p.ParseKeyIdentifier(kidStr) + if err != nil { + p.Logger.ErrorContext(ctx, "invalid kid", "kid", kidStr, "err", err) + return "", err400("bad request") + } + return kid, nil + } + if strings.Contains(err.Error(), "legacy") { + // legacy nano without KID + kids, err := p.LegacyKIDs(a) + if err != nil { + return "", fmt.Errorf("failure looking up legacy KID: %w", err) + } + if len(kids) == 0 { + p.Logger.ErrorContext(ctx, "failure to find legacy kids for ec", "err", err) + return "", err400("bad request") + } + if len(kids) > 1 { + p.Logger.WarnContext(ctx, "multiple legacy kids for ec; only trying one") + } + return kids[0], nil } + return "", fmt.Errorf("failed to get KID: %w", err) +} - const ( - kIvLen = 12 - ) - iv := make([]byte, kIvLen) +func extractNanoPolicy(symmetricKey recrypt.UnwrappedKey, header sdk.NanoTDFHeader) (*Policy, error) { tagSize, err := sdk.SizeOfAuthTagForCipher(header.GetCipher()) if err != nil { return nil, fmt.Errorf("SizeOfAuthTagForCipher failed:%w", err) } - policyData, err := gcm.DecryptWithIVAndTagSize(iv, header.EncryptedPolicyBody, tagSize) + policyData, err := symmetricKey.DecryptNanoPolicy(header.EncryptedPolicyBody, tagSize) if err != nil { return nil, fmt.Errorf("Error decrypting policy body:%w", err) } @@ -531,17 +532,3 @@ func extractNanoPolicy(symmetricKey []byte, header sdk.NanoTDFHeader) (*Policy, } return &policy, nil } - -func wrapKeyAES(sessionKey, dek []byte) ([]byte, error) { - gcm, err := ocrypto.NewAESGcm(sessionKey) - if err != nil { - return nil, fmt.Errorf("crypto.NewAESGcm:%w", err) - } - - cipherText, err := gcm.Encrypt(dek) - if err != nil { - return nil, fmt.Errorf("crypto.AsymEncryption.encrypt:%w", err) - } - - return cipherText, nil -} diff --git a/service/kas/access/rewrap_test.go b/service/kas/access/rewrap_test.go index a9eff6e97..ed63699cc 100644 --- a/service/kas/access/rewrap_test.go +++ b/service/kas/access/rewrap_test.go @@ -17,6 +17,7 @@ import ( "github.com/lestrrat-go/jwx/v2/jws" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/service/kas/recrypt" "github.com/opentdf/platform/service/logger" ctxAuth "github.com/opentdf/platform/service/pkg/auth" "github.com/stretchr/testify/assert" @@ -206,8 +207,7 @@ func keyAccessWrappedRaw(t *testing.T, policyBindingAsString bool) KeyAccess { wrappedKey, err := asym.Encrypt([]byte(plainKey)) require.NoError(t, err, "rewrap: encryptWithPublicKey failed") - logger := logger.CreateTestLogger() - bindingBytes, err := generateHMACDigest(context.Background(), policyBytes, []byte(plainKey), *logger) + bindingBytes, err := recrypt.NewAESUnwrappedKey([]byte(plainKey)).Digest(policyBytes) require.NoError(t, err) dst := make([]byte, hex.EncodedLen(len(bindingBytes))) @@ -349,7 +349,8 @@ func TestParseAndVerifyRequest(t *testing.T) { require.NotNil(t, verified, "unable to load request body") require.NotNil(t, verified.ClientPublicKey, "unable to load public key") - policy, err := verifyAndParsePolicy(context.Background(), verified, []byte(plainKey), *logger) + k0 := recrypt.NewAESUnwrappedKey([]byte(plainKey)) + policy, err := verifyAndParsePolicy(context.Background(), verified, k0, *logger) if !tt.shouldError { require.NoError(t, err, "failed to verify policy body=[%v]", tt.body) assert.Len(t, policy.Body.DataAttributes, 2, "incorrect policy body=[%v]", policy.Body) diff --git a/service/kas/kas.go b/service/kas/kas.go index 07f5f27e0..642e783b0 100644 --- a/service/kas/kas.go +++ b/service/kas/kas.go @@ -9,7 +9,6 @@ import ( "github.com/mitchellh/mapstructure" kaspb "github.com/opentdf/platform/protocol/go/kas" "github.com/opentdf/platform/protocol/go/kas/kasconnect" - "github.com/opentdf/platform/service/internal/security" "github.com/opentdf/platform/service/kas/access" "github.com/opentdf/platform/service/pkg/serviceregistry" ) @@ -37,46 +36,22 @@ func NewRegistration() *serviceregistry.Service[kasconnect.AccessServiceHandler] if err := mapstructure.Decode(srp.Config, &kasCfg); err != nil { panic(fmt.Errorf("invalid kas cfg [%v] %w", srp.Config, err)) } - - switch { - case kasCfg.ECCertID != "" && len(kasCfg.Keyring) > 0: - panic("invalid kas cfg: please specify keyring or eccertid, not both") - case len(kasCfg.Keyring) == 0: - deprecatedOrDefault := func(kid, alg string) { - if kid == "" { - kid = srp.OTDF.CryptoProvider.FindKID(alg) - } - if kid == "" { - srp.Logger.Warn("no known key for alg", "algorithm", alg) - return - } - kasCfg.Keyring = append(kasCfg.Keyring, access.CurrentKeyFor{ - Algorithm: alg, - KID: kid, - }) - kasCfg.Keyring = append(kasCfg.Keyring, access.CurrentKeyFor{ - Algorithm: alg, - KID: kid, - Legacy: true, - }) - } - deprecatedOrDefault(kasCfg.ECCertID, security.AlgorithmECP256R1) - deprecatedOrDefault(kasCfg.RSACertID, security.AlgorithmRSA2048) - default: - kasCfg.Keyring = append(kasCfg.Keyring, inferLegacyKeys(kasCfg.Keyring)...) - } + srp.Logger.Debug("kas config", "config", kasCfg) p := &access.Provider{ - URI: *kasURI, - AttributeSvc: nil, - CryptoProvider: srp.OTDF.CryptoProvider, - SDK: srp.SDK, - Logger: srp.Logger, - KASConfig: kasCfg, - Tracer: srp.Tracer, + URI: *kasURI, + AttributeSvc: nil, + SDK: srp.SDK, + Logger: srp.Logger, + KASConfig: kasCfg, + Tracer: srp.Tracer, } - srp.Logger.Debug("kas config", "config", kasCfg) + // Create crypto provider + srp.Logger.Info("creating crypto provider") + if _, err := p.LoadStandardCryptoProvider(); err != nil { + panic(fmt.Errorf("failed to create crypto provider %w", err)) + } if err := srp.RegisterReadinessCheck("kas", p.IsReady); err != nil { srp.Logger.Error("failed to register kas readiness check", slog.String("error", err.Error())) @@ -87,19 +62,3 @@ func NewRegistration() *serviceregistry.Service[kasconnect.AccessServiceHandler] }, } } - -// If there exists *any* legacy keys, returns empty list. -// Otherwise, create a copy with legacy=true for all values -func inferLegacyKeys(keys []access.CurrentKeyFor) []access.CurrentKeyFor { - for _, k := range keys { - if k.Legacy { - return nil - } - } - l := make([]access.CurrentKeyFor, len(keys)) - for i, k := range keys { - l[i] = k - l[i].Legacy = true - } - return l -} diff --git a/service/kas/kidless_test.go b/service/kas/kidless_test.go deleted file mode 100644 index 02d027557..000000000 --- a/service/kas/kidless_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package kas - -import ( - "testing" - - "github.com/opentdf/platform/service/internal/security" - "github.com/opentdf/platform/service/kas/access" - "github.com/stretchr/testify/assert" -) - -func TestInferLegacyKeys_empty(t *testing.T) { - assert.Empty(t, inferLegacyKeys(nil)) -} - -func TestInferLegacyKeys_singles(t *testing.T) { - one := []access.CurrentKeyFor{ - { - Algorithm: security.AlgorithmRSA2048, - KID: "rsa", - }, - } - - oneLegacy := []access.CurrentKeyFor{ - { - Algorithm: security.AlgorithmRSA2048, - KID: "rsa", - Legacy: true, - }, - } - - assert.Equal(t, oneLegacy, inferLegacyKeys(one)) - assert.False(t, one[0].Legacy) - assert.True(t, oneLegacy[0].Legacy) -} - -func TestInferLegacyKeys_Mixed(t *testing.T) { - in := []access.CurrentKeyFor{ - { - Algorithm: security.AlgorithmRSA2048, - KID: "a", - }, - { - Algorithm: security.AlgorithmECP256R1, - KID: "b", - }, - { - Algorithm: security.AlgorithmECP256R1, - KID: "c", - Legacy: true, - }, - { - Algorithm: security.AlgorithmECP256R1, - KID: "d", - }, - } - - assert.Empty(t, inferLegacyKeys(in)) -} diff --git a/service/kas/recrypt/recrypt.go b/service/kas/recrypt/recrypt.go new file mode 100644 index 000000000..7a2dac88e --- /dev/null +++ b/service/kas/recrypt/recrypt.go @@ -0,0 +1,110 @@ +package recrypt + +import "github.com/opentdf/platform/lib/ocrypto" + +// A package with an interface for cryptography operations, +// such that they can be implemented either +// through an HSM +// or with go crypto primitives. + +// KeyIdentifier uniquely identifies a key within this crypto provider. +type KeyIdentifier string + +// Algorithm identifies a cryptographic algorithm. +type Algorithm string + +// KeyFormat identifies a key format. +type KeyFormat string + +// CryptoProviders implement KAS key unwrap functionality. +// These include: +// - Presenting stable public keys for wrapping client encryption keys. +// - Backward compatibility +// - Key agreement for nanoTDF and other EC based solutions +// +// This may be Closeable +type CryptoProvider interface { + // Return current preferred key identifier(s) for wrapping with the given algorithm. + CurrentKID(alg Algorithm) ([]KeyIdentifier, error) + + // Return one or more 'legacy' key identifiers that can be used when no KID is presented + // [optional] + LegacyKIDs(a Algorithm) ([]KeyIdentifier, error) + + // Returns a public key or cert for the given key ID and algorithm in the given format. + // Currently, only the JWK format supports multiple keys at once. + PublicKey(a Algorithm, k []KeyIdentifier, f KeyFormat) (string, error) + + // Perform an unwrap using the given alg/keyid pair on the given wrapped key bytes + Unwrap(k KeyIdentifier, ciphertext []byte) (UnwrappedKey, error) + + // Derive a shared key. Note: alg includes curve if present? + Derive(k KeyIdentifier, ephemeralPublicKeyBytes []byte) (UnwrappedKey, error) + + ParseAlgorithm(s string) (Algorithm, error) + ParseKeyFormat(s string) (KeyFormat, error) + ParseKeyIdentifier(s string) (KeyIdentifier, error) +} + +// Optional type to implement closeable crypto providers +// Probably not needed? But maybe we should do this anyway? +type Closeable interface { + Close() +} + +// Optional type for a crypto provider that can generate keys +type KeyGenerator interface { + // Generate a new key of the given algorithm, with an optional identifier + GenerateKey(a Algorithm, id KeyIdentifier) (KeyIdentifier, error) +} + +type NanoWrapResponse struct { + EntityWrappedKey []byte + SessionPublicKey string +} + +type UnwrappedKey interface { + Digest(msg []byte) ([]byte, error) + Wrap(within ocrypto.AsymEncryption) ([]byte, error) + NanoWrap(within []byte) (*NanoWrapResponse, error) + DecryptNanoPolicy(cipherText []byte, tagSize int) ([]byte, error) +} + +type KeyDetails struct { + // The key identifier + ID KeyIdentifier + // The algorithm of the key + Algorithm Algorithm + // If the key is 'current' for the given algorithm + Current bool + // Label value, if present on the key. + Label string + // Public PEM value, if available + Public string +} + +// Optional type for a crypto provider that can list its keys +type Lister interface { + // List all keys in the provider + List() ([]KeyDetails, error) +} + +const ( + // No algorithm specified in a request + AlgorithmUndefined Algorithm = "" + // Key agreement along P-256 + AlgorithmECP256R1 Algorithm = "ec:secp256r1" + // Key agreement along P-256 + AlgorithmECP384R1 Algorithm = "ec:secp384r1" + // Key agreement along P-256 + AlgorithmECP521R1 Algorithm = "ec:secp521r1" + // Used for encryption with RSA of the KAO + AlgorithmRSA2048 Algorithm = "rsa:2048" + + // Unspecified key format + KeyFormatUndefined KeyFormat = "" + // JavaScript Object Notation Web Key (JWK) + KeyFormatJWK KeyFormat = "jwk" + // Privacy Enhanced Mail (PEM) format + KeyFormatPEM KeyFormat = "pkcs8" +) diff --git a/service/kas/recrypt/standard.go b/service/kas/recrypt/standard.go new file mode 100644 index 000000000..5b106964c --- /dev/null +++ b/service/kas/recrypt/standard.go @@ -0,0 +1,650 @@ +package recrypt + +import ( + "crypto" + "crypto/ecdh" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "log/slog" + "math/big" + "time" + + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/sdk" +) + +type keyHolder struct { + Algorithm + KeyIdentifier + crypto.PrivateKey + certPEM []byte + publicPEM []byte +} + +// Implementation of the recrypt CryptoProvider interface using standard go crypto primitives. +type Standard struct { + keys map[KeyIdentifier]keyHolder + currentKIDsByAlg map[Algorithm][]KeyIdentifier + legacyKIDs map[Algorithm][]KeyIdentifier +} + +func NewStandard() *Standard { + return &Standard{ + keys: map[KeyIdentifier]keyHolder{}, + currentKIDsByAlg: map[Algorithm][]KeyIdentifier{}, + legacyKIDs: map[Algorithm][]KeyIdentifier{}, + } +} + +// StandardOption is a functional option type for configuring the Standard struct. +type StandardOption func(*Standard) error + +// WithKey adds the given key by type and id. +func WithKey(id KeyIdentifier, alg Algorithm, privateKey crypto.PrivateKey, certPEM []byte, isCurrent, checkForLegacy bool) StandardOption { + return func(s *Standard) error { + s.keys[id] = keyHolder{ + Algorithm: alg, + KeyIdentifier: id, + PrivateKey: privateKey, + certPEM: certPEM, + } + if isCurrent { + s.currentKIDsByAlg[alg] = append(s.currentKIDsByAlg[alg], id) + return nil + } + if checkForLegacy { + s.legacyKIDs[alg] = append(s.legacyKIDs[alg], id) + } + return nil + } +} + +func (s *Standard) ParseKeyIdentifier(k string) (KeyIdentifier, error) { + return KeyIdentifier(k), nil +} + +func (s *Standard) ParseAlgorithm(a string) (Algorithm, error) { + switch a { + case string(AlgorithmRSA2048): + return AlgorithmRSA2048, nil + case string(AlgorithmECP256R1): + return AlgorithmECP256R1, nil + case string(AlgorithmECP384R1): + return AlgorithmECP384R1, nil + case string(AlgorithmECP521R1): + return AlgorithmECP521R1, nil + case "": + return AlgorithmUndefined, nil + } + return AlgorithmUndefined, fmt.Errorf("invalid algorithm [%s]", a) +} + +func curveFor(a Algorithm) (elliptic.Curve, error) { + switch a { //nolint:exhaustive // We only support EC algorithms + case AlgorithmECP256R1: + return elliptic.P256(), nil + case AlgorithmECP384R1: + return elliptic.P384(), nil + case AlgorithmECP521R1: + return elliptic.P521(), nil + default: + return nil, fmt.Errorf("unsupported curve or algorithm [%s]", a) + } +} + +func (s *Standard) ParseKeyFormat(f string) (KeyFormat, error) { + switch f { + case string(KeyFormatJWK): + return KeyFormatJWK, nil + case string(KeyFormatPEM): + return KeyFormatPEM, nil + case "": + return KeyFormatUndefined, nil + } + return KeyFormatUndefined, fmt.Errorf("invalid key format [%s]", f) +} + +// NewStandardWithOptions creates a new Standard instance with the provided options. +func NewStandardWithOptions(opts ...StandardOption) (*Standard, error) { + s := &Standard{ + keys: map[KeyIdentifier]keyHolder{}, + currentKIDsByAlg: map[Algorithm][]KeyIdentifier{}, + legacyKIDs: map[Algorithm][]KeyIdentifier{}, + } + for _, opt := range opts { + if err := opt(s); err != nil { + return nil, err + } + } + return s, nil +} + +func (s *Standard) CurrentKID(alg Algorithm) ([]KeyIdentifier, error) { + kids, ok := s.currentKIDsByAlg[alg] + if !ok { + return nil, fmt.Errorf("no current KIDs for algorithm %s", alg) + } + return kids, nil +} + +func (s *Standard) LegacyKIDs(a Algorithm) ([]KeyIdentifier, error) { + kid, ok := s.legacyKIDs[a] + if !ok { + return nil, fmt.Errorf("no legacy KIDs for algorithm %s", a) + } + return kid, nil +} + +func (s *Standard) PublicKey(a Algorithm, k []KeyIdentifier, f KeyFormat) (string, error) { + if len(k) == 0 { + k = s.currentKIDsByAlg[a] + } + if len(k) > 1 && f != KeyFormatJWK { + return "", fmt.Errorf("only JWK format supports multiple keys at once") + } + + switch f { + case KeyFormatJWK: + jwks := jwk.NewSet() + for _, kid := range k { + holder, ok := s.keys[kid] + if !ok { + return "", fmt.Errorf("key not found [%s]", kid) + } + var j jwk.Key + var err error + switch secret := holder.PrivateKey.(type) { + case *ecdsa.PrivateKey: + j, err = jwk.FromRaw(secret.Public()) + case *rsa.PrivateKey: + j, err = jwk.FromRaw(secret.Public()) + default: + return "", fmt.Errorf("invalid algorithm [%s] or format [%s]", a, f) + } + if err != nil { + return "", fmt.Errorf("jwk.FromRaw failed for key [%s]: %w", kid, err) + } + if err := jwks.AddKey(j); err != nil { + return "", fmt.Errorf("jwk.AddKey failed for key [%s]: %w", kid, err) + } + } + asjson, err := json.Marshal(jwks) + if err != nil { + return "", fmt.Errorf("jwk.FromRaw: %w", err) + } + + return string(asjson), nil + case KeyFormatPEM: + fallthrough + case KeyFormatUndefined: + holder, ok := s.keys[k[0]] + if !ok { + return "", fmt.Errorf("key not found [%s]", k[0]) + } + if len(holder.publicPEM) > 0 { + return string(holder.publicPEM), nil + } + switch secret := holder.PrivateKey.(type) { + case *ecdh.PrivateKey: + publicKeyBytes, err := x509.MarshalPKIXPublicKey(secret.PublicKey()) + if err != nil { + return "", fmt.Errorf("x509.MarshalPKIXPublicKey failed: %w", err) + } + + holder.publicPEM = pem.EncodeToMemory( + &pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyBytes, + }, + ) + return string(holder.publicPEM), nil + + case *ecdsa.PrivateKey: + publicPEM, err := ocrypto.ECPublicKeyInPemFormat(secret.PublicKey) + if err != nil { + return "", fmt.Errorf("failed to get public key in PEM format: %w", err) + } + holder.publicPEM = []byte(publicPEM) + return publicPEM, nil + case *rsa.PrivateKey: + publicKeyBytes, err := x509.MarshalPKIXPublicKey(&secret.PublicKey) + if err != nil { + return "", fmt.Errorf("x509.MarshalPKIXPublicKey failed: %w", err) + } + + publicPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyBytes, + }, + ) + holder.publicPEM = publicPEM + return string(publicPEM), nil + } + return "", fmt.Errorf("invalid algorithm [%T] or format [%s]", holder.PrivateKey, f) + } + return "", fmt.Errorf("invalid format [%s]", f) +} + +func (s *Standard) Unwrap(k KeyIdentifier, ciphertext []byte) (UnwrappedKey, error) { + holder, ok := s.keys[k] + if !ok || holder.PrivateKey == nil { + return nil, fmt.Errorf("key not found [%s]", k) + } + secret, ok := holder.PrivateKey.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("key is not an RSA key [%s]", k) + } + + data, err := secret.Decrypt(nil, ciphertext, &rsa.OAEPOptions{Hash: crypto.SHA1}) + if err != nil { + return nil, fmt.Errorf("error decrypting data: %w", err) + } + + return aesUnwrappedKey{ + h: *s, + value: data, + }, nil +} + +func (s *Standard) Derive(k KeyIdentifier, compressedDataPublicKey []byte) (UnwrappedKey, error) { + privateKeyHolder, ok := s.keys[k] + if !ok { + return nil, fmt.Errorf("key not found") + } + + crv, err := curveFor(privateKeyHolder.Algorithm) + if err != nil { + return nil, err + } + + // server private bits + ecdhKey, err := ocrypto.ConvertToECDHPrivateKey(privateKeyHolder.PrivateKey) + if err != nil { + return nil, fmt.Errorf("derive: ConvertToECDHPrivateKey failed: %w", err) + } + + // client public bits + ephemeralECDSAPublicKey, err := ocrypto.UncompressECPubKey(crv, compressedDataPublicKey) + if err != nil { + return nil, err + } + + derBytes, err := x509.MarshalPKIXPublicKey(ephemeralECDSAPublicKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal ECDSA public key: %w", err) + } + pemBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: derBytes, + } + ephemeralECDSAPublicKeyPEM := pem.EncodeToMemory(pemBlock) + + ecdhPublicKey, err := ocrypto.ECPubKeyFromPem(ephemeralECDSAPublicKeyPEM) + if err != nil { + return nil, fmt.Errorf("ocrypto.ECPubKeyFromPem failed: %w", err) + } + + symmetricKey, err := ecdhKey.ECDH(ecdhPublicKey) + if err != nil { + return nil, fmt.Errorf("there was a problem deriving a shared ECDH key: %w", err) + } + + derivedKey, err := ocrypto.CalculateHKDF(versionSalt(), symmetricKey) + if err != nil { + return nil, fmt.Errorf("ocrypto.CalculateHKDF failed:%w", err) + } + + return aesUnwrappedKey{ + h: *s, + value: derivedKey, + }, nil +} + +func (s *Standard) Close() { + // Nothing to do +} + +func (s *Standard) GenerateKey(a Algorithm, id KeyIdentifier) (KeyIdentifier, error) { + switch a { + case AlgorithmRSA2048: + return s.generateRSAKeyPair(id) + case AlgorithmUndefined: + return "", fmt.Errorf("not implemented") + case AlgorithmECP256R1: + return s.generateECKeyPair(id, a, ocrypto.ECCModeSecp256r1) + case AlgorithmECP384R1: + return s.generateECKeyPair(id, a, ocrypto.ECCModeSecp384r1) + case AlgorithmECP521R1: + return s.generateECKeyPair(id, a, ocrypto.ECCModeSecp521r1) + default: + return "", fmt.Errorf("unsupported algorithm [%s]", a) + } +} + +func certTemplate() (*x509.Certificate, error) { + // generate a random serial number (a real cert authority would have some logic behind this) + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) //nolint:mnd // 128 bit uid is sufficiently unique + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, fmt.Errorf("failed to generate serial number [%w]", err) + } + + tmpl := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{CommonName: "kas"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * 30 * 365), //nolint:mnd // About a year to expire + BasicConstraintsValid: true, + } + return &tmpl, nil +} + +func (s *Standard) generateRSAKeyPair(id KeyIdentifier) (KeyIdentifier, error) { + keyRSA, err := rsa.GenerateKey(rand.Reader, 2048) //nolint:mnd // 256 byte key + if err != nil { + return "", fmt.Errorf("unable to generate rsa key [%w]", err) + } + + certTemplate, err := certTemplate() + if err != nil { + return "", fmt.Errorf("unable to create cert template [%w]", err) + } + + // self signed cert + pubBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, keyRSA.Public(), keyRSA) + if err != nil { + return "", fmt.Errorf("unable to create cert [%w]", err) + } + _, err = x509.ParseCertificate(pubBytes) + if err != nil { + return "", fmt.Errorf("unable to parse cert [%w]", err) + } + // Encode public key to PKCS#1 ASN.1 PEM. + pubPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "CERTIFICATE", + Bytes: pubBytes, + }, + ) + + s.keys[id] = keyHolder{ + Algorithm: AlgorithmRSA2048, + KeyIdentifier: id, + PrivateKey: keyRSA, + publicPEM: pubPEM, + } + return id, nil +} + +func (s *Standard) generateECKeyPair(id KeyIdentifier, a Algorithm, m ocrypto.ECCMode) (KeyIdentifier, error) { + ephemeralKeyPair, err := ocrypto.NewECKeyPair(m) + if err != nil { + return "", fmt.Errorf("ocrypto.NewECKeyPair failed: %w", err) + } + + pubKeyInPem, err := ephemeralKeyPair.PublicKeyInPemFormat() + if err != nil { + return "", fmt.Errorf("failed to get public key in PEM format: %w", err) + } + pubKeyBytes := []byte(pubKeyInPem) + + privKey, err := ocrypto.ConvertToECDHPrivateKey(ephemeralKeyPair.PrivateKey) + if err != nil { + return "", fmt.Errorf("failed to convert private key to ECDH: %w", err) + } + s.keys[id] = keyHolder{ + Algorithm: a, + KeyIdentifier: id, + PrivateKey: privKey, + publicPEM: pubKeyBytes, + } + return id, nil +} + +func (s *Standard) DestroyKey(id KeyIdentifier) error { + // removes id key from s.keys, and other fields + holder, ok := s.keys[id] + if !ok { + // already deleted + return nil + } + delete(s.keys, id) + // remove from currentKIDsByAlg + alg := holder.Algorithm + for i, kid := range s.currentKIDsByAlg[alg] { + if kid == id { + s.currentKIDsByAlg[alg] = append(s.currentKIDsByAlg[alg][:i], s.currentKIDsByAlg[alg][i+1:]...) + break + } + } + for i, kid := range s.legacyKIDs[alg] { + if kid == id { + s.legacyKIDs[alg] = append(s.legacyKIDs[alg][:i], s.legacyKIDs[alg][i+1:]...) + break + } + } + return nil +} + +func (s Standard) List() ([]KeyDetails, error) { + currentKeyIDs := make(map[KeyIdentifier]bool) + for _, kids := range s.currentKIDsByAlg { + for _, kid := range kids { + currentKeyIDs[kid] = true + } + } + + var keys []KeyDetails + for _, holder := range s.keys { + keys = append(keys, KeyDetails{ + ID: holder.KeyIdentifier, + Algorithm: holder.Algorithm, + Public: string(holder.publicPEM), + Current: currentKeyIDs[holder.KeyIdentifier], + }) + } + return keys, nil +} + +func RandomBytes(size int) ([]byte, error) { + data := make([]byte, size) + _, err := rand.Read(data) + if err != nil { + return nil, fmt.Errorf("rand.Read failed: %w", err) + } + + return data, nil +} + +func CalculateSHA256Hmac(secret, data []byte) []byte { + // Create a new HMAC by defining the hash type and the secret + hash := hmac.New(sha256.New, secret) + + // compute the HMAC + hash.Write(data) + dataHmac := hash.Sum(nil) + + return dataHmac +} + +func (s Standard) CreateKeyAccessObject(url string, kid KeyIdentifier, pk string, po sdk.PolicyObject) ([]byte, *sdk.KeyAccess, error) { + dek, err := RandomBytes(32) //nolint:mnd // 256 bits, standard for AES keys + if err != nil { + slog.Error("ocrypto.RandomBytes failed", "err", err) + return nil, nil, fmt.Errorf("ocrypto.RandomBytes failed:%w", err) + } + + pos, err := json.Marshal(po) + if err != nil { + slog.Error("json.Marshal failed", "err", err) + return nil, nil, fmt.Errorf("json.Marshal failed:%w", err) + } + pob := make([]byte, base64.StdEncoding.EncodedLen(len(pos))) + base64.StdEncoding.Encode(pob, pos) + + policyBinding := sdk.PolicyBinding{ + Alg: "HS256", + // FIXME this is encoded AGAIN into base64 in current code. Choose one or the other, or both? + Hash: hex.EncodeToString(CalculateSHA256Hmac(dek, pob)), + } + + a, err := ocrypto.NewAsymEncryption(pk) + if err != nil { + slog.Error("ocrypto.NewAsymEncryption failed", "err", err) + return nil, nil, fmt.Errorf("ocrypto.NewAsymEncryption failed:%w", err) + } + wk, err := a.Encrypt(dek) + if err != nil { + slog.Error("ocrypto.AsymEncryption.encrypt failed", "err", err) + return nil, nil, fmt.Errorf("ocrypto.AsymEncryption.encrypt failed:%w", err) + } + return dek, &sdk.KeyAccess{ + KeyType: "wrapped", + KasURL: url, + KID: string(kid), + Protocol: "kas", + PolicyBinding: policyBinding, + WrappedKey: string(ocrypto.Base64Encode(wk)), + }, nil +} + +func versionSalt() []byte { + digest := sha256.New() + digest.Write([]byte("L1L")) + return digest.Sum(nil) +} + +func NewECKeyPair() (*ecdsa.PrivateKey, error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("ec.GenerateKey failed: %w", err) + } + return privateKey, nil +} + +type aesUnwrappedKey struct { + h Standard + value []byte +} + +func NewAESUnwrappedKey(value []byte) UnwrappedKey { + return aesUnwrappedKey{value: value} +} + +func (k aesUnwrappedKey) Digest(msg []byte) ([]byte, error) { + mac := hmac.New(sha256.New, k.value) + _, err := mac.Write(msg) + if err != nil { + return nil, fmt.Errorf("user input invalid policy hmac") + } + return mac.Sum(nil), nil +} + +func (k aesUnwrappedKey) Wrap(within ocrypto.AsymEncryption) ([]byte, error) { + return within.Encrypt(k.value) +} + +func (k aesUnwrappedKey) generateEphemeralKasKeys() (any, []byte, error) { + ephemeralKeyPair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1) + if err != nil { + return nil, nil, fmt.Errorf("ocrypto.NewECKeyPair failed: %w", err) + } + + pubKeyInPem, err := ephemeralKeyPair.PublicKeyInPemFormat() + if err != nil { + return nil, nil, fmt.Errorf("failed to get public key in PEM format: %w", err) + } + pubKeyBytes := []byte(pubKeyInPem) + + privKey, err := ocrypto.ConvertToECDHPrivateKey(ephemeralKeyPair.PrivateKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to convert provate key to ECDH: %w", err) + } + return privKey, pubKeyBytes, nil +} + +func (k aesUnwrappedKey) generateNanoTDFSessionKey(privateKey any, ephemeralPublicKeyPEM []byte) ([]byte, error) { + ecdhKey, err := ocrypto.ConvertToECDHPrivateKey(privateKey) + if err != nil { + return nil, fmt.Errorf("GenerateNanoTDFSessionKey failed to ConvertToECDHPrivateKey: %w", err) + } + ephemeralECDHPublicKey, err := ocrypto.ECPubKeyFromPem(ephemeralPublicKeyPEM) + if err != nil { + return nil, fmt.Errorf("GenerateNanoTDFSessionKey failed to ocrypto.ECPubKeyFromPem: %w", err) + } + // shared secret + sessionKey, err := ecdhKey.ECDH(ephemeralECDHPublicKey) + if err != nil { + return nil, fmt.Errorf("GenerateNanoTDFSessionKey failed to ecdhKey.ECDH: %w", err) + } + + salt := versionSalt() + derivedKey, err := ocrypto.CalculateHKDF(salt, sessionKey) + if err != nil { + return nil, fmt.Errorf("ocrypto.CalculateHKDF failed:%w", err) + } + return derivedKey, nil +} + +func wrapKeyAES(sessionKey, dek []byte) ([]byte, error) { + gcm, err := ocrypto.NewAESGcm(sessionKey) + if err != nil { + return nil, fmt.Errorf("crypto.NewAESGcm:%w", err) + } + + cipherText, err := gcm.Encrypt(dek) + if err != nil { + return nil, fmt.Errorf("crypto.AsymEncryption.encrypt:%w", err) + } + + return cipherText, nil +} + +func (k aesUnwrappedKey) NanoWrap(within []byte) (*NanoWrapResponse, error) { + privateKeyHandle, publicKeyHandle, err := k.generateEphemeralKasKeys() + if err != nil { + return nil, fmt.Errorf("failed to generate keypair: %w", err) + } + + sessionKey, err := k.generateNanoTDFSessionKey(privateKeyHandle, within) + if err != nil { + return nil, fmt.Errorf("failed to generate session key: %w", err) + } + + cipherText, err := wrapKeyAES(sessionKey, k.value) + if err != nil { + return nil, fmt.Errorf("failed to encrypt key: %w", err) + } + + return &NanoWrapResponse{ + EntityWrappedKey: cipherText, + SessionPublicKey: string(publicKeyHandle), + }, nil +} + +func (k aesUnwrappedKey) DecryptNanoPolicy(cipherText []byte, tagSize int) ([]byte, error) { + gcm, err := ocrypto.NewAESGcm(k.value) + if err != nil { + return nil, fmt.Errorf("crypto.NewAESGcm:%w", err) + } + + const ( + kIvLen = 12 + ) + iv := make([]byte, kIvLen) + policyData, err := gcm.DecryptWithIVAndTagSize(iv, cipherText, tagSize) + return policyData, err +} diff --git a/service/kas/recrypt/standard_test.go b/service/kas/recrypt/standard_test.go new file mode 100644 index 000000000..ccb2bab21 --- /dev/null +++ b/service/kas/recrypt/standard_test.go @@ -0,0 +1,122 @@ +package recrypt + +import ( + "log/slog" + "testing" + + "github.com/opentdf/platform/lib/ocrypto" + "github.com/opentdf/platform/sdk" + "github.com/stretchr/testify/suite" +) + +type StandardTestSuite struct { + suite.Suite + *Standard +} + +func (s *StandardTestSuite) SetupSuite() { + s.Standard = NewStandard() +} + +func TestTDF(t *testing.T) { + suite.Run(t, new(StandardTestSuite)) +} + +func (s *StandardTestSuite) TestListingKeys() { + _, err := s.List() + s.Require().NoError(err) +} + +func (s *StandardTestSuite) TestRSA() { + key, err := s.GenerateKey("rsa:2048", "test-key") + defer func() { s.Require().NoError(s.DestroyKey("test-key")) }() + s.Require().NoError(err) + s.Equal(KeyIdentifier("test-key"), key) + + keys, err := s.List() + s.Require().NoError(err) + s.NotEmpty(keys) + // uses testify.Suite to assert that keys contains a KeyDetails with ID "test-key" + var kaskd *KeyDetails + s.Condition(func() bool { + for _, k := range keys { + slog.Info("checking key", "key", k) + if k.ID == "test-key" { + kaskd = &k + return true + } + } + return false + }, "Key %s not found in list %v", "test-key", keys) + + s.Require().NotNil(kaskd) + + policy := sdk.PolicyObject{} + dek, kao, err := s.CreateKeyAccessObject("http://kas.us", kaskd.ID, kaskd.Public, policy) + s.Require().NoError(err) + s.Len(dek, 32) + s.Equal("http://kas.us", kao.KasURL) + + // Use the key to encrypt a value, then decrypt it. + // use SDK to produce wrapped key? + wk, err := ocrypto.Base64Decode([]byte(kao.WrappedKey)) + s.Require().NoError(err) + + decrypted, err := s.Unwrap(kaskd.ID, wk) + s.Require().NoError(err) + auk, ok := decrypted.(aesUnwrappedKey) + s.Require().True(ok) + s.Equal(dek, auk.value) +} + +func (s *StandardTestSuite) TestEC() { + key, err := s.GenerateKey("ec:secp256r1", "ec-key") + defer func() { s.Require().NoError(s.DestroyKey("ec-key")) }() + s.Require().NoError(err) + s.Equal(KeyIdentifier("ec-key"), key) + + keys, err := s.List() + s.Require().NoError(err) + s.NotEmpty(keys) + // uses testify.Suite to assert that keys contains a KeyDetails with ID "test-key" + var kaskd *KeyDetails + s.Condition(func() bool { + for _, k := range keys { + slog.Info("checking key", "key", k) + if k.ID == KeyIdentifier("ec-key") { + kaskd = &k + slog.Info("found EC key pair", "kid", "ec-key", "public", k.Public) + return true + } + } + return false + }, "Key %s not found in list %v", "test-key", keys) + + s.Require().NotNil(kaskd) + + kasPublicKey, err := ocrypto.ECPubKeyFromPem([]byte(kaskd.Public)) + s.Require().NoError(err) + + sk, err := NewECKeyPair() + s.Require().NoError(err) + + skdh, err := sk.ECDH() + s.Require().NoError(err) + ecdhSecret, err := ocrypto.ComputeECDHKeyFromECDHKeys(kasPublicKey, skdh) + s.Require().NoError(err) + + aesKey, err := ocrypto.CalculateHKDF(versionSalt(), ecdhSecret) + s.Require().NoError(err) + + // We can create an AES GCM instance with the derived key + _, err = ocrypto.NewAESGcm(aesKey) + s.Require().NoError(err) + + compressedPubKey, err := ocrypto.CompressedECPublicKey(ocrypto.ECCModeSecp256r1, sk.PublicKey) + s.Require().NoError(err) + actual, err := s.Derive(kaskd.ID, compressedPubKey) + s.Require().NoError(err) + auk, ok := actual.(aesUnwrappedKey) + s.Require().True(ok) + s.Equal(aesKey, auk.value) +} diff --git a/service/pkg/server/testdata/all-no-config.yaml b/service/pkg/server/testdata/all-no-config.yaml index ff4e1f299..2c8da1a3c 100644 --- a/service/pkg/server/testdata/all-no-config.yaml +++ b/service/pkg/server/testdata/all-no-config.yaml @@ -5,18 +5,6 @@ logger: type: text output: stdout services: - kas: - keyring: - - kid: e1 - alg: ec:secp256r1 - - kid: e1 - alg: ec:secp256r1 - legacy: true - - kid: r1 - alg: rsa:2048 - - kid: r1 - alg: rsa:2048 - legacy: true entityresolution: log_level: info url: http://localhost:8888/auth diff --git a/test/tdf-roundtrips.bats b/test/tdf-roundtrips.bats index d9f9ef12b..533737c46 100755 --- a/test/tdf-roundtrips.bats +++ b/test/tdf-roundtrips.bats @@ -86,7 +86,7 @@ wait_for_green echo "[INFO] validating default key is r2" - [ $(grpcurl "localhost:8080" "kas.AccessService/PublicKey" | jq -e -r .kid) = r2 ] + [ "$(grpcurl "localhost:8080" "kas.AccessService/PublicKey" | jq -e -r .kid)" = r2 ] echo "[INFO] decrypting after key rotation" go run ./examples decrypt sensitive-with-no-kid.txt.tdf | grep "Hello Legacy" @@ -111,7 +111,7 @@ wait_for_green echo "[INFO] validating default key is r1" - [ $(grpcurl "localhost:8080" "kas.AccessService/PublicKey" | jq -e -r .kid) = r1 ] + [ "$(grpcurl "localhost:8080" "kas.AccessService/PublicKey" | jq -e -r .kid)" = r1 ] echo "[INFO] decrypting after key rotation" go run ./examples decrypt sensitive-with-no-kid.txt.tdf | grep "Hello Legacy" @@ -122,7 +122,8 @@ wait_for_green() { limit=5 for i in $(seq 1 $limit); do - if [ $(grpcurl "localhost:8080" "grpc.health.v1.Health.Check" | jq -e -r .status) = SERVING ]; then + grpcurl "localhost:8080" "grpc.health.v1.Health.Check" + if [ "$(grpcurl "localhost:8080" "grpc.health.v1.Health.Check" | jq -e -r .status)" = SERVING ]; then return 0 fi sleep 4 @@ -209,14 +210,24 @@ services: keyring: - kid: ${ec_current_key} alg: ec:secp256r1 + active: true + private: kas-${ec_current_key}-private.pem + cert: kas-${ec_current_key}-cert.pem - kid: ${ec_legacy_key} alg: ec:secp256r1 legacy: true + private: kas-${ec_legacy_key}-private.pem + cert: kas-${ec_legacy_key}-cert.pem - kid: ${rsa_current_key} + private: kas-${rsa_current_key}-private.pem + cert: kas-${rsa_current_key}-cert.pem alg: rsa:2048 + active: true - kid: ${rsa_legacy_key} alg: rsa:2048 legacy: true + private: kas-${rsa_legacy_key}-private.pem + cert: kas-${rsa_legacy_key}-cert.pem policy: enabled: true authorization: @@ -244,26 +255,6 @@ server: issuer: http://localhost:8888/auth/realms/opentdf cors: enabled: false - cryptoProvider: - type: standard - standard: - keys: - - kid: r2 - alg: rsa:2048 - private: kas-r2-private.pem - cert: kas-r2-cert.pem - - kid: e2 - alg: ec:secp256r1 - private: kas-e2-private.pem - cert: kas-e2-cert.pem - - kid: r1 - alg: rsa:2048 - private: kas-private.pem - cert: kas-cert.pem - - kid: e1 - alg: ec:secp256r1 - private: kas-ec-private.pem - cert: kas-ec-cert.pem port: 8080 opa: embedded: true @@ -274,8 +265,10 @@ setup_file() { if [ -f opentdf.yaml ]; then cp opentdf.yaml opentdf-test-backup.yaml.bak fi - openssl req -x509 -nodes -newkey RSA:2048 -subj "/CN=kas" -keyout kas-r2-private.pem -out kas-r2-cert.pem -days 365 + openssl req -x509 -nodes -newkey RSA:2048 -subj "/CN=kas" -keyout kas-r1-private.pem -out kas-r1-cert.pem -days 365 + openssl req -x509 -nodes -newkey RSA:2048 -subj "/CN=kas" -keyout kas-r1-private.pem -out kas-r1-cert.pem -days 365 openssl ecparam -name prime256v1 >ecparams.tmp + openssl req -x509 -nodes -newkey ec:ecparams.tmp -subj "/CN=kas" -keyout kas-e1-private.pem -out kas-e1-cert.pem -days 365 openssl req -x509 -nodes -newkey ec:ecparams.tmp -subj "/CN=kas" -keyout kas-e2-private.pem -out kas-e2-cert.pem -days 365 } From 177c88405962ab3122a3283e4bd3b20ffe0a9c1d Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Tue, 7 Jan 2025 16:53:24 -0500 Subject: [PATCH 2/9] WIP support deprecated configuration --- service/internal/security/config.go | 143 ++++++++++++++++++ service/internal/security/config_test.go | 107 +++++++++++++ service/internal/server/server.go | 17 ++- service/pkg/server/services.go | 7 +- .../pkg/serviceregistry/serviceregistry.go | 4 +- 5 files changed, 269 insertions(+), 9 deletions(-) create mode 100644 service/internal/security/config.go create mode 100644 service/internal/security/config_test.go diff --git a/service/internal/security/config.go b/service/internal/security/config.go new file mode 100644 index 000000000..d592a40b4 --- /dev/null +++ b/service/internal/security/config.go @@ -0,0 +1,143 @@ +package security + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +// Copied from service/kas/access to avoid dep loop. To be removed. +type CurrentKeyFor struct { + Algorithm string `mapstructure:"alg"` + KID string `mapstructure:"kid"` + Private string `mapstructure:"private"` + Certificate string `mapstructure:"cert"` + Active bool `mapstructure:"active"` + Legacy bool `mapstructure:"legacy"` +} + +// locate finds the index of the key in the Keyring slice. +func (k *KASConfigDupe) locate(kid string) (int, bool) { + for i, key := range k.Keyring { + if key.KID == kid { + return i, true + } + } + return -1, false +} + +// For entries in keyring that appear with the same value for their KID field, +// consolidate them into a single entry. If one of the copies has 'Legacy' set, let the consolidated entry have 'Legacy' set. +// If one of the entries does not have `Legacy` set, set the value of `Active`. +func (k *KASConfigDupe) consolidate() { + seen := make(map[string]int) + for i, key := range k.Keyring { + if j, ok := seen[key.KID]; ok { + if key.Legacy { + k.Keyring[j].Legacy = true + } else { + k.Keyring[j].Active = key.Active + } + k.Keyring = append(k.Keyring[:i], k.Keyring[i+1:]...) + i-- + } else { + seen[key.KID] = i + } + } +} + +// Deprecated +type KeyPairInfo struct { + // Valid algorithm. May be able to be derived from Private but it is better to just say it. + Algorithm string `mapstructure:"alg" json:"alg"` + // Key identifier. Should be short + KID string `mapstructure:"kid" json:"kid"` + // Implementation specific locator for private key; + // for 'standard' crypto service this is the path to a PEM file + Private string `mapstructure:"private" json:"private"` + // Optional locator for the corresponding certificate. + // If not found, only public key (derivable from Private) is available. + Certificate string `mapstructure:"cert" json:"cert"` + // Optional enumeration of intended usages of keypair + Usage string `mapstructure:"usage" json:"usage"` + // Optional long form description of key pair including purpose and life cycle information + Purpose string `mapstructure:"purpose" json:"purpose"` +} + +// Deprecated +type StandardKeyInfo struct { + PrivateKeyPath string `mapstructure:"private_key_path" json:"private_key_path"` + PublicKeyPath string `mapstructure:"public_key_path" json:"public_key_path"` +} + +// Deprecated +type CryptoConfig2024 struct { + Keys []KeyPairInfo `mapstructure:"keys" json:"keys"` + // Deprecated + RSAKeys map[string]StandardKeyInfo `mapstructure:"rsa,omitempty" json:"rsa,omitempty"` + // Deprecated + ECKeys map[string]StandardKeyInfo `mapstructure:"ec,omitempty" json:"ec,omitempty"` +} + +type KASConfigDupe struct { + // Which keys are currently the default. + Keyring []CurrentKeyFor `mapstructure:"keyring" json:"keyring"` + // Deprecated + ECCertID string `mapstructure:"eccertid" json:"eccertid"` + // Deprecated + RSACertID string `mapstructure:"rsacertid" json:"rsacertid"` +} + +func (c CryptoConfig2024) MarshalTo(within map[string]any) error { + var kasCfg KASConfigDupe + if err := mapstructure.Decode(within, &kasCfg); err != nil { + return fmt.Errorf("invalid kas cfg [%v] %w", within, err) + } + kasCfg.consolidate() + for kid, stdKeyInfo := range c.RSAKeys { + if i, ok := kasCfg.locate(kid); ok { + kasCfg.Keyring[i].Private = stdKeyInfo.PrivateKeyPath + kasCfg.Keyring[i].Certificate = stdKeyInfo.PublicKeyPath + continue + } + k := CurrentKeyFor{ + Algorithm: "rsa:2048", + KID: kid, + Private: stdKeyInfo.PrivateKeyPath, + Certificate: stdKeyInfo.PublicKeyPath, + Active: true, + Legacy: true, + } + kasCfg.Keyring = append(kasCfg.Keyring, k) + } + for kid, stdKeyInfo := range c.ECKeys { + if i, ok := kasCfg.locate(kid); ok { + kasCfg.Keyring[i].Private = stdKeyInfo.PrivateKeyPath + kasCfg.Keyring[i].Certificate = stdKeyInfo.PublicKeyPath + continue + } + k := CurrentKeyFor{ + Algorithm: "ec:secp256r1", + KID: kid, + Private: stdKeyInfo.PrivateKeyPath, + Certificate: stdKeyInfo.PublicKeyPath, + Active: true, + Legacy: true, + } + kasCfg.Keyring = append(kasCfg.Keyring, k) + } + for _, k := range c.Keys { + if i, ok := kasCfg.locate(k.KID); ok { + kasCfg.Keyring[i].Private = k.Private + kasCfg.Keyring[i].Certificate = k.Certificate + continue + } + kasCfg.Keyring = append(kasCfg.Keyring, CurrentKeyFor{ + Algorithm: k.Algorithm, + KID: k.KID, + Private: k.Private, + Certificate: k.Certificate, + }) + } + return nil +} diff --git a/service/internal/security/config_test.go b/service/internal/security/config_test.go new file mode 100644 index 000000000..5f01c83c9 --- /dev/null +++ b/service/internal/security/config_test.go @@ -0,0 +1,107 @@ +package security + +import ( + "testing" + + "github.com/mitchellh/mapstructure" + "github.com/stretchr/testify/assert" +) + +func TestMarshalTo(t *testing.T) { + tests := []struct { + name string + config CryptoConfig2024 + input map[string]any + expected KASConfigDupe + wantErr bool + }{ + { + name: "older input (pre-2024, no legacy)", + config: CryptoConfig2024{ + RSAKeys: map[string]StandardKeyInfo{ + "rsa1": {PrivateKeyPath: "rsa1_private.pem", PublicKeyPath: "rsa1_public.pem"}, + }, + ECKeys: map[string]StandardKeyInfo{ + "ec1": {PrivateKeyPath: "ec1_private.pem", PublicKeyPath: "ec1_public.pem"}, + }, + }, + input: map[string]any{ + "eccertid": "ec1", + "rsacertid": "rsa1", + }, + expected: KASConfigDupe{ + Keyring: []CurrentKeyFor{ + {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem", Active: true, Legacy: true}, + {Algorithm: "ec:secp256r1", KID: "ec1", Private: "ec1_private.pem", Certificate: "ec1_public.pem", Active: true, Legacy: true}, + }, + }, + wantErr: false, + }, + { + name: "older input (pre-2024, supports legacy)", + config: CryptoConfig2024{ + RSAKeys: map[string]StandardKeyInfo{ + "rsa1": {PrivateKeyPath: "rsa1_private.pem", PublicKeyPath: "rsa1_public.pem"}, + }, + ECKeys: map[string]StandardKeyInfo{ + "ec1": {PrivateKeyPath: "ec1_private.pem", PublicKeyPath: "ec1_public.pem"}, + }, + }, + input: map[string]any{}, + expected: KASConfigDupe{ + Keyring: []CurrentKeyFor{ + {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem", Active: true, Legacy: true}, + {Algorithm: "ec:secp256r1", KID: "ec1", Private: "ec1_private.pem", Certificate: "ec1_public.pem", Active: true, Legacy: true}, + }, + }, + wantErr: false, + }, + { + name: "older input (2024)", + config: CryptoConfig2024{ + Keys: []KeyPairInfo{ + {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem"}, + {Algorithm: "ec:secp256r1", KID: "ec1", Private: "ec1_private.pem", Certificate: "ec1_public.pem"}, + }, + }, + input: map[string]any{ + "keyring": []map[string]any{ + {"alg": "rsa:2048", "kid": "rsa1", "private": "rsa1_private.pem", "cert": "rsa1_public.pem", "active": true, "legacy": true}, + {"alg": "ec:secp256r1", "kid": "ec1", "private": "ec1_private.pem", "cert": "ec1_public.pem", "active": true, "legacy": true}, + }, + }, + expected: KASConfigDupe{ + Keyring: []CurrentKeyFor{ + {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem", Active: true, Legacy: true}, + {Algorithm: "ec:secp256r1", KID: "ec1", Private: "ec1_private.pem", Certificate: "ec1_public.pem", Active: true, Legacy: true}, + }, + }, + wantErr: false, + }, + { + name: "Invalid input", + config: CryptoConfig2024{ + RSAKeys: map[string]StandardKeyInfo{}, + ECKeys: map[string]StandardKeyInfo{}, + Keys: []KeyPairInfo{}, + }, + input: map[string]any{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.MarshalTo(tt.input) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + var result KASConfigDupe + err = mapstructure.Decode(tt.input, &result) + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} diff --git a/service/internal/server/server.go b/service/internal/server/server.go index adf44de0d..3bf07516b 100644 --- a/service/internal/server/server.go +++ b/service/internal/server/server.go @@ -21,6 +21,7 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" sdkAudit "github.com/opentdf/platform/sdk/audit" "github.com/opentdf/platform/service/internal/auth" + "github.com/opentdf/platform/service/internal/security" "github.com/opentdf/platform/service/internal/server/memhttp" "github.com/opentdf/platform/service/logger" "github.com/opentdf/platform/service/logger/audit" @@ -45,11 +46,13 @@ func (e Error) Error() string { // Configurations for the server type Config struct { - Auth auth.Config `mapstructure:"auth" json:"auth"` - GRPC GRPCConfig `mapstructure:"grpc" json:"grpc"` - TLS TLSConfig `mapstructure:"tls" json:"tls"` - CORS CORSConfig `mapstructure:"cors" json:"cors"` - WellKnownConfigRegister func(namespace string, config any) error `mapstructure:"-" json:"-"` + Auth auth.Config `mapstructure:"auth" json:"auth"` + GRPC GRPCConfig `mapstructure:"grpc" json:"grpc"` + // Deprecated: Specify all crypto details in the `services.kas.keyring` struct + security.CryptoConfig2024 `mapstructure:"cryptoProvider" json:"cryptoProvider"` + TLS TLSConfig `mapstructure:"tls" json:"tls"` + CORS CORSConfig `mapstructure:"cors" json:"cors"` + WellKnownConfigRegister func(namespace string, config any) error `mapstructure:"-" json:"-"` // Port to listen on Port int `mapstructure:"port" json:"port" default:"8080"` Host string `mapstructure:"host,omitempty" json:"host"` @@ -108,7 +111,8 @@ type ConnectRPC struct { } type OpenTDFServer struct { - AuthN *auth.Authentication + AuthN *auth.Authentication + *Config GRPCGatewayMux *runtime.ServeMux HTTPServer *http.Server ConnectRPCInProcess *inProcessServer @@ -196,6 +200,7 @@ func NewOpenTDFServer(config Config, logger *logger.Logger) (*OpenTDFServer, err o := OpenTDFServer{ AuthN: authN, + Config: &config, GRPCGatewayMux: grpcGatewayMux, HTTPServer: httpServer, ConnectRPC: connectRPC, diff --git a/service/pkg/server/services.go b/service/pkg/server/services.go index bd7df9994..c1e0bbf77 100644 --- a/service/pkg/server/services.go +++ b/service/pkg/server/services.go @@ -167,8 +167,13 @@ func startServices(ctx context.Context, cfg config.Config, otdf *server.OpenTDFS } } + svcConfig := cfg.Services[ns] + if ns == "kas" { + + } + err = svc.Start(ctx, serviceregistry.RegistrationParams{ - Config: cfg.Services[svc.GetNamespace()], + Config: svcConfig, Logger: svcLogger, DBClient: svcDBClient, SDK: client, diff --git a/service/pkg/serviceregistry/serviceregistry.go b/service/pkg/serviceregistry/serviceregistry.go index 7e2ffbff6..95d0db492 100644 --- a/service/pkg/serviceregistry/serviceregistry.go +++ b/service/pkg/serviceregistry/serviceregistry.go @@ -37,9 +37,9 @@ type RegistrationParams struct { // gRPC Inter Process Communication (IPC) between services. This ensures the services are // communicating with each other by contract as well as supporting the various deployment models // that OpenTDF supports. - SDK *sdk.SDK + *sdk.SDK // Logger is the logger that can be used to log messages. This logger is scoped to the service - Logger *logger.Logger + *logger.Logger trace.Tracer ////// The following functions are optional and intended to be called by the service ////// From 6809c53932f9314d4b5873104680bf76e9d5a844 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Thu, 9 Jan 2025 17:15:52 -0500 Subject: [PATCH 3/9] fixes --- service/internal/security/config.go | 27 ++++++++++++++------ service/internal/security/config_test.go | 32 +++++++++++++++--------- service/internal/server/server.go | 8 +++--- service/pkg/server/services.go | 8 +++++- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/service/internal/security/config.go b/service/internal/security/config.go index d592a40b4..e2053a1f2 100644 --- a/service/internal/security/config.go +++ b/service/internal/security/config.go @@ -31,19 +31,20 @@ func (k *KASConfigDupe) locate(kid string) (int, bool) { // If one of the entries does not have `Legacy` set, set the value of `Active`. func (k *KASConfigDupe) consolidate() { seen := make(map[string]int) - for i, key := range k.Keyring { + consolidated := make([]CurrentKeyFor, 0, len(k.Keyring)/2) //nolint:mnd // There are at most two of each of the new kind of keys. + for _, key := range k.Keyring { if j, ok := seen[key.KID]; ok { if key.Legacy { - k.Keyring[j].Legacy = true + consolidated[j].Legacy = true } else { - k.Keyring[j].Active = key.Active + consolidated[j].Active = key.Active } - k.Keyring = append(k.Keyring[:i], k.Keyring[i+1:]...) - i-- } else { - seen[key.KID] = i + seen[key.KID] = len(consolidated) + consolidated = append(consolidated, key) } } + k.Keyring = consolidated } // Deprecated @@ -93,6 +94,9 @@ func (c CryptoConfig2024) MarshalTo(within map[string]any) error { if err := mapstructure.Decode(within, &kasCfg); err != nil { return fmt.Errorf("invalid kas cfg [%v] %w", within, err) } + if len(kasCfg.Keyring) > 0 && (kasCfg.ECCertID != "" || kasCfg.RSACertID != "") { + return fmt.Errorf("invalid kas cfg [%v]", within) + } kasCfg.consolidate() for kid, stdKeyInfo := range c.RSAKeys { if i, ok := kasCfg.locate(kid); ok { @@ -105,7 +109,7 @@ func (c CryptoConfig2024) MarshalTo(within map[string]any) error { KID: kid, Private: stdKeyInfo.PrivateKeyPath, Certificate: stdKeyInfo.PublicKeyPath, - Active: true, + Active: kasCfg.RSACertID == "" || kasCfg.RSACertID == kid, Legacy: true, } kasCfg.Keyring = append(kasCfg.Keyring, k) @@ -121,7 +125,7 @@ func (c CryptoConfig2024) MarshalTo(within map[string]any) error { KID: kid, Private: stdKeyInfo.PrivateKeyPath, Certificate: stdKeyInfo.PublicKeyPath, - Active: true, + Active: kasCfg.ECCertID == "" || kasCfg.ECCertID == kid, Legacy: true, } kasCfg.Keyring = append(kasCfg.Keyring, k) @@ -139,5 +143,12 @@ func (c CryptoConfig2024) MarshalTo(within map[string]any) error { Certificate: k.Certificate, }) } + kasCfg.ECCertID = "" + kasCfg.RSACertID = "" + delete(within, "rsacertid") + delete(within, "eccertid") + if err := mapstructure.Decode(kasCfg, &within); err != nil { + return fmt.Errorf("failed serializing kas cfg [%v] %w", kasCfg, err) + } return nil } diff --git a/service/internal/security/config_test.go b/service/internal/security/config_test.go index 5f01c83c9..6f7d35dfb 100644 --- a/service/internal/security/config_test.go +++ b/service/internal/security/config_test.go @@ -5,6 +5,7 @@ import ( "github.com/mitchellh/mapstructure" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMarshalTo(t *testing.T) { @@ -16,13 +17,14 @@ func TestMarshalTo(t *testing.T) { wantErr bool }{ { - name: "older input (pre-2024, no legacy)", + name: "upgrade2023CertID", config: CryptoConfig2024{ RSAKeys: map[string]StandardKeyInfo{ "rsa1": {PrivateKeyPath: "rsa1_private.pem", PublicKeyPath: "rsa1_public.pem"}, }, ECKeys: map[string]StandardKeyInfo{ "ec1": {PrivateKeyPath: "ec1_private.pem", PublicKeyPath: "ec1_public.pem"}, + "ec2": {PrivateKeyPath: "ec2_private.pem", PublicKeyPath: "ec2_public.pem"}, }, }, input: map[string]any{ @@ -33,12 +35,13 @@ func TestMarshalTo(t *testing.T) { Keyring: []CurrentKeyFor{ {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem", Active: true, Legacy: true}, {Algorithm: "ec:secp256r1", KID: "ec1", Private: "ec1_private.pem", Certificate: "ec1_public.pem", Active: true, Legacy: true}, + {Algorithm: "ec:secp256r1", KID: "ec2", Private: "ec2_private.pem", Certificate: "ec2_public.pem", Active: false, Legacy: true}, }, }, wantErr: false, }, { - name: "older input (pre-2024, supports legacy)", + name: "upgrade2023NoCertIDs", config: CryptoConfig2024{ RSAKeys: map[string]StandardKeyInfo{ "rsa1": {PrivateKeyPath: "rsa1_private.pem", PublicKeyPath: "rsa1_public.pem"}, @@ -57,7 +60,7 @@ func TestMarshalTo(t *testing.T) { wantErr: false, }, { - name: "older input (2024)", + name: "upgrade2024H2", config: CryptoConfig2024{ Keys: []KeyPairInfo{ {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem"}, @@ -79,13 +82,18 @@ func TestMarshalTo(t *testing.T) { wantErr: false, }, { - name: "Invalid input", + name: "invalid input confusing", config: CryptoConfig2024{ RSAKeys: map[string]StandardKeyInfo{}, ECKeys: map[string]StandardKeyInfo{}, Keys: []KeyPairInfo{}, }, - input: map[string]any{}, + input: map[string]any{ + "keyring": []map[string]any{ + {"alg": "rsa:2048", "kid": "rsa1", "private": "rsa1_private.pem", "cert": "rsa1_public.pem", "active": true, "legacy": true}, + }, + "rsacertid": "rsa1", + }, wantErr: true, }, } @@ -94,14 +102,14 @@ func TestMarshalTo(t *testing.T) { t.Run(tt.name, func(t *testing.T) { err := tt.config.MarshalTo(tt.input) if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - var result KASConfigDupe - err = mapstructure.Decode(tt.input, &result) - assert.NoError(t, err) - assert.Equal(t, tt.expected, result) + require.Error(t, err) + return } + require.NoError(t, err) + var result KASConfigDupe + err = mapstructure.Decode(tt.input, &result) + require.NoError(t, err) + assert.Equal(t, tt.expected, result) }) } } diff --git a/service/internal/server/server.go b/service/internal/server/server.go index 3bf07516b..b5726ab27 100644 --- a/service/internal/server/server.go +++ b/service/internal/server/server.go @@ -49,10 +49,10 @@ type Config struct { Auth auth.Config `mapstructure:"auth" json:"auth"` GRPC GRPCConfig `mapstructure:"grpc" json:"grpc"` // Deprecated: Specify all crypto details in the `services.kas.keyring` struct - security.CryptoConfig2024 `mapstructure:"cryptoProvider" json:"cryptoProvider"` - TLS TLSConfig `mapstructure:"tls" json:"tls"` - CORS CORSConfig `mapstructure:"cors" json:"cors"` - WellKnownConfigRegister func(namespace string, config any) error `mapstructure:"-" json:"-"` + *security.CryptoConfig2024 `mapstructure:"cryptoProvider" json:"cryptoProvider"` + TLS TLSConfig `mapstructure:"tls" json:"tls"` + CORS CORSConfig `mapstructure:"cors" json:"cors"` + WellKnownConfigRegister func(namespace string, config any) error `mapstructure:"-" json:"-"` // Port to listen on Port int `mapstructure:"port" json:"port" default:"8080"` Host string `mapstructure:"host,omitempty" json:"host"` diff --git a/service/pkg/server/services.go b/service/pkg/server/services.go index c1e0bbf77..f76152ee5 100644 --- a/service/pkg/server/services.go +++ b/service/pkg/server/services.go @@ -169,7 +169,13 @@ func startServices(ctx context.Context, cfg config.Config, otdf *server.OpenTDFS svcConfig := cfg.Services[ns] if ns == "kas" { - + // Upgrade the the kas configuration, if there is a legacy `CryptoProvider` configuration + // present in the otdf server config. + if cfg.Server.CryptoConfig2024 != nil { + if err := cfg.Server.CryptoConfig2024.MarshalTo(svcConfig); err != nil { + return fmt.Errorf("failed to update kas key configuration from legacy server.cryptoprovider field: %w", err) + } + } } err = svc.Start(ctx, serviceregistry.RegistrationParams{ From a0af13f824302fec5558382720bab34ccd39ebdf Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Thu, 9 Jan 2025 17:31:45 -0500 Subject: [PATCH 4/9] fixes --- service/internal/security/config.go | 5 +++ service/internal/security/config_test.go | 42 ++++++++++++++---------- service/pkg/server/services.go | 1 + 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/service/internal/security/config.go b/service/internal/security/config.go index e2053a1f2..d3a069ade 100644 --- a/service/internal/security/config.go +++ b/service/internal/security/config.go @@ -73,6 +73,11 @@ type StandardKeyInfo struct { // Deprecated type CryptoConfig2024 struct { + Type string `mapstructure:"type"` + Standard `mapstructure:"standard"` +} + +type Standard struct { Keys []KeyPairInfo `mapstructure:"keys" json:"keys"` // Deprecated RSAKeys map[string]StandardKeyInfo `mapstructure:"rsa,omitempty" json:"rsa,omitempty"` diff --git a/service/internal/security/config_test.go b/service/internal/security/config_test.go index 6f7d35dfb..ac911795b 100644 --- a/service/internal/security/config_test.go +++ b/service/internal/security/config_test.go @@ -19,12 +19,14 @@ func TestMarshalTo(t *testing.T) { { name: "upgrade2023CertID", config: CryptoConfig2024{ - RSAKeys: map[string]StandardKeyInfo{ - "rsa1": {PrivateKeyPath: "rsa1_private.pem", PublicKeyPath: "rsa1_public.pem"}, - }, - ECKeys: map[string]StandardKeyInfo{ - "ec1": {PrivateKeyPath: "ec1_private.pem", PublicKeyPath: "ec1_public.pem"}, - "ec2": {PrivateKeyPath: "ec2_private.pem", PublicKeyPath: "ec2_public.pem"}, + Standard: Standard{ + RSAKeys: map[string]StandardKeyInfo{ + "rsa1": {PrivateKeyPath: "rsa1_private.pem", PublicKeyPath: "rsa1_public.pem"}, + }, + ECKeys: map[string]StandardKeyInfo{ + "ec1": {PrivateKeyPath: "ec1_private.pem", PublicKeyPath: "ec1_public.pem"}, + "ec2": {PrivateKeyPath: "ec2_private.pem", PublicKeyPath: "ec2_public.pem"}, + }, }, }, input: map[string]any{ @@ -43,11 +45,13 @@ func TestMarshalTo(t *testing.T) { { name: "upgrade2023NoCertIDs", config: CryptoConfig2024{ - RSAKeys: map[string]StandardKeyInfo{ - "rsa1": {PrivateKeyPath: "rsa1_private.pem", PublicKeyPath: "rsa1_public.pem"}, - }, - ECKeys: map[string]StandardKeyInfo{ - "ec1": {PrivateKeyPath: "ec1_private.pem", PublicKeyPath: "ec1_public.pem"}, + Standard: Standard{ + RSAKeys: map[string]StandardKeyInfo{ + "rsa1": {PrivateKeyPath: "rsa1_private.pem", PublicKeyPath: "rsa1_public.pem"}, + }, + ECKeys: map[string]StandardKeyInfo{ + "ec1": {PrivateKeyPath: "ec1_private.pem", PublicKeyPath: "ec1_public.pem"}, + }, }, }, input: map[string]any{}, @@ -62,9 +66,11 @@ func TestMarshalTo(t *testing.T) { { name: "upgrade2024H2", config: CryptoConfig2024{ - Keys: []KeyPairInfo{ - {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem"}, - {Algorithm: "ec:secp256r1", KID: "ec1", Private: "ec1_private.pem", Certificate: "ec1_public.pem"}, + Standard: Standard{ + Keys: []KeyPairInfo{ + {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem"}, + {Algorithm: "ec:secp256r1", KID: "ec1", Private: "ec1_private.pem", Certificate: "ec1_public.pem"}, + }, }, }, input: map[string]any{ @@ -84,9 +90,11 @@ func TestMarshalTo(t *testing.T) { { name: "invalid input confusing", config: CryptoConfig2024{ - RSAKeys: map[string]StandardKeyInfo{}, - ECKeys: map[string]StandardKeyInfo{}, - Keys: []KeyPairInfo{}, + Standard: Standard{ + RSAKeys: map[string]StandardKeyInfo{}, + ECKeys: map[string]StandardKeyInfo{}, + Keys: []KeyPairInfo{}, + }, }, input: map[string]any{ "keyring": []map[string]any{ diff --git a/service/pkg/server/services.go b/service/pkg/server/services.go index f76152ee5..e247fdce1 100644 --- a/service/pkg/server/services.go +++ b/service/pkg/server/services.go @@ -169,6 +169,7 @@ func startServices(ctx context.Context, cfg config.Config, otdf *server.OpenTDFS svcConfig := cfg.Services[ns] if ns == "kas" { + logger.Debug("updating kas key configuration", slog.String("namespace", ns), slog.Any("legacyConfig", cfg.Server.CryptoConfig2024)) // Upgrade the the kas configuration, if there is a legacy `CryptoProvider` configuration // present in the otdf server config. if cfg.Server.CryptoConfig2024 != nil { From 44e1ccba931aebf7ec10eea516e19d2e23404cbb Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Fri, 10 Jan 2025 10:01:54 -0500 Subject: [PATCH 5/9] updgrade fifiixedss --- service/internal/security/config.go | 8 ++- service/internal/security/config_test.go | 69 ++++++++++++++++++++++-- test/tdf-roundtrips.bats | 3 +- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/service/internal/security/config.go b/service/internal/security/config.go index d3a069ade..4ecbae4ad 100644 --- a/service/internal/security/config.go +++ b/service/internal/security/config.go @@ -34,13 +34,11 @@ func (k *KASConfigDupe) consolidate() { consolidated := make([]CurrentKeyFor, 0, len(k.Keyring)/2) //nolint:mnd // There are at most two of each of the new kind of keys. for _, key := range k.Keyring { if j, ok := seen[key.KID]; ok { - if key.Legacy { - consolidated[j].Legacy = true - } else { - consolidated[j].Active = key.Active - } + consolidated[j].Legacy = consolidated[j].Legacy || key.Legacy + consolidated[j].Active = consolidated[j].Active || !key.Legacy } else { seen[key.KID] = len(consolidated) + key.Active = !key.Legacy consolidated = append(consolidated, key) } } diff --git a/service/internal/security/config_test.go b/service/internal/security/config_test.go index ac911795b..c7ea338ee 100644 --- a/service/internal/security/config_test.go +++ b/service/internal/security/config_test.go @@ -64,7 +64,7 @@ func TestMarshalTo(t *testing.T) { wantErr: false, }, { - name: "upgrade2024H2", + name: "upgrade2024H2A", config: CryptoConfig2024{ Standard: Standard{ Keys: []KeyPairInfo{ @@ -75,8 +75,10 @@ func TestMarshalTo(t *testing.T) { }, input: map[string]any{ "keyring": []map[string]any{ - {"alg": "rsa:2048", "kid": "rsa1", "private": "rsa1_private.pem", "cert": "rsa1_public.pem", "active": true, "legacy": true}, - {"alg": "ec:secp256r1", "kid": "ec1", "private": "ec1_private.pem", "cert": "ec1_public.pem", "active": true, "legacy": true}, + {"alg": "rsa:2048", "kid": "rsa1"}, + {"alg": "ec:secp256r1", "kid": "ec1"}, + {"alg": "rsa:2048", "kid": "rsa1", "legacy": true}, + {"alg": "ec:secp256r1", "kid": "ec1", "legacy": true}, }, }, expected: KASConfigDupe{ @@ -87,6 +89,65 @@ func TestMarshalTo(t *testing.T) { }, wantErr: false, }, + { + name: "upgrade2024H2A", + config: CryptoConfig2024{ + Standard: Standard{ + Keys: []KeyPairInfo{ + {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem"}, + {Algorithm: "ec:secp256r1", KID: "ec1", Private: "ec1_private.pem", Certificate: "ec1_public.pem"}, + }, + }, + }, + input: map[string]any{ + "keyring": []map[string]any{ + {"alg": "rsa:2048", "kid": "rsa1"}, + {"alg": "ec:secp256r1", "kid": "ec1"}, + }, + }, + expected: KASConfigDupe{ + Keyring: []CurrentKeyFor{ + {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem", Active: true, Legacy: false}, + {Algorithm: "ec:secp256r1", KID: "ec1", Private: "ec1_private.pem", Certificate: "ec1_public.pem", Active: true, Legacy: false}, + }, + }, + wantErr: false, + }, + { + name: "upgrade2024H2B", + config: CryptoConfig2024{ + Standard: Standard{ + Keys: []KeyPairInfo{ + {Algorithm: "ec:secp256r1", KID: "ec2", Private: "ec2_private.pem", Certificate: "ec2_public.pem"}, + {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem"}, + {Algorithm: "ec:secp256r1", KID: "ec1", Private: "ec1_private.pem", Certificate: "ec1_public.pem"}, + {Algorithm: "rsa:2048", KID: "rsa3", Private: "rsa3_private.pem", Certificate: "rsa3_public.pem"}, + {Algorithm: "rsa:2048", KID: "rsa2", Private: "rsa2_private.pem", Certificate: "rsa2_public.pem"}, + {Algorithm: "ec:secp256r1", KID: "ec3", Private: "ec3_private.pem", Certificate: "ec3_public.pem"}, + }, + }, + }, + input: map[string]any{ + "keyring": []map[string]any{ + {"alg": "rsa:2048", "kid": "rsa1"}, + {"alg": "ec:secp256r1", "kid": "ec1", "legacy": true}, + {"alg": "ec:secp256r1", "kid": "ec1"}, + {"alg": "rsa:2048", "kid": "rsa2", "legacy": true}, + {"alg": "ec:secp256r1", "kid": "ec2", "legacy": true}, + }, + }, + expected: KASConfigDupe{ + Keyring: []CurrentKeyFor{ + {Algorithm: "rsa:2048", KID: "rsa1", Private: "rsa1_private.pem", Certificate: "rsa1_public.pem", Active: true, Legacy: false}, + {Algorithm: "rsa:2048", KID: "rsa2", Private: "rsa2_private.pem", Certificate: "rsa2_public.pem", Active: false, Legacy: true}, + {Algorithm: "rsa:2048", KID: "rsa3", Private: "rsa3_private.pem", Certificate: "rsa3_public.pem", Active: false, Legacy: false}, + {Algorithm: "ec:secp256r1", KID: "ec1", Private: "ec1_private.pem", Certificate: "ec1_public.pem", Active: true, Legacy: true}, + {Algorithm: "ec:secp256r1", KID: "ec2", Private: "ec2_private.pem", Certificate: "ec2_public.pem", Active: false, Legacy: true}, + {Algorithm: "ec:secp256r1", KID: "ec3", Private: "ec3_private.pem", Certificate: "ec3_public.pem", Active: false, Legacy: false}, + }, + }, + wantErr: false, + }, { name: "invalid input confusing", config: CryptoConfig2024{ @@ -117,7 +178,7 @@ func TestMarshalTo(t *testing.T) { var result KASConfigDupe err = mapstructure.Decode(tt.input, &result) require.NoError(t, err) - assert.Equal(t, tt.expected, result) + assert.ElementsMatch(t, tt.expected.Keyring, result.Keyring) }) } } diff --git a/test/tdf-roundtrips.bats b/test/tdf-roundtrips.bats index 533737c46..43a951078 100755 --- a/test/tdf-roundtrips.bats +++ b/test/tdf-roundtrips.bats @@ -122,6 +122,7 @@ wait_for_green() { limit=5 for i in $(seq 1 $limit); do + grpcurl "localhost:8080" "grpc.health.v1.Health.Check" if [ "$(grpcurl "localhost:8080" "grpc.health.v1.Health.Check" | jq -e -r .status)" = SERVING ]; then return 0 @@ -266,7 +267,7 @@ setup_file() { cp opentdf.yaml opentdf-test-backup.yaml.bak fi openssl req -x509 -nodes -newkey RSA:2048 -subj "/CN=kas" -keyout kas-r1-private.pem -out kas-r1-cert.pem -days 365 - openssl req -x509 -nodes -newkey RSA:2048 -subj "/CN=kas" -keyout kas-r1-private.pem -out kas-r1-cert.pem -days 365 + openssl req -x509 -nodes -newkey RSA:2048 -subj "/CN=kas" -keyout kas-r2-private.pem -out kas-r2-cert.pem -days 365 openssl ecparam -name prime256v1 >ecparams.tmp openssl req -x509 -nodes -newkey ec:ecparams.tmp -subj "/CN=kas" -keyout kas-e1-private.pem -out kas-e1-cert.pem -days 365 openssl req -x509 -nodes -newkey ec:ecparams.tmp -subj "/CN=kas" -keyout kas-e2-private.pem -out kas-e2-cert.pem -days 365 From 7a0da5ad95c07a5c7439b6546e78df878923b638 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Fri, 10 Jan 2025 11:49:40 -0500 Subject: [PATCH 6/9] example and test fixes --- examples/cmd/encrypt.go | 15 +----------- examples/cmd/examples.go | 11 ++++++++- sdk/kas_client.go | 6 ++--- sdk/tdf.go | 3 +++ service/internal/security/config_test.go | 31 +++++++++++++++++++++++- service/kas/access/publicKey.go | 23 ++++++++++++++---- service/kas/access/rewrap.go | 5 ++-- test/tdf-roundtrips.bats | 8 +++--- 8 files changed, 72 insertions(+), 30 deletions(-) diff --git a/examples/cmd/encrypt.go b/examples/cmd/encrypt.go index fdf242792..de7e58b94 100644 --- a/examples/cmd/encrypt.go +++ b/examples/cmd/encrypt.go @@ -36,7 +36,7 @@ func init() { encryptCmd.Flags().BoolVar(&nanoFormat, "nano", false, "Output in nanoTDF format") encryptCmd.Flags().BoolVar(&autoconfigure, "autoconfigure", true, "Use attribute grants to select kases") encryptCmd.Flags().BoolVar(&noKIDInKAO, "no-kid-in-kao", false, "[deprecated] Disable storing key identifiers in TDF KAOs") - encryptCmd.Flags().BoolVar(&noKIDInNano, "no-kid-in-nano", true, "Disable storing key identifiers in nanoTDF KAS ResourceLocator") + encryptCmd.Flags().BoolVar(&noKIDInNano, "no-kid-in-nano", false, "Disable storing key identifiers in nanoTDF KAS ResourceLocator") encryptCmd.Flags().StringVarP(&outputName, "output", "o", "sensitive.txt.tdf", "name or path of output file; - for stdout") encryptCmd.Flags().IntVarP(&collection, "collection", "c", 0, "number of nano's to create for collection. If collection >0 (default) then output will be _") @@ -51,19 +51,6 @@ func encrypt(cmd *cobra.Command, args []string) error { plainText := args[0] in := strings.NewReader(plainText) - opts := []sdk.Option{ - sdk.WithInsecurePlaintextConn(), - sdk.WithClientCredentials("opentdf-sdk", "secret", nil), - } - - if noKIDInKAO { - opts = append(opts, sdk.WithNoKIDInKAO()) - } - // double negative always gets me - if !noKIDInNano { - opts = append(opts, sdk.WithNoKIDInNano()) - } - // Create new offline client client, err := newSDK() if err != nil { diff --git a/examples/cmd/examples.go b/examples/cmd/examples.go index 002642a66..a35a98410 100644 --- a/examples/cmd/examples.go +++ b/examples/cmd/examples.go @@ -48,7 +48,16 @@ func newSDK() (*sdk.SDK, error) { if storeCollectionHeaders { opts = append(opts, sdk.WithStoreCollectionHeaders()) } - if clientCredentials != "" { + + if noKIDInKAO { + opts = append(opts, sdk.WithNoKIDInKAO()) + } + if noKIDInNano { + opts = append(opts, sdk.WithNoKIDInNano()) + } + if clientCredentials == "" { + opts = append(opts, sdk.WithClientCredentials("opentdf-sdk", "secret", nil)) + } else { i := strings.Index(clientCredentials, ":") if i < 0 { return nil, fmt.Errorf("invalid client id/secret pair") diff --git a/sdk/kas_client.go b/sdk/kas_client.go index 5d1fe065f..92e0d9819 100644 --- a/sdk/kas_client.go +++ b/sdk/kas_client.go @@ -361,9 +361,6 @@ func (s SDK) getPublicKey(ctx context.Context, url, algorithm string) (*KASInfo, } kid := resp.GetKid() - if s.config.tdfFeatures.noKID { - kid = "" - } ki := KASInfo{ URL: url, @@ -371,6 +368,9 @@ func (s SDK) getPublicKey(ctx context.Context, url, algorithm string) (*KASInfo, KID: kid, PublicKey: resp.GetPublicKey(), } + if s.config.tdfFeatures.noKID { + ki.KID = "" + } if s.kasKeyCache != nil { s.kasKeyCache.store(ki) } diff --git a/sdk/tdf.go b/sdk/tdf.go index e936812e7..234ed5d62 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -453,6 +453,9 @@ func (s SDK) prepareManifest(ctx context.Context, t *TDFObject, tdfConfig TDFCon SplitID: splitID, WrappedKey: string(ocrypto.Base64Encode(wrappedKey)), } + if s.config.tdfFeatures.noKID { + keyAccess.KID = "" + } manifest.EncryptionInformation.KeyAccessObjs = append(manifest.EncryptionInformation.KeyAccessObjs, keyAccess) } diff --git a/service/internal/security/config_test.go b/service/internal/security/config_test.go index c7ea338ee..5786cd6d1 100644 --- a/service/internal/security/config_test.go +++ b/service/internal/security/config_test.go @@ -17,7 +17,7 @@ func TestMarshalTo(t *testing.T) { wantErr bool }{ { - name: "upgrade2023CertID", + name: "upgrade2023CertIDA", config: CryptoConfig2024{ Standard: Standard{ RSAKeys: map[string]StandardKeyInfo{ @@ -42,6 +42,35 @@ func TestMarshalTo(t *testing.T) { }, wantErr: false, }, + { + name: "upgrade2023CertIDB", + config: CryptoConfig2024{ + Standard: Standard{ + RSAKeys: map[string]StandardKeyInfo{ + "r1": {PrivateKeyPath: "r1_private.pem", PublicKeyPath: "r1_public.pem"}, + "r2": {PrivateKeyPath: "r2_private.pem", PublicKeyPath: "r2_public.pem"}, + }, + ECKeys: map[string]StandardKeyInfo{ + "e1": {PrivateKeyPath: "e1_private.pem", PublicKeyPath: "e1_public.pem"}, + "e2": {PrivateKeyPath: "e2_private.pem", PublicKeyPath: "e2_public.pem"}, + }, + }, + }, + input: map[string]any{ + "enabled": true, + "eccertid": "e1", + "rsacertid": "r1", + }, + expected: KASConfigDupe{ + Keyring: []CurrentKeyFor{ + {Algorithm: "rsa:2048", KID: "r1", Private: "r1_private.pem", Certificate: "r1_public.pem", Active: true, Legacy: true}, + {Algorithm: "rsa:2048", KID: "r2", Private: "r2_private.pem", Certificate: "r2_public.pem", Active: false, Legacy: true}, + {Algorithm: "ec:secp256r1", KID: "e1", Private: "e1_private.pem", Certificate: "e1_public.pem", Active: true, Legacy: true}, + {Algorithm: "ec:secp256r1", KID: "e2", Private: "e2_private.pem", Certificate: "e2_public.pem", Active: false, Legacy: true}, + }, + }, + wantErr: false, + }, { name: "upgrade2023NoCertIDs", config: CryptoConfig2024{ diff --git a/service/kas/access/publicKey.go b/service/kas/access/publicKey.go index 1c2f1adc7..a6bd2b0e9 100644 --- a/service/kas/access/publicKey.go +++ b/service/kas/access/publicKey.go @@ -26,12 +26,18 @@ func (p Provider) LegacyPublicKey(ctx context.Context, req *connect.Request[kasp if err != nil { return nil, err } - kid, err := p.CryptoProvider.CurrentKID(algorithm) + kids, err := p.CryptoProvider.CurrentKID(algorithm) if err != nil { return nil, err } + if len(kids) == 0 { + return nil, security.ErrCertNotFound + } + if len(kids) > 1 { + p.Logger.ErrorContext(ctx, "multiple keys found for algorithm", "algorithm", algorithm, "kids", kids) + } fmt := recrypt.KeyFormatPEM - pem, err := p.CryptoProvider.PublicKey(algorithm, kid, fmt) + pem, err := p.CryptoProvider.PublicKey(algorithm, kids[:1], fmt) if err != nil { p.Logger.ErrorContext(ctx, "CryptoProvider.ECPublicKey failed", "err", err) return nil, connect.NewError(connect.CodeInternal, errors.Join(ErrConfig, errors.New("configuration error"))) @@ -54,14 +60,21 @@ func (p Provider) PublicKey(ctx context.Context, req *connect.Request[kaspb.Publ algorithm = recrypt.AlgorithmRSA2048 } - kid, err := p.CryptoProvider.CurrentKID(algorithm) + kids, err := p.CryptoProvider.CurrentKID(algorithm) if err != nil { return nil, connect.NewError(connect.CodeNotFound, err) } + if len(kids) == 0 { + return nil, security.ErrCertNotFound + } fmt, err := p.CryptoProvider.ParseKeyFormat(req.Msg.GetFmt()) if err != nil { return nil, connect.NewError(connect.CodeInvalidArgument, err) } + if len(kids) > 1 && fmt != recrypt.KeyFormatJWK { + p.Logger.WarnContext(ctx, "multiple active keys found for algorithm, only returning the first one", "algorithm", algorithm, "kids", kids, "fmt", fmt) + kids = kids[:1] + } r := func(value string, kid []recrypt.KeyIdentifier, err error) (*connect.Response[kaspb.PublicKeyResponse], error) { if errors.Is(err, security.ErrCertNotFound) { @@ -78,8 +91,8 @@ func (p Provider) PublicKey(ctx context.Context, req *connect.Request[kaspb.Publ return connect.NewResponse(&kaspb.PublicKeyResponse{PublicKey: value, Kid: string(kid[0])}), nil } - v, err := p.CryptoProvider.PublicKey(algorithm, kid, fmt) - return r(v, kid, err) + v, err := p.CryptoProvider.PublicKey(algorithm, kids, fmt) + return r(v, kids, err) } func exportRsaPublicKeyAsPemStr(pubkey *rsa.PublicKey) (string, error) { diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index 214ce7c40..d9c3b59d4 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -274,8 +274,8 @@ func (p *Provider) Rewrap(ctx context.Context, req *connect.Request[kaspb.Rewrap } if body.Algorithm == "" { - p.Logger.DebugContext(ctx, "default rewrap algorithm") body.Algorithm = "rsa:2048" + p.Logger.DebugContext(ctx, "default rewrap algorithm", "alg", body.Algorithm) } if body.Algorithm == "ec:secp256r1" { @@ -315,12 +315,13 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, body *RequestBody, entity *en return nil, err400("bad request") } } + p.Logger.DebugContext(ctx, "paging through legacy KIDs for kid free kao", "kids", kidsToCheck) symmetricKey, err := p.CryptoProvider.Unwrap(kidsToCheck[0], body.KeyAccess.WrappedKey) for _, kid := range kidsToCheck[1:] { if err == nil { break } - p.Logger.DebugContext(ctx, "continue paging through legacy KIDs for kid free kao", "err", err) + p.Logger.DebugContext(ctx, "continue paging through legacy KIDs for kid free kao", "err", err, "kid", kid) symmetricKey, err = p.CryptoProvider.Unwrap(kid, body.KeyAccess.WrappedKey) } if err != nil { diff --git a/test/tdf-roundtrips.bats b/test/tdf-roundtrips.bats index 43a951078..cca00e3af 100755 --- a/test/tdf-roundtrips.bats +++ b/test/tdf-roundtrips.bats @@ -176,15 +176,15 @@ server: standard: rsa: r1: - private_key_path: kas-private.pem - public_key_path: kas-cert.pem + private_key_path: kas-r1-private.pem + public_key_path: kas-r1-cert.pem r2: private_key_path: kas-r2-private.pem public_key_path: kas-r2-cert.pem ec: e1: - private_key_path: kas-ec-private.pem - public_key_path: kas-ec-cert.pem + private_key_path: kas-e1-private.pem + public_key_path: kas-e1-cert.pem e2: private_key_path: kas-e2-private.pem public_key_path: kas-e2-cert.pem From 42d9649603bc9c243f3c466510b2107c9b803ae2 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Fri, 10 Jan 2025 12:05:22 -0500 Subject: [PATCH 7/9] Update configuration.md --- docs/configuration.md | 42 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 95da77f4d..de33335e6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,8 +20,6 @@ The platform is designed as a modular monolith, meaning that all services are bu - core: Runs essential services, including policy, authorization, and wellknown services. - kas: Runs the Key Access Server (KAS) service. - - | Field | Description | Default | Environment Variable | | -------- | -------------| -------- | -------------------- | | `mode` | Drives which services to run. Following modes are supported. (all, core, kas) | `all` | OPENTDF_MODE | @@ -80,7 +78,6 @@ Root level key `server` | `auth.skew` | The amount of time drift allowed between a tokens `exp` claim and the server time. | `1m` | OPENTDF_SERVER_AUTH_SKEW | | `auth.public_client_id` | The oidc client id. This is leveraged by otdfctl. | | OPENTDF_SERVER_AUTH_PUBLIC_CLIENT_ID | | `auth.enforceDPoP` | If true, DPoP bindings on Access Tokens are enforced. | `false` | OPENTDF_SERVER_AUTH_ENFORCEDPOP | -| `cryptoProvider` | A list of public/private keypairs and their use. Described [below](#crypto-provider) | empty | | | `enable_pprof` | Enable golang performance profiling | `false` | OPENTDF_SERVER_ENABLE_PPROF | | `grpc.reflection` | The configuration for the grpc server. | `true` | OPENTDF_SERVER_GRPC_REFLECTION | | `host` | The host address for the server. | `""` | OPENTDF_SERVER_HOST | @@ -117,27 +114,6 @@ server: cert: kas-ec-cert.pem ``` -### Crypto Provider - -To configure the Key Access Server, -you must define a set of one or more public keypairs -and a method for loading and using them. - -The crypto provider is implemented as an interface, -allowing multiple implementations. - -Root level key `cryptoProvider` - -Environment Variable: `OPENTDF_SERVER_CRYPTOPROVIDER_STANDARD='[{"alg":"rsa:2048","kid":"k1","private":"kas-private.pem","cert":"kas-cert.pem"}]'` - -| Field | Description | Default | -| ----------------------------------- | ------------------------------------------------------------------------- | ---------- | -| `cryptoProvider.type` | The type of crypto provider to use. | `standard` | -| `cryptoProvider.standard.*.alg` | An enum for the associated crypto type. E.g. `rsa:2048` or `ec:secp256r1` | | -| `cryptoProvider.standard.*.kid` | A short, globally unique, stable identifier for this keypair. | | -| `cryptoProvider.standard.*.private` | Path to the private key as a PEM file. | | -| `cryptoProvider.standard.*.cert` | (Optional) Path to a public cert for the keypair. | | - ## Database Configuration The database configuration is used to define how the application connects to its database. @@ -177,14 +153,20 @@ Root level key `services` Root level key `kas` -Environment Variable: `OPENTDF_SERVICES_KAS_KEYRING='[{"kid":"k1","alg":"rsa:2048"},{"kid":"k2","alg":"ec:secp256r1"}]'` +To configure the Key Access Server, +you must define a set of one or more public keypairs +and a method for loading and using them. + +Environment Variable: `OPENTDF_SERVICES_KAS_KEYRING='[{"kid":"k1","alg":"rsa:2048",etc...}'` | Field | Description | Default | | ------------------ | ------------------------------------------------------------------------------- | -------- | -| `keyring.*.kid` | Which key id this is binding | | -| `keyring.*.alg` | (Optional) Associated algorithm. (Allows reusing KID with different algorithms) | | +| `keyring.*.kid` | A short, globally unique, stable identifier for this keypair | | +| `keyring.*.alg` | An enum for the associated cryptographic mechanism. E.g. `rsa:2048` or `ec:secp256r1` | | | `keyring.*.active` | Marks the current public key for new TDFs with the specific algorithm; please specify exactly 1 for each currently recommended mechanism | false | | `keyring.*.legacy` | Indicates this may be used for TDFs with no key ID; default if all unspecified. | false | +| `keyring.*.private` | Path to the private key as a PEM file. | | +| `keyring.*.cert` | (Optional) Path to a public cert for the keypair. | | Example: @@ -195,13 +177,18 @@ services: keyring: - kid: e2 alg: ec:secp256r1 + private: ./keys/ec2-private.pem - kid: e1 alg: ec:secp256r1 + private: ./keys/ec1-private.pem legacy: true - kid: r2f alg: rsa:2048 + private: ./keys/r2-private.pem + cert: ./keys/r2-cert.pem - kid: r1 alg: rsa:2048 + private: ./keys/r1-private.pem legacy: true ``` @@ -334,4 +321,3 @@ server: #### Managing Authorization Policy Admins can manage the authorization policy directly in the YAML configuration file. For detailed configuration options, refer to the [Casbin documentation](https://casbin.org/docs/en/syntax-for-models). - From 9276d240cecb7db3d93a8b263078bb4a9e9794cf Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Fri, 10 Jan 2025 12:05:58 -0500 Subject: [PATCH 8/9] Update configuration.md --- docs/configuration.md | 99 +++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index de33335e6..9ae4e5a43 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,9 +20,9 @@ The platform is designed as a modular monolith, meaning that all services are bu - core: Runs essential services, including policy, authorization, and wellknown services. - kas: Runs the Key Access Server (KAS) service. -| Field | Description | Default | Environment Variable | -| -------- | -------------| -------- | -------------------- | -| `mode` | Drives which services to run. Following modes are supported. (all, core, kas) | `all` | OPENTDF_MODE | +| Field | Description | Default | Environment Variable | +| ------ | ----------------------------------------------------------------------------- | ------- | -------------------- | +| `mode` | Drives which services to run. Following modes are supported. (all, core, kas) | `all` | OPENTDF_MODE | ## SDK Configuration @@ -30,16 +30,16 @@ The sdk configuration is used when operating the service in mode `kas`. When run Root level key `sdk_config` -| Field | Description | Default | Environment Variable | -| -------- | -------------| -------- | -------------------- | -| `core.endpoint` | The core platform endpoint to connect to | | OPENTDF_SDK_CONFIG_ENDPOINT | -| `core.plaintext` | Use a plaintext grpc connection | `false` | OPENTDF_SDK_CONFIG_PLAINTEXT | -| `core.insecure` | Use an insecure tls connection | `false` | | -| `entityresolution.endpoint` | The entityresolution endpoint to connect to | | | -| `entityresolution.plaintext` | Use a plaintext ERS grpc connection | `false` | | -| `entityresolution.insecure` | Use an insecure tls connection | `false` | | -| `client_id` | OAuth client id | | OPENTDF_SDK_CONFIG_CLIENT_ID | -| `client_secret` | The clients credentials | | OPENTDF_SDK_CONFIG_CLIENT_SECRET | +| Field | Description | Default | Environment Variable | +| ---------------------------- | ------------------------------------------- | ------- | -------------------------------- | +| `core.endpoint` | The core platform endpoint to connect to | | OPENTDF_SDK_CONFIG_ENDPOINT | +| `core.plaintext` | Use a plaintext grpc connection | `false` | OPENTDF_SDK_CONFIG_PLAINTEXT | +| `core.insecure` | Use an insecure tls connection | `false` | | +| `entityresolution.endpoint` | The entityresolution endpoint to connect to | | | +| `entityresolution.plaintext` | Use a plaintext ERS grpc connection | `false` | | +| `entityresolution.insecure` | Use an insecure tls connection | `false` | | +| `client_id` | OAuth client id | | OPENTDF_SDK_CONFIG_CLIENT_ID | +| `client_secret` | The clients credentials | | OPENTDF_SDK_CONFIG_CLIENT_SECRET | ## Logger Configuration @@ -47,8 +47,8 @@ The logger configuration is used to define how the application logs its output. Root level key `logger` -| Field | Description | Default | Environment Variable | -| -------- | -------------------------------- | -------- | -------------------- | +| Field | Description | Default | Environment Variable | +| -------- | -------------------------------- | -------- | --------------------- | | `level` | The logging level. | `info` | OPENTDF_LOGGER_LEVEL | | `type` | The format of the log output. | `json` | OPENTDF_LOGGER_TYPE | | `output` | The output destination for logs. | `stdout` | OPENTDF_LOGGER_OUTPUT | @@ -69,7 +69,7 @@ The server configuration is used to define how the application runs its server. Root level key `server` | Field | Description | Default | Environment Variable | -|-------------------------|---------------------------------------------------------------------------------------------------------------|---------|--------------------------------------| +| ----------------------- | ------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------ | | `auth.audience` | The audience for the IDP. | | OPENTDF_SERVER_AUTH_AUDIENCE | | `auth.issuer` | The issuer for the IDP. | | OPENTDF_SERVER_AUTH_ISSUER | | `auth.policy` | The Casbin policy for enforcing authorization on endpoints. Described [below](#casbin-endpoint-authorization) | | | @@ -120,15 +120,15 @@ The database configuration is used to define how the application connects to its Root level key `db` -| Field | Description | Default | Environment Variables | -| -------------- | --------------------------------------------- | ----------- | --------------------- | -| `host` | The host address for the database. | `localhost` | OPENTDF_DB_HOST | -| `port` | The port number for the database. | `5432` | OPENTDF_DB_PORT | -| `database` | The name of the database. | `opentdf` | OPENTDF_DB_DATABASE | -| `user` | The username for the database. | `postgres` | OPENTDF_DB_USER | -| `password` | The password for the database. | `changeme` | OPENTDF_DB_PASSWORD | -| `sslmode` | The ssl mode for the database | `prefer` | OPENTDF_DB_SSLMODE | -| `schema` | The schema for the database. | `opentdf` | OPENTDF_DB_SCHEMA | +| Field | Description | Default | Environment Variables | +| -------------- | --------------------------------------------- | ----------- | ----------------------- | +| `host` | The host address for the database. | `localhost` | OPENTDF_DB_HOST | +| `port` | The port number for the database. | `5432` | OPENTDF_DB_PORT | +| `database` | The name of the database. | `opentdf` | OPENTDF_DB_DATABASE | +| `user` | The username for the database. | `postgres` | OPENTDF_DB_USER | +| `password` | The password for the database. | `changeme` | OPENTDF_DB_PASSWORD | +| `sslmode` | The ssl mode for the database | `prefer` | OPENTDF_DB_SSLMODE | +| `schema` | The schema for the database. | `opentdf` | OPENTDF_DB_SCHEMA | | `runMigration` | Whether to run the database migration or not. | `true` | OPENTDF_DB_RUNMIGRATION | Example: @@ -159,14 +159,14 @@ and a method for loading and using them. Environment Variable: `OPENTDF_SERVICES_KAS_KEYRING='[{"kid":"k1","alg":"rsa:2048",etc...}'` -| Field | Description | Default | -| ------------------ | ------------------------------------------------------------------------------- | -------- | -| `keyring.*.kid` | A short, globally unique, stable identifier for this keypair | | -| `keyring.*.alg` | An enum for the associated cryptographic mechanism. E.g. `rsa:2048` or `ec:secp256r1` | | -| `keyring.*.active` | Marks the current public key for new TDFs with the specific algorithm; please specify exactly 1 for each currently recommended mechanism | false | -| `keyring.*.legacy` | Indicates this may be used for TDFs with no key ID; default if all unspecified. | false | -| `keyring.*.private` | Path to the private key as a PEM file. | | -| `keyring.*.cert` | (Optional) Path to a public cert for the keypair. | | +| Field | Description | Default | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `keyring.*.kid` | A short, globally unique, stable identifier for this keypair | | +| `keyring.*.alg` | An enum for the associated cryptographic mechanism. E.g. `rsa:2048` or `ec:secp256r1` | | +| `keyring.*.active` | Marks the current public key for new TDFs with the specific algorithm; please specify exactly 1 for each currently recommended mechanism | false | +| `keyring.*.legacy` | Indicates this may be used for TDFs with no key ID; default if all unspecified. | false | +| `keyring.*.private` | Path to the private key as a PEM file. | | +| `keyring.*.cert` | (Optional) Path to a public cert for the keypair. | | Example: @@ -196,9 +196,9 @@ services: Root level key `authorization` -| Field | Description | Default | Environment Variables | -| --------- | ------------------------ | ------- | --------------------- | -| `rego.path` | Path to rego policy file | Leverages embedded rego policy | OPENTDF_SERVICES_AUTHORIZATION_REGO_PATH | +| Field | Description | Default | Environment Variables | +| ------------ | ------------------------------- | -------------------------------------- | ----------------------------------------- | +| `rego.path` | Path to rego policy file | Leverages embedded rego policy | OPENTDF_SERVICES_AUTHORIZATION_REGO_PATH | | `rego.query` | Rego query to execute in policy | `data.opentdf.entitlements.attributes` | OPENTDF_SERVICES_AUTHORIZATION_REGO_QUERY | Example: @@ -238,9 +238,9 @@ OpenTDF uses Casbin to manage authorization policies. This document provides an 2. **Username Claim**: The claim in the OIDC token that should be used to extract a username. 3. **Group Claim**: The claim in the OIDC token that should be used to find the group claims. 4. **Map (Deprecated)**: Mapping between policy roles and IdP roles. -4. **Extension**: Policy that will extend the builtin policy -4. **CSV**: The authorization policy in CSV format. This will override the builtin policy. -5. **Model**: The Casbin policy model. This should only be set if you have a deep understanding of how casbin works. +5. **Extension**: Policy that will extend the builtin policy +6. **CSV**: The authorization policy in CSV format. This will override the builtin policy. +7. **Model**: The Casbin policy model. This should only be set if you have a deep understanding of how casbin works. #### Configuration in opentdf-example.yaml @@ -256,17 +256,16 @@ server: audience: 'http://localhost:8080' issuer: http://keycloak:8888/auth/realms/opentdf policy: - ## Deprecated ## Dot notation is used to access nested claims (i.e. realm_access.roles) - claim: "realm_access.roles" + claim: 'realm_access.roles' ## Dot notation is used to access the username claim - username_claim: "email" + username_claim: 'email' ## Dot notation is used to access the groups claim - group_claim: "realm_access.roles" - + group_claim: 'realm_access.roles' + ## Deprecated: Use standard casbin policy groupings (g, , ) ## Maps the external role to the OpenTDF role ## Note: left side is used in the policy, right side is the external role @@ -283,7 +282,7 @@ server: g, alice@opentdf.io, role:standard ## Custom policy (see examples https://github.com/casbin/casbin/tree/master/examples) - ## This will overwrite the builtin policy. Use with caution. + ## This will overwrite the builtin policy. Use with caution. csv: | p, role:admin, *, *, allow p, role:standard, policy:attributes, read, allow @@ -294,20 +293,20 @@ server: p, role:unknown, kas.AccessService/Rewrap, *, allow ## Custom model (see https://casbin.org/docs/syntax-for-models/) - ## Avoid setting this unless you have a deep understanding of how casbin works. + ## Avoid setting this unless you have a deep understanding of how casbin works. model: | [request_definition] r = sub, res, act, obj - + [policy_definition] p = sub, res, act, obj, eft - + [role_definition] g = _, _ - + [policy_effect] e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) - + [matchers] m = g(r.sub, p.sub) && globOrRegexMatch(r.res, p.res) && globOrRegexMatch(r.act, p.act) && globOrRegexMatch(r.obj, p.obj) ``` From 57a5d9bc552f06da9fd9e408126aacbcf8e31364 Mon Sep 17 00:00:00 2001 From: David Mihalcik Date: Fri, 10 Jan 2025 14:33:57 -0500 Subject: [PATCH 9/9] rename to simplify --- service/kas/access/provider.go | 4 ++-- service/kas/access/publicKey.go | 14 +++++++------- service/kas/access/rewrap.go | 6 +++--- service/kas/recrypt/recrypt.go | 2 +- service/kas/recrypt/standard.go | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/service/kas/access/provider.go b/service/kas/access/provider.go index 8c0515ba1..82e6ddb09 100644 --- a/service/kas/access/provider.go +++ b/service/kas/access/provider.go @@ -25,7 +25,7 @@ type Provider struct { URI url.URL `json:"uri"` SDK *otdf.SDK AttributeSvc *url.URL - recrypt.CryptoProvider + recrypt.Provider Logger *logger.Logger Config *serviceregistry.ServiceConfig KASConfig @@ -74,7 +74,7 @@ func (p *Provider) LoadStandardCryptoProvider() (*recrypt.Standard, error) { if err != nil { return nil, fmt.Errorf("recrypt.NewStandardWithOptions failed: %w", err) } - p.CryptoProvider = c + p.Provider = c return c, nil } diff --git a/service/kas/access/publicKey.go b/service/kas/access/publicKey.go index a6bd2b0e9..0bdf28fff 100644 --- a/service/kas/access/publicKey.go +++ b/service/kas/access/publicKey.go @@ -22,11 +22,11 @@ const ( ) func (p Provider) LegacyPublicKey(ctx context.Context, req *connect.Request[kaspb.LegacyPublicKeyRequest]) (*connect.Response[wrapperspb.StringValue], error) { - algorithm, err := p.CryptoProvider.ParseAlgorithm(req.Msg.GetAlgorithm()) + algorithm, err := p.ParseAlgorithm(req.Msg.GetAlgorithm()) if err != nil { return nil, err } - kids, err := p.CryptoProvider.CurrentKID(algorithm) + kids, err := p.CurrentKID(algorithm) if err != nil { return nil, err } @@ -37,7 +37,7 @@ func (p Provider) LegacyPublicKey(ctx context.Context, req *connect.Request[kasp p.Logger.ErrorContext(ctx, "multiple keys found for algorithm", "algorithm", algorithm, "kids", kids) } fmt := recrypt.KeyFormatPEM - pem, err := p.CryptoProvider.PublicKey(algorithm, kids[:1], fmt) + pem, err := p.Provider.PublicKey(algorithm, kids[:1], fmt) if err != nil { p.Logger.ErrorContext(ctx, "CryptoProvider.ECPublicKey failed", "err", err) return nil, connect.NewError(connect.CodeInternal, errors.Join(ErrConfig, errors.New("configuration error"))) @@ -52,7 +52,7 @@ func (p Provider) PublicKey(ctx context.Context, req *connect.Request[kaspb.Publ defer span.End() } - algorithm, err := p.CryptoProvider.ParseAlgorithm(req.Msg.GetAlgorithm()) + algorithm, err := p.ParseAlgorithm(req.Msg.GetAlgorithm()) if err != nil { return nil, connect.NewError(connect.CodeNotFound, err) } @@ -60,14 +60,14 @@ func (p Provider) PublicKey(ctx context.Context, req *connect.Request[kaspb.Publ algorithm = recrypt.AlgorithmRSA2048 } - kids, err := p.CryptoProvider.CurrentKID(algorithm) + kids, err := p.CurrentKID(algorithm) if err != nil { return nil, connect.NewError(connect.CodeNotFound, err) } if len(kids) == 0 { return nil, security.ErrCertNotFound } - fmt, err := p.CryptoProvider.ParseKeyFormat(req.Msg.GetFmt()) + fmt, err := p.ParseKeyFormat(req.Msg.GetFmt()) if err != nil { return nil, connect.NewError(connect.CodeInvalidArgument, err) } @@ -91,7 +91,7 @@ func (p Provider) PublicKey(ctx context.Context, req *connect.Request[kaspb.Publ return connect.NewResponse(&kaspb.PublicKeyResponse{PublicKey: value, Kid: string(kid[0])}), nil } - v, err := p.CryptoProvider.PublicKey(algorithm, kids, fmt) + v, err := p.Provider.PublicKey(algorithm, kids, fmt) return r(v, kids, err) } diff --git a/service/kas/access/rewrap.go b/service/kas/access/rewrap.go index d9c3b59d4..53e6332e5 100644 --- a/service/kas/access/rewrap.go +++ b/service/kas/access/rewrap.go @@ -316,13 +316,13 @@ func (p *Provider) tdf3Rewrap(ctx context.Context, body *RequestBody, entity *en } } p.Logger.DebugContext(ctx, "paging through legacy KIDs for kid free kao", "kids", kidsToCheck) - symmetricKey, err := p.CryptoProvider.Unwrap(kidsToCheck[0], body.KeyAccess.WrappedKey) + symmetricKey, err := p.Provider.Unwrap(kidsToCheck[0], body.KeyAccess.WrappedKey) for _, kid := range kidsToCheck[1:] { if err == nil { break } p.Logger.DebugContext(ctx, "continue paging through legacy KIDs for kid free kao", "err", err, "kid", kid) - symmetricKey, err = p.CryptoProvider.Unwrap(kid, body.KeyAccess.WrappedKey) + symmetricKey, err = p.Provider.Unwrap(kid, body.KeyAccess.WrappedKey) } if err != nil { p.Logger.WarnContext(ctx, "failure to decrypt dek", "err", err) @@ -424,7 +424,7 @@ func (p *Provider) nanoTDFRewrap(ctx context.Context, body *RequestBody, entity } p.Logger.DebugContext(ctx, "nanoTDFRewrap", "kid", kid) - symmetricKey, err := p.CryptoProvider.Derive(kid, header.EphemeralKey) + symmetricKey, err := p.Provider.Derive(kid, header.EphemeralKey) if err != nil { return nil, fmt.Errorf("failed to generate symmetric key: %w", err) } diff --git a/service/kas/recrypt/recrypt.go b/service/kas/recrypt/recrypt.go index 7a2dac88e..39a316d16 100644 --- a/service/kas/recrypt/recrypt.go +++ b/service/kas/recrypt/recrypt.go @@ -23,7 +23,7 @@ type KeyFormat string // - Key agreement for nanoTDF and other EC based solutions // // This may be Closeable -type CryptoProvider interface { +type Provider interface { // Return current preferred key identifier(s) for wrapping with the given algorithm. CurrentKID(alg Algorithm) ([]KeyIdentifier, error) diff --git a/service/kas/recrypt/standard.go b/service/kas/recrypt/standard.go index 5b106964c..b4043b3e0 100644 --- a/service/kas/recrypt/standard.go +++ b/service/kas/recrypt/standard.go @@ -33,7 +33,7 @@ type keyHolder struct { publicPEM []byte } -// Implementation of the recrypt CryptoProvider interface using standard go crypto primitives. +// Implementation of the recrypt.Provider interface using standard go crypto primitives. type Standard struct { keys map[KeyIdentifier]keyHolder currentKIDsByAlg map[Algorithm][]KeyIdentifier