Skip to content

Raw public keys support for JWT authentication #6680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 3, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -57,6 +57,15 @@ and this project adheres Fto [Semantic Versioning](http://semver.org/spec/v2.0.0
- All definitions in CCF's public headers are now under the `ccf::` namespace. Any application code which references any of these types directly (notably `StartupConfig`, `http_status`, `LoggerLevel`), they will now need to be prefixed with the `ccf::` namespace.
- `cchost` now requires `--config`.

### Changed

- JWT authentication now supports raw public keys along with certificates (#6601).
- Public key information ('n' and 'e', or 'x', 'y' and 'crv' fields) now have a priority if defined in JWK set, 'x5c' remains as a backup option.
- Has same side-effects as #5809 does please see the changelog entry for that change for more details. In short:
- stale JWKs may be used for JWT validation on older nodes during the upgrade.
- old tables are not cleaned up, #6222 is tracking those.
- A deprecated `GET /gov/jwt_keys/all` has been altered because of #6601, as soon as JWT certificates are no longer stored in CCF. A new "public_key" field has been added, "cert" is now left empty.

## [6.0.0-dev7]

[6.0.0-dev7]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev7
59 changes: 57 additions & 2 deletions doc/schemas/gov_openapi.json
Original file line number Diff line number Diff line change
@@ -799,6 +799,24 @@
"type": "string"
},
"OpenIDJWKMetadata": {
"properties": {
"constraint": {
"$ref": "#/components/schemas/string"
},
"issuer": {
"$ref": "#/components/schemas/string"
},
"public_key": {
"$ref": "#/components/schemas/base64string"
}
},
"required": [
"issuer",
"public_key"
],
"type": "object"
},
"OpenIDJWKMetadataLegacy": {
"properties": {
"cert": {
"$ref": "#/components/schemas/base64string"
@@ -811,11 +829,17 @@
}
},
"required": [
"cert",
"issuer"
"issuer",
"cert"
],
"type": "object"
},
"OpenIDJWKMetadataLegacy_array": {
"items": {
"$ref": "#/components/schemas/OpenIDJWKMetadataLegacy"
},
"type": "array"
},
"OpenIDJWKMetadata_array": {
"items": {
"$ref": "#/components/schemas/OpenIDJWKMetadata"
@@ -1228,6 +1252,12 @@
},
"type": "object"
},
"string_to_OpenIDJWKMetadataLegacy_array": {
"additionalProperties": {
"$ref": "#/components/schemas/OpenIDJWKMetadataLegacy_array"
},
"type": "object"
},
"string_to_OpenIDJWKMetadata_array": {
"additionalProperties": {
"$ref": "#/components/schemas/OpenIDJWKMetadata_array"
@@ -1752,6 +1782,31 @@
"get": {
"deprecated": true,
"operationId": "GetGovKvJwtPublicSigningKeysMetadata",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/string_to_OpenIDJWKMetadataLegacy_array"
}
}
},
"description": "Default response description"
},
"default": {
"$ref": "#/components/responses/default"
}
},
"summary": "This route is auto-generated from the KV schema.",
"x-ccf-forwarding": {
"$ref": "#/components/x-ccf-forwarding/sometimes"
}
}
},
"/gov/kv/jwt/public_signing_keys_metadata_v2": {
"get": {
"deprecated": true,
"operationId": "GetGovKvJwtPublicSigningKeysMetadataV2",
"responses": {
"200": {
"content": {
3 changes: 2 additions & 1 deletion include/ccf/crypto/ecdsa.h
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

#include "ccf/crypto/curve.h"

#include <span>
#include <vector>

namespace ccf::crypto
@@ -28,7 +29,7 @@ namespace ccf::crypto
* @param signature The signature in IEEE P1363 encoding
*/
std::vector<uint8_t> ecdsa_sig_p1363_to_der(
const std::vector<uint8_t>& signature);
std::span<const uint8_t> signature);

std::vector<uint8_t> ecdsa_sig_der_to_p1363(
const std::vector<uint8_t>& signature, CurveID curveId);
22 changes: 20 additions & 2 deletions include/ccf/crypto/jwk.h
Original file line number Diff line number Diff line change
@@ -27,13 +27,12 @@ namespace ccf::crypto
JsonWebKeyType kty;
std::optional<std::string> kid = std::nullopt;
std::optional<std::vector<std::string>> x5c = std::nullopt;
std::optional<std::string> issuer = std::nullopt;

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

enum class JsonWebKeyECCurve
{
@@ -47,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)
7 changes: 7 additions & 0 deletions include/ccf/crypto/rsa_public_key.h
Original file line number Diff line number Diff line change
@@ -84,6 +84,13 @@ namespace ccf::crypto
MDType md_type = MDType::NONE,
size_t salt_legth = 0) = 0;

virtual bool verify_pkcs1(
const uint8_t* contents,
size_t contents_size,
const uint8_t* signature,
size_t signature_size,
MDType md_type = MDType::NONE) = 0;

struct Components
{
std::vector<uint8_t> n;
4 changes: 2 additions & 2 deletions include/ccf/endpoints/authentication/jwt_auth.h
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ namespace ccf
nlohmann::json payload;
};

struct VerifiersCache;
struct PublicKeysCache;

bool validate_issuer(
const std::string& iss,
@@ -28,7 +28,7 @@ namespace ccf
{
protected:
static const OpenAPISecuritySchema security_schema;
std::unique_ptr<VerifiersCache> verifiers;
std::unique_ptr<PublicKeysCache> keys_cache;

public:
static constexpr auto SECURITY_SCHEME_NAME = "jwt";
29 changes: 23 additions & 6 deletions include/ccf/service/tables/jwt.h
Original file line number Diff line number Diff line change
@@ -37,34 +37,51 @@ namespace ccf
using JwtIssuer = std::string;
using JwtKeyId = std::string;
using Cert = std::vector<uint8_t>;
using PublicKey = std::vector<uint8_t>;

struct OpenIDJWKMetadata
{
Cert cert;
PublicKey public_key;
JwtIssuer issuer;
std::optional<JwtIssuer> constraint;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(OpenIDJWKMetadata);
DECLARE_JSON_REQUIRED_FIELDS(OpenIDJWKMetadata, cert, issuer);
DECLARE_JSON_REQUIRED_FIELDS(OpenIDJWKMetadata, issuer, public_key);
DECLARE_JSON_OPTIONAL_FIELDS(OpenIDJWKMetadata, constraint);

using JwtIssuers = ServiceMap<JwtIssuer, JwtIssuerMetadata>;
using JwtPublicSigningKeys =
using JwtPublicSigningKeysMetadata =
ServiceMap<JwtKeyId, std::vector<OpenIDJWKMetadata>>;

struct OpenIDJWKMetadataLegacy
{
Cert cert;
JwtIssuer issuer;
std::optional<JwtIssuer> constraint;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(OpenIDJWKMetadataLegacy);
DECLARE_JSON_REQUIRED_FIELDS(OpenIDJWKMetadataLegacy, issuer, cert);
DECLARE_JSON_OPTIONAL_FIELDS(OpenIDJWKMetadataLegacy, constraint);

using JwtPublicSigningKeysMetadataLegacy =
ServiceMap<JwtKeyId, std::vector<OpenIDJWKMetadataLegacy>>;

using JwtIssuers = ServiceMap<JwtIssuer, JwtIssuerMetadata>;

namespace Tables
{
static constexpr auto JWT_ISSUERS = "public:ccf.gov.jwt.issuers";

static constexpr auto JWT_PUBLIC_SIGNING_KEYS_METADATA =
"public:ccf.gov.jwt.public_signing_keys_metadata";
"public:ccf.gov.jwt.public_signing_keys_metadata_v2";

namespace Legacy
{
static constexpr auto JWT_PUBLIC_SIGNING_KEYS =
"public:ccf.gov.jwt.public_signing_key";
static constexpr auto JWT_PUBLIC_SIGNING_KEY_ISSUER =
"public:ccf.gov.jwt.public_signing_key_issuer";
static constexpr auto JWT_PUBLIC_SIGNING_KEYS_METADATA =
"public:ccf.gov.jwt.public_signing_keys_metadata";

using JwtPublicSigningKeys =
ccf::kv::RawCopySerialisedMap<JwtKeyId, Cert>;
@@ -75,7 +92,7 @@ namespace ccf

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

bool operator!=(const JsonWebKeySet& rhs) const
{
31 changes: 22 additions & 9 deletions samples/constitutions/default/actions.js
Original file line number Diff line number Diff line change
@@ -130,15 +130,28 @@ function checkJwks(value, field) {
for (const [i, jwk] of value.keys.entries()) {
checkType(jwk.kid, "string", `${field}.keys[${i}].kid`);
checkType(jwk.kty, "string", `${field}.keys[${i}].kty`);
checkType(jwk.x5c, "array", `${field}.keys[${i}].x5c`);
checkLength(jwk.x5c, 1, null, `${field}.keys[${i}].x5c`);
for (const [j, b64der] of jwk.x5c.entries()) {
checkType(b64der, "string", `${field}.keys[${i}].x5c[${j}]`);
const pem =
"-----BEGIN CERTIFICATE-----\n" +
b64der +
"\n-----END CERTIFICATE-----";
checkX509CertBundle(pem, `${field}.keys[${i}].x5c[${j}]`);
if (jwk.x5c) {
checkType(jwk.x5c, "array", `${field}.keys[${i}].x5c`);
checkLength(jwk.x5c, 1, null, `${field}.keys[${i}].x5c`);
for (const [j, b64der] of jwk.x5c.entries()) {
checkType(b64der, "string", `${field}.keys[${i}].x5c[${j}]`);
const pem =
"-----BEGIN CERTIFICATE-----\n" +
b64der +
"\n-----END CERTIFICATE-----";
checkX509CertBundle(pem, `${field}.keys[${i}].x5c[${j}]`);
}
} 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/e for RSA key type, or x/y/crv for EC key type",
);
}
}
}
2 changes: 1 addition & 1 deletion src/crypto/ecdsa.cpp
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ namespace ccf::crypto
}

std::vector<uint8_t> ecdsa_sig_p1363_to_der(
const std::vector<uint8_t>& signature)
std::span<const uint8_t> signature)
{
auto half_size = signature.size() / 2;
return ecdsa_sig_from_r_s(
27 changes: 27 additions & 0 deletions src/crypto/openssl/rsa_public_key.cpp
Original file line number Diff line number Diff line change
@@ -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 pattern 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(
@@ -208,6 +219,22 @@ namespace ccf::crypto
pctx, signature, signature_size, hash.data(), hash.size()) == 1;
}

bool RSAPublicKey_OpenSSL::verify_pkcs1(
const uint8_t* contents,
size_t contents_size,
const uint8_t* signature,
size_t signature_size,
MDType md_type)
{
auto hash = OpenSSLHashProvider().Hash(contents, contents_size, md_type);
Unique_EVP_PKEY_CTX pctx(key);
CHECK1(EVP_PKEY_verify_init(pctx));
CHECK1(EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PADDING));
CHECK1(EVP_PKEY_CTX_set_signature_md(pctx, get_md_type(md_type)));
return EVP_PKEY_verify(
pctx, signature, signature_size, hash.data(), hash.size()) == 1;
}

std::vector<uint8_t> RSAPublicKey_OpenSSL::bn_bytes(const BIGNUM* bn)
{
std::vector<uint8_t> r(BN_num_bytes(bn));
7 changes: 7 additions & 0 deletions src/crypto/openssl/rsa_public_key.h
Original file line number Diff line number Diff line change
@@ -55,6 +55,13 @@ namespace ccf::crypto
MDType md_type = MDType::NONE,
size_t salt_length = 0) override;

virtual bool verify_pkcs1(
const uint8_t* contents,
size_t contents_size,
const uint8_t* signature,
size_t signature_size,
MDType md_type = MDType::NONE) override;

virtual Components components() const override;

static std::vector<uint8_t> bn_bytes(const BIGNUM* bn);
Loading