diff --git a/Cargo.lock b/Cargo.lock index e5698266..2ae18a06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -366,6 +366,20 @@ dependencies = [ "shlex", ] +[[package]] +name = "cert-tools" +version = "0.0.0-dev" +dependencies = [ + "clap", + "hex", + "openssl", + "snafu 0.8.7", + "stackable-secret-operator-utils", + "stackable-telemetry 0.6.1 (git+https://github.com/stackabletech/operator-rs.git?tag=stackable-telemetry-0.6.1)", + "tracing", + "tracing-subscriber", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -1136,6 +1150,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hex-literal" version = "0.3.4" @@ -3057,7 +3077,7 @@ dependencies = [ "serde_json", "snafu 0.8.7", "stackable-operator", - "stackable-secret-operator-crd-utils", + "stackable-secret-operator-utils", "tokio", "tracing", "tracing-subscriber", @@ -3091,7 +3111,7 @@ dependencies = [ "snafu 0.8.7", "stackable-operator-derive", "stackable-shared", - "stackable-telemetry", + "stackable-telemetry 0.6.1 (git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.96.0)", "stackable-versioned", "strum", "tokio", @@ -3138,7 +3158,7 @@ dependencies = [ "socket2", "stackable-krb5-provision-keytab", "stackable-operator", - "stackable-secret-operator-crd-utils", + "stackable-secret-operator-utils", "strum", "sys-mount", "tempfile", @@ -3151,15 +3171,6 @@ dependencies = [ "tonic-reflection", "tracing", "uuid", - "yasna", -] - -[[package]] -name = "stackable-secret-operator-crd-utils" -version = "0.0.0-dev" -dependencies = [ - "serde", - "stackable-operator", ] [[package]] @@ -3178,6 +3189,19 @@ dependencies = [ "walkdir", ] +[[package]] +name = "stackable-secret-operator-utils" +version = "0.0.0-dev" +dependencies = [ + "anyhow", + "openssl", + "p12", + "serde", + "snafu 0.8.7", + "stackable-operator", + "yasna", +] + [[package]] name = "stackable-shared" version = "0.0.2" @@ -3218,6 +3242,30 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "stackable-telemetry" +version = "0.6.1" +source = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-telemetry-0.6.1#958f62f2befdd984ad9fcd272d0214055c3a7601" +dependencies = [ + "axum", + "clap", + "futures-util", + "opentelemetry", + "opentelemetry-appender-tracing", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "pin-project", + "snafu 0.8.7", + "strum", + "tokio", + "tower", + "tracing", + "tracing-appender", + "tracing-opentelemetry", + "tracing-subscriber", +] + [[package]] name = "stackable-versioned" version = "0.8.1" diff --git a/Cargo.nix b/Cargo.nix index a16f4047..464a50e2 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -38,6 +38,16 @@ rec { # You can override the features with # workspaceMembers."${crateName}".build.override { features = [ "default" "feature1" ... ]; }. workspaceMembers = { + "cert-tools" = rec { + packageId = "cert-tools"; + build = internal.buildRustCrateWithFeatures { + packageId = "cert-tools"; + }; + + # Debug support which might change between releases. + # File a bug if you depend on any for non-debug work! + debug = internal.debugCrate { inherit packageId; }; + }; "p12" = rec { packageId = "p12"; build = internal.buildRustCrateWithFeatures { @@ -68,20 +78,20 @@ rec { # File a bug if you depend on any for non-debug work! debug = internal.debugCrate { inherit packageId; }; }; - "stackable-secret-operator-crd-utils" = rec { - packageId = "stackable-secret-operator-crd-utils"; + "stackable-secret-operator-olm-deployer" = rec { + packageId = "stackable-secret-operator-olm-deployer"; build = internal.buildRustCrateWithFeatures { - packageId = "stackable-secret-operator-crd-utils"; + packageId = "stackable-secret-operator-olm-deployer"; }; # Debug support which might change between releases. # File a bug if you depend on any for non-debug work! debug = internal.debugCrate { inherit packageId; }; }; - "stackable-secret-operator-olm-deployer" = rec { - packageId = "stackable-secret-operator-olm-deployer"; + "stackable-secret-operator-utils" = rec { + packageId = "stackable-secret-operator-utils"; build = internal.buildRustCrateWithFeatures { - packageId = "stackable-secret-operator-olm-deployer"; + packageId = "stackable-secret-operator-utils"; }; # Debug support which might change between releases. @@ -1219,6 +1229,60 @@ rec { }; resolvedDefaultFeatures = [ "parallel" ]; }; + "cert-tools" = rec { + crateName = "cert-tools"; + version = "0.0.0-dev"; + edition = "2021"; + crateBin = [ + { + name = "cert-tools"; + path = "src/main.rs"; + requiredFeatures = [ ]; + } + ]; + src = lib.cleanSourceWith { filter = sourceFilter; src = ./rust/cert-tools; }; + authors = [ + "Stackable GmbH " + ]; + dependencies = [ + { + name = "clap"; + packageId = "clap"; + features = [ "derive" ]; + } + { + name = "stackable-telemetry"; + packageId = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-telemetry-0.6.1#stackable-telemetry@0.6.1"; + features = [ "clap" ]; + } + { + name = "hex"; + packageId = "hex"; + } + { + name = "openssl"; + packageId = "openssl"; + } + { + name = "snafu"; + packageId = "snafu 0.8.7"; + } + { + name = "stackable-secret-operator-utils"; + packageId = "stackable-secret-operator-utils"; + } + { + name = "tracing"; + packageId = "tracing"; + } + { + name = "tracing-subscriber"; + packageId = "tracing-subscriber"; + features = [ "env-filter" ]; + } + ]; + + }; "cexpr" = rec { crateName = "cexpr"; version = "0.6.0"; @@ -3253,6 +3317,226 @@ rec { }; resolvedDefaultFeatures = [ "read" "read-core" ]; }; + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.96.0#stackable-telemetry@0.6.1" = rec { + crateName = "stackable-telemetry"; + version = "0.6.1"; + edition = "2024"; + workspace_member = null; + src = pkgs.fetchgit { + url = "https://github.com/stackabletech/operator-rs.git"; + rev = "89f484ca4e86b565e083e9ad7573e21dbe29a3af"; + sha256 = "05xhfz0bd09095ljkaj950r80bchdb202d8nka95cq356y4wha4c"; + }; + libName = "stackable_telemetry"; + authors = [ + "Stackable GmbH " + ]; + dependencies = [ + { + name = "axum"; + packageId = "axum"; + features = [ "http2" ]; + } + { + name = "clap"; + packageId = "clap"; + optional = true; + features = [ "derive" "cargo" "env" ]; + } + { + name = "futures-util"; + packageId = "futures-util"; + } + { + name = "opentelemetry"; + packageId = "opentelemetry"; + features = [ "logs" ]; + } + { + name = "opentelemetry-appender-tracing"; + packageId = "opentelemetry-appender-tracing"; + } + { + name = "opentelemetry-otlp"; + packageId = "opentelemetry-otlp"; + features = [ "grpc-tonic" "gzip-tonic" "logs" ]; + } + { + name = "opentelemetry-semantic-conventions"; + packageId = "opentelemetry-semantic-conventions"; + } + { + name = "opentelemetry_sdk"; + packageId = "opentelemetry_sdk"; + features = [ "rt-tokio" "logs" "rt-tokio" "spec_unstable_logs_enabled" ]; + } + { + name = "pin-project"; + packageId = "pin-project"; + } + { + name = "snafu"; + packageId = "snafu 0.8.7"; + } + { + name = "strum"; + packageId = "strum"; + features = [ "derive" ]; + } + { + name = "tokio"; + packageId = "tokio"; + features = [ "macros" "rt-multi-thread" "fs" ]; + } + { + name = "tower"; + packageId = "tower"; + features = [ "util" ]; + } + { + name = "tracing"; + packageId = "tracing"; + } + { + name = "tracing-appender"; + packageId = "tracing-appender"; + } + { + name = "tracing-opentelemetry"; + packageId = "tracing-opentelemetry"; + } + { + name = "tracing-subscriber"; + packageId = "tracing-subscriber"; + features = [ "env-filter" "json" "env-filter" ]; + } + ]; + devDependencies = [ + { + name = "tokio"; + packageId = "tokio"; + features = [ "macros" "rt-multi-thread" "fs" ]; + } + { + name = "tracing-opentelemetry"; + packageId = "tracing-opentelemetry"; + } + ]; + features = { + "clap" = [ "dep:clap" ]; + }; + resolvedDefaultFeatures = [ "clap" ]; + }; + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-telemetry-0.6.1#stackable-telemetry@0.6.1" = rec { + crateName = "stackable-telemetry"; + version = "0.6.1"; + edition = "2024"; + workspace_member = null; + src = pkgs.fetchgit { + url = "https://github.com/stackabletech/operator-rs.git"; + rev = "958f62f2befdd984ad9fcd272d0214055c3a7601"; + sha256 = "0hiymhr40ix4jv9dmvp5d009xs6v0frvllr2xkf5mw43rcg44mgd"; + }; + libName = "stackable_telemetry"; + authors = [ + "Stackable GmbH " + ]; + dependencies = [ + { + name = "axum"; + packageId = "axum"; + features = [ "http2" ]; + } + { + name = "clap"; + packageId = "clap"; + optional = true; + features = [ "derive" "cargo" "env" ]; + } + { + name = "futures-util"; + packageId = "futures-util"; + } + { + name = "opentelemetry"; + packageId = "opentelemetry"; + features = [ "logs" ]; + } + { + name = "opentelemetry-appender-tracing"; + packageId = "opentelemetry-appender-tracing"; + } + { + name = "opentelemetry-otlp"; + packageId = "opentelemetry-otlp"; + features = [ "grpc-tonic" "gzip-tonic" "logs" ]; + } + { + name = "opentelemetry-semantic-conventions"; + packageId = "opentelemetry-semantic-conventions"; + } + { + name = "opentelemetry_sdk"; + packageId = "opentelemetry_sdk"; + features = [ "rt-tokio" "logs" "rt-tokio" "spec_unstable_logs_enabled" ]; + } + { + name = "pin-project"; + packageId = "pin-project"; + } + { + name = "snafu"; + packageId = "snafu 0.8.7"; + } + { + name = "strum"; + packageId = "strum"; + features = [ "derive" ]; + } + { + name = "tokio"; + packageId = "tokio"; + features = [ "macros" "rt-multi-thread" "fs" ]; + } + { + name = "tower"; + packageId = "tower"; + features = [ "util" ]; + } + { + name = "tracing"; + packageId = "tracing"; + } + { + name = "tracing-appender"; + packageId = "tracing-appender"; + } + { + name = "tracing-opentelemetry"; + packageId = "tracing-opentelemetry"; + } + { + name = "tracing-subscriber"; + packageId = "tracing-subscriber"; + features = [ "env-filter" "json" "env-filter" ]; + } + ]; + devDependencies = [ + { + name = "tokio"; + packageId = "tokio"; + features = [ "macros" "rt-multi-thread" "fs" ]; + } + { + name = "tracing-opentelemetry"; + packageId = "tracing-opentelemetry"; + } + ]; + features = { + "clap" = [ "dep:clap" ]; + }; + resolvedDefaultFeatures = [ "clap" ]; + }; "git2" = rec { crateName = "git2"; version = "0.20.2"; @@ -3521,6 +3805,21 @@ rec { sha256 = "1sjmpsdl8czyh9ywl3qcsfsq9a307dg4ni2vnlwgnzzqhc4y0113"; }; + "hex" = rec { + crateName = "hex"; + version = "0.4.3"; + edition = "2018"; + sha256 = "0w1a4davm1lgzpamwnba907aysmlrnygbqmfis2mqjx5m552a93z"; + authors = [ + "KokaKiwi " + ]; + features = { + "default" = [ "std" ]; + "serde" = [ "dep:serde" ]; + "std" = [ "alloc" ]; + }; + resolvedDefaultFeatures = [ "alloc" "default" "std" ]; + }; "hex-literal" = rec { crateName = "hex-literal"; version = "0.3.4"; @@ -9963,8 +10262,9 @@ rec { features = [ "time" "telemetry" "versioned" ]; } { - name = "stackable-secret-operator-crd-utils"; - packageId = "stackable-secret-operator-crd-utils"; + name = "stackable-secret-operator-utils"; + packageId = "stackable-secret-operator-utils"; + features = [ "crd" ]; } { name = "tokio"; @@ -10033,6 +10333,12 @@ rec { name = "futures"; packageId = "futures 0.3.31"; } + { + name = "stackable-telemetry"; + packageId = "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.96.0#stackable-telemetry@0.6.1"; + optional = true; + features = [ "clap" ]; + } { name = "http"; packageId = "http"; @@ -10099,12 +10405,6 @@ rec { name = "stackable-shared"; packageId = "stackable-shared"; } - { - name = "stackable-telemetry"; - packageId = "stackable-telemetry"; - optional = true; - features = [ "clap" ]; - } { name = "stackable-versioned"; packageId = "stackable-versioned"; @@ -10287,8 +10587,9 @@ rec { features = [ "time" "telemetry" "versioned" ]; } { - name = "stackable-secret-operator-crd-utils"; - packageId = "stackable-secret-operator-crd-utils"; + name = "stackable-secret-operator-utils"; + packageId = "stackable-secret-operator-utils"; + features = [ "crd" ]; } { name = "strum"; @@ -10340,10 +10641,6 @@ rec { packageId = "uuid"; features = [ "v4" ]; } - { - name = "yasna"; - packageId = "yasna"; - } ]; buildDependencies = [ { @@ -10363,29 +10660,6 @@ rec { } ]; - }; - "stackable-secret-operator-crd-utils" = rec { - crateName = "stackable-secret-operator-crd-utils"; - version = "0.0.0-dev"; - edition = "2021"; - src = lib.cleanSourceWith { filter = sourceFilter; src = ./rust/crd-utils; }; - libName = "stackable_secret_operator_crd_utils"; - authors = [ - "Stackable GmbH " - ]; - dependencies = [ - { - name = "serde"; - packageId = "serde"; - features = [ "derive" ]; - } - { - name = "stackable-operator"; - packageId = "stackable-operator"; - features = [ "time" "telemetry" "versioned" ]; - } - ]; - }; "stackable-secret-operator-olm-deployer" = rec { crateName = "stackable-secret-operator-olm-deployer"; @@ -10452,6 +10726,56 @@ rec { ]; }; + "stackable-secret-operator-utils" = rec { + crateName = "stackable-secret-operator-utils"; + version = "0.0.0-dev"; + edition = "2021"; + src = lib.cleanSourceWith { filter = sourceFilter; src = ./rust/utils; }; + libName = "stackable_secret_operator_utils"; + authors = [ + "Stackable GmbH " + ]; + dependencies = [ + { + name = "openssl"; + packageId = "openssl"; + } + { + name = "p12"; + packageId = "p12"; + } + { + name = "serde"; + packageId = "serde"; + optional = true; + features = [ "derive" ]; + } + { + name = "snafu"; + packageId = "snafu 0.8.7"; + } + { + name = "stackable-operator"; + packageId = "stackable-operator"; + optional = true; + features = [ "time" "telemetry" "versioned" ]; + } + { + name = "yasna"; + packageId = "yasna"; + } + ]; + devDependencies = [ + { + name = "anyhow"; + packageId = "anyhow"; + } + ]; + features = { + "crd" = [ "dep:stackable-operator" "dep:serde" ]; + }; + resolvedDefaultFeatures = [ "crd" "default" ]; + }; "stackable-shared" = rec { crateName = "stackable-shared"; version = "0.0.2"; @@ -10527,116 +10851,6 @@ rec { }; resolvedDefaultFeatures = [ "default" "time" ]; }; - "stackable-telemetry" = rec { - crateName = "stackable-telemetry"; - version = "0.6.1"; - edition = "2024"; - workspace_member = null; - src = pkgs.fetchgit { - url = "https://github.com/stackabletech/operator-rs.git"; - rev = "89f484ca4e86b565e083e9ad7573e21dbe29a3af"; - sha256 = "05xhfz0bd09095ljkaj950r80bchdb202d8nka95cq356y4wha4c"; - }; - libName = "stackable_telemetry"; - authors = [ - "Stackable GmbH " - ]; - dependencies = [ - { - name = "axum"; - packageId = "axum"; - features = [ "http2" ]; - } - { - name = "clap"; - packageId = "clap"; - optional = true; - features = [ "derive" "cargo" "env" ]; - } - { - name = "futures-util"; - packageId = "futures-util"; - } - { - name = "opentelemetry"; - packageId = "opentelemetry"; - features = [ "logs" ]; - } - { - name = "opentelemetry-appender-tracing"; - packageId = "opentelemetry-appender-tracing"; - } - { - name = "opentelemetry-otlp"; - packageId = "opentelemetry-otlp"; - features = [ "grpc-tonic" "gzip-tonic" "logs" ]; - } - { - name = "opentelemetry-semantic-conventions"; - packageId = "opentelemetry-semantic-conventions"; - } - { - name = "opentelemetry_sdk"; - packageId = "opentelemetry_sdk"; - features = [ "rt-tokio" "logs" "rt-tokio" "spec_unstable_logs_enabled" ]; - } - { - name = "pin-project"; - packageId = "pin-project"; - } - { - name = "snafu"; - packageId = "snafu 0.8.7"; - } - { - name = "strum"; - packageId = "strum"; - features = [ "derive" ]; - } - { - name = "tokio"; - packageId = "tokio"; - features = [ "macros" "rt-multi-thread" "fs" ]; - } - { - name = "tower"; - packageId = "tower"; - features = [ "util" ]; - } - { - name = "tracing"; - packageId = "tracing"; - } - { - name = "tracing-appender"; - packageId = "tracing-appender"; - } - { - name = "tracing-opentelemetry"; - packageId = "tracing-opentelemetry"; - } - { - name = "tracing-subscriber"; - packageId = "tracing-subscriber"; - features = [ "env-filter" "json" "env-filter" ]; - } - ]; - devDependencies = [ - { - name = "tokio"; - packageId = "tokio"; - features = [ "macros" "rt-multi-thread" "fs" ]; - } - { - name = "tracing-opentelemetry"; - packageId = "tracing-opentelemetry"; - } - ]; - features = { - "clap" = [ "dep:clap" ]; - }; - resolvedDefaultFeatures = [ "clap" ]; - }; "stackable-versioned" = rec { crateName = "stackable-versioned"; version = "0.8.1"; diff --git a/Cargo.toml b/Cargo.toml index a3386bdd..32b5ee66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ clap = "4.5" const_format = "0.2.34" futures = { version = "0.3", features = ["compat"] } h2 = "0.4" +hex = "0.4" kube-runtime = { version = "1.0", features = ["unstable-runtime-stream-control"] } ldap3 = { version = "0.11", default-features = false, features = ["gssapi", "tls"] } libc = "0.2" diff --git a/crate-hashes.json b/crate-hashes.json index 30a54dc0..12b48568 100644 --- a/crate-hashes.json +++ b/crate-hashes.json @@ -8,5 +8,6 @@ "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.96.0#stackable-telemetry@0.6.1": "05xhfz0bd09095ljkaj950r80bchdb202d8nka95cq356y4wha4c", "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.96.0#stackable-versioned-macros@0.8.1": "05xhfz0bd09095ljkaj950r80bchdb202d8nka95cq356y4wha4c", "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-operator-0.96.0#stackable-versioned@0.8.1": "05xhfz0bd09095ljkaj950r80bchdb202d8nka95cq356y4wha4c", + "git+https://github.com/stackabletech/operator-rs.git?tag=stackable-telemetry-0.6.1#stackable-telemetry@0.6.1": "0hiymhr40ix4jv9dmvp5d009xs6v0frvllr2xkf5mw43rcg44mgd", "git+https://github.com/stackabletech/product-config.git?tag=0.7.0#product-config@0.7.0": "0gjsm80g6r75pm3824dcyiz4ysq1ka4c1if6k1mjm9cnd5ym0gny" } \ No newline at end of file diff --git a/rust/cert-tools/Cargo.toml b/rust/cert-tools/Cargo.toml new file mode 100644 index 00000000..1172ce53 --- /dev/null +++ b/rust/cert-tools/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cert-tools" +description = "A CLI tool to merge two truststores in PEM or PKCS12 format in such as way that they are accepted by the JVM" +version = "0.0.0-dev" +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +publish = false + +[dependencies] +stackable-secret-operator-utils = { path = "../utils" } +stackable-telemetry = { git = "https://github.com/stackabletech/operator-rs.git", features = ["clap"], tag = "stackable-telemetry-0.6.1" } + +clap = { workspace = true, features = ["derive"] } +hex.workspace = true +openssl.workspace = true +snafu.workspace = true +tracing.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/rust/cert-tools/src/cert_ext.rs b/rust/cert-tools/src/cert_ext.rs new file mode 100644 index 00000000..019a0dfc --- /dev/null +++ b/rust/cert-tools/src/cert_ext.rs @@ -0,0 +1,26 @@ +use openssl::{ + hash::{DigestBytes, MessageDigest}, + string::OpensslString, + x509::X509, +}; +use snafu::ResultExt; + +pub trait CertExt { + fn serial_as_hex(&self) -> Result; + fn sha256_digest(&self) -> Result; +} + +impl CertExt for X509 { + fn serial_as_hex(&self) -> Result { + self.serial_number() + .to_bn() + .whatever_context("failed to get certificate serial number as BigNumber")? + .to_hex_str() + .whatever_context("failed to convert certificate serial number to hex string") + } + + fn sha256_digest(&self) -> Result { + self.digest(MessageDigest::sha256()) + .whatever_context("failed to get certificate digest") + } +} diff --git a/rust/cert-tools/src/cli_args.rs b/rust/cert-tools/src/cli_args.rs new file mode 100644 index 00000000..3eb63929 --- /dev/null +++ b/rust/cert-tools/src/cli_args.rs @@ -0,0 +1,115 @@ +use std::{fs, path::PathBuf}; + +use clap::{Parser, Subcommand}; +use openssl::x509::X509; +use snafu::{ResultExt, ensure_whatever}; +use stackable_telemetry::tracing::TelemetryOptions; + +use crate::parsers::{parse_pem_contents, parse_pkcs12_file_workaround}; + +#[derive(Parser, Debug)] +#[command(version, about)] +pub struct Cli { + #[command(subcommand)] + pub command: CliCommand, + + #[command(flatten, next_help_heading = "Tracing options")] + pub telemetry: TelemetryOptions, +} + +#[derive(Subcommand, Debug)] +pub enum CliCommand { + /// Generate PKCS12 truststore files from PEM or PKCS12 files + GeneratePkcs12Truststore(GeneratePkcs12), +} + +#[derive(Parser, Debug)] +pub struct GeneratePkcs12 { + /// The path to output the resulting PKCS12 to + #[arg(long)] + pub out: PathBuf, + + /// The password used to encrypt the outputted PKCS12 truststore. Defaults to an empty string. + #[arg(long, default_value = "")] + pub out_password: String, + + /// List of PEM certificate(s) + #[arg(long = "pem")] + pub pems: Vec, + + /// List of PKCS12 truststore(s) + /// + /// You can either use `truststore.p12` (which uses an empty password by default), or specify + /// the password using `truststore.p12:changeit`. + #[arg(long = "pkcs12", value_parser = parse_cli_pkcs12_source)] + pub pkcs12s: Vec, +} + +#[derive(Debug)] +pub enum CertInput { + Pem(PathBuf), + Pkcs12(Pkcs12Source), +} + +#[derive(Clone, Debug)] +pub struct Pkcs12Source { + path: PathBuf, + password: String, +} + +fn parse_cli_pkcs12_source(cli_argument: &str) -> Result { + let mut parts = cli_argument.splitn(2, ':'); + let path = parts + .next() + .ok_or_else(|| "missing path part".to_string())?; + let password = parts.next().unwrap_or("").to_string(); + + Ok(Pkcs12Source { + path: PathBuf::from(path), + password, + }) +} + +impl GeneratePkcs12 { + pub fn certificate_sources(&self) -> Vec { + let pems = self.pems.iter().cloned().map(CertInput::Pem); + let pkcs12s = self.pkcs12s.iter().cloned().map(CertInput::Pkcs12); + pems.chain(pkcs12s).collect() + } +} + +impl CertInput { + pub fn read(&self) -> Result, snafu::Whatever> { + let read_file_fn = |path| { + fs::read(path).with_whatever_context(|_| format!("failed to read from file {self:?}")) + }; + + match self { + CertInput::Pem(path) => { + let file_contents = read_file_fn(path)?; + + let certs = parse_pem_contents(&file_contents).with_whatever_context(|_| { + format!("failed to parse PEM contents from {path:?}",) + })?; + ensure_whatever!( + !certs.is_empty(), + "The PEM file at {path:?} contained no certificates", + ); + + Ok(certs) + } + CertInput::Pkcs12(Pkcs12Source { path, password }) => { + let file_contents = read_file_fn(path)?; + + parse_pkcs12_file_workaround(&file_contents, password) + } + } + } + + pub fn path(&self) -> &PathBuf { + match self { + CertInput::Pem(path) => path, + CertInput::Pkcs12(Pkcs12Source { path, .. }) => path, + } + } +} diff --git a/rust/cert-tools/src/main.rs b/rust/cert-tools/src/main.rs new file mode 100644 index 00000000..ab20a89d --- /dev/null +++ b/rust/cert-tools/src/main.rs @@ -0,0 +1,102 @@ +use std::{collections::HashMap, fs}; + +use cert_ext::CertExt; +use clap::Parser; +use cli_args::{Cli, CliCommand, GeneratePkcs12}; +use openssl::x509::X509; +use snafu::{ResultExt, ensure_whatever}; +use stackable_secret_operator_utils::pkcs12::pkcs12_truststore; +use stackable_telemetry::Tracing; +use tracing::{info, warn}; + +mod cert_ext; +mod cli_args; +mod parsers; + +#[snafu::report] +pub fn main() -> Result<(), snafu::Whatever> { + let cli = Cli::parse(); + + // Use `CONSOLE_LOG_LEVEL` to modify the console log level + let _tracing_guard = Tracing::pre_configured("cert-tools", cli.telemetry) + .init() + .whatever_context("failed to initialize tracing")?; + + match cli.command { + CliCommand::GeneratePkcs12Truststore(cli_args) => generate_pkcs12_truststore(cli_args)?, + } + + Ok(()) +} + +fn generate_pkcs12_truststore(cli_args: GeneratePkcs12) -> Result<(), snafu::Whatever> { + let certificate_sources = cli_args.certificate_sources(); + ensure_whatever!( + !certificate_sources.is_empty(), + "The list of certificate sources can not be empty. Please provide at least on --pem or --pkcs12." + ); + let certificate_sources = certificate_sources + .iter() + .map(|source| { + let certificate = source.read().with_whatever_context(|_| { + format!( + "failed to read certificate source {path:?}", + path = source.path() + ) + })?; + Ok((source, certificate)) + }) + .collect::, _>>()?; + + let mut certificates = HashMap::, X509>::new(); + for (source, certificates_list) in certificate_sources.into_iter() { + info!(?source, "Importing certificates"); + + for certificate in certificates_list { + let sha256 = certificate.sha256_digest()?; + + // Trying to stick to https://opentelemetry.io/docs/specs/semconv/registry/attributes/tls/#tls-attributes + // Converting `Asn1TimeRef` to a ISO 8601 timestamp really sucks, so we omitted that. + if let Some(existing) = certificates.get(&*sha256) { + warn!( + ?source, + hash.sha256 = hex::encode(sha256).to_uppercase(), + existing.not_before = ?existing.not_before(), + existing.not_after = ?existing.not_after(), + existing.subject = ?existing.subject_name(), + existing.issuer = ?existing.issuer_name(), + existing.serial = ?existing.serial_as_hex()?, + new.not_before = ?certificate.not_before(), + new.not_after = ?certificate.not_after(), + new.subject = ?certificate.subject_name(), + new.issuer = ?certificate.issuer_name(), + new.serial = ?existing.serial_as_hex()?, + "Skipped certificate as a cert with the same SHA256 hash was already added", + ); + } else { + info!( + subject = ?certificate.subject_name(), + issuer = ?certificate.issuer_name(), + not_before = ?certificate.not_before(), + not_after = ?certificate.not_after(), + serial = ?certificate.serial_as_hex()?, + ?source, + "Added certificate" + ); + certificates.insert(sha256.to_vec(), certificate); + } + } + } + + let pkcs12_truststore_bytes = + pkcs12_truststore(certificates.values().map(|c| &**c), &cli_args.out_password) + .whatever_context("failed to create PKCS12 truststore from certificates")?; + fs::write(&cli_args.out, &pkcs12_truststore_bytes).with_whatever_context(|_| { + format!( + "failed to write to output PKCS12 truststore at {:?}", + cli_args.out + ) + })?; + + Ok(()) +} diff --git a/rust/cert-tools/src/parsers.rs b/rust/cert-tools/src/parsers.rs new file mode 100644 index 00000000..57bdee41 --- /dev/null +++ b/rust/cert-tools/src/parsers.rs @@ -0,0 +1,110 @@ +use std::{ + io::Write, + process::{Command, Stdio}, +}; + +use openssl::{pkcs12::Pkcs12, x509::X509}; +use snafu::{OptionExt, ResultExt, whatever}; +use stackable_secret_operator_utils::pem::split_pem_certificates; + +pub fn parse_pem_contents(pem_bytes: &[u8]) -> Result, snafu::Whatever> { + let pems = split_pem_certificates(pem_bytes); + pems.into_iter() + .map(|pem| X509::from_pem(pem).whatever_context("failed to parse PEM encoded certificate")) + .collect() +} + +/// This function is how we would *should* do it. +/// +/// But with legacy old truststores generated by secret-operator (as of 2025-09), this fails with OpenSSL 3 because it +/// removed the old, legacy algorithms: +/// +/// `error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:crypto/evp/evp_fetch.c:355:Global default library context, Algorithm (RC2-40-CBC : 0), Properties ()` +/// +/// I tried this code to load the legacy provider: +/// +/// ```ignore +/// const LEGACY_PROVIDER_NAME: &str = "legacy"; +/// +/// unsafe { +/// let provider_name = +/// std::ffi::CString::new(LEGACY_PROVIDER_NAME).expect("constant CString is always valid"); +/// let provider = ffi::OSSL_PROVIDER_load(ptr::null_mut(), provider_name.as_ptr()); +/// if provider.is_null() { +/// bail!("Failed to load OpenSSL provider {LEGACY_PROVIDER_NAME}"); +/// } +/// } +/// ``` +/// +/// It helped a bit, but we got the next error: +/// +/// `error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:crypto/evp/evp_fetch.c:375:Global default library context, Algorithm (PKCS12KDF : 0), Properties (), error:1180006B:PKCS12 routines:pkcs12_gen_mac:key gen error:crypto/pkcs12/p12_mutl.c:267:, error:1180006D:PKCS12 routines:PKCS12_verify_mac:mac generation error:crypto/pkcs12/p12_mutl.c:331:, error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:crypto/evp/evp_fetch.c:375:Global default library context, Algorithm (PKCS12KDF : 0), Properties (), error:1180006B:PKCS12 routines:pkcs12_gen_mac:key gen error:crypto/pkcs12/p12_mutl.c:267:, error:1180006D:PKCS12 routines:PKCS12_verify_mac:mac generation error:crypto/pkcs12/p12_mutl.c:331:, error:11800071:PKCS12 routines:PKCS12_parse:mac verify failure:crypto/pkcs12/p12_kiss.c:67:` +/// +/// So I ditched that effort and we are now shelling out to the CLI. Sorry! +/// The proper solution would be that secret-operator writes PKCS12 truststores using modern algorithms. +/// For that we probably(?) drop the p12 crate? +#[allow(unused)] +pub fn parse_pkcs12_file( + file_contents: &[u8], + password: &str, +) -> Result, snafu::Whatever> { + let parsed = Pkcs12::from_der(file_contents) + .whatever_context("failed to parse PKCS12 DER encoded file")? + .parse2(password) + .whatever_context("Failed to parse PKCS12 using the provided password")?; + + parsed + .ca + .whatever_context("pkcs12 truststore did not contain a CA")? + .into_iter() + .map(Ok) + .collect() +} + +/// Workaround for [`parse_pkcs12_file`]. Please read it's documentation for details. +/// +/// Yes, I hate it as well... +pub fn parse_pkcs12_file_workaround( + file_contents: &[u8], + password: &str, +) -> Result, snafu::Whatever> { + let mut child = Command::new("openssl") + .args(&[ + "pkcs12", + "-nokeys", + "-password", + &format!("pass:{password}"), + // That's the important part!!! + "-legacy", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .whatever_context("Failed to spawn openssl process")?; + + { + let stdin = child + .stdin + .as_mut() + .whatever_context("Failed to open openssl process stdin")?; + stdin + .write_all(file_contents) + .whatever_context("Failed to write PKCS12 data to openssl process stdin")?; + } + + let output = child + .wait_with_output() + .whatever_context("Failed to read openssl process output")?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + whatever!("openssl process failed with STDERR:\n{stderr}"); + } + + parse_pem_contents(&output.stdout).with_whatever_context(|_| { + format!( + "failed to parse openssl process output, which should be PEM. STDOUT:\n{stdout}", + stdout = String::from_utf8_lossy(&output.stdout) + ) + }) +} diff --git a/rust/crd-utils/Cargo.toml b/rust/crd-utils/Cargo.toml deleted file mode 100644 index a1f84185..00000000 --- a/rust/crd-utils/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "stackable-secret-operator-crd-utils" -version.workspace = true -authors.workspace = true -license.workspace = true -edition.workspace = true -repository.workspace = true -publish = false - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -serde.workspace = true -stackable-operator.workspace = true diff --git a/rust/krb5-provision-keytab/Cargo.toml b/rust/krb5-provision-keytab/Cargo.toml index b0e9b622..823a6102 100644 --- a/rust/krb5-provision-keytab/Cargo.toml +++ b/rust/krb5-provision-keytab/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true publish = false [dependencies] -stackable-secret-operator-crd-utils = { path = "../crd-utils" } +stackable-secret-operator-utils = { path = "../utils", features = ["crd"] } krb5.workspace = true byteorder.workspace = true diff --git a/rust/krb5-provision-keytab/src/active_directory.rs b/rust/krb5-provision-keytab/src/active_directory.rs index f30de6b7..5aee86e9 100644 --- a/rust/krb5-provision-keytab/src/active_directory.rs +++ b/rust/krb5-provision-keytab/src/active_directory.rs @@ -14,7 +14,7 @@ use stackable_operator::{ k8s_openapi::api::core::v1::Secret, kube::{self, runtime::reflector::ObjectRef}, }; -use stackable_secret_operator_crd_utils::SecretReference; +use stackable_secret_operator_utils::crd::SecretReference; use crate::credential_cache::{self, CredentialCache}; diff --git a/rust/krb5-provision-keytab/src/credential_cache.rs b/rust/krb5-provision-keytab/src/credential_cache.rs index af201d02..01f9d498 100644 --- a/rust/krb5-provision-keytab/src/credential_cache.rs +++ b/rust/krb5-provision-keytab/src/credential_cache.rs @@ -8,7 +8,7 @@ use stackable_operator::{ runtime::reflector::ObjectRef, }, }; -use stackable_secret_operator_crd_utils::SecretReference; +use stackable_secret_operator_utils::crd::SecretReference; const OPERATOR_NAME: &str = "secrets.stackable.tech"; const FIELD_MANAGER_SCOPE: &str = "krb5-provision-keytab"; diff --git a/rust/krb5-provision-keytab/src/lib.rs b/rust/krb5-provision-keytab/src/lib.rs index 2bb1f5a8..7c94df92 100644 --- a/rust/krb5-provision-keytab/src/lib.rs +++ b/rust/krb5-provision-keytab/src/lib.rs @@ -7,7 +7,7 @@ use std::{ use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Snafu}; -use stackable_secret_operator_crd_utils::SecretReference; +use stackable_secret_operator_utils::crd::SecretReference; use tokio::{io::AsyncWriteExt, process::Command}; #[derive(Serialize, Deserialize)] diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index f325c501..9c4e2a0d 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -10,7 +10,7 @@ publish = false [dependencies] stackable-krb5-provision-keytab = { path = "../krb5-provision-keytab" } -stackable-secret-operator-crd-utils = { path = "../crd-utils" } +stackable-secret-operator-utils = { path = "../utils", features = ["crd"] } p12 = { path = "../p12" } anyhow.workspace = true @@ -40,7 +40,6 @@ tonic-prost.workspace = true tonic-reflection.workspace = true tracing.workspace = true uuid.workspace = true -yasna.workspace = true rand.workspace = true const_format.workspace = true diff --git a/rust/operator-binary/src/backend/kerberos_keytab.rs b/rust/operator-binary/src/backend/kerberos_keytab.rs index 0efe4da4..70ecd009 100644 --- a/rust/operator-binary/src/backend/kerberos_keytab.rs +++ b/rust/operator-binary/src/backend/kerberos_keytab.rs @@ -10,7 +10,7 @@ use stackable_operator::{ k8s_openapi::api::core::v1::Secret, kube::runtime::reflector::ObjectRef, }; -use stackable_secret_operator_crd_utils::SecretReference; +use stackable_secret_operator_utils::crd::SecretReference; use tempfile::tempdir; use tokio::{ fs::File, diff --git a/rust/operator-binary/src/backend/tls/ca.rs b/rust/operator-binary/src/backend/tls/ca.rs index 1ee1d0d2..d7fdb2c8 100644 --- a/rust/operator-binary/src/backend/tls/ca.rs +++ b/rust/operator-binary/src/backend/tls/ca.rs @@ -32,7 +32,7 @@ use stackable_operator::{ }, shared::time::Duration, }; -use stackable_secret_operator_crd_utils::{ConfigMapReference, SecretReference}; +use stackable_secret_operator_utils::crd::{ConfigMapReference, SecretReference}; use time::OffsetDateTime; use tracing::{info, info_span, warn}; diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 51a4d678..41b156f7 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -6,7 +6,7 @@ use stackable_operator::{ shared::time::Duration, versioned::versioned, }; -use stackable_secret_operator_crd_utils::{ConfigMapReference, SecretReference}; +use stackable_secret_operator_utils::crd::{ConfigMapReference, SecretReference}; use crate::format::SecretFormat; diff --git a/rust/operator-binary/src/format/convert.rs b/rust/operator-binary/src/format/convert.rs index d4cc3e79..a7d837e6 100644 --- a/rust/operator-binary/src/format/convert.rs +++ b/rust/operator-binary/src/format/convert.rs @@ -1,17 +1,14 @@ -use openssl::{ - error::ErrorStack as OpensslError, - pkcs12::Pkcs12, - pkey::PKey, - stack::Stack, - x509::{X509, X509Ref}, +use openssl::{pkcs12::Pkcs12, pkey::PKey, stack::Stack, x509::X509}; +use snafu::{ResultExt, Snafu}; +use stackable_secret_operator_utils::{ + pem::split_pem_certificates, + pkcs12::{TlsToPkcs12Error, pkcs12_truststore}, }; -use snafu::{OptionExt, ResultExt, Snafu}; use super::{ SecretFormat, WellKnownSecretData, well_known::{CompatibilityOptions, TlsPem, TlsPkcs12}, }; -use crate::format::utils::split_pem_certificates; pub fn convert( from: WellKnownSecretData, @@ -52,7 +49,7 @@ pub fn convert_tls_to_pkcs12( pem: TlsPem, p12_password: &str, ) -> Result { - use tls_to_pkcs12_error::*; + use stackable_secret_operator_utils::pkcs12::tls_to_pkcs12_error::*; let cert = pem .certificate_pem .map(|cert| X509::from_pem(&cert).context(LoadCertSnafu)) @@ -85,119 +82,3 @@ pub fn convert_tls_to_pkcs12( .transpose()?, }) } - -fn bmp_string(s: &str) -> Vec { - s.encode_utf16() - .chain([0]) // null-termination character - .flat_map(u16::to_be_bytes) - .collect() -} - -fn pkcs12_truststore<'a>( - ca_list: impl IntoIterator, - p12_password: &str, -) -> Result, TlsToPkcs12Error> { - // We can't use OpenSSL's `Pkcs12`, since it doesn't let us add new attributes to the SafeBags being created, - // and Java refuses to trust CA bags without the `java_trusted_ca_oid` attribute set. - // OpenSSL's current master branch contains the `PKCS12_create_ex2` function - // (https://www.openssl.org/docs/manmaster/man3/PKCS12_create_ex.html), but it is not currently in - // OpenSSL 3.1 (as of 3.1.1), and it is not wrapped by rust-openssl. - - // Required for Java to trust the certificate, from - // https://github.com/openjdk/jdk/blob/990e3a700dce3441bd9506ca571c1790e57849a9/src/java.base/share/classes/sun/security/util/KnownOIDs.java#L414-L415 - let java_oracle_trusted_key_usage_oid = - yasna::models::ObjectIdentifier::from_slice(&[2, 16, 840, 1, 113894, 746875, 1, 1]); - - // We don't care about actually encrypting the truststore securely, but if we use a random salt then the pkcs#12 bundle will be different for every write - // (=> TrustStore controller will get stuck reconciling indefinitely.) - // So let's just use a fixed salt instead. - struct DummyRng; - impl p12::Rng for DummyRng { - fn generate_salt(&mut self) -> Option<[u8; 8]> { - Some([0; 8]) - } - } - - let mut truststore_bags = Vec::new(); - for ca in ca_list { - truststore_bags.push(p12::SafeBag { - bag: p12::SafeBagKind::CertBag(p12::CertBag::X509( - ca.to_der() - .context(tls_to_pkcs12_error::SerializeCaForTruststoreSnafu)?, - )), - attributes: vec![p12::PKCS12Attribute::Other(p12::OtherAttribute { - oid: java_oracle_trusted_key_usage_oid.clone(), - data: Vec::new(), - })], - }); - } - let password_as_bmp_string = bmp_string(p12_password); - let encrypted_data = p12::ContentInfo::EncryptedData( - p12::EncryptedData::from_safe_bags( - &truststore_bags[..], - &password_as_bmp_string, - &mut DummyRng, - ) - .context(tls_to_pkcs12_error::EncryptDataForTruststoreSnafu)?, - ); - let truststore_data = yasna::construct_der(|w| { - w.write_sequence_of(|w| { - encrypted_data.write(w.next()); - }); - }); - Ok(p12::PFX { - version: 3, - mac_data: Some(p12::MacData::new( - &truststore_data, - &password_as_bmp_string, - &mut DummyRng, - )), - auth_safe: p12::ContentInfo::Data(truststore_data), - } - .to_der()) -} - -#[derive(Snafu, Debug)] -#[snafu(module)] -pub enum TlsToPkcs12Error { - #[snafu(display("failed to load certificate"))] - LoadCert { source: OpensslError }, - - #[snafu(display("failed to load private key"))] - LoadKey { source: OpensslError }, - - #[snafu(display("failed to load CA certificate"))] - LoadCa { source: OpensslError }, - - #[snafu(display("failed to build keystore"))] - BuildKeystore { source: OpensslError }, - - #[snafu(display("failed to serialize CA certificate for truststore"))] - SerializeCaForTruststore { source: OpensslError }, - - #[snafu(display("failed to encrypt data for truststore"))] - EncryptDataForTruststore, -} - -#[cfg(test)] -mod tests { - use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa, x509::X509}; - - use crate::format::convert::pkcs12_truststore; - - #[test] - fn pkcs12_truststore_should_be_deterministic() -> anyhow::Result<()> { - let pkey = PKey::try_from(Rsa::generate(2048)?)?; - let mut x509 = X509::builder()?; - x509.set_pubkey(&pkey)?; - x509.set_version(3 - 1)?; - x509.sign(&pkey, MessageDigest::sha256())?; - let cert = x509.build(); - let password = ""; - assert_eq!( - pkcs12_truststore([cert.as_ref()], password)?, - pkcs12_truststore([cert.as_ref()], password)?, - ); - Ok(()) - } -} diff --git a/rust/operator-binary/src/format/mod.rs b/rust/operator-binary/src/format/mod.rs index 9995a5f2..7a268b9c 100644 --- a/rust/operator-binary/src/format/mod.rs +++ b/rust/operator-binary/src/format/mod.rs @@ -10,7 +10,6 @@ pub use self::{ use crate::format::well_known::NamingOptions; mod convert; -mod utils; pub mod well_known; pub type SecretFiles = HashMap>; diff --git a/rust/operator-binary/src/format/utils.rs b/rust/operator-binary/src/format/utils.rs deleted file mode 100644 index 6799f5e4..00000000 --- a/rust/operator-binary/src/format/utils.rs +++ /dev/null @@ -1,60 +0,0 @@ -/// Splits a byte sequence of PEM-encoded certificates. -pub fn split_pem_certificates(pem: &[u8]) -> impl Iterator { - SplitPemCertificates { - pem_iter: pem.iter(), - } -} -struct SplitPemCertificates<'a> { - pem_iter: std::slice::Iter<'a, u8>, -} -impl<'a> Iterator for SplitPemCertificates<'a> { - type Item = &'a [u8]; - - fn next(&mut self) -> Option { - const HEADER: &[u8] = b"-----BEGIN CERTIFICATE-----"; - let slice = self.pem_iter.as_slice(); - if slice.is_empty() { - return None; - } - let mut len = 0; - while let Some(chr) = self.pem_iter.next() { - len += 1; - if *chr == b'\n' && self.pem_iter.as_slice().starts_with(HEADER) { - break; - } - } - Some(&slice[..len]) - } -} - -#[cfg(test)] -mod tests { - use crate::format::utils::split_pem_certificates; - - #[test] - fn test_split_pem_certificates() { - assert_eq!( - split_pem_certificates( - b"-----BEGIN CERTIFICATE----- -foo ------BEGIN CERTIFICATE----- -bar ------BEGIN CERTIFICATE----- -baz -" - ) - .collect::>(), - vec![ - b"-----BEGIN CERTIFICATE----- -foo -", - b"-----BEGIN CERTIFICATE----- -bar -", - b"-----BEGIN CERTIFICATE----- -baz -", - ] - ) - } -} diff --git a/rust/utils/Cargo.toml b/rust/utils/Cargo.toml new file mode 100644 index 00000000..f24b7049 --- /dev/null +++ b/rust/utils/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "stackable-secret-operator-utils" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +publish = false + +[features] +default = [] +crd = ["dep:stackable-operator", "dep:serde"] + +[dependencies] +p12 = { path = "../p12" } +stackable-operator = { workspace = true, optional = true } + +openssl.workspace = true +serde = { workspace = true, optional = true } +snafu.workspace = true +yasna.workspace = true + +[dev-dependencies] +anyhow.workspace = true diff --git a/rust/crd-utils/src/lib.rs b/rust/utils/src/crd.rs similarity index 100% rename from rust/crd-utils/src/lib.rs rename to rust/utils/src/crd.rs diff --git a/rust/utils/src/lib.rs b/rust/utils/src/lib.rs new file mode 100644 index 00000000..6511a347 --- /dev/null +++ b/rust/utils/src/lib.rs @@ -0,0 +1,5 @@ +pub mod pem; +pub mod pkcs12; + +#[cfg(feature = "crd")] +pub mod crd; diff --git a/rust/utils/src/pem.rs b/rust/utils/src/pem.rs new file mode 100644 index 00000000..529ac4b1 --- /dev/null +++ b/rust/utils/src/pem.rs @@ -0,0 +1,169 @@ +/// Splits a byte sequence of PEM-encoded certificates. +/// +/// It can tolerate additional contents between the actual PEM certificates, as e.g. the +/// `openssl pkcs12` command produces. +pub fn split_pem_certificates(pem: &[u8]) -> Vec<&[u8]> { + const HEADER: &[u8] = b"-----BEGIN CERTIFICATE-----"; + const FOOTER: &[u8] = b"-----END CERTIFICATE-----"; + + let mut certs = Vec::new(); + let mut pos = 0; + + while pos + HEADER.len() <= pem.len() { + // Find the next header + if &pem[pos..pos + HEADER.len()] != HEADER { + pos += 1; + continue; + } + + let start = pos; + pos += HEADER.len(); + + // Find the matching footer + while pos + FOOTER.len() <= pem.len() { + if &pem[pos..pos + FOOTER.len()] == FOOTER { + pos += FOOTER.len(); // include footer + certs.push(&pem[start..pos]); + break; + } + pos += 1; + } + } + + certs +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_split_pem_certificates() { + assert_eq!( + split_pem_certificates( + b"-----BEGIN CERTIFICATE----- +foo +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +bar +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +baz +-----END CERTIFICATE----- +" + ), + vec![ + b"-----BEGIN CERTIFICATE----- +foo +-----END CERTIFICATE-----", + b"-----BEGIN CERTIFICATE----- +bar +-----END CERTIFICATE-----", + b"-----BEGIN CERTIFICATE----- +baz +-----END CERTIFICATE-----", + ] + ) + } + + #[test] + fn test_split_openssl_cli_pkcs12_output() { + // openssl pkcs12 -in truststore.p12 -password pass: -nokeys -legacy + let cli_output = b" +Bag Attributes + Trusted key usage (Oracle): +subject=CN=secret-operator self-signed +issuer=CN=secret-operator self-signed +-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIIKt7H+4AWKFYwDQYJKoZIhvcNAQELBQAwJjEkMCIGA1UE +Awwbc2VjcmV0LW9wZXJhdG9yIHNlbGYtc2lnbmVkMB4XDTI1MDkwMzExMDIwMVoX +DTI1MDkwMzExMDgwMVowJjEkMCIGA1UEAwwbc2VjcmV0LW9wZXJhdG9yIHNlbGYt +c2lnbmVkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAviEC2WtidVLN +qU6BO8qQ3PPThYBfia6UbfU8y5k8qPKHOJhYjtCKPTqCD82ht/UgzoXJ4zzqKL9B +2cBid+zj3/fxSRDKaPBMQvthC13M6zOz5ig/Ry24iaIaiz5ASDuqaQ9Hw/Y7viPB +pxkypTR59tYHa4+1D8xPtQCUixpxgxfRPAehZibrlP8TZrb6wSEjuicXljh9pevn +jw/TxFcNZVHgDw2N6RqhgaurcS/i4ScWxELXrdqi1K6G2twcWw2SPiU3xujXAMG7 +lGISeJJnecD/rHzMT13TYqmbu65tSrVfG9YRqbGqgMfk5faFzCoIZZ447OA2coE7 +JA/CJ3djVQIDAQABo00wSzAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQ1LwpK +L93s9BJgZpJ6vRnX2SzzMzAJBgNVHSMEAjAAMA4GA1UdDwEB/wQEAwIBhjANBgkq +hkiG9w0BAQsFAAOCAQEADOrNsdHBc4qf8wJNmm2RIRaepJDxsMiCmEh9A7JLFZEb +IsOIbynhAvfHCCZqSDeRNaMdxjKax6STy6QDy/U5jrwAFS1/1yFR7zqGE9IDOmrs +5uxDCYU23p1fIjvr+Uz7lk/EwXVTtqjzLAp+NP4YIQNFWfdKv6me3F2Czz4yfTRj +sZ5ggeW3l6nFHGTDkdXqGs9BSTvckUVIUV6o0x2Opl05gS4TrxiTYpVYK0a3ofib +nHJm5NEUs17nlq9n5u3zP49d0WEpoEseahRBBp7/coC/yc4M3JHnEHccW/zjZ2U0 +khi8URKEGEx1aL1Uu8D3Xd8BLXOjDjZWn8A0hznRGQ== +-----END CERTIFICATE----- +Bag Attributes + Trusted key usage (Oracle): +subject=CN=secret-operator self-signed +issuer=CN=secret-operator self-signed +-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIIDkvHj4cwRngwDQYJKoZIhvcNAQELBQAwJjEkMCIGA1UE +Awwbc2VjcmV0LW9wZXJhdG9yIHNlbGYtc2lnbmVkMB4XDTI1MDkwMzExMDI0MFoX +DTI1MDkwMzExMDg0MFowJjEkMCIGA1UEAwwbc2VjcmV0LW9wZXJhdG9yIHNlbGYt +c2lnbmVkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp5EBsqaJItLd +3UtgC9bWv3VIprp3+FQPobQCMQpW7jne4gC11QxBuuIDN+LqlAZSfwt8UHy5B/LW +MUzgu+kfvXxZkYZsKmGNDi9GSH/fPkOV+rBDG6BOsXdQCmSPJZjCLuNWuysYfI3L +CQVoV0rG8H+APrX7N3vLosph3chYWgwb0teaKlqlGROVFiFISuMezSdyCkJbHkXB +Qjicj96FGa+jJX9zULJt07AWl2wsFbCL/+bDyOQs4LNQ+yQnhxXhepo4M9haLxrM +sd6JvmeCKNf17OSVe4a1rGc0hpZ+80AJ3D+cfMBoBPGkAk7njAI7HzpqNd/qFZo+ +elfX3v4PQwIDAQABo00wSzAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQ3k3Wa +ZJsuVxSQaKZMFbmEviqTlzAJBgNVHSMEAjAAMA4GA1UdDwEB/wQEAwIBhjANBgkq +hkiG9w0BAQsFAAOCAQEAe851UwqPYYxKsCbOsIVqe9aamPOQ+70PNaEALWfkEpVS +kbJcsjw7m4wmG0m/Y4fTL+sdppYhShSPOV5vn09mW04hjOlaKRUyIGhkp9qaTIFh +bznnQ19zvnfHci8rs1LCIgjGL+iZ8aoUE8nyOeSbm1A8Yc0NcKw/WdC+MJ3jyFqe +hjfFpCCYu94nKhCQ5RhCfQBHmZ/IzwxTSDUUE3PoD2g4Rex9etISPY1CV5cJBjgv +VYIk9WlwUc6mepdY3CX/Oko6WEilm2y1zdKohGrsEnTeN6oG2l9XFdvqj71UBNop +iVAldveMLcVOv2D9jU48lYJFRagJc6wpCBOK0/Exjg== +-----END CERTIFICATE----- + "; + + assert_eq!( + split_pem_certificates(cli_output) + .iter() + .map(|bytes| String::from_utf8(bytes.to_vec()) + .expect("PEM certificate is not valid utf-8")) + .collect::>(), + vec![ + "-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIIKt7H+4AWKFYwDQYJKoZIhvcNAQELBQAwJjEkMCIGA1UE +Awwbc2VjcmV0LW9wZXJhdG9yIHNlbGYtc2lnbmVkMB4XDTI1MDkwMzExMDIwMVoX +DTI1MDkwMzExMDgwMVowJjEkMCIGA1UEAwwbc2VjcmV0LW9wZXJhdG9yIHNlbGYt +c2lnbmVkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAviEC2WtidVLN +qU6BO8qQ3PPThYBfia6UbfU8y5k8qPKHOJhYjtCKPTqCD82ht/UgzoXJ4zzqKL9B +2cBid+zj3/fxSRDKaPBMQvthC13M6zOz5ig/Ry24iaIaiz5ASDuqaQ9Hw/Y7viPB +pxkypTR59tYHa4+1D8xPtQCUixpxgxfRPAehZibrlP8TZrb6wSEjuicXljh9pevn +jw/TxFcNZVHgDw2N6RqhgaurcS/i4ScWxELXrdqi1K6G2twcWw2SPiU3xujXAMG7 +lGISeJJnecD/rHzMT13TYqmbu65tSrVfG9YRqbGqgMfk5faFzCoIZZ447OA2coE7 +JA/CJ3djVQIDAQABo00wSzAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQ1LwpK +L93s9BJgZpJ6vRnX2SzzMzAJBgNVHSMEAjAAMA4GA1UdDwEB/wQEAwIBhjANBgkq +hkiG9w0BAQsFAAOCAQEADOrNsdHBc4qf8wJNmm2RIRaepJDxsMiCmEh9A7JLFZEb +IsOIbynhAvfHCCZqSDeRNaMdxjKax6STy6QDy/U5jrwAFS1/1yFR7zqGE9IDOmrs +5uxDCYU23p1fIjvr+Uz7lk/EwXVTtqjzLAp+NP4YIQNFWfdKv6me3F2Czz4yfTRj +sZ5ggeW3l6nFHGTDkdXqGs9BSTvckUVIUV6o0x2Opl05gS4TrxiTYpVYK0a3ofib +nHJm5NEUs17nlq9n5u3zP49d0WEpoEseahRBBp7/coC/yc4M3JHnEHccW/zjZ2U0 +khi8URKEGEx1aL1Uu8D3Xd8BLXOjDjZWn8A0hznRGQ== +-----END CERTIFICATE-----", + "-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIIDkvHj4cwRngwDQYJKoZIhvcNAQELBQAwJjEkMCIGA1UE +Awwbc2VjcmV0LW9wZXJhdG9yIHNlbGYtc2lnbmVkMB4XDTI1MDkwMzExMDI0MFoX +DTI1MDkwMzExMDg0MFowJjEkMCIGA1UEAwwbc2VjcmV0LW9wZXJhdG9yIHNlbGYt +c2lnbmVkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp5EBsqaJItLd +3UtgC9bWv3VIprp3+FQPobQCMQpW7jne4gC11QxBuuIDN+LqlAZSfwt8UHy5B/LW +MUzgu+kfvXxZkYZsKmGNDi9GSH/fPkOV+rBDG6BOsXdQCmSPJZjCLuNWuysYfI3L +CQVoV0rG8H+APrX7N3vLosph3chYWgwb0teaKlqlGROVFiFISuMezSdyCkJbHkXB +Qjicj96FGa+jJX9zULJt07AWl2wsFbCL/+bDyOQs4LNQ+yQnhxXhepo4M9haLxrM +sd6JvmeCKNf17OSVe4a1rGc0hpZ+80AJ3D+cfMBoBPGkAk7njAI7HzpqNd/qFZo+ +elfX3v4PQwIDAQABo00wSzAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQ3k3Wa +ZJsuVxSQaKZMFbmEviqTlzAJBgNVHSMEAjAAMA4GA1UdDwEB/wQEAwIBhjANBgkq +hkiG9w0BAQsFAAOCAQEAe851UwqPYYxKsCbOsIVqe9aamPOQ+70PNaEALWfkEpVS +kbJcsjw7m4wmG0m/Y4fTL+sdppYhShSPOV5vn09mW04hjOlaKRUyIGhkp9qaTIFh +bznnQ19zvnfHci8rs1LCIgjGL+iZ8aoUE8nyOeSbm1A8Yc0NcKw/WdC+MJ3jyFqe +hjfFpCCYu94nKhCQ5RhCfQBHmZ/IzwxTSDUUE3PoD2g4Rex9etISPY1CV5cJBjgv +VYIk9WlwUc6mepdY3CX/Oko6WEilm2y1zdKohGrsEnTeN6oG2l9XFdvqj71UBNop +iVAldveMLcVOv2D9jU48lYJFRagJc6wpCBOK0/Exjg== +-----END CERTIFICATE-----" + ] + ); + } +} diff --git a/rust/utils/src/pkcs12.rs b/rust/utils/src/pkcs12.rs new file mode 100644 index 00000000..4c6cde13 --- /dev/null +++ b/rust/utils/src/pkcs12.rs @@ -0,0 +1,119 @@ +use openssl::{error::ErrorStack as OpensslError, x509::X509Ref}; +use snafu::{OptionExt, ResultExt, Snafu}; + +#[derive(Snafu, Debug)] +#[snafu(module, visibility(pub))] +pub enum TlsToPkcs12Error { + #[snafu(display("failed to load certificate"))] + LoadCert { source: OpensslError }, + + #[snafu(display("failed to load private key"))] + LoadKey { source: OpensslError }, + + #[snafu(display("failed to load CA certificate"))] + LoadCa { source: OpensslError }, + + #[snafu(display("failed to build keystore"))] + BuildKeystore { source: OpensslError }, + + #[snafu(display("failed to serialize CA certificate for truststore"))] + SerializeCaForTruststore { source: OpensslError }, + + #[snafu(display("failed to encrypt data for truststore"))] + EncryptDataForTruststore, +} + +pub fn pkcs12_truststore<'a>( + ca_list: impl IntoIterator, + p12_password: &str, +) -> Result, TlsToPkcs12Error> { + // We can't use OpenSSL's `Pkcs12`, since it doesn't let us add new attributes to the SafeBags being created, + // and Java refuses to trust CA bags without the `java_trusted_ca_oid` attribute set. + // OpenSSL's current master branch contains the `PKCS12_create_ex2` function + // (https://www.openssl.org/docs/manmaster/man3/PKCS12_create_ex.html), but it is not currently in + // OpenSSL 3.1 (as of 3.1.1), and it is not wrapped by rust-openssl. + // See https://github.com/sfackler/rust-openssl/blob/d21f42333698edeebd7327a4e25412b191757e31/openssl/src/pkcs12.rs#L225 + + // Required for Java to trust the certificate, from + // https://github.com/openjdk/jdk/blob/990e3a700dce3441bd9506ca571c1790e57849a9/src/java.base/share/classes/sun/security/util/KnownOIDs.java#L414-L415 + let java_oracle_trusted_key_usage_oid = + yasna::models::ObjectIdentifier::from_slice(&[2, 16, 840, 1, 113894, 746875, 1, 1]); + + // We don't care about actually encrypting the truststore securely, but if we use a random salt then the pkcs#12 bundle will be different for every write + // (=> TrustStore controller will get stuck reconciling indefinitely.) + // So let's just use a fixed salt instead. + struct DummyRng; + impl p12::Rng for DummyRng { + fn generate_salt(&mut self) -> Option<[u8; 8]> { + Some([0; 8]) + } + } + + let mut truststore_bags = Vec::new(); + for ca in ca_list { + truststore_bags.push(p12::SafeBag { + bag: p12::SafeBagKind::CertBag(p12::CertBag::X509( + ca.to_der() + .context(tls_to_pkcs12_error::SerializeCaForTruststoreSnafu)?, + )), + attributes: vec![p12::PKCS12Attribute::Other(p12::OtherAttribute { + oid: java_oracle_trusted_key_usage_oid.clone(), + data: Vec::new(), + })], + }); + } + let password_as_bmp_string = bmp_string(p12_password); + let encrypted_data = p12::ContentInfo::EncryptedData( + p12::EncryptedData::from_safe_bags( + &truststore_bags[..], + &password_as_bmp_string, + &mut DummyRng, + ) + .context(tls_to_pkcs12_error::EncryptDataForTruststoreSnafu)?, + ); + let truststore_data = yasna::construct_der(|w| { + w.write_sequence_of(|w| { + encrypted_data.write(w.next()); + }); + }); + Ok(p12::PFX { + version: 3, + mac_data: Some(p12::MacData::new( + &truststore_data, + &password_as_bmp_string, + &mut DummyRng, + )), + auth_safe: p12::ContentInfo::Data(truststore_data), + } + .to_der()) +} + +fn bmp_string(s: &str) -> Vec { + s.encode_utf16() + .chain([0]) // null-termination character + .flat_map(u16::to_be_bytes) + .collect() +} + +#[cfg(test)] +mod tests { + use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa, x509::X509}; + + use super::*; + + #[test] + fn pkcs12_truststore_should_be_deterministic() -> anyhow::Result<()> { + let pkey = PKey::try_from(Rsa::generate(2048)?)?; + let mut x509 = X509::builder()?; + x509.set_pubkey(&pkey)?; + x509.set_version(3 - 1)?; + x509.sign(&pkey, MessageDigest::sha256())?; + let cert = x509.build(); + let password = ""; + assert_eq!( + pkcs12_truststore([cert.as_ref()], password)?, + pkcs12_truststore([cert.as_ref()], password)?, + ); + Ok(()) + } +}