use core::fmt;
use std::collections::BTreeMap;
use reqwest::{
Method, StatusCode, Url,
header::{CONTENT_TYPE, HeaderValue},
};
use secrecy::{ExposeSecret, SecretString};
use serde::{
Deserialize, Deserializer, Serialize,
de::{IgnoredAny, MapAccess, SeqAccess, Visitor},
};
use crate::{
Authenticated, Client, Error, Result,
path::{validate_endpoint_path, validate_mount_path},
response::{
Empty, ListEntries, ResponseEnvelope, deserialize_bounded_string_map,
deserialize_bounded_string_vec,
},
};
#[derive(Debug)]
pub struct Pki<'a> {
client: &'a Client<Authenticated>,
mount: Vec<String>,
}
#[cfg(feature = "operator-ops")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PkiRootDeletion(());
#[cfg(feature = "operator-ops")]
impl PkiRootDeletion {
#[must_use]
pub fn confirm() -> Self {
Self(())
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct PkiRole {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issuer_ref: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_domains: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_bare_domains: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_subdomains: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_glob_domains: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_any_name: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enforce_hostnames: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_localhost: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_wildcard_certificates: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub key_usage: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ext_key_usage: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ext_key_usage_oids: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub server_flag: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_flag: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code_signing_flag: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email_protection_flag: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key_bits: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_ttl: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub not_before_duration: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key_usage_critical: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub basic_constraints_valid_for_non_ca: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub no_store: Option<bool>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PkiRoleList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl ListEntries for PkiRoleList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct PkiUrlsConfig {
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub issuing_certificates: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub crl_distribution_points: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ocsp_servers: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_templating: Option<bool>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PkiKeyGenerationType {
Internal,
Exported,
Existing,
}
impl PkiKeyGenerationType {
fn as_path_segment(self) -> &'static str {
match self {
Self::Internal => "internal",
Self::Exported => "exported",
Self::Existing => "existing",
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PkiGenerateRootRequest {
pub common_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub private_key_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_bits: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclude_cn_from_sans: Option<bool>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PkiGenerateIntermediateRequest {
pub common_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub private_key_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_bits: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_ref: Option<String>,
}
#[derive(Clone, Deserialize)]
pub struct PkiAuthorityBundle {
#[serde(default)]
pub certificate: Option<String>,
#[serde(default)]
pub csr: Option<String>,
#[serde(default)]
pub issuing_ca: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub ca_chain: Vec<String>,
#[serde(default)]
pub private_key: Option<SecretString>,
#[serde(default)]
pub private_key_type: Option<String>,
#[serde(default)]
pub serial_number: Option<String>,
#[serde(default)]
pub expiration: Option<u64>,
}
impl fmt::Debug for PkiAuthorityBundle {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PkiAuthorityBundle")
.field("certificate", &self.certificate)
.field("csr", &self.csr)
.field("issuing_ca", &self.issuing_ca)
.field("ca_chain", &self.ca_chain)
.field(
"private_key",
&self.private_key.as_ref().map(|_| "<redacted>"),
)
.field("private_key_type", &self.private_key_type)
.field("serial_number", &self.serial_number)
.field("expiration", &self.expiration)
.finish()
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PkiSignIntermediateRequest {
pub csr: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub common_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub use_csr_values: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_path_length: Option<i64>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub permitted_dns_domains: Vec<String>,
}
#[derive(Clone, Debug, Serialize)]
pub struct PkiSetSignedIntermediateRequest {
pub certificate: String,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct PkiCrlConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expiry: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disable: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ocsp_disable: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_rebuild: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_rebuild_grace_period: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_delta: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delta_rebuild_interval: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PkiTidyRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub tidy_cert_store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tidy_revoked_certs: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tidy_revocation_queue: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub safety_buffer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tidy_acme: Option<bool>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PkiTidyStatus {
#[serde(default)]
pub safety_buffer: Option<u64>,
#[serde(default)]
pub tidy_cert_store: Option<bool>,
#[serde(default)]
pub tidy_revoked_certs: Option<bool>,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub running: Option<bool>,
#[serde(default)]
pub state: Option<String>,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub time_started: Option<String>,
#[serde(default)]
pub time_finished: Option<String>,
#[serde(default)]
pub revoked_cert_deleted_count: Option<u64>,
#[serde(default)]
pub cert_store_deleted_count: Option<u64>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PkiRotateCrlResponse {
#[serde(default)]
pub success: bool,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PkiIssueRequest {
pub common_name: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub alt_names: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ip_sans: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub uri_sans: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub private_key_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclude_cn_from_sans: Option<bool>,
}
impl PkiIssueRequest {
pub fn new(common_name: impl Into<String>) -> Self {
Self {
common_name: common_name.into(),
..Self::default()
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PkiSignRequest {
pub csr: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub common_name: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub alt_names: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ip_sans: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub uri_sans: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
}
#[derive(Clone, Deserialize)]
pub struct PkiCertificateBundle {
pub certificate: String,
#[serde(default)]
pub issuing_ca: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub ca_chain: Vec<String>,
#[serde(default)]
pub private_key: Option<SecretString>,
#[serde(default)]
pub private_key_type: Option<String>,
#[serde(default)]
pub serial_number: Option<String>,
#[serde(default)]
pub expiration: Option<u64>,
}
impl fmt::Debug for PkiCertificateBundle {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PkiCertificateBundle")
.field("certificate", &self.certificate)
.field("issuing_ca", &self.issuing_ca)
.field("ca_chain", &self.ca_chain)
.field(
"private_key",
&self.private_key.as_ref().map(|_| "<redacted>"),
)
.field("private_key_type", &self.private_key_type)
.field("serial_number", &self.serial_number)
.field("expiration", &self.expiration)
.finish()
}
}
#[derive(Clone, Debug, Serialize)]
pub struct PkiRevokeRequest {
pub serial_number: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct PkiRevokeResponse {
#[serde(default)]
pub revocation_time: Option<u64>,
#[serde(default)]
pub revocation_time_rfc3339: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PkiCertificateList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl ListEntries for PkiCertificateList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct PkiCertificate {
pub certificate: String,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PkiIssuerList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl ListEntries for PkiIssuerList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PkiIssuerInfo {
#[serde(default)]
pub issuer_id: Option<String>,
#[serde(default)]
pub issuer_name: Option<String>,
#[serde(default)]
pub key_id: Option<String>,
#[serde(default)]
pub key_name: Option<String>,
#[serde(default)]
pub certificate: Option<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
pub ca_chain: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
pub manual_chain: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
pub crl_distribution_points: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
pub issuing_certificates: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
pub ocsp_servers: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
pub usage: Vec<String>,
#[serde(default)]
pub leaf_not_after_behavior: Option<String>,
#[serde(default)]
pub revocation_time: Option<u64>,
#[serde(default)]
pub revocation_time_rfc3339: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PkiKeyList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
}
impl ListEntries for PkiKeyList {
fn entries(&self) -> &[String] {
&self.keys
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PkiKeyInfo {
#[serde(default)]
pub key_id: Option<String>,
#[serde(default)]
pub key_name: Option<String>,
#[serde(default)]
pub key_type: Option<String>,
#[serde(default)]
pub key_bits: Option<u64>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PkiIssuerPatch {
#[serde(skip_serializing_if = "Option::is_none")]
pub issuer_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub manual_chain: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub leaf_not_after_behavior: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PkiImportResponse {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub imported_issuers: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub imported_keys: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub existing_issuers: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub existing_keys: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_map")]
pub mapping: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct PkiAcmeConfig {
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_issuers: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_string_or_vec")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_roles: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_role_ext_key_usage: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_directory_policy: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dns_resolver: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub eab_policy: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
}
#[derive(Clone, Deserialize)]
pub struct PkiAcmeEabToken {
#[serde(default)]
pub created_on: Option<String>,
pub id: String,
#[serde(default)]
pub key_type: Option<String>,
#[serde(default)]
pub acme_directory: Option<String>,
pub key: SecretString,
}
impl fmt::Debug for PkiAcmeEabToken {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PkiAcmeEabToken")
.field("created_on", &self.created_on)
.field("id", &self.id)
.field("key_type", &self.key_type)
.field("acme_directory", &self.acme_directory)
.field("key", &"<redacted>")
.finish()
}
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PkiAcmeEabInfo {
#[serde(default)]
pub created_on: Option<String>,
#[serde(default)]
pub key_type: Option<String>,
#[serde(default)]
pub acme_directory: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct PkiAcmeEabList {
#[serde(default, deserialize_with = "deserialize_bounded_string_vec")]
pub keys: Vec<String>,
#[serde(default, deserialize_with = "deserialize_bounded_eab_info_map")]
pub key_info: BTreeMap<String, PkiAcmeEabInfo>,
}
impl ListEntries for PkiAcmeEabList {
fn entries(&self) -> &[String] {
&self.keys
}
}
impl Client<Authenticated> {
pub fn pki(&self, mount: impl Into<String>) -> Result<Pki<'_>> {
let mount = mount.into();
Ok(Pki {
client: self,
mount: validate_mount_path(&mount)?,
})
}
}
impl Pki<'_> {
pub async fn generate_root(
&self,
generation_type: PkiKeyGenerationType,
request: &PkiGenerateRootRequest,
) -> Result<PkiAuthorityBundle> {
self.enveloped(
Method::POST,
&self.path(&["root", "generate", generation_type.as_path_segment()])?,
Some(request),
)
.await
}
#[cfg(feature = "operator-ops")]
pub async fn delete_root(&self, _confirmation: PkiRootDeletion) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&self.path(&["root"])?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn generate_intermediate(
&self,
generation_type: PkiKeyGenerationType,
request: &PkiGenerateIntermediateRequest,
) -> Result<PkiAuthorityBundle> {
self.enveloped(
Method::POST,
&self.path(&[
"intermediate",
"generate",
generation_type.as_path_segment(),
])?,
Some(request),
)
.await
}
pub async fn sign_intermediate(
&self,
request: &PkiSignIntermediateRequest,
) -> Result<PkiCertificateBundle> {
self.enveloped(
Method::POST,
&self.path(&["root", "sign-intermediate"])?,
Some(request),
)
.await
}
pub async fn set_signed_intermediate(
&self,
request: &PkiSetSignedIntermediateRequest,
) -> Result<Empty> {
self.client
.request_json(
Method::POST,
&self.path(&["intermediate", "set-signed"])?,
Some(request),
)
.await
}
pub async fn write_role(&self, name: &str, role: &PkiRole) -> Result<Empty> {
self.client
.request_json(Method::POST, &self.path(&["roles", name])?, Some(role))
.await
}
pub async fn patch_role(&self, name: &str, patch: &PkiRole) -> Result<PkiRole> {
self.enveloped_with_headers(
Method::PATCH,
&self.path(&["roles", name])?,
&[(
CONTENT_TYPE,
HeaderValue::from_static("application/merge-patch+json"),
)],
Some(patch),
)
.await
}
pub async fn read_role(&self, name: &str) -> Result<PkiRole> {
self.enveloped(
Method::GET,
&self.path(&["roles", name])?,
Option::<&Empty>::None,
)
.await
}
pub async fn list_roles(&self) -> Result<PkiRoleList> {
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
self.enveloped(method, &self.path(&["roles"])?, Option::<&Empty>::None)
.await
}
pub async fn delete_role(&self, name: &str) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&self.path(&["roles", name])?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn read_urls(&self) -> Result<PkiUrlsConfig> {
self.enveloped(
Method::GET,
&self.path(&["config", "urls"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn write_urls(&self, config: &PkiUrlsConfig) -> Result<Empty> {
self.client
.request_json(Method::POST, &self.path(&["config", "urls"])?, Some(config))
.await
}
pub async fn read_crl_config(&self) -> Result<PkiCrlConfig> {
self.enveloped(
Method::GET,
&self.path(&["config", "crl"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn write_crl_config(&self, config: &PkiCrlConfig) -> Result<Empty> {
self.client
.request_json(Method::POST, &self.path(&["config", "crl"])?, Some(config))
.await
}
pub async fn rotate_crl(&self) -> Result<PkiRotateCrlResponse> {
self.enveloped(
Method::POST,
&self.path(&["crl", "rotate"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn tidy(&self, request: &PkiTidyRequest) -> Result<Empty> {
self.client
.request_json(Method::POST, &self.path(&["tidy"])?, Some(request))
.await
}
pub async fn tidy_status(&self) -> Result<PkiTidyStatus> {
self.enveloped(
Method::GET,
&self.path(&["tidy-status"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn tidy_cancel(&self) -> Result<PkiTidyStatus> {
self.enveloped(
Method::POST,
&self.path(&["tidy-cancel"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn issue(
&self,
role: &str,
request: &PkiIssueRequest,
) -> Result<PkiCertificateBundle> {
self.enveloped(Method::POST, &self.path(&["issue", role])?, Some(request))
.await
}
pub async fn sign(&self, role: &str, request: &PkiSignRequest) -> Result<PkiCertificateBundle> {
self.enveloped(Method::POST, &self.path(&["sign", role])?, Some(request))
.await
}
pub async fn revoke(&self, request: &PkiRevokeRequest) -> Result<PkiRevokeResponse> {
self.enveloped(Method::POST, &self.path(&["revoke"])?, Some(request))
.await
}
pub async fn list_certificates(&self) -> Result<PkiCertificateList> {
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
self.enveloped(method, &self.path(&["certs"])?, Option::<&Empty>::None)
.await
}
pub async fn read_certificate(&self, serial: &str) -> Result<PkiCertificate> {
self.enveloped(
Method::GET,
&self.path(&["cert", serial])?,
Option::<&Empty>::None,
)
.await
}
pub async fn list_issuers(&self) -> Result<PkiIssuerList> {
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
self.enveloped(method, &self.path(&["issuers"])?, Option::<&Empty>::None)
.await
}
pub async fn read_issuer(&self, issuer_ref: &str) -> Result<PkiIssuerInfo> {
self.enveloped(
Method::GET,
&self.path(&["issuer", issuer_ref])?,
Option::<&Empty>::None,
)
.await
}
pub async fn delete_issuer(&self, issuer_ref: &str) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&self.path(&["issuer", issuer_ref])?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn list_keys(&self) -> Result<PkiKeyList> {
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
self.enveloped(method, &self.path(&["keys"])?, Option::<&Empty>::None)
.await
}
pub async fn read_key(&self, key_ref: &str) -> Result<PkiKeyInfo> {
self.enveloped(
Method::GET,
&self.path(&["key", key_ref])?,
Option::<&Empty>::None,
)
.await
}
pub async fn delete_key(&self, key_ref: &str) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&self.path(&["key", key_ref])?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub async fn patch_issuer(
&self,
issuer_ref: &str,
patch: &PkiIssuerPatch,
) -> Result<PkiIssuerInfo> {
self.enveloped_with_headers(
Method::PATCH,
&self.path(&["issuer", issuer_ref])?,
&[(
CONTENT_TYPE,
HeaderValue::from_static("application/merge-patch+json"),
)],
Some(patch),
)
.await
}
pub async fn revoke_issuer(&self, issuer_ref: &str) -> Result<PkiIssuerInfo> {
self.enveloped(
Method::POST,
&self.path(&["issuer", issuer_ref, "revoke"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn import_ca_bundle(&self, pem_bundle: &SecretString) -> Result<PkiImportResponse> {
let payload = PkiPemBundlePayload {
pem_bundle: pem_bundle.expose_secret(),
};
self.enveloped(Method::POST, &self.path(&["config", "ca"])?, Some(&payload))
.await
}
pub async fn import_issuer_bundle(
&self,
pem_bundle: &SecretString,
) -> Result<PkiImportResponse> {
let payload = PkiPemBundlePayload {
pem_bundle: pem_bundle.expose_secret(),
};
self.enveloped(
Method::POST,
&self.path(&["issuers", "import", "bundle"])?,
Some(&payload),
)
.await
}
pub async fn import_issuer_certificates(&self, pem_bundle: &str) -> Result<PkiImportResponse> {
let payload = PkiPemBundlePayload { pem_bundle };
self.enveloped(
Method::POST,
&self.path(&["issuers", "import", "cert"])?,
Some(&payload),
)
.await
}
pub async fn import_key(
&self,
pem_bundle: &SecretString,
key_name: Option<&str>,
) -> Result<PkiKeyInfo> {
let payload = PkiImportKeyPayload {
pem_bundle: pem_bundle.expose_secret(),
key_name,
};
self.enveloped(
Method::POST,
&self.path(&["keys", "import"])?,
Some(&payload),
)
.await
}
pub async fn rename_key(&self, key_ref: &str, key_name: &str) -> Result<PkiKeyInfo> {
let payload = PkiRenameKeyPayload { key_name };
self.enveloped(Method::POST, &self.path(&["key", key_ref])?, Some(&payload))
.await
}
pub async fn read_acme_config(&self) -> Result<PkiAcmeConfig> {
self.enveloped(
Method::GET,
&self.path(&["config", "acme"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn write_acme_config(&self, config: &PkiAcmeConfig) -> Result<PkiAcmeConfig> {
self.enveloped(Method::POST, &self.path(&["config", "acme"])?, Some(config))
.await
}
pub async fn generate_acme_eab(&self) -> Result<PkiAcmeEabToken> {
self.enveloped(
Method::POST,
&self.path(&["acme", "new-eab"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn generate_issuer_acme_eab(&self, issuer_ref: &str) -> Result<PkiAcmeEabToken> {
self.enveloped(
Method::POST,
&self.path(&["issuer", issuer_ref, "acme", "new-eab"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn generate_role_acme_eab(&self, role: &str) -> Result<PkiAcmeEabToken> {
self.enveloped(
Method::POST,
&self.path(&["roles", role, "acme", "new-eab"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn generate_issuer_role_acme_eab(
&self,
issuer_ref: &str,
role: &str,
) -> Result<PkiAcmeEabToken> {
self.enveloped(
Method::POST,
&self.path(&["issuer", issuer_ref, "roles", role, "acme", "new-eab"])?,
Option::<&Empty>::None,
)
.await
}
pub async fn list_acme_eab_tokens(&self) -> Result<PkiAcmeEabList> {
let method =
Method::from_bytes(b"LIST").map_err(|error| Error::InvalidHeader(error.to_string()))?;
self.enveloped(method, &self.path(&["eab"])?, Option::<&Empty>::None)
.await
}
pub async fn delete_acme_eab_token(&self, key_id: &str) -> Result<Empty> {
self.client
.request_json_accepting(
Method::DELETE,
&self.path(&["eab", key_id])?,
Option::<&Empty>::None,
&[StatusCode::OK, StatusCode::NO_CONTENT],
)
.await
}
pub fn acme_directory_url(&self) -> Result<Url> {
self.client
.url_for_path(&self.path(&["acme", "directory"])?)
}
pub fn issuer_acme_directory_url(&self, issuer_ref: &str) -> Result<Url> {
self.client
.url_for_path(&self.path(&["issuer", issuer_ref, "acme", "directory"])?)
}
pub fn role_acme_directory_url(&self, role: &str) -> Result<Url> {
self.client
.url_for_path(&self.path(&["roles", role, "acme", "directory"])?)
}
pub fn issuer_role_acme_directory_url(&self, issuer_ref: &str, role: &str) -> Result<Url> {
self.client.url_for_path(&self.path(&[
"issuer",
issuer_ref,
"roles",
role,
"acme",
"directory",
])?)
}
async fn enveloped<T, B>(&self, method: Method, path: &str, request: Option<&B>) -> Result<T>
where
T: for<'de> Deserialize<'de>,
B: Serialize + ?Sized,
{
let envelope: ResponseEnvelope<T> = self.client.request_json(method, path, request).await?;
Ok(envelope.data)
}
async fn enveloped_with_headers<T, B>(
&self,
method: Method,
path: &str,
headers: &[(reqwest::header::HeaderName, HeaderValue)],
request: Option<&B>,
) -> Result<T>
where
T: for<'de> Deserialize<'de>,
B: Serialize + ?Sized,
{
let envelope: ResponseEnvelope<T> = self
.client
.request_json_headers_accepting(method, path, headers, request, &[StatusCode::OK])
.await?;
Ok(envelope.data)
}
fn path(&self, tail: &[&str]) -> Result<String> {
let mut segments = self.mount.clone();
for segment in tail {
segments.extend(validate_endpoint_path(segment)?);
}
Ok(segments.join("/"))
}
}
#[derive(Serialize)]
struct PkiPemBundlePayload<'a> {
pem_bundle: &'a str,
}
#[derive(Serialize)]
struct PkiImportKeyPayload<'a> {
pem_bundle: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
key_name: Option<&'a str>,
}
#[derive(Serialize)]
struct PkiRenameKeyPayload<'a> {
key_name: &'a str,
}
fn deserialize_bounded_string_or_vec<'de, D>(
deserializer: D,
) -> core::result::Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(StringOrListVisitor::<{ crate::response::MAX_RESPONSE_STRINGS }>)
}
struct StringOrListVisitor<const MAX: usize>;
impl<'de, const MAX: usize> Visitor<'de> for StringOrListVisitor<MAX> {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"a comma-separated string or a list of at most {MAX} strings"
)
}
fn visit_unit<E>(self) -> core::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Vec::new())
}
fn visit_str<E>(self, value: &str) -> core::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
if value.trim().is_empty() {
return Ok(Vec::new());
}
let values: Vec<String> = value
.split(',')
.map(str::trim)
.filter(|part| !part.is_empty())
.map(str::to_owned)
.collect();
if values.len() > MAX {
return Err(E::custom("OpenBao string list exceeds item limit"));
}
Ok(values)
}
fn visit_string<E>(self, value: String) -> core::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_str(&value)
}
fn visit_seq<A>(self, mut seq: A) -> core::result::Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut values = Vec::new();
while values.len() < MAX {
let Some(value) = seq.next_element::<String>()? else {
return Ok(values);
};
values.push(value);
}
if seq.next_element::<IgnoredAny>()?.is_some() {
return Err(serde::de::Error::custom(
"OpenBao string list exceeds item limit",
));
}
Ok(values)
}
}
fn deserialize_bounded_eab_info_map<'de, D>(
deserializer: D,
) -> core::result::Result<BTreeMap<String, PkiAcmeEabInfo>, D::Error>
where
D: Deserializer<'de>,
{
deserializer
.deserialize_map(BoundedEabInfoMapVisitor::<{ crate::response::MAX_RESPONSE_STRINGS }>)
}
struct BoundedEabInfoMapVisitor<const MAX: usize>;
impl<'de, const MAX: usize> Visitor<'de> for BoundedEabInfoMapVisitor<MAX> {
type Value = BTreeMap<String, PkiAcmeEabInfo>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"a map of at most {MAX} ACME EAB metadata entries"
)
}
fn visit_map<A>(self, mut map: A) -> core::result::Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut values = BTreeMap::new();
while values.len() < MAX {
let Some((key, value)) = map.next_entry::<String, PkiAcmeEabInfo>()? else {
return Ok(values);
};
values.insert(key, value);
}
if map.next_entry::<IgnoredAny, IgnoredAny>()?.is_some() {
return Err(serde::de::Error::custom(
"OpenBao ACME EAB metadata exceeds item limit",
));
}
Ok(values)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
#![allow(deprecated)]
use crate::{Client, OpenBaoConfig};
use secrecy::{ExposeSecret, SecretString};
use super::{
PkiAcmeConfig, PkiAcmeEabList, PkiAcmeEabToken, PkiAuthorityBundle, PkiCertificateBundle,
PkiImportResponse, PkiIssuerInfo, PkiIssuerList, PkiKeyList, PkiRole, PkiRoleList,
};
#[test]
fn pki_role_accepts_string_and_array_lists() {
let role: PkiRole = serde_json::from_str(
r#"{"allowed_domains":"example.com,api.example.com","key_usage":["DigitalSignature"]}"#,
)
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(role.allowed_domains, ["example.com", "api.example.com"]);
assert_eq!(role.key_usage, ["DigitalSignature"]);
}
#[test]
fn pki_certificate_bundle_redacts_private_key_debug() {
let bundle: PkiCertificateBundle = serde_json::from_str(
r#"{"certificate":"cert","private_key":"secret-key","serial_number":"01"}"#,
)
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(
bundle.private_key.as_ref().map(SecretString::expose_secret),
Some("secret-key")
);
let debug = format!("{bundle:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("secret-key"));
}
#[test]
fn pki_authority_bundle_redacts_private_key_debug() {
let bundle: PkiAuthorityBundle =
serde_json::from_str(r#"{"csr":"csr","private_key":"authority-key"}"#)
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(
bundle.private_key.as_ref().map(SecretString::expose_secret),
Some("authority-key")
);
let debug = format!("{bundle:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("authority-key"));
}
#[test]
fn pki_role_list_is_bounded() {
let mut keys = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
keys.push(format!("role-{index}"));
}
let value = serde_json::json!({ "keys": keys });
let error = match serde_json::from_value::<PkiRoleList>(value) {
Ok(_) => panic!("oversized PKI role list unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
#[test]
fn pki_issuer_info_accepts_string_and_array_lists() {
let issuer: PkiIssuerInfo = serde_json::from_str(
r#"{
"issuer_id":"issuer-1",
"manual_chain":"root,intermediate",
"usage":["issuing-certificates","crl-signing"],
"issuing_certificates":"https://bao.example/v1/pki/ca"
}"#,
)
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(issuer.manual_chain, ["root", "intermediate"]);
assert_eq!(issuer.usage, ["issuing-certificates", "crl-signing"]);
assert_eq!(
issuer.issuing_certificates,
["https://bao.example/v1/pki/ca"]
);
}
#[test]
fn pki_issuer_and_key_lists_are_bounded() {
let mut keys = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
keys.push(format!("item-{index}"));
}
let issuer_error =
match serde_json::from_value::<PkiIssuerList>(serde_json::json!({ "keys": keys })) {
Ok(_) => panic!("oversized PKI issuer list unexpectedly decoded"),
Err(error) => error,
};
assert!(issuer_error.to_string().contains("exceeds item limit"));
let mut keys = Vec::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
keys.push(format!("item-{index}"));
}
let key_error =
match serde_json::from_value::<PkiKeyList>(serde_json::json!({ "keys": keys })) {
Ok(_) => panic!("oversized PKI key list unexpectedly decoded"),
Err(error) => error,
};
assert!(key_error.to_string().contains("exceeds item limit"));
}
#[test]
fn pki_acme_config_accepts_string_and_array_lists() {
let config: PkiAcmeConfig = serde_json::from_str(
r#"{
"allowed_issuers":"*",
"allowed_roles":["web","api"],
"enabled":true,
"eab_policy":"always-required"
}"#,
)
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(config.allowed_issuers, ["*"]);
assert_eq!(config.allowed_roles, ["web", "api"]);
assert_eq!(config.enabled, Some(true));
}
#[test]
fn pki_acme_eab_token_redacts_key_debug() {
let token: PkiAcmeEabToken = serde_json::from_str(
r#"{"id":"eab-1","key_type":"hs","acme_directory":"acme/directory","key":"hmac-secret"}"#,
)
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(token.key.expose_secret(), "hmac-secret");
let debug = format!("{token:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("hmac-secret"));
}
#[test]
fn pki_acme_eab_metadata_map_is_bounded() {
let mut keys = Vec::new();
let mut key_info = serde_json::Map::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
let key = format!("eab-{index}");
keys.push(key.clone());
key_info.insert(
key,
serde_json::json!({
"created_on": "2026-05-29T00:00:00Z",
"key_type": "hs",
"acme_directory": "acme/directory"
}),
);
}
let value = serde_json::json!({ "keys": keys, "key_info": key_info });
let error = match serde_json::from_value::<PkiAcmeEabList>(value) {
Ok(_) => panic!("oversized PKI ACME EAB list unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
#[test]
fn pki_import_response_maps_are_bounded() {
let mut mapping = serde_json::Map::new();
for index in 0..=crate::response::MAX_RESPONSE_STRINGS {
mapping.insert(format!("issuer-{index}"), serde_json::json!("key"));
}
let value = serde_json::json!({
"imported_issuers": [],
"imported_keys": [],
"existing_issuers": [],
"existing_keys": [],
"mapping": mapping
});
let error = match serde_json::from_value::<PkiImportResponse>(value) {
Ok(_) => panic!("oversized PKI import mapping unexpectedly decoded"),
Err(error) => error,
};
assert!(error.to_string().contains("exceeds item limit"));
}
#[test]
fn pki_acme_directory_urls_are_validated_and_built_from_base_url() {
let config =
OpenBaoConfig::new("https://bao.example.com").unwrap_or_else(|error| panic!("{error}"));
let client = Client::from_config(config)
.unwrap_or_else(|error| panic!("{error}"))
.with_token(SecretString::from("token"));
let pki = client.pki("pki").unwrap_or_else(|error| panic!("{error}"));
assert_eq!(
pki.acme_directory_url()
.unwrap_or_else(|error| panic!("{error}"))
.as_str(),
"https://bao.example.com/v1/pki/acme/directory"
);
assert_eq!(
pki.issuer_acme_directory_url("issuer-1")
.unwrap_or_else(|error| panic!("{error}"))
.as_str(),
"https://bao.example.com/v1/pki/issuer/issuer-1/acme/directory"
);
assert_eq!(
pki.role_acme_directory_url("web")
.unwrap_or_else(|error| panic!("{error}"))
.as_str(),
"https://bao.example.com/v1/pki/roles/web/acme/directory"
);
assert_eq!(
pki.issuer_role_acme_directory_url("issuer-1", "web")
.unwrap_or_else(|error| panic!("{error}"))
.as_str(),
"https://bao.example.com/v1/pki/issuer/issuer-1/roles/web/acme/directory"
);
assert!(pki.issuer_acme_directory_url("../issuer").is_err());
assert!(pki.role_acme_directory_url("web?x=1").is_err());
}
}