Skip to content
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

JWT issuer validation #6175

Merged
merged 16 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
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
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Removed

- Removed unused `openenclave.verifyOpenEnclaveEvidence` API from JS/TS.
- Removed unused `openenclave.verifyOpenEnclaveEvidence` API from JS/TS

### Changed

- Added token.iss claim validation to JWT authentication (#5809). Must-knows:
- Supports both the [OpenID requirements](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation) and the [Entra specification](https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens#validate-the-issuer) of it.
- All keys fetched after the upgrade will not work against tokens missing the 'iss' claim if the issuer has been specified in the .well-known/openid-configuration/.
- Due to an internal schema change, networks that are in the process of upgrading to this version may see inconsistent authorization behaviour while the network contains nodes of different versions (depending which node executes the auto-refresh, any nodes on the other version will not use any newly provided keys). We recommend a full upgrade to this version, removing any nodes on prior versions, followed by a key and issuer refresh.
- A future release will remove the old tables entirely. Until then, some redundant state will be retained in the ledger. This is tracked in [#6222](https://github.com/microsoft/CCF/issues/6222).

## [5.0.0-dev17]

Expand Down
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,12 @@ if(BUILD_TESTS)
)
target_link_libraries(endpoint_registry_test PRIVATE ccf_endpoints.host)

add_unit_test(
jwt_auth_test
${CMAKE_CURRENT_SOURCE_DIR}/src/endpoints/test/test_jwt_auth.cpp
)
target_link_libraries(jwt_auth_test PRIVATE ccf_endpoints.host)

add_unit_test(
tx_status_test
${CMAKE_CURRENT_SOURCE_DIR}/src/node/rpc/test/tx_status_test.cpp
Expand Down
13 changes: 11 additions & 2 deletions doc/audit/builtin_maps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ JWT issuers.
``jwt.public_signing_keys``
~~~~~~~~~~~~~~~~~~~~~~~~~~~

JWT signing keys.
JWT signing keys, used until 5.0.

**Key** JWT Key ID, represented as a string.

Expand All @@ -375,12 +375,21 @@ JWT signing keys.
``jwt.public_signing_key_issuer``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

JWT signing key to Issuer mapping.
JWT signing key to Issuer mapping, used until 5.0.

**Key** JWT Key ID, represented as a string.

**Value** JWT issuer URL, represented as a string.

``jwt.public_signing_keys_metadata``
~~~~~~~~~~~~~~~~~~~~~~~~~~~

JWT signing keys.

**Key** JWT Key ID, represented as a string.

**Value** List of (DER-encoded key/certificate, issuer, constraint) used to validate the Issuer during authorization, represented as JSON.

``constitution``
~~~~~~~~~~~~~~~~

Expand Down
70 changes: 64 additions & 6 deletions doc/schemas/gov_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,12 @@
],
"type": "object"
},
"KeyIdInfo_array": {
"items": {
"$ref": "#/components/schemas/KeyIdInfo"
},
"type": "array"
},
"MDType": {
"enum": [
"NONE",
Expand Down Expand Up @@ -858,6 +864,30 @@
],
"type": "string"
},
"OpenIDJWKMetadata": {
"properties": {
"cert": {
"$ref": "#/components/schemas/base64string"
},
"constraint": {
"$ref": "#/components/schemas/string"
},
"issuer": {
"$ref": "#/components/schemas/string"
}
},
"required": [
"cert",
"issuer"
],
"type": "object"
},
"OpenIDJWKMetadata_array": {
"items": {
"$ref": "#/components/schemas/OpenIDJWKMetadata"
},
"type": "array"
},
"Pem": {
"type": "string"
},
Expand Down Expand Up @@ -1257,9 +1287,15 @@
},
"type": "object"
},
"string_to_KeyIdInfo": {
"string_to_KeyIdInfo_array": {
"additionalProperties": {
"$ref": "#/components/schemas/KeyIdInfo"
"$ref": "#/components/schemas/KeyIdInfo_array"
},
"type": "object"
},
"string_to_OpenIDJWKMetadata_array": {
"additionalProperties": {
"$ref": "#/components/schemas/OpenIDJWKMetadata_array"
},
"type": "object"
},
Expand Down Expand Up @@ -1547,7 +1583,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/string_to_KeyIdInfo"
"$ref": "#/components/schemas/string_to_KeyIdInfo_array"
}
}
},
Expand All @@ -1557,7 +1593,7 @@
"$ref": "#/components/responses/default"
}
},
"summary": "This endpoint is deprecated. It is replaced by /gov/kv/jwt/public_signing_keys, /gov/kv/jwt/public_signing_key_issue, and /gov/kv/jwt/issuers endpoints.",
"summary": "This endpoint is deprecated. It is replaced by /gov/kv/jwt/public_signing_keys_metadata and /gov/kv/jwt/issuers endpoints.",
"x-ccf-forwarding": {
"$ref": "#/components/x-ccf-forwarding/always"
}
Expand Down Expand Up @@ -1739,6 +1775,28 @@
}
}
},
"/gov/kv/jwt/public_signing_key": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/string_to_base64string"
}
}
},
"description": "Default response description"
},
"default": {
"$ref": "#/components/responses/default"
}
},
"x-ccf-forwarding": {
"$ref": "#/components/x-ccf-forwarding/sometimes"
}
}
},
"/gov/kv/jwt/public_signing_key_issuer": {
"get": {
"responses": {
Expand All @@ -1761,14 +1819,14 @@
}
}
},
"/gov/kv/jwt/public_signing_keys": {
"/gov/kv/jwt/public_signing_keys_metadata": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/string_to_base64string"
"$ref": "#/components/schemas/string_to_OpenIDJWKMetadata_array"
}
}
},
Expand Down
3 changes: 1 addition & 2 deletions include/ccf/common_auth_policies.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ namespace ccf
std::make_shared<MemberCertAuthnPolicy>();

/** Authenticate using JWT, validating the token using the
* @c public:ccf.gov.jwt.public_signing_key_issuer and
* @c public:ccf.gov.jwt.public_signing_keys tables */
* @c public:ccf.gov.jwt.public_signing_keys_metadata table */
static std::shared_ptr<JwtAuthnPolicy> jwt_auth_policy =
std::make_shared<JwtAuthnPolicy>();

Expand Down
3 changes: 2 additions & 1 deletion include/ccf/crypto/jwk.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ namespace 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);
DECLARE_JSON_OPTIONAL_FIELDS(JsonWebKey, kid, x5c, issuer);

enum class JsonWebKeyECCurve
{
Expand Down
5 changes: 5 additions & 0 deletions include/ccf/endpoints/authentication/jwt_auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ namespace ccf

struct VerifiersCache;

bool validate_issuer(
const std::string& iss,
const std::optional<std::string>& tid,
std::string constraint);

class JwtAuthnPolicy : public AuthnPolicy
{
protected:
Expand Down
34 changes: 27 additions & 7 deletions include/ccf/service/tables/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,38 @@ namespace ccf
using JwtKeyId = std::string;
using Cert = std::vector<uint8_t>;

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

using JwtIssuers = ServiceMap<JwtIssuer, JwtIssuerMetadata>;
using JwtPublicSigningKeys = kv::RawCopySerialisedMap<JwtKeyId, Cert>;
using JwtPublicSigningKeyIssuer =
maxtropets marked this conversation as resolved.
Show resolved Hide resolved
kv::RawCopySerialisedMap<JwtKeyId, JwtIssuer>;
using JwtPublicSigningKeys =
ServiceMap<JwtKeyId, std::vector<OpenIDJWKMetadata>>;

namespace Tables
{
static constexpr auto JWT_ISSUERS = "public:ccf.gov.jwt.issuers";
static constexpr auto JWT_PUBLIC_SIGNING_KEYS =
"public:ccf.gov.jwt.public_signing_keys";
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";

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";

using JwtPublicSigningKeys = kv::RawCopySerialisedMap<JwtKeyId, Cert>;
using JwtPublicSigningKeyIssuer =
kv::RawCopySerialisedMap<JwtKeyId, JwtIssuer>;
}
}

struct JsonWebKeySet
Expand Down
Loading