Skip to content

Commit

Permalink
Allow to pass auth credentials as options (#56)
Browse files Browse the repository at this point in the history
* Add --api-key flag

Add common test script

* Improve credentials logic
  • Loading branch information
zugzwang committed Mar 9, 2022
1 parent 7164ff1 commit 5be4549
Show file tree
Hide file tree
Showing 15 changed files with 433 additions and 247 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion openpgp-dsm/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "openpgp-dsm"
version = "0.1.1"
version = "0.2.1-beta"
authors = ["zugzwang <francisco.vialprado@fortanix.com>"]
edition = "2018"

Expand Down
202 changes: 127 additions & 75 deletions openpgp-dsm/src/lib.rs
Expand Up @@ -80,24 +80,98 @@ pub struct DsmAgent {
role: Role,
}

const DSM_LABEL_PGP: &str = "sq_dsm";
const ENV_API_KEY: &str = "FORTANIX_API_KEY";
const ENV_API_ENDPOINT: &str = "FORTANIX_API_ENDPOINT";
const ENV_APP_UUID: &str = "FORTANIX_APP_UUID";
const ENV_HTTP_PROXY: &str = "http_proxy";
const ENV_NO_PROXY: &str = "no_proxy";
const ENV_P12: &str = "FORTANIX_PKCS12_ID";
const MIN_DSM_VERSION: &str = "4.2.0";
/// The version of this crate.
pub const SQ_DSM_VERSION: &str = env!("CARGO_PKG_VERSION");
const DSM_LABEL_PGP: &str = "sq_dsm";
const ENV_API_KEY: &str = "FORTANIX_API_KEY";
const ENV_API_ENDPOINT: &str = "FORTANIX_API_ENDPOINT";
const ENV_APP_UUID: &str = "FORTANIX_APP_UUID";
const ENV_HTTP_PROXY: &str = "http_proxy";
const ENV_NO_PROXY: &str = "no_proxy";
const ENV_P12: &str = "FORTANIX_PKCS12_ID";
const MIN_DSM_VERSION: &str = "4.2.0";
// As seen on sdkms-client-rust/blob/master/examples/approval_request.rs
const OP_APPROVAL_MSG: &str = "This operation requires approval";
const OP_APPROVAL_MSG: &str = "This operation requires approval";

#[derive(Clone)]
enum Auth {
pub enum Auth {
ApiKey(String),
// App UUID and PKCS12 identity
Cert(Uuid, Identity),
}

impl Auth {
pub fn from_options_or_env(
cli_api_key: Option<&str>,
cli_client_cert: Option<&str>,
cli_app_uuid: Option<&str>,
) -> Result<Self> {
// Try API key
let api_key = match (cli_api_key, env::var(ENV_API_KEY).ok()) {
(Some(api_key), None) => Some(api_key.to_string()),
(None, Some(api_key)) => Some(api_key),
(Some(api_key), Some(_)) => {
println!(
"API key both in parameters and env; ignoring env"
);
Some(api_key.to_string())
},
(None, None) => None,
};

// Try client cert
let cert_based = {
let client_cert = match (cli_client_cert, env::var(ENV_P12).ok()) {
(Some(cert), None) => Some(cert.to_string()),
(None, Some(cert)) => Some(cert),
(Some(cert), Some(_)) => {
println!(
"P12 cert both in parameters and env; ignoring env"
);
Some(cert.to_string())
},
(None, None) => None,
};

let app_uuid = match (cli_app_uuid, env::var(ENV_APP_UUID).ok()) {
(Some(id), None) => Some(id.to_string()),
(None, Some(id)) => Some(id),
(Some(id), Some(_)) => {
println!(
"APP UUID both in parameters and env; ignoring env"
);
Some(id.to_string())
},
(None, None) => None,
};

match (client_cert, app_uuid) {
(Some(cert), Some(uuid)) => Some((cert, uuid)),
_ => None,
}
};

match (api_key, cert_based) {
(Some(api_key), None) => Ok(Auth::ApiKey(api_key)),
(Some(api_key), Some(_)) => {
println!(
"Multiple auth methods found. Using API key"
);

Ok(Auth::ApiKey(api_key))
},
(None, Some((client_cert, app_uuid))) => {
let p12_id = try_unlock_p12(client_cert)?;

let uuid = Uuid::parse_str(&app_uuid)
.context("bad app UUID")?;
Ok(Auth::Cert(uuid, p12_id))
}
(None, None) => return Err(Error::msg("no auth credentials found")),
}
}
}

#[derive(Clone)]
pub struct Credentials {
api_endpoint: String,
Expand Down Expand Up @@ -225,65 +299,9 @@ impl <S: Into<Cow<'static, str>> + Display> OperateOrAskApproval<S> for DsmClien
}

impl Credentials {
pub fn new_from_env() -> Result<Self> {
pub fn new(auth: Auth) -> Result<Self> {
let api_endpoint = env::var(ENV_API_ENDPOINT)
.with_context(|| format!("{} env var absent", ENV_API_ENDPOINT))?;

let auth = match (
env::var(ENV_API_KEY).ok(),
env::var(ENV_P12).ok(),
) {
(Some(api_key), other) => {
if other.is_some() {
println!("Both {}, {} are set, using API key auth",
ENV_API_KEY, ENV_P12);
}
Auth::ApiKey(api_key)
},
(None, Some(cert_file)) => {
let app_uuid = Uuid::parse_str(
&env::var(ENV_APP_UUID).context(
format!("Need {} for cert-based auth", ENV_APP_UUID))?
).context("bad app UUID")?;
let mut cert_stream = File::open(cert_file.clone())
.context(format!("opening {}", cert_file))?;
let mut cert = Vec::new();
cert_stream.read_to_end(&mut cert)
.context(format!("reading {}", cert_file))?;
// Try to unlock certificate without password
let mut first = true;
let id = if let Ok(id) = Identity::from_pkcs12(&cert, "") {
id
} else {
loop {
// Prompt the user for PKCS12 password
match rpassword::read_password_from_tty(
Some(
&format!(
"{}Enter password to unlock {}: ",
if first { "" } else { "Invalid password. " },
cert_file))
) {
Ok(p) => {
first = false;
if let Ok(id) = Identity::from_pkcs12(&cert, &p) {
break id;
}
},
Err(err) => {
return Err(Error::msg(format!(
"While reading password: {}", err)
));
}
}
}
};
Auth::Cert(app_uuid, id)
}
(None, None) => return Err(Error::msg(format!(
"at least one of {}, {} env var is needed",
ENV_API_KEY, ENV_P12))),
};
.with_context(|| format!("{} absent", ENV_API_ENDPOINT))?;

Ok(Self { api_endpoint, auth })
}
Expand Down Expand Up @@ -448,6 +466,7 @@ pub fn generate_key(
user_id: Option<&str>,
algo: Option<&str>,
exportable: bool,
credentials: Credentials,
) -> Result<()> {
// User ID
let uid: UserID = match user_id {
Expand All @@ -467,7 +486,6 @@ pub fn generate_key(
_ => unreachable!("argument has a default value"),
};

let credentials = Credentials::new_from_env()?;
let dsm_client = credentials.dsm_client()?;

info!("create primary key");
Expand Down Expand Up @@ -611,10 +629,9 @@ pub fn generate_key(
/// Extracts the certificate of the corresponding PGP key. Note that this
/// certificate, created at key-generation time, is stored in the custom
/// metadata of the Security Object representing the primary key.
pub fn extract_cert(key_name: &str) -> Result<Cert> {
pub fn extract_cert(key_name: &str, cred: Credentials) -> Result<Cert> {
info!("dsm extract_cert");
let credentials = Credentials::new_from_env()?;
let dsm_client = credentials.dsm_client()?;
let dsm_client = cred.dsm_client()?;

let metadata = {
let sobject = dsm_client
Expand All @@ -635,10 +652,9 @@ pub fn extract_cert(key_name: &str) -> Result<Cert> {
Ok(Cert::from_str(&key_md.certificate)?)
}

pub fn extract_tsk_from_dsm(key_name: &str) -> Result<Cert> {
pub fn extract_tsk_from_dsm(key_name: &str, cred: Credentials) -> Result<Cert> {
// Extract all secrets as packets
let credentials = Credentials::new_from_env()?;
let dsm_client = credentials.dsm_client()?;
let dsm_client = cred.dsm_client()?;

let mut packets = Vec::<Packet>::with_capacity(2);

Expand Down Expand Up @@ -1377,3 +1393,39 @@ fn api_curve_from_sequoia_curve(curve: SequoiaCurve) -> Result<ApiCurve> {
curve @ _ => Err(Error::msg(format!("unsupported curve {}", curve))),
}
}

fn try_unlock_p12(cert_file: String) -> Result<Identity> {
let mut cert_stream = File::open(cert_file.clone())
.context(format!("opening {}", cert_file))?;
let mut cert = Vec::new();
cert_stream.read_to_end(&mut cert)
.context(format!("reading {}", cert_file))?;
// Try to unlock certificate without password first
let mut first = true;
if let Ok(id) = Identity::from_pkcs12(&cert, "") {
Ok(id)
} else {
loop {
// Prompt the user for PKCS12 password
match rpassword::read_password_from_tty(
Some(
&format!(
"{}Enter password to unlock {}: ",
if first { "" } else { "Invalid password. " },
cert_file))
) {
Ok(p) => {
first = false;
if let Ok(id) = Identity::from_pkcs12(&cert, &p) {
break Ok(id)
}
},
Err(err) => {
return Err(Error::msg(format!(
"While reading password: {}", err)
));
}
}
}
}
}
1 change: 1 addition & 0 deletions sq/Cargo.toml
Expand Up @@ -45,6 +45,7 @@ env_logger = "0.9.0"
clap = "2.33"
sequoia-openpgp = { path = "../openpgp", version = "1.0.0", default-features = false }
subplot-build = "0.1.0"
openpgp-dsm = { path = "../openpgp-dsm", default-features = false }

[dev-dependencies]
assert_cli = "0.6"
Expand Down
43 changes: 22 additions & 21 deletions sq/Makefile
Expand Up @@ -37,24 +37,25 @@ install: build-release

test-dsm:
cargo run -- --version
./tests/dsm/key_expiration.sh --rsa2k
./tests/dsm/key_expiration.sh --rsa3k
./tests/dsm/key_expiration.sh --rsa4k
./tests/dsm/key_expiration.sh --p256
./tests/dsm/key_expiration.sh --p384
./tests/dsm/key_expiration.sh --p521
./tests/dsm/key_expiration.sh --cv25519
./tests/dsm/extract_dsm_import_gpg_auto.tcl --rsa2k
./tests/dsm/extract_dsm_import_gpg_auto.tcl --rsa3k
./tests/dsm/extract_dsm_import_gpg_auto.tcl --rsa4k
./tests/dsm/extract_dsm_import_gpg_auto.tcl --p256
./tests/dsm/extract_dsm_import_gpg_auto.tcl --p384
./tests/dsm/extract_dsm_import_gpg_auto.tcl --p521
./tests/dsm/extract_dsm_import_gpg_auto.tcl --cv25519
./tests/dsm/sq_roundtrips.sh --rsa2k
./tests/dsm/sq_roundtrips.sh --rsa3k
./tests/dsm/sq_roundtrips.sh --rsa4k
./tests/dsm/sq_roundtrips.sh --p256
./tests/dsm/sq_roundtrips.sh --p384
./tests/dsm/sq_roundtrips.sh --p521
./tests/dsm/sq_roundtrips.sh --cv25519
./tests/dsm/key_expiration.sh -c rsa2k
./tests/dsm/key_expiration.sh -c rsa3k
./tests/dsm/key_expiration.sh -c rsa4k
./tests/dsm/key_expiration.sh -c nistp256
./tests/dsm/key_expiration.sh -c nistp384
./tests/dsm/key_expiration.sh -c nistp521
./tests/dsm/key_expiration.sh -c cv25519
./tests/dsm/extract_dsm_import_gpg_auto.tcl -c rsa2k
./tests/dsm/extract_dsm_import_gpg_auto.tcl -c rsa3k
./tests/dsm/extract_dsm_import_gpg_auto.tcl -c rsa4k
./tests/dsm/extract_dsm_import_gpg_auto.tcl -c nistp256
./tests/dsm/extract_dsm_import_gpg_auto.tcl -c nistp384
./tests/dsm/extract_dsm_import_gpg_auto.tcl -c nistp521
./tests/dsm/extract_dsm_import_gpg_auto.tcl -c cv25519
./tests/dsm/sq_roundtrips.sh -x -c cv25519 # Passing api-key
./tests/dsm/sq_roundtrips.sh -c rsa2k
./tests/dsm/sq_roundtrips.sh -c rsa3k
./tests/dsm/sq_roundtrips.sh -c rsa4k
./tests/dsm/sq_roundtrips.sh -c nistp256
./tests/dsm/sq_roundtrips.sh -c nistp384
./tests/dsm/sq_roundtrips.sh -c nistp521
./tests/dsm/sq_roundtrips.sh -c cv25519
24 changes: 22 additions & 2 deletions sq/src/commands/key.rs
Expand Up @@ -75,12 +75,19 @@ fn generate(config: Config, m: &ArgMatches) -> Result<()> {
builder = builder.set_validity_period(d);

if let Some(dsm_key_name) = m.value_of("dsm-key") {
// Fortanix DSM
let dsm_secret = dsm::Auth::from_options_or_env(
m.value_of("api-key"),
m.value_of("client-cert"),
m.value_of("app-uuid"),
)?;
return dsm::generate_key(
dsm_key_name,
d,
m.value_of("userid"),
m.value_of("cipher-suite"),
m.is_present("dsm-exportable"),
dsm::Credentials::new(dsm_secret)?,
);
}

Expand Down Expand Up @@ -292,7 +299,14 @@ fn extract_cert(config: Config, m: &ArgMatches) -> Result<()> {

let cert = match m.value_of("dsm-key") {
Some(key_name) => {
dsm::extract_cert(key_name)?
// Fortanix DSM
let dsm_secret = dsm::Auth::from_options_or_env(
m.value_of("api-key"),
m.value_of("client-cert"),
m.value_of("app-uuid"),
)?;
let dsm_auth = dsm::Credentials::new(dsm_secret)?;
dsm::extract_cert(key_name, dsm_auth)?
}
None => {
let input = open_or_stdin(m.value_of("input"))?;
Expand All @@ -309,8 +323,14 @@ fn extract_cert(config: Config, m: &ArgMatches) -> Result<()> {
}

fn extract_dsm(config: Config, m: &ArgMatches) -> Result<()> {
let dsm_secret = dsm::Auth::from_options_or_env(
m.value_of("api-key"),
m.value_of("client-cert"),
m.value_of("app-uuid"),
)?;
let dsm_auth = dsm::Credentials::new(dsm_secret)?;
let key = match m.value_of("dsm-key") {
Some(key_name) => dsm::extract_tsk_from_dsm(key_name)?,
Some(key_name) => dsm::extract_tsk_from_dsm(key_name, dsm_auth)?,
None => unreachable!("name is compulsory")
};

Expand Down
1 change: 1 addition & 0 deletions sq/src/secrets.rs
Expand Up @@ -9,6 +9,7 @@ use sequoia_openpgp::packet::Key;
use sequoia_openpgp::types::HashAlgorithm;

pub use openpgp_dsm::Credentials;
pub use openpgp_dsm::Auth;
use openpgp_dsm::DsmAgent;

/// A Secret can be a private key loaded from memory, or stored externally. It
Expand Down

0 comments on commit 5be4549

Please sign in to comment.