зеркало из https://github.com/microsoft/CCF.git
865 строки
31 KiB
Python
865 строки
31 KiB
Python
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
# Licensed under the Apache 2.0 License.
|
|
import os
|
|
import tempfile
|
|
import json
|
|
import time
|
|
import infra.network
|
|
import infra.path
|
|
import infra.proc
|
|
import infra.net
|
|
import infra.crypto
|
|
import infra.e2e_args
|
|
import infra.proposal
|
|
import suite.test_requirements as reqs
|
|
import infra.jwt_issuer
|
|
from infra.runner import ConcurrentRunner
|
|
import ca_certs
|
|
import ccf.ledger
|
|
from ccf.tx_id import TxID
|
|
import infra.clients
|
|
import http
|
|
|
|
from loguru import logger as LOG
|
|
|
|
|
|
def get_jwt_issuers(args, node):
|
|
with node.api_versioned_client(api_version=args.gov_api_version) as c:
|
|
r = c.get("/gov/service/jwk")
|
|
assert r.status_code == http.HTTPStatus.OK, r
|
|
body = r.body.json()
|
|
return body["issuers"]
|
|
|
|
|
|
def get_jwt_keys(args, node):
|
|
with node.api_versioned_client(api_version=args.gov_api_version) as c:
|
|
r = c.get("/gov/service/jwk")
|
|
assert r.status_code == http.HTTPStatus.OK, r
|
|
body = r.body.json()
|
|
return body["keys"]
|
|
|
|
|
|
def set_issuer_with_keys(network, primary, issuer, kids):
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump({"issuer": issuer.name}, metadata_fp)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
json.dump(issuer.create_jwks_for_kids(kids), jwks_fp)
|
|
jwks_fp.flush()
|
|
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, issuer.name, jwks_fp.name
|
|
)
|
|
|
|
|
|
@reqs.description("Refresh JWT issuer")
|
|
def test_refresh_jwt_issuer(network, args):
|
|
assert network.jwt_issuer.server, "JWT server is not started"
|
|
network.jwt_issuer.refresh_keys()
|
|
network.jwt_issuer.wait_for_refresh(network, args)
|
|
|
|
# Check that more transactions can be issued
|
|
network.txs.issue(network)
|
|
return network
|
|
|
|
|
|
@reqs.description("Multiple JWT issuers can't share same kid different pem")
|
|
def test_jwt_mulitple_issuers_same_kids_different_pem(network, args):
|
|
primary, _ = network.find_nodes()
|
|
|
|
issuer1 = infra.jwt_issuer.JwtIssuer("https://example.issuer1")
|
|
issuer2 = infra.jwt_issuer.JwtIssuer("https://example.issuer2")
|
|
|
|
set_issuer_with_keys(network, primary, issuer1, ["kid1"])
|
|
set_issuer_with_keys(network, primary, issuer2, ["kid1"])
|
|
|
|
network.consortium.remove_jwt_issuer(primary, issuer1.name)
|
|
network.consortium.remove_jwt_issuer(primary, issuer2.name)
|
|
|
|
|
|
@reqs.description("Multiple JWT issuers can share same kid same pem")
|
|
def test_jwt_mulitple_issuers_same_kids_same_pem(network, args):
|
|
primary, _ = network.find_nodes()
|
|
|
|
issuer1 = infra.jwt_issuer.JwtIssuer("https://example.issuer1")
|
|
|
|
issuer2 = infra.jwt_issuer.JwtIssuer("https://example.issuer2")
|
|
issuer2.cert_pem = issuer1.cert_pem
|
|
|
|
set_issuer_with_keys(network, primary, issuer1, ["kid1"])
|
|
set_issuer_with_keys(network, primary, issuer2, ["kid1"])
|
|
|
|
network.consortium.remove_jwt_issuer(primary, issuer1.name)
|
|
network.consortium.remove_jwt_issuer(primary, issuer2.name)
|
|
|
|
|
|
@reqs.description("Issuer constraint gets overwritten properly for same issuer+kid")
|
|
def test_jwt_same_issuer_constraint_overwritten(network, args):
|
|
primary, _ = network.find_nodes()
|
|
|
|
issuer = infra.jwt_issuer.JwtIssuer("https://example.issuer")
|
|
keys = issuer.create_jwks_for_kids(["kid1"])
|
|
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump({"issuer": issuer.name}, metadata_fp)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
json.dump(keys, jwks_fp)
|
|
jwks_fp.flush()
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, issuer.name, jwks_fp.name
|
|
)
|
|
|
|
service_keys = get_jwt_keys(args, primary)
|
|
assert service_keys["kid1"][0]["constraint"] == issuer.name
|
|
|
|
new_constraint = "https://example.issuer/very/specific"
|
|
keys["keys"][0]["issuer"] = new_constraint
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
json.dump(keys, jwks_fp)
|
|
jwks_fp.flush()
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, issuer.name, jwks_fp.name
|
|
)
|
|
|
|
service_keys = get_jwt_keys(args, primary)
|
|
assert service_keys["kid1"][0]["constraint"] == new_constraint
|
|
|
|
network.consortium.remove_jwt_issuer(primary, issuer.name)
|
|
|
|
|
|
@reqs.description("Only able to set keys with issuer constraints matching the url")
|
|
def test_jwt_issuer_domain_match(network, args):
|
|
"""Check domains match. Additional subdomains permitted. For example, https://limited.facebook.com
|
|
may provide keys with issuer constraint https://facebook.com."""
|
|
|
|
primary, _ = network.find_nodes()
|
|
|
|
issuer = infra.jwt_issuer.JwtIssuer("https://trusted.issuer.com/something")
|
|
keys = issuer.create_jwks_for_kids(["kid1"])
|
|
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump({"issuer": issuer.name}, metadata_fp)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
json.dump(keys, jwks_fp)
|
|
jwks_fp.flush()
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, issuer.name, jwks_fp.name
|
|
)
|
|
|
|
service_keys = get_jwt_keys(args, primary)
|
|
assert service_keys["kid1"][0]["issuer"] == issuer.name
|
|
|
|
keys["keys"][0]["issuer"] = "https://issuer.com"
|
|
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
json.dump(keys, jwks_fp)
|
|
jwks_fp.flush()
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, issuer.name, jwks_fp.name
|
|
)
|
|
|
|
garbage = ["", "garbage", "https://another.com", "https://issuer.com.domain"]
|
|
for constraint in garbage:
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
keys["keys"][0]["issuer"] = constraint
|
|
json.dump(keys, jwks_fp)
|
|
jwks_fp.flush()
|
|
try:
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, issuer.name, jwks_fp.name
|
|
)
|
|
except infra.proposal.ProposalNotAccepted:
|
|
pass
|
|
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):
|
|
primary, _ = network.find_nodes()
|
|
|
|
keys = {
|
|
infra.jwt_issuer.JwtIssuer("https://example.issuer1"): [
|
|
"issuer1_kid1",
|
|
"issuer1_kid2",
|
|
],
|
|
infra.jwt_issuer.JwtIssuer("https://example.issuer2"): [
|
|
"issuer2_kid1",
|
|
"issuer2_kid2",
|
|
],
|
|
}
|
|
|
|
LOG.info("Register JWT issuer with multiple kids")
|
|
for issuer, kids in keys.items():
|
|
set_issuer_with_keys(network, primary, issuer, kids)
|
|
|
|
LOG.info("Check that JWT endpoint returns all keys and issuers")
|
|
service_issuers = get_jwt_issuers(args, primary)
|
|
service_keys = get_jwt_keys(args, primary)
|
|
|
|
for issuer, kids in keys.items():
|
|
assert issuer.name in service_issuers, service_issuers
|
|
for kid in kids:
|
|
assert kid in service_keys, service_keys
|
|
assert service_keys[kid][0]["issuer"] == issuer.name
|
|
assert service_keys[kid][0]["constraint"] == issuer.name
|
|
assert service_keys[kid][0]["certificate"] == issuer.cert_pem
|
|
|
|
|
|
@reqs.description("JWT without key policy")
|
|
def test_jwt_without_key_policy(network, args):
|
|
primary, _ = network.find_nodes()
|
|
|
|
issuer = infra.jwt_issuer.JwtIssuer("https://example.issuer")
|
|
kid = "my_kid_not_key_policy"
|
|
|
|
network.consortium.remove_jwt_issuer(primary, issuer.name)
|
|
|
|
LOG.info("Try to add JWT signing key without matching issuer")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
json.dump(issuer.create_jwks(kid), jwks_fp)
|
|
jwks_fp.flush()
|
|
try:
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, issuer.name, jwks_fp.name
|
|
)
|
|
except infra.proposal.ProposalNotAccepted:
|
|
pass
|
|
else:
|
|
assert False, "Proposal should not have been created"
|
|
|
|
LOG.info("Add JWT issuer")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump({"issuer": issuer.name}, metadata_fp)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
|
|
LOG.info("Try to add a public key instead of a certificate")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
json.dump(issuer.create_jwks(kid, test_invalid_is_key=True), jwks_fp)
|
|
jwks_fp.flush()
|
|
try:
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, issuer.name, jwks_fp.name
|
|
)
|
|
except (infra.proposal.ProposalNotAccepted, infra.proposal.ProposalNotCreated):
|
|
pass
|
|
else:
|
|
assert False, "Proposal should not have been created"
|
|
|
|
LOG.info("Add JWT signing key with matching issuer")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
json.dump(issuer.create_jwks(kid), jwks_fp)
|
|
jwks_fp.flush()
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, issuer.name, jwks_fp.name
|
|
)
|
|
|
|
keys = get_jwt_keys(args, primary)
|
|
stored_cert = keys[kid][0]["certificate"]
|
|
|
|
assert stored_cert == issuer.cert_pem, "input cert is not equal to stored cert"
|
|
|
|
LOG.info("Remove JWT issuer")
|
|
network.consortium.remove_jwt_issuer(primary, issuer.name)
|
|
|
|
keys = get_jwt_keys(args, primary)
|
|
assert (
|
|
kid not in keys
|
|
), f"JWT key associated with issuer {issuer.name} was not removed: {keys[kid]}"
|
|
|
|
LOG.info("Add JWT issuer with initial keys")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump({"issuer": issuer.name, "jwks": issuer.create_jwks(kid)}, metadata_fp)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
|
|
keys = get_jwt_keys(args, primary)
|
|
stored_cert = keys[kid][0]["certificate"]
|
|
|
|
assert stored_cert == issuer.cert_pem, "input cert is not equal to stored cert"
|
|
|
|
return network
|
|
|
|
|
|
def make_attested_cert(network, args):
|
|
keygen = os.path.join(args.binary_dir, "keygenerator.sh")
|
|
oeutil = os.path.join(args.oe_binary, "oeutil")
|
|
infra.proc.ccall(
|
|
keygen, "--name", "attested", "--gen-enc-key", path=network.common_dir
|
|
).check_returncode()
|
|
privk = os.path.join(network.common_dir, "attested_enc_privk.pem")
|
|
pubk = os.path.join(network.common_dir, "attested_enc_pubk.pem")
|
|
der = os.path.join(network.common_dir, "oe_cert.der")
|
|
infra.proc.ccall(
|
|
oeutil,
|
|
"generate-evidence",
|
|
"-f",
|
|
"cert",
|
|
privk,
|
|
pubk,
|
|
"-o",
|
|
der,
|
|
# To ensure in-process attestation is always used, clear the env to unset the SGX_AESM_ADDR variable
|
|
env={},
|
|
).check_returncode()
|
|
pem = os.path.join(network.common_dir, "oe_cert.pem")
|
|
infra.proc.ccall(
|
|
"openssl", "x509", "-inform", "der", "-in", der, "-out", pem
|
|
).check_returncode()
|
|
return pem
|
|
|
|
|
|
@reqs.description("JWT with SGX key policy")
|
|
def test_jwt_with_sgx_key_policy(network, args):
|
|
primary, _ = network.find_nodes()
|
|
oe_cert_path = make_attested_cert(network, args)
|
|
|
|
with open(oe_cert_path, encoding="utf-8") as f:
|
|
oe_cert_pem = f.read()
|
|
|
|
kid = "my_kid_with_policy"
|
|
issuer = infra.jwt_issuer.JwtIssuer(
|
|
"https://example.issuer_with_policy", oe_cert_pem
|
|
)
|
|
|
|
oesign = os.path.join(args.oe_binary, "oesign")
|
|
oeutil_enc = os.path.join(args.oe_binary, "oeutil_enc.signed")
|
|
sc = infra.proc.ccall(
|
|
oesign,
|
|
"dump",
|
|
"-e",
|
|
oeutil_enc,
|
|
)
|
|
sc.check_returncode()
|
|
lines = sc.stdout.decode().split()
|
|
for line in lines:
|
|
if line.startswith("mrsigner="):
|
|
mrsigner = line.strip().split("=")[1]
|
|
break
|
|
else:
|
|
assert False, f"Could not find mrsigner in {lines}"
|
|
|
|
matching_key_policy = {
|
|
"sgx_claims": {
|
|
"signer_id": mrsigner,
|
|
"attributes": "0300000000000000",
|
|
}
|
|
}
|
|
|
|
mismatching_key_policy = {
|
|
"sgx_claims": {
|
|
"signer_id": "da9ad7331448980aa28890ce73e433638377f179ab4456b2fe237193193a8d0a",
|
|
"attributes": "0300000000000000",
|
|
}
|
|
}
|
|
|
|
LOG.info("Add JWT issuer with SGX key policy")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump(
|
|
{"issuer": issuer.name, "key_policy": matching_key_policy}, metadata_fp
|
|
)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
|
|
LOG.info("Try to add a non-OE-attested cert")
|
|
non_oe_issuer_name = "non_oe_issuer"
|
|
non_oe_issuer = infra.jwt_issuer.JwtIssuer(non_oe_issuer_name)
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
json.dump(non_oe_issuer.create_jwks(kid), jwks_fp)
|
|
jwks_fp.flush()
|
|
try:
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, non_oe_issuer_name, jwks_fp.name
|
|
)
|
|
except infra.proposal.ProposalNotAccepted:
|
|
pass
|
|
else:
|
|
assert False, "Proposal should not have been created"
|
|
|
|
LOG.info("Add an OE-attested cert with matching claims")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
json.dump(issuer.create_jwks(kid), jwks_fp)
|
|
jwks_fp.flush()
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, issuer.name, jwks_fp.name
|
|
)
|
|
|
|
LOG.info("Update JWT issuer with mismatching SGX key policy")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump(
|
|
{
|
|
"issuer": issuer.name,
|
|
"key_policy": mismatching_key_policy,
|
|
},
|
|
metadata_fp,
|
|
)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
|
|
LOG.info("Try to add an OE-attested cert with mismatching claims")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
json.dump(non_oe_issuer.create_jwks(kid), jwks_fp)
|
|
jwks_fp.flush()
|
|
try:
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, non_oe_issuer_name, jwks_fp.name
|
|
)
|
|
except infra.proposal.ProposalNotAccepted:
|
|
pass
|
|
else:
|
|
assert False, "Proposal should not have been created"
|
|
|
|
return network
|
|
|
|
|
|
@reqs.description("JWT with SGX key filter")
|
|
def test_jwt_with_sgx_key_filter(network, args):
|
|
primary, _ = network.find_nodes()
|
|
|
|
oe_cert_path = make_attested_cert(network, args)
|
|
with open(oe_cert_path, encoding="utf-8") as f:
|
|
oe_cert_pem = f.read()
|
|
|
|
oe_issuer = infra.jwt_issuer.JwtIssuer("https://example.oe_issuer", oe_cert_pem)
|
|
non_oe_issuer = infra.jwt_issuer.JwtIssuer("https://example.non_oe_issuer")
|
|
|
|
oe_kid = "oe_kid"
|
|
non_oe_kid = "non_oe_kid"
|
|
|
|
LOG.info("Add JWT issuer with SGX key filter")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump({"issuer": oe_issuer.name, "key_filter": "sgx"}, metadata_fp)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
|
|
LOG.info("Add multiple certs (1 SGX, 1 non-SGX)")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
|
oe_jwks = oe_issuer.create_jwks(oe_kid)
|
|
non_oe_jwks = non_oe_issuer.create_jwks(non_oe_kid)
|
|
jwks = {"keys": non_oe_jwks["keys"] + oe_jwks["keys"]}
|
|
json.dump(jwks, jwks_fp)
|
|
jwks_fp.flush()
|
|
network.consortium.set_jwt_public_signing_keys(
|
|
primary, oe_issuer.name, jwks_fp.name
|
|
)
|
|
|
|
stored_jwt_signing_keys = get_jwt_keys(args, primary)
|
|
assert non_oe_kid not in stored_jwt_signing_keys, stored_jwt_signing_keys
|
|
assert oe_kid in stored_jwt_signing_keys, stored_jwt_signing_keys
|
|
|
|
return network
|
|
|
|
|
|
def check_kv_jwt_key_matches(args, network, kid, cert_pem):
|
|
primary, _ = network.find_nodes()
|
|
latest_jwt_signing_keys = get_jwt_keys(args, primary)
|
|
|
|
if cert_pem is None:
|
|
assert kid not in latest_jwt_signing_keys
|
|
else:
|
|
# Necessary to get an AssertionError if the key is not found yet,
|
|
# when used from with_timeout()
|
|
assert kid in latest_jwt_signing_keys
|
|
stored_cert = latest_jwt_signing_keys[kid][0]["certificate"]
|
|
assert stored_cert == cert_pem, "input cert is not equal to stored cert"
|
|
|
|
|
|
def check_kv_jwt_keys_not_empty(args, network, issuer):
|
|
primary, _ = network.find_nodes()
|
|
latest_jwt_signing_keys = get_jwt_keys(args, primary)
|
|
|
|
for _, data in latest_jwt_signing_keys.items():
|
|
for key in data:
|
|
if key["issuer"] == issuer:
|
|
return
|
|
|
|
assert False, "No keys for issuer"
|
|
|
|
|
|
def get_jwt_refresh_endpoint_metrics(primary) -> dict:
|
|
# Note that these metrics are local to a node. So if the primary changes, or
|
|
# a different node has processed jwt_keys/refresh, you may not see the values
|
|
# you expect
|
|
with primary.client() as c:
|
|
r = c.get("/node/jwt_keys/refresh/metrics")
|
|
assert r.status_code == 200, r
|
|
return r.body.json()
|
|
|
|
|
|
@reqs.description("JWT with auto_refresh enabled")
|
|
def test_jwt_key_auto_refresh(network, args):
|
|
primary, _ = network.find_nodes()
|
|
|
|
ca_cert_bundle_name = "jwt"
|
|
kid = "the_kid"
|
|
issuer_host = "localhost"
|
|
issuer_port = args.issuer_port
|
|
|
|
issuer = infra.jwt_issuer.JwtIssuer(
|
|
f"https://{issuer_host}:{issuer_port}", cn=issuer_host
|
|
)
|
|
|
|
LOG.info("Add CA cert for JWT issuer")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as ca_cert_bundle_fp:
|
|
ca_cert_bundle_fp.write(issuer.tls_cert)
|
|
ca_cert_bundle_fp.flush()
|
|
network.consortium.set_ca_cert_bundle(
|
|
primary, ca_cert_bundle_name, ca_cert_bundle_fp.name
|
|
)
|
|
|
|
LOG.info("Start OpenID endpoint server")
|
|
with issuer.start_openid_server(issuer_port, kid) as server:
|
|
# Send oversized headers with the payload that will cause the CCF client to
|
|
# fail parsing and log an error.
|
|
server.inject_oversized_header = True
|
|
req_count = server.request_count
|
|
LOG.info("Add JWT issuer with auto-refresh")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump(
|
|
{
|
|
"issuer": issuer.name,
|
|
"auto_refresh": True,
|
|
"ca_cert_bundle_name": ca_cert_bundle_name,
|
|
},
|
|
metadata_fp,
|
|
)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
# Make sure we did serve at least one request with oversized headers to CCF before
|
|
# reverting to normal headers.
|
|
assert (
|
|
server.request_count > req_count
|
|
), "No request was served with oversized headers"
|
|
server.inject_oversized_header = False
|
|
|
|
LOG.info("Check that keys got refreshed")
|
|
# Note: refresh interval is set to 1s, see network args below.
|
|
with_timeout(
|
|
lambda: check_kv_jwt_key_matches(args, network, kid, issuer.cert_pem),
|
|
timeout=5,
|
|
)
|
|
|
|
LOG.info("Check that JWT refresh has attempts and successes and no failures")
|
|
m = get_jwt_refresh_endpoint_metrics(primary)
|
|
assert m["attempts"] > 0, m
|
|
assert m["successes"] > 0, m
|
|
assert m["failures"] == 0, m
|
|
|
|
LOG.info("Serve invalid JWKS")
|
|
server.jwks = {"foo": "bar"}
|
|
|
|
LOG.info("Check that JWT refresh endpoint has some failures")
|
|
|
|
def check_has_failures():
|
|
m = get_jwt_refresh_endpoint_metrics(primary)
|
|
assert m["failures"] > 0, m
|
|
|
|
with_timeout(check_has_failures, timeout=5)
|
|
|
|
LOG.info("Check that JWT refresh has fewer successes than attempts")
|
|
m = get_jwt_refresh_endpoint_metrics(primary)
|
|
assert m["attempts"] > m["successes"], m
|
|
|
|
LOG.info("Restart OpenID endpoint server with new keys")
|
|
kid2 = "the_kid_2"
|
|
issuer.refresh_keys(kid2)
|
|
with issuer.start_openid_server(issuer_port, kid2):
|
|
LOG.info("Check that keys got refreshed")
|
|
with_timeout(
|
|
lambda: check_kv_jwt_key_matches(args, network, kid, None), timeout=5
|
|
)
|
|
check_kv_jwt_key_matches(args, network, kid2, issuer.cert_pem)
|
|
|
|
return network
|
|
|
|
|
|
@reqs.description("JWT with auto_refresh enabled, check for duplicate entries")
|
|
def test_jwt_key_auto_refresh_entries(network, args):
|
|
primary, _ = network.find_nodes()
|
|
|
|
ca_cert_bundle_name = "jwt"
|
|
kid = "the_kid_no_duplicates"
|
|
issuer_host = "localhost"
|
|
issuer_port = args.issuer_port
|
|
|
|
issuer = infra.jwt_issuer.JwtIssuer(
|
|
f"https://{issuer_host}:{issuer_port}", cn=issuer_host
|
|
)
|
|
|
|
LOG.info("Add CA cert for JWT issuer")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as ca_cert_bundle_fp:
|
|
ca_cert_bundle_fp.write(issuer.tls_cert)
|
|
ca_cert_bundle_fp.flush()
|
|
network.consortium.set_ca_cert_bundle(
|
|
primary, ca_cert_bundle_name, ca_cert_bundle_fp.name
|
|
)
|
|
|
|
LOG.info("Start OpenID endpoint server")
|
|
with issuer.start_openid_server(issuer_port, kid):
|
|
LOG.info("Add JWT issuer with auto-refresh")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump(
|
|
{
|
|
"issuer": issuer.name,
|
|
"auto_refresh": True,
|
|
"ca_cert_bundle_name": ca_cert_bundle_name,
|
|
},
|
|
metadata_fp,
|
|
)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
|
|
LOG.info("Check that keys got refreshed")
|
|
# Note: refresh interval is set to 1s, see network args below.
|
|
with_timeout(
|
|
lambda: check_kv_jwt_key_matches(args, network, kid, issuer.cert_pem),
|
|
timeout=5,
|
|
)
|
|
|
|
LOG.info("Check that JWT refresh has attempts and successes")
|
|
m = get_jwt_refresh_endpoint_metrics(primary)
|
|
attempts = m["attempts"]
|
|
successes = m["successes"]
|
|
assert attempts > 0, attempts
|
|
assert successes > 0, successes
|
|
|
|
# Wait long enough for at least one refresh to take place
|
|
time.sleep(args.jwt_key_refresh_interval_s)
|
|
|
|
m = get_jwt_refresh_endpoint_metrics(primary)
|
|
assert m["attempts"] > attempts, m["attempts"]
|
|
assert m["successes"] > successes, m["successes"]
|
|
|
|
# Force chunking
|
|
network.get_latest_ledger_public_state()
|
|
# Check that despite refreshing JWTs multiple times, only a single
|
|
# transaction was created for this kid.
|
|
ledger_directories = primary.remote.ledger_paths()
|
|
ledger = ccf.ledger.Ledger(ledger_directories)
|
|
|
|
last_key_refresh = None
|
|
for chunk in ledger:
|
|
for tx in chunk:
|
|
txid = TxID(tx.gcm_header.view, tx.gcm_header.seqno)
|
|
tables = tx.get_public_domain().get_tables()
|
|
if "public:ccf.gov.jwt.public_signing_keys_metadata" in tables:
|
|
pub_keys = tables["public:ccf.gov.jwt.public_signing_keys_metadata"]
|
|
if kid.encode() in pub_keys:
|
|
if last_key_refresh is None:
|
|
LOG.info(f"Refresh found for kid: {kid} at {txid}")
|
|
last_key_refresh = txid
|
|
else:
|
|
assert (
|
|
last_key_refresh == txid
|
|
), "Duplicate JWT refresh transaction"
|
|
assert last_key_refresh, "Missing JWT refresh transaction"
|
|
|
|
return network
|
|
|
|
|
|
@reqs.description("JWT with auto_refresh enabled, initial refresh")
|
|
def test_jwt_key_initial_refresh(network, args):
|
|
primary, _ = network.find_nodes()
|
|
|
|
ca_cert_bundle_name = "jwt"
|
|
kid = f"my_kid_autorefresh_{primary.local_node_id}"
|
|
issuer_host = "localhost"
|
|
issuer_port = args.issuer_port
|
|
|
|
issuer = infra.jwt_issuer.JwtIssuer(
|
|
f"https://{issuer_host}:{issuer_port}", cn=issuer_host
|
|
)
|
|
|
|
LOG.info("Add CA cert for JWT issuer")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as ca_cert_bundle_fp:
|
|
ca_cert_bundle_fp.write(issuer.tls_cert)
|
|
ca_cert_bundle_fp.flush()
|
|
network.consortium.set_ca_cert_bundle(
|
|
primary, ca_cert_bundle_name, ca_cert_bundle_fp.name
|
|
)
|
|
|
|
LOG.info("Start OpenID endpoint server")
|
|
with issuer.start_openid_server(issuer_port, kid):
|
|
LOG.info("Add JWT issuer with auto-refresh")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump(
|
|
{
|
|
"issuer": issuer.name,
|
|
"auto_refresh": True,
|
|
"ca_cert_bundle_name": ca_cert_bundle_name,
|
|
},
|
|
metadata_fp,
|
|
)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
|
|
LOG.info("Check that keys got refreshed")
|
|
# Auto-refresh interval has been set to a large value so that it doesn't happen within the timeout.
|
|
# This is testing the one-off refresh after adding a new issuer.
|
|
with_timeout(
|
|
lambda: check_kv_jwt_key_matches(args, network, kid, issuer.cert_pem),
|
|
timeout=5,
|
|
)
|
|
|
|
LOG.info("Check that JWT refresh endpoint has no failures")
|
|
m = get_jwt_refresh_endpoint_metrics(primary)
|
|
assert m["failures"] == 0, m["failures"]
|
|
assert m["successes"] > 0, m["successes"]
|
|
|
|
return network
|
|
|
|
|
|
# Root CA for login.microsoftonline.com:443
|
|
# Used as root of trust by CCF (after being set via set_ca_cert_bundle)
|
|
# for the purpose of fetching JWK list and JWKs
|
|
DIGICERT_GLOBAL_ROOT_CA = """-----BEGIN CERTIFICATE-----
|
|
MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
|
|
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
|
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
|
|
QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
|
|
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
|
|
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
|
|
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
|
|
CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
|
|
nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
|
|
43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
|
|
T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
|
|
gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
|
|
BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
|
|
TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
|
|
DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
|
|
hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
|
|
06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
|
|
PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
|
|
YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
|
|
CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
|
|
-----END CERTIFICATE-----"""
|
|
|
|
|
|
def test_jwt_key_refresh_aad(network, args):
|
|
primary, _ = network.find_nodes()
|
|
|
|
LOG.info("Add CA cert for Entra JWT issuer")
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as ca_cert_bundle_fp:
|
|
ca_cert_bundle_fp.write(DIGICERT_GLOBAL_ROOT_CA)
|
|
ca_cert_bundle_fp.flush()
|
|
network.consortium.set_ca_cert_bundle(primary, "aad", ca_cert_bundle_fp.name)
|
|
|
|
issuer = "https://login.microsoftonline.com/common/v2.0/"
|
|
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
|
json.dump(
|
|
{
|
|
"issuer": issuer,
|
|
"auto_refresh": True,
|
|
"ca_cert_bundle_name": "aad",
|
|
},
|
|
metadata_fp,
|
|
)
|
|
metadata_fp.flush()
|
|
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
|
|
|
LOG.info("Check that keys got refreshed")
|
|
with_timeout(lambda: check_kv_jwt_keys_not_empty(args, network, issuer), timeout=5)
|
|
|
|
|
|
def with_timeout(fn, timeout):
|
|
t0 = time.time()
|
|
while True:
|
|
try:
|
|
return fn()
|
|
except (TimeoutError, AssertionError):
|
|
if time.time() - t0 < timeout:
|
|
time.sleep(0.1)
|
|
else:
|
|
raise
|
|
|
|
|
|
def run_auto(args):
|
|
with infra.network.network(
|
|
args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb
|
|
) as network:
|
|
network.start_and_open(args)
|
|
test_jwt_mulitple_issuers_same_kids_different_pem(network, args)
|
|
test_jwt_mulitple_issuers_same_kids_same_pem(network, args)
|
|
test_jwt_same_issuer_constraint_overwritten(network, args)
|
|
test_jwt_issuer_domain_match(network, args)
|
|
test_jwt_endpoint(network, args)
|
|
test_jwt_without_key_policy(network, args)
|
|
if args.enclave_platform == "sgx":
|
|
test_jwt_with_sgx_key_policy(network, args)
|
|
test_jwt_with_sgx_key_filter(network, args)
|
|
test_jwt_key_auto_refresh(network, args)
|
|
|
|
# Check that auto refresh also works on backups
|
|
primary, _ = network.find_primary()
|
|
primary.stop()
|
|
network.wait_for_new_primary(primary)
|
|
test_jwt_key_auto_refresh(network, args)
|
|
# Check that we can refresh keys for AAD endpoint
|
|
test_jwt_key_refresh_aad(network, args)
|
|
test_jwt_key_auto_refresh_entries(network, args)
|
|
|
|
|
|
def run_manual(args):
|
|
with infra.network.network(
|
|
args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb
|
|
) as network:
|
|
network.start_and_open(args)
|
|
test_jwt_key_initial_refresh(network, args)
|
|
|
|
# Check that initial refresh also works on backups
|
|
primary, _ = network.find_primary()
|
|
primary.stop()
|
|
network.wait_for_new_primary(primary)
|
|
test_jwt_key_initial_refresh(network, args)
|
|
|
|
|
|
def run_ca_cert(args):
|
|
with infra.network.network(
|
|
args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb
|
|
) as network:
|
|
network.start_and_open(args)
|
|
ca_certs.test_cert_store(network, args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cr = ConcurrentRunner()
|
|
|
|
cr.add(
|
|
"auto",
|
|
run_auto,
|
|
package="samples/apps/logging/liblogging",
|
|
nodes=infra.e2e_args.min_nodes(cr.args, f=1),
|
|
jwt_key_refresh_interval_s=1,
|
|
issuer_port=12345,
|
|
)
|
|
|
|
cr.add(
|
|
"manual",
|
|
run_manual,
|
|
package="samples/apps/logging/liblogging",
|
|
nodes=infra.e2e_args.min_nodes(cr.args, f=1),
|
|
jwt_key_refresh_interval_s=100000,
|
|
issuer_port=12346,
|
|
)
|
|
|
|
cr.add(
|
|
"ca_cert",
|
|
run_ca_cert,
|
|
package="samples/apps/logging/liblogging",
|
|
nodes=infra.e2e_args.max_nodes(cr.args, f=0),
|
|
)
|
|
|
|
cr.run()
|