diff --git a/.clang-tidy b/.clang-tidy index ab2a8131..36586010 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -22,6 +22,7 @@ Checks: '*, -llvmlibc-restrict-system-libc-headers, -misc-non-private-member-variables-in-classes, -misc-use-anonymous-namespace, + -misc-no-recursion, -modernize-use-nodiscard, -modernize-use-trailing-return-type, -readability-function-cognitive-complexity, diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e4327f7..bf9d8798 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,6 +114,8 @@ target_include_directories(${LIB_NAME} ${OPENSSL_INCLUDE_DIR} ) +install(TARGETS ${LIB_NAME} EXPORT mlspp-targets) + ### ### Tests ### @@ -125,9 +127,7 @@ endif() ### Exports ### set(CMAKE_EXPORT_PACKAGE_REGISTRY ON) -export(EXPORT mlspp-targets - NAMESPACE MLSPP:: - FILE ${CMAKE_CURRENT_BINARY_DIR}/mlspp-targets.cmake) +export(EXPORT mlspp-targets NAMESPACE MLSPP:: FILE mlspp-targets.cmake) export(PACKAGE MLSPP) configure_package_config_file(cmake/config.cmake.in @@ -144,8 +144,6 @@ write_basic_package_version_file( ### Install ### -install(TARGETS ${LIB_NAME} EXPORT mlspp-targets) - install( DIRECTORY include/ @@ -156,7 +154,16 @@ install( FILES ${CMAKE_CURRENT_BINARY_DIR}/mlspp-config.cmake ${CMAKE_CURRENT_BINARY_DIR}/mlspp-config-version.cmake - ${CMAKE_CURRENT_BINARY_DIR}/mlspp-targets.cmake + DESTINATION + ${CMAKE_INSTALL_DATADIR}/mlspp) + +install( + EXPORT + mlspp-targets + NAMESPACE + MLSPP:: + FILE + mlspp-targets.cmake DESTINATION ${CMAKE_INSTALL_DATADIR}/mlspp) diff --git a/Makefile b/Makefile index 29745db0..7626b1e1 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,9 @@ BUILD_DIR=build TEST_DIR=build/test CLANG_FORMAT=clang-format -i CLANG_TIDY=OFF +OPENSSL3_MANIFEST=alternatives/openssl_3 -.PHONY: all dev test ctest dtest dbtest libs test-libs test-all everything ci clean cclean format +.PHONY: all dev dev3 test ctest dtest dbtest libs test-libs test-all everything ci ci3 clean cclean format all: ${BUILD_DIR} cmake --build ${BUILD_DIR} --target mlspp @@ -22,6 +23,10 @@ dev: # too slow, and we can run them in CI cmake -B${BUILD_DIR} -DTESTING=ON -DCMAKE_BUILD_TYPE=Debug . +dev3: + # Like `dev`, but using OpenSSL 3 + cmake -B${BUILD_DIR} -DTESTING=ON -DCMAKE_BUILD_TYPE=Debug -DVCPKG_MANIFEST_DIR=${OPENSSL3_MANIFEST} . + test: ${BUILD_DIR} test/* cmake --build ${BUILD_DIR} --target mlspp_test @@ -53,7 +58,12 @@ everything: ${BUILD_DIR} ci: cmake -B ${BUILD_DIR} -DTESTING=ON -DCLANG_TIDY=ON -DSANITIZERS=ON \ - -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE="${VCPKG_TOOLCHAIN_FILE}" . + -DCMAKE_BUILD_TYPE=Debug . + +ci3: + # Like `ci`, but using OpenSSL 3 + cmake -B ${BUILD_DIR} -DTESTING=ON -DCLANG_TIDY=ON -DSANITIZERS=ON \ + -DCMAKE_BUILD_TYPE=Debug -DVCPKG_MANIFEST_DIR=${OPENSSL3_MANIFEST} . clean: cmake --build ${BUILD_DIR} --target clean diff --git a/alternatives/openssl_3/vcpkg.json b/alternatives/openssl_3/vcpkg.json index 4b4d7657..97c9cc3e 100644 --- a/alternatives/openssl_3/vcpkg.json +++ b/alternatives/openssl_3/vcpkg.json @@ -7,7 +7,8 @@ "name": "openssl", "version>=": "3.0.7" }, - "doctest" + "doctest", + "nlohmann-json" ], "builtin-baseline": "5908d702d61cea1429b223a0b7a10ab86bad4c78", "overrides": [ diff --git a/include/mls/credential.h b/include/mls/credential.h index c7e251df..e1616119 100644 --- a/include/mls/credential.h +++ b/include/mls/credential.h @@ -47,6 +47,18 @@ struct X509Credential SignatureScheme _signature_scheme; }; +struct UserInfoVCCredential +{ + UserInfoVCCredential() = default; + explicit UserInfoVCCredential(bytes userinfo_vc_jwt); + + bytes userinfo_vc_jwt; + + bool valid_for(const SignaturePublicKey& pub) const; + + TLS_SERIALIZABLE(userinfo_vc_jwt) +}; + tls::ostream& operator<<(tls::ostream& str, const X509Credential& obj); @@ -62,6 +74,9 @@ enum struct CredentialType : uint16_t basic = 1, x509 = 2, + userinfo_vc_draft_00 = 0xFE00, + multi_draft_00 = 0xFF00, + // GREASE values, included here mainly so that debugger output looks nice GREASE_0 = 0x0A0A, GREASE_1 = 0x1A1A, @@ -80,6 +95,31 @@ enum struct CredentialType : uint16_t GREASE_E = 0xEAEA, }; +// struct { +// Credential credential; +// SignaturePublicKey credential_key; +// opaque signature; +// } CredentialBinding +// +// struct { +// CredentialBinding bindings; +// } MultiCredential; +struct CredentialBinding; +struct CredentialBindingInput; + +struct MultiCredential +{ + MultiCredential() = default; + MultiCredential(const std::vector& binding_inputs, + const SignaturePublicKey& signature_key); + + std::vector bindings; + + bool valid_for(const SignaturePublicKey& pub) const; + + TLS_SERIALIZABLE(bindings) +}; + // struct { // CredentialType credential_type; // select (credential_type) { @@ -90,9 +130,10 @@ enum struct CredentialType : uint16_t // opaque cert_data<1..2^24-1>; // }; // } Credential; -class Credential +struct Credential { -public: + Credential() = default; + CredentialType type() const; template @@ -103,6 +144,10 @@ class Credential static Credential basic(const bytes& identity); static Credential x509(const std::vector& der_chain); + static Credential userinfo_vc(const bytes& userinfo_vc_jwt); + static Credential multi( + const std::vector& binding_inputs, + const SignaturePublicKey& signature_key); bool valid_for(const SignaturePublicKey& pub) const; @@ -110,7 +155,43 @@ class Credential TLS_TRAITS(tls::variant) private: - var::variant _cred; + using SpecificCredential = var::variant; + + Credential(SpecificCredential specific); + SpecificCredential _cred; +}; + +// XXX(RLB): This struct needs to appear below Credential so that all types are +// concrete at the appropriate points. +struct CredentialBindingInput +{ + CipherSuite cipher_suite; + Credential credential; + const SignaturePrivateKey& credential_priv; +}; + +struct CredentialBinding +{ + CipherSuite cipher_suite; + Credential credential; + SignaturePublicKey credential_key; + bytes signature; + + CredentialBinding() = default; + CredentialBinding(CipherSuite suite_in, + Credential credential_in, + const SignaturePrivateKey& credential_priv, + const SignaturePublicKey& signature_key); + + bool valid_for(const SignaturePublicKey& signature_key) const; + + TLS_SERIALIZABLE(cipher_suite, credential, credential_key, signature) + +private: + bytes to_be_signed(const SignaturePublicKey& signature_key) const; }; } // namespace mls @@ -119,5 +200,9 @@ namespace tls { TLS_VARIANT_MAP(mls::CredentialType, mls::BasicCredential, basic) TLS_VARIANT_MAP(mls::CredentialType, mls::X509Credential, x509) +TLS_VARIANT_MAP(mls::CredentialType, + mls::UserInfoVCCredential, + userinfo_vc_draft_00) +TLS_VARIANT_MAP(mls::CredentialType, mls::MultiCredential, multi_draft_00) -} // namespace TLS +} // namespace tls diff --git a/include/mls/crypto.h b/include/mls/crypto.h index ab507ebb..d25653bc 100644 --- a/include/mls/crypto.h +++ b/include/mls/crypto.h @@ -205,10 +205,14 @@ extern const std::string mls_content; extern const std::string leaf_node; extern const std::string key_package; extern const std::string group_info; +extern const std::string multi_credential; } // namespace sign_label struct SignaturePublicKey { + static SignaturePublicKey from_jwk(CipherSuite suite, + const std::string& json_str); + bytes data; bool verify(const CipherSuite& suite, @@ -216,6 +220,8 @@ struct SignaturePublicKey const bytes& message, const bytes& signature) const; + std::string to_jwk(CipherSuite suite) const; + TLS_SERIALIZABLE(data) }; @@ -224,6 +230,8 @@ struct SignaturePrivateKey static SignaturePrivateKey generate(CipherSuite suite); static SignaturePrivateKey parse(CipherSuite suite, const bytes& data); static SignaturePrivateKey derive(CipherSuite suite, const bytes& secret); + static SignaturePrivateKey from_jwk(CipherSuite suite, + const std::string& json_str); SignaturePrivateKey() = default; @@ -235,6 +243,7 @@ struct SignaturePrivateKey const bytes& message) const; void set_public_key(CipherSuite suite); + std::string to_jwk(CipherSuite suite) const; TLS_SERIALIZABLE(data) diff --git a/lib/bytes/include/bytes/bytes.h b/lib/bytes/include/bytes/bytes.h index ddb5dacf..713a3c64 100644 --- a/lib/bytes/include/bytes/bytes.h +++ b/lib/bytes/include/bytes/bytes.h @@ -106,6 +106,9 @@ struct bytes std::vector _data; }; +std::string +to_ascii(const bytes& data); + bytes from_ascii(const std::string& ascii); diff --git a/lib/bytes/src/bytes.cpp b/lib/bytes/src/bytes.cpp index 509e2532..6eb0fe34 100644 --- a/lib/bytes/src/bytes.cpp +++ b/lib/bytes/src/bytes.cpp @@ -79,6 +79,12 @@ bytes::operator^(const bytes& rhs) const return out; } +std::string +to_ascii(const bytes& data) +{ + return { data.begin(), data.end() }; +} + bytes from_ascii(const std::string& ascii) { diff --git a/lib/hpke/CMakeLists.txt b/lib/hpke/CMakeLists.txt index 30cf5aa9..afbc32f1 100644 --- a/lib/hpke/CMakeLists.txt +++ b/lib/hpke/CMakeLists.txt @@ -3,6 +3,7 @@ set(CURRENT_LIB_NAME hpke) ### ### Dependencies ### +find_package(nlohmann_json REQUIRED) find_package(OpenSSL 1.1 REQUIRED) ### @@ -14,7 +15,11 @@ file(GLOB_RECURSE LIB_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src add_library(${CURRENT_LIB_NAME} ${LIB_HEADERS} ${LIB_SOURCES}) add_dependencies(${CURRENT_LIB_NAME} bytes tls_syntax) -target_link_libraries(${CURRENT_LIB_NAME} PRIVATE bytes tls_syntax OpenSSL::Crypto) +target_link_libraries(${CURRENT_LIB_NAME} + PRIVATE + nlohmann_json::nlohmann_json OpenSSL::Crypto + PUBLIC + bytes tls_syntax) target_include_directories(${CURRENT_LIB_NAME} PUBLIC $ diff --git a/lib/hpke/include/hpke/base64.h b/lib/hpke/include/hpke/base64.h new file mode 100644 index 00000000..7f809729 --- /dev/null +++ b/lib/hpke/include/hpke/base64.h @@ -0,0 +1,20 @@ +#pragma once + +#include +using namespace bytes_ns; + +namespace hpke { + +std::string +to_base64(const bytes& data); + +std::string +to_base64url(const bytes& data); + +bytes +from_base64(const std::string& enc); + +bytes +from_base64url(const std::string& enc); + +} // namespace hpke diff --git a/lib/hpke/include/hpke/signature.h b/lib/hpke/include/hpke/signature.h index 8ee0b39b..39dae06f 100644 --- a/lib/hpke/include/hpke/signature.h +++ b/lib/hpke/include/hpke/signature.h @@ -46,9 +46,16 @@ struct Signature virtual bytes serialize(const PublicKey& pk) const = 0; virtual std::unique_ptr deserialize(const bytes& enc) const = 0; - virtual bytes serialize_private(const PrivateKey& sk) const; + virtual bytes serialize_private(const PrivateKey& sk) const = 0; virtual std::unique_ptr deserialize_private( - const bytes& skm) const; + const bytes& skm) const = 0; + + virtual std::unique_ptr import_jwk_private( + const std::string& json_str) const = 0; + virtual std::unique_ptr import_jwk( + const std::string& json_str) const = 0; + virtual std::string export_jwk_private(const PrivateKey& env) const = 0; + virtual std::string export_jwk(const PublicKey& env) const = 0; virtual bytes sign(const bytes& data, const PrivateKey& sk) const = 0; virtual bool verify(const bytes& data, diff --git a/lib/hpke/src/base64.cpp b/lib/hpke/src/base64.cpp new file mode 100644 index 00000000..7ff46eac --- /dev/null +++ b/lib/hpke/src/base64.cpp @@ -0,0 +1,101 @@ +#include + +#include "openssl_common.h" + +#include +#include +#include + +namespace hpke { + +std::string +to_base64(const bytes& data) +{ + if (data.empty()) { + return ""; + } + + const auto data_size = static_cast(data.size()); + + // base64 encoding produces 4 characters for every 3 input bytes (rounded up) + const auto out_size = (data_size + 2) / 3 * 4; + auto out = bytes(out_size + 1); // NUL terminator + + const auto result = EVP_EncodeBlock(out.data(), data.data(), data_size); + if (result != out_size) { + throw openssl_error(); + } + + out.resize(out.size() - 1); // strip NUL terminator + return to_ascii(out); +} + +std::string +to_base64url(const bytes& data) +{ + if (data.empty()) { + return ""; + } + + auto encoded = to_base64(data); + + auto pad_start = encoded.find_first_of('='); + if (pad_start != std::string::npos) { + encoded = encoded.substr(0, pad_start); + } + + std::replace(encoded.begin(), encoded.end(), '+', '-'); + std::replace(encoded.begin(), encoded.end(), '/', '_'); + + return encoded; +} + +bytes +from_base64(const std::string& enc) +{ + if (enc.length() == 0) { + return {}; + } + + if (enc.length() % 4 != 0) { + throw std::runtime_error("Base64 length is not divisible by 4"); + } + + const auto in = from_ascii(enc); + const auto in_size = static_cast(in.size()); + const auto out_size = in_size / 4 * 3; + auto out = bytes(out_size); + + const auto result = EVP_DecodeBlock(out.data(), in.data(), in_size); + if (result != out_size) { + throw openssl_error(); + } + + if (enc.substr(enc.length() - 2, enc.length()) == "==") { + out.resize(out.size() - 2); + } else if (enc.substr(enc.length() - 1, enc.length()) == "=") { + out.resize(out.size() - 1); + } + + return out; +} + +bytes +from_base64url(const std::string& enc) +{ + if (enc.empty()) { + return {}; + } + + auto enc_copy = enc; + std::replace(enc_copy.begin(), enc_copy.end(), '-', '+'); + std::replace(enc_copy.begin(), enc_copy.end(), '_', '/'); + + while (enc_copy.length() % 4 != 0) { + enc_copy += "="; + } + + return from_base64(enc_copy); +} + +} // namespace hpke diff --git a/lib/hpke/src/group.cpp b/lib/hpke/src/group.cpp index 5afcd025..754fb277 100644 --- a/lib/hpke/src/group.cpp +++ b/lib/hpke/src/group.cpp @@ -321,7 +321,7 @@ struct ECKeyGroup : public EVPGroup auto* group = EC_GROUP_new_by_curve_name_ex(nullptr, nullptr, curve_nid); auto group_ptr = make_typed_unique(group); #else - auto eckey = make_typed_unique(new_ec_key()); + auto eckey = new_ec_key(); const auto* group = EC_KEY_get0_group(eckey.get()); #endif @@ -358,7 +358,8 @@ struct ECKeyGroup : public EVPGroup EC_KEY_set_private_key(eckey.get(), sk.get()); EC_KEY_set_public_key(eckey.get(), pt.get()); - return std::make_unique(to_pkey(eckey.release())); + auto pkey = to_pkey(eckey.release()); + return std::make_unique(pkey.release()); #endif } @@ -449,7 +450,7 @@ struct ECKeyGroup : public EVPGroup } return std::make_unique(key.release()); #else - auto eckey = make_typed_unique(new_ec_key()); + auto eckey = new_ec_key(); auto* eckey_ptr = eckey.get(); const auto* data_ptr = enc.data(); if (nullptr == @@ -460,7 +461,8 @@ struct ECKeyGroup : public EVPGroup throw openssl_error(); } - return std::make_unique(to_pkey(eckey.release())); + auto pkey = to_pkey(eckey.release()); + return std::make_unique(pkey.release()); #endif } @@ -512,7 +514,7 @@ struct ECKeyGroup : public EVPGroup auto key = keypair_evp_key(priv); return std::make_unique(key.release()); #else - auto eckey = make_typed_unique(new_ec_key()); + auto eckey = new_ec_key(); const auto* group = EC_KEY_get0_group(eckey.get()); const auto d = make_typed_unique( BN_bin2bn(skm.data(), static_cast(skm.size()), nullptr)); @@ -522,7 +524,180 @@ struct ECKeyGroup : public EVPGroup EC_KEY_set_private_key(eckey.get(), d.get()); EC_KEY_set_public_key(eckey.get(), pt.get()); - return std::make_unique(to_pkey(eckey.release())); + auto pkey = to_pkey(eckey.release()); + return std::make_unique(pkey.release()); +#endif + } + + // EC Key + std::tuple coordinates( + const Group::PublicKey& pk) const override + { + auto bn_x = make_typed_unique(BN_new()); + auto bn_y = make_typed_unique(BN_new()); + const auto& rpk = dynamic_cast(pk); + +#if defined(WITH_OPENSSL3) + // Raw pointer OK here because it becomes managed as soon as possible + OSSL_PARAM* param_ptr = nullptr; + if (1 != EVP_PKEY_todata(rpk.pkey.get(), EVP_PKEY_PUBLIC_KEY, ¶m_ptr)) { + throw openssl_error(); + } + + auto param = make_typed_unique(param_ptr); + + // Raw pointer OK here because it is non-owning + const auto* pk_param = + OSSL_PARAM_locate_const(param.get(), OSSL_PKEY_PARAM_PUB_KEY); + if (pk_param == nullptr) { + throw std::runtime_error("Failed to locate OSSL_PKEY_PARAM_PUB_KEY"); + } + + // Copy the octet string representation of the key into a buffer + auto len = size_t(0); + if (1 != OSSL_PARAM_get_octet_string(pk_param, nullptr, 0, &len)) { + throw std::runtime_error("Failed to get OSSL_PKEY_PARAM_PUB_KEY len"); + } + + auto buf = bytes(len); + auto* buf_ptr = buf.data(); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + auto* buf_ptr_void = reinterpret_cast(buf_ptr); + if (1 != + OSSL_PARAM_get_octet_string(pk_param, &buf_ptr_void, len, nullptr)) { + throw std::runtime_error("Failed to get OSSL_PKEY_PARAM_PUB_KEY data"); + } + + // Parse the octet string representation into an EC_POINT + auto group = make_typed_unique( + EC_GROUP_new_by_curve_name_ex(nullptr, nullptr, curve_nid)); + if (group == nullptr) { + throw openssl_error(); + } + + auto point = make_typed_unique(EC_POINT_new(group.get())); + if (point == nullptr) { + throw openssl_error(); + } + + if (1 != + EC_POINT_oct2point(group.get(), point.get(), buf_ptr, len, nullptr)) { + throw openssl_error(); + } + + // Retrieve the affine coordinates of the point + if (1 != EC_POINT_get_affine_coordinates( + group.get(), point.get(), bn_x.get(), bn_y.get(), nullptr)) { + throw openssl_error(); + } +#else + // Raw pointers are non-owning + auto* pub = EVP_PKEY_get0_EC_KEY(rpk.pkey.get()); + const auto* point = EC_KEY_get0_public_key(pub); + const auto* group = EC_KEY_get0_group(pub); + + if (1 != EC_POINT_get_affine_coordinates_GFp( + group, point, bn_x.get(), bn_y.get(), nullptr)) { + throw openssl_error(); + } +#endif + const auto x_size = BN_num_bytes(bn_x.get()); + auto x = bytes(x_size); + if (BN_bn2bin(bn_x.get(), x.data()) != x_size) { + throw openssl_error(); + } + + const auto y_size = BN_num_bytes(bn_y.get()); + auto y = bytes(y_size); + if (BN_bn2bin(bn_y.get(), y.data()) != y_size) { + throw openssl_error(); + } + + const auto zeros_needed_x = dh_size - x.size(); + const auto zeros_needed_y = dh_size - y.size(); + auto leading_zeros_x = bytes(zeros_needed_x, 0); + auto leading_zeros_y = bytes(zeros_needed_y, 0); + + return { leading_zeros_x + x, leading_zeros_y + y }; + } + + // EC Key + std::unique_ptr public_key_from_coordinates( + const bytes& x, + const bytes& y) const override + { + auto bn_x = make_typed_unique( + BN_bin2bn(x.data(), static_cast(x.size()), nullptr)); + auto bn_y = make_typed_unique( + BN_bin2bn(y.data(), static_cast(y.size()), nullptr)); + + if (bn_x == nullptr || bn_y == nullptr) { + throw std::runtime_error("Failed to convert bn_x or bn_y"); + } + +#if defined(WITH_OPENSSL3) + const auto group = make_typed_unique( + EC_GROUP_new_by_curve_name_ex(nullptr, nullptr, curve_nid)); + if (group == nullptr) { + throw std::runtime_error("Failed to create EC_GROUP"); + } + + // Construct a point with the given coordinates + auto point = make_typed_unique(EC_POINT_new(group.get())); + if (group == nullptr) { + throw std::runtime_error("Failed to create EC_POINT"); + } + + if (1 != EC_POINT_set_affine_coordinates( + group.get(), point.get(), bn_x.get(), bn_y.get(), nullptr)) { + throw openssl_error(); + } + + // Serialize the point + const auto point_size = EC_POINT_point2oct(group.get(), + point.get(), + POINT_CONVERSION_UNCOMPRESSED, + nullptr, + 0, + nullptr); + if (0 == point_size) { + throw openssl_error(); + } + + auto pub = bytes(point_size); + if (EC_POINT_point2oct(group.get(), + point.get(), + POINT_CONVERSION_UNCOMPRESSED, + pub.data(), + point_size, + nullptr) != point_size) { + throw openssl_error(); + } + + // Initialize a public key from the serialized point + auto key = public_evp_key(pub); + return std::make_unique(key.release()); +#else + auto eckey = new_ec_key(); + if (eckey == nullptr) { + throw std::runtime_error("Failed to create EC_KEY"); + } + + // Group pointer is non-owning + const auto* group = EC_KEY_get0_group(eckey.get()); + auto point = make_typed_unique(EC_POINT_new(group)); + + if (1 != EC_POINT_set_affine_coordinates_GFp( + group, point.get(), bn_x.get(), bn_y.get(), nullptr)) { + throw openssl_error(); + } + + if (1 != EC_KEY_set_public_key(eckey.get(), point.get())) { + throw openssl_error(); + } + + auto pkey = to_pkey(eckey.release()); + return std::make_unique(pkey.release()); #endif } @@ -530,17 +705,17 @@ struct ECKeyGroup : public EVPGroup int curve_nid; #if !defined(WITH_OPENSSL3) - EC_KEY* new_ec_key() const + typed_unique_ptr new_ec_key() const { - return EC_KEY_new_by_curve_name(curve_nid); + return make_typed_unique(EC_KEY_new_by_curve_name(curve_nid)); } - static EVP_PKEY* to_pkey(EC_KEY* eckey) + static typed_unique_ptr to_pkey(EC_KEY* eckey) { auto* pkey = EVP_PKEY_new(); // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) EVP_PKEY_assign_EC_KEY(pkey, eckey); - return pkey; + return make_typed_unique(pkey); } #endif @@ -651,6 +826,30 @@ struct RawKeyGroup : public EVPGroup return std::make_unique(pkey); } + // Raw Key + std::tuple coordinates( + const Group::PublicKey& pk) const override + { + const auto& rpk = dynamic_cast(pk); + auto raw = bytes(pk_size); + auto* data_ptr = raw.data(); + auto data_len = raw.size(); + + if (1 != EVP_PKEY_get_raw_public_key(rpk.pkey.get(), data_ptr, &data_len)) { + throw openssl_error(); + } + + return { raw, {} }; + } + + // Raw Key + std::unique_ptr public_key_from_coordinates( + const bytes& x, + const bytes& /* y */) const override + { + return deserialize(x); + } + private: const int evp_type; @@ -812,11 +1011,54 @@ group_sk_size(Group::ID group_id) } } +static inline std::string +group_jwk_curve_name(Group::ID group_id) +{ + switch (group_id) { + case Group::ID::P256: + return "P-256"; + case Group::ID::P384: + return "P-384"; + case Group::ID::P521: + return "P-521"; + case Group::ID::Ed25519: + return "Ed25519"; + case Group::ID::Ed448: + return "Ed448"; + case Group::ID::X25519: + return "X25519"; + case Group::ID::X448: + return "X448"; + default: + throw std::runtime_error("Unknown group"); + } +} + +static inline std::string +group_jwk_key_type(Group::ID group_id) +{ + switch (group_id) { + case Group::ID::P256: + case Group::ID::P384: + case Group::ID::P521: + return "EC"; + case Group::ID::Ed25519: + case Group::ID::Ed448: + case Group::ID::X25519: + case Group::ID::X448: + return "OKP"; + default: + throw std::runtime_error("Unknown group"); + } +} + Group::Group(ID group_id_in, const KDF& kdf_in) : id(group_id_in) , dh_size(group_dh_size(group_id_in)) , pk_size(group_pk_size(group_id_in)) , sk_size(group_sk_size(group_id_in)) + , jwk_key_type(group_jwk_key_type(group_id_in)) + , jwk_curve_name(group_jwk_curve_name(group_id_in)) , kdf(kdf_in) { } diff --git a/lib/hpke/src/group.h b/lib/hpke/src/group.h index ace8d7a9..f6b19f9a 100644 --- a/lib/hpke/src/group.h +++ b/lib/hpke/src/group.h @@ -43,6 +43,8 @@ struct Group const size_t dh_size; const size_t pk_size; const size_t sk_size; + const std::string jwk_key_type; + const std::string jwk_curve_name; virtual std::unique_ptr generate_key_pair() const = 0; virtual std::unique_ptr derive_key_pair( @@ -63,6 +65,11 @@ struct Group const bytes& sig, const PublicKey& pk) const = 0; + virtual std::tuple coordinates(const PublicKey& pk) const = 0; + virtual std::unique_ptr public_key_from_coordinates( + const bytes& x, + const bytes& y) const = 0; + protected: const KDF& kdf; diff --git a/lib/hpke/src/rsa.cpp b/lib/hpke/src/rsa.cpp index 5bb6b521..236de856 100644 --- a/lib/hpke/src/rsa.cpp +++ b/lib/hpke/src/rsa.cpp @@ -146,6 +146,32 @@ RSASignature::verify(const bytes& data, return rv == 1; } +// TODO(RLB) Implement these methods. No concrete need, but might be nice for +// completeness. +std::unique_ptr +RSASignature::import_jwk_private(const std::string& /* json_str */) const +{ + throw std::runtime_error("not implemented"); +} + +std::unique_ptr +RSASignature::import_jwk(const std::string& /* json_str */) const +{ + throw std::runtime_error("not implemented"); +} + +std::string +RSASignature::export_jwk_private(const Signature::PrivateKey& /* sk */) const +{ + throw std::runtime_error("not implemented"); +} + +std::string +RSASignature::export_jwk(const Signature::PublicKey& /* pk */) const +{ + throw std::runtime_error("not implemented"); +} + const EVP_MD* RSASignature::digest_to_md(Digest::ID digest) { @@ -177,4 +203,5 @@ RSASignature::digest_to_sig(Digest::ID digest) throw std::runtime_error("Unsupported digest"); } } + } // namespace hpke diff --git a/lib/hpke/src/rsa.h b/lib/hpke/src/rsa.h index 684435ae..8457252f 100644 --- a/lib/hpke/src/rsa.h +++ b/lib/hpke/src/rsa.h @@ -78,6 +78,14 @@ struct RSASignature : public Signature const bytes& sig, const Signature::PublicKey& pk) const override; + std::unique_ptr import_jwk_private( + const std::string& json_str) const override; + std::unique_ptr import_jwk( + const std::string& json_str) const override; + std::string export_jwk_private( + const Signature::PrivateKey& sk) const override; + std::string export_jwk(const Signature::PublicKey& pk) const override; + private: const EVP_MD* md; diff --git a/lib/hpke/src/signature.cpp b/lib/hpke/src/signature.cpp index a79cafea..890257a8 100644 --- a/lib/hpke/src/signature.cpp +++ b/lib/hpke/src/signature.cpp @@ -1,14 +1,22 @@ +#include #include #include +#include #include "dhkem.h" #include "common.h" #include "group.h" #include "rsa.h" +#include +#include +#include +#include #include #include +using nlohmann::json; + namespace hpke { struct GroupSignature : public Signature @@ -103,8 +111,97 @@ struct GroupSignature : public Signature return group.verify(data, sig, rpk); } + std::unique_ptr import_jwk_private( + const std::string& json_str) const override + { + const auto jwk_json = validate_jwk_json(json_str, true); + + const auto d = from_base64url(jwk_json.at("d")); + auto gsk = group.deserialize_private(d); + + return std::make_unique(gsk.release()); + } + + std::unique_ptr import_jwk( + const std::string& json_str) const override + { + const auto jwk_json = validate_jwk_json(json_str, false); + + const auto x = from_base64url(jwk_json.at("x")); + auto y = bytes{}; + if (jwk_json.contains("y")) { + y = from_base64url(jwk_json.at("y")); + } + + return group.public_key_from_coordinates(x, y); + } + + std::string export_jwk(const Signature::PublicKey& pk) const override + { + const auto& gpk = dynamic_cast(pk); + const auto jwk_json = export_jwk_json(gpk); + return jwk_json.dump(); + } + + std::string export_jwk_private(const Signature::PrivateKey& sk) const override + { + const auto& gssk = dynamic_cast(sk); + const auto& gsk = gssk.group_priv; + const auto gpk = gsk->public_key(); + + auto jwk_json = export_jwk_json(*gpk); + + // encode the private key + const auto enc = serialize_private(sk); + jwk_json.emplace("d", to_base64url(enc)); + + return jwk_json.dump(); + } + private: const Group& group; + + json validate_jwk_json(const std::string& json_str, bool private_key) const + { + json jwk_json = json::parse(json_str); + + if (jwk_json.empty() || !jwk_json.contains("kty") || + !jwk_json.contains("crv") || !jwk_json.contains("x") || + (private_key && !jwk_json.contains("d"))) { + throw std::runtime_error("malformed JWK"); + } + + if (jwk_json.at("kty") != group.jwk_key_type) { + throw std::runtime_error("invalid JWK key type"); + } + + if (jwk_json.at("crv") != group.jwk_curve_name) { + throw std::runtime_error("invalid JWK curve"); + } + + return jwk_json; + } + + json export_jwk_json(const Group::PublicKey& pk) const + { + const auto [x, y] = group.coordinates(pk); + + json jwk_json = json::object({ + { "crv", group.jwk_curve_name }, + { "kty", group.jwk_key_type }, + }); + + if (group.jwk_key_type == "EC") { + jwk_json.emplace("x", to_base64url(x)); + jwk_json.emplace("y", to_base64url(y)); + } else if (group.jwk_key_type == "OKP") { + jwk_json.emplace("x", to_base64url(x)); + } else { + throw std::runtime_error("unknown key type"); + } + + return jwk_json; + } }; template<> @@ -176,18 +273,6 @@ Signature::Signature(Signature::ID id_in) { } -bytes -Signature::serialize_private(const PrivateKey& /* unused */) const -{ - throw std::runtime_error("Not implemented"); -} - -std::unique_ptr -Signature::deserialize_private(const bytes& /* unused */) const -{ - throw std::runtime_error("Not implemented"); -} - std::unique_ptr Signature::generate_rsa(size_t bits) { diff --git a/lib/hpke/test/base64.cpp b/lib/hpke/test/base64.cpp new file mode 100644 index 00000000..25f1c007 --- /dev/null +++ b/lib/hpke/test/base64.cpp @@ -0,0 +1,32 @@ +#include +#include + +using namespace hpke; +using namespace bytes_ns; + +TEST_CASE("To Base64 / To Base64Url") +{ + struct KnownAnswerTest + { + bytes data; + std::string base64; + std::string base64u; + }; + + const std::vector cases{ + { from_ascii("hello there"), "aGVsbG8gdGhlcmU=", "aGVsbG8gdGhlcmU" }, + { from_ascii("A B C D E F "), "QSBCIEMgRCBFIEYg", "QSBCIEMgRCBFIEYg" }, + { from_ascii("hello\xfethere"), "aGVsbG/+dGhlcmU=", "aGVsbG_-dGhlcmU" }, + { from_ascii("\xfe"), "/g==", "_g" }, + { from_ascii("\x01\x02"), "AQI=", "AQI" }, + { from_ascii("\x01"), "AQ==", "AQ" }, + { from_ascii(""), "", "" }, + }; + + for (const auto& tc : cases) { + REQUIRE(to_base64(tc.data) == tc.base64); + REQUIRE(to_base64url(tc.data) == tc.base64u); + REQUIRE(from_base64(tc.base64) == tc.data); + REQUIRE(from_base64url(tc.base64u) == tc.data); + } +} diff --git a/lib/hpke/test/signature.cpp b/lib/hpke/test/signature.cpp index 44c4d5f0..c8f8a149 100644 --- a/lib/hpke/test/signature.cpp +++ b/lib/hpke/test/signature.cpp @@ -1,9 +1,13 @@ #include #include +#include + +#include #include "common.h" -#include +using nlohmann::json; + TEST_CASE("Signature Known-Answer") { ensure_fips_if_required(); @@ -237,3 +241,137 @@ TEST_CASE("Signature Round-Trip") CHECK(sig.verify(data, signature2, *pub3)); } } + +TEST_CASE("Signature Key JWK Round-Trip") +{ + ensure_fips_if_required(); + const std::vector ids{ + Signature::ID::P256_SHA256, Signature::ID::P384_SHA384, + Signature::ID::P521_SHA512, Signature::ID::Ed25519, + Signature::ID::Ed448, + }; + + for (const auto& id : ids) { + if (fips() && fips_disable(id)) { + continue; + } + + const auto& sig = select_signature(id); + + const auto priv = sig.generate_key_pair(); + const auto encoded_priv = sig.export_jwk_private(*priv); + const auto decoded_priv = sig.import_jwk_private(encoded_priv); + CHECK(sig.serialize_private(*priv) == sig.serialize_private(*decoded_priv)); + + const auto pub = priv->public_key(); + const auto encoded_pub = sig.export_jwk(*pub); + const auto decoded_pub = sig.import_jwk(encoded_pub); + CHECK(sig.serialize(*pub) == sig.serialize(*decoded_pub)); + } +} + +TEST_CASE("Signature Key JWK Known-Answer") +{ + ensure_fips_if_required(); + + struct KnownAnswerTest + { + Signature::ID id; + std::string jwk_priv; + std::string jwk_pub; + }; + + // XXX(RLB) clang-format wants to indent this really far to the right. + // clang-format off + const std::vector cases{ + { + Signature::ID::P256_SHA256, + R"({ + "crv": "P-256", + "kty": "EC", + "d": "1J2hVVHFHYyeyfPmcXMG0uHLBn3i742jbRyUBGe2Jmg", + "x": "Bo8xjILpecBFkEwDa0H8p0_xCv7UPE2XMj8KcEe_LGE", + "y":"LbiO9VbkY1eRrR6UgdL7W4jhqej6Sfu6n6HvTRm5ySs" + })", + R"({ + "crv": "P-256", + "kty": "EC", + "x": "Bo8xjILpecBFkEwDa0H8p0_xCv7UPE2XMj8KcEe_LGE", + "y":"LbiO9VbkY1eRrR6UgdL7W4jhqej6Sfu6n6HvTRm5ySs" + })", + }, + { + Signature::ID::P384_SHA384, + R"({ + "crv": "P-384", + "kty": "EC", + "d": "uLHpTwv2Pucmo4ZVgO-0IYJQEyKQlJdukO1M6vZp0EYyvgCJhzoYWsBTbIUEOG8e", + "x": "apvkQ2jdEkTr5MkLePjAt2vW-Sk_djA7WjcPN_Waw_MS327r3WFCrchYEFvrMLru", + "y":"In9wbO3tnH9CQn0NMJm3DvgLtBF3OmcfOnW9sAy1I1-yHVs_N8Bph2Z46YbJGbdS" + })", + R"({ + "crv": "P-384", + "kty": "EC", + "x": "apvkQ2jdEkTr5MkLePjAt2vW-Sk_djA7WjcPN_Waw_MS327r3WFCrchYEFvrMLru", + "y":"In9wbO3tnH9CQn0NMJm3DvgLtBF3OmcfOnW9sAy1I1-yHVs_N8Bph2Z46YbJGbdS" + })", + }, + { + Signature::ID::P521_SHA512, + R"({ + "crv": "P-521", + "kty": "EC", + "d": "AckbSdZaMoA4ylPYStWyAUU20Wo5Yu8hsiocCmhqNniJ42ZXYzZ4EzQElB_sU6RGbPgwQmiKWTZ0jbi5NmZTC6mv", + "x": "AH9tp56TOvXzN_JWFGEAmk0HdkpDSPZBZOwX-xuT3RV0JjFVq3VWzMtMrf_x_Okt5QVNLBi-41no47VDDTK5UQiM", + "y":"AJtFy8ogaNhBxLx6CtjoOG5ptR3-z7CEz9I9ioIOJ9Q2a0vtixuNGa4ILQUY8vz-5_AuqHEWkIaK3sqrryYGPqdH" + })", + R"({ + "crv": "P-521", + "kty": "EC", + "x": "AH9tp56TOvXzN_JWFGEAmk0HdkpDSPZBZOwX-xuT3RV0JjFVq3VWzMtMrf_x_Okt5QVNLBi-41no47VDDTK5UQiM", + "y":"AJtFy8ogaNhBxLx6CtjoOG5ptR3-z7CEz9I9ioIOJ9Q2a0vtixuNGa4ILQUY8vz-5_AuqHEWkIaK3sqrryYGPqdH" + })", + }, + { + Signature::ID::Ed25519, + R"({ + "crv": "Ed25519", + "kty": "OKP", + "d": "kH7Q5NRfGgX1GSTTSz7ofQuQZNnLE5jWKP_RKT76Um8", + "x":"Rv-rXr1oUaa29TdaXzIhJEOV3eJYzMqy_luvV0T80no" + })", + R"({ + "crv": "Ed25519", + "kty": "OKP", + "x": "Rv-rXr1oUaa29TdaXzIhJEOV3eJYzMqy_luvV0T80no" + })", + }, + { + Signature::ID::Ed448, + R"({ + "crv": "Ed448", + "kty": "OKP", + "d": "NSnqhxrVgCDdREhKNkRsegr5o4WGIMUIdG9enGCBb413B4KgJ8URiZVPaVn0-AslPgHdkF--oFGV", + "x":"0P_035gQbBr8mHe_4uLu8wUI22JiSBYWq9Yzb3Tr3C4ksfv85Xo5OIRrWzE-L0QFRez78nrNA4wA" + })", + R"({ + "crv": "Ed448", + "kty": "OKP", + "x":"0P_035gQbBr8mHe_4uLu8wUI22JiSBYWq9Yzb3Tr3C4ksfv85Xo5OIRrWzE-L0QFRez78nrNA4wA" + })", + }, + }; + // clang-format on + + for (const auto& tc : cases) { + const auto& sig = select_signature(tc.id); + + auto imported_priv = sig.import_jwk_private(tc.jwk_priv); + auto exported_priv = sig.export_jwk_private(*imported_priv); + CHECK(json::parse(tc.jwk_priv) == json::parse(exported_priv)); + + auto imported_pub = sig.import_jwk(tc.jwk_pub); + auto exported_pub = sig.export_jwk(*imported_pub); + CHECK(json::parse(tc.jwk_pub) == json::parse(exported_pub)); + } +} diff --git a/lib/tls_syntax/CMakeLists.txt b/lib/tls_syntax/CMakeLists.txt index 43c75c20..44ba14fd 100644 --- a/lib/tls_syntax/CMakeLists.txt +++ b/lib/tls_syntax/CMakeLists.txt @@ -9,7 +9,7 @@ file(GLOB_RECURSE LIB_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src add_library(${CURRENT_LIB_NAME} ${LIB_HEADERS} ${LIB_SOURCES}) add_dependencies(${CURRENT_LIB_NAME} third_party) -target_link_libraries(${CURRENT_LIB_NAME} third_party) +target_link_libraries(${CURRENT_LIB_NAME} PUBLIC third_party) target_include_directories(${CURRENT_LIB_NAME} PUBLIC $ diff --git a/src/core_types.cpp b/src/core_types.cpp index 6bfdc08f..e8f27901 100644 --- a/src/core_types.cpp +++ b/src/core_types.cpp @@ -45,9 +45,11 @@ const std::array all_supported_ciphersuites = { CipherSuite::ID::X448_CHACHA20POLY1305_SHA512_Ed448, }; -const std::array all_supported_credentials = { +const std::array all_supported_credentials = { CredentialType::basic, CredentialType::x509, + CredentialType::userinfo_vc_draft_00, + CredentialType::multi_draft_00 }; Capabilities diff --git a/src/credential.cpp b/src/credential.cpp index 88737f32..bb6ba3ae 100644 --- a/src/credential.cpp +++ b/src/credential.cpp @@ -109,6 +109,95 @@ operator==(const X509Credential& lhs, const X509Credential& rhs) return lhs.der_chain == rhs.der_chain; } +/// +/// UserInfoVCCredential +/// +UserInfoVCCredential::UserInfoVCCredential(bytes userinfo_vc_jwt) + : userinfo_vc_jwt(std::move(userinfo_vc_jwt)) +{ +} + +bool +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +UserInfoVCCredential::valid_for(const SignaturePublicKey& /* pub */) const +{ + // TODO(RLB) Extract payload -> did:jwk, compare + throw NotImplementedError(); +} + +/// +/// CredentialBinding and MultiCredential +/// + +struct CredentialBindingTBS +{ + const CipherSuite& cipher_suite; + const Credential& credential; + const SignaturePublicKey& credential_key; + const SignaturePublicKey& signature_key; + + TLS_SERIALIZABLE(cipher_suite, credential, credential_key, signature_key) +}; + +CredentialBinding::CredentialBinding(CipherSuite cipher_suite_in, + Credential credential_in, + const SignaturePrivateKey& credential_priv, + const SignaturePublicKey& signature_key) + : cipher_suite(cipher_suite_in) + , credential(std::move(credential_in)) + , credential_key(credential_priv.public_key) +{ + if (credential.type() == CredentialType::multi_draft_00) { + throw InvalidParameterError("Multi-credentials cannot be nested"); + } + + if (!credential.valid_for(credential_key)) { + throw InvalidParameterError("Credential key does not match credential"); + } + + signature = credential_priv.sign( + cipher_suite, sign_label::multi_credential, to_be_signed(signature_key)); +} + +bytes +CredentialBinding::to_be_signed(const SignaturePublicKey& signature_key) const +{ + return tls::marshal(CredentialBindingTBS{ + cipher_suite, credential, credential_key, signature_key }); +} + +bool +CredentialBinding::valid_for(const SignaturePublicKey& signature_key) const +{ + auto valid_self = credential.valid_for(credential_key); + auto valid_other = credential_key.verify(cipher_suite, + sign_label::multi_credential, + to_be_signed(signature_key), + signature); + + return valid_self && valid_other; +} + +MultiCredential::MultiCredential( + const std::vector& binding_inputs, + const SignaturePublicKey& signature_key) +{ + bindings = + stdx::transform(binding_inputs, [&](auto&& input) { + return CredentialBinding(input.cipher_suite, + input.credential, + input.credential_priv, + signature_key); + }); +} + +bool +MultiCredential::valid_for(const SignaturePublicKey& pub) const +{ + return stdx::all_of( + bindings, [&](const auto& binding) { return binding.valid_for(pub); }); +} + /// /// Credential /// @@ -122,17 +211,26 @@ Credential::type() const Credential Credential::basic(const bytes& identity) { - Credential cred; - cred._cred = BasicCredential{ identity }; - return cred; + return { BasicCredential{ identity } }; } Credential Credential::x509(const std::vector& der_chain) { - Credential cred; - cred._cred = X509Credential{ der_chain }; - return cred; + return { X509Credential{ der_chain } }; +} + +Credential +Credential::multi(const std::vector& binding_inputs, + const SignaturePublicKey& signature_key) +{ + return { MultiCredential{ binding_inputs, signature_key } }; +} + +Credential +Credential::userinfo_vc(const bytes& userinfo_vc_jwt) +{ + return { UserInfoVCCredential{ userinfo_vc_jwt } }; } bool @@ -140,11 +238,17 @@ Credential::valid_for(const SignaturePublicKey& pub) const { const auto pub_key_match = overloaded{ [&](const X509Credential& x509) { return x509.valid_for(pub); }, - [](const BasicCredential& /* basic */) { return true; }, + [&](const UserInfoVCCredential& vc) { return vc.valid_for(pub); }, + [&](const MultiCredential& multi) { return multi.valid_for(pub); }, }; return var::visit(pub_key_match, _cred); } +Credential::Credential(SpecificCredential specific) + : _cred(std::move(specific)) +{ +} + } // namespace mls diff --git a/src/crypto.cpp b/src/crypto.cpp index 3236e026..ecedadfc 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -362,6 +362,7 @@ const std::string mls_content = "FramedContentTBS"; const std::string leaf_node = "LeafNodeTBS"; const std::string key_package = "KeyPackageTBS"; const std::string group_info = "GroupInfoTBS"; +const std::string multi_credential = "MultiCredential"; } // namespace sign_label struct SignContent @@ -383,6 +384,21 @@ SignaturePublicKey::verify(const CipherSuite& suite, return suite.sig().verify(content, signature, *pub); } +SignaturePublicKey +SignaturePublicKey::from_jwk(CipherSuite suite, const std::string& json_str) +{ + auto pub = suite.sig().import_jwk(json_str); + auto pub_data = suite.sig().serialize(*pub); + return SignaturePublicKey{ pub_data }; +} + +std::string +SignaturePublicKey::to_jwk(CipherSuite suite) const +{ + auto pub = suite.sig().deserialize(data); + return suite.sig().export_jwk(*pub); +} + SignaturePrivateKey SignaturePrivateKey::generate(CipherSuite suite) { @@ -437,4 +453,21 @@ SignaturePrivateKey::set_public_key(CipherSuite suite) public_key.data = suite.sig().serialize(*pub); } +SignaturePrivateKey +SignaturePrivateKey::from_jwk(CipherSuite suite, const std::string& json_str) +{ + auto priv = suite.sig().import_jwk_private(json_str); + auto priv_data = suite.sig().serialize_private(*priv); + auto pub = priv->public_key(); + auto pub_data = suite.sig().serialize(*pub); + return { priv_data, pub_data }; +} + +std::string +SignaturePrivateKey::to_jwk(CipherSuite suite) const +{ + const auto priv = suite.sig().deserialize_private(data); + return suite.sig().export_jwk_private(*priv); +} + } // namespace mls diff --git a/src/treekem.cpp b/src/treekem.cpp index 211c2f8a..92254104 100644 --- a/src/treekem.cpp +++ b/src/treekem.cpp @@ -531,7 +531,7 @@ TreeKEMPublicKey::parent_hash_valid() const } std::vector -TreeKEMPublicKey::resolve(NodeIndex index) const // NOLINT(misc-no-recursion) +TreeKEMPublicKey::resolve(NodeIndex index) const { auto at_leaf = (index.level() == 0); if (!node_at(index).blank()) { @@ -797,7 +797,7 @@ struct TreeHashInput }; const bytes& -TreeKEMPublicKey::get_hash(NodeIndex index) // NOLINT(misc-no-recursion) +TreeKEMPublicKey::get_hash(NodeIndex index) { if (hashes.count(index) > 0) { return hashes.at(index); @@ -905,7 +905,6 @@ TreeKEMPublicKey::parent_hashes( } const bytes& -// NOLINTNEXTLINE(misc-no-recursion) TreeKEMPublicKey::original_tree_hash(TreeHashCache& cache, NodeIndex index, std::vector parent_except) const diff --git a/test/credential.cpp b/test/credential.cpp index 34ba3b1c..5c1a342e 100644 --- a/test/credential.cpp +++ b/test/credential.cpp @@ -18,6 +18,101 @@ TEST_CASE("Basic Credential") REQUIRE(basic.identity == user_id); } +TEST_CASE("X.509 Credential") +{ + auto suite = CipherSuite{ CipherSuite::ID::P256_AES128GCM_SHA256_P256 }; + + const auto priv_data = from_hex( + "afdee46291cd304277fdd2599f125c4cc0aa1e539df7fd7c8032f632c5d0d3a4"); + + const auto leaf_der = + from_hex("308201363081dda003020102020100300a06082a8648ce3d0403023014311230" + "100603550403130946616b6520434120303020170d3233303730313138353334" + "385a180f32313232303630393138353334385a30123110300e06035504031307" + "5375626a6563743059301306072a8648ce3d020106082a8648ce3d0301070342" + "00041dd062efebc93e62c8c5a4984e6a1df6192ae87eaea0666e4fc5ff8d610c" + "2e465d077fafd72e249ce5ba602188df4203b78422380239bbcb9ab88b6940ba" + "f6a8a320301e300e0603551d0f0101ff0404030202a4300c0603551d130101ff" + "04023000300a06082a8648ce3d0403020348003045022041d8341e498806ec4b" + "7aa097deaeffe6b752ecc49e5093ad0d2ffba7ca629eee0221009f24accf35a9" + "05dcd8720ad6dfa881e7614f3b6abd9f49ebf21e2439060402eb"); + + auto priv = SignaturePrivateKey::parse(suite, priv_data); + auto pub = priv.public_key; + + auto cred = Credential::x509({ leaf_der }); + REQUIRE(cred.valid_for(pub)); + + const auto& x509 = cred.get(); + REQUIRE(x509.der_chain.size() == 1); + REQUIRE(x509.der_chain.front().data == leaf_der); +} + +TEST_CASE("Multi Credential") +{ + auto suite = CipherSuite{ CipherSuite::ID::P256_AES128GCM_SHA256_P256 }; + + // First X.509 Credential + const auto leaf_der_1 = + from_hex("308201363081dda003020102020100300a06082a8648ce3d0403023014311230" + "100603550403130946616b6520434120303020170d3233303730313138353334" + "385a180f32313232303630393138353334385a30123110300e06035504031307" + "5375626a6563743059301306072a8648ce3d020106082a8648ce3d0301070342" + "00041dd062efebc93e62c8c5a4984e6a1df6192ae87eaea0666e4fc5ff8d610c" + "2e465d077fafd72e249ce5ba602188df4203b78422380239bbcb9ab88b6940ba" + "f6a8a320301e300e0603551d0f0101ff0404030202a4300c0603551d130101ff" + "04023000300a06082a8648ce3d0403020348003045022041d8341e498806ec4b" + "7aa097deaeffe6b752ecc49e5093ad0d2ffba7ca629eee0221009f24accf35a9" + "05dcd8720ad6dfa881e7614f3b6abd9f49ebf21e2439060402eb"); + const auto cred_priv_data_1 = from_hex( + "afdee46291cd304277fdd2599f125c4cc0aa1e539df7fd7c8032f632c5d0d3a4"); + + auto cred_priv_1 = SignaturePrivateKey::parse(suite, cred_priv_data_1); + auto cred_pub_1 = cred_priv_1.public_key; + + auto cred_1 = Credential::x509({ leaf_der_1 }); + REQUIRE(cred_1.valid_for(cred_pub_1)); + + // Second X.509 Credential + const auto leaf_der_2 = + from_hex("308201353081dda003020102020100300a06082a8648ce3d0403023014311230" + "100603550403130946616b6520434120303020170d3233303730313139303234" + "305a180f32313232303630393139303234305a30123110300e06035504031307" + "5375626a6563743059301306072a8648ce3d020106082a8648ce3d0301070342" + "0004e7f7987f024d0d1b420018a585929e690f95b6fe7b23ec1ff6532b1c55c4" + "75ef36b826e4b54bd60b8823f3fc222c28369771a9ed0a644df351e16ad495dc" + "fb54a320301e300e0603551d0f0101ff0404030202a4300c0603551d130101ff" + "04023000300a06082a8648ce3d040302034700304402200d7e0e5362cfe4d551" + "cbb6a5b2b64541e30e86e10e734e84c1e24b46d1e098bc022037fc32a59b4062" + "c14b3323a20a0c7a5e05bbd3f27e22dc225ddd69ca771b90fc"); + const auto cred_priv_data_2 = from_hex( + "8915f49863cb24d6553fab0036da18a0fec431ae0cc94255010f6ed35555631e"); + + auto cred_priv_2 = SignaturePrivateKey::parse(suite, cred_priv_data_2); + auto cred_pub_2 = cred_priv_2.public_key; + + auto cred_2 = Credential::x509({ leaf_der_2 }); + REQUIRE(cred_2.valid_for(cred_pub_2)); + + // Multi-Credential + auto priv = SignaturePrivateKey::generate(suite); + auto pub = priv.public_key; + + auto cred = Credential::multi( + { { suite, cred_1, cred_priv_1 }, { suite, cred_2, cred_priv_2 } }, pub); + REQUIRE(cred.valid_for(pub)); + + auto multi = cred.get(); + const auto& bindings = multi.bindings; + REQUIRE(bindings.size() == 2); + REQUIRE(bindings[0].credential == cred_1); + REQUIRE(bindings[0].credential_key == cred_pub_1); + REQUIRE(bindings[0].credential.valid_for(bindings[0].credential_key)); + REQUIRE(bindings[1].credential == cred_2); + REQUIRE(bindings[1].credential_key == cred_pub_2); + REQUIRE(bindings[1].credential.valid_for(bindings[1].credential_key)); +} + TEST_CASE("X509 Credential Depth 2") { // Chain is of depth 2 diff --git a/test/crypto.cpp b/test/crypto.cpp index 8dd930c2..a21b517e 100644 --- a/test/crypto.cpp +++ b/test/crypto.cpp @@ -1,7 +1,6 @@ #include #include #include - #include using namespace mls; @@ -91,6 +90,24 @@ TEST_CASE("Signature Key Serializion") } } +TEST_CASE("Signature Key JWK Import/Export") +{ + for (auto suite_id : all_supported_suites) { + const auto suite = CipherSuite{ suite_id }; + const auto priv = SignaturePrivateKey::generate(suite); + const auto pub = priv.public_key; + + const auto encoded_priv = priv.to_jwk(suite); + const auto decoded_priv = + SignaturePrivateKey::from_jwk(suite, encoded_priv); + REQUIRE(decoded_priv == priv); + + const auto encoded_pub = pub.to_jwk(suite); + const auto decoded_pub = SignaturePublicKey::from_jwk(suite, encoded_pub); + REQUIRE(decoded_pub == pub); + } +} + TEST_CASE("Crypto Interop") { for (auto suite : all_supported_suites) { diff --git a/vcpkg.json b/vcpkg.json index f5870561..0ffd459e 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -7,7 +7,8 @@ "name": "openssl", "version>=": "1.1.1n" }, - "doctest" + "doctest", + "nlohmann-json" ], "builtin-baseline": "3b3bd424827a1f7f4813216f6b32b6c61e386b2e", "overrides": [