Skip to content

Commit

Permalink
feat(core): KID in NanoTDF KAS ResourceLocator borrowed from Protocol (
Browse files Browse the repository at this point in the history
…#1222)

- Adds `identifier` field to Resource Locator
- Updates Protocol Enum with `identifier `  size

Closes #1203 
Issue: #1203 
Specification: opentdf/spec#40
ADR: #900

---------

Co-authored-by: sujankota <sreddy@virtru.com>
Co-authored-by: David Mihalcik <dmihalcik@virtru.com>
Co-authored-by: Tyler Biscoe <biscoe@virtru.com>
  • Loading branch information
4 people committed Aug 23, 2024
1 parent 2ffc66b commit e5ee4ef
Show file tree
Hide file tree
Showing 13 changed files with 541 additions and 51 deletions.
6 changes: 6 additions & 0 deletions examples/cmd/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var (
nanoFormat bool
autoconfigure bool
noKIDInKAO bool
noKIDInNano bool
outputName string
dataAttributes []string
)
Expand All @@ -32,6 +33,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().StringVarP(&outputName, "output", "o", "sensitive.txt.tdf", "name or path of output file; - for stdout")

ExamplesCmd.AddCommand(&encryptCmd)
Expand All @@ -54,6 +56,10 @@ func encrypt(cmd *cobra.Command, args []string) error {
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()
Expand Down
44 changes: 33 additions & 11 deletions sdk/nanotdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ type NanoTDFHeader struct {
ecdsaPolicyBindingS []byte
}

func (header *NanoTDFHeader) GetKasURL() ResourceLocator {
return header.kasURL
}

// GetCipher -- get the cipher from the nano tdf header
func (header *NanoTDFHeader) GetCipher() CipherMode {
return header.sigCfg.cipher
Expand Down Expand Up @@ -659,6 +663,12 @@ func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error)

// CreateNanoTDF - reads plain text from the given reader and saves it to the writer, subject to the given options
func (s SDK) CreateNanoTDF(writer io.Writer, reader io.Reader, config NanoTDFConfig) (uint32, error) {
if writer == nil {
return 0, fmt.Errorf("writer is nil")
}
if reader == nil {
return 0, fmt.Errorf("reader is nil")
}
var totalSize uint32
buf := bytes.Buffer{}
size, err := buf.ReadFrom(reader)
Expand All @@ -670,18 +680,30 @@ func (s SDK) CreateNanoTDF(writer io.Writer, reader io.Reader, config NanoTDFCon
return 0, errors.New("exceeds max size for nano tdf")
}

kasURL, err := config.kasURL.getURL()
kasURL, err := config.kasURL.GetURL()
if err != nil {
return 0, fmt.Errorf("config.kasURL failed:%w", err)
}

kasPublicKey, err := getECPublicKey(kasURL, s.dialOptions...)
if kasURL == "https://" || kasURL == "http://" {
return 0, errors.New("config.kasUrl is empty")
}
kasPublicKey, kid, err := getECPublicKeyKid(kasURL, s.dialOptions...)
if err != nil {
return 0, fmt.Errorf("getECPublicKey failed:%w", err)
}

slog.Debug("CreateNanoTDF", slog.String("header size", kasPublicKey))

// kid from kasPublicKey endpoint
slog.Debug("kasPublicKey", slog.String("kid", kid))

// update KAS URL with kid if set
if kid != "" && !s.nanoFeatures.noKID {
err = config.kasURL.setURLWithIdentifier(kasURL, kid)
if err != nil {
return 0, fmt.Errorf("getECPublicKey setURLWithIdentifier failed:%w", err)
}
}

config.kasPublicKey, err = ocrypto.ECPubKeyFromPem([]byte(kasPublicKey))
if err != nil {
return 0, fmt.Errorf("ocrypto.ECPubKeyFromPem failed: %w", err)
Expand Down Expand Up @@ -771,7 +793,7 @@ func (s SDK) ReadNanoTDFContext(ctx context.Context, writer io.Writer, reader io
return 0, fmt.Errorf("readSeeker.Seek failed: %w", err)
}

kasURL, err := header.kasURL.getURL()
kasURL, err := header.kasURL.GetURL()
if err != nil {
return 0, fmt.Errorf("readSeeker.Seek failed: %w", err)
}
Expand Down Expand Up @@ -844,17 +866,17 @@ func (s SDK) ReadNanoTDFContext(ctx context.Context, writer io.Writer, reader io
return uint32(writeLen), nil
}

// getECPublicKey - Contact the specified KAS and get its public key
func getECPublicKey(kasURL string, opts ...grpc.DialOption) (string, error) {
// getECPublicKeyKid - Contact the specified KAS and get its public key
func getECPublicKeyKid(kasURL string, opts ...grpc.DialOption) (string, string, error) {
req := kas.PublicKeyRequest{}
req.Algorithm = "ec:secp256r1"
grpcAddress, err := getGRPCAddress(kasURL)
if err != nil {
return "", err
return "", "", err
}
conn, err := grpc.Dial(grpcAddress, opts...)
if err != nil {
return "", fmt.Errorf("error connecting to grpc service at %s: %w", kasURL, err)
return "", "", fmt.Errorf("error connecting to grpc service at %s: %w", kasURL, err)
}
defer conn.Close()

Expand All @@ -864,10 +886,10 @@ func getECPublicKey(kasURL string, opts ...grpc.DialOption) (string, error) {
resp, err := serviceClient.PublicKey(ctx, &req)

if err != nil {
return "", fmt.Errorf("error making request to KAS: %w", err)
return "", "", fmt.Errorf("error making request to KAS: %w", err)
}

return resp.GetPublicKey(), nil
return resp.GetPublicKey(), resp.GetKid(), nil
}

type requestBody struct {
Expand Down
2 changes: 1 addition & 1 deletion sdk/nanotdf_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestNanoTDFConfig2(t *testing.T) {
t.Fatal(err)
}

readKasURL, err := conf.kasURL.getURL()
readKasURL, err := conf.kasURL.GetURL()
if err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion sdk/nanotdf_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestNanoTDFPolicy(t *testing.T) {
t.Fatal(err)
}

fullURL, err := pb2.rp.url.getURL()
fullURL, err := pb2.rp.url.GetURL()
if err != nil {
t.Fatal(err)
}
Expand Down
87 changes: 87 additions & 0 deletions sdk/nanotdf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sdk
import (
"bytes"
"encoding/gob"
"errors"
"io"
"os"
"testing"
Expand Down Expand Up @@ -239,3 +240,89 @@ func NotTestCreateNanoTDF(t *testing.T) {
t.Fatal(err)
}
}

func TestGetECPublicKeyKid(t *testing.T) {
var tests = []struct {
name string
kasURL string
dialOption grpc.DialOption
shouldFail bool
}{
{
name: "Valid URL, Unreachable gRPC server",
kasURL: "http://localhost",
dialOption: grpc.WithBlock(),
shouldFail: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, _, err := getECPublicKeyKid(test.kasURL, test.dialOption)
if (err != nil) != test.shouldFail {
t.Errorf("Error does not match the expected outcome. Error: %v", err)
}
})
}
}

func TestCreateNanoTDF(t *testing.T) {
tests := []struct {
name string
writer io.Writer
reader io.Reader
config NanoTDFConfig
expectedError error
}{
{
name: "Nil writer",
writer: nil,
reader: bytes.NewReader([]byte("test data")),
config: NanoTDFConfig{},
expectedError: errors.New("writer is nil"),
},
{
name: "Nil reader",
writer: new(bytes.Buffer),
reader: nil,
config: NanoTDFConfig{},
expectedError: errors.New("reader is nil"),
},
{
name: "Empty NanoTDFConfig",
writer: new(bytes.Buffer),
reader: bytes.NewReader([]byte("test data")),
config: NanoTDFConfig{},
expectedError: errors.New("config.kasUrl is empty"),
},
{
name: "KAS Identifier NanoTDFConfig",
writer: new(bytes.Buffer),
reader: bytes.NewReader([]byte("test data")),
config: NanoTDFConfig{
kasURL: ResourceLocator{
protocol: 1,
body: "kas.com",
identifier: "e0",
},
},
expectedError: errors.New("getECPublicKey failed:error connecting to grpc service at https://kas.com: grpc: no transport security set (use grpc.WithTransportCredentials(insecure.NewCredentials()) explicitly or set credentials)"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var s SDK
_, err := s.CreateNanoTDF(tt.writer, tt.reader, tt.config)
if err != nil {
if tt.expectedError == nil {
t.Errorf("unexpected error: %v", err)
} else if err.Error() != tt.expectedError.Error() {
t.Errorf("expected error: %v, got: %v", tt.expectedError, err)
}
} else if tt.expectedError != nil {
t.Errorf("expected error: %v, got nil", tt.expectedError)
}
})
}
}
15 changes: 15 additions & 0 deletions sdk/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type config struct {
dpopKey *ocrypto.RsaKeyPair
ipc bool
tdfFeatures tdfFeatures
nanoFeatures nanoFeatures
customAccessTokenSource auth.AccessTokenSource
oauthAccessTokenSource oauth2.TokenSource
coreConn *grpc.ClientConn
Expand All @@ -42,6 +43,12 @@ type tdfFeatures struct {
noKID bool
}

// Options specific to NanoTDF protocol features
type nanoFeatures struct {
// noKID For backward compatibility, don't store the KID in the KAS ResourceLocator.
noKID bool
}

type PlatformConfiguration map[string]interface{}

func (c *config) build() []grpc.DialOption {
Expand Down Expand Up @@ -200,3 +207,11 @@ func WithCustomCoreConnection(conn *grpc.ClientConn) Option {
c.coreConn = conn
}
}

// WithNoKIDInNano disables storing the KID in the KAS ResourceLocator.
// This allows generating NanoTDF files that are compatible with legacy file formats (no KID).
func WithNoKIDInNano() Option {
return func(c *config) {
c.nanoFeatures.noKID = true
}
}
39 changes: 39 additions & 0 deletions sdk/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package sdk

import (
"testing"
)

func TestWithKIDInNano(t *testing.T) {
tests := []struct {
name string
kid bool
want bool
}{
{
name: "noKID to be true",
kid: false,
want: true,
},
{
name: "noKID to be false",
kid: true,
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &config{}

if !tt.kid {
option := WithNoKIDInNano()
option(c)
}

if c.nanoFeatures.noKID != tt.want {
t.Errorf("WithKIDInNano() = %v, want %v", c.nanoFeatures.noKID, tt.want)
}
})
}
}
Loading

0 comments on commit e5ee4ef

Please sign in to comment.