diff --git a/Cargo.lock b/Cargo.lock index 3f5b822ac..1c7e2e54f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -939,7 +939,7 @@ dependencies = [ [[package]] name = "cache" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "tokio", @@ -1131,7 +1131,7 @@ dependencies = [ [[package]] name = "command" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "komodo_client", "run_command", @@ -1157,7 +1157,7 @@ checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" [[package]] name = "config" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "colored", "indexmap 2.11.4", @@ -1442,7 +1442,7 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "database" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "async-compression", @@ -1750,7 +1750,7 @@ dependencies = [ [[package]] name = "environment" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "formatting", @@ -1760,7 +1760,7 @@ dependencies = [ [[package]] name = "environment_file" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "thiserror 2.0.17", ] @@ -1856,7 +1856,7 @@ dependencies = [ [[package]] name = "formatting" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "serror", ] @@ -2028,7 +2028,7 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "git" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "cache", @@ -2628,7 +2628,7 @@ dependencies = [ [[package]] name = "interpolate" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "komodo_client", @@ -2759,7 +2759,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "chrono", @@ -2785,7 +2785,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "async_timing_util", @@ -2821,7 +2821,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "arc-swap", @@ -2895,7 +2895,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "arc-swap", @@ -3018,7 +3018,7 @@ dependencies = [ [[package]] name = "logger" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "komodo_client", @@ -3290,7 +3290,7 @@ dependencies = [ [[package]] name = "noise" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "base64 0.22.1", @@ -3797,7 +3797,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "periphery_client" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "komodo_client", "resolver_api", @@ -4344,7 +4344,7 @@ dependencies = [ [[package]] name = "response" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "axum", @@ -4666,7 +4666,7 @@ dependencies = [ [[package]] name = "secret_file" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "tokio", ] @@ -5679,7 +5679,7 @@ dependencies = [ [[package]] name = "transport" -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index 07330f448..59c055d2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "2.0.0-dev-45" +version = "2.0.0-dev-46" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" diff --git a/bin/core/src/api/execute/maintenance.rs b/bin/core/src/api/execute/maintenance.rs index cd15637ac..e73106099 100644 --- a/bin/core/src/api/execute/maintenance.rs +++ b/bin/core/src/api/execute/maintenance.rs @@ -21,7 +21,7 @@ use komodo_client::{ stack::StackState, }, }; -use noise::key::generate_write_keys; +use noise::key::EncodedKeyPair; use periphery_client::api; use reqwest::StatusCode; use resolver_api::Resolve; @@ -32,7 +32,7 @@ use crate::{ api::execute::{ ExecuteArgs, pull_deployment_inner, pull_stack_inner, }, - config::{core_config, core_private_key, core_public_key}, + config::{core_config, core_keys}, helpers::{periphery_client, update::update_update}, resource::rotate_server_keys, state::{ @@ -506,14 +506,15 @@ impl Resolve for RotateCoreKeys { ); } - let keys = generate_write_keys(private_key_path)?; + let keys = + EncodedKeyPair::generate_write_async(private_key_path).await?; - info!("New Public Key: {}", keys.public); + let public_key = keys.public.to_string(); + info!("New Public Key: {public_key}"); - core_private_key().store(Arc::new(keys.private.into_inner())); - core_public_key().store(Arc::new(keys.public.clone())); + core_keys().store(Arc::new(keys)); - let mut log = format!("New Public Key: {}\n", keys.public); + let mut log = format!("New Public Key: {public_key}\n"); for (server, state) in servers { match state { @@ -539,7 +540,7 @@ impl Resolve for RotateCoreKeys { let periphery = periphery_client(&server).await?; let res = periphery .request(api::keys::RotateCorePublicKey { - public_key: keys.public.to_string(), + public_key: public_key.clone(), }) .await; match res { diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index 40f18c016..677fd0135 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -28,7 +28,7 @@ use uuid::Uuid; use crate::{ auth::auth_request, - config::{core_config, core_public_key}, + config::{core_config, core_keys}, helpers::periphery_client, resource, }; @@ -308,7 +308,7 @@ impl Resolve for GetCoreInfo { .map(|i| i.namespace.to_string()) .collect(), timezone: config.timezone.clone(), - public_key: core_public_key().load().to_string(), + public_key: core_keys().load().public.to_string(), }; Ok(info) } diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs index cb71ad9bf..b5f0187cb 100644 --- a/bin/core/src/config.rs +++ b/bin/core/src/config.rs @@ -20,35 +20,22 @@ use komodo_client::entities::{ }, logger::LogConfig, }; -use noise::key::{SpkiPublicKey, load_maybe_generate_private_key}; +use noise::key::{EncodedKeyPair, SpkiPublicKey}; /// Should call in startup to ensure Core errors without valid private key. -pub fn core_private_key() -> &'static ArcSwap { - static CORE_PRIVATE_KEY: OnceLock> = +pub fn core_keys() -> &'static ArcSwap { + static CORE_KEYS: OnceLock> = OnceLock::new(); - CORE_PRIVATE_KEY.get_or_init(|| { + CORE_KEYS.get_or_init(|| { let config = core_config(); - let private_key = + let keys = if let Some(path) = config.private_key.strip_prefix("file:") { - load_maybe_generate_private_key(path).unwrap() + EncodedKeyPair::load_maybe_generate(path) } else { - config.private_key.clone() - }; - ArcSwap::new(Arc::new(private_key)) - }) -} - -/// Should call in startup to ensure Core errors without valid private key. -pub fn core_public_key() -> &'static ArcSwap { - static CORE_PUBLIC_KEY: OnceLock> = - OnceLock::new(); - CORE_PUBLIC_KEY.get_or_init(|| { - let public_key = SpkiPublicKey::from_private_key( - core_private_key().load().as_str(), - ) - .context("Got invalid private key") - .unwrap(); - ArcSwap::new(Arc::new(public_key)) + EncodedKeyPair::from_private_key(&config.private_key) + } + .unwrap(); + ArcSwap::new(Arc::new(keys)) }) } diff --git a/bin/core/src/connection/mod.rs b/bin/core/src/connection/mod.rs index 585c8f7bf..22f1b04ae 100644 --- a/bin/core/src/connection/mod.rs +++ b/bin/core/src/connection/mod.rs @@ -31,7 +31,7 @@ use transport::{ }; use crate::{ - config::{core_private_key, periphery_public_keys}, + config::{core_keys, periphery_public_keys}, periphery::ConnectionChannels, state::db_client, }; @@ -309,7 +309,7 @@ impl PeripheryConnection { L::login(LoginFlowArgs { socket, identifiers, - private_key: core_private_key().load().as_str(), + private_key: core_keys().load().private.as_str(), public_key_validator: self.args.borrow(), }) .await?; diff --git a/bin/core/src/connection/server.rs b/bin/core/src/connection/server.rs index c02c877fa..19038e210 100644 --- a/bin/core/src/connection/server.rs +++ b/bin/core/src/connection/server.rs @@ -31,7 +31,7 @@ use transport::{ use crate::{ api::write::WriteArgs, - config::core_private_key, + config::core_keys, helpers::query::id_or_name_filter, resource::KomodoResource, state::{db_client, periphery_connections}, @@ -162,7 +162,7 @@ async fn onboard_server_handler( let onboarding_key = match ServerLoginFlow::login(LoginFlowArgs { socket: &mut socket, identifiers: identifiers.build(query.as_bytes()), - private_key: core_private_key().load().as_str(), + private_key: core_keys().load().private.as_str(), public_key_validator: CreationKeyValidator, }) .await diff --git a/bin/core/src/main.rs b/bin/core/src/main.rs index 9abf8d8fb..450ed0776 100644 --- a/bin/core/src/main.rs +++ b/bin/core/src/main.rs @@ -13,7 +13,7 @@ use tower_http::{ services::{ServeDir, ServeFile}, }; -use crate::config::{core_config, core_public_key}; +use crate::config::{core_config, core_keys}; mod alert; mod api; @@ -54,7 +54,7 @@ async fn app() -> anyhow::Result<()> { } // Init + log public key. Will crash if invalid private key here. - info!("Public Key: {}", core_public_key().load()); + info!("Public Key: {}", core_keys().load().public); rustls::crypto::aws_lc_rs::default_provider() .install_default() diff --git a/bin/periphery/src/api/keys.rs b/bin/periphery/src/api/keys.rs index f880a5727..640105748 100644 --- a/bin/periphery/src/api/keys.rs +++ b/bin/periphery/src/api/keys.rs @@ -1,18 +1,14 @@ use std::sync::Arc; use komodo_client::entities::NoData; -use noise::key::{ - EncodedKeyPair, SpkiPublicKey, generate_write_keys, -}; +use noise::key::{EncodedKeyPair, SpkiPublicKey}; use periphery_client::api::keys::{ RotateCorePublicKey, RotatePrivateKey, RotatePrivateKeyResponse, }; use resolver_api::Resolve; use crate::{ - config::{ - periphery_config, periphery_private_key, periphery_public_key, - }, + config::{periphery_config, periphery_keys}, connection::core_public_keys, }; @@ -36,22 +32,24 @@ impl Resolve for RotatePrivateKey { .into_inner(), }); } - Some(path) => generate_write_keys(path)?, + Some(path) => { + EncodedKeyPair::generate_write_async(path).await? + } }, - None => generate_write_keys( - config.root_directory.join("keys/periphery.key"), - )?, + None => { + EncodedKeyPair::generate_write_async( + config.root_directory.join("keys/periphery.key"), + ) + .await? + } }; - info!("New Public Key: {}", keys.public); + let public_key = keys.public.to_string(); + info!("New Public Key: {public_key}"); - periphery_private_key() - .store(Arc::new(keys.private.into_inner())); - periphery_public_key().store(Arc::new(keys.public.clone())); + periphery_keys().store(Arc::new(keys)); - Ok(RotatePrivateKeyResponse { - public_key: keys.public.into_inner(), - }) + Ok(RotatePrivateKeyResponse { public_key }) } } @@ -75,9 +73,13 @@ impl Resolve for RotateCorePublicKey { return Ok(NoData {}); }; - SpkiPublicKey::from(self.public_key) - .write_pem_async(path) - .await?; + let public_key = SpkiPublicKey::from(self.public_key); + + // Check equality at path before trying to rewrite. + match SpkiPublicKey::from_file(path) { + Ok(existing) if existing == public_key => {} + _ => public_key.write_pem_async(path).await?, + } core_public_keys().refresh(); diff --git a/bin/periphery/src/api/mod.rs b/bin/periphery/src/api/mod.rs index 1ff7e92f8..9efa1e261 100644 --- a/bin/periphery/src/api/mod.rs +++ b/bin/periphery/src/api/mod.rs @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize}; use crate::{ api::compose::list_compose_projects, - config::{periphery_config, periphery_public_key}, + config::{periphery_config, periphery_keys}, docker::docker_client, stats::stats_client, }; @@ -229,7 +229,7 @@ impl Resolve for PollStatus { Ok(PollStatusResponse { periphery_info: PeripheryInformation { version: env!("CARGO_PKG_VERSION").to_string(), - public_key: periphery_public_key().load().to_string(), + public_key: periphery_keys().load().public.to_string(), terminals_disabled: config.disable_terminals, container_exec_disabled: config.disable_container_exec, stats_polling_rate: config.stats_polling_rate, diff --git a/bin/periphery/src/config.rs b/bin/periphery/src/config.rs index 9cb7eb377..041e4eca9 100644 --- a/bin/periphery/src/config.rs +++ b/bin/periphery/src/config.rs @@ -3,7 +3,6 @@ use std::{ sync::{Arc, OnceLock}, }; -use anyhow::Context; use arc_swap::ArcSwap; use clap::Parser; use colored::Colorize; @@ -15,39 +14,29 @@ use komodo_client::entities::{ config::periphery::{CliArgs, Env, PeripheryConfig}, logger::{LogConfig, LogLevel}, }; -use noise::key::{SpkiPublicKey, load_maybe_generate_private_key}; +use noise::key::EncodedKeyPair; /// Should call in startup to ensure Periphery errors without valid private key. -pub fn periphery_private_key() -> &'static ArcSwap { - static PERIPHERY_PRIVATE_KEY: OnceLock> = +pub fn periphery_keys() -> &'static ArcSwap { + static PERIPHERY_KEYS: OnceLock> = OnceLock::new(); - PERIPHERY_PRIVATE_KEY.get_or_init(|| { + PERIPHERY_KEYS.get_or_init(|| { let config = periphery_config(); - let private_key = config.private_key.clone().unwrap_or(format!( - "file:{}/keys/periphery.key", - config.root_directory.display() - )); let private_key = + config.private_key.clone().unwrap_or_else(|| { + format!( + "file:{}/keys/periphery.key", + config.root_directory.display() + ) + }); + let keys = if let Some(path) = private_key.strip_prefix("file:") { - load_maybe_generate_private_key(path).unwrap() + EncodedKeyPair::load_maybe_generate(path) } else { - private_key - }; - ArcSwap::new(Arc::new(private_key)) - }) -} - -/// Should call in startup to ensure Periphery errors without valid private key. -pub fn periphery_public_key() -> &'static ArcSwap { - static PERIPHERY_PUBLIC_KEY: OnceLock> = - OnceLock::new(); - PERIPHERY_PUBLIC_KEY.get_or_init(|| { - let public_key = SpkiPublicKey::from_private_key( - periphery_private_key().load().as_str(), - ) - .context("Got invalid private key") - .unwrap(); - ArcSwap::new(Arc::new(public_key)) + EncodedKeyPair::from_private_key(&private_key) + } + .unwrap(); + ArcSwap::new(Arc::new(keys)) }) } diff --git a/bin/periphery/src/connection/client.rs b/bin/periphery/src/connection/client.rs index 3998efd54..e71cf8231 100644 --- a/bin/periphery/src/connection/client.rs +++ b/bin/periphery/src/connection/client.rs @@ -14,7 +14,7 @@ use transport::{ use crate::{ api::Args, - config::{periphery_config, periphery_public_key}, + config::{periphery_config, periphery_keys}, connection::{core_channels, core_public_keys}, }; @@ -176,7 +176,7 @@ async fn handle_onboarding( // Post onboarding login 1: Send public key socket - .send(periphery_public_key().load().as_bytes()) + .send(periphery_keys().load().public.as_bytes()) .await .context("Failed to send public key bytes")?; diff --git a/bin/periphery/src/connection/mod.rs b/bin/periphery/src/connection/mod.rs index be82dee7c..36968e87c 100644 --- a/bin/periphery/src/connection/mod.rs +++ b/bin/periphery/src/connection/mod.rs @@ -1,10 +1,9 @@ use std::{ - fs::read_to_string, sync::{Arc, OnceLock}, time::Duration, }; -use anyhow::{Context as _, anyhow}; +use anyhow::anyhow; use arc_swap::ArcSwap; use bytes::Bytes; use cache::CloneCache; @@ -24,7 +23,7 @@ use uuid::Uuid; use crate::{ api::{Args, PeripheryRequest}, - config::{periphery_config, periphery_private_key}, + config::{periphery_config, periphery_keys}, }; pub mod client; @@ -68,18 +67,13 @@ impl CorePublicKeys { let core_public_keys = core_public_keys .iter() .flat_map(|public_key| { - let res = || { - if let Some(path) = public_key.strip_prefix("file:") { - let contents = - read_to_string(path).with_context(|| { - format!("Failed to read public key at {path:?}") - })?; - SpkiPublicKey::from_maybe_pem(&contents) - } else { - SpkiPublicKey::from_maybe_pem(&public_key) - } + let res = if let Some(path) = public_key.strip_prefix("file:") + { + SpkiPublicKey::from_file(path) + } else { + SpkiPublicKey::from_maybe_pem(&public_key) }; - match (res(), config.server_enabled) { + match (res, config.server_enabled) { (Ok(public_key), _) => Some(public_key), (Err(e), false) => { // If only outbound connections, only warn. @@ -123,7 +117,7 @@ async fn handle_login( L::login(LoginFlowArgs { socket, identifiers, - private_key: periphery_private_key().load().as_str(), + private_key: periphery_keys().load().private.as_str(), public_key_validator: core_public_keys(), }) .await diff --git a/bin/periphery/src/main.rs b/bin/periphery/src/main.rs index 751f6132c..21e84ca93 100644 --- a/bin/periphery/src/main.rs +++ b/bin/periphery/src/main.rs @@ -1,7 +1,7 @@ use futures::{StreamExt, stream::FuturesUnordered}; use komodo_client::entities::config::periphery::Command; -use crate::config::{periphery_args, periphery_public_key}; +use crate::config::{periphery_args, periphery_keys}; #[macro_use] extern crate tracing; @@ -27,7 +27,7 @@ async fn app() -> anyhow::Result<()> { } // Init + log public key. Will crash if invalid private key here. - info!("Public Key: {}", periphery_public_key().load()); + info!("Public Key: {}", periphery_keys().load().public); rustls::crypto::aws_lc_rs::default_provider() .install_default() diff --git a/lib/noise/src/key/mod.rs b/lib/noise/src/key/mod.rs index 213310f9f..827107e69 100644 --- a/lib/noise/src/key/mod.rs +++ b/lib/noise/src/key/mod.rs @@ -29,51 +29,66 @@ pub struct EncodedKeyPair { } impl EncodedKeyPair { - pub fn generate() -> anyhow::Result { + pub fn generate() -> anyhow::Result { let builder = snow::Builder::new(crate::NOISE_XX_PARAMS.parse()?); let keypair = builder .generate_keypair() .context("Failed to generate keypair")?; let private = Pkcs8PrivateKey::from_raw_bytes(&keypair.private)?; let public = SpkiPublicKey::from_raw_bytes(&keypair.public)?; - Ok(EncodedKeyPair { private, public }) + Ok(Self { private, public }) + } + + pub fn generate_write_sync( + path: impl AsRef, + ) -> anyhow::Result { + let path = path.as_ref(); + // Generate and write pems to path + let keys = Self::generate()?; + keys.private.write_pem_sync(path)?; + keys.public.write_pem_sync(path.with_extension("pub"))?; + Ok(keys) + } + + pub async fn generate_write_async( + path: impl AsRef, + ) -> anyhow::Result { + let path = path.as_ref(); + // Generate and write pems to path + let keys = Self::generate()?; + keys.private.write_pem_async(path).await?; + keys + .public + .write_pem_async(path.with_extension("pub")) + .await?; + Ok(keys) + } + + pub fn load_maybe_generate( + private_key_path: impl AsRef, + ) -> anyhow::Result { + let path = private_key_path.as_ref(); + + let exists = path.try_exists().with_context(|| { + format!("Invalid private key path: {path:?}") + })?; + + if !exists { + return Self::generate_write_sync(path); + } + + let private = Pkcs8PrivateKey::from_file(private_key_path)?; + let public = private.compute_public_key()?; + + Ok(Self { private, public }) } pub fn from_private_key( maybe_pkcs8_private_key: &str, - ) -> anyhow::Result { + ) -> anyhow::Result { let private = Pkcs8PrivateKey::from_maybe_raw_bytes(maybe_pkcs8_private_key)?; let public = private.compute_public_key()?; Ok(Self { private, public }) } } - -pub fn load_maybe_generate_private_key( - path: impl AsRef, -) -> anyhow::Result { - let path = path.as_ref(); - if path - .try_exists() - .with_context(|| format!("Invalid private key path: {path:?}"))? - { - // Already exists, load it - std::fs::read_to_string(path).with_context(|| { - format!("Failed to read private key at {path:?}") - }) - } else { - let keys = generate_write_keys(path)?; - Ok(keys.private.into_inner()) - } -} - -pub fn generate_write_keys( - path: impl AsRef, -) -> anyhow::Result { - let path = path.as_ref(); - // Generate and write pems to path - let keys = EncodedKeyPair::generate()?; - keys.private.write_pem_sync(path)?; - keys.public.write_pem_sync(path.with_extension("pub"))?; - Ok(keys) -} diff --git a/lib/noise/src/key/private.rs b/lib/noise/src/key/private.rs index 11c3a13b5..2a41f786a 100644 --- a/lib/noise/src/key/private.rs +++ b/lib/noise/src/key/private.rs @@ -68,6 +68,15 @@ impl Pkcs8PrivateKey { }) } + pub fn from_file(path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); + let contents = + std::fs::read_to_string(path).with_context(|| { + format!("Failed to read private key at {path:?}") + })?; + Self::from_maybe_raw_bytes(&contents) + } + /// - For raw bytes: converts to pkcs8 and returns /// - For pkcs8 base64: clones and returns /// - For pkcs8 base64 pem: unwraps and returns diff --git a/lib/noise/src/key/public.rs b/lib/noise/src/key/public.rs index abf56d9a2..52d1dcd2c 100644 --- a/lib/noise/src/key/public.rs +++ b/lib/noise/src/key/public.rs @@ -50,7 +50,7 @@ impl SpkiPublicKey { let path = path.as_ref(); tracing::info!("Writing public key to {path:?}"); secret_file::write_sync(path, self.as_pem()).with_context(|| { - format!("Failed to write private key pem to {path:?}") + format!("Failed to write public key pem to {path:?}") }) } @@ -63,10 +63,19 @@ impl SpkiPublicKey { secret_file::write_async(path, self.as_pem()) .await .with_context(|| { - format!("Failed to write private key pem to {path:?}") + format!("Failed to write public key pem to {path:?}") }) } + pub fn from_file(path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); + let contents = + std::fs::read_to_string(path).with_context(|| { + format!("Failed to read public key at {path:?}") + })?; + Self::from_maybe_pem(&contents) + } + /// Accepts pem rfc7468 (openssl) or base64 der (second line of rfc7468 pem). pub fn from_maybe_pem( public_key_maybe_pem: &str,