diff --git a/aws-esdk-cpp/Cargo.toml b/aws-esdk-cpp/Cargo.toml new file mode 100644 index 000000000..15a8be733 --- /dev/null +++ b/aws-esdk-cpp/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "aws-esdk-cpp-stub" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +path = "src/lib.rs" diff --git a/aws-esdk-cpp/aws-esdk-cpp/.duvet/config.toml b/aws-esdk-cpp/aws-esdk-cpp/.duvet/config.toml new file mode 100644 index 000000000..6e50e17e7 --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/.duvet/config.toml @@ -0,0 +1,19 @@ +'$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" + +[[source]] +pattern = "./src/**/*.rs" + +[[source]] +pattern = "./include/**/*.h" + +[[source]] +pattern = "./*.cpp" + +[[specification]] +source = "spec/shim/shim.md" + +[[specification]] +source = "spec/shim/esdk-shim.md" + +[report.snapshot] +enabled = true diff --git a/aws-esdk-cpp/aws-esdk-cpp/.gitignore b/aws-esdk-cpp/aws-esdk-cpp/.gitignore new file mode 100644 index 000000000..11db1487d --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/.gitignore @@ -0,0 +1,23 @@ +# Cargo build output +/target/ +Cargo.lock + +# Compiled demo / test binaries (built via the Makefile) +/basic_hierarchical_keyring_example +/test_esdk + +# Object files / archives +*.o +*.a + +# Agent tooling scratch / transcripts +.agent-reports/ + +# CMake build output +/build/ +/build-cmake/ + +# Duvet: generated requirement extraction, snapshot, and reports +.duvet/requirements/ +.duvet/snapshot.txt +.duvet/reports/ diff --git a/aws-esdk-cpp/aws-esdk-cpp/CMakeLists.txt b/aws-esdk-cpp/aws-esdk-cpp/CMakeLists.txt new file mode 100644 index 000000000..4cb273e56 --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/CMakeLists.txt @@ -0,0 +1,63 @@ +# CMake integration for the AWS Encryption SDK C++ bindings (aws-esdk-cpp). +# +# Consumers do: +# add_subdirectory(aws-esdk-cpp) +# target_link_libraries(my_app PRIVATE AWS::esdk) +# #include +# +# This drives `cargo build` to produce the Rust staticlib and the cxx-generated +# C++ sources, then exposes them behind the AWS::esdk target. +cmake_minimum_required(VERSION 3.20) +project(aws_esdk_cpp LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(ESDK_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) +set(ESDK_PROFILE debug) +set(ESDK_TARGET_DIR ${ESDK_ROOT}/target) + +# Build the Rust staticlib + cxx-generated sources before we reference them. +execute_process( + COMMAND cargo build + WORKING_DIRECTORY ${ESDK_ROOT} + RESULT_VARIABLE _esdk_cargo_result +) +if(NOT _esdk_cargo_result EQUAL 0) + message(FATAL_ERROR "cargo build failed (exit ${_esdk_cargo_result})") +endif() + +set(ESDK_STATICLIB ${ESDK_TARGET_DIR}/${ESDK_PROFILE}/libaws_esdk_cpp.a) +set(ESDK_BRIDGE_CC ${ESDK_TARGET_DIR}/cxxbridge/aws-esdk-cpp/src/lib.rs.cc) + +# The cxx runtime archive lives in a hashed build dir; locate it post-build. +file(GLOB_RECURSE _esdk_cxxbridge_rt + ${ESDK_TARGET_DIR}/${ESDK_PROFILE}/build/cxx-*/out/libcxxbridge1.a) +list(GET _esdk_cxxbridge_rt 0 ESDK_CXXBRIDGE_RT) + +# The public target: compiles the generated C++ bridge and links the Rust lib. +add_library(aws_esdk_cpp STATIC ${ESDK_BRIDGE_CC}) +target_include_directories(aws_esdk_cpp PUBLIC + ${ESDK_ROOT}/include + ${ESDK_TARGET_DIR}/cxxbridge +) +target_link_libraries(aws_esdk_cpp PUBLIC + ${ESDK_STATICLIB} + ${ESDK_CXXBRIDGE_RT} + pthread + ${CMAKE_DL_LIBS} +) +if(APPLE) + target_link_libraries(aws_esdk_cpp PUBLIC + "-framework Security" + "-framework CoreFoundation" + "-framework SystemConfiguration" + ) +endif() +add_library(AWS::esdk ALIAS aws_esdk_cpp) + +# Build the integration test only when this is the top-level project. +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + add_executable(esdk_integration_test test_esdk.cpp) + target_link_libraries(esdk_integration_test PRIVATE AWS::esdk) +endif() diff --git a/aws-esdk-cpp/aws-esdk-cpp/Cargo.toml b/aws-esdk-cpp/aws-esdk-cpp/Cargo.toml new file mode 100644 index 000000000..d17fe7bf2 --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "aws-esdk-cpp" +version = "0.1.0" +edition = "2024" +keywords = ["crypto", "cryptography", "security", "encryption", "c++"] +categories = ["cryptography", "external-ffi-bindings"] +license = "ISC AND (Apache-2.0 OR ISC)" +description = "C++ shim exposing the Rust implementation of the AWS Encryption SDK to C++ callers." +repository = "https://github.com/aws/aws-encryption-sdk" +authors = ["AWS-CryptoTools"] +documentation = "https://docs.rs/crate/aws-esdk-cpp" +readme = "README.md" + +[lib] +crate-type = ["staticlib", "cdylib"] + +[dependencies] +aws-config = "1.8.12" +aws-mpl-legacy = { package = "aws-mpl-native", path = "../../mpl" } +aws-sdk-dynamodb = "1.103.0" +aws-sdk-kms = "1.98.0" +aws-smithy-types = "1.3.6" +cxx = "1.0.194" +tokio = {version = "1.49.0", features = ["full"] } +aws-esdk = { path = "../../esdk" } + +[build-dependencies] +cxx-build = "1.0.194" +cc = "1.2.54" + +# This nested package is not part of the root workspace (the root member is the +# sibling stub crate). Declare it as its own workspace root so `cargo` builds it +# standalone; `mpl`/`esdk` are consumed as path dependencies above. +[workspace] diff --git a/aws-esdk-cpp/aws-esdk-cpp/Makefile b/aws-esdk-cpp/aws-esdk-cpp/Makefile new file mode 100644 index 000000000..c2c45bcdc --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/Makefile @@ -0,0 +1,43 @@ +# Simple Makefile - all C++ bridge code is compiled into the Rust static library. + +# Idiomatic example. Requires C++17 (the idiomatic header uses std::string_view). +# Needs live AWS resources / ESDK_TEST_* env vars to run; builds without them. +example: examples/basic_hierarchical_keyring_example.cpp include/aws/esdk/EncryptionSDK.h src/lib.rs target/debug/libaws_esdk_cpp.a + g++ examples/basic_hierarchical_keyring_example.cpp target/cxxbridge/aws-esdk-cpp/src/lib.rs.cc \ + $$(find target/debug/build -path "*/cxx-*/out/libcxxbridge1.a" | head -n1) \ + -Ltarget/debug -laws_esdk_cpp \ + -Itarget/cxxbridge -Iinclude \ + -lpthread -ldl \ + -std=c++17 \ + -o basic_hierarchical_keyring_example + +# Idiomatic-facade integration tests. Requires C++17 (the idiomatic header uses +# std::string_view). Run ./test_esdk with ESDK_TEST_* env vars set (it skips +# cleanly if they are unset). +test_esdk: test_esdk.cpp include/aws/esdk/EncryptionSDK.h src/lib.rs target/debug/libaws_esdk_cpp.a + g++ test_esdk.cpp target/cxxbridge/aws-esdk-cpp/src/lib.rs.cc \ + $$(find target/debug/build -path "*/cxx-*/out/libcxxbridge1.a" | head -n1) \ + -Ltarget/debug -laws_esdk_cpp \ + -Itarget/cxxbridge -Iinclude \ + -lpthread -ldl \ + -std=c++17 \ + -o test_esdk + +clean: + rm -f basic_hierarchical_keyring_example test_esdk + +clippy: + cargo clippy --tests --examples + +test: + cargo build + cargo build --release + make example + make test_esdk + +duvet: + @test -e spec || ln -s ../../spec spec + rm -rf .duvet/reports .duvet/requirements + duvet report + +.PHONY: all clean duvet test clippy diff --git a/aws-esdk-cpp/aws-esdk-cpp/README.md b/aws-esdk-cpp/aws-esdk-cpp/README.md new file mode 100644 index 000000000..6bf96e504 --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/README.md @@ -0,0 +1,19 @@ +# aws-esdk-cpp + +A C++ shim over the Rust implementation of the AWS Encryption SDK. + +This crate exposes the Rust AWS Encryption SDK (`aws-esdk`) to C++ callers through +an idiomatic, generator-agnostic C++ API. Consumers program against +`Aws::Esdk::` types and `#include `; the underlying +Rust bindings (generated with `cxx`) are an implementation detail hidden behind +the facade. + +The shim's own behavioral contract — type translation, resource lifetimes, error +propagation, delegation to the core library — is specified in +[`spec/shim/shim.md`](spec/shim/shim.md) (generic) and +[`spec/shim/esdk-shim.md`](spec/shim/esdk-shim.md) (ESDK-specific), and tracked +with Duvet annotations in the source. + +## Status + +Work in progress. diff --git a/aws-esdk-cpp/aws-esdk-cpp/build.rs b/aws-esdk-cpp/aws-esdk-cpp/build.rs new file mode 100644 index 000000000..7f170b316 --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/build.rs @@ -0,0 +1,11 @@ +fn main() { + // Generate the cxx bridge code and compile it + cxx_build::bridge("src/lib.rs") + .flag_if_supported("-std=c++11") + .compile("cxxbridge-aws-mpl-cxx"); + + // Tell cargo to link the C++ standard library + println!("cargo:rustc-link-lib=c++"); + + println!("cargo:rerun-if-changed=src/lib.rs"); +} diff --git a/aws-esdk-cpp/aws-esdk-cpp/examples/basic_hierarchical_keyring_example.cpp b/aws-esdk-cpp/aws-esdk-cpp/examples/basic_hierarchical_keyring_example.cpp new file mode 100644 index 000000000..3557b45bb --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/examples/basic_hierarchical_keyring_example.cpp @@ -0,0 +1,57 @@ +#include +#include +#include // Idiomatic C++ wrapper layer + +int main() { + try { + // Create AWS clients + auto client_config = Aws::Esdk::ClientConfig() + .with_max_retry_attempts(5); + + auto kms = Aws::Esdk::KmsClient(client_config); + auto ddb = Aws::Esdk::DdbClient(client_config); + + // Create keystore with constructor - all config in one place + auto keystore = Aws::Esdk::KeyStore( + "KeyStoreDdbTable", // table_name + "KeyStoreDdbTable", // logical_key_store_name + "arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126", // kms_arn + kms, + ddb + ); + + // Create hierarchical keyring with constructor + auto keyring = Aws::Esdk::HierarchicalKeyring( + keystore, + "3ce7656b-e166-40f3-8c9b-a920ce6596cd", // branch_key_id + 4242, // ttl_seconds + 42 // cache_capacity + ); + + // Create encryption SDK client + Aws::Esdk::EncryptionSDK sdk; + + // Encrypt with std::vector - idiomatic C++ + std::vector plaintext = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'}; + auto ciphertext = sdk.encrypt(plaintext, keyring); + + // Decrypt + auto decrypted = sdk.decrypt(ciphertext, keyring); + + // Validate results + if (plaintext != decrypted.plaintext) { + std::cerr << "Decryption failed: plaintext mismatch\n"; + return 1; + } + + std::cout << "Success! Encryption and decryption completed successfully.\n"; + + // Automatic cleanup via RAII - no manual delete calls needed! + + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << "\n"; + return 1; + } + + return 0; +} diff --git a/aws-esdk-cpp/aws-esdk-cpp/include/aws/esdk/EncryptionSDK.h b/aws-esdk-cpp/aws-esdk-cpp/include/aws/esdk/EncryptionSDK.h new file mode 100644 index 000000000..25f60ffb1 --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/include/aws/esdk/EncryptionSDK.h @@ -0,0 +1,264 @@ +// aws_esdk.hpp — idiomatic C++ facade over the cxx-generated bindings. +// +// Design goals (learnings from the binding-strategy review): +// * Stable, GENERATOR-AGNOSTIC public API: no cxx type (rust::*, the generated +// enums/structs) appears in any public signature. Swapping the binding +// backend later touches only this file's internals, not consumer code. +// * RAII + exceptions: cxx throws rust::Error on failure; we translate that +// into Aws::Esdk::EsdkException so callers never see a cxx type. +// * Memory safety at the handle seam: the bridge passes handles as raw +// `*const` pointers inside config structs, which can dangle if a C++ caller +// drops a client before it is used. Each wrapper here OWNS its dependencies +// via shared_ptr and declares them before the object that points at them, +// so a dependency can never be destroyed while something references it. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "rust/cxx.h" // rust::Error, rust::Box, rust::Vec, rust::Slice +#include "aws-esdk-cpp/src/lib.rs.h" // cxx-generated bindings (global namespace) + +namespace Aws { +namespace Esdk { + +// ---- File-wide owned-interface / lifetime invariants (see spec/shim/shim.md) - +//= spec/shim/shim.md#owned-interface +//= type=implication +//= reason=The facade defines its own Aws::Esdk:: enum for every core-library enumeration it exposes (AlgorithmSuiteId, CommitmentPolicy, CacheKind). +//# - For each core-library enumeration it exposes, the shim MUST define its own +//# corresponding enumeration type. +// +//= spec/shim/shim.md#owned-interface +//= type=implication +//= reason=Every public method signature uses only Aws::Esdk:: types and standard-library types; no generated or core-library type appears. +//# - Each public function the shim exposes MUST use only owned-interface types or +//# standard-library types in its signature. +// +//= spec/shim/shim.md#owned-interface +//= type=implication +//= reason=Every wrapper stores its generated handle in a private member and exposes it only through the internal ffi() accessor. +//# - Each owned interface that represents a core-library resource MUST hold the +//# generated handle privately and MUST NOT expose it in its public API. + +// ---- Public exception: hides cxx's rust::Error ------------------------------ +//= spec/shim/shim.md#owned-interface +//= type=implication +//= reason=EsdkException is the shim's own error type; detail::guard translates every cxx rust::Error into it, so no cxx or core-library error type appears on the public surface. +//# - The shim MUST define its own error type for its public surface. +class EsdkException : public std::runtime_error { +public: + explicit EsdkException(const std::string& msg) : std::runtime_error(msg) {} +}; + +// ---- Public enums: our own, so the generated enums never leak ---------------- +enum class AlgorithmSuiteId : std::uint16_t { + AesGcm128 = 0x0014, + AesGcm192 = 0x0046, + AesGcm256 = 0x0078, + AesGcm128Hkdf256 = 0x0114, + AesGcm192Hkdf256 = 0x0146, + AesGcm256Hkdf256 = 0x0178, + AesGcm128Hkdf256Ecdsa256 = 0x0214, + AesGcm192Hkdf384Ecdsa384 = 0x0346, + AesGcm256Hkdf384Ecdsa384 = 0x0378, + AesGcm256Hkdf512Commit = 0x0478, + AesGcm256Hkdf512CommitEcdsa384 = 0x0578, +}; + +enum class CommitmentPolicy : std::uint8_t { + ForbidEncryptAllowDecrypt = 0, + RequireEncryptAllowDecrypt = 1, + RequireEncryptRequireDecrypt = 2, +}; + +enum class CacheKind : std::uint8_t { None = 0, MultiThreaded = 1 }; + +namespace detail { + +// Run a cxx call, translating rust::Error -> EsdkException so no cxx exception +// type escapes this header. +template +inline auto guard(F&& f) -> decltype(f()) { + try { + return f(); + } catch (const ::rust::Error& e) { + throw EsdkException(e.what()); + } +} + +// Enum translation is the only code that knows the generated enum types exist. +// Values are kept identical to the generated enums, so casts are exact. +inline ::EsdkAlgorithmSuiteId to_ffi(AlgorithmSuiteId a) { + return static_cast<::EsdkAlgorithmSuiteId>(static_cast(a)); +} +inline ::EsdkCommitmentPolicy to_ffi(CommitmentPolicy p) { + return static_cast<::EsdkCommitmentPolicy>(static_cast(p)); +} +inline ::CacheType to_ffi(CacheKind c) { + return static_cast<::CacheType>(static_cast(c)); +} + +inline ::rust::Vec<::EncryptionContextItem> to_ec( + const std::map& ec) { + ::rust::Vec<::EncryptionContextItem> out; + for (const auto& kv : ec) { + ::EncryptionContextItem item; + item.key = ::rust::String(kv.first); + item.value = ::rust::String(kv.second); + out.push_back(std::move(item)); + } + return out; +} + +// Reverse translations, for values the core library returns to the target. +inline AlgorithmSuiteId alg_from_ffi(::EsdkAlgorithmSuiteId a) { + return static_cast(static_cast(a)); +} + +inline std::map from_ec( + const ::rust::Vec<::EncryptionContextItem>& ec) { + std::map out; + for (const auto& item : ec) { + out.emplace(std::string(item.key), std::string(item.value)); + } + return out; +} + +} // namespace detail + +// ---- Fluent client config ---------------------------------------------------- +class ClientConfig { + ::MplAwsClientConfig cfg_ = ::default_client_config(); +public: + ClientConfig& with_max_retry_attempts(std::uint32_t n) { cfg_.retry.max_attempts = n; return *this; } + ClientConfig& with_adaptive_retry(bool a) { cfg_.retry.mode_adaptive = a; return *this; } + ClientConfig& with_region(const std::string& r) { cfg_.region = ::rust::String(r); return *this; } + const ::MplAwsClientConfig& raw() const { return cfg_; } // internal; POD-ish, low leak risk +}; + +// ---- Handle wrappers: copyable (shared_ptr), so they can be owned as deps ----- +class KmsClient { + std::shared_ptr<::rust::Box<::MplKmsClient>> inner_; + const ::MplKmsClient* ffi() const { return &**inner_; } + friend class KeyStore; +public: + explicit KmsClient(const ClientConfig& cfg) + : inner_(std::make_shared<::rust::Box<::MplKmsClient>>( + detail::guard([&] { return ::create_kms_client(cfg.raw()); }))) {} +}; + +class DdbClient { + std::shared_ptr<::rust::Box<::MplDdbClient>> inner_; + const ::MplDdbClient* ffi() const { return &**inner_; } + friend class KeyStore; +public: + explicit DdbClient(const ClientConfig& cfg) + : inner_(std::make_shared<::rust::Box<::MplDdbClient>>( + detail::guard([&] { return ::create_ddb_client(cfg.raw()); }))) {} +}; + +class KeyStore { + // Declared before inner_ so they outlive it; shared_ptr keeps the Rust + // clients alive for as long as this KeyStore exists -> the raw pointers the + // bridge stores can never dangle. + KmsClient kms_; + DdbClient ddb_; + std::shared_ptr<::rust::Box<::KeyStore>> inner_; + const ::KeyStore* ffi() const { return &**inner_; } + friend class HierarchicalKeyring; +public: + KeyStore(const std::string& table_name, + const std::string& logical_key_store_name, + const std::string& kms_arn, + KmsClient kms, + DdbClient ddb) + : kms_(std::move(kms)), + ddb_(std::move(ddb)), + inner_(std::make_shared<::rust::Box<::KeyStore>>(detail::guard([&] { + ::KeyStoreConfig c = ::default_keystore_config(); + c.ddb_table_name = ::rust::String(table_name); + c.logical_key_store_name = ::rust::String(logical_key_store_name); + c.kms_configuration_type = ::KmsConfigurationType::KmsKeyArn; + c.kms_configuration_value = ::rust::String(kms_arn); + c.kms_client = kms_.ffi(); + c.ddb_client = ddb_.ffi(); + return ::create_keystore(c); + }))) {} +}; + +class HierarchicalKeyring { + KeyStore key_store_; // owns the keystore (and transitively its clients) + std::shared_ptr<::rust::Box<::Keyring>> inner_; + const ::Keyring* ffi() const { return &**inner_; } + friend class EncryptionSDK; +public: + HierarchicalKeyring(KeyStore key_store, + const std::string& branch_key_id, + std::uint32_t ttl_seconds, + std::uint32_t cache_capacity, + CacheKind cache = CacheKind::MultiThreaded) + : key_store_(std::move(key_store)), + inner_(std::make_shared<::rust::Box<::Keyring>>(detail::guard([&] { + ::HierarchicalKeyringInput in = ::default_hierarchical_keyring_input(); + in.branch_key_id = ::rust::String(branch_key_id); + in.key_store = key_store_.ffi(); + in.ttl = ttl_seconds; + in.cache = detail::to_ffi(cache); + in.multi_threaded_cache.entryCapacity = cache_capacity; + return ::create_hierarchical_keyring(in); + }))) {} +}; + +// ---- Decrypt result ---------------------------------------------------------- +// decrypt returns not just the plaintext but the encryption context and algorithm +// suite the core library reported, so the caller can inspect what was actually +// used and authenticated (for example, to make access-control decisions). +struct DecryptResult { + std::vector plaintext; + std::map encryption_context; + AlgorithmSuiteId algorithm_suite_id; +}; + +// ---- SDK facade -------------------------------------------------------------- +class EncryptionSDK { +public: + std::vector encrypt( + const std::vector& plaintext, + const HierarchicalKeyring& keyring, + AlgorithmSuiteId alg = AlgorithmSuiteId::AesGcm256Hkdf512CommitEcdsa384, + CommitmentPolicy policy = CommitmentPolicy::RequireEncryptRequireDecrypt, + const std::map& encryption_context = {}) { + // Start from the Rust-defined defaults, then override — so we never + // re-declare defaults in C++ and risk diverging from the Rust core. + ::EncryptInput in = ::default_encrypt_input(); + in.keyring = keyring.ffi(); + in.plaintext = ::rust::Slice(plaintext.data(), plaintext.size()); + in.algorithm_suite_id = detail::to_ffi(alg); + in.commitment_policy = detail::to_ffi(policy); + in.encryption_context = detail::to_ec(encryption_context); + ::EncryptOutput out = detail::guard([&] { return ::encrypt(in); }); + return std::vector(out.ciphertext.begin(), out.ciphertext.end()); + } + + DecryptResult decrypt( + const std::vector& ciphertext, + const HierarchicalKeyring& keyring) { + ::DecryptInput in = ::default_decrypt_input(); + in.keyring = keyring.ffi(); + in.ciphertext = ::rust::Slice(ciphertext.data(), ciphertext.size()); + ::DecryptOutput out = detail::guard([&] { return ::decrypt(in); }); + DecryptResult result; + result.plaintext = std::vector(out.plaintext.begin(), out.plaintext.end()); + result.encryption_context = detail::from_ec(out.encryption_context); + result.algorithm_suite_id = detail::alg_from_ffi(out.algorithm_suite_id); + return result; + } +}; + +} // namespace Esdk +} // namespace Aws diff --git a/aws-esdk-cpp/aws-esdk-cpp/spec b/aws-esdk-cpp/aws-esdk-cpp/spec new file mode 120000 index 000000000..409f03297 --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/spec @@ -0,0 +1 @@ +../../spec \ No newline at end of file diff --git a/aws-esdk-cpp/aws-esdk-cpp/src/lib.rs b/aws-esdk-cpp/aws-esdk-cpp/src/lib.rs new file mode 100644 index 000000000..0bff41e85 --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/src/lib.rs @@ -0,0 +1,920 @@ +//! # aws-esdk-cpp — a C++ shim over the Rust AWS Encryption SDK +//! +//! This crate is a shim that exposes the native Rust AWS Encryption SDK to C++ +//! callers. Its behavioral contract is specified in `spec/shim/shim.md` and +//! `spec/shim/esdk-shim.md`. +//! +//! ## Layering +//! +//! 1. This bridge (`#[cxx::bridge] mod ffi`) + the Rust glue below it. +//! 2. The cxx-generated bindings (`lib.rs.h` / `lib.rs.cc`). +//! 3. A hand-written idiomatic facade, `aws_esdk.hpp`, which is what consumers +//! use. `main.cpp` uses the raw generated bindings directly; +//! `main_idiomatic.cpp` uses the facade. + +#![allow(clippy::boxed_local, reason = "need to pass Box to destructors")] + +// ---- Crate-wide conformance (see spec/shim/shim.md and spec/shim/esdk-shim.md) ---- +// +// The generic Shim Specification requirements below are file-wide: they describe +// behavior that EVERY translation / operation / resource in this crate obeys, so +// they are recorded here once as implications rather than pinned to a single site. +//= spec/shim/esdk-shim.md#overview +//= type=implication +//= reason=This crate's annotations against shim.md are the evidence that the ESDK shim conforms to the generic Shim Specification. +//# An ESDK shim MUST conform to the generic [Shim Specification](./shim.md). +// +//= spec/shim/shim.md#conformance-and-testing +//= type=implication +//= reason=This crate carries annotations against both shim.md and esdk-shim.md, evidencing conformance to both. +//# - A shim MUST conform to this specification in addition to its core-library-specific +//# shim specification. +// +//= spec/shim/shim.md#type-translation +//= type=implication +//= reason=Every concrete translation in this crate returns a Result and follows the convert-or-error rules. +//# Every such translation MUST obey the following rules. +// +//= spec/shim/shim.md#type-translation +//= type=implication +//= reason=Every conversion function in this crate converts between the target and core-library representations in the direction(s) it supports. +//# - A translation MUST convert between the target representation and the core +//# library representation, preserving meaning in both directions it supports. +// +//= spec/shim/shim.md#type-translation +//= type=implication +//= reason=Every target-to-core conversion returns Err for a target value that has no core-library representation. +//# - When the target supplies a value that has no corresponding core-library +//# representation, the translation MUST return an error. +// +//= spec/shim/shim.md#type-translation +//= type=implication +//= reason=Every core-to-target conversion returns Err for a core-library value the shim does not define. +//# - When the core library produces a value the shim does not define, the +//# translation MUST return an error. +// +//= spec/shim/shim.md#delegation +//= type=implication +//= reason=No function in this crate validates operation or resource-creation inputs; each value is translated and passed to the core library, which performs all validation. +//# - The shim MUST NOT validate operation or resource-creation input values itself; +//# it MUST translate each input and defer its validation to the core library. +// +//= spec/shim/shim.md#delegation +//= type=implication +//= reason=Every operation in this crate produces its result by calling the corresponding aws_esdk core-library function. +//# - Each operation the shim exposes MUST produce its result by invoking the +//# corresponding core-library operation. +// +//= spec/shim/shim.md#operation-contracts +//= type=implication +//= reason=Every operation invokes the core library, satisfying Delegation. +//# - Each operation MUST invoke the core library per [Delegation](#delegation). +// +//= spec/shim/shim.md#operation-contracts +//= type=implication +//= reason=Every operation input/output is either passed through unmodified or converted by a Type-translation function. +//# - Except where an input or output is passed through unmodified, each MUST be +//# translated per [Type translation](#type-translation). +// +//= spec/shim/shim.md#creating-and-releasing-resources +//= type=implication +//= reason=Each resource kind has its own create_* function in the cxx bridge (keyring, key store, KMS client, DynamoDB client). +//# - For each resource kind, the shim MUST provide a means for the target to create an +//# instance backed by the core library. +// +//= spec/shim/shim.md#creating-and-releasing-resources +//= type=implication +//= reason=Each resource is a Box owned by the target; dropping the owned interface frees the backing core-library resource. +//# - The shim MUST release the core-library resource backing an owned interface once +//# the target no longer holds it. +// +//= spec/shim/shim.md#creating-and-releasing-resources +//= type=implication +//= reason=Dropping the Box at end of scope releases the resource automatically when the owned interface is destroyed. +//# The shim MAY release it automatically when the +//# owned interface is destroyed, or through an explicit target operation. +// +//= spec/shim/shim.md#concurrency +//= type=exception +//= reason=The shim adds no concurrency feature of its own and does not exercise concurrent use in this no-AWS crate; bridge entry points defer any threading behavior to the core library. +//# If a core library resource or operation supports concurrent use from multiple threads, +//# the shim library SHOULD support it as well. +// +//= spec/shim/shim.md#streaming +//= type=exception +//= reason=The owned interface exposes one-shot encrypt/decrypt (Vec in, Vec out); streamed input/output is not surfaced. +//# If a core library resource or operation supports [streamed](../client-apis/streaming.md) input/outputs, +//# the shim library SHOULD support it as well. +use aws_config::{AppName, Region, SdkConfig}; +use std::sync::LazyLock; + +static DAFNY_TOKIO_RUNTIME: LazyLock = LazyLock::new(|| { + // Building a multi-thread Tokio runtime only fails if the OS cannot spawn a + // thread or install the I/O/timer drivers — an environment failure with no + // meaningful library-level recovery. We surface it as a panic at first use + // rather than threading a `Result` through every bridge entry point. + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() +}); + +#[cxx::bridge] +mod ffi { + struct EncryptionContextItem { + key: String, + value: String, + } + enum EsdkAlgorithmSuiteId { + AlgAes128GcmIv12Tag16NoKdf = 0x0014, + AlgAes192GcmIv12Tag16NoKdf = 0x0046, + AlgAes256GcmIv12Tag16NoKdf = 0x0078, + AlgAes128GcmIv12Tag16HkdfSha256 = 0x0114, + AlgAes192GcmIv12Tag16HkdfSha256 = 0x0146, + AlgAes256GcmIv12Tag16HkdfSha256 = 0x0178, + AlgAes128GcmIv12Tag16HkdfSha256EcdsaP256 = 0x0214, + AlgAes192GcmIv12Tag16HkdfSha384EcdsaP384 = 0x0346, + AlgAes256GcmIv12Tag16HkdfSha384EcdsaP384 = 0x0378, + AlgAes256GcmHkdfSha512CommitKey = 0x0478, + AlgAes256GcmHkdfSha512CommitKeyEcdsaP384 = 0x0578, + } + + struct EncryptInput<'a> { + /// Algorithm Suite. See + pub algorithm_suite_id: EsdkAlgorithmSuiteId, + /// Key-Value pairs to associate with the encrypted data + pub encryption_context: Vec, + /// Bytes of plaintext data per frame. Default 4096. + pub frame_length: u32, + /// The source of cryptographic materials + pub keyring: *const Keyring, + /// data to be encrypted + pub plaintext: &'a [u8], + /// default is no limit + pub max_encrypted_data_keys: u32, + /// default is `EsdkCommitmentPolicy::RequireEncryptRequireDecrypt` + pub commitment_policy: EsdkCommitmentPolicy, + } + struct EncryptOutput { + /// Algorithm Suite. See + pub algorithm_suite_id: EsdkAlgorithmSuiteId, + /// data to be decrypted + pub ciphertext: Vec, + /// Key-Value pairs to associate with the encrypted data + pub encryption_context: Vec, + } + struct DecryptInput<'a> { + /// data to be decrypted + pub ciphertext: &'a [u8], + /// Key-Value pairs to associate with the encrypted data + pub encryption_context: Vec, + /// The source of cryptographic materials + pub keyring: *const Keyring, + /// default is no limit + pub max_encrypted_data_keys: u32, + /// default is `EsdkCommitmentPolicy::RequireEncryptRequireDecrypt` + pub commitment_policy: EsdkCommitmentPolicy, + } + struct DecryptOutput { + /// Algorithm Suite. See + pub algorithm_suite_id: EsdkAlgorithmSuiteId, + /// Key-Value pairs to associate with the encrypted data + pub encryption_context: Vec, + /// decrypted data + pub plaintext: Vec, + } + + enum EsdkCommitmentPolicy { + ForbidEncryptAllowDecrypt, + RequireEncryptAllowDecrypt, + RequireEncryptRequireDecrypt, + } + + struct RetryConfig { + mode_adaptive: bool, + max_attempts: u32, + initial_backoff_milli: u64, + max_backoff_milli: u64, + reconnect_all: bool, + use_static_exponential_base: bool, + } + + enum KmsConfigurationType { + KmsKeyArn, + KmsMrKeyArn, + Discovery, + MrDiscovery, + } + + enum CacheType { + NoCache, + MultiThreadedCache, + } + + struct MplAwsClientConfig { + env: bool, + region: String, + retry: RetryConfig, + } + + struct MultiThreadedCacheConfig { + entryCapacity: u32, + entryPruningTailSize: u32, + } + + struct KeyStoreConfig { + ddb_table_name: String, + kms_configuration_type: KmsConfigurationType, + kms_configuration_value: String, + logical_key_store_name: String, + id: String, + grant_tokens: Vec, + ddb_client: *const MplDdbClient, + kms_client: *const MplKmsClient, + } + + struct HierarchicalKeyringInput { + branch_key_id: String, + key_store: *const KeyStore, + ttl: u32, + cache: CacheType, + multi_threaded_cache: MultiThreadedCacheConfig, + partition_id: String, + } + + extern "Rust" { + type MplDdbClient; + fn create_ddb_client(value: &MplAwsClientConfig) -> Result>; + fn delete_ddb_client(client: Box) -> Result<()>; + + type KeyStore; + fn create_keystore(value: &KeyStoreConfig) -> Result>; + fn delete_keystore(client: Box) -> Result<()>; + + type Keyring; + fn create_hierarchical_keyring(value: &HierarchicalKeyringInput) -> Result>; + fn delete_keyring(client: Box) -> Result<()>; + + type MplKmsClient; + fn create_kms_client(value: &MplAwsClientConfig) -> Result>; + fn delete_kms_client(client: Box) -> Result<()>; + + fn encrypt(input: &EncryptInput) -> Result; + fn decrypt(input: &DecryptInput) -> Result; + + fn default_client_config() -> MplAwsClientConfig; + fn default_keystore_config() -> KeyStoreConfig; + fn default_hierarchical_keyring_input() -> HierarchicalKeyringInput; + fn default_encrypt_input() -> EncryptInput<'static>; + fn default_decrypt_input() -> DecryptInput<'static>; + } +} + +struct MplKmsClient { + client: aws_sdk_kms::Client, +} +struct MplDdbClient { + client: aws_sdk_dynamodb::Client, +} + +struct KeyStore { + client: aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::client::Client, +} + +struct Keyring { + client: aws_mpl_legacy::dafny::types::keyring::KeyringRef, +} + +//= spec/shim/esdk-shim.md#commitment-policy +//# - The shim MUST translate the target commitment policy to the core ESDK +//# commitment policy of the same meaning. +fn commitment_policy_target_to_core( + x: ffi::EsdkCommitmentPolicy, +) -> Result { + use aws_mpl_legacy::commitment::EsdkCommitmentPolicy as New; + use ffi::EsdkCommitmentPolicy as Old; + match x { + Old::ForbidEncryptAllowDecrypt => Ok(New::ForbidEncryptAllowDecrypt), + Old::RequireEncryptAllowDecrypt => Ok(New::RequireEncryptAllowDecrypt), + Old::RequireEncryptRequireDecrypt => Ok(New::RequireEncryptRequireDecrypt), + //= spec/shim/esdk-shim.md#commitment-policy + //# - The shim MUST return an error when the target commitment policy is not a supported + //# value. + _ => Err(format!("unrecognized EsdkCommitmentPolicy value: {}", x.repr)), + } +} + +fn default_encrypt_input() -> ffi::EncryptInput<'static> { + ffi::EncryptInput { + algorithm_suite_id: ffi::EsdkAlgorithmSuiteId::AlgAes256GcmHkdfSha512CommitKeyEcdsaP384, + encryption_context: Vec::default(), + frame_length: 4096, + keyring: std::ptr::null(), + plaintext: &[], + max_encrypted_data_keys: 0, + commitment_policy: ffi::EsdkCommitmentPolicy::RequireEncryptRequireDecrypt, + } +} + +fn default_decrypt_input() -> ffi::DecryptInput<'static> { + ffi::DecryptInput { + ciphertext: &[], + encryption_context: Vec::default(), + keyring: std::ptr::null(), + max_encrypted_data_keys: 0, + commitment_policy: ffi::EsdkCommitmentPolicy::RequireEncryptRequireDecrypt, + } +} + +fn default_hierarchical_keyring_input() -> ffi::HierarchicalKeyringInput { + ffi::HierarchicalKeyringInput { + branch_key_id: String::default(), + key_store: std::ptr::null(), + ttl: 300, + cache: ffi::CacheType::MultiThreadedCache, + multi_threaded_cache: ffi::MultiThreadedCacheConfig { + entryCapacity: 1000, + entryPruningTailSize: 1, + }, + partition_id: String::default(), + } +} + +fn default_keystore_config() -> ffi::KeyStoreConfig { + ffi::KeyStoreConfig { + ddb_table_name: String::default(), + kms_configuration_type: ffi::KmsConfigurationType::KmsKeyArn, + kms_configuration_value: String::default(), + logical_key_store_name: String::default(), + id: String::default(), + grant_tokens: Vec::default(), + ddb_client: std::ptr::null(), + kms_client: std::ptr::null(), + } +} + +fn default_client_config() -> ffi::MplAwsClientConfig { + ffi::MplAwsClientConfig { + env: true, + region: String::default(), + retry: default_retry_config(), + } +} + +fn default_retry_config() -> ffi::RetryConfig { + ffi::RetryConfig { + mode_adaptive: false, + max_attempts: 0, + initial_backoff_milli: 0, + max_backoff_milli: 0, + reconnect_all: false, + use_static_exponential_base: false, + } +} + +//= spec/shim/esdk-shim.md#algorithm-suite-identifier +//# - The shim MUST translate a target algorithm suite identifier to the core ESDK +//# algorithm suite that has the same two-byte ID. +fn alg_id_target_to_core( + e: ffi::EsdkAlgorithmSuiteId, +) -> Result { + use aws_mpl_legacy::suites::EsdkAlgorithmSuiteId as New; + use ffi::EsdkAlgorithmSuiteId as Old; + match e { + Old::AlgAes128GcmIv12Tag16NoKdf => Ok(New::AlgAes128GcmIv12Tag16NoKdf), + Old::AlgAes192GcmIv12Tag16NoKdf => Ok(New::AlgAes192GcmIv12Tag16NoKdf), + Old::AlgAes256GcmIv12Tag16NoKdf => Ok(New::AlgAes256GcmIv12Tag16NoKdf), + Old::AlgAes128GcmIv12Tag16HkdfSha256 => Ok(New::AlgAes128GcmIv12Tag16HkdfSha256), + Old::AlgAes192GcmIv12Tag16HkdfSha256 => Ok(New::AlgAes192GcmIv12Tag16HkdfSha256), + Old::AlgAes256GcmIv12Tag16HkdfSha256 => Ok(New::AlgAes256GcmIv12Tag16HkdfSha256), + Old::AlgAes128GcmIv12Tag16HkdfSha256EcdsaP256 => { + Ok(New::AlgAes128GcmIv12Tag16HkdfSha256EcdsaP256) + } + Old::AlgAes192GcmIv12Tag16HkdfSha384EcdsaP384 => { + Ok(New::AlgAes192GcmIv12Tag16HkdfSha384EcdsaP384) + } + Old::AlgAes256GcmIv12Tag16HkdfSha384EcdsaP384 => { + Ok(New::AlgAes256GcmIv12Tag16HkdfSha384EcdsaP384) + } + Old::AlgAes256GcmHkdfSha512CommitKey => Ok(New::AlgAes256GcmHkdfSha512CommitKey), + Old::AlgAes256GcmHkdfSha512CommitKeyEcdsaP384 => { + Ok(New::AlgAes256GcmHkdfSha512CommitKeyEcdsaP384) + } + //= spec/shim/esdk-shim.md#algorithm-suite-identifier + //# - The shim MUST return an error when the target value is not one of the algorithm + //# suite IDs defined by the core ESDK. + _ => Err(format!("unrecognized EsdkAlgorithmSuiteId value: {:#06x}", e.repr)), + } +} + +//= spec/shim/esdk-shim.md#algorithm-suite-identifier +//# - The shim MUST translate a core ESDK algorithm suite to the target algorithm +//# suite identifier that has the same two-byte ID. +fn alg_id_core_to_target( + e: aws_mpl_legacy::suites::EsdkAlgorithmSuiteId, +) -> Result { + use aws_mpl_legacy::suites::EsdkAlgorithmSuiteId as Old; + use ffi::EsdkAlgorithmSuiteId as New; + match e { + Old::AlgAes128GcmIv12Tag16NoKdf => Ok(New::AlgAes128GcmIv12Tag16NoKdf), + Old::AlgAes192GcmIv12Tag16NoKdf => Ok(New::AlgAes192GcmIv12Tag16NoKdf), + Old::AlgAes256GcmIv12Tag16NoKdf => Ok(New::AlgAes256GcmIv12Tag16NoKdf), + Old::AlgAes128GcmIv12Tag16HkdfSha256 => Ok(New::AlgAes128GcmIv12Tag16HkdfSha256), + Old::AlgAes192GcmIv12Tag16HkdfSha256 => Ok(New::AlgAes192GcmIv12Tag16HkdfSha256), + Old::AlgAes256GcmIv12Tag16HkdfSha256 => Ok(New::AlgAes256GcmIv12Tag16HkdfSha256), + Old::AlgAes128GcmIv12Tag16HkdfSha256EcdsaP256 => { + Ok(New::AlgAes128GcmIv12Tag16HkdfSha256EcdsaP256) + } + Old::AlgAes192GcmIv12Tag16HkdfSha384EcdsaP384 => { + Ok(New::AlgAes192GcmIv12Tag16HkdfSha384EcdsaP384) + } + Old::AlgAes256GcmIv12Tag16HkdfSha384EcdsaP384 => { + Ok(New::AlgAes256GcmIv12Tag16HkdfSha384EcdsaP384) + } + Old::AlgAes256GcmHkdfSha512CommitKey => Ok(New::AlgAes256GcmHkdfSha512CommitKey), + Old::AlgAes256GcmHkdfSha512CommitKeyEcdsaP384 => { + Ok(New::AlgAes256GcmHkdfSha512CommitKeyEcdsaP384) + } + //= spec/shim/esdk-shim.md#algorithm-suite-identifier + //= type=implication + //= reason=The core ESDK enum is exhaustive over its defined suites; a value it does not define cannot be constructed, so this defensive arm cannot be reached by a runtime test. + //# - The shim MUST return an error when the core ESDK produces an algorithm suite + //# the shim does not define. + _ => Err("core returned an algorithm suite id unknown to the C++ bindings".to_string()), + } +} + +//= spec/shim/esdk-shim.md#encryption-context +//# - The shim MUST translate the core ESDK's encryption context, given as a +//# key-value map, to the target's list of key-value pairs, preserving every pair +//# and the exact key and value of each. +fn encryption_context_core_to_target( + x: std::collections::HashMap, +) -> Vec { + x.into_iter() + .map(|(k, v)| ffi::EncryptionContextItem { key: k, value: v }) + .collect() +} + +//= spec/shim/esdk-shim.md#encryption-context +//# - The shim MUST translate the target's encryption context, given as a list of +//# key-value pairs, to the core ESDK's key-value map, preserving every pair and +//# the exact key and value of each. +fn encryption_context_target_to_core(x: &[ffi::EncryptionContextItem]) -> std::collections::HashMap { + x.iter().map(|x| (x.key.clone(), x.value.clone())).collect() +} + +//= spec/shim/esdk-shim.md#maximum-encrypted-data-keys +//# The shim MUST translate zero +//# to the core ESDK's "no limit" representation, and a positive value `n` to a +//# limit of `n`. +fn max_edks_target_to_core(n: u32) -> Option { + std::num::NonZeroUsize::new(usize::try_from(n).unwrap_or(usize::MAX)) +} + +//= spec/shim/esdk-shim.md#frame-length +//# - The shim MUST translate the target frame length using the core ESDK's frame +//# length constructor, and MUST return an error when the core ESDK rejects the +//# value. +fn make_frame_length(frame_length: u32) -> Result { + aws_esdk::FrameLength::new(frame_length).map_err(|e| format!("invalid frame_length: {:?}", e)) +} + +/// The user-agent this shim publishes: target language (C++), core language +/// (Rust), and the crate's published version. +fn shim_user_agent() -> String { + //= spec/shim/esdk-shim.md#service-client-configuration + //# - The shim MUST set a user-agent on each service client of the form + //# `AwsEncryptionSdk-Shim---`, where + //# `` is the target language, `` is the core + //# ESDK's implementation language, and `` is the shim's published version. + format!("AwsEncryptionSdk-Shim-C++-Rust-{}", env!("CARGO_PKG_VERSION")) +} + +/// Merge the shim's user-agent with any user-agent already present in the loaded +/// configuration. +fn merge_user_agent(current_app_name: &str, shim_ua: &str) -> String { + if current_app_name.is_empty() { + shim_ua.to_string() + } else { + //= spec/shim/esdk-shim.md#service-client-configuration + //# - The shim MUST preserve any user-agent already present in the loaded + //# configuration, appending its own rather than replacing it. + format!("{} {}", current_app_name, shim_ua) + } +} + +fn encrypt(input: &ffi::EncryptInput) -> Result { + //= spec/shim/esdk-shim.md#materials-source + //# - The shim MUST return an error, and MUST NOT invoke the core ESDK, when no + //# materials source is supplied. + if input.keyring.is_null() { + return Err("keyring is null in encrypt".to_string()); + } + + let mut core_input = aws_esdk::EncryptInput::with_legacy_keyring( + //= spec/shim/esdk-shim.md#encrypt-inputs + //# - `encrypt` MUST pass the target-supplied plaintext to the core ESDK unmodified. + input.plaintext, + //= spec/shim/esdk-shim.md#encrypt-inputs + //# - `encrypt` MUST provide the target-supplied encryption context to the core ESDK, + //# converted as defined in [Encryption context](#encryption-context). + encryption_context_target_to_core(&input.encryption_context), + //= spec/shim/esdk-shim.md#materials-source + //= type=implication + //= reason=EncryptInput carries exactly one materials-source field (keyring); the type admits no second source. + //# - Each of `encrypt` and `decrypt` MUST be supplied with exactly one materials + //# source (see [Resources](#resources)). + unsafe { (*input.keyring).client.clone() }, + ); + //= spec/shim/esdk-shim.md#encrypt-inputs + //# - `encrypt` MUST provide the target-supplied maximum-encrypted-data-keys value to + //# the core ESDK, converted as defined in + //# [Maximum encrypted data keys](#maximum-encrypted-data-keys). + core_input.max_encrypted_data_keys = max_edks_target_to_core(input.max_encrypted_data_keys); + //= spec/shim/esdk-shim.md#encrypt-inputs + //# - `encrypt` MUST provide the target-supplied frame length to the core ESDK, + //# converted as defined in [Frame length](#frame-length). + core_input.frame_length = make_frame_length(input.frame_length)?; + //= spec/shim/esdk-shim.md#encrypt-inputs + //# - `encrypt` MUST provide the target-supplied commitment policy to the core ESDK, + //# converted as defined in [Commitment policy](#commitment-policy). + core_input.commitment_policy = commitment_policy_target_to_core(input.commitment_policy)?; + //= spec/shim/esdk-shim.md#encrypt-inputs + //# - `encrypt` MUST provide the target-supplied algorithm suite to the core ESDK, + //# converted as defined in [Algorithm suite identifier](#algorithm-suite-identifier). + core_input.algorithm_suite_id = Some(alg_id_target_to_core(input.algorithm_suite_id)?); + let output = DAFNY_TOKIO_RUNTIME.block_on(aws_esdk::encrypt(&core_input)); + //= spec/shim/shim.md#delegation + //# - When a core-library operation returns an error, the shim MUST return an error to + //# the target and MUST NOT report the operation as successful. + // + //= spec/shim/shim.md#delegation + //# - An error the shim returns for a failed core-library operation SHOULD include + //# context identifying which operation failed. + let output = output.map_err(|e| format!("encrypt failed: {:?}", e))?; + Ok(ffi::EncryptOutput { + //= spec/shim/esdk-shim.md#encrypt-outputs + //# - `encrypt` MUST return the used algorithm suite, converted as defined in + //# [Algorithm suite identifier](#algorithm-suite-identifier). + algorithm_suite_id: alg_id_core_to_target(output.algorithm_suite_id)?, + //= spec/shim/esdk-shim.md#encrypt-outputs + //# - `encrypt` MUST return the core ESDK's ciphertext unmodified. + ciphertext: output.ciphertext, + //= spec/shim/esdk-shim.md#encrypt-outputs + //# - `encrypt` MUST return the result encryption context, converted as defined in + //# [Encryption context](#encryption-context). + encryption_context: encryption_context_core_to_target(output.encryption_context), + }) +} +fn decrypt(input: &ffi::DecryptInput) -> Result { + //= spec/shim/esdk-shim.md#materials-source + //# - The shim MUST return an error, and MUST NOT invoke the core ESDK, when no + //# materials source is supplied. + if input.keyring.is_null() { + return Err("keyring is null in decrypt".to_string()); + } + + let mut core_input = aws_esdk::DecryptInput::with_legacy_keyring( + //= spec/shim/esdk-shim.md#decrypt-inputs + //# - `decrypt` MUST pass the target-supplied ciphertext to the core ESDK unmodified. + input.ciphertext, + //= spec/shim/esdk-shim.md#decrypt-inputs + //# - `decrypt` MUST provide the target-supplied encryption context to the core ESDK, + //# converted as defined in [Encryption context](#encryption-context). + encryption_context_target_to_core(&input.encryption_context), + unsafe { (*input.keyring).client.clone() }, + ); + //= spec/shim/esdk-shim.md#decrypt-inputs + //# - `decrypt` MUST provide the target-supplied maximum-encrypted-data-keys value to + //# the core ESDK, converted as defined in + //# [Maximum encrypted data keys](#maximum-encrypted-data-keys). + core_input.max_encrypted_data_keys = max_edks_target_to_core(input.max_encrypted_data_keys); + //= spec/shim/esdk-shim.md#decrypt-inputs + //# - `decrypt` MUST provide the target-supplied commitment policy to the core ESDK, + //# converted as defined in [Commitment policy](#commitment-policy). + core_input.commitment_policy = commitment_policy_target_to_core(input.commitment_policy)?; + let output = DAFNY_TOKIO_RUNTIME.block_on(aws_esdk::decrypt(&core_input)); + let output = output.map_err(|e| format!("decrypt failed: {:?}", e))?; + Ok(ffi::DecryptOutput { + //= spec/shim/esdk-shim.md#decrypt-outputs + //# - `decrypt` MUST return the used algorithm suite, converted as defined in + //# [Algorithm suite identifier](#algorithm-suite-identifier). + algorithm_suite_id: alg_id_core_to_target(output.algorithm_suite_id)?, + //= spec/shim/esdk-shim.md#decrypt-outputs + //# - `decrypt` MUST return the core ESDK's plaintext unmodified. + plaintext: output.plaintext, + //= spec/shim/esdk-shim.md#decrypt-outputs + //# - `decrypt` MUST return the result encryption context, converted as defined in + //# [Encryption context](#encryption-context). + encryption_context: encryption_context_core_to_target(output.encryption_context), + }) +} + +//= spec/shim/esdk-shim.md#service-client-configuration +//# - If the target does not supply a retry configuration, the shim MAY apply a +//# default retry configuration. +fn make_retry_config(config: &ffi::RetryConfig) -> aws_config::retry::RetryConfig { + let mut out_config = if config.mode_adaptive { + aws_config::retry::RetryConfig::adaptive() + } else { + aws_config::retry::RetryConfig::standard() + }; + if config.max_attempts > 0 { + out_config = out_config.with_max_attempts(config.max_attempts); + } + if config.initial_backoff_milli > 0 { + out_config = out_config.with_initial_backoff(std::time::Duration::from_millis( + config.initial_backoff_milli, + )); + } + if config.max_backoff_milli > 0 { + out_config = + out_config.with_max_backoff(std::time::Duration::from_millis(config.max_backoff_milli)); + } + if config.reconnect_all { + out_config = out_config + .with_reconnect_mode(aws_sdk_kms::config::retry::ReconnectMode::ReuseAllConnections); + } + if config.use_static_exponential_base { + out_config = out_config.with_use_static_exponential_base(true); + } + out_config +} +fn delete_kms_client(_client: Box) -> Result<(), String> { + Ok(()) +} +fn delete_ddb_client(_client: Box) -> Result<(), String> { + Ok(()) +} +fn delete_keystore(_client: Box) -> Result<(), String> { + Ok(()) +} +fn delete_keyring(_client: Box) -> Result<(), String> { + Ok(()) +} + +fn make_cache_type( + config: &ffi::HierarchicalKeyringInput, +) -> Result { + match config.cache { + //= spec/shim/esdk-shim.md#cache-configuration + //# - The shim MUST translate a no-cache selection. + ffi::CacheType::NoCache => Ok(aws_mpl_legacy::dafny::types::CacheType::No( + aws_mpl_legacy::dafny::types::NoCache::builder() + .build() + .map_err(|e| format!("failed to build NoCache: {:?}", e))?, + )), + //= spec/shim/esdk-shim.md#cache-configuration + //# - The shim MUST translate a multi-threaded cache selection. + ffi::CacheType::MultiThreadedCache => { + let entry_capacity = i32::try_from(config.multi_threaded_cache.entryCapacity) + .map_err(|_| { + format!( + "entry_capacity {} exceeds the core ESDK's i32 limit", + config.multi_threaded_cache.entryCapacity + ) + })?; + let entry_pruning_tail_size = + i32::try_from(config.multi_threaded_cache.entryPruningTailSize).map_err(|_| { + format!( + "entry_pruning_tail_size {} exceeds the core ESDK's i32 limit", + config.multi_threaded_cache.entryPruningTailSize + ) + })?; + Ok(aws_mpl_legacy::dafny::types::CacheType::MultiThreaded( + aws_mpl_legacy::dafny::types::MultiThreadedCache::builder() + .entry_capacity(entry_capacity) + .entry_pruning_tail_size(entry_pruning_tail_size) + .build() + .map_err(|e| format!("failed to build MultiThreadedCache: {:?}", e))?, + )) + } + //= spec/shim/esdk-shim.md#cache-configuration + //# - The shim MUST return an error when the target cache type is not a supported + //# value. + _ => Err("Invalid CacheType in HierarchicalKeyringInput".to_string()), + } +} + +fn make_kms_config( + config: &ffi::KeyStoreConfig, +) -> Result { + match config.kms_configuration_type { + //= spec/shim/esdk-shim.md#kms-configuration + //# - The shim MUST translate a KMS key ARN configuration. + ffi::KmsConfigurationType::KmsKeyArn => Ok( + aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::types::KmsConfiguration::KmsKeyArn( + config.kms_configuration_value.clone(), + ), + ), + //= spec/shim/esdk-shim.md#kms-configuration + //# - The shim MUST translate a KMS multi-Region-key ARN configuration. + ffi::KmsConfigurationType::KmsMrKeyArn => Ok( + aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::types::KmsConfiguration::KmsMrKeyArn( + config.kms_configuration_value.clone(), + ), + ), + //= spec/shim/esdk-shim.md#kms-configuration + //# - The shim MUST translate a discovery configuration. + ffi::KmsConfigurationType::Discovery => Ok( + aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::types::KmsConfiguration::Discovery( + aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::types::Discovery::builder() + .build() + .map_err(|e| format!("failed to build Discovery: {:?}", e))?, + ), + ), + //= spec/shim/esdk-shim.md#kms-configuration + //# - The shim MUST translate a multi-Region-key discovery configuration. + ffi::KmsConfigurationType::MrDiscovery => Ok( + aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::types::KmsConfiguration::MrDiscovery( + aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::types::MrDiscovery::builder() + .build() + .map_err(|e| format!("failed to build MrDiscovery: {:?}", e))?, + ), + ), + //= spec/shim/esdk-shim.md#kms-configuration + //# - The shim MUST return an error when the target KMS configuration type is not a + //# supported value. + _ => Err("Invalid KmsConfigurationType".to_string()), + } +} + +//= spec/shim/esdk-shim.md#create-hierarchical-keyring +//# - The shim MUST provide an operation that creates a hierarchical keyring backed +//# by the core ESDK. +fn create_hierarchical_keyring( + input: &ffi::HierarchicalKeyringInput, +) -> Result, String> { + let mpl_config = aws_mpl_legacy::dafny::types::MaterialProvidersConfig::builder() + .build() + .map_err(|e| format!("failed to build MaterialProvidersConfig: {:?}", e))?; + let mpl = aws_mpl_legacy::dafny::Client::from_conf(mpl_config) + .map_err(|e| format!("failed to create MPL client: {:?}", e))?; + //= spec/shim/esdk-shim.md#create-hierarchical-keyring + //# - Creating a hierarchical keyring MUST provide the target-supplied cache + //# configuration to the core ESDK, converted as defined in + //# [Cache configuration](#cache-configuration). + // + //= spec/shim/esdk-shim.md#create-hierarchical-keyring + //# - Creating a hierarchical keyring MUST provide the target-supplied branch key id + //# and time-to-live to the core ESDK. + let mut builder = mpl + .create_aws_kms_hierarchical_keyring() + .cache(make_cache_type(input)?) + .branch_key_id(input.branch_key_id.clone()) + .ttl_seconds(input.ttl); + //= spec/shim/esdk-shim.md#create-hierarchical-keyring + //# - Creating a hierarchical keyring MUST require a key store handle, checked as + //# defined in + //# [Shim Specification: Handles and lifetimes](./shim.md#handles-and-lifetimes), + //# and MUST provide it to the core ESDK. + if input.key_store.is_null() { + return Err("key_store is null in create_hierarchical_keyring".to_string()); + } else { + builder = builder.key_store(unsafe { (*input.key_store).client.clone() }) + } + //= spec/shim/esdk-shim.md#create-hierarchical-keyring + //# - Creating a hierarchical keyring MUST provide the target-supplied partition id to + //# the core ESDK when present; an unset value is omitted. + // The "when present" branch is exercised by `round_trip_integration` (partition + // id set). The "unset ⇒ omitted" branch simply skips the builder call; the + // omission is a construction guarantee, not separately testable without AWS. + if !input.partition_id.is_empty() { + builder = builder.partition_id(input.partition_id.clone()); + } + + let keyring = DAFNY_TOKIO_RUNTIME + .block_on(builder.send()) + .map_err(|e| format!("failed to create hierarchical keyring: {:?}", e))?; + + let keyring = Keyring { client: keyring }; + Ok(Box::new(keyring)) +} + +//= spec/shim/esdk-shim.md#create-key-store +//# - The shim MUST provide an operation that creates a key store backed by the core +//# ESDK. +fn create_keystore(input: &ffi::KeyStoreConfig) -> Result, String> { + let mut builder = aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::types::key_store_config::KeyStoreConfig::builder(); + //= spec/shim/esdk-shim.md#create-key-store + //# - Creating a key store MUST require a KMS client handle and a DynamoDB client + //# handle, checked as defined in + //# [Shim Specification: Handles and lifetimes](./shim.md#handles-and-lifetimes), + //# and MUST provide both to the core ESDK. + // + //= spec/shim/shim.md#handles-and-lifetimes + //# - Before dereferencing a target-supplied resource handle, the shim MUST check it + //# and MUST return an error when a required handle is absent. + if input.kms_client.is_null() { + return Err("kms_client is null in create_keystore".to_string()); + } else { + builder = builder.kms_client(unsafe { (*input.kms_client).client.clone() }); + } + if input.ddb_client.is_null() { + return Err("ddb_client is null in create_keystore".to_string()); + } else { + builder = builder.ddb_client(unsafe { (*input.ddb_client).client.clone() }); + } + // Input values are translated and passed through; the core ESDK validates + // them (the shim does not short-circuit on empty/invalid values). + //= spec/shim/esdk-shim.md#create-key-store + //# - Creating a key store MUST provide the target-supplied table name and logical key + //# store name to the core ESDK. + builder = builder.ddb_table_name(input.ddb_table_name.clone()); + builder = builder.logical_key_store_name(input.logical_key_store_name.clone()); + //= spec/shim/esdk-shim.md#create-key-store + //# - Creating a key store MUST provide the target-supplied KMS configuration to the + //# core ESDK, converted as defined in [KMS configuration](#kms-configuration). + builder = builder.kms_configuration(make_kms_config(input)?); + //= spec/shim/esdk-shim.md#create-key-store + //# - Creating a key store MUST provide the target-supplied key store id and grant + //# tokens to the core ESDK when present; an unset value is omitted. + // The "when present" branch is exercised by `round_trip_integration` (id and + // grant tokens set). The "unset ⇒ omitted" branch simply skips the builder + // call, so whether the core received the value is only observable through a + // real key store; the omission is a construction guarantee, not separately + // testable without AWS. + if !input.id.is_empty() { + builder = builder.id(input.id.clone()); + } + if !input.grant_tokens.is_empty() { + builder = builder.grant_tokens(input.grant_tokens.clone()); + } + let config = builder + .build() + .map_err(|e| format!("failed to build KeyStoreConfig: {:?}", e))?; + + let store = aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::client::Client::from_conf(config) + .map_err(|e| format!("failed to create key store client: {:?}", e))?; + let store = KeyStore { client: store }; + Ok(Box::new(store)) +} + +//= spec/shim/esdk-shim.md#create-kms-client +//# - The shim MUST provide an operation that creates a KMS service client backed by +//# the core ESDK. +fn create_kms_client(input: &ffi::MplAwsClientConfig) -> Result, String> { + //= spec/shim/esdk-shim.md#create-kms-client + //# - Creating a KMS client MUST apply the target-supplied client configuration, as + //# defined in [Service client configuration](#service-client-configuration). + let sdk_config = create_sdk_config(input)?; + let client = aws_sdk_kms::Client::new(&sdk_config); + let client = MplKmsClient { client }; + Ok(Box::new(client)) +} + +//= spec/shim/esdk-shim.md#create-dynamodb-client +//# - The shim MUST provide an operation that creates a DynamoDB service client +//# backed by the core ESDK. +fn create_ddb_client(input: &ffi::MplAwsClientConfig) -> Result, String> { + //= spec/shim/esdk-shim.md#create-dynamodb-client + //# - Creating a DynamoDB client MUST apply the target-supplied client configuration, + //# as defined in [Service client configuration](#service-client-configuration). + let sdk_config = create_sdk_config(input)?; + let client = aws_sdk_dynamodb::Client::new(&sdk_config); + let client = MplDdbClient { client }; + Ok(Box::new(client)) +} + +//= spec/shim/esdk-shim.md#service-client-configuration +//= type=implication +//= reason=create_kms_client and create_ddb_client each build their SdkConfig from the same MplAwsClientConfig, so one configuration can be applied to multiple clients. +//# - The shim SHOULD allow one client configuration to be applied to multiple +//# service clients. +fn create_sdk_config(input: &ffi::MplAwsClientConfig) -> Result { + let shared_config = DAFNY_TOKIO_RUNTIME.block_on(aws_config::load_defaults( + aws_config::BehaviorVersion::latest(), + )); + + // Target (C++) and core (Rust) languages are fixed for this shim; the version + // is the crate version from Cargo.toml (i.e. the version this crate publishes). + let user_agent_string = shim_user_agent(); + let current_app_name = shared_config + .app_name() + .map(|app_name| app_name.to_string()) + .unwrap_or_default(); + let new_app_name = merge_user_agent(¤t_app_name, &user_agent_string); + let app_name = AppName::new(new_app_name).map_err(|e| format!("invalid app name: {:?}", e))?; + //= spec/shim/esdk-shim.md#service-client-configuration + //# - If the target supplies a retry configuration, the shim MUST apply it to each + //# service client. + let mut builder = shared_config + .to_builder() + .app_name(app_name) + .retry_config(make_retry_config(&input.retry)); + //= spec/shim/esdk-shim.md#service-client-configuration + //# - If the target supplies a region, the shim MUST apply it to each service client. + if !input.region.is_empty() { + builder = builder.region(Region::new(input.region.clone())); + } + Ok(builder.build()) +} + +//= spec/shim/esdk-shim.md#conformance-and-testing +//= type=exception +//= reason=Cross-implementation message interoperability is exercised by the shared ESDK test-vector suite maintained outside this shim crate, not within it. +//# - The ESDK shim SHOULD demonstrate message interoperability with other ESDK +//# implementations (for example via a shared test-vector suite). +#[cfg(test)] +mod tests; diff --git a/aws-esdk-cpp/aws-esdk-cpp/src/tests.rs b/aws-esdk-cpp/aws-esdk-cpp/src/tests.rs new file mode 100644 index 000000000..9e796c22e --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/src/tests.rs @@ -0,0 +1,658 @@ +//! Unit tests for the shim's pure conversion layer. +//! +//! These test the shim's OWN behavior (correct mapping + no target abort on +//! bad input), not ESDK crypto correctness. No AWS resources required. +//! +//! Declared from `lib.rs` as `#[cfg(test)] mod tests;`, so it is a child of the +//! crate root and reaches the crate-private conversion functions via `super::`. + +use super::*; +use ffi::EsdkAlgorithmSuiteId as F; +use ffi::EsdkCommitmentPolicy as P; + +// The complete set of algorithm-suite discriminants the shim supports. +// Mirrors the `ffi::EsdkAlgorithmSuiteId` enum in the bridge. +const ALG_REPRS: [u16; 11] = [ + 0x0014, 0x0046, 0x0078, 0x0114, 0x0146, 0x0178, 0x0214, 0x0346, 0x0378, 0x0478, 0x0578, +]; + +// alg_id_target_to_core: ffi enum -> core enum + +/// Exhaustiveness (ffi -> core): the set of values `alg_id_target_to_core` accepts must +/// be EXACTLY the known suite ids — no more, no fewer. Adding a suite id without +/// updating the conversion (or vice versa) fails this test. +//= spec/shim/shim.md#conformance-and-testing +//= type=test +//# - The shim SHOULD be tested for the behavior it owns — translation, handle +//# validation, error propagation, and lifetime safety — rather than by re-testing +//# the core library's own behavior. +// +//= spec/shim/esdk-shim.md#conformance-and-testing +//= type=test +//# - The ESDK shim MUST be tested for the behavior it owns — translation, handle +//# validation, error propagation, and lifetime safety — rather than by re-testing +//# the core ESDK's cryptographic behavior. +#[test] +fn alg_id_target_to_core_accepts_exactly_known_reprs() { + let mut accepted: Vec = (0..=u16::MAX) + .filter(|&n| alg_id_target_to_core(F { repr: n }).is_ok()) + .collect(); + accepted.sort_unstable(); + let mut expected = ALG_REPRS.to_vec(); + expected.sort_unstable(); + //= spec/shim/esdk-shim.md#algorithm-suite-identifier + //= type=test + //# - The shim MUST translate a target algorithm suite identifier to the core ESDK + //# algorithm suite that has the same two-byte ID. + assert_eq!(accepted, expected); +} + +/// Security property: NO value a C++ caller can supply may abort the target. +/// Sweeping the entire u16 domain proves `alg_id_target_to_core` returns Ok/Err for +/// every possible input and never panics. +//= spec/shim/shim.md#type-translation +//= type=test +//# - A translation reports failure by returning an error; it MUST NOT panic, +//# abort, or otherwise terminate the target process. +#[test] +fn alg_id_target_to_core_never_panics_over_full_range() { + for n in 0..=u16::MAX { + let _ = alg_id_target_to_core(F { repr: n }); + } +} + +/// An unknown suite id returns an error rather than panicking. +#[test] +fn alg_id_target_to_core_rejects_unknown() { + //= spec/shim/esdk-shim.md#algorithm-suite-identifier + //= type=test + //# - The shim MUST return an error when the target value is not one of the algorithm + //# suite IDs defined by the core ESDK. + // + //= spec/shim/shim.md#type-translation + //= type=test + //# - When the target supplies a value that has no corresponding core-library + //# representation, the translation MUST return an error. + let err = alg_id_target_to_core(F { repr: 0xFFFF }).unwrap_err(); + assert!(err.contains("unrecognized"), "unexpected error: {err}"); + assert!(alg_id_target_to_core(F { repr: 0x0000 }).is_err()); +} + +// alg_id_core_to_target: core enum -> ffi enum + +/// Every core variant maps to the expected ffi discriminant. +#[test] +fn alg_id_core_to_target_maps_every_core_variant() { + use aws_mpl_legacy::suites::EsdkAlgorithmSuiteId as C; + let pairs: [(C, u16); 11] = [ + (C::AlgAes128GcmIv12Tag16NoKdf, 0x0014), + (C::AlgAes192GcmIv12Tag16NoKdf, 0x0046), + (C::AlgAes256GcmIv12Tag16NoKdf, 0x0078), + (C::AlgAes128GcmIv12Tag16HkdfSha256, 0x0114), + (C::AlgAes192GcmIv12Tag16HkdfSha256, 0x0146), + (C::AlgAes256GcmIv12Tag16HkdfSha256, 0x0178), + (C::AlgAes128GcmIv12Tag16HkdfSha256EcdsaP256, 0x0214), + (C::AlgAes192GcmIv12Tag16HkdfSha384EcdsaP384, 0x0346), + (C::AlgAes256GcmIv12Tag16HkdfSha384EcdsaP384, 0x0378), + (C::AlgAes256GcmHkdfSha512CommitKey, 0x0478), + (C::AlgAes256GcmHkdfSha512CommitKeyEcdsaP384, 0x0578), + ]; + for (core, repr) in pairs { + //= spec/shim/esdk-shim.md#algorithm-suite-identifier + //= type=test + //# - The shim MUST translate a core ESDK algorithm suite to the target algorithm + //# suite identifier that has the same two-byte ID. + assert_eq!(alg_id_core_to_target(core).unwrap().repr, repr); + } +} + +/// Round-trip: every known ffi suite id survives ffi -> core -> ffi. +#[test] +fn alg_id_round_trips_through_both_conversions() { + for &n in &ALG_REPRS { + let core = alg_id_target_to_core(F { repr: n }).expect("known suite id converts"); + //= spec/shim/shim.md#type-translation + //= type=test + //= reason=The suite id survives ffi -> core -> ffi unchanged, proving the translation preserves meaning in both directions. + //# - A translation MUST convert between the target representation and the core + //# library representation, preserving meaning in both directions it supports. + let back = alg_id_core_to_target(core).expect("round-trips back"); + assert_eq!(back.repr, n); + } +} + +// commitment_policy_target_to_core: ffi enum -> core enum + +/// Exhaustiveness + correctness: each known repr maps to the core commitment +/// policy of the same meaning, and exactly the reprs {0,1,2} are accepted. +#[test] +fn commitment_policy_target_to_core_accepts_exactly_known_reprs() { + use aws_mpl_legacy::commitment::EsdkCommitmentPolicy as C; + let pairs: [(P, C); 3] = [ + (P::ForbidEncryptAllowDecrypt, C::ForbidEncryptAllowDecrypt), + (P::RequireEncryptAllowDecrypt, C::RequireEncryptAllowDecrypt), + (P::RequireEncryptRequireDecrypt, C::RequireEncryptRequireDecrypt), + ]; + for (target, expected_core) in pairs { + //= spec/shim/esdk-shim.md#commitment-policy + //= type=test + //# - The shim MUST translate the target commitment policy to the core ESDK + //# commitment policy of the same meaning. + assert_eq!( + commitment_policy_target_to_core(target).unwrap(), + expected_core + ); + } + + let accepted: Vec = (0..=u8::MAX) + .filter(|&n| commitment_policy_target_to_core(P { repr: n }).is_ok()) + .collect(); + //= spec/shim/esdk-shim.md#commitment-policy + //= type=test + //= reason=Sweeping 0..=u8::MAX, only 0,1,2 are accepted; every other value returns an error. + //# - The shim MUST return an error when the target commitment policy is not a supported + //# value. + assert_eq!(accepted, vec![0u8, 1, 2]); +} + +/// No commitment-policy value from C++ can abort the target. +#[test] +fn commitment_policy_target_to_core_never_panics_over_full_range() { + for n in 0..=u8::MAX { + let _ = commitment_policy_target_to_core(P { repr: n }); + } +} + +// encryption context: HashMap <-> Vec + +#[test] +fn encryption_context_round_trips() { + use std::collections::HashMap; + let mut m = HashMap::new(); + m.insert("k1".to_string(), "v1".to_string()); + m.insert("k2".to_string(), "v2".to_string()); + let list = encryption_context_core_to_target(m.clone()); + assert_eq!(list.len(), 2); + //= spec/shim/esdk-shim.md#encryption-context + //= type=test + //# - The shim MUST translate the target's encryption context, given as a list of + //# key-value pairs, to the core ESDK's key-value map, preserving every pair and + //# the exact key and value of each. + // + //= spec/shim/esdk-shim.md#encryption-context + //= type=test + //= reason=Round-trip map -> list -> map equals the original, proving the core-to-target conversion preserves every pair, key and value. + //# - The shim MUST translate the core ESDK's encryption context, given as a + //# key-value map, to the target's list of key-value pairs, preserving every pair + //# and the exact key and value of each. + assert_eq!(encryption_context_target_to_core(&list), m); +} + +#[test] +fn encryption_context_empty() { + // Regression: the empty map/list must translate to an empty result in both + // directions (no spurious pairs introduced), guarding the conversion helpers' + // edge case. + use std::collections::HashMap; + let empty: HashMap = HashMap::new(); + assert!(encryption_context_core_to_target(empty).is_empty()); + assert!(encryption_context_target_to_core(&[]).is_empty()); +} + +// Orchestration error paths (NO AWS resources required) +// These reach the null-checks / enum-validation in the create_*/encrypt/decrypt +// functions, which all return before any network call. They exercise the +// error-handling lines without credentials, so they run in ordinary CI. + +#[test] +fn encrypt_rejects_null_keyring() { + // default_encrypt_input() has a null keyring pointer. + //= spec/shim/esdk-shim.md#materials-source + //= type=test + //# - The shim MUST return an error, and MUST NOT invoke the core ESDK, when no + //# materials source is supplied. + let err = match encrypt(&default_encrypt_input()) { + Ok(_) => panic!("expected error"), + Err(e) => e, + }; + assert!(err.contains("keyring is null"), "unexpected error: {err}"); +} + +#[test] +fn decrypt_rejects_null_keyring() { + // Regression coverage for the symmetric decrypt null-check. The + // materials-source requirement is annotated once (on encrypt) to avoid a + // duplicate citation. + let err = match decrypt(&default_decrypt_input()) { + Ok(_) => panic!("expected error"), + Err(e) => e, + }; + assert!(err.contains("keyring is null"), "unexpected error: {err}"); +} + +#[test] +fn create_keystore_rejects_null_kms_client() { + // default config has null kms_client / ddb_client pointers. + let err = match create_keystore(&default_keystore_config()) { + Ok(_) => panic!("expected error"), + Err(e) => e, + }; + //= spec/shim/shim.md#handles-and-lifetimes + //= type=test + //# - Before dereferencing a target-supplied resource handle, the shim MUST check it + //# and MUST return an error when a required handle is absent. + // + //= spec/shim/esdk-shim.md#create-key-store + //= type=test + //# - Creating a key store MUST require a KMS client handle and a DynamoDB client + //# handle, checked as defined in + //# [Shim Specification: Handles and lifetimes](./shim.md#handles-and-lifetimes), + //# and MUST provide both to the core ESDK. + assert!(err.contains("kms_client is null"), "unexpected error: {err}"); +} + +#[test] +fn create_hierarchical_keyring_rejects_null_key_store() { + // default input has a null key_store pointer; the handle check fires before + // any network call. + //= spec/shim/esdk-shim.md#create-hierarchical-keyring + //= type=test + //# - Creating a hierarchical keyring MUST require a key store handle, checked as + //# defined in + //# [Shim Specification: Handles and lifetimes](./shim.md#handles-and-lifetimes), + //# and MUST provide it to the core ESDK. + let err = match create_hierarchical_keyring(&default_hierarchical_keyring_input()) { + Ok(_) => panic!("expected error"), + Err(e) => e, + }; + assert!(err.contains("key_store is null"), "unexpected error: {err}"); +} + +#[test] +fn make_kms_config_rejects_unknown_type() { + let mut cfg = default_keystore_config(); + cfg.kms_configuration_type = ffi::KmsConfigurationType { repr: 99 }; + //= spec/shim/esdk-shim.md#kms-configuration + //= type=test + //# - The shim MUST return an error when the target KMS configuration type is not a + //# supported value. + let err = make_kms_config(&cfg).unwrap_err(); + assert!(err.contains("Invalid KmsConfigurationType"), "unexpected error: {err}"); +} + +#[test] +fn make_kms_config_accepts_key_arn() { + // default type is KmsKeyArn -> exercises the happy arm (local, no AWS). + use aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::types::KmsConfiguration; + //= spec/shim/esdk-shim.md#kms-configuration + //= type=test + //# - The shim MUST translate a KMS key ARN configuration. + assert!(matches!( + make_kms_config(&default_keystore_config()).unwrap(), + KmsConfiguration::KmsKeyArn(_) + )); +} + +#[test] +fn make_kms_config_accepts_mr_key_arn() { + use aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::types::KmsConfiguration; + let mut cfg = default_keystore_config(); + cfg.kms_configuration_type = ffi::KmsConfigurationType::KmsMrKeyArn; + //= spec/shim/esdk-shim.md#kms-configuration + //= type=test + //# - The shim MUST translate a KMS multi-Region-key ARN configuration. + assert!(matches!( + make_kms_config(&cfg).unwrap(), + KmsConfiguration::KmsMrKeyArn(_) + )); +} + +#[test] +fn make_kms_config_accepts_discovery() { + use aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::types::KmsConfiguration; + let mut cfg = default_keystore_config(); + cfg.kms_configuration_type = ffi::KmsConfigurationType::Discovery; + //= spec/shim/esdk-shim.md#kms-configuration + //= type=test + //# - The shim MUST translate a discovery configuration. + assert!(matches!( + make_kms_config(&cfg).unwrap(), + KmsConfiguration::Discovery(_) + )); +} + +#[test] +fn make_kms_config_accepts_mr_discovery() { + use aws_mpl_legacy::dafny::deps::aws_cryptography_keyStore::types::KmsConfiguration; + let mut cfg = default_keystore_config(); + cfg.kms_configuration_type = ffi::KmsConfigurationType::MrDiscovery; + //= spec/shim/esdk-shim.md#kms-configuration + //= type=test + //# - The shim MUST translate a multi-Region-key discovery configuration. + assert!(matches!( + make_kms_config(&cfg).unwrap(), + KmsConfiguration::MrDiscovery(_) + )); +} + +#[test] +fn make_cache_type_rejects_unknown() { + let mut input = default_hierarchical_keyring_input(); + input.cache = ffi::CacheType { repr: 99 }; + //= spec/shim/esdk-shim.md#cache-configuration + //= type=test + //# - The shim MUST return an error when the target cache type is not a supported + //# value. + let err = make_cache_type(&input).unwrap_err(); + assert!(err.contains("Invalid CacheType"), "unexpected error: {err}"); +} + +#[test] +fn make_cache_type_builds_multi_threaded() { + // default cache is MultiThreaded -> exercises that arm (local, no AWS). + use aws_mpl_legacy::dafny::types::CacheType; + //= spec/shim/esdk-shim.md#cache-configuration + //= type=test + //# - The shim MUST translate a multi-threaded cache selection. + assert!(matches!( + make_cache_type(&default_hierarchical_keyring_input()).unwrap(), + CacheType::MultiThreaded(_) + )); +} + +#[test] +fn make_cache_type_builds_no_cache() { + use aws_mpl_legacy::dafny::types::CacheType; + let mut input = default_hierarchical_keyring_input(); + input.cache = ffi::CacheType::NoCache; + //= spec/shim/esdk-shim.md#cache-configuration + //= type=test + //# - The shim MUST translate a no-cache selection. + assert!(matches!( + make_cache_type(&input).unwrap(), + CacheType::No(_) + )); +} + +// Frame length + max-encrypted-data-keys translation (pure, no AWS) + +#[test] +fn make_frame_length_translates_and_rejects_zero() { + //= spec/shim/esdk-shim.md#frame-length + //= type=test + //# - The shim MUST translate the target frame length using the core ESDK's frame + //# length constructor, and MUST return an error when the core ESDK rejects the + //# value. + let err = make_frame_length(0).unwrap_err(); + assert!( + err.contains("invalid frame_length"), + "unexpected error: {err}" + ); + assert!(make_frame_length(4096).is_ok()); + assert!(make_frame_length(1).is_ok()); +} + +#[test] +fn max_edks_zero_is_no_limit_positive_is_limit() { + //= spec/shim/esdk-shim.md#maximum-encrypted-data-keys + //= type=test + //# The shim MUST translate zero + //# to the core ESDK's "no limit" representation, and a positive value `n` to a + //# limit of `n`. + assert_eq!(max_edks_target_to_core(0), None); + assert_eq!(max_edks_target_to_core(1), std::num::NonZeroUsize::new(1)); + assert_eq!(max_edks_target_to_core(42), std::num::NonZeroUsize::new(42)); +} + +// Service client user-agent + retry translation (pure, no AWS) + +#[test] +fn user_agent_form_and_append() { + // Empty existing app-name -> the shim's own user-agent, in the required form. + //= spec/shim/esdk-shim.md#service-client-configuration + //= type=test + //# - The shim MUST set a user-agent on each service client of the form + //# `AwsEncryptionSdk-Shim---`, where + //# `` is the target language, `` is the core + //# ESDK's implementation language, and `` is the shim's published version. + let ua = shim_user_agent(); + assert_eq!(merge_user_agent("", &ua), ua); + assert!(ua.starts_with("AwsEncryptionSdk-Shim-C++-Rust-")); + + // Existing app-name is preserved and the shim's UA appended, not replaced. + //= spec/shim/esdk-shim.md#service-client-configuration + //= type=test + //# - The shim MUST preserve any user-agent already present in the loaded + //# configuration, appending its own rather than replacing it. + let merged = merge_user_agent("existing-agent/1.0", &ua); + assert!(merged.starts_with("existing-agent/1.0"), "must preserve existing: {merged}"); + assert!(merged.contains(&ua), "must append shim UA: {merged}"); +} + +#[test] +fn make_retry_config_applies_a_default() { + // With no explicit values the shim still produces a (default) retry config. + //= spec/shim/esdk-shim.md#service-client-configuration + //= type=test + //= reason=make_retry_config always returns a standard/adaptive RetryConfig even when the target supplies no values. + //# - If the target does not supply a retry configuration, the shim MAY apply a + //# default retry configuration. + let cfg = default_retry_config(); + let _ = make_retry_config(&cfg); + // With an explicit max_attempts the translation carries it through. + let mut with_attempts = default_retry_config(); + with_attempts.max_attempts = 5; + assert_eq!(make_retry_config(&with_attempts).max_attempts(), 5); +} + +// Service client SdkConfig assembly (builds config, no credentials used) + +#[test] +fn create_sdk_config_applies_region_retry_and_user_agent() { + let mut cfg = default_client_config(); + cfg.region = "us-west-2".to_string(); + cfg.retry.max_attempts = 7; + let sdk = create_sdk_config(&cfg).expect("build sdk config"); + + let region: Option<&str> = sdk.region().map(|r| r.as_ref()); + //= spec/shim/esdk-shim.md#service-client-configuration + //= type=test + //# - If the target supplies a region, the shim MUST apply it to each service client. + assert_eq!(region, Some("us-west-2")); + + //= spec/shim/esdk-shim.md#service-client-configuration + //= type=test + //# - If the target supplies a retry configuration, the shim MUST apply it to each + //# service client. + assert_eq!(sdk.retry_config().map(|r| r.max_attempts()), Some(7)); + + let app_name: &str = sdk.app_name().expect("app name is set").as_ref(); + // Regression: the assembled SdkConfig carries the shim's user-agent; the + // user-agent form/append requirements are annotated in user_agent_form_and_append. + assert!(app_name.contains("AwsEncryptionSdk-Shim-C++-Rust-")); +} + +// Full round-trip integration test (REQUIRES real AWS resources) +// Skips cleanly when the ESDK_TEST_* env vars are unset, so it never fails +// credential-less CI. Run under `cargo llvm-cov` in a creds-enabled job to +// cover the create_*/encrypt/decrypt success paths. + +fn test_env(name: &str) -> Option { + std::env::var(name).ok().filter(|s| !s.is_empty()) +} + +#[test] +fn round_trip_integration() { + let (kms_arn, table, logical, branch) = match ( + test_env("ESDK_TEST_KMS_KEY_ARN"), + test_env("ESDK_TEST_DDB_TABLE"), + test_env("ESDK_TEST_LOGICAL_KEYSTORE"), + test_env("ESDK_TEST_BRANCH_KEY_ID"), + ) { + (Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d), + _ => { + eprintln!( + "SKIP round_trip_integration: set ESDK_TEST_KMS_KEY_ARN, \ + ESDK_TEST_DDB_TABLE, ESDK_TEST_LOGICAL_KEYSTORE, ESDK_TEST_BRANCH_KEY_ID" + ); + return; + } + }; + + let mut client_cfg = default_client_config(); + client_cfg.region = test_env("ESDK_TEST_REGION").unwrap_or_default(); + + //= spec/shim/esdk-shim.md#create-kms-client + //= type=test + //# - The shim MUST provide an operation that creates a KMS service client backed by + //# the core ESDK. + // + //= spec/shim/esdk-shim.md#create-kms-client + //= type=test + //# - Creating a KMS client MUST apply the target-supplied client configuration, as + //# defined in [Service client configuration](#service-client-configuration). + let kms = create_kms_client(&client_cfg).expect("create kms client"); + + //= spec/shim/esdk-shim.md#create-dynamodb-client + //= type=test + //# - The shim MUST provide an operation that creates a DynamoDB service client + //# backed by the core ESDK. + // + //= spec/shim/esdk-shim.md#create-dynamodb-client + //= type=test + //# - Creating a DynamoDB client MUST apply the target-supplied client configuration, + //# as defined in [Service client configuration](#service-client-configuration). + let ddb = create_ddb_client(&client_cfg).expect("create ddb client"); + + let mut ks_cfg = default_keystore_config(); + //= spec/shim/esdk-shim.md#create-key-store + //= type=test + //# - Creating a key store MUST provide the target-supplied table name and logical key + //# store name to the core ESDK. + ks_cfg.ddb_table_name = table; + ks_cfg.logical_key_store_name = logical; + //= spec/shim/esdk-shim.md#create-key-store + //= type=test + //# - Creating a key store MUST provide the target-supplied KMS configuration to the + //# core ESDK, converted as defined in [KMS configuration](#kms-configuration). + ks_cfg.kms_configuration_type = ffi::KmsConfigurationType::KmsKeyArn; + ks_cfg.kms_configuration_value = kms_arn; + ks_cfg.kms_client = &*kms as *const MplKmsClient; + ks_cfg.ddb_client = &*ddb as *const MplDdbClient; + //= spec/shim/esdk-shim.md#create-key-store + //= type=test + //# - Creating a key store MUST provide the target-supplied key store id and grant + //# tokens to the core ESDK when present; an unset value is omitted. + ks_cfg.id = "shim-integration-test".to_string(); + ks_cfg.grant_tokens = vec!["grant-token-example".to_string()]; + //= spec/shim/esdk-shim.md#create-key-store + //= type=test + //# - The shim MUST provide an operation that creates a key store backed by the core + //# ESDK. + let keystore = create_keystore(&ks_cfg).expect("create keystore"); + + let mut kr_in = default_hierarchical_keyring_input(); + //= spec/shim/esdk-shim.md#create-hierarchical-keyring + //= type=test + //# - Creating a hierarchical keyring MUST provide the target-supplied branch key id + //# and time-to-live to the core ESDK. + kr_in.branch_key_id = branch; + kr_in.key_store = &*keystore as *const KeyStore; + //= spec/shim/esdk-shim.md#create-hierarchical-keyring + //= type=test + //# - Creating a hierarchical keyring MUST provide the target-supplied cache + //# configuration to the core ESDK, converted as defined in + //# [Cache configuration](#cache-configuration). + kr_in.cache = ffi::CacheType::MultiThreadedCache; + //= spec/shim/esdk-shim.md#create-hierarchical-keyring + //= type=test + //# - Creating a hierarchical keyring MUST provide the target-supplied partition id to + //# the core ESDK when present; an unset value is omitted. + kr_in.partition_id = "shim-partition".to_string(); + //= spec/shim/esdk-shim.md#create-hierarchical-keyring + //= type=test + //# - The shim MUST provide an operation that creates a hierarchical keyring backed + //# by the core ESDK. + let keyring = create_hierarchical_keyring(&kr_in).expect("create keyring"); + + let plaintext = b"Hello from the Rust integration test".to_vec(); + let mut ec = std::collections::HashMap::new(); + ec.insert("purpose".to_string(), "integration-test".to_string()); + + let mut enc_in = default_encrypt_input(); + enc_in.keyring = &*keyring as *const Keyring; + enc_in.plaintext = plaintext.as_slice(); + enc_in.encryption_context = ec + .iter() + .map(|(k, v)| ffi::EncryptionContextItem { key: k.clone(), value: v.clone() }) + .collect(); + enc_in.algorithm_suite_id = ffi::EsdkAlgorithmSuiteId::AlgAes256GcmHkdfSha512CommitKeyEcdsaP384; + // commitment policy, frame length, and max-EDKs are forwarded plumbing + // (plain source-side `implementation`); values here equal the ESDK defaults, so + // asserting on them would not falsify the forwarding — no `type=test` here. + enc_in.commitment_policy = ffi::EsdkCommitmentPolicy::RequireEncryptRequireDecrypt; + enc_in.frame_length = 4096; + enc_in.max_encrypted_data_keys = 3; + + let enc_out = encrypt(&enc_in).expect("encrypt"); + // Ciphertext differs from plaintext (encryption happened); the "unmodified" + // guarantee is proven below by the successful decrypt back to `plaintext`. + assert_ne!(enc_out.ciphertext, plaintext); + //= spec/shim/esdk-shim.md#encrypt-outputs + //= type=test + //# - `encrypt` MUST return the used algorithm suite, converted as defined in + //# [Algorithm suite identifier](#algorithm-suite-identifier). + // + //= spec/shim/esdk-shim.md#encrypt-inputs + //= type=test + //= reason=The returned used-suite equals the suite set on the input, proving the target-supplied algorithm suite reached the core ESDK. + //# - `encrypt` MUST provide the target-supplied algorithm suite to the core ESDK, + //# converted as defined in [Algorithm suite identifier](#algorithm-suite-identifier). + assert_eq!(enc_out.algorithm_suite_id.repr, enc_in.algorithm_suite_id.repr); + //= spec/shim/esdk-shim.md#encrypt-outputs + //= type=test + //# - `encrypt` MUST return the result encryption context, converted as defined in + //# [Encryption context](#encryption-context). + // + //= spec/shim/esdk-shim.md#encrypt-inputs + //= type=test + //= reason=The result encryption context contains the target-supplied pair, proving the encryption context reached the core ESDK. + //# - `encrypt` MUST provide the target-supplied encryption context to the core ESDK, + //# converted as defined in [Encryption context](#encryption-context). + assert_eq!( + encryption_context_target_to_core(&enc_out.encryption_context).get("purpose"), + Some(&"integration-test".to_string()) + ); + + let mut dec_in = default_decrypt_input(); + dec_in.keyring = &*keyring as *const Keyring; + dec_in.ciphertext = enc_out.ciphertext.as_slice(); + // commitment policy and max-EDKs are forwarded plumbing (plain source-side + // `implementation`); values here equal the ESDK defaults, so asserting on + // them would not falsify the forwarding — no `type=test` here. + dec_in.commitment_policy = ffi::EsdkCommitmentPolicy::RequireEncryptRequireDecrypt; + dec_in.max_encrypted_data_keys = 3; + //= spec/shim/esdk-shim.md#decrypt-inputs + //= type=test + //= reason=decrypt supplies the same encryption-context pair used at encrypt time; a successful round-trip proves it was provided to the core ESDK. + //# - `decrypt` MUST provide the target-supplied encryption context to the core ESDK, + //# converted as defined in [Encryption context](#encryption-context). + dec_in.encryption_context = ec + .iter() + .map(|(k, v)| ffi::EncryptionContextItem { key: k.clone(), value: v.clone() }) + .collect(); + + let dec_out = decrypt(&dec_in).expect("decrypt"); + // Round-trip fidelity — plaintext/ciphertext passed and returned unmodified, + // and the core operation actually invoked — is proven end-to-end through the + // C++ facade in test_esdk.cpp (see the `type=test` citations there). + assert_eq!(dec_out.plaintext, plaintext); + // The decrypt-output fields (used suite, result encryption context) are now + // surfaced by the C++ facade's DecryptResult and asserted there (test_esdk.cpp); + // these bridge-level checks remain but carry no duvet citation. + assert_eq!(dec_out.algorithm_suite_id.repr, enc_in.algorithm_suite_id.repr); + assert_eq!( + encryption_context_target_to_core(&dec_out.encryption_context).get("purpose"), + Some(&"integration-test".to_string()) + ); +} diff --git a/aws-esdk-cpp/aws-esdk-cpp/test_esdk.cpp b/aws-esdk-cpp/aws-esdk-cpp/test_esdk.cpp new file mode 100644 index 000000000..4e4a5eeae --- /dev/null +++ b/aws-esdk-cpp/aws-esdk-cpp/test_esdk.cpp @@ -0,0 +1,253 @@ +// test_esdk.cpp — integration tests for the idiomatic C++ facade (aws_esdk.hpp). +// +// These test the SHIM's own behavior through the public C++ API: +// * encrypt/decrypt round-trip fidelity, +// * encryption-context handling, +// * failures surface as Aws::Esdk::EsdkException (not a crash), +// * tampered ciphertext is rejected, +// * handle lifetime safety (a keyring keeps its clients/keystore alive even +// after the local wrappers go out of scope). +// +// They exercise the real crypto path, so they need real AWS resources. Provide +// them via environment variables; if any required var is unset the suite prints +// SKIP and exits 0 (so it never fails credential-less CI): +// +// ESDK_TEST_KMS_KEY_ARN KMS key ARN backing the key store +// ESDK_TEST_DDB_TABLE DynamoDB table name of the key store +// ESDK_TEST_LOGICAL_KEYSTORE logical key store name +// ESDK_TEST_BRANCH_KEY_ID branch key id provisioned in the key store +// ESDK_TEST_REGION (optional) AWS region, e.g. us-west-2 +// +// Build: see the `test_esdk` target in the Makefile (requires -std=c++17). + +#include +#include +#include +#include +#include + +#include + +// ---- Tiny assertion harness (no external test framework dependency) --------- +static int g_tests = 0; +static int g_failures = 0; + +#define CHECK(cond) \ + do { \ + ++g_tests; \ + if (!(cond)) { \ + ++g_failures; \ + std::cerr << "FAIL: " << #cond << " (" << __FILE__ << ":" \ + << __LINE__ << ")\n"; \ + } \ + } while (0) + +#define CHECK_THROWS_ESDK(expr) \ + do { \ + ++g_tests; \ + bool threw = false; \ + try { \ + expr; \ + } catch (const Aws::Esdk::EsdkException&) { \ + threw = true; \ + } catch (...) { \ + } \ + if (!threw) { \ + ++g_failures; \ + std::cerr << "FAIL: expected EsdkException from " << #expr << "\n"; \ + } \ + } while (0) + +namespace { + +struct TestConfig { + std::string kms_key_arn; + std::string ddb_table; + std::string logical_keystore; + std::string branch_key_id; + std::string region; +}; + +std::vector to_bytes(const std::string& s) { + return std::vector(s.begin(), s.end()); +} + +// Build a keyring. Deliberately constructs the clients/keystore as locals and +// moves them in, so the returned keyring is the sole owner — used by the +// lifetime test to prove the wrappers keep their dependencies alive. +Aws::Esdk::HierarchicalKeyring make_keyring(const TestConfig& cfg) { + auto client_config = Aws::Esdk::ClientConfig().with_max_retry_attempts(3); + if (!cfg.region.empty()) { + client_config.with_region(cfg.region); + } + Aws::Esdk::KmsClient kms(client_config); + Aws::Esdk::DdbClient ddb(client_config); + Aws::Esdk::KeyStore keystore(cfg.ddb_table, cfg.logical_keystore, + cfg.kms_key_arn, kms, ddb); + return Aws::Esdk::HierarchicalKeyring(std::move(keystore), cfg.branch_key_id, + /*ttl_seconds*/ 600, /*cache_capacity*/ 100); +} + +void test_round_trip(const TestConfig& cfg) { + auto keyring = make_keyring(cfg); + Aws::Esdk::EncryptionSDK sdk; + auto plaintext = to_bytes("Hello World"); + auto ciphertext = sdk.encrypt(plaintext, keyring); + CHECK(ciphertext != plaintext); + + //= spec/shim/shim.md#delegation + //= type=test + //= reason=Encrypting then decrypting back to the plaintext through the C++ facade proves the shim invoked the core library's encrypt and decrypt operations. + //# - Each operation the shim exposes MUST produce its result by invoking the + //# corresponding core-library operation. + // + //= spec/shim/esdk-shim.md#encrypt-inputs + //= type=test + //= reason=Round-tripping back to the original plaintext proves encrypt passed the target plaintext to the core library unmodified. + //# - `encrypt` MUST pass the target-supplied plaintext to the core ESDK unmodified. + // + //= spec/shim/esdk-shim.md#encrypt-outputs + //= type=test + //= reason=Decrypting the returned ciphertext back to the original plaintext proves encrypt returned the core library's ciphertext unmodified. + //# - `encrypt` MUST return the core ESDK's ciphertext unmodified. + // + //= spec/shim/esdk-shim.md#decrypt-inputs + //= type=test + //= reason=Decrypting this exact ciphertext back to the original plaintext proves it reached the core library unmodified; any modification would fail authentication. + //# - `decrypt` MUST pass the target-supplied ciphertext to the core ESDK unmodified. + // + //= spec/shim/esdk-shim.md#decrypt-outputs + //= type=test + //= reason=The recovered plaintext equals the original, proving decrypt returned the core library's plaintext unmodified. + //# - `decrypt` MUST return the core ESDK's plaintext unmodified. + auto decrypted = sdk.decrypt(ciphertext, keyring); + CHECK(decrypted.plaintext == plaintext); +} + +void test_round_trip_with_encryption_context(const TestConfig& cfg) { + auto keyring = make_keyring(cfg); + Aws::Esdk::EncryptionSDK sdk; + auto plaintext = to_bytes("context matters"); + std::map ec = {{"purpose", "test"}, {"origin", "cxx"}}; + auto ciphertext = sdk.encrypt(plaintext, keyring, + Aws::Esdk::AlgorithmSuiteId::AesGcm256Hkdf512CommitEcdsa384, + Aws::Esdk::CommitmentPolicy::RequireEncryptRequireDecrypt, ec); + auto decrypted = sdk.decrypt(ciphertext, keyring); + CHECK(decrypted.plaintext == plaintext); + + //= spec/shim/esdk-shim.md#decrypt-outputs + //= type=test + //= reason=The decrypt result exposes the encryption context the core library returned; it contains every pair supplied at encrypt time. + //# - `decrypt` MUST return the result encryption context, converted as defined in + //# [Encryption context](#encryption-context). + for (const auto& kv : ec) { + CHECK(decrypted.encryption_context.count(kv.first) == 1); + CHECK(decrypted.encryption_context[kv.first] == kv.second); + } + + //= spec/shim/esdk-shim.md#decrypt-outputs + //= type=test + //= reason=The decrypt result exposes the algorithm suite the core library used; it equals the suite selected at encrypt time. + //# - `decrypt` MUST return the used algorithm suite, converted as defined in + //# [Algorithm suite identifier](#algorithm-suite-identifier). + CHECK(decrypted.algorithm_suite_id == + Aws::Esdk::AlgorithmSuiteId::AesGcm256Hkdf512CommitEcdsa384); +} + +void test_decrypt_garbage_throws(const TestConfig& cfg) { + auto keyring = make_keyring(cfg); + Aws::Esdk::EncryptionSDK sdk; + auto garbage = to_bytes("this is not a valid ESDK message"); + // A failure must surface as an exception, never a crash/abort. + //= spec/shim/shim.md#delegation + //= type=test + //# - When a core-library operation returns an error, the shim MUST return an error to + //# the target and MUST NOT report the operation as successful. + CHECK_THROWS_ESDK(sdk.decrypt(garbage, keyring)); + + // The surfaced error identifies which operation failed (SHOULD context): + // EsdkException carries the core error string, which is prefixed "decrypt failed". + std::string msg; + try { + sdk.decrypt(garbage, keyring); + } catch (const Aws::Esdk::EsdkException& e) { + msg = e.what(); + } + //= spec/shim/shim.md#delegation + //= type=test + //# - An error the shim returns for a failed core-library operation SHOULD include + //# context identifying which operation failed. + CHECK(msg.find("decrypt") != std::string::npos); +} + +void test_tampered_ciphertext_throws(const TestConfig& cfg) { + auto keyring = make_keyring(cfg); + Aws::Esdk::EncryptionSDK sdk; + auto ciphertext = sdk.encrypt(to_bytes("tamper me"), keyring); + CHECK(!ciphertext.empty()); + ciphertext[ciphertext.size() / 2] ^= 0xFF; // flip a byte in the body + CHECK_THROWS_ESDK(sdk.decrypt(ciphertext, keyring)); +} + +// #3: the keyring is built inside make_keyring(); all local KmsClient/DdbClient/ +// KeyStore wrappers are destroyed when it returns. If the facade did NOT keep +// them alive, the raw pointers the bridge stored would dangle and this would +// crash. Succeeding proves the shared_ptr ownership works. +void test_lifetime_safety(const TestConfig& cfg) { + Aws::Esdk::HierarchicalKeyring keyring = make_keyring(cfg); + Aws::Esdk::EncryptionSDK sdk; + auto plaintext = to_bytes("outlives its dependencies"); + //= spec/shim/shim.md#handles-and-lifetimes + //= type=test + //= reason=make_keyring builds the KmsClient/DdbClient/KeyStore as locals and returns only the keyring; a successful encrypt+decrypt after those locals are destroyed proves the keyring retained ownership of its dependencies, so a handle could not outlive the resource it refers to. + //# - An owned interface that depends on another resource MUST retain ownership of + //# that resource for its own lifetime, so that a handle passed across the boundary + //# cannot outlive the resource it refers to. + auto ciphertext = sdk.encrypt(plaintext, keyring); + auto decrypted = sdk.decrypt(ciphertext, keyring); + CHECK(decrypted.plaintext == plaintext); +} + +const char* env_or_null(const char* name) { + const char* v = std::getenv(name); + return (v && *v) ? v : nullptr; +} + +} // namespace + +int main() { + const char* kms = env_or_null("ESDK_TEST_KMS_KEY_ARN"); + const char* ddb = env_or_null("ESDK_TEST_DDB_TABLE"); + const char* logical = env_or_null("ESDK_TEST_LOGICAL_KEYSTORE"); + const char* branch = env_or_null("ESDK_TEST_BRANCH_KEY_ID"); + const char* region = env_or_null("ESDK_TEST_REGION"); + + if (!kms || !ddb || !logical || !branch) { + std::cout << "SKIP: set ESDK_TEST_KMS_KEY_ARN, ESDK_TEST_DDB_TABLE, " + "ESDK_TEST_LOGICAL_KEYSTORE, and ESDK_TEST_BRANCH_KEY_ID " + "to run the C++ integration tests.\n"; + return 0; + } + + TestConfig cfg{kms, ddb, logical, branch, region ? region : ""}; + + // Each test builds its own keyring so a failure in one does not cascade. + try { + test_round_trip(cfg); + test_round_trip_with_encryption_context(cfg); + test_decrypt_garbage_throws(cfg); + test_tampered_ciphertext_throws(cfg); + test_lifetime_safety(cfg); + } catch (const std::exception& e) { + std::cerr << "FATAL: unexpected exception aborted the suite: " << e.what() << "\n"; + return 2; + } + + std::cout << (g_tests - g_failures) << "/" << g_tests << " checks passed\n"; + if (g_failures > 0) { + std::cerr << g_failures << " check(s) FAILED\n"; + return 1; + } + std::cout << "All C++ integration tests passed.\n"; + return 0; +} diff --git a/aws-esdk-cpp/src/lib.rs b/aws-esdk-cpp/src/lib.rs new file mode 100644 index 000000000..8ca453f83 --- /dev/null +++ b/aws-esdk-cpp/src/lib.rs @@ -0,0 +1 @@ +// Stub lib for aws-esdk-cpp-stub — tooling workaround, see aws-esdk-cpp/Cargo.toml.