Skip to content
Merged
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
848 changes: 470 additions & 378 deletions Cargo.lock

Large diffs are not rendered by default.

120 changes: 61 additions & 59 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,40 @@ edition = "2024"
repository = "https://github.com/stackabletech/operator-rs"

[workspace.dependencies]
arc-swap = "1.9.2"
async-trait = "0.1.91"
arc-swap = "1.9.3"
async-trait = "0.1.92"
axum = { version = "0.8.9", features = ["http2"] }
base16ct = "1.0"
base64 = "0.23"
built = "0.8.1"
clap = { version = "4.6.5", features = ["derive", "cargo", "env"] }
const_format = "0.2.36"
# Pinned to the old RustCrypto generation (const-oid 0.9 / digest 0.10 / signature 2.x / spki 0.7).
# `stackable-certs` builds certificates for both ECDSA and RSA keypairs, so ecdsa/p256 and rsa have
# to agree with x509-cert on the generation of the shared der/spki/signature crates. rsa is the
# laggard: 0.9 is still its only stable release line and it is on the old generation, so const-oid,
# ecdsa, p256, rand_core, sha2, signature and x509-cert all have to stay behind with it. Bumping any
# of them individually splits the dependency graph into two incompatible halves and rsa's key and
# signature types stop satisfying x509-cert's spki traits.
# Unblocked once rsa 0.10 (currently at 0.10.0-rc.18) is released: https://github.com/RustCrypto/RSA
# See also the ecdsa, p256, rand_core, sha2, signature and x509-cert pins below.
const-oid = { version = "0.9.6", features = ["db"] }
clap = { version = "4.6.7", features = ["derive", "cargo", "env"] }
const_format = "0.2.37"
# The RustCrypto crates const-oid, ecdsa, p256, rand_core, rsa, sha2, signature and x509-cert have
# to be bumped together. `stackable-certs` builds certificates for both ECDSA and RSA keypairs, so
# ecdsa/p256 and rsa have to agree with x509-cert on the generation of the shared der/spki/signature
# crates. Bumping any of them individually splits the dependency graph into two incompatible halves
# and rsa's key and signature types stop satisfying x509-cert's spki traits.
# See also the ecdsa, p256, rand_core, rsa, sha2, signature and x509-cert entries below.
const-oid = { version = "0.10.2", features = ["db"] }
convert_case = "0.12.0"
darling = "0.24.0"
darling = "0.24.1"
delegate = "0.13.5"
dockerfile-parser = "0.9.0"
# Pinned to the old RustCrypto generation, see the const-oid comment above.
ecdsa = { version = "0.16.9", features = ["digest", "pem"] }
# Part of the RustCrypto ecosystem, see the const-oid comment above.
ecdsa = { version = "0.17.0", features = ["digest", "pem"] }
educe = { version = "0.8.1", default-features = false, features = ["Clone", "Debug", "Default", "PartialEq", "Eq"] }
either = "1.17.0"
futures = "0.3.33"
futures-util = "0.3.33"
hickory-resolver = "0.26.1"
either = "1.19.0"
futures = "0.3.34"
futures-util = "0.3.34"
hickory-resolver = "0.26.3"
http = "1.5.0"
humantime = "2.4.0"
indexmap = "2.14.0"
indoc = "2.0.7"
jiff = "0.2.35"
insta = { version = "1.48.0", features = ["glob"] }
hyper = { version = "1.11.0", features = ["full"] }
hyper-util = "0.1.20"
indexmap = "2.14.2"
indoc = "2.0.8"
jiff = "0.2.38"
insta = { version = "1.49.0", features = ["glob"] }
hyper = { version = "1.12.0", features = ["full"] }
hyper-util = "0.1.21"
itertools = "0.15.0"
java-properties = "2.0.0"
json-patch = "4.2.0"
Expand All @@ -53,62 +51,66 @@ k8s-openapi = { version = "0.28.0", default-features = false, features = ["schem
# We use ring instead of aws-lc-rs, as this currently fails to build in "make run-dev"
kube = { version = "4.2.0", default-features = false, features = ["client", "jsonpatch", "runtime", "derive", "admission", "rustls-tls", "ring"] }
local-ip-address = "0.6.13"
opentelemetry = "0.33.0"
opentelemetry_sdk = { version = "0.33.0", features = ["rt-tokio"] }
opentelemetry-appender-tracing = "0.33.0"
opentelemetry-otlp = "0.33.0"
opentelemetry-semantic-conventions = "0.33.0"
# Pinned to the old RustCrypto generation, see the const-oid comment above.
p256 = { version = "0.13.2", features = ["ecdsa"] }
opentelemetry = "0.33.1"
opentelemetry_sdk = { version = "0.33.1", features = ["rt-tokio"] }
opentelemetry-appender-tracing = "0.33.1"
opentelemetry-otlp = "0.33.1"
opentelemetry-semantic-conventions = "0.33.1"
# Part of the RustCrypto ecosystem, see the const-oid comment above.
p256 = { version = "0.14.0", features = ["ecdsa"] }
paste = "1.0.15"
pin-project = "1.1.13"
prettyplease = "0.3.0"
proc-macro2 = "1.0.107"
quote = "1.0.47"
rand = "0.10.2"
# Pinned to the old RustCrypto generation, see the const-oid comment above. This is the rand_core
# that rsa 0.9 and ecdsa 0.16 take their RNG from; it is independent of the `rand` version above,
# which is only used to generate random Secrets in stackable-operator.
Comment thread
Techassi marked this conversation as resolved.
rand_core = "0.6.4"
rand = "0.10.3"
# This crate is independent of the `rand` version above, which is only used to generate random
# Secrets in stackable-operator.
rand_core = "0.10.1"
regex = "1.13.1"
rsa = { version = "0.9.10", features = ["sha2"] }
rstest = "0.27.0"
rstest_reuse = "0.7.0"
schemars = { version = "1.2.2", features = ["url2"] }
# Part of the RustCrypto ecosystem, see the const-oid comment above. Pinned exactly, as release
# candidates can contain breaking changes. Switch back to a regular version requirement once rsa
# 0.10.0 is released: https://github.com/RustCrypto/RSA
# We are ok with using a RC version, as it enables us to move the entire ecosystem forward and e.g.
# fixes RUSTSEC-2026-0387
rsa = { version = "=0.10.0-rc.19", features = ["sha2"] }
rstest = "0.28.0"
rstest_reuse = "0.7.1"
schemars = { version = "1.2.3", features = ["url2"] }
semver = "1.0.28"
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
serde_json = "1.0.154"
yaml_serde = "0.10.7"
# Pinned to the old RustCrypto generation, see the const-oid comment above. sha2 0.11 uses
# digest 0.11 and signature 3.0, which rsa 0.9 does not support.
sha2 = { version = "0.10.9", features = ["oid"] }
signature = "2.2.0"
# Part of the RustCrypto ecosystem, see the const-oid comment above.
sha2 = { version = "0.11.1", features = ["oid"] }
# Part of the RustCrypto ecosystem, see the const-oid comment above.
signature = "3.0.0"
similar-asserts = "2.0.0"
snafu = "0.9.2"
stackable-operator-derive = { path = "stackable-operator-derive" }
strum = { version = "0.28.0", features = ["derive"] }
syn = "3.0.3"
syn = "3.0.9"
sysinfo = "0.39.6"
tempfile = "3.27.0"
time = { version = "0.3.55" }
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread", "fs"] }
tokio = { version = "1.54.1", features = ["macros", "rt-multi-thread", "fs"] }
# We use ring instead of aws-lc-rs, as this currently fails to build in "make run-dev"
tokio-rustls = { version = "0.26.4", default-features = false, features = ["ring", "logging", "tls12"] }
tokio-test = "0.4.5"
tokio-rustls = { version = "0.26.6", default-features = false, features = ["ring", "logging", "tls12"] }
tokio-test = "0.4.6"
tower = { version = "0.5.3", features = ["util"] }
tower-http = { version = "0.7.0", features = ["trace"] } # We don't actually use this crate, but we need to enable the `trace` feature
tower-http = { version = "0.7.1", features = ["trace"] } # We don't actually use this crate, but we need to enable the `trace` feature
tracing = "0.1.44"
tracing-appender = "0.2.5"
tracing-opentelemetry = "0.34.0"
tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json"] }
trybuild = "1.0.120"
trybuild = "1.0.122"
url = { version = "2.5.8", features = ["serde"] }
uuid = "1.24.0"
uuid = "1.29.0"
winnow = "1.0.4"
# Pinned to the old RustCrypto generation, see the const-oid comment above.
x509-cert = { version = "0.2.5", features = ["builder"] }
xml = "1.3.0"
zeroize = "1.9.0"
# Part of the RustCrypto ecosystem, see the const-oid comment above.
x509-cert = { version = "0.3.0", features = ["builder"] }
xml = "1.4.0"
zeroize = "1.9.1"

[workspace.lints.clippy]
# Enable all pedantic lints (with lower priority so individual lints can override)
Expand Down
12 changes: 12 additions & 0 deletions crates/stackable-certs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Changed

- BREAKING: Bump the RustCrypto dependencies to their new generation, notably `x509-cert` 0.3,
`rsa` 0.10.0-rc.19, `ecdsa` 0.17, `p256` 0.14 and `rand_core` 0.10 ([#1315]).
- `SigningKey::new_with_rng` of the `ecdsa` and `rsa` modules now takes a `rand_core` 0.10
`CryptoRng`.
- The source of `Error::AddCertificateExtension` is now `x509_cert::der::Error`.
- The generated certificates are unchanged (apart from the order of the extensions), as the
`Root` and `Leaf` profiles removed in `x509-cert` 0.3 are reimplemented in this crate.

[#1315]: https://github.com/stackabletech/operator-rs/pull/1315

## [0.4.1] - 2026-07-06

Note: There are only dependency bumps in this release.
Expand Down
69 changes: 37 additions & 32 deletions crates/stackable-certs/src/ca/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use stackable_shared::{secret::SecretReference, time::Duration};
use tracing::{debug, instrument};
use x509_cert::{
Certificate,
builder::{Builder, CertificateBuilder, Profile},
builder::{Builder, CertificateBuilder},
der::{DecodePem, asn1::Ia5String, pem::LineEnding, referenced::OwnedToRef},
ext::pkix::{AuthorityKeyIdentifier, ExtendedKeyUsage, SubjectAltName, name::GeneralName},
name::Name,
Expand All @@ -21,10 +21,12 @@ use x509_cert::{

use crate::{
CertificatePair,
ca::profile::Profile,
keys::{CertificateKeypair, ecdsa, rsa},
};

mod consts;
mod profile;
pub use consts::*;

pub const TLS_SECRET_TYPE: &str = "kubernetes.io/tls";
Expand Down Expand Up @@ -60,7 +62,7 @@ pub enum Error {
CreateCertificateBuilder { source: x509_cert::builder::Error },

#[snafu(display("failed to add certificate extension"))]
AddCertificateExtension { source: x509_cert::builder::Error },
AddCertificateExtension { source: x509_cert::der::Error },

#[snafu(display("failed to build certificate"))]
BuildCertificate { source: x509_cert::builder::Error },
Expand Down Expand Up @@ -91,10 +93,6 @@ impl PartialEq for Error {
Self::CreateCertificateBuilder { source: lhs_source },
Self::CreateCertificateBuilder { source: rhs_source },
)
| (
Self::AddCertificateExtension { source: lhs_source },
Self::AddCertificateExtension { source: rhs_source },
)
| (
Self::BuildCertificate { source: lhs_source },
Self::BuildCertificate { source: rhs_source },
Expand All @@ -114,6 +112,12 @@ impl PartialEq for Error {
),
_ => false,
},
(
Self::AddCertificateExtension { source: lhs_source },
Self::AddCertificateExtension { source: rhs_source },
) => lhs_source == rhs_source,
// FIXME: This calls `PartialEq::eq` for `Error` again, which recurses infinitely (and
// overflows the stack) for every variant not matched above.
(lhs, rhs) => lhs == rhs,
}
}
Expand Down Expand Up @@ -248,24 +252,19 @@ where
let aki = AuthorityKeyIdentifier::try_from(spki.owned_to_ref())
.context(ParseAuthorityKeyIdentifierSnafu)?;

let signer = signing_key_pair.signing_key();
let mut builder = CertificateBuilder::new(
Profile::Root,
serial_number,
validity,
subject,
spki,
signer,
)
.context(CreateCertificateBuilderSnafu)?;
let mut builder =
CertificateBuilder::new(Profile::Root { subject }, serial_number, validity, spki)
.context(CreateCertificateBuilderSnafu)?;

// Add extension constructed above
builder
.add_extension(&aki)
.context(AddCertificateExtensionSnafu)?;

debug!("create and sign CA certificate");
let certificate = builder.build().context(BuildCertificateSnafu)?;
let certificate = builder
.build::<_, S::Signature>(signing_key_pair.signing_key())
.context(BuildCertificateSnafu)?;

Ok(Self {
certificate_pair: CertificatePair {
Expand Down Expand Up @@ -318,33 +317,37 @@ where
let spki = SubjectPublicKeyInfoOwned::from_pem(spki_pem.as_bytes())
.context(DecodeSpkiFromPemSnafu)?;

let signer = self.certificate_pair.key_pair.signing_key();
let mut builder = CertificateBuilder::new(
Profile::Leaf {
issuer: self
.certificate_pair
.certificate
.tbs_certificate
.issuer
.tbs_certificate()
.issuer()
.clone(),
subject,
enable_key_agreement: false,
// FIXME(@Techassi): We should disable this EKU as it is considered unsafe.
// It was used in the past for static RSA key exchanges where the public key of the
// server's certificate was used to encrypt the shared secret.
enable_key_encipherment: true,
Comment thread
Techassi marked this conversation as resolved.
},
serial_number,
validity,
subject,
spki,
signer,
)
.context(CreateCertificateBuilderSnafu)?;

// The leaf certificate can be used for WWW client and server
// authentication. This is a base requirement for TLS certs.
//
// The extension is explicitly marked as critical. This was the default up to x509-cert
// 0.2, which marked it as critical unless anyExtendedKeyUsage was present.
builder
.add_extension(&ExtendedKeyUsage(vec![
ID_KP_CLIENT_AUTH,
ID_KP_SERVER_AUTH,
]))
.add_extension((
true,
&ExtendedKeyUsage(vec![ID_KP_CLIENT_AUTH, ID_KP_SERVER_AUTH]),
))
.context(AddCertificateExtensionSnafu)?;

let sans = subject_alternative_dns_names
Expand All @@ -362,7 +365,9 @@ where
.context(AddCertificateExtensionSnafu)?;

debug!("create and sign leaf certificate");
let certificate = builder.build().context(BuildCertificateSnafu)?;
let certificate = builder
.build::<_, S::Signature>(self.certificate_pair.key_pair.signing_key())
.context(BuildCertificateSnafu)?;

Ok(CertificatePair {
certificate,
Expand Down Expand Up @@ -540,24 +545,24 @@ mod tests {
}

fn assert_cert_attributes(cert: &Certificate) {
let cert = &cert.tbs_certificate;
let cert = cert.tbs_certificate();
let expected_subject = Name::from_str("CN=Product Certificate for pod")
.expect("constant subject must be valid");

// Test subject
assert_eq!(cert.subject, expected_subject);
assert_eq!(cert.subject(), &expected_subject);

// Test SAN extension is present
let extensions = cert.extensions.as_ref().expect("cert must have extensions");
let extensions = cert.extensions().expect("cert must have extensions");
assert!(
extensions
.iter()
.any(|ext| ext.extn_id == ObjectIdentifier::new_unwrap("2.5.29.17"))
);

// Test lifetime
let not_before = cert.validity.not_before.to_system_time();
let not_after = cert.validity.not_after.to_system_time();
let not_before = cert.validity().not_before.to_system_time();
let not_after = cert.validity().not_after.to_system_time();
assert_eq!(
not_after
.duration_since(not_before)
Expand Down
Loading
Loading