diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a0c575..934b01bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file. - Use `--file-log-max-files` (or `FILE_LOG_MAX_FILES`) to limit the number of log files kept. - Use `--file-log-rotation-period` (or `FILE_LOG_ROTATION_PERIOD`) to configure the frequency of rotation. - Use `--console-log-format` (or `CONSOLE_LOG_FORMAT`) to set the format to `plain` (default) or `json`. +- Added TrustStore CRD for requesting CA certificate information ([#557]). ### Changed @@ -38,6 +39,7 @@ All notable changes to this project will be documented in this file. - Use `json` file extension for log files ([#586]). +[#557]: https://github.com/stackabletech/secret-operator/pull/557 [#572]: https://github.com/stackabletech/secret-operator/pull/572 [#581]: https://github.com/stackabletech/secret-operator/pull/581 [#586]: https://github.com/stackabletech/secret-operator/pull/586 diff --git a/Cargo.lock b/Cargo.lock index 0258d125..b79365de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1140,6 +1140,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + [[package]] name = "hmac" version = "0.12.1" @@ -2187,14 +2193,13 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "p12" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4873306de53fe82e7e484df31e1e947d61514b6ea2ed6cd7b45d63006fd9224" +version = "0.0.0-dev" dependencies = [ "cbc", "cipher", "des", "getrandom 0.2.15", + "hex-literal", "hmac", "lazy_static", "rc2", @@ -3122,8 +3127,10 @@ dependencies = [ "async-trait", "built", "clap", + "const_format", "futures 0.3.31", "h2", + "kube-runtime", "libc", "openssl", "p12", diff --git a/Cargo.nix b/Cargo.nix index db403867..dec2fb64 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -58,6 +58,16 @@ rec { # File a bug if you depend on any for non-debug work! debug = internal.debugCrate { inherit packageId; }; }; + "p12" = rec { + packageId = "p12"; + build = internal.buildRustCrateWithFeatures { + packageId = "p12"; + }; + + # 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-krb5-provision-keytab" = rec { packageId = "stackable-krb5-provision-keytab"; build = internal.buildRustCrateWithFeatures { @@ -3571,6 +3581,18 @@ rec { edition = "2021"; sha256 = "1sjmpsdl8czyh9ywl3qcsfsq9a307dg4ni2vnlwgnzzqhc4y0113"; + }; + "hex-literal" = rec { + crateName = "hex-literal"; + version = "0.3.4"; + edition = "2018"; + sha256 = "1q54yvyy0zls9bdrx15hk6yj304npndy9v4crn1h1vd95sfv5gby"; + procMacro = true; + libName = "hex_literal"; + authors = [ + "RustCrypto Developers" + ]; + }; "hmac" = rec { crateName = "hmac"; @@ -5769,6 +5791,7 @@ rec { features = { "unstable-runtime" = [ "unstable-runtime-subscribe" "unstable-runtime-stream-control" "unstable-runtime-reconcile-on" ]; }; + resolvedDefaultFeatures = [ "unstable-runtime-stream-control" ]; }; "lazy_static" = rec { crateName = "lazy_static"; @@ -7185,9 +7208,9 @@ rec { }; "p12" = rec { crateName = "p12"; - version = "0.6.3"; + version = "0.0.0-dev"; edition = "2021"; - sha256 = "094jzl331mj5gg6xcbpanqa1bmj7x7hk3pw4wkkq5zjkvq3371yl"; + src = lib.cleanSourceWith { filter = sourceFilter; src = ./rust/p12; }; authors = [ "hjiayz " "Marc-Antoine Perennou " @@ -7233,6 +7256,12 @@ rec { features = [ "std" ]; } ]; + devDependencies = [ + { + name = "hex-literal"; + packageId = "hex-literal"; + } + ]; }; "parking" = rec { @@ -10252,6 +10281,10 @@ rec { name = "clap"; packageId = "clap"; } + { + name = "const_format"; + packageId = "const_format"; + } { name = "futures"; packageId = "futures 0.3.31"; @@ -10261,6 +10294,11 @@ rec { name = "h2"; packageId = "h2"; } + { + name = "kube-runtime"; + packageId = "kube-runtime"; + features = [ "unstable-runtime-stream-control" ]; + } { name = "libc"; packageId = "libc"; diff --git a/Cargo.toml b/Cargo.toml index 05febd4d..54e8530a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,13 +19,14 @@ bindgen = "0.71" built = { version = "0.7", features = ["chrono", "git2"] } byteorder = "1.5" clap = "4.5" +const_format = "0.2.34" futures = { version = "0.3", features = ["compat"] } h2 = "0.4" +kube-runtime = { version = "0.99", features = ["unstable-runtime-stream-control"] } ldap3 = { version = "0.11", default-features = false, features = ["gssapi", "tls"] } libc = "0.2" native-tls = "0.2" openssl = "0.10" -p12 = "0.6" pin-project = "1.1" pkg-config = "0.3" prost = "0.13" diff --git a/deploy/helm/secret-operator/crds/crds.yaml b/deploy/helm/secret-operator/crds/crds.yaml index 5909c60b..f04a9b8f 100644 --- a/deploy/helm/secret-operator/crds/crds.yaml +++ b/deploy/helm/secret-operator/crds/crds.yaml @@ -218,6 +218,15 @@ spec: description: The Secret objects are located in the same namespace as the Pod object. Should be used for Secrets that are provisioned by the application administrator. type: object type: object + trustStoreConfigMapName: + description: |- + Name of a ConfigMap that contains the information required to validate against this SecretClass. + + Resolved relative to `search_namespace`. + + Required to request a TrustStore for this SecretClass. + nullable: true + type: string required: - searchNamespace type: object @@ -346,3 +355,53 @@ spec: served: true storage: true subresources: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: truststores.secrets.stackable.tech + annotations: + helm.sh/resource-policy: keep +spec: + group: secrets.stackable.tech + names: + categories: [] + kind: TrustStore + plural: truststores + shortNames: [] + singular: truststore + scope: Namespaced + versions: + - additionalPrinterColumns: [] + name: v1alpha1 + schema: + openAPIV3Schema: + description: Auto-generated derived type for TrustStoreSpec via `CustomResource` + properties: + spec: + description: |- + A [TrustStore](https://docs.stackable.tech/home/nightly/secret-operator/truststore) requests information about how to validate secrets issued by a [SecretClass](https://docs.stackable.tech/home/nightly/secret-operator/secretclass). + + The requested information is written to a ConfigMap with the same name as the TrustStore. + properties: + format: + description: The [format](https://docs.stackable.tech/home/nightly/secret-operator/secretclass#format) that the data should be converted into. + enum: + - tls-pem + - tls-pkcs12 + - kerberos + nullable: true + type: string + secretClassName: + description: The name of the SecretClass that the request concerns. + type: string + required: + - secretClassName + type: object + required: + - spec + title: TrustStore + type: object + served: true + storage: true + subresources: {} diff --git a/deploy/helm/secret-operator/templates/roles.yaml b/deploy/helm/secret-operator/templates/roles.yaml index b6e36c36..aecff9f5 100644 --- a/deploy/helm/secret-operator/templates/roles.yaml +++ b/deploy/helm/secret-operator/templates/roles.yaml @@ -55,6 +55,16 @@ rules: - create - patch - update + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - patch + - get + - watch + - list - apiGroups: - "" resources: @@ -96,8 +106,11 @@ rules: - secrets.stackable.tech resources: - secretclasses + - truststores verbs: - get + - watch + - list - apiGroups: - listeners.stackable.tech resources: @@ -114,6 +127,13 @@ rules: - get - patch - create + - apiGroups: + - events.k8s.io + resources: + - events + verbs: + - create + - patch {{ if .Capabilities.APIVersions.Has "security.openshift.io/v1" }} - apiGroups: - security.openshift.io diff --git a/docs/modules/secret-operator/examples/secretclass-tls.yaml b/docs/modules/secret-operator/examples/secretclass-tls.yaml index 66db033d..a325ede3 100644 --- a/docs/modules/secret-operator/examples/secretclass-tls.yaml +++ b/docs/modules/secret-operator/examples/secretclass-tls.yaml @@ -17,3 +17,4 @@ spec: pod: {} # or... name: my-namespace + trustStoreConfigMapName: tls-ca # <4> diff --git a/docs/modules/secret-operator/examples/truststore-tls.yaml b/docs/modules/secret-operator/examples/truststore-tls.yaml new file mode 100644 index 00000000..b8239d8e --- /dev/null +++ b/docs/modules/secret-operator/examples/truststore-tls.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: TrustStore +metadata: + name: truststore-pem # <1> +spec: + secretClassName: tls # <2> + format: tls-pem # <3> diff --git a/docs/modules/secret-operator/pages/secretclass.adoc b/docs/modules/secret-operator/pages/secretclass.adoc index bae74638..9bfbc8a3 100644 --- a/docs/modules/secret-operator/pages/secretclass.adoc +++ b/docs/modules/secret-operator/pages/secretclass.adoc @@ -17,6 +17,7 @@ include::example$secretclass-tls.yaml[] <1> Backends are mutually exclusive, only one may be used by each SecretClass <2> Configures and selects the xref:#backend-autotls[] backend <3> Configures and selects the xref:#backend-k8ssearch[] backend +<4> Provides a trust root to be requested by xref:truststore.adoc[] [#backend] == Backend @@ -28,6 +29,8 @@ Each SecretClass is a associated with a single backend, which dictates the mecha *Format*: xref:#format-tls-pem[] +*TrustStore*: Yes + Issues a TLS certificate signed by the Secret Operator. The certificate authority can be provided by the administrator, or managed automatically by the Secret Operator. @@ -150,6 +153,8 @@ spec: *Format*: xref:#format-tls-pem[] +*TrustStore*: No + Injects a TLS certificate issued by {cert-manager}[Cert-Manager]. WARNING: This backend is experimental, and subject to change. @@ -213,6 +218,8 @@ spec: *Format*: xref:#format-kerberos[] +*TrustStore*: No + Creates a Kerberos keytab file for a selected realm. The Kerberos KDC and administrator credentials must be provided by the administrator. IMPORTANT: Only MIT Kerberos (krb5) and Active Directory are currently supported. @@ -368,6 +375,8 @@ spec: *Format*: Free-form +*TrustStore*: If configured + This backend can be used to mount `Secret` across namespaces into pods. The `Secret` object is selected based on two things: 1. The xref:scope.adoc[scopes] specified on the `Volume` using the attribute `secrets.stackable.tech/scope`. @@ -444,14 +453,16 @@ spec: pod: {} # or... name: my-namespace + trustStoreConfigMapName: tls-ca ---- `k8sSearch`:: Declares that the `k8sSearch` backend is used. -`k8sSearch.searchNamespace`:: Configures the namespace searched for `Secret` objects. -`k8sSearch.searchNamespace.pod`:: The `Secret` objects are located in the same namespace as the `Pod` object. Should be used +`k8sSearch.searchNamespace`:: Configures the namespace searched for Secrets. +`k8sSearch.searchNamespace.pod`:: The Secret objects are located in the same namespace as the Pod. Should be used for secrets that are provisioned by the application administrator. -`k8sSearch.searchNamespace.name`:: The `Secret` objects are located in a single global namespace. Should be used for secrets +`k8sSearch.searchNamespace.name`:: The Secrets are located in a single global namespace. Should be used for secrets that are provisioned by the cluster administrator. +`k8sSearch.trustStoreConfigMapName`:: ConfigMap used to provision xref:truststore.adoc[]. ==== Format diff --git a/docs/modules/secret-operator/pages/truststore.adoc b/docs/modules/secret-operator/pages/truststore.adoc new file mode 100644 index 00000000..0250b5f5 --- /dev/null +++ b/docs/modules/secret-operator/pages/truststore.adoc @@ -0,0 +1,22 @@ += TrustStore +:description: A TrustStore in Kubernetes retrieves the trust anchors from a SecretClass. + +A _TrustStore_ is a Kubernetes resource that can be used to request the trust anchor information (such as the TLS certificate authorities) from a xref:secretclass.adoc[]. + +This can be used to access a protected service from other services that do not require their own certificates (or from clients running outside of Kubernetes). + +A TrustStore looks like this: + +[source,yaml] +---- +include::example$truststore-tls.yaml[] +---- +<1> Also used to name the created ConfigMap +<2> The name of the xref:secretclass.adoc[] +<3> The requested xref:secretclass.adoc#format[format] + +This will create a ConfigMap named `truststore-pem` containing a `ca.crt` with the trust root certificates. +It can then either be mounted into a Pod or retrieved and used from outside of Kubernetes. + +NOTE: Make sure to have a procedure for updating the retrieved certificates. + The Secret Operator will automatically rotate the xref:secretclass.adoc#backend-autotls[autoTls] certificate authority as needed, but all trust roots will require some form of update occasionally. diff --git a/docs/modules/secret-operator/partials/nav.adoc b/docs/modules/secret-operator/partials/nav.adoc index 7ff84ca7..a776810e 100644 --- a/docs/modules/secret-operator/partials/nav.adoc +++ b/docs/modules/secret-operator/partials/nav.adoc @@ -6,6 +6,7 @@ ** xref:secret-operator:secretclass.adoc[] ** xref:secret-operator:scope.adoc[] ** xref:secret-operator:volume.adoc[] +** xref:secret-operator:truststore.adoc[] * Guides ** xref:secret-operator:cert-manager.adoc[] * xref:secret-operator:security.adoc[] diff --git a/rust/crd-utils/src/lib.rs b/rust/crd-utils/src/lib.rs index 90cffabb..d2b90479 100644 --- a/rust/crd-utils/src/lib.rs +++ b/rust/crd-utils/src/lib.rs @@ -5,7 +5,10 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; use stackable_operator::{ k8s_openapi::api::core::v1::{ConfigMap, Secret}, - kube::{api::DynamicObject, runtime::reflector::ObjectRef}, + kube::{ + api::{DynamicObject, ObjectMeta, PartialObjectMeta}, + runtime::reflector::ObjectRef, + }, schemars::{self, JsonSchema}, }; @@ -81,3 +84,20 @@ impl From<&SecretReference> for ObjectRef { ObjectRef::::from(val).erase() } } + +impl SecretReference { + fn matches(&self, secret_meta: &ObjectMeta) -> bool { + secret_meta.name.as_deref() == Some(&self.name) + && secret_meta.namespace.as_deref() == Some(&self.namespace) + } +} +impl PartialEq for SecretReference { + fn eq(&self, secret: &Secret) -> bool { + self.matches(&secret.metadata) + } +} +impl PartialEq> for SecretReference { + fn eq(&self, secret: &PartialObjectMeta) -> bool { + self.matches(&secret.metadata) + } +} diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index f39e0d9f..3988919d 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -11,16 +11,16 @@ publish = false [dependencies] stackable-krb5-provision-keytab = { path = "../krb5-provision-keytab" } stackable-secret-operator-crd-utils = { path = "../crd-utils" } -stackable-operator.workspace = true +p12 = { path = "../p12" } anyhow.workspace = true async-trait.workspace = true clap.workspace = true futures.workspace = true h2.workspace = true +kube-runtime.workspace = true libc.workspace = true openssl.workspace = true -p12.workspace = true pin-project.workspace = true prost-types.workspace = true prost.workspace = true @@ -28,6 +28,7 @@ serde_json.workspace = true serde.workspace = true snafu.workspace = true socket2.workspace = true +stackable-operator.workspace = true strum.workspace = true sys-mount.workspace = true tempfile.workspace = true @@ -40,6 +41,7 @@ tracing.workspace = true uuid.workspace = true yasna.workspace = true rand.workspace = true +const_format.workspace = true [dev-dependencies] serde_yaml.workspace = true diff --git a/rust/operator-binary/src/backend/cert_manager.rs b/rust/operator-binary/src/backend/cert_manager.rs index 5c38f91c..bdd108d6 100644 --- a/rust/operator-binary/src/backend/cert_manager.rs +++ b/rust/operator-binary/src/backend/cert_manager.rs @@ -14,6 +14,7 @@ use stackable_operator::{ use super::{ ScopeAddressesError, SecretBackend, SecretBackendError, SecretContents, SecretVolumeSelector, + TrustSelector, k8s_search::LABEL_SCOPE_NODE, pod_info::{Address, PodInfo, SchedulingPodInfo}, scope::SecretScope, @@ -61,6 +62,9 @@ pub enum Error { source: stackable_operator::client::Error, certificate: ObjectRef, }, + + #[snafu(display("the certManager backend does not currently support TrustStore exports"))] + TrustExportUnsupported, } impl SecretBackendError for Error { @@ -71,6 +75,22 @@ impl SecretBackendError for Error { Error::GetSecret { .. } => tonic::Code::Unavailable, Error::GetCertManagerCertificate { .. } => tonic::Code::Unavailable, Error::ApplyCertManagerCertificate { .. } => tonic::Code::Unavailable, + Error::TrustExportUnsupported => tonic::Code::FailedPrecondition, + } + } + + fn secondary_object(&self) -> Option> { + match self { + Error::NoPvcName => None, + Error::ScopeAddresses { source, .. } => source.secondary_object(), + Error::GetSecret { secret, .. } => Some(secret.clone().erase()), + Error::ApplyCertManagerCertificate { certificate, .. } => { + Some(certificate.clone().erase()) + } + Error::GetCertManagerCertificate { certificate, .. } => { + Some(certificate.clone().erase()) + } + Error::TrustExportUnsupported => None, } } } @@ -174,6 +194,13 @@ impl SecretBackend for CertManager { ))) } + async fn get_trust_data( + &self, + _selector: &TrustSelector, + ) -> Result { + TrustExportUnsupportedSnafu.fail() + } + async fn get_qualified_node_names( &self, selector: &SecretVolumeSelector, diff --git a/rust/operator-binary/src/backend/dynamic.rs b/rust/operator-binary/src/backend/dynamic.rs index 7f129641..4fb6d068 100644 --- a/rust/operator-binary/src/backend/dynamic.rs +++ b/rust/operator-binary/src/backend/dynamic.rs @@ -42,6 +42,10 @@ impl SecretBackendError for DynError { fn grpc_code(&self) -> tonic::Code { self.0.grpc_code() } + + fn secondary_object(&self) -> Option> { + self.0.secondary_object() + } } pub struct DynamicAdapter(B); @@ -67,6 +71,16 @@ impl SecretBackend for DynamicAdapter { .map_err(|err| DynError(Box::new(err))) } + async fn get_trust_data( + &self, + selector: &super::TrustSelector, + ) -> Result { + self.0 + .get_trust_data(selector) + .await + .map_err(|err| DynError(Box::new(err))) + } + async fn get_qualified_node_names( &self, selector: &SecretVolumeSelector, @@ -104,6 +118,13 @@ impl SecretBackendError for FromClassError { FromClassError::KerberosKeytab { source } => source.grpc_code(), } } + + fn secondary_object(&self) -> Option> { + match self { + FromClassError::Tls { source } => source.secondary_object(), + FromClassError::KerberosKeytab { source } => source.secondary_object(), + } + } } pub async fn from_class( @@ -111,12 +132,14 @@ pub async fn from_class( class: SecretClass, ) -> Result, FromClassError> { Ok(match class.spec.backend { - crd::SecretClassBackend::K8sSearch(crd::K8sSearchBackend { search_namespace }) => { - from(super::K8sSearch { - client: Unloggable(client.clone()), - search_namespace, - }) - } + crd::SecretClassBackend::K8sSearch(crd::K8sSearchBackend { + search_namespace, + trust_store_config_map_name, + }) => from(super::K8sSearch { + client: Unloggable(client.clone()), + search_namespace, + trust_store_config_map_name, + }), crd::SecretClassBackend::AutoTls(crd::AutoTlsBackend { ca, additional_trust_roots, @@ -179,6 +202,15 @@ impl SecretBackendError for FromSelectorError { FromSelectorError::FromClass { source, .. } => source.grpc_code(), } } + + fn secondary_object(&self) -> Option> { + match self { + FromSelectorError::GetSecretClass { class, .. } => Some(class.clone().erase()), + FromSelectorError::FromClass { source, class } => source + .secondary_object() + .or_else(|| Some(class.clone().erase())), + } + } } pub async fn from_selector( diff --git a/rust/operator-binary/src/backend/k8s_search.rs b/rust/operator-binary/src/backend/k8s_search.rs index e4efd119..5a7a50bf 100644 --- a/rust/operator-binary/src/backend/k8s_search.rs +++ b/rust/operator-binary/src/backend/k8s_search.rs @@ -3,17 +3,20 @@ use std::collections::{BTreeMap, HashSet}; use async_trait::async_trait; +use kube_runtime::reflector::ObjectRef; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ k8s_openapi::{ - ByteString, api::core::v1::Secret, apimachinery::pkg::apis::meta::v1::LabelSelector, + ByteString, + api::core::v1::{ConfigMap, Secret}, + apimachinery::pkg::apis::meta::v1::LabelSelector, }, kube::api::ListParams, kvp::{LabelError, LabelSelectorExt, Labels}, }; use super::{ - SecretBackend, SecretBackendError, SecretContents, SecretVolumeSelector, + SecretBackend, SecretBackendError, SecretContents, SecretVolumeSelector, TrustSelector, pod_info::{PodInfo, SchedulingPodInfo}, scope::SecretScope, }; @@ -45,6 +48,15 @@ pub enum Error { #[snafu(display("failed to build label"))] BuildLabel { source: LabelError }, + + #[snafu(display("no trust store ConfigMap is configured for this backend"))] + NoTrustStore, + + #[snafu(display("failed to query for trust store source {configmap}"))] + GetTrustStore { + source: stackable_operator::client::Error, + configmap: ObjectRef, + }, } impl SecretBackendError for Error { @@ -55,6 +67,20 @@ impl SecretBackendError for Error { Error::NoSecret { .. } => tonic::Code::FailedPrecondition, Error::NoListener { .. } => tonic::Code::FailedPrecondition, Error::BuildLabel { .. } => tonic::Code::FailedPrecondition, + Error::NoTrustStore => tonic::Code::FailedPrecondition, + Error::GetTrustStore { .. } => tonic::Code::Internal, + } + } + + fn secondary_object(&self) -> Option> { + match self { + Error::SecretSelector { .. } => None, + Error::SecretQuery { .. } => None, + Error::NoSecret { .. } => None, + Error::NoListener { .. } => None, + Error::BuildLabel { .. } => None, + Error::NoTrustStore => None, + Error::GetTrustStore { configmap, .. } => Some(configmap.clone().erase()), } } } @@ -64,15 +90,7 @@ pub struct K8sSearch { // Not secret per se, but isn't Debug: https://github.com/stackabletech/secret-operator/issues/411 pub client: Unloggable, pub search_namespace: SearchNamespace, -} - -impl K8sSearch { - fn search_ns_for_pod<'a>(&'a self, selector: &'a SecretVolumeSelector) -> &'a str { - match &self.search_namespace { - SearchNamespace::Pod {} => &selector.namespace, - SearchNamespace::Name(ns) => ns, - } - } + pub trust_store_config_map_name: Option, } #[async_trait] @@ -89,7 +107,7 @@ impl SecretBackend for K8sSearch { let secret = self .client .list::( - self.search_ns_for_pod(selector), + self.search_namespace.resolve(&selector.namespace), &ListParams::default().labels(&label_selector), ) .await @@ -107,6 +125,37 @@ impl SecretBackend for K8sSearch { ))) } + async fn get_trust_data( + &self, + selector: &TrustSelector, + ) -> Result { + let cm_name = self + .trust_store_config_map_name + .as_deref() + .context(NoTrustStoreSnafu)?; + let cm_ns = self.search_namespace.resolve(&selector.namespace); + let cm = self + .client + .get::(cm_name, cm_ns) + .await + .with_context(|_| GetTrustStoreSnafu { + configmap: ObjectRef::::new(cm_name).within(cm_ns), + })?; + let binary_data = cm + .binary_data + .unwrap_or_default() + .into_iter() + .map(|(k, ByteString(v))| (k, v)); + let str_data = cm + .data + .unwrap_or_default() + .into_iter() + .map(|(k, v)| (k, v.into_bytes())); + Ok(SecretContents::new(SecretData::Unknown( + binary_data.chain(str_data).collect(), + ))) + } + async fn get_qualified_node_names( &self, selector: &SecretVolumeSelector, @@ -118,7 +167,7 @@ impl SecretBackend for K8sSearch { Ok(Some( self.client .list::( - self.search_ns_for_pod(selector), + self.search_namespace.resolve(&selector.namespace), &ListParams::default().labels(&label_selector), ) .await diff --git a/rust/operator-binary/src/backend/kerberos_keytab.rs b/rust/operator-binary/src/backend/kerberos_keytab.rs index cb843343..d57bf4d8 100644 --- a/rust/operator-binary/src/backend/kerberos_keytab.rs +++ b/rust/operator-binary/src/backend/kerberos_keytab.rs @@ -64,8 +64,11 @@ pub enum Error { #[snafu(display("generated invalid Kerberos principal for pod"))] PodPrincipal { source: InvalidKerberosPrincipal }, - #[snafu(display("failed to read keytab"))] - ReadKeytab { source: std::io::Error }, + #[snafu(display("failed to read the provisioned keytab"))] + ReadProvisionedKeytab { source: std::io::Error }, + + #[snafu(display("the kerberosKeytab backend does not currently support TrustStore exports"))] + TrustExportUnsupported, } impl SecretBackendError for Error { fn grpc_code(&self) -> tonic::Code { @@ -77,8 +80,24 @@ impl SecretBackendError for Error { Error::WriteAdminKeytab { .. } => tonic::Code::Unavailable, Error::ProvisionKeytab { .. } => tonic::Code::Unavailable, Error::PodPrincipal { .. } => tonic::Code::FailedPrecondition, - Error::ReadKeytab { .. } => tonic::Code::Unavailable, + Error::ReadProvisionedKeytab { .. } => tonic::Code::Unavailable, Error::ScopeAddresses { .. } => tonic::Code::Unavailable, + Error::TrustExportUnsupported => tonic::Code::FailedPrecondition, + } + } + + fn secondary_object(&self) -> Option> { + match self { + Error::ScopeAddresses { source, .. } => source.secondary_object(), + Error::LoadAdminKeytab { secret, .. } => Some(secret.clone().erase()), + Error::NoAdminKeytabKeyInSecret { secret } => Some(secret.clone().erase()), + Error::TempSetup { .. } => None, + Error::WriteConfig { .. } => None, + Error::WriteAdminKeytab { .. } => None, + Error::ProvisionKeytab { .. } => None, + Error::PodPrincipal { .. } => None, + Error::ReadProvisionedKeytab { .. } => None, + Error::TrustExportUnsupported => None, } } } @@ -271,11 +290,11 @@ cluster.local = {realm_name} let mut keytab_data = Vec::new(); let mut keytab_file = File::open(keytab_file_path) .await - .context(ReadKeytabSnafu)?; + .context(ReadProvisionedKeytabSnafu)?; keytab_file .read_to_end(&mut keytab_data) .await - .context(ReadKeytabSnafu)?; + .context(ReadProvisionedKeytabSnafu)?; Ok(SecretContents::new(SecretData::WellKnown( WellKnownSecretData::Kerberos(well_known::Kerberos { keytab: keytab_data, @@ -283,4 +302,11 @@ cluster.local = {realm_name} }), ))) } + + async fn get_trust_data( + &self, + _selector: &super::TrustSelector, + ) -> Result { + TrustExportUnsupportedSnafu.fail() + } } diff --git a/rust/operator-binary/src/backend/mod.rs b/rust/operator-binary/src/backend/mod.rs index ca63d6c6..ef596958 100644 --- a/rust/operator-binary/src/backend/mod.rs +++ b/rust/operator-binary/src/backend/mod.rs @@ -14,17 +14,22 @@ use async_trait::async_trait; pub use cert_manager::CertManager; pub use k8s_search::K8sSearch; pub use kerberos_keytab::KerberosKeytab; +use kube_runtime::reflector::ObjectRef; use pod_info::Address; use scope::SecretScope; use serde::{Deserialize, Deserializer, Serialize, de::Unexpected}; use snafu::{OptionExt, Snafu}; use stackable_operator::{ + commons::listener::PodListeners, k8s_openapi::chrono::{DateTime, FixedOffset}, + kube::api::DynamicObject, time::Duration, }; pub use tls::TlsGenerate; use self::pod_info::SchedulingPodInfo; +#[cfg(doc)] +use crate::crd::TrustStore; use crate::format::{ SecretData, SecretFormat, well_known::{CompatibilityOptions, NamingOptions}, @@ -135,6 +140,12 @@ pub struct SecretVolumeSelector { pub cert_manager_cert_lifetime: Option, } +/// Configuration provided by the [`TrustStore`] selecting what trust data should be provided. +pub struct TrustSelector { + /// The name of the [`TrustStore`]'s `Namespace`. + pub namespace: String, +} + /// Internal parameters of [`SecretVolumeSelector`] managed by secret-operator itself. // These are optional even if they are set unconditionally, because otherwise we will // fail to restore volumes (after Node reboots etc) from before they were added during upgrades. @@ -167,8 +178,25 @@ fn default_cert_jitter_factor() -> f64 { #[derive(Snafu, Debug)] #[snafu(module)] pub enum ScopeAddressesError { - #[snafu(display("no addresses found for listener {listener}"))] - NoListenerAddresses { listener: String }, + #[snafu(display( + "listener addresses were not fetched (this is likely a bug in secret-operator)" + ))] + ListenerAddressesNotFetched, + + #[snafu(display("no addresses found for listener {listener} on {pod_listeners}"))] + NoListenerAddresses { + listener: String, + pod_listeners: ObjectRef, + }, +} + +impl ScopeAddressesError { + pub fn secondary_object(&self) -> Option> { + match self { + Self::ListenerAddressesNotFetched => None, + Self::NoListenerAddresses { pod_listeners, .. } => Some(pod_listeners.clone().erase()), + } + } } impl SecretVolumeSelector { @@ -204,11 +232,17 @@ impl SecretVolumeSelector { scope::SecretScope::Service { name } => vec![Address::Dns(format!( "{name}.{namespace}.svc.{cluster_domain}", ))], - scope::SecretScope::ListenerVolume { name } => pod_info - .listener_addresses - .get(name) - .context(NoListenerAddressesSnafu { listener: name })? - .to_vec(), + scope::SecretScope::ListenerVolume { name } => match &pod_info.listener_addresses { + Some(listener_addresses) => listener_addresses + .by_listener_volume_name + .get(name) + .with_context(|| NoListenerAddressesSnafu { + listener: name, + pod_listeners: listener_addresses.source.clone(), + })? + .to_vec(), + None => return ListenerAddressesNotFetchedSnafu.fail(), + }, }) } @@ -272,6 +306,9 @@ pub trait SecretBackend: Debug + Send + Sync { pod_info: pod_info::PodInfo, ) -> Result; + async fn get_trust_data(&self, selector: &TrustSelector) + -> Result; + /// Try to predict which nodes would be able to provision this secret. /// /// Should return `None` if no constraints apply, `Some(HashSet::new())` is interpreted as "no nodes match the given constraints". @@ -290,12 +327,17 @@ pub trait SecretBackend: Debug + Send + Sync { pub trait SecretBackendError: std::error::Error + Send + Sync + 'static { fn grpc_code(&self) -> tonic::Code; + fn secondary_object(&self) -> Option>; } impl SecretBackendError for Infallible { fn grpc_code(&self) -> tonic::Code { match *self {} } + + fn secondary_object(&self) -> Option> { + match *self {} + } } #[cfg(test)] diff --git a/rust/operator-binary/src/backend/pod_info.rs b/rust/operator-binary/src/backend/pod_info.rs index 8969e2ba..a9caa0d3 100644 --- a/rust/operator-binary/src/backend/pod_info.rs +++ b/rust/operator-binary/src/backend/pod_info.rs @@ -6,6 +6,7 @@ use std::{ }; use futures::{StreamExt, TryStreamExt}; +use kube_runtime::reflector::Lookup; use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ commons::{ @@ -109,7 +110,7 @@ pub struct PodInfo { pub service_name: Option, pub node_name: String, pub node_ips: Vec, - pub listener_addresses: HashMap>, + pub listener_addresses: Option, pub kubernetes_cluster_domain: DomainName, pub scheduling: SchedulingPodInfo, } @@ -134,10 +135,10 @@ impl PodInfo { })?; let scheduling = SchedulingPodInfo::from_pod(client, &pod, scopes).await?; let listener_addresses = if !scheduling.volume_listener_names.is_empty() { - pod_listener_addresses(client, &pod, &scheduling, scopes).await? + Some(ListenerAddresses::fetch_for_pod(client, &pod, &scheduling, scopes).await?) } else { // We don't care about the listener addresses if there is no listener scope, so we can save the API call - HashMap::new() + None }; Ok(Self { // This will generally be empty, since Kubernetes assigns pod IPs *after* CSI plugins are successful @@ -335,55 +336,66 @@ async fn listener_pvc_is_node_scoped( Ok(listener_class.spec.service_type == ServiceType::NodePort) } -async fn pod_listener_addresses( - client: &stackable_operator::client::Client, - pod: &Pod, - pod_info: &SchedulingPodInfo, - scopes: &[SecretScope], -) -> Result>, FromPodError> { - use from_pod_error::*; - let pod_listeners_name = format!( - "pod-{}", - pod.metadata.uid.as_deref().context(NoPodUidSnafu)? - ); - let listeners = client - .get::(&pod_listeners_name, &pod_info.namespace) - .await - .context(GetPodListenersSnafu { - pod_listeners: ObjectRef::::new(&pod_listeners_name) - .within(&pod_info.namespace), - pod: ObjectRef::from_obj(pod), - })?; - let listeners_ref = ObjectRef::from_obj(&listeners); - scopes - .iter() - .filter_map(|scope| match scope { - SecretScope::ListenerVolume { name } => Some(name), - _ => None, - }) - .map(|listener| { - let addresses = listeners - .spec - .listeners - .get(listener) - .and_then(|ingresses| ingresses.ingress_addresses.as_ref()) - .context(NoPodListenerAddressesSnafu { - pod_listeners: listeners_ref.clone(), - listener, - })?; - Ok(( - listener.clone(), - addresses - .iter() - .map(|ingr| { - (ingr.address_type, &*ingr.address).try_into().context( - IllegalAddressSnafu { - address: &ingr.address, - }, - ) - }) - .collect::, FromPodError>>()?, - )) +#[derive(Debug)] +pub struct ListenerAddresses { + pub source: ObjectRef, + pub by_listener_volume_name: HashMap>, +} + +impl ListenerAddresses { + async fn fetch_for_pod( + client: &stackable_operator::client::Client, + pod: &Pod, + pod_info: &SchedulingPodInfo, + scopes: &[SecretScope], + ) -> Result { + use from_pod_error::*; + let pod_listeners_name = format!( + "pod-{}", + pod.metadata.uid.as_deref().context(NoPodUidSnafu)? + ); + let pod_listeners = client + .get::(&pod_listeners_name, &pod_info.namespace) + .await + .context(GetPodListenersSnafu { + pod_listeners: ObjectRef::::new(&pod_listeners_name) + .within(&pod_info.namespace), + pod: ObjectRef::from_obj(pod), + })?; + let listeners_ref = ObjectRef::from_obj(&pod_listeners); + Ok(ListenerAddresses { + source: pod_listeners.to_object_ref(()), + by_listener_volume_name: scopes + .iter() + .filter_map(|scope| match scope { + SecretScope::ListenerVolume { name } => Some(name), + _ => None, + }) + .map(|listener| { + let addresses = pod_listeners + .spec + .listeners + .get(listener) + .and_then(|ingresses| ingresses.ingress_addresses.as_ref()) + .context(NoPodListenerAddressesSnafu { + pod_listeners: listeners_ref.clone(), + listener, + })?; + Ok(( + listener.clone(), + addresses + .iter() + .map(|ingr| { + (ingr.address_type, &*ingr.address).try_into().context( + IllegalAddressSnafu { + address: &ingr.address, + }, + ) + }) + .collect::, FromPodError>>()?, + )) + }) + .collect::, FromPodError>>()?, }) - .collect::, FromPodError>>() + } } diff --git a/rust/operator-binary/src/backend/tls/ca.rs b/rust/operator-binary/src/backend/tls/ca.rs index 57d87b04..cf951ad0 100644 --- a/rust/operator-binary/src/backend/tls/ca.rs +++ b/rust/operator-binary/src/backend/tls/ca.rs @@ -2,6 +2,7 @@ use std::{collections::BTreeMap, ffi::OsStr, fmt::Display, path::Path}; +use kube_runtime::reflector::Lookup; use openssl::{ asn1::{Asn1Integer, Asn1Time}, bn::{BigNum, MsbOption}, @@ -58,16 +59,16 @@ pub enum Error { #[snafu(display("failed to generate certificate key"))] GenerateKey { source: openssl::error::ErrorStack }, - #[snafu(display("failed to load {config_map}"))] - FindConfigMap { + #[snafu(display("failed to load CA from {secret}"))] + FindCertificateAuthority { source: kube::Error, - config_map: ObjectRef, + secret: ObjectRef, }, - #[snafu(display("failed to load {secret}"))] - FindSecret { - source: kube::Error, - secret: ObjectRef, + #[snafu(display("failed to load extra trust root from {object}"))] + FindExtraTrustRoot { + source: stackable_operator::client::Error, + object: ObjectRef, }, #[snafu(display("CA {secret} does not exist, and autoGenerate is false"))] @@ -123,8 +124,8 @@ impl SecretBackendError for Error { match self { Error::GenerateKey { .. } => tonic::Code::Internal, Error::MissingCertificate { .. } => tonic::Code::FailedPrecondition, - Error::FindConfigMap { .. } => tonic::Code::Unavailable, - Error::FindSecret { .. } => tonic::Code::Unavailable, + Error::FindCertificateAuthority { .. } => tonic::Code::Unavailable, + Error::FindExtraTrustRoot { .. } => tonic::Code::Unavailable, Error::CaNotFoundAndGenDisabled { .. } => tonic::Code::FailedPrecondition, Error::LoadCertificate { .. } => tonic::Code::FailedPrecondition, Error::UnsupportedCertificateFormat { .. } => tonic::Code::InvalidArgument, @@ -135,13 +136,33 @@ impl SecretBackendError for Error { Error::SaveRequestedButForbidden { .. } => tonic::Code::FailedPrecondition, } } + + fn secondary_object(&self) -> Option> { + match self { + Error::GenerateKey { .. } => None, + Error::FindCertificateAuthority { secret, .. } => Some(secret.clone().erase()), + Error::FindExtraTrustRoot { object, .. } => Some(object.clone()), + Error::CaNotFoundAndGenDisabled { secret } => Some(secret.clone().erase()), + Error::MissingCertificate { secret, .. } => Some(secret.clone().erase()), + Error::LoadCertificate { object, .. } => Some(object.clone()), + Error::UnsupportedCertificateFormat { object, .. } => Some(object.clone()), + Error::ParseLifetime { secret, .. } => Some(secret.clone().erase()), + Error::BuildCertificate { .. } => None, + Error::SerializeCertificate { .. } => None, + Error::SaveCaCertificate { secret, .. } => Some(secret.clone().erase()), + Error::SaveRequestedButForbidden => None, + } + } } #[derive(Debug, Snafu)] #[snafu(module)] pub enum GetCaError { - #[snafu(display("No CA will live until at least {cutoff}"))] - NoCaLivesLongEnough { cutoff: OffsetDateTime }, + #[snafu(display("no CA in {secret} will live until at least {cutoff}"))] + NoCaLivesLongEnough { + cutoff: OffsetDateTime, + secret: ObjectRef, + }, } impl SecretBackendError for GetCaError { @@ -150,6 +171,12 @@ impl SecretBackendError for GetCaError { GetCaError::NoCaLivesLongEnough { .. } => tonic::Code::FailedPrecondition, } } + + fn secondary_object(&self) -> Option> { + match self { + GetCaError::NoCaLivesLongEnough { secret, .. } => Some(secret.clone().erase()), + } + } } #[derive(Debug)] @@ -210,7 +237,8 @@ impl CertificateAuthority { let now = OffsetDateTime::now_utc(); let not_before = now - Duration::from_minutes_unchecked(5); let not_after = now + config.ca_certificate_lifetime; - let conf = Conf::new(ConfMethod::default()).unwrap(); + let conf = + Conf::new(ConfMethod::default()).expect("failed to initialize OpenSSL configuration"); let private_key_length = match config.key_generation { CertificateKeyGeneration::Rsa { length } => length, @@ -311,6 +339,7 @@ impl CertificateAuthority { /// Manages multiple [`CertificateAuthorities`](`CertificateAuthority`), rotating them as needed. #[derive(Debug)] pub struct Manager { + source_secret: ObjectRef, certificate_authorities: Vec, additional_trusted_certificates: Vec, } @@ -324,10 +353,10 @@ impl Manager { ) -> Result { // Use entry API rather than apply so that we crash and retry on conflicts (to avoid creating spurious certs that we throw away immediately) let secrets_api = &client.get_api::(&secret_ref.namespace); - let ca_secret = secrets_api + let mut ca_secret = secrets_api .entry(&secret_ref.name) .await - .with_context(|_| FindSecretSnafu { secret: secret_ref })?; + .with_context(|_| FindCertificateAuthoritySnafu { secret: secret_ref })?; let mut update_ca_secret = false; let mut certificate_authorities = match &ca_secret { Entry::Occupied(ca_secret) => { @@ -425,8 +454,8 @@ impl Manager { info!(secret = %secret_ref, "CA has been modified, saving"); // Sort CAs by age to avoid spurious writes certificate_authorities.sort_by_key(|ca| ca.not_after); - let mut ca_secret = ca_secret.or_insert(Secret::default); - ca_secret.get_mut().data = Some( + let mut occupied_ca_secret = ca_secret.or_insert(Secret::default); + occupied_ca_secret.get_mut().data = Some( certificate_authorities .iter() .enumerate() @@ -454,10 +483,11 @@ impl Manager { }) .collect::>()?, ); - ca_secret + occupied_ca_secret .commit(&PostParams::default()) .await .context(SaveCaCertificateSnafu { secret: secret_ref })?; + ca_secret = Entry::Occupied(occupied_ca_secret); } else { return SaveRequestedButForbiddenSnafu.fail(); } @@ -467,10 +497,10 @@ impl Manager { for entry in additional_trust_roots { let certs = match entry { AdditionalTrustRoot::ConfigMap(config_map) => { - Self::read_certificates_from_config_map(client, config_map).await? + Self::read_extra_trust_roots_from_config_map(client, config_map).await? } AdditionalTrustRoot::Secret(secret) => { - Self::read_certificates_from_secret(client, secret).await? + Self::read_extra_trust_roots_from_secret(client, secret).await? } }; additional_trusted_certificates.extend(certs); @@ -479,6 +509,10 @@ impl Manager { Ok(Self { certificate_authorities, additional_trusted_certificates, + source_secret: ca_secret + .get() + .map(|secret| secret.to_object_ref(())) + .unwrap_or_else(|| secret_ref.into()), }) } @@ -486,18 +520,17 @@ impl Manager { /// /// The keys are assumed to be filenames and their extensions denote the expected format of the /// certificate. - async fn read_certificates_from_config_map( + async fn read_extra_trust_roots_from_config_map( client: &stackable_operator::client::Client, config_map_ref: &ConfigMapReference, ) -> Result> { let mut certificates = vec![]; - let config_map_api = &client.get_api::(&config_map_ref.namespace); - let config_map = config_map_api - .get(&config_map_ref.name) + let config_map = client + .get::(&config_map_ref.name, &config_map_ref.namespace) .await - .with_context(|_| FindConfigMapSnafu { - config_map: config_map_ref, + .context(FindExtraTrustRootSnafu { + object: config_map_ref, })?; let config_map_data = config_map.data.unwrap_or_default(); @@ -511,7 +544,7 @@ impl Manager { .map(|(key, ByteString(value))| (key, value.as_ref())), ); for (key, value) in data { - let certs = Self::deserialize_certificate(key, value, config_map_ref.into())?; + let certs = Self::deserialize_certificate(key, value, config_map_ref)?; info!( ?certs, %config_map_ref, @@ -528,21 +561,20 @@ impl Manager { /// /// The keys are assumed to be filenames and their extensions denote the expected format of the /// certificate. - async fn read_certificates_from_secret( + async fn read_extra_trust_roots_from_secret( client: &stackable_operator::client::Client, secret_ref: &SecretReference, ) -> Result> { let mut certificates = vec![]; - let secrets_api = &client.get_api::(&secret_ref.namespace); - let secret = secrets_api - .get(&secret_ref.name) + let secret = client + .get::(&secret_ref.name, &secret_ref.namespace) .await - .with_context(|_| FindSecretSnafu { secret: secret_ref })?; + .context(FindExtraTrustRootSnafu { object: secret_ref })?; let secret_data = secret.data.unwrap_or_default(); for (key, ByteString(value)) in &secret_data { - let certs = Self::deserialize_certificate(key, value, secret_ref.into())?; + let certs = Self::deserialize_certificate(key, value, secret_ref)?; info!( ?certs, %secret_ref, @@ -560,7 +592,7 @@ impl Manager { fn deserialize_certificate( key: &str, value: &[u8], - object_ref: ObjectRef, + object_ref: impl Into>, ) -> Result> { let extension = Path::new(key).extension().and_then(OsStr::to_str); @@ -592,8 +624,9 @@ impl Manager { .filter(|ca| ca.not_after > valid_until_at_least) // pick the oldest valid CA, since it will be trusted by the most peers .min_by_key(|ca| ca.not_after) - .context(NoCaLivesLongEnoughSnafu { + .with_context(|| NoCaLivesLongEnoughSnafu { cutoff: valid_until_at_least, + secret: self.source_secret.clone(), }) } diff --git a/rust/operator-binary/src/backend/tls/mod.rs b/rust/operator-binary/src/backend/tls/mod.rs index 69995f4a..adcbda4b 100644 --- a/rust/operator-binary/src/backend/tls/mod.rs +++ b/rust/operator-binary/src/backend/tls/mod.rs @@ -127,6 +127,23 @@ impl SecretBackendError for Error { Error::JitterOutOfRange { .. } => tonic::Code::InvalidArgument, } } + + fn secondary_object( + &self, + ) -> Option> + { + match self { + Error::ScopeAddresses { source, .. } => source.secondary_object(), + Error::GenerateKey { .. } => None, + Error::LoadCa { source } => source.secondary_object(), + Error::PickCa { source } => source.secondary_object(), + Error::BuildCertificate { .. } => None, + Error::SerializeCertificate { .. } => None, + Error::InvalidCertLifetime { .. } => None, + Error::TooShortCertLifetimeRequiresTimeTravel { .. } => None, + Error::JitterOutOfRange { .. } => None, + } + } } #[derive(Debug)] @@ -239,7 +256,8 @@ impl SecretBackend for TlsGenerate { .fail()?; } - let conf = Conf::new(ConfMethod::default()).unwrap(); + let conf = + Conf::new(ConfMethod::default()).expect("failed to initialize OpenSSL configuration"); let pod_key_length = match self.key_generation { CertificateKeyGeneration::Rsa { length } => length, @@ -334,12 +352,16 @@ impl SecretBackend for TlsGenerate { .context(SerializeCertificateSnafu { tpe: CertType::Ca }) }), )?, - certificate_pem: pod_cert - .to_pem() - .context(SerializeCertificateSnafu { tpe: CertType::Pod })?, - key_pem: pod_key - .private_key_to_pem_pkcs8() - .context(SerializeCertificateSnafu { tpe: CertType::Pod })?, + certificate_pem: Some( + pod_cert + .to_pem() + .context(SerializeCertificateSnafu { tpe: CertType::Pod })?, + ), + key_pem: Some( + pod_key + .private_key_to_pem_pkcs8() + .context(SerializeCertificateSnafu { tpe: CertType::Pod })?, + ), }, ))) .expires_after( @@ -347,6 +369,24 @@ impl SecretBackend for TlsGenerate { ), ) } + + async fn get_trust_data( + &self, + _selector: &super::TrustSelector, + ) -> Result { + Ok(SecretContents::new(SecretData::WellKnown( + WellKnownSecretData::TlsPem(well_known::TlsPem { + ca_pem: iterator_try_concat_bytes(self.ca_manager.trust_roots().into_iter().map( + |ca| { + ca.to_pem() + .context(SerializeCertificateSnafu { tpe: CertType::Ca }) + }, + ))?, + certificate_pem: None, + key_pem: None, + }), + ))) + } } #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/crd.rs b/rust/operator-binary/src/crd.rs index e9cefca8..c613040c 100644 --- a/rust/operator-binary/src/crd.rs +++ b/rust/operator-binary/src/crd.rs @@ -4,13 +4,14 @@ use serde::{Deserialize, Serialize}; use snafu::Snafu; use stackable_operator::{ commons::networking::{HostName, KerberosRealmName}, - kube::CustomResource, + k8s_openapi::api::core::v1::{ConfigMap, Secret}, + kube::{CustomResource, api::PartialObjectMeta}, schemars::{self, JsonSchema, schema::Schema}, time::Duration, }; use stackable_secret_operator_crd_utils::{ConfigMapReference, SecretReference}; -use crate::backend; +use crate::{backend, format::SecretFormat}; /// A [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass) is a cluster-global Kubernetes resource /// that defines a category of secrets that the Secret Operator knows how to provision. @@ -64,14 +65,66 @@ pub enum SecretClassBackend { KerberosKeytab(KerberosKeytabBackend), } +impl SecretClassBackend { + // Currently no `refers_to_*` method actually returns more than one element, + // but returning `Iterator` instead of `Option` to ensure that all consumers are ready + // for adding more conditions. + + // The matcher methods are on the CRD type rather than the initialized `Backend` impls + // to avoid having to initialize the backend for each watch event. + + /// Returns the conditions where the backend refers to `config_map`. + pub fn refers_to_config_map( + &self, + config_map: &PartialObjectMeta, + ) -> impl Iterator { + let cm_namespace = config_map.metadata.namespace.as_deref(); + match self { + Self::K8sSearch(backend) => { + let name_matches = backend.trust_store_config_map_name == config_map.metadata.name; + cm_namespace + .filter(|_| name_matches) + .and_then(|cm_ns| backend.search_namespace.matches_namespace(cm_ns)) + } + Self::AutoTls(_) => None, + Self::CertManager(_) => None, + Self::KerberosKeytab(_) => None, + } + .into_iter() + } + + /// Returns the conditions where the backend refers to `secret`. + pub fn refers_to_secret( + &self, + secret: &PartialObjectMeta, + ) -> impl Iterator { + match self { + Self::AutoTls(backend) => { + (backend.ca.secret == *secret).then_some(SearchNamespaceMatchCondition::True) + } + Self::K8sSearch(_) => None, + Self::CertManager(_) => None, + Self::KerberosKeytab(_) => None, + } + .into_iter() + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct K8sSearchBackend { /// Configures the namespace searched for Secret objects. pub search_namespace: SearchNamespace, + + /// Name of a ConfigMap that contains the information required to validate against this SecretClass. + /// + /// Resolved relative to `search_namespace`. + /// + /// Required to request a TrustStore for this SecretClass. + pub trust_store_config_map_name: Option, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, JsonSchema)] #[serde(rename_all = "camelCase")] pub enum SearchNamespace { /// The Secret objects are located in the same namespace as the Pod object. @@ -83,6 +136,55 @@ pub enum SearchNamespace { Name(String), } +impl SearchNamespace { + pub fn resolve<'a>(&'a self, pod_namespace: &'a str) -> &'a str { + match self { + SearchNamespace::Pod {} => pod_namespace, + SearchNamespace::Name(ns) => ns, + } + } + + /// Returns [`Some`] if this `SearchNamespace` could possibly match an object in the namespace + /// `object_namespace`, otherwise [`None`]. + /// + /// This is optimistic, you then need to call [`SearchNamespaceMatchCondition::matches_pod_namespace`] + /// to evaluate the match for a specific pod's namespace. + pub fn matches_namespace( + &self, + object_namespace: &str, + ) -> Option { + match self { + SearchNamespace::Pod {} => Some(SearchNamespaceMatchCondition::IfPodIsInNamespace { + namespace: object_namespace.to_string(), + }), + SearchNamespace::Name(ns) => { + (ns == object_namespace).then_some(SearchNamespaceMatchCondition::True) + } + } + } +} + +/// A partially evaluated match returned by [`SearchNamespace::matches_namespace`]. +/// Use [`Self::matches_pod_namespace`] to evaluate fully. +#[derive(Debug)] +pub enum SearchNamespaceMatchCondition { + /// The target object matches the search namespace. + True, + + /// The target object only matches the search namespace if mounted into a pod in + /// `namespace`. + IfPodIsInNamespace { namespace: String }, +} + +impl SearchNamespaceMatchCondition { + pub fn matches_pod_namespace(&self, pod_ns: &str) -> bool { + match self { + Self::True => true, + Self::IfPodIsInNamespace { namespace } => namespace == pod_ns, + } + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct AutoTlsBackend { @@ -395,6 +497,31 @@ impl Deref for KerberosPrincipal { } } +/// A [TrustStore](DOCS_BASE_URL_PLACEHOLDER/secret-operator/truststore) requests information about how to +/// validate secrets issued by a [SecretClass](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass). +/// +/// The requested information is written to a ConfigMap with the same name as the TrustStore. +#[derive(CustomResource, Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[kube( + group = "secrets.stackable.tech", + version = "v1alpha1", + kind = "TrustStore", + namespaced, + crates( + kube_core = "stackable_operator::kube::core", + k8s_openapi = "stackable_operator::k8s_openapi", + schemars = "stackable_operator::schemars" + ) +)] +#[serde(rename_all = "camelCase")] +pub struct TrustStoreSpec { + /// The name of the SecretClass that the request concerns. + pub secret_class_name: String, + + /// The [format](DOCS_BASE_URL_PLACEHOLDER/secret-operator/secretclass#format) that the data should be converted into. + pub format: Option, +} + #[cfg(test)] mod test { use super::*; diff --git a/rust/operator-binary/src/format/convert.rs b/rust/operator-binary/src/format/convert.rs index e0efc800..d4cc3e79 100644 --- a/rust/operator-binary/src/format/convert.rs +++ b/rust/operator-binary/src/format/convert.rs @@ -53,8 +53,14 @@ pub fn convert_tls_to_pkcs12( p12_password: &str, ) -> Result { use tls_to_pkcs12_error::*; - let cert = X509::from_pem(&pem.certificate_pem).context(LoadCertSnafu)?; - let key = PKey::private_key_from_pem(&pem.key_pem).context(LoadKeySnafu)?; + let cert = pem + .certificate_pem + .map(|cert| X509::from_pem(&cert).context(LoadCertSnafu)) + .transpose()?; + let key = pem + .key_pem + .map(|key| PKey::private_key_from_pem(&key).context(LoadKeySnafu)) + .transpose()?; let mut ca_stack = Stack::::new().context(LoadCaSnafu)?; for ca in split_pem_certificates(&pem.ca_pem) { @@ -65,13 +71,18 @@ pub fn convert_tls_to_pkcs12( Ok(TlsPkcs12 { truststore: pkcs12_truststore(&ca_stack, p12_password)?, - keystore: Pkcs12::builder() - .ca(ca_stack) - .cert(&cert) - .pkey(&key) - .build2(p12_password) - .and_then(|store| store.to_der()) - .context(BuildKeystoreSnafu)?, + keystore: cert + .zip(key) + .map(|(cert, key)| { + Pkcs12::builder() + .ca(ca_stack) + .cert(&cert) + .pkey(&key) + .build2(p12_password) + .and_then(|store| store.to_der()) + .context(BuildKeystoreSnafu) + }) + .transpose()?, }) } @@ -97,6 +108,16 @@ fn pkcs12_truststore<'a>( 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 { @@ -112,8 +133,12 @@ fn pkcs12_truststore<'a>( } 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) - .context(tls_to_pkcs12_error::EncryptDataForTruststoreSnafu)?, + 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| { @@ -122,7 +147,11 @@ fn pkcs12_truststore<'a>( }); Ok(p12::PFX { version: 3, - mac_data: Some(p12::MacData::new(&truststore_data, &password_as_bmp_string)), + mac_data: Some(p12::MacData::new( + &truststore_data, + &password_as_bmp_string, + &mut DummyRng, + )), auth_safe: p12::ContentInfo::Data(truststore_data), } .to_der()) @@ -149,3 +178,26 @@ pub enum TlsToPkcs12Error { #[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/well_known.rs b/rust/operator-binary/src/format/well_known.rs index 0df85e67..ce513058 100644 --- a/rust/operator-binary/src/format/well_known.rs +++ b/rust/operator-binary/src/format/well_known.rs @@ -1,5 +1,6 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use snafu::{OptionExt, Snafu}; +use stackable_operator::schemars::{self, JsonSchema}; use strum::EnumDiscriminants; use super::{ConvertError, SecretFiles, convert}; @@ -16,14 +17,14 @@ const FILE_KERBEROS_KEYTAB_KRB5_CONF: &str = "krb5.conf"; #[derive(Debug)] pub struct TlsPem { - pub certificate_pem: Vec, - pub key_pem: Vec, + pub certificate_pem: Option>, + pub key_pem: Option>, pub ca_pem: Vec, } #[derive(Debug)] pub struct TlsPkcs12 { - pub keystore: Vec, + pub keystore: Option>, pub truststore: Vec, } @@ -36,7 +37,7 @@ pub struct Kerberos { #[derive(Debug, EnumDiscriminants)] #[strum_discriminants( name(SecretFormat), - derive(Deserialize), + derive(Serialize, Deserialize, JsonSchema), serde(rename_all = "kebab-case") )] pub enum WellKnownSecretData { @@ -53,19 +54,23 @@ impl WellKnownSecretData { key_pem, ca_pem, }) => [ - (names.tls_pem_cert_name, certificate_pem), - (names.tls_pem_key_name, key_pem), - (names.tls_pem_ca_name, ca_pem), + Some(names.tls_pem_cert_name).zip(certificate_pem), + Some(names.tls_pem_key_name).zip(key_pem), + Some((names.tls_pem_ca_name, ca_pem)), ] - .into(), + .into_iter() + .flatten() + .collect(), WellKnownSecretData::TlsPkcs12(TlsPkcs12 { keystore, truststore, }) => [ - (names.tls_pkcs12_keystore_name, keystore), - (names.tls_pkcs12_truststore_name, truststore), + Some(names.tls_pkcs12_keystore_name).zip(keystore), + Some((names.tls_pkcs12_truststore_name, truststore)), ] - .into(), + .into_iter() + .flatten() + .collect(), WellKnownSecretData::Kerberos(Kerberos { keytab, krb5_conf }) => [ (FILE_KERBEROS_KEYTAB_KEYTAB.to_string(), keytab), (FILE_KERBEROS_KEYTAB_KRB5_CONF.to_string(), krb5_conf), @@ -84,13 +89,13 @@ impl WellKnownSecretData { if let Ok(certificate_pem) = take_file(SecretFormat::TlsPem, FILE_PEM_CERT_CERT) { let mut take_file = |file| take_file(SecretFormat::TlsPem, file); Ok(WellKnownSecretData::TlsPem(TlsPem { - certificate_pem, - key_pem: take_file(FILE_PEM_CERT_KEY)?, + certificate_pem: Some(certificate_pem), + key_pem: Some(take_file(FILE_PEM_CERT_KEY)?), ca_pem: take_file(FILE_PEM_CERT_CA)?, })) } else if let Ok(keystore) = take_file(SecretFormat::TlsPkcs12, FILE_PKCS12_CERT_KEYSTORE) { Ok(WellKnownSecretData::TlsPkcs12(TlsPkcs12 { - keystore, + keystore: Some(keystore), truststore: take_file(SecretFormat::TlsPkcs12, FILE_PKCS12_CERT_TRUSTSTORE)?, })) } else if let Ok(keytab) = take_file(SecretFormat::Kerberos, FILE_KERBEROS_KEYTAB_KEYTAB) { @@ -183,6 +188,18 @@ pub struct NamingOptions { pub tls_pem_ca_name: String, } +impl Default for NamingOptions { + fn default() -> Self { + Self { + tls_pkcs12_keystore_name: default_pkcs12_keystore_name(), + tls_pkcs12_truststore_name: default_pkcs12_truststore_name(), + tls_pem_cert_name: default_tls_pem_cert_name(), + tls_pem_key_name: default_tls_pem_key_name(), + tls_pem_ca_name: default_tls_pem_ca_name(), + } + } +} + fn default_pkcs12_keystore_name() -> String { FILE_PKCS12_CERT_KEYSTORE.to_owned() } diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 701a7fcf..90d903c6 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -1,4 +1,4 @@ -use std::{os::unix::prelude::FileTypeExt, path::PathBuf}; +use std::{os::unix::prelude::FileTypeExt, path::PathBuf, pin::pin}; use anyhow::Context; use clap::Parser; @@ -10,11 +10,7 @@ use futures::{FutureExt, TryStreamExt}; use grpc::csi::v1::{ controller_server::ControllerServer, identity_server::IdentityServer, node_server::NodeServer, }; -use stackable_operator::{ - CustomResourceExt, - telemetry::{Tracing, tracing::TelemetryOptions}, - utils::cluster_info::KubernetesClusterInfoOpts, -}; +use stackable_operator::{CustomResourceExt, cli::ProductOperatorRun, telemetry::Tracing}; use tokio::signal::unix::{SignalKind, signal}; use tokio_stream::wrappers::UnixListenerStream; use tonic::transport::Server; @@ -26,6 +22,7 @@ mod csi_server; mod external_crd; mod format; mod grpc; +mod truststore_controller; mod utils; pub const OPERATOR_NAME: &str = "secrets.stackable.tech"; @@ -54,11 +51,8 @@ struct SecretOperatorRun { #[clap(long, env)] privileged: bool, - #[command(flatten)] - pub telemetry_arguments: TelemetryOptions, - - #[command(flatten)] - pub cluster_info_opts: KubernetesClusterInfoOpts, + #[clap(flatten)] + common: ProductOperatorRun, } mod built_info { @@ -71,13 +65,19 @@ async fn main() -> anyhow::Result<()> { match opts.cmd { stackable_operator::cli::Command::Crd => { crd::SecretClass::print_yaml_schema(built_info::PKG_VERSION)?; + crd::TrustStore::print_yaml_schema(built_info::PKG_VERSION)?; } stackable_operator::cli::Command::Run(SecretOperatorRun { csi_endpoint, node_name, - telemetry_arguments, privileged, - cluster_info_opts, + common: + ProductOperatorRun { + product_config: _, + watch_namespace, + telemetry_arguments, + cluster_info_opts, + }, }) => { // NOTE (@NickLarsenNZ): Before stackable-telemetry was used: // - The console log level was set by `SECRET_PROVISIONER_LOG`, and is now `CONSOLE_LOG` (when using Tracing::pre_configured). @@ -108,30 +108,38 @@ async fn main() -> anyhow::Result<()> { let _ = std::fs::remove_file(&csi_endpoint); } let mut sigterm = signal(SignalKind::terminate())?; - Server::builder() - .add_service( - tonic_reflection::server::Builder::configure() - .include_reflection_service(true) - .register_encoded_file_descriptor_set(grpc::FILE_DESCRIPTOR_SET_BYTES) - .build_v1()?, - ) - .add_service(IdentityServer::new(SecretProvisionerIdentity)) - .add_service(ControllerServer::new(SecretProvisionerController { - client: client.clone(), - })) - .add_service(NodeServer::new(SecretProvisionerNode { - client, - node_name, - privileged, - })) - .serve_with_incoming_shutdown( - UnixListenerStream::new( - uds_bind_private(csi_endpoint).context("failed to bind CSI listener")?, + let csi_server = pin!( + Server::builder() + .add_service( + tonic_reflection::server::Builder::configure() + .include_reflection_service(true) + .register_encoded_file_descriptor_set(grpc::FILE_DESCRIPTOR_SET_BYTES) + .build_v1()?, ) - .map_ok(TonicUnixStream), - sigterm.recv().map(|_| ()), - ) - .await?; + .add_service(IdentityServer::new(SecretProvisionerIdentity)) + .add_service(ControllerServer::new(SecretProvisionerController { + client: client.clone(), + })) + .add_service(NodeServer::new(SecretProvisionerNode { + client: client.clone(), + node_name, + privileged, + })) + .serve_with_incoming_shutdown( + UnixListenerStream::new( + uds_bind_private(csi_endpoint) + .context("failed to bind CSI listener")?, + ) + .map_ok(TonicUnixStream), + sigterm.recv().map(|_| ()), + ) + ); + let truststore_controller = + pin!(truststore_controller::start(&client, &watch_namespace).map(Ok)); + futures::future::select(csi_server, truststore_controller) + .await + .factor_first() + .0?; } } Ok(()) diff --git a/rust/operator-binary/src/truststore_controller.rs b/rust/operator-binary/src/truststore_controller.rs new file mode 100644 index 00000000..e817d86e --- /dev/null +++ b/rust/operator-binary/src/truststore_controller.rs @@ -0,0 +1,309 @@ +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use const_format::concatcp; +use futures::StreamExt; +use kube_runtime::{ + WatchStreamExt as _, + events::{Recorder, Reporter}, + reflector::Lookup, +}; +use snafu::{OptionExt as _, ResultExt as _, Snafu}; +use stackable_operator::{ + builder::meta::ObjectMetaBuilder, + k8s_openapi::{ + ByteString, + api::core::v1::{ConfigMap, Secret}, + }, + kube::{ + Resource, + api::PartialObjectMeta, + core::{DeserializeGuard, error_boundary}, + runtime::{ + Controller, controller, + reflector::{self, ObjectRef}, + watcher, + }, + }, + logging::controller::{ReconcilerError, report_controller_reconciled}, + namespace::WatchNamespace, +}; +use strum::{EnumDiscriminants, IntoStaticStr}; + +use crate::{ + OPERATOR_NAME, + backend::{self, SecretBackendError, TrustSelector}, + crd::{SearchNamespaceMatchCondition, SecretClass, TrustStore}, + format::{ + self, + well_known::{CompatibilityOptions, NamingOptions}, + }, + utils::Flattened, +}; + +const CONTROLLER_NAME: &str = "truststore"; +const FULL_CONTROLLER_NAME: &str = concatcp!(CONTROLLER_NAME, ".", OPERATOR_NAME); + +pub async fn start(client: &stackable_operator::client::Client, watch_namespace: &WatchNamespace) { + let (secretclasses, secretclasses_writer) = reflector::store(); + let controller = Controller::new( + watch_namespace.get_api::>(client), + watcher::Config::default(), + ); + let truststores = controller.store(); + let event_recorder = Arc::new(Recorder::new(client.as_kube_client(), Reporter { + controller: FULL_CONTROLLER_NAME.to_string(), + instance: None, + })); + controller + .watches_stream( + watcher( + client.get_api::>(&()), + watcher::Config::default(), + ) + .reflect(secretclasses_writer) + .touched_objects(), + { + let truststores = truststores.clone(); + move |secretclass| { + truststores + .state() + .into_iter() + .filter(move |ts| { + ts.0.as_ref().is_ok_and(|ts| { + Some(&ts.spec.secret_class_name) == secretclass.meta().name.as_ref() + }) + }) + .map(|ts| ObjectRef::from_obj(&*ts)) + } + }, + ) + // TODO: merge this into the other ConfigMap watch + .owns( + watch_namespace.get_api::>(client), + watcher::Config::default(), + ) + .watches( + watch_namespace.get_api::>(client), + watcher::Config::default(), + secretclass_dependency_watch_mapper( + truststores.clone(), + secretclasses.clone(), + |secretclass, cm| secretclass.spec.backend.refers_to_config_map(cm), + ), + ) + .watches( + watch_namespace.get_api::>(client), + watcher::Config::default(), + secretclass_dependency_watch_mapper( + truststores, + secretclasses, + |secretclass, secret| secretclass.spec.backend.refers_to_secret(secret), + ), + ) + .run( + reconcile, + error_policy, + Arc::new(Ctx { + client: client.clone(), + }), + ) + .for_each_concurrent(16, move |res| { + let event_recorder = event_recorder.clone(); + async move { + report_controller_reconciled(&event_recorder, FULL_CONTROLLER_NAME, &res).await + } + }) + .await; +} + +/// Resolves modifications to dependencies of [`SecretClass`] objects into +/// a list of affected [`TrustStore`]s. +fn secretclass_dependency_watch_mapper( + truststores: reflector::Store>, + secretclasses: reflector::Store>, + reference_conditions: impl Copy + Fn(&SecretClass, &Dep) -> Conds, +) -> impl Fn(Dep) -> Vec>> +where + Conds: IntoIterator, +{ + move |dep| { + let potentially_matching_secretclasses = secretclasses + .state() + .into_iter() + .filter_map(move |sc| { + sc.0.as_ref().ok().and_then(|sc| { + let conditions = reference_conditions(sc, &dep) + .into_iter() + .collect::>(); + (!conditions.is_empty()).then(|| (ObjectRef::from_obj(sc), conditions)) + }) + }) + .collect::, Vec>>(); + truststores + .state() + .into_iter() + .filter(move |ts| { + ts.0.as_ref().is_ok_and(|ts| { + let Some(ts_namespace) = ts.metadata.namespace.as_deref() else { + return false; + }; + let secret_class_ref = + ObjectRef::::new(&ts.spec.secret_class_name); + potentially_matching_secretclasses + .get(&secret_class_ref) + .is_some_and(|conds| { + conds + .iter() + .any(|cond| cond.matches_pod_namespace(ts_namespace)) + }) + }) + }) + .map(|ts| ObjectRef::from_obj(&*ts)) + .collect() + } +} + +#[derive(Debug, Snafu, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("TrustStore object is invalid"))] + InvalidTrustStore { + source: error_boundary::InvalidObject, + }, + + #[snafu(display("failed to get {secret_class} for TrustStore"))] + GetSecretClass { + source: stackable_operator::client::Error, + secret_class: ObjectRef, + }, + + #[snafu(display("failed to initialize SecretClass backend for {secret_class}"))] + InitBackend { + source: backend::dynamic::FromClassError, + secret_class: ObjectRef, + }, + + #[snafu(display("failed to get trust data from backend"))] + BackendGetTrustData { source: backend::dynamic::DynError }, + + #[snafu(display("TrustStore has no associated Namespace"))] + NoTrustStoreNamespace, + + #[snafu(display("failed to convert trust data into desired format"))] + FormatData { + source: format::IntoFilesError, + secret_class: ObjectRef, + }, + + #[snafu(display("failed to build owner reference to the TrustStore"))] + BuildOwnerReference { + source: stackable_operator::builder::meta::Error, + }, + + #[snafu(display("failed to apply target {config_map} for the TrustStore"))] + ApplyTrustStoreConfigMap { + source: stackable_operator::client::Error, + config_map: ObjectRef, + }, +} +type Result = std::result::Result; +impl ReconcilerError for Error { + fn category(&self) -> &'static str { + ErrorDiscriminants::from(self).into() + } + + fn secondary_object(&self) -> Option> { + match self { + Error::InvalidTrustStore { .. } => None, + Error::GetSecretClass { secret_class, .. } => Some(secret_class.clone().erase()), + Error::InitBackend { secret_class, .. } => Some(secret_class.clone().erase()), + Error::BackendGetTrustData { source } => source.secondary_object(), + Error::NoTrustStoreNamespace => None, + Error::FormatData { secret_class, .. } => Some(secret_class.clone().erase()), + Error::BuildOwnerReference { .. } => None, + Error::ApplyTrustStoreConfigMap { config_map, .. } => Some(config_map.clone().erase()), + } + } +} + +struct Ctx { + client: stackable_operator::client::Client, +} + +async fn reconcile( + truststore: Arc>, + ctx: Arc, +) -> Result { + let truststore = truststore + .0 + .as_ref() + .map_err(error_boundary::InvalidObject::clone) + .context(InvalidTrustStoreSnafu)?; + let secret_class_name = &truststore.spec.secret_class_name; + let secret_class = ctx + .client + .get::(secret_class_name, &()) + .await + .context(GetSecretClassSnafu { + secret_class: ObjectRef::::new(secret_class_name), + })?; + let secret_class_ref = secret_class.to_object_ref(()); + let backend = backend::dynamic::from_class(&ctx.client, secret_class) + .await + .with_context(|_| InitBackendSnafu { + secret_class: secret_class_ref.clone(), + })?; + let selector = TrustSelector { + namespace: truststore + .metadata + .namespace + .clone() + .context(NoTrustStoreNamespaceSnafu)?, + }; + let trust_data = backend + .get_trust_data(&selector) + .await + .context(BackendGetTrustDataSnafu)?; + let (Flattened(string_data), Flattened(binary_data)) = trust_data + .data + .into_files( + truststore.spec.format, + NamingOptions::default(), + CompatibilityOptions::default(), + ) + .context(FormatDataSnafu { + secret_class: secret_class_ref, + })? + .into_iter() + // Try to put valid UTF-8 data into `data`, but fall back to `binary_data` otherwise + .map(|(k, v)| match String::from_utf8(v) { + Ok(v) => (Some((k, v)), None), + Err(v) => (None, Some((k, ByteString(v.into_bytes())))), + }) + .collect(); + let trust_cm = ConfigMap { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(truststore) + .ownerreference_from_resource(truststore, None, Some(true)) + .context(BuildOwnerReferenceSnafu)? + .build(), + data: Some(string_data), + binary_data: Some(binary_data), + ..Default::default() + }; + ctx.client + .apply_patch(CONTROLLER_NAME, &trust_cm, &trust_cm) + .await + .context(ApplyTrustStoreConfigMapSnafu { + config_map: &trust_cm, + })?; + Ok(controller::Action::await_change()) +} + +fn error_policy( + _obj: Arc>, + _error: &Error, + _ctx: Arc, +) -> controller::Action { + controller::Action::requeue(Duration::from_secs(5)) +} diff --git a/rust/operator-binary/src/utils.rs b/rust/operator-binary/src/utils.rs index e3e7dd71..1af8d521 100644 --- a/rust/operator-binary/src/utils.rs +++ b/rust/operator-binary/src/utils.rs @@ -201,6 +201,34 @@ impl DerefMut for Unloggable { } } +/// Wrapper type for [`Iterator::collect`] that flattens the incoming [`Iterator`]. +/// +/// This isn't super useful for "regular" collects (just call [`Iterator::flatten`]!), +/// but it can be composed with the [`FromIterator`] impl on [`tuple`]s to partition +/// an incoming iterator while giving each branch a unique type. +#[derive(Default, Debug, PartialEq, Eq)] +pub struct Flattened(pub T); + +impl Extend for Flattened +where + E: IntoIterator, + T: Extend, +{ + fn extend>(&mut self, iter: I2) { + self.0.extend(iter.into_iter().flatten()); + } +} + +impl FromIterator for Flattened +where + I: IntoIterator, + T: FromIterator, +{ + fn from_iter>(iter: I2) -> Self { + Self(iter.into_iter().flatten().collect()) + } +} + #[cfg(test)] mod tests { use futures::StreamExt; @@ -208,7 +236,7 @@ mod tests { use time::OffsetDateTime; use super::{asn1time_to_offsetdatetime, iterator_try_concat_bytes}; - use crate::utils::{FmtByteSlice, error_full_message, trystream_any}; + use crate::utils::{Flattened, FmtByteSlice, error_full_message, trystream_any}; #[test] fn fmt_hex_byte_slice() { @@ -295,4 +323,27 @@ mod tests { .unwrap() ); } + + #[test] + fn flattened_collect_single() { + let Flattened(small @ Vec:: { .. }) = [2, 10, 1000, 5, 2000] + .into_iter() + .map(|x| u8::try_from(x).ok()) + .collect(); + assert_eq!(small, vec![2, 10, 5]); + } + + #[test] + fn flattened_collect_split() { + let (Flattened(small @ Vec:: { .. }), Flattened(big @ Vec:: { .. })) = + [2, 10, 1000, 5, 2000] + .into_iter() + .map(|x| match u8::try_from(x) { + Ok(x) => (Some(x), None), + Err(_) => (None, Some(x)), + }) + .collect(); + assert_eq!(small, vec![2, 10, 5]); + assert_eq!(big, vec![1000, 2000]); + } } diff --git a/rust/p12/.github/FUNDING.yml b/rust/p12/.github/FUNDING.yml new file mode 100644 index 00000000..4f2b4e4c --- /dev/null +++ b/rust/p12/.github/FUNDING.yml @@ -0,0 +1,2 @@ +--- +github: Keruspe diff --git a/rust/p12/.github/workflows/build-and-test.yaml b/rust/p12/.github/workflows/build-and-test.yaml new file mode 100644 index 00000000..cddb0575 --- /dev/null +++ b/rust/p12/.github/workflows/build-and-test.yaml @@ -0,0 +1,44 @@ +--- +name: Build and test + +on: + push: + branches: + - master + pull_request: + +jobs: + build_and_test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + rust: [nightly, beta, stable, 1.56.0] + steps: + - uses: actions/checkout@v2 + + - name: Install latest ${{ matrix.rust }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + profile: minimal + override: true + + - name: Run cargo check + uses: actions-rs/cargo@v1 + with: + command: check + args: --all --bins --examples --tests --all-features + + - name: Run cargo check (without dev-dependencies to catch missing feature flags) + if: startsWith(matrix.rust, 'nightly') + uses: actions-rs/cargo@v1 + with: + command: check + args: -Z features=dev_dep + + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/rust/p12/.github/workflows/lint.yaml b/rust/p12/.github/workflows/lint.yaml new file mode 100644 index 00000000..3ccbedbf --- /dev/null +++ b/rust/p12/.github/workflows/lint.yaml @@ -0,0 +1,39 @@ +--- +name: Lint + +on: + push: + branches: + - master + pull_request: + +jobs: + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + components: clippy + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features -- -W clippy::all + + rustfmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + components: rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check diff --git a/rust/p12/.github/workflows/security.yaml b/rust/p12/.github/workflows/security.yaml new file mode 100644 index 00000000..e8e5b74f --- /dev/null +++ b/rust/p12/.github/workflows/security.yaml @@ -0,0 +1,18 @@ +--- +name: Security audit + +on: + push: + branches: + - master + pull_request: + +jobs: + security_audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/rust/p12/.gitignore b/rust/p12/.gitignore new file mode 100644 index 00000000..aaf79aa6 --- /dev/null +++ b/rust/p12/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +test.p12 diff --git a/rust/p12/Cargo.toml b/rust/p12/Cargo.toml new file mode 100644 index 00000000..21706521 --- /dev/null +++ b/rust/p12/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "p12" +version.workspace = true +authors = ["hjiayz ", "Marc-Antoine Perennou "] +edition = "2021" +keywords = ["pkcs12", "pkcs"] +description = "pure rust pkcs12 tool (Stackable fork)" +homepage = "https://github.com/hjiayz/p12" +repository = "https://github.com/hjiayz/p12" +readme = "README.md" +license = "MIT OR Apache-2.0" +rust-version = "1.56.0" + +# Dependencies are tracked inline for now, to minimize divergence from upstream + +[dependencies] +des = "^0.8" +getrandom = "^0.2" +hmac = "^0.12" +lazy_static = "^1.4" +rc2 = "^0.8" +sha1 = "^0.10" + +[dependencies.cbc] +version = "^0.1" +features = ["block-padding"] + +[dependencies.cipher] +version = "^0.4.2" +features = ["alloc", "block-padding"] + +[dependencies.yasna] +version = "^0.5" +features = ["std"] + +[dev-dependencies] +hex-literal = "^0.3.1" diff --git a/rust/p12/LICENSE-APACHE b/rust/p12/LICENSE-APACHE new file mode 100644 index 00000000..7eb53864 --- /dev/null +++ b/rust/p12/LICENSE-APACHE @@ -0,0 +1,14 @@ +Copyright (c) 2021 Marc-Antoine Perennou +Copyright 2020 hjiayz + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/rust/p12/LICENSE-MIT b/rust/p12/LICENSE-MIT new file mode 100644 index 00000000..a424e020 --- /dev/null +++ b/rust/p12/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2021 Marc-Antoine Perennou +Copyright (c) 2020 hjiayz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN diff --git a/rust/p12/README.md b/rust/p12/README.md new file mode 100644 index 00000000..6a3d1319 --- /dev/null +++ b/rust/p12/README.md @@ -0,0 +1,7 @@ +# p12 + +Forked from + +pure rust pkcs12 tool + +License: MIT OR Apache-2.0 diff --git a/rust/p12/ca.der b/rust/p12/ca.der new file mode 100644 index 00000000..93a59375 Binary files /dev/null and b/rust/p12/ca.der differ diff --git a/rust/p12/clientcert.der b/rust/p12/clientcert.der new file mode 100644 index 00000000..50f6b4eb Binary files /dev/null and b/rust/p12/clientcert.der differ diff --git a/rust/p12/clientkey.der b/rust/p12/clientkey.der new file mode 100644 index 00000000..d218b2e1 Binary files /dev/null and b/rust/p12/clientkey.der differ diff --git a/rust/p12/src/lib.rs b/rust/p12/src/lib.rs new file mode 100644 index 00000000..30a2006b --- /dev/null +++ b/rust/p12/src/lib.rs @@ -0,0 +1,1144 @@ +//! +//! pure rust pkcs12 tool +//! +//! + +use getrandom::getrandom; +use hmac::{Hmac, Mac}; +use lazy_static::lazy_static; +use sha1::{Digest, Sha1}; +use yasna::{ASN1Error, ASN1ErrorKind, BERReader, DERWriter, Tag, models::ObjectIdentifier}; + +type HmacSha1 = Hmac; + +fn as_oid(s: &'static [u64]) -> ObjectIdentifier { + ObjectIdentifier::from_slice(s) +} + +lazy_static! { + static ref OID_DATA_CONTENT_TYPE: ObjectIdentifier = as_oid(&[1, 2, 840, 113_549, 1, 7, 1]); + static ref OID_ENCRYPTED_DATA_CONTENT_TYPE: ObjectIdentifier = + as_oid(&[1, 2, 840, 113_549, 1, 7, 6]); + static ref OID_FRIENDLY_NAME: ObjectIdentifier = as_oid(&[1, 2, 840, 113_549, 1, 9, 20]); + static ref OID_LOCAL_KEY_ID: ObjectIdentifier = as_oid(&[1, 2, 840, 113_549, 1, 9, 21]); + static ref OID_CERT_TYPE_X509_CERTIFICATE: ObjectIdentifier = + as_oid(&[1, 2, 840, 113_549, 1, 9, 22, 1]); + static ref OID_CERT_TYPE_SDSI_CERTIFICATE: ObjectIdentifier = + as_oid(&[1, 2, 840, 113_549, 1, 9, 22, 2]); + static ref OID_PBE_WITH_SHA_AND3_KEY_TRIPLE_DESCBC: ObjectIdentifier = + as_oid(&[1, 2, 840, 113_549, 1, 12, 1, 3]); + static ref OID_SHA1: ObjectIdentifier = as_oid(&[1, 3, 14, 3, 2, 26]); + static ref OID_PBE_WITH_SHA1_AND40_BIT_RC2_CBC: ObjectIdentifier = + as_oid(&[1, 2, 840, 113_549, 1, 12, 1, 6]); + static ref OID_KEY_BAG: ObjectIdentifier = as_oid(&[1, 2, 840, 113_549, 1, 12, 10, 1, 1]); + static ref OID_PKCS8_SHROUDED_KEY_BAG: ObjectIdentifier = + as_oid(&[1, 2, 840, 113_549, 1, 12, 10, 1, 2]); + static ref OID_CERT_BAG: ObjectIdentifier = as_oid(&[1, 2, 840, 113_549, 1, 12, 10, 1, 3]); + static ref OID_CRL_BAG: ObjectIdentifier = as_oid(&[1, 2, 840, 113_549, 1, 12, 10, 1, 4]); + static ref OID_SECRET_BAG: ObjectIdentifier = as_oid(&[1, 2, 840, 113_549, 1, 12, 10, 1, 5]); + static ref OID_SAFE_CONTENTS_BAG: ObjectIdentifier = + as_oid(&[1, 2, 840, 113_549, 1, 12, 10, 1, 6]); +} + +const ITERATIONS: u64 = 2048; + +fn sha1(bytes: &[u8]) -> Vec { + let mut hasher = Sha1::new(); + hasher.update(bytes); + hasher.finalize().to_vec() +} + +#[derive(Debug, Clone)] +pub struct EncryptedContentInfo { + pub content_encryption_algorithm: AlgorithmIdentifier, + pub encrypted_content: Vec, +} + +impl EncryptedContentInfo { + pub fn parse(r: BERReader) -> Result { + r.read_sequence(|r| { + let content_type = r.next().read_oid()?; + debug_assert_eq!(content_type, *OID_DATA_CONTENT_TYPE); + let content_encryption_algorithm = AlgorithmIdentifier::parse(r.next())?; + let encrypted_content = r + .next() + .read_tagged_implicit(Tag::context(0), |r| r.read_bytes())?; + Ok(EncryptedContentInfo { + content_encryption_algorithm, + encrypted_content, + }) + }) + } + + pub fn data(&self, password: &[u8]) -> Option> { + self.content_encryption_algorithm + .decrypt_pbe(&self.encrypted_content, password) + } + + pub fn write(&self, w: DERWriter) { + w.write_sequence(|w| { + w.next().write_oid(&OID_DATA_CONTENT_TYPE); + self.content_encryption_algorithm.write(w.next()); + w.next() + .write_tagged_implicit(Tag::context(0), |w| w.write_bytes(&self.encrypted_content)); + }) + } + + pub fn to_der(&self) -> Vec { + yasna::construct_der(|w| self.write(w)) + } + + pub fn from_safe_bags( + safe_bags: &[SafeBag], + password: &[u8], + rng: &mut impl Rng, + ) -> Option { + let data = yasna::construct_der(|w| { + w.write_sequence_of(|w| { + for sb in safe_bags { + sb.write(w.next()); + } + }) + }); + let salt = rng.generate_salt()?.to_vec(); + let encrypted_content = + pbe_with_sha1_and40_bit_rc2_cbc_encrypt(&data, password, &salt, ITERATIONS)?; + let content_encryption_algorithm = + AlgorithmIdentifier::PbewithSHAAnd40BitRC2CBC(Pkcs12PbeParams { + salt, + iterations: ITERATIONS, + }); + Some(EncryptedContentInfo { + content_encryption_algorithm, + encrypted_content, + }) + } +} + +#[derive(Debug, Clone)] +pub struct EncryptedData { + pub encrypted_content_info: EncryptedContentInfo, +} + +impl EncryptedData { + pub fn parse(r: BERReader) -> Result { + r.read_sequence(|r| { + let version = r.next().read_u8()?; + debug_assert_eq!(version, 0); + + let encrypted_content_info = EncryptedContentInfo::parse(r.next())?; + Ok(EncryptedData { + encrypted_content_info, + }) + }) + } + + pub fn data(&self, password: &[u8]) -> Option> { + self.encrypted_content_info.data(password) + } + + pub fn write(&self, w: DERWriter) { + w.write_sequence(|w| { + w.next().write_u8(0); + self.encrypted_content_info.write(w.next()); + }) + } + + pub fn from_safe_bags( + safe_bags: &[SafeBag], + password: &[u8], + rng: &mut impl Rng, + ) -> Option { + let encrypted_content_info = + EncryptedContentInfo::from_safe_bags(safe_bags, password, rng)?; + Some(EncryptedData { + encrypted_content_info, + }) + } +} + +#[derive(Debug, Clone)] +pub struct OtherContext { + pub content_type: ObjectIdentifier, + pub content: Vec, +} + +#[derive(Debug, Clone)] +pub enum ContentInfo { + Data(Vec), + EncryptedData(EncryptedData), + OtherContext(OtherContext), +} + +impl ContentInfo { + pub fn parse(r: BERReader) -> Result { + r.read_sequence(|r| { + let content_type = r.next().read_oid()?; + if content_type == *OID_DATA_CONTENT_TYPE { + let data = r.next().read_tagged(Tag::context(0), |r| r.read_bytes())?; + return Ok(ContentInfo::Data(data)); + } + if content_type == *OID_ENCRYPTED_DATA_CONTENT_TYPE { + let result = r.next().read_tagged(Tag::context(0), |r| { + Ok(ContentInfo::EncryptedData(EncryptedData::parse(r)?)) + }); + return result; + } + + let content = r.next().read_tagged(Tag::context(0), |r| r.read_der())?; + Ok(ContentInfo::OtherContext(OtherContext { + content_type, + content, + })) + }) + } + + pub fn data(&self, password: &[u8]) -> Option> { + match self { + ContentInfo::Data(data) => Some(data.to_owned()), + ContentInfo::EncryptedData(encrypted) => encrypted.data(password), + ContentInfo::OtherContext(_) => None, + } + } + + pub fn oid(&self) -> ObjectIdentifier { + match self { + ContentInfo::Data(_) => OID_DATA_CONTENT_TYPE.clone(), + ContentInfo::EncryptedData(_) => OID_ENCRYPTED_DATA_CONTENT_TYPE.clone(), + ContentInfo::OtherContext(other) => other.content_type.clone(), + } + } + + pub fn write(&self, w: DERWriter) { + match self { + ContentInfo::Data(data) => w.write_sequence(|w| { + w.next().write_oid(&OID_DATA_CONTENT_TYPE); + w.next() + .write_tagged(Tag::context(0), |w| w.write_bytes(data)) + }), + ContentInfo::EncryptedData(encrypted_data) => w.write_sequence(|w| { + w.next().write_oid(&OID_ENCRYPTED_DATA_CONTENT_TYPE); + w.next() + .write_tagged(Tag::context(0), |w| encrypted_data.write(w)) + }), + ContentInfo::OtherContext(other) => w.write_sequence(|w| { + w.next().write_oid(&other.content_type); + w.next() + .write_tagged(Tag::context(0), |w| w.write_der(&other.content)) + }), + } + } + + pub fn to_der(&self) -> Vec { + yasna::construct_der(|w| self.write(w)) + } + + pub fn from_der(der: &[u8]) -> Result { + yasna::parse_der(der, Self::parse) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Pkcs12PbeParams { + pub salt: Vec, + pub iterations: u64, +} + +impl Pkcs12PbeParams { + pub fn parse(r: BERReader) -> Result { + r.read_sequence(|r| { + let salt = r.next().read_bytes()?; + let iterations = r.next().read_u64()?; + Ok(Pkcs12PbeParams { salt, iterations }) + }) + } + + pub fn write(&self, w: DERWriter) { + w.write_sequence(|w| { + w.next().write_bytes(&self.salt); + w.next().write_u64(self.iterations); + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct OtherAlgorithmIdentifier { + pub algorithm_type: ObjectIdentifier, + pub params: Option>, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum AlgorithmIdentifier { + Sha1, + PbewithSHAAnd40BitRC2CBC(Pkcs12PbeParams), + PbeWithSHAAnd3KeyTripleDESCBC(Pkcs12PbeParams), + OtherAlg(OtherAlgorithmIdentifier), +} + +impl AlgorithmIdentifier { + pub fn parse(r: BERReader) -> Result { + r.read_sequence(|r| { + let algorithm_type = r.next().read_oid()?; + if algorithm_type == *OID_SHA1 { + r.read_optional(|r| r.read_null())?; + return Ok(AlgorithmIdentifier::Sha1); + } + if algorithm_type == *OID_PBE_WITH_SHA1_AND40_BIT_RC2_CBC { + let params = Pkcs12PbeParams::parse(r.next())?; + return Ok(AlgorithmIdentifier::PbewithSHAAnd40BitRC2CBC(params)); + } + if algorithm_type == *OID_PBE_WITH_SHA_AND3_KEY_TRIPLE_DESCBC { + let params = Pkcs12PbeParams::parse(r.next())?; + return Ok(AlgorithmIdentifier::PbeWithSHAAnd3KeyTripleDESCBC(params)); + } + let params = r.read_optional(|r| r.read_der())?; + Ok(AlgorithmIdentifier::OtherAlg(OtherAlgorithmIdentifier { + algorithm_type, + params, + })) + }) + } + + pub fn decrypt_pbe(&self, ciphertext: &[u8], password: &[u8]) -> Option> { + match self { + AlgorithmIdentifier::Sha1 => None, + AlgorithmIdentifier::PbewithSHAAnd40BitRC2CBC(param) => { + pbe_with_sha1_and40_bit_rc2_cbc(ciphertext, password, ¶m.salt, param.iterations) + } + AlgorithmIdentifier::PbeWithSHAAnd3KeyTripleDESCBC(param) => { + pbe_with_sha_and3_key_triple_des_cbc( + ciphertext, + password, + ¶m.salt, + param.iterations, + ) + } + AlgorithmIdentifier::OtherAlg(_) => None, + } + } + + pub fn write(&self, w: DERWriter) { + w.write_sequence(|w| match self { + AlgorithmIdentifier::Sha1 => { + w.next().write_oid(&OID_SHA1); + w.next().write_null(); + } + AlgorithmIdentifier::PbewithSHAAnd40BitRC2CBC(p) => { + w.next().write_oid(&OID_PBE_WITH_SHA1_AND40_BIT_RC2_CBC); + p.write(w.next()); + } + AlgorithmIdentifier::PbeWithSHAAnd3KeyTripleDESCBC(p) => { + w.next().write_oid(&OID_PBE_WITH_SHA_AND3_KEY_TRIPLE_DESCBC); + p.write(w.next()); + } + AlgorithmIdentifier::OtherAlg(other) => { + w.next().write_oid(&other.algorithm_type); + if let Some(der) = &other.params { + w.next().write_der(der); + } + } + }) + } +} + +#[derive(Debug)] +pub struct DigestInfo { + pub digest_algorithm: AlgorithmIdentifier, + pub digest: Vec, +} + +impl DigestInfo { + pub fn parse(r: BERReader) -> Result { + r.read_sequence(|r| { + let digest_algorithm = AlgorithmIdentifier::parse(r.next())?; + let digest = r.next().read_bytes()?; + Ok(DigestInfo { + digest_algorithm, + digest, + }) + }) + } + + pub fn write(&self, w: DERWriter) { + w.write_sequence(|w| { + self.digest_algorithm.write(w.next()); + w.next().write_bytes(&self.digest); + }) + } +} + +#[derive(Debug)] +pub struct MacData { + pub mac: DigestInfo, + pub salt: Vec, + pub iterations: u32, +} + +impl MacData { + pub fn parse(r: BERReader) -> Result { + r.read_sequence(|r| { + let mac = DigestInfo::parse(r.next())?; + let salt = r.next().read_bytes()?; + let iterations = r.next().read_u32()?; + Ok(MacData { + mac, + salt, + iterations, + }) + }) + } + + pub fn write(&self, w: DERWriter) { + w.write_sequence(|w| { + self.mac.write(w.next()); + w.next().write_bytes(&self.salt); + w.next().write_u32(self.iterations); + }) + } + + pub fn verify_mac(&self, data: &[u8], password: &[u8]) -> bool { + debug_assert_eq!(self.mac.digest_algorithm, AlgorithmIdentifier::Sha1); + let key = pbepkcs12sha1(password, &self.salt, self.iterations as u64, 3, 20); + let mut mac = HmacSha1::new_from_slice(&key).unwrap(); + mac.update(data); + mac.verify_slice(&self.mac.digest).is_ok() + } + + pub fn new(data: &[u8], password: &[u8], rng: &mut impl Rng) -> MacData { + let salt = rng.generate_salt().unwrap(); + let key = pbepkcs12sha1(password, &salt, ITERATIONS, 3, 20); + let mut mac = HmacSha1::new_from_slice(&key).unwrap(); + mac.update(data); + let digest = mac.finalize().into_bytes().to_vec(); + MacData { + mac: DigestInfo { + digest_algorithm: AlgorithmIdentifier::Sha1, + digest, + }, + salt: salt.to_vec(), + iterations: ITERATIONS as u32, + } + } +} + +/// Random number generator +pub trait Rng { + fn generate_salt(&mut self) -> Option<[u8; 8]>; +} + +pub struct SystemRng; +impl Rng for SystemRng { + fn generate_salt(&mut self) -> Option<[u8; 8]> { + let mut buf = [0u8; 8]; + if getrandom(&mut buf).is_ok() { + Some(buf) + } else { + None + } + } +} + +#[derive(Debug)] +pub struct PFX { + pub version: u8, + pub auth_safe: ContentInfo, + pub mac_data: Option, +} + +impl PFX { + pub fn new( + cert_der: &[u8], + key_der: &[u8], + ca_der: Option<&[u8]>, + password: &str, + name: &str, + rng: &mut impl Rng, + ) -> Option { + let mut cas = vec![]; + if let Some(ca) = ca_der { + cas.push(ca); + } + Self::new_with_cas(cert_der, key_der, &cas, password, name, rng) + } + + pub fn new_with_cas( + cert_der: &[u8], + key_der: &[u8], + ca_der_list: &[&[u8]], + password: &str, + name: &str, + rng: &mut impl Rng, + ) -> Option { + let password = bmp_string(password); + let salt = rng.generate_salt()?.to_vec(); + let encrypted_data = + pbe_with_sha_and3_key_triple_des_cbc_encrypt(key_der, &password, &salt, ITERATIONS)?; + let param = Pkcs12PbeParams { + salt, + iterations: ITERATIONS, + }; + let key_bag_inner = SafeBagKind::Pkcs8ShroudedKeyBag(EncryptedPrivateKeyInfo { + encryption_algorithm: AlgorithmIdentifier::PbeWithSHAAnd3KeyTripleDESCBC(param), + encrypted_data, + }); + let friendly_name = PKCS12Attribute::FriendlyName(name.to_owned()); + let local_key_id = PKCS12Attribute::LocalKeyId(sha1(cert_der)); + let key_bag = SafeBag { + bag: key_bag_inner, + attributes: vec![friendly_name.clone(), local_key_id.clone()], + }; + let cert_bag_inner = SafeBagKind::CertBag(CertBag::X509(cert_der.to_owned())); + let cert_bag = SafeBag { + bag: cert_bag_inner, + attributes: vec![friendly_name, local_key_id], + }; + let mut cert_bags = vec![cert_bag]; + for ca in ca_der_list { + cert_bags.push(SafeBag { + bag: SafeBagKind::CertBag(CertBag::X509((*ca).to_owned())), + attributes: vec![], + }); + } + let contents = yasna::construct_der(|w| { + w.write_sequence_of(|w| { + ContentInfo::EncryptedData( + EncryptedData::from_safe_bags(&cert_bags, &password, rng) + .ok_or_else(|| ASN1Error::new(ASN1ErrorKind::Invalid)) + .unwrap(), + ) + .write(w.next()); + ContentInfo::Data(yasna::construct_der(|w| { + w.write_sequence_of(|w| { + key_bag.write(w.next()); + }) + })) + .write(w.next()); + }); + }); + let mac_data = MacData::new(&contents, &password, rng); + Some(PFX { + version: 3, + auth_safe: ContentInfo::Data(contents), + mac_data: Some(mac_data), + }) + } + + pub fn parse(bytes: &[u8]) -> Result { + yasna::parse_der(bytes, |r| { + r.read_sequence(|r| { + let version = r.next().read_u8()?; + let auth_safe = ContentInfo::parse(r.next())?; + let mac_data = r.read_optional(MacData::parse)?; + Ok(PFX { + version, + auth_safe, + mac_data, + }) + }) + }) + } + + pub fn write(&self, w: DERWriter) { + w.write_sequence(|w| { + w.next().write_u8(self.version); + self.auth_safe.write(w.next()); + if let Some(mac_data) = &self.mac_data { + mac_data.write(w.next()) + } + }) + } + + pub fn to_der(&self) -> Vec { + yasna::construct_der(|w| self.write(w)) + } + + pub fn bags(&self, password: &str) -> Result, ASN1Error> { + let password = bmp_string(password); + + let data = self + .auth_safe + .data(&password) + .ok_or_else(|| ASN1Error::new(ASN1ErrorKind::Invalid))?; + + let contents = yasna::parse_der(&data, |r| r.collect_sequence_of(ContentInfo::parse))?; + + let mut result = vec![]; + for content in contents.iter() { + let data = content + .data(&password) + .ok_or_else(|| ASN1Error::new(ASN1ErrorKind::Invalid))?; + + let safe_bags = yasna::parse_der(&data, |r| r.collect_sequence_of(SafeBag::parse))?; + + for safe_bag in safe_bags.iter() { + result.push(safe_bag.to_owned()) + } + } + Ok(result) + } + + //DER-encoded X.509 certificate + pub fn cert_bags(&self, password: &str) -> Result>, ASN1Error> { + self.cert_x509_bags(password) + } + + //DER-encoded X.509 certificate + pub fn cert_x509_bags(&self, password: &str) -> Result>, ASN1Error> { + let mut result = vec![]; + for safe_bag in self.bags(password)? { + if let Some(cert) = safe_bag.bag.get_x509_cert() { + result.push(cert); + } + } + Ok(result) + } + + pub fn cert_sdsi_bags(&self, password: &str) -> Result, ASN1Error> { + let mut result = vec![]; + for safe_bag in self.bags(password)? { + if let Some(cert) = safe_bag.bag.get_sdsi_cert() { + result.push(cert); + } + } + Ok(result) + } + + pub fn key_bags(&self, password: &str) -> Result>, ASN1Error> { + let bmp_password = bmp_string(password); + let mut result = vec![]; + for safe_bag in self.bags(password)? { + if let Some(key) = safe_bag.bag.get_key(&bmp_password) { + result.push(key); + } + } + Ok(result) + } + + pub fn verify_mac(&self, password: &str) -> bool { + let bmp_password = bmp_string(password); + if let Some(mac_data) = &self.mac_data { + return match self.auth_safe.data(&bmp_password) { + Some(data) => mac_data.verify_mac(&data, &bmp_password), + None => false, + }; + } + true + } +} + +#[inline(always)] +fn pbepkcs12sha1core(d: &[u8], i: &[u8], a: &mut Vec, iterations: u64) -> Vec { + let mut ai: Vec = d.iter().chain(i.iter()).cloned().collect(); + for _ in 0..iterations { + ai = sha1(&ai); + } + a.append(&mut ai.clone()); + ai +} + +#[allow(clippy::many_single_char_names)] +fn pbepkcs12sha1(pass: &[u8], salt: &[u8], iterations: u64, id: u8, size: u64) -> Vec { + const U: u64 = 160 / 8; + const V: u64 = 512 / 8; + let r: u64 = iterations; + let d = [id; V as usize]; + fn get_len(s: usize) -> usize { + let s = s as u64; + (V * ((s + V - 1) / V)) as usize + } + let s = salt.iter().cycle().take(get_len(salt.len())); + let p = pass.iter().cycle().take(get_len(pass.len())); + let mut i: Vec = s.chain(p).cloned().collect(); + let c = (size + U - 1) / U; + let mut a: Vec = vec![]; + for _ in 1..c { + let ai = pbepkcs12sha1core(&d, &i, &mut a, r); + + let b: Vec = ai.iter().cycle().take(V as usize).cloned().collect(); + + let b_iter = b.iter().rev().cycle().take(i.len()); + let i_b_iter = i.iter_mut().rev().zip(b_iter); + let mut inc = 1u8; + for (i3, (ii, bi)) in i_b_iter.enumerate() { + if ((i3 as u64) % V) == 0 { + inc = 1; + } + let (ii2, inc2) = ii.overflowing_add(*bi); + let (ii3, inc3) = ii2.overflowing_add(inc); + inc = (inc2 || inc3) as u8; + *ii = ii3; + } + } + + pbepkcs12sha1core(&d, &i, &mut a, r); + + a.iter().take(size as usize).cloned().collect() +} + +fn pbe_with_sha1_and40_bit_rc2_cbc( + data: &[u8], + password: &[u8], + salt: &[u8], + iterations: u64, +) -> Option> { + use cbc::{ + Decryptor, + cipher::{BlockDecryptMut, KeyIvInit, block_padding::Pkcs7}, + }; + use rc2::Rc2; + type Rc2Cbc = Decryptor; + + let dk = pbepkcs12sha1(password, salt, iterations, 1, 5); + let iv = pbepkcs12sha1(password, salt, iterations, 2, 8); + + let rc2 = Rc2Cbc::new_from_slices(&dk, &iv).ok()?; + rc2.decrypt_padded_vec_mut::(data).ok() +} + +fn pbe_with_sha1_and40_bit_rc2_cbc_encrypt( + data: &[u8], + password: &[u8], + salt: &[u8], + iterations: u64, +) -> Option> { + use cbc::{ + Encryptor, + cipher::{BlockEncryptMut, KeyIvInit, block_padding::Pkcs7}, + }; + use rc2::Rc2; + type Rc2Cbc = Encryptor; + + let dk = pbepkcs12sha1(password, salt, iterations, 1, 5); + let iv = pbepkcs12sha1(password, salt, iterations, 2, 8); + + let rc2 = Rc2Cbc::new_from_slices(&dk, &iv).ok()?; + Some(rc2.encrypt_padded_vec_mut::(data)) +} + +fn pbe_with_sha_and3_key_triple_des_cbc( + data: &[u8], + password: &[u8], + salt: &[u8], + iterations: u64, +) -> Option> { + use cbc::{ + Decryptor, + cipher::{BlockDecryptMut, KeyIvInit, block_padding::Pkcs7}, + }; + use des::TdesEde3; + type TDesCbc = Decryptor; + + let dk = pbepkcs12sha1(password, salt, iterations, 1, 24); + let iv = pbepkcs12sha1(password, salt, iterations, 2, 8); + + let tdes = TDesCbc::new_from_slices(&dk, &iv).ok()?; + tdes.decrypt_padded_vec_mut::(data).ok() +} + +fn pbe_with_sha_and3_key_triple_des_cbc_encrypt( + data: &[u8], + password: &[u8], + salt: &[u8], + iterations: u64, +) -> Option> { + use cbc::{ + Encryptor, + cipher::{BlockEncryptMut, KeyIvInit, block_padding::Pkcs7}, + }; + use des::TdesEde3; + type TDesCbc = Encryptor; + + let dk = pbepkcs12sha1(password, salt, iterations, 1, 24); + let iv = pbepkcs12sha1(password, salt, iterations, 2, 8); + + let tdes = TDesCbc::new_from_slices(&dk, &iv).ok()?; + Some(tdes.encrypt_padded_vec_mut::(data)) +} + +fn bmp_string(s: &str) -> Vec { + let utf16: Vec = s.encode_utf16().collect(); + + let mut bytes = Vec::with_capacity(utf16.len() * 2 + 2); + for c in utf16 { + bytes.push((c / 256) as u8); + bytes.push((c % 256) as u8); + } + bytes.push(0x00); + bytes.push(0x00); + bytes +} + +#[derive(Debug, Clone)] +pub enum CertBag { + X509(Vec), + SDSI(String), +} + +impl CertBag { + pub fn parse(r: BERReader) -> Result { + r.read_sequence(|r| { + let oid = r.next().read_oid()?; + if oid == *OID_CERT_TYPE_X509_CERTIFICATE { + let x509 = r.next().read_tagged(Tag::context(0), |r| r.read_bytes())?; + return Ok(CertBag::X509(x509)); + }; + if oid == *OID_CERT_TYPE_SDSI_CERTIFICATE { + let sdsi = r + .next() + .read_tagged(Tag::context(0), |r| r.read_ia5_string())?; + return Ok(CertBag::SDSI(sdsi)); + } + Err(ASN1Error::new(ASN1ErrorKind::Invalid)) + }) + } + + pub fn write(&self, w: DERWriter) { + w.write_sequence(|w| match self { + CertBag::X509(x509) => { + w.next().write_oid(&OID_CERT_TYPE_X509_CERTIFICATE); + w.next() + .write_tagged(Tag::context(0), |w| w.write_bytes(x509)); + } + CertBag::SDSI(sdsi) => { + w.next().write_oid(&OID_CERT_TYPE_SDSI_CERTIFICATE); + w.next() + .write_tagged(Tag::context(0), |w| w.write_ia5_string(sdsi)); + } + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct EncryptedPrivateKeyInfo { + pub encryption_algorithm: AlgorithmIdentifier, + pub encrypted_data: Vec, +} + +impl EncryptedPrivateKeyInfo { + pub fn parse(r: BERReader) -> Result { + r.read_sequence(|r| { + let encryption_algorithm = AlgorithmIdentifier::parse(r.next())?; + + let encrypted_data = r.next().read_bytes()?; + + Ok(EncryptedPrivateKeyInfo { + encryption_algorithm, + encrypted_data, + }) + }) + } + + pub fn write(&self, w: DERWriter) { + w.write_sequence(|w| { + self.encryption_algorithm.write(w.next()); + w.next().write_bytes(&self.encrypted_data); + }) + } + + pub fn decrypt(&self, password: &[u8]) -> Option> { + self.encryption_algorithm + .decrypt_pbe(&self.encrypted_data, password) + } +} + +#[test] +fn test_encrypted_private_key_info() { + let epki = EncryptedPrivateKeyInfo { + encryption_algorithm: AlgorithmIdentifier::Sha1, + encrypted_data: b"foo".to_vec(), + }; + let der = yasna::construct_der(|w| { + epki.write(w); + }); + let epki2 = yasna::parse_ber(&der, EncryptedPrivateKeyInfo::parse).unwrap(); + assert_eq!(epki2, epki); +} + +#[derive(Debug, Clone)] +pub struct OtherBag { + pub bag_id: ObjectIdentifier, + pub bag_value: Vec, +} + +#[derive(Debug, Clone)] +pub enum SafeBagKind { + //KeyBag(), + Pkcs8ShroudedKeyBag(EncryptedPrivateKeyInfo), + CertBag(CertBag), + //CRLBag(), + //SecretBag(), + //SafeContents(Vec), + OtherBagKind(OtherBag), +} + +impl SafeBagKind { + pub fn parse(r: BERReader, bag_id: ObjectIdentifier) -> Result { + if bag_id == *OID_CERT_BAG { + return Ok(SafeBagKind::CertBag(CertBag::parse(r)?)); + } + if bag_id == *OID_PKCS8_SHROUDED_KEY_BAG { + return Ok(SafeBagKind::Pkcs8ShroudedKeyBag( + EncryptedPrivateKeyInfo::parse(r)?, + )); + } + let bag_value = r.read_der()?; + Ok(SafeBagKind::OtherBagKind(OtherBag { bag_id, bag_value })) + } + + pub fn write(&self, w: DERWriter) { + match self { + SafeBagKind::Pkcs8ShroudedKeyBag(epk) => epk.write(w), + SafeBagKind::CertBag(cb) => cb.write(w), + SafeBagKind::OtherBagKind(other) => w.write_der(&other.bag_value), + } + } + + pub fn oid(&self) -> ObjectIdentifier { + match self { + SafeBagKind::Pkcs8ShroudedKeyBag(_) => OID_PKCS8_SHROUDED_KEY_BAG.clone(), + SafeBagKind::CertBag(_) => OID_CERT_BAG.clone(), + SafeBagKind::OtherBagKind(other) => other.bag_id.clone(), + } + } + + pub fn get_x509_cert(&self) -> Option> { + if let SafeBagKind::CertBag(CertBag::X509(x509)) = self { + return Some(x509.to_owned()); + } + None + } + + pub fn get_sdsi_cert(&self) -> Option { + if let SafeBagKind::CertBag(CertBag::SDSI(sdsi)) = self { + return Some(sdsi.to_owned()); + } + None + } + + pub fn get_key(&self, password: &[u8]) -> Option> { + if let SafeBagKind::Pkcs8ShroudedKeyBag(kb) = self { + return kb.decrypt(password); + } + None + } +} + +#[derive(Debug, Clone)] +pub struct OtherAttribute { + pub oid: ObjectIdentifier, + pub data: Vec>, +} + +#[derive(Debug, Clone)] +pub enum PKCS12Attribute { + FriendlyName(String), + LocalKeyId(Vec), + Other(OtherAttribute), +} + +impl PKCS12Attribute { + pub fn parse(r: BERReader) -> Result { + r.read_sequence(|r| { + let oid = r.next().read_oid()?; + if oid == *OID_FRIENDLY_NAME { + let name = r + .next() + .collect_set_of(|s| s.read_bmp_string())? + .pop() + .ok_or_else(|| ASN1Error::new(ASN1ErrorKind::Invalid))?; + return Ok(PKCS12Attribute::FriendlyName(name)); + } + if oid == *OID_LOCAL_KEY_ID { + let local_key_id = r + .next() + .collect_set_of(|s| s.read_bytes())? + .pop() + .ok_or_else(|| ASN1Error::new(ASN1ErrorKind::Invalid))?; + return Ok(PKCS12Attribute::LocalKeyId(local_key_id)); + } + + let data = r.next().collect_set_of(|s| s.read_der())?; + let other = OtherAttribute { oid, data }; + Ok(PKCS12Attribute::Other(other)) + }) + } + + pub fn write(&self, w: DERWriter) { + w.write_sequence(|w| match self { + PKCS12Attribute::FriendlyName(name) => { + w.next().write_oid(&OID_FRIENDLY_NAME); + w.next().write_set_of(|w| { + w.next().write_bmp_string(name); + }) + } + PKCS12Attribute::LocalKeyId(id) => { + w.next().write_oid(&OID_LOCAL_KEY_ID); + w.next().write_set_of(|w| w.next().write_bytes(id)) + } + PKCS12Attribute::Other(other) => { + w.next().write_oid(&other.oid); + w.next().write_set_of(|w| { + for bytes in other.data.iter() { + w.next().write_der(bytes); + } + }) + } + }) + } +} +#[derive(Debug, Clone)] +pub struct SafeBag { + pub bag: SafeBagKind, + pub attributes: Vec, +} + +impl SafeBag { + pub fn parse(r: BERReader) -> Result { + r.read_sequence(|r| { + let oid = r.next().read_oid()?; + + let bag = r + .next() + .read_tagged(Tag::context(0), |r| SafeBagKind::parse(r, oid))?; + + let attributes = r + .read_optional(|r| r.collect_set_of(PKCS12Attribute::parse))? + .unwrap_or_else(Vec::new); + + Ok(SafeBag { bag, attributes }) + }) + } + + pub fn write(&self, w: DERWriter) { + w.write_sequence(|w| { + w.next().write_oid(&self.bag.oid()); + w.next() + .write_tagged(Tag::context(0), |w| self.bag.write(w)); + if !self.attributes.is_empty() { + w.next().write_set_of(|w| { + for attr in &self.attributes { + attr.write(w.next()); + } + }) + } + }) + } + + pub fn friendly_name(&self) -> Option { + for attr in self.attributes.iter() { + if let PKCS12Attribute::FriendlyName(name) = attr { + return Some(name.to_owned()); + } + } + None + } + + pub fn local_key_id(&self) -> Option> { + for attr in self.attributes.iter() { + if let PKCS12Attribute::LocalKeyId(id) = attr { + return Some(id.to_owned()); + } + } + None + } +} + +#[test] +fn test_create_p12() { + use std::{ + fs::File, + io::{Read, Write}, + }; + let mut cafile = File::open("ca.der").unwrap(); + let mut ca = vec![]; + cafile.read_to_end(&mut ca).unwrap(); + let mut fcert = File::open("clientcert.der").unwrap(); + let mut fkey = File::open("clientkey.der").unwrap(); + let mut cert = vec![]; + fcert.read_to_end(&mut cert).unwrap(); + let mut key = vec![]; + fkey.read_to_end(&mut key).unwrap(); + let p12 = PFX::new(&cert, &key, Some(&ca), "changeit", "look", &mut SystemRng) + .unwrap() + .to_der(); + + let pfx = PFX::parse(&p12).unwrap(); + + let keys = pfx.key_bags("changeit").unwrap(); + assert_eq!(keys[0], key); + + let certs = pfx.cert_x509_bags("changeit").unwrap(); + assert_eq!(certs[0], cert); + assert_eq!(certs[1], ca); + assert!(pfx.verify_mac("changeit")); + + let mut fp12 = File::create("test.p12").unwrap(); + fp12.write_all(&p12).unwrap(); +} +#[test] +fn test_create_p12_without_password() { + use std::{ + fs::File, + io::{Read, Write}, + }; + let mut cafile = File::open("ca.der").unwrap(); + let mut ca = vec![]; + cafile.read_to_end(&mut ca).unwrap(); + let mut fcert = File::open("clientcert.der").unwrap(); + + let mut cert = vec![]; + fcert.read_to_end(&mut cert).unwrap(); + + let p12 = PFX::new(&cert, &[], Some(&ca), "", "look", &mut SystemRng) + .unwrap() + .to_der(); + + let pfx = PFX::parse(&p12).unwrap(); + + let certs = pfx.cert_x509_bags("").unwrap(); + assert_eq!(certs[0], cert); + assert_eq!(certs[1], ca); + assert!(pfx.verify_mac("")); + + let mut fp12 = File::create("test.p12").unwrap(); + fp12.write_all(&p12).unwrap(); +} + +#[test] +fn test_bmp_string() { + let value = bmp_string("Beavis"); + assert!( + value + == [ + 0x00, 0x42, 0x00, 0x65, 0x00, 0x61, 0x00, 0x76, 0x00, 0x69, 0x00, 0x73, 0x00, 0x00 + ] + ) +} + +#[test] +fn test_pbepkcs12sha1() { + use hex_literal::hex; + let pass = bmp_string(""); + assert_eq!(pass, vec![0, 0]); + let salt = hex!("9af4702958a8e95c"); + let iterations = 2048; + let id = 1; + let size = 24; + let result = pbepkcs12sha1(&pass, &salt, iterations, id, size); + let res = hex!("c2294aa6d02930eb5ce9c329eccb9aee1cb136baea746557"); + assert_eq!(result, res); +} + +#[test] +fn test_pbepkcs12sha1_2() { + use hex_literal::hex; + let pass = bmp_string(""); + assert_eq!(pass, vec![0, 0]); + let salt = hex!("9af4702958a8e95c"); + let iterations = 2048; + let id = 2; + let size = 8; + let result = pbepkcs12sha1(&pass, &salt, iterations, id, size); + let res = hex!("8e9f8fc7664378bc"); + assert_eq!(result, res); +} diff --git a/tests/templates/kuttl/tls-truststore/00-patch-ns.yaml.j2 b/tests/templates/kuttl/tls-truststore/00-patch-ns.yaml.j2 new file mode 100644 index 00000000..67185acf --- /dev/null +++ b/tests/templates/kuttl/tls-truststore/00-patch-ns.yaml.j2 @@ -0,0 +1,9 @@ +{% if test_scenario['values']['openshift'] == 'true' %} +# see https://github.com/stackabletech/issues/issues/566 +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: kubectl patch namespace $NAMESPACE -p '{"metadata":{"labels":{"pod-security.kubernetes.io/enforce":"privileged"}}}' + timeout: 120 +{% endif %} diff --git a/tests/templates/kuttl/tls-truststore/01-secretclass.yaml b/tests/templates/kuttl/tls-truststore/01-secretclass.yaml new file mode 100644 index 00000000..26ebc567 --- /dev/null +++ b/tests/templates/kuttl/tls-truststore/01-secretclass.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: envsubst '$NAMESPACE' < secretclass.yaml | kubectl --namespace=$NAMESPACE apply -f - diff --git a/tests/templates/kuttl/tls-truststore/02-assert.yaml b/tests/templates/kuttl/tls-truststore/02-assert.yaml new file mode 100644 index 00000000..624f35c6 --- /dev/null +++ b/tests/templates/kuttl/tls-truststore/02-assert.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 5 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: truststore-pem +# data is validated in 03-assert.yaml +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: truststore-pkcs12 +# data is validated in 03-assert.yaml +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: truststore-k8ssearch +data: + foo: bar + # Should be decoded as a valid string + baz: hello +binaryData: + # Should stay binary since it is not legal UTF-8 + actuallyBinary: aWxsZWdhbIB1dGYtOA== diff --git a/tests/templates/kuttl/tls-truststore/02-truststore.yaml b/tests/templates/kuttl/tls-truststore/02-truststore.yaml new file mode 100644 index 00000000..55a2a567 --- /dev/null +++ b/tests/templates/kuttl/tls-truststore/02-truststore.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: envsubst '$NAMESPACE' < truststore.yaml | kubectl --namespace=$NAMESPACE apply -f - diff --git a/tests/templates/kuttl/tls-truststore/03-assert.yaml b/tests/templates/kuttl/tls-truststore/03-assert.yaml new file mode 100644 index 00000000..f81a9795 --- /dev/null +++ b/tests/templates/kuttl/tls-truststore/03-assert.yaml @@ -0,0 +1,8 @@ +# Validate certificates generated by step 02 +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 5 +commands: + - script: kubectl --namespace=$NAMESPACE get cm/truststore-pem --output=jsonpath='{.data.ca\.crt}' | openssl x509 -noout + - script: kubectl --namespace=$NAMESPACE get cm/truststore-pkcs12 --output=jsonpath='{.binaryData.truststore\.p12}' | base64 -d | openssl pkcs12 -noout -passin 'pass:' -legacy diff --git a/tests/templates/kuttl/tls-truststore/secretclass.yaml b/tests/templates/kuttl/tls-truststore/secretclass.yaml new file mode 100644 index 00000000..77c29a66 --- /dev/null +++ b/tests/templates/kuttl/tls-truststore/secretclass.yaml @@ -0,0 +1,38 @@ +# $NAMESPACE will be replaced with the namespace of the test case. +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: SecretClass +metadata: + name: tls-$NAMESPACE +spec: + backend: + autoTls: + ca: + secret: + name: secret-provisioner-tls-ca + namespace: $NAMESPACE + autoGenerate: true +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: SecretClass +metadata: + name: k8ssearch-$NAMESPACE +spec: + backend: + k8sSearch: + searchNamespace: + name: $NAMESPACE + trustStoreConfigMapName: truststore-source-k8ssearch +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: truststore-source-k8ssearch +data: + foo: bar +binaryData: + # baz: "hello" + baz: aGVsbG8= + # actuallyBinary: "illegal{0x80}utf-8" (where {0x..} is a raw byte in hex) + # in this case, illegal since a byte starting with 10 is a continuation that must be preceded by a byte starting with 11 or 10 + actuallyBinary: aWxsZWdhbIB1dGYtOA== diff --git a/tests/templates/kuttl/tls-truststore/truststore.yaml b/tests/templates/kuttl/tls-truststore/truststore.yaml new file mode 100644 index 00000000..f0d66b89 --- /dev/null +++ b/tests/templates/kuttl/tls-truststore/truststore.yaml @@ -0,0 +1,24 @@ +# $NAMESPACE will be replaced with the namespace of the test case. +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: TrustStore +metadata: + name: truststore-pem +spec: + secretClassName: tls-$NAMESPACE + format: tls-pem +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: TrustStore +metadata: + name: truststore-pkcs12 +spec: + secretClassName: tls-$NAMESPACE + format: tls-pkcs12 +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: TrustStore +metadata: + name: truststore-k8ssearch +spec: + secretClassName: k8ssearch-$NAMESPACE diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 892153e2..b8f29998 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -37,6 +37,9 @@ tests: - custom-secret-names - rsa-key-length - openshift + - name: tls-truststore + dimensions: + - openshift - name: cert-manager-tls dimensions: - openshift