Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,6 @@ func readPipedStdin() []byte {
return nil
}

func readBytesFromFile(filePath string) []byte {
fileToEncrypt, err := os.Open(filePath)
if err != nil {
cli.ExitWithError(fmt.Sprintf("Failed to open file at path: %s", filePath), err)
}
defer fileToEncrypt.Close()

bytes, err := io.ReadAll(fileToEncrypt)
if err != nil {
cli.ExitWithError(fmt.Sprintf("Failed to read bytes from file at path: %s", filePath), err)
}
return bytes
}

func init() {
designCmd := man.Docs.GetCommand("dev/design-system",
man.WithRun(dev_designSystem),
Expand Down
20 changes: 18 additions & 2 deletions cmd/tdf-decrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import (

"github.com/opentdf/otdfctl/pkg/cli"
"github.com/opentdf/otdfctl/pkg/man"
"github.com/opentdf/otdfctl/pkg/utils"
"github.com/spf13/cobra"
)

var TDF = "tdf"

var assertionVerification string

const TDF_MAX_FILE_SIZE = int64(10 * 1024 * 1024 * 1024) // 10 GB

func dev_tdfDecryptCmd(cmd *cobra.Command, args []string) {
c := cli.New(cmd, args, cli.WithPrintJson())
h := NewHandler(c)
Expand All @@ -26,16 +31,20 @@ func dev_tdfDecryptCmd(cmd *cobra.Command, args []string) {
// Prefer file argument over piped input over default filename
bytesToDecrypt := piped
var tdfFile string
var err error
if len(args) > 0 {
tdfFile = args[0]
bytesToDecrypt = readBytesFromFile(tdfFile)
bytesToDecrypt, err = utils.ReadBytesFromFile(tdfFile, TDF_MAX_FILE_SIZE)
if err != nil {
cli.ExitWithError("Failed to read file:", err)
}
}

if len(bytesToDecrypt) == 0 {
cli.ExitWithError("Must provide ONE of the following to decrypt: [file argument, stdin input]", errors.New("no input provided"))
}

decrypted, err := h.DecryptBytes(bytesToDecrypt, disableAssertionVerification)
decrypted, err := h.DecryptBytes(bytesToDecrypt, assertionVerification, disableAssertionVerification)
if err != nil {
cli.ExitWithError("Failed to decrypt file", err)
}
Expand Down Expand Up @@ -74,6 +83,13 @@ func init() {
decryptCmd.GetDocFlag("tdf-type").Default,
decryptCmd.GetDocFlag("tdf-type").Description,
)
decryptCmd.Flags().StringVarP(
&assertionVerification,
decryptCmd.GetDocFlag("with-assertion-verification-keys").Name,
decryptCmd.GetDocFlag("with-assertion-verification-keys").Shorthand,
"",
decryptCmd.GetDocFlag("with-assertion-verification-keys").Description,
)
decryptCmd.Flags().Bool(
decryptCmd.GetDocFlag("no-verify-assertions").Name,
decryptCmd.GetDocFlag("no-verify-assertions").DefaultAsBool(),
Expand Down
9 changes: 8 additions & 1 deletion cmd/tdf-encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/gabriel-vasile/mimetype"
"github.com/opentdf/otdfctl/pkg/cli"
"github.com/opentdf/otdfctl/pkg/man"
"github.com/opentdf/otdfctl/pkg/utils"
"github.com/spf13/cobra"
)

Expand All @@ -24,6 +25,8 @@ const (
var attrValues []string
var assertions string

const INPUT_MAX_FILE_SIZE = int64(10 * 1024 * 1024 * 1024) // 10 GB

func dev_tdfEncryptCmd(cmd *cobra.Command, args []string) {
c := cli.New(cmd, args, cli.WithPrintJson())
h := NewHandler(c)
Expand Down Expand Up @@ -63,8 +66,12 @@ func dev_tdfEncryptCmd(cmd *cobra.Command, args []string) {

// prefer filepath argument over stdin input
bytesSlice := piped
var err error
if filePath != "" {
bytesSlice = readBytesFromFile(filePath)
bytesSlice, err = utils.ReadBytesFromFile(filePath, INPUT_MAX_FILE_SIZE)
if err != nil {
cli.ExitWithError("Failed to read file:", err)
}
}

// auto-detect mime type if not provided
Expand Down
16 changes: 16 additions & 0 deletions docs/man/decrypt/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ command:
- name: no-verify-assertions
description: disable verification of assertions
default: false
- name: with-assertion-verification-keys
description: >
EXPERIMENTAL: path to JSON file of keys to verify signed assertions. See examples for more information.
---

Decrypt a Trusted Data Format (TDF) file and output the contents to stdout or a file in the current working directory.
Expand Down Expand Up @@ -40,3 +43,16 @@ Advanced piping is supported
$ echo "hello world" | otdfctl encrypt | otdfctl decrypt | cat
hello world
```

### ZTDF Assertion Verification (experimental)

To verify the signed assertions (metadata bound to the TDF), you can provide verification keys. The supported assertion signing algorithms are HS256 and RS256 so the keys provided should either be an HS256 key or a public RS256 key.
```shell
# decrypt file and write to standard output
otdfctl decrypt hello.txt.tdf --with-assertion-verification-keys my_assertion_verification_keys.json
```
Where my_assertion_verification_keys.json looks like:
```json
{"keys":{"assertion1":{ "alg":"HS256","key":"k0cn4xBcY+49z5gs4OHUs/kbQ3/T8p+uUW9pIQ/9aqE="},"assertion2":{ "alg":"RS256","key":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmr0wRsdXN0O9NiltxoGy\nC6ZYwHbdiPVzvOnm9ven5g7Fpm3HOmygdi021WX1OlSua+OSrXGPjM2xbY3LTrFH\nQXQEITjraXQRp5vlKDbBnOrtjYDaKazBXgTYVdelE4AIAuQaGoTudMasHBGiLPEW\niTL4ySec0NzHn2s72Q4hn5/KJpIJOGqj0SlNViufdNylkjrJ3apoYFv1Mhwi3EF/\niFZQ5encDDJmcG/UYF3msbuHRzArJJQ733BNRvicWF/nqixKxprvm8Ts8a54tr8N\nZ7cEu1u5G6AY/pZFGk4ml8q3v5o1ja7xw2dgpJlS8Tl88tUzs+7GG8Ib8n7mHqeP\nTQIDAQAB\n-----END PUBLIC KEY-----\n"}}}
```
If no verification keys are provided, the SDK will default to verifying using the payload key. If the assertions were not signed with the payload key, the decrypt call will fail.
24 changes: 20 additions & 4 deletions docs/man/encrypt/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ command:
default: /kas
- name: with-assertions
description: >
EXPERIMENTAL: JSON string of assertions to bind metadata to the TDF. See examples for more information.
EXPERIMENTAL: JSON string or path to a JSON file of assertions to bind metadata to the TDF. See examples for more information. WARNING: Providing keys in a JSON string is strongly discouraged. If including sensitive keys, instead provide a path to a JSON file containing that information.
---

Build a Trusted Data Format (TDF) with encrypted content from a specified file or input from stdin utilizing OpenTDF platform.
Expand Down Expand Up @@ -86,12 +86,28 @@ otdfctl encrypt hello.txt --tdf-type nano --out hello.txt.tdf

## ZTDF Assertions (experimental)

Assertions are a way to bind metadata to the TDF data object in a cryptographically secure way.
Assertions are a way to bind metadata to the TDF data object in a cryptographically secure way. The data is signed with the provided signing key, or if none is provided, the payload key. The signing key algorithms supported are HS256 and RS256.

### STANAG 5636

The following example demonstrates how to bind a STANAG 5636 metadata assertion to the TDF data object.
The following example demonstrates how to bind a STANAG 5636 metadata assertion, to the TDF data object.

```shell
otdfctl encrypt hello.txt --out hello.txt.tdf --with-assertions '[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"}}]'
otdfctl encrypt hello.txt --out hello.txt.tdf --with-assertions '[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"}]'
```

We also support providing an assertions json file.
You can optionally provide your own signing key. In this example, we provide an RS256 private key.
```json
[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}","signingKey":{"alg":"RS256","key":"-----BEGIN PRIVATE KEY-----\nMIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQCavTBGx1c3Q702\nKW3GgbILpljAdt2I9XO86eb296fmDsWmbcc6bKB2LTbVZfU6VK5r45KtcY+MzbFt\njctOsUdBdAQhOOtpdBGnm+UoNsGc6u2NgNoprMFeBNhV16UTgAgC5BoahO50xqwc\nEaIs8RaJMvjJJ5zQ3MefazvZDiGfn8omkgk4aqPRKU1WK5903KWSOsndqmhgW/Uy\nHCLcQX+IVlDl6dwMMmZwb9RgXeaxu4dHMCsklDvfcE1G+JxYX+eqLErGmu+bxOzx\nrni2vw1ntwS7W7kboBj+lkUaTiaXyre/mjWNrvHDZ2CkmVLxOXzy1TOz7sYbwhvy\nfuYep49NAgMBAAECgf8N2RrYrTRyIZmlzMJZgpc4gCujIqSPjJfEn3D5XC5+w9XA\nu/lfONZbn/9Y6/CeTgRcpYRNKO9QI0pb3RQzgiLBO+/Z1UJjtORxR0gXdJ0XXVTz\ntLWsD4dCycpkyT8snLkMQFdzXXRAefNyYdavOVz0kvCNgGgw606rZhkYbtHUCM3X\nb1LZFcIAYrpftKUXxn+xOcSjIKdqKoUlBW6Yk7iTjJuy/Su63gTJ5PbgKpNvK7Xu\nyzu4L7t2pswE5pWxb7uMMpTujqLNYiaXDlzpy/fPN8EjL1mhKzia365+EJ3uKH8c\nQ9dz/1g36lSQnD/lus0cES9xXzQ6+1izc17dTsECgYEA1XGM4PVxCt4TaApDoT7X\npeLDG9pQW55DQQiix4A/0EmQgxf6WN0uZ4b8lds02JhNBGVUIe2nyTNknV+9styu\nJsKJhq+KjrcHmE8uy18++G2cZuOM2S49p8y0HPA8YBcRBC4fAoKFFG3cmrIJW5Vu\nMzzaN+W3/1h/xdkUTpI1lYkCgYEAuZdHWrMNt96WMUuaSwu2tg3BHaYhSeyIcbwi\nm2mIOeLQ6gGtGqyALC6N/K8Ie8KwkisTI9GqcX8O9FrkZx4RvkQrONUaS4aXEJ28\nEZzwJenybkSuWunypVLMmp/pN7+mZZ7GUaDbXTF6pg4GOrlp6MIUk4plJYGXXumg\nqaXvPqUCgYA0pmvf2etmiN00nsOL9Npw+vyx1CpaTzG7ywuMNqCHGn5hN/rzDKwz\nsWKA/K+OdhMZcH1OWTc4NEsvXryGcFUtDnOqG4cMKS3gbjfWxsnbsf4QizTlJbjj\nuWT8dm4OLeJuq4nOrq9xGKCAMEaKptOmI+6YNzwp6oSqIyAVOY+qMQKBgDM7IlRU\nNwY5qIYlE4uByUcKFvQDRw8r/yI+R+NUx2kLRpZCLjG9yofntgQ5oQLg5HME9vyd\nRQqdg1hKuuAIOeem07OVh/OvTIYmtKK8CsK8iNKNnP+1suiWKarJV8yu19UXdjFU\nURmxreSm3GtbgXPiF2H/AxrOYiWuIk6SYq+NAoGAZy96GLP3HfA41UWFZH6b8ZdP\nM6CXKDDvHOk06S/hwmhvq3UO5lQULZ+pd+aURv/TDF9DXhZIyl1CXqyOYB5IqJjk\nAFI8A9n/naq7GyIZZRjzJu2blhSjW3ukkS/5CO4zJ6HfauSUjQA4u+5RStjeK3zd\nF267fElUPN4+pSOAhPI=\n-----END PRIVATE KEY-----\n"}}]
```
```shell
otdfctl encrypt hello.txt --out hello.txt.tdf --with-assertions my_assertions_signed_rs256.json
```
Signing with HS256 is also available.
```json
[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}","signingKey":{"alg":"HS256","key":"k0cn4xBcY+49z5gs4OHUs/kbQ3/T8p+uUW9pIQ/9aqE="}}]
```
```shell
otdfctl encrypt hello.txt --out hello.txt.tdf --with-assertions my_assertions_signed_hs256.json
```
38 changes: 37 additions & 1 deletion e2e/encrypt-decrypt.bats
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,30 @@ setup_file() {
VAL_ID=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy attributes values create --attribute-id "$ATTR_ID" -v value1 --json | jq -r '.id')
# entitles opentdf client id for client credentials CLI user
SCS='[{"condition_groups":[{"conditions":[{"operator":1,"subject_external_values":["opentdf"],"subject_external_selector_value":".clientId"}],"boolean_operator":2}]}]'
ASSERTIONS='[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"}}]'

# assertions setup
HS256_KEY=$(openssl rand -base64 32)
RS_PRIVATE_KEY=rs_private_key.pem
RS_PUBLIC_KEY=rs_public_key.pem
openssl genpkey -algorithm RSA -out $RS_PRIVATE_KEY -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in $RS_PRIVATE_KEY -out $RS_PUBLIC_KEY

export ASSERTIONS='[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"}}]'

export SIGNED_ASSERTIONS_HS256=signed_assertions_hs256.json
export SIGNED_ASSERTION_VERIFICATON_HS256=assertion_verification_hs256.json
export SIGNED_ASSERTIONS_RS256=signed_assertion_rs256.json
export SIGNED_ASSERTION_VERIFICATON_RS256=assertion_verification_rs256.json
echo '[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"},"signingKey":{"alg":"HS256","key":"replace"}}]' > $SIGNED_ASSERTIONS_HS256
jq --arg pem "$(echo $HS256_KEY)" '.[0].signingKey.key = $pem' $SIGNED_ASSERTIONS_HS256 > tmp.json && mv tmp.json $SIGNED_ASSERTIONS_HS256
echo '{"keys":{"assertion1":{"alg":"HS256","key":"replace"}}}' > $SIGNED_ASSERTION_VERIFICATON_HS256
jq --arg pem "$(echo $HS256_KEY)" '.keys.assertion1.key = $pem' $SIGNED_ASSERTION_VERIFICATON_HS256 > tmp.json && mv tmp.json $SIGNED_ASSERTION_VERIFICATON_HS256
echo '[{"id":"assertion1","type":"handling","scope":"tdo","appliesToState":"encrypted","statement":{"format":"json+stanag5636","schema":"urn:nato:stanag:5636:A:1:elements:json","value":"{\"ocl\":\"2024-10-21T20:47:36Z\"}"},"signingKey":{"alg":"RS256","key":"replace"}}]' > $SIGNED_ASSERTIONS_RS256
jq --arg pem "$(<$RS_PRIVATE_KEY)" '.[0].signingKey.key = $pem' $SIGNED_ASSERTIONS_RS256 > tmp.json && mv tmp.json $SIGNED_ASSERTIONS_RS256
echo '{"keys":{"assertion1":{"alg":"RS256","key":"replace"}}}' > $SIGNED_ASSERTION_VERIFICATON_RS256
jq --arg pem "$(<$RS_PUBLIC_KEY)" '.keys.assertion1.key = $pem' $SIGNED_ASSERTION_VERIFICATON_RS256 > tmp.json && mv tmp.json $SIGNED_ASSERTION_VERIFICATON_RS256


SM=$(./otdfctl --host $HOST $WITH_CREDS $DEBUG_LEVEL policy subject-mappings create --action-standard DECRYPT -a "$VAL_ID" --subject-condition-set-new "$SCS")
export FQN="https://testing-enc-dec.io/attr/attr1/value/value1"
export MIXED_CASE_FQN="https://Testing-Enc-Dec.io/attr/Attr1/value/VALUE1"
Expand All @@ -34,6 +57,7 @@ teardown() {

teardown_file(){
./otdfctl --host "$HOST" $WITH_CREDS policy attributes namespaces unsafe delete --id "$NS_ID" --force
rm -f $SIGNED_ASSERTIONS_HS256 $SIGNED_ASSERTION_VERIFICATON_HS256 $SIGNED_ASSERTIONS_RS256 $SIGNED_ASSERTION_VERIFICATON_RS256
}

@test "roundtrip TDF3, no attributes, file" {
Expand All @@ -57,6 +81,18 @@ teardown_file(){
./otdfctl decrypt --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS $OUTFILE_TXT | grep "$SECRET_TEXT"
}

@test "roundtrip TDF3, assertions with HS265 keys and verificaion, file" {
./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a $FQN --with-assertions $SIGNED_ASSERTIONS_HS256 --tdf-type tdf3 $INFILE_GO_MOD
./otdfctl decrypt -o $RESULTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --with-assertion-verification-keys $SIGNED_ASSERTION_VERIFICATON_HS256 --tdf-type tdf3 $OUTFILE_GO_MOD
diff $INFILE_GO_MOD $RESULTFILE_GO_MOD
}

@test "roundtrip TDF3, assertions with RS256 keys and verificaion, file" {
./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS -a $FQN --with-assertions $SIGNED_ASSERTIONS_RS256 --tdf-type tdf3 $INFILE_GO_MOD
./otdfctl decrypt -o $RESULTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --with-assertion-verification-keys $SIGNED_ASSERTION_VERIFICATON_RS256 --tdf-type tdf3 $OUTFILE_GO_MOD
diff $INFILE_GO_MOD $RESULTFILE_GO_MOD
}

@test "roundtrip NANO, no attributes, file" {
./otdfctl encrypt -o $OUTFILE_GO_MOD --host $HOST --tls-no-verify $DEBUG_LEVEL $WITH_CREDS --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
Expand Down
Loading
Loading