use std::error::Error;
use std::fmt::{self, Display, Formatter};
use lodestone_wire::{Digest, Identity, U64};
use crate::credential::{
CredentialActivationError, CredentialMaterial, CredentialProvider, CredentialProviderError, FileCredentialProvider,
SecretBytes, credential_verifier, decode_file_credential, encode_file_credential,
};
use crate::secure_publication::{PublishedFileIdentity, SecureCreateNewFilePublisher, SecurePublicationError};
pub const BOOTSTRAP_API_KEY_GENERATION: u64 = 1;
pub const BOOTSTRAP_API_KEY_ID: &str = "00000000000000000000000000000001";
pub const MAX_BOOTSTRAP_CREDENTIAL_BYTES: usize = 4 * 1024;
pub struct PreparedBootstrapCredential {
material: CredentialMaterial,
}
impl PreparedBootstrapCredential {
pub fn generate() -> Result<Self, BootstrapCredentialError> {
let material = CredentialMaterial::new(
Identity {
value: BOOTSTRAP_API_KEY_ID.to_owned(),
},
U64 {
value: BOOTSTRAP_API_KEY_GENERATION.to_string(),
},
SecretBytes::generate().map_err(BootstrapCredentialError::Credential)?,
)
.map_err(BootstrapCredentialError::Credential)?;
Ok(Self { material })
}
pub async fn load_reference(secret_reference: &str) -> Result<Self, BootstrapCredentialError> {
let material = FileCredentialProvider
.provide(secret_reference)
.await
.map_err(BootstrapCredentialError::Credential)?;
let prepared = Self { material };
prepared.validate()?;
Ok(prepared)
}
pub async fn load_staged(
publisher: &SecureCreateNewFilePublisher,
) -> Result<Option<Self>, BootstrapCredentialError> {
let bytes = publisher
.read_stage(MAX_BOOTSTRAP_CREDENTIAL_BYTES)
.await
.map_err(BootstrapCredentialError::Publication)?;
Self::decode_optional(bytes)
}
pub async fn load_published(
publisher: &SecureCreateNewFilePublisher,
) -> Result<Option<Self>, BootstrapCredentialError> {
let bytes = publisher
.read_published(MAX_BOOTSTRAP_CREDENTIAL_BYTES)
.await
.map_err(BootstrapCredentialError::Publication)?;
Self::decode_optional(bytes)
}
fn decode_optional(bytes: Option<Vec<u8>>) -> Result<Option<Self>, BootstrapCredentialError> {
let Some(mut bytes) = bytes else {
return Ok(None);
};
let decoded = decode_file_credential(&bytes);
bytes.fill(0);
std::hint::black_box(&mut bytes);
let material = decoded.map_err(BootstrapCredentialError::Credential)?;
let prepared = Self { material };
prepared.validate()?;
Ok(Some(prepared))
}
#[must_use]
pub const fn api_key_id(&self) -> &Identity {
self.material.api_key_id()
}
#[must_use]
pub const fn generation(&self) -> &U64 {
self.material.generation()
}
pub fn verifier(&self) -> Result<Digest, BootstrapCredentialError> {
self.validate()?;
credential_verifier(self.material.api_key_id(), self.material.secret())
.map_err(BootstrapCredentialError::Activation)
}
pub async fn stage(
&self,
publisher: &SecureCreateNewFilePublisher,
expected: Option<PublishedFileIdentity>,
) -> Result<PublishedFileIdentity, BootstrapCredentialError> {
self.validate()?;
let bytes = encode_file_credential(
self.material.api_key_id(),
self.material.generation(),
self.material.secret(),
)
.map_err(BootstrapCredentialError::Credential)?;
publisher
.stage_sensitive(bytes, MAX_BOOTSTRAP_CREDENTIAL_BYTES, expected)
.await
.map_err(BootstrapCredentialError::Publication)
}
fn validate(&self) -> Result<(), BootstrapCredentialError> {
if crate::config::parse_identity(&self.material.api_key_id().value).is_none()
|| self.material.generation().value != BOOTSTRAP_API_KEY_GENERATION.to_string()
{
return Err(BootstrapCredentialError::InvalidIdentity);
}
Ok(())
}
}
#[derive(Debug)]
pub enum BootstrapCredentialError {
InvalidIdentity,
Credential(CredentialProviderError),
Activation(CredentialActivationError),
Publication(SecurePublicationError),
}
impl BootstrapCredentialError {
#[must_use]
pub const fn code(&self) -> &'static str {
match self {
Self::InvalidIdentity => "invalid_identity",
Self::Credential(_) => "credential",
Self::Activation(_) => "activation",
Self::Publication(error) => error.code(),
}
}
}
impl Display for BootstrapCredentialError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::InvalidIdentity => "bootstrap credential identity or generation is invalid",
Self::Credential(_) => "bootstrap credential material is invalid",
Self::Activation(_) => "bootstrap credential verifier derivation failed",
Self::Publication(_) => "bootstrap credential secure publication failed",
})
}
}
impl Error for BootstrapCredentialError {}
#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
mod tests {
use std::fs;
use std::os::unix::fs::PermissionsExt as _;
use std::path::{Path, PathBuf};
use super::*;
struct TemporaryDirectory(PathBuf);
impl TemporaryDirectory {
fn create() -> Result<Self, Box<dyn Error>> {
let mut nonce = [0_u8; 16];
getrandom::fill(&mut nonce)?;
let root = fs::canonicalize(std::env::temp_dir())?
.join(format!("lodestone-bootstrap-credential-test-{}", lower_hex(&nonce)));
fs::create_dir(&root)?;
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))?;
Ok(Self(root))
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TemporaryDirectory {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
#[tokio::test]
async fn generated_material_stages_and_reloads_with_the_same_verifier() -> Result<(), Box<dyn Error>> {
let temporary = TemporaryDirectory::create()?;
let stage = temporary.path().join("stage");
fs::create_dir(&stage)?;
fs::set_permissions(&stage, fs::Permissions::from_mode(0o700))?;
let destination = temporary.path().join("bootstrap.json");
let publisher = SecureCreateNewFilePublisher::open(stage, "bootstrap.stage".to_owned(), destination).await?;
let generated = PreparedBootstrapCredential::generate()?;
assert_eq!(generated.api_key_id().value, BOOTSTRAP_API_KEY_ID);
let verifier = generated.verifier()?;
let identity = generated.stage(&publisher, None).await?;
let reloaded = PreparedBootstrapCredential::load_staged(&publisher)
.await?
.ok_or("staged credential missing")?;
assert_eq!(reloaded.verifier()?, verifier);
assert_eq!(reloaded.api_key_id(), generated.api_key_id());
publisher.publish(identity).await?;
Ok(())
}
fn lower_hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut output = String::with_capacity(bytes.len().saturating_mul(2));
for byte in bytes {
output.push(char::from(HEX[usize::from(byte >> 4)]));
output.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
output
}
}