Skip to content

Commit

Permalink
Raw public keys support for JWT authentication (#6680)
Browse files Browse the repository at this point in the history
Co-authored-by: Amaury Chamayou <[email protected]>
  • Loading branch information
maxtropets and achamayou authored Jan 3, 2025
1 parent 5f823c1 commit f0d567d
Show file tree
Hide file tree
Showing 22 changed files with 672 additions and 204 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 57 additions & 2 deletions doc/schemas/gov_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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": {
Expand Down
3 changes: 2 additions & 1 deletion include/ccf/crypto/ecdsa.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "ccf/crypto/curve.h"

#include <span>
#include <vector>

namespace ccf::crypto
Expand All @@ -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);
Expand Down
22 changes: 20 additions & 2 deletions include/ccf/crypto/jwk.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions include/ccf/crypto/rsa_public_key.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions include/ccf/endpoints/authentication/jwt_auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace ccf
nlohmann::json payload;
};

struct VerifiersCache;
struct PublicKeysCache;

bool validate_issuer(
const std::string& iss,
Expand All @@ -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";
Expand Down
29 changes: 23 additions & 6 deletions include/ccf/service/tables/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand All @@ -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
{
Expand Down
31 changes: 22 additions & 9 deletions samples/constitutions/default/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/crypto/ecdsa.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
27 changes: 27 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 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(
Expand Down Expand Up @@ -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));
Expand Down
7 changes: 7 additions & 0 deletions src/crypto/openssl/rsa_public_key.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit f0d567d

Please sign in to comment.