Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
maxtropets committed Dec 26, 2024
1 parent c929f24 commit 0c3ef0e
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 177 deletions.
34 changes: 19 additions & 15 deletions include/ccf/crypto/jwk.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,6 @@ namespace ccf::crypto
DECLARE_JSON_REQUIRED_FIELDS(JsonWebKey, kty);
DECLARE_JSON_OPTIONAL_FIELDS(JsonWebKey, kid, x5c);

struct JsonWebKeyExtended
{
JsonWebKeyType kty;
std::optional<std::string> kid = std::nullopt;
std::optional<std::vector<std::string>> x5c = std::nullopt;
std::optional<std::string> n = std::nullopt;
std::optional<std::string> e = std::nullopt;
std::optional<std::string> issuer = std::nullopt;

bool operator==(const JsonWebKeyExtended&) const = default;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JsonWebKeyExtended);
DECLARE_JSON_REQUIRED_FIELDS(JsonWebKeyExtended, kty);
DECLARE_JSON_OPTIONAL_FIELDS(JsonWebKeyExtended, kid, x5c, n, e, issuer);

enum class JsonWebKeyECCurve
{
P256 = 0,
Expand All @@ -61,6 +46,25 @@ namespace ccf::crypto
{JsonWebKeyECCurve::P384, "P-384"},
{JsonWebKeyECCurve::P521, "P-521"}});

struct JsonWebKeyData
{
JsonWebKeyType kty;
std::optional<std::string> kid = std::nullopt;
std::optional<std::vector<std::string>> x5c = std::nullopt;
std::optional<std::string> n = std::nullopt;
std::optional<std::string> e = std::nullopt;
std::optional<std::string> x = std::nullopt;
std::optional<std::string> y = std::nullopt;
std::optional<JsonWebKeyECCurve> crv = std::nullopt;
std::optional<std::string> issuer = std::nullopt;

bool operator==(const JsonWebKeyData&) const = default;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JsonWebKeyData);
DECLARE_JSON_REQUIRED_FIELDS(JsonWebKeyData, kty);
DECLARE_JSON_OPTIONAL_FIELDS(
JsonWebKeyData, kid, x5c, n, e, x, y, crv, issuer);

static JsonWebKeyECCurve curve_id_to_jwk_curve(CurveID curve_id)
{
switch (curve_id)
Expand Down
2 changes: 1 addition & 1 deletion include/ccf/service/tables/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ namespace ccf

struct JsonWebKeySet
{
std::vector<ccf::crypto::JsonWebKeyExtended> keys;
std::vector<ccf::crypto::JsonWebKeyData> keys;

bool operator!=(const JsonWebKeySet& rhs) const
{
Expand Down
4 changes: 4 additions & 0 deletions samples/constitutions/default/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ function checkJwks(value, field) {
} else if (jwk.n && jwk.e) {
checkType(jwk.n, "string", `${field}.keys[${i}].n`);
checkType(jwk.e, "string", `${field}.keys[${i}].e`);
} else if (jwk.x && jwk.y) {
checkType(jwk.x, "string", `${field}.keys[${i}].x`);
checkType(jwk.y, "string", `${field}.keys[${i}].y`);
checkType(jwk.crv, "string", `${field}.keys[${i}].crv`);
} else {
throw new Error("JWK must contain either x5c or n and e");
}
Expand Down
11 changes: 11 additions & 0 deletions src/crypto/openssl/rsa_public_key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ namespace ccf::crypto
auto msg = OpenSSL::error_string(ec);
throw std::runtime_error(fmt::format("OpenSSL error: {}", msg));
}

// As it's a common patter to rely on successful key wrapper construction as a
// confirmation of a concrete key type, this must fail for non-RSA keys.
#if defined(OPENSSL_VERSION_MAJOR) && OPENSSL_VERSION_MAJOR >= 3
if (!key || EVP_PKEY_get_base_id(key) != EVP_PKEY_RSA)
#else
if (!key || !EVP_PKEY_get0_RSA(key))
#endif
{
throw std::logic_error("invalid RSA key");
}
}

std::pair<Unique_BIGNUM, Unique_BIGNUM> get_modulus_and_exponent(
Expand Down
62 changes: 53 additions & 9 deletions src/endpoints/authentication/jwt_auth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#include "ccf/endpoints/authentication/jwt_auth.h"

#include "ccf/crypto/ecdsa.h"
#include "ccf/crypto/public_key.h"
#include "ccf/crypto/rsa_key_pair.h"
#include "ccf/ds/nonstd.h"
#include "ccf/pal/locking.h"
Expand Down Expand Up @@ -88,22 +90,67 @@ namespace ccf
static constexpr size_t DEFAULT_MAX_KEYS = 10;

using DER = std::vector<uint8_t>;
using KeyVariant =
std::variant<ccf::crypto::RSAPublicKeyPtr, ccf::crypto::PublicKeyPtr>;
ccf::pal::Mutex keys_lock;
LRU<DER, ccf::crypto::RSAPublicKeyPtr> keys;
LRU<DER, KeyVariant> keys;

PublicKeysCache(size_t max_keys = DEFAULT_MAX_KEYS) : keys(max_keys) {}

ccf::crypto::RSAPublicKeyPtr get_key(const DER& der)
bool verify(
const uint8_t* contents,
size_t contents_size,
const uint8_t* signature,
size_t signature_size,
const DER& der)
{
std::lock_guard<ccf::pal::Mutex> guard(keys_lock);

auto it = keys.find(der);
if (it == keys.end())
{
it = keys.insert(der, ccf::crypto::make_rsa_public_key(der));
try
{
it = keys.insert(der, ccf::crypto::make_rsa_public_key(der));
}
catch (const std::exception&)
{
it = keys.insert(der, ccf::crypto::make_public_key(der));
}
}

return it->second;
const auto& key = it->second;
if (std::holds_alternative<ccf::crypto::RSAPublicKeyPtr>(key))
{
LOG_DEBUG_FMT("Verify der: {} as RSA key", der);

// Obsolote PKCS1 padding is chosen for JWT, as explained in details in
// https://github.com/microsoft/CCF/issues/6601#issuecomment-2512059875.
return std::get<ccf::crypto::RSAPublicKeyPtr>(key)->verify_pkcs1(
contents,
contents_size,
signature,
signature_size,
ccf::crypto::MDType::SHA256);
}
else if (std::holds_alternative<ccf::crypto::PublicKeyPtr>(key))
{
LOG_DEBUG_FMT("Verify der: {} as EC key", der);

const auto sig_der = ccf::crypto::ecdsa_sig_p1363_to_der(
std::vector<uint8_t>(signature, signature + signature_size));
return std::get<ccf::crypto::PublicKeyPtr>(key)->verify(
contents,
contents_size,
sig_der.data(),
sig_der.size(),
ccf::crypto::MDType::SHA256);
}
else
{
LOG_DEBUG_FMT("Key not found for der: {}", der);
return false;
}
}
};

Expand Down Expand Up @@ -191,15 +238,12 @@ namespace ccf

for (const auto& metadata : *token_keys)
{
const auto pubkey = keys_cache->get_key(metadata.public_key);
// Obsolote PKCS1 padding is chosen for JWT, as explained in details here:
// https://github.com/microsoft/CCF/issues/6601#issuecomment-2512059875.
if (!pubkey->verify_pkcs1(
if (!keys_cache->verify(
(uint8_t*)token.signed_content.data(),
token.signed_content.size(),
token.signature.data(),
token.signature.size(),
ccf::crypto::MDType::SHA256))
metadata.public_key))
{
error_reason = "Signature verification failed";
continue;
Expand Down
8 changes: 6 additions & 2 deletions src/http/http_jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ namespace http
{
enum class JwtCryptoAlgorithm
{
RS256
RS256,
ES256,
};
DECLARE_JSON_ENUM(JwtCryptoAlgorithm, {{JwtCryptoAlgorithm::RS256, "RS256"}});
DECLARE_JSON_ENUM(
JwtCryptoAlgorithm,
{{JwtCryptoAlgorithm::RS256, "RS256"},
{JwtCryptoAlgorithm::ES256, "ES256"}});

struct JwtHeader
{
Expand Down
139 changes: 94 additions & 45 deletions src/node/rpc/jwt_management.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,59 +15,108 @@

namespace
{
std::vector<uint8_t> try_parse_jwk(const ccf::crypto::JsonWebKeyExtended& jwk)
std::vector<uint8_t> try_parse_raw_rsa(const ccf::crypto::JsonWebKeyData& jwk)
{
const auto& kid = jwk.kid.value();
if (
jwk.e.has_value() && !jwk.e->empty() && jwk.n.has_value() &&
!jwk.n->empty())
if (!jwk.e || jwk.e->empty() || !jwk.n || jwk.n->empty())
{
std::vector<uint8_t> der;
ccf::crypto::JsonWebKeyRSAPublic data;
data.kty = ccf::crypto::JsonWebKeyType::RSA;
data.kid = jwk.kid;
data.n = jwk.n.value();
data.e = jwk.e.value();
try
{
const auto pubkey = ccf::crypto::make_rsa_public_key(data);
return pubkey->public_key_der();
}
catch (const std::invalid_argument& exc)
{
throw std::logic_error(
fmt::format("Failed to construct RSA public key: {}", exc.what()));
}
return {};
}
else if (jwk.x5c.has_value() && !jwk.x5c->empty())

std::vector<uint8_t> der;
ccf::crypto::JsonWebKeyRSAPublic data;
data.kty = ccf::crypto::JsonWebKeyType::RSA;
data.kid = jwk.kid.value();
data.n = jwk.n.value();
data.e = jwk.e.value();
try
{
auto& der_base64 = jwk.x5c.value()[0];
ccf::Cert der;
try
{
der = ccf::crypto::raw_from_b64(der_base64);
}
catch (const std::invalid_argument& e)
{
throw std::logic_error(
fmt::format("Could not parse x5c of key id {}: {}", kid, e.what()));
}
try
{
auto verifier = ccf::crypto::make_unique_verifier(der);
return verifier->public_key_der();
}
catch (std::invalid_argument& exc)
{
throw std::logic_error(fmt::format(
"JWKS kid {} has an invalid X.509 certificate: {}", kid, exc.what()));
}
const auto pubkey = ccf::crypto::make_rsa_public_key(data);
return pubkey->public_key_der();
}
catch (const std::invalid_argument& exc)
{
throw std::logic_error(
fmt::format("Failed to construct RSA public key: {}", exc.what()));
}
}

std::vector<uint8_t> try_parse_raw_ec(const ccf::crypto::JsonWebKeyData& jwk)
{
if (!jwk.x || jwk.x->empty() || !jwk.y || jwk.y->empty() || !jwk.crv)
{
return {};
}

ccf::crypto::JsonWebKeyECPublic data;
data.kty = ccf::crypto::JsonWebKeyType::EC;
data.kid = jwk.kid.value();
data.crv = jwk.crv.value();
data.x = jwk.x.value();
data.y = jwk.y.value();
try
{
const auto pubkey = ccf::crypto::make_public_key(data);
return pubkey->public_key_der();
}
catch (const std::invalid_argument& exc)
{
throw std::logic_error(
fmt::format("Failed to construct EC public key: {}", exc.what()));
}
}

std::vector<uint8_t> try_parse_x5c(const ccf::crypto::JsonWebKeyData& jwk)
{
if (!jwk.x5c || jwk.x5c->empty())
{
return {};
}
else

const auto& kid = jwk.kid.value();
auto& der_base64 = jwk.x5c.value()[0];
ccf::Cert der;
try
{
der = ccf::crypto::raw_from_b64(der_base64);
}
catch (const std::invalid_argument& e)
{
throw std::logic_error(
fmt::format("JWKS kid {} has neither x5c or RSA public key", kid));
fmt::format("Could not parse x5c of key id {}: {}", kid, e.what()));
}
try
{
auto verifier = ccf::crypto::make_unique_verifier(der);
return verifier->public_key_der();
}
catch (std::invalid_argument& exc)
{
throw std::logic_error(fmt::format(
"JWKS kid {} has an invalid X.509 certificate: {}", kid, exc.what()));
}
}

std::vector<uint8_t> try_parse_jwk(const ccf::crypto::JsonWebKeyData& jwk)
{
const auto& kid = jwk.kid.value();
auto key = try_parse_raw_rsa(jwk);
if (!key.empty())
{
return key;
}
key = try_parse_raw_ec(jwk);
if (!key.empty())
{
return key;
}
key = try_parse_x5c(jwk);
if (!key.empty())
{
return key;
}

throw std::logic_error(
fmt::format("JWKS kid {} has neither RSA/EC public key or x5c", kid));
}
}

Expand Down
6 changes: 2 additions & 4 deletions tests/infra/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,8 @@ def pub_key_pem_to_der(pem: str) -> bytes:
return cert.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)


def create_jwt(body_claims: dict, key_priv_pem: str, key_id: str) -> str:
return jwt.encode(
body_claims, key_priv_pem, algorithm="RS256", headers={"kid": key_id}
)
def create_jwt(body_claims: dict, key_priv_pem: str, key_id: str, alg="RS256") -> str:
return jwt.encode(body_claims, key_priv_pem, algorithm=alg, headers={"kid": key_id})


def cert_pem_to_der(pem: str) -> bytes:
Expand Down
Loading

0 comments on commit 0c3ef0e

Please sign in to comment.