Skip to content

Commit 0458b02

Browse files
authored
Support CA cert bundles (#2222)
1 parent c8aa7a5 commit 0458b02

File tree

12 files changed

+120
-91
lines changed

12 files changed

+120
-91
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [0.18.4]
9+
10+
### Changed
11+
12+
- `set_ca_cert`/`remove_ca_cert` proposals have been renamed `set_ca_cert_bundle`/`remove_ca_cert_bundle` and now also accept a bundle of certificates encoded as concatenated PEM string (#2221).
13+
814
## [0.18.3]
915

1016
### Changed
@@ -694,6 +700,7 @@ Some discrepancies with the TR remain, and are being tracked under https://githu
694700

695701
Initial pre-release
696702

703+
[0.18.4]: https://github.com/microsoft/CCF/releases/tag/ccf-0.18.4
697704
[0.18.3]: https://github.com/microsoft/CCF/releases/tag/ccf-0.18.3
698705
[0.18.2]: https://github.com/microsoft/CCF/releases/tag/ccf-0.18.2
699706
[0.18.1]: https://github.com/microsoft/CCF/releases/tag/ccf-0.18.1

python/ccf/proposal_generator.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -419,25 +419,30 @@ def set_recovery_threshold(threshold: int, **kwargs):
419419

420420

421421
@cli_proposal
422-
def set_ca_cert(cert_name, cert_path, skip_checks=False, **kwargs):
423-
with open(cert_path) as f:
424-
cert_pem = f.read()
422+
def set_ca_cert_bundle(cert_bundle_name, cert_bundle_path, skip_checks=False, **kwargs):
423+
with open(cert_bundle_path) as f:
424+
cert_bundle_pem = f.read()
425425

426426
if not skip_checks:
427-
try:
428-
x509.load_pem_x509_certificate(
429-
cert_pem.encode(), crypto_backends.default_backend()
430-
)
431-
except Exception as exc:
432-
raise ValueError("Cannot parse PEM certificate") from exc
427+
delim = "-----END CERTIFICATE-----"
428+
for cert_pem in cert_bundle_pem.split(delim):
429+
if not cert_pem.strip():
430+
continue
431+
cert_pem += delim
432+
try:
433+
x509.load_pem_x509_certificate(
434+
cert_pem.encode(), crypto_backends.default_backend()
435+
)
436+
except Exception as exc:
437+
raise ValueError("Cannot parse PEM certificate") from exc
433438

434-
args = {"name": cert_name, "cert": cert_pem}
435-
return build_proposal("set_ca_cert", args, **kwargs)
439+
args = {"name": cert_bundle_name, "cert_bundle": cert_bundle_pem}
440+
return build_proposal("set_ca_cert_bundle", args, **kwargs)
436441

437442

438443
@cli_proposal
439-
def remove_ca_cert(cert_name, **kwargs):
440-
return build_proposal("remove_ca_cert", cert_name, **kwargs)
444+
def remove_ca_cert_bundle(cert_bundle_name, **kwargs):
445+
return build_proposal("remove_ca_cert_bundle", cert_bundle_name, **kwargs)
441446

442447

443448
@cli_proposal
@@ -448,7 +453,7 @@ def set_jwt_issuer(json_path: str, **kwargs):
448453
"issuer": obj["issuer"],
449454
"key_filter": obj.get("key_filter", "all"),
450455
"key_policy": obj.get("key_policy"),
451-
"ca_cert_name": obj.get("ca_cert_name"),
456+
"ca_cert_bundle_name": obj.get("ca_cert_bundle_name"),
452457
"auto_refresh": obj.get("auto_refresh", False),
453458
"jwks": obj.get("jwks"),
454459
}

src/node/certs.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
namespace ccf
77
{
88
using CertDERs = kv::Map<Cert, ObjectId>;
9-
using CACertDERs = kv::Map<std::string, Cert>;
9+
using CACertBundlePEMs = kv::Map<std::string, std::string>;
1010
// Mapping from hex-encoded digest of PEM cert to entity id
1111
using CertDigests = kv::Map<std::string, ObjectId>;
1212
}

src/node/entities.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,11 @@ namespace ccf
9696
static constexpr auto ENDPOINTS = "public:gov.endpoints";
9797
static constexpr auto SERVICE_PRINCIPALS = "public:gov.service_principals";
9898

99+
// TLS
100+
static constexpr auto CA_CERT_BUNDLE_PEMS =
101+
"public:ccf.gov.tls.ca_cert_bundles";
102+
99103
// JWT issuers
100-
static constexpr auto CA_CERT_DERS = "public:ccf.gov.jwt.ca_certs_der";
101104
static constexpr auto JWT_ISSUERS = "public:ccf.gov.jwt.issuers";
102105
static constexpr auto JWT_PUBLIC_SIGNING_KEYS =
103106
"public:ccf.gov.jwt.public_signing_keys";

src/node/jwt.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ namespace ccf
4141
{
4242
JwtIssuerKeyFilter key_filter;
4343
std::optional<JwtIssuerKeyPolicy> key_policy;
44-
std::optional<std::string> ca_cert_name;
44+
std::optional<std::string> ca_cert_bundle_name;
4545
bool auto_refresh = false;
4646

47-
MSGPACK_DEFINE(key_filter, key_policy, ca_cert_name, auto_refresh);
47+
MSGPACK_DEFINE(key_filter, key_policy, ca_cert_bundle_name, auto_refresh);
4848
};
4949

5050
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JwtIssuerMetadata);
5151
DECLARE_JSON_REQUIRED_FIELDS(JwtIssuerMetadata, key_filter);
5252
DECLARE_JSON_OPTIONAL_FIELDS(
53-
JwtIssuerMetadata, key_policy, ca_cert_name, auto_refresh);
53+
JwtIssuerMetadata, key_policy, ca_cert_bundle_name, auto_refresh);
5454

5555
using JwtIssuer = std::string;
5656
using JwtKeyId = std::string;

src/node/jwt_key_auto_refresh.h

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,8 @@ namespace ccf
281281
{
282282
auto tx = network.tables->create_read_only_tx();
283283
auto jwt_issuers = tx.ro(network.jwt_issuers);
284-
auto ca_certs = tx.ro(network.ca_certs);
285-
jwt_issuers->foreach([this, &ca_certs](
284+
auto ca_cert_bundles = tx.ro(network.ca_cert_bundles);
285+
jwt_issuers->foreach([this, &ca_cert_bundles](
286286
const JwtIssuer& issuer,
287287
const JwtIssuerMetadata& metadata) {
288288
if (!metadata.auto_refresh)
@@ -295,14 +295,15 @@ namespace ccf
295295
}
296296
LOG_DEBUG_FMT(
297297
"JWT key auto-refresh: Refreshing keys for issuer '{}'", issuer);
298-
auto& ca_cert_name = metadata.ca_cert_name.value();
299-
auto ca_cert_der = ca_certs->get(ca_cert_name);
300-
if (!ca_cert_der.has_value())
298+
auto& ca_cert_bundle_name = metadata.ca_cert_bundle_name.value();
299+
auto ca_cert_bundle_pem = ca_cert_bundles->get(ca_cert_bundle_name);
300+
if (!ca_cert_bundle_pem.has_value())
301301
{
302302
LOG_FAIL_FMT(
303-
"JWT key auto-refresh: CA cert with name '{}' for issuer '{}' not "
303+
"JWT key auto-refresh: CA cert bundle with name '{}' for issuer "
304+
"'{}' not "
304305
"found",
305-
ca_cert_name,
306+
ca_cert_bundle_name,
306307
issuer);
307308
send_refresh_jwt_keys_error();
308309
return true;
@@ -313,7 +314,7 @@ namespace ccf
313314
auto metadata_url_port =
314315
!metadata_url.port.empty() ? metadata_url.port : "443";
315316

316-
auto ca = std::make_shared<tls::CA>(ca_cert_der.value());
317+
auto ca = std::make_shared<tls::CA>(ca_cert_bundle_pem.value());
317318
auto ca_cert = std::make_shared<tls::Cert>(
318319
ca,
319320
std::nullopt,

src/node/network_tables.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ namespace ccf
7474
SubmittedShares submitted_shares;
7575
Configuration config;
7676

77-
CACertDERs ca_certs;
77+
CACertBundlePEMs ca_cert_bundles;
7878

7979
JwtIssuers jwt_issuers;
8080
JwtPublicSigningKeys jwt_public_signing_keys;
@@ -133,7 +133,7 @@ namespace ccf
133133
encrypted_ledger_secrets(Tables::ENCRYPTED_PAST_LEDGER_SECRET),
134134
submitted_shares(Tables::SUBMITTED_SHARES),
135135
config(Tables::CONFIGURATION),
136-
ca_certs(Tables::CA_CERT_DERS),
136+
ca_cert_bundles(Tables::CA_CERT_BUNDLE_PEMS),
137137
jwt_issuers(Tables::JWT_ISSUERS),
138138
jwt_public_signing_keys(Tables::JWT_PUBLIC_SIGNING_KEYS),
139139
jwt_public_signing_key_issuer(Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER),
@@ -171,7 +171,7 @@ namespace ccf
171171
std::ref(member_acks),
172172
std::ref(governance_history),
173173
std::ref(config),
174-
std::ref(ca_certs),
174+
std::ref(ca_cert_bundles),
175175
std::ref(jwt_issuers),
176176
std::ref(jwt_public_signing_keys),
177177
std::ref(jwt_public_signing_key_issuer),

src/node/rpc/member_frontend.h

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,13 @@ namespace ccf
171171
DECLARE_JSON_TYPE(SetJwtPublicSigningKeys)
172172
DECLARE_JSON_REQUIRED_FIELDS(SetJwtPublicSigningKeys, issuer, jwks)
173173

174-
struct SetCaCert
174+
struct SetCaCertBundle
175175
{
176176
std::string name;
177-
std::string cert;
177+
std::string cert_bundle;
178178
};
179-
DECLARE_JSON_TYPE(SetCaCert)
180-
DECLARE_JSON_REQUIRED_FIELDS(SetCaCert, name, cert)
179+
DECLARE_JSON_TYPE(SetCaCertBundle)
180+
DECLARE_JSON_REQUIRED_FIELDS(SetCaCertBundle, name, cert_bundle)
181181

182182
class MemberEndpoints : public CommonEndpointRegistry
183183
{
@@ -679,35 +679,35 @@ namespace ccf
679679
users->put(parsed.user_id, user_info.value());
680680
return true;
681681
}},
682-
{"set_ca_cert",
682+
{"set_ca_cert_bundle",
683683
[this](
684684
const ProposalId& proposal_id,
685685
kv::Tx& tx,
686686
const nlohmann::json& args) {
687-
const auto parsed = args.get<SetCaCert>();
688-
auto ca_certs = tx.rw(this->network.ca_certs);
689-
std::vector<uint8_t> cert_der;
687+
const auto parsed = args.get<SetCaCertBundle>();
688+
auto ca_cert_bundles = tx.rw(this->network.ca_cert_bundles);
690689
try
691690
{
692-
cert_der = crypto::cert_pem_to_der(parsed.cert);
691+
tls::CA(parsed.cert_bundle);
693692
}
694-
catch (const std::invalid_argument& e)
693+
catch (const std::logic_error& e)
695694
{
696695
LOG_FAIL_FMT(
697-
"Proposal {}: certificate is not a valid X.509 certificate in "
696+
"Proposal {}: 'cert_bundle' is not a valid X.509 certificate "
697+
"bundle in "
698698
"PEM format: {}",
699699
proposal_id,
700700
e.what());
701701
return false;
702702
}
703-
ca_certs->put(parsed.name, cert_der);
703+
ca_cert_bundles->put(parsed.name, parsed.cert_bundle);
704704
return true;
705705
}},
706-
{"remove_ca_cert",
706+
{"remove_ca_cert_bundle",
707707
[this](const ProposalId&, kv::Tx& tx, const nlohmann::json& args) {
708-
const auto cert_name = args.get<std::string>();
709-
auto ca_certs = tx.rw(this->network.ca_certs);
710-
ca_certs->remove(cert_name);
708+
const auto cert_bundle_name = args.get<std::string>();
709+
auto ca_cert_bundles = tx.rw(this->network.ca_cert_bundles);
710+
ca_cert_bundles->remove(cert_bundle_name);
711711
return true;
712712
}},
713713
{"set_jwt_issuer",
@@ -717,24 +717,24 @@ namespace ccf
717717
const nlohmann::json& args) {
718718
const auto parsed = args.get<SetJwtIssuer>();
719719
auto issuers = tx.rw(this->network.jwt_issuers);
720-
auto ca_certs = tx.ro(this->network.ca_certs);
720+
auto ca_cert_bundles = tx.ro(this->network.ca_cert_bundles);
721721

722722
if (parsed.auto_refresh)
723723
{
724-
if (!parsed.ca_cert_name.has_value())
724+
if (!parsed.ca_cert_bundle_name.has_value())
725725
{
726726
LOG_FAIL_FMT(
727-
"Proposal {}: ca_cert_name is missing but required if "
727+
"Proposal {}: ca_cert_bundle_name is missing but required if "
728728
"auto_refresh is true",
729729
proposal_id);
730730
return false;
731731
}
732-
if (!ca_certs->has(parsed.ca_cert_name.value()))
732+
if (!ca_cert_bundles->has(parsed.ca_cert_bundle_name.value()))
733733
{
734734
LOG_FAIL_FMT(
735-
"Proposal {}: No CA cert found with name '{}'",
735+
"Proposal {}: No CA cert list found with name '{}'",
736736
proposal_id,
737-
parsed.ca_cert_name.value());
737+
parsed.ca_cert_bundle_name.value());
738738
return false;
739739
}
740740
http::URL issuer_url;

src/runtime_config/default_whitelists.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ namespace ccf
2525
Tables::MODULES,
2626
Tables::SERVICE,
2727
Tables::CONFIGURATION,
28-
Tables::CA_CERT_DERS,
28+
Tables::CA_CERT_BUNDLE_PEMS,
2929
Tables::SERVICE_PRINCIPALS,
3030
Tables::JWT_ISSUERS,
3131
Tables::JWT_PUBLIC_SIGNING_KEYS,
@@ -40,7 +40,7 @@ namespace ccf
4040
Tables::APP_SCRIPTS,
4141
Tables::MODULES,
4242
Tables::CONFIGURATION,
43-
Tables::CA_CERT_DERS,
43+
Tables::CA_CERT_BUNDLE_PEMS,
4444
Tables::SERVICE_PRINCIPALS,
4545
Tables::JWT_ISSUERS,
4646
Tables::JWT_PUBLIC_SIGNING_KEYS,

tests/ca_certs.py

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import os
44
import tempfile
55
import http
6-
from cryptography import x509
7-
import cryptography.hazmat.backends as crypto_backends
86
import infra.network
97
import infra.path
108
import infra.proc
@@ -29,60 +27,59 @@ def test_cert_store(network, args):
2927
f.write("foo")
3028
f.flush()
3129
try:
32-
ccf.proposal_generator.set_ca_cert(cert_name, f.name)
30+
ccf.proposal_generator.set_ca_cert_bundle(cert_name, f.name)
3331
except ValueError:
3432
pass
3533
else:
36-
assert False, "set_ca_cert should have raised an error"
34+
assert False, "set_ca_cert_bundle should have raised an error"
3735

3836
LOG.info("Member makes a ca cert update proposal with malformed cert")
3937
with tempfile.NamedTemporaryFile("w") as f:
4038
f.write("foo")
4139
f.flush()
4240
try:
43-
network.consortium.set_ca_cert(primary, cert_name, f.name, skip_checks=True)
41+
network.consortium.set_ca_cert_bundle(
42+
primary, cert_name, f.name, skip_checks=True
43+
)
4444
except infra.proposal.ProposalNotAccepted:
4545
pass
4646
else:
4747
assert False, "Proposal should not have been accepted"
4848

49-
LOG.info("Member makes a ca cert update proposal with valid cert")
49+
LOG.info("Member makes a ca cert update proposal with valid certs")
5050
key_priv_pem, _ = infra.crypto.generate_rsa_keypair(2048)
5151
cert_pem = infra.crypto.generate_cert(key_priv_pem)
52+
key2_priv_pem, _ = infra.crypto.generate_rsa_keypair(2048)
53+
cert2_pem = infra.crypto.generate_cert(key2_priv_pem)
5254
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as cert_pem_fp:
5355
cert_pem_fp.write(cert_pem)
56+
cert_pem_fp.write(cert2_pem)
5457
cert_pem_fp.flush()
55-
network.consortium.set_ca_cert(primary, cert_name, cert_pem_fp.name)
58+
network.consortium.set_ca_cert_bundle(primary, cert_name, cert_pem_fp.name)
5659

5760
with primary.client(
5861
f"member{network.consortium.get_any_active_member().member_id}"
5962
) as c:
6063
r = c.post(
61-
"/gov/read", {"table": "public:ccf.gov.jwt.ca_certs_der", "key": cert_name}
64+
"/gov/read",
65+
{"table": "public:ccf.gov.tls.ca_cert_bundles", "key": cert_name},
6266
)
6367
assert r.status_code == http.HTTPStatus.OK.value, r.status_code
64-
cert_ref = x509.load_pem_x509_certificate(
65-
cert_pem.encode(), crypto_backends.default_backend()
66-
)
67-
cert_kv = x509.load_der_x509_certificate(
68-
# Note that /gov/read returns all data as JSON.
69-
# Here, the stored data is a uint8 array, therefore it
70-
# is returned as an array of integers.
71-
bytes(r.body.json()),
72-
crypto_backends.default_backend(),
73-
)
68+
cert_ref = cert_pem + cert2_pem
69+
cert_kv = r.body.json()
7470
assert (
7571
cert_ref == cert_kv
76-
), f"stored cert not equal to input cert: {cert_ref} != {cert_kv}"
72+
), f"stored cert not equal to input certs: {cert_ref} != {cert_kv}"
7773

7874
LOG.info("Member removes a ca cert")
79-
network.consortium.remove_ca_cert(primary, cert_name)
75+
network.consortium.remove_ca_cert_bundle(primary, cert_name)
8076

8177
with primary.client(
8278
f"member{network.consortium.get_any_active_member().member_id}"
8379
) as c:
8480
r = c.post(
85-
"/gov/read", {"table": "public:ccf.gov.jwt.ca_certs_der", "key": cert_name}
81+
"/gov/read",
82+
{"table": "public:ccf.gov.tls.ca_cert_bundles", "key": cert_name},
8683
)
8784
assert r.status_code == http.HTTPStatus.NOT_FOUND.value, r.status_code
8885

0 commit comments

Comments
 (0)