Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/openshell-bootstrap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ tokio = { workspace = true }
tracing = { workspace = true }

[dev-dependencies]
x509-parser = "0.16"

[lints]
workspace = true
59 changes: 58 additions & 1 deletion crates/openshell-bootstrap/src/pki.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

use crate::jwt::{JwtKeyMaterial, generate_jwt_key};
use miette::{IntoDiagnostic, Result, WrapErr};
use rcgen::{BasicConstraints, CertificateParams, DnType, Ia5String, IsCa, KeyPair, SanType};
use rcgen::{
BasicConstraints, CertificateParams, DnType, Ia5String, IsCa, KeyPair, KeyUsagePurpose, SanType,
};
use std::net::IpAddr;

/// All PEM-encoded materials produced by [`generate_pki`].
Expand Down Expand Up @@ -59,6 +61,7 @@ pub fn generate_pki(extra_sans: &[String]) -> Result<PkiBundle> {
.into_diagnostic()
.wrap_err("failed to create CA params")?;
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
ca_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
ca_params
.distinguished_name
.push(DnType::OrganizationName, "openshell");
Expand All @@ -80,6 +83,7 @@ pub fn generate_pki(extra_sans: &[String]) -> Result<PkiBundle> {
.into_diagnostic()
.wrap_err("failed to create server cert params")?;
server_params.subject_alt_names = server_sans;
server_params.use_authority_key_identifier_extension = true;
server_params
.distinguished_name
.push(DnType::CommonName, "openshell-server");
Expand All @@ -96,6 +100,7 @@ pub fn generate_pki(extra_sans: &[String]) -> Result<PkiBundle> {
let mut client_params = CertificateParams::new(Vec::<String>::new())
.into_diagnostic()
.wrap_err("failed to create client cert params")?;
client_params.use_authority_key_identifier_extension = true;
client_params
.distinguished_name
.push(DnType::CommonName, "openshell-client");
Expand Down Expand Up @@ -178,6 +183,58 @@ mod tests {
assert!(bundle.server_cert_pem.contains("BEGIN CERTIFICATE"));
}

#[test]
fn generate_pki_emits_strict_verifier_extensions() {
use x509_parser::pem::parse_x509_pem;
use x509_parser::prelude::{FromDer, ParsedExtension, X509Certificate};

let bundle = generate_pki(&[]).expect("generate_pki failed");
let parse = |pem: &str| -> Vec<u8> {
parse_x509_pem(pem.as_bytes())
.expect("valid PEM")
.1
.contents
};
let ca_der = parse(&bundle.ca_cert_pem);
let ca = X509Certificate::from_der(&ca_der).expect("valid CA cert").1;
let ca_key_usage = ca
.key_usage()
.expect("readable key usage")
.expect("CA has a key usage extension");
assert!(ca_key_usage.value.key_cert_sign());
assert!(ca_key_usage.value.crl_sign());
let ca_ski = ca
.get_extension_unique(&x509_parser::oid_registry::OID_X509_EXT_SUBJECT_KEY_IDENTIFIER)
.expect("readable SKI")
.expect("CA has a Subject Key Identifier");

for (name, pem) in [
("server", &bundle.server_cert_pem),
("client", &bundle.client_cert_pem),
] {
let der = parse(pem);
let cert = X509Certificate::from_der(&der).expect("valid leaf cert").1;
let aki = cert
.get_extension_unique(
&x509_parser::oid_registry::OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER,
)
.expect("readable AKI")
.unwrap_or_else(|| panic!("{name} cert has no Authority Key Identifier"));
let (
ParsedExtension::AuthorityKeyIdentifier(aki),
ParsedExtension::SubjectKeyIdentifier(ski),
) = (aki.parsed_extension(), ca_ski.parsed_extension())
else {
panic!("{name}: unexpected extension shapes");
};
let key_id = aki
.key_identifier
.as_ref()
.unwrap_or_else(|| panic!("{name} AKI has no key identifier"));
assert_eq!(key_id.0, ski.0, "{name} AKI must match the CA SKI");
}
}

#[test]
fn build_server_sans_includes_defaults_and_extras() {
let extras = vec!["192.168.1.100".to_string(), "remote.host".to_string()];
Expand Down
Loading