Skip to content

Commit

Permalink
CR changes
Browse files Browse the repository at this point in the history
  • Loading branch information
maxtropets committed Jun 2, 2024
1 parent 245f9c1 commit f451327
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 149 deletions.
3 changes: 3 additions & 0 deletions include/ccf/service/tables/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ namespace ccf
"public:ccf.gov.jwt.public_signing_key";
static constexpr auto JWT_PUBLIC_SIGNING_KEY_ISSUER =
"public:ccf.gov.jwt.public_signing_key_issuer";

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

static constexpr auto JWT_PUBLIC_SIGNING_KEY_CERTS =
Expand Down
15 changes: 14 additions & 1 deletion src/endpoints/authentication/jwt_auth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ namespace
bool validate_issuer(
const http::JwtVerifier::Token& token, std::string issuer)
{
LOG_INFO_FMT(
LOG_DEBUG_FMT(
"Verify token.iss {} and token.tid {} against published key issuer {}",
token.payload_typed.iss,
token.payload_typed.tid,
Expand Down Expand Up @@ -196,6 +196,19 @@ namespace ccf
else
{
auto identity = std::make_unique<JwtAuthnIdentity>();

if (validated_issuer.empty())
{
auto fallback_issuers =
tx.ro<ccf::Tables::Legacy::JwtPublicSigningKeyIssuer>(
ccf::Tables::Legacy::JWT_PUBLIC_SIGNING_KEY_ISSUER);
const auto& issuer = fallback_issuers->get(key_id);
if (issuer)
{
validated_issuer = *issuer;
}
}

identity->key_issuer = validated_issuer;
identity->header = std::move(token.header);
identity->payload = std::move(token.payload);
Expand Down
37 changes: 28 additions & 9 deletions src/node/rpc/jwt_management.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@

namespace ccf
{
static void legacy_remove_jwt_public_signing_keys(
kv::Tx& tx, std::string issuer)
{
auto keys =
tx.rw<JwtPublicSigningKeys>(Tables::Legacy::JWT_PUBLIC_SIGNING_KEYS);
auto key_issuer = tx.rw<Tables::Legacy::JwtPublicSigningKeyIssuer>(
Tables::Legacy::JWT_PUBLIC_SIGNING_KEY_ISSUER);

key_issuer->foreach(
[&issuer, &keys, &key_issuer](const auto& k, const auto& v) {
if (v == issuer)
{
keys->remove(k);
key_issuer->remove(k);
}
return true;
});
}

static bool check_issuer(
const std::string& issuer, const std::string& constraint)
{
Expand All @@ -34,24 +53,24 @@ namespace ccf
}

// Either constraint's domain == issuer's domain or it is a subdomain, e.g.:
// limited.facebok.com
// .facebok.com
// limited.facebook.com
// .facebook.com
if (issuer_domain != constraint_domain)
{
const auto start_seek =
issuer_domain.size() - (constraint_domain.size() + 1);
const auto count_seek = constraint_domain.size() + 1;
const auto pattern = "." + constraint_domain;

return start_seek > 0 // at least one letter preceeds .issuer.domain
&& issuer_domain.substr(start_seek, count_seek) == pattern;
return issuer_domain.ends_with(pattern);
}

return true;
}

static void remove_jwt_public_signing_keys(kv::Tx& tx, std::string issuer)
{
// Unlike resetting JWT keys for a particular issuer, removing keys can be
// safely done on both table revisions, as soon as the application shouldn't
// use them anyway after being ask about that explicitly.
legacy_remove_jwt_public_signing_keys(tx, issuer);

auto keys =
tx.rw<JwtPublicSigningKeys>(Tables::JWT_PUBLIC_SIGNING_KEY_CERTS);
auto key_issuers =
Expand Down Expand Up @@ -313,7 +332,7 @@ namespace ccf
value.constraint = it->second;
}

LOG_INFO_FMT(
LOG_DEBUG_FMT(
"Save JWT issuer for kid {} where issuer: {}, issuer constraint: {}",
kid,
value.issuer,
Expand Down
192 changes: 53 additions & 139 deletions tests/js-custom-authorization/custom_authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,42 @@ def temporary_js_limits(network, primary, **kwargs):
network.consortium.set_js_runtime_options(primary, **default_kwargs)


def set_issuer_with_a_key(primary, network, issuer, kid, constraint):
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
jwt_cert_der = infra.crypto.cert_pem_to_der(issuer.cert_pem)
der_b64 = base64.b64encode(jwt_cert_der).decode("ascii")
data = {
"issuer": issuer.issuer_url,
"auto_refresh": False,
"jwks": {
"keys": [
{
"kty": "RSA",
"kid": kid,
"x5c": [der_b64],
"issuer": constraint,
}
]
},
}
json.dump(data, metadata_fp)
metadata_fp.flush()
network.consortium.set_jwt_issuer(primary, metadata_fp.name)


def try_auth(primary, issuer, kid, iss, tid):
with primary.client("user0") as c:
LOG.info(f"Creating JWT with kid={kid} iss={iss} tenant={tid}")
r = c.get(
"/app/jwt",
headers=infra.jwt_issuer.make_bearer_header(
issuer.issue_jwt(kid, claims={"iss": iss, "tid": tid})
),
)
assert r.status_code
return r.status_code


@reqs.description("Test stack size limit")
def test_stack_size_limit(network, args):
primary, _ = network.find_nodes()
Expand Down Expand Up @@ -311,25 +347,8 @@ def test_jwt_auth(network, args):
jwt_kid = "my_key_id"

LOG.info("Add JWT issuer with initial keys")
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
jwt_cert_der = infra.crypto.cert_pem_to_der(issuer.cert_pem)
der_b64 = base64.b64encode(jwt_cert_der).decode("ascii")
data = {
"issuer": issuer.name,
"jwks": {
"keys": [
{
"kty": "RSA",
"kid": jwt_kid,
"x5c": [der_b64],
"issuer": issuer.name,
}
]
},
}
json.dump(data, metadata_fp)
metadata_fp.flush()
network.consortium.set_jwt_issuer(primary, metadata_fp.name)

set_issuer_with_a_key(primary, network, issuer, jwt_kid, issuer.name)

LOG.info("Calling jwt endpoint after storing keys")
with primary.client("user0") as c:
Expand Down Expand Up @@ -369,19 +388,6 @@ def test_jwt_auth(network, args):
return network


def try_auth(primary, issuer, kid, iss, tid):
with primary.client("user0") as c:
LOG.info("Calling JWT with kid from issuer for tenant")
r = c.get(
"/app/jwt",
headers=infra.jwt_issuer.make_bearer_header(
issuer.issue_jwt(kid, claims={"iss": iss, "tid": tid})
),
)
assert r.status_code
return r.status_code


@reqs.description("JWT authentication as by MSFT Entra (single tenant)")
def test_jwt_auth_msft_single_tenant(network, args):
"""For a specific tenant, only tokens with this issuer+tenant can auth."""
Expand All @@ -396,26 +402,7 @@ def test_jwt_auth_msft_single_tenant(network, args):
issuer = infra.jwt_issuer.JwtIssuer(name="https://login.microsoftonline.com")
jwt_kid = "my_key_id"

with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
jwt_cert_der = infra.crypto.cert_pem_to_der(issuer.cert_pem)
der_b64 = base64.b64encode(jwt_cert_der).decode("ascii")
data = {
"issuer": issuer.name,
"auto_refresh": False,
"jwks": {
"keys": [
{
"kty": "RSA",
"kid": jwt_kid,
"x5c": [der_b64],
"issuer": ISSUER_TENANT,
}
]
},
}
json.dump(data, metadata_fp)
metadata_fp.flush()
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
set_issuer_with_a_key(primary, network, issuer, jwt_kid, ISSUER_TENANT)

assert (
try_auth(primary, issuer, jwt_kid, ISSUER_TENANT, "garbage_tenant")
Expand Down Expand Up @@ -447,7 +434,7 @@ def test_jwt_auth_msft_multitenancy(network, args):
COMMNON_ISSUER = "https://login.microsoftonline.com/{tenantid}/v2.0"
TENANT_ID = "9188050d-6c67-4c5b-b112-36a304b66da"
ISSUER_TENANT = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
ANOTHER_TENANT_ID = "ANOTHER-6c67-4c5b-b112-36a304b66da"
ANOTHER_TENANT_ID = "deadbeef-6c67-4c5b-b112-36a304b66da"
ISSUER_ANOTHER = f"https://login.microsoftonline.com/{ANOTHER_TENANT_ID}/v2.0"

issuer = infra.jwt_issuer.JwtIssuer(name="https://login.microsoftonline.com")
Expand Down Expand Up @@ -528,65 +515,30 @@ def test_jwt_auth_msft_same_kids_different_issuers(network, args):

TENANT_ID = "9188050d-6c67-4c5b-b112-36a304b66da"
ISSUER_TENANT = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
ANOTHER_TENANT_ID = "ANOTHER-6c67-4c5b-b112-36a304b66da"
ANOTHER_TENANT_ID = "deadbeef-6c67-4c5b-b112-36a304b66da"
ISSUER_ANOTHER = f"https://login.microsoftonline.com/{ANOTHER_TENANT_ID}/v2.0"

issuer = infra.jwt_issuer.JwtIssuer(name=ISSUER_TENANT)
another = infra.jwt_issuer.JwtIssuer(name=ISSUER_ANOTHER)

# Immitate same key sharing
another.cert_pem, another.key_priv_pem = issuer.cert_pem, issuer.key_priv_pem

jwt_kid = "my_key_id"

with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
jwt_cert_der = infra.crypto.cert_pem_to_der(issuer.cert_pem)
der_b64 = base64.b64encode(jwt_cert_der).decode("ascii")
data = {
"issuer": issuer.issuer_url,
"auto_refresh": False,
"jwks": {
"keys": [
{
"kty": "RSA",
"kid": jwt_kid,
"x5c": [der_b64],
"issuer": ISSUER_TENANT,
}
]
},
}
json.dump(data, metadata_fp)
metadata_fp.flush()
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
set_issuer_with_a_key(primary, network, issuer, jwt_kid, ISSUER_TENANT)

assert try_auth(primary, issuer, jwt_kid, ISSUER_TENANT, TENANT_ID) == HTTPStatus.OK
assert (
try_auth(primary, issuer, jwt_kid, ISSUER_ANOTHER, ANOTHER_TENANT_ID)
try_auth(primary, another, jwt_kid, ISSUER_ANOTHER, ANOTHER_TENANT_ID)
== HTTPStatus.UNAUTHORIZED
)

with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
jwt_cert_der = infra.crypto.cert_pem_to_der(issuer.cert_pem)
der_b64 = base64.b64encode(jwt_cert_der).decode("ascii")
data = {
"issuer": another.issuer_url,
"auto_refresh": False,
"jwks": {
"keys": [
{
"kty": "RSA",
"kid": jwt_kid,
"x5c": [der_b64],
"issuer": ISSUER_ANOTHER,
}
]
},
}
json.dump(data, metadata_fp)
metadata_fp.flush()
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
set_issuer_with_a_key(primary, network, another, jwt_kid, ISSUER_ANOTHER)

assert try_auth(primary, issuer, jwt_kid, ISSUER_TENANT, TENANT_ID) == HTTPStatus.OK
assert (
try_auth(primary, issuer, jwt_kid, ISSUER_ANOTHER, ANOTHER_TENANT_ID)
try_auth(primary, another, jwt_kid, ISSUER_ANOTHER, ANOTHER_TENANT_ID)
== HTTPStatus.OK
)

Expand All @@ -597,7 +549,7 @@ def test_jwt_auth_msft_same_kids_different_issuers(network, args):
== HTTPStatus.UNAUTHORIZED
)
assert (
try_auth(primary, issuer, jwt_kid, ISSUER_ANOTHER, ANOTHER_TENANT_ID)
try_auth(primary, another, jwt_kid, ISSUER_ANOTHER, ANOTHER_TENANT_ID)
== HTTPStatus.OK
)

Expand All @@ -608,7 +560,7 @@ def test_jwt_auth_msft_same_kids_different_issuers(network, args):
== HTTPStatus.UNAUTHORIZED
)
assert (
try_auth(primary, issuer, jwt_kid, ISSUER_ANOTHER, ANOTHER_TENANT_ID)
try_auth(primary, another, jwt_kid, ISSUER_ANOTHER, ANOTHER_TENANT_ID)
== HTTPStatus.UNAUTHORIZED
)

Expand All @@ -627,59 +579,21 @@ def test_jwt_auth_msft_same_kids_overwrite_constraint(network, args):
COMMNON_ISSUER = "https://login.microsoftonline.com/{tenantid}/v2.0"
TENANT_ID = "9188050d-6c67-4c5b-b112-36a304b66da"
ISSUER_TENANT = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
ANOTHER_TENANT_ID = "ANOTHER-6c67-4c5b-b112-36a304b66da"
ANOTHER_TENANT_ID = "deadbeef-6c67-4c5b-b112-36a304b66da"
ISSUER_ANOTHER = f"https://login.microsoftonline.com/{ANOTHER_TENANT_ID}/v2.0"

issuer = infra.jwt_issuer.JwtIssuer(name=ISSUER_TENANT)
jwt_kid = "my_key_id"

with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
jwt_cert_der = infra.crypto.cert_pem_to_der(issuer.cert_pem)
der_b64 = base64.b64encode(jwt_cert_der).decode("ascii")
data = {
"issuer": issuer.issuer_url,
"auto_refresh": False,
"jwks": {
"keys": [
{
"kty": "RSA",
"kid": jwt_kid,
"x5c": [der_b64],
"issuer": COMMNON_ISSUER,
}
]
},
}
json.dump(data, metadata_fp)
metadata_fp.flush()
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
set_issuer_with_a_key(primary, network, issuer, jwt_kid, COMMNON_ISSUER)

assert try_auth(primary, issuer, jwt_kid, ISSUER_TENANT, TENANT_ID) == HTTPStatus.OK
assert (
try_auth(primary, issuer, jwt_kid, ISSUER_ANOTHER, ANOTHER_TENANT_ID)
== HTTPStatus.OK
)

with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
jwt_cert_der = infra.crypto.cert_pem_to_der(issuer.cert_pem)
der_b64 = base64.b64encode(jwt_cert_der).decode("ascii")
data = {
"issuer": issuer.issuer_url,
"auto_refresh": False,
"jwks": {
"keys": [
{
"kty": "RSA",
"kid": jwt_kid,
"x5c": [der_b64],
"issuer": ISSUER_TENANT,
}
]
},
}
json.dump(data, metadata_fp)
metadata_fp.flush()
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
set_issuer_with_a_key(primary, network, issuer, jwt_kid, ISSUER_TENANT)

assert try_auth(primary, issuer, jwt_kid, ISSUER_TENANT, TENANT_ID) == HTTPStatus.OK
assert (
Expand Down
2 changes: 2 additions & 0 deletions tests/jwt_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ def test_jwt_issuer_domain_match(network, args):
else:
assert False, f"Constraint {constraint} must not be allowed"

network.consortium.remove_jwt_issuer(primary, issuer.name)


@reqs.description("Multiple JWT issuers registered at once")
def test_jwt_endpoint(network, args):
Expand Down

0 comments on commit f451327

Please sign in to comment.