From 5a555dca73ce89bfb4bb685303144a518a9469f1 Mon Sep 17 00:00:00 2001 From: Prabakaran Chinnasamy Date: Wed, 29 Jul 2026 13:01:24 +0530 Subject: [PATCH] OpenSSL Provider: Support AES-CBC cipher, Random number and ECDSA --- score/crypto/src/api/BUILD | 3 + score/crypto/src/api/config/BUILD | 4 + .../config/cipher_context_config.hpp | 6 +- .../config/random_context_config.hpp | 6 +- .../config/sign_context_config.hpp | 6 +- .../verify_signature_context_config.hpp | 6 +- score/crypto/src/api/contexts/BUILD | 12 + .../contexts/i_cipher_context.hpp | 6 +- .../contexts/i_random_context.hpp | 6 +- .../{future => }/contexts/i_sign_context.hpp | 6 +- .../contexts/i_verify_signature_context.hpp | 6 +- .../api/contexts/src/cipher_context_impl.cpp | 379 ++++++++++ .../api/contexts/src/cipher_context_impl.hpp | 90 +++ .../src/key_management_context_impl.cpp | 22 +- .../api/contexts/src/random_context_impl.cpp | 222 ++++++ .../api/contexts/src/random_context_impl.hpp | 79 ++ .../api/contexts/src/sign_context_impl.cpp | 352 +++++++++ .../api/contexts/src/sign_context_impl.hpp | 94 +++ .../src/verify_signature_context_impl.cpp | 327 ++++++++ .../src/verify_signature_context_impl.hpp | 89 +++ score/crypto/src/api/future/config/BUILD | 4 - score/crypto/src/api/future/contexts/BUILD | 33 - score/crypto/src/api/i_crypto_context.hpp | 49 +- .../src/api/src/crypto_context_impl.cpp | 315 ++++++-- .../src/api/src/crypto_context_impl.hpp | 5 + score/crypto/src/backend/BUILD | 4 +- score/crypto/src/daemon/common/BUILD | 1 + score/crypto/src/daemon/common/actors.hpp | 4 + .../src/daemon/common/algorithm_info.hpp | 103 +++ .../src/daemon/common/context_types.hpp | 99 +++ .../daemon/control_plane/control_protocol.h | 22 + .../interfaces/key_management_operations.hpp | 5 +- .../key_management/interfaces/key_types.hpp | 46 ++ .../src/daemon/mediator/src/mediator_impl.cpp | 98 ++- .../src/daemon/mediator/src/mediator_impl.hpp | 25 + .../executors/key_mgmt_request_parser.hpp | 34 +- .../executors/src/key_mgmt_executor.cpp | 3 +- .../crypto/src/daemon/provider/handler/BUILD | 24 + .../operations/cipher_handler_operations.hpp | 120 +++ .../operations/random_handler_operations.hpp | 68 ++ .../signature_handler_operations.hpp | 149 ++++ .../pkcs11_key_slot_handler.cpp | 4 +- .../key_management/pkcs11_key_store.cpp | 10 +- .../key_management/pkcs11_key_store.hpp | 17 +- .../factory/pkcs11_handler_factory.hpp | 11 +- .../provider/score_provider/openssl/BUILD | 9 + .../key_management/openssl_key_factory.cpp | 188 ++++- .../key_management/openssl_key_factory.hpp | 23 +- .../key_management/openssl_key_handler.cpp | 54 +- .../key_management/openssl_key_handler.hpp | 50 +- .../cipher/openssl_cipher_handler.cpp | 317 ++++++++ .../cipher/openssl_cipher_handler.hpp | 104 +++ .../factory/openssl_handler_factory.cpp | 48 ++ .../factory/openssl_handler_factory.hpp | 6 + .../random/openssl_random_handler.cpp | 85 +++ .../random/openssl_random_handler.hpp | 77 ++ .../signature/openssl_ecdsa_handler.cpp | 417 +++++++++++ .../signature/openssl_ecdsa_handler.hpp | 124 ++++ .../score_provider/operations/cipher/BUILD | 36 + .../operations/cipher/cipher_executor.hpp | 69 ++ .../cipher/score_cipher_handler.hpp | 145 ++++ .../operations/cipher/src/cipher_executor.cpp | 280 +++++++ .../cipher/src/score_cipher_handler.cpp | 108 +++ .../factory/score_handler_factory.hpp | 30 +- .../factory/src/score_handler_factory.cpp | 44 ++ .../score_provider/operations/random/BUILD | 34 + .../operations/random/random_executor.hpp | 51 ++ .../random/score_random_handler.hpp | 100 +++ .../operations/random/src/random_executor.cpp | 100 +++ .../random/src/score_random_handler.cpp | 62 ++ .../score_provider/operations/signature/BUILD | 37 + .../signature/score_signature_handler.hpp | 144 ++++ .../signature/signature_executor.hpp | 71 ++ .../signature/src/score_signature_handler.cpp | 106 +++ .../signature/src/signature_executor.cpp | 323 ++++++++ .../test_pkcs11_key_handler.cpp | 4 +- score/tests/conftest.py | 17 + score/tests/integration_tests/BUILD | 100 ++- .../integration_tests/integration_test.py | 54 ++ .../score_api_cipher_test.cpp | 676 +++++++++++++++++ .../score_api_ecdsa_test.cpp | 696 ++++++++++++++++++ .../score_api_key_permissions_test.cpp | 630 ++++++++++++++++ .../score_api_random_test.cpp | 268 +++++++ .../block_cipher/CBC-AES128/BUILD | 28 + .../block_cipher/CBC-AES128/reference.md | 47 ++ .../CBC-AES128/vector1_ciphertext.bin | 1 + .../block_cipher/CBC-AES128/vector1_iv.bin | 1 + .../block_cipher/CBC-AES128/vector1_key.bin | 1 + .../CBC-AES128/vector1_plaintext.bin | 1 + .../CBC-AES128/vector2_ciphertext.bin | 1 + .../block_cipher/CBC-AES128/vector2_iv.bin | 1 + .../block_cipher/CBC-AES128/vector2_key.bin | 1 + .../CBC-AES128/vector2_plaintext.bin | 1 + .../block_cipher/CBC-AES192/BUILD | 28 + .../block_cipher/CBC-AES192/reference.md | 47 ++ .../CBC-AES192/vector1_ciphertext.bin | 1 + .../block_cipher/CBC-AES192/vector1_iv.bin | 1 + .../block_cipher/CBC-AES192/vector1_key.bin | 1 + .../CBC-AES192/vector1_plaintext.bin | 1 + .../CBC-AES192/vector2_ciphertext.bin | 2 + .../block_cipher/CBC-AES192/vector2_iv.bin | 1 + .../block_cipher/CBC-AES192/vector2_key.bin | 1 + .../CBC-AES192/vector2_plaintext.bin | 1 + .../block_cipher/CBC-AES256/BUILD | 28 + .../block_cipher/CBC-AES256/reference.md | 47 ++ .../CBC-AES256/vector1_ciphertext.bin | 2 + .../block_cipher/CBC-AES256/vector1_iv.bin | 1 + .../block_cipher/CBC-AES256/vector1_key.bin | 1 + .../CBC-AES256/vector1_plaintext.bin | 1 + .../CBC-AES256/vector2_ciphertext.bin | 1 + .../block_cipher/CBC-AES256/vector2_iv.bin | 1 + .../block_cipher/CBC-AES256/vector2_key.bin | 1 + .../CBC-AES256/vector2_plaintext.bin | 1 + score/tests/test_vectors/config/BUILD | 4 +- .../config/integration_openssl_aes128_cbc.kv | 5 + .../config/integration_openssl_aes192_cbc.kv | 5 + .../config/integration_openssl_aes256_cbc.kv | 5 + .../config/integration_openssl_ecdsa_p256.kv | 5 + .../integration_openssl_ecdsa_p256_pub.kv | 5 + .../config/integration_openssl_ecdsa_p384.kv | 5 + .../integration_openssl_ecdsa_p384_pub.kv | 5 + .../config/integration_openssl_ecdsa_p521.kv | 5 + .../integration_openssl_ecdsa_p521_pub.kv | 5 + .../config/integration_test_config.json | 438 ++++++++--- score/tests/test_vectors/ecdsa/BUILD | 36 + .../ecdsa/P256-SHA256/reference.md | 54 ++ .../ecdsa/P256-SHA256/siggen_message.bin | 1 + .../ecdsa/P256-SHA256/siggen_private_key.der | Bin 0 -> 138 bytes .../ecdsa/P256-SHA256/siggen_public_key.der | Bin 0 -> 91 bytes .../ecdsa/P256-SHA256/siggen_signature.bin | 1 + .../P256-SHA256/sigver_invalid_message.bin | 1 + .../P256-SHA256/sigver_invalid_public_key.der | Bin 0 -> 91 bytes .../P256-SHA256/sigver_invalid_signature.bin | 1 + .../ecdsa/P384-SHA384/reference.md | 54 ++ .../ecdsa/P384-SHA384/siggen_message.bin | Bin 0 -> 128 bytes .../ecdsa/P384-SHA384/siggen_private_key.der | Bin 0 -> 185 bytes .../ecdsa/P384-SHA384/siggen_public_key.der | Bin 0 -> 120 bytes .../ecdsa/P384-SHA384/siggen_signature.bin | Bin 0 -> 96 bytes .../P384-SHA384/sigver_invalid_message.bin | Bin 0 -> 128 bytes .../P384-SHA384/sigver_invalid_public_key.der | Bin 0 -> 120 bytes .../P384-SHA384/sigver_invalid_signature.bin | Bin 0 -> 96 bytes .../ecdsa/P521-SHA512/reference.md | 54 ++ .../ecdsa/P521-SHA512/siggen_message.bin | 1 + .../ecdsa/P521-SHA512/siggen_private_key.der | Bin 0 -> 241 bytes .../ecdsa/P521-SHA512/siggen_public_key.der | Bin 0 -> 158 bytes .../ecdsa/P521-SHA512/siggen_signature.bin | Bin 0 -> 132 bytes .../P521-SHA512/sigver_invalid_message.bin | 1 + .../P521-SHA512/sigver_invalid_public_key.der | Bin 0 -> 158 bytes .../P521-SHA512/sigver_invalid_signature.bin | Bin 0 -> 132 bytes 149 files changed, 9261 insertions(+), 342 deletions(-) rename score/crypto/src/api/{future => }/config/cipher_context_config.hpp (93%) rename score/crypto/src/api/{future => }/config/random_context_config.hpp (90%) rename score/crypto/src/api/{future => }/config/sign_context_config.hpp (91%) rename score/crypto/src/api/{future => }/config/verify_signature_context_config.hpp (90%) rename score/crypto/src/api/{future => }/contexts/i_cipher_context.hpp (95%) rename score/crypto/src/api/{future => }/contexts/i_random_context.hpp (92%) rename score/crypto/src/api/{future => }/contexts/i_sign_context.hpp (94%) rename score/crypto/src/api/{future => }/contexts/i_verify_signature_context.hpp (92%) create mode 100644 score/crypto/src/api/contexts/src/cipher_context_impl.cpp create mode 100644 score/crypto/src/api/contexts/src/cipher_context_impl.hpp create mode 100644 score/crypto/src/api/contexts/src/random_context_impl.cpp create mode 100644 score/crypto/src/api/contexts/src/random_context_impl.hpp create mode 100644 score/crypto/src/api/contexts/src/sign_context_impl.cpp create mode 100644 score/crypto/src/api/contexts/src/sign_context_impl.hpp create mode 100644 score/crypto/src/api/contexts/src/verify_signature_context_impl.cpp create mode 100644 score/crypto/src/api/contexts/src/verify_signature_context_impl.hpp create mode 100644 score/crypto/src/daemon/common/context_types.hpp create mode 100644 score/crypto/src/daemon/provider/handler/operations/cipher_handler_operations.hpp create mode 100644 score/crypto/src/daemon/provider/handler/operations/random_handler_operations.hpp create mode 100644 score/crypto/src/daemon/provider/handler/operations/signature_handler_operations.hpp create mode 100644 score/crypto/src/daemon/provider/score_provider/openssl/operations/cipher/openssl_cipher_handler.cpp create mode 100644 score/crypto/src/daemon/provider/score_provider/openssl/operations/cipher/openssl_cipher_handler.hpp create mode 100644 score/crypto/src/daemon/provider/score_provider/openssl/operations/random/openssl_random_handler.cpp create mode 100644 score/crypto/src/daemon/provider/score_provider/openssl/operations/random/openssl_random_handler.hpp create mode 100644 score/crypto/src/daemon/provider/score_provider/openssl/operations/signature/openssl_ecdsa_handler.cpp create mode 100644 score/crypto/src/daemon/provider/score_provider/openssl/operations/signature/openssl_ecdsa_handler.hpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/cipher/BUILD create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/cipher/cipher_executor.hpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/cipher/score_cipher_handler.hpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/cipher/src/cipher_executor.cpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/cipher/src/score_cipher_handler.cpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/random/BUILD create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/random/random_executor.hpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/random/score_random_handler.hpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/random/src/random_executor.cpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/random/src/score_random_handler.cpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/signature/BUILD create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/signature/score_signature_handler.hpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/signature/signature_executor.hpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/signature/src/score_signature_handler.cpp create mode 100644 score/crypto/src/daemon/provider/score_provider/operations/signature/src/signature_executor.cpp create mode 100644 score/tests/integration_tests/score_api_cipher_test.cpp create mode 100644 score/tests/integration_tests/score_api_ecdsa_test.cpp create mode 100644 score/tests/integration_tests/score_api_key_permissions_test.cpp create mode 100644 score/tests/integration_tests/score_api_random_test.cpp create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES128/BUILD create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES128/reference.md create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES128/vector1_ciphertext.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES128/vector1_iv.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES128/vector1_key.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES128/vector1_plaintext.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES128/vector2_ciphertext.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES128/vector2_iv.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES128/vector2_key.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES128/vector2_plaintext.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES192/BUILD create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES192/reference.md create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES192/vector1_ciphertext.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES192/vector1_iv.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES192/vector1_key.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES192/vector1_plaintext.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES192/vector2_ciphertext.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES192/vector2_iv.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES192/vector2_key.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES192/vector2_plaintext.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES256/BUILD create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES256/reference.md create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES256/vector1_ciphertext.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES256/vector1_iv.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES256/vector1_key.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES256/vector1_plaintext.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES256/vector2_ciphertext.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES256/vector2_iv.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES256/vector2_key.bin create mode 100644 score/tests/test_vectors/block_cipher/CBC-AES256/vector2_plaintext.bin create mode 100644 score/tests/test_vectors/config/integration_openssl_aes128_cbc.kv create mode 100644 score/tests/test_vectors/config/integration_openssl_aes192_cbc.kv create mode 100644 score/tests/test_vectors/config/integration_openssl_aes256_cbc.kv create mode 100644 score/tests/test_vectors/config/integration_openssl_ecdsa_p256.kv create mode 100644 score/tests/test_vectors/config/integration_openssl_ecdsa_p256_pub.kv create mode 100644 score/tests/test_vectors/config/integration_openssl_ecdsa_p384.kv create mode 100644 score/tests/test_vectors/config/integration_openssl_ecdsa_p384_pub.kv create mode 100644 score/tests/test_vectors/config/integration_openssl_ecdsa_p521.kv create mode 100644 score/tests/test_vectors/config/integration_openssl_ecdsa_p521_pub.kv create mode 100644 score/tests/test_vectors/ecdsa/BUILD create mode 100644 score/tests/test_vectors/ecdsa/P256-SHA256/reference.md create mode 100644 score/tests/test_vectors/ecdsa/P256-SHA256/siggen_message.bin create mode 100644 score/tests/test_vectors/ecdsa/P256-SHA256/siggen_private_key.der create mode 100644 score/tests/test_vectors/ecdsa/P256-SHA256/siggen_public_key.der create mode 100644 score/tests/test_vectors/ecdsa/P256-SHA256/siggen_signature.bin create mode 100644 score/tests/test_vectors/ecdsa/P256-SHA256/sigver_invalid_message.bin create mode 100644 score/tests/test_vectors/ecdsa/P256-SHA256/sigver_invalid_public_key.der create mode 100644 score/tests/test_vectors/ecdsa/P256-SHA256/sigver_invalid_signature.bin create mode 100644 score/tests/test_vectors/ecdsa/P384-SHA384/reference.md create mode 100644 score/tests/test_vectors/ecdsa/P384-SHA384/siggen_message.bin create mode 100644 score/tests/test_vectors/ecdsa/P384-SHA384/siggen_private_key.der create mode 100644 score/tests/test_vectors/ecdsa/P384-SHA384/siggen_public_key.der create mode 100644 score/tests/test_vectors/ecdsa/P384-SHA384/siggen_signature.bin create mode 100644 score/tests/test_vectors/ecdsa/P384-SHA384/sigver_invalid_message.bin create mode 100644 score/tests/test_vectors/ecdsa/P384-SHA384/sigver_invalid_public_key.der create mode 100644 score/tests/test_vectors/ecdsa/P384-SHA384/sigver_invalid_signature.bin create mode 100644 score/tests/test_vectors/ecdsa/P521-SHA512/reference.md create mode 100644 score/tests/test_vectors/ecdsa/P521-SHA512/siggen_message.bin create mode 100644 score/tests/test_vectors/ecdsa/P521-SHA512/siggen_private_key.der create mode 100644 score/tests/test_vectors/ecdsa/P521-SHA512/siggen_public_key.der create mode 100644 score/tests/test_vectors/ecdsa/P521-SHA512/siggen_signature.bin create mode 100644 score/tests/test_vectors/ecdsa/P521-SHA512/sigver_invalid_message.bin create mode 100644 score/tests/test_vectors/ecdsa/P521-SHA512/sigver_invalid_public_key.der create mode 100644 score/tests/test_vectors/ecdsa/P521-SHA512/sigver_invalid_signature.bin diff --git a/score/crypto/src/api/BUILD b/score/crypto/src/api/BUILD index 5cf44811d..1e5a7996b 100644 --- a/score/crypto/src/api/BUILD +++ b/score/crypto/src/api/BUILD @@ -22,8 +22,11 @@ cc_library( "//score/crypto/src/daemon/control_plane:control_operations", "//score/crypto/src/daemon/key_management:key_management_operations", "//score/crypto/src/daemon/mediator:mediator_operations", + "//score/crypto/src/daemon/provider/handler:cipher_handler_operations", "//score/crypto/src/daemon/provider/handler:hash_handler_operations", "//score/crypto/src/daemon/provider/handler:mac_handler_operations", + "//score/crypto/src/daemon/provider/handler:random_handler_operations", + "//score/crypto/src/daemon/provider/handler:signature_handler_operations", ], ) diff --git a/score/crypto/src/api/config/BUILD b/score/crypto/src/api/config/BUILD index 5ff3b764a..ea6dcbbd7 100644 --- a/score/crypto/src/api/config/BUILD +++ b/score/crypto/src/api/config/BUILD @@ -17,11 +17,15 @@ cc_library( name = "context_configs", hdrs = [ "base_context_config.hpp", + "cipher_context_config.hpp", "hash_context_config.hpp", "key_management_context_config.hpp", "key_operation_params.hpp", "mac_context_config.hpp", "permission_builder.hpp", + "random_context_config.hpp", + "sign_context_config.hpp", + "verify_signature_context_config.hpp", ], includes = ["."], visibility = ["//visibility:public"], diff --git a/score/crypto/src/api/future/config/cipher_context_config.hpp b/score/crypto/src/api/config/cipher_context_config.hpp similarity index 93% rename from score/crypto/src/api/future/config/cipher_context_config.hpp rename to score/crypto/src/api/config/cipher_context_config.hpp index cb99e2a5b..9b4240118 100644 --- a/score/crypto/src/api/future/config/cipher_context_config.hpp +++ b/score/crypto/src/api/config/cipher_context_config.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_CIPHER_CONTEXT_CONFIG_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_CIPHER_CONTEXT_CONFIG_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONFIG_CIPHER_CONTEXT_CONFIG_HPP +#define SCORE_CRYPTO_SRC_API_CONFIG_CIPHER_CONTEXT_CONFIG_HPP #include "score/crypto/src/api/common/crypto_resource_guard.hpp" #include "score/crypto/src/api/common/types.hpp" @@ -100,4 +100,4 @@ struct CipherContextConfig : public BaseContextConfig } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_CIPHER_CONTEXT_CONFIG_HPP +#endif // SCORE_CRYPTO_SRC_API_CONFIG_CIPHER_CONTEXT_CONFIG_HPP diff --git a/score/crypto/src/api/future/config/random_context_config.hpp b/score/crypto/src/api/config/random_context_config.hpp similarity index 90% rename from score/crypto/src/api/future/config/random_context_config.hpp rename to score/crypto/src/api/config/random_context_config.hpp index d1d0f76a9..89cf38934 100644 --- a/score/crypto/src/api/future/config/random_context_config.hpp +++ b/score/crypto/src/api/config/random_context_config.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_RANDOM_CONTEXT_CONFIG_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_RANDOM_CONTEXT_CONFIG_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONFIG_RANDOM_CONTEXT_CONFIG_HPP +#define SCORE_CRYPTO_SRC_API_CONFIG_RANDOM_CONTEXT_CONFIG_HPP #include "score/crypto/src/api/config/base_context_config.hpp" @@ -67,4 +67,4 @@ struct RandomContextConfig : public BaseContextConfig } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_RANDOM_CONTEXT_CONFIG_HPP +#endif // SCORE_CRYPTO_SRC_API_CONFIG_RANDOM_CONTEXT_CONFIG_HPP diff --git a/score/crypto/src/api/future/config/sign_context_config.hpp b/score/crypto/src/api/config/sign_context_config.hpp similarity index 91% rename from score/crypto/src/api/future/config/sign_context_config.hpp rename to score/crypto/src/api/config/sign_context_config.hpp index 1b999fad0..b60403da0 100644 --- a/score/crypto/src/api/future/config/sign_context_config.hpp +++ b/score/crypto/src/api/config/sign_context_config.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_SIGN_CONTEXT_CONFIG_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_SIGN_CONTEXT_CONFIG_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONFIG_SIGN_CONTEXT_CONFIG_HPP +#define SCORE_CRYPTO_SRC_API_CONFIG_SIGN_CONTEXT_CONFIG_HPP #include "score/crypto/src/api/config/base_context_config.hpp" @@ -76,4 +76,4 @@ struct SignContextConfig : public BaseContextConfig } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_SIGN_CONTEXT_CONFIG_HPP +#endif // SCORE_CRYPTO_SRC_API_CONFIG_SIGN_CONTEXT_CONFIG_HPP diff --git a/score/crypto/src/api/future/config/verify_signature_context_config.hpp b/score/crypto/src/api/config/verify_signature_context_config.hpp similarity index 90% rename from score/crypto/src/api/future/config/verify_signature_context_config.hpp rename to score/crypto/src/api/config/verify_signature_context_config.hpp index 674f1e609..b2683939b 100644 --- a/score/crypto/src/api/future/config/verify_signature_context_config.hpp +++ b/score/crypto/src/api/config/verify_signature_context_config.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_VERIFY_SIGNATURE_CONTEXT_CONFIG_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_VERIFY_SIGNATURE_CONTEXT_CONFIG_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONFIG_VERIFY_SIGNATURE_CONTEXT_CONFIG_HPP +#define SCORE_CRYPTO_SRC_API_CONFIG_VERIFY_SIGNATURE_CONTEXT_CONFIG_HPP #include "score/crypto/src/api/config/base_context_config.hpp" @@ -76,4 +76,4 @@ struct VerifySignatureContextConfig : public BaseContextConfig } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_VERIFY_SIGNATURE_CONTEXT_CONFIG_HPP +#endif // SCORE_CRYPTO_SRC_API_CONFIG_VERIFY_SIGNATURE_CONTEXT_CONFIG_HPP diff --git a/score/crypto/src/api/contexts/BUILD b/score/crypto/src/api/contexts/BUILD index 6f905b7b3..9f69f7bdb 100644 --- a/score/crypto/src/api/contexts/BUILD +++ b/score/crypto/src/api/contexts/BUILD @@ -32,9 +32,13 @@ cc_library( cc_library( name = "crypto_contexts", hdrs = [ + "i_cipher_context.hpp", "i_hash_context.hpp", "i_key_management_context.hpp", "i_mac_context.hpp", + "i_random_context.hpp", + "i_sign_context.hpp", + "i_verify_signature_context.hpp", ], includes = ["."], visibility = ["//visibility:public"], @@ -51,14 +55,22 @@ cc_library( cc_library( name = "crypto_contexts_impl", srcs = [ + "src/cipher_context_impl.cpp", "src/hash_context_impl.cpp", "src/key_management_context_impl.cpp", "src/mac_context_impl.cpp", + "src/random_context_impl.cpp", + "src/sign_context_impl.cpp", + "src/verify_signature_context_impl.cpp", ], hdrs = [ + "src/cipher_context_impl.hpp", "src/hash_context_impl.hpp", "src/key_management_context_impl.hpp", "src/mac_context_impl.hpp", + "src/random_context_impl.hpp", + "src/sign_context_impl.hpp", + "src/verify_signature_context_impl.hpp", ], includes = ["."], visibility = ["//:__subpackages__"], diff --git a/score/crypto/src/api/future/contexts/i_cipher_context.hpp b/score/crypto/src/api/contexts/i_cipher_context.hpp similarity index 95% rename from score/crypto/src/api/future/contexts/i_cipher_context.hpp rename to score/crypto/src/api/contexts/i_cipher_context.hpp index b1c0edd26..f181f6379 100644 --- a/score/crypto/src/api/future/contexts/i_cipher_context.hpp +++ b/score/crypto/src/api/contexts/i_cipher_context.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_CIPHER_CONTEXT_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_CIPHER_CONTEXT_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_I_CIPHER_CONTEXT_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_I_CIPHER_CONTEXT_HPP #include "score/crypto/src/api/contexts/i_streaming_output_context.hpp" #include "score/result/result.h" @@ -115,4 +115,4 @@ class ICipherContext : public IStreamingOutputContext } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_CIPHER_CONTEXT_HPP +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_I_CIPHER_CONTEXT_HPP diff --git a/score/crypto/src/api/future/contexts/i_random_context.hpp b/score/crypto/src/api/contexts/i_random_context.hpp similarity index 92% rename from score/crypto/src/api/future/contexts/i_random_context.hpp rename to score/crypto/src/api/contexts/i_random_context.hpp index 5e3a10455..820490623 100644 --- a/score/crypto/src/api/future/contexts/i_random_context.hpp +++ b/score/crypto/src/api/contexts/i_random_context.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_RANDOM_CONTEXT_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_RANDOM_CONTEXT_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_I_RANDOM_CONTEXT_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_I_RANDOM_CONTEXT_HPP #include "score/crypto/src/api/contexts/i_context.hpp" #include "score/result/result.h" @@ -68,4 +68,4 @@ class IRandomContext : public IContext } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_RANDOM_CONTEXT_HPP +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_I_RANDOM_CONTEXT_HPP diff --git a/score/crypto/src/api/future/contexts/i_sign_context.hpp b/score/crypto/src/api/contexts/i_sign_context.hpp similarity index 94% rename from score/crypto/src/api/future/contexts/i_sign_context.hpp rename to score/crypto/src/api/contexts/i_sign_context.hpp index f52c20edc..c730bdebd 100644 --- a/score/crypto/src/api/future/contexts/i_sign_context.hpp +++ b/score/crypto/src/api/contexts/i_sign_context.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_SIGN_CONTEXT_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_SIGN_CONTEXT_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_I_SIGN_CONTEXT_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_I_SIGN_CONTEXT_HPP #include "score/crypto/src/api/contexts/i_streaming_output_context.hpp" #include "score/result/result.h" @@ -81,4 +81,4 @@ class ISignContext : public IStreamingOutputContext } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_SIGN_CONTEXT_HPP +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_I_SIGN_CONTEXT_HPP diff --git a/score/crypto/src/api/future/contexts/i_verify_signature_context.hpp b/score/crypto/src/api/contexts/i_verify_signature_context.hpp similarity index 92% rename from score/crypto/src/api/future/contexts/i_verify_signature_context.hpp rename to score/crypto/src/api/contexts/i_verify_signature_context.hpp index 1b516e426..88f0ab4c4 100644 --- a/score/crypto/src/api/future/contexts/i_verify_signature_context.hpp +++ b/score/crypto/src/api/contexts/i_verify_signature_context.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_VERIFY_SIGNATURE_CONTEXT_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_VERIFY_SIGNATURE_CONTEXT_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_I_VERIFY_SIGNATURE_CONTEXT_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_I_VERIFY_SIGNATURE_CONTEXT_HPP #include "score/crypto/src/api/contexts/i_streaming_context.hpp" #include "score/result/result.h" @@ -74,4 +74,4 @@ class IVerifySignatureContext : public IStreamingContext } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_VERIFY_SIGNATURE_CONTEXT_HPP +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_I_VERIFY_SIGNATURE_CONTEXT_HPP diff --git a/score/crypto/src/api/contexts/src/cipher_context_impl.cpp b/score/crypto/src/api/contexts/src/cipher_context_impl.cpp new file mode 100644 index 000000000..a31ffb4b1 --- /dev/null +++ b/score/crypto/src/api/contexts/src/cipher_context_impl.cpp @@ -0,0 +1,379 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/api/contexts/src/cipher_context_impl.hpp" + +#include "score/crypto/src/api/common/error_domain.hpp" +#include "score/crypto/src/api/common/types.hpp" + +#include "score/crypto/src/api/control_plane/i_connection.hpp" +#include "score/crypto/src/daemon/common/actors.hpp" +#include "score/crypto/src/daemon/control_plane/control_protocol.h" +#include "score/crypto/src/daemon/mediator/mediator_operations.hpp" +#include "score/crypto/src/daemon/provider/handler/operations/cipher_handler_operations.hpp" + +#include "score/result/result.h" +#include "score/span.hpp" + +#include "score/mw/log/logging.h" +#include +#include + +#include +#include +#include +#include + +namespace score +{ + +namespace crypto +{ + +namespace +{ +namespace proto = ::score::crypto::daemon::control_plane::protocol; +namespace actors = ::score::crypto::daemon::common::actors; +namespace cipher_ops = ::score::crypto::daemon::provider::handler::cipher_handler_operations; + +constexpr std::string_view kLogPrefix = "[API][CipherContextImpl] ERROR: "; +} // namespace + +CipherContextImpl::CipherContextImpl(std::shared_ptr connection, + uint64_t context_id, + AlgorithmId algorithm, + std::shared_ptr transcoder) + : m_connection(std::move(connection)), + m_context_id(context_id), + m_algorithm(std::move(algorithm)), + m_transcoder(std::move(transcoder)) +{ +} + +CipherContextImpl::CipherContextImpl(CipherContextImpl&& other) noexcept + : m_connection(std::move(other.m_connection)), + m_context_id(std::exchange(other.m_context_id, 0)), + m_algorithm(std::move(other.m_algorithm)), + m_transcoder(std::move(other.m_transcoder)) +{ +} + +CipherContextImpl& CipherContextImpl::operator=(CipherContextImpl&& other) noexcept +{ + if (this != &other) + { + CloseContext(); + m_connection = std::move(other.m_connection); + m_context_id = std::exchange(other.m_context_id, 0); + m_algorithm = std::move(other.m_algorithm); + m_transcoder = std::move(other.m_transcoder); + } + return *this; +} + +CipherContextImpl::~CipherContextImpl() +{ + CloseContext(); +} + +void CipherContextImpl::CloseContext() noexcept +{ + if (m_context_id == 0) + { + return; // moved-from instance — nothing to close + } + + if (!m_connection) + { + score::mw::log::LogError() << kLogPrefix << "Connection is not initialized during destruction"; + return; + } + + auto context_close_res = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation(score::crypto::daemon::mediator::operations::CloseContext()) + .build(); + + if (!context_close_res.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Failed to build CTX_CLOSE request during destruction"; + return; + } + + auto response_res = m_connection->SendRequest(context_close_res.value()); + + auto validator = proto::ControlResponseValidator::FromResult(response_res); + validator.expectOperation(score::crypto::daemon::mediator::operations::CloseContext()).expectSuccess(); + + if (!validator.isValid()) + { + score::mw::log::LogError() << kLogPrefix << "CTX_CLOSE response validation failed: " << validator.getError(); + } +} + +score::Result CipherContextImpl::Init(std::optional> iv) +{ + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_INIT}); + + // IV-less modes (ECB) pass std::nullopt; the daemon rejects a missing IV for + // modes that require one, so no client-side algorithm table is needed here. + // The token has to outlive build() and SendRequest(): a pool slot is released + // by ~TranscoderSpan, and the daemon reads the slot while the call is in flight. + TranscoderSpan iv_tspan; + if (iv.has_value()) + { + auto iv_tspan_result = m_transcoder->Acquire(iv.value()); + if (!iv_tspan_result.has_value()) + { + return score::Result{score::unexpect, iv_tspan_result.error()}; + } + iv_tspan = std::move(iv_tspan_result.value()); + m_transcoder->AppendInputBuffer(builder, iv_tspan); + } + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build CIPHER_INIT request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_INIT}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + return std::monostate{}; +} + +score::Result CipherContextImpl::Update(score::cpp::span /*data*/) +{ + // A cipher Update always produces output, so the output-less streaming + // signature inherited from IStreamingContext cannot be honoured. Callers + // use the ICipherContext::Update(input, output) overload instead. + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kUnsupportedOperation, "Cipher Update requires an output buffer")}; +} + +score::Result CipherContextImpl::Update(score::cpp::span input, + score::cpp::span output) +{ + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_UPDATE}); + + auto input_tspan_result = m_transcoder->Acquire(input); + if (!input_tspan_result.has_value()) + { + return score::Result{score::unexpect, input_tspan_result.error()}; + } + TranscoderSpan input_tspan = std::move(input_tspan_result.value()); + m_transcoder->AppendInputBuffer(builder, input_tspan); + + auto output_tspan_result = m_transcoder->Acquire(output, /*is_output=*/true); + if (!output_tspan_result.has_value()) + { + return score::Result{score::unexpect, output_tspan_result.error()}; + } + TranscoderSpan output_tspan = std::move(output_tspan_result.value()); + m_transcoder->AppendOutputBuffer(builder, output_tspan); + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build CIPHER_UPDATE request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_UPDATE}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + return m_transcoder->ExtractOutputBuffer(output_tspan, validator); +} + +score::Result CipherContextImpl::Finalize(score::cpp::span output) +{ + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_FINALIZE}); + + auto output_tspan_result = m_transcoder->Acquire(output, /*is_output=*/true); + if (!output_tspan_result.has_value()) + { + return score::Result{score::unexpect, output_tspan_result.error()}; + } + TranscoderSpan output_tspan = std::move(output_tspan_result.value()); + m_transcoder->AppendOutputBuffer(builder, output_tspan); + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build CIPHER_FINALIZE request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_FINALIZE}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + return m_transcoder->ExtractOutputBuffer(output_tspan, validator); +} + +score::Result CipherContextImpl::SingleShot(score::cpp::span iv, + score::cpp::span input, + score::cpp::span output) +{ + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_SS}); + + // The IV slot is always sent, empty for ECB, so that the input and output + // buffers keep fixed positions on the wire. + auto iv_tspan_result = m_transcoder->Acquire(iv); + if (!iv_tspan_result.has_value()) + { + return score::Result{score::unexpect, iv_tspan_result.error()}; + } + TranscoderSpan iv_tspan = std::move(iv_tspan_result.value()); + m_transcoder->AppendInputBuffer(builder, iv_tspan); + + auto input_tspan_result = m_transcoder->Acquire(input); + if (!input_tspan_result.has_value()) + { + return score::Result{score::unexpect, input_tspan_result.error()}; + } + TranscoderSpan input_tspan = std::move(input_tspan_result.value()); + m_transcoder->AppendInputBuffer(builder, input_tspan); + + auto output_tspan_result = m_transcoder->Acquire(output, /*is_output=*/true); + if (!output_tspan_result.has_value()) + { + return score::Result{score::unexpect, output_tspan_result.error()}; + } + TranscoderSpan output_tspan = std::move(output_tspan_result.value()); + m_transcoder->AppendOutputBuffer(builder, output_tspan); + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build CIPHER_SS request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_SS}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + return m_transcoder->ExtractOutputBuffer(output_tspan, validator); +} + +score::Result CipherContextImpl::Reset() +{ + auto control_req_result = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_RESET}) + .build(); + if (!control_req_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build CIPHER_RESET request")}; + } + + auto control_response_res = m_connection->SendRequest(control_req_result.value()); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_RESET}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + return std::monostate{}; +} + +std::size_t CipherContextImpl::GetOutputSize() const noexcept +{ + auto control_req_result = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_GET_OUTPUT_SIZE}) + .build(); + if (!control_req_result.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Failed to build CIPHER_GET_OUTPUT_SIZE request"; + return 0U; + } + + auto control_response_res = m_connection->SendRequest(control_req_result.value()); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_CIPHER_HANDLER, cipher_ops::CIPHER_GET_OUTPUT_SIZE}).expectSuccess(); + + if (!validator.isValid()) + { + score::mw::log::LogError() << kLogPrefix << validator.getError(); + return 0U; + } + + auto size_result = validator.getParameterAt(0, 0); + if (!size_result.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "CIPHER_GET_OUTPUT_SIZE response has invalid parameter type"; + return 0U; + } + + return static_cast(size_result.value()); +} + +} // namespace crypto + +} // namespace score diff --git a/score/crypto/src/api/contexts/src/cipher_context_impl.hpp b/score/crypto/src/api/contexts/src/cipher_context_impl.hpp new file mode 100644 index 000000000..9f2fac40a --- /dev/null +++ b/score/crypto/src/api/contexts/src/cipher_context_impl.hpp @@ -0,0 +1,90 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_CIPHER_CONTEXT_IMPL_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_CIPHER_CONTEXT_IMPL_HPP + +#include "score/crypto/src/api/contexts/i_cipher_context.hpp" + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/data_plane/i_buffer_transcoder.hpp" + +#include "score/crypto/src/api/control_plane/i_connection.hpp" + +#include +#include +#include +#include + +namespace score +{ + +namespace crypto +{ + +/// @brief Concrete ICipherContext implementation that delegates to the crypto daemon via IPC. +/// +/// Each instance is bound to a daemon-side cipher context (identified by context_id) +/// created during construction. The key, algorithm and direction (encrypt / decrypt) +/// are fixed at CTX_CREATE time and are not repeated per operation. +class CipherContextImpl final : public ICipherContext +{ + public: + /// @brief Constructs a cipher context bound to an existing daemon-side context. + /// @param connection Shared connection for IPC communication + /// @param context_id Daemon-assigned context identifier (from CTX_CREATE response) + /// @param algorithm Algorithm name (e.g., "AES-256-CBC") + /// @param transcoder Stack-shared buffer-routing abstraction (pool/bulk/in-band). + /// Shared with all other contexts in the same CryptoStack. + /// When non-null, handles transparent copying via pool SHM. + CipherContextImpl(std::shared_ptr connection, + uint64_t context_id, + AlgorithmId algorithm, + std::shared_ptr transcoder = nullptr); + + ~CipherContextImpl() override; + + CipherContextImpl(const CipherContextImpl&) = delete; + CipherContextImpl& operator=(const CipherContextImpl&) = delete; + CipherContextImpl(CipherContextImpl&&) noexcept; + CipherContextImpl& operator=(CipherContextImpl&&) noexcept; + + // -- IStreamingContext -- + score::Result Init(std::optional> iv) override; + score::Result Update(score::cpp::span data) override; + score::Result Reset() override; + + // -- IStreamingOutputContext -- + score::Result Finalize(score::cpp::span output) override; + std::size_t GetOutputSize() const noexcept override; + + // -- ICipherContext -- + score::Result Update(score::cpp::span input, score::cpp::span output) override; + score::Result SingleShot(score::cpp::span iv, + score::cpp::span input, + score::cpp::span output) override; + + private: + void CloseContext() noexcept; + + std::shared_ptr m_connection; + score::crypto::daemon::control_plane::protocol::DataNodeId m_context_id; + AlgorithmId m_algorithm; + std::shared_ptr m_transcoder; ///< Stack-shared transcoder; null allowed. +}; + +} // namespace crypto + +} // namespace score + +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_CIPHER_CONTEXT_IMPL_HPP diff --git a/score/crypto/src/api/contexts/src/key_management_context_impl.cpp b/score/crypto/src/api/contexts/src/key_management_context_impl.cpp index bd4e2d5e3..32e1fed8c 100644 --- a/score/crypto/src/api/contexts/src/key_management_context_impl.cpp +++ b/score/crypto/src/api/contexts/src/key_management_context_impl.cpp @@ -177,12 +177,22 @@ KeyManagementContextImpl::~KeyManagementContextImpl() = default; score::Result KeyManagementContextImpl::GenerateKey(const GenerateKeyParams& params) { - auto control_req_result = proto::ControlRequestBuilder() - .forDataNodeId(m_context_id) - .operation({actors::OP_ACTOR_KEY_MANAGEMENT, keymgmt_ops::KEY_GENERATE}) - .with_in_string(params.algorithm) - .with_in_val_uint32(static_cast(params.permissions)) - .build(); + auto request_builder = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation({actors::OP_ACTOR_KEY_MANAGEMENT, keymgmt_ops::KEY_GENERATE}) + .with_in_string(params.algorithm) + .with_in_val_uint32(static_cast(params.permissions)); + + // Only sent when the caller restricted the public half. Omitting the + // parameter is what tells the daemon to leave it unrestricted, so an + // unconditional send would silently turn the default into kAll-explicit. + if (params.public_key_permissions.has_value()) + { + request_builder = + request_builder.with_in_val_uint32(static_cast(params.public_key_permissions.value())); + } + + auto control_req_result = request_builder.build(); if (!control_req_result.has_value()) { diff --git a/score/crypto/src/api/contexts/src/random_context_impl.cpp b/score/crypto/src/api/contexts/src/random_context_impl.cpp new file mode 100644 index 000000000..f4de2c2d6 --- /dev/null +++ b/score/crypto/src/api/contexts/src/random_context_impl.cpp @@ -0,0 +1,222 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/api/contexts/src/random_context_impl.hpp" + +#include "score/crypto/src/api/common/error_domain.hpp" +#include "score/crypto/src/api/common/types.hpp" + +#include "score/crypto/src/api/control_plane/i_connection.hpp" +#include "score/crypto/src/daemon/common/actors.hpp" +#include "score/crypto/src/daemon/control_plane/control_protocol.h" +#include "score/crypto/src/daemon/mediator/mediator_operations.hpp" +#include "score/crypto/src/daemon/provider/handler/operations/random_handler_operations.hpp" + +#include "score/result/result.h" +#include "score/span.hpp" + +#include "score/mw/log/logging.h" +#include +#include + +#include +#include +#include + +namespace score +{ + +namespace crypto +{ + +namespace +{ +namespace proto = ::score::crypto::daemon::control_plane::protocol; +namespace actors = ::score::crypto::daemon::common::actors; +namespace random_ops = ::score::crypto::daemon::provider::handler::random_handler_operations; + +constexpr std::string_view kLogPrefix = "[API][RandomContextImpl] ERROR: "; +} // namespace + +RandomContextImpl::RandomContextImpl(std::shared_ptr connection, + uint64_t context_id, + AlgorithmId algorithm, + std::shared_ptr transcoder) + : m_connection(std::move(connection)), + m_context_id(context_id), + m_algorithm(std::move(algorithm)), + m_transcoder(std::move(transcoder)) +{ +} + +RandomContextImpl::RandomContextImpl(RandomContextImpl&& other) noexcept + : m_connection(std::move(other.m_connection)), + m_context_id(std::exchange(other.m_context_id, 0)), + m_algorithm(std::move(other.m_algorithm)), + m_transcoder(std::move(other.m_transcoder)) +{ +} + +RandomContextImpl& RandomContextImpl::operator=(RandomContextImpl&& other) noexcept +{ + if (this != &other) + { + CloseContext(); + m_connection = std::move(other.m_connection); + m_context_id = std::exchange(other.m_context_id, 0); + m_algorithm = std::move(other.m_algorithm); + m_transcoder = std::move(other.m_transcoder); + } + return *this; +} + +RandomContextImpl::~RandomContextImpl() +{ + CloseContext(); +} + +void RandomContextImpl::CloseContext() noexcept +{ + if (m_context_id == 0) + { + return; // moved-from instance — nothing to close + } + + if (!m_connection) + { + score::mw::log::LogError() << kLogPrefix << "Connection is not initialized during destruction"; + return; + } + + auto context_close_res = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation(score::crypto::daemon::mediator::operations::CloseContext()) + .build(); + + if (!context_close_res.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Failed to build CTX_CLOSE request during destruction"; + return; + } + + auto response_res = m_connection->SendRequest(context_close_res.value()); + + auto validator = proto::ControlResponseValidator::FromResult(response_res); + validator.expectOperation(score::crypto::daemon::mediator::operations::CloseContext()).expectSuccess(); + + if (!validator.isValid()) + { + score::mw::log::LogError() << kLogPrefix << "CTX_CLOSE response validation failed: " << validator.getError(); + } +} + +score::Result RandomContextImpl::Generate(score::cpp::span output) +{ + if (output.empty()) + { + return std::size_t{0U}; + } + + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_RANDOM_HANDLER, random_ops::RANDOM_GENERATE}); + + auto tspan_result = m_transcoder->Acquire(output, /*is_output=*/true); + if (!tspan_result.has_value()) + { + return score::Result{score::unexpect, tspan_result.error()}; + } + TranscoderSpan tspan = std::move(tspan_result.value()); + m_transcoder->AppendOutputBuffer(builder, tspan); + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build RANDOM_GENERATE request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_RANDOM_HANDLER, random_ops::RANDOM_GENERATE}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + auto written = m_transcoder->ExtractOutputBuffer(tspan, validator); + if (!written.has_value()) + { + return written; + } + + // The daemon must fill the whole buffer. A short write would silently leave + // part of it unrandomised, so it is reported as an error rather than a + // partial result the caller might use as key or IV material. + if (written.value() != output.size()) + { + score::mw::log::LogError() << kLogPrefix << "RANDOM_GENERATE produced" << written.value() << "bytes, expected" + << output.size(); + return score::Result{ + score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, "RANDOM_GENERATE returned an unexpected byte count")}; + } + + return written; +} + +score::Result RandomContextImpl::Seed(score::cpp::span seed) +{ + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_RANDOM_HANDLER, random_ops::RANDOM_SEED}); + + auto tspan_result = m_transcoder->Acquire(seed); + if (!tspan_result.has_value()) + { + return score::Result{score::unexpect, tspan_result.error()}; + } + TranscoderSpan tspan = std::move(tspan_result.value()); + m_transcoder->AppendInputBuffer(builder, tspan); + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build RANDOM_SEED request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_RANDOM_HANDLER, random_ops::RANDOM_SEED}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + return std::monostate{}; +} + +} // namespace crypto + +} // namespace score diff --git a/score/crypto/src/api/contexts/src/random_context_impl.hpp b/score/crypto/src/api/contexts/src/random_context_impl.hpp new file mode 100644 index 000000000..c55eb2f4a --- /dev/null +++ b/score/crypto/src/api/contexts/src/random_context_impl.hpp @@ -0,0 +1,79 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_RANDOM_CONTEXT_IMPL_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_RANDOM_CONTEXT_IMPL_HPP + +#include "score/crypto/src/api/contexts/i_random_context.hpp" + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/data_plane/i_buffer_transcoder.hpp" + +#include "score/crypto/src/api/control_plane/i_connection.hpp" + +#include +#include +#include + +namespace score +{ + +namespace crypto +{ + +/// @brief Concrete IRandomContext implementation that delegates to the crypto daemon via IPC. +/// +/// Each instance is bound to a daemon-side random context (identified by +/// context_id) created during construction. Unlike the streaming contexts the +/// random context carries no state machine: Generate() and Seed() are valid at +/// any point in the context's lifetime. +class RandomContextImpl final : public IRandomContext +{ + public: + /// @brief Constructs a random context bound to an existing daemon-side context. + /// @param connection Shared connection for IPC communication + /// @param context_id Daemon-assigned context identifier (from CTX_CREATE response) + /// @param algorithm RNG algorithm name; empty selects the provider default + /// @param transcoder Stack-shared buffer-routing abstraction (pool/bulk/in-band). + /// Shared with all other contexts in the same CryptoStack. + /// When non-null, handles transparent copying via pool SHM. + RandomContextImpl(std::shared_ptr connection, + uint64_t context_id, + AlgorithmId algorithm, + std::shared_ptr transcoder = nullptr); + + ~RandomContextImpl() override; + + RandomContextImpl(const RandomContextImpl&) = delete; + RandomContextImpl& operator=(const RandomContextImpl&) = delete; + RandomContextImpl(RandomContextImpl&&) noexcept; + RandomContextImpl& operator=(RandomContextImpl&&) noexcept; + + // -- IRandomContext -- + score::Result Generate(score::cpp::span output) override; + score::Result Seed(score::cpp::span seed) override; + + private: + void CloseContext() noexcept; + + std::shared_ptr m_connection; + score::crypto::daemon::control_plane::protocol::DataNodeId m_context_id; + AlgorithmId m_algorithm; + std::shared_ptr m_transcoder; ///< Stack-shared transcoder; null allowed. +}; + +} // namespace crypto + +} // namespace score + +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_RANDOM_CONTEXT_IMPL_HPP diff --git a/score/crypto/src/api/contexts/src/sign_context_impl.cpp b/score/crypto/src/api/contexts/src/sign_context_impl.cpp new file mode 100644 index 000000000..da4da4caa --- /dev/null +++ b/score/crypto/src/api/contexts/src/sign_context_impl.cpp @@ -0,0 +1,352 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/api/contexts/src/sign_context_impl.hpp" + +#include "score/crypto/src/api/common/error_domain.hpp" +#include "score/crypto/src/api/common/types.hpp" + +#include "score/crypto/src/api/control_plane/i_connection.hpp" +#include "score/crypto/src/daemon/common/actors.hpp" +#include "score/crypto/src/daemon/control_plane/control_protocol.h" +#include "score/crypto/src/daemon/mediator/mediator_operations.hpp" +#include "score/crypto/src/daemon/provider/handler/operations/signature_handler_operations.hpp" + +#include "score/result/result.h" +#include "score/span.hpp" + +#include "score/mw/log/logging.h" +#include +#include + +#include +#include +#include +#include + +namespace score +{ + +namespace crypto +{ + +namespace +{ +namespace proto = ::score::crypto::daemon::control_plane::protocol; +namespace actors = ::score::crypto::daemon::common::actors; +namespace sign_ops = ::score::crypto::daemon::provider::handler::sign_handler_operations; + +constexpr std::string_view kLogPrefix = "[API][SignContextImpl] ERROR: "; +} // namespace + +SignContextImpl::SignContextImpl(std::shared_ptr connection, + uint64_t context_id, + AlgorithmId algorithm, + std::shared_ptr transcoder) + : m_connection(std::move(connection)), + m_context_id(context_id), + m_algorithm(std::move(algorithm)), + m_transcoder(std::move(transcoder)) +{ +} + +SignContextImpl::SignContextImpl(SignContextImpl&& other) noexcept + : m_connection(std::move(other.m_connection)), + m_context_id(std::exchange(other.m_context_id, 0)), + m_algorithm(std::move(other.m_algorithm)), + m_transcoder(std::move(other.m_transcoder)) +{ +} + +SignContextImpl& SignContextImpl::operator=(SignContextImpl&& other) noexcept +{ + if (this != &other) + { + CloseContext(); + m_connection = std::move(other.m_connection); + m_context_id = std::exchange(other.m_context_id, 0); + m_algorithm = std::move(other.m_algorithm); + m_transcoder = std::move(other.m_transcoder); + } + return *this; +} + +SignContextImpl::~SignContextImpl() +{ + CloseContext(); +} + +void SignContextImpl::CloseContext() noexcept +{ + if (m_context_id == 0) + { + return; // moved-from instance — nothing to close + } + + if (!m_connection) + { + score::mw::log::LogError() << kLogPrefix << "Connection is not initialized during destruction"; + return; + } + + auto context_close_res = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation(score::crypto::daemon::mediator::operations::CloseContext()) + .build(); + + if (!context_close_res.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Failed to build CTX_CLOSE request during destruction"; + return; + } + + auto response_res = m_connection->SendRequest(context_close_res.value()); + + auto validator = proto::ControlResponseValidator::FromResult(response_res); + validator.expectOperation(score::crypto::daemon::mediator::operations::CloseContext()).expectSuccess(); + + if (!validator.isValid()) + { + score::mw::log::LogError() << kLogPrefix << "CTX_CLOSE response validation failed: " << validator.getError(); + } +} + +score::Result SignContextImpl::Init(std::optional> iv) +{ + if (iv.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Init with an IV is not applicable to a signature context"; + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kUnsupportedOperation, "Signature contexts do not take an IV")}; + } + + auto control_req_result = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_INIT}) + .build(); + if (!control_req_result.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Failed to build SIGN_INIT request"; + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build SIGN_INIT request")}; + } + + auto control_response_res = m_connection->SendRequest(control_req_result.value()); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_INIT}).expectSuccess(); + + if (!validator.isValid()) + { + score::mw::log::LogError() << kLogPrefix << validator.getError(); + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "SIGN_INIT daemon response invalid")}; + } + + return std::monostate{}; +} + +score::Result SignContextImpl::Update(score::cpp::span data) +{ + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_UPDATE}); + + auto tspan_result = m_transcoder->Acquire(data); + if (!tspan_result.has_value()) + { + return score::Result{score::unexpect, tspan_result.error()}; + } + TranscoderSpan tspan = std::move(tspan_result.value()); + m_transcoder->AppendInputBuffer(builder, tspan); + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build SIGN_UPDATE request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_UPDATE}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + return std::monostate{}; +} + +score::Result SignContextImpl::SignFinalize(score::cpp::span signature) +{ + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_FINALIZE}); + + auto tspan_result = m_transcoder->Acquire(signature, /*is_output=*/true); + if (!tspan_result.has_value()) + { + return score::Result{score::unexpect, tspan_result.error()}; + } + TranscoderSpan tspan = std::move(tspan_result.value()); + m_transcoder->AppendOutputBuffer(builder, tspan); + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build SIGN_FINALIZE request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_FINALIZE}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + return m_transcoder->ExtractOutputBuffer(tspan, validator); +} + +score::Result SignContextImpl::Finalize(score::cpp::span output) +{ + return SignFinalize(output); +} + +score::Result SignContextImpl::SingleShot(score::cpp::span data, + score::cpp::span signature) +{ + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_SS}); + + auto data_tspan_result = m_transcoder->Acquire(data); + if (!data_tspan_result.has_value()) + { + return score::Result{score::unexpect, data_tspan_result.error()}; + } + TranscoderSpan data_tspan = std::move(data_tspan_result.value()); + m_transcoder->AppendInputBuffer(builder, data_tspan); + + auto sig_tspan_result = m_transcoder->Acquire(signature, /*is_output=*/true); + if (!sig_tspan_result.has_value()) + { + return score::Result{score::unexpect, sig_tspan_result.error()}; + } + TranscoderSpan sig_tspan = std::move(sig_tspan_result.value()); + m_transcoder->AppendOutputBuffer(builder, sig_tspan); + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build SIGN_SS request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_SS}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + return m_transcoder->ExtractOutputBuffer(sig_tspan, validator); +} + +score::Result SignContextImpl::Reset() +{ + auto control_req_result = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_RESET}) + .build(); + if (!control_req_result.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Failed to build SIGN_RESET request"; + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build SIGN_RESET request")}; + } + + auto control_response_res = m_connection->SendRequest(control_req_result.value()); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_RESET}).expectSuccess(); + + if (!validator.isValid()) + { + score::mw::log::LogError() << kLogPrefix << validator.getError(); + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "SIGN_RESET daemon response invalid")}; + } + + return std::monostate{}; +} + +std::size_t SignContextImpl::GetSignatureSize() const noexcept +{ + auto control_req_result = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_GET_SIZE}) + .build(); + if (!control_req_result.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Failed to build SIGN_GET_SIZE request"; + return 0U; + } + + auto control_response_res = m_connection->SendRequest(control_req_result.value()); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_SIGN_HANDLER, sign_ops::SIGN_GET_SIZE}).expectSuccess(); + + if (!validator.isValid()) + { + score::mw::log::LogError() << kLogPrefix << validator.getError(); + return 0U; + } + + auto size_result = validator.getParameterAt(0, 0); + if (!size_result.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "SIGN_GET_SIZE response has invalid parameter type"; + return 0U; + } + + return static_cast(size_result.value()); +} + +std::size_t SignContextImpl::GetOutputSize() const noexcept +{ + return GetSignatureSize(); +} + +} // namespace crypto + +} // namespace score diff --git a/score/crypto/src/api/contexts/src/sign_context_impl.hpp b/score/crypto/src/api/contexts/src/sign_context_impl.hpp new file mode 100644 index 000000000..17bcbe1df --- /dev/null +++ b/score/crypto/src/api/contexts/src/sign_context_impl.hpp @@ -0,0 +1,94 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_SIGN_CONTEXT_IMPL_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_SIGN_CONTEXT_IMPL_HPP + +#include "score/crypto/src/api/contexts/i_sign_context.hpp" + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/data_plane/i_buffer_transcoder.hpp" + +#include "score/crypto/src/api/control_plane/i_connection.hpp" + +#include +#include +#include +#include + +namespace score +{ + +namespace crypto +{ + +/// @brief Concrete ISignContext implementation that delegates to the crypto daemon via IPC. +/// +/// Each instance is bound to a daemon-side signature context (identified by +/// context_id) created during construction. The private key and algorithm are +/// fixed at CTX_CREATE time. +/// +/// ECDSA signatures are produced in the fixed-length IEEE P1363 encoding r‖s +/// (64 bytes for P-256, 96 for P-384, 132 for P-521), identically across all +/// providers, so a signature made by one provider verifies under another. +class SignContextImpl final : public ISignContext +{ + public: + /// @brief Constructs a signing context bound to an existing daemon-side context. + /// @param connection Shared connection for IPC communication + /// @param context_id Daemon-assigned context identifier (from CTX_CREATE response) + /// @param algorithm Algorithm name (e.g., "ECDSA-P256-SHA256") + /// @param transcoder Stack-shared buffer-routing abstraction (pool/bulk/in-band). + /// Shared with all other contexts in the same CryptoStack. + /// When non-null, handles transparent copying via pool SHM. + SignContextImpl(std::shared_ptr connection, + uint64_t context_id, + AlgorithmId algorithm, + std::shared_ptr transcoder = nullptr); + + ~SignContextImpl() override; + + SignContextImpl(const SignContextImpl&) = delete; + SignContextImpl& operator=(const SignContextImpl&) = delete; + SignContextImpl(SignContextImpl&&) noexcept; + SignContextImpl& operator=(SignContextImpl&&) noexcept; + + // -- IStreamingContext -- + score::Result Init(std::optional> iv) override; + score::Result Update(score::cpp::span data) override; + score::Result Reset() override; + + // -- IStreamingOutputContext -- + score::Result Finalize(score::cpp::span output) override; + std::size_t GetOutputSize() const noexcept override; + + // -- ISignContext -- + score::Result SignFinalize(score::cpp::span signature) override; + score::Result SingleShot(score::cpp::span data, + score::cpp::span signature) override; + std::size_t GetSignatureSize() const noexcept override; + + private: + void CloseContext() noexcept; + + std::shared_ptr m_connection; + score::crypto::daemon::control_plane::protocol::DataNodeId m_context_id; + AlgorithmId m_algorithm; + std::shared_ptr m_transcoder; ///< Stack-shared transcoder; null allowed. +}; + +} // namespace crypto + +} // namespace score + +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_SIGN_CONTEXT_IMPL_HPP diff --git a/score/crypto/src/api/contexts/src/verify_signature_context_impl.cpp b/score/crypto/src/api/contexts/src/verify_signature_context_impl.cpp new file mode 100644 index 000000000..ff6024365 --- /dev/null +++ b/score/crypto/src/api/contexts/src/verify_signature_context_impl.cpp @@ -0,0 +1,327 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/api/contexts/src/verify_signature_context_impl.hpp" + +#include "score/crypto/src/api/common/error_domain.hpp" +#include "score/crypto/src/api/common/types.hpp" + +#include "score/crypto/src/api/control_plane/i_connection.hpp" +#include "score/crypto/src/daemon/common/actors.hpp" +#include "score/crypto/src/daemon/control_plane/control_protocol.h" +#include "score/crypto/src/daemon/mediator/mediator_operations.hpp" +#include "score/crypto/src/daemon/provider/handler/operations/signature_handler_operations.hpp" + +#include "score/result/result.h" +#include "score/span.hpp" + +#include "score/mw/log/logging.h" +#include +#include + +#include +#include +#include +#include + +namespace score +{ + +namespace crypto +{ + +namespace +{ +namespace proto = ::score::crypto::daemon::control_plane::protocol; +namespace actors = ::score::crypto::daemon::common::actors; +namespace verify_ops = ::score::crypto::daemon::provider::handler::verify_handler_operations; + +constexpr std::string_view kLogPrefix = "[API][VerifySignatureContextImpl] ERROR: "; +} // namespace + +VerifySignatureContextImpl::VerifySignatureContextImpl( + std::shared_ptr connection, + uint64_t context_id, + AlgorithmId algorithm, + std::shared_ptr transcoder) + : m_connection(std::move(connection)), + m_context_id(context_id), + m_algorithm(std::move(algorithm)), + m_transcoder(std::move(transcoder)) +{ +} + +VerifySignatureContextImpl::VerifySignatureContextImpl(VerifySignatureContextImpl&& other) noexcept + : m_connection(std::move(other.m_connection)), + m_context_id(std::exchange(other.m_context_id, 0)), + m_algorithm(std::move(other.m_algorithm)), + m_transcoder(std::move(other.m_transcoder)) +{ +} + +VerifySignatureContextImpl& VerifySignatureContextImpl::operator=(VerifySignatureContextImpl&& other) noexcept +{ + if (this != &other) + { + CloseContext(); + m_connection = std::move(other.m_connection); + m_context_id = std::exchange(other.m_context_id, 0); + m_algorithm = std::move(other.m_algorithm); + m_transcoder = std::move(other.m_transcoder); + } + return *this; +} + +VerifySignatureContextImpl::~VerifySignatureContextImpl() +{ + CloseContext(); +} + +void VerifySignatureContextImpl::CloseContext() noexcept +{ + if (m_context_id == 0) + { + return; // moved-from instance — nothing to close + } + + if (!m_connection) + { + score::mw::log::LogError() << kLogPrefix << "Connection is not initialized during destruction"; + return; + } + + auto context_close_res = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation(score::crypto::daemon::mediator::operations::CloseContext()) + .build(); + + if (!context_close_res.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Failed to build CTX_CLOSE request during destruction"; + return; + } + + auto response_res = m_connection->SendRequest(context_close_res.value()); + + auto validator = proto::ControlResponseValidator::FromResult(response_res); + validator.expectOperation(score::crypto::daemon::mediator::operations::CloseContext()).expectSuccess(); + + if (!validator.isValid()) + { + score::mw::log::LogError() << kLogPrefix << "CTX_CLOSE response validation failed: " << validator.getError(); + } +} + +score::Result VerifySignatureContextImpl::Init(std::optional> iv) +{ + if (iv.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Init with an IV is not applicable to a verification context"; + return score::Result{ + score::unexpect, + MakeError(CryptoErrorCode::kUnsupportedOperation, "Verification contexts do not take an IV")}; + } + + auto control_req_result = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation({actors::OP_ACTOR_VERIFY_HANDLER, verify_ops::VERIFY_INIT}) + .build(); + if (!control_req_result.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Failed to build VERIFY_INIT request"; + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build VERIFY_INIT request")}; + } + + auto control_response_res = m_connection->SendRequest(control_req_result.value()); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_VERIFY_HANDLER, verify_ops::VERIFY_INIT}).expectSuccess(); + + if (!validator.isValid()) + { + score::mw::log::LogError() << kLogPrefix << validator.getError(); + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "VERIFY_INIT daemon response invalid")}; + } + + return std::monostate{}; +} + +score::Result VerifySignatureContextImpl::Update(score::cpp::span data) +{ + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_VERIFY_HANDLER, verify_ops::VERIFY_UPDATE}); + + auto tspan_result = m_transcoder->Acquire(data); + if (!tspan_result.has_value()) + { + return score::Result{score::unexpect, tspan_result.error()}; + } + TranscoderSpan tspan = std::move(tspan_result.value()); + m_transcoder->AppendInputBuffer(builder, tspan); + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build VERIFY_UPDATE request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_VERIFY_HANDLER, verify_ops::VERIFY_UPDATE}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + return std::monostate{}; +} + +score::Result VerifySignatureContextImpl::VerifyFinalize(score::cpp::span signature) +{ + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_VERIFY_HANDLER, verify_ops::VERIFY_FINALIZE}); + + auto tspan_result = m_transcoder->Acquire(signature); + if (!tspan_result.has_value()) + { + return score::Result{score::unexpect, tspan_result.error()}; + } + TranscoderSpan tspan = std::move(tspan_result.value()); + m_transcoder->AppendInputBuffer(builder, tspan); + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build VERIFY_FINALIZE request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_VERIFY_HANDLER, verify_ops::VERIFY_FINALIZE}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + auto verify_result = validator.getParameterAt(0, 0); + if (!verify_result.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "VERIFY_FINALIZE response has invalid parameter type"; + return score::Result{ + score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, "VERIFY_FINALIZE response has invalid parameter type")}; + } + + return verify_result.value(); +} + +score::Result VerifySignatureContextImpl::SingleShot(score::cpp::span data, + score::cpp::span signature) +{ + proto::OperationRequestBuilder builder; + builder.operation({actors::OP_ACTOR_VERIFY_HANDLER, verify_ops::VERIFY_SS}); + + auto data_tspan_result = m_transcoder->Acquire(data); + if (!data_tspan_result.has_value()) + { + return score::Result{score::unexpect, data_tspan_result.error()}; + } + TranscoderSpan data_tspan = std::move(data_tspan_result.value()); + m_transcoder->AppendInputBuffer(builder, data_tspan); + + auto sig_tspan_result = m_transcoder->Acquire(signature); + if (!sig_tspan_result.has_value()) + { + return score::Result{score::unexpect, sig_tspan_result.error()}; + } + TranscoderSpan sig_tspan = std::move(sig_tspan_result.value()); + m_transcoder->AppendInputBuffer(builder, sig_tspan); + + auto control_request_result = builder.build(); + if (!control_request_result.has_value()) + { + return score::Result{score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, "Failed to build VERIFY_SS request")}; + } + + proto::ControlRequest control_req{}; + control_req.operation = control_request_result.value(); + control_req.data_node_id = m_context_id; + auto control_response_res = m_connection->SendRequest(control_req); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_VERIFY_HANDLER, verify_ops::VERIFY_SS}).expectSuccess(); + + if (!validator.isValid()) + { + return score::Result{score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + } + + auto verify_result = validator.getParameterAt(0, 0); + if (!verify_result.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "VERIFY_SS response has invalid parameter type"; + return score::Result{ + score::unexpect, + MakeError(CryptoErrorCode::kOperationFailed, "VERIFY_SS response has invalid parameter type")}; + } + + return verify_result.value(); +} + +score::Result VerifySignatureContextImpl::Reset() +{ + auto control_req_result = proto::ControlRequestBuilder() + .forDataNodeId(m_context_id) + .operation({actors::OP_ACTOR_VERIFY_HANDLER, verify_ops::VERIFY_RESET}) + .build(); + if (!control_req_result.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "Failed to build VERIFY_RESET request"; + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "Failed to build VERIFY_RESET request")}; + } + + auto control_response_res = m_connection->SendRequest(control_req_result.value()); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation({actors::OP_ACTOR_VERIFY_HANDLER, verify_ops::VERIFY_RESET}).expectSuccess(); + + if (!validator.isValid()) + { + score::mw::log::LogError() << kLogPrefix << validator.getError(); + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kOperationFailed, "VERIFY_RESET daemon response invalid")}; + } + + return std::monostate{}; +} + +} // namespace crypto + +} // namespace score diff --git a/score/crypto/src/api/contexts/src/verify_signature_context_impl.hpp b/score/crypto/src/api/contexts/src/verify_signature_context_impl.hpp new file mode 100644 index 000000000..f1d4f45e6 --- /dev/null +++ b/score/crypto/src/api/contexts/src/verify_signature_context_impl.hpp @@ -0,0 +1,89 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_VERIFY_SIGNATURE_CONTEXT_IMPL_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_VERIFY_SIGNATURE_CONTEXT_IMPL_HPP + +#include "score/crypto/src/api/contexts/i_verify_signature_context.hpp" + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/data_plane/i_buffer_transcoder.hpp" + +#include "score/crypto/src/api/control_plane/i_connection.hpp" + +#include +#include +#include +#include + +namespace score +{ + +namespace crypto +{ + +/// @brief Concrete IVerifySignatureContext implementation that delegates to the +/// crypto daemon via IPC. +/// +/// Each instance is bound to a daemon-side verification context (identified by +/// context_id) created during construction. The public key and algorithm are +/// fixed at CTX_CREATE time. +/// +/// A signature that does not verify is reported as a successful call returning +/// false — only transport or state errors produce an error Result. +class VerifySignatureContextImpl final : public IVerifySignatureContext +{ + public: + /// @brief Constructs a verification context bound to an existing daemon-side context. + /// @param connection Shared connection for IPC communication + /// @param context_id Daemon-assigned context identifier (from CTX_CREATE response) + /// @param algorithm Algorithm name (e.g., "ECDSA-P256-SHA256") + /// @param transcoder Stack-shared buffer-routing abstraction (pool/bulk/in-band). + /// Shared with all other contexts in the same CryptoStack. + /// When non-null, handles transparent copying via pool SHM. + VerifySignatureContextImpl(std::shared_ptr connection, + uint64_t context_id, + AlgorithmId algorithm, + std::shared_ptr transcoder = nullptr); + + ~VerifySignatureContextImpl() override; + + VerifySignatureContextImpl(const VerifySignatureContextImpl&) = delete; + VerifySignatureContextImpl& operator=(const VerifySignatureContextImpl&) = delete; + VerifySignatureContextImpl(VerifySignatureContextImpl&&) noexcept; + VerifySignatureContextImpl& operator=(VerifySignatureContextImpl&&) noexcept; + + // -- IStreamingContext -- + score::Result Init(std::optional> iv) override; + score::Result Update(score::cpp::span data) override; + score::Result Reset() override; + + // -- IVerifySignatureContext -- + score::Result VerifyFinalize(score::cpp::span signature) override; + score::Result SingleShot(score::cpp::span data, + score::cpp::span signature) override; + + private: + void CloseContext() noexcept; + + std::shared_ptr m_connection; + score::crypto::daemon::control_plane::protocol::DataNodeId m_context_id; + AlgorithmId m_algorithm; + std::shared_ptr m_transcoder; ///< Stack-shared transcoder; null allowed. +}; + +} // namespace crypto + +} // namespace score + +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_SRC_VERIFY_SIGNATURE_CONTEXT_IMPL_HPP diff --git a/score/crypto/src/api/future/config/BUILD b/score/crypto/src/api/future/config/BUILD index bf631ee2c..38af7eaa7 100644 --- a/score/crypto/src/api/future/config/BUILD +++ b/score/crypto/src/api/future/config/BUILD @@ -24,11 +24,7 @@ load("@rules_cc//cc:defs.bzl", "cc_library") # "aead_context_config.hpp", # "certificate_context_config.hpp", # "certificate_verification_context_config.hpp", -# "cipher_context_config.hpp", # "csr_generation_context_config.hpp", -# "random_context_config.hpp", -# "sign_context_config.hpp", -# "verify_signature_context_config.hpp", # ], # deps = [ # "//score/crypto/src/api/common:crypto_common", diff --git a/score/crypto/src/api/future/contexts/BUILD b/score/crypto/src/api/future/contexts/BUILD index b425f9caf..bde81d5fd 100644 --- a/score/crypto/src/api/future/contexts/BUILD +++ b/score/crypto/src/api/future/contexts/BUILD @@ -18,28 +18,6 @@ load("@rules_cc//cc:defs.bzl", "cc_library") -# -- Cipher (symmetric encrypt/decrypt) -- -# cc_library( -# name = "cipher_context", -# hdrs = ["i_cipher_context.hpp"], -# deps = [ -# "//score/crypto/src/api/contexts:context_bases", -# "//score/crypto/src/api/common:crypto_common", -# "@score_baselibs//score/result", -# ], -# ) - -# -- Sign / Verify -- -# cc_library( -# name = "sign_contexts", -# hdrs = ["i_sign_context.hpp", "i_verify_signature_context.hpp"], -# deps = [ -# "//score/crypto/src/api/contexts:context_bases", -# "//score/crypto/src/api/common:crypto_common", -# "@score_baselibs//score/result", -# ], -# ) - # -- AEAD -- # cc_library( # name = "aead_context", @@ -51,17 +29,6 @@ load("@rules_cc//cc:defs.bzl", "cc_library") # ], # ) -# -- Random -- -# cc_library( -# name = "random_context", -# hdrs = ["i_random_context.hpp"], -# deps = [ -# "//score/crypto/src/api/contexts:context_bases", -# "//score/crypto/src/api/common:crypto_common", -# "@score_baselibs//score/result", -# ], -# ) - # -- Certificate management + verification + CSR -- # cc_library( # name = "certificate_contexts", diff --git a/score/crypto/src/api/i_crypto_context.hpp b/score/crypto/src/api/i_crypto_context.hpp index 2e614f724..9dfab1958 100644 --- a/score/crypto/src/api/i_crypto_context.hpp +++ b/score/crypto/src/api/i_crypto_context.hpp @@ -31,14 +31,22 @@ namespace crypto // requires forward declarations for parameter and return types. // Config types (used as const& parameters) +class CipherContextConfig; class HashContextConfig; class KeyManagementContextConfig; class MacContextConfig; +class RandomContextConfig; +class SignContextConfig; +class VerifySignatureContextConfig; // Operation contexts (returned as std::unique_ptr) +class ICipherContext; class IHashContext; class IKeyManagementContext; class IMacContext; +class IRandomContext; +class ISignContext; +class IVerifySignatureContext; // Typed object interfaces (returned as std::unique_ptr) class IKeyObject; @@ -48,19 +56,11 @@ class IKeySlotObject; // class AeadContextConfig; // class CertificateContextConfig; // class CertificateVerificationContextConfig; -// class CipherContextConfig; // class CsrGenerationContextConfig; -// class RandomContextConfig; -// class SignContextConfig; -// class VerifySignatureContextConfig; // class IAeadContext; // class ICertificateManagementContext; // class ICertificateVerificationContext; -// class ICipherContext; // class ICsrGenerationContext; -// class IRandomContext; -// class ISignContext; -// class IVerifySignatureContext; // class ICertificateObject; // class ICertSlotObject; // class IProviderObject; @@ -137,25 +137,34 @@ class ICryptoContext virtual score::Result> CreateKeyManagementContext( const KeyManagementContextConfig& config) = 0; - // The following factory methods are declared but not yet active. - // Each is implemented in score/crypto/src/api/future/contexts/ - // and will be moved here together with its IPC implementation. + /// @brief Creates a cipher context. + /// @param config Cipher configuration (algorithm + key + direction required) + /// @note The direction is fixed for the lifetime of the context; create a + /// second context to run the opposite direction with the same key. + virtual score::Result> CreateCipherContext(const CipherContextConfig& config) = 0; + + /// @brief Creates a signature generation context. + /// @param config Sign configuration (algorithm + private key required) + virtual score::Result> CreateSignContext(const SignContextConfig& config) = 0; - // virtual score::Result> CreateCipherContext( - // const CipherContextConfig& config) = 0; + /// @brief Creates a signature verification context. + /// @param config Verification configuration (algorithm + key required) + /// @note The key handle is the same one returned by GenerateKey() for an + /// asymmetric algorithm; the daemon selects its public half. + virtual score::Result> CreateVerifySignatureContext( + const VerifySignatureContextConfig& config) = 0; - // virtual score::Result> CreateSignContext( - // const SignContextConfig& config) = 0; + /// @brief Creates a random number generation context. + /// @param config Random configuration (algorithm and provider both optional) + virtual score::Result> CreateRandomContext(const RandomContextConfig& config) = 0; - // virtual score::Result> CreateVerifySignatureContext( - // const VerifySignatureContextConfig& config) = 0; + // The following factory methods are declared but not yet active. + // Each is implemented in score/crypto/src/api/future/contexts/ + // and will be moved here together with its IPC implementation. // virtual score::Result> CreateAeadContext( // const AeadContextConfig& config) = 0; - // virtual score::Result> CreateRandomContext( - // const RandomContextConfig& config) = 0; - // virtual score::Result> CreateCertificateManagementContext( // const CertificateContextConfig& config) = 0; diff --git a/score/crypto/src/api/src/crypto_context_impl.cpp b/score/crypto/src/api/src/crypto_context_impl.cpp index 1afc8e301..047205089 100644 --- a/score/crypto/src/api/src/crypto_context_impl.cpp +++ b/score/crypto/src/api/src/crypto_context_impl.cpp @@ -15,13 +15,22 @@ #include "score/crypto/src/api/common/error_domain.hpp" #include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/config/cipher_context_config.hpp" #include "score/crypto/src/api/config/hash_context_config.hpp" #include "score/crypto/src/api/config/key_management_context_config.hpp" #include "score/crypto/src/api/config/mac_context_config.hpp" +#include "score/crypto/src/api/config/random_context_config.hpp" +#include "score/crypto/src/api/config/sign_context_config.hpp" +#include "score/crypto/src/api/config/verify_signature_context_config.hpp" +#include "score/crypto/src/api/contexts/src/cipher_context_impl.hpp" #include "score/crypto/src/api/contexts/src/hash_context_impl.hpp" #include "score/crypto/src/api/contexts/src/key_management_context_impl.hpp" #include "score/crypto/src/api/contexts/src/mac_context_impl.hpp" +#include "score/crypto/src/api/contexts/src/random_context_impl.hpp" +#include "score/crypto/src/api/contexts/src/sign_context_impl.hpp" +#include "score/crypto/src/api/contexts/src/verify_signature_context_impl.hpp" #include "score/crypto/src/api/src/provider_type_converter.hpp" +#include "score/crypto/src/daemon/common/context_types.hpp" #include "score/crypto/src/daemon/control_plane/control_protocol.h" #include "score/crypto/src/api/control_plane/i_connection.hpp" @@ -34,20 +43,144 @@ #include // Full definitions needed for Result> return types +#include "score/crypto/src/api/contexts/i_cipher_context.hpp" #include "score/crypto/src/api/contexts/i_hash_context.hpp" #include "score/crypto/src/api/contexts/i_key_management_context.hpp" #include "score/crypto/src/api/contexts/i_mac_context.hpp" +#include "score/crypto/src/api/contexts/i_random_context.hpp" +#include "score/crypto/src/api/contexts/i_sign_context.hpp" +#include "score/crypto/src/api/contexts/i_verify_signature_context.hpp" #include "score/crypto/src/api/objects/i_key_object.hpp" #include "score/crypto/src/api/objects/i_key_slot_object.hpp" #include "score/crypto/src/daemon/mediator/mediator_operations.hpp" +#include +#include + namespace score { namespace crypto { +/// The context-type ids are owned by the daemon side, which dispatches on them. +namespace daemon_common = ::score::crypto::daemon::common; + +namespace +{ + +/// @brief Describes one CTX_CREATE call on the wire. +/// +/// Wire layout is positional and shared by every context type: +/// [0] context_type, [1] algorithm, [2] provider_type (or no-param), +/// [3] key_node_id (keyed contexts only), [4] mode byte (cipher direction +/// or MAC/signature OperationMode). +struct ContextCreationRequest +{ + std::string_view context_type{}; + const AlgorithmId* algorithm{nullptr}; + std::optional provider_type{std::nullopt}; + std::optional key_node_id{std::nullopt}; + std::optional mode{std::nullopt}; +}; + +/// @brief Sends CTX_CREATE to the daemon and returns the new context's node id. +/// +/// Centralises the request/validate/extract sequence that is identical for all +/// context types, so each factory below only has to describe its parameters and +/// wrap the resulting id in the right context implementation. +score::Result CreateDaemonContext( + const std::shared_ptr& connection, + const ContextCreationRequest& request) +{ + namespace proto = ::score::crypto::daemon::control_plane::protocol; + + auto builder = proto::ControlRequestBuilder() + .forDataNodeId(connection->GetConnectionNodeId()) + .operation(score::crypto::daemon::mediator::operations::CreateContext()) + .with_in_string(request.context_type) + .with_in_string(*request.algorithm); + + if (request.provider_type.has_value()) + { + builder = builder.with_in_val_uint8(ProviderTypeConverter::ToWireValue(request.provider_type.value())); + } + else + { + builder = builder.with_no_param(); + } + + if (request.key_node_id.has_value()) + { + builder = builder.with_in_val_uint64(request.key_node_id.value()); + } + + if (request.mode.has_value()) + { + builder = builder.with_in_val_uint8(request.mode.value()); + } + + auto control_req_result = builder.build(); + if (!control_req_result.has_value()) + { + score::mw::log::LogError() << "[API][CryptoContextImpl] ERROR: Failed to build CTX_CREATE request for" + << request.context_type; + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kContextCreationFailed, "Failed to build CTX_CREATE request")}; + } + + auto control_response_res = connection->SendRequest(control_req_result.value()); + + auto validator = proto::ControlResponseValidator::FromResult(control_response_res); + validator.expectOperation(score::crypto::daemon::mediator::operations::CreateContext()).expectSuccess(); + + if (!validator.isValid()) + { + score::mw::log::LogError() << "[API][CryptoContextImpl] ERROR:" << validator.getError(); + // Forward the daemon's own verdict when it gave one — a key whose policy + // forbids this context must surface as kKeyOperationNotPermitted, which + // the caller can act on, rather than a generic creation failure. + return score::Result{ + score::unexpect, + MakeError(validator.getOperationError().value_or(CryptoErrorCode::kContextCreationFailed), + "CTX_CREATE daemon response invalid")}; + } + + auto ctx_id_result = validator.getParameterAt(0, 0); + if (!ctx_id_result.has_value()) + { + score::mw::log::LogError() << "[API][CryptoContextImpl] ERROR: CTX_CREATE response has invalid context_id type"; + return score::Result{ + score::unexpect, + MakeError(CryptoErrorCode::kContextCreationFailed, "CTX_CREATE response has invalid context_id type")}; + } + + return ctx_id_result.value(); +} + +/// @brief Rejects a key handle that cannot drive a keyed operation context. +score::Result ValidateOperationKey(const CryptoResourceId& key, std::string_view context_type) +{ + if (key.id == 0U) + { + score::mw::log::LogError() << "[API][CryptoContextImpl] ERROR: " << context_type << " invalid / missing key id"; + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kContextCreationFailed, "invalid / missing key id")}; + } + + if ((key.type != ResourceType::kKey) && (key.type != ResourceType::kKeySlot)) + { + score::mw::log::LogError() << "[API][CryptoContextImpl] ERROR: " << context_type << " invalid key type"; + return score::Result{ + score::unexpect, MakeError(CryptoErrorCode::kUnsupportedOperation, "invalid key resource type")}; + } + + return std::monostate{}; +} + +} // namespace + CryptoContextImpl::CryptoContextImpl(std::shared_ptr connection, std::shared_ptr transcoder) : m_connection(std::move(connection)), m_transcoder(std::move(transcoder)) @@ -69,7 +202,7 @@ score::Result> CryptoContextImpl::CreateHashContex auto request_builder = proto::ControlRequestBuilder() .forDataNodeId(m_connection->GetConnectionNodeId()) .operation(score::crypto::daemon::mediator::operations::CreateContext()) - .with_in_string("HASH") + .with_in_string(daemon_common::context_types::kHash) .with_in_string(config.algorithm); if (config.provider_type.has_value()) @@ -195,28 +328,40 @@ score::Result CryptoContextImpl::ResolveResource(const Resourc score::Result> CryptoContextImpl::CreateMacContext(const MacContextConfig& config) { - namespace proto = ::score::crypto::daemon::control_plane::protocol; - - if (config.key.id == 0) + auto key_check = ValidateOperationKey(config.key, "CreateMacContext"); + if (!key_check.has_value()) { - return score::Result>{ - score::unexpect, - MakeError(CryptoErrorCode::kContextCreationFailed, "CreateMacContext invalid / missing key id")}; + return score::Result>{score::unexpect, key_check.error()}; } - if (config.key.type != ResourceType::kKey && config.key.type != ResourceType::kKeySlot) + ContextCreationRequest request{}; + request.context_type = daemon_common::context_types::kMac; + request.algorithm = &config.algorithm; + request.provider_type = config.provider_type; + request.key_node_id = config.key.id; + // Routes the daemon to C_Sign* or C_Verify* (EVP_MAC either way for OpenSSL). + request.mode = static_cast(config.operation_mode); + + auto context_id = CreateDaemonContext(m_connection, request); + if (!context_id.has_value()) { - return score::Result>{ - score::unexpect, MakeError(CryptoErrorCode::kUnsupportedOperation, "CreateMacContext invalid key type")}; + return score::Result>{score::unexpect, context_id.error()}; } - // Send CTX_CREATE to the daemon to create a server-side MAC context. - // MAC context requires: context type "MAC", algorithm, and key id. + return std::make_unique(m_connection, context_id.value(), config.algorithm, m_transcoder); +} + +score::Result> CryptoContextImpl::CreateKeyManagementContext( + const KeyManagementContextConfig& config) +{ + namespace proto = ::score::crypto::daemon::control_plane::protocol; + + // Send CTX_CREATE to the daemon to create a server-side key management context. auto request_builder = proto::ControlRequestBuilder() .forDataNodeId(m_connection->GetConnectionNodeId()) .operation(score::crypto::daemon::mediator::operations::CreateContext()) - .with_in_string("MAC") - .with_in_string(config.algorithm); + .with_in_string(daemon_common::context_types::kKeyManagement) + .with_in_string(""); // no algorithm for key management if (config.provider_type.has_value()) { @@ -228,102 +373,140 @@ score::Result> CryptoContextImpl::CreateMacContext( request_builder = request_builder.with_no_param(); } - request_builder = request_builder.with_in_val_uint64(config.key.id); - - // Serialize operation_mode (param[4]) so the daemon can route to C_Sign* or C_Verify*. - request_builder = request_builder.with_in_val_uint8(static_cast(config.operation_mode)); - auto control_req_result = request_builder.build(); if (!control_req_result.has_value()) { - return score::Result>{ + return score::Result>{ score::unexpect, - MakeError(CryptoErrorCode::kContextCreationFailed, "Failed to build CTX_CREATE request for MAC")}; + MakeError(CryptoErrorCode::kContextCreationFailed, "Failed to build CTX_CREATE request for KEY_MGMT")}; } - // Send CTX_CREATE request to daemon auto control_response_res = m_connection->SendRequest(control_req_result.value()); - // Validate CTX_CREATE response auto validator = proto::ControlResponseValidator::FromResult(control_response_res); validator.expectOperation(score::crypto::daemon::mediator::operations::CreateContext()).expectSuccess(); if (!validator.isValid()) { - return score::Result>{ + return score::Result>{ score::unexpect, - MakeError(CryptoErrorCode::kContextCreationFailed, "CTX_CREATE MAC daemon response invalid")}; + MakeError(CryptoErrorCode::kContextCreationFailed, "CTX_CREATE KEY_MGMT daemon response invalid")}; } auto ctx_id_result = validator.getParameterAt(0, 0); if (!ctx_id_result.has_value()) { - return score::Result>{ + return score::Result>{ score::unexpect, - MakeError(CryptoErrorCode::kContextCreationFailed, "CTX_CREATE MAC response has invalid context_id type")}; + MakeError(CryptoErrorCode::kContextCreationFailed, + "CTX_CREATE KEY_MGMT response has invalid context_id type")}; } const uint64_t context_id = ctx_id_result.value(); - auto mac_ctx = std::make_unique(m_connection, context_id, config.algorithm, m_transcoder); + auto key_mgmt_ctx = std::make_unique(m_connection, context_id); - return mac_ctx; + return key_mgmt_ctx; } -score::Result> CryptoContextImpl::CreateKeyManagementContext( - const KeyManagementContextConfig& config) -{ - namespace proto = ::score::crypto::daemon::control_plane::protocol; - - // Send CTX_CREATE to the daemon to create a server-side key management context. - auto request_builder = proto::ControlRequestBuilder() - .forDataNodeId(m_connection->GetConnectionNodeId()) - .operation(score::crypto::daemon::mediator::operations::CreateContext()) - .with_in_string("KEY_MANAGEMENT") - .with_in_string(""); // no algorithm for key management +// --------------------------------------------------------------------------- +// Context Factory — Cipher / Sign / Verify / Random +// --------------------------------------------------------------------------- - if (config.provider_type.has_value()) +score::Result> CryptoContextImpl::CreateCipherContext(const CipherContextConfig& config) +{ + auto key_check = ValidateOperationKey(config.key, "CreateCipherContext"); + if (!key_check.has_value()) { - request_builder = - request_builder.with_in_val_uint8(ProviderTypeConverter::ToWireValue(config.provider_type.value())); + return score::Result>{score::unexpect, key_check.error()}; } - else + + ContextCreationRequest request{}; + request.context_type = daemon_common::context_types::kCipher; + request.algorithm = &config.algorithm; + request.provider_type = config.provider_type; + request.key_node_id = config.key.id; + // The daemon routes to EVP_EncryptInit / EVP_DecryptInit (or C_EncryptInit / + // C_DecryptInit) based on this byte. + request.mode = static_cast(config.direction); + + auto context_id = CreateDaemonContext(m_connection, request); + if (!context_id.has_value()) { - request_builder = request_builder.with_no_param(); + return score::Result>{score::unexpect, context_id.error()}; } - auto control_req_result = request_builder.build(); - if (!control_req_result.has_value()) + return std::make_unique(m_connection, context_id.value(), config.algorithm, m_transcoder); +} + +score::Result> CryptoContextImpl::CreateSignContext(const SignContextConfig& config) +{ + auto key_check = ValidateOperationKey(config.key, "CreateSignContext"); + if (!key_check.has_value()) { - return score::Result>{ - score::unexpect, - MakeError(CryptoErrorCode::kContextCreationFailed, "Failed to build CTX_CREATE request for KEY_MGMT")}; + return score::Result>{score::unexpect, key_check.error()}; } - auto control_response_res = m_connection->SendRequest(control_req_result.value()); + ContextCreationRequest request{}; + request.context_type = daemon_common::context_types::kSign; + request.algorithm = &config.algorithm; + request.provider_type = config.provider_type; + request.key_node_id = config.key.id; + // A signing context always uses the private half of the key pair, regardless + // of what the caller left in BaseContextConfig::operation_mode. + request.mode = static_cast(OperationMode::kGenerate); + + auto context_id = CreateDaemonContext(m_connection, request); + if (!context_id.has_value()) + { + return score::Result>{score::unexpect, context_id.error()}; + } - auto validator = proto::ControlResponseValidator::FromResult(control_response_res); - validator.expectOperation(score::crypto::daemon::mediator::operations::CreateContext()).expectSuccess(); + return std::make_unique(m_connection, context_id.value(), config.algorithm, m_transcoder); +} - if (!validator.isValid()) +score::Result> CryptoContextImpl::CreateVerifySignatureContext( + const VerifySignatureContextConfig& config) +{ + auto key_check = ValidateOperationKey(config.key, "CreateVerifySignatureContext"); + if (!key_check.has_value()) { - return score::Result>{ - score::unexpect, - MakeError(CryptoErrorCode::kContextCreationFailed, "CTX_CREATE KEY_MGMT daemon response invalid")}; + return score::Result>{score::unexpect, key_check.error()}; } - auto ctx_id_result = validator.getParameterAt(0, 0); - if (!ctx_id_result.has_value()) + ContextCreationRequest request{}; + request.context_type = daemon_common::context_types::kVerify; + request.algorithm = &config.algorithm; + request.provider_type = config.provider_type; + request.key_node_id = config.key.id; + // Signals the daemon to bind the public half of the key pair. + request.mode = static_cast(OperationMode::kVerify); + + auto context_id = CreateDaemonContext(m_connection, request); + if (!context_id.has_value()) { - return score::Result>{ - score::unexpect, - MakeError(CryptoErrorCode::kContextCreationFailed, - "CTX_CREATE KEY_MGMT response has invalid context_id type")}; + return score::Result>{score::unexpect, context_id.error()}; } - const uint64_t context_id = ctx_id_result.value(); - auto key_mgmt_ctx = std::make_unique(m_connection, context_id); + return std::make_unique( + m_connection, context_id.value(), config.algorithm, m_transcoder); +} - return key_mgmt_ctx; +score::Result> CryptoContextImpl::CreateRandomContext(const RandomContextConfig& config) +{ + // No key and no mode byte: an RNG context is keyless, so the wire call stops + // after the provider-type slot. + ContextCreationRequest request{}; + request.context_type = daemon_common::context_types::kRandom; + request.algorithm = &config.algorithm; + request.provider_type = config.provider_type; + + auto context_id = CreateDaemonContext(m_connection, request); + if (!context_id.has_value()) + { + return score::Result>{score::unexpect, context_id.error()}; + } + + return std::make_unique(m_connection, context_id.value(), config.algorithm, m_transcoder); } // --------------------------------------------------------------------------- diff --git a/score/crypto/src/api/src/crypto_context_impl.hpp b/score/crypto/src/api/src/crypto_context_impl.hpp index ae84ec686..d44b08a41 100644 --- a/score/crypto/src/api/src/crypto_context_impl.hpp +++ b/score/crypto/src/api/src/crypto_context_impl.hpp @@ -62,6 +62,11 @@ class CryptoContextImpl final : public ICryptoContext score::Result> CreateMacContext(const MacContextConfig& config) override; score::Result> CreateKeyManagementContext( const KeyManagementContextConfig& config) override; + score::Result> CreateCipherContext(const CipherContextConfig& config) override; + score::Result> CreateSignContext(const SignContextConfig& config) override; + score::Result> CreateVerifySignatureContext( + const VerifySignatureContextConfig& config) override; + score::Result> CreateRandomContext(const RandomContextConfig& config) override; // -- Queries -- score::Result QueryCapabilities(const AlgorithmId& algorithm) override; diff --git a/score/crypto/src/backend/BUILD b/score/crypto/src/backend/BUILD index 756d6b254..6cf8fa65d 100644 --- a/score/crypto/src/backend/BUILD +++ b/score/crypto/src/backend/BUILD @@ -77,7 +77,9 @@ config_setting( ":score_crypto_score_backend_enabled": "True", ":score_crypto_score_openssl_enabled": "True", }, - visibility = ["//:__subpackages__"], + # Public: the integration test selects on this, and py_itf_test does not + # accept a package-scoped config_setting as a select key. + visibility = ["//visibility:public"], ) # ============================================================================ diff --git a/score/crypto/src/daemon/common/BUILD b/score/crypto/src/daemon/common/BUILD index 1cfebd98d..738fe162b 100644 --- a/score/crypto/src/daemon/common/BUILD +++ b/score/crypto/src/daemon/common/BUILD @@ -17,6 +17,7 @@ cc_library( name = "common", hdrs = [ "actors.hpp", + "context_types.hpp", "daemon_error.hpp", "secure_memory.hpp", "types.hpp", diff --git a/score/crypto/src/daemon/common/actors.hpp b/score/crypto/src/daemon/common/actors.hpp index bd7b3e417..71aa4edb1 100644 --- a/score/crypto/src/daemon/common/actors.hpp +++ b/score/crypto/src/daemon/common/actors.hpp @@ -27,6 +27,10 @@ inline constexpr OperationActor OP_ACTOR_PROVIDER = 3; inline constexpr OperationActor OP_ACTOR_HASH_HANDLER = 4; inline constexpr OperationActor OP_ACTOR_KEY_MANAGEMENT = 5; inline constexpr OperationActor OP_ACTOR_MAC_HANDLER = 6; +inline constexpr OperationActor OP_ACTOR_CIPHER_HANDLER = 7; +inline constexpr OperationActor OP_ACTOR_SIGN_HANDLER = 8; +inline constexpr OperationActor OP_ACTOR_VERIFY_HANDLER = 9; +inline constexpr OperationActor OP_ACTOR_RANDOM_HANDLER = 10; // Starting point for custom actors inline constexpr OperationActor CUSTOM_ACTOR_START = 1 << (std::numeric_limits::digits - 1); diff --git a/score/crypto/src/daemon/common/algorithm_info.hpp b/score/crypto/src/daemon/common/algorithm_info.hpp index 1c8c62bcf..bd7435d03 100644 --- a/score/crypto/src/daemon/common/algorithm_info.hpp +++ b/score/crypto/src/daemon/common/algorithm_info.hpp @@ -106,6 +106,9 @@ inline constexpr KeyAlgorithmInfo kKeyAlgorithms[] = { {"AES-256-GCM", 32U}, {"AES-128-CMAC", 16U}, {"AES-256-CMAC", 32U}, + {"ECDSA-P256", 32U}, + {"ECDSA-P384", 48U}, + {"ECDSA-P521", 66U}, }; /// @brief Look up default key size by algorithm name. @@ -122,6 +125,106 @@ inline constexpr KeyAlgorithmInfo kKeyAlgorithms[] = { return std::nullopt; } +// --------------------------------------------------------------------------- +// Symmetric cipher properties (provider-independent) +// --------------------------------------------------------------------------- + +struct CipherAlgorithmInfo +{ + std::string_view name; + std::size_t key_size; ///< Key length in bytes + std::size_t block_size; ///< Cipher block size in bytes; 1 for stream modes + std::size_t iv_size; ///< Required IV / nonce length in bytes; 0 when none +}; + +// Currently only AES-CBC is supported by the daemon, but this table can be +// extended to include other symmetric ciphers (AES-CTR, AES-ECB, etc.) +inline constexpr CipherAlgorithmInfo kCipherAlgorithms[] = { + {"AES-128-CBC", 16U, 16U, 16U}, + {"AES-192-CBC", 24U, 16U, 16U}, + {"AES-256-CBC", 32U, 16U, 16U}, +}; + +/// @brief Look up symmetric cipher properties by algorithm name. +/// @return the entry, or std::nullopt if the algorithm is unknown. +[[nodiscard]] inline constexpr std::optional LookupCipher(std::string_view algorithm) noexcept +{ + for (const auto& entry : kCipherAlgorithms) + { + if (entry.name == algorithm) + { + return entry; + } + } + return std::nullopt; +} + +// --------------------------------------------------------------------------- +// Elliptic-curve / ECDSA properties (provider-independent) +// --------------------------------------------------------------------------- + +/// @brief Properties of a NIST prime curve and the ECDSA variant built on it. +/// +/// @note @c signature_size is the IEEE P1363 fixed-length encoding r?s, which +/// is the on-the-wire signature format of this stack. It is twice the +/// byte length of the field order, so P-521 yields 2 * 66 = 132 bytes. +struct EcCurveInfo +{ + std::string_view name; ///< Curve identifier as used in AlgorithmId, e.g. "P256" + std::string_view openssl_name; ///< OpenSSL group name, e.g. "prime256v1" / "secp384r1" + std::size_t field_size; ///< Byte length of one coordinate / of r and s + std::size_t signature_size; ///< P1363 signature length = 2 * field_size + std::size_t key_bits; ///< Nominal key strength in bits +}; + +/// @note Curve names are spelled without an inner hyphen ("P256", not "P-256") +/// so that the hyphen is unambiguously the separator in composite +/// identifiers such as "ECDSA-P256-SHA256". This matches the AlgorithmId +/// examples documented in score/crypto/src/api/common/types.hpp. +inline constexpr EcCurveInfo kEcCurves[] = { + {"P256", "prime256v1", 32U, 64U, 256U}, + {"P384", "secp384r1", 48U, 96U, 384U}, + // NIST's largest prime curve is P-521 (not P-512); 521 bits is 66 bytes. + {"P521", "secp521r1", 66U, 132U, 521U}, +}; + +/// @brief Extract the curve of an ECDSA signature algorithm identifier. +/// +/// Accepts signature algorithms like ECDSA-P256-SHA256 i.e.ECDSA--. +[[nodiscard]] inline constexpr std::optional LookupEcCurveOfAlgorithm(std::string_view algorithm) noexcept +{ + for (const auto& entry : kEcCurves) + { + if (algorithm.find(entry.name) != std::string_view::npos) + { + return entry; + } + } + return std::nullopt; +} + +/// @brief Extract the message-digest name of a signature algorithm identifier. +/// +/// "ECDSA-P256-SHA256" -> "SHA256". +[[nodiscard]] inline constexpr std::optional LookupSignatureDigest( + std::string_view algorithm) noexcept +{ + for (const auto& entry : kHashAlgorithms) + { + if (algorithm.find(entry.name) != std::string_view::npos) + { + return entry.name; + } + } + return std::nullopt; +} + +/// @brief True when the identifier names an ECDSA key or signature algorithm. +[[nodiscard]] inline constexpr bool IsEcdsaAlgorithm(std::string_view algorithm) noexcept +{ + return (algorithm.find("ECDSA") != std::string_view::npos) && LookupEcCurveOfAlgorithm(algorithm).has_value(); +} + } // namespace score::crypto::daemon::common #endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_ALGORITHM_INFO_HPP diff --git a/score/crypto/src/daemon/common/context_types.hpp b/score/crypto/src/daemon/common/context_types.hpp new file mode 100644 index 000000000..658620303 --- /dev/null +++ b/score/crypto/src/daemon/common/context_types.hpp @@ -0,0 +1,99 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_COMMON_CONTEXT_TYPES_HPP +#define SCORE_CRYPTO_SRC_DAEMON_COMMON_CONTEXT_TYPES_HPP + +#include "score/crypto/src/api/common/types.hpp" + +#include +#include +#include + +namespace score::crypto::daemon::common +{ + +/// @brief Canonical context-type identifiers sent as CTX_CREATE param[0]. +/// +/// The client writes these, the mediator dispatches on them, and every provider +/// handler factory keys its lookup table off them — so they live here rather +/// than being spelled out as literals in each of those places. +namespace context_types +{ +inline constexpr std::string_view kHash = "HASH"; +inline constexpr std::string_view kMac = "MAC"; +inline constexpr std::string_view kCipher = "CIPHER"; +inline constexpr std::string_view kSign = "SIGN"; +inline constexpr std::string_view kVerify = "VERIFY"; +inline constexpr std::string_view kRandom = "RANDOM"; +inline constexpr std::string_view kKeyManagement = "KEY_MANAGEMENT"; +} // namespace context_types + +/// @brief The key permission a context of this type consumes for its whole lifetime. +/// +/// A context is bound to one key and one direction at CTX_CREATE and cannot +/// change either afterwards, so the permission it needs is fully determined +/// here. That makes context creation the right place to enforce it: the check +/// happens once, and no later operation on the context can escape it. +/// +/// @param context_type CTX_CREATE param[0]. +/// @param mode CTX_CREATE param[4] — a CipherDirection for CIPHER +/// contexts, an OperationMode for MAC/SIGN/VERIFY. +/// +/// @return The required permission, or std::nullopt when the context type +/// consumes no key permission: +/// - HASH and RANDOM bind no key at all. +/// - KEY_MANAGEMENT binds no key at CTX_CREATE; its operations carry +/// their own key references and are checked individually (kDerive +/// for DeriveKey, kWrap for WrapKey, kExport for ExportKey). +[[nodiscard]] inline constexpr std::optional RequiredKeyPermission( + std::string_view context_type, + std::optional mode) noexcept +{ + using Permission = score::crypto::KeyOperationPermission; + + if (context_type == context_types::kMac) + { + // One permission covers both directions: verifying a MAC means + // recomputing it, so a key that can verify can also generate. + return Permission::kMac; + } + if (context_type == context_types::kSign) + { + return Permission::kSign; + } + if (context_type == context_types::kVerify) + { + return Permission::kVerify; + } + if (context_type == context_types::kCipher) + { + if (!mode.has_value()) + { + // Direction is mandatory for cipher contexts, so this is a + // malformed request. Demanding both bits fails closed: a key + // granted only one direction cannot slip through on a request + // that declined to say which direction it wanted. + return Permission::kEncrypt | Permission::kDecrypt; + } + return (static_cast(mode.value()) == score::crypto::CipherDirection::kEncrypt) + ? Permission::kEncrypt + : Permission::kDecrypt; + } + + return std::nullopt; +} + +} // namespace score::crypto::daemon::common + +#endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_CONTEXT_TYPES_HPP diff --git a/score/crypto/src/daemon/control_plane/control_protocol.h b/score/crypto/src/daemon/control_plane/control_protocol.h index 7821c06c1..86255242a 100644 --- a/score/crypto/src/daemon/control_plane/control_protocol.h +++ b/score/crypto/src/daemon/control_plane/control_protocol.h @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -588,6 +589,7 @@ class ControlResponseValidator auto errorCode = static_cast(op.result); m_isValid = false; + m_operationError = errorCode; m_errorMsg = "Operation at index " + std::to_string(m_currentOpIndex) + " failed with error code " + std::string(score::crypto::kCryptoErrorDomain.MessageFor( static_cast(errorCode))); @@ -596,6 +598,22 @@ class ControlResponseValidator return *this; } + /// The error code the daemon reported, when the failure came from the daemon. + /// + /// expectSuccess() folds the code into a human-readable message for logging; + /// this exposes the code itself so a caller can forward the daemon's verdict + /// instead of collapsing every failure into one generic error. A client that + /// asked for something the key policy forbids needs to see + /// kKeyOperationNotPermitted, not "context creation failed". + /// + /// Returns std::nullopt when validation failed for a client-side reason — + /// no response at all, wrong operation id, missing or mistyped parameter — + /// because there is no daemon verdict to report in those cases. + [[nodiscard]] std::optional getOperationError() const + { + return m_operationError; + } + // ======================================================================== // Parameter Verification & Extraction (applies to specific operation) // ======================================================================== @@ -684,6 +702,10 @@ class ControlResponseValidator std::string m_errorMsg; bool m_logErrors = false; + /// Set only by expectSuccess(), so it stays empty for client-side + /// validation failures where the daemon returned no verdict. + std::optional m_operationError; + void logError() { if (m_logErrors) diff --git a/score/crypto/src/daemon/key_management/interfaces/key_management_operations.hpp b/score/crypto/src/daemon/key_management/interfaces/key_management_operations.hpp index 7ea07fb13..c369d16da 100644 --- a/score/crypto/src/daemon/key_management/interfaces/key_management_operations.hpp +++ b/score/crypto/src/daemon/key_management/interfaces/key_management_operations.hpp @@ -32,7 +32,10 @@ using OperationAction = common::OperationAction; // KEY_GENERATE (ephemeral) // Request: data_node_id = context_id, // param[0]: string — key algorithm (e.g., "AES-256", "HMAC-SHA256") -// param[1]: uint32 — permissions bitmask (KeyOperationPermission) +// param[1]: uint32 — permissions bitmask (KeyOperationPermission); +// for an asymmetric key this governs the private half +// param[2]: uint32 — public-half permissions bitmask (optional, asymmetric +// only); absent means the public half is unrestricted // Response: status_code (SUCCESS/error) // param[0]: uint64 — daemon-assigned ephemeral key resource id // param[1]: uint16 — primary provider id (optional) diff --git a/score/crypto/src/daemon/key_management/interfaces/key_types.hpp b/score/crypto/src/daemon/key_management/interfaces/key_types.hpp index 5c4b9a345..692b1f792 100644 --- a/score/crypto/src/daemon/key_management/interfaces/key_types.hpp +++ b/score/crypto/src/daemon/key_management/interfaces/key_types.hpp @@ -44,11 +44,57 @@ struct ProviderKeyHandle std::uint64_t opaque_id{0U}; common::ProviderId provider_id{common::kInvalidProviderId}; bool is_asymmetric{false}; + + /// Operations this key may perform. For an asymmetric key this governs the + /// private half only — see public_key_permissions and GrantedPermissionsFor(). score::crypto::KeyOperationPermission permissions{score::crypto::KeyOperationPermission::kNone}; + + /// Operations the public half may perform (asymmetric keys only). + /// + /// std::nullopt means unrestricted, which is the documented default of + /// GenerateKeyParams::public_key_permissions: a public key is public + /// information, so withholding kVerify/kEncrypt by default would cost + /// compatibility without protecting anything. Always nullopt for + /// symmetric keys, which have no second half. + std::optional public_key_permissions{std::nullopt}; + common::AlgorithmId algorithm{}; std::size_t key_size{0U}; }; +/// @brief The permission set that governs @p required for this key. +/// +/// An asymmetric key carries two permission sets because its halves are used +/// by different operations: the private half signs, decrypts, agrees and +/// derives; the public half verifies, encrypts and wraps. Checking a verify +/// request against the private half's permissions would deny a correctly +/// provisioned sign-only key its legitimate public use, so the caller's +/// intended operation selects which set applies. +/// +/// A symmetric key has one half and one permission set, so `permissions` +/// always applies. +/// +/// @param handle The key whose permissions are being consulted. +/// @param required The single permission bit the caller intends to exercise. +[[nodiscard]] inline score::crypto::KeyOperationPermission GrantedPermissionsFor( + const ProviderKeyHandle& handle, + score::crypto::KeyOperationPermission required) noexcept +{ + using Permission = score::crypto::KeyOperationPermission; + + /// Operations that consume the public half of a key pair. + constexpr Permission kPublicHalfOperations = Permission::kVerify | Permission::kEncrypt | Permission::kWrap; + + // HasPermission(kPublicHalfOperations, required) asks whether `required` is + // a subset of the public-half operations, i.e. "is this a public-half use?". + if (handle.is_asymmetric && score::crypto::HasPermission(kPublicHalfOperations, required)) + { + return handle.public_key_permissions.value_or(Permission::kAll); + } + + return handle.permissions; +} + // --------------------------------------------------------------------------- // Request parameter structs // --------------------------------------------------------------------------- diff --git a/score/crypto/src/daemon/mediator/src/mediator_impl.cpp b/score/crypto/src/daemon/mediator/src/mediator_impl.cpp index b4cd5d8ab..19340ba7d 100644 --- a/score/crypto/src/daemon/mediator/src/mediator_impl.cpp +++ b/score/crypto/src/daemon/mediator/src/mediator_impl.cpp @@ -16,13 +16,16 @@ #include #include +#include #include #include #include +#include #include #include "score/crypto/src/api/common/error_domain.hpp" #include "score/crypto/src/daemon/common/actors.hpp" +#include "score/crypto/src/daemon/common/context_types.hpp" #include "score/crypto/src/daemon/common/operation_names.hpp" #include "score/crypto/src/daemon/common/types.hpp" #include "score/crypto/src/daemon/config/inc/config.hpp" @@ -32,6 +35,7 @@ #include "score/crypto/src/daemon/data_manager/i_data_manager.hpp" #include "score/crypto/src/daemon/data_plane/src/shm_data_node.hpp" #include "score/crypto/src/daemon/key_management/interfaces/i_key_handler.hpp" +#include "score/crypto/src/daemon/key_management/interfaces/key_types.hpp" #include "score/crypto/src/daemon/mediator/i_mediator.hpp" #include "score/crypto/src/daemon/mediator/mediator_operations.hpp" #include "score/crypto/src/daemon/mediator/src/mediator_impl.hpp" @@ -51,14 +55,14 @@ namespace score::crypto::daemon::mediator /// @brief Decode a ProviderType wire value (from the IPC protocol) into the /// daemon-internal CryptoProviderType capability classification. /// -/// The wire encoding is the uint8_t value of the client-side mw::crypto::ProviderType +/// The wire encoding is the uint8_t value of the client-side crypto::ProviderType /// enumerator (0=kDefault, 1=kHardware, 2=kSoftware, 3=kHardwarePreferred, 4=kSoftwarePreferred). /// kHardwarePreferred / kSoftwarePreferred are resolved to their primary type; the /// daemon's ProviderManager::GetProvider() handles fallback to SOFTWARE/HARDWARE if /// the preferred type is not registered. static common::CryptoProviderType FromWireProviderType(std::uint8_t wire_value) noexcept { - // Wire values match mw::crypto::ProviderType enumerator positions: + // Wire values match crypto::ProviderType enumerator positions: // 0=kDefault, 1=kHardware, 2=kSoftware, 3=kHardwarePreferred, 4=kSoftwarePreferred switch (wire_value) { @@ -75,6 +79,25 @@ static common::CryptoProviderType FromWireProviderType(std::uint8_t wire_value) } } +/// @brief Read the optional mode byte from CTX_CREATE param[4]. +/// +/// Carries a CipherDirection for cipher contexts and an OperationMode for +/// MAC/SIGN/VERIFY. Absent for context types that have only one mode. +static std::optional ExtractContextMode(const common::RequestParameters& params) noexcept +{ + constexpr std::size_t kModeParamIndex = 4U; + if (params.size() <= kModeParamIndex) + { + return std::nullopt; + } + const auto* mode = std::get_if(¶ms[kModeParamIndex]); + if (mode == nullptr) + { + return std::nullopt; + } + return *mode; +} + MediatorImpl::MediatorImpl(MediatorDependencies deps) : IMediator(std::move(deps)) { if (m_km_service) @@ -248,6 +271,54 @@ bool MediatorImpl::ForwardSingleOperation(const control_plane::ControlRequest& r return ExecuteOperation(exec_ctx, handler, responseBuilder); } +score::crypto::Expected +MediatorImpl::BindAndAuthorizeKey(std::uint64_t client_id, + std::uint64_t context_node_id, + std::uint64_t& key_node_id, + const common::ProviderId& provider_id, + std::string_view context_type, + const common::RequestParameters& params) +{ + if (!m_km_service) + { + score::mw::log::LogError() << "[SCORE_API_MED] ERROR - key binding requires key management service"; + return score::crypto::make_unexpected(score::crypto::CryptoErrorCode::kUnsupportedOperation); + } + + auto bind_res = m_km_service->BindKeyToContext(client_id, context_node_id, key_node_id, provider_id); + if (!bind_res.has_value()) + { + score::mw::log::LogError() << "[SCORE_API_MED] ERROR - key binding failed for key_node_id=" << key_node_id; + return score::crypto::make_unexpected(score::crypto::CryptoErrorCode::kInvalidArgument); + } + + key_node_id = static_cast(bind_res.value().resolved_node_id); + auto key_handler = bind_res.value().key_handler; + + // A context is bound to one key, one operation and one direction for its + // whole life, so a single check here covers every operation that will ever + // run on it. Enforcing at CTX_CREATE also fails fast: the client learns the + // key is not usable for this purpose before it streams any data. + const auto required = common::RequiredKeyPermission(context_type, ExtractContextMode(params)); + if (!required.has_value()) + { + return key_handler; + } + + const auto& handle = key_handler->GetHandle(); + const auto granted = key_management::GrantedPermissionsFor(handle, required.value()); + if (!score::crypto::HasPermission(granted, required.value())) + { + score::mw::log::LogError() << "[SCORE_API_MED] ERROR - key does not permit this operation" + << " (context_type=" << context_type << ", key_node_id=" << key_node_id + << ", required=" << static_cast(required.value()) + << ", granted=" << static_cast(granted) << ")"; + return score::crypto::make_unexpected(score::crypto::CryptoErrorCode::kKeyOperationNotPermitted); + } + + return key_handler; +} + bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::control_plane::ControlRequest& request, const control_plane::SingleOperationRequest& operation, control_plane::protocol::OperationResponseBuilder& responseBuilder) @@ -363,32 +434,19 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c } auto context_node_id = context_id_res.value(); - // --- Optional key binding: resolve key and bind to context node --- + // --- Optional key binding: resolve key, bind to context node, authorize --- key_management::IKeyHandler::Sptr bound_key_handler; if (has_key_binding) { - if (!m_km_service) - { - score::mw::log::LogError() << "[SCORE_API_MED] ERROR - key binding requires key management service"; - m_data_manager->deleteNode(client_id, context_node_id); - responseBuilder.operation(operation.operationId) - .return_error(score::crypto::CryptoErrorCode::kUnsupportedOperation); - return false; - } - - auto bind_res = - m_km_service->BindKeyToContext(client_id, context_node_id, key_node_id, provider->GetProviderId()); + auto bind_res = BindAndAuthorizeKey( + client_id, context_node_id, key_node_id, provider->GetProviderId(), context_type, operation.parameters); if (!bind_res.has_value()) { - score::mw::log::LogError() << "[SCORE_API_MED] ERROR - key binding failed for key_node_id=" << key_node_id; m_data_manager->deleteNode(client_id, context_node_id); - responseBuilder.operation(operation.operationId) - .return_error(score::crypto::CryptoErrorCode::kInvalidArgument); + responseBuilder.operation(operation.operationId).return_error(bind_res.error()); return false; } - - key_node_id = static_cast(bind_res.value().resolved_node_id); - bound_key_handler = bind_res.value().key_handler; + bound_key_handler = bind_res.value(); } // --- Build InitializationParams and initialize the handler --- diff --git a/score/crypto/src/daemon/mediator/src/mediator_impl.hpp b/score/crypto/src/daemon/mediator/src/mediator_impl.hpp index e9fecd234..45bee863e 100644 --- a/score/crypto/src/daemon/mediator/src/mediator_impl.hpp +++ b/score/crypto/src/daemon/mediator/src/mediator_impl.hpp @@ -69,6 +69,31 @@ class MediatorImpl : public IMediator const control_plane::SingleOperationRequest& operation, score::crypto::daemon::control_plane::protocol::OperationResponseBuilder& responseBuilder); + /// @brief Resolves a client-supplied key reference and authorizes it for the context. + /// + /// Performs the two steps that must not be separated: binding the key to the + /// context node, and checking that the key's permissions actually cover what + /// the context intends to do with it. Called only when CTX_CREATE carried a + /// key reference. + /// + /// @param client_id Authenticated client requesting the context. + /// @param context_node_id Node the key is being bound to. + /// @param key_node_id In: the client-supplied reference (key or slot). + /// Out: the resolved live key node id. + /// @param provider_id Provider that will own the context. + /// @param context_type CTX_CREATE param[0]. + /// @param params Full CTX_CREATE parameter list (param[4] carries the mode). + /// + /// @return The bound key handler, or the error code to report to the client. + /// The caller owns cleanup of @p context_node_id on failure. + score::crypto::Expected BindAndAuthorizeKey( + std::uint64_t client_id, + std::uint64_t context_node_id, + std::uint64_t& key_node_id, + const common::ProviderId& provider_id, + std::string_view context_type, + const common::RequestParameters& params); + // Private helpers // Shared operation execution helper - handles parameter extraction, execution, and response building bool ExecuteOperation(const OperationExecutionContext& exec_ctx, diff --git a/score/crypto/src/daemon/provider/executors/key_mgmt_request_parser.hpp b/score/crypto/src/daemon/provider/executors/key_mgmt_request_parser.hpp index cfcc5c714..7b43fa29f 100644 --- a/score/crypto/src/daemon/provider/executors/key_mgmt_request_parser.hpp +++ b/score/crypto/src/daemon/provider/executors/key_mgmt_request_parser.hpp @@ -68,31 +68,39 @@ namespace key_mgmt_request_parser return *val; } -/// Try to extract an optional uint64 permission value at the given index. +/// Try to extract an optional permission bitmask at the given index. /// -/// Returns std::nullopt when the index is out of range or the variant -/// alternative does not hold a uint64_t (both are silently acceptable). -[[nodiscard]] inline std::optional ExtractOptionalPermissions(const common::RequestParameters& request, - std::size_t index) +/// KeyOperationPermission is a uint32_t enum and the API serialises it with +/// with_in_val_uint32(), so uint32_t is the alternative to look for. The +/// variant keeps uint32_t and uint64_t distinct and the flatbuffers transport +/// preserves that distinction, so reading the wrong width yields nullopt — +/// silently downgrading a restricted key to the kAll default. +/// +/// Returns std::nullopt when the index is out of range or the parameter is not +/// a uint32_t (both mean "caller did not specify permissions"). +[[nodiscard]] inline std::optional ExtractOptionalPermissions( + const common::RequestParameters& request, + std::size_t index) { if (request.size() <= index) { return std::nullopt; } - const auto* val = std::get_if(&request[index]); + const auto* val = std::get_if(&request[index]); if (val == nullptr) { return std::nullopt; } - return *val; + return static_cast(*val); } /// Build a KeyGenerationRequest from the packed request parameters. /// /// Expected layout: -/// request[0] = algorithm (string_view, required) -/// request[1] = permissions (uint64_t, optional) +/// request[0] = algorithm (string_view, required) +/// request[1] = permissions (uint32_t, optional) +/// request[2] = public_key_permissions (uint32_t, optional, asymmetric only) [[nodiscard]] inline Expected BuildGenerationRequest(const common::RequestParameters& request) { @@ -108,9 +116,15 @@ BuildGenerationRequest(const common::RequestParameters& request) const auto perm = ExtractOptionalPermissions(request, 1U); if (perm.has_value()) { - req.permissions = static_cast(perm.value()); + req.permissions = perm.value(); } + // Left as nullopt when absent, which the key factory reads as "public half + // unrestricted" — the contract documented on GenerateKeyParams. The public + // half of a key pair is public information, so withholding kVerify/kEncrypt + // by default would cost compatibility without buying confidentiality. + req.public_key_permissions = ExtractOptionalPermissions(request, 2U); + return req; } diff --git a/score/crypto/src/daemon/provider/executors/src/key_mgmt_executor.cpp b/score/crypto/src/daemon/provider/executors/src/key_mgmt_executor.cpp index 8228d55e0..98aaf82d8 100644 --- a/score/crypto/src/daemon/provider/executors/src/key_mgmt_executor.cpp +++ b/score/crypto/src/daemon/provider/executors/src/key_mgmt_executor.cpp @@ -90,7 +90,8 @@ Expected diff --git a/score/crypto/src/daemon/provider/handler/BUILD b/score/crypto/src/daemon/provider/handler/BUILD index 040b0fdb1..fa34b7117 100644 --- a/score/crypto/src/daemon/provider/handler/BUILD +++ b/score/crypto/src/daemon/provider/handler/BUILD @@ -99,3 +99,27 @@ cc_library( visibility = ["//:__subpackages__"], deps = ["//score/crypto/src/daemon/common"], ) + +# Cipher handler operations header-only library +cc_library( + name = "cipher_handler_operations", + hdrs = ["operations/cipher_handler_operations.hpp"], + visibility = ["//:__subpackages__"], + deps = ["//score/crypto/src/daemon/common"], +) + +# Signature (sign + verify) handler operations header-only library +cc_library( + name = "signature_handler_operations", + hdrs = ["operations/signature_handler_operations.hpp"], + visibility = ["//:__subpackages__"], + deps = ["//score/crypto/src/daemon/common"], +) + +# Random handler operations header-only library +cc_library( + name = "random_handler_operations", + hdrs = ["operations/random_handler_operations.hpp"], + visibility = ["//:__subpackages__"], + deps = ["//score/crypto/src/daemon/common"], +) diff --git a/score/crypto/src/daemon/provider/handler/operations/cipher_handler_operations.hpp b/score/crypto/src/daemon/provider/handler/operations/cipher_handler_operations.hpp new file mode 100644 index 000000000..0f3e4a17f --- /dev/null +++ b/score/crypto/src/daemon/provider/handler/operations/cipher_handler_operations.hpp @@ -0,0 +1,120 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_HANDLER_OPERATIONS_CIPHER_HANDLER_OPERATIONS_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_HANDLER_OPERATIONS_CIPHER_HANDLER_OPERATIONS_HPP + +#include "score/crypto/src/daemon/common/types.hpp" + +#include + +namespace score +{ +namespace crypto +{ +namespace daemon +{ +namespace provider +{ +namespace handler +{ +namespace cipher_handler_operations +{ +using OperationAction = common::OperationAction; + +// ============================================================================ +// Common symmetric cipher operations +// ============================================================================ +// The direction (encrypt / decrypt) is fixed at CTX_CREATE time via param[4] +// and is therefore not repeated on any of the operations below. +// ============================================================================ + +// CIPHER_INIT +// Request: data_node_id = context_id, +// param[0]: optional DataBuffer — initialization vector / nonce. +// Required for IV-based modes (CBC, CTR); absent for ECB. +// Response: status_code (SUCCESS/error) +// no output parameters +// Effect: Calls InitCipher(), transitions state IDLE → INITIALIZED +inline constexpr OperationAction CIPHER_INIT = 1; + +// CIPHER_UPDATE +// Request: data_node_id = context_id, +// param[0]: DataBuffer — input chunk (plaintext when encrypting, +// ciphertext when decrypting) +// param[1]: DataShm(InOut) — caller's output buffer. Must hold the +// input length plus one block: EVP emits a buffered partial +// block ahead of the current chunk. +// Response: status_code (SUCCESS/error) +// param[0]: uint64_t — bytes written. May be shorter than the input +// (block buffering) or zero. +// Effect: Calls UpdateCipher(), transitions state INITIALIZED/ACTIVE → ACTIVE +inline constexpr OperationAction CIPHER_UPDATE = 2; + +// CIPHER_FINALIZE +// Request: data_node_id = context_id, +// param[0]: DataShm(InOut) — caller's output buffer for the trailing +// bytes (final padded block for CBC-with-padding, nothing +// for stream modes) +// Response: status_code (SUCCESS/error) +// param[0]: uint64_t — bytes written +// Effect: Calls FinalizeCipher(), clears stream context, transitions state → IDLE +inline constexpr OperationAction CIPHER_FINALIZE = 3; + +// CIPHER_SS (Single-Shot) +// Request: data_node_id = context_id, +// param[0]: DataBuffer — initialization vector (may be empty for ECB) +// param[1]: DataBuffer — full input +// param[2]: DataShm(InOut) — caller's output buffer, sized for the +// input length plus one block +// Response: status_code (SUCCESS/error) +// param[0]: uint64_t — bytes written +// Effect: Requires IDLE state; performs init + update + finalize in one call +inline constexpr OperationAction CIPHER_SS = 4; + +// CIPHER_GET_OUTPUT_SIZE +// Request: data_node_id = context_id, +// no operation parameters +// Response: status_code (SUCCESS/error) +// param[0]: uint64_t — cipher block size in bytes (16 for AES, +// 1 for stream modes such as CTR) +// Effect: Stateless query; does not affect the stream state +inline constexpr OperationAction CIPHER_GET_OUTPUT_SIZE = 5; + +// CIPHER_RESET +// Request: data_node_id = context_id, +// no operation parameters +// Response: status_code (SUCCESS/error) +// no output parameters +// Effect: Calls Reset(), discards intermediate state, transitions state → IDLE. +// Key binding, algorithm and direction are preserved. +inline constexpr OperationAction CIPHER_RESET = 6; + +// CIPHER_GET_IV_SIZE +// Request: data_node_id = context_id, +// no operation parameters +// Response: status_code (SUCCESS/error) +// param[0]: uint64_t — required IV length in bytes (0 for ECB) +// Effect: Stateless query; does not affect the stream state +inline constexpr OperationAction CIPHER_GET_IV_SIZE = 7; + +inline constexpr OperationAction CIPHER_CUSTOM_OP_START = 1 << (std::numeric_limits::digits - 1); + +} // namespace cipher_handler_operations +} // namespace handler +} // namespace provider +} // namespace daemon +} // namespace crypto +} // namespace score + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_HANDLER_OPERATIONS_CIPHER_HANDLER_OPERATIONS_HPP diff --git a/score/crypto/src/daemon/provider/handler/operations/random_handler_operations.hpp b/score/crypto/src/daemon/provider/handler/operations/random_handler_operations.hpp new file mode 100644 index 000000000..2743a8bc5 --- /dev/null +++ b/score/crypto/src/daemon/provider/handler/operations/random_handler_operations.hpp @@ -0,0 +1,68 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_HANDLER_OPERATIONS_RANDOM_HANDLER_OPERATIONS_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_HANDLER_OPERATIONS_RANDOM_HANDLER_OPERATIONS_HPP + +#include "score/crypto/src/daemon/common/types.hpp" + +#include + +namespace score +{ +namespace crypto +{ +namespace daemon +{ +namespace provider +{ +namespace handler +{ +namespace random_handler_operations +{ +using OperationAction = common::OperationAction; + +// ============================================================================ +// Random number generation operations +// ============================================================================ +// The random context is non-streaming: there is no state machine and every +// operation is valid at any time. +// ============================================================================ + +// RANDOM_GENERATE +// Request: data_node_id = context_id, +// param[0]: DataShm(InOut) — caller's output buffer; its length is the +// number of random bytes requested +// Response: status_code (SUCCESS/error) +// param[0]: uint64_t — bytes written, always the full buffer length +inline constexpr OperationAction RANDOM_GENERATE = 1; + +// RANDOM_SEED +// Request: data_node_id = context_id, +// param[0]: DataBuffer — additional entropy to mix into the RNG state +// Response: status_code (SUCCESS/error) +// no output parameters +// Effect: Providers whose entropy source cannot be seeded externally report +// SUCCESS without changing any state. +inline constexpr OperationAction RANDOM_SEED = 2; + +inline constexpr OperationAction RANDOM_CUSTOM_OP_START = 1 << (std::numeric_limits::digits - 1); + +} // namespace random_handler_operations +} // namespace handler +} // namespace provider +} // namespace daemon +} // namespace crypto +} // namespace score + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_HANDLER_OPERATIONS_RANDOM_HANDLER_OPERATIONS_HPP diff --git a/score/crypto/src/daemon/provider/handler/operations/signature_handler_operations.hpp b/score/crypto/src/daemon/provider/handler/operations/signature_handler_operations.hpp new file mode 100644 index 000000000..7a273610e --- /dev/null +++ b/score/crypto/src/daemon/provider/handler/operations/signature_handler_operations.hpp @@ -0,0 +1,149 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_HANDLER_OPERATIONS_SIGNATURE_HANDLER_OPERATIONS_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_HANDLER_OPERATIONS_SIGNATURE_HANDLER_OPERATIONS_HPP + +#include "score/crypto/src/daemon/common/types.hpp" + +#include + +namespace score +{ +namespace crypto +{ +namespace daemon +{ +namespace provider +{ +namespace handler +{ + +// ============================================================================ +// Signature generation operations (OP_ACTOR_SIGN_HANDLER) +// ============================================================================ +namespace sign_handler_operations +{ +using OperationAction = common::OperationAction; + +// SIGN_INIT +// Request: data_node_id = context_id, no operation parameters +// Response: status_code (SUCCESS/error), no output parameters +// Effect: Calls InitSign(), transitions state IDLE → INITIALIZED +inline constexpr OperationAction SIGN_INIT = 1; + +// SIGN_UPDATE +// Request: data_node_id = context_id, +// param[0]: DataBuffer — message chunk to be signed +// Response: status_code (SUCCESS/error), no output parameters +// Effect: Calls UpdateSign(), transitions state INITIALIZED/ACTIVE → ACTIVE +inline constexpr OperationAction SIGN_UPDATE = 2; + +// SIGN_FINALIZE +// Request: data_node_id = context_id, +// param[0]: DataShm(InOut) — caller's signature output buffer, at +// least GetSignatureSize() bytes. For ECDSA the signature +// is the fixed-length IEEE P1363 form r‖s. +// Response: status_code (SUCCESS/error) +// param[0]: uint64_t — bytes written +// Effect: Calls FinalizeSign(), transitions state → IDLE +inline constexpr OperationAction SIGN_FINALIZE = 3; + +// SIGN_SS (Single-Shot) +// Request: data_node_id = context_id, +// param[0]: DataBuffer — full message to sign +// param[1]: DataShm(InOut) — caller's signature output buffer +// Response: status_code (SUCCESS/error) +// param[0]: uint64_t — bytes written +// Effect: Requires IDLE state; performs init + update + finalize in one call +inline constexpr OperationAction SIGN_SS = 4; + +// SIGN_GET_SIZE +// Request: data_node_id = context_id, no operation parameters +// Response: status_code (SUCCESS/error) +// param[0]: uint64_t — signature length in bytes +// (64 for P-256, 96 for P-384, 132 for P-521) +// Effect: Stateless query; does not affect the stream state +inline constexpr OperationAction SIGN_GET_SIZE = 5; + +// SIGN_RESET +// Request: data_node_id = context_id, no operation parameters +// Response: status_code (SUCCESS/error), no output parameters +// Effect: Calls Reset(); key binding and algorithm are preserved +inline constexpr OperationAction SIGN_RESET = 6; + +inline constexpr OperationAction SIGN_CUSTOM_OP_START = 1 << (std::numeric_limits::digits - 1); + +} // namespace sign_handler_operations + +// ============================================================================ +// Signature verification operations (OP_ACTOR_VERIFY_HANDLER) +// ============================================================================ +namespace verify_handler_operations +{ +using OperationAction = common::OperationAction; + +// VERIFY_INIT +// Request: data_node_id = context_id, no operation parameters +// Response: status_code (SUCCESS/error), no output parameters +// Effect: Calls InitVerify(), transitions state IDLE → INITIALIZED +inline constexpr OperationAction VERIFY_INIT = 1; + +// VERIFY_UPDATE +// Request: data_node_id = context_id, +// param[0]: DataBuffer — message chunk whose signature is checked +// Response: status_code (SUCCESS/error), no output parameters +// Effect: Calls UpdateVerify(), transitions state INITIALIZED/ACTIVE → ACTIVE +inline constexpr OperationAction VERIFY_UPDATE = 2; + +// VERIFY_FINALIZE +// Request: data_node_id = context_id, +// param[0]: DataBuffer — signature to check (P1363 r‖s for ECDSA) +// Response: status_code (SUCCESS/error) +// param[0]: bool — true when the signature is valid +// Effect: Calls FinalizeVerify(), transitions state → IDLE. +// An invalid signature is reported as SUCCESS + false, not as an error. +inline constexpr OperationAction VERIFY_FINALIZE = 3; + +// VERIFY_SS (Single-Shot) +// Request: data_node_id = context_id, +// param[0]: DataBuffer — full message +// param[1]: DataBuffer — signature to check +// Response: status_code (SUCCESS/error) +// param[0]: bool — true when the signature is valid +// Effect: Requires IDLE state; performs init + update + finalize in one call +inline constexpr OperationAction VERIFY_SS = 4; + +// VERIFY_GET_SIZE +// Request: data_node_id = context_id, no operation parameters +// Response: status_code (SUCCESS/error) +// param[0]: uint64_t — expected signature length in bytes +// Effect: Stateless query; does not affect the stream state +inline constexpr OperationAction VERIFY_GET_SIZE = 5; + +// VERIFY_RESET +// Request: data_node_id = context_id, no operation parameters +// Response: status_code (SUCCESS/error), no output parameters +// Effect: Calls Reset(); key binding and algorithm are preserved +inline constexpr OperationAction VERIFY_RESET = 6; + +inline constexpr OperationAction VERIFY_CUSTOM_OP_START = 1 << (std::numeric_limits::digits - 1); + +} // namespace verify_handler_operations +} // namespace handler +} // namespace provider +} // namespace daemon +} // namespace crypto +} // namespace score + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_HANDLER_OPERATIONS_SIGNATURE_HANDLER_OPERATIONS_HPP diff --git a/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_slot_handler.cpp b/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_slot_handler.cpp index 9c01b2302..35f511281 100644 --- a/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_slot_handler.cpp +++ b/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_slot_handler.cpp @@ -175,8 +175,8 @@ Pkcs11KeySlotHandler::LoadKey(const key_management::KeySlotConfig& slot) search_tmpl.id = id_bytes; search_tmpl.obj_class = obj_class; - const auto handle = - m_key_store->RegisterTokenObject(search_tmpl, slot.algorithm, static_cast(value_len)); + const auto handle = m_key_store->RegisterTokenObject( + search_tmpl, slot.algorithm, static_cast(value_len), slot.allowed_operations); m_provider->ReleaseSession(session, reqs); diff --git a/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.cpp b/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.cpp index 01bfab2af..75ae3b3e0 100644 --- a/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.cpp +++ b/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.cpp @@ -74,9 +74,11 @@ key_management::ProviderKeyHandle Pkcs11KeyStore::Register(CK_SESSION_HANDLE ses }; } -key_management::ProviderKeyHandle Pkcs11KeyStore::RegisterTokenObject(const SearchTemplate& search_template, - const std::string& algorithm, - std::size_t key_size) noexcept +key_management::ProviderKeyHandle Pkcs11KeyStore::RegisterTokenObject( + const SearchTemplate& search_template, + const std::string& algorithm, + std::size_t key_size, + score::crypto::KeyOperationPermission permissions) noexcept { const std::lock_guard lock(m_map_mutex); const uint64_t opaque_id = m_next_opaque_id++; @@ -87,7 +89,7 @@ key_management::ProviderKeyHandle Pkcs11KeyStore::RegisterTokenObject(const Sear return key_management::ProviderKeyHandle{ .opaque_id = opaque_id, .provider_id = m_provider.lock() ? m_provider.lock()->GetProviderId() : common::kInvalidProviderId, - .permissions = score::crypto::KeyOperationPermission::kNone, + .permissions = permissions, .algorithm = algorithm, .key_size = key_size, }; diff --git a/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.hpp b/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.hpp index 56d9dad9a..8bc5778dc 100644 --- a/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.hpp +++ b/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.hpp @@ -101,7 +101,10 @@ class Pkcs11KeyStore CK_OBJECT_HANDLE object, const std::string& algorithm, std::size_t key_size, - score::crypto::KeyOperationPermission permissions = score::crypto::KeyOperationPermission::kNone) noexcept; + // No default: a key registered with kNone cannot be used by any context, + // and silently defaulting to that is how the token-object path lost its + // slot policy. + score::crypto::KeyOperationPermission permissions) noexcept; /// Register a persistent token object by storing its search template. /// @@ -109,9 +112,15 @@ class Pkcs11KeyStore /// C_GetAttributeValue succeed. No session is stored: the caller releases the /// find session back to the pool immediately. Future access uses ResolveObject() /// which re-runs C_FindObjects on the calling handler's session. - [[nodiscard]] key_management::ProviderKeyHandle RegisterTokenObject(const SearchTemplate& search_template, - const std::string& algorithm, - std::size_t key_size) noexcept; + /// + /// @param permissions The slot's allowed_operations. Must be passed through: + /// it is what the daemon checks when a context asks to use this key, + /// and defaulting it to kNone would make the slot unusable. + [[nodiscard]] key_management::ProviderKeyHandle RegisterTokenObject( + const SearchTemplate& search_template, + const std::string& algorithm, + std::size_t key_size, + score::crypto::KeyOperationPermission permissions) noexcept; /// Resolve a PKCS#11 key for use on a crypto handler session. /// diff --git a/score/crypto/src/daemon/provider/pkcs11/operations/factory/pkcs11_handler_factory.hpp b/score/crypto/src/daemon/provider/pkcs11/operations/factory/pkcs11_handler_factory.hpp index 77a95e6ac..83e4f285b 100644 --- a/score/crypto/src/daemon/provider/pkcs11/operations/factory/pkcs11_handler_factory.hpp +++ b/score/crypto/src/daemon/provider/pkcs11/operations/factory/pkcs11_handler_factory.hpp @@ -14,6 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_PKCS11_OPERATIONS_FACTORY_PKCS11_HANDLER_FACTORY_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_PKCS11_OPERATIONS_FACTORY_PKCS11_HANDLER_FACTORY_HPP +#include "score/crypto/src/daemon/common/context_types.hpp" #include "score/crypto/src/daemon/common/types.hpp" #include "score/crypto/src/daemon/key_management/core/key_management_service.hpp" #include "score/crypto/src/daemon/provider/handler/i_crypto_handler_factory.hpp" @@ -22,6 +23,7 @@ #include "score/result/result.h" #include +#include namespace score::crypto::daemon::provider::pkcs11 { @@ -32,9 +34,12 @@ namespace score::crypto::daemon::provider::pkcs11 class Pkcs11Provider; /// @brief Predefined handler IDs supported by the PKCS#11 factory. -inline constexpr const char* const kHashHandlerId = "HASH"; -inline constexpr const char* const kMacHandlerId = "MAC"; -inline constexpr const char* const kKeyManagementHandlerId = "KEY_MANAGEMENT"; +/// +/// A subset of the shared context-type ids: this provider offers no cipher, +/// signature or random contexts. +inline constexpr std::string_view kHashHandlerId = common::context_types::kHash; +inline constexpr std::string_view kMacHandlerId = common::context_types::kMac; +inline constexpr std::string_view kKeyManagementHandlerId = common::context_types::kKeyManagement; /// @brief Factory that creates PKCS#11-backed crypto handlers. /// diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/BUILD b/score/crypto/src/daemon/provider/score_provider/openssl/BUILD index 1ba4c2fd0..aaed70c70 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/BUILD +++ b/score/crypto/src/daemon/provider/score_provider/openssl/BUILD @@ -45,10 +45,13 @@ cc_library( cc_library( name = "provider_openssl_headers", hdrs = [ + "operations/cipher/openssl_cipher_handler.hpp", "operations/factory/openssl_handler_factory.hpp", "operations/hash/openssl_hash_handler.hpp", "operations/key_management/openssl_key_management_handler.hpp", "operations/mac/openssl_hmac_handler.hpp", + "operations/random/openssl_random_handler.hpp", + "operations/signature/openssl_ecdsa_handler.hpp", "provider_openssl.hpp", ], includes = ["."], @@ -62,10 +65,13 @@ cc_library( "//score/crypto/src/daemon/key_management:key_management_headers", "//score/crypto/src/daemon/provider/executors:key_mgmt_executor", "//score/crypto/src/daemon/provider/score_provider", + "//score/crypto/src/daemon/provider/score_provider/operations/cipher:score_cipher_handler", "//score/crypto/src/daemon/provider/score_provider/operations/factory:score_handler_factory", "//score/crypto/src/daemon/provider/score_provider/operations/hash:score_hash_handler", "//score/crypto/src/daemon/provider/score_provider/operations/key_management:score_key_management_handler", "//score/crypto/src/daemon/provider/score_provider/operations/mac:score_mac_handler", + "//score/crypto/src/daemon/provider/score_provider/operations/random:score_random_handler", + "//score/crypto/src/daemon/provider/score_provider/operations/signature:score_signature_handler", ], ) @@ -75,10 +81,13 @@ cc_library( "detail/openssl_algorithm_info.hpp", "key_management/openssl_key_factory.cpp", "key_management/openssl_key_handler.cpp", + "operations/cipher/openssl_cipher_handler.cpp", "operations/factory/openssl_handler_factory.cpp", "operations/hash/openssl_hash_handler.cpp", "operations/key_management/openssl_key_management_handler.cpp", "operations/mac/openssl_hmac_handler.cpp", + "operations/random/openssl_random_handler.cpp", + "operations/signature/openssl_ecdsa_handler.cpp", "provider_openssl.cpp", ], implementation_deps = [ diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.cpp index dd3b1bb7c..87471c748 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.cpp @@ -16,21 +16,55 @@ #include "score/crypto/src/daemon/common/algorithm_info.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.hpp" -#include // OPENSSL_cleanse -#include // RAND_bytes +#include // OSSL_PKEY_PARAM_GROUP_NAME +#include // OPENSSL_cleanse +#include // EVP_PKEY_CTX, EVP_PKEY_generate +#include // OSSL_PARAM +#include // RAND_bytes +#include // d2i_AutoPrivateKey / d2i_PUBKEY + +#include "score/mw/log/logging.h" #include #include +#include #include namespace score::crypto::daemon::provider::openssl { +namespace +{ +constexpr std::string_view kLogPrefix = "[OPENSSL_KEY_FACTORY] "; + +/// Builds the key handle metadata shared by every key this factory produces. +key_management::ProviderKeyHandle MakeHandle(common::ProviderId provider_id, + const common::AlgorithmId& algorithm, + score::crypto::KeyOperationPermission permissions, + std::size_t key_size, + bool is_asymmetric) noexcept +{ + key_management::ProviderKeyHandle handle{}; + handle.provider_id = provider_id; + handle.permissions = permissions; + handle.is_asymmetric = is_asymmetric; + handle.algorithm = algorithm; + handle.key_size = key_size; + return handle; +} +} // namespace + OpenSslKeyFactory::OpenSslKeyFactory(common::ProviderId provider_id) : m_provider_id(provider_id){}; ::score::crypto::Expected OpenSslKeyFactory::GenerateKey(const key_management::KeyGenerationRequest& request) { + if (::score::crypto::daemon::common::IsEcdsaAlgorithm( + std::string_view{request.algorithm.data(), request.algorithm.size()})) + { + return GenerateEcKey(request); + } + const std::size_t key_size = DetermineKeySize(request.algorithm); if (key_size == 0U) { @@ -45,18 +79,87 @@ OpenSslKeyFactory::GenerateKey(const key_management::KeyGenerationRequest& reque return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kOperationFailed); } - key_management::ProviderKeyHandle handle{}; + auto handle = MakeHandle(m_provider_id, request.algorithm, request.permissions, key_size, false); handle.opaque_id = static_cast( reinterpret_cast(key_bytes.data())); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - handle.provider_id = m_provider_id; - handle.permissions = request.permissions; - handle.is_asymmetric = false; - handle.algorithm = request.algorithm; - handle.key_size = key_size; return std::make_shared(std::move(key_bytes), handle); } +::score::crypto::Expected +OpenSslKeyFactory::GenerateEcKey(const key_management::KeyGenerationRequest& request) +{ + const auto curve = ::score::crypto::daemon::common::LookupEcCurveOfAlgorithm( + std::string_view{request.algorithm.data(), request.algorithm.size()}); + if (!curve.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "GenerateEcKey: unsupported curve in algorithm '" + << request.algorithm << "'"; + return ::score::crypto::make_unexpected( + ::score::crypto::daemon::common::DaemonErrorCode::kUnsupportedAlgorithm); + } + + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr); + if (ctx == nullptr) + { + score::mw::log::LogError() << kLogPrefix << "GenerateEcKey: EVP_PKEY_CTX_new_from_name(\"EC\") failed"; + return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kAllocationFailed); + } + + EVP_PKEY* pkey = nullptr; + const bool ok = [&]() -> bool { + if (EVP_PKEY_keygen_init(ctx) != 1) + { + score::mw::log::LogError() << kLogPrefix << "GenerateEcKey: EVP_PKEY_keygen_init failed"; + return false; + } + + // OSSL_PARAM takes a non-const char*, but only reads the group name. + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + char* group_name = const_cast(curve->openssl_name.data()); + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, group_name, curve->openssl_name.size()), + OSSL_PARAM_construct_end(), + }; + if (EVP_PKEY_CTX_set_params(ctx, params) != 1) + { + score::mw::log::LogError() << kLogPrefix << "GenerateEcKey: failed to select group " + << std::string{curve->openssl_name}; + return false; + } + + if (EVP_PKEY_generate(ctx, &pkey) != 1) + { + score::mw::log::LogError() << kLogPrefix << "GenerateEcKey: EVP_PKEY_generate failed"; + return false; + } + return true; + }(); + + EVP_PKEY_CTX_free(ctx); + + // Take ownership immediately so every path below is leak-free. + EvpPkeyPtr owned_pkey{pkey}; + + if (!ok || !owned_pkey) + { + return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kKeyGenerationFailed); + } + + auto handle = MakeHandle(m_provider_id, request.algorithm, request.permissions, curve->field_size, true); + handle.opaque_id = static_cast( + reinterpret_cast(owned_pkey.get())); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + + // One EVP_PKEY holds both halves, so one handler serves the sign and the + // verify context — which is exactly why the two permission sets have to + // travel together rather than being folded into one bitmask here. + handle.public_key_permissions = request.public_key_permissions; + + score::mw::log::LogDebug() << kLogPrefix << "Generated EC key pair on curve " << std::string{curve->name}; + + return std::make_shared(std::move(owned_pkey), handle); +} + ::score::crypto::Expected OpenSslKeyFactory::ImportKey(const key_management::KeyImportRequest& request) { @@ -65,20 +168,77 @@ OpenSslKeyFactory::ImportKey(const key_management::KeyImportRequest& request) return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); } + if (::score::crypto::daemon::common::IsEcdsaAlgorithm( + std::string_view{request.algorithm.data(), request.algorithm.size()})) + { + return ImportEcKey(request); + } + std::vector key_bytes(request.key_data, request.key_data + request.key_data_size); - key_management::ProviderKeyHandle handle{}; + auto handle = MakeHandle(m_provider_id, request.algorithm, request.permissions, request.key_data_size, false); handle.opaque_id = static_cast( reinterpret_cast(key_bytes.data())); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - handle.provider_id = m_provider_id; - handle.permissions = request.permissions; - handle.is_asymmetric = false; - handle.algorithm = request.algorithm; - handle.key_size = request.key_data_size; return std::make_shared(std::move(key_bytes), handle); } +::score::crypto::Expected +OpenSslKeyFactory::ImportEcKey(const key_management::KeyImportRequest& request) +{ + const auto curve = ::score::crypto::daemon::common::LookupEcCurveOfAlgorithm( + std::string_view{request.algorithm.data(), request.algorithm.size()}); + if (!curve.has_value()) + { + return ::score::crypto::make_unexpected( + ::score::crypto::daemon::common::DaemonErrorCode::kUnsupportedAlgorithm); + } + + if (request.format != score::crypto::FormatType::kDer) + { + score::mw::log::LogError() << kLogPrefix << "ImportEcKey: only DER-encoded EC keys are supported"; + return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kInvalidFormat); + } + + // d2i_AutoPrivateKey advances the pointer it is given, so hand it a copy. + const unsigned char* der = request.key_data; + EVP_PKEY* pkey = d2i_AutoPrivateKey(nullptr, &der, static_cast(request.key_data_size)); + if (pkey == nullptr) + { + // Fall back to a SubjectPublicKeyInfo blob: a verify-only slot legitimately + // holds just the public half. + const unsigned char* spki = request.key_data; + pkey = d2i_PUBKEY(nullptr, &spki, static_cast(request.key_data_size)); + } + + // Take ownership immediately so every path below is leak-free. + EvpPkeyPtr owned_pkey{pkey}; + + if (!owned_pkey) + { + score::mw::log::LogError() << kLogPrefix << "ImportEcKey: could not parse DER key material"; + return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kInvalidFormat); + } + + if (EVP_PKEY_get_base_id(owned_pkey.get()) != EVP_PKEY_EC) + { + score::mw::log::LogError() << kLogPrefix << "ImportEcKey: parsed key is not an EC key"; + return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kIncompatibleKeyType); + } + + auto handle = MakeHandle(m_provider_id, request.algorithm, request.permissions, curve->field_size, true); + handle.opaque_id = static_cast( + reinterpret_cast(owned_pkey.get())); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + + // An import request carries one permission set for whatever material it + // contains, so it governs both halves. Leaving this nullopt would mean + // "public half unrestricted", which is the right default for a generated + // key but would let an imported key ignore its slot's allowed_operations. + handle.public_key_permissions = request.permissions; + + return std::make_shared(std::move(owned_pkey), handle); +} + // static std::size_t OpenSslKeyFactory::DetermineKeySize(const common::AlgorithmId& algorithm) noexcept { diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.hpp index 921cec594..8bddadb91 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.hpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.hpp @@ -49,22 +49,39 @@ class OpenSslKeyFactory final : public key_management::IKeyFactory OpenSslKeyFactory(OpenSslKeyFactory&&) = delete; OpenSslKeyFactory& operator=(OpenSslKeyFactory&&) = delete; - /// Generate a symmetric key using OpenSSL RAND_bytes. + /// Generate a symmetric key using OpenSSL RAND_bytes, or an ECDSA key pair. /// - /// Key size is derived from request.algorithm: + /// Symmetric key size is derived from request.algorithm: /// HMAC-SHA256 → 32 B | HMAC-SHA384 → 48 B | HMAC-SHA512 → 64 B /// AES-128-* → 16 B | AES-192-* → 24 B | AES-256-* → 32 B + /// + /// An algorithm naming an ECDSA curve ("ECDSA-P256", "ECDSA-P384", + /// "ECDSA-P521") produces a key pair instead; the returned handler owns a + /// single EVP_PKEY carrying both halves. [[nodiscard]] ::score::crypto::Expected GenerateKey(const key_management::KeyGenerationRequest& request) override; - /// Import raw key material by copying into a new heap buffer. + /// Import raw key material by copying into a new heap buffer, or parse a + /// DER-encoded EC key when request.algorithm names an ECDSA curve. [[nodiscard]] ::score::crypto::Expected ImportKey(const key_management::KeyImportRequest& request) override; private: common::ProviderId m_provider_id{common::kInvalidProviderId}; + + /// Generate an EC key pair on the curve named by request.algorithm. + [[nodiscard]] ::score::crypto::Expected + GenerateEcKey(const key_management::KeyGenerationRequest& request); + + /// Parse DER key material (PKCS#8 private key or SubjectPublicKeyInfo) into + /// an EVP_PKEY. + [[nodiscard]] ::score::crypto::Expected + ImportEcKey(const key_management::KeyImportRequest& request); + /// Map well-known algorithm names to symmetric key sizes in bytes. /// Returns 0 for unknown algorithms. [[nodiscard]] static std::size_t DetermineKeySize(const common::AlgorithmId& algorithm) noexcept; diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.cpp index 29c7e1bf0..cf84e9bea 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.cpp @@ -14,13 +14,29 @@ #include "score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.hpp" #include // OPENSSL_cleanse +#include // EVP_PKEY_free +#include +#include namespace score::crypto::daemon::provider::openssl { +void EvpPkeyDeleter::operator()(evp_pkey_st* pkey) const noexcept +{ + // EVP_PKEY is a typedef for evp_pkey_st, so the forward-declared pointer is + // already the right type. EVP_PKEY_free scrubs the private component + // internally and tolerates nullptr. + EVP_PKEY_free(pkey); +} + OpenSslKeyHandler::OpenSslKeyHandler(std::vector key_bytes, const key_management::ProviderKeyHandle& handle) noexcept - : m_key_bytes{std::move(key_bytes)}, m_handle{handle}, m_released{false} + : m_key_bytes{std::move(key_bytes)}, m_pkey{nullptr}, m_handle{handle}, m_released{false} +{ +} + +OpenSslKeyHandler::OpenSslKeyHandler(EvpPkeyPtr pkey, const key_management::ProviderKeyHandle& handle) noexcept + : m_key_bytes{}, m_pkey{std::move(pkey)}, m_handle{handle}, m_released{false} { } @@ -41,12 +57,22 @@ common::ProviderId OpenSslKeyHandler::GetProviderId() const noexcept ::score::crypto::Expected OpenSslKeyHandler::Release() { - if (!m_released && !m_key_bytes.empty()) + if (m_released) + { + return std::monostate{}; + } + + if (!m_key_bytes.empty()) { OPENSSL_cleanse(m_key_bytes.data(), m_key_bytes.size()); m_key_bytes.clear(); - m_released = true; } + + // Releasing early is an optimisation, not a requirement: if this is never + // called, ~OpenSslKeyHandler destroys m_pkey and EvpPkeyDeleter runs anyway. + m_pkey.reset(); + + m_released = true; return std::monostate{}; } @@ -61,6 +87,12 @@ const std::uint8_t* OpenSslKeyHandler::GetRawKeyBytes(std::size_t& out_size) con return m_key_bytes.data(); } +evp_pkey_st* OpenSslKeyHandler::GetPkey() const noexcept +{ + // Release() resets the pointer, so the released case needs no separate check. + return m_pkey.get(); +} + ::score::crypto::Expected OpenSslKeyHandler::Export() const { @@ -69,7 +101,21 @@ OpenSslKeyHandler::Export() const return ::score::crypto::make_unexpected( ::score::crypto::daemon::common::DaemonErrorCode::kKeyOperationNotPermitted); } - if (m_released || m_key_bytes.empty()) + if (m_released) + { + return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kInternalError); + } + + // Asymmetric keys have no raw-byte representation. Exporting them requires a + // choice of encoding (PKCS#8 / SubjectPublicKeyInfo, DER or PEM) that this + // interface cannot express, so it is handled by the key-export operation + // rather than here. + if (m_pkey) + { + return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kKeyNotExportable); + } + + if (m_key_bytes.empty()) { return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kInternalError); } diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.hpp index a9a80282c..54a4cba96 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.hpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.hpp @@ -21,20 +21,54 @@ #include #include +// Forward declaration of OpenSSL's EVP_PKEY so that this header — which is part +// of a target without an OpenSSL dependency — stays free of . +// EVP_PKEY is `typedef struct evp_pkey_st EVP_PKEY`, so a pointer to the +// incomplete type is layout-compatible with the real one. +struct evp_pkey_st; + namespace score::crypto::daemon::provider::openssl { -/// Owns a single heap-allocated key material buffer. +/// Calls EVP_PKEY_free() on the managed key. /// -/// Crypto operation handlers (MAC, cipher) downcast the IKeyHandler to this -/// type and call GetRawKeyBytes() for direct access to the managed memory. +/// Defined out-of-line so that this header stays free of ; that is +/// also why EvpPkeyPtr manages an incomplete type, which is well-formed as long +/// as the deleter is not instantiated here. +struct EvpPkeyDeleter +{ + void operator()(evp_pkey_st* pkey) const noexcept; +}; + +/// Owning handle to an OpenSSL EVP_PKEY. +using EvpPkeyPtr = std::unique_ptr; + +/// Owns the key material of a single OpenSSL key. /// -/// Destruction calls Release() as a safety net; Release() is idempotent. +/// Two shapes are supported, distinguished by ProviderKeyHandle::is_asymmetric: +/// +/// - **Symmetric** (AES, HMAC): a heap-allocated byte buffer. Crypto operation +/// handlers (MAC, cipher) downcast the IKeyHandler to this type and call +/// GetRawKeyBytes() for direct access. +/// - **Asymmetric** (ECDSA): an owned EVP_PKEY holding the key pair. Sign and +/// verify handlers call GetPkey(); the same object serves both, because an +/// EVP_PKEY produced by key generation carries the private and public half. +/// +/// Destruction calls Release() as a safety net; Release() is idempotent and +/// zeroizes / frees whichever representation is in use. class OpenSslKeyHandler final : public key_management::IKeyHandler { public: + /// Constructs a symmetric key handler taking ownership of @p key_bytes. OpenSslKeyHandler(std::vector key_bytes, const key_management::ProviderKeyHandle& handle) noexcept; + /// Constructs an asymmetric key handler taking ownership of @p pkey. + /// + /// Taking an EvpPkeyPtr rather than a raw pointer makes the ownership + /// transfer explicit at the call site and keeps the caller's error paths + /// leak-free. + OpenSslKeyHandler(EvpPkeyPtr pkey, const key_management::ProviderKeyHandle& handle) noexcept; + ~OpenSslKeyHandler() override; OpenSslKeyHandler(const OpenSslKeyHandler&) = delete; @@ -53,11 +87,17 @@ class OpenSslKeyHandler final : public key_management::IKeyHandler [[nodiscard]] common::ProviderId GetProviderId() const noexcept override; - /// Direct access to managed key material without opaque_id round-trip. + /// Direct access to managed symmetric key material without opaque_id round-trip. + /// Returns nullptr for asymmetric keys or after Release(). [[nodiscard]] const std::uint8_t* GetRawKeyBytes(std::size_t& out_size) const noexcept; + /// Direct access to the managed EVP_PKEY for asymmetric keys. + /// Returns nullptr for symmetric keys or after Release(). Ownership stays here. + [[nodiscard]] evp_pkey_st* GetPkey() const noexcept; + private: std::vector m_key_bytes; + EvpPkeyPtr m_pkey; key_management::ProviderKeyHandle m_handle; bool m_released; }; diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/operations/cipher/openssl_cipher_handler.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/operations/cipher/openssl_cipher_handler.cpp new file mode 100644 index 000000000..d342cc286 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/openssl/operations/cipher/openssl_cipher_handler.cpp @@ -0,0 +1,317 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/daemon/provider/score_provider/openssl/operations/cipher/openssl_cipher_handler.hpp" + +#include "score/crypto/src/daemon/common/algorithm_info.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/provider/handler/src/handler_utils.hpp" +#include "score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.hpp" + +#include "score/mw/log/logging.h" + +#include +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::provider::score_provider::openssl::handler +{ + +using common::ResponseParameters; +using common::StreamOperationState; +using ::score::crypto::daemon::common::DaemonErrorCode; +namespace algo_info = ::score::crypto::daemon::common; +using ::score::crypto::daemon::provider::handler::handler_utils::CheckAndGetSpan; + +// --------------------------------------------------------------------------- +// Construction / destruction +// --------------------------------------------------------------------------- + +OpenSslCipherHandler::OpenSslCipherHandler(std::unique_ptr executor, + const common::AlgorithmId& algorithm) + : ScoreCipherHandler{std::move(executor), algorithm} +{ +} + +OpenSslCipherHandler::~OpenSslCipherHandler() +{ + CleanupContext(); +} + +void OpenSslCipherHandler::CleanupContext() noexcept +{ + if (m_ctx != nullptr) + { + EVP_CIPHER_CTX_free(m_ctx); + m_ctx = nullptr; + } + if (m_cipher != nullptr) + { + EVP_CIPHER_free(m_cipher); + m_cipher = nullptr; + } +} + +// --------------------------------------------------------------------------- +// Static helpers +// --------------------------------------------------------------------------- + +bool OpenSslCipherHandler::IsAlgorithmSupported(const common::AlgorithmId& algorithm) noexcept +{ + // The provider-independent table is the single source of truth for which AES + // modes this stack exposes; OpenSSL happens to accept the same names. + return algo_info::LookupCipher(algorithm).has_value(); +} + +bool OpenSslCipherHandler::GetBoundKeyMaterial(const std::uint8_t*& key_bytes, std::size_t& key_len) const noexcept +{ + if (m_init_params.bound_key_handler == nullptr) + { + return false; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) - provider id verified in InitializeContext + const auto* openssl_key = static_cast( + m_init_params.bound_key_handler); + key_bytes = openssl_key->GetRawKeyBytes(key_len); + return (key_bytes != nullptr) && (key_len > 0U); +} + +// --------------------------------------------------------------------------- +// Handler interface +// --------------------------------------------------------------------------- + +::score::crypto::Expected OpenSslCipherHandler::InitializeContext( + const ::score::crypto::daemon::provider::handler::InitializationParams& init_params) +{ + const auto info = algo_info::LookupCipher(m_algorithm); + if (!info.has_value()) + { + score::mw::log::LogError() << LOG_PREFIX << "Unsupported algorithm:" << m_algorithm; + return ::score::crypto::make_unexpected(DaemonErrorCode::kUnsupportedAlgorithm); + } + + // Picks up the encrypt/decrypt direction from CTX_CREATE param[4]. + auto base_result = ScoreCipherHandler::InitializeContext(init_params); + if (!base_result.has_value()) + { + return base_result; + } + + CleanupContext(); + + // OpenSSL's algorithm names match the identifiers used by this stack + // ("AES-256-CBC", "AES-128-CTR", ...), so no translation table is needed. + const std::string algorithm_name{m_algorithm.data(), m_algorithm.size()}; + m_cipher = EVP_CIPHER_fetch(nullptr, algorithm_name.c_str(), nullptr); + if (m_cipher == nullptr) + { + score::mw::log::LogError() << LOG_PREFIX << "EVP_CIPHER_fetch failed for" << m_algorithm; + return ::score::crypto::make_unexpected(DaemonErrorCode::kUnsupportedAlgorithm); + } + + m_ctx = EVP_CIPHER_CTX_new(); + if (m_ctx == nullptr) + { + score::mw::log::LogError() << LOG_PREFIX << "EVP_CIPHER_CTX_new failed"; + CleanupContext(); + return ::score::crypto::make_unexpected(DaemonErrorCode::kAllocationFailed); + } + + if (init_params.bound_key_handler == nullptr) + { + score::mw::log::LogError() << LOG_PREFIX << "InitializeContext: cipher context requires a bound key"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + } + + // Provider-id check validates the key comes from the same provider (no dynamic_cast/RTTI). + if (init_params.bound_key_handler->GetProviderId() != init_params.provider_id) + { + score::mw::log::LogError() << LOG_PREFIX << "InitializeContext: bound key is not an OpenSSL key handler" + << " (key provider_id=" << init_params.bound_key_handler->GetProviderId() + << ", expected=" << init_params.provider_id << ")"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + } + + m_init_params = init_params; + + const std::uint8_t* key_bytes{nullptr}; + std::size_t key_len{0U}; + if (!GetBoundKeyMaterial(key_bytes, key_len)) + { + score::mw::log::LogError() << LOG_PREFIX << "InitializeContext: bound key has no raw key material"; + m_init_params = {}; + return ::score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + } + + // Catch a key/algorithm length mismatch here rather than letting OpenSSL + // silently accept a short key later. + if (key_len != info->key_size) + { + score::mw::log::LogError() << LOG_PREFIX << "InitializeContext: key length" << key_len + << "does not match algorithm requirement" << info->key_size; + m_init_params = {}; + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmMismatch); + } + + m_state = StreamOperationState::IDLE; + return std::monostate{}; +} + +::score::crypto::Expected OpenSslCipherHandler::Reset() +{ + return InitializeContext(m_init_params); +} + +// --------------------------------------------------------------------------- +// ScoreCipherHandler interface +// --------------------------------------------------------------------------- + +::score::crypto::Expected OpenSslCipherHandler::InitCipher( + std::optional iv) +{ + if ((m_ctx == nullptr) || (m_cipher == nullptr)) + { + score::mw::log::LogError() << LOG_PREFIX << "InitCipher: cipher context not allocated"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kStreamNotInitialized); + } + + const std::uint8_t* key_bytes{nullptr}; + std::size_t key_len{0U}; + if (!GetBoundKeyMaterial(key_bytes, key_len)) + { + score::mw::log::LogError() << LOG_PREFIX << "InitCipher: no valid key material"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kStreamNotInitialized); + } + + const std::size_t required_iv_size = GetIvSize(); + const std::uint8_t* iv_bytes{nullptr}; + std::size_t iv_len{0U}; + + if (iv.has_value()) + { + const auto ivSpan = CheckAndGetSpan(iv.value()); + if (!ivSpan.has_value()) + { + return ::score::crypto::make_unexpected(ivSpan.error()); + } + iv_bytes = ivSpan.value().data(); + iv_len = ivSpan.value().size(); + } + + if (iv_len != required_iv_size) + { + score::mw::log::LogError() << LOG_PREFIX << "InitCipher: IV length" << iv_len << "but algorithm requires" + << required_iv_size; + return ::score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + } + + const int rv = (GetDirection() == score::crypto::CipherDirection::kEncrypt) + ? EVP_EncryptInit_ex2(m_ctx, m_cipher, key_bytes, iv_bytes, nullptr) + : EVP_DecryptInit_ex2(m_ctx, m_cipher, key_bytes, iv_bytes, nullptr); + if (rv != 1) + { + score::mw::log::LogError() << LOG_PREFIX << "InitCipher: EVP cipher init failed"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmInitializationFailed); + } + + return std::monostate{}; +} + +::score::crypto::Expected OpenSslCipherHandler::UpdateCipher( + const common::RequestParameter& input, + score::cpp::span output) +{ + if (m_ctx == nullptr) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kStreamNotInitialized); + } + + const auto inputSpan = CheckAndGetSpan(input); + if (!inputSpan.has_value()) + { + return ::score::crypto::make_unexpected(inputSpan.error()); + } + + // EVP may emit up to one extra block beyond the input length when it flushes + // a previously buffered partial block, so the caller has to leave room for it. + const std::size_t block_size = GetBlockSize(); + if (output.size() < inputSpan.value().size() + block_size) + { + score::mw::log::LogError() << LOG_PREFIX << "UpdateCipher: output buffer holds" << output.size() + << "bytes, needs" << (inputSpan.value().size() + block_size); + return ::score::crypto::make_unexpected(DaemonErrorCode::kInsufficientBufferSize); + } + + int out_len = 0; + const int rv = + (GetDirection() == score::crypto::CipherDirection::kEncrypt) + ? EVP_EncryptUpdate( + m_ctx, output.data(), &out_len, inputSpan.value().data(), static_cast(inputSpan.value().size())) + : EVP_DecryptUpdate( + m_ctx, output.data(), &out_len, inputSpan.value().data(), static_cast(inputSpan.value().size())); + if (rv != 1) + { + score::mw::log::LogError() << LOG_PREFIX << "UpdateCipher: EVP cipher update failed"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); + } + + return static_cast(out_len); +} + +::score::crypto::Expected OpenSslCipherHandler::FinalizeCipher( + score::cpp::span output) +{ + if (m_ctx == nullptr) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kStreamNotInitialized); + } + + // At most one padded block is emitted here and stream modes emit nothing, so + // EVP writes into a scratch block first: the caller's remaining space may be + // shorter than a block and still be exactly right. + std::array scratch{}; + + int out_len = 0; + const int rv = (GetDirection() == score::crypto::CipherDirection::kEncrypt) + ? EVP_EncryptFinal_ex(m_ctx, scratch.data(), &out_len) + : EVP_DecryptFinal_ex(m_ctx, scratch.data(), &out_len); + if (rv != 1) + { + // For decryption this is the normal signal that the padding is wrong — + // i.e. the ciphertext or the key does not match. + score::mw::log::LogError() << LOG_PREFIX << "FinalizeCipher: EVP cipher finalize failed"; + m_state = StreamOperationState::IDLE; + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); + } + + const auto produced = static_cast(out_len); + if (produced > output.size()) + { + score::mw::log::LogError() << LOG_PREFIX << "FinalizeCipher: output buffer holds" << output.size() + << "bytes, needs" << produced; + m_state = StreamOperationState::IDLE; + return ::score::crypto::make_unexpected(DaemonErrorCode::kInsufficientBufferSize); + } + if (produced > 0U) + { + std::memcpy(output.data(), scratch.data(), produced); + } + + m_state = StreamOperationState::IDLE; + return produced; +} + +} // namespace score::crypto::daemon::provider::score_provider::openssl::handler diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/operations/cipher/openssl_cipher_handler.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/operations/cipher/openssl_cipher_handler.hpp new file mode 100644 index 000000000..9d3530f0a --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/openssl/operations/cipher/openssl_cipher_handler.hpp @@ -0,0 +1,104 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_OPERATIONS_CIPHER_OPENSSL_CIPHER_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_OPERATIONS_CIPHER_OPENSSL_CIPHER_HANDLER_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/types.hpp" +#include "score/crypto/src/daemon/provider/handler/handler_init_params.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/cipher/cipher_executor.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/cipher/score_cipher_handler.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::provider::score_provider::openssl::handler +{ + +/// @brief OpenSSL symmetric cipher handler built on the EVP_CIPHER interface. +/// +/// Supports AES-128/192/256 in CBC — whatever kCipherAlgorithms lists. CBC uses +/// OpenSSL's default PKCS#7 padding, so ciphertext is one block longer than +/// plaintext and Finalize() emits that trailing block. +/// +/// The bound key must come from the same provider and carry raw symmetric key +/// material of the length the algorithm requires — a key generated as +/// "AES-256-CBC" or plain "AES-256" both work, a 16-byte key under an AES-256 +/// algorithm is rejected at Init time. +class OpenSslCipherHandler final + : public ::score::crypto::daemon::provider::score_provider::operations::cipher::ScoreCipherHandler +{ + public: + using Sptr = std::shared_ptr; + + explicit OpenSslCipherHandler(std::unique_ptr executor, + const common::AlgorithmId& algorithm); + ~OpenSslCipherHandler() override; + + OpenSslCipherHandler(const OpenSslCipherHandler&) = delete; + OpenSslCipherHandler& operator=(const OpenSslCipherHandler&) = delete; + OpenSslCipherHandler(OpenSslCipherHandler&&) = delete; + OpenSslCipherHandler& operator=(OpenSslCipherHandler&&) = delete; + + // ----------------------------------------------------------------------- + // Handler interface + // ----------------------------------------------------------------------- + + [[nodiscard]] ::score::crypto::Expected + InitializeContext(const ::score::crypto::daemon::provider::handler::InitializationParams& init_params) override; + + [[nodiscard]] ::score::crypto::Expected Reset() + override; + + // ----------------------------------------------------------------------- + // ScoreCipherHandler interface + // ----------------------------------------------------------------------- + + [[nodiscard]] ::score::crypto::Expected + InitCipher(std::optional iv) override; + + [[nodiscard]] ::score::crypto::Expected UpdateCipher( + const common::RequestParameter& input, + score::cpp::span output) override; + + [[nodiscard]] ::score::crypto::Expected + FinalizeCipher(score::cpp::span output) override; + + /// @brief Check if the given algorithm is supported by this handler. + [[nodiscard]] static bool IsAlgorithmSupported(const common::AlgorithmId& algorithm) noexcept; + + private: + /// @brief Retrieve the bound key's raw bytes. + /// @return true if key material of the expected length is available. + [[nodiscard]] bool GetBoundKeyMaterial(const std::uint8_t*& key_bytes, std::size_t& key_len) const noexcept; + + /// @brief Free the EVP cipher context and fetched algorithm. + void CleanupContext() noexcept; + + EVP_CIPHER* m_cipher{nullptr}; + EVP_CIPHER_CTX* m_ctx{nullptr}; + ::score::crypto::daemon::provider::handler::InitializationParams m_init_params; + + static constexpr std::string_view LOG_PREFIX = "[OPENSSL_CIPHER_HANDLER]"; +}; + +} // namespace score::crypto::daemon::provider::score_provider::openssl::handler + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_OPERATIONS_CIPHER_OPENSSL_CIPHER_HANDLER_HPP diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/operations/factory/openssl_handler_factory.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/operations/factory/openssl_handler_factory.cpp index 3c309c211..73617afab 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/operations/factory/openssl_handler_factory.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/operations/factory/openssl_handler_factory.cpp @@ -14,11 +14,17 @@ #include "score/crypto/src/daemon/provider/score_provider/openssl/operations/factory/openssl_handler_factory.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/provider/executors/key_mgmt_executor.hpp" +#include "score/crypto/src/daemon/provider/score_provider/openssl/operations/cipher/openssl_cipher_handler.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/operations/hash/openssl_hash_handler.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/operations/key_management/openssl_key_management_handler.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/operations/mac/openssl_hmac_handler.hpp" +#include "score/crypto/src/daemon/provider/score_provider/openssl/operations/random/openssl_random_handler.hpp" +#include "score/crypto/src/daemon/provider/score_provider/openssl/operations/signature/openssl_ecdsa_handler.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/cipher/cipher_executor.hpp" #include "score/crypto/src/daemon/provider/score_provider/operations/hash/hash_executor.hpp" #include "score/crypto/src/daemon/provider/score_provider/operations/mac/mac_executor.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/random/random_executor.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/signature/signature_executor.hpp" #include "score/result/result.h" namespace score::crypto::daemon::provider::score_provider::openssl::handler @@ -68,4 +74,46 @@ score::Result OpenSslHandlerFactory::CreateKeyManagementHandler() return std::make_shared(std::move(executor)); } +score::Result OpenSslHandlerFactory::CreateCipherHandler(const common::AlgorithmId& algorithm) +{ + if (!OpenSslCipherHandler::IsAlgorithmSupported(algorithm)) + { + score::result::Error error( + static_cast(score::crypto::CryptoErrorCode::kUnsupportedAlgorithm), + score::crypto::kCryptoErrorDomain, + "Algorithm not supported for handler: " + algorithm); + return score::Result(score::unexpect, error); + } + auto cipher_executor = std::make_unique(); + return std::make_shared(std::move(cipher_executor), algorithm); +} + +score::Result OpenSslHandlerFactory::CreateSignatureHandler(const common::AlgorithmId& algorithm) +{ + if (!OpenSslEcdsaHandler::IsAlgorithmSupported(algorithm)) + { + score::result::Error error( + static_cast(score::crypto::CryptoErrorCode::kUnsupportedAlgorithm), + score::crypto::kCryptoErrorDomain, + "Algorithm not supported for handler: " + algorithm); + return score::Result(score::unexpect, error); + } + auto signature_executor = std::make_unique(); + return std::make_shared(std::move(signature_executor), algorithm); +} + +score::Result OpenSslHandlerFactory::CreateRandomHandler(const common::AlgorithmId& algorithm) +{ + if (!OpenSslRandomHandler::IsAlgorithmSupported(algorithm)) + { + score::result::Error error( + static_cast(score::crypto::CryptoErrorCode::kUnsupportedAlgorithm), + score::crypto::kCryptoErrorDomain, + "Algorithm not supported for handler: " + algorithm); + return score::Result(score::unexpect, error); + } + auto random_executor = std::make_unique(); + return std::make_shared(std::move(random_executor), algorithm); +} + } // namespace score::crypto::daemon::provider::score_provider::openssl::handler diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/operations/factory/openssl_handler_factory.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/operations/factory/openssl_handler_factory.hpp index 2675e0c00..00c844593 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/operations/factory/openssl_handler_factory.hpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/operations/factory/openssl_handler_factory.hpp @@ -43,6 +43,12 @@ class OpenSslHandlerFactory final const common::AlgorithmId& algorithm) override; [[nodiscard]] ::score::Result<::score::crypto::daemon::provider::handler::Handler::Sptr> CreateKeyManagementHandler() override; + [[nodiscard]] ::score::Result<::score::crypto::daemon::provider::handler::Handler::Sptr> CreateCipherHandler( + const common::AlgorithmId& algorithm) override; + [[nodiscard]] ::score::Result<::score::crypto::daemon::provider::handler::Handler::Sptr> CreateSignatureHandler( + const common::AlgorithmId& algorithm) override; + [[nodiscard]] ::score::Result<::score::crypto::daemon::provider::handler::Handler::Sptr> CreateRandomHandler( + const common::AlgorithmId& algorithm) override; }; } // namespace score::crypto::daemon::provider::score_provider::openssl::handler diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/operations/random/openssl_random_handler.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/operations/random/openssl_random_handler.cpp new file mode 100644 index 000000000..f3e76ca08 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/openssl/operations/random/openssl_random_handler.cpp @@ -0,0 +1,85 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/daemon/provider/score_provider/openssl/operations/random/openssl_random_handler.hpp" + +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/provider/handler/src/handler_utils.hpp" + +#include // OPENSSL_cleanse +#include + +#include "score/mw/log/logging.h" + +#include +#include + +namespace score::crypto::daemon::provider::score_provider::openssl::handler +{ + +using ::score::crypto::daemon::common::DaemonErrorCode; +using ::score::crypto::daemon::provider::handler::handler_utils::CheckAndGetSpan; + +namespace +{ +/// OpenSSL's public RNG is a CTR-DRBG; the empty identifier means +/// "whatever the provider considers its default". +constexpr std::string_view kDefaultAlgorithm{}; +constexpr std::string_view kCtrDrbgAlgorithm{"CTR-DRBG"}; +} // namespace + +OpenSslRandomHandler::OpenSslRandomHandler(std::unique_ptr executor, + const common::AlgorithmId& algorithm) + : ScoreRandomHandler{std::move(executor), algorithm} +{ +} + +bool OpenSslRandomHandler::IsAlgorithmSupported(const common::AlgorithmId& algorithm) noexcept +{ + const std::string_view name{algorithm.data(), algorithm.size()}; + return (name == kDefaultAlgorithm) || (name == kCtrDrbgAlgorithm); +} + +::score::crypto::Expected OpenSslRandomHandler::GenerateRandom( + score::cpp::span output) +{ + if (RAND_bytes(output.data(), static_cast(output.size())) != 1) + { + // Never leave a partially-filled buffer behind: a caller that ignored the + // error would otherwise use predictable bytes as key or IV material. + OPENSSL_cleanse(output.data(), output.size()); + score::mw::log::LogError() << LOG_PREFIX << "GenerateRandom: RAND_bytes failed for" << output.size() << "bytes"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kOperationFailed); + } + + return output.size(); +} + +::score::crypto::Expected OpenSslRandomHandler::SeedRandom( + const common::RequestParameter& seed) +{ + const auto seedSpan = CheckAndGetSpan(seed); + if (!seedSpan.has_value()) + { + return ::score::crypto::make_unexpected(seedSpan.error()); + } + + // Entropy estimate 0.0: application-supplied material is stirred into the + // pool but is not credited as entropy, so a caller passing predictable bytes + // cannot degrade the generator. + RAND_add(seedSpan.value().data(), static_cast(seedSpan.value().size()), 0.0); + + return std::monostate{}; +} + +} // namespace score::crypto::daemon::provider::score_provider::openssl::handler diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/operations/random/openssl_random_handler.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/operations/random/openssl_random_handler.hpp new file mode 100644 index 000000000..fc51123d5 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/openssl/operations/random/openssl_random_handler.hpp @@ -0,0 +1,77 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_OPERATIONS_RANDOM_OPENSSL_RANDOM_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_OPERATIONS_RANDOM_OPENSSL_RANDOM_HANDLER_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/types.hpp" +#include "score/crypto/src/daemon/provider/handler/handler_init_params.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/random/random_executor.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/random/score_random_handler.hpp" + +#include +#include +#include +#include + +namespace score::crypto::daemon::provider::score_provider::openssl::handler +{ + +/// @brief OpenSSL random number generator handler. +/// +/// Draws from OpenSSL's public RNG (RAND_bytes), which is a NIST SP 800-90A +/// CTR-DRBG seeded from the operating system entropy source and reseeded +/// automatically. Seed() feeds additional entropy via RAND_add without +/// increasing the assessed entropy estimate, so caller-supplied material can +/// never weaken the pool. +/// +/// Accepted algorithm identifiers are the empty string (provider default) and +/// "CTR-DRBG"; anything else is rejected so that a caller asking for a specific +/// generator never silently gets a different one. +class OpenSslRandomHandler final + : public ::score::crypto::daemon::provider::score_provider::operations::random::ScoreRandomHandler +{ + public: + using Sptr = std::shared_ptr; + + explicit OpenSslRandomHandler(std::unique_ptr executor, + const common::AlgorithmId& algorithm); + ~OpenSslRandomHandler() override = default; + + OpenSslRandomHandler(const OpenSslRandomHandler&) = delete; + OpenSslRandomHandler& operator=(const OpenSslRandomHandler&) = delete; + OpenSslRandomHandler(OpenSslRandomHandler&&) = delete; + OpenSslRandomHandler& operator=(OpenSslRandomHandler&&) = delete; + + // ----------------------------------------------------------------------- + // ScoreRandomHandler interface + // ----------------------------------------------------------------------- + + [[nodiscard]] ::score::crypto::Expected + GenerateRandom(score::cpp::span output) override; + + [[nodiscard]] ::score::crypto::Expected + SeedRandom(const common::RequestParameter& seed) override; + + /// @brief Check if the given RNG algorithm identifier is supported. + [[nodiscard]] static bool IsAlgorithmSupported(const common::AlgorithmId& algorithm) noexcept; + + private: + static constexpr std::string_view LOG_PREFIX = "[OPENSSL_RANDOM_HANDLER]"; +}; + +} // namespace score::crypto::daemon::provider::score_provider::openssl::handler + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_OPERATIONS_RANDOM_OPENSSL_RANDOM_HANDLER_HPP diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/operations/signature/openssl_ecdsa_handler.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/operations/signature/openssl_ecdsa_handler.cpp new file mode 100644 index 000000000..d3a7176f4 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/openssl/operations/signature/openssl_ecdsa_handler.cpp @@ -0,0 +1,417 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/daemon/provider/score_provider/openssl/operations/signature/openssl_ecdsa_handler.hpp" + +#include "score/crypto/src/daemon/common/algorithm_info.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/provider/handler/src/handler_utils.hpp" +#include "score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.hpp" + +#include +#include +#include + +#include "score/mw/log/logging.h" + +#include +#include +#include + +namespace score::crypto::daemon::provider::score_provider::openssl::handler +{ + +using common::ResponseParameters; +using common::StreamOperationState; +using ::score::crypto::daemon::common::DaemonErrorCode; +using ::score::crypto::daemon::provider::handler::handler_utils::CheckAndGetSpan; +namespace algo_info = ::score::crypto::daemon::common; + +// --------------------------------------------------------------------------- +// Construction / destruction +// --------------------------------------------------------------------------- + +OpenSslEcdsaHandler::OpenSslEcdsaHandler(std::unique_ptr executor, + const common::AlgorithmId& algorithm) + : ScoreSignatureHandler{std::move(executor), algorithm} +{ +} + +OpenSslEcdsaHandler::~OpenSslEcdsaHandler() +{ + CleanupContext(); +} + +void OpenSslEcdsaHandler::CleanupContext() noexcept +{ + if (m_md_ctx != nullptr) + { + EVP_MD_CTX_free(m_md_ctx); + m_md_ctx = nullptr; + } +} + +// --------------------------------------------------------------------------- +// Static helpers +// --------------------------------------------------------------------------- + +bool OpenSslEcdsaHandler::IsAlgorithmSupported(const common::AlgorithmId& algorithm) noexcept +{ + const std::string_view algo{algorithm.data(), algorithm.size()}; + // A *signature* algorithm must name the digest as well as the curve: there + // is no implied default pairing. Requiring it here means the bare key form + // ("ECDSA-P256") is rejected at CTX_CREATE rather than surviving until the + // first Init(), where the failure would be much harder to attribute. + return algo_info::IsEcdsaAlgorithm(algo) && algo_info::LookupSignatureDigest(algo).has_value(); +} + +EVP_PKEY* OpenSslEcdsaHandler::GetBoundPkey() const noexcept +{ + if (m_init_params.bound_key_handler == nullptr) + { + return nullptr; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) - provider id verified in InitializeContext + const auto* openssl_key = static_cast( + m_init_params.bound_key_handler); + return openssl_key->GetPkey(); +} + +// --------------------------------------------------------------------------- +// Signature encoding conversion +// +// OpenSSL speaks DER ECDSA-Sig-Value (SEQUENCE { INTEGER r, INTEGER s }); the +// stack's wire format is the fixed-length IEEE P1363 concatenation r‖s. The +// two helpers below are the only place that difference exists. +// --------------------------------------------------------------------------- + +::score::crypto::Expected OpenSslEcdsaHandler::DerToP1363(const std::uint8_t* der, + std::size_t der_len, + std::size_t field_size) +{ + const std::uint8_t* der_cursor = der; + ECDSA_SIG* sig = d2i_ECDSA_SIG(nullptr, &der_cursor, static_cast(der_len)); + if (sig == nullptr) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kInvalidFormat); + } + + const BIGNUM* r = nullptr; + const BIGNUM* s = nullptr; + ECDSA_SIG_get0(sig, &r, &s); + + common::OwnedBuffer out(field_size * 2U); + // BN_bn2binpad left-pads with zeros to exactly field_size bytes, which is + // what makes the P1363 form fixed-length. + const int r_written = BN_bn2binpad(r, out.data(), static_cast(field_size)); + const int s_written = BN_bn2binpad(s, out.data() + field_size, static_cast(field_size)); + ECDSA_SIG_free(sig); + + if ((r_written < 0) || (s_written < 0)) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); + } + + return out; +} + +::score::crypto::Expected OpenSslEcdsaHandler::P1363ToDer(const std::uint8_t* raw, + std::size_t raw_len, + std::size_t field_size) +{ + if (raw_len != (field_size * 2U)) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kInvalidFormat); + } + + BIGNUM* r = BN_bin2bn(raw, static_cast(field_size), nullptr); + BIGNUM* s = BN_bin2bn(raw + field_size, static_cast(field_size), nullptr); + ECDSA_SIG* sig = ECDSA_SIG_new(); + + if ((r == nullptr) || (s == nullptr) || (sig == nullptr)) + { + BN_free(r); + BN_free(s); + if (sig != nullptr) + { + ECDSA_SIG_free(sig); + } + return ::score::crypto::make_unexpected(DaemonErrorCode::kAllocationFailed); + } + + // ECDSA_SIG_set0 takes ownership of r and s on success. + if (ECDSA_SIG_set0(sig, r, s) != 1) + { + BN_free(r); + BN_free(s); + ECDSA_SIG_free(sig); + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); + } + + std::uint8_t* der = nullptr; + const int der_len = i2d_ECDSA_SIG(sig, &der); + ECDSA_SIG_free(sig); + + if ((der_len <= 0) || (der == nullptr)) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); + } + + common::OwnedBuffer out(der, der + der_len); + OPENSSL_free(der); + return out; +} + +// --------------------------------------------------------------------------- +// Handler interface +// --------------------------------------------------------------------------- + +::score::crypto::Expected OpenSslEcdsaHandler::InitializeContext( + const ::score::crypto::daemon::provider::handler::InitializationParams& init_params) +{ + if (!IsAlgorithmSupported(m_algorithm)) + { + score::mw::log::LogError() << LOG_PREFIX << "Unsupported algorithm:" << m_algorithm; + return ::score::crypto::make_unexpected(DaemonErrorCode::kUnsupportedAlgorithm); + } + + // Picks up the sign/verify OperationMode from CTX_CREATE param[4]. + auto base_result = ScoreSignatureHandler::InitializeContext(init_params); + if (!base_result.has_value()) + { + return base_result; + } + + CleanupContext(); + + if (init_params.bound_key_handler == nullptr) + { + score::mw::log::LogError() << LOG_PREFIX << "InitializeContext: signature context requires a bound key"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + } + + // Provider-id check validates the key comes from the same provider (no dynamic_cast/RTTI). + if (init_params.bound_key_handler->GetProviderId() != init_params.provider_id) + { + score::mw::log::LogError() << LOG_PREFIX << "InitializeContext: bound key is not an OpenSSL key handler" + << " (key provider_id=" << init_params.bound_key_handler->GetProviderId() + << ", expected=" << init_params.provider_id << ")"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + } + + m_init_params = init_params; + + EVP_PKEY* pkey = GetBoundPkey(); + if (pkey == nullptr) + { + score::mw::log::LogError() << LOG_PREFIX << "InitializeContext: bound key holds no EC key pair"; + m_init_params = {}; + return ::score::crypto::make_unexpected(DaemonErrorCode::kIncompatibleKeyType); + } + + if (EVP_PKEY_get_base_id(pkey) != EVP_PKEY_EC) + { + score::mw::log::LogError() << LOG_PREFIX << "InitializeContext: bound key is not an EC key"; + m_init_params = {}; + return ::score::crypto::make_unexpected(DaemonErrorCode::kIncompatibleKeyType); + } + + m_md_ctx = EVP_MD_CTX_new(); + if (m_md_ctx == nullptr) + { + score::mw::log::LogError() << LOG_PREFIX << "EVP_MD_CTX_new failed"; + m_init_params = {}; + return ::score::crypto::make_unexpected(DaemonErrorCode::kAllocationFailed); + } + + m_state = StreamOperationState::IDLE; + return std::monostate{}; +} + +::score::crypto::Expected OpenSslEcdsaHandler::Reset() +{ + return InitializeContext(m_init_params); +} + +// --------------------------------------------------------------------------- +// ScoreSignatureHandler interface +// --------------------------------------------------------------------------- + +::score::crypto::Expected OpenSslEcdsaHandler::InitSignature() +{ + if (m_md_ctx == nullptr) + { + score::mw::log::LogError() << LOG_PREFIX << "InitSignature: digest context not allocated"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kStreamNotInitialized); + } + + EVP_PKEY* pkey = GetBoundPkey(); + if (pkey == nullptr) + { + score::mw::log::LogError() << LOG_PREFIX << "InitSignature: no bound key"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kStreamNotInitialized); + } + + const auto digest_name = algo_info::LookupSignatureDigest(std::string_view{m_algorithm.data(), m_algorithm.size()}); + if (!digest_name.has_value()) + { + score::mw::log::LogError() << LOG_PREFIX << "InitSignature: no digest for algorithm" << m_algorithm; + return ::score::crypto::make_unexpected(DaemonErrorCode::kUnsupportedAlgorithm); + } + + // A fresh EVP_MD_CTX per stream: EVP_DigestSignInit cannot restart a context + // that already carries accumulated data. + EVP_MD_CTX_free(m_md_ctx); + m_md_ctx = EVP_MD_CTX_new(); + if (m_md_ctx == nullptr) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kAllocationFailed); + } + + const std::string digest{digest_name.value()}; + const int rv = (GetOperationMode() == score::crypto::OperationMode::kVerify) + ? EVP_DigestVerifyInit_ex(m_md_ctx, nullptr, digest.c_str(), nullptr, nullptr, pkey, nullptr) + : EVP_DigestSignInit_ex(m_md_ctx, nullptr, digest.c_str(), nullptr, nullptr, pkey, nullptr); + if (rv != 1) + { + score::mw::log::LogError() << LOG_PREFIX << "InitSignature: EVP_Digest{Sign,Verify}Init failed"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmInitializationFailed); + } + + return std::monostate{}; +} + +::score::crypto::Expected OpenSslEcdsaHandler::UpdateSignature( + const common::RequestParameter& data) +{ + if (m_md_ctx == nullptr) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kStreamNotInitialized); + } + + const auto dataSpan = CheckAndGetSpan(data); + if (!dataSpan.has_value()) + { + return ::score::crypto::make_unexpected(dataSpan.error()); + } + + const int rv = (GetOperationMode() == score::crypto::OperationMode::kVerify) + ? EVP_DigestVerifyUpdate(m_md_ctx, dataSpan.value().data(), dataSpan.value().size()) + : EVP_DigestSignUpdate(m_md_ctx, dataSpan.value().data(), dataSpan.value().size()); + if (rv != 1) + { + score::mw::log::LogError() << LOG_PREFIX << "UpdateSignature: EVP_Digest{Sign,Verify}Update failed"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); + } + + return std::monostate{}; +} + +::score::crypto::Expected OpenSslEcdsaHandler::FinalizeSign( + score::cpp::span signature) +{ + if (m_md_ctx == nullptr) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kStreamNotInitialized); + } + + const auto curve = algo_info::LookupEcCurveOfAlgorithm(std::string_view{m_algorithm.data(), m_algorithm.size()}); + if (!curve.has_value()) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kUnsupportedAlgorithm); + } + + // P1363 is r‖s, two field-sized integers, so the length is known before signing. + const std::size_t p1363_len = 2U * curve->field_size; + if (signature.size() < p1363_len) + { + score::mw::log::LogError() << LOG_PREFIX << "FinalizeSign: output buffer holds" << signature.size() + << "bytes, needs" << p1363_len; + return ::score::crypto::make_unexpected(DaemonErrorCode::kInsufficientBufferSize); + } + + // First call sizes the DER buffer, second fills it. + std::size_t der_len = 0U; + if (EVP_DigestSignFinal(m_md_ctx, nullptr, &der_len) != 1) + { + score::mw::log::LogError() << LOG_PREFIX << "FinalizeSign: EVP_DigestSignFinal size query failed"; + m_state = StreamOperationState::IDLE; + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); + } + + std::vector der(der_len); + if (EVP_DigestSignFinal(m_md_ctx, der.data(), &der_len) != 1) + { + score::mw::log::LogError() << LOG_PREFIX << "FinalizeSign: EVP_DigestSignFinal failed"; + m_state = StreamOperationState::IDLE; + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); + } + der.resize(der_len); + + auto p1363 = DerToP1363(der.data(), der.size(), curve->field_size); + m_state = StreamOperationState::IDLE; + if (!p1363.has_value()) + { + score::mw::log::LogError() << LOG_PREFIX << "FinalizeSign: DER to P1363 conversion failed"; + return ::score::crypto::make_unexpected(p1363.error()); + } + + std::memcpy(signature.data(), p1363.value().data(), p1363.value().size()); + return p1363.value().size(); +} + +::score::crypto::Expected OpenSslEcdsaHandler::FinalizeVerify( + const common::RequestParameter& signature) +{ + if (m_md_ctx == nullptr) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kStreamNotInitialized); + } + + const auto curve = algo_info::LookupEcCurveOfAlgorithm(std::string_view{m_algorithm.data(), m_algorithm.size()}); + if (!curve.has_value()) + { + return ::score::crypto::make_unexpected(DaemonErrorCode::kUnsupportedAlgorithm); + } + + const auto sigSpan = CheckAndGetSpan(signature); + if (!sigSpan.has_value()) + { + return ::score::crypto::make_unexpected(sigSpan.error()); + } + + // A signature of the wrong length is a malformed input rather than a + // mismatching one, so it is reported as an error, not as "not verified". + auto der = P1363ToDer(sigSpan.value().data(), sigSpan.value().size(), curve->field_size); + if (!der.has_value()) + { + m_state = StreamOperationState::IDLE; + score::mw::log::LogError() << LOG_PREFIX << "FinalizeVerify: malformed P1363 signature of length" + << sigSpan.value().size(); + return ::score::crypto::make_unexpected(der.error()); + } + + const int rv = EVP_DigestVerifyFinal(m_md_ctx, der.value().data(), der.value().size()); + m_state = StreamOperationState::IDLE; + + // rv == 1 verified, rv == 0 signature mismatch (a normal result), rv < 0 error. + if (rv < 0) + { + score::mw::log::LogError() << LOG_PREFIX << "FinalizeVerify: EVP_DigestVerifyFinal failed"; + return ::score::crypto::make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); + } + + return rv == 1; +} + +} // namespace score::crypto::daemon::provider::score_provider::openssl::handler diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/operations/signature/openssl_ecdsa_handler.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/operations/signature/openssl_ecdsa_handler.hpp new file mode 100644 index 000000000..e203dddc0 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/openssl/operations/signature/openssl_ecdsa_handler.hpp @@ -0,0 +1,124 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_OPERATIONS_SIGNATURE_OPENSSL_ECDSA_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_OPERATIONS_SIGNATURE_OPENSSL_ECDSA_HANDLER_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/types.hpp" +#include "score/crypto/src/daemon/provider/handler/handler_init_params.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/signature/score_signature_handler.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/signature/signature_executor.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::provider::score_provider::openssl::handler +{ + +/// @brief OpenSSL ECDSA signature handler built on the EVP_DigestSign / +/// EVP_DigestVerify interface. +/// +/// Supports ECDSA on P-256, P-384 and P-521 with the NIST-paired digest +/// (SHA-256 / SHA-384 / SHA-512), selectable through algorithm identifiers such +/// as "ECDSA-P256-SHA256". The digest is mandatory: a bare "ECDSA-P256" names +/// a key, not a signature scheme, and is rejected at context creation. +/// +/// One class serves both directions: which one this context performs comes from +/// the OperationMode fixed at CTX_CREATE time. The bound key is a single +/// EVP_PKEY carrying the pair, so signing uses its private half and verification +/// its public half without needing two separate key resources. +/// +/// @par Signature encoding +/// OpenSSL natively produces and consumes DER-encoded ECDSA-Sig-Value. This +/// handler converts to and from the fixed-length IEEE P1363 form r‖s at its +/// boundary so that signatures interoperate byte-for-byte with the PKCS#11 +/// provider, which is natively P1363. +class OpenSslEcdsaHandler final + : public ::score::crypto::daemon::provider::score_provider::operations::signature::ScoreSignatureHandler +{ + public: + using Sptr = std::shared_ptr; + + explicit OpenSslEcdsaHandler(std::unique_ptr executor, + const common::AlgorithmId& algorithm); + ~OpenSslEcdsaHandler() override; + + OpenSslEcdsaHandler(const OpenSslEcdsaHandler&) = delete; + OpenSslEcdsaHandler& operator=(const OpenSslEcdsaHandler&) = delete; + OpenSslEcdsaHandler(OpenSslEcdsaHandler&&) = delete; + OpenSslEcdsaHandler& operator=(OpenSslEcdsaHandler&&) = delete; + + // ----------------------------------------------------------------------- + // Handler interface + // ----------------------------------------------------------------------- + + [[nodiscard]] ::score::crypto::Expected + InitializeContext(const ::score::crypto::daemon::provider::handler::InitializationParams& init_params) override; + + [[nodiscard]] ::score::crypto::Expected Reset() + override; + + // ----------------------------------------------------------------------- + // ScoreSignatureHandler interface + // ----------------------------------------------------------------------- + + [[nodiscard]] ::score::crypto::Expected + InitSignature() override; + + [[nodiscard]] ::score::crypto::Expected + UpdateSignature(const common::RequestParameter& data) override; + + [[nodiscard]] ::score::crypto::Expected FinalizeSign( + score::cpp::span signature) override; + + [[nodiscard]] ::score::crypto::Expected FinalizeVerify( + const common::RequestParameter& signature) override; + + /// @brief Check if the given algorithm is supported by this handler. + [[nodiscard]] static bool IsAlgorithmSupported(const common::AlgorithmId& algorithm) noexcept; + + private: + /// @brief Retrieve the bound key pair. + /// @return nullptr when no key is bound or it is not an EC key. + [[nodiscard]] EVP_PKEY* GetBoundPkey() const noexcept; + + /// @brief Free the message-digest context. + void CleanupContext() noexcept; + + /// @brief Convert a DER ECDSA-Sig-Value into fixed-length r‖s. + /// @param field_size Byte length of r and of s. + [[nodiscard]] static ::score::crypto::Expected + DerToP1363(const std::uint8_t* der, std::size_t der_len, std::size_t field_size); + + /// @brief Convert fixed-length r‖s into a DER ECDSA-Sig-Value. + [[nodiscard]] static ::score::crypto::Expected + P1363ToDer(const std::uint8_t* raw, std::size_t raw_len, std::size_t field_size); + + EVP_MD_CTX* m_md_ctx{nullptr}; + ::score::crypto::daemon::provider::handler::InitializationParams m_init_params; + + static constexpr std::string_view LOG_PREFIX = "[OPENSSL_ECDSA_HANDLER]"; +}; + +} // namespace score::crypto::daemon::provider::score_provider::openssl::handler + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_OPERATIONS_SIGNATURE_OPENSSL_ECDSA_HANDLER_HPP diff --git a/score/crypto/src/daemon/provider/score_provider/operations/cipher/BUILD b/score/crypto/src/daemon/provider/score_provider/operations/cipher/BUILD new file mode 100644 index 000000000..5b7b85d15 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/cipher/BUILD @@ -0,0 +1,36 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_cc//cc:defs.bzl", "cc_library") + +# Cipher executor + abstract base cipher handler for the score interface family. +cc_library( + name = "score_cipher_handler", + srcs = [ + "src/cipher_executor.cpp", + "src/score_cipher_handler.cpp", + ], + hdrs = [ + "cipher_executor.hpp", + "score_cipher_handler.hpp", + ], + visibility = ["//:__subpackages__"], + deps = [ + "//score/crypto/src/daemon/common", + "//score/crypto/src/daemon/common:algorithm_info", + "//score/crypto/src/daemon/provider/handler:cipher_handler_operations", + "//score/crypto/src/daemon/provider/handler:handler_headers", + "//score/crypto/src/daemon/provider/handler:handler_utils_impl", + "//score/crypto/src/api/common:crypto_common", + ], +) diff --git a/score/crypto/src/daemon/provider/score_provider/operations/cipher/cipher_executor.hpp b/score/crypto/src/daemon/provider/score_provider/operations/cipher/cipher_executor.hpp new file mode 100644 index 000000000..8bab6bd67 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/cipher/cipher_executor.hpp @@ -0,0 +1,69 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_CIPHER_CIPHER_EXECUTOR_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_CIPHER_CIPHER_EXECUTOR_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/types.hpp" + +namespace score::crypto::daemon::provider::score_provider::operations::cipher +{ + +class ScoreCipherHandler; + +/// @brief Stateless executor implementing the strategy / visitor pattern for +/// symmetric cipher operations under the score interface family. +/// +/// Mirrors HashExecutor and MacExecutor: +/// - Orchestrates operation flow and validates stream state transitions +/// - Extracts IPC buffer parameters from RequestParameters +/// - Routes operations to the typed ScoreCipherHandler methods +/// - Packs results back into ResponseParameters +class CipherExecutor +{ + public: + [[nodiscard]] Expected Execute( + ScoreCipherHandler& handler, + const common::OperationIdentifier& operationId, + common::RequestParameters& request); + + private: + [[nodiscard]] Expected ExecuteInit(ScoreCipherHandler& handler, + common::RequestParameters& request); + + [[nodiscard]] Expected ExecuteUpdate( + ScoreCipherHandler& handler, + common::RequestParameters& request); + + [[nodiscard]] Expected ExecuteFinalize( + ScoreCipherHandler& handler, + common::RequestParameters& request); + + [[nodiscard]] Expected ExecuteSingleShot( + ScoreCipherHandler& handler, + common::RequestParameters& request); + + [[nodiscard]] Expected ExecuteReset(ScoreCipherHandler& handler, + common::RequestParameters& request); + + [[nodiscard]] static Expected ValidateStreamTransition( + common::OperationAction action, + common::StreamOperationState currentState, + common::StreamOperationState& nextState); +}; + +} // namespace score::crypto::daemon::provider::score_provider::operations::cipher + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_CIPHER_CIPHER_EXECUTOR_HPP diff --git a/score/crypto/src/daemon/provider/score_provider/operations/cipher/score_cipher_handler.hpp b/score/crypto/src/daemon/provider/score_provider/operations/cipher/score_cipher_handler.hpp new file mode 100644 index 000000000..f7ffdaf4c --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/cipher/score_cipher_handler.hpp @@ -0,0 +1,145 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_CIPHER_SCORE_CIPHER_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_CIPHER_SCORE_CIPHER_HANDLER_HPP + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/types.hpp" +#include "score/crypto/src/daemon/provider/handler/i_handler.hpp" + +#include "score/span.hpp" + +#include +#include +#include +#include + +namespace score::crypto::daemon::provider::score_provider::operations::cipher +{ + +class CipherExecutor; + +/// @brief Abstract base handler for symmetric cipher operations under the score +/// interface family. +/// +/// Mirrors ScoreMacHandler: the daemon's Handler::Execute() is delegated to the +/// injected CipherExecutor, which validates the stream state machine and routes +/// to the typed methods below. Concrete score-interface providers (e.g. OpenSSL) +/// inherit and override those typed methods. +/// +/// Typed methods default to kUnsupportedOperation so that a partially-implemented +/// provider still compiles and returns a clear error at runtime. +/// +/// The cipher direction is fixed for the lifetime of the context: it is read from +/// CTX_CREATE param[4] in InitializeContext() and never changes afterwards, so a +/// context created for encryption can never accidentally decrypt. +class ScoreCipherHandler : public handler::Handler +{ + public: + using Sptr = std::shared_ptr; + + ScoreCipherHandler() = delete; + + /// @param executor Cipher executor injected by the handler factory. + /// @param algorithm Algorithm identifier (e.g. "AES-256-CBC"). + explicit ScoreCipherHandler(std::unique_ptr executor, const common::AlgorithmId& algorithm); + + ~ScoreCipherHandler() override = default; + + // ----------------------------------------------------------------------- + // Handler interface + // ----------------------------------------------------------------------- + + [[nodiscard]] Expected Execute( + const common::OperationIdentifier& operationId, + common::RequestParameters& request) override; + + [[nodiscard]] Expected InitializeContext( + const handler::InitializationParams& init_params) override; + + [[nodiscard]] Expected Reset() override; + + // ----------------------------------------------------------------------- + // Stream state management + // ----------------------------------------------------------------------- + + [[nodiscard]] common::StreamOperationState GetOperationState() const noexcept + { + return m_state; + } + + void SetOperationState(common::StreamOperationState state) noexcept + { + m_state = state; + } + + [[nodiscard]] const common::AlgorithmId& GetAlgorithm() const noexcept + { + return m_algorithm; + } + + [[nodiscard]] score::crypto::CipherDirection GetDirection() const noexcept + { + return m_direction; + } + + // ----------------------------------------------------------------------- + // Typed cipher operations — override in concrete provider handlers + // ----------------------------------------------------------------------- + + /// @brief Cipher block size in bytes (1 for stream modes such as CTR). + [[nodiscard]] virtual std::size_t GetBlockSize() const noexcept; + + /// @brief Required IV / nonce length in bytes (0 for ECB). + [[nodiscard]] virtual std::size_t GetIvSize() const noexcept; + + /// @brief Initialize the cipher stream with the bound key and the given IV. + /// @param iv Absent for IV-less modes; implementations reject a missing IV + /// when GetIvSize() is non-zero. + [[nodiscard]] virtual Expected InitCipher( + std::optional iv); + + /// @brief Process one input chunk into the caller's output buffer. + /// @param output Caller-provided buffer, already resolved from shared memory. + /// @return Bytes written, which may legitimately be zero while a block cipher + /// buffers a partial block. + [[nodiscard]] virtual Expected UpdateCipher( + const common::RequestParameter& input, + score::cpp::span output); + + /// @brief Finish the stream, writing any trailing bytes (final padded block). + /// @return Bytes written, zero for stream modes. + [[nodiscard]] virtual Expected FinalizeCipher( + score::cpp::span output); + + protected: + common::AlgorithmId m_algorithm; + common::StreamOperationState m_state{common::StreamOperationState::IDLE}; + score::crypto::CipherDirection m_direction{score::crypto::CipherDirection::kEncrypt}; + + /// @brief Reads the cipher direction from CTX_CREATE param[4]. + /// + /// Leaves m_direction untouched when the parameter is absent or has the + /// wrong type, so the kEncrypt default applies. + void ExtractDirection(const handler::InitializationParams& init_params) noexcept; + + private: + std::unique_ptr m_executor; +}; + +} // namespace score::crypto::daemon::provider::score_provider::operations::cipher + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_CIPHER_SCORE_CIPHER_HANDLER_HPP diff --git a/score/crypto/src/daemon/provider/score_provider/operations/cipher/src/cipher_executor.cpp b/score/crypto/src/daemon/provider/score_provider/operations/cipher/src/cipher_executor.cpp new file mode 100644 index 000000000..1312d06ba --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/cipher/src/cipher_executor.cpp @@ -0,0 +1,280 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/daemon/provider/score_provider/operations/cipher/cipher_executor.hpp" +#include "score/crypto/src/daemon/provider/handler/operations/cipher_handler_operations.hpp" +#include "score/crypto/src/daemon/provider/handler/src/handler_utils.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/cipher/score_cipher_handler.hpp" + +#include +#include +#include + +namespace score::crypto::daemon::provider::score_provider::operations::cipher +{ + +namespace handler = ::score::crypto::daemon::provider::handler; +using common::DaemonErrorCode; +using common::RequestParameters; +using common::ResponseParameters; +using common::StreamOperationState; +using ::score::crypto::daemon::provider::handler::handler_utils::CheckAndGetSpan; + +// --------------------------------------------------------------------------- +// Public entry point +// --------------------------------------------------------------------------- + +Expected CipherExecutor::Execute(ScoreCipherHandler& handler_ref, + const common::OperationIdentifier& operationId, + RequestParameters& request) +{ + namespace ops = handler::cipher_handler_operations; + + if (operationId.operationAction == ops::CIPHER_GET_OUTPUT_SIZE) + { + ResponseParameters response; + response.push_back(static_cast(handler_ref.GetBlockSize())); + return response; + } + + if (operationId.operationAction == ops::CIPHER_GET_IV_SIZE) + { + ResponseParameters response; + response.push_back(static_cast(handler_ref.GetIvSize())); + return response; + } + + if (operationId.operationAction == ops::CIPHER_RESET) + { + auto res = ExecuteReset(handler_ref, request); + if (!res.has_value()) + { + return make_unexpected(res.error()); + } + return ResponseParameters{}; + } + + if (operationId.operationAction == ops::CIPHER_SS) + { + if (handler_ref.GetOperationState() == StreamOperationState::STREAM_ACTIVE) + { + return make_unexpected(DaemonErrorCode::kOperationInProgress); + } + auto result = ExecuteSingleShot(handler_ref, request); + // Single-shot always ends the stream, successfully or not, so the context + // is left ready for the next call rather than stuck mid-stream. + handler_ref.SetOperationState(StreamOperationState::IDLE); + return result; + } + + // Streaming operations: validate the state machine transition first. + const StreamOperationState currentState = handler_ref.GetOperationState(); + StreamOperationState nextState = StreamOperationState::IDLE; + const auto validation = ValidateStreamTransition(operationId.operationAction, currentState, nextState); + if (!validation.has_value()) + { + return make_unexpected(validation.error()); + } + + if (operationId.operationAction == ops::CIPHER_INIT) + { + auto result = ExecuteInit(handler_ref, request); + if (!result.has_value()) + { + return make_unexpected(result.error()); + } + handler_ref.SetOperationState(nextState); + return ResponseParameters{}; + } + + if (operationId.operationAction == ops::CIPHER_UPDATE) + { + auto result = ExecuteUpdate(handler_ref, request); + if (result.has_value()) + { + handler_ref.SetOperationState(nextState); + } + return result; + } + + if (operationId.operationAction == ops::CIPHER_FINALIZE) + { + auto result = ExecuteFinalize(handler_ref, request); + if (result.has_value()) + { + handler_ref.SetOperationState(nextState); + } + return result; + } + + return make_unexpected(DaemonErrorCode::kInvalidOperation); +} + +// --------------------------------------------------------------------------- +// Operation implementations +// --------------------------------------------------------------------------- + +Expected CipherExecutor::ExecuteInit(ScoreCipherHandler& handler_ref, + RequestParameters& request) +{ + std::optional iv; + if (!request.empty()) + { + iv.emplace(request[0]); + } + return handler_ref.InitCipher(iv); +} + +Expected CipherExecutor::ExecuteUpdate(ScoreCipherHandler& handler_ref, + RequestParameters& request) +{ + // request[0] = input chunk, request[1] = caller-provided output buffer + if (request.size() < 2U) + { + return make_unexpected(DaemonErrorCode::kInsufficientParameters); + } + + const auto outputSpan = CheckAndGetSpan(request[1]); + if (!outputSpan.has_value()) + { + return make_unexpected(outputSpan.error()); + } + + auto written = handler_ref.UpdateCipher(request[0], outputSpan.value()); + if (!written.has_value()) + { + return make_unexpected(written.error()); + } + + ResponseParameters response; + response.push_back(static_cast(written.value())); + return response; +} + +Expected CipherExecutor::ExecuteFinalize(ScoreCipherHandler& handler_ref, + RequestParameters& request) +{ + if (request.empty()) + { + return make_unexpected(DaemonErrorCode::kInsufficientParameters); + } + + const auto outputSpan = CheckAndGetSpan(request[0]); + if (!outputSpan.has_value()) + { + return make_unexpected(outputSpan.error()); + } + + auto written = handler_ref.FinalizeCipher(outputSpan.value()); + if (!written.has_value()) + { + return make_unexpected(written.error()); + } + + ResponseParameters response; + response.push_back(static_cast(written.value())); + return response; +} + +Expected CipherExecutor::ExecuteSingleShot(ScoreCipherHandler& handler_ref, + RequestParameters& request) +{ + // request[0] = IV (empty for ECB), request[1] = input data, request[2] = output buffer + if (request.size() < 3U) + { + return make_unexpected(DaemonErrorCode::kInsufficientParameters); + } + + const auto outputSpan = CheckAndGetSpan(request[2]); + if (!outputSpan.has_value()) + { + return make_unexpected(outputSpan.error()); + } + + std::optional iv; + if (handler_ref.GetIvSize() > 0U) + { + iv.emplace(request[0]); + } + + auto init = handler_ref.InitCipher(iv); + if (!init.has_value()) + { + return make_unexpected(init.error()); + } + + auto update = handler_ref.UpdateCipher(request[1], outputSpan.value()); + if (!update.has_value()) + { + return make_unexpected(update.error()); + } + + // Finalize appends to whatever Update already wrote, so the caller sees one + // contiguous result in its own buffer. + auto final_res = handler_ref.FinalizeCipher(outputSpan.value().subspan(update.value())); + if (!final_res.has_value()) + { + return make_unexpected(final_res.error()); + } + + ResponseParameters response; + response.push_back(static_cast(update.value() + final_res.value())); + return response; +} + +Expected CipherExecutor::ExecuteReset(ScoreCipherHandler& handler_ref, + RequestParameters& /*request*/) +{ + return handler_ref.Reset(); +} + +// --------------------------------------------------------------------------- +// Stream state machine +// --------------------------------------------------------------------------- + +// static +Expected CipherExecutor::ValidateStreamTransition( + const common::OperationAction action, + const StreamOperationState currentState, + StreamOperationState& nextState) +{ + namespace ops = handler::cipher_handler_operations; + + handler::handler_utils::StreamOperation op{}; + if (action == ops::CIPHER_INIT) + { + op = handler::handler_utils::StreamOperation::kInit; + } + else if (action == ops::CIPHER_UPDATE) + { + op = handler::handler_utils::StreamOperation::kUpdate; + } + else if (action == ops::CIPHER_FINALIZE) + { + op = handler::handler_utils::StreamOperation::kFinalize; + } + else + { + return make_unexpected(DaemonErrorCode::kInvalidOperation); + } + + const auto result = handler::handler_utils::ValidateStreamOperationSequence(currentState, op); + if (!result.has_value()) + { + return make_unexpected(result.error()); + } + nextState = result.value(); + return std::monostate{}; +} + +} // namespace score::crypto::daemon::provider::score_provider::operations::cipher diff --git a/score/crypto/src/daemon/provider/score_provider/operations/cipher/src/score_cipher_handler.cpp b/score/crypto/src/daemon/provider/score_provider/operations/cipher/src/score_cipher_handler.cpp new file mode 100644 index 000000000..d695bdde8 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/cipher/src/score_cipher_handler.cpp @@ -0,0 +1,108 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/daemon/provider/score_provider/operations/cipher/score_cipher_handler.hpp" +#include "score/crypto/src/daemon/common/algorithm_info.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/cipher/cipher_executor.hpp" + +#include +#include + +namespace score::crypto::daemon::provider::score_provider::operations::cipher +{ + +using common::DaemonErrorCode; +using common::ResponseParameters; +using common::StreamOperationState; + +namespace +{ +/// CTX_CREATE wire slot carrying the cipher direction byte. +constexpr std::size_t kDirectionParamIndex = 4U; +} // namespace + +ScoreCipherHandler::ScoreCipherHandler(std::unique_ptr executor, const common::AlgorithmId& algorithm) + : m_algorithm{algorithm}, m_state{StreamOperationState::IDLE}, m_executor{std::move(executor)} +{ +} + +Expected ScoreCipherHandler::Execute( + const common::OperationIdentifier& operationId, + common::RequestParameters& request) +{ + return m_executor->Execute(*this, operationId, request); +} + +void ScoreCipherHandler::ExtractDirection(const handler::InitializationParams& init_params) noexcept +{ + if (init_params.context_creation_params.size() <= kDirectionParamIndex) + { + return; + } + const auto* direction_val = std::get_if(&init_params.context_creation_params[kDirectionParamIndex]); + if (direction_val != nullptr) + { + m_direction = static_cast(*direction_val); + } +} + +Expected ScoreCipherHandler::InitializeContext( + const handler::InitializationParams& init_params) +{ + ExtractDirection(init_params); + m_state = StreamOperationState::IDLE; + return std::monostate{}; +} + +Expected ScoreCipherHandler::Reset() +{ + m_state = StreamOperationState::IDLE; + return std::monostate{}; +} + +// --------------------------------------------------------------------------- +// Default typed operations — algorithm metadata is provider-independent, the +// actual crypto is not and returns unsupported unless overridden. +// --------------------------------------------------------------------------- + +std::size_t ScoreCipherHandler::GetBlockSize() const noexcept +{ + const auto info = + ::score::crypto::daemon::common::LookupCipher(std::string_view{m_algorithm.data(), m_algorithm.size()}); + return info.has_value() ? info->block_size : 0U; +} + +std::size_t ScoreCipherHandler::GetIvSize() const noexcept +{ + const auto info = + ::score::crypto::daemon::common::LookupCipher(std::string_view{m_algorithm.data(), m_algorithm.size()}); + return info.has_value() ? info->iv_size : 0U; +} + +Expected ScoreCipherHandler::InitCipher(std::optional /*iv*/) +{ + return make_unexpected(DaemonErrorCode::kUnsupportedOperation); +} + +Expected ScoreCipherHandler::UpdateCipher(const common::RequestParameter& /*input*/, + score::cpp::span /*output*/) +{ + return make_unexpected(DaemonErrorCode::kUnsupportedOperation); +} + +Expected ScoreCipherHandler::FinalizeCipher(score::cpp::span /*output*/) +{ + return make_unexpected(DaemonErrorCode::kUnsupportedOperation); +} + +} // namespace score::crypto::daemon::provider::score_provider::operations::cipher diff --git a/score/crypto/src/daemon/provider/score_provider/operations/factory/score_handler_factory.hpp b/score/crypto/src/daemon/provider/score_provider/operations/factory/score_handler_factory.hpp index 9a71c64d8..530fbc904 100644 --- a/score/crypto/src/daemon/provider/score_provider/operations/factory/score_handler_factory.hpp +++ b/score/crypto/src/daemon/provider/score_provider/operations/factory/score_handler_factory.hpp @@ -14,6 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_FACTORY_SCORE_HANDLER_FACTORY_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_FACTORY_SCORE_HANDLER_FACTORY_HPP +#include "score/crypto/src/daemon/common/context_types.hpp" #include "score/crypto/src/daemon/common/types.hpp" #include "score/crypto/src/daemon/key_management/core/key_management_service.hpp" #include "score/crypto/src/daemon/key_management/interfaces/i_key_factory.hpp" @@ -22,6 +23,7 @@ #include "score/result/result.h" #include +#include namespace score::crypto::daemon::provider::score_provider::operations::factory { @@ -60,14 +62,36 @@ class ScoreHandlerFactory : public handler::ICryptoHandlerFactory /// Override in concrete provider to create a key management handler. Default returns unsupported. [[nodiscard]] virtual ::score::Result CreateKeyManagementHandler(); + /// Override in concrete provider to create a symmetric cipher handler. Default returns unsupported. + [[nodiscard]] virtual ::score::Result CreateCipherHandler( + const common::AlgorithmId& algorithm); + + /// Override in concrete provider to create a signature handler. + /// + /// Serves both the SIGN and VERIFY context types: the direction is carried + /// in the OperationMode parameter of CTX_CREATE, not in the handler id, so + /// one implementation covers both. Default returns unsupported. + [[nodiscard]] virtual ::score::Result CreateSignatureHandler( + const common::AlgorithmId& algorithm); + + /// Override in concrete provider to create a random handler. Default returns unsupported. + [[nodiscard]] virtual ::score::Result CreateRandomHandler( + const common::AlgorithmId& algorithm); + std::shared_ptr m_key_factory; std::shared_ptr m_slot_handler; key_management::KeyManagementService::Sptr m_km_service; private: - static constexpr const char* HASH = "HASH"; - static constexpr const char* MAC = "MAC"; - static constexpr const char* KEY_MANAGEMENT = "KEY_MANAGEMENT"; + // The context-type ids are shared with the client and the mediator, which + // dispatches on the same strings — see common/context_types.hpp. + static constexpr std::string_view HASH = common::context_types::kHash; + static constexpr std::string_view MAC = common::context_types::kMac; + static constexpr std::string_view KEY_MANAGEMENT = common::context_types::kKeyManagement; + static constexpr std::string_view CIPHER = common::context_types::kCipher; + static constexpr std::string_view SIGN = common::context_types::kSign; + static constexpr std::string_view VERIFY = common::context_types::kVerify; + static constexpr std::string_view RANDOM = common::context_types::kRandom; }; } // namespace score::crypto::daemon::provider::score_provider::operations::factory diff --git a/score/crypto/src/daemon/provider/score_provider/operations/factory/src/score_handler_factory.cpp b/score/crypto/src/daemon/provider/score_provider/operations/factory/src/score_handler_factory.cpp index d9c05aca7..1a2458b72 100644 --- a/score/crypto/src/daemon/provider/score_provider/operations/factory/src/score_handler_factory.cpp +++ b/score/crypto/src/daemon/provider/score_provider/operations/factory/src/score_handler_factory.cpp @@ -42,6 +42,20 @@ ::score::Result ScoreHandlerFactory::CreateHandler(const { return CreateKeyManagementHandler(); } + if (handlerId == CIPHER) + { + return CreateCipherHandler(algorithm); + } + // SIGN and VERIFY map to the same handler; the OperationMode in the + // CTX_CREATE parameters selects which half of the key pair it binds. + if ((handlerId == SIGN) || (handlerId == VERIFY)) + { + return CreateSignatureHandler(algorithm); + } + if (handlerId == RANDOM) + { + return CreateRandomHandler(algorithm); + } ::score::result::Error error( static_cast<::score::result::ErrorCode>(::score::crypto::CryptoErrorCode::kUnsupportedOperation), @@ -81,4 +95,34 @@ ::score::Result ScoreHandlerFactory::CreateKeyManagement return ::score::Result(::score::unexpect, error); } +::score::Result ScoreHandlerFactory::CreateCipherHandler( + const common::AlgorithmId& /*algorithm*/) +{ + ::score::result::Error error( + static_cast<::score::result::ErrorCode>(::score::crypto::CryptoErrorCode::kUnsupportedOperation), + ::score::crypto::kCryptoErrorDomain, + "Cipher handler not supported by this score provider"); + return ::score::Result(::score::unexpect, error); +} + +::score::Result ScoreHandlerFactory::CreateSignatureHandler( + const common::AlgorithmId& /*algorithm*/) +{ + ::score::result::Error error( + static_cast<::score::result::ErrorCode>(::score::crypto::CryptoErrorCode::kUnsupportedOperation), + ::score::crypto::kCryptoErrorDomain, + "Signature handler not supported by this score provider"); + return ::score::Result(::score::unexpect, error); +} + +::score::Result ScoreHandlerFactory::CreateRandomHandler( + const common::AlgorithmId& /*algorithm*/) +{ + ::score::result::Error error( + static_cast<::score::result::ErrorCode>(::score::crypto::CryptoErrorCode::kUnsupportedOperation), + ::score::crypto::kCryptoErrorDomain, + "Random handler not supported by this score provider"); + return ::score::Result(::score::unexpect, error); +} + } // namespace score::crypto::daemon::provider::score_provider::operations::factory diff --git a/score/crypto/src/daemon/provider/score_provider/operations/random/BUILD b/score/crypto/src/daemon/provider/score_provider/operations/random/BUILD new file mode 100644 index 000000000..1d115adcc --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/random/BUILD @@ -0,0 +1,34 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_cc//cc:defs.bzl", "cc_library") + +# Random executor + abstract base random handler for the score interface family. +cc_library( + name = "score_random_handler", + srcs = [ + "src/random_executor.cpp", + "src/score_random_handler.cpp", + ], + hdrs = [ + "random_executor.hpp", + "score_random_handler.hpp", + ], + visibility = ["//:__subpackages__"], + deps = [ + "//score/crypto/src/daemon/common", + "//score/crypto/src/daemon/provider/handler:handler_headers", + "//score/crypto/src/daemon/provider/handler:handler_utils_impl", + "//score/crypto/src/daemon/provider/handler:random_handler_operations", + ], +) diff --git a/score/crypto/src/daemon/provider/score_provider/operations/random/random_executor.hpp b/score/crypto/src/daemon/provider/score_provider/operations/random/random_executor.hpp new file mode 100644 index 000000000..da180cdda --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/random/random_executor.hpp @@ -0,0 +1,51 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_RANDOM_RANDOM_EXECUTOR_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_RANDOM_RANDOM_EXECUTOR_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/types.hpp" + +namespace score::crypto::daemon::provider::score_provider::operations::random +{ + +class ScoreRandomHandler; + +/// @brief Stateless executor for random number generation under the score +/// interface family. +/// +/// Simpler than the streaming executors: there is no state machine to validate, +/// only parameter extraction, a request-size bound, and result packing. +class RandomExecutor +{ + public: + [[nodiscard]] Expected Execute( + ScoreRandomHandler& handler, + const common::OperationIdentifier& operationId, + common::RequestParameters& request); + + private: + [[nodiscard]] Expected ExecuteGenerate( + ScoreRandomHandler& handler, + common::RequestParameters& request); + + [[nodiscard]] Expected ExecuteSeed( + ScoreRandomHandler& handler, + common::RequestParameters& request); +}; + +} // namespace score::crypto::daemon::provider::score_provider::operations::random + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_RANDOM_RANDOM_EXECUTOR_HPP diff --git a/score/crypto/src/daemon/provider/score_provider/operations/random/score_random_handler.hpp b/score/crypto/src/daemon/provider/score_provider/operations/random/score_random_handler.hpp new file mode 100644 index 000000000..33be26083 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/random/score_random_handler.hpp @@ -0,0 +1,100 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_RANDOM_SCORE_RANDOM_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_RANDOM_SCORE_RANDOM_HANDLER_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/types.hpp" +#include "score/crypto/src/daemon/provider/handler/i_handler.hpp" + +#include "score/span.hpp" + +#include +#include +#include + +namespace score::crypto::daemon::provider::score_provider::operations::random +{ + +class RandomExecutor; + +/// @brief Abstract base handler for random number generation under the score +/// interface family. +/// +/// Unlike the hash / MAC / cipher handlers this context is not streaming: there +/// is no state machine, and Generate() / Seed() are valid at any time. The +/// executor therefore only demultiplexes the two operations and marshals buffers. +class ScoreRandomHandler : public handler::Handler +{ + public: + using Sptr = std::shared_ptr; + + ScoreRandomHandler() = delete; + + /// @param executor Random executor injected by the handler factory. + /// @param algorithm RNG algorithm identifier; empty selects the provider default. + explicit ScoreRandomHandler(std::unique_ptr executor, const common::AlgorithmId& algorithm); + + ~ScoreRandomHandler() override = default; + + // ----------------------------------------------------------------------- + // Handler interface + // ----------------------------------------------------------------------- + + [[nodiscard]] Expected Execute( + const common::OperationIdentifier& operationId, + common::RequestParameters& request) override; + + [[nodiscard]] Expected InitializeContext( + const handler::InitializationParams& init_params) override; + + [[nodiscard]] Expected Reset() override; + + [[nodiscard]] const common::AlgorithmId& GetAlgorithm() const noexcept + { + return m_algorithm; + } + + /// @brief Upper bound on bytes served by a single Generate() call. + /// + /// Bounds the daemon-side allocation a client can trigger with one request. + static constexpr std::size_t kMaxGenerateBytes = 64U * 1024U; + + // ----------------------------------------------------------------------- + // Typed random operations — override in concrete provider handlers + // ----------------------------------------------------------------------- + + /// @brief Fill @p output with cryptographically secure random bytes. + /// @param output Caller-provided buffer, already resolved from shared memory. + /// @return Bytes written, always the full length of @p output on success. + [[nodiscard]] virtual Expected GenerateRandom( + score::cpp::span output); + + /// @brief Mix additional entropy into the generator state. + /// @note Providers without an externally seedable source report success + /// without changing any state. + [[nodiscard]] virtual Expected SeedRandom( + const common::RequestParameter& seed); + + protected: + common::AlgorithmId m_algorithm; + + private: + std::unique_ptr m_executor; +}; + +} // namespace score::crypto::daemon::provider::score_provider::operations::random + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_RANDOM_SCORE_RANDOM_HANDLER_HPP diff --git a/score/crypto/src/daemon/provider/score_provider/operations/random/src/random_executor.cpp b/score/crypto/src/daemon/provider/score_provider/operations/random/src/random_executor.cpp new file mode 100644 index 000000000..8586925a0 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/random/src/random_executor.cpp @@ -0,0 +1,100 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/daemon/provider/score_provider/operations/random/random_executor.hpp" +#include "score/crypto/src/daemon/provider/handler/operations/random_handler_operations.hpp" +#include "score/crypto/src/daemon/provider/handler/src/handler_utils.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/random/score_random_handler.hpp" + +#include +#include +#include + +namespace score::crypto::daemon::provider::score_provider::operations::random +{ + +namespace handler = ::score::crypto::daemon::provider::handler; +using common::DaemonErrorCode; +using common::RequestParameters; +using common::ResponseParameters; +using ::score::crypto::daemon::provider::handler::handler_utils::CheckAndGetSpan; + +Expected RandomExecutor::Execute(ScoreRandomHandler& handler_ref, + const common::OperationIdentifier& operationId, + RequestParameters& request) +{ + namespace ops = handler::random_handler_operations; + + if (operationId.operationAction == ops::RANDOM_GENERATE) + { + return ExecuteGenerate(handler_ref, request); + } + + if (operationId.operationAction == ops::RANDOM_SEED) + { + return ExecuteSeed(handler_ref, request); + } + + return make_unexpected(DaemonErrorCode::kInvalidOperation); +} + +Expected RandomExecutor::ExecuteGenerate(ScoreRandomHandler& handler_ref, + RequestParameters& request) +{ + // request[0] = caller-provided output buffer; its length is the byte count. + if (request.empty()) + { + return make_unexpected(DaemonErrorCode::kInsufficientParameters); + } + + const auto outputSpan = CheckAndGetSpan(request[0]); + if (!outputSpan.has_value()) + { + return make_unexpected(outputSpan.error()); + } + + // Bound the work a single client request can trigger in the daemon. + if (outputSpan.value().size() > ScoreRandomHandler::kMaxGenerateBytes) + { + return make_unexpected(DaemonErrorCode::kQuotaExceeded); + } + + auto generated = handler_ref.GenerateRandom(outputSpan.value()); + if (!generated.has_value()) + { + return make_unexpected(generated.error()); + } + + ResponseParameters response; + response.push_back(static_cast(generated.value())); + return response; +} + +Expected RandomExecutor::ExecuteSeed(ScoreRandomHandler& handler_ref, + RequestParameters& request) +{ + if (request.empty()) + { + return make_unexpected(DaemonErrorCode::kInsufficientParameters); + } + + auto seeded = handler_ref.SeedRandom(request[0]); + if (!seeded.has_value()) + { + return make_unexpected(seeded.error()); + } + + return ResponseParameters{}; +} + +} // namespace score::crypto::daemon::provider::score_provider::operations::random diff --git a/score/crypto/src/daemon/provider/score_provider/operations/random/src/score_random_handler.cpp b/score/crypto/src/daemon/provider/score_provider/operations/random/src/score_random_handler.cpp new file mode 100644 index 000000000..1bb1fd710 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/random/src/score_random_handler.cpp @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/daemon/provider/score_provider/operations/random/score_random_handler.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/random/random_executor.hpp" + +namespace score::crypto::daemon::provider::score_provider::operations::random +{ + +using common::DaemonErrorCode; +using common::ResponseParameters; + +ScoreRandomHandler::ScoreRandomHandler(std::unique_ptr executor, const common::AlgorithmId& algorithm) + : m_algorithm{algorithm}, m_executor{std::move(executor)} +{ +} + +Expected ScoreRandomHandler::Execute( + const common::OperationIdentifier& operationId, + common::RequestParameters& request) +{ + return m_executor->Execute(*this, operationId, request); +} + +Expected ScoreRandomHandler::InitializeContext( + const handler::InitializationParams& /*init_params*/) +{ + // A random context holds no per-request state and binds no key, so there is + // nothing to set up beyond what the constructor already did. + return std::monostate{}; +} + +Expected ScoreRandomHandler::Reset() +{ + return std::monostate{}; +} + +// --------------------------------------------------------------------------- +// Default typed operations — return unsupported unless overridden +// --------------------------------------------------------------------------- + +Expected ScoreRandomHandler::GenerateRandom(score::cpp::span /*output*/) +{ + return make_unexpected(DaemonErrorCode::kUnsupportedOperation); +} + +Expected ScoreRandomHandler::SeedRandom(const common::RequestParameter& /*seed*/) +{ + return make_unexpected(DaemonErrorCode::kUnsupportedOperation); +} + +} // namespace score::crypto::daemon::provider::score_provider::operations::random diff --git a/score/crypto/src/daemon/provider/score_provider/operations/signature/BUILD b/score/crypto/src/daemon/provider/score_provider/operations/signature/BUILD new file mode 100644 index 000000000..d3e737840 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/signature/BUILD @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_cc//cc:defs.bzl", "cc_library") + +# Signature executor + abstract base signature handler (sign and verify) for the +# score interface family. +cc_library( + name = "score_signature_handler", + srcs = [ + "src/score_signature_handler.cpp", + "src/signature_executor.cpp", + ], + hdrs = [ + "score_signature_handler.hpp", + "signature_executor.hpp", + ], + visibility = ["//:__subpackages__"], + deps = [ + "//score/crypto/src/daemon/common", + "//score/crypto/src/daemon/common:algorithm_info", + "//score/crypto/src/daemon/provider/handler:handler_headers", + "//score/crypto/src/daemon/provider/handler:handler_utils_impl", + "//score/crypto/src/daemon/provider/handler:signature_handler_operations", + "//score/crypto/src/api/common:crypto_common", + ], +) diff --git a/score/crypto/src/daemon/provider/score_provider/operations/signature/score_signature_handler.hpp b/score/crypto/src/daemon/provider/score_provider/operations/signature/score_signature_handler.hpp new file mode 100644 index 000000000..e01bdab95 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/signature/score_signature_handler.hpp @@ -0,0 +1,144 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_SIGNATURE_SCORE_SIGNATURE_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_SIGNATURE_SCORE_SIGNATURE_HANDLER_HPP + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/types.hpp" +#include "score/crypto/src/daemon/provider/handler/i_handler.hpp" + +#include "score/span.hpp" + +#include +#include +#include + +namespace score::crypto::daemon::provider::score_provider::operations::signature +{ + +class SignatureExecutor; + +/// @brief Abstract base handler for digital signature generation and verification +/// under the score interface family. +/// +/// Signing and verification share one handler class because they differ only in +/// which half of the key pair is used and in the final provider call +/// (EVP_DigestSignFinal vs EVP_DigestVerifyFinal). Which of the two a context +/// performs is fixed at CTX_CREATE time via the OperationMode byte in param[4] +/// and is exposed through GetOperationMode(). +/// +/// The daemon's Handler::Execute() is delegated to the injected SignatureExecutor, +/// which validates the stream state machine and routes to the typed methods below. +/// +/// Signature encoding: this stack uses the fixed-length IEEE P1363 form r‖s for +/// ECDSA on every provider, so a signature produced by one provider verifies +/// under another. Providers whose native output is DER convert at this boundary. +class ScoreSignatureHandler : public handler::Handler +{ + public: + using Sptr = std::shared_ptr; + + ScoreSignatureHandler() = delete; + + /// @param executor Signature executor injected by the handler factory. + /// @param algorithm Algorithm identifier (e.g. "ECDSA-P256-SHA256"). + explicit ScoreSignatureHandler(std::unique_ptr executor, const common::AlgorithmId& algorithm); + + ~ScoreSignatureHandler() override = default; + + // ----------------------------------------------------------------------- + // Handler interface + // ----------------------------------------------------------------------- + + [[nodiscard]] Expected Execute( + const common::OperationIdentifier& operationId, + common::RequestParameters& request) override; + + [[nodiscard]] Expected InitializeContext( + const handler::InitializationParams& init_params) override; + + [[nodiscard]] Expected Reset() override; + + // ----------------------------------------------------------------------- + // Stream state management + // ----------------------------------------------------------------------- + + [[nodiscard]] common::StreamOperationState GetOperationState() const noexcept + { + return m_state; + } + + void SetOperationState(common::StreamOperationState state) noexcept + { + m_state = state; + } + + [[nodiscard]] const common::AlgorithmId& GetAlgorithm() const noexcept + { + return m_algorithm; + } + + [[nodiscard]] score::crypto::OperationMode GetOperationMode() const noexcept + { + return m_operation_mode; + } + + // ----------------------------------------------------------------------- + // Typed signature operations — override in concrete provider handlers + // ----------------------------------------------------------------------- + + /// @brief Signature length in bytes for the configured algorithm. + [[nodiscard]] virtual std::size_t GetSignatureSize() const noexcept; + + /// @brief Start a signing or verification stream using the bound key. + [[nodiscard]] virtual Expected InitSignature(); + + /// @brief Feed a message chunk into the active stream. + [[nodiscard]] virtual Expected UpdateSignature( + const common::RequestParameter& data); + + /// @brief Produce the signature over the accumulated message. + /// @param signature Caller-provided buffer, already resolved from shared memory. + /// @return Bytes written. + /// @note Only valid when GetOperationMode() == kGenerate. + [[nodiscard]] virtual Expected FinalizeSign( + score::cpp::span signature); + + /// @brief Check @p signature against the accumulated message. + /// @return false for a well-formed but incorrect signature; an error only for + /// malformed input or a provider failure. + /// @note Only valid when GetOperationMode() == kVerify. + [[nodiscard]] virtual Expected FinalizeVerify( + const common::RequestParameter& signature); + + protected: + common::AlgorithmId m_algorithm; + common::StreamOperationState m_state{common::StreamOperationState::IDLE}; + score::crypto::OperationMode m_operation_mode{score::crypto::OperationMode::kGenerate}; + + /// @brief Reads the OperationMode from CTX_CREATE param[4]. + /// + /// Leaves m_operation_mode untouched when the parameter is absent or has the + /// wrong type, so the kGenerate default applies. + void ExtractOperationMode(const handler::InitializationParams& init_params) noexcept; + + private: + std::unique_ptr m_executor; +}; + +} // namespace score::crypto::daemon::provider::score_provider::operations::signature + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_SIGNATURE_SCORE_SIGNATURE_HANDLER_HPP diff --git a/score/crypto/src/daemon/provider/score_provider/operations/signature/signature_executor.hpp b/score/crypto/src/daemon/provider/score_provider/operations/signature/signature_executor.hpp new file mode 100644 index 000000000..91d0022d5 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/signature/signature_executor.hpp @@ -0,0 +1,71 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_SIGNATURE_SIGNATURE_EXECUTOR_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_SIGNATURE_SIGNATURE_EXECUTOR_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/types.hpp" + +namespace score::crypto::daemon::provider::score_provider::operations::signature +{ + +class ScoreSignatureHandler; + +/// @brief Stateless executor implementing the strategy / visitor pattern for +/// signature operations under the score interface family. +/// +/// Handles both the OP_ACTOR_SIGN_HANDLER and OP_ACTOR_VERIFY_HANDLER actors. +/// The two share the same action numbering; the actor decides whether FINALIZE +/// produces a signature or checks one, and the executor rejects a request whose +/// actor contradicts the context's OperationMode. +class SignatureExecutor +{ + public: + [[nodiscard]] Expected Execute( + ScoreSignatureHandler& handler, + const common::OperationIdentifier& operationId, + common::RequestParameters& request); + + private: + [[nodiscard]] Expected ExecuteSignFinalize( + ScoreSignatureHandler& handler, + common::RequestParameters& request); + + [[nodiscard]] Expected ExecuteSignSingleShot( + ScoreSignatureHandler& handler, + common::RequestParameters& request); + + [[nodiscard]] Expected ExecuteVerifySingleShot( + ScoreSignatureHandler& handler, + common::RequestParameters& request); + + [[nodiscard]] Expected ExecuteVerifyFinalize( + ScoreSignatureHandler& handler, + common::RequestParameters& request); + + [[nodiscard]] static Expected ValidateStreamTransition( + common::OperationAction action, + common::StreamOperationState currentState, + common::StreamOperationState& nextState); + + /// @brief Rejects a request whose actor does not match the context's mode. + [[nodiscard]] static Expected ValidateActorMatchesMode( + common::OperationActor actor, + const ScoreSignatureHandler& handler); +}; + +} // namespace score::crypto::daemon::provider::score_provider::operations::signature + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPERATIONS_SIGNATURE_SIGNATURE_EXECUTOR_HPP diff --git a/score/crypto/src/daemon/provider/score_provider/operations/signature/src/score_signature_handler.cpp b/score/crypto/src/daemon/provider/score_provider/operations/signature/src/score_signature_handler.cpp new file mode 100644 index 000000000..381094790 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/signature/src/score_signature_handler.cpp @@ -0,0 +1,106 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/daemon/provider/score_provider/operations/signature/score_signature_handler.hpp" +#include "score/crypto/src/daemon/common/algorithm_info.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/signature/signature_executor.hpp" + +#include +#include + +namespace score::crypto::daemon::provider::score_provider::operations::signature +{ + +using common::DaemonErrorCode; +using common::ResponseParameters; +using common::StreamOperationState; + +namespace +{ +/// CTX_CREATE wire slot carrying the OperationMode byte. +constexpr std::size_t kOperationModeParamIndex = 4U; +} // namespace + +ScoreSignatureHandler::ScoreSignatureHandler(std::unique_ptr executor, + const common::AlgorithmId& algorithm) + : m_algorithm{algorithm}, m_state{StreamOperationState::IDLE}, m_executor{std::move(executor)} +{ +} + +Expected ScoreSignatureHandler::Execute( + const common::OperationIdentifier& operationId, + common::RequestParameters& request) +{ + return m_executor->Execute(*this, operationId, request); +} + +void ScoreSignatureHandler::ExtractOperationMode(const handler::InitializationParams& init_params) noexcept +{ + if (init_params.context_creation_params.size() <= kOperationModeParamIndex) + { + return; + } + const auto* mode_val = std::get_if(&init_params.context_creation_params[kOperationModeParamIndex]); + if (mode_val != nullptr) + { + m_operation_mode = static_cast(*mode_val); + } +} + +Expected ScoreSignatureHandler::InitializeContext( + const handler::InitializationParams& init_params) +{ + ExtractOperationMode(init_params); + m_state = StreamOperationState::IDLE; + return std::monostate{}; +} + +Expected ScoreSignatureHandler::Reset() +{ + m_state = StreamOperationState::IDLE; + return std::monostate{}; +} + +// --------------------------------------------------------------------------- +// Default typed operations +// --------------------------------------------------------------------------- + +std::size_t ScoreSignatureHandler::GetSignatureSize() const noexcept +{ + const auto curve = ::score::crypto::daemon::common::LookupEcCurveOfAlgorithm( + std::string_view{m_algorithm.data(), m_algorithm.size()}); + return curve.has_value() ? curve->signature_size : 0U; +} + +Expected ScoreSignatureHandler::InitSignature() +{ + return make_unexpected(DaemonErrorCode::kUnsupportedOperation); +} + +Expected ScoreSignatureHandler::UpdateSignature( + const common::RequestParameter& /*data*/) +{ + return make_unexpected(DaemonErrorCode::kUnsupportedOperation); +} + +Expected ScoreSignatureHandler::FinalizeSign(score::cpp::span /*signature*/) +{ + return make_unexpected(DaemonErrorCode::kUnsupportedOperation); +} + +Expected ScoreSignatureHandler::FinalizeVerify(const common::RequestParameter& /*signature*/) +{ + return make_unexpected(DaemonErrorCode::kUnsupportedOperation); +} + +} // namespace score::crypto::daemon::provider::score_provider::operations::signature diff --git a/score/crypto/src/daemon/provider/score_provider/operations/signature/src/signature_executor.cpp b/score/crypto/src/daemon/provider/score_provider/operations/signature/src/signature_executor.cpp new file mode 100644 index 000000000..40fc439fc --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/operations/signature/src/signature_executor.cpp @@ -0,0 +1,323 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/daemon/provider/score_provider/operations/signature/signature_executor.hpp" +#include "score/crypto/src/daemon/common/actors.hpp" +#include "score/crypto/src/daemon/provider/handler/operations/signature_handler_operations.hpp" +#include "score/crypto/src/daemon/provider/handler/src/handler_utils.hpp" +#include "score/crypto/src/daemon/provider/score_provider/operations/signature/score_signature_handler.hpp" + +namespace score::crypto::daemon::provider::score_provider::operations::signature +{ + +namespace handler = ::score::crypto::daemon::provider::handler; +namespace actors = ::score::crypto::daemon::common::actors; +namespace sign_ops = ::score::crypto::daemon::provider::handler::sign_handler_operations; +namespace verify_ops = ::score::crypto::daemon::provider::handler::verify_handler_operations; + +using common::DaemonErrorCode; +using common::RequestParameters; +using common::ResponseParameters; +using common::StreamOperationState; +using ::score::crypto::daemon::provider::handler::handler_utils::CheckAndGetSpan; + +// The sign and verify actors deliberately share one action numbering so that a +// single dispatch table can serve both. Keep the two headers in lock-step. +static_assert(sign_ops::SIGN_INIT == verify_ops::VERIFY_INIT, "sign/verify INIT actions must match"); +static_assert(sign_ops::SIGN_UPDATE == verify_ops::VERIFY_UPDATE, "sign/verify UPDATE actions must match"); +static_assert(sign_ops::SIGN_FINALIZE == verify_ops::VERIFY_FINALIZE, "sign/verify FINALIZE actions must match"); +static_assert(sign_ops::SIGN_SS == verify_ops::VERIFY_SS, "sign/verify single-shot actions must match"); +static_assert(sign_ops::SIGN_GET_SIZE == verify_ops::VERIFY_GET_SIZE, "sign/verify GET_SIZE actions must match"); +static_assert(sign_ops::SIGN_RESET == verify_ops::VERIFY_RESET, "sign/verify RESET actions must match"); + +// --------------------------------------------------------------------------- +// Public entry point +// --------------------------------------------------------------------------- + +Expected SignatureExecutor::Execute(ScoreSignatureHandler& handler_ref, + const common::OperationIdentifier& operationId, + RequestParameters& request) +{ + const auto mode_check = ValidateActorMatchesMode(operationId.operationActor, handler_ref); + if (!mode_check.has_value()) + { + return make_unexpected(mode_check.error()); + } + + const bool is_verify = (operationId.operationActor == actors::OP_ACTOR_VERIFY_HANDLER); + const auto action = operationId.operationAction; + + // SIGN_GET_SIZE and VERIFY_GET_SIZE share the same action value. + if (action == sign_ops::SIGN_GET_SIZE) + { + ResponseParameters response; + response.push_back(static_cast(handler_ref.GetSignatureSize())); + return response; + } + + if (action == sign_ops::SIGN_RESET) + { + auto res = handler_ref.Reset(); + if (!res.has_value()) + { + return make_unexpected(res.error()); + } + return ResponseParameters{}; + } + + if (action == sign_ops::SIGN_SS) + { + if (handler_ref.GetOperationState() == StreamOperationState::STREAM_ACTIVE) + { + return make_unexpected(DaemonErrorCode::kOperationInProgress); + } + auto result = + is_verify ? ExecuteVerifySingleShot(handler_ref, request) : ExecuteSignSingleShot(handler_ref, request); + // A single-shot always ends the stream, so the context is left reusable + // even when the operation failed part-way through. + handler_ref.SetOperationState(StreamOperationState::IDLE); + return result; + } + + // Streaming operations: validate the state machine transition first. + const StreamOperationState currentState = handler_ref.GetOperationState(); + StreamOperationState nextState = StreamOperationState::IDLE; + const auto validation = ValidateStreamTransition(action, currentState, nextState); + if (!validation.has_value()) + { + return make_unexpected(validation.error()); + } + + if (action == sign_ops::SIGN_INIT) + { + auto result = handler_ref.InitSignature(); + if (!result.has_value()) + { + return make_unexpected(result.error()); + } + handler_ref.SetOperationState(nextState); + return ResponseParameters{}; + } + + if (action == sign_ops::SIGN_UPDATE) + { + if (request.empty()) + { + return make_unexpected(DaemonErrorCode::kInsufficientParameters); + } + auto result = handler_ref.UpdateSignature(request[0]); + if (!result.has_value()) + { + return make_unexpected(result.error()); + } + handler_ref.SetOperationState(nextState); + return ResponseParameters{}; + } + + if (action == sign_ops::SIGN_FINALIZE) + { + auto result = + is_verify ? ExecuteVerifyFinalize(handler_ref, request) : ExecuteSignFinalize(handler_ref, request); + if (result.has_value()) + { + handler_ref.SetOperationState(nextState); + } + return result; + } + + return make_unexpected(DaemonErrorCode::kInvalidOperation); +} + +// --------------------------------------------------------------------------- +// Operation implementations +// --------------------------------------------------------------------------- + +Expected SignatureExecutor::ExecuteSignFinalize(ScoreSignatureHandler& handler_ref, + RequestParameters& request) +{ + // request[0] = caller-provided signature output buffer + if (request.empty()) + { + return make_unexpected(DaemonErrorCode::kInsufficientParameters); + } + + const auto outputSpan = CheckAndGetSpan(request[0]); + if (!outputSpan.has_value()) + { + return make_unexpected(outputSpan.error()); + } + + auto written = handler_ref.FinalizeSign(outputSpan.value()); + if (!written.has_value()) + { + return make_unexpected(written.error()); + } + + ResponseParameters response; + response.push_back(static_cast(written.value())); + return response; +} + +Expected SignatureExecutor::ExecuteSignSingleShot( + ScoreSignatureHandler& handler_ref, + RequestParameters& request) +{ + // request[0] = message, request[1] = caller-provided signature output buffer + if (request.size() < 2U) + { + return make_unexpected(DaemonErrorCode::kInsufficientParameters); + } + + const auto outputSpan = CheckAndGetSpan(request[1]); + if (!outputSpan.has_value()) + { + return make_unexpected(outputSpan.error()); + } + + auto init = handler_ref.InitSignature(); + if (!init.has_value()) + { + return make_unexpected(init.error()); + } + + auto update = handler_ref.UpdateSignature(request[0]); + if (!update.has_value()) + { + return make_unexpected(update.error()); + } + + auto written = handler_ref.FinalizeSign(outputSpan.value()); + if (!written.has_value()) + { + return make_unexpected(written.error()); + } + + ResponseParameters response; + response.push_back(static_cast(written.value())); + return response; +} + +Expected SignatureExecutor::ExecuteVerifySingleShot( + ScoreSignatureHandler& handler_ref, + RequestParameters& request) +{ + // request[0] = message, request[1] = signature + if (request.size() < 2U) + { + return make_unexpected(DaemonErrorCode::kInsufficientParameters); + } + + auto init = handler_ref.InitSignature(); + if (!init.has_value()) + { + return make_unexpected(init.error()); + } + + auto update = handler_ref.UpdateSignature(request[0]); + if (!update.has_value()) + { + return make_unexpected(update.error()); + } + + auto verified = handler_ref.FinalizeVerify(request[1]); + if (!verified.has_value()) + { + return make_unexpected(verified.error()); + } + + ResponseParameters response; + response.push_back(verified.value()); + return response; +} + +Expected SignatureExecutor::ExecuteVerifyFinalize( + ScoreSignatureHandler& handler_ref, + RequestParameters& request) +{ + if (request.empty()) + { + return make_unexpected(DaemonErrorCode::kInsufficientParameters); + } + + auto verified = handler_ref.FinalizeVerify(request[0]); + if (!verified.has_value()) + { + return make_unexpected(verified.error()); + } + + ResponseParameters response; + response.push_back(verified.value()); + return response; +} + +// --------------------------------------------------------------------------- +// Validation helpers +// --------------------------------------------------------------------------- + +// static +Expected SignatureExecutor::ValidateActorMatchesMode( + const common::OperationActor actor, + const ScoreSignatureHandler& handler_ref) +{ + const bool wants_verify = (actor == actors::OP_ACTOR_VERIFY_HANDLER); + const bool is_verify_ctx = (handler_ref.GetOperationMode() == score::crypto::OperationMode::kVerify); + + if (actor != actors::OP_ACTOR_SIGN_HANDLER && actor != actors::OP_ACTOR_VERIFY_HANDLER) + { + return make_unexpected(DaemonErrorCode::kInvalidOperation); + } + + // A context bound to the private key must not serve verification requests + // and vice versa — the key half was chosen at CTX_CREATE time. + if (wants_verify != is_verify_ctx) + { + return make_unexpected(DaemonErrorCode::kInvalidOperation); + } + + return std::monostate{}; +} + +// static +Expected SignatureExecutor::ValidateStreamTransition( + const common::OperationAction action, + const StreamOperationState currentState, + StreamOperationState& nextState) +{ + handler::handler_utils::StreamOperation op{}; + if (action == sign_ops::SIGN_INIT) + { + op = handler::handler_utils::StreamOperation::kInit; + } + else if (action == sign_ops::SIGN_UPDATE) + { + op = handler::handler_utils::StreamOperation::kUpdate; + } + else if (action == sign_ops::SIGN_FINALIZE) + { + op = handler::handler_utils::StreamOperation::kFinalize; + } + else + { + return make_unexpected(DaemonErrorCode::kInvalidOperation); + } + + const auto result = handler::handler_utils::ValidateStreamOperationSequence(currentState, op); + if (!result.has_value()) + { + return make_unexpected(result.error()); + } + nextState = result.value(); + return std::monostate{}; +} + +} // namespace score::crypto::daemon::provider::score_provider::operations::signature diff --git a/score/crypto/tests/key_management/test_pkcs11_key_handler.cpp b/score/crypto/tests/key_management/test_pkcs11_key_handler.cpp index 7b503bf5c..587120511 100644 --- a/score/crypto/tests/key_management/test_pkcs11_key_handler.cpp +++ b/score/crypto/tests/key_management/test_pkcs11_key_handler.cpp @@ -117,7 +117,9 @@ class Pkcs11KeyHandlerTest : public tests::softhsm::SofthsmTestFixture auto factory = m_provider->GetCryptoHandlerFactory(); ASSERT_NE(factory, nullptr); - auto handler_result = factory->CreateHandler(pkcs11_ns::kKeyManagementHandlerId, ""); + // HandlerId is a std::string, and the shared context-type ids are + // string_views, which do not convert implicitly. + auto handler_result = factory->CreateHandler(std::string{pkcs11_ns::kKeyManagementHandlerId}, ""); ASSERT_TRUE(handler_result.has_value()) << "CreateHandler failed for key management"; auto handler = handler_result.value(); m_km_handler = std::dynamic_pointer_cast(handler); diff --git a/score/tests/conftest.py b/score/tests/conftest.py index 552bf9660..812d835dd 100644 --- a/score/tests/conftest.py +++ b/score/tests/conftest.py @@ -43,6 +43,12 @@ def pytest_addoption(parser: pytest.Parser): default=False, help="Enable PKCS#11-dependent integration-test setup.", ) + parser.addoption( + "--openssl-backend-enabled", + action="store_true", + default=False, + help="Enable OpenSSL-dependent integration tests.", + ) def _absolute_path(rel_path: Path) -> Path: @@ -66,6 +72,17 @@ def _absolute_path(rel_path: Path) -> Path: ) +@pytest.fixture +def openssl_backend(request: pytest.FixtureRequest) -> None: + """Skip a test that only the OpenSSL provider can serve. + + Cipher, random generation and ECDSA exist in no other provider, so a daemon + built without the OpenSSL backend fails every such case at context creation. + """ + if not request.config.getoption("--openssl-backend-enabled"): + pytest.skip("requires a daemon built with the OpenSSL backend") + + @pytest.fixture(scope="session") def target_os(target: Target) -> str: """Return the OS name of the target, as reported by `uname -s`.""" diff --git a/score/tests/integration_tests/BUILD b/score/tests/integration_tests/BUILD index b3bd3dbbb..7034c509f 100644 --- a/score/tests/integration_tests/BUILD +++ b/score/tests/integration_tests/BUILD @@ -82,6 +82,84 @@ cc_binary( ], ) +cc_binary( + name = "score_api_cipher_test", + srcs = ["score_api_cipher_test.cpp"], + data = [ + "//score/tests/test_vectors/block_cipher/CBC-AES128:block_cipher_test_vectors", + "//score/tests/test_vectors/block_cipher/CBC-AES192:block_cipher_test_vectors", + "//score/tests/test_vectors/block_cipher/CBC-AES256:block_cipher_test_vectors", + "//score/tests/test_vectors/config:integration_test_config", + ], + dynamic_deps = ["//third_party/grpc:libgrpc_shared"], + target_compatible_with = select({ + "//score/crypto/src/backend:openssl_backend_active": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + "//score/crypto/src/api:crypto_stack", + "//score/tests/utility", + "@googletest//:gtest", + ], +) + +cc_binary( + name = "score_api_random_test", + srcs = ["score_api_random_test.cpp"], + data = [ + "//score/tests/test_vectors/config:integration_test_config", + ], + dynamic_deps = ["//third_party/grpc:libgrpc_shared"], + target_compatible_with = select({ + "//score/crypto/src/backend:openssl_backend_active": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + "//score/crypto/src/api:crypto_stack", + "//score/tests/utility", + "@googletest//:gtest", + ], +) + +cc_binary( + name = "score_api_ecdsa_test", + srcs = ["score_api_ecdsa_test.cpp"], + data = [ + "//score/tests/test_vectors/config:integration_test_config", + "//score/tests/test_vectors/ecdsa:ecdsa_test_vectors", + ], + dynamic_deps = ["//third_party/grpc:libgrpc_shared"], + target_compatible_with = select({ + "//score/crypto/src/backend:openssl_backend_active": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + "//score/crypto/src/api:crypto_stack", + "//score/tests/utility", + "@googletest//:gtest", + ], +) + +cc_binary( + name = "score_api_key_permissions_test", + srcs = ["score_api_key_permissions_test.cpp"], + data = [ + "//score/tests/test_vectors/config:integration_test_config", + ], + defines = select({ + "//score/crypto/src/backend:pkcs11_backend_enabled": ["SCORE_CRYPTO_HARDWARE_BACKEND_ENABLED=1"], + "//conditions:default": [], + }) + select({ + "//score/crypto/src/backend:openssl_backend_active": ["SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED=1"], + "//conditions:default": [], + }), + dynamic_deps = ["//third_party/grpc:libgrpc_shared"], + deps = [ + "//score/crypto/src/api:crypto_stack", + "@googletest//:gtest", + ], +) + cc_binary( name = "score_demo", srcs = ["score_demo.cpp"], @@ -110,12 +188,20 @@ integration_test( binaries = [ ":score_api_hash_test", ":score_api_mac_test", + ":score_api_key_permissions_test", ":hash_performance_test", ":score_demo", "//score/crypto/src/daemon:crypto_daemon", ] + select({ "//score/crypto/src/backend:pkcs11_backend_enabled": [":init_pkcs11_token"], "//conditions:default": [], + }) + select({ + "//score/crypto/src/backend:openssl_backend_active": [ + ":score_api_cipher_test", + ":score_api_random_test", + ":score_api_ecdsa_test", + ], + "//conditions:default": [], }), configs = [ "//score/tests/config:logging.json", @@ -124,11 +210,23 @@ integration_test( extra_args = select({ "//score/crypto/src/backend:pkcs11_backend_enabled": ["--pkcs11-backend-enabled"], "//conditions:default": [], + }) + select({ + "//score/crypto/src/backend:openssl_backend_active": ["--openssl-backend-enabled"], + "//conditions:default": [], }), file_pkgs = [ "//score/tests/test_vectors/hash:hash_test_vectors_pkg", "//score/tests/test_vectors/mac:mac_test_vectors_pkg", - ], + ] + select({ + # Read by the cipher and ECDSA tests, which need the OpenSSL provider. + "//score/crypto/src/backend:openssl_backend_active": [ + "//score/tests/test_vectors/block_cipher/CBC-AES128:block_cipher_test_vectors_pkg", + "//score/tests/test_vectors/block_cipher/CBC-AES192:block_cipher_test_vectors_pkg", + "//score/tests/test_vectors/block_cipher/CBC-AES256:block_cipher_test_vectors_pkg", + "//score/tests/test_vectors/ecdsa:ecdsa_test_vectors_pkg", + ], + "//conditions:default": [], + }), libraries = [ "//score/crypto/src/api/control_plane", "//third_party/grpc:libgrpc_shared", diff --git a/score/tests/integration_tests/integration_test.py b/score/tests/integration_tests/integration_test.py index 4afc1f7cb..75870ba0a 100644 --- a/score/tests/integration_tests/integration_test.py +++ b/score/tests/integration_tests/integration_test.py @@ -162,6 +162,60 @@ def test_score_api_mac(self, target: Target, target_os: str, install_dir: str): }, ) + def test_score_api_cipher( + self, target: Target, target_os: str, install_dir: str, openssl_backend: None + ): + """Test SCORE symmetric encryption / decryption API.""" + run_test_app( + target, + target_os, + Path(f"{install_dir}/bin/score_api_cipher_test"), + env={ + "LD_LIBRARY_PATH": f"{install_dir}/lib", + "TEST_VECTORS_DIR": f"{install_dir}/share/test_vectors", + }, + ) + + def test_score_api_random( + self, target: Target, target_os: str, install_dir: str, openssl_backend: None + ): + """Test SCORE random number generation API.""" + run_test_app( + target, + target_os, + Path(f"{install_dir}/bin/score_api_random_test"), + env={ + "LD_LIBRARY_PATH": f"{install_dir}/lib", + "TEST_VECTORS_DIR": f"{install_dir}/share/test_vectors", + }, + ) + + def test_score_api_ecdsa( + self, target: Target, target_os: str, install_dir: str, openssl_backend: None + ): + """Test SCORE ECDSA key generation, signing and verification API.""" + run_test_app( + target, + target_os, + Path(f"{install_dir}/bin/score_api_ecdsa_test"), + env={ + "LD_LIBRARY_PATH": f"{install_dir}/lib", + "TEST_VECTORS_DIR": f"{install_dir}/share/test_vectors", + }, + ) + + def test_score_api_key_permissions(self, target: Target, target_os: str, install_dir: str): + """Test SCORE key operation permission enforcement at context creation.""" + run_test_app( + target, + target_os, + Path(f"{install_dir}/bin/score_api_key_permissions_test"), + env={ + "LD_LIBRARY_PATH": f"{install_dir}/lib", + "TEST_VECTORS_DIR": f"{install_dir}/share/test_vectors", + }, + ) + def test_hash_performance_test(self, target: Target, target_os: str, install_dir: str): """Test concurrent and sequential hash operations.""" run_test_app( diff --git a/score/tests/integration_tests/score_api_cipher_test.cpp b/score/tests/integration_tests/score_api_cipher_test.cpp new file mode 100644 index 000000000..e09af032f --- /dev/null +++ b/score/tests/integration_tests/score_api_cipher_test.cpp @@ -0,0 +1,676 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/// @file score_api_cipher_example.cpp +/// @brief Demonstrates symmetric encryption and decryption using the score::crypto API. +/// +/// Shows: +/// - AES key generation via IKeyManagementContext::GenerateKey (ephemeral path) +/// - Key loading from a named key slot via ResolveResource + LoadKey +/// - Streaming encryption (Init → Update* → Finalize) and decryption +/// - Single-shot encryption / decryption via SingleShot() +/// - Context reuse via Reset() +/// - That a wrong key or tampered ciphertext does not yield the plaintext +/// - Agreement with the NIST CAVP AES-CBC vectors under the vectors' own key +/// +/// Both key sources are exercised, and each establishes what it can: +/// +/// * With a generated key the ciphertext is unpredictable, so correctness is +/// established by round-tripping — decrypt(encrypt(m)) == m — over the NIST +/// plaintexts, plus the usual tamper checks. +/// * With the NIST key loaded from a key slot the expected ciphertext is +/// known, so the test is a real known-answer test against CAVP. +/// +/// @note CAVP ciphertexts carry no padding while these contexts always apply +/// PKCS#7, so an encryption produces the vector's ciphertext followed by +/// one extra padding block. See the reference.md beside each vector set. + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/config/cipher_context_config.hpp" +#include "score/crypto/src/api/config/key_management_context_config.hpp" +#include "score/crypto/src/api/config/key_operation_params.hpp" +#include "score/crypto/src/api/config/random_context_config.hpp" +#include "score/crypto/src/api/contexts/i_cipher_context.hpp" +#include "score/crypto/src/api/contexts/i_key_management_context.hpp" +#include "score/crypto/src/api/contexts/i_random_context.hpp" +#include "score/crypto/src/api/crypto_stack_factory.hpp" +#include "score/crypto/src/api/i_crypto_context.hpp" +#include "score/crypto/src/api/i_crypto_stack.hpp" +#include "score/tests/utility/test_utility.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace score::crypto; +using tests::utility::print_hex; +using tests::utility::read_bin; + +namespace +{ + +#ifdef __QNXNTO__ +constexpr auto kControlSocketEndpoint = "unix:///opt/crypto_daemon.sock"; +#else +constexpr auto kControlSocketEndpoint = "unix:///tmp/crypto_daemon.sock"; +#endif + +// ========================================================================= +// NIST CAVP AES-CBC vectors +// ========================================================================= + +/// Resolves a test-vector path against the deployment layout. +/// +/// The ITF harness deploys the vectors under the install directory and points +/// TEST_VECTORS_DIR at them; the fallback is where the harness places them by +/// default, so the binary is still runnable by hand on a deployed target. +std::string GetTestVectorPath(const std::string_view relative_path) +{ + const char* dir = std::getenv("TEST_VECTORS_DIR"); + return std::string{dir != nullptr ? dir : "/opt/crypto/share/test_vectors"} + std::string{relative_path}; +} + +/// One CAVS multi-block message test record: key, IV, plaintext and the +/// unpadded ciphertext CAVP expects for them. +struct NistCbcVector +{ + std::vector key; + std::vector iv; + std::vector plaintext; + std::vector ciphertext; +}; + +/// Reads one vector of a mode directory, e.g. ("CBC-AES128", "vector2"). +/// Records a gtest failure and returns false when a file is missing, which means +/// the vectors were not deployed. +bool LoadNistCbcVector(const std::string& mode, const std::string& prefix, NistCbcVector& out) +{ + const std::string base = GetTestVectorPath("/block_cipher/") + mode + "/" + prefix + "_"; + + out.key = read_bin(base + "key.bin"); + out.iv = read_bin(base + "iv.bin"); + out.plaintext = read_bin(base + "plaintext.bin"); + out.ciphertext = read_bin(base + "ciphertext.bin"); + + EXPECT_FALSE(out.key.empty()) << "missing " << base << "key.bin"; + EXPECT_FALSE(out.iv.empty()) << "missing " << base << "iv.bin"; + EXPECT_FALSE(out.plaintext.empty()) << "missing " << base << "plaintext.bin"; + EXPECT_FALSE(out.ciphertext.empty()) << "missing " << base << "ciphertext.bin"; + EXPECT_EQ(out.plaintext.size(), out.ciphertext.size()) << "CAVP records are unpadded, so the sizes must match"; + + return !out.plaintext.empty() && (out.plaintext.size() == out.ciphertext.size()); +} + +// ========================================================================= +// Parameterized Test Data +// ========================================================================= + +/// @brief Parameters for cipher tests using a generated (ephemeral) key. +/// +/// The key is random per run, so the ciphertext cannot be compared against the +/// vector — only the round trip and the tamper behaviour can. +struct CipherTestData +{ + std::string test_case_name; + std::optional provider_type; + std::string cipher_algorithm; ///< e.g. "AES-256-CBC" + std::string key_algorithm; ///< e.g. "AES-256-CBC" + std::size_t iv_size; ///< 16 for CBC/CTR, 0 for ECB + bool block_padded; ///< true when Finalize() emits a padding block + std::string vector_dir; ///< CAVP directory the messages come from +}; + +/// @brief Parameters for cipher tests using the CAVP key loaded from a slot. +/// +/// With the vector's own key the expected ciphertext is known, so these cases +/// are known-answer tests rather than round-trip tests. +struct KeySlotCipherTestData +{ + std::string test_case_name; + std::optional provider_type; + std::string cipher_algorithm; + std::string vector_dir; + std::string vector_prefix; ///< which vector of that directory the slot holds + std::string key_slot_name; +}; + +// ========================================================================= +// Helpers +// ========================================================================= + +/// Creates a cipher context for one direction over the given key. +std::unique_ptr MakeCipherContext(ICryptoContext& ctx, + const std::string& algorithm, + const std::optional& provider_type, + const CryptoResourceGuard& key, + CipherDirection direction) +{ + CipherContextConfig config; + config.SetAlgorithm(algorithm).SetKey(key).SetDirection(direction); + if (provider_type.has_value()) + { + config.SetProviderType(provider_type.value()); + } + + auto result = ctx.CreateCipherContext(config); + EXPECT_TRUE(result.has_value()) << "Failed to create cipher context for " << algorithm; + if (!result.has_value()) + { + return nullptr; + } + return std::move(result.value()); +} + +/// Streaming transform: Init(iv) → Update(chunk1) → Update(chunk2) → Finalize. +/// Returns the concatenation of everything the context produced. +void StreamingTransform(ICipherContext& cipher, + const std::vector& iv, + const std::vector& input, + std::size_t block_size, + std::vector& output) +{ + output.clear(); + + // IV-less modes (ECB) must pass std::nullopt rather than an empty span. + std::optional> iv_arg{}; + if (!iv.empty()) + { + iv_arg = score::cpp::span{iv.data(), iv.size()}; + } + ASSERT_TRUE(cipher.Init(iv_arg)) << "Cipher Init failed"; + + // Split mid-message rather than on a block boundary: that leaves a partial + // block for the cipher to buffer across the two Update calls, which is the + // interesting path. Both chunks must be non-empty — an Update with no data + // is not a meaningful request and the daemon rejects it. + ASSERT_GE(input.size(), 2U) << "Test input must be at least two bytes"; + const auto split = static_cast(input.size() / 2U); + const std::vector chunk1(input.begin(), input.begin() + split); + const std::vector chunk2(input.begin() + split, input.end()); + + // Worst case one extra block per Update plus one on Finalize. + std::vector scratch(input.size() + (2U * block_size)); + + auto n1 = cipher.Update({chunk1.data(), chunk1.size()}, {scratch.data(), scratch.size()}); + ASSERT_TRUE(n1.has_value()) << "Cipher Update (chunk 1) failed"; + output.insert(output.end(), scratch.begin(), scratch.begin() + static_cast(n1.value())); + + auto n2 = cipher.Update({chunk2.data(), chunk2.size()}, {scratch.data(), scratch.size()}); + ASSERT_TRUE(n2.has_value()) << "Cipher Update (chunk 2) failed"; + output.insert(output.end(), scratch.begin(), scratch.begin() + static_cast(n2.value())); + + auto n3 = cipher.Finalize({scratch.data(), scratch.size()}); + ASSERT_TRUE(n3.has_value()) << "Cipher Finalize failed"; + output.insert(output.end(), scratch.begin(), scratch.begin() + static_cast(n3.value())); +} + +/// Reset → SingleShot(iv, input) and return the produced bytes. +void SingleShotTransform(ICipherContext& cipher, + const std::vector& iv, + const std::vector& input, + std::size_t block_size, + std::vector& output) +{ + ASSERT_TRUE(cipher.Reset()) << "Reset before SingleShot failed"; + + std::vector scratch(input.size() + (2U * block_size)); + auto n = cipher.SingleShot({iv.data(), iv.size()}, {input.data(), input.size()}, {scratch.data(), scratch.size()}); + ASSERT_TRUE(n.has_value()) << "Cipher SingleShot failed"; + + output.assign(scratch.begin(), scratch.begin() + static_cast(n.value())); +} + +/// The leading bytes of a PKCS#7-padded ciphertext are the unpadded CAVP +/// ciphertext; the trailing block is the padding this stack adds. +void ExpectMatchesNistCiphertext(const std::vector& produced, const NistCbcVector& vector) +{ + ASSERT_GE(produced.size(), vector.ciphertext.size()) << "Ciphertext is shorter than the CAVP vector"; + + const std::vector prefix(produced.begin(), + produced.begin() + static_cast(vector.ciphertext.size())); + print_hex("Produced", prefix, prefix.size()); + print_hex("CAVP ", vector.ciphertext, vector.ciphertext.size()); + EXPECT_EQ(prefix, vector.ciphertext) << "Ciphertext does not match the NIST CAVP vector"; +} + +/// Explicit key release and assertion. +void ReleaseAndAssertKey(CryptoResourceGuard& key) +{ + auto release_result = key.Release(); + ASSERT_TRUE(release_result.has_value()) << "Key release failed"; + EXPECT_FALSE(key.IsActive()) << "Key should be inactive after Release"; +} + +// ========================================================================= +// Test 1: encrypt / decrypt round trip with a generated key +// ========================================================================= + +class CipherRoundTripTest : public ::testing::TestWithParam +{ +}; + +TEST_P(CipherRoundTripTest, EncryptDecryptRoundTrip) +{ + const auto test_data = GetParam(); + + // The messages come from the CAVP records of this mode: a single block and a + // four-block message. Their content does not matter under a random key, but + // taking them from the vectors keeps both tests on the same inputs. + NistCbcVector short_vector; + NistCbcVector long_vector; + ASSERT_TRUE(LoadNistCbcVector(test_data.vector_dir, "vector1", short_vector)); + ASSERT_TRUE(LoadNistCbcVector(test_data.vector_dir, "vector2", long_vector)); + + const auto& plaintext = long_vector.plaintext; + const auto& plaintext_alt = short_vector.plaintext; + + // ========================================================================= + // 1. Create the crypto stack and a crypto context + // ========================================================================= + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()) << "Failed to create crypto stack"; + auto& stack = stack_result.value(); + + auto ctx_result = stack->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()) << "Failed to create crypto context"; + auto& ctx = ctx_result.value(); + + // ========================================================================= + // 2. Generate an ephemeral AES key permitted to encrypt and decrypt + // ========================================================================= + KeyManagementContextConfig key_mgmt_config; + if (test_data.provider_type.has_value()) + { + key_mgmt_config.SetProviderType(test_data.provider_type.value()); + } + + auto key_mgmt_result = ctx->CreateKeyManagementContext(key_mgmt_config); + ASSERT_TRUE(key_mgmt_result.has_value()) << "Failed to create key management context"; + auto& key_mgmt = key_mgmt_result.value(); + + GenerateKeyParams key_gen_params; + key_gen_params.SetAlgorithm(test_data.key_algorithm) + .SetPermissions(KeyOperationPermission::kEncrypt | KeyOperationPermission::kDecrypt); + + auto key_result = key_mgmt->GenerateKey(key_gen_params); + ASSERT_TRUE(key_result.has_value()) << "Failed to generate cipher key"; + auto key = std::move(key_result.value()); + ASSERT_TRUE(key.IsActive()) << "Key guard should be active after generation"; + + // ========================================================================= + // 3. Obtain a random IV of the length the algorithm requires + // ========================================================================= + std::vector iv(test_data.iv_size, 0U); + if (test_data.iv_size > 0U) + { + RandomContextConfig random_config; + if (test_data.provider_type.has_value()) + { + random_config.SetProviderType(test_data.provider_type.value()); + } + auto random_result = ctx->CreateRandomContext(random_config); + ASSERT_TRUE(random_result.has_value()) << "Failed to create random context"; + + auto generated = random_result.value()->Generate({iv.data(), iv.size()}); + ASSERT_TRUE(generated.has_value()) << "Failed to generate IV"; + ASSERT_EQ(generated.value(), test_data.iv_size); + print_hex("IV", iv, iv.size()); + } + + // ========================================================================= + // 4. Streaming encryption + // ========================================================================= + auto encrypt_ctx = + MakeCipherContext(*ctx, test_data.cipher_algorithm, test_data.provider_type, key, CipherDirection::kEncrypt); + ASSERT_NE(encrypt_ctx, nullptr); + + const std::size_t block_size = encrypt_ctx->GetOutputSize(); + ASSERT_GT(block_size, 0U) << "Cipher block size query failed"; + + std::vector ciphertext; + ASSERT_NO_FATAL_FAILURE(StreamingTransform(*encrypt_ctx, iv, plaintext, block_size, ciphertext)); + print_hex("Ciphertext", ciphertext, ciphertext.size()); + + ASSERT_FALSE(ciphertext.empty()); + EXPECT_NE(ciphertext, plaintext) << "Ciphertext must not equal plaintext"; + + // A padded block mode grows the message; a stream mode keeps it the same size. + if (test_data.block_padded) + { + EXPECT_GT(ciphertext.size(), plaintext.size()) << "Padded mode should append a padding block"; + EXPECT_EQ(ciphertext.size() % block_size, 0U) << "Padded ciphertext must be a whole number of blocks"; + } + else + { + EXPECT_EQ(ciphertext.size(), plaintext.size()) << "Stream mode must preserve the message length"; + } + + // Under a random key the CAVP ciphertext must not appear — this is the + // control for the known-answer test below, which uses the CAVP key. + if (ciphertext.size() >= long_vector.ciphertext.size()) + { + const std::vector prefix( + ciphertext.begin(), ciphertext.begin() + static_cast(long_vector.ciphertext.size())); + EXPECT_NE(prefix, long_vector.ciphertext) << "A random key reproduced the CAVP ciphertext"; + } + + // ========================================================================= + // 5. Streaming decryption recovers the plaintext + // ========================================================================= + auto decrypt_ctx = + MakeCipherContext(*ctx, test_data.cipher_algorithm, test_data.provider_type, key, CipherDirection::kDecrypt); + ASSERT_NE(decrypt_ctx, nullptr); + + std::vector recovered; + ASSERT_NO_FATAL_FAILURE(StreamingTransform(*decrypt_ctx, iv, ciphertext, block_size, recovered)); + EXPECT_EQ(recovered, plaintext) << "Decryption did not recover the original plaintext"; + + // ========================================================================= + // 6. Single-shot encryption produces the same ciphertext as streaming + // ========================================================================= + std::vector ciphertext_ss; + ASSERT_NO_FATAL_FAILURE(SingleShotTransform(*encrypt_ctx, iv, plaintext, block_size, ciphertext_ss)); + EXPECT_EQ(ciphertext_ss, ciphertext) << "SingleShot and streaming encryption must agree"; + + // ========================================================================= + // 7. Single-shot decryption round trip + // ========================================================================= + std::vector recovered_ss; + ASSERT_NO_FATAL_FAILURE(SingleShotTransform(*decrypt_ctx, iv, ciphertext_ss, block_size, recovered_ss)); + EXPECT_EQ(recovered_ss, plaintext) << "SingleShot decryption did not recover the plaintext"; + + // ========================================================================= + // 8. Context reuse via Reset() with a different message + // ========================================================================= + std::vector ciphertext_alt; + ASSERT_NO_FATAL_FAILURE(SingleShotTransform(*encrypt_ctx, iv, plaintext_alt, block_size, ciphertext_alt)); + EXPECT_NE(ciphertext_alt, ciphertext) << "A different message must produce different ciphertext"; + + std::vector recovered_alt; + ASSERT_NO_FATAL_FAILURE(SingleShotTransform(*decrypt_ctx, iv, ciphertext_alt, block_size, recovered_alt)); + EXPECT_EQ(recovered_alt, plaintext_alt) << "Round trip after Reset failed"; + + // ========================================================================= + // 9. A different IV yields different ciphertext for the same message + // ========================================================================= + if (test_data.iv_size > 0U) + { + std::vector other_iv = iv; + other_iv[0] ^= 0xFFU; + + std::vector ciphertext_other_iv; + ASSERT_NO_FATAL_FAILURE( + SingleShotTransform(*encrypt_ctx, other_iv, plaintext, block_size, ciphertext_other_iv)); + EXPECT_NE(ciphertext_other_iv, ciphertext) << "Changing the IV must change the ciphertext"; + } + + // ========================================================================= + // 10. Tampered ciphertext must not decrypt back to the plaintext + // ========================================================================= + // + // A padded mode detects the tampering and fails outright; a stream mode has + // no integrity check and simply yields different bytes. Both outcomes are + // acceptable — what must never happen is recovering the original message. + std::vector tampered = ciphertext; + tampered[0] ^= 0xFFU; + + ASSERT_TRUE(decrypt_ctx->Reset()) << "Reset before tampered decrypt failed"; + std::vector tampered_out(tampered.size() + (2U * block_size)); + auto tampered_result = decrypt_ctx->SingleShot( + {iv.data(), iv.size()}, {tampered.data(), tampered.size()}, {tampered_out.data(), tampered_out.size()}); + + if (tampered_result.has_value()) + { + tampered_out.resize(tampered_result.value()); + EXPECT_NE(tampered_out, plaintext) << "Tampered ciphertext must not decrypt to the original plaintext"; + } + + // ========================================================================= + // 11. Explicit key release + // ========================================================================= + ASSERT_NO_FATAL_FAILURE(ReleaseAndAssertKey(key)); +} + +// ========================================================================= +// Test 2: known-answer test with the CAVP key loaded from a key slot +// ========================================================================= + +class KeySlotCipherTest : public ::testing::TestWithParam +{ +}; + +TEST_P(KeySlotCipherTest, MatchesNistVector) +{ + const auto test_data = GetParam(); + + NistCbcVector vector; + ASSERT_TRUE(LoadNistCbcVector(test_data.vector_dir, test_data.vector_prefix, vector)); + + // ========================================================================= + // 1. Create the crypto stack and a crypto context + // ========================================================================= + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()) << "Failed to create crypto stack"; + auto& stack = stack_result.value(); + + auto ctx_result = stack->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()) << "Failed to create crypto context"; + auto& ctx = ctx_result.value(); + + // ========================================================================= + // 2. Load the CAVP key from its pre-provisioned key slot + // ========================================================================= + // + // The slot's deployment descriptor points at the vector's own key file, so + // the daemon encrypts under exactly the key NIST used — which is what makes + // the expected ciphertext below reproducible. The application never sees the + // key material. + KeyManagementContextConfig key_mgmt_config; + if (test_data.provider_type.has_value()) + { + key_mgmt_config.SetProviderType(test_data.provider_type.value()); + } + + auto key_mgmt_result = ctx->CreateKeyManagementContext(key_mgmt_config); + ASSERT_TRUE(key_mgmt_result.has_value()) << "Failed to create key management context"; + auto& key_mgmt = key_mgmt_result.value(); + + auto slot_result = ctx->ResolveResource(test_data.key_slot_name, ResourceType::kKeySlot); + ASSERT_TRUE(slot_result.has_value()) << "Failed to resolve key slot: " << test_data.key_slot_name; + + auto key_result = key_mgmt->LoadKey(slot_result.value()); + ASSERT_TRUE(key_result.has_value()) << "Failed to load key from slot: " << test_data.key_slot_name; + auto key = std::move(key_result.value()); + ASSERT_TRUE(key.IsActive()) << "Key guard should be active after loading from slot"; + + // ========================================================================= + // 3. Encrypt the CAVP plaintext under the CAVP IV + // ========================================================================= + auto encrypt_ctx = + MakeCipherContext(*ctx, test_data.cipher_algorithm, test_data.provider_type, key, CipherDirection::kEncrypt); + ASSERT_NE(encrypt_ctx, nullptr); + + const std::size_t block_size = encrypt_ctx->GetOutputSize(); + ASSERT_GT(block_size, 0U) << "Cipher block size query failed"; + + std::vector ciphertext; + ASSERT_NO_FATAL_FAILURE(SingleShotTransform(*encrypt_ctx, vector.iv, vector.plaintext, block_size, ciphertext)); + + // PKCS#7 appends a full block to a whole-block message, so the produced + // ciphertext is the vector's followed by one more block. + EXPECT_EQ(ciphertext.size(), vector.ciphertext.size() + block_size) + << "Padded ciphertext should be one block longer than the CAVP vector"; + ASSERT_NO_FATAL_FAILURE(ExpectMatchesNistCiphertext(ciphertext, vector)); + + // ========================================================================= + // 4. Streaming encryption agrees with the vector too + // ========================================================================= + // + // Streaming buffers a partial block across the two Update calls, so it is a + // different code path to SingleShot and worth checking against the vector + // rather than only against SingleShot's own output. + std::vector ciphertext_streamed; + ASSERT_TRUE(encrypt_ctx->Reset()) << "Reset before streaming encryption failed"; + ASSERT_NO_FATAL_FAILURE( + StreamingTransform(*encrypt_ctx, vector.iv, vector.plaintext, block_size, ciphertext_streamed)); + EXPECT_EQ(ciphertext_streamed, ciphertext) << "Streaming and single-shot encryption must agree"; + + // ========================================================================= + // 5. Decryption recovers the CAVP plaintext + // ========================================================================= + auto decrypt_ctx = + MakeCipherContext(*ctx, test_data.cipher_algorithm, test_data.provider_type, key, CipherDirection::kDecrypt); + ASSERT_NE(decrypt_ctx, nullptr); + + std::vector recovered; + ASSERT_NO_FATAL_FAILURE(SingleShotTransform(*decrypt_ctx, vector.iv, ciphertext, block_size, recovered)); + EXPECT_EQ(recovered, vector.plaintext) << "Decryption did not recover the CAVP plaintext"; + + // ========================================================================= + // 6. The unpadded CAVP ciphertext on its own does not decrypt + // ========================================================================= + // + // Documented consequence of always-on PKCS#7: the vector's ciphertext has no + // padding block, so the padding check has nothing valid to strip. Whether + // that surfaces as an error or as different bytes, the plaintext must not + // come back — feeding CAVP ciphertext straight to a decrypt context is a + // usage error, not a supported path. + { + ASSERT_TRUE(decrypt_ctx->Reset()) << "Reset before unpadded decrypt failed"; + std::vector out(vector.ciphertext.size() + (2U * block_size)); + auto result = decrypt_ctx->SingleShot({vector.iv.data(), vector.iv.size()}, + {vector.ciphertext.data(), vector.ciphertext.size()}, + {out.data(), out.size()}); + if (result.has_value()) + { + out.resize(result.value()); + EXPECT_NE(out, vector.plaintext) << "Unpadded CAVP ciphertext must not decrypt cleanly"; + } + } + + // ========================================================================= + // 7. A different IV breaks agreement with the vector + // ========================================================================= + // + // Confirms the IV really reached the cipher rather than the match in step 3 + // coming from somewhere else. + { + std::vector other_iv = vector.iv; + other_iv[0] ^= 0xFFU; + + std::vector ciphertext_other_iv; + ASSERT_NO_FATAL_FAILURE( + SingleShotTransform(*encrypt_ctx, other_iv, vector.plaintext, block_size, ciphertext_other_iv)); + EXPECT_NE(ciphertext_other_iv, ciphertext) << "Changing the IV must change the ciphertext"; + } + + // ========================================================================= + // 8. Explicit key release + // ========================================================================= + ASSERT_NO_FATAL_FAILURE(ReleaseAndAssertKey(key)); +} + +// ========================================================================= +// Test Vector Constants +// ========================================================================= + +constexpr std::size_t kAesIvSize = 16U; + +// The slot holding each mode's CAVP key. All are OpenSSL slots: the key material +// is a file the daemon imports, which is what FileBackedSlotHandler does. +const std::string kAes128CbcKeySlot = "AES128_CBC_IntegrationTestKey_OpenSSL"; +const std::string kAes192CbcKeySlot = "AES192_CBC_IntegrationTestKey_OpenSSL"; +const std::string kAes256CbcKeySlot = "AES256_CBC_IntegrationTestKey_OpenSSL"; + +/// The slots are provisioned with the four-block vector's key. +const std::string kKeySlotVector = "vector2"; + +// ========================================================================= +// Test Suites +// ========================================================================= + +// Symmetric ciphers are provided only by the OpenSSL (software) provider, so +// every case pins ProviderType::kSoftware. Leaving the provider unset would +// resolve to the daemon's DEFAULT provider — currently SoftHSM — which offers +// no cipher handler. +INSTANTIATE_TEST_SUITE_P(CbcOnSoftwareProvider, + CipherRoundTripTest, + ::testing::Values(CipherTestData{"AES256_CBC_SoftwareProvider", + ProviderType::kSoftware, + "AES-256-CBC", + "AES-256-CBC", + kAesIvSize, + true, + "CBC-AES256"}, + CipherTestData{"AES192_CBC_SoftwareProvider", + ProviderType::kSoftware, + "AES-192-CBC", + "AES-192-CBC", + kAesIvSize, + true, + "CBC-AES192"}, + CipherTestData{"AES128_CBC_SoftwareProvider", + ProviderType::kSoftware, + "AES-128-CBC", + "AES-128-CBC", + kAesIvSize, + true, + "CBC-AES128"}), + [](const testing::TestParamInfo& info) { + return info.param.test_case_name; + }); + +INSTANTIATE_TEST_SUITE_P(NistVectorsOnSoftwareProvider, + KeySlotCipherTest, + ::testing::Values(KeySlotCipherTestData{"AES256_CBC_KeySlot_SoftwareProvider", + ProviderType::kSoftware, + "AES-256-CBC", + "CBC-AES256", + kKeySlotVector, + kAes256CbcKeySlot}, + KeySlotCipherTestData{"AES192_CBC_KeySlot_SoftwareProvider", + ProviderType::kSoftware, + "AES-192-CBC", + "CBC-AES192", + kKeySlotVector, + kAes192CbcKeySlot}, + KeySlotCipherTestData{"AES128_CBC_KeySlot_SoftwareProvider", + ProviderType::kSoftware, + "AES-128-CBC", + "CBC-AES128", + kKeySlotVector, + kAes128CbcKeySlot}), + [](const testing::TestParamInfo& info) { + return info.param.test_case_name; + }); + +} // namespace + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/score/tests/integration_tests/score_api_ecdsa_test.cpp b/score/tests/integration_tests/score_api_ecdsa_test.cpp new file mode 100644 index 000000000..6bb8f7f79 --- /dev/null +++ b/score/tests/integration_tests/score_api_ecdsa_test.cpp @@ -0,0 +1,696 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/// @file score_api_ecdsa_example.cpp +/// @brief Demonstrates ECDSA key generation, signing and verification using the +/// score::crypto API. +/// +/// Shows: +/// - Asymmetric key-pair generation on P-256, P-384 and P-521 via +/// IKeyManagementContext::GenerateKey (ephemeral path) +/// - Key loading from a named key slot via ResolveResource + LoadKey, for both +/// a NIST key pair and a public-key-only slot +/// - Streaming signature generation (Init → Update* → SignFinalize) +/// - Streaming verification (Init → Update* → VerifyFinalize) +/// - Single-shot signing and verification +/// - Rejection of a tampered signature and of a tampered message +/// - Verification of the NIST FIPS 186-4 CAVP signatures, including a record +/// NIST authored to be rejected +/// - Context reuse via Reset() +/// +/// One generated key resource represents the whole pair: a sign context binds +/// its private half, a verify context its public half. +/// +/// @par What the NIST vectors establish here +/// +/// ECDSA is randomised, so re-signing a CAVP message does not reproduce the +/// CAVP signature — the per-message secret `k` is not settable through this API, +/// and must not be. The vectors are therefore used on the verification side: a +/// `SigGen` record's published signature must verify under its published key, +/// and a `SigVer` record marked `Result = F` must not. Signing is checked by +/// verifying what was just signed. +/// +/// @note NIST's largest prime curve is P-521 (not P-512), so that is the curve +/// used for the highest-strength cases here. + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/config/key_management_context_config.hpp" +#include "score/crypto/src/api/config/key_operation_params.hpp" +#include "score/crypto/src/api/config/sign_context_config.hpp" +#include "score/crypto/src/api/config/verify_signature_context_config.hpp" +#include "score/crypto/src/api/contexts/i_key_management_context.hpp" +#include "score/crypto/src/api/contexts/i_sign_context.hpp" +#include "score/crypto/src/api/contexts/i_verify_signature_context.hpp" +#include "score/crypto/src/api/crypto_stack_factory.hpp" +#include "score/crypto/src/api/i_crypto_context.hpp" +#include "score/crypto/src/api/i_crypto_stack.hpp" +#include "score/result/result.h" +#include "score/tests/utility/test_utility.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace score::crypto; +using tests::utility::print_hex; +using tests::utility::read_bin; + +namespace +{ + +#ifdef __QNXNTO__ +constexpr auto kControlSocketEndpoint = "unix:///opt/crypto_daemon.sock"; +#else +constexpr auto kControlSocketEndpoint = "unix:///tmp/crypto_daemon.sock"; +#endif + +// ========================================================================= +// NIST FIPS 186-4 CAVP vectors +// ========================================================================= + +/// Resolves a test-vector path against the deployment layout. +/// +/// The ITF harness deploys the vectors under the install directory and points +/// TEST_VECTORS_DIR at them; the fallback is where the harness places them by +/// default, so the binary is still runnable by hand on a deployed target. +std::string GetTestVectorPath(const std::string_view relative_path) +{ + const char* dir = std::getenv("TEST_VECTORS_DIR"); + return std::string{dir != nullptr ? dir : "/opt/crypto/share/test_vectors"} + std::string{relative_path}; +} + +/// One curve/digest pair's vector set: a SigGen record that must verify and a +/// SigVer record NIST marked as a failure. +struct NistEcdsaVector +{ + std::vector message; ///< SigGen Msg + std::vector signature; ///< SigGen (R, S) in P1363 form + std::vector invalid_message; ///< SigVer Msg of the Result = F record + std::vector invalid_signature; ///< its (R, S), which must not verify +}; + +/// Reads the vector set of one curve/digest directory, e.g. "P256-SHA256". +/// Records a gtest failure and returns false when a file is missing, which means +/// the vectors were not deployed. +bool LoadNistEcdsaVector(const std::string& curve_dir, std::size_t signature_size, NistEcdsaVector& out) +{ + const std::string dir = GetTestVectorPath("/ecdsa/") + curve_dir + "/"; + + out.message = read_bin(dir + "siggen_message.bin"); + out.signature = read_bin(dir + "siggen_signature.bin"); + out.invalid_message = read_bin(dir + "sigver_invalid_message.bin"); + out.invalid_signature = read_bin(dir + "sigver_invalid_signature.bin"); + + EXPECT_FALSE(out.message.empty()) << "missing " << dir << "siggen_message.bin"; + EXPECT_FALSE(out.invalid_message.empty()) << "missing " << dir << "sigver_invalid_message.bin"; + EXPECT_EQ(out.signature.size(), signature_size) << "unexpected P1363 length in " << dir; + EXPECT_EQ(out.invalid_signature.size(), signature_size) << "unexpected P1363 length in " << dir; + + return !out.message.empty() && (out.signature.size() == signature_size) && + (out.invalid_signature.size() == signature_size); +} + +// ========================================================================= +// Parameterized Test Data +// ========================================================================= + +/// @brief Parameters for ECDSA tests over a generated (ephemeral) key pair. +struct EcdsaTestData +{ + std::string test_case_name; + std::optional provider_type; + std::string key_algorithm; ///< e.g. "ECDSA-P256" + std::string signature_algorithm; ///< e.g. "ECDSA-P256-SHA256" + std::size_t expected_signature_size; + std::string vector_dir; ///< CAVP directory the messages come from +}; + +/// @brief Parameters for ECDSA tests over a key pair loaded from a key slot. +/// +/// With NIST's own key pair in the slot, the CAVP signature becomes an expected +/// value rather than just a shape to check. +struct KeySlotEcdsaTestData +{ + std::string test_case_name; + std::optional provider_type; + std::string signature_algorithm; + std::size_t expected_signature_size; + std::string vector_dir; + std::string key_slot_name; ///< slot holding the SigGen key pair + std::string public_key_slot_name; ///< VERIFY-only slot holding the SigVer failure key +}; + +// ========================================================================= +// Helpers +// ========================================================================= + +std::unique_ptr MakeSignContext(ICryptoContext& ctx, + const std::string& algorithm, + const std::optional& provider_type, + const CryptoResourceGuard& key) +{ + SignContextConfig config; + config.SetAlgorithm(algorithm).SetKey(key); + if (provider_type.has_value()) + { + config.SetProviderType(provider_type.value()); + } + + auto result = ctx.CreateSignContext(config); + EXPECT_TRUE(result.has_value()) << "Failed to create sign context for " << algorithm; + if (!result.has_value()) + { + return nullptr; + } + return std::move(result.value()); +} + +std::unique_ptr MakeVerifyContext(ICryptoContext& ctx, + const std::string& algorithm, + const std::optional& provider_type, + const CryptoResourceGuard& key) +{ + VerifySignatureContextConfig config; + config.SetAlgorithm(algorithm).SetKey(key); + if (provider_type.has_value()) + { + config.SetProviderType(provider_type.value()); + } + + auto result = ctx.CreateVerifySignatureContext(config); + EXPECT_TRUE(result.has_value()) << "Failed to create verify context for " << algorithm; + if (!result.has_value()) + { + return nullptr; + } + return std::move(result.value()); +} + +/// Reset → Init → Update(chunk1) → Update(chunk2) → SignFinalize. +void StreamingSign(ISignContext& sign, + const std::vector& message, + std::size_t signature_size, + std::vector& signature) +{ + signature.assign(signature_size, 0U); + + ASSERT_TRUE(sign.Reset()) << "Reset before signing failed"; + ASSERT_TRUE(sign.Init()) << "Sign Init failed"; + + const auto split = static_cast(message.size()) / 2; + const std::vector chunk1(message.begin(), message.begin() + split); + const std::vector chunk2(message.begin() + split, message.end()); + + ASSERT_TRUE(sign.Update({chunk1.data(), chunk1.size()})) << "Sign Update (chunk 1) failed"; + ASSERT_TRUE(sign.Update({chunk2.data(), chunk2.size()})) << "Sign Update (chunk 2) failed"; + + auto written = sign.SignFinalize({signature.data(), signature.size()}); + ASSERT_TRUE(written.has_value()) << "SignFinalize failed"; + ASSERT_EQ(written.value(), signature_size) << "Unexpected ECDSA signature length"; + + print_hex("Signature", signature, signature.size()); +} + +/// Reset → Init → Update(message) → VerifyFinalize(signature). +void StreamingVerify(IVerifySignatureContext& verify, + const std::vector& message, + const std::vector& signature, + bool expected_valid) +{ + ASSERT_TRUE(verify.Reset()) << "Reset before verification failed"; + ASSERT_TRUE(verify.Init()) << "Verify Init failed"; + ASSERT_TRUE(verify.Update({message.data(), message.size()})) << "Verify Update failed"; + + auto result = verify.VerifyFinalize({signature.data(), signature.size()}); + ASSERT_TRUE(result.has_value()) << "VerifyFinalize call failed"; + EXPECT_EQ(result.value(), expected_valid) + << "Signature verification returned " << result.value() << ", expected " << expected_valid; +} + +/// Resolves a named slot and loads the key material it holds. +score::Result LoadKeyFromSlot(ICryptoContext& ctx, + IKeyManagementContext& key_mgmt, + const std::string& slot_name) +{ + auto slot = ctx.ResolveResource(slot_name, ResourceType::kKeySlot); + EXPECT_TRUE(slot.has_value()) << "Failed to resolve key slot: " << slot_name; + if (!slot.has_value()) + { + return score::Result{score::unexpect, slot.error()}; + } + return key_mgmt.LoadKey(slot.value()); +} + +/// Explicit key release and assertion. +void ReleaseAndAssertKey(CryptoResourceGuard& key) +{ + auto release_result = key.Release(); + ASSERT_TRUE(release_result.has_value()) << "Key release failed"; + EXPECT_FALSE(key.IsActive()) << "Key should be inactive after Release"; +} + +// ========================================================================= +// Test 1: ECDSA key generation, sign and verify with an ephemeral key pair +// ========================================================================= + +class EcdsaSignVerifyTest : public ::testing::TestWithParam +{ +}; + +TEST_P(EcdsaSignVerifyTest, GenerateSignAndVerify) +{ + const auto test_data = GetParam(); + + // The two messages come from the CAVP records of this curve. Their content + // does not matter under a freshly generated key, but taking them from the + // vectors keeps both ECDSA tests on the same inputs. + NistEcdsaVector vector; + ASSERT_TRUE(LoadNistEcdsaVector(test_data.vector_dir, test_data.expected_signature_size, vector)); + + const auto& message = vector.message; + const auto& message_alt = vector.invalid_message; + + // ========================================================================= + // 1. Create the crypto stack and a crypto context + // ========================================================================= + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()) << "Failed to create crypto stack"; + auto& stack = stack_result.value(); + + auto ctx_result = stack->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()) << "Failed to create crypto context"; + auto& ctx = ctx_result.value(); + + // ========================================================================= + // 2. Generate an ECDSA key pair + // ========================================================================= + // + // The returned guard names one daemon resource that carries both halves: + // the sign context below binds the private key, the verify context the + // public key, without the application ever handling key material. + KeyManagementContextConfig key_mgmt_config; + if (test_data.provider_type.has_value()) + { + key_mgmt_config.SetProviderType(test_data.provider_type.value()); + } + + auto key_mgmt_result = ctx->CreateKeyManagementContext(key_mgmt_config); + ASSERT_TRUE(key_mgmt_result.has_value()) << "Failed to create key management context"; + auto& key_mgmt = key_mgmt_result.value(); + + GenerateKeyParams key_gen_params; + key_gen_params.SetAlgorithm(test_data.key_algorithm) + .SetPermissions(KeyOperationPermission::kSign) + .SetPublicKeyPermissions(KeyOperationPermission::kVerify); + + auto key_result = key_mgmt->GenerateKey(key_gen_params); + ASSERT_TRUE(key_result.has_value()) << "Failed to generate ECDSA key pair for " << test_data.key_algorithm; + auto key = std::move(key_result.value()); + ASSERT_TRUE(key.IsActive()) << "Key guard should be active after generation"; + + // ========================================================================= + // 3. Create the sign and verify contexts + // ========================================================================= + auto sign_ctx = MakeSignContext(*ctx, test_data.signature_algorithm, test_data.provider_type, key); + ASSERT_NE(sign_ctx, nullptr); + auto verify_ctx = MakeVerifyContext(*ctx, test_data.signature_algorithm, test_data.provider_type, key); + ASSERT_NE(verify_ctx, nullptr); + + // ========================================================================= + // 4. The reported signature size matches the curve + // ========================================================================= + // + // Signatures are the fixed-length IEEE P1363 form r‖s, so the size is + // exactly twice the field size — 64, 96 or 132 bytes. + EXPECT_EQ(sign_ctx->GetSignatureSize(), test_data.expected_signature_size) + << "Unexpected signature size for " << test_data.signature_algorithm; + + // ========================================================================= + // 5. Streaming sign, then streaming verify + // ========================================================================= + std::vector signature; + ASSERT_NO_FATAL_FAILURE(StreamingSign(*sign_ctx, message, test_data.expected_signature_size, signature)); + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, message, signature, true)); + + // The CAVP signature belongs to NIST's key pair, not this generated one, so + // it must not verify here. That the same bytes *do* verify under the slot + // key in the test below is what tells the two apart. + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, message, vector.signature, false)); + + // ========================================================================= + // 6. A tampered signature must not verify + // ========================================================================= + { + std::vector tampered = signature; + tampered[0] ^= 0xFFU; // corrupt r + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, message, tampered, false)); + + std::vector tampered_s = signature; + tampered_s[tampered_s.size() - 1U] ^= 0xFFU; // corrupt s + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, message, tampered_s, false)); + } + + // ========================================================================= + // 7. A different message must not verify against the same signature + // ========================================================================= + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, message_alt, signature, false)); + + // ========================================================================= + // 8. Single-shot signing and verification + // ========================================================================= + { + ASSERT_TRUE(sign_ctx->Reset()) << "Reset before SingleShot sign failed"; + + std::vector ss_signature(test_data.expected_signature_size, 0U); + auto written = + sign_ctx->SingleShot({message.data(), message.size()}, {ss_signature.data(), ss_signature.size()}); + ASSERT_TRUE(written.has_value()) << "SingleShot signing failed"; + EXPECT_EQ(written.value(), test_data.expected_signature_size); + + ASSERT_TRUE(verify_ctx->Reset()) << "Reset before SingleShot verify failed"; + auto verified = + verify_ctx->SingleShot({message.data(), message.size()}, {ss_signature.data(), ss_signature.size()}); + ASSERT_TRUE(verified.has_value()) << "SingleShot verification call failed"; + EXPECT_TRUE(verified.value()) << "SingleShot signature should verify"; + + // ECDSA is randomised: two signatures over the same message under the + // same key differ, yet both verify. That is expected, not a defect. + EXPECT_NE(ss_signature, signature) << "Two ECDSA signatures over one message should differ"; + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, message, ss_signature, true)); + } + + // ========================================================================= + // 9. Context reuse via Reset() with a different message + // ========================================================================= + std::vector signature_alt; + ASSERT_NO_FATAL_FAILURE(StreamingSign(*sign_ctx, message_alt, test_data.expected_signature_size, signature_alt)); + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, message_alt, signature_alt, true)); + + // The signature over the first message must still not verify the second. + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, message, signature_alt, false)); + + // ========================================================================= + // 10. Reset mid-stream discards partial work + // ========================================================================= + { + ASSERT_TRUE(sign_ctx->Reset()); + ASSERT_TRUE(sign_ctx->Init()); + ASSERT_TRUE(sign_ctx->Update({message.data(), message.size()})); + ASSERT_TRUE(sign_ctx->Reset()) << "Mid-stream Reset failed"; + + // After discarding the first message, sign the alternative one and + // confirm the result authenticates that message and not the first. + std::vector after_abort; + ASSERT_NO_FATAL_FAILURE(StreamingSign(*sign_ctx, message_alt, test_data.expected_signature_size, after_abort)); + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, message_alt, after_abort, true)); + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, message, after_abort, false)); + } + + // ========================================================================= + // 11. A signature from an unrelated key pair must not verify + // ========================================================================= + { + auto other_key_result = key_mgmt->GenerateKey(key_gen_params); + ASSERT_TRUE(other_key_result.has_value()) << "Failed to generate second ECDSA key pair"; + auto other_key = std::move(other_key_result.value()); + + auto other_sign_ctx = MakeSignContext(*ctx, test_data.signature_algorithm, test_data.provider_type, other_key); + ASSERT_NE(other_sign_ctx, nullptr); + + std::vector foreign_signature; + ASSERT_NO_FATAL_FAILURE( + StreamingSign(*other_sign_ctx, message, test_data.expected_signature_size, foreign_signature)); + + // Same message, valid signature — but made with a different key. + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, message, foreign_signature, false)); + } + + // ========================================================================= + // 12. Explicit key release + // ========================================================================= + // + // The contexts hold their own reference to the bound key material, so + // releasing the guard here is safe and mirrors the MAC example. + ASSERT_NO_FATAL_FAILURE(ReleaseAndAssertKey(key)); +} + +// ========================================================================= +// Test 2: NIST CAVP verification with the key pair loaded from a key slot +// ========================================================================= + +class KeySlotEcdsaTest : public ::testing::TestWithParam +{ +}; + +TEST_P(KeySlotEcdsaTest, VerifiesNistVectors) +{ + const auto test_data = GetParam(); + + NistEcdsaVector vector; + ASSERT_TRUE(LoadNistEcdsaVector(test_data.vector_dir, test_data.expected_signature_size, vector)); + + // ========================================================================= + // 1. Create the crypto stack and a crypto context + // ========================================================================= + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()) << "Failed to create crypto stack"; + auto& stack = stack_result.value(); + + auto ctx_result = stack->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()) << "Failed to create crypto context"; + auto& ctx = ctx_result.value(); + + // ========================================================================= + // 2. Load NIST's key pair from its pre-provisioned key slot + // ========================================================================= + // + // The slot's deployment descriptor points at the DER encoding of the CAVP + // private key, so the daemon holds NIST's key pair: its private half signs + // and its public half verifies, and the application never sees either. + KeyManagementContextConfig key_mgmt_config; + if (test_data.provider_type.has_value()) + { + key_mgmt_config.SetProviderType(test_data.provider_type.value()); + } + + auto key_mgmt_result = ctx->CreateKeyManagementContext(key_mgmt_config); + ASSERT_TRUE(key_mgmt_result.has_value()) << "Failed to create key management context"; + auto& key_mgmt = key_mgmt_result.value(); + + auto key_result = LoadKeyFromSlot(*ctx, *key_mgmt, test_data.key_slot_name); + ASSERT_TRUE(key_result.has_value()) << "Failed to load key from slot: " << test_data.key_slot_name; + auto key = std::move(key_result.value()); + ASSERT_TRUE(key.IsActive()) << "Key guard should be active after loading from slot"; + + // ========================================================================= + // 3. The published CAVP signature verifies under the published key + // ========================================================================= + // + // This is the known-answer half of the ECDSA testing: bytes NIST produced, + // verified by this stack, with no value this stack computed in between. + auto verify_ctx = MakeVerifyContext(*ctx, test_data.signature_algorithm, test_data.provider_type, key); + ASSERT_NE(verify_ctx, nullptr); + + print_hex("CAVP signature", vector.signature, vector.signature.size()); + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, vector.message, vector.signature, true)); + + // Single-shot verification of the same vector. + { + ASSERT_TRUE(verify_ctx->Reset()) << "Reset before SingleShot verify failed"; + auto verified = verify_ctx->SingleShot({vector.message.data(), vector.message.size()}, + {vector.signature.data(), vector.signature.size()}); + ASSERT_TRUE(verified.has_value()) << "SingleShot verification call failed"; + EXPECT_TRUE(verified.value()) << "SingleShot verification of the CAVP signature failed"; + } + + // ========================================================================= + // 4. Corrupting either half of the CAVP signature breaks verification + // ========================================================================= + { + std::vector tampered_r = vector.signature; + tampered_r[0] ^= 0xFFU; + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, vector.message, tampered_r, false)); + + std::vector tampered_s = vector.signature; + tampered_s[tampered_s.size() - 1U] ^= 0xFFU; + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, vector.message, tampered_s, false)); + } + + // ========================================================================= + // 5. The CAVP signature does not authenticate a different message + // ========================================================================= + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, vector.invalid_message, vector.signature, false)); + + // ========================================================================= + // 6. Signing with the slot key produces a signature that verifies + // ========================================================================= + // + // NIST's per-message secret is not settable here, so the signature differs + // from the CAVP one every run. What is checkable is that it verifies under + // the same public half that just accepted NIST's own signature. + auto sign_ctx = MakeSignContext(*ctx, test_data.signature_algorithm, test_data.provider_type, key); + ASSERT_NE(sign_ctx, nullptr); + + std::vector signature; + ASSERT_NO_FATAL_FAILURE(StreamingSign(*sign_ctx, vector.message, test_data.expected_signature_size, signature)); + EXPECT_NE(signature, vector.signature) << "A randomised signature reproduced the CAVP value"; + ASSERT_NO_FATAL_FAILURE(StreamingVerify(*verify_ctx, vector.message, signature, true)); + + // ========================================================================= + // 7. A VERIFY-only slot rejects the NIST failure record + // ========================================================================= + // + // This slot holds only the public half — a SubjectPublicKeyInfo blob — of a + // SigVer record NIST marked `Result = F`. Unlike a bit-flipped signature it + // is a well-formed value authored to be rejected, and it is the only case + // that exercises importing a public key on its own. + auto public_key_result = LoadKeyFromSlot(*ctx, *key_mgmt, test_data.public_key_slot_name); + ASSERT_TRUE(public_key_result.has_value()) << "Failed to load key from slot: " << test_data.public_key_slot_name; + auto public_key = std::move(public_key_result.value()); + ASSERT_TRUE(public_key.IsActive()) << "Public key guard should be active after loading from slot"; + + auto public_verify_ctx = + MakeVerifyContext(*ctx, test_data.signature_algorithm, test_data.provider_type, public_key); + ASSERT_NE(public_verify_ctx, nullptr); + + ASSERT_NO_FATAL_FAILURE( + StreamingVerify(*public_verify_ctx, vector.invalid_message, vector.invalid_signature, false)); + + // ========================================================================= + // 8. A VERIFY-only slot cannot be used to sign + // ========================================================================= + // + // The slot grants VERIFY alone, so the daemon must refuse the sign context + // at creation rather than at the first Update. + { + SignContextConfig config; + config.SetAlgorithm(test_data.signature_algorithm).SetKey(public_key); + if (test_data.provider_type.has_value()) + { + config.SetProviderType(test_data.provider_type.value()); + } + + auto refused = ctx->CreateSignContext(config); + EXPECT_FALSE(refused.has_value()) << "A VERIFY-only key slot must not yield a sign context"; + } + + // ========================================================================= + // 9. Explicit key release + // ========================================================================= + ASSERT_NO_FATAL_FAILURE(ReleaseAndAssertKey(public_key)); + ASSERT_NO_FATAL_FAILURE(ReleaseAndAssertKey(key)); +} + +// ========================================================================= +// Test Vector Constants +// ========================================================================= + +// P1363 signature sizes: 2 * field size. +constexpr std::size_t kP256SignatureSize = 64U; +constexpr std::size_t kP384SignatureSize = 96U; +constexpr std::size_t kP521SignatureSize = 132U; + +// ========================================================================= +// Test Suites +// ========================================================================= + +// ECDSA is provided only by the OpenSSL (software) provider, so every case pins +// ProviderType::kSoftware. Leaving the provider unset resolves to the daemon's +// DEFAULT provider — currently SoftHSM — which offers neither EC key generation +// nor a signature handler. +INSTANTIATE_TEST_SUITE_P(CurvesOnSoftwareProvider, + EcdsaSignVerifyTest, + ::testing::Values( + EcdsaTestData{ + "ECDSA_P256_SHA256_SoftwareProvider", + ProviderType::kSoftware, + "ECDSA-P256", + "ECDSA-P256-SHA256", + kP256SignatureSize, + "P256-SHA256", + }, + EcdsaTestData{ + "ECDSA_P384_SHA384_SoftwareProvider", + ProviderType::kSoftware, + "ECDSA-P384", + "ECDSA-P384-SHA384", + kP384SignatureSize, + "P384-SHA384", + }, + EcdsaTestData{ + "ECDSA_P521_SHA512_SoftwareProvider", + ProviderType::kSoftware, + "ECDSA-P521", + "ECDSA-P521-SHA512", + kP521SignatureSize, + "P521-SHA512", + }), + [](const testing::TestParamInfo& info) { + return info.param.test_case_name; + }); + +INSTANTIATE_TEST_SUITE_P(NistVectorsOnSoftwareProvider, + KeySlotEcdsaTest, + ::testing::Values( + KeySlotEcdsaTestData{ + "ECDSA_P256_SHA256_KeySlot_SoftwareProvider", + ProviderType::kSoftware, + "ECDSA-P256-SHA256", + kP256SignatureSize, + "P256-SHA256", + "ECDSA_P256_IntegrationTestKey_OpenSSL", + "ECDSA_P256_PublicKeySlot_OpenSSL", + }, + KeySlotEcdsaTestData{ + "ECDSA_P384_SHA384_KeySlot_SoftwareProvider", + ProviderType::kSoftware, + "ECDSA-P384-SHA384", + kP384SignatureSize, + "P384-SHA384", + "ECDSA_P384_IntegrationTestKey_OpenSSL", + "ECDSA_P384_PublicKeySlot_OpenSSL", + }, + KeySlotEcdsaTestData{ + "ECDSA_P521_SHA512_KeySlot_SoftwareProvider", + ProviderType::kSoftware, + "ECDSA-P521-SHA512", + kP521SignatureSize, + "P521-SHA512", + "ECDSA_P521_IntegrationTestKey_OpenSSL", + "ECDSA_P521_PublicKeySlot_OpenSSL", + }), + [](const testing::TestParamInfo& info) { + return info.param.test_case_name; + }); + +} // namespace + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/score/tests/integration_tests/score_api_key_permissions_test.cpp b/score/tests/integration_tests/score_api_key_permissions_test.cpp new file mode 100644 index 000000000..4fbf95c45 --- /dev/null +++ b/score/tests/integration_tests/score_api_key_permissions_test.cpp @@ -0,0 +1,630 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/// @file score_api_key_permissions_test.cpp +/// @brief Verifies that KeyOperationPermission is enforced when a context binds a key. +/// +/// A key generated with restricted permissions must be usable for exactly the +/// operations it was granted and no others. The daemon enforces this once, at +/// context creation: a context is bound to one key and one direction for its +/// whole life, so a context that was allowed to exist can never exceed its +/// grant afterwards. +/// +/// Each case therefore asserts on CreateXxxContext() rather than on the +/// operation itself, and pairs every denial with the matching permitted case — +/// a test that only checks denials would also pass if context creation were +/// broken for every key. +/// +/// Covered: +/// - MAC needs kMac, on every provider the build enables +/// - Cipher needs kEncrypt or kDecrypt, per direction +/// - Signing needs kSign on the private half +/// - Verification needs kVerify on the public half, which defaults to +/// unrestricted when the caller does not narrow it +/// - Keyless contexts (hash, random) are unaffected + +#include "score/crypto/src/api/common/error_domain.hpp" +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/config/cipher_context_config.hpp" +#include "score/crypto/src/api/config/hash_context_config.hpp" +#include "score/crypto/src/api/config/key_management_context_config.hpp" +#include "score/crypto/src/api/config/key_operation_params.hpp" +#include "score/crypto/src/api/config/mac_context_config.hpp" +#include "score/crypto/src/api/config/random_context_config.hpp" +#include "score/crypto/src/api/config/sign_context_config.hpp" +#include "score/crypto/src/api/config/verify_signature_context_config.hpp" +#include "score/crypto/src/api/contexts/i_cipher_context.hpp" +#include "score/crypto/src/api/contexts/i_hash_context.hpp" +#include "score/crypto/src/api/contexts/i_key_management_context.hpp" +#include "score/crypto/src/api/contexts/i_mac_context.hpp" +#include "score/crypto/src/api/contexts/i_random_context.hpp" +#include "score/crypto/src/api/contexts/i_sign_context.hpp" +#include "score/crypto/src/api/contexts/i_verify_signature_context.hpp" +#include "score/crypto/src/api/crypto_stack_factory.hpp" +#include "score/crypto/src/api/i_crypto_context.hpp" +#include "score/crypto/src/api/i_crypto_stack.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace score::crypto; + +namespace +{ + +#ifdef __QNXNTO__ +constexpr auto kControlSocketEndpoint = "unix:///opt/crypto_daemon.sock"; +#else +constexpr auto kControlSocketEndpoint = "unix:///tmp/crypto_daemon.sock"; +#endif + +// ========================================================================= +// Constants +// ========================================================================= + +#ifdef SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED +/// Cipher, signature and EC key generation exist only in the OpenSSL provider, +/// so those cases pin kSoftware. Leaving the provider unset would resolve to the +/// daemon's DEFAULT provider, currently SoftHSM, which offers none of them. +constexpr ProviderType kSoftwareProvider = ProviderType::kSoftware; +#endif + +/// The keyless cases need a provider but do not depend on which: no key means no +/// grant to check, whichever provider serves the context. +#ifdef SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED +constexpr ProviderType kKeylessProvider = ProviderType::kSoftware; +#else +constexpr ProviderType kKeylessProvider = ProviderType::kHardware; +#endif + +#if !defined(SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED) && !defined(SCORE_CRYPTO_HARDWARE_BACKEND_ENABLED) +static_assert(false, "At least one crypto backend must be enabled for the key permission tests"); +#endif + +/// MAC is the one keyed operation both providers implement, so the MAC cases run +/// against each one the build enables. Enforcement is provider-independent — it happens in the mediator +/// — but the two key factories build their handles differently, and only the +/// PKCS#11 one also translates the grant into CKA_* attributes on the token +/// object. A permission that never reached the daemon would leave a SoftHSM key +/// fully permissive *and* extractable, so this axis is worth covering. +constexpr ProviderType kMacProviders[] = { +#ifdef SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED + ProviderType::kSoftware, +#endif +#ifdef SCORE_CRYPTO_HARDWARE_BACKEND_ENABLED + ProviderType::kHardware, +#endif +}; + +// The daemon's algorithm tables spell these without a hyphen before the +// digest size — "HMAC-SHA-256" is not a recognised identifier. +constexpr const char* kMacAlgorithm = "HMAC-SHA256"; +#ifdef SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED +constexpr const char* kCipherAlgorithm = "AES-256-CBC"; +constexpr const char* kEcKeyAlgorithm = "ECDSA-P256"; +constexpr const char* kEcSignatureAlgorithm = "ECDSA-P256-SHA256"; +#endif + +/// A real permission that is not kMac, used for the "wrong grant" MAC cases. +/// +/// kSign rather than kEncrypt: BuildUsageFlags maps it to CKA_SIGN, which is +/// valid on a PKCS#11 generic-secret key, whereas CKA_ENCRYPT is not. The point +/// of these cases is that the daemon rejects the context, so the key must be +/// creatable on both providers first. +constexpr KeyOperationPermission kNotMac = KeyOperationPermission::kSign; + +// ========================================================================= +// Fixture +// ========================================================================= + +/// Owns the stack and crypto context. The key management context is created +/// per test rather than here, because its provider varies: a key is generated by +/// whichever provider owns the key management context it was requested from. +class KeyPermissionTest : public ::testing::Test +{ + protected: + void SetUp() override + { + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()) << "Failed to create crypto stack"; + m_stack = std::move(stack_result.value()); + + auto ctx_result = m_stack->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()) << "Failed to create crypto context"; + m_ctx = std::move(ctx_result.value()); + } + + /// Creates a key management context on @p provider. + /// + /// Callers must keep it alive for as long as they use keys generated from + /// it: an ephemeral key node is a child of the context that created it. + std::unique_ptr MakeKeyMgmtContext(ProviderType provider) + { + KeyManagementContextConfig config; + config.SetProviderType(provider); + auto result = m_ctx->CreateKeyManagementContext(config); + EXPECT_TRUE(result.has_value()) << "Failed to create key management context"; + if (!result.has_value()) + { + return nullptr; + } + return std::move(result.value()); + } + + /// Generates an ephemeral key with exactly the given permissions. + /// + /// Returns the Result rather than the guard: a guard cannot be + /// default-constructed, so there is no inactive value to hand back on + /// failure, and every caller has to check anyway. + /// + /// @param public_permissions Left unset to exercise the documented default + /// for the public half of an asymmetric key. + static score::Result GenerateKey( + IKeyManagementContext& key_mgmt, + const std::string& algorithm, + KeyOperationPermission permissions, + std::optional public_permissions = std::nullopt) + { + GenerateKeyParams params; + params.SetAlgorithm(algorithm).SetPermissions(permissions); + if (public_permissions.has_value()) + { + params.SetPublicKeyPermissions(public_permissions.value()); + } + + // Generation itself is never permission-checked: the grant describes how + // the key may later be used, so a kNone key must still be creatable. + return key_mgmt.GenerateKey(params); + } + + std::unique_ptr m_stack; + std::unique_ptr m_ctx; +}; + +/// MAC cases, run once per provider that implements MAC. +class MacKeyPermissionTest : public KeyPermissionTest, public ::testing::WithParamInterface +{ +}; + +// ========================================================================= +// Assertions +// ========================================================================= + +/// Asserts that a context creation failed specifically because the key policy +/// forbids the operation — not for some unrelated reason such as an unsupported +/// algorithm, which would make the test pass without testing anything. +template +void ExpectNotPermitted(const ContextResult& result, const char* what) +{ + ASSERT_FALSE(result.has_value()) << what << " succeeded, but the key does not permit this operation"; + EXPECT_EQ(*result.error(), static_cast(CryptoErrorCode::kKeyOperationNotPermitted)) + << what << " failed with '" << result.error().Message() << "' instead of kKeyOperationNotPermitted"; +} + +// ========================================================================= +// Context factories +// ========================================================================= + +auto MakeMacContext(ICryptoContext& ctx, const CryptoResourceGuard& key, ProviderType provider) +{ + MacContextConfig config; + config.SetAlgorithm(kMacAlgorithm).SetKey(key).SetProviderType(provider); + return ctx.CreateMacContext(config); +} + +#ifdef SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED +auto MakeCipherContext(ICryptoContext& ctx, const CryptoResourceGuard& key, CipherDirection direction) +{ + CipherContextConfig config; + config.SetAlgorithm(kCipherAlgorithm).SetKey(key).SetDirection(direction).SetProviderType(kSoftwareProvider); + return ctx.CreateCipherContext(config); +} + +auto MakeSignContext(ICryptoContext& ctx, const CryptoResourceGuard& key) +{ + SignContextConfig config; + config.SetAlgorithm(kEcSignatureAlgorithm).SetKey(key).SetProviderType(kSoftwareProvider); + return ctx.CreateSignContext(config); +} + +auto MakeVerifyContext(ICryptoContext& ctx, const CryptoResourceGuard& key) +{ + VerifySignatureContextConfig config; + config.SetAlgorithm(kEcSignatureAlgorithm).SetKey(key).SetProviderType(kSoftwareProvider); + return ctx.CreateVerifySignatureContext(config); +} +#endif + +// ========================================================================= +// MAC — run against every provider that implements it +// ========================================================================= + +TEST_P(MacKeyPermissionTest, MacContextRequiresMacPermission) +{ + // This is the regression test for permissions never reaching the daemon: + // read at the wrong width, the grant below decoded as "unset" and silently + // became kAll, so the context was created and nothing was enforced. + auto key_mgmt = MakeKeyMgmtContext(GetParam()); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = GenerateKey(*key_mgmt, kMacAlgorithm, kNotMac); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + ExpectNotPermitted(MakeMacContext(*m_ctx, key, GetParam()), "CreateMacContext with a key that lacks kMac"); +} + +TEST_P(MacKeyPermissionTest, MacContextAcceptsMacPermission) +{ + auto key_mgmt = MakeKeyMgmtContext(GetParam()); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = GenerateKey(*key_mgmt, kMacAlgorithm, KeyOperationPermission::kMac); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + auto mac_ctx = MakeMacContext(*m_ctx, key, GetParam()); + ASSERT_TRUE(mac_ctx.has_value()) << "CreateMacContext rejected a key that grants kMac"; +} + +TEST_P(MacKeyPermissionTest, MacContextRejectsKeyWithNoPermissions) +{ + auto key_mgmt = MakeKeyMgmtContext(GetParam()); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = GenerateKey(*key_mgmt, kMacAlgorithm, KeyOperationPermission::kNone); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + ExpectNotPermitted(MakeMacContext(*m_ctx, key, GetParam()), "CreateMacContext with a kNone key"); +} + +TEST_P(MacKeyPermissionTest, MacVerifyModeUsesTheSamePermissionAsGenerate) +{ + // Verifying a MAC means recomputing it, so kMac covers both directions and + // there is no separate "verify MAC" grant to withhold. On PKCS#11 this is + // also why BuildUsageFlags maps kMac to CKA_SIGN *and* CKA_VERIFY. + auto key_mgmt = MakeKeyMgmtContext(GetParam()); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = GenerateKey(*key_mgmt, kMacAlgorithm, KeyOperationPermission::kMac); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + MacContextConfig config; + config.SetAlgorithm(kMacAlgorithm).SetKey(key).SetProviderType(GetParam()).SetOperationMode(OperationMode::kVerify); + + auto mac_ctx = m_ctx->CreateMacContext(config); + ASSERT_TRUE(mac_ctx.has_value()) << "A kMac key must serve a verify-mode MAC context too"; +} + +INSTANTIATE_TEST_SUITE_P(PerProvider, + MacKeyPermissionTest, + ::testing::ValuesIn(kMacProviders), + [](const testing::TestParamInfo& info) { + return (info.param == ProviderType::kSoftware) ? "SoftwareProvider" : "HardwareProvider"; + }); + +// Everything up to the key slot cases below is cipher or signature work, which +// only the OpenSSL provider implements. +#ifdef SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED + +// ========================================================================= +// Cipher — the direction selects which permission is required +// ========================================================================= + +TEST_F(KeyPermissionTest, EncryptOnlyKeyCannotDecrypt) +{ + auto key_mgmt = MakeKeyMgmtContext(kSoftwareProvider); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = GenerateKey(*key_mgmt, kCipherAlgorithm, KeyOperationPermission::kEncrypt); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + auto encrypt_ctx = MakeCipherContext(*m_ctx, key, CipherDirection::kEncrypt); + ASSERT_TRUE(encrypt_ctx.has_value()) << "An encrypt-permitted key must produce an encryption context"; + + ExpectNotPermitted(MakeCipherContext(*m_ctx, key, CipherDirection::kDecrypt), + "CreateCipherContext(kDecrypt) with an encrypt-only key"); +} + +TEST_F(KeyPermissionTest, DecryptOnlyKeyCannotEncrypt) +{ + auto key_mgmt = MakeKeyMgmtContext(kSoftwareProvider); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = GenerateKey(*key_mgmt, kCipherAlgorithm, KeyOperationPermission::kDecrypt); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + auto decrypt_ctx = MakeCipherContext(*m_ctx, key, CipherDirection::kDecrypt); + ASSERT_TRUE(decrypt_ctx.has_value()) << "A decrypt-permitted key must produce a decryption context"; + + ExpectNotPermitted(MakeCipherContext(*m_ctx, key, CipherDirection::kEncrypt), + "CreateCipherContext(kEncrypt) with a decrypt-only key"); +} + +TEST_F(KeyPermissionTest, KeyGrantingBothDirectionsRoundTrips) +{ + auto key_mgmt = MakeKeyMgmtContext(kSoftwareProvider); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = + GenerateKey(*key_mgmt, kCipherAlgorithm, KeyOperationPermission::kEncrypt | KeyOperationPermission::kDecrypt); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + auto encrypt_ctx = MakeCipherContext(*m_ctx, key, CipherDirection::kEncrypt); + ASSERT_TRUE(encrypt_ctx.has_value()); + auto decrypt_ctx = MakeCipherContext(*m_ctx, key, CipherDirection::kDecrypt); + ASSERT_TRUE(decrypt_ctx.has_value()); + + // Both contexts over one key must still work together — enforcement must not + // have introduced a per-key exclusivity that a round trip would hit. + constexpr std::size_t kBlockSize = 16U; + const std::vector plaintext(64U, 0xA5U); + const std::vector iv(kBlockSize, 0x11U); + + // SingleShot takes the IV itself, so no separate Init() is needed. One extra + // block of headroom covers the CBC padding block emitted on finalisation. + std::vector ciphertext(plaintext.size() + kBlockSize, 0U); + auto encrypted = encrypt_ctx.value()->SingleShot( + {iv.data(), iv.size()}, {plaintext.data(), plaintext.size()}, {ciphertext.data(), ciphertext.size()}); + ASSERT_TRUE(encrypted.has_value()) << "Encryption failed"; + ciphertext.resize(encrypted.value()); + + std::vector recovered(ciphertext.size() + kBlockSize, 0U); + auto decrypted = decrypt_ctx.value()->SingleShot( + {iv.data(), iv.size()}, {ciphertext.data(), ciphertext.size()}, {recovered.data(), recovered.size()}); + ASSERT_TRUE(decrypted.has_value()) << "Decryption failed"; + recovered.resize(decrypted.value()); + + EXPECT_EQ(recovered, plaintext) << "Round trip through two permitted contexts did not recover the plaintext"; +} + +// ========================================================================= +// ECDSA — the two halves of a key pair carry separate permissions +// ========================================================================= + +TEST_F(KeyPermissionTest, SigningRequiresSignPermissionOnThePrivateHalf) +{ + // kVerify on the private half is deliberately the wrong grant: signing + // consumes the private key, so only kSign unlocks it. + auto key_mgmt = MakeKeyMgmtContext(kSoftwareProvider); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = GenerateKey(*key_mgmt, kEcKeyAlgorithm, KeyOperationPermission::kVerify); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + ExpectNotPermitted(MakeSignContext(*m_ctx, key), "CreateSignContext with a key that lacks kSign"); +} + +TEST_F(KeyPermissionTest, SigningAcceptsSignPermission) +{ + auto key_mgmt = MakeKeyMgmtContext(kSoftwareProvider); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = GenerateKey(*key_mgmt, kEcKeyAlgorithm, KeyOperationPermission::kSign); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + auto sign_ctx = MakeSignContext(*m_ctx, key); + ASSERT_TRUE(sign_ctx.has_value()) << "CreateSignContext rejected a key that grants kSign"; +} + +TEST_F(KeyPermissionTest, VerificationHonoursExplicitPublicHalfPermissions) +{ + // The private half may sign; the public half is narrowed to kEncrypt, which + // does not include kVerify. Without the public/private split this would + // wrongly consult the private half's kSign and reject nothing. + auto key_mgmt = MakeKeyMgmtContext(kSoftwareProvider); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = + GenerateKey(*key_mgmt, kEcKeyAlgorithm, KeyOperationPermission::kSign, KeyOperationPermission::kEncrypt); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + auto sign_ctx = MakeSignContext(*m_ctx, key); + ASSERT_TRUE(sign_ctx.has_value()) << "Restricting the public half must not affect signing"; + + ExpectNotPermitted(MakeVerifyContext(*m_ctx, key), + "CreateVerifySignatureContext with a public half that lacks kVerify"); +} + +TEST_F(KeyPermissionTest, VerificationAcceptsExplicitVerifyOnThePublicHalf) +{ + auto key_mgmt = MakeKeyMgmtContext(kSoftwareProvider); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = + GenerateKey(*key_mgmt, kEcKeyAlgorithm, KeyOperationPermission::kSign, KeyOperationPermission::kVerify); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + auto sign_ctx = MakeSignContext(*m_ctx, key); + ASSERT_TRUE(sign_ctx.has_value()) << "Sign context rejected despite kSign on the private half"; + auto verify_ctx = MakeVerifyContext(*m_ctx, key); + ASSERT_TRUE(verify_ctx.has_value()) << "Verify context rejected despite kVerify on the public half"; +} + +TEST_F(KeyPermissionTest, PublicHalfDefaultsToUnrestricted) +{ + // Documented contract of GenerateKeyParams::public_key_permissions: omitting + // it leaves the public half unrestricted, so a sign-only key still verifies. + // A public key is public information, so this default protects nothing that + // withholding kVerify would. + auto key_mgmt = MakeKeyMgmtContext(kSoftwareProvider); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = GenerateKey(*key_mgmt, kEcKeyAlgorithm, KeyOperationPermission::kSign); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + auto verify_ctx = MakeVerifyContext(*m_ctx, key); + ASSERT_TRUE(verify_ctx.has_value()) << "An unrestricted public half must permit verification"; +} + +TEST_F(KeyPermissionTest, PrivateHalfRestrictionDoesNotLeakIntoThePublicHalf) +{ + // kNone on the private half must not be read as "nothing at all works": + // the public half was left unrestricted and is a separate grant. + auto key_mgmt = MakeKeyMgmtContext(kSoftwareProvider); + ASSERT_NE(key_mgmt, nullptr); + + auto key_result = GenerateKey(*key_mgmt, kEcKeyAlgorithm, KeyOperationPermission::kNone); + ASSERT_TRUE(key_result.has_value()) << "GenerateKey failed"; + auto& key = key_result.value(); + + ExpectNotPermitted(MakeSignContext(*m_ctx, key), "CreateSignContext with a kNone private half"); + + auto verify_ctx = MakeVerifyContext(*m_ctx, key); + ASSERT_TRUE(verify_ctx.has_value()) << "The public half was not restricted, so verification must be permitted"; +} + +#endif // SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED + +// ========================================================================= +// Configured key slots — the slot's allowed_operations is the grant +// ========================================================================= + +/// One provider's pair of slots over the same key material. +/// +/// The two slots deploy identical key material and differ only in +/// allowed_operations, so a denial on the restricted one cannot be explained by +/// the key, the algorithm or the provider — only by the policy. Without the +/// permitted slot as a control, a test that broke slot loading outright would +/// still "pass". +struct SlotPolicyTestData +{ + const char* test_case_name; + ProviderType provider; + const char* mac_slot; ///< allowed_operations = "MAC" + const char* sign_only_slot; ///< allowed_operations = "SIGN" — same key material +}; + +class SlotPolicyTest : public KeyPermissionTest, public ::testing::WithParamInterface +{ + protected: + /// Resolves a configured slot by name and loads its key material. + score::Result LoadSlotKey(IKeyManagementContext& key_mgmt, const char* slot_name) + { + auto slot = m_ctx->ResolveResource(slot_name, ResourceType::kKeySlot); + EXPECT_TRUE(slot.has_value()) << "Failed to resolve key slot: " << slot_name; + if (!slot.has_value()) + { + return score::Result{score::unexpect, slot.error()}; + } + return key_mgmt.LoadKey(slot.value()); + } +}; + +TEST_P(SlotPolicyTest, SlotAllowedOperationsGovernsMacContext) +{ + const auto test_data = GetParam(); + + auto key_mgmt = MakeKeyMgmtContext(test_data.provider); + ASSERT_NE(key_mgmt, nullptr); + + // Control: the slot that grants MAC over this key material must work. This + // is also what would fail if a slot handler dropped allowed_operations and + // registered the key as kNone. + { + auto key_result = LoadSlotKey(*key_mgmt, test_data.mac_slot); + ASSERT_TRUE(key_result.has_value()) << "Failed to load key from slot: " << test_data.mac_slot; + auto& key = key_result.value(); + + auto mac_ctx = MakeMacContext(*m_ctx, key, test_data.provider); + ASSERT_TRUE(mac_ctx.has_value()) << "A slot granting MAC must produce a MAC context"; + } + + // Same key material, SIGN instead of MAC: loading still succeeds — the policy + // governs use, not loading — and the MAC context is refused. + { + auto key_result = LoadSlotKey(*key_mgmt, test_data.sign_only_slot); + ASSERT_TRUE(key_result.has_value()) + << "Loading a key from a slot must not depend on allowed_operations: " << test_data.sign_only_slot; + auto& key = key_result.value(); + + ExpectNotPermitted(MakeMacContext(*m_ctx, key, test_data.provider), + "CreateMacContext with a key from a slot restricted to SIGN"); + } +} + +INSTANTIATE_TEST_SUITE_P(PerProvider, + SlotPolicyTest, + ::testing::Values( +#ifdef SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED + SlotPolicyTestData{"SoftwareProvider", + ProviderType::kSoftware, + "HMAC_SHA256_IntegrationTestKey_OpenSSL", + "HMAC_SHA256_SignOnlySlot_OpenSSL"} +#endif +#if defined(SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED) && defined(SCORE_CRYPTO_HARDWARE_BACKEND_ENABLED) + , +#endif +#ifdef SCORE_CRYPTO_HARDWARE_BACKEND_ENABLED + // Exercises the PKCS#11 token-object path, which registers + // the slot policy separately from the file-backed one. + SlotPolicyTestData{"HardwareProvider", + ProviderType::kHardware, + "HMAC_SHA256_IntegrationTestKey", + "HMAC_SHA256_SignOnlySlot"} +#endif + ), + [](const testing::TestParamInfo& info) { + return info.param.test_case_name; + }); + +// ========================================================================= +// Keyless contexts +// ========================================================================= + +TEST_F(KeyPermissionTest, KeylessContextsAreUnaffected) +{ + // Hash and random bind no key, so there is no permission to check. This + // guards the "no key permission required" branch of the enforcement path: + // a mapping that fell through to a default grant would break these. + HashContextConfig hash_config; + hash_config.SetAlgorithm("SHA256").SetProviderType(kKeylessProvider); + auto hash_ctx = m_ctx->CreateHashContext(hash_config); + EXPECT_TRUE(hash_ctx.has_value()) << "Hash context creation must not require a key permission"; + +#ifdef SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED + // No algorithm: the RNG context takes the provider's default generator. + RandomContextConfig random_config; + random_config.SetProviderType(kSoftwareProvider); + auto random_ctx = m_ctx->CreateRandomContext(random_config); + EXPECT_TRUE(random_ctx.has_value()) << "Random context creation must not require a key permission"; +#endif +} + +} // namespace + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/score/tests/integration_tests/score_api_random_test.cpp b/score/tests/integration_tests/score_api_random_test.cpp new file mode 100644 index 000000000..40543152d --- /dev/null +++ b/score/tests/integration_tests/score_api_random_test.cpp @@ -0,0 +1,268 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +/// @file score_api_random_example.cpp +/// @brief Demonstrates random number generation using the score::crypto API. +/// +/// Shows: +/// - Creating a random context with and without a provider preference +/// - Generating buffers of various sizes +/// - Seeding the generator with additional entropy +/// +/// A statistical quality assessment of the RNG is out of scope here — that is +/// the provider's responsibility and is covered by its own certification. What +/// these tests establish is that the plumbing is correct: the requested number +/// of bytes arrives, the whole buffer is written, and successive draws differ. + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/config/random_context_config.hpp" +#include "score/crypto/src/api/contexts/i_random_context.hpp" +#include "score/crypto/src/api/crypto_stack_factory.hpp" +#include "score/crypto/src/api/i_crypto_context.hpp" +#include "score/crypto/src/api/i_crypto_stack.hpp" +#include "score/tests/utility/test_utility.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace score::crypto; +using tests::utility::print_hex; + +namespace +{ + +#ifdef __QNXNTO__ +constexpr auto kControlSocketEndpoint = "unix:///opt/crypto_daemon.sock"; +#else +constexpr auto kControlSocketEndpoint = "unix:///tmp/crypto_daemon.sock"; +#endif + +struct RandomTestData +{ + std::string test_case_name; + std::optional provider_type; + std::string algorithm; ///< empty selects the provider default +}; + +/// Creates a stack, a crypto context and a random context in one step. +/// Returns nullptr (with a gtest failure already recorded) on any error. +struct RandomFixture +{ + ICryptoStack::Uptr stack; + ICryptoContext::Uptr ctx; + std::unique_ptr random; +}; + +void MakeRandomFixture(const RandomTestData& data, RandomFixture& out) +{ + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()) << "Failed to create crypto stack"; + out.stack = std::move(stack_result.value()); + + auto ctx_result = out.stack->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()) << "Failed to create crypto context"; + out.ctx = std::move(ctx_result.value()); + + RandomContextConfig config; + if (!data.algorithm.empty()) + { + config.SetAlgorithm(data.algorithm); + } + if (data.provider_type.has_value()) + { + config.SetProviderType(data.provider_type.value()); + } + + auto random_result = out.ctx->CreateRandomContext(config); + ASSERT_TRUE(random_result.has_value()) << "Failed to create random context"; + out.random = std::move(random_result.value()); +} + +class RandomGenerationTest : public ::testing::TestWithParam +{ +}; + +TEST_P(RandomGenerationTest, GenerateAndSeed) +{ + const auto test_data = GetParam(); + + RandomFixture fixture; + ASSERT_NO_FATAL_FAILURE(MakeRandomFixture(test_data, fixture)); + auto& random = *fixture.random; + + // ========================================================================= + // 1. Generate the common sizes an application actually asks for + // ========================================================================= + // + // 12 and 16 bytes are AEAD nonces and CBC/CTR IVs, 32 bytes is an AES-256 + // key's worth of material, 1 byte exercises the smallest legal request. + for (const std::size_t size : {std::size_t{1U}, std::size_t{12U}, std::size_t{16U}, std::size_t{32U}}) + { + // Pre-fill with a recognisable pattern so a short write is detectable. + std::vector buffer(size, 0xAAU); + + auto generated = random.Generate({buffer.data(), buffer.size()}); + ASSERT_TRUE(generated.has_value()) << "Generate(" << size << ") failed"; + EXPECT_EQ(generated.value(), size) << "Generate must report exactly the requested byte count"; + + print_hex("Random", buffer, buffer.size()); + } + + // ========================================================================= + // 2. A large request is served in full + // ========================================================================= + { + constexpr std::size_t kLargeSize = 4096U; + std::vector buffer(kLargeSize, 0U); + + auto generated = random.Generate({buffer.data(), buffer.size()}); + ASSERT_TRUE(generated.has_value()) << "Generate(4096) failed"; + EXPECT_EQ(generated.value(), kLargeSize); + + // An all-zero 4 KiB block would mean the buffer was never written. The + // probability of a working RNG producing it is negligible. + const bool all_zero = std::all_of(buffer.begin(), buffer.end(), [](uint8_t b) { + return b == 0U; + }); + EXPECT_FALSE(all_zero) << "4 KiB of random data must not be all zero"; + } + + // ========================================================================= + // 3. Successive draws differ + // ========================================================================= + // + // Ten independent 32-byte draws must all be distinct. A generator that + // repeats within ten draws of 256 bits is broken, not unlucky. + { + constexpr std::size_t kDraws = 10U; + constexpr std::size_t kDrawSize = 32U; + std::set> seen; + + for (std::size_t i = 0U; i < kDraws; ++i) + { + std::vector buffer(kDrawSize, 0U); + auto generated = random.Generate({buffer.data(), buffer.size()}); + ASSERT_TRUE(generated.has_value()) << "Generate for draw " << i << " failed"; + seen.insert(std::move(buffer)); + } + + EXPECT_EQ(seen.size(), kDraws) << "Successive random draws must not repeat"; + } + + // ========================================================================= + // 4. Seeding with additional entropy + // ========================================================================= + // + // Seeding is advisory: a provider whose entropy source cannot be seeded + // externally reports success without changing state. Either way the call + // must succeed and the generator must keep working afterwards. + { + const std::vector seed{0x01U, + 0x02U, + 0x03U, + 0x04U, + 0x05U, + 0x06U, + 0x07U, + 0x08U, + 0x09U, + 0x0AU, + 0x0BU, + 0x0CU, + 0x0DU, + 0x0EU, + 0x0FU, + 0x10U}; + + auto seeded = random.Seed({seed.data(), seed.size()}); + ASSERT_TRUE(seeded.has_value()) << "Seed failed"; + + std::vector after_seed(32U, 0U); + auto generated = random.Generate({after_seed.data(), after_seed.size()}); + ASSERT_TRUE(generated.has_value()) << "Generate after Seed failed"; + EXPECT_EQ(generated.value(), after_seed.size()); + } + + // ========================================================================= + // 5. A zero-length request is a no-op, not an error + // ========================================================================= + { + std::vector empty; + auto generated = random.Generate({empty.data(), empty.size()}); + ASSERT_TRUE(generated.has_value()) << "Generate of zero bytes should succeed"; + EXPECT_EQ(generated.value(), 0U); + } +} + +// Random generation is provided only by the OpenSSL (software) provider, so +// every case pins ProviderType::kSoftware. Leaving the provider unset — or +// asking for kDefault — resolves to the daemon's DEFAULT provider, currently +// SoftHSM, which offers no random handler. +INSTANTIATE_TEST_SUITE_P( + SelectionOfProviderType, + RandomGenerationTest, + ::testing::Values(RandomTestData{"Random_SoftwareProvider", ProviderType::kSoftware, ""}, + RandomTestData{"Random_CtrDrbg_SoftwareProvider", ProviderType::kSoftware, "CTR-DRBG"}), + [](const testing::TestParamInfo& info) { + return info.param.test_case_name; + }); + +// ========================================================================= +// Two contexts draw independently +// ========================================================================= + +TEST(RandomIndependenceTest, TwoContextsProduceDifferentData) +{ + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()); + auto& stack = stack_result.value(); + + auto ctx_result = stack->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()); + auto& ctx = ctx_result.value(); + + RandomContextConfig config; + config.SetProviderType(ProviderType::kSoftware); // only provider offering an RNG + auto first = ctx->CreateRandomContext(config); + ASSERT_TRUE(first.has_value()); + auto second = ctx->CreateRandomContext(config); + ASSERT_TRUE(second.has_value()); + + std::vector a(32U, 0U); + std::vector b(32U, 0U); + ASSERT_TRUE(first.value()->Generate({a.data(), a.size()}).has_value()); + ASSERT_TRUE(second.value()->Generate({b.data(), b.size()}).has_value()); + + EXPECT_NE(a, b) << "Two random contexts must not return identical data"; +} + +} // namespace + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/score/tests/test_vectors/block_cipher/CBC-AES128/BUILD b/score/tests/test_vectors/block_cipher/CBC-AES128/BUILD new file mode 100644 index 000000000..2fef6ca02 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES128/BUILD @@ -0,0 +1,28 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "strip_prefix") + +filegroup( + name = "block_cipher_test_vectors", + srcs = glob(["*.bin"]), + visibility = ["//visibility:public"], +) + +pkg_files( + name = "block_cipher_test_vectors_pkg", + srcs = glob(["*.bin"]), + # preserve test_vectors/block_cipher/CBC-AES128/ folders + strip_prefix = strip_prefix.from_root("score/tests"), + visibility = ["//visibility:public"], +) diff --git a/score/tests/test_vectors/block_cipher/CBC-AES128/reference.md b/score/tests/test_vectors/block_cipher/CBC-AES128/reference.md new file mode 100644 index 000000000..cc9e91419 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES128/reference.md @@ -0,0 +1,47 @@ + + +# AES-128-CBC — NIST CAVP Multi-block Message Test vectors + +Source: +Archive: `aesmmt.zip`, file `CBCMMT128.rsp`, `[ENCRYPT]` section. + +Each vector is four files: `*_key.bin`, `*_iv.bin`, `*_plaintext.bin` and the +expected `*_ciphertext.bin`. + +## Padding + +CAVP plaintexts are whole blocks and the expected ciphertext carries **no +padding**. The cipher contexts of this stack always apply PKCS#7 padding, so +encrypting `*_plaintext.bin` yields `*_ciphertext.bin` **followed by one extra +padding block**: a test compares the leading `len(plaintext)` bytes against the +vector, then decrypts the full padded ciphertext to recover the plaintext. +Feeding `*_ciphertext.bin` to a decrypt context directly would fail the padding +check, which is expected and not a defect. + +### vector1 — CBCMMT128.rsp [ENCRYPT] COUNT = 0 + +``` +KEY = 1f8e4973953f3fb0bd6b16662e9a3c17 +IV = 2fe2b333ceda8f98f4a99b40d2cd34a8 +PLAINTEXT = 45cf12964fc824ab76616ae2f4bf0822 +CIPHERTEXT = 0f61c4d44c5147c03c195ad7e2cc12b2 +``` +### vector2 — CBCMMT128.rsp [ENCRYPT] COUNT = 3 + +``` +KEY = b7f3c9576e12dd0db63e8f8fac2b9a39 +IV = c80f095d8bb1a060699f7c19974a1aa0 +PLAINTEXT = 9ac19954ce1319b354d3220460f71c1e373f1cd336240881160cfde46ebfed2e791e8d5a1a136ebd1dc469dec00c4187722b841cdabcb22c1be8a14657da200e +CIPHERTEXT = 19b9609772c63f338608bf6eb52ca10be65097f89c1e0905c42401fd47791ae2c5440b2d473116ca78bd9ff2fb6015cfd316524eae7dcb95ae738ebeae84a467 +``` diff --git a/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_ciphertext.bin b/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_ciphertext.bin new file mode 100644 index 000000000..45ea5eca4 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_ciphertext.bin @@ -0,0 +1 @@ +aLQG<Z \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_iv.bin b/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_iv.bin new file mode 100644 index 000000000..d676f355f --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_iv.bin @@ -0,0 +1 @@ +/3ڏ@4 \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_key.bin b/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_key.bin new file mode 100644 index 000000000..8dae0b3e4 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_key.bin @@ -0,0 +1 @@ +Is??kf.< \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_plaintext.bin b/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_plaintext.bin new file mode 100644 index 000000000..c1e7a6f81 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES128/vector1_plaintext.bin @@ -0,0 +1 @@ +EO$vaj" \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_ciphertext.bin b/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_ciphertext.bin new file mode 100644 index 000000000..7bd87f6c1 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_ciphertext.bin @@ -0,0 +1 @@ +`r?3n, P $GyD -G1x`RN}˕sg \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_iv.bin b/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_iv.bin new file mode 100644 index 000000000..f58e954f9 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_iv.bin @@ -0,0 +1 @@ + ]`i|J \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_key.bin b/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_key.bin new file mode 100644 index 000000000..804b8d421 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_key.bin @@ -0,0 +1 @@ +Wn >+9 \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_plaintext.bin b/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_plaintext.bin new file mode 100644 index 000000000..32b083320 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES128/vector2_plaintext.bin @@ -0,0 +1 @@ +TT"`7?6$ n.yZni Ar+ڼ,FW  \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES192/BUILD b/score/tests/test_vectors/block_cipher/CBC-AES192/BUILD new file mode 100644 index 000000000..2507c0d96 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES192/BUILD @@ -0,0 +1,28 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "strip_prefix") + +filegroup( + name = "block_cipher_test_vectors", + srcs = glob(["*.bin"]), + visibility = ["//visibility:public"], +) + +pkg_files( + name = "block_cipher_test_vectors_pkg", + srcs = glob(["*.bin"]), + # preserve test_vectors/block_cipher/CBC-AES192/ folders + strip_prefix = strip_prefix.from_root("score/tests"), + visibility = ["//visibility:public"], +) diff --git a/score/tests/test_vectors/block_cipher/CBC-AES192/reference.md b/score/tests/test_vectors/block_cipher/CBC-AES192/reference.md new file mode 100644 index 000000000..ae7bb4caa --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES192/reference.md @@ -0,0 +1,47 @@ + + +# AES-192-CBC — NIST CAVP Multi-block Message Test vectors + +Source: +Archive: `aesmmt.zip`, file `CBCMMT192.rsp`, `[ENCRYPT]` section. + +Each vector is four files: `*_key.bin`, `*_iv.bin`, `*_plaintext.bin` and the +expected `*_ciphertext.bin`. + +## Padding + +CAVP plaintexts are whole blocks and the expected ciphertext carries **no +padding**. The cipher contexts of this stack always apply PKCS#7 padding, so +encrypting `*_plaintext.bin` yields `*_ciphertext.bin` **followed by one extra +padding block**: a test compares the leading `len(plaintext)` bytes against the +vector, then decrypts the full padded ciphertext to recover the plaintext. +Feeding `*_ciphertext.bin` to a decrypt context directly would fail the padding +check, which is expected and not a defect. + +### vector1 — CBCMMT192.rsp [ENCRYPT] COUNT = 0 + +``` +KEY = ba75f4d1d9d7cf7f551445d56cc1a8ab2a078e15e049dc2c +IV = 531ce78176401666aa30db94ec4a30eb +PLAINTEXT = c51fc276774dad94bcdc1d2891ec8668 +CIPHERTEXT = 70dd95a14ee975e239df36ff4aee1d5d +``` +### vector2 — CBCMMT192.rsp [ENCRYPT] COUNT = 3 + +``` +KEY = 067bb17b4df785697eaccf961f98e212cb75e6797ce935cb +IV = 8b59c9209c529ca8391c9fc0ce033c38 +PLAINTEXT = db3785a889b4bd387754da222f0e4c2d2bfe0d79e05bc910fba941beea30f1239eacf0068f4619ec01c368e986fca6b7c58e490579d29611bd10087986eff54f +CIPHERTEXT = d5f5589760bf9c762228fde236de1fa2dd2dad448db3fa9be0c4196efd46a35c84dd1ac77d9db58c95918cb317a6430a08d2fb6a8e8b0f1c9b72c7a344dc349f +``` diff --git a/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_ciphertext.bin b/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_ciphertext.bin new file mode 100644 index 000000000..b49e69c17 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_ciphertext.bin @@ -0,0 +1 @@ +pݕNu96J] \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_iv.bin b/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_iv.bin new file mode 100644 index 000000000..87bd6df1f --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_iv.bin @@ -0,0 +1 @@ +Sv@f0۔J0 \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_key.bin b/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_key.bin new file mode 100644 index 000000000..b21a47e89 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_key.bin @@ -0,0 +1 @@ +uUEl*I, \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_plaintext.bin b/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_plaintext.bin new file mode 100644 index 000000000..10be28112 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES192/vector1_plaintext.bin @@ -0,0 +1 @@ +vwM(h \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_ciphertext.bin b/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_ciphertext.bin new file mode 100644 index 000000000..4bc39fee9 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_ciphertext.bin @@ -0,0 +1,2 @@ +X`v"(6-DnF\}C +jrǣD4 \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_iv.bin b/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_iv.bin new file mode 100644 index 000000000..099828fdf --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_iv.bin @@ -0,0 +1 @@ +Y R9<8 \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_key.bin b/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_key.bin new file mode 100644 index 000000000..de4da4d4e --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_key.bin @@ -0,0 +1 @@ +{{Mi~ϖuy|5 \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_plaintext.bin b/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_plaintext.bin new file mode 100644 index 000000000..c42aec2ae --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES192/vector2_plaintext.bin @@ -0,0 +1 @@ +78wT"/L-+ y[A0#FhŎIyҖyO \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES256/BUILD b/score/tests/test_vectors/block_cipher/CBC-AES256/BUILD new file mode 100644 index 000000000..6951da9c9 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES256/BUILD @@ -0,0 +1,28 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "strip_prefix") + +filegroup( + name = "block_cipher_test_vectors", + srcs = glob(["*.bin"]), + visibility = ["//visibility:public"], +) + +pkg_files( + name = "block_cipher_test_vectors_pkg", + srcs = glob(["*.bin"]), + # preserve test_vectors/block_cipher/CBC-AES256/ folders + strip_prefix = strip_prefix.from_root("score/tests"), + visibility = ["//visibility:public"], +) diff --git a/score/tests/test_vectors/block_cipher/CBC-AES256/reference.md b/score/tests/test_vectors/block_cipher/CBC-AES256/reference.md new file mode 100644 index 000000000..4d5aa5663 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES256/reference.md @@ -0,0 +1,47 @@ + + +# AES-256-CBC — NIST CAVP Multi-block Message Test vectors + +Source: +Archive: `aesmmt.zip`, file `CBCMMT256.rsp`, `[ENCRYPT]` section. + +Each vector is four files: `*_key.bin`, `*_iv.bin`, `*_plaintext.bin` and the +expected `*_ciphertext.bin`. + +## Padding + +CAVP plaintexts are whole blocks and the expected ciphertext carries **no +padding**. The cipher contexts of this stack always apply PKCS#7 padding, so +encrypting `*_plaintext.bin` yields `*_ciphertext.bin` **followed by one extra +padding block**: a test compares the leading `len(plaintext)` bytes against the +vector, then decrypts the full padded ciphertext to recover the plaintext. +Feeding `*_ciphertext.bin` to a decrypt context directly would fail the padding +check, which is expected and not a defect. + +### vector1 — CBCMMT256.rsp [ENCRYPT] COUNT = 0 + +``` +KEY = 6ed76d2d97c69fd1339589523931f2a6cff554b15f738f21ec72dd97a7330907 +IV = 851e8764776e6796aab722dbb644ace8 +PLAINTEXT = 6282b8c05c5c1530b97d4816ca434762 +CIPHERTEXT = 6acc04142e100a65f51b97adf5172c41 +``` +### vector2 — CBCMMT256.rsp [ENCRYPT] COUNT = 3 + +``` +KEY = 0493ff637108af6a5b8e90ac1fdf035a3d4bafd1afb573be7ade9e8682e663e5 +IV = c0cd2bebccbb6c49920bd5482ac756e8 +PLAINTEXT = 8b37f9148df4bb25956be6310c73c8dc58ea9714ff49b643107b34c9bff096a94fedd6823526abc27a8e0b16616eee254ab4567dd68e8ccd4c38ac563b13639c +CIPHERTEXT = 05d5c77729421b08b737e41119fa4438d1f570cc772a4d6c3df7ffeda0384ef84288ce37fc4c4c7d1125a499b051364c389fd639bdda647daa3bdadab2eb5594 +``` diff --git a/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_ciphertext.bin b/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_ciphertext.bin new file mode 100644 index 000000000..0919a2f8e --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_ciphertext.bin @@ -0,0 +1,2 @@ +j. +e,A \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_iv.bin b/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_iv.bin new file mode 100644 index 000000000..cfe006547 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_iv.bin @@ -0,0 +1 @@ +dwng"۶D \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_key.bin b/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_key.bin new file mode 100644 index 000000000..c59575a82 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_key.bin @@ -0,0 +1 @@ +nm-Ɵ3R91T_s!rݗ3  \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_plaintext.bin b/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_plaintext.bin new file mode 100644 index 000000000..b5afb28cf --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES256/vector1_plaintext.bin @@ -0,0 +1 @@ +b\\0}HCGb \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_ciphertext.bin b/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_ciphertext.bin new file mode 100644 index 000000000..7111e9073 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_ciphertext.bin @@ -0,0 +1 @@ +w)B7D8pw*Ml=8NB7LL}%Q6L89d};ڲU \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_iv.bin b/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_iv.bin new file mode 100644 index 000000000..c49bf8f70 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_iv.bin @@ -0,0 +1 @@ ++̻lI H*V \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_key.bin b/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_key.bin new file mode 100644 index 000000000..ce2858764 --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_key.bin @@ -0,0 +1 @@ +cqj[Z=Kѯszޞc \ No newline at end of file diff --git a/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_plaintext.bin b/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_plaintext.bin new file mode 100644 index 000000000..6873047fc --- /dev/null +++ b/score/tests/test_vectors/block_cipher/CBC-AES256/vector2_plaintext.bin @@ -0,0 +1 @@ +7%k1 sXIC{4ɿOւ5&z an%JV}֎L8V;c \ No newline at end of file diff --git a/score/tests/test_vectors/config/BUILD b/score/tests/test_vectors/config/BUILD index 3521f7541..4a79fa01e 100644 --- a/score/tests/test_vectors/config/BUILD +++ b/score/tests/test_vectors/config/BUILD @@ -25,9 +25,7 @@ genrule( filegroup( name = "integration_test_config", - srcs = [ - "integration_openssl_hmac.kv", - "integration_softhsm_hmac.kv", + srcs = glob(["*.kv"]) + [ ":generate_integration_test_config", ], visibility = ["//:__subpackages__"], diff --git a/score/tests/test_vectors/config/integration_openssl_aes128_cbc.kv b/score/tests/test_vectors/config/integration_openssl_aes128_cbc.kv new file mode 100644 index 000000000..a9ce13f2b --- /dev/null +++ b/score/tests/test_vectors/config/integration_openssl_aes128_cbc.kv @@ -0,0 +1,5 @@ +[metadata] +availability=active +[key] +key_path=/opt/crypto/share/test_vectors/block_cipher/CBC-AES128/vector2_key.bin +key_format=raw diff --git a/score/tests/test_vectors/config/integration_openssl_aes192_cbc.kv b/score/tests/test_vectors/config/integration_openssl_aes192_cbc.kv new file mode 100644 index 000000000..9589bb7c9 --- /dev/null +++ b/score/tests/test_vectors/config/integration_openssl_aes192_cbc.kv @@ -0,0 +1,5 @@ +[metadata] +availability=active +[key] +key_path=/opt/crypto/share/test_vectors/block_cipher/CBC-AES192/vector2_key.bin +key_format=raw diff --git a/score/tests/test_vectors/config/integration_openssl_aes256_cbc.kv b/score/tests/test_vectors/config/integration_openssl_aes256_cbc.kv new file mode 100644 index 000000000..905ffbf39 --- /dev/null +++ b/score/tests/test_vectors/config/integration_openssl_aes256_cbc.kv @@ -0,0 +1,5 @@ +[metadata] +availability=active +[key] +key_path=/opt/crypto/share/test_vectors/block_cipher/CBC-AES256/vector2_key.bin +key_format=raw diff --git a/score/tests/test_vectors/config/integration_openssl_ecdsa_p256.kv b/score/tests/test_vectors/config/integration_openssl_ecdsa_p256.kv new file mode 100644 index 000000000..c50cfc229 --- /dev/null +++ b/score/tests/test_vectors/config/integration_openssl_ecdsa_p256.kv @@ -0,0 +1,5 @@ +[metadata] +availability=active +[key] +key_path=/opt/crypto/share/test_vectors/ecdsa/P256-SHA256/siggen_private_key.der +key_format=der diff --git a/score/tests/test_vectors/config/integration_openssl_ecdsa_p256_pub.kv b/score/tests/test_vectors/config/integration_openssl_ecdsa_p256_pub.kv new file mode 100644 index 000000000..4b007bbef --- /dev/null +++ b/score/tests/test_vectors/config/integration_openssl_ecdsa_p256_pub.kv @@ -0,0 +1,5 @@ +[metadata] +availability=active +[key] +key_path=/opt/crypto/share/test_vectors/ecdsa/P256-SHA256/sigver_invalid_public_key.der +key_format=der diff --git a/score/tests/test_vectors/config/integration_openssl_ecdsa_p384.kv b/score/tests/test_vectors/config/integration_openssl_ecdsa_p384.kv new file mode 100644 index 000000000..449803e37 --- /dev/null +++ b/score/tests/test_vectors/config/integration_openssl_ecdsa_p384.kv @@ -0,0 +1,5 @@ +[metadata] +availability=active +[key] +key_path=/opt/crypto/share/test_vectors/ecdsa/P384-SHA384/siggen_private_key.der +key_format=der diff --git a/score/tests/test_vectors/config/integration_openssl_ecdsa_p384_pub.kv b/score/tests/test_vectors/config/integration_openssl_ecdsa_p384_pub.kv new file mode 100644 index 000000000..bbd52137d --- /dev/null +++ b/score/tests/test_vectors/config/integration_openssl_ecdsa_p384_pub.kv @@ -0,0 +1,5 @@ +[metadata] +availability=active +[key] +key_path=/opt/crypto/share/test_vectors/ecdsa/P384-SHA384/sigver_invalid_public_key.der +key_format=der diff --git a/score/tests/test_vectors/config/integration_openssl_ecdsa_p521.kv b/score/tests/test_vectors/config/integration_openssl_ecdsa_p521.kv new file mode 100644 index 000000000..e893a10f2 --- /dev/null +++ b/score/tests/test_vectors/config/integration_openssl_ecdsa_p521.kv @@ -0,0 +1,5 @@ +[metadata] +availability=active +[key] +key_path=/opt/crypto/share/test_vectors/ecdsa/P521-SHA512/siggen_private_key.der +key_format=der diff --git a/score/tests/test_vectors/config/integration_openssl_ecdsa_p521_pub.kv b/score/tests/test_vectors/config/integration_openssl_ecdsa_p521_pub.kv new file mode 100644 index 000000000..9ab883b11 --- /dev/null +++ b/score/tests/test_vectors/config/integration_openssl_ecdsa_p521_pub.kv @@ -0,0 +1,5 @@ +[metadata] +availability=active +[key] +key_path=/opt/crypto/share/test_vectors/ecdsa/P521-SHA512/sigver_invalid_public_key.der +key_format=der diff --git a/score/tests/test_vectors/config/integration_test_config.json b/score/tests/test_vectors/config/integration_test_config.json index 54d039393..e9c254b0e 100644 --- a/score/tests/test_vectors/config/integration_test_config.json +++ b/score/tests/test_vectors/config/integration_test_config.json @@ -1,109 +1,329 @@ -{ - "key_slot_config": - { - "slot_entries": [ - { - "slot_name": "test/AES-256", - "algorithm": "AES-256", - "provider_names": ["OPENSSL"], - "allowed_operations": "MAC", - "allowed_uids": [0, 1000], - "allowed_write_uids": [0], - "deployment_path": "/opt/crypto/etc/test_hmac_sha256.kv", - "deployment_format": "kv" - }, - { - "slot_name": "test/aes-256-cmac", - "algorithm": "AES-256", - "provider_names": ["OPENSSL"], - "allowed_operations": "MAC", - "allowed_uids": [0, 1000], - "allowed_write_uids": [0], - "deployment_path" : "/opt/crypto/etc/test_aes256_cmac.kv", - "deployment_format" : "kv" - }, - { - "slot_name": "test/pkcs11-AES-256", - "algorithm": "AES-256", - "provider_names": ["PKCS11_ENGINE"], - "allowed_operations": "MAC", - "allowed_uids": [0, 1000], - "allowed_write_uids": [0], - "deployment_path" : "/opt/crypto/etc/test_pkcs11_hmac.kv", - "deployment_format" : "kv" - }, - { - "slot_name": "HMAC_SHA256_IntegrationTestKey_OpenSSL", - "algorithm": "AES-256", - "provider_names": ["OPENSSL"], - "allowed_operations": "MAC", - "allowed_uids": [0, 1000], - "allowed_write_uids": [0], - "deployment_path" : "/opt/crypto/etc/integration_openssl_hmac.kv", - "deployment_format" : "kv" - }, - { - "slot_name": "HMAC_SHA256_IntegrationTestKey", - "algorithm": "AES-256", - "provider_names": ["PKCS11_ENGINE"], - "allowed_operations": "MAC", - "allowed_uids": [0, 1000], - "allowed_write_uids": [0], - "deployment_path" : "/opt/crypto/etc/integration_softhsm_hmac.kv", - "deployment_format" : "kv" - } - ], - "app_resource_entries": [ - { - "uid": 0, - "app_resource_id": "test/hmac", - "slot_name": "test/hmac-sha256" - }, - { - "uid": 0, - "app_resource_id": "test/cmac", - "slot_name": "test/aes-256-cmac" - }, - { - "uid": 0, - "app_resource_id": "test/pkcs11/hmac", - "slot_name": "test/pkcs11-hmac-sha256" - }, - { - "uid": 0, - "app_resource_id": "HMAC_SHA256_IntegrationTestKey_OpenSSL", - "slot_name": "HMAC_SHA256_IntegrationTestKey_OpenSSL" - }, - { - "uid": 0, - "app_resource_id": "HMAC_SHA256_IntegrationTestKey", - "slot_name": "HMAC_SHA256_IntegrationTestKey" - }, - { - "uid": 1000, - "app_resource_id": "test/hmac", - "slot_name": "test/hmac-sha256" - }, - { - "uid": 1000, - "app_resource_id": "test/cmac", - "slot_name": "test/aes-256-cmac" - }, - { - "uid": 1000, - "app_resource_id": "test/pkcs11/hmac", - "slot_name": "test/pkcs11-hmac-sha256" - }, - { - "uid": 1000, - "app_resource_id": "HMAC_SHA256_IntegrationTestKey_OpenSSL", - "slot_name": "HMAC_SHA256_IntegrationTestKey_OpenSSL" - }, - { - "uid": 1000, - "app_resource_id": "HMAC_SHA256_IntegrationTestKey", - "slot_name": "HMAC_SHA256_IntegrationTestKey" - } - ] - } -} +{ + "key_slot_config": + { + "slot_entries": [ + { + "slot_name": "test/AES-256", + "algorithm": "AES-256", + "provider_names": ["OPENSSL"], + "allowed_operations": "MAC", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path": "/opt/crypto/etc/test_hmac_sha256.kv", + "deployment_format": "kv" + }, + { + "slot_name": "test/aes-256-cmac", + "algorithm": "AES-256", + "provider_names": ["OPENSSL"], + "allowed_operations": "MAC", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/test_aes256_cmac.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "test/pkcs11-AES-256", + "algorithm": "AES-256", + "provider_names": ["PKCS11_ENGINE"], + "allowed_operations": "MAC", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/test_pkcs11_hmac.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "HMAC_SHA256_IntegrationTestKey_OpenSSL", + "algorithm": "AES-256", + "provider_names": ["OPENSSL"], + "allowed_operations": "MAC", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_openssl_hmac.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "HMAC_SHA256_IntegrationTestKey", + "algorithm": "AES-256", + "provider_names": ["PKCS11_ENGINE"], + "allowed_operations": "MAC", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_softhsm_hmac.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "HMAC_SHA256_SignOnlySlot_OpenSSL", + "algorithm": "AES-256", + "provider_names": ["OPENSSL"], + "allowed_operations": "SIGN", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_openssl_hmac.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "HMAC_SHA256_SignOnlySlot", + "algorithm": "AES-256", + "provider_names": ["PKCS11_ENGINE"], + "allowed_operations": "SIGN", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_softhsm_hmac.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "AES128_CBC_IntegrationTestKey_OpenSSL", + "algorithm": "AES-128-CBC", + "provider_names": ["OPENSSL"], + "allowed_operations": "ENCRYPT|DECRYPT", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_openssl_aes128_cbc.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "AES192_CBC_IntegrationTestKey_OpenSSL", + "algorithm": "AES-192-CBC", + "provider_names": ["OPENSSL"], + "allowed_operations": "ENCRYPT|DECRYPT", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_openssl_aes192_cbc.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "AES256_CBC_IntegrationTestKey_OpenSSL", + "algorithm": "AES-256-CBC", + "provider_names": ["OPENSSL"], + "allowed_operations": "ENCRYPT|DECRYPT", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_openssl_aes256_cbc.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "ECDSA_P256_IntegrationTestKey_OpenSSL", + "algorithm": "ECDSA-P256", + "provider_names": ["OPENSSL"], + "allowed_operations": "SIGN|VERIFY", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_openssl_ecdsa_p256.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "ECDSA_P384_IntegrationTestKey_OpenSSL", + "algorithm": "ECDSA-P384", + "provider_names": ["OPENSSL"], + "allowed_operations": "SIGN|VERIFY", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_openssl_ecdsa_p384.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "ECDSA_P521_IntegrationTestKey_OpenSSL", + "algorithm": "ECDSA-P521", + "provider_names": ["OPENSSL"], + "allowed_operations": "SIGN|VERIFY", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_openssl_ecdsa_p521.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "ECDSA_P256_PublicKeySlot_OpenSSL", + "algorithm": "ECDSA-P256", + "provider_names": ["OPENSSL"], + "allowed_operations": "VERIFY", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_openssl_ecdsa_p256_pub.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "ECDSA_P384_PublicKeySlot_OpenSSL", + "algorithm": "ECDSA-P384", + "provider_names": ["OPENSSL"], + "allowed_operations": "VERIFY", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_openssl_ecdsa_p384_pub.kv", + "deployment_format" : "kv" + }, + { + "slot_name": "ECDSA_P521_PublicKeySlot_OpenSSL", + "algorithm": "ECDSA-P521", + "provider_names": ["OPENSSL"], + "allowed_operations": "VERIFY", + "allowed_uids": [0, 1000], + "allowed_write_uids": [0], + "deployment_path" : "/opt/crypto/etc/integration_openssl_ecdsa_p521_pub.kv", + "deployment_format" : "kv" + } + ], + "app_resource_entries": [ + { + "uid": 0, + "app_resource_id": "test/hmac", + "slot_name": "test/hmac-sha256" + }, + { + "uid": 0, + "app_resource_id": "test/cmac", + "slot_name": "test/aes-256-cmac" + }, + { + "uid": 0, + "app_resource_id": "test/pkcs11/hmac", + "slot_name": "test/pkcs11-hmac-sha256" + }, + { + "uid": 0, + "app_resource_id": "HMAC_SHA256_IntegrationTestKey_OpenSSL", + "slot_name": "HMAC_SHA256_IntegrationTestKey_OpenSSL" + }, + { + "uid": 0, + "app_resource_id": "HMAC_SHA256_IntegrationTestKey", + "slot_name": "HMAC_SHA256_IntegrationTestKey" + }, + { + "uid": 0, + "app_resource_id": "HMAC_SHA256_SignOnlySlot_OpenSSL", + "slot_name": "HMAC_SHA256_SignOnlySlot_OpenSSL" + }, + { + "uid": 0, + "app_resource_id": "HMAC_SHA256_SignOnlySlot", + "slot_name": "HMAC_SHA256_SignOnlySlot" + }, + { + "uid": 1000, + "app_resource_id": "test/hmac", + "slot_name": "test/hmac-sha256" + }, + { + "uid": 1000, + "app_resource_id": "test/cmac", + "slot_name": "test/aes-256-cmac" + }, + { + "uid": 1000, + "app_resource_id": "test/pkcs11/hmac", + "slot_name": "test/pkcs11-hmac-sha256" + }, + { + "uid": 1000, + "app_resource_id": "HMAC_SHA256_IntegrationTestKey_OpenSSL", + "slot_name": "HMAC_SHA256_IntegrationTestKey_OpenSSL" + }, + { + "uid": 1000, + "app_resource_id": "HMAC_SHA256_IntegrationTestKey", + "slot_name": "HMAC_SHA256_IntegrationTestKey" + }, + { + "uid": 1000, + "app_resource_id": "HMAC_SHA256_SignOnlySlot_OpenSSL", + "slot_name": "HMAC_SHA256_SignOnlySlot_OpenSSL" + }, + { + "uid": 1000, + "app_resource_id": "HMAC_SHA256_SignOnlySlot", + "slot_name": "HMAC_SHA256_SignOnlySlot" + }, + { + "uid": 0, + "app_resource_id": "AES128_CBC_IntegrationTestKey_OpenSSL", + "slot_name": "AES128_CBC_IntegrationTestKey_OpenSSL" + }, + { + "uid": 0, + "app_resource_id": "AES192_CBC_IntegrationTestKey_OpenSSL", + "slot_name": "AES192_CBC_IntegrationTestKey_OpenSSL" + }, + { + "uid": 0, + "app_resource_id": "AES256_CBC_IntegrationTestKey_OpenSSL", + "slot_name": "AES256_CBC_IntegrationTestKey_OpenSSL" + }, + { + "uid": 0, + "app_resource_id": "ECDSA_P256_IntegrationTestKey_OpenSSL", + "slot_name": "ECDSA_P256_IntegrationTestKey_OpenSSL" + }, + { + "uid": 0, + "app_resource_id": "ECDSA_P384_IntegrationTestKey_OpenSSL", + "slot_name": "ECDSA_P384_IntegrationTestKey_OpenSSL" + }, + { + "uid": 0, + "app_resource_id": "ECDSA_P521_IntegrationTestKey_OpenSSL", + "slot_name": "ECDSA_P521_IntegrationTestKey_OpenSSL" + }, + { + "uid": 0, + "app_resource_id": "ECDSA_P256_PublicKeySlot_OpenSSL", + "slot_name": "ECDSA_P256_PublicKeySlot_OpenSSL" + }, + { + "uid": 0, + "app_resource_id": "ECDSA_P384_PublicKeySlot_OpenSSL", + "slot_name": "ECDSA_P384_PublicKeySlot_OpenSSL" + }, + { + "uid": 0, + "app_resource_id": "ECDSA_P521_PublicKeySlot_OpenSSL", + "slot_name": "ECDSA_P521_PublicKeySlot_OpenSSL" + }, + { + "uid": 1000, + "app_resource_id": "AES128_CBC_IntegrationTestKey_OpenSSL", + "slot_name": "AES128_CBC_IntegrationTestKey_OpenSSL" + }, + { + "uid": 1000, + "app_resource_id": "AES192_CBC_IntegrationTestKey_OpenSSL", + "slot_name": "AES192_CBC_IntegrationTestKey_OpenSSL" + }, + { + "uid": 1000, + "app_resource_id": "AES256_CBC_IntegrationTestKey_OpenSSL", + "slot_name": "AES256_CBC_IntegrationTestKey_OpenSSL" + }, + { + "uid": 1000, + "app_resource_id": "ECDSA_P256_IntegrationTestKey_OpenSSL", + "slot_name": "ECDSA_P256_IntegrationTestKey_OpenSSL" + }, + { + "uid": 1000, + "app_resource_id": "ECDSA_P384_IntegrationTestKey_OpenSSL", + "slot_name": "ECDSA_P384_IntegrationTestKey_OpenSSL" + }, + { + "uid": 1000, + "app_resource_id": "ECDSA_P521_IntegrationTestKey_OpenSSL", + "slot_name": "ECDSA_P521_IntegrationTestKey_OpenSSL" + }, + { + "uid": 1000, + "app_resource_id": "ECDSA_P256_PublicKeySlot_OpenSSL", + "slot_name": "ECDSA_P256_PublicKeySlot_OpenSSL" + }, + { + "uid": 1000, + "app_resource_id": "ECDSA_P384_PublicKeySlot_OpenSSL", + "slot_name": "ECDSA_P384_PublicKeySlot_OpenSSL" + }, + { + "uid": 1000, + "app_resource_id": "ECDSA_P521_PublicKeySlot_OpenSSL", + "slot_name": "ECDSA_P521_PublicKeySlot_OpenSSL" + } + ] + } +} diff --git a/score/tests/test_vectors/ecdsa/BUILD b/score/tests/test_vectors/ecdsa/BUILD new file mode 100644 index 000000000..d704e9e85 --- /dev/null +++ b/score/tests/test_vectors/ecdsa/BUILD @@ -0,0 +1,36 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "strip_prefix") + +# NIST FIPS 186-4 ECDSA test vectors, one directory per curve/digest pairing. + +filegroup( + name = "ecdsa_test_vectors", + srcs = glob([ + "*/*.bin", + "*/*.der", + ]), + visibility = ["//visibility:public"], +) + +pkg_files( + name = "ecdsa_test_vectors_pkg", + srcs = glob([ + "*/*.bin", + "*/*.der", + ]), + # preserve test_vectors/ecdsa// folders + strip_prefix = strip_prefix.from_root("score/tests"), + visibility = ["//visibility:public"], +) diff --git a/score/tests/test_vectors/ecdsa/P256-SHA256/reference.md b/score/tests/test_vectors/ecdsa/P256-SHA256/reference.md new file mode 100644 index 000000000..667920564 --- /dev/null +++ b/score/tests/test_vectors/ecdsa/P256-SHA256/reference.md @@ -0,0 +1,54 @@ + + +# ECDSA P256 / SHA256 — NIST FIPS 186-4 CAVP vectors + +Source: +Archive: `186-4ecdsatestvectors.zip`. + +## Positive case — `SigGen.txt`, section `[P-256,SHA-256]`, first record + +`siggen_private_key.der` is the DER (PKCS#8) encoding of the private key `d`, +`siggen_public_key.der` the SubjectPublicKeyInfo of `(Qx, Qy)`, and +`siggen_signature.bin` the vector's `(R, S)` in the fixed-length IEEE P1363 +form `r||s` (32 + 32 bytes) that this stack uses on the wire. + +``` +Msg = 5905238877c77421f73e43ee3da6f2d9e2ccad5fc942dcec0cbd25482935faaf416983fe165b1a045ee2bcd2e6dca3bdf46c4310a7461f9a37960ca672d3feb5473e253605fb1ddfd28065b53cb5858a8ad28175bf9bd386a5e471ea7a65c17cc934a9d791e91491eb3754d03799790fe2d308d16146d5c9b0d0debd97d79ce8 +d = 519b423d715f8b581f4fa8ee59f4771a5b44c8130b4e3eacca54a56dda72b464 +Qx = 1ccbe91c075fc7f4f033bfa248db8fccd3565de94bbfb12f3c59ff46c271bf83 +Qy = ce4014c68811f9a21a1fdb2c0e6113e06db7ca93b7404e78dc7ccd5ca89a4ca9 +R = f3ac8061b514795b8843e3d6629527ed2afd6b1f6a555a7acabb5e6f79c8c2ac +S = 8bf77819ca05a6b2786c76262bf7371cef97b218e96f175a3ccdda2acc058903 +``` + +ECDSA is randomised, so re-signing `siggen_message.bin` does **not** reproduce +`siggen_signature.bin` — NIST's per-message secret `k` is not settable through +this API. The vector is therefore used for *verification*: the published +signature must verify under the published key, and a signature this stack +produces must verify too. + +## Negative case — `SigVer.rsp`, section `[P-256,SHA-256]`, first `Result = F` record + +``` +Msg = e4796db5f785f207aa30d311693b3702821dff1168fd2e04c0836825aefd850d9aa60326d88cde1a23c7745351392ca2288d632c264f197d05cd424a30336c19fd09bb229654f0222fcb881a4b35c290a093ac159ce13409111ff0358411133c24f5b8e2090d6db6558afc36f06ca1f6ef779785adba68db27a409859fc4c4a0 +Qx = 87f8f2b218f49845f6f10eec3877136269f5c1a54736dbdf69f89940cad41555 +Qy = e15f369036f49842fac7a86c8a2b0557609776814448b8f5e84aa9f4395205e9 +R = d19ff48b324915576416097d2544f7cbdf8768b1454ad20e0baac50e211f23b0 +S = a3e81e59311cdfff2d4784949f7a2cb50ba6c3a91fa54710568e61aca3e847c6 +Result = F (3 - S changed) +``` + +Verification of `sigver_invalid_signature.bin` under +`sigver_invalid_public_key.der` must fail. Unlike a bit-flipped signature this +is a well-formed value NIST authored to be rejected. diff --git a/score/tests/test_vectors/ecdsa/P256-SHA256/siggen_message.bin b/score/tests/test_vectors/ecdsa/P256-SHA256/siggen_message.bin new file mode 100644 index 000000000..d9096e527 --- /dev/null +++ b/score/tests/test_vectors/ecdsa/P256-SHA256/siggen_message.bin @@ -0,0 +1 @@ +Y#wt!>C=̭_B %H)5Ai[^ܣlCF7 rG>%6Ҁe<ҁuӆqze|4ב7T7yaFɰ޽ל \ No newline at end of file diff --git a/score/tests/test_vectors/ecdsa/P256-SHA256/siggen_private_key.der b/score/tests/test_vectors/ecdsa/P256-SHA256/siggen_private_key.der new file mode 100644 index 0000000000000000000000000000000000000000..c27ff7d98f05f0548abf5b32171a62f50e3dd32c GIT binary patch literal 138 zcmV;50CoQ`frkPC05B5<2P%e0&OHJF1_&yKNX|V20S5$aFlzz<0R$jXn?gNtUyE2D zPpIx$^miItM932hPCl&4RHbd&a~Zug(L92Wc1d;dm# uo5=rehYI&MpK}m7)*<+Fk(B&x9lk{22f5o%P2TR{S8=E2Y|M&TJ}Uw6G$ybB literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/ecdsa/P256-SHA256/siggen_signature.bin b/score/tests/test_vectors/ecdsa/P256-SHA256/siggen_signature.bin new file mode 100644 index 000000000..76aed5fc9 --- /dev/null +++ b/score/tests/test_vectors/ecdsa/P256-SHA256/siggen_signature.bin @@ -0,0 +1 @@ +ay[Cb'*kjUZzʻ^oy¬xxlv&+7oZ<* \ No newline at end of file diff --git a/score/tests/test_vectors/ecdsa/P256-SHA256/sigver_invalid_message.bin b/score/tests/test_vectors/ecdsa/P256-SHA256/sigver_invalid_message.bin new file mode 100644 index 000000000..858529e78 --- /dev/null +++ b/score/tests/test_vectors/ecdsa/P256-SHA256/sigver_invalid_message.bin @@ -0,0 +1 @@ +ym0i;7h.h% &،#tSQ9,(c,&O}BJ03l "T"/ˈK54 5<$ mU6lwh' Ġ \ No newline at end of file diff --git a/score/tests/test_vectors/ecdsa/P256-SHA256/sigver_invalid_public_key.der b/score/tests/test_vectors/ecdsa/P256-SHA256/sigver_invalid_public_key.der new file mode 100644 index 0000000000000000000000000000000000000000..c41e82e3b8059f873482eb661c3ae14b64800ad0 GIT binary patch literal 91 zcmXqrG!SNE*J|@PXUoLM#sOw9GqN)~F|f4%__Rsl%M90VANk%`lnWx!R*Tnr(efc + +# ECDSA P384 / SHA384 — NIST FIPS 186-4 CAVP vectors + +Source: +Archive: `186-4ecdsatestvectors.zip`. + +## Positive case — `SigGen.txt`, section `[P-384,SHA-384]`, first record + +`siggen_private_key.der` is the DER (PKCS#8) encoding of the private key `d`, +`siggen_public_key.der` the SubjectPublicKeyInfo of `(Qx, Qy)`, and +`siggen_signature.bin` the vector's `(R, S)` in the fixed-length IEEE P1363 +form `r||s` (48 + 48 bytes) that this stack uses on the wire. + +``` +Msg = 6b45d88037392e1371d9fd1cd174e9c1838d11c3d6133dc17e65fa0c485dcca9f52d41b60161246039e42ec784d49400bffdb51459f5de654091301a09378f93464d52118b48d44b30d781eb1dbed09da11fb4c818dbd442d161aba4b9edc79f05e4b7e401651395b53bd8b5bd3f2aaa6a00877fa9b45cadb8e648550b4c6cbe +d = 201b432d8df14324182d6261db3e4b3f46a8284482d52e370da41e6cbdf45ec2952f5db7ccbce3bc29449f4fb080ac97 +Qx = c2b47944fb5de342d03285880177ca5f7d0f2fcad7678cce4229d6e1932fcac11bfc3c3e97d942a3c56bf34123013dbf +Qy = 37257906a8223866eda0743c519616a76a758ae58aee81c5fd35fbf3a855b7754a36d4a0672df95d6c44a81cf7620c2d +R = 50835a9251bad008106177ef004b091a1e4235cd0da84fff54542b0ed755c1d6f251609d14ecf18f9e1ddfe69b946e32 +S = 0475f3d30c6463b646e8d3bf2455830314611cbde404be518b14464fdb195fdcc92eb222e61f426a4a592c00a6a89721 +``` + +ECDSA is randomised, so re-signing `siggen_message.bin` does **not** reproduce +`siggen_signature.bin` — NIST's per-message secret `k` is not settable through +this API. The vector is therefore used for *verification*: the published +signature must verify under the published key, and a signature this stack +produces must verify too. + +## Negative case — `SigVer.rsp`, section `[P-384,SHA-384]`, first `Result = F` record + +``` +Msg = 4132833a525aecc8a1a6dea9f4075f44feefce810c4668423b38580417f7bdca5b21061a45eaa3cbe2a7035ed189523af8002d65c2899e65735e4d93a16503c145059f365c32b3acc6270e29a09131299181c98b3c76769a18faf21f6b4a8f271e6bf908e238afe8002e27c63417bda758f846e1e3b8e62d7f05ebd98f1f9154 +Qx = 1f94eb6f439a3806f8054dd79124847d138d14d4f52bac93b042f2ee3cdb7dc9e09925c2a5fee70d4ce08c61e3b19160 +Qy = 1c4fd111f6e33303069421deb31e873126be35eeb436fe2034856a3ed1e897f26c846ee3233cd16240989a7990c19d8c +R = 3c15c3cedf2a6fbff2f906e661f5932f2542f0ce68e2a8182e5ed3858f33bd3c5666f17ac39e52cb004b80a0d4ba73cd +S = 9de879083cbb0a97973c94f1963d84f581e4c6541b7d000f9850deb25154b23a37dd72267bdd72665cc7027f88164fab +Result = F (2 - R changed) +``` + +Verification of `sigver_invalid_signature.bin` under +`sigver_invalid_public_key.der` must fail. Unlike a bit-flipped signature this +is a well-formed value NIST authored to be rejected. diff --git a/score/tests/test_vectors/ecdsa/P384-SHA384/siggen_message.bin b/score/tests/test_vectors/ecdsa/P384-SHA384/siggen_message.bin new file mode 100644 index 0000000000000000000000000000000000000000..a5dd7e8d58d8bc7b86579fa7eb7ed71895f78683 GIT binary patch literal 128 zcmV-`0Du2$Mc9BhIW7}%+5H^Rbm_r^jS<7v6FtFxW%>+AUCgQVEkU*cVI*KVO;QnyNYqO(*MaLDzR;baAGF9A+tfnQVXLIM?Z=-5 iXD#_%Y(%IW_hJk!u3Jrw literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/ecdsa/P384-SHA384/siggen_public_key.der b/score/tests/test_vectors/ecdsa/P384-SHA384/siggen_public_key.der new file mode 100644 index 0000000000000000000000000000000000000000..b26eccee6892ebc9adc9ce4f61586ffc0b9ebc9d GIT binary patch literal 120 zcmV-;0EhoDb}$eI2P%e0&OHJF1_djD1OOrfVgLlfw0T7PUE@N~GKGi%cgkOV4=>8s zXN=B5Dc0eWFUr9i{5(FF*+QemYx6-P0X@GrC3yy@A~OU literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/ecdsa/P384-SHA384/siggen_signature.bin b/score/tests/test_vectors/ecdsa/P384-SHA384/siggen_signature.bin new file mode 100644 index 0000000000000000000000000000000000000000..172c696a1fb47efd77315f9a5af415c5897ac01d GIT binary patch literal 96 zcmV-m0H6O*gIbbNy3hy^VR!EUO9>htLN(0|s89b?R4WeGRl(NsQDB`E?D3DD9pC1g zlx{Kvb@S5Rf7W*VH~~W1in#=6h=?m8DHGVF0vx#A3|zMSu6misFxuW C*eZYk literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/ecdsa/P384-SHA384/sigver_invalid_message.bin b/score/tests/test_vectors/ecdsa/P384-SHA384/sigver_invalid_message.bin new file mode 100644 index 0000000000000000000000000000000000000000..02c41c17c7cef9b6fcc4baa55826e2b0ed20364f GIT binary patch literal 128 zcmV-`0Du2MGJ`r&TI|T7rrxRa2VX?~@6Le?MrcAiI9LQ1_r1znAqE;n>Z8lzrvqNm ziBdZF04-(0iJoP1UQLssWdp%Q1)nxtGPA74Ck`o~kufQefys+Jc6OQ=`tl!ZN{=TV iYxxM`IIrjcE+@t`7rm!g_(tL5xaKW?1?$<5ACXkskUMJt literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/ecdsa/P384-SHA384/sigver_invalid_public_key.der b/score/tests/test_vectors/ecdsa/P384-SHA384/sigver_invalid_public_key.der new file mode 100644 index 0000000000000000000000000000000000000000..0d0716840270ada4e2e26c561ab328708a5634e1 GIT binary patch literal 120 zcmV-;0EhoDb}$eI2P%e0&OHJF1_djD1OOrfVgLjmlI#IOMsx% zx^vB)=y?b{y9$?=Je2X4J%sgv + +# ECDSA P521 / SHA512 — NIST FIPS 186-4 CAVP vectors + +Source: +Archive: `186-4ecdsatestvectors.zip`. + +## Positive case — `SigGen.txt`, section `[P-521,SHA-512]`, first record + +`siggen_private_key.der` is the DER (PKCS#8) encoding of the private key `d`, +`siggen_public_key.der` the SubjectPublicKeyInfo of `(Qx, Qy)`, and +`siggen_signature.bin` the vector's `(R, S)` in the fixed-length IEEE P1363 +form `r||s` (66 + 66 bytes) that this stack uses on the wire. + +``` +Msg = 9ecd500c60e701404922e58ab20cc002651fdee7cbc9336adda33e4c1088fab1964ecb7904dc6856865d6c8e15041ccf2d5ac302e99d346ff2f686531d25521678d4fd3f76bbf2c893d246cb4d7693792fe18172108146853103a51f824acc621cb7311d2463c3361ea707254f2b052bc22cb8012873dcbb95bf1a5cc53ab89f +d = 0f749d32704bc533ca82cef0acf103d8f4fba67f08d2678e515ed7db886267ffaf02fab0080dca2359b72f574ccc29a0f218c8655c0cccf9fee6c5e567aa14cb926 +Qx = 061387fd6b95914e885f912edfbb5fb274655027f216c4091ca83e19336740fd81aedfe047f51b42bdf68161121013e0d55b117a14e4303f926c8debb77a7fdaad1 +Qy = 0e7d0c75c38626e895ca21526b9f9fdf84dcecb93f2b233390550d2b1463b7ee3f58df7346435ff0434199583c97c665a97f12f706f2357da4b40288def888e59e6 +R = 04de826ea704ad10bc0f7538af8a3843f284f55c8b946af9235af5af74f2b76e099e4bc72fd79d28a380f8d4b4c919ac290d248c37983ba05aea42e2dd79fdd33e8 +S = 087488c859a96fea266ea13bf6d114c429b163be97a57559086edb64aed4a18594b46fb9efc7fd25d8b2de8f09ca0587f54bd287299f47b2ff124aac566e8ee3b43 +``` + +ECDSA is randomised, so re-signing `siggen_message.bin` does **not** reproduce +`siggen_signature.bin` — NIST's per-message secret `k` is not settable through +this API. The vector is therefore used for *verification*: the published +signature must verify under the published key, and a signature this stack +produces must verify too. + +## Negative case — `SigVer.rsp`, section `[P-521,SHA-512]`, first `Result = F` record + +``` +Msg = a0732a605c785a2cc9a3ff84cbaf29175040f7a0cc35f4ea8eeff267c1f92f06f46d3b35437195185d322cbd775fd24741e86ee9236ba5b374a2ac29803554d715fa4656ac31778f103f88d68434dd2013d4c4e9848a11198b390c3d600d712893513e179cd3d31fb06c6e2a1016fb96ffd970b1489e36a556ab3b537eb29dff +Qx = 12a593f568ca2571e543e00066ecd3a3272a57e1c94fe311e5df96afc1b792e5862720fc730e62052bbf3e118d3a078f0144fc00c9d8baaaa8298ff63981d09d911 +Qy = 17cea5ae75a74100ee03cdf2468393eef55ddabfe8fd5718e88903eb9fd241e8cbf9c68ae16f4a1db26c6352afcb1894a9812da6d32cb862021c86cd8aa483afc26 +R = 1aac7692baf3aa94a97907307010895efc1337cdd686f9ef2fd8404796a74701e55b03ceef41f3e6f50a0eeea11869c4789a3e8ab5b77324961d081e1a3377ccc91 +S = 009c1e7d93d056b5a97759458d58c49134a45071854b8a6b8272f9fe7e78e1f3d8097e8a6e731f7ab4851eb26d5aa4fdadba6296dc7af835fe3d1b6dba4b031d5f3 +Result = F (2 - R changed) +``` + +Verification of `sigver_invalid_signature.bin` under +`sigver_invalid_public_key.der` must fail. Unlike a bit-flipped signature this +is a well-formed value NIST authored to be rejected. diff --git a/score/tests/test_vectors/ecdsa/P521-SHA512/siggen_message.bin b/score/tests/test_vectors/ecdsa/P521-SHA512/siggen_message.bin new file mode 100644 index 000000000..3a1b62d03 --- /dev/null +++ b/score/tests/test_vectors/ecdsa/P521-SHA512/siggen_message.bin @@ -0,0 +1 @@ +P `@I"劲 e3jݣ>LNyhV]l-Z4oS%Rx?vȓFMvy/rF1Jb1$c6%O++,(sܻ\: \ No newline at end of file diff --git a/score/tests/test_vectors/ecdsa/P521-SHA512/siggen_private_key.der b/score/tests/test_vectors/ecdsa/P521-SHA512/siggen_private_key.der new file mode 100644 index 0000000000000000000000000000000000000000..8c41737e781a25954329098fc264143f8a28e651 GIT binary patch literal 241 zcmV5Iv7ix@Yi>CV1r)?R~h0CV%?yFRK86+@dv`a`kk~!kP~ujD}Uf%+H_hY+hD+ zp-j0Zp@E45frbDC0AVKq!sxh>lt2Wms&) literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/ecdsa/P521-SHA512/siggen_public_key.der b/score/tests/test_vectors/ecdsa/P521-SHA512/siggen_public_key.der new file mode 100644 index 0000000000000000000000000000000000000000..7b027b6cd7f70df2b1d61e0860e2f0bdc05549b5 GIT binary patch literal 158 zcmV;P0Ac?yftxT81_vsJNX|V10R{ytfdl{}1A&GB1OQ<;f7ZEK6zGNd67Bo7`zJ!fZzEUQ MOF$@%?}(0B=C|cWO#lD@ literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/ecdsa/P521-SHA512/siggen_signature.bin b/score/tests/test_vectors/ecdsa/P521-SHA512/siggen_signature.bin new file mode 100644 index 0000000000000000000000000000000000000000..3459e122cb82723d54b0b3814e6c5c6e5151aa6a GIT binary patch literal 132 zcmV-~0DJ!cP3R`-a7xh&!1q&%_@jhBC{I<$xkj&&HLqItPb+rdndH23{dv-gI1i0W zOp%(xkkUxQd4swIuB0w4*Pq=p=m3XEjD?z({-S2;6TfW{OhTI$JL!5?Rgi}5wo2_v m7+Fh3`=0!N(p`%!=NpL?mI)X{Y2UT literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/ecdsa/P521-SHA512/sigver_invalid_message.bin b/score/tests/test_vectors/ecdsa/P521-SHA512/sigver_invalid_message.bin new file mode 100644 index 000000000..f07ffb7f6 --- /dev/null +++ b/score/tests/test_vectors/ecdsa/P521-SHA512/sigver_invalid_message.bin @@ -0,0 +1 @@ +s*`\xZ,ɣ˯)P@5g/m;5Cq]2,w_GAn#kt)5TFV1w?ք4 鄊9 =` q(Q>ln*pH6V;S~ \ No newline at end of file diff --git a/score/tests/test_vectors/ecdsa/P521-SHA512/sigver_invalid_public_key.der b/score/tests/test_vectors/ecdsa/P521-SHA512/sigver_invalid_public_key.der new file mode 100644 index 0000000000000000000000000000000000000000..c8f78b892ad5df919ff1a914ce52a704938952cd GIT binary patch literal 158 zcmV;P0Ac?yftxT81_vsJNX|V10R{ytfdl{}1A&GB1OX~pKUR#QR~}S800wT&Ix=#l zejJqkF&ZYzzm&>s;YvR|6`aP3E2?=eCk@~ zT67Q&;5^?XXgNObRo$!pkJWLGh>$+H{UjcYzno~U7WARpCdM@?{IQ8jm=fA;GRuY_ MA;@glsz^HgCUL+++yDRo literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/ecdsa/P521-SHA512/sigver_invalid_signature.bin b/score/tests/test_vectors/ecdsa/P521-SHA512/sigver_invalid_signature.bin new file mode 100644 index 0000000000000000000000000000000000000000..e23c1c962257db7765db7bbedde66294db41d125 GIT binary patch literal 132 zcmV-~0DJ!ds>f+7uR5tpmymM@0SJ}v!83f_Xm6hK{e%R0YIJZORj@qn^dCNNP@wMW z5r&*ciKFPNTX!-^VbFo$qc?oakpKz7=h;04Yg(6elvve_NfSy%2N+birno0BpXcX} mA3cDV=%(i}_p3-z>n7EzPukn2DQ(BEgJ0v(w%eqzG1c>xLql%> literal 0 HcmV?d00001