diff --git a/CMakeLists.txt b/CMakeLists.txt index eebbfc10d..3b4b3d54c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,7 @@ endif() ### # External libraries +find_package(nlohmann_json REQUIRED) find_package(OpenSSL REQUIRED) if ( OPENSSL_FOUND ) if (${OPENSSL_VERSION} VERSION_GREATER_EQUAL 3) @@ -105,7 +106,11 @@ file(GLOB_RECURSE LIB_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src add_library(${LIB_NAME} ${LIB_HEADERS} ${LIB_SOURCES}) add_dependencies(${LIB_NAME} bytes tls_syntax hpke) -target_link_libraries(${LIB_NAME} bytes tls_syntax hpke) +target_link_libraries(${LIB_NAME} + PUBLIC + bytes tls_syntax hpke + PRIVATE + nlohmann_json::nlohmann_json) target_include_directories(${LIB_NAME} PUBLIC $ diff --git a/include/mls/crypto.h b/include/mls/crypto.h index 42b89f670..5066ad6af 100644 --- a/include/mls/crypto.h +++ b/include/mls/crypto.h @@ -217,6 +217,8 @@ struct SignaturePublicKey const bytes& message, const bytes& signature) const; + std::string to_jwk(CipherSuite suite) const; + TLS_SERIALIZABLE(data) }; @@ -236,6 +238,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/CMakeLists.txt b/lib/bytes/CMakeLists.txt index ad4165c4e..e420273b4 100644 --- a/lib/bytes/CMakeLists.txt +++ b/lib/bytes/CMakeLists.txt @@ -9,7 +9,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} tls_syntax) -target_link_libraries(${CURRENT_LIB_NAME} tls_syntax) +target_link_libraries(${CURRENT_LIB_NAME} + PUBLIC + tls_syntax + PRIVATE + OpenSSL::Crypto) target_include_directories(${CURRENT_LIB_NAME} PUBLIC $ diff --git a/lib/hpke/include/hpke/signature.h b/lib/hpke/include/hpke/signature.h index 8ee0b39ba..fdc091966 100644 --- a/lib/hpke/include/hpke/signature.h +++ b/lib/hpke/include/hpke/signature.h @@ -50,6 +50,9 @@ struct Signature virtual std::unique_ptr deserialize_private( const bytes& skm) const; + virtual std::string export_jwk_private(const bytes& env) const; + virtual std::string export_jwk(const bytes& env) const; + virtual bytes sign(const bytes& data, const PrivateKey& sk) const = 0; virtual bool verify(const bytes& data, const bytes& sig, diff --git a/lib/hpke/src/group.cpp b/lib/hpke/src/group.cpp index c6b42b3b6..00800972c 100644 --- a/lib/hpke/src/group.cpp +++ b/lib/hpke/src/group.cpp @@ -13,6 +13,8 @@ #include "openssl/param_build.h" #endif +#include + namespace hpke { static inline size_t @@ -526,6 +528,42 @@ struct ECKeyGroup : public EVPGroup #endif } + void split_key(const Group::PublicKey& pk, bytes& x, bytes& y) const override + { +#if defined(WITH_OPENSSL3) + throw std::runtime_error("openssl 3... todo"); +#endif + BIGNUM* bnX = BN_new(); + BIGNUM* bnY = BN_new(); + const auto& rpk = dynamic_cast(pk); + EC_KEY* pub = EVP_PKEY_get0_EC_KEY(rpk.pkey.get()); + const EC_POINT* point = EC_KEY_get0_public_key(pub); + const EC_GROUP* group = EC_KEY_get0_group(pub); + + if (1 != + EC_POINT_get_affine_coordinates_GFp(group, point, bnX, bnY, nullptr)) { + BN_free(bnX); + BN_free(bnY); + throw openssl_error(); + } + auto outX = bytes(BN_num_bytes(bnX)); + auto outY = bytes(BN_num_bytes(bnY)); + + if (BN_bn2bin(bnX, outX.data()) != int(outX.size())) { + throw openssl_error(); + } + + if (BN_bn2bin(bnY, outY.data()) != int(outY.size())) { + throw openssl_error(); + } + const auto zeros_neededX = dh_size - outX.size(); + const auto zeros_neededY = dh_size - outY.size(); + auto leading_zerosX = bytes(zeros_neededX, 0); + auto leading_zerosY = bytes(zeros_neededY, 0); + x = leading_zerosX + outX; + y = leading_zerosY + outY; + } + private: int curve_nid; @@ -648,6 +686,13 @@ struct RawKeyGroup : public EVPGroup return std::make_unique(pkey); } + void split_key(const Group::PublicKey& /*unused*/, + bytes& /*unused*/, + bytes& /*unused*/) const override + { + throw std::runtime_error("Unsupported group"); + } + private: const int evp_type; @@ -809,11 +854,51 @@ group_sk_size(Group::ID group_id) } } +static inline std::string +group_jwt_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::X25519: + case Group::ID::Ed25519: + case Group::ID::X448: + case Group::ID::Ed448: + throw std::runtime_error("Unsupported group"); + default: + throw std::runtime_error("Unknown group"); + } +} + +static inline std::string +group_jwt_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::X25519: + case Group::ID::Ed25519: + case Group::ID::X448: + case Group::ID::Ed448: + throw std::runtime_error("Unsupported group"); + 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)) + , jwt_key_type(group_jwt_key_type(group_id_in)) + , jwt_curve_name(group_jwt_curve_name(group_id_in)) , kdf(kdf_in) { } diff --git a/lib/hpke/src/group.h b/lib/hpke/src/group.h index ace8d7a92..8274a533c 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 jwt_key_type; + const std::string jwt_curve_name; virtual std::unique_ptr generate_key_pair() const = 0; virtual std::unique_ptr derive_key_pair( @@ -63,6 +65,10 @@ struct Group const bytes& sig, const PublicKey& pk) const = 0; + virtual void split_key(const Group::PublicKey& pk, + bytes& x, + bytes& y) const = 0; + protected: const KDF& kdf; diff --git a/lib/hpke/src/signature.cpp b/lib/hpke/src/signature.cpp index a79cafeaa..ec0384475 100644 --- a/lib/hpke/src/signature.cpp +++ b/lib/hpke/src/signature.cpp @@ -1,11 +1,16 @@ #include #include +#include #include "dhkem.h" #include "common.h" #include "group.h" #include "rsa.h" +#include +#include +#include +#include #include #include @@ -103,6 +108,49 @@ struct GroupSignature : public Signature return group.verify(data, sig, rpk); } + // hpke::GroupSignature + std::string export_jwk(const bytes& enc) const override + { + bytes x; + bytes y; + nlohmann::json json_jwk; + json_jwk["crv"] = group.jwt_curve_name; + json_jwk["kty"] = group.jwt_key_type; + + std::unique_ptr pk = deserialize(enc); + const auto& rpk = + dynamic_cast(*(pk.release())); + + group.split_key(rpk, x, y); + json_jwk["x"] = to_base64url(x); + json_jwk["y"] = to_base64url(y); + + return json_jwk.dump(); + } + + // hpke::GroupSignature + std::string export_jwk_private(const bytes& enc) const override + { + bytes x; + bytes y; + nlohmann::json json_jwk; + json_jwk["crv"] = group.jwt_curve_name; + json_jwk["kty"] = group.jwt_key_type; + + // encode the private key + json_jwk["d"] = to_base64url(enc); + + const auto priv = deserialize_private(enc); + const auto& rpk = + dynamic_cast(*(priv->public_key().release())); + + group.split_key(rpk, x, y); + json_jwk["x"] = to_base64url(x); + json_jwk["y"] = to_base64url(y); + + return json_jwk.dump(); + } + private: const Group& group; }; @@ -182,6 +230,18 @@ Signature::serialize_private(const PrivateKey& /* unused */) const throw std::runtime_error("Not implemented"); } +std::string +Signature::export_jwk(const bytes& /* unused */) const +{ + throw std::runtime_error("Not implemented."); +} + +std::string +Signature::export_jwk_private(const bytes& /* unused */) const +{ + throw std::runtime_error("Not implemented."); +} + std::unique_ptr Signature::deserialize_private(const bytes& /* unused */) const { diff --git a/src/crypto.cpp b/src/crypto.cpp index 9611fbd56..28dfdf69d 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -384,6 +384,12 @@ SignaturePublicKey::verify(const CipherSuite& suite, return suite.sig().verify(content, signature, *pub); } +std::string +SignaturePublicKey::to_jwk(CipherSuite suite) const +{ + return suite.sig().export_jwk(data); +} + SignaturePrivateKey SignaturePrivateKey::generate(CipherSuite suite) { @@ -438,4 +444,10 @@ SignaturePrivateKey::set_public_key(CipherSuite suite) public_key.data = suite.sig().serialize(*pub); } +std::string +SignaturePrivateKey::to_jwk(CipherSuite suite) const +{ + return suite.sig().export_jwk_private(data); +} + } // namespace mls diff --git a/test/crypto.cpp b/test/crypto.cpp index 8dd930c25..dfb922a97 100644 --- a/test/crypto.cpp +++ b/test/crypto.cpp @@ -2,10 +2,12 @@ #include #include +#include #include using namespace mls; using namespace mls_vectors; +using namespace nlohmann; TEST_CASE("Basic HPKE") { @@ -91,6 +93,110 @@ TEST_CASE("Signature Key Serializion") } } +TEST_CASE("Signature Key Serializion To JWK") +{ + + struct KnownAnswerTest + { + CipherSuite suite; + bool supported; + bytes pk; + std::string kty; + std::string crv; + std::string d; + std::string x; + std::string y; + }; + + std::vector cases{ + { CipherSuite::ID::P256_AES128GCM_SHA256_P256, + true, + from_hex( + "cae90bad54df6973c64f7e4116ee78409045ed43e9668d0d474948a510f38acf"), + "EC", + "P-256", + "yukLrVTfaXPGT35BFu54QJBF7UPpZo0NR0lIpRDzis8", + "nUV1xGxWcUobNQrV0DsSN_z7P8hwVivmUji8EIJnrGg", + "2TGu_-lIxa7fn8PW-3gMNod-CjwwoAiLIhkbcsHtSdw" }, + { CipherSuite::ID::X25519_AES128GCM_SHA256_Ed25519, + false, + bytes(), + "", + "", + "", + "", + "" }, + { CipherSuite::ID::X25519_CHACHA20POLY1305_SHA256_Ed25519, + false, + bytes(), + "", + "", + "", + "", + "" }, + { CipherSuite::ID::X448_AES256GCM_SHA512_Ed448, + false, + bytes(), + "", + "", + "", + "", + "" }, + { CipherSuite::ID::P521_AES256GCM_SHA512_P521, + false, + bytes(), + "", + "", + "", + "", + "" }, + { CipherSuite::ID::X448_CHACHA20POLY1305_SHA512_Ed448, + false, + bytes(), + "", + "", + "", + "", + "" }, + { CipherSuite::ID::P384_AES256GCM_SHA384_P384, + false, + bytes(), + "", + "", + "", + "", + "" } + }; + + for (const auto& tc : cases) { + + if (!tc.supported) { + continue; + } + + // CipherSuite::ID::P256_AES128GCM_SHA256_P256, + const CipherSuite suite{ tc.suite }; + + // Private Key + auto private_key = SignaturePrivateKey::parse(suite, tc.pk); + auto jwk_str = private_key.to_jwk(tc.suite); + auto jwk_json = json::parse(jwk_str); + REQUIRE(jwk_json["kty"] == tc.kty); + REQUIRE(jwk_json["crv"] == tc.crv); + REQUIRE(jwk_json["d"] == tc.d); + REQUIRE(jwk_json["x"] == tc.x); + REQUIRE(jwk_json["y"] == tc.y); + + // Public Key + auto jwk_pk_str = private_key.public_key.to_jwk(tc.suite); + auto jwk_pk_json = json::parse(jwk_pk_str); + REQUIRE(jwk_pk_json["kty"] == tc.kty); + REQUIRE(jwk_pk_json["crv"] == tc.crv); + REQUIRE(jwk_pk_json["x"] == tc.x); + REQUIRE(jwk_pk_json["y"] == tc.y); + } +} + TEST_CASE("Crypto Interop") { for (auto suite : all_supported_suites) { diff --git a/vcpkg.json b/vcpkg.json index f58705611..a09b6e0be 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -3,11 +3,12 @@ "version-string": "0.1", "description": "Cisco MLS C++ library", "dependencies": [ + "doctest", + "nlohmann-json", { "name": "openssl", "version>=": "1.1.1n" - }, - "doctest" + } ], "builtin-baseline": "3b3bd424827a1f7f4813216f6b32b6c61e386b2e", "overrides": [