client/client.go (491 lines of code) (raw):

// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package client is the client library for STET. package client import ( "context" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "fmt" "io" "net/url" "path" "strings" kms "cloud.google.com/go/kms/apiv1" rpb "cloud.google.com/go/kms/apiv1/kmspb" spb "cloud.google.com/go/kms/apiv1/kmspb" "github.com/GoogleCloudPlatform/stet/client/cloudkms" "github.com/GoogleCloudPlatform/stet/client/confidentialspace" "github.com/GoogleCloudPlatform/stet/client/jwt" "github.com/GoogleCloudPlatform/stet/client/securesession" "github.com/GoogleCloudPlatform/stet/client/shares" "github.com/GoogleCloudPlatform/stet/client/vpc" configpb "github.com/GoogleCloudPlatform/stet/proto/config_go_proto" glog "github.com/golang/glog" "github.com/google/uuid" "google.golang.org/api/option" "google.golang.org/protobuf/proto" ) const ( // Identifier for GCP KMS used in KEK URIs, from https://developers.google.com/tink/get-key-uri gcpKeyPrefix = "gcp-kms://" ) // StetMetadata represents metadata associated with data encrypted/decrypted by the client. type StetMetadata struct { KeyUris []string BlobID string } type secureSessionClient interface { ConfidentialWrap(ctx context.Context, keyPath string, resourceName string, plaintext []byte) ([]byte, error) ConfidentialUnwrap(ctx context.Context, keyPath string, resourceName string, wrappedBlob []byte) ([]byte, error) EndSession(context.Context) error } // StetClient provides Encryption and Decryption services through the Split Trust Encryption Tool. type StetClient struct { // Contains test KMS clients. testKMSClients *cloudkms.ClientFactory testConfspaceConfig *confidentialspace.Config // Client for contacting the Cloud EKM service. Initialized via initializeCloudEkmClient. // Only used to retrieve connection information for EXTERNAL_VPC protected keys. testCloudEKMClient vpc.CloudEKMClient // Fake Secure Session Client for testing purposes. testSecureSessionClient secureSessionClient // TLS certs to use for establishing communication with EKM. Used for specifying TLS certs for VPC // connections. ekmCertPool *x509.CertPool // Whether to skip verification of the inner TLS session cert. InsecureSkipVerify bool // The version of STET, if set. This is used to construct user agent // strings for Cloud KMS requests. Version string } // newCloudEKMClient initializes the StetClient's `cloudEKMClient`. // Performs a no-op if it has already been initialized. func (c *StetClient) newCloudEKMClient(ctx context.Context, credentials string) (vpc.CloudEKMClient, error) { if c.testCloudEKMClient != nil { return c.testCloudEKMClient, nil } opts := []option.ClientOption{} if len(credentials) != 0 { opts = append(opts, option.WithCredentialsJSON([]byte(credentials))) } var err error client, err := kms.NewEkmClient(ctx, opts...) if err != nil { return nil, fmt.Errorf("error creating Cloud EKM client: %v", err) } return client, nil } // parseEKMKeyURI takes in the key URI for a key stored in an EKM, and returns // the address for connecting to the EKM, and the key path for the resource. func parseEKMKeyURI(keyURI string) (string, string, error) { u, err := url.Parse(keyURI) if err != nil { return "", "", fmt.Errorf("could not parse: %v", err) } addr := fmt.Sprintf("%s://%s", u.Scheme, u.Hostname()) return addr, path.Base(keyURI), nil } // ekmSecureSessionWrap creates a secure session with the external EKM denoted by the given URI, and uses it to encrypt unwrappedShare. func (c *StetClient) ekmSecureSessionWrap(ctx context.Context, unwrappedShare []byte, md kekMetadata, ekmCertPool *x509.CertPool) ([]byte, error) { addr, keyPath, err := parseEKMKeyURI(md.uri) if err != nil { return nil, err } var ekmClient secureSessionClient if c.testSecureSessionClient != nil { ekmClient = c.testSecureSessionClient } else { authToken, err := jwt.GenerateTokenWithAudience(ctx, addr) if err != nil { return nil, err } ekmClient, err = securesession.EstablishSecureSession(ctx, md.uri, authToken, securesession.HTTPCertPool(ekmCertPool), securesession.SkipTLSVerify(c.InsecureSkipVerify)) if err != nil { return nil, fmt.Errorf("error establishing secure session: %v", err) } } wrappedBlob, err := ekmClient.ConfidentialWrap(ctx, keyPath, md.resourceName, unwrappedShare) if err != nil { return nil, fmt.Errorf("error wrapping with secure session: %v", err) } if err := ekmClient.EndSession(ctx); err != nil { return nil, fmt.Errorf("error ending secure session: %v", err) } return wrappedBlob, nil } // ekmSecureSessionUnwrap creates a secure session with the external EKM denoted by the given URI, and uses it to decrypt wrappedShare. func (c *StetClient) ekmSecureSessionUnwrap(ctx context.Context, wrappedShare []byte, md kekMetadata, ekmCertPool *x509.CertPool) ([]byte, error) { addr, keyPath, err := parseEKMKeyURI(md.uri) if err != nil { return nil, err } var ekmClient secureSessionClient if c.testSecureSessionClient != nil { ekmClient = c.testSecureSessionClient } else { authToken, err := jwt.GenerateTokenWithAudience(ctx, addr) if err != nil { return nil, err } ekmClient, err = securesession.EstablishSecureSession(ctx, md.uri, authToken, securesession.HTTPCertPool(ekmCertPool), securesession.SkipTLSVerify(c.InsecureSkipVerify)) if err != nil { return nil, fmt.Errorf("error establishing secure session: %v", err) } } unwrappedBlob, err := ekmClient.ConfidentialUnwrap(ctx, keyPath, md.resourceName, wrappedShare) if err != nil { return nil, fmt.Errorf("error unwrapping with secure session: %v", err) } if err := ekmClient.EndSession(ctx); err != nil { return nil, fmt.Errorf("error ending secure session: %v", err) } return unwrappedBlob, nil } type kekMetadata struct { protectionLevel rpb.ProtectionLevel uri string resourceName string } // Retrieves the CryptoKey of a CloudKMS KEK URI. func getKekCryptoKey(ctx context.Context, kmsClient cloudkms.Client, kekInfo *configpb.KekInfo) (*rpb.CryptoKey, error) { _, ok := kekInfo.GetKekType().(*configpb.KekInfo_KekUri) // No-op if this does not describe a KEK URI. if !ok { return nil, fmt.Errorf("cannot retrieve KEK Metadata for a non-KEK") } uri := kekInfo.GetKekUri() // Verify that the URI indicates a GCP KMS key. if !strings.HasPrefix(uri, gcpKeyPrefix) { return nil, fmt.Errorf("%v does not have the expected URI prefix, want %v", uri, gcpKeyPrefix) } cryptoKey, err := kmsClient.GetCryptoKey(ctx, &spb.GetCryptoKeyRequest{Name: strings.TrimPrefix(uri, gcpKeyPrefix)}) if err != nil { return nil, fmt.Errorf("error retrieving key metadata: %v", err) } cryptoKeyVer := cryptoKey.GetPrimary() if cryptoKeyVer.GetState() != rpb.CryptoKeyVersion_ENABLED { return nil, fmt.Errorf("CryptoKeyVersion for %v is not enabled", uri) } if cryptoKeyVer.ProtectionLevel == rpb.ProtectionLevel_PROTECTION_LEVEL_UNSPECIFIED { return nil, fmt.Errorf("unspecified protection level %v", cryptoKeyVer.GetProtectionLevel()) } return cryptoKey, nil } func externalKEKMetadata(cryptoKey *rpb.CryptoKey) (*kekMetadata, error) { cryptoKeyVer := cryptoKey.GetPrimary() if cryptoKeyVer.ExternalProtectionLevelOptions == nil { return nil, fmt.Errorf("CryptoKeyVersion %s does not have external protection level options despite being EXTERNAL protection level", cryptoKeyVer.GetName()) } kmd := &kekMetadata{ protectionLevel: rpb.ProtectionLevel_EXTERNAL, uri: cryptoKeyVer.GetExternalProtectionLevelOptions().GetExternalKeyUri(), resourceName: cryptoKeyVer.GetName(), } return kmd, nil } func (c *StetClient) getExternalVPCKeyInfo(ctx context.Context, cryptoKey *rpb.CryptoKey, credentials string) (*kekMetadata, *x509.CertPool, error) { ekmClient, err := c.newCloudEKMClient(ctx, credentials) if err != nil { return nil, nil, fmt.Errorf("error creating KMS EKM Client: %w", err) } defer ekmClient.Close() ekmURI, ekmCerts, err := vpc.GetURIAndCerts(ctx, ekmClient, cryptoKey) if err != nil { return nil, nil, fmt.Errorf("Error getting uri and certificates for KEK %v: %v", cryptoKey.GetName(), err) } return &kekMetadata{ protectionLevel: rpb.ProtectionLevel_EXTERNAL_VPC, uri: ekmURI, resourceName: cryptoKey.GetPrimary().GetName(), }, ekmCerts, nil } // wrapShares encrypts the given shares using either the given key URIs or the // asymmetric key provided in the corresponding KekInfo struct. It returns a // list of wrapped shares, and a list of key URIs used for shares that were // wrapped by communicating with an external KMS (these lists might not // correspond one-to-one if some shares are wrapped via asymmetric key). type sharesOpts struct { kekInfos []*configpb.KekInfo asymmetricKeys *configpb.AsymmetricKeys confSpaceConfig *confidentialspace.Config } func (c *StetClient) wrapShares(ctx context.Context, unwrappedShares [][]byte, opts sharesOpts) (wrappedShares []*configpb.WrappedShare, keyURIs []string, err error) { if len(unwrappedShares) != len(opts.kekInfos) { return nil, nil, fmt.Errorf("number of shares to wrap (%d) does not match number of KEKs (%d)", len(unwrappedShares), len(opts.kekInfos)) } var kmsClients *cloudkms.ClientFactory if c.testKMSClients != nil { kmsClients = c.testKMSClients } else { kmsClients = cloudkms.NewClientFactory(c.Version) } defer kmsClients.Close() for i, share := range unwrappedShares { wrapped := &configpb.WrappedShare{ Hash: shares.HashShare(share), } kek := opts.kekInfos[i] switch x := kek.KekType.(type) { case *configpb.KekInfo_RsaFingerprint: key, err := PublicKeyForRSAFingerprint(kek, opts.asymmetricKeys) if err != nil { return nil, nil, fmt.Errorf("failed to find public key for RSA fingerprint: %w", err) } wrapped.Share, err = rsa.EncryptOAEP(sha256.New(), rand.Reader, key, share, nil) if err != nil { return nil, nil, fmt.Errorf("error wrapping key share: %v", err) } case *configpb.KekInfo_KekUri: // Configure CloudKMS Client, with Confidential Space credentials if applicable. creds := "" if opts.confSpaceConfig != nil { creds = opts.confSpaceConfig.FindMatchingCredentials(kek.GetKekUri(), configpb.CredentialMode_ENCRYPT_ONLY_MODE) } kmsClient, err := kmsClients.Client(ctx, creds) if err != nil { return nil, nil, fmt.Errorf("error initializing Cloud KMS Client with credentials \"%v\": %v", creds, err) } cryptoKey, err := getKekCryptoKey(ctx, kmsClient, kek) if err != nil { return nil, nil, fmt.Errorf("Error retrieving KEK Metadata: %v", err) } var uri string // Wrap share via KMS. switch pl := cryptoKey.GetPrimary().ProtectionLevel; pl { case rpb.ProtectionLevel_SOFTWARE, rpb.ProtectionLevel_HSM: var err error wrapOpts := cloudkms.WrapOpts{ Share: share, KeyName: strings.TrimPrefix(kek.GetKekUri(), gcpKeyPrefix), } wrapped.Share, err = cloudkms.WrapShare(ctx, kmsClient, wrapOpts) if err != nil { return nil, nil, fmt.Errorf("error wrapping key share: %v", err) } uri = kek.GetKekUri() case rpb.ProtectionLevel_EXTERNAL: kmd, err := externalKEKMetadata(cryptoKey) if err != nil { return nil, nil, fmt.Errorf("error creating KEK Metadata: %v", err) } // A nil ekmCertPool indicates the host's Root CAs will be used to connect to the EKM. ekmWrappedShare, err := c.ekmSecureSessionWrap(ctx, share, *kmd, nil) if err != nil { return nil, nil, fmt.Errorf("error wrapping with secure session: %v", err) } wrapped.Share = ekmWrappedShare uri = kmd.uri case rpb.ProtectionLevel_EXTERNAL_VPC: kmd, ekmCerts, err := c.getExternalVPCKeyInfo(ctx, cryptoKey, creds) if err != nil { return nil, nil, fmt.Errorf("error getting external VPC key info: %v", err) } ekmWrappedShare, err := c.ekmSecureSessionWrap(ctx, share, *kmd, ekmCerts) if err != nil { return nil, nil, fmt.Errorf("error wrapping with secure session: %v", err) } wrapped.Share = ekmWrappedShare uri = kmd.uri default: return nil, nil, fmt.Errorf("unsupported protection level %v", pl) } // Return the URI used: the Cloud KMS one in the case of a software // or HSM key, and the external key URI for an external key. keyURIs = append(keyURIs, uri) default: return nil, nil, fmt.Errorf("unsupported KekInfo type: %v", x) } wrappedShares = append(wrappedShares, wrapped) } return wrappedShares, keyURIs, nil } // unwrapAndValidateShares decrypts the given wrapped share based on its URI. func (c *StetClient) unwrapAndValidateShares(ctx context.Context, wrappedShares []*configpb.WrappedShare, opts sharesOpts) ([]shares.UnwrappedShare, error) { if len(wrappedShares) != len(opts.kekInfos) { return nil, fmt.Errorf("number of shares to unwrap (%d) does not match number of KEKs (%d)", len(wrappedShares), len(opts.kekInfos)) } var kmsClients *cloudkms.ClientFactory if c.testKMSClients != nil { kmsClients = c.testKMSClients } else { kmsClients = cloudkms.NewClientFactory(c.Version) } defer kmsClients.Close() // In order to support k-of-n decryption, don't exit early if share // share unwrapping fails. Attempt to unwrap all shares and just // return the subset of ones that succeeded, and let the Shamir's // implementation handle the subset of shares. var unwrappedShares []shares.UnwrappedShare for i, wrapped := range wrappedShares { unwrapped := shares.UnwrappedShare{} kek := opts.kekInfos[i] glog.Infof("Attempting to unwrap share #%v, URI %v", i+1, kek.GetKekUri()) switch x := kek.KekType.(type) { case *configpb.KekInfo_RsaFingerprint: key, err := PrivateKeyForRSAFingerprint(kek, opts.asymmetricKeys) if err != nil { glog.Errorf("Failed to find private key for RSA fingerprint: %v", err) continue } unwrapped.Share, err = rsa.DecryptOAEP(sha256.New(), rand.Reader, key, wrapped.GetShare(), nil) if err != nil { glog.Errorf("Error unwrapping key share for %v: %v", kek.GetKekUri(), err) continue } case *configpb.KekInfo_KekUri: // Configure CloudKMS Client, with Confidential Space credentials if applicable. creds := "" if opts.confSpaceConfig != nil { creds = opts.confSpaceConfig.FindMatchingCredentials(kek.GetKekUri(), configpb.CredentialMode_DECRYPT_ONLY_MODE) } kmsClient, err := kmsClients.Client(ctx, creds) if err != nil { glog.Errorf("Error initializing Cloud KMS Client with credentials \"%v\" for %v: %v", creds, kek.GetKekUri(), err) continue } cryptoKey, err := getKekCryptoKey(ctx, kmsClient, kek) if err != nil { glog.Errorf("Error retrieving KEK Metadata for %v: %v", kek.GetKekUri(), err) continue } var uri string // Unwrap share via KMS. switch pl := cryptoKey.GetPrimary().ProtectionLevel; pl { case rpb.ProtectionLevel_SOFTWARE, rpb.ProtectionLevel_HSM: unwrapOpts := cloudkms.UnwrapOpts{ Share: wrapped.GetShare(), KeyName: strings.TrimPrefix(kek.GetKekUri(), gcpKeyPrefix), } unwrapped.Share, err = cloudkms.UnwrapShare(ctx, kmsClient, unwrapOpts) if err != nil { glog.Errorf("Error unwrapping key sharefor %v: %v", kek.GetKekUri(), err) continue } uri = kek.GetKekUri() case rpb.ProtectionLevel_EXTERNAL: kmd, err := externalKEKMetadata(cryptoKey) if err != nil { return nil, fmt.Errorf("error creating KEK Metadata: %v", err) } unwrapped.Share, err = c.ekmSecureSessionUnwrap(ctx, wrapped.GetShare(), *kmd, nil) if err != nil { glog.Warningf("Error unwrapping with external EKM for %v: %v", kmd.uri, err) continue } uri = kmd.uri case rpb.ProtectionLevel_EXTERNAL_VPC: kmd, ekmCerts, err := c.getExternalVPCKeyInfo(ctx, cryptoKey, creds) if err != nil { return nil, fmt.Errorf("error getting external VPC key info: %v", err) } unwrapped.Share, err = c.ekmSecureSessionUnwrap(ctx, wrapped.GetShare(), *kmd, ekmCerts) if err != nil { glog.Errorf("Error unwrapping with external EKM for %v: %v", kmd.uri, err) continue } uri = kmd.uri default: glog.Errorf("Unsupported protection level for %v: %v", kek.GetKekUri(), pl) continue } // Return the URI used: the Cloud KMS one in the case of a software // or HSM key, and the external key URI for an external key. unwrapped.URI = uri default: glog.Errorf("Unsupported KekInfo type for %v: %v", kek.GetKekUri(), x) continue } if !shares.ValidateShare(unwrapped.Share, wrapped.GetHash()) { glog.Errorf("Unwrapped share %v does not have the expected hash", i) continue } glog.Infof("Successfully unwrapped share %v", unwrapped.URI) unwrappedShares = append(unwrappedShares, unwrapped) } return unwrappedShares, nil } func (c *StetClient) newConfSpaceConfig(stetConfig *configpb.StetConfig) *confidentialspace.Config { if c.testConfspaceConfig != nil { return c.testConfspaceConfig } if csConfigs := stetConfig.GetConfidentialSpaceConfigs(); csConfigs != nil { return confidentialspace.NewConfig(csConfigs) } return nil } // Encrypt generates a DEK and creates EncryptedData in accordance with the EKM encryption protocol. func (c *StetClient) Encrypt(ctx context.Context, input io.Reader, output io.Writer, stetConfig *configpb.StetConfig, blobID string) (*StetMetadata, error) { config := stetConfig.GetEncryptConfig() if config == nil { return nil, fmt.Errorf("nil EncryptConfig passed to Encrypt()") } keyCfg := config.GetKeyConfig() dataEncryptionKey := shares.NewDEK() shares, err := shares.CreateDEKShares(dataEncryptionKey, keyCfg) if err != nil { return nil, fmt.Errorf("error creating DEK shares: %v", err) } // Set blob ID if specified, otherwise generate UUID. if blobID == "" { blobID = uuid.NewString() } // Create metadata. metadata := &configpb.Metadata{BlobId: blobID, KeyConfig: keyCfg} var keyURIs []string opts := sharesOpts{ kekInfos: keyCfg.GetKekInfos(), asymmetricKeys: stetConfig.GetAsymmetricKeys(), confSpaceConfig: c.newConfSpaceConfig(stetConfig), } metadata.Shares, keyURIs, err = c.wrapShares(ctx, shares, opts) if err != nil { return nil, fmt.Errorf("error wrapping shares: %v", err) } // Create AAD from metadata. aad, err := MetadataToAAD(metadata) if err != nil { return nil, fmt.Errorf("error serializing metadata: %v", err) } // Marshal the metadata into serialized bytes. metadataBytes, err := proto.Marshal(metadata) if err != nil { return nil, fmt.Errorf("failed to serialize metadata: %v", err) } // Write the header and metadata to `output`. if err := WriteSTETHeader(output, len(metadataBytes)); err != nil { return nil, fmt.Errorf("failed to write encrypted file header: %v", err) } if _, err := output.Write(metadataBytes); err != nil { return nil, fmt.Errorf("failed to write metadata: %v", err) } // Pass `output` to the AEAD encryption function to write the ciphertext. if err := AeadEncrypt(dataEncryptionKey, input, output, aad); err != nil { return nil, fmt.Errorf("error encrypting data: %v", err) } return &StetMetadata{ KeyUris: keyURIs, BlobID: metadata.GetBlobId(), }, nil } // Returns whether the number of unwrapped shares is sufficient for combining the DEK based // on the splitting func enoughUnwrappedShares(shares []shares.UnwrappedShare, config *configpb.KeyConfig) error { numShares := len(shares) // Return error if no unwrapped shares found. if numShares == 0 { return fmt.Errorf("no unwrapped shares") } // Otherwise, verify the number of shares is enough for the specified shamir threshold. if _, ok := config.GetKeySplittingAlgorithm().(*configpb.KeyConfig_Shamir); ok { if int64(numShares) < config.GetShamir().GetThreshold() { return fmt.Errorf("number of unwrapped shares %v is less than threshold needed %v", numShares, config.GetShamir().GetThreshold()) } } return nil } // Decrypt writes the decrypted data to the `output` writer, and returns the // key URIs used during decryption and the blob ID decrypted. func (c *StetClient) Decrypt(ctx context.Context, input io.Reader, output io.Writer, stetConfig *configpb.StetConfig) (*StetMetadata, error) { config := stetConfig.GetDecryptConfig() if config == nil { return nil, fmt.Errorf("nil DecryptConfig passed to Decrypt()") } metadata, err := ReadMetadata(input) if err != nil { return nil, fmt.Errorf("error reading metadata: %v", err) } // Find matching KeyConfig. var matchingKeyConfig *configpb.KeyConfig for _, keyCfg := range config.GetKeyConfigs() { if proto.Equal(keyCfg, metadata.GetKeyConfig()) { matchingKeyConfig = keyCfg break } } if matchingKeyConfig == nil { return nil, fmt.Errorf("no known KeyConfig matches given data") } // Unwrap shares and validate. opts := sharesOpts{ kekInfos: matchingKeyConfig.GetKekInfos(), asymmetricKeys: stetConfig.GetAsymmetricKeys(), confSpaceConfig: c.newConfSpaceConfig(stetConfig), } unwrappedShares, err := c.unwrapAndValidateShares(ctx, metadata.GetShares(), opts) if err != nil { return nil, fmt.Errorf("error unwrapping and validating shares: %v", err) } // Verify we have enough unwrapped shares for the key config. if err := enoughUnwrappedShares(unwrappedShares, matchingKeyConfig); err != nil { return nil, fmt.Errorf("not enough unwrapped shares to recombine DEK, see logs for unwrap details: %v", err) } else if len(unwrappedShares) < len(matchingKeyConfig.GetKekInfos()) { glog.Warningf("Recieved enough unwrapped shares to recombine DEK, but not all shares unwrapped successfully: %v of %v unwrapped, see logs for unwrap details.", len(unwrappedShares), len(matchingKeyConfig.GetKekInfos())) } combinedShares, err := shares.CombineUnwrappedShares(matchingKeyConfig, unwrappedShares) if err != nil { return nil, fmt.Errorf("error combining unwrapped shares: %v", err) } var combinedDEK shares.DEK copy(combinedDEK[:], combinedShares) // Generate AAD and decrypt ciphertext. aad, err := MetadataToAAD(metadata) if err != nil { return nil, fmt.Errorf("error serializing metadata: %v", err) } // Now `input` is at the start of ciphertext to pass to Tink. if err := AeadDecrypt(combinedDEK, input, output, aad); err != nil { return nil, fmt.Errorf("error decrypting data: %v", err) } // Return URIs of keys used during decryption. var keyURIs []string for _, unwrapped := range unwrappedShares { if unwrapped.URI != "" { keyURIs = append(keyURIs, unwrapped.URI) } } return &StetMetadata{ KeyUris: keyURIs, BlobID: metadata.GetBlobId(), }, nil }