Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,20 @@ jobs:
- name: CLI App Add + Verify (Smoke)
run: |
mkdir -p tmp certs
cat > tmp/agent.toml <<'EOF'
ROOT_CA_FINGERPRINT="$(openssl x509 -in "$BOOTROOT_SECRETS_DIR/certs/root_ca.crt" -noout -fingerprint -sha256 | cut -d= -f2 | tr -d ':' | tr 'A-F' 'a-f')"
INTERMEDIATE_CA_FINGERPRINT=""
if [ -f "$BOOTROOT_SECRETS_DIR/certs/intermediate_ca.crt" ]; then
INTERMEDIATE_CA_FINGERPRINT="$(openssl x509 -in "$BOOTROOT_SECRETS_DIR/certs/intermediate_ca.crt" -noout -fingerprint -sha256 | cut -d= -f2 | tr -d ':' | tr 'A-F' 'a-f')"
fi
if [ -z "${ROOT_CA_FINGERPRINT:-}" ]; then
echo "Failed to read root CA fingerprint"
exit 1
fi
TRUSTED_CA_FINGERPRINTS="\"${ROOT_CA_FINGERPRINT}\""
if [ -n "${INTERMEDIATE_CA_FINGERPRINT:-}" ]; then
TRUSTED_CA_FINGERPRINTS="${TRUSTED_CA_FINGERPRINTS}, \"${INTERMEDIATE_CA_FINGERPRINT}\""
fi
cat > tmp/agent.toml <<EOF
email = "admin@example.com"
server = "https://localhost:9000/acme/acme/directory"
domain = "trusted.domain"
Expand All @@ -188,6 +201,10 @@ jobs:
http_responder_timeout_secs = 5
http_responder_token_ttl_secs = 300

[trust]
ca_bundle_path = "${BOOTROOT_SECRETS_DIR}/certs/root_ca.crt"
trusted_ca_sha256 = [${TRUSTED_CA_FINGERPRINTS}]

[[profiles]]
service_name = "edge-proxy"
instance_id = "001"
Expand Down
104 changes: 82 additions & 22 deletions src/acme/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use tracing::{debug, info, warn};

use crate::acme::types::{Authorization, Order};
use crate::config::AcmeSettings;
use crate::config::{AcmeSettings, TrustSettings};
use crate::eab::EabCredentials;

const ALG_ES256: &str = "ES256";
Expand Down Expand Up @@ -45,7 +45,11 @@ impl AcmeClient {
///
/// # Errors
/// Returns error if account key generation fails or HTTP client build fails.
pub fn new(directory_url: String, settings: &AcmeSettings) -> Result<Self> {
pub fn new(
directory_url: String,
settings: &AcmeSettings,
trust: &TrustSettings,
) -> Result<Self> {
let rng = ring::rand::SystemRandom::new();
let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
.map_err(|_| anyhow::anyhow!("Failed to generate account key"))?;
Expand All @@ -54,9 +58,7 @@ impl AcmeClient {
.map_err(|_| anyhow::anyhow!("Failed to parse generated key pair"))?;

Ok(Self {
client: Client::builder()
.danger_accept_invalid_certs(true)
.build()?,
client: Self::build_http_client(trust)?,
directory_url,
directory: None,
key_pair,
Expand All @@ -72,6 +74,17 @@ impl AcmeClient {
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
}

fn build_http_client(trust: &TrustSettings) -> Result<Client> {
let mut builder = Client::builder();
if let Some(bundle_path) = &trust.ca_bundle_path {
let pem = std::fs::read(bundle_path)
.with_context(|| format!("Failed to read CA bundle {}", bundle_path.display()))?;
let cert = reqwest::Certificate::from_pem(&pem).context("Invalid CA bundle")?;
builder = builder.add_root_certificate(cert);
}
builder.build().context("Failed to build HTTP client")
}

/// Fetches the ACME directory and caches it.
///
/// # Errors
Expand Down Expand Up @@ -529,6 +542,7 @@ mod tests {
use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate};

use super::*;
use crate::config::TrustSettings;

fn test_settings() -> AcmeSettings {
AcmeSettings {
Expand All @@ -544,15 +558,28 @@ mod tests {
}
}

fn test_trust() -> TrustSettings {
TrustSettings::default()
}

#[test]
fn test_client_initialization() {
let client = AcmeClient::new("http://example.com".to_string(), &test_settings());
let client = AcmeClient::new(
"http://example.com".to_string(),
&test_settings(),
&test_trust(),
);
assert!(client.is_ok());
}

#[test]
fn test_compute_key_authorization() {
let client = AcmeClient::new("http://example.com".to_string(), &test_settings()).unwrap();
let client = AcmeClient::new(
"http://example.com".to_string(),
&test_settings(),
&test_trust(),
)
.unwrap();
let token = "test_token_123_xyz";
let ka = client.compute_key_authorization(token).unwrap();
assert!(ka.starts_with(token));
Expand All @@ -571,7 +598,12 @@ mod tests {

#[test]
fn test_external_account_binding_structure() {
let client = AcmeClient::new("http://example.com".to_string(), &test_settings()).unwrap();
let client = AcmeClient::new(
"http://example.com".to_string(),
&test_settings(),
&test_trust(),
)
.unwrap();
let key = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"test-secret");
let creds = EabCredentials {
kid: "kid-123".to_string(),
Expand Down Expand Up @@ -641,8 +673,12 @@ mod tests {
.mount(&server)
.await;

let mut client =
AcmeClient::new(format!("{}/directory", server.uri()), &test_settings()).unwrap();
let mut client = AcmeClient::new(
format!("{}/directory", server.uri()),
&test_settings(),
&test_trust(),
)
.unwrap();
client
.create_order(&["example.internal".to_string(), "192.0.2.10".to_string()])
.await
Expand Down Expand Up @@ -720,8 +756,12 @@ mod tests {
.mount(&server)
.await;

let mut client =
AcmeClient::new(format!("{}/directory", server.uri()), &test_settings()).unwrap();
let mut client = AcmeClient::new(
format!("{}/directory", server.uri()),
&test_settings(),
&test_trust(),
)
.unwrap();
client.fetch_directory().await.unwrap();

assert_eq!(calls.load(Ordering::SeqCst), 3);
Expand All @@ -748,8 +788,12 @@ mod tests {
.mount(&server)
.await;

let mut client =
AcmeClient::new(format!("{}/directory", server.uri()), &test_settings()).unwrap();
let mut client = AcmeClient::new(
format!("{}/directory", server.uri()),
&test_settings(),
&test_trust(),
)
.unwrap();
let nonce = client.get_nonce().await.unwrap();

assert_eq!(nonce, "nonce-123");
Expand Down Expand Up @@ -792,8 +836,12 @@ mod tests {
.mount(&server)
.await;

let mut client =
AcmeClient::new(format!("{}/directory", server.uri()), &test_settings()).unwrap();
let mut client = AcmeClient::new(
format!("{}/directory", server.uri()),
&test_settings(),
&test_trust(),
)
.unwrap();
let order = client
.poll_order(&format!("{}/order/1", server.uri()))
.await
Expand All @@ -816,8 +864,12 @@ mod tests {
.mount(&server)
.await;

let mut client =
AcmeClient::new(format!("{}/directory", server.uri()), &test_settings()).unwrap();
let mut client = AcmeClient::new(
format!("{}/directory", server.uri()),
&test_settings(),
&test_trust(),
)
.unwrap();
let err = client.fetch_directory().await.unwrap_err();

assert_eq!(calls.load(Ordering::SeqCst), 3);
Expand Down Expand Up @@ -845,8 +897,12 @@ mod tests {
.mount(&server)
.await;

let mut client =
AcmeClient::new(format!("{}/directory", server.uri()), &test_settings()).unwrap();
let mut client = AcmeClient::new(
format!("{}/directory", server.uri()),
&test_settings(),
&test_trust(),
)
.unwrap();
let err = client.get_nonce().await.unwrap_err();

assert!(err.to_string().contains("Missing Replay-Nonce header"));
Expand Down Expand Up @@ -879,8 +935,12 @@ mod tests {
.mount(&server)
.await;

let mut client =
AcmeClient::new(format!("{}/directory", server.uri()), &test_settings()).unwrap();
let mut client = AcmeClient::new(
format!("{}/directory", server.uri()),
&test_settings(),
&test_trust(),
)
.unwrap();
let err = client
.poll_order(&format!("{}/order/2", server.uri()))
.await
Expand Down
2 changes: 1 addition & 1 deletion src/acme/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ pub async fn issue_certificate(
profile: &crate::config::DaemonProfileSettings,
eab_creds: Option<crate::eab::EabCredentials>,
) -> Result<()> {
let mut client = AcmeClient::new(settings.server.clone(), &settings.acme)?;
let mut client = AcmeClient::new(settings.server.clone(), &settings.acme, &settings.trust)?;

client.fetch_directory().await?;
tracing::debug!("Directory loaded.");
Expand Down
66 changes: 66 additions & 0 deletions src/commands/init/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
pub(crate) const DEFAULT_OPENBAO_URL: &str = "http://localhost:8200";
pub(crate) const DEFAULT_KV_MOUNT: &str = "secret";
pub(crate) const DEFAULT_SECRETS_DIR: &str = "secrets";
pub(crate) const DEFAULT_COMPOSE_FILE: &str = "docker-compose.yml";
pub(crate) const DEFAULT_STEPCA_URL: &str = "https://localhost:9000";
pub(crate) const DEFAULT_STEPCA_PROVISIONER: &str = "acme";

pub(crate) const DEFAULT_CA_NAME: &str = "Bootroot CA";
pub(crate) const DEFAULT_CA_PROVISIONER: &str = "admin";
pub(crate) const DEFAULT_CA_DNS: &str = "localhost,bootroot-ca";
pub(crate) const DEFAULT_CA_ADDRESS: &str = ":9000";
pub(crate) const SECRET_BYTES: usize = 32;
pub(crate) const DEFAULT_RESPONDER_TOKEN_TTL_SECS: u64 = 60;
pub(crate) const DEFAULT_RESPONDER_ADMIN_URL: &str = "http://bootroot-http01:8080";
pub(crate) const RESPONDER_TEMPLATE_DIR: &str = "templates";
pub(crate) const RESPONDER_TEMPLATE_NAME: &str = "responder.toml.ctmpl";
pub(crate) const RESPONDER_CONFIG_DIR: &str = "responder";
pub(crate) const RESPONDER_CONFIG_NAME: &str = "responder.toml";
pub(crate) const RESPONDER_COMPOSE_OVERRIDE_NAME: &str = "docker-compose.responder.override.yml";
pub(crate) const STEPCA_PASSWORD_TEMPLATE_NAME: &str = "password.txt.ctmpl";
pub(crate) const STEPCA_CA_JSON_TEMPLATE_NAME: &str = "ca.json.ctmpl";
pub(crate) const OPENBAO_AGENT_DIR: &str = "openbao";
pub(crate) const OPENBAO_AGENT_STEPCA_DIR: &str = "stepca";
pub(crate) const OPENBAO_AGENT_RESPONDER_DIR: &str = "responder";
pub(crate) const OPENBAO_AGENT_CONFIG_NAME: &str = "agent.hcl";
pub(crate) const OPENBAO_AGENT_ROLE_ID_NAME: &str = "role_id";
pub(crate) const OPENBAO_AGENT_SECRET_ID_NAME: &str = "secret_id";
pub(crate) const OPENBAO_AGENT_COMPOSE_OVERRIDE_NAME: &str =
"docker-compose.openbao-agent.override.yml";
pub(crate) const OPENBAO_AGENT_STEPCA_SERVICE: &str = "openbao-agent-stepca";
pub(crate) const OPENBAO_AGENT_RESPONDER_SERVICE: &str = "openbao-agent-responder";
pub(crate) const DEFAULT_EAB_ENDPOINT_PATH: &str = "eab";
pub(crate) const DEFAULT_DB_USER: &str = "stepca";
pub(crate) const DEFAULT_DB_NAME: &str = "stepca";
pub(crate) const CA_CERTS_DIR: &str = "certs";
pub(crate) const CA_ROOT_CERT_FILENAME: &str = "root_ca.crt";
pub(crate) const CA_INTERMEDIATE_CERT_FILENAME: &str = "intermediate_ca.crt";
pub(crate) const CA_TRUST_KEY: &str = "trusted_ca_sha256";

mod openbao_constants {
pub(crate) const INIT_SECRET_SHARES: u8 = 3;
pub(crate) const INIT_SECRET_THRESHOLD: u8 = 2;
pub(crate) const TOKEN_TTL: &str = "1h";
pub(crate) const SECRET_ID_TTL: &str = "24h";

pub(crate) const POLICY_BOOTROOT_AGENT: &str = "bootroot-agent";
pub(crate) const POLICY_BOOTROOT_RESPONDER: &str = "bootroot-responder";
pub(crate) const POLICY_BOOTROOT_STEPCA: &str = "bootroot-stepca";

pub(crate) const APPROLE_BOOTROOT_AGENT: &str = "bootroot-agent-role";
pub(crate) const APPROLE_BOOTROOT_RESPONDER: &str = "bootroot-responder-role";
pub(crate) const APPROLE_BOOTROOT_STEPCA: &str = "bootroot-stepca-role";

pub(crate) const PATH_STEPCA_PASSWORD: &str = "bootroot/stepca/password";
pub(crate) const PATH_STEPCA_DB: &str = "bootroot/stepca/db";
pub(crate) const PATH_RESPONDER_HMAC: &str = "bootroot/responder/hmac";
pub(crate) const PATH_AGENT_EAB: &str = "bootroot/agent/eab";
pub(crate) const PATH_CA_TRUST: &str = "bootroot/ca";
}

pub(crate) use openbao_constants::{
APPROLE_BOOTROOT_AGENT, APPROLE_BOOTROOT_RESPONDER, APPROLE_BOOTROOT_STEPCA,
INIT_SECRET_SHARES, INIT_SECRET_THRESHOLD, PATH_AGENT_EAB, PATH_CA_TRUST, PATH_RESPONDER_HMAC,
PATH_STEPCA_DB, PATH_STEPCA_PASSWORD, POLICY_BOOTROOT_AGENT, POLICY_BOOTROOT_RESPONDER,
POLICY_BOOTROOT_STEPCA, SECRET_ID_TTL, TOKEN_TTL,
};
Loading
Loading