diff --git a/.github/spellcheck.ignore b/.github/spellcheck.ignore index 47e4d2b9..0b252620 100644 --- a/.github/spellcheck.ignore +++ b/.github/spellcheck.ignore @@ -49,6 +49,7 @@ decrypt decryptable decrypted dev +ecdsa encodings enum https diff --git a/cmd/tdf-encrypt.go b/cmd/tdf-encrypt.go index 42625ecb..83c27a5a 100644 --- a/cmd/tdf-encrypt.go +++ b/cmd/tdf-encrypt.go @@ -92,7 +92,8 @@ func dev_tdfEncryptCmd(cmd *cobra.Command, args []string) { if tdfType == TDF3 { encrypted, err = h.EncryptBytes(bytesSlice, values, fileMimeType, kasURLPath) } else if tdfType == NANO { - encrypted, err = h.EncryptNanoBytes(bytesSlice, values, kasURLPath) + ecdsaBinding := c.Flags.GetOptionalBool("ecdsa-binding") + encrypted, err = h.EncryptNanoBytes(bytesSlice, values, kasURLPath, ecdsaBinding) } else { cli.ExitWithError("Failed to encrypt", fmt.Errorf("unrecognized tdf-type: %s", tdfType)) } @@ -151,6 +152,11 @@ func init() { encryptCmd.GetDocFlag("tdf-type").Default, encryptCmd.GetDocFlag("tdf-type").Description, ) + encryptCmd.Flags().Bool( + encryptCmd.GetDocFlag("ecdsa-binding").Name, + false, + encryptCmd.GetDocFlag("ecdsa-binding").Description, + ) encryptCmd.Command.GroupID = "tdf" encryptCmd.Flags().String( encryptCmd.GetDocFlag("kas-url-path").Name, diff --git a/cmd/tdf-inspect.go b/cmd/tdf-inspect.go index 802d9855..5ed4f5a2 100644 --- a/cmd/tdf-inspect.go +++ b/cmd/tdf-inspect.go @@ -2,6 +2,8 @@ package cmd import ( "errors" + "fmt" + "strings" "github.com/opentdf/otdfctl/pkg/cli" "github.com/opentdf/otdfctl/pkg/handlers" @@ -24,6 +26,13 @@ type tdfInspectManifest struct { EncryptionInformation sdk.EncryptionInformation `json:"encryptionInformation"` } +type nanoInspectResult struct { + Cipher string `json:"cipher"` + ECDSAEnabled bool `json:"ecdsaEnabled"` + Kas string `json:"kas"` + KID string `json:"kid"` +} + type tdfInspectResult struct { Manifest tdfInspectManifest `json:"manifest"` Attributes []string `json:"attributes"` @@ -42,30 +51,58 @@ func tdf_InspectCmd(cmd *cobra.Command, args []string) { result, errs := h.InspectTDF(data) for _, err := range errs { if errors.Is(err, handlers.ErrTDFInspectFailNotValidTDF) { - c.ExitWithError("not a valid ZTDF", err) + c.ExitWithError("not a valid TDF", err) } else if errors.Is(err, handlers.ErrTDFInspectFailNotInspectable) { c.ExitWithError("failed to inspect TDF", err) } } - m := tdfInspectResult{ - Manifest: tdfInspectManifest{ - Algorithm: result.Manifest.Algorithm, - KeyAccessType: result.Manifest.KeyAccessType, - MimeType: result.Manifest.MimeType, - Policy: result.Manifest.Policy, - Protocol: result.Manifest.Protocol, - SegmentHashAlgorithm: result.Manifest.SegmentHashAlgorithm, - Signature: result.Manifest.Signature, - Type: result.Manifest.Type, - Method: result.Manifest.Method, - IntegrityInformation: result.Manifest.IntegrityInformation, - EncryptionInformation: result.Manifest.EncryptionInformation, - }, - Attributes: result.Attributes, - } + if result.ZTDFManifest != nil { + m := tdfInspectResult{ + Manifest: tdfInspectManifest{ + Algorithm: result.ZTDFManifest.Algorithm, + KeyAccessType: result.ZTDFManifest.KeyAccessType, + MimeType: result.ZTDFManifest.MimeType, + Policy: result.ZTDFManifest.Policy, + Protocol: result.ZTDFManifest.Protocol, + SegmentHashAlgorithm: result.ZTDFManifest.SegmentHashAlgorithm, + Signature: result.ZTDFManifest.Signature, + Type: result.ZTDFManifest.Type, + Method: result.ZTDFManifest.Method, + IntegrityInformation: result.ZTDFManifest.IntegrityInformation, + EncryptionInformation: result.ZTDFManifest.EncryptionInformation, + }, + Attributes: result.Attributes, + } - c.PrintJson(m) + c.PrintJson(m) + } else if result.NanoHeader != nil { + kas, err := result.NanoHeader.GetKasURL().GetURL() + if err != nil { + c.ExitWithError("not a valid NanoTDF", err) + } + kid, err := result.NanoHeader.GetKasURL().GetIdentifier() + if err != nil { + c.ExitWithError("not a valid NanoTDF", err) + } + cipher := result.NanoHeader.GetCipher() + cipherBytes, err := sdk.SizeOfAuthTagForCipher(cipher) + if err != nil { + c.ExitWithError("not a valid NanoTDF", err) + } + cipherName := fmt.Sprintf("AES-%d", 8*cipherBytes) + + n := nanoInspectResult{ + Kas: kas, + KID: strings.TrimRight(kid, "\u0000"), + ECDSAEnabled: result.NanoHeader.IsEcdsaBindingEnabled(), + Cipher: cipherName, + } + + c.PrintJson(n) + } else { + c.ExitWithError("failed to inspect TDF", nil) + } } func init() { diff --git a/docs/man/encrypt/_index.md b/docs/man/encrypt/_index.md index 78a460be..f6d74d8c 100644 --- a/docs/man/encrypt/_index.md +++ b/docs/man/encrypt/_index.md @@ -19,6 +19,8 @@ command: - tdf3 - nano default: tdf3 + - name: ecdsa-binding + description: For nano type containers only, enables ECDSA policy binding - name: kas-url-path description: URL path to the KAS service at the platform endpoint domain. Leading slash is required if needed. default: /kas diff --git a/go.mod b/go.mod index da0c555d..0065cc2f 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/google/uuid v1.6.0 github.com/itchyny/gojq v0.12.16 github.com/opentdf/platform/protocol/go v0.2.14 - github.com/opentdf/platform/sdk v0.3.10 + github.com/opentdf/platform/sdk v0.3.12 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index b2c11793..751467a1 100644 --- a/go.sum +++ b/go.sum @@ -224,6 +224,8 @@ github.com/opentdf/platform/protocol/go v0.2.14 h1:0wqKDVTpuPICyH37ecKxR2+tZNsgX github.com/opentdf/platform/protocol/go v0.2.14/go.mod h1:WqDcnFQJb0v8ivRQPidbehcL8ils5ZSZYXkuv0nyvsI= github.com/opentdf/platform/sdk v0.3.10 h1:WoPtM6IcwwDIEqCcLq2jb6pd15bFXmEDaju9MKd6JtM= github.com/opentdf/platform/sdk v0.3.10/go.mod h1:XqFivuo4tcqxGwJF9ORnLB3S5bjrgJwiaj6BAJUXJXg= +github.com/opentdf/platform/sdk v0.3.12 h1:1WBiogmIoFseG4xj3j0NXpcQz7a8huHos2KKwaFpYDs= +github.com/opentdf/platform/sdk v0.3.12/go.mod h1:XqFivuo4tcqxGwJF9ORnLB3S5bjrgJwiaj6BAJUXJXg= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= diff --git a/pkg/handlers/nano-tdf.go b/pkg/handlers/nano-tdf.go index a0a85c73..994bc127 100644 --- a/pkg/handlers/nano-tdf.go +++ b/pkg/handlers/nano-tdf.go @@ -5,7 +5,7 @@ import ( "io" ) -func (h Handler) EncryptNanoBytes(b []byte, values []string, kasUrlPath string) (*bytes.Buffer, error) { +func (h Handler) EncryptNanoBytes(b []byte, values []string, kasUrlPath string, ecdsaBinding bool) (*bytes.Buffer, error) { var encrypted []byte enc := bytes.NewBuffer(encrypted) @@ -16,6 +16,9 @@ func (h Handler) EncryptNanoBytes(b []byte, values []string, kasUrlPath string) nanoTDFConfig.SetKasURL(h.platformEndpoint + kasUrlPath) nanoTDFConfig.SetAttributes(values) + if ecdsaBinding { + nanoTDFConfig.EnableECDSAPolicyBinding() + } // TODO: validate values are FQNs or return an error [https://github.com/opentdf/platform/issues/515] _, err = h.sdk.CreateNanoTDF(enc, bytes.NewReader(b), *nanoTDFConfig) diff --git a/pkg/handlers/tdf.go b/pkg/handlers/tdf.go index 461561a5..d4b9062c 100644 --- a/pkg/handlers/tdf.go +++ b/pkg/handlers/tdf.go @@ -3,6 +3,7 @@ package handlers import ( "bytes" "errors" + "fmt" "io" "strings" @@ -47,12 +48,26 @@ func (h Handler) DecryptTDF(toDecrypt []byte) (*bytes.Buffer, error) { } type TDFInspect struct { - Manifest sdk.Manifest + NanoHeader *sdk.NanoTDFHeader + ZTDFManifest *sdk.Manifest Attributes []string UnencryptedMetadata []byte } func (h Handler) InspectTDF(toInspect []byte) (TDFInspect, []error) { + if len(toInspect) < 3 { + return TDFInspect{}, []error{fmt.Errorf("tdf too small [%d] bytes", len(toInspect))} + } + switch { + case bytes.Equal([]byte("PK"), toInspect[0:2]): + return h.InspectZTDF(toInspect) + case bytes.Equal([]byte("L1L"), toInspect[0:3]): + return h.InspectNanoTDF(toInspect) + } + return TDFInspect{}, []error{fmt.Errorf("tdf format unrecognized")} +} + +func (h Handler) InspectZTDF(toInspect []byte) (TDFInspect, []error) { // grouping errors so we don't impact the piping of the data errs := []error{} @@ -74,9 +89,25 @@ func (h Handler) InspectTDF(toInspect []byte) (TDFInspect, []error) { errs = append(errs, errors.Join(ErrTDFUnableToReadUnencryptedMetadata, err)) } + m := tdfreader.Manifest() return TDFInspect{ - Manifest: tdfreader.Manifest(), + ZTDFManifest: &m, Attributes: attributes, UnencryptedMetadata: unencryptedMetadata, }, errs } + +func (h Handler) InspectNanoTDF(toInspect []byte) (TDFInspect, []error) { + header, size, err := sdk.NewNanoTDFHeaderFromReader(bytes.NewReader(toInspect)) + if err != nil { + return TDFInspect{}, []error{errors.Join(ErrTDFInspectFailNotValidTDF, err)} + } + r := TDFInspect{ + NanoHeader: &header, + } + remainder := uint32(len(toInspect)) - size + if remainder < 18 { + return r, []error{ErrTDFInspectFailNotValidTDF} + } + return r, nil +} diff --git a/tests/encrypt-decrypt.bats b/tests/encrypt-decrypt.bats index efaef79f..10e7140e 100755 --- a/tests/encrypt-decrypt.bats +++ b/tests/encrypt-decrypt.bats @@ -53,6 +53,15 @@ teardown() { diff $INFILE_GO_MOD $RESULTFILE_GO_MOD } +@test "roundtrip NANO, no attributes, file, ecdsa binding" { + ./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --ecdsa-binding --tdf-type nano $INFILE_GO_MOD + ./otdfctl decrypt -o $RESULTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --tdf-type nano $OUTFILE_GO_MOD + diff $INFILE_GO_MOD $RESULTFILE_GO_MOD + ./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_GO_MOD + ecdsa_enabled="$(./otdfctl --host $HOST --tls-no-verify $WITH_CREDS inspect $OUTFILE_GO_MOD | jq .ecdsaEnabled)" + [[ "$ecdsa_enabled" == true ]] +} + @test "roundtrip NANO, one attribute, stdin" { echo $SECRET_TEXT | ./otdfctl encrypt --tdf-type nano -o $OUT_TXT --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a $FQN ./otdfctl decrypt --tdf-type nano --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS $OUTFILE_TXT | grep "$SECRET_TEXT"