diff --git a/cmd/dev.go b/cmd/dev.go index ff79d882..db5c44cb 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -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), diff --git a/cmd/tdf-decrypt.go b/cmd/tdf-decrypt.go index 70d65661..7651cdf4 100644 --- a/cmd/tdf-decrypt.go +++ b/cmd/tdf-decrypt.go @@ -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) @@ -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) } @@ -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(), diff --git a/cmd/tdf-encrypt.go b/cmd/tdf-encrypt.go index aa9e20ae..9b8a027f 100644 --- a/cmd/tdf-encrypt.go +++ b/cmd/tdf-encrypt.go @@ -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" ) @@ -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) @@ -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 diff --git a/docs/man/decrypt/_index.md b/docs/man/decrypt/_index.md index 35ed235b..682d53a5 100644 --- a/docs/man/decrypt/_index.md +++ b/docs/man/decrypt/_index.md @@ -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. @@ -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. \ No newline at end of file diff --git a/docs/man/encrypt/_index.md b/docs/man/encrypt/_index.md index 2b6c76b8..811452c9 100644 --- a/docs/man/encrypt/_index.md +++ b/docs/man/encrypt/_index.md @@ -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. @@ -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 +``` \ No newline at end of file diff --git a/e2e/encrypt-decrypt.bats b/e2e/encrypt-decrypt.bats index 44af89c7..4c8717fc 100755 --- a/e2e/encrypt-decrypt.bats +++ b/e2e/encrypt-decrypt.bats @@ -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" @@ -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" { @@ -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 diff --git a/pkg/handlers/tdf.go b/pkg/handlers/tdf.go index b789c1e4..1aee1503 100644 --- a/pkg/handlers/tdf.go +++ b/pkg/handlers/tdf.go @@ -2,27 +2,33 @@ package handlers import ( "bytes" + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "errors" "fmt" "io" "strings" + "github.com/opentdf/otdfctl/pkg/utils" "github.com/opentdf/platform/sdk" ) var ( - ErrTDFInspectFailNotValidTDF = errors.New("file or input is not a valid TDF") - ErrTDFInspectFailNotInspectable = errors.New("file or input is not inspectable") - ErrTDFUnableToReadAttributes = errors.New("unable to read attributes from TDF") - ErrTDFUnableToReadUnencryptedMetadata = errors.New("unable to read unencrypted metadata from TDF") - ErrTDFUnableToReadAssertions = errors.New("unable to read assertions") + ErrTDFInspectFailNotValidTDF = errors.New("file or input is not a valid TDF") + ErrTDFInspectFailNotInspectable = errors.New("file or input is not inspectable") + ErrTDFUnableToReadAttributes = errors.New("unable to read attributes from TDF") + ErrTDFUnableToReadUnencryptedMetadata = errors.New("unable to read unencrypted metadata from TDF") + ErrTDFUnableToReadAssertions = errors.New("unable to read assertions") + ErrTDFUnableToReadAssertionVerificationKeys = errors.New("unable to read assertion verification keys") ) const ( - TDF_TYPE_ZTDF = "ztdf" - TDF_TYPE_TDF3 = "tdf3" // alias for TDF - TDF_TYPE_NANO = "nano" + TDF_TYPE_ZTDF = "ztdf" + TDF_TYPE_TDF3 = "tdf3" // alias for TDF + TDF_TYPE_NANO = "nano" + MAX_ASSERTIONS_FILE_SIZE = int64(5 * 1024 * 1024) // 5MB ) type TDFInspect struct { @@ -52,10 +58,28 @@ func (h Handler) EncryptBytes(tdfType string, unencrypted []byte, attrValues []s } var assertionConfigs []sdk.AssertionConfig + //nolint:nestif // nested its mainly for error catching and handling case of string vs file if assertions != "" { err := json.Unmarshal([]byte(assertions), &assertionConfigs) if err != nil { - return nil, errors.Join(ErrTDFUnableToReadAssertions, err) + // if unable to marshal to json, interpret as file string and try to read from file + assertionBytes, err := utils.ReadBytesFromFile(assertions, MAX_ASSERTIONS_FILE_SIZE) + if err != nil { + return nil, fmt.Errorf("unable to read assertions file: %w", err) + } + err = json.Unmarshal(assertionBytes, &assertionConfigs) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal assertions json: %w", err) + } + } + for i, config := range assertionConfigs { + if (config.SigningKey != sdk.AssertionKey{}) { + correctedKey, err := correctKeyType(config.SigningKey, false) + if err != nil { + return nil, fmt.Errorf("error with assertion signing key: %w", err) + } + assertionConfigs[i].SigningKey.Key = correctedKey + } } opts = append(opts, sdk.WithAssertions(assertionConfigs...)) } @@ -91,7 +115,7 @@ func (h Handler) EncryptBytes(tdfType string, unencrypted []byte, attrValues []s } } -func (h Handler) DecryptBytes(toDecrypt []byte, disableAssertionCheck bool) (*bytes.Buffer, error) { +func (h Handler) DecryptBytes(toDecrypt []byte, assertionVerificationKeysFile string, disableAssertionCheck bool) (*bytes.Buffer, error) { out := &bytes.Buffer{} pt := io.Writer(out) ec := bytes.NewReader(toDecrypt) @@ -101,7 +125,28 @@ func (h Handler) DecryptBytes(toDecrypt []byte, disableAssertionCheck bool) (*by return nil, err } case sdk.Standard: - r, err := h.sdk.LoadTDF(ec, sdk.WithDisableAssertionVerification(disableAssertionCheck)) + opts := []sdk.TDFReaderOption{sdk.WithDisableAssertionVerification(disableAssertionCheck)} + var assertionVerificationKeys sdk.AssertionVerificationKeys + if assertionVerificationKeysFile != "" { + // read the file + assertionVerificationBytes, err := utils.ReadBytesFromFile(assertionVerificationKeysFile, MAX_ASSERTIONS_FILE_SIZE) + if err != nil { + return nil, fmt.Errorf("unable to read assertions verification keys file: %w", err) + } + err = json.Unmarshal(assertionVerificationBytes, &assertionVerificationKeys) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal assertion verification keys json: %w", err) + } + for assertionName, key := range assertionVerificationKeys.Keys { + correctedKey, err := correctKeyType(key, true) + if err != nil { + return nil, fmt.Errorf("error with assertion signing key: %w", err) + } + assertionVerificationKeys.Keys[assertionName] = sdk.AssertionKey{Alg: key.Alg, Key: correctedKey} + } + opts = append(opts, sdk.WithAssertionVerificationKeys(assertionVerificationKeys)) + } + r, err := h.sdk.LoadTDF(ec, opts...) if err != nil { return nil, err } @@ -170,3 +215,63 @@ func (h Handler) InspectTDF(toInspect []byte) (TDFInspect, []error) { return TDFInspect{}, []error{fmt.Errorf("tdf format unrecognized")} } } + +func correctKeyType(assertionKey sdk.AssertionKey, public bool) (interface{}, error) { + strKey, ok := assertionKey.Key.(string) + if !ok { + return nil, errors.New("unable to convert assertion key to string") + } + //nolint:nestif // nested its within switch mainly for error catching + if assertionKey.Alg == sdk.AssertionKeyAlgHS256 { + // convert the hs256 key to []byte + return []byte(strKey), nil + } else if assertionKey.Alg == sdk.AssertionKeyAlgRS256 { + // Decode the PEM block + block, _ := pem.Decode([]byte(strKey)) + if block == nil { + return nil, errors.New("failed to decode PEM block") + } + + // Check the block type and parse accordingly + var privateKey *rsa.PrivateKey + var publicKey *rsa.PublicKey + var err error + switch block.Type { + case "RSA PRIVATE KEY": + privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + publicKey = &privateKey.PublicKey + case "PRIVATE KEY": + parsedKey, parseErr := x509.ParsePKCS8PrivateKey(block.Bytes) + if parseErr != nil { + return nil, fmt.Errorf("failed to parse PKCS#8 private key: %w", parseErr) + } + privateKey, ok = parsedKey.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("parsed key is not an RSA private key") + } + publicKey = &privateKey.PublicKey + case "RSA PUBLIC KEY": + publicKey, err = x509.ParsePKCS1PublicKey(block.Bytes) + case "PUBLIC KEY": + parsedKey, parseErr := x509.ParsePKIXPublicKey(block.Bytes) + if parseErr != nil { + return nil, fmt.Errorf("failed to parse PKIX public key: %w", parseErr) + } + publicKey, ok = parsedKey.(*rsa.PublicKey) + if !ok { + return nil, errors.New("parsed key is not an RSA public key") + } + default: + return nil, fmt.Errorf("unsupported key type: %s", block.Type) + } + + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + if public { + return publicKey, nil + } + return privateKey, nil + } + return nil, fmt.Errorf("unsupported signing key alg: %v", assertionKey.Alg) +} diff --git a/pkg/utils/read.go b/pkg/utils/read.go new file mode 100644 index 00000000..093a3192 --- /dev/null +++ b/pkg/utils/read.go @@ -0,0 +1,34 @@ +package utils + +import ( + "fmt" + "io" + "os" +) + +func ReadBytesFromFile(filePath string, maxBytes int64) ([]byte, error) { + fileInfo, err := os.Stat(filePath) + if err != nil { + return nil, fmt.Errorf("failed to stat file at path %s: %w", filePath, err) + } + + // Check if the file size exceeds the limit + if fileInfo.Size() > maxBytes { + return nil, fmt.Errorf("file size exceeds the limit of %d bytes", maxBytes) + } + + fileToEncrypt, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("failed to open file at path %s: %w", filePath, err) + } + defer fileToEncrypt.Close() + + // Limit the reader to the specified maximum number of bytes + limitedReader := io.LimitReader(fileToEncrypt, maxBytes) + bytes, err := io.ReadAll(limitedReader) + if err != nil { + return nil, fmt.Errorf("failed to read bytes from file at path %s: %w", filePath, err) + } + + return bytes, nil +}