Skip to content

Commit

Permalink
WIP: sign debug credential signing requests
Browse files Browse the repository at this point in the history
  • Loading branch information
mx-shift authored and flihp committed Mar 26, 2024
1 parent a07fb20 commit d1ae3d7
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 25 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ env_logger = "0.10.2"
fs_extra = "1.3.0"
hex = { version = "0.4.3", features = ["serde"] }
log = "0.4.21"
lpc55_sign = { git = "https://github.com/oxidecomputer/lpc55_support", default-features = false, version = "0.3.4" }
lpc55_sign = { git = "https://github.com/oxidecomputer/lpc55_support", default-features = false, version = "0.3.1" }
lpc55_areas = { git = "https://github.com/oxidecomputer/lpc55_support", default-features = false, version = "0.2.4" }
num-bigint = "0.4.4"
# p256 v0.13 has a dependency that requires rustc 1.65 but we're pinned
# to 1.64 till offline-keystore-os supports it
Expand All @@ -32,5 +33,5 @@ thiserror = "1.0.58"
# vsss-rs v3 has a dependency that requires rustc 1.65 but we're pinned
# to 1.64 till offline-keystore-os supports it
vsss-rs = "2.7.1"
yubihsm = { version = "0.41.0", features = ["usb"] }
yubihsm = { version = "0.41.0", features = ["usb", "untested"] }
zeroize = "1.7.0"
176 changes: 153 additions & 23 deletions src/ca.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,49 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use anyhow::Result;
use anyhow::{anyhow, Result};
use fs_extra::dir::CopyOptions;
use log::{debug, error, info, warn};
use std::{
env,
fs::{self, OpenOptions, Permissions},
io,
os::unix::fs::PermissionsExt,
path::Path,
path::{Path, PathBuf},
process::{Child, Command, Stdio},
str::FromStr,
thread,
time::Duration,
};
use tempfile::{NamedTempFile, TempDir};
use thiserror::Error;
use yubihsm::Client;
use zeroize::Zeroizing;

use crate::config::{
self, CsrSpec, KeySpec, Purpose, Transport, ENV_PASSWORD, KEYSPEC_EXT,
use crate::{
config::{
self, CsrSpec, DcsrSpec, KeySpec, Purpose, Transport, ENV_PASSWORD,
KEYSPEC_EXT,
},
hsm::Hsm,
};

/// Name of file in root of a CA directory with key spec used to generate key
/// in HSM.
const CA_KEY_SPEC: &str = "key.spec";

/// Name of file in root of a CA directory containing the CA's own certificate.
const CA_CERT: &str = "ca.cert.pem";

const CSRSPEC_EXT: &str = ".csrspec.json";
const DCSRSPEC_EXT: &str = ".dcsrspec.json";

#[derive(Error, Debug)]
pub enum CaError {
#[error("Invalid path to CsrSpec file")]
BadCsrSpecPath,
#[error("Invalid path to DcsrSpec file")]
BadDcsrSpecPath,
#[error("Invalid purpose for root CA key")]
BadPurpose,
#[error("path not a directory")]
Expand Down Expand Up @@ -171,6 +182,14 @@ fn passwd_to_env(env_str: &str) -> Result<()> {
Ok(())
}

fn passwd_from_env(env_str: &str) -> Result<String> {
Ok(std::env::var(env_str)?
.strip_prefix("0002")
.ok_or_else(|| anyhow!("Missing key identifier prefix in environment variable \"{env_str}\" that is expected to contain an HSM password"))?
.to_string()
)
}

/// Start the yubihsm-connector process.
/// NOTE: The connector dumps ~10 lines of text for each command.
/// We can increase verbosity with the `-debug` flag, but the only way
Expand Down Expand Up @@ -403,29 +422,33 @@ fn initialize_keyspec(
}

pub fn sign(
csr_spec: &Path,
spec: &Path,
state: &Path,
publish: &Path,
transport: Transport,
) -> Result<()> {
let csr_spec = fs::canonicalize(csr_spec)?;
debug!("canonical CsrSpec path: {}", &csr_spec.display());
let spec = fs::canonicalize(spec)?;
debug!("canonical spec path: {}", &spec.display());

let paths = if csr_spec.is_file() {
vec![csr_spec.clone()]
let paths = if spec.is_file() {
vec![spec.clone()]
} else {
config::files_with_ext(&csr_spec, CSRSPEC_EXT)?
config::files_with_ext(&spec, CSRSPEC_EXT)?
.into_iter()
.chain(config::files_with_ext(&spec, DCSRSPEC_EXT)?.into_iter())
.collect::<Vec<PathBuf>>()
};

if paths.is_empty() {
return Err(anyhow::anyhow!(
"no files with extension \"{}\" found in dir: {}",
"no files with extensions \"{}\" or \"{}\" found in dir: {}",
CSRSPEC_EXT,
&csr_spec.display()
DCSRSPEC_EXT,
&spec.display()
));
}

let connector = match transport {
let connector_task = match transport {
// The yubihsm pkcs#11 module relies on the yubihsm-connector. If
// we've been using the Usb connector up to this point we assume the
// daemon is not running and that we must start it.
Expand All @@ -437,21 +460,46 @@ pub fn sign(

let tmp_dir = TempDir::new()?;
for path in paths {
// process csr spec
info!("Signing CSR from CsrSpec: {:?}", path);
if let Err(e) = sign_csrspec(&path, &tmp_dir, state, publish) {
// Ignore possible error from killing connector because we already
// have an error to report and it'll be more interesting.
if let Some(mut c) = connector {
let _ = c.kill();
let filename = path.file_name().unwrap().to_string_lossy();

if filename.ends_with(CSRSPEC_EXT) {
// process csr spec
info!("Signing CSR from CsrSpec: {:?}", path);
if let Err(e) = sign_csrspec(&path, &tmp_dir, state, publish) {
// Ignore possible error from killing connector because we already
// have an error to report and it'll be more interesting.
if let Some(mut c) = connector_task {
let _ = c.kill();
}
return Err(e);
}
return Err(e);
} else if filename.ends_with(DCSRSPEC_EXT) {
let hsm = Hsm::new(
0x0002,
&passwd_from_env("OKM_HSM_PKCS11_AUTH")?,
publish,
state,
false,
transport,
)?;

info!("Signing DCSR from DcsrSpec: {:?}", path);
if let Err(e) = sign_dcsrspec(&path, &hsm.client, state, publish) {
// Ignore possible error from killing connector because we already
// have an error to report and it'll be more interesting.
if let Some(mut c) = connector_task {
let _ = c.kill();
}
return Err(e);
}
} else {
error!("Unknown input spec: {}", path.display());
}
}

// kill connector
if connector.is_some() {
connector.unwrap().kill()?;
if connector_task.is_some() {
connector_task.unwrap().kill()?;
}

Ok(())
Expand Down Expand Up @@ -572,6 +620,88 @@ pub fn sign_csrspec(
Ok(())
}

fn sign_dcsrspec(
dcsr_spec_path: &Path,
client: &Client,
state: &Path,
publish: &Path,
) -> Result<()> {
let dcsr_spec_json = std::fs::read_to_string(dcsr_spec_path)?;
let dcsr_spec: DcsrSpec = serde_json::from_str(&dcsr_spec_json)?;

let root_cert_paths = dcsr_spec
.root_labels
.iter()
.map(|x| state.join(x.to_string()).join(CA_CERT))
.collect::<Vec<PathBuf>>();
let root_certs = lpc55_sign::cert::read_certs(&root_cert_paths)?;

// Load signer's public key
let mut signer_public_key = None;
for (idx, label) in dcsr_spec.root_labels.iter().enumerate() {
if *label == dcsr_spec.label {
if let Some(x) = root_certs.get(idx) {
signer_public_key = Some(lpc55_sign::cert::public_key(x)?)
}
}
}
let signer_public_key = signer_public_key.ok_or_else(|| {
anyhow!(
"DcsrSpec label \"{}\" must also be one of the root labels",
dcsr_spec.label
)
})?;

// Load signing key's KeySpec
let key_spec = state.join(dcsr_spec.label.to_string()).join(CA_KEY_SPEC);

debug!("Getting KeySpec from: {}", key_spec.display());
let json = fs::read_to_string(key_spec)?;
debug!("spec as json: {}", json);

let key_spec = KeySpec::from_str(&json)?;
debug!("KeySpec: {:#?}", key_spec);

// Get prefix from DcsrSpec file. We us this to generate file names for the
// output file.
let dcsr_filename = match dcsr_spec_path
.file_name()
.ok_or(CaError::BadDcsrSpecPath)?
.to_os_string()
.into_string()
{
Ok(s) => s,
Err(_) => return Err(CaError::BadCsrSpecPath.into()),
};
let dcsr_prefix = match dcsr_filename.find('.') {
Some(i) => dcsr_filename[..i].to_string(),
None => dcsr_filename,
};

// Construct the to-be-signed debug credential
let dc_tbs = lpc55_sign::debug_auth::debug_credential_tbs(
root_certs,
signer_public_key,
dcsr_spec.dcsr,
)?;

// Sign it using the private key stored in the HSM.
let dc_sig = client.sign_rsa_pkcs1v15_sha256(key_spec.id, &dc_tbs)?;

// Append the signature to the TBS debug credential to make a complete debug
// credential
let mut dc = Vec::new();
dc.extend_from_slice(&dc_tbs);
dc.extend_from_slice(&dc_sig.into_vec());

// Write the debug credential to the output directory
let dc_path = publish.join(format!("{}.dc.bin", dcsr_prefix));
debug!("writing debug credential to: {}", dc_path.display());
std::fs::write(dc_path, &dc)?;

Ok(())
}

/// Create the directory structure and initial files expected by the `openssl ca` tool.
fn bootstrap_ca(key_spec: &KeySpec, pkcs11_path: &Path) -> Result<()> {
// create directories expected by `openssl ca`: crl, newcerts
Expand Down

0 comments on commit d1ae3d7

Please sign in to comment.