diff --git a/sdk/kas_client.go b/sdk/kas_client.go index 81e23bded..a22966bd1 100644 --- a/sdk/kas_client.go +++ b/sdk/kas_client.go @@ -67,7 +67,7 @@ func newKASClient(dialOptions []grpc.DialOption, accessTokenSource auth.AccessTo } // there is no connection caching as of now -func (k *KASClient) makeRewrapRequest(keyAccess KeyAccess, policy string) (*kas.RewrapResponse, error) { +func (k *KASClient) makeRewrapRequest(ctx context.Context, keyAccess KeyAccess, policy string) (*kas.RewrapResponse, error) { rewrapRequest, err := k.getRewrapRequest(keyAccess, policy) if err != nil { return nil, err @@ -83,7 +83,6 @@ func (k *KASClient) makeRewrapRequest(keyAccess KeyAccess, policy string) (*kas. } defer conn.Close() - ctx := context.Background() serviceClient := kas.NewAccessServiceClient(conn) response, err := serviceClient.Rewrap(ctx, rewrapRequest) @@ -94,8 +93,8 @@ func (k *KASClient) makeRewrapRequest(keyAccess KeyAccess, policy string) (*kas. return response, nil } -func (k *KASClient) unwrap(keyAccess KeyAccess, policy string) ([]byte, error) { - response, err := k.makeRewrapRequest(keyAccess, policy) +func (k *KASClient) unwrap(ctx context.Context, keyAccess KeyAccess, policy string) ([]byte, error) { + response, err := k.makeRewrapRequest(ctx, keyAccess, policy) if err != nil { return nil, fmt.Errorf("error making request to kas: %w", err) } @@ -156,7 +155,7 @@ func (k *KASClient) getNanoTDFRewrapRequest(header string, kasURL string, pubKey return &rewrapRequest, nil } -func (k *KASClient) makeNanoTDFRewrapRequest(header string, kasURL string, pubKey string) (*kas.RewrapResponse, error) { +func (k *KASClient) makeNanoTDFRewrapRequest(ctx context.Context, header string, kasURL string, pubKey string) (*kas.RewrapResponse, error) { rewrapRequest, err := k.getNanoTDFRewrapRequest(header, kasURL, pubKey) if err != nil { return nil, err @@ -172,7 +171,6 @@ func (k *KASClient) makeNanoTDFRewrapRequest(header string, kasURL string, pubKe } defer conn.Close() - ctx := context.Background() serviceClient := kas.NewAccessServiceClient(conn) response, err := serviceClient.Rewrap(ctx, rewrapRequest) @@ -183,7 +181,7 @@ func (k *KASClient) makeNanoTDFRewrapRequest(header string, kasURL string, pubKe return response, nil } -func (k *KASClient) unwrapNanoTDF(header string, kasURL string) ([]byte, error) { +func (k *KASClient) unwrapNanoTDF(ctx context.Context, header string, kasURL string) ([]byte, error) { keypair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1) if err != nil { return nil, fmt.Errorf("ocrypto.NewECKeyPair failed :%w", err) @@ -199,7 +197,7 @@ func (k *KASClient) unwrapNanoTDF(header string, kasURL string) ([]byte, error) return nil, fmt.Errorf("ocrypto.NewECKeyPair.PrivateKeyInPemFormat failed :%w", err) } - response, err := k.makeNanoTDFRewrapRequest(header, kasURL, publicKeyAsPem) + response, err := k.makeNanoTDFRewrapRequest(ctx, header, kasURL, publicKeyAsPem) if err != nil { return nil, fmt.Errorf("error making request to kas: %w", err) } @@ -287,18 +285,59 @@ func (k *KASClient) getRewrapRequest(keyAccess KeyAccess, policy string) (*kas.R return &rewrapRequest, nil } -type publicKeyWithID struct { - publicKey, kid string +type kasKeyRequest struct { + url, algorithm string +} + +type timeStampedKASInfo struct { + KASInfo + time.Time +} + +// Caches the most recent key info for a given KAS URL and algorithm +type kasKeyCache struct { + c map[kasKeyRequest]timeStampedKASInfo +} + +func newKasKeyCache() *kasKeyCache { + return &kasKeyCache{make(map[kasKeyRequest]timeStampedKASInfo)} +} + +func (c *kasKeyCache) clear() { + c.c = make(map[kasKeyRequest]timeStampedKASInfo) +} + +func (c *kasKeyCache) get(url, algorithm string) *KASInfo { + cacheKey := kasKeyRequest{url, algorithm} + now := time.Now() + cv, ok := c.c[cacheKey] + if !ok { + return nil + } + ago := now.Add(-1 * time.Hour) + if ago.After(cv.Time) { + delete(c.c, cacheKey) + return nil + } + return &cv.KASInfo } -func (s SDK) getPublicKey(kasInfo KASInfo) (*publicKeyWithID, error) { - grpcAddress, err := getGRPCAddress(kasInfo.URL) +func (c *kasKeyCache) store(ki KASInfo) { + cacheKey := kasKeyRequest{ki.URL, ki.Algorithm} + c.c[cacheKey] = timeStampedKASInfo{ki, time.Now()} +} + +func (s SDK) getPublicKey(url, algorithm string) (*KASInfo, error) { + if cachedValue := s.kasKeyCache.get(url, algorithm); nil != cachedValue { + return cachedValue, nil + } + grpcAddress, err := getGRPCAddress(url) if err != nil { return nil, err } conn, err := grpc.Dial(grpcAddress, s.dialOptions...) if err != nil { - return nil, fmt.Errorf("error connecting to grpc service at %s: %w", kasInfo.URL, err) + return nil, fmt.Errorf("error connecting to grpc service at %s: %w", url, err) } defer conn.Close() @@ -306,7 +345,7 @@ func (s SDK) getPublicKey(kasInfo KASInfo) (*publicKeyWithID, error) { serviceClient := kas.NewAccessServiceClient(conn) req := kas.PublicKeyRequest{ - Algorithm: "rsa:2048", + Algorithm: algorithm, } if s.config.tdfFeatures.noKID { req.V = "1" @@ -322,5 +361,12 @@ func (s SDK) getPublicKey(kasInfo KASInfo) (*publicKeyWithID, error) { kid = "" } - return &publicKeyWithID{resp.GetPublicKey(), kid}, nil + ki := KASInfo{ + URL: url, + Algorithm: algorithm, + KID: kid, + PublicKey: resp.GetPublicKey(), + } + s.kasKeyCache.store(ki) + return &ki, nil } diff --git a/sdk/manifest.go b/sdk/manifest.go index 1d40d2c32..de631cba0 100644 --- a/sdk/manifest.go +++ b/sdk/manifest.go @@ -27,6 +27,7 @@ type KeyAccess struct { PolicyBinding string `json:"policyBinding"` EncryptedMetadata string `json:"encryptedMetadata,omitempty"` KID string `json:"kid,omitempty"` + SplitID string `json:"sid,omitempty"` } type Method struct { diff --git a/sdk/nanotdf.go b/sdk/nanotdf.go index e370cb520..9f69295c8 100644 --- a/sdk/nanotdf.go +++ b/sdk/nanotdf.go @@ -749,6 +749,11 @@ func (s SDK) CreateNanoTDF(writer io.Writer, reader io.Reader, config NanoTDFCon // ReadNanoTDF - read the nano tdf and return the decrypted data from it func (s SDK) ReadNanoTDF(writer io.Writer, reader io.ReadSeeker) (uint32, error) { + return s.ReadNanoTDFContext(context.Background(), writer, reader) +} + +// ReadNanoTDFContext - allows cancelling the reader +func (s SDK) ReadNanoTDFContext(ctx context.Context, writer io.Writer, reader io.ReadSeeker) (uint32, error) { header, headerSize, err := NewNanoTDFHeaderFromReader(reader) if err != nil { return 0, err @@ -782,7 +787,7 @@ func (s SDK) ReadNanoTDF(writer io.Writer, reader io.ReadSeeker) (uint32, error) return 0, fmt.Errorf("newKASClient failed: %w", err) } - symmetricKey, err := client.unwrapNanoTDF(string(encodedHeader), kasURL) + symmetricKey, err := client.unwrapNanoTDF(ctx, string(encodedHeader), kasURL) if err != nil { return 0, fmt.Errorf("readSeeker.Seek failed: %w", err) } diff --git a/sdk/sdk.go b/sdk/sdk.go index f4bbb291f..1f0366305 100644 --- a/sdk/sdk.go +++ b/sdk/sdk.go @@ -29,6 +29,8 @@ import ( ) const ( + // Failure while connecting to a service. + // Check your configuration and/or retry. ErrGrpcDialFailed = Error("failed to dial grpc endpoint") ErrShutdownFailed = Error("failed to shutdown sdk") ErrPlatformConfigFailed = Error("failed to retrieve platform configuration") @@ -43,6 +45,7 @@ func (c Error) Error() string { type SDK struct { config + *kasKeyCache conn *grpc.ClientConn dialOptions []grpc.DialOption tokenSource auth.AccessTokenSource @@ -146,6 +149,7 @@ func New(platformEndpoint string, opts ...Option) (*SDK, error) { return &SDK{ config: *cfg, + kasKeyCache: newKasKeyCache(), conn: defaultConn, dialOptions: dialOptions, tokenSource: accessTokenSource, diff --git a/sdk/tdf.go b/sdk/tdf.go index 180443bed..19b6e6a86 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -2,6 +2,7 @@ package sdk import ( "bytes" + "context" "encoding/hex" "encoding/json" "errors" @@ -79,6 +80,10 @@ type TDFObject struct { payloadKey [kKeySize]byte } +func (t TDFObject) Size() int64 { + return t.size +} + // CreateTDF reads plain text from the given reader and saves it to the writer, subject to the given options func (s SDK) CreateTDF(writer io.Writer, reader io.ReadSeeker, opts ...TDFOption) (*TDFObject, error) { //nolint:funlen, gocognit, lll // Better readability keeping it as is inputSize, err := reader.Seek(0, io.SeekEnd) @@ -100,14 +105,8 @@ func (s SDK) CreateTDF(writer io.Writer, reader io.ReadSeeker, opts ...TDFOption return nil, fmt.Errorf("NewTDFConfig failed: %w", err) } - // How do we want to handle different dial options for different KAS servers? - err = s.fillInPublicKeys(tdfConfig.kasInfoList) - if err != nil { - return nil, err - } - tdfObject := &TDFObject{} - err = tdfObject.prepareManifest(*tdfConfig) + err = s.prepareManifest(tdfObject, *tdfConfig) if err != nil { return nil, fmt.Errorf("fail to create a new split key: %w", err) } @@ -242,7 +241,7 @@ func (r *Reader) Manifest() Manifest { } // prepare the manifest for TDF -func (t *TDFObject) prepareManifest(tdfConfig TDFConfig) error { //nolint:funlen,gocognit // Better readability keeping it as is +func (s SDK) prepareManifest(t *TDFObject, tdfConfig TDFConfig) error { //nolint:funlen,gocognit // Better readability keeping it as is manifest := Manifest{} if len(tdfConfig.kasInfoList) == 0 { return errInvalidKasInfo @@ -262,53 +261,78 @@ func (t *TDFObject) prepareManifest(tdfConfig TDFConfig) error { //nolint:funlen base64PolicyObject := ocrypto.Base64Encode(policyObjectAsStr) symKeys := make([][]byte, 0, len(tdfConfig.kasInfoList)) + latestKASInfo := make(map[string]KASInfo) + if len(tdfConfig.splitPlan) == 0 { + // Default split plan: Split keys across all kases + tdfConfig.splitPlan = make([]splitStep, len(tdfConfig.kasInfoList)) + for i, kasInfo := range tdfConfig.kasInfoList { + tdfConfig.splitPlan[i].kas = kasInfo.URL + if len(tdfConfig.kasInfoList) > 1 { + tdfConfig.splitPlan[i].splitID = fmt.Sprintf("s-%d", i) + } + if kasInfo.PublicKey != "" { + latestKASInfo[kasInfo.URL] = kasInfo + } + } + } + // Seed anything passed in manually for _, kasInfo := range tdfConfig.kasInfoList { - if len(kasInfo.PublicKey) == 0 { - return errKasPubKeyMissing + if kasInfo.PublicKey != "" { + latestKASInfo[kasInfo.URL] = kasInfo + } + } + + // split plan: restructure by conjunctions + conjunction := make(map[string][]KASInfo) + var splitIDs []string + + for _, splitInfo := range tdfConfig.splitPlan { + // Public key was passed in with kasInfoList + ki, ok := latestKASInfo[splitInfo.kas] + if !ok || ki.PublicKey == "" { + k, err := s.getPublicKey(splitInfo.kas, "rsa:2048") + if err != nil { + return fmt.Errorf("unable to retrieve public key from KAS at [%s]: %w", splitInfo.kas, err) + } + latestKASInfo[splitInfo.kas] = *k + ki = *k } + if _, ok = conjunction[splitInfo.splitID]; ok { + conjunction[splitInfo.splitID] = append(conjunction[splitInfo.splitID], ki) + } else { + conjunction[splitInfo.splitID] = []KASInfo{ki} + splitIDs = append(splitIDs, splitInfo.splitID) + } + } + for _, splitID := range splitIDs { symKey, err := ocrypto.RandomBytes(kKeySize) if err != nil { return fmt.Errorf("ocrypto.RandomBytes failed:%w", err) } + symKeys = append(symKeys, symKey) - keyAccess := KeyAccess{} - keyAccess.KeyType = kWrapped - keyAccess.KasURL = kasInfo.URL - keyAccess.KID = kasInfo.KID - keyAccess.Protocol = kKasProtocol - - // add policyBinding + // policy binding policyBinding := hex.EncodeToString(ocrypto.CalculateSHA256Hmac(symKey, base64PolicyObject)) - keyAccess.PolicyBinding = string(ocrypto.Base64Encode([]byte(policyBinding))) - - // wrap the key with kas public key - asymEncrypt, err := ocrypto.NewAsymEncryption(kasInfo.PublicKey) - if err != nil { - return fmt.Errorf("ocrypto.NewAsymEncryption failed:%w", err) - } - - wrappedKey, err := asymEncrypt.Encrypt(symKey) - if err != nil { - return fmt.Errorf("ocrypto.AsymEncryption.encrypt failed:%w", err) - } - keyAccess.WrappedKey = string(ocrypto.Base64Encode(wrappedKey)) + pbstring := string(ocrypto.Base64Encode([]byte(policyBinding))) + // encrypted metadata // add meta data + var encryptedMetadata string if len(tdfConfig.metaData) > 0 { gcm, err := ocrypto.NewAESGcm(symKey) if err != nil { return fmt.Errorf("ocrypto.NewAESGcm failed:%w", err) } - encryptedMetaData, err := gcm.Encrypt([]byte(tdfConfig.metaData)) + emb, err := gcm.Encrypt([]byte(tdfConfig.metaData)) if err != nil { return fmt.Errorf("ocrypto.AesGcm.encrypt failed:%w", err) } - iv := encryptedMetaData[:ocrypto.GcmStandardNonceSize] + iv := emb[:ocrypto.GcmStandardNonceSize] metadata := EncryptedMetadata{ - Cipher: string(ocrypto.Base64Encode(encryptedMetaData)), + Cipher: string(ocrypto.Base64Encode(emb)), Iv: string(ocrypto.Base64Encode(iv)), } @@ -316,12 +340,38 @@ func (t *TDFObject) prepareManifest(tdfConfig TDFConfig) error { //nolint:funlen if err != nil { return fmt.Errorf(" json.Marshal failed:%w", err) } - - keyAccess.EncryptedMetadata = string(ocrypto.Base64Encode(metadataJSON)) + encryptedMetadata = string(ocrypto.Base64Encode(metadataJSON)) } - symKeys = append(symKeys, symKey) - manifest.EncryptionInformation.KeyAccessObjs = append(manifest.EncryptionInformation.KeyAccessObjs, keyAccess) + for _, kasInfo := range conjunction[splitID] { + if len(kasInfo.PublicKey) == 0 { + return errKasPubKeyMissing + } + + // wrap the key with kas public key + asymEncrypt, err := ocrypto.NewAsymEncryption(kasInfo.PublicKey) + if err != nil { + return fmt.Errorf("ocrypto.NewAsymEncryption failed:%w", err) + } + + wrappedKey, err := asymEncrypt.Encrypt(symKey) + if err != nil { + return fmt.Errorf("ocrypto.AsymEncryption.encrypt failed:%w", err) + } + + keyAccess := KeyAccess{ + KeyType: kWrapped, + KasURL: kasInfo.URL, + KID: kasInfo.KID, + Protocol: kKasProtocol, + PolicyBinding: pbstring, + EncryptedMetadata: encryptedMetadata, + SplitID: splitID, + WrappedKey: string(ocrypto.Base64Encode(wrappedKey)), + } + + manifest.EncryptionInformation.KeyAccessObjs = append(manifest.EncryptionInformation.KeyAccessObjs, keyAccess) + } } manifest.EncryptionInformation.Policy = string(base64PolicyObject) @@ -392,12 +442,21 @@ func (s SDK) LoadTDF(reader io.ReadSeeker) (*Reader, error) { }, nil } +// Do any network based operations required. +// This allows making the requests cancellable +func (r *Reader) Init(ctx context.Context) error { + if r.payloadKey != nil { + return nil + } + return r.doPayloadKeyUnwrap(ctx) +} + // Read reads up to len(p) bytes into p. It returns the number of bytes // read (0 <= n <= len(p)) and any error encountered. It returns an // io.EOF error when the stream ends. func (r *Reader) Read(p []byte) (int, error) { if r.payloadKey == nil { - err := r.doPayloadKeyUnwrap() + err := r.doPayloadKeyUnwrap(context.Background()) if err != nil { return 0, fmt.Errorf("reader.Read failed: %w", err) } @@ -412,7 +471,7 @@ func (r *Reader) Read(p []byte) (int, error) { // when an error occurs. This implements the io.WriterTo interface. func (r *Reader) WriteTo(writer io.Writer) (int64, error) { if r.payloadKey == nil { - err := r.doPayloadKeyUnwrap() + err := r.doPayloadKeyUnwrap(context.Background()) if err != nil { return 0, fmt.Errorf("reader.WriteTo failed: %w", err) } @@ -473,7 +532,7 @@ func (r *Reader) WriteTo(writer io.Writer) (int64, error) { // NOTE: For larger tdf sizes use sdk.GetTDFPayload for better performance func (r *Reader) ReadAt(buf []byte, offset int64) (int, error) { //nolint:funlen, gocognit // Better readability keeping it as is for now if r.payloadKey == nil { - err := r.doPayloadKeyUnwrap() + err := r.doPayloadKeyUnwrap(context.Background()) if err != nil { return 0, fmt.Errorf("reader.ReadAt failed: %w", err) } @@ -568,7 +627,7 @@ func (r *Reader) ReadAt(buf []byte, offset int64) (int, error) { //nolint:funlen // UnencryptedMetadata return decrypted metadata in manifest. func (r *Reader) UnencryptedMetadata() ([]byte, error) { if r.payloadKey == nil { - err := r.doPayloadKeyUnwrap() + err := r.doPayloadKeyUnwrap(context.Background()) if err != nil { return nil, fmt.Errorf("reader.UnencryptedMetadata failed: %w", err) } @@ -617,23 +676,45 @@ func (r *Reader) DataAttributes() ([]string, error) { } // Unwraps the payload key, if possible, using the access service -func (r *Reader) doPayloadKeyUnwrap() error { //nolint:gocognit // Better readability keeping it as is +func (r *Reader) doPayloadKeyUnwrap(ctx context.Context) error { //nolint:gocognit // Better readability keeping it as is var unencryptedMetadata []byte var payloadKey [kKeySize]byte + knownSplits := make(map[string]bool) + foundSplits := make(map[string]bool) + skippedSplits := make(map[splitStep]error) + mixedSplits := len(r.manifest.KeyAccessObjs) > 1 && r.manifest.KeyAccessObjs[0].SplitID != "" + for _, keyAccessObj := range r.manifest.EncryptionInformation.KeyAccessObjs { client, err := newKASClient(r.dialOptions, r.tokenSource, r.kasSessionKey) if err != nil { return fmt.Errorf("newKASClient failed:%w", err) } - wrappedKey, err := client.unwrap(keyAccessObj, r.manifest.EncryptionInformation.Policy) - if err != nil { - return fmt.Errorf("doPayloadKeyUnwrap splitKey.rewrap failed: %w", err) + ss := splitStep{keyAccessObj.KasURL, keyAccessObj.SplitID} + + var wrappedKey []byte + if !mixedSplits { + wrappedKey, err = client.unwrap(ctx, keyAccessObj, r.manifest.EncryptionInformation.Policy) + if err != nil { + return fmt.Errorf("doPayloadKeyUnwrap splitKey.rewrap failed: %w", err) + } + } else { + knownSplits[ss.splitID] = true + if foundSplits[ss.splitID] { + // already found + continue + } + wrappedKey, err = client.unwrap(ctx, keyAccessObj, r.manifest.EncryptionInformation.Policy) + if err != nil { + skippedSplits[ss] = fmt.Errorf("kao unwrap failed for %v: %w", ss, err) + continue + } } for keyByteIndex, keyByte := range wrappedKey { payloadKey[keyByteIndex] ^= keyByte } + foundSplits[ss.splitID] = true if len(keyAccessObj.EncryptedMetadata) != 0 { gcm, err := ocrypto.NewAESGcm(wrappedKey) @@ -663,6 +744,15 @@ func (r *Reader) doPayloadKeyUnwrap() error { //nolint:gocognit // Better readab } } + if mixedSplits && len(knownSplits) > len(foundSplits) { + v := make([]error, 1, len(skippedSplits)) + v[0] = fmt.Errorf("splitKey.unable to reconstruct split key: %v", skippedSplits) + for _, e := range skippedSplits { + v = append(v, e) + } + return errors.Join(v...) + } + res, err := validateRootSignature(r.manifest, payloadKey[:]) if err != nil { return fmt.Errorf("splitKey.validateRootSignature failed: %w", err) @@ -741,20 +831,3 @@ func validateRootSignature(manifest Manifest, secret []byte) (bool, error) { return false, nil } - -func (s SDK) fillInPublicKeys(kasInfos []KASInfo) error { - for idx, kasInfo := range kasInfos { - if kasInfo.PublicKey != "" { - continue - } - - publicKey, err := s.getPublicKey(kasInfo) - if err != nil { - return fmt.Errorf("unable to retrieve public key from KAS at [%s]: %w", kasInfo.URL, err) - } - - kasInfos[idx].PublicKey = publicKey.publicKey - kasInfos[idx].KID = publicKey.kid - } - return nil -} diff --git a/sdk/tdf_config.go b/sdk/tdf_config.go index e2152fdc9..c64140a50 100644 --- a/sdk/tdf_config.go +++ b/sdk/tdf_config.go @@ -41,6 +41,10 @@ type KASInfo struct { type TDFOption func(*TDFConfig) error +type splitStep struct { + kas, splitID string +} + // TDFConfig Internal config struct for building TDF options. type TDFConfig struct { defaultSegmentSize int64 @@ -55,6 +59,7 @@ type TDFConfig struct { assertions []Assertion //nolint:unused // TODO attributes []string kasInfoList []KASInfo + splitPlan []splitStep } func newTDFConfig(opt ...TDFOption) (*TDFConfig, error) { @@ -118,6 +123,14 @@ func WithKasInformation(kasInfoList ...KASInfo) TDFOption { } } +func withSplitPlan(p ...splitStep) TDFOption { + return func(c *TDFConfig) error { + c.splitPlan = make([]splitStep, len(p)) + copy(c.splitPlan, p) + return nil + } +} + // WithMetaData returns an Option that add metadata to TDF. func WithMetaData(metaData string) TDFOption { return func(c *TDFConfig) error { diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index 05cdcfcd6..eebc308cf 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -9,10 +9,12 @@ import ( "io" "log/slog" "net" + "net/url" "os" "strconv" "strings" "testing" + "time" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/opentdf/platform/lib/ocrypto" @@ -22,8 +24,6 @@ import ( "google.golang.org/grpc/test/bufconn" "google.golang.org/protobuf/types/known/structpb" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -42,10 +42,12 @@ const ( ) type tdfTest struct { + n string fileSize int64 tdfFileSize float64 checksum string mimeType string + splitPlan []splitStep } const ( @@ -67,63 +69,130 @@ wVyElqp317Ksz+GtTIc+DE6oryxK3tZd4hrj9fXT4KiJvQ4pcRjpePgH7B8= -----END CERTIFICATE-----` mockRSAPrivateKey1 = `-----BEGIN PRIVATE KEY----- - MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOpiotrvV2i5h6 - clHMzDGgh3h/kMa0LoGx2OkDPd8jogycUh7pgE5GNiN2lpSmFkjxwYMXnyrwr9Ex - yczBWJ7sRGDCDaQg5fjVUIloZ8FJVbn+sEcfQ9iX6vmI9/S++oGK79QM3V8M8cp4 - 1r/T1YVmuzUHE1say/TLHGhjtGkxHDF8qFy6Z2rYFTCVJQHNqGmwNVGd0qG7gim8 - 6Hawu/CMYj4jG9oITlj8rJtQOaJ6ZqemQVoNmb3j1LkyeUKzRIt+86aoBiz+T3Tf - OEvXF6xgBj3XoiOhPYK+abFPYcrArvb6oubT8NjjQoj3j0sXWUnIIMg+e4f+XNVU - 54ZzDaLZAgMBAAECggEBALb0yK0PlMUyzHnEUwXV1y5AIoAWhsYp0qvJ1msHUVKz - +yQ/VJz4+tQQxI8OvGbbnhNkd5LnWdYkYzsIZl7b/kBCPcQw3Zo+4XLCzhUAn1E1 - M+n42c8le1LtN6Z7mVWoZh7DPONy7t+ABvm7b7S1+1i78DPmgCeWYZGeAhIcPXG6 - 5AxWIV3jigxksE6kYY9Y7DmtsZgMRrdV7SU8VtgPtT7tua8z5/U3Av0WINyKBSoM - 0yDHsAg57KnM8znx2JWLtHd0Mk5bBuu2DLbtyKNrVUAUuMPzrLGBh9S9QRd934KU - uFAi1TEfgEachnGgSHJpzVzr2ur1tifABnQ7GNXObe0CgYEA6KowK0subdDY+uGW - ciP2XDAMerbJJeL0/UIGPb/LUmskniio2493UBGgY2FsRyvbzJ+/UAOjIPyIxhj7 - 78ZyVG8BmIzKan1RRVh//O+5yvks/eTOYjWeQ1Lcgqs3q4YAO13CEBZgKWKTUomg - mskFJq04tndeSIyhDaW+BuWaXA8CgYEA42ABz3pql+DH7oL5C4KYBymK6wFBBOqk - dVk+ftyJQ6PzuZKpfsu4aPIjKm71lkTgK6O9o08s3SckAdu6vLukq2TZFF+a+9OI - lu5ww7GvfdMTgLAaFchD4bPlOInh1KVjBc1MwGXpl0ROde5pi8+WUrv9QJuoQfB/ - 4rhYdbJLSpcCgYA41mqSCPm8pgp7r2RbWeGzP6Gs0L5u3PTQcbKonxQCfF4jrPcj - O/b/vm6aGJClClfVsyi/WUQeqNKY4j2Zo7cGXV/cbnh8b0TNVgNePQn8Rcbx91Vb - tJGHDNUFruIYqtGfrxXbbDvtoEExJqHvbjAt9J8oJB0KSCCH/vdfI/QDjQKBgQCD - xLPH5Y24js/O7aAeh4RLQkv7fTKNAt5kE2AgbPYveOhZ9yC7Fpy8VPcENGGmwCuZ - nr7b0ZqSX4iCezBxB92aZktXf0B2CFT0AyLehi7JoHWA8o1rai/MsVB5v45ciawl - RKDiLy18OF2wAoawO5FGSSOvOYX9EL9MSMEbFESF6QKBgCVlZ9pPC+55rGT6AcEL - tUpDs+/wZvcmfsFd8xC5mMUN0DatAVzVAUI95+tQaWU3Uj+bqHq0lC6Wy2VceG0D - D+7EicjdGFN/2WVPXiYX1fblkxasZY+wChYBrPLjA9g0qOzzmXbRBph5QxDuQjJ6 - qcddVKB624a93ZBssn7OivnR - -----END PRIVATE KEY-----` -) +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOpiotrvV2i5h6 +clHMzDGgh3h/kMa0LoGx2OkDPd8jogycUh7pgE5GNiN2lpSmFkjxwYMXnyrwr9Ex +yczBWJ7sRGDCDaQg5fjVUIloZ8FJVbn+sEcfQ9iX6vmI9/S++oGK79QM3V8M8cp4 +1r/T1YVmuzUHE1say/TLHGhjtGkxHDF8qFy6Z2rYFTCVJQHNqGmwNVGd0qG7gim8 +6Hawu/CMYj4jG9oITlj8rJtQOaJ6ZqemQVoNmb3j1LkyeUKzRIt+86aoBiz+T3Tf +OEvXF6xgBj3XoiOhPYK+abFPYcrArvb6oubT8NjjQoj3j0sXWUnIIMg+e4f+XNVU +54ZzDaLZAgMBAAECggEBALb0yK0PlMUyzHnEUwXV1y5AIoAWhsYp0qvJ1msHUVKz ++yQ/VJz4+tQQxI8OvGbbnhNkd5LnWdYkYzsIZl7b/kBCPcQw3Zo+4XLCzhUAn1E1 +M+n42c8le1LtN6Z7mVWoZh7DPONy7t+ABvm7b7S1+1i78DPmgCeWYZGeAhIcPXG6 +5AxWIV3jigxksE6kYY9Y7DmtsZgMRrdV7SU8VtgPtT7tua8z5/U3Av0WINyKBSoM +0yDHsAg57KnM8znx2JWLtHd0Mk5bBuu2DLbtyKNrVUAUuMPzrLGBh9S9QRd934KU +uFAi1TEfgEachnGgSHJpzVzr2ur1tifABnQ7GNXObe0CgYEA6KowK0subdDY+uGW +ciP2XDAMerbJJeL0/UIGPb/LUmskniio2493UBGgY2FsRyvbzJ+/UAOjIPyIxhj7 +78ZyVG8BmIzKan1RRVh//O+5yvks/eTOYjWeQ1Lcgqs3q4YAO13CEBZgKWKTUomg +mskFJq04tndeSIyhDaW+BuWaXA8CgYEA42ABz3pql+DH7oL5C4KYBymK6wFBBOqk +dVk+ftyJQ6PzuZKpfsu4aPIjKm71lkTgK6O9o08s3SckAdu6vLukq2TZFF+a+9OI +lu5ww7GvfdMTgLAaFchD4bPlOInh1KVjBc1MwGXpl0ROde5pi8+WUrv9QJuoQfB/ +4rhYdbJLSpcCgYA41mqSCPm8pgp7r2RbWeGzP6Gs0L5u3PTQcbKonxQCfF4jrPcj +O/b/vm6aGJClClfVsyi/WUQeqNKY4j2Zo7cGXV/cbnh8b0TNVgNePQn8Rcbx91Vb +tJGHDNUFruIYqtGfrxXbbDvtoEExJqHvbjAt9J8oJB0KSCCH/vdfI/QDjQKBgQCD +xLPH5Y24js/O7aAeh4RLQkv7fTKNAt5kE2AgbPYveOhZ9yC7Fpy8VPcENGGmwCuZ +nr7b0ZqSX4iCezBxB92aZktXf0B2CFT0AyLehi7JoHWA8o1rai/MsVB5v45ciawl +RKDiLy18OF2wAoawO5FGSSOvOYX9EL9MSMEbFESF6QKBgCVlZ9pPC+55rGT6AcEL +tUpDs+/wZvcmfsFd8xC5mMUN0DatAVzVAUI95+tQaWU3Uj+bqHq0lC6Wy2VceG0D +D+7EicjdGFN/2WVPXiYX1fblkxasZY+wChYBrPLjA9g0qOzzmXbRBph5QxDuQjJ6 +qcddVKB624a93ZBssn7OivnR +-----END PRIVATE KEY-----` + + mockRSAPrivateKey2 = `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCissi7TDbI5k6J +32DSoS+jKhwoC2a3eaLwe65Fly5brQRGdNHgXuQ85g2fKLr4D84PWW+3rXGIH9Cf +AXqx9WsBBz1Nb2h+SBh68KACN/gbtLo12fMis4/LO5o7/MfbrpwtARh+w+eNDcM1 +bzzYSMPuzoTxmvllGveZhRaacaAqRajZELRdHgotXPR31PjtCWPHxhErKdMkZ4R8 +Nrl7nDunAiVXp+YiqeVlDzxlI7QhEMsHCokDfjvb05LT48FHmLYeHWEAOBNii/Tn +8rotlArjeaksr2+cG0jrbjb2DnJVVbeg2JoMtgVyrlg1y1UcW6bISA785JCGicrd +MVglmfXPAgMBAAECggEAJkbun9YJ65D3eEtj8Zn3ZalCD4/DHjZRRcerU/cB8pKN +d3ADcoiQpN0w5jmEZ1j8jzLo7CszkyV9BPOppJWLE6Za301vJYqbq8zRsEPvrMED +sCizIX5iPZurqSJK+N2nI5Vm6Gf5oX9T5k3h4DaaViQjNd5Sf11tVCJyE2rZFiiF +sS08O//k5dO3W1mf2hZ7VGWGMjYGzV14/X0IPb0ov+1kGKpHa8hnqhfqfyjsSfQ2 +gBYhn4Rg/aYY1UgomJsxzmmROzbKcdS+95Zy5BrdUJJiK1gzDhu2OZE/c2UgiuUo +kHiIV6rqtnSz7Pk3+fboC2PXLDfYaLD4ocf69ea7TQKBgQDkeHnLbn8ly/qR4/Ac +Tgui1Uze5KTF2GM8n+gh3Sb3i5uQbkLneDrS/4d+Qgq2+wPOjmte7O5ZqnGmhqY/ +QBXBBWF2IsM6cP5YrTBrxQHdaB4ALyIkH7t/qswRKeNwmluMwRSVdD54H8ge/Rcs +9JeUQzWJ25xriOPR9gyeRIo8rQKBgQC2TXaq4ZW4bW2fXr0I/X4O3nw92Bpsqzl7 +AhI1x1y+MuzpTlZOwFpxZsYvzYy9k34Bq9/Uz3lzw1VhJF79ozJ1BjcLzTWpEugC +0QvePjx/OtvVqH6m/ZPftlgHldC8JSC/CFwGhKvhNvtnNcd1jJeZk1QLYEZh5l9P +nlGmpWKv6wKBgQC7HVhSnfqQUBC1b0L1S44IHD1Kx2OTjXco7aXGJkOFtdcAYO12 +eWdj61ditl/kIIyrnMSfB9jlosxVoC2D2851ORzrDelqcaQ9qAniGYU/ecgoSnHh +uANtucpLvEzDqgeUrYVYKc4Hv6+8gXd7oA6MpMayUyQ2hfRfvu3yqRu2OQKBgGlm +ghysTocR5ZaGDN9cyHxKUCTlg+meWZ5wBR1IxatGAEmnvCjN97ynAiDzQ9L7qpfG +yqPczMiMgBmpEK6uo2abkEnnfIXjY3b1bFozO4EIA8AVKhzccZmfcGf6S3PsN3Gb +oLE4FbQhuNrkcgzZm3D0iFwHbsn9is+apnSmHFe/AoGBANUjuB3adekqf3PsMEMa +zZFcHltBa/fRS6nMr3Vofm8tVDHlvSBAchTLrY1CAKJyNDllWqzts34iXacQk5BX +WYNzdvj1FGrOgkpHbG1XwI6kQNXGjaddo+8JmHBhHW7m1MrUsIUSgV3C5tdi0p5a +RomV0C3jlGK/HfVVrWTBtlEV +-----END PRIVATE KEY-----` + mockRSAPublicKey2 = `-----BEGIN CERTIFICATE----- +MIIC/TCCAeWgAwIBAgIUDnM4QkMGj+2UWW4USnhziNyi3XowDQYJKoZIhvcNAQEL +BQAwDjEMMAoGA1UEAwwDa2FzMB4XDTI0MDYxNDE0MTYwOFoXDTI1MDYxNDE0MTYw +OFowDjEMMAoGA1UEAwwDa2FzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAorLIu0w2yOZOid9g0qEvoyocKAtmt3mi8HuuRZcuW60ERnTR4F7kPOYNnyi6 ++A/OD1lvt61xiB/QnwF6sfVrAQc9TW9ofkgYevCgAjf4G7S6NdnzIrOPyzuaO/zH +266cLQEYfsPnjQ3DNW882EjD7s6E8Zr5ZRr3mYUWmnGgKkWo2RC0XR4KLVz0d9T4 +7Qljx8YRKynTJGeEfDa5e5w7pwIlV6fmIqnlZQ88ZSO0IRDLBwqJA34729OS0+PB +R5i2Hh1hADgTYov05/K6LZQK43mpLK9vnBtI62429g5yVVW3oNiaDLYFcq5YNctV +HFumyEgO/OSQhonK3TFYJZn1zwIDAQABo1MwUTAdBgNVHQ4EFgQUOnXMGYIbKsdc +wMDdsekltIUKxv4wHwYDVR0jBBgwFoAUOnXMGYIbKsdcwMDdsekltIUKxv4wDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABC07euj4ZZHqKEhTe6kG +utKN25psXOe6lk+0crGzC425MI7y5lKHfkfm+eMGDBfG+w42FerUiQKtc2sxzarR +vUJOdNQyhqx8kPJ6cSGPWx/tsCLe95zUhDRBv0N07OoLpJWpu8IRdMwiKjKWjutW +McR2P+L6Ih0Mwr+H72SU3PWL1pNZVmZW3jAvu+7s6tyP3gkIdrz6BGtdO38DkhQ3 +6NY6wKbZ+U+ME8mqrDy8OAqm8z1bm2YXYTdfgS3ypt+KaDwZee3gpIk8jhce0UTr +spiUiGZJJRd1+A5i4HvEOpo/gITdYE2jZF9afj4pgz9AQshCg6Fw8mIZasabT4MH +7g== +-----END CERTIFICATE-----` -var testHarnesses = []tdfTest{ //nolint:gochecknoglobals // requires for testing tdf - { - fileSize: 5, - tdfFileSize: 1557, - checksum: "ed968e840d10d2d313a870bc131a4e2c311d7ad09bdf32b3418147221f51a6e2", - }, - { - fileSize: 5, - tdfFileSize: 1557, - checksum: "ed968e840d10d2d313a870bc131a4e2c311d7ad09bdf32b3418147221f51a6e2", - mimeType: "text/plain", - }, - { - fileSize: oneKB, - tdfFileSize: 2581, - checksum: "2edc986847e209b4016e141a6dc8716d3207350f416969382d431539bf292e4a", - }, - { - fileSize: hundredMB, - tdfFileSize: 104866410, - checksum: "cee41e98d0a6ad65cc0ec77a2ba50bf26d64dc9007f7f1c7d7df68b8b71291a6", - }, - { - fileSize: 5 * hundredMB, - tdfFileSize: 524324210, - checksum: "d2fb707e70a804cf2ea770c9229295689831b4c88879c62bdb966e77e7336f18", - }, -} + mockRSAPrivateKey3 = `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCZTCZCLl6OaFNl +0WGnXI8thwz38LWwncdp74dzOotz+CC5nmoclEhFoPQPMbCI5ncH+GnGwEG9D0GI +paOGzAniVz8BlLS01WuYiLFleh1tUmyOu3nIXs5ke/2MCs1+cUK7Ghii3q6xKHfT +CHu+X+pL9PYIssDwHpqevLPsXFD7kFnRRKkmMXqLKcOgr4+qcNqQ7YN/30SJv6lw +3/FznVyhZnULi/0MDmMGHtpA5ypqycx1xLo0QygAWc9iH+lpjNbu6IvkI0dW1vjI +KMmg+BBA5uPu424lurefChCDURH32a+VXQHNr+f1j4SZJRod0Q4eDTgOadYrxj2/ +U045/f0jAgMBAAECggEAQBAuvOGb6m92ysohwUtRGnmh1cvmYhTNzVuog2Mn/CLp +qiilt6PQQCjvVZoyaEPH4rDRo5mc32GMxYpTOHX0e35yejqm+htmh6w4Vmwd+B3F ++DAoyK+2GRAn+WpaTkkO1holyYq9/pMm4C5faEO1KmEIoMHzF2Xyv/ukRVafEUGw +Mltp8PxSnaLL+PHQJT8XqTyC6uT3h0ntXh7ShDXA/ihg6hy0zOBJ8ZWHMlZt4koP +jscLm+JqqjOPGrddoUzjBwDavjgsWmgC6AGlkL+knLrrLuYql5m4VXcgwYCGxMNE +vlulEtC/2qWPYJVy9Y2cKAlel++kCUEb75s6RPGcwQKBgQDRbSJ9U8zgpTMirFDb +/0PgdYPK2p/5co96Y08sT+TlGmsduDVJhrXPLnUccYBhUREm10pPG9Lw92XRV0hm +17I+7UijjNw2ZX2z9mjCMIUFo974SIRXfGlk8kFpqIYLzm3dl6HuNG8KCs/kgIkk +kqQyEWXarQAv+QZz+klOQVzfCwKBgQC7Y4n1kTQTDnq+wXehPq5VS34/Bpu7lzF8 +fAKAF33xQ/fyijXFo7uX+Z3rWcyOK1TzmppTcD7M/rmnZECbM99p8c9zGUwggnzs +4Y9yT5xhbSP2ecER+KdHsLbyOWlmWch0iq6rOVWhRzwcUYtc5SoTqxexawAtFowk +sTGKHuEJSQKBgQCTELZ1mBF5d8kPAj7OHtXFnABuxVQt0dsbsP16Oqickg7Ckgcp +mOW3lgI7dSEYNdt7kRfnsbxR5wmjFk4LmlDbi7nE0DgcIu1BITqzk2r2aPs9E3+M +CBvi/ZQd5HAtfkr8n2zhYATR4oHXDsQ/4JJZboo+I9rL1W5Ip2wu/gt/vQKBgFLV +W2Sr/SL3YZb1GpayiIm3x2TA3RJ9cSigANLyj3+ZFf+mzMJC8Gfrtb0VgvDNgs30 +Z4e+tGQVraerD0wMEBRbCeLNKfOs+uATjT9wpaYDgsQvagMxsXBlU1mbu1W9Fnk9 +3JxfydRzEsVJ3pr/yivLk7ufmwJTVzvZABcYM03RAoGAcBpkAdrm30xQaizQ3PhW +FEeNF82AQ5HeMn+pWQEWh5H0OLl86anWyVInceIYCmiYSSyA2HQkeaPbx6uX9drW +mWG6WforNiPLQhygVLbihu38LDhaU8El4dItCuOz0J08vN3DaLry0Lo5riflBmGT +899NI+svMPeDL5zxN5h1FXA= +-----END PRIVATE KEY-----` + mockRSAPublicKey3 = `-----BEGIN CERTIFICATE----- +MIIC/TCCAeWgAwIBAgIUWLo+ebtVODHDFM4OrwNGpVodcUswDQYJKoZIhvcNAQEL +BQAwDjEMMAoGA1UEAwwDa2FzMB4XDTI0MDYxNDE0MTY1N1oXDTI1MDYxNDE0MTY1 +N1owDjEMMAoGA1UEAwwDa2FzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAmUwmQi5ejmhTZdFhp1yPLYcM9/C1sJ3Hae+HczqLc/gguZ5qHJRIRaD0DzGw +iOZ3B/hpxsBBvQ9BiKWjhswJ4lc/AZS0tNVrmIixZXodbVJsjrt5yF7OZHv9jArN +fnFCuxoYot6usSh30wh7vl/qS/T2CLLA8B6anryz7FxQ+5BZ0USpJjF6iynDoK+P +qnDakO2Df99Eib+pcN/xc51coWZ1C4v9DA5jBh7aQOcqasnMdcS6NEMoAFnPYh/p +aYzW7uiL5CNHVtb4yCjJoPgQQObj7uNuJbq3nwoQg1ER99mvlV0Bza/n9Y+EmSUa +HdEOHg04DmnWK8Y9v1NOOf39IwIDAQABo1MwUTAdBgNVHQ4EFgQUe+m7UJKzFLkc +uMdt6yHhqcvh+pEwHwYDVR0jBBgwFoAUe+m7UJKzFLkcuMdt6yHhqcvh+pEwDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcF85bMOadHZeYXmJ9nFv +9I5v/Jynju2uI5F2813VD05iJRke1dcPVcT2Dj1PucYV19Wo0pCMdWOkHhF6p9pZ +Iuxu2zA7cGQNhhUi6MKr5cUWl6tBprAghzdwEH1cZQsBiV3ki7fCCiDURIJaTlNq +/AGxRqo7/Mzh/3wci/i/XyY/FfiIr+beHuB2SPCm6hdizRH6vPAmquVAUGq2lmhl +uOnQR2c7Dix39LZQCiEfPSUnTAKJCyMpolky7Vq31PsPKk+gK19XftfH/Aul21vt +ZwVW7fLwZ2SSmC9cOjSkzZw/eDwwIRNgo94OL4mw5cXSPOuMeO8Tugc6LO4v91SO +yg== +-----END CERTIFICATE-----` +) type TestReadAt struct { segmentSize int64 @@ -140,54 +209,6 @@ type partialReadTdfTest struct { const payload = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -var partialTDFTestHarnesses = []partialReadTdfTest{ //nolint:gochecknoglobals // requires for testing tdf - { - payload: payload, // len: 62 - kasInfoList: []KASInfo{ - { - URL: "http://localhost:65432/api/kas", - PublicKey: mockRSAPublicKey1, - }, - { - URL: "http://localhost:65432/api/kas", - PublicKey: mockRSAPublicKey1, - }, - }, - readAtTests: []TestReadAt{ - { - segmentSize: 2, - dataOffset: 26, - dataLength: 26, - expectedPayload: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", - }, - { - segmentSize: 2 * oneMB, - dataOffset: 61, - dataLength: 1, - expectedPayload: "9", - }, - { - segmentSize: 2, - dataOffset: 0, - dataLength: 62, - expectedPayload: payload, - }, - { - segmentSize: int64(len(payload)), - dataOffset: 0, - dataLength: len(payload), - expectedPayload: payload, - }, - { - segmentSize: 1, - dataOffset: 26, - dataLength: 26, - expectedPayload: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", - }, - }, - }, -} - var buffer []byte //nolint:gochecknoglobals // for testing func init() { @@ -198,11 +219,14 @@ func init() { } } +type keyInfo struct { + kid, private, public string +} + type TDFSuite struct { suite.Suite - tcTerminate func() - sdk *SDK - kas FakeKas + sdk *SDK + kases []FakeKas } func (s *TDFSuite) SetupSuite() { @@ -210,9 +234,8 @@ func (s *TDFSuite) SetupSuite() { s.startBackend() } -func (s *TDFSuite) TearDownSuite() { - // Tear down the test environment - s.tcTerminate() +func (s *TDFSuite) SetupTest() { + s.sdk.kasKeyCache.clear() } func TestTDF(t *testing.T) { @@ -232,7 +255,7 @@ func (s *TDFSuite) Test_SimpleTDF() { { kasURLs := []KASInfo{ { - URL: "http://localhost:65432/", + URL: "https://a.kas/", PublicKey: "", }, } @@ -310,7 +333,53 @@ func (s *TDFSuite) Test_SimpleTDF() { } func (s *TDFSuite) Test_TDFReader() { //nolint:gocognit // requires for testing tdf - for _, test := range partialTDFTestHarnesses { // create .txt file + for _, test := range []partialReadTdfTest{ //nolint:gochecknoglobals // requires for testing tdf + { + payload: payload, // len: 62 + kasInfoList: []KASInfo{ + { + URL: "http://localhost:65432/api/kas", + PublicKey: mockRSAPublicKey1, + }, + { + URL: "http://localhost:65432/api/kas", + PublicKey: mockRSAPublicKey1, + }, + }, + readAtTests: []TestReadAt{ + { + segmentSize: 2, + dataOffset: 26, + dataLength: 26, + expectedPayload: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + }, + { + segmentSize: 2 * oneMB, + dataOffset: 61, + dataLength: 1, + expectedPayload: "9", + }, + { + segmentSize: 2, + dataOffset: 0, + dataLength: 62, + expectedPayload: payload, + }, + { + segmentSize: int64(len(payload)), + dataOffset: 0, + dataLength: len(payload), + expectedPayload: payload, + }, + { + segmentSize: 1, + dataOffset: 26, + dataLength: 26, + expectedPayload: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + }, + }, + }, + } { // create .txt file kasInfoList := test.kasInfoList // reset public keys so we have to get them from the service @@ -367,98 +436,259 @@ func (s *TDFSuite) Test_TDFReader() { //nolint:gocognit // requires for testing } func (s *TDFSuite) Test_TDF() { - for index, test := range testHarnesses { - // create .txt file - plaintTextFileName := strconv.Itoa(index) + ".txt" - tdfFileName := plaintTextFileName + ".tdf" - decryptedTdfFileName := tdfFileName + ".txt" - - kasInfoList := []KASInfo{ - s.kas.KASInfo, - } - kasInfoList[0].PublicKey = "" + for index, test := range []tdfTest{ + { + n: "small", + fileSize: 5, + tdfFileSize: 1557, + checksum: "ed968e840d10d2d313a870bc131a4e2c311d7ad09bdf32b3418147221f51a6e2", + }, + { + n: "small-with-mime-type", + fileSize: 5, + tdfFileSize: 1557, + checksum: "ed968e840d10d2d313a870bc131a4e2c311d7ad09bdf32b3418147221f51a6e2", + mimeType: "text/plain", + }, + { + n: "1-kiB", + fileSize: oneKB, + tdfFileSize: 2581, + checksum: "2edc986847e209b4016e141a6dc8716d3207350f416969382d431539bf292e4a", + }, + { + n: "medium", + fileSize: hundredMB, + tdfFileSize: 104866410, + checksum: "cee41e98d0a6ad65cc0ec77a2ba50bf26d64dc9007f7f1c7d7df68b8b71291a6", + }, + } { + s.Run(test.n, func() { + // create .txt file + plaintTextFileName := test.n + "-" + strconv.Itoa(index) + ".txt" + tdfFileName := plaintTextFileName + ".tdf" + decryptedTdfFileName := tdfFileName + ".txt" + + kasInfoList := make([]KASInfo, len(s.kases)) + for i, ki := range s.kases { + kasInfoList[i] = ki.KASInfo + kasInfoList[i].PublicKey = "" + } + kasInfoList[0].PublicKey = "" - // test encrypt - testEncrypt(s.T(), s.sdk, kasInfoList, plaintTextFileName, tdfFileName, test) + defer func() { + // Remove the test files + _ = os.Remove(plaintTextFileName) + _ = os.Remove(tdfFileName) + }() - // test decrypt with reader - testDecryptWithReader(s.T(), s.sdk, tdfFileName, decryptedTdfFileName, test) + // test encrypt + s.testEncrypt(s.sdk, kasInfoList, plaintTextFileName, tdfFileName, test) - // Remove the test files - _ = os.Remove(plaintTextFileName) - _ = os.Remove(tdfFileName) + // test decrypt with reader + s.testDecryptWithReader(s.sdk, tdfFileName, decryptedTdfFileName, test) + }) + } +} + +func (s *TDFSuite) Test_KeyRotation() { + for index, test := range []tdfTest{ + { + n: "rotate", + fileSize: 5, + tdfFileSize: 1557, + checksum: "ed968e840d10d2d313a870bc131a4e2c311d7ad09bdf32b3418147221f51a6e2", + }, + } { + s.Run(test.n, func() { + // create .txt file + plainTextFileName := test.n + "-" + strconv.Itoa(index) + ".txt" + tdfFileName := plainTextFileName + ".tdf" + decryptedTdfFileName := tdfFileName + ".txt" + tdf2Name := plainTextFileName + "-r2.tdf" + + kasInfoList := []KASInfo{s.kases[0].KASInfo} + kasInfoList[0].PublicKey = "" + + defer func() { + // Remove the test files + _ = os.Remove(plainTextFileName) + _ = os.Remove(tdfFileName) + _ = os.Remove(tdf2Name) + }() + + tdo := s.testEncrypt(s.sdk, kasInfoList, plainTextFileName, tdfFileName, test) + s.Equal("r1", tdo.manifest.EncryptionInformation.KeyAccessObjs[0].KID) + + defer rotateKey(&s.kases[0], "r2", mockRSAPrivateKey2, mockRSAPublicKey2)() + s.testDecryptWithReader(s.sdk, tdfFileName, decryptedTdfFileName, test) + + kasInfoList[0].PublicKey = "" + kasInfoList[0].KID = "" + s.sdk.kasKeyCache.clear() + tdo2 := s.testEncrypt(s.sdk, kasInfoList, tdf2Name, tdfFileName, test) + s.Equal("r2", tdo2.manifest.EncryptionInformation.KeyAccessObjs[0].KID) + + defer rotateKey(&s.kases[0], "r3", mockRSAPrivateKey3, mockRSAPublicKey3)() + s.testDecryptWithReader(s.sdk, tdfFileName, decryptedTdfFileName, test) + }) + } +} + +func (s *TDFSuite) Test_KeySplits() { + for index, test := range []tdfTest{ + { + n: "shared", + fileSize: 5, + tdfFileSize: 2664, + checksum: "ed968e840d10d2d313a870bc131a4e2c311d7ad09bdf32b3418147221f51a6e2", + splitPlan: []splitStep{ + {"https://a.kas/", "a"}, + {"https://b.kas/", "a"}, + {"https://c.kas/", "a"}, + }, + }, + { + n: "split", + fileSize: 5, + tdfFileSize: 2664, + checksum: "ed968e840d10d2d313a870bc131a4e2c311d7ad09bdf32b3418147221f51a6e2", + splitPlan: []splitStep{ + {"https://a.kas/", "a"}, + {"https://b.kas/", "b"}, + {"https://c.kas/", "c"}, + }, + }, + { + n: "mixture", + fileSize: 5, + tdfFileSize: 3211, + checksum: "ed968e840d10d2d313a870bc131a4e2c311d7ad09bdf32b3418147221f51a6e2", + splitPlan: []splitStep{ + {"https://a.kas/", "a"}, + {"https://b.kas/", "a"}, + {"https://b.kas/", "b"}, + {"https://c.kas/", "b"}, + }, + }, + } { + s.Run(test.n, func() { + plaintTextFileName := test.n + "-" + strconv.Itoa(index) + ".txt" + tdfFileName := plaintTextFileName + ".tdf" + decryptedTdfFileName := tdfFileName + ".txt" + + kasInfoList := make([]KASInfo, len(s.kases)) + for i, ki := range s.kases { + kasInfoList[i] = ki.KASInfo + kasInfoList[i].PublicKey = "" + } + kasInfoList[0].PublicKey = "" + + defer func() { + _ = os.Remove(plaintTextFileName) + _ = os.Remove(tdfFileName) + }() + + // test encrypt + tdo := s.testEncrypt(s.sdk, kasInfoList, plaintTextFileName, tdfFileName, test) + s.Equal(test.splitPlan[0].kas, tdo.manifest.EncryptionInformation.KeyAccessObjs[0].KasURL) + s.Len(tdo.manifest.EncryptionInformation.KeyAccessObjs, len(test.splitPlan)) + + // test decrypt with reader + s.testDecryptWithReader(s.sdk, tdfFileName, decryptedTdfFileName, test) + }) + } +} + +func rotateKey(k *FakeKas, kid, private, public string) func() { + old := *k + k.privateKey = private + k.KASInfo.KID = kid + k.KASInfo.PublicKey = public + k.legakeys[old.KID] = keyInfo{old.KID, old.privateKey, old.KASInfo.PublicKey} + return func() { + delete(k.legakeys, old.KID) + k.privateKey = old.privateKey + k.KASInfo.KID = old.KASInfo.KID + k.KASInfo.PublicKey = old.KASInfo.PublicKey } } // create tdf -func testEncrypt(t *testing.T, sdk *SDK, kasInfoList []KASInfo, plainTextFilename, tdfFileName string, test tdfTest) { +func (s *TDFSuite) testEncrypt(sdk *SDK, kasInfoList []KASInfo, plainTextFilename, tdfFileName string, test tdfTest) *TDFObject { // create a plain text file - createFileName(buffer, plainTextFilename, test.fileSize) + s.createFileName(buffer, plainTextFilename, test.fileSize) // open file readSeeker, err := os.Open(plainTextFilename) - require.NoError(t, err) + s.Require().NoError(err) defer func(readSeeker *os.File) { err := readSeeker.Close() - require.NoError(t, err) + s.Require().NoError(err) }(readSeeker) fileWriter, err := os.Create(tdfFileName) - require.NoError(t, err) + s.Require().NoError(err) defer func(fileWriter *os.File) { err := fileWriter.Close() - require.NoError(t, err) + s.Require().NoError(err) }(fileWriter) // CreateTDF TDFConfig - var options []TDFOption + + encryptOpts := []TDFOption{WithKasInformation(kasInfoList...)} if test.mimeType != "" { - options = []TDFOption{ - WithKasInformation(kasInfoList...), - WithMimeType(test.mimeType), - } + encryptOpts = append(encryptOpts, WithMimeType(test.mimeType)) + } + if len(test.splitPlan) == 0 { + encryptOpts = append(encryptOpts, withSplitPlan(splitStep{kasInfoList[0].URL, ""})) } else { - options = []TDFOption{ - WithKasInformation(kasInfoList...), - } + encryptOpts = append(encryptOpts, withSplitPlan(test.splitPlan...)) } - tdfObj, err := sdk.CreateTDF(fileWriter, readSeeker, options...) - require.NoError(t, err) - assert.InDelta(t, float64(test.tdfFileSize), float64(tdfObj.size), .04*float64(test.tdfFileSize)) + tdfObj, err := sdk.CreateTDF(fileWriter, readSeeker, encryptOpts...) + s.Require().NoError(err) + + s.InDelta(float64(test.tdfFileSize), float64(tdfObj.size), .04*float64(test.tdfFileSize)) + return tdfObj } -func testDecryptWithReader(t *testing.T, sdk *SDK, tdfFile, decryptedTdfFileName string, test tdfTest) { +func (s *TDFSuite) testDecryptWithReader(sdk *SDK, tdfFile, decryptedTdfFileName string, test tdfTest) { readSeeker, err := os.Open(tdfFile) - require.NoError(t, err) + s.Require().NoError(err) defer func(readSeeker *os.File) { err := readSeeker.Close() - require.NoError(t, err) + s.Require().NoError(err) }(readSeeker) r, err := sdk.LoadTDF(readSeeker) - require.NoError(t, err) + s.Require().NoError(err) + + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(60*time.Millisecond)) + defer cancel() + err = r.Init(ctx) + s.Require().NoError(err) + s.Require().NotNil(r.payloadKey) if test.mimeType != "" { - assert.Equal(t, test.mimeType, r.Manifest().Payload.MimeType, "mimeType does not match") + s.Equal(test.mimeType, r.Manifest().Payload.MimeType, "mimeType does not match") } { fileWriter, err := os.Create(decryptedTdfFileName) - require.NoError(t, err) + s.Require().NoError(err) defer func(fileWriter *os.File) { err := fileWriter.Close() - require.NoError(t, err) + s.Require().NoError(err) }(fileWriter) _, err = io.Copy(fileWriter, r) - require.NoError(t, err) + s.Require().NoError(err) } - res := checkIdentical(t, decryptedTdfFileName, test.checksum) - assert.True(t, res, "decrypted text didn't match plain text") + s.True(s.checkIdentical(decryptedTdfFileName, test.checksum), "decrypted text didn't match plain text") var bufSize int64 = 5 buf := make([]byte, bufSize) @@ -467,18 +697,16 @@ func testDecryptWithReader(t *testing.T, sdk *SDK, tdfFile, decryptedTdfFileName // read last 5 bytes n, err := r.ReadAt(buf, test.fileSize-(bufSize)) if err != nil { - require.ErrorIs(t, err, io.EOF) + s.Require().ErrorIs(err, io.EOF) } - assert.Equal(t, resultBuf[:n], buf[:n], "decrypted text didn't match plain text with ReadAt interface") + s.Equal(resultBuf[:n], buf[:n], "decrypted text didn't match plain text with ReadAt interface") _ = os.Remove(decryptedTdfFileName) } -func createFileName(buf []byte, filename string, size int64) { +func (s *TDFSuite) createFileName(buf []byte, filename string, size int64) { f, err := os.Create(filename) - if err != nil { - panic(fmt.Sprintf("os.CreateTDF failed: %v", err)) - } + s.Require().NoError(err) totalBytes := size var bytesToWrite int64 @@ -491,14 +719,10 @@ func createFileName(buf []byte, filename string, size int64) { totalBytes = 0 } _, err := f.Write(buf[:bytesToWrite]) - if err != nil { - panic(fmt.Sprintf("io.Write failed: %v", err)) - } + s.Require().NoError(err) } err = f.Close() - if err != nil { - panic(fmt.Sprintf("os.Close failed: %v", err)) - } + s.Require().NoError(err) } func (s *TDFSuite) startBackend() { @@ -514,22 +738,54 @@ func (s *TDFSuite) startBackend() { fwk := &FakeWellKnown{v: wellknownCfg} - grpcListener := bufconn.Listen(1024 * 1024) - - grpcServer := grpc.NewServer() - s.kas = FakeKas{privateKey: mockRSAPrivateKey1, KASInfo: KASInfo{ - URL: "http://localhost:65432/", PublicKey: mockRSAPublicKey1, KID: "r1", Algorithm: "rsa:2048"}, - } - kaspb.RegisterAccessServiceServer(grpcServer, &s.kas) - wellknownpb.RegisterWellKnownServiceServer(grpcServer, fwk) - go func() { - if err := grpcServer.Serve(grpcListener); err != nil { - panic(fmt.Sprintf("failed to serve: %v", err)) - } - }() + listeners := make(map[string]*bufconn.Listener) dialer := func(ctx context.Context, host string) (net.Conn, error) { + l, ok := listeners[host] + if !ok { + slog.ErrorContext(ctx, "unable to dial host!", "ctx", ctx, "host", host) + return nil, fmt.Errorf("unknown host [%s]", host) + } slog.InfoContext(ctx, "dialing with custom dialer (local grpc)", "ctx", ctx, "host", host) - return grpcListener.Dial() + return l.Dial() + } + + s.kases = make([]FakeKas, 4) + + for i, ki := range []struct { + url, private, public string + }{ + {"http://localhost:65432/", mockRSAPrivateKey1, mockRSAPublicKey1}, + {"https://a.kas/", mockRSAPrivateKey1, mockRSAPublicKey1}, + {"https://b.kas/", mockRSAPrivateKey2, mockRSAPublicKey2}, + {"https://c.kas/", mockRSAPrivateKey3, mockRSAPublicKey3}, + } { + grpcListener := bufconn.Listen(1024 * 1024) + url, err := url.Parse(ki.url) + s.Require().NoError(err) + var origin string + switch { + case url.Port() == "80": + origin = url.Hostname() + case url.Port() != "": + origin = url.Hostname() + ":" + url.Port() + case url.Scheme == "https": + origin = url.Hostname() + ":443" + default: + origin = url.Hostname() + } + listeners[origin] = grpcListener + + grpcServer := grpc.NewServer() + s.kases[i] = FakeKas{s: s, privateKey: ki.private, KASInfo: KASInfo{ + URL: ki.url, PublicKey: ki.public, KID: "r1", Algorithm: "rsa:2048"}, + legakeys: map[string]keyInfo{}, + } + kaspb.RegisterAccessServiceServer(grpcServer, &s.kases[i]) + wellknownpb.RegisterWellKnownServiceServer(grpcServer, fwk) + go func() { + err := grpcServer.Serve(grpcListener) + s.NoError(err) + }() } ats := getTokenSource(s.T()) @@ -540,14 +796,8 @@ func (s *TDFSuite) startBackend() { WithTokenEndpoint("http://localhost:65432/auth/token"), WithInsecurePlaintextConn(), WithExtraDialOptions(grpc.WithContextDialer(dialer))) - if err != nil { - panic(fmt.Sprintf("error creating SDK with authconfig: %v", err)) - } + s.Require().NoError(err) s.sdk = sdk - - s.tcTerminate = func() { - slog.Info("terminando") - } } type FakeWellKnown struct { @@ -570,6 +820,8 @@ type FakeKas struct { kaspb.UnimplementedAccessServiceServer KASInfo privateKey string + s *TDFSuite + legakeys map[string]keyInfo } func (f *FakeKas) Rewrap(_ context.Context, in *kaspb.RewrapRequest) (*kaspb.RewrapResponse, error) { @@ -601,51 +853,43 @@ func (f *FakeKas) PublicKey(_ context.Context, _ *kaspb.PublicKeyRequest) (*kasp func (f *FakeKas) getRewrappedKey(rewrapRequest string) []byte { bodyData := RequestBody{} err := json.Unmarshal([]byte(rewrapRequest), &bodyData) - if err != nil { - panic(fmt.Sprintf("json.Unmarshal failed: %v", err)) - } + f.s.Require().NoError(err, "json.Unmarshal failed") + wrappedKey, err := ocrypto.Base64Decode([]byte(bodyData.WrappedKey)) - if err != nil { - panic(fmt.Sprintf("ocrypto.Base64Decode failed: %v", err)) - } + f.s.Require().NoError(err, "ocrypto.Base64Decode failed") + kasPrivateKey := strings.ReplaceAll(f.privateKey, "\n\t", "\n") - asymDecrypt, err := ocrypto.NewAsymDecryption(kasPrivateKey) - if err != nil { - panic(fmt.Sprintf("ocrypto.NewAsymDecryption failed: %v", err)) + if bodyData.KID != "" && bodyData.KID != f.KID { + // old kid + lk, ok := f.legakeys[bodyData.KID] + f.s.Require().True(ok, "unable to find key [%s]", bodyData.KID) + kasPrivateKey = strings.ReplaceAll(lk.private, "\n\t", "\n") } + + asymDecrypt, err := ocrypto.NewAsymDecryption(kasPrivateKey) + f.s.Require().NoError(err, "ocrypto.NewAsymDecryption failed") symmetricKey, err := asymDecrypt.Decrypt(wrappedKey) - if err != nil { - panic(fmt.Sprintf("ocrypto.Decrypt failed: %v", err)) - } + f.s.Require().NoError(err, "ocrypto.Decrypt failed") asymEncrypt, err := ocrypto.NewAsymEncryption(bodyData.ClientPublicKey) - if err != nil { - panic(fmt.Sprintf("ocrypto.NewAsymEncryption failed: %v", err)) - } + f.s.Require().NoError(err, "ocrypto.NewAsymEncryption failed") entityWrappedKey, err := asymEncrypt.Encrypt(symmetricKey) - if err != nil { - panic(fmt.Sprintf("ocrypto.encrypt failed: %v", err)) - } + f.s.Require().NoError(err, "ocrypto.encrypt failed") return entityWrappedKey } -func checkIdentical(t *testing.T, file, checksum string) bool { +func (s *TDFSuite) checkIdentical(file, checksum string) bool { f, err := os.Open(file) - if err != nil { - t.Fatalf("os.Open failed: %v", err) - } + s.Require().NoError(err, "os.Open failed") + defer func(f *os.File) { err := f.Close() - if err != nil { - t.Fatalf("f.Close failed: %v", err) - } + s.Require().NoError(err, "os.Close failed") }(f) h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - t.Fatalf("io.Copy failed: %v", err) - } - c := h.Sum(nil) + _, err = io.Copy(h, f) + s.Require().NoError(err, "io.Copy failed") - // slog.Info(fmt.Sprintf("%x", c)) + c := h.Sum(nil) return checksum == fmt.Sprintf("%x", c) } diff --git a/service/kas/access/keyaccess.go b/service/kas/access/keyaccess.go index e651ec5a7..7e2dd00bf 100644 --- a/service/kas/access/keyaccess.go +++ b/service/kas/access/keyaccess.go @@ -7,6 +7,7 @@ type KeyAccess struct { Type string `json:"type"` URL string `json:"url"` KID string `json:"kid,omitempty"` + SID string `json:"sid,omitempty"` WrappedKey []byte `json:"wrappedKey,omitempty"` Header []byte `json:"header,omitempty"` Algorithm string `json:"algorithm,omitempty"`