Skip to content

Commit f0d567d

Browse files
Raw public keys support for JWT authentication (#6680)
Co-authored-by: Amaury Chamayou <[email protected]>
1 parent 5f823c1 commit f0d567d

File tree

22 files changed

+672
-204
lines changed

22 files changed

+672
-204
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ and this project adheres Fto [Semantic Versioning](http://semver.org/spec/v2.0.0
5757
- 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.
5858
- `cchost` now requires `--config`.
5959

60+
### Changed
61+
62+
- JWT authentication now supports raw public keys along with certificates (#6601).
63+
- 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.
64+
- Has same side-effects as #5809 does please see the changelog entry for that change for more details. In short:
65+
- stale JWKs may be used for JWT validation on older nodes during the upgrade.
66+
- old tables are not cleaned up, #6222 is tracking those.
67+
- 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.
68+
6069
## [6.0.0-dev7]
6170

6271
[6.0.0-dev7]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev7

doc/schemas/gov_openapi.json

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,24 @@
799799
"type": "string"
800800
},
801801
"OpenIDJWKMetadata": {
802+
"properties": {
803+
"constraint": {
804+
"$ref": "#/components/schemas/string"
805+
},
806+
"issuer": {
807+
"$ref": "#/components/schemas/string"
808+
},
809+
"public_key": {
810+
"$ref": "#/components/schemas/base64string"
811+
}
812+
},
813+
"required": [
814+
"issuer",
815+
"public_key"
816+
],
817+
"type": "object"
818+
},
819+
"OpenIDJWKMetadataLegacy": {
802820
"properties": {
803821
"cert": {
804822
"$ref": "#/components/schemas/base64string"
@@ -811,11 +829,17 @@
811829
}
812830
},
813831
"required": [
814-
"cert",
815-
"issuer"
832+
"issuer",
833+
"cert"
816834
],
817835
"type": "object"
818836
},
837+
"OpenIDJWKMetadataLegacy_array": {
838+
"items": {
839+
"$ref": "#/components/schemas/OpenIDJWKMetadataLegacy"
840+
},
841+
"type": "array"
842+
},
819843
"OpenIDJWKMetadata_array": {
820844
"items": {
821845
"$ref": "#/components/schemas/OpenIDJWKMetadata"
@@ -1228,6 +1252,12 @@
12281252
},
12291253
"type": "object"
12301254
},
1255+
"string_to_OpenIDJWKMetadataLegacy_array": {
1256+
"additionalProperties": {
1257+
"$ref": "#/components/schemas/OpenIDJWKMetadataLegacy_array"
1258+
},
1259+
"type": "object"
1260+
},
12311261
"string_to_OpenIDJWKMetadata_array": {
12321262
"additionalProperties": {
12331263
"$ref": "#/components/schemas/OpenIDJWKMetadata_array"
@@ -1752,6 +1782,31 @@
17521782
"get": {
17531783
"deprecated": true,
17541784
"operationId": "GetGovKvJwtPublicSigningKeysMetadata",
1785+
"responses": {
1786+
"200": {
1787+
"content": {
1788+
"application/json": {
1789+
"schema": {
1790+
"$ref": "#/components/schemas/string_to_OpenIDJWKMetadataLegacy_array"
1791+
}
1792+
}
1793+
},
1794+
"description": "Default response description"
1795+
},
1796+
"default": {
1797+
"$ref": "#/components/responses/default"
1798+
}
1799+
},
1800+
"summary": "This route is auto-generated from the KV schema.",
1801+
"x-ccf-forwarding": {
1802+
"$ref": "#/components/x-ccf-forwarding/sometimes"
1803+
}
1804+
}
1805+
},
1806+
"/gov/kv/jwt/public_signing_keys_metadata_v2": {
1807+
"get": {
1808+
"deprecated": true,
1809+
"operationId": "GetGovKvJwtPublicSigningKeysMetadataV2",
17551810
"responses": {
17561811
"200": {
17571812
"content": {

include/ccf/crypto/ecdsa.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "ccf/crypto/curve.h"
66

7+
#include <span>
78
#include <vector>
89

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

3334
std::vector<uint8_t> ecdsa_sig_der_to_p1363(
3435
const std::vector<uint8_t>& signature, CurveID curveId);

include/ccf/crypto/jwk.h

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,12 @@ namespace ccf::crypto
2727
JsonWebKeyType kty;
2828
std::optional<std::string> kid = std::nullopt;
2929
std::optional<std::vector<std::string>> x5c = std::nullopt;
30-
std::optional<std::string> issuer = std::nullopt;
3130

3231
bool operator==(const JsonWebKey&) const = default;
3332
};
3433
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JsonWebKey);
3534
DECLARE_JSON_REQUIRED_FIELDS(JsonWebKey, kty);
36-
DECLARE_JSON_OPTIONAL_FIELDS(JsonWebKey, kid, x5c, issuer);
35+
DECLARE_JSON_OPTIONAL_FIELDS(JsonWebKey, kid, x5c);
3736

3837
enum class JsonWebKeyECCurve
3938
{
@@ -47,6 +46,25 @@ namespace ccf::crypto
4746
{JsonWebKeyECCurve::P384, "P-384"},
4847
{JsonWebKeyECCurve::P521, "P-521"}});
4948

49+
struct JsonWebKeyData
50+
{
51+
JsonWebKeyType kty;
52+
std::optional<std::string> kid = std::nullopt;
53+
std::optional<std::vector<std::string>> x5c = std::nullopt;
54+
std::optional<std::string> n = std::nullopt;
55+
std::optional<std::string> e = std::nullopt;
56+
std::optional<std::string> x = std::nullopt;
57+
std::optional<std::string> y = std::nullopt;
58+
std::optional<JsonWebKeyECCurve> crv = std::nullopt;
59+
std::optional<std::string> issuer = std::nullopt;
60+
61+
bool operator==(const JsonWebKeyData&) const = default;
62+
};
63+
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JsonWebKeyData);
64+
DECLARE_JSON_REQUIRED_FIELDS(JsonWebKeyData, kty);
65+
DECLARE_JSON_OPTIONAL_FIELDS(
66+
JsonWebKeyData, kid, x5c, n, e, x, y, crv, issuer);
67+
5068
static JsonWebKeyECCurve curve_id_to_jwk_curve(CurveID curve_id)
5169
{
5270
switch (curve_id)

include/ccf/crypto/rsa_public_key.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ namespace ccf::crypto
8484
MDType md_type = MDType::NONE,
8585
size_t salt_legth = 0) = 0;
8686

87+
virtual bool verify_pkcs1(
88+
const uint8_t* contents,
89+
size_t contents_size,
90+
const uint8_t* signature,
91+
size_t signature_size,
92+
MDType md_type = MDType::NONE) = 0;
93+
8794
struct Components
8895
{
8996
std::vector<uint8_t> n;

include/ccf/endpoints/authentication/jwt_auth.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace ccf
1717
nlohmann::json payload;
1818
};
1919

20-
struct VerifiersCache;
20+
struct PublicKeysCache;
2121

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

3333
public:
3434
static constexpr auto SECURITY_SCHEME_NAME = "jwt";

include/ccf/service/tables/jwt.h

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,34 +37,51 @@ namespace ccf
3737
using JwtIssuer = std::string;
3838
using JwtKeyId = std::string;
3939
using Cert = std::vector<uint8_t>;
40+
using PublicKey = std::vector<uint8_t>;
4041

4142
struct OpenIDJWKMetadata
4243
{
43-
Cert cert;
44+
PublicKey public_key;
4445
JwtIssuer issuer;
4546
std::optional<JwtIssuer> constraint;
4647
};
4748
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(OpenIDJWKMetadata);
48-
DECLARE_JSON_REQUIRED_FIELDS(OpenIDJWKMetadata, cert, issuer);
49+
DECLARE_JSON_REQUIRED_FIELDS(OpenIDJWKMetadata, issuer, public_key);
4950
DECLARE_JSON_OPTIONAL_FIELDS(OpenIDJWKMetadata, constraint);
5051

51-
using JwtIssuers = ServiceMap<JwtIssuer, JwtIssuerMetadata>;
52-
using JwtPublicSigningKeys =
52+
using JwtPublicSigningKeysMetadata =
5353
ServiceMap<JwtKeyId, std::vector<OpenIDJWKMetadata>>;
5454

55+
struct OpenIDJWKMetadataLegacy
56+
{
57+
Cert cert;
58+
JwtIssuer issuer;
59+
std::optional<JwtIssuer> constraint;
60+
};
61+
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(OpenIDJWKMetadataLegacy);
62+
DECLARE_JSON_REQUIRED_FIELDS(OpenIDJWKMetadataLegacy, issuer, cert);
63+
DECLARE_JSON_OPTIONAL_FIELDS(OpenIDJWKMetadataLegacy, constraint);
64+
65+
using JwtPublicSigningKeysMetadataLegacy =
66+
ServiceMap<JwtKeyId, std::vector<OpenIDJWKMetadataLegacy>>;
67+
68+
using JwtIssuers = ServiceMap<JwtIssuer, JwtIssuerMetadata>;
69+
5570
namespace Tables
5671
{
5772
static constexpr auto JWT_ISSUERS = "public:ccf.gov.jwt.issuers";
5873

5974
static constexpr auto JWT_PUBLIC_SIGNING_KEYS_METADATA =
60-
"public:ccf.gov.jwt.public_signing_keys_metadata";
75+
"public:ccf.gov.jwt.public_signing_keys_metadata_v2";
6176

6277
namespace Legacy
6378
{
6479
static constexpr auto JWT_PUBLIC_SIGNING_KEYS =
6580
"public:ccf.gov.jwt.public_signing_key";
6681
static constexpr auto JWT_PUBLIC_SIGNING_KEY_ISSUER =
6782
"public:ccf.gov.jwt.public_signing_key_issuer";
83+
static constexpr auto JWT_PUBLIC_SIGNING_KEYS_METADATA =
84+
"public:ccf.gov.jwt.public_signing_keys_metadata";
6885

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

7693
struct JsonWebKeySet
7794
{
78-
std::vector<ccf::crypto::JsonWebKey> keys;
95+
std::vector<ccf::crypto::JsonWebKeyData> keys;
7996

8097
bool operator!=(const JsonWebKeySet& rhs) const
8198
{

samples/constitutions/default/actions.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,28 @@ function checkJwks(value, field) {
130130
for (const [i, jwk] of value.keys.entries()) {
131131
checkType(jwk.kid, "string", `${field}.keys[${i}].kid`);
132132
checkType(jwk.kty, "string", `${field}.keys[${i}].kty`);
133-
checkType(jwk.x5c, "array", `${field}.keys[${i}].x5c`);
134-
checkLength(jwk.x5c, 1, null, `${field}.keys[${i}].x5c`);
135-
for (const [j, b64der] of jwk.x5c.entries()) {
136-
checkType(b64der, "string", `${field}.keys[${i}].x5c[${j}]`);
137-
const pem =
138-
"-----BEGIN CERTIFICATE-----\n" +
139-
b64der +
140-
"\n-----END CERTIFICATE-----";
141-
checkX509CertBundle(pem, `${field}.keys[${i}].x5c[${j}]`);
133+
if (jwk.x5c) {
134+
checkType(jwk.x5c, "array", `${field}.keys[${i}].x5c`);
135+
checkLength(jwk.x5c, 1, null, `${field}.keys[${i}].x5c`);
136+
for (const [j, b64der] of jwk.x5c.entries()) {
137+
checkType(b64der, "string", `${field}.keys[${i}].x5c[${j}]`);
138+
const pem =
139+
"-----BEGIN CERTIFICATE-----\n" +
140+
b64der +
141+
"\n-----END CERTIFICATE-----";
142+
checkX509CertBundle(pem, `${field}.keys[${i}].x5c[${j}]`);
143+
}
144+
} else if (jwk.n && jwk.e) {
145+
checkType(jwk.n, "string", `${field}.keys[${i}].n`);
146+
checkType(jwk.e, "string", `${field}.keys[${i}].e`);
147+
} else if (jwk.x && jwk.y) {
148+
checkType(jwk.x, "string", `${field}.keys[${i}].x`);
149+
checkType(jwk.y, "string", `${field}.keys[${i}].y`);
150+
checkType(jwk.crv, "string", `${field}.keys[${i}].crv`);
151+
} else {
152+
throw new Error(
153+
"JWK must contain either x5c, or n/e for RSA key type, or x/y/crv for EC key type",
154+
);
142155
}
143156
}
144157
}

src/crypto/ecdsa.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ namespace ccf::crypto
4545
}
4646

4747
std::vector<uint8_t> ecdsa_sig_p1363_to_der(
48-
const std::vector<uint8_t>& signature)
48+
std::span<const uint8_t> signature)
4949
{
5050
auto half_size = signature.size() / 2;
5151
return ecdsa_sig_from_r_s(

src/crypto/openssl/rsa_public_key.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ namespace ccf::crypto
5454
auto msg = OpenSSL::error_string(ec);
5555
throw std::runtime_error(fmt::format("OpenSSL error: {}", msg));
5656
}
57+
58+
// As it's a common pattern to rely on successful key wrapper construction as a
59+
// confirmation of a concrete key type, this must fail for non-RSA keys.
60+
#if defined(OPENSSL_VERSION_MAJOR) && OPENSSL_VERSION_MAJOR >= 3
61+
if (!key || EVP_PKEY_get_base_id(key) != EVP_PKEY_RSA)
62+
#else
63+
if (!key || !EVP_PKEY_get0_RSA(key))
64+
#endif
65+
{
66+
throw std::logic_error("invalid RSA key");
67+
}
5768
}
5869

5970
std::pair<Unique_BIGNUM, Unique_BIGNUM> get_modulus_and_exponent(
@@ -208,6 +219,22 @@ namespace ccf::crypto
208219
pctx, signature, signature_size, hash.data(), hash.size()) == 1;
209220
}
210221

222+
bool RSAPublicKey_OpenSSL::verify_pkcs1(
223+
const uint8_t* contents,
224+
size_t contents_size,
225+
const uint8_t* signature,
226+
size_t signature_size,
227+
MDType md_type)
228+
{
229+
auto hash = OpenSSLHashProvider().Hash(contents, contents_size, md_type);
230+
Unique_EVP_PKEY_CTX pctx(key);
231+
CHECK1(EVP_PKEY_verify_init(pctx));
232+
CHECK1(EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PADDING));
233+
CHECK1(EVP_PKEY_CTX_set_signature_md(pctx, get_md_type(md_type)));
234+
return EVP_PKEY_verify(
235+
pctx, signature, signature_size, hash.data(), hash.size()) == 1;
236+
}
237+
211238
std::vector<uint8_t> RSAPublicKey_OpenSSL::bn_bytes(const BIGNUM* bn)
212239
{
213240
std::vector<uint8_t> r(BN_num_bytes(bn));

0 commit comments

Comments
 (0)