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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

maxtropets
Copy link

@maxtropets maxtropets commented May 10, 2024

@maxtropets maxtropets changed the title WIP WIP: JWT issuer validation May 10, 2024
@maxtropets maxtropets force-pushed the f/5809-jwt-issuer-policy-check branch 4 times, most recently from c5c28c9 to b656996 Compare May 21, 2024 14:47
@maxtropets maxtropets force-pushed the f/5809-jwt-issuer-policy-check branch 3 times, most recently from 8d2f033 to ff4e113 Compare May 23, 2024 12:29
@maxtropets maxtropets changed the title WIP: JWT issuer validation JWT issuer validation May 23, 2024
@maxtropets maxtropets self-assigned this May 23, 2024
@maxtropets maxtropets requested review from achamayou and eddyashton and removed request for achamayou and eddyashton May 23, 2024 13:10
@maxtropets maxtropets marked this pull request as ready for review May 23, 2024 13:11
@maxtropets maxtropets requested a review from a team as a code owner May 23, 2024 13:11
@maxtropets maxtropets force-pushed the f/5809-jwt-issuer-policy-check branch 2 times, most recently from b2bc164 to a66369a Compare May 28, 2024 10:18
@maxtropets
Copy link
Author

As noted above, kept the old tables but moved them under "Legacy" namespace. We can work with them if needed but kept them explicitly obsolete.

@maxtropets maxtropets marked this pull request as draft May 28, 2024 10:46
@maxtropets
Copy link
Author

Converted back to "Draft" to include #5177 and #6204

@maxtropets maxtropets force-pushed the f/5809-jwt-issuer-policy-check branch from 180e94a to 4c1885e Compare May 29, 2024 12:40
@maxtropets maxtropets force-pushed the f/5809-jwt-issuer-policy-check branch 2 times, most recently from 66dcd1e to a5d6e0b Compare May 29, 2024 15:47
@maxtropets maxtropets marked this pull request as ready for review May 29, 2024 16:04
Do not allow issuers to provide keys with mismatching constraints.
Thus, issuers with a particular domain should specify key iss token
constraints for that domain.
@maxtropets maxtropets force-pushed the f/5809-jwt-issuer-policy-check branch from a5d6e0b to 0097276 Compare May 29, 2024 16:10
Copy link
Member

@eddyashton eddyashton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review - need to take another look at the C++ changes.

service_keys = get_jwt_keys(args, primary)
assert service_keys["kid1"]["issuers"][issuer.name] == issuer.name

keys["keys"][0]["issuer"] = "https://issuer.com"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment or brief LOG.info explaining this line? It looks like it's setting a permitted constraint (in contrast to the later garbage constraints), but its not obvious to me why we're setting this nested field directly.

pass
else:
assert False, f"Constraint {constraint} must not be allowed"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

The other tests remove their temporary issuer to clean up the network before closing - should this do the same?


def try_auth(primary, issuer, kid, iss, tid):
with primary.client("user0") as c:
LOG.info("Calling JWT with kid from issuer for tenant")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LOG.info("Calling JWT with kid from issuer for tenant")
LOG.info(f"Creating JWT with kid={kid} from issuer={iss} for tenant={tid}")

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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ANOTHER_TENANT_ID = "ANOTHER-6c67-4c5b-b112-36a304b66da"
ANOTHER_TENANT_ID = "aaaaaaa-6c67-4c5b-b112-36a304b66da"

Nit: Although we treat these as strings for now, they're really GUIDs - we should use a string of the right shape so that it will still work if we add more validation later. Hopefully it's clear that this is still an invalid/constructed GUID.

Comment on lines +539 to +558
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do this block several times - can we pull it out to a helper function?

bool validate_issuer(
const http::JwtVerifier::Token& token, std::string issuer)
{
LOG_INFO_FMT(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LOG_INFO_FMT(
if (!token.payload_typed.tid.has_value())
{
return false;
}
LOG_INFO_FMT(

We check this condition at line 50, and in the return statement. I think we can just early-out here if there's no tenant ID specified, and simplify the later conditions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I guess this would need to go after the is_microsoft_entra check? I'll keep reading and work out how tid is populated.

Comment on lines +37 to +38
// limited.facebok.com
// .facebok.com
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// limited.facebok.com
// .facebok.com
// limited.facebook.com
// .facebook.com

Comment on lines +41 to +47
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be replaced with ends_with()?

Suggested change
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("." + constraint_domain);

auto keys = tx.rw<JwtPublicSigningKeys>(Tables::JWT_PUBLIC_SIGNING_KEYS);
auto key_issuer =
tx.rw<JwtPublicSigningKeyIssuer>(Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER);
auto keys =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep the original code as well here, to remove entries from the Legacy tables.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason we don't delete anything (here and in other places where you've commented) from the old tables in this revision is partially described here

#6175 (comment)

Example. Nodes=[A,B,C]

A->A' (upgrades)
A becomes / remains a leader
A fetches new keys and deletes them from both table revisions
A goes down
B becomes a leader

Now, if we clean both old issuers+certs, we just stop authenticating. If we leave kids only - we can't serve remove/set keys properly because kids and issuers went async.

We could've only introduced new issuers table, however we did introduce both new certs (matches the previous schema) and new issuers.

The reason for that was to keep issuers+cert synced for both prev/new code, and to support the scenario described above.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I understand and agree now - not worth peppering any remove() calls to legacy tables throughout the current code, because we don't want to try and precisely match the old write semantics. Instead we will deal with the old legacy state by adding a clear() call to the legacy tables, in a future release.

value.constraint = it->second;
}

LOG_INFO_FMT(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LOG_INFO_FMT(
LOG_DEBUG_FMT(

I think INFO is too verbose for this, and will lead to a lot of logging spam in the output.

src/node/rpc/jwt_management.h Show resolved Hide resolved
else
{
auto identity = std::make_unique<JwtAuthnIdentity>();
identity->key_issuer = key_issuers->get(key_id).value();
identity->key_issuer = validated_issuer;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never use the Legacy::JWT_PUBLIC_SIGNING_KEY_ISSUER table, which means (if I'm reading right) that this validated_issuer string will still be empty, if issuers is empty and we went through the fallback_keys path? Can we add a path in the fallback case above, something like:

if (!issuers)
{
  auto fallback_issuer = tx.ro<...>(ccf::Tables::Legacy::JWT_PUBLIC_SIGNING_KEY_ISSUER);
  validated_issuer = fallback_issuer->get(key_id).value_or({});
}

Or maybe we set issuers to a single-entry vector containing the value from the legacy map, and go through this validation path? Whatever makes sense, but I think we should try harder to use the fallback values if we're doing any fallback handling here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants