use std::{
io::{Read, Write},
process::Stdio,
};
use anyhow::Context;
use openssl::pkey::PKey;
use openssl::{
cms::{CMSOptions, CmsContentInfo},
hash::MessageDigest,
stack::Stack,
symm::Cipher,
x509::X509,
};
use sequoia_openpgp::{
crypto::Password,
parse::{
Parse,
stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper},
},
policy::StandardPolicy,
serialize::stream::{Armorer, Encryptor, LiteralWriter, Message},
types::{AEADAlgorithm, SymmetricAlgorithm},
};
use serde::{Deserialize, Serialize};
use crate::server::{config::Pkcs11Binding, db};
#[derive(Debug, Clone, Serialize, Deserialize)]
enum BoundSecret {
None { secret: String },
Pkcs11WithCMS {
fingerprint: String,
secret: String,
},
}
impl BoundSecret {
pub(crate) fn bind(bindings: &[Pkcs11Binding], secret: &[u8]) -> anyhow::Result<Vec<Self>> {
let secret_str = str::from_utf8(secret)
.map_err(|_| anyhow::anyhow!("Secrets that are bound must be encoded with UTF-8"))?;
let mut bound_secrets = bindings
.iter()
.map(|binding| {
tracing::info!(certificate=?binding.certificate, "binding secret");
let certificate = std::fs::read_to_string(&binding.certificate)?;
let certificate = X509::from_pem(certificate.as_bytes())?;
let mut cert_stack = Stack::new()?;
cert_stack.push(certificate)?;
let encrypted = CmsContentInfo::encrypt(
&cert_stack,
secret,
Cipher::aes_256_gcm(),
CMSOptions::empty(),
)?;
let pem = encrypted.to_pem()?;
let certificate = cert_stack.pop().expect("we just pushed a cert");
let fingerprint = hex::encode_upper(certificate.digest(MessageDigest::sha256())?);
Ok::<_, anyhow::Error>(BoundSecret::Pkcs11WithCMS {
fingerprint,
secret: String::from_utf8(pem)?,
})
})
.collect::<Result<Vec<_>, _>>()?;
if bound_secrets.is_empty() {
tracing::trace!("binding isn't enabled for this secret");
bound_secrets.push(BoundSecret::None {
secret: secret_str.to_string(),
});
}
tracing::debug!(
tokens_bound = bindings.len(),
"secret has been successfully bound"
);
Ok(bound_secrets)
}
pub(crate) fn unbind(&self, bindings: &[Pkcs11Binding]) -> anyhow::Result<Password> {
match self {
BoundSecret::None { secret } => return Ok(Password::from(secret.as_bytes())),
BoundSecret::Pkcs11WithCMS {
fingerprint,
secret,
} => {
for binding in bindings.iter().filter(|binding| binding.can_unbind()) {
if let Ok(secret) =
binding_decrypt(binding.clone(), secret.clone().into_bytes())
.map(Password::from)
{
return Ok(secret);
} else {
tracing::debug!(
certificate = fingerprint,
key_uri = binding.private_key,
"Failed to unbind key password"
);
}
}
}
}
Err(anyhow::anyhow!("Unable to unbind key password"))
}
}
fn binding_decrypt(binding: Pkcs11Binding, data: Vec<u8>) -> anyhow::Result<Vec<u8>> {
let private_key = binding.private_key.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"Binding configuration is missing the 'private_key' field and can't be used to decrypt"
)
})?;
let mut command = std::process::Command::new("openssl");
let mut child = command
.args([
"cms",
"-decrypt",
"-inform",
"pem",
"-provider",
"pkcs11",
"-passin",
"stdin",
"-inkey",
])
.arg(private_key)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| anyhow::anyhow!("openssl-cms command missing stdin"))?;
binding
.pin
.ok_or_else(|| anyhow::anyhow!("Binding must include a PIN"))?
.map(|pin| stdin.write_all(pin))?;
stdin.write_all(b"\n")?;
stdin.write_all(&data)?;
drop(stdin);
let output = child.wait_with_output()?;
let stderr = String::from_utf8_lossy(&output.stderr);
if !output.status.success() {
return Err(anyhow::anyhow!(
"Failed to decrypt data via PKCS#11 using openssl-cms (exited {:?}): {stderr}",
output.status.code()
));
}
Ok(output.stdout)
}
pub async fn decrypt_key_password(
bindings: &[Pkcs11Binding],
user_password: Password,
data: &[u8],
) -> anyhow::Result<Password> {
let key_bindings: Vec<BoundSecret> = symmetric_decrypt(user_password, data)
.map(|data| serde_json::from_slice(&data))
.context("User password is invalid")?
.map_err(|_e| anyhow::anyhow!("JSON content in database could not be deserialized!"))?;
key_bindings
.iter()
.map(|s| s.unbind(bindings).ok())
.find(|result| result.is_some())
.flatten()
.ok_or_else(|| anyhow::anyhow!("Unable to unbind key password"))
}
pub fn encrypt_key_password(
bindings: &[Pkcs11Binding],
user_password: Password,
key_password: Password,
) -> anyhow::Result<Vec<u8>> {
let bound_passwords = key_password.map(|password| BoundSecret::bind(bindings, password))?;
tracing::debug!(
tokens_bound = bindings.len(),
"Key password has been successfully bound"
);
symmetric_encrypt(
user_password,
serde_json::to_vec(&bound_passwords)?.as_slice(),
)
.context("Failed to PGP-encrypt the bound password")
}
pub(crate) fn bind_with_pkcs11(bindings: &[Pkcs11Binding], secret: &str) -> anyhow::Result<String> {
let bound = BoundSecret::bind(bindings, secret.as_bytes())?;
serde_json::to_string(&bound).context("Failed to serialize bound secret")
}
pub(crate) fn unbind_with_pkcs11(
bindings: &[Pkcs11Binding],
bound_secret: &str,
) -> anyhow::Result<String> {
let bound_secrets: Vec<BoundSecret> =
serde_json::from_str(bound_secret).context("Failed to deserialize bound key material")?;
bound_secrets
.iter()
.find_map(|s| s.unbind(bindings).ok())
.ok_or_else(|| anyhow::anyhow!("Unable to unbind key material"))
.and_then(|password| {
password
.map(|p| String::from_utf8(p.to_vec()))
.map_err(|e| anyhow::anyhow!("Unbound key material is not valid UTF-8: {e}"))
})
}
pub(crate) async fn decrypt_private_key(
key: &db::Key,
encrypted_password: &[u8],
bindings: &[Pkcs11Binding],
user_password: Password,
) -> anyhow::Result<PKey<openssl::pkey::Private>> {
let key_material = if let Some(material) = &key.key_material {
material
} else {
return Err(anyhow::anyhow!(
"Can't decrypt private key for a PKCS#11 token"
));
};
let key_password = decrypt_key_password(bindings, user_password, encrypted_password).await?;
let encrypted_pem = unbind_with_pkcs11(bindings, key_material)?;
Ok(key_password.map(|passphrase| {
PKey::private_key_from_pem_passphrase(encrypted_pem.as_bytes(), passphrase)
})?)
}
struct SymmetricHelper {
password: Password,
}
impl DecryptionHelper for SymmetricHelper {
fn decrypt(
&mut self,
_pkesks: &[sequoia_openpgp::packet::PKESK],
symmetric_session_keys: &[sequoia_openpgp::packet::SKESK],
_sym_algo: Option<sequoia_openpgp::types::SymmetricAlgorithm>,
decrypt: &mut dyn FnMut(
Option<sequoia_openpgp::types::SymmetricAlgorithm>,
&sequoia_openpgp::crypto::SessionKey,
) -> bool,
) -> sequoia_openpgp::Result<Option<sequoia_openpgp::Cert>> {
for session_key in symmetric_session_keys {
if session_key
.decrypt(&self.password)
.map(|(algorithm, session_key)| decrypt(algorithm, &session_key))
.unwrap_or(false)
{
return Ok(None);
}
}
Err(anyhow::anyhow!("Bad passphrase"))
}
}
impl VerificationHelper for SymmetricHelper {
fn get_certs(
&mut self,
_ids: &[sequoia_openpgp::KeyHandle],
) -> sequoia_openpgp::Result<Vec<sequoia_openpgp::Cert>> {
Ok(vec![])
}
fn check(
&mut self,
_structure: sequoia_openpgp::parse::stream::MessageStructure<'_>,
) -> sequoia_openpgp::Result<()> {
Ok(())
}
}
fn symmetric_encrypt(password: Password, data: &[u8]) -> anyhow::Result<Vec<u8>> {
let mut buffer = vec![];
{
let message = Armorer::new(Message::new(&mut buffer)).build()?;
let encryptor = Encryptor::with_passwords(message, Some(password))
.symmetric_algo(SymmetricAlgorithm::AES256)
.aead_algo(AEADAlgorithm::GCM)
.build()?;
let mut message = LiteralWriter::new(encryptor).build()?;
message.write_all(data)?;
message.finalize()?;
}
Ok(buffer)
}
fn symmetric_decrypt(password: Password, data: &[u8]) -> anyhow::Result<Vec<u8>> {
let policy = StandardPolicy::new();
let helper = SymmetricHelper { password };
let mut decryptor = DecryptorBuilder::from_bytes(&data)?.with_policy(&policy, None, helper)?;
let mut user_passphrase = vec![];
decryptor.read_to_end(&mut user_passphrase)?;
Ok(user_passphrase)
}
pub mod sigul {
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
struct SigulPkcs11BoundPassword {
method: String,
value: String,
token: String,
}
fn gpg_symmetric_decrypt(password: Password, data: &[u8]) -> anyhow::Result<Vec<u8>> {
let mut child = std::process::Command::new("gpg")
.args([
"--batch",
"--yes",
"--quiet",
"--decrypt",
"--passphrase-fd",
"0",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("Failed to spawn pgp process")?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| anyhow::anyhow!("Failed to open pgp stdin"))?;
password.map(|p| {
stdin.write_all(p)?;
stdin.write_all(b"\n")?;
stdin.write_all(data)
})?;
drop(stdin);
let output = child
.wait_with_output()
.context("Failed to wait for pgp process")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!(
"gpg decryption failed (exit code {:?}): {}",
output.status.code(),
stderr
));
}
Ok(output.stdout)
}
pub async fn unbind_key_password(
user_password: Password,
encrypted_passphrase: &[u8],
sigul_binding: &Option<Pkcs11Binding>,
) -> anyhow::Result<Password> {
let bound_key_password = gpg_symmetric_decrypt(user_password, encrypted_passphrase)
.context("Decryption via the user password failed")?;
tracing::debug!("User password used to decrypt key access; unbinding...");
let mut bound_key_password = String::from_utf8(bound_key_password)
.map_err(|_| anyhow::anyhow!("Passwords are expected to be UTF-8"))?;
let key_password = loop {
match bound_key_password.as_str() {
bound_object if bound_key_password.starts_with("{") => {
if let Some(sigul_binding) = sigul_binding.as_ref() {
let pkcs11_binding: SigulPkcs11BoundPassword =
serde_json::from_str(bound_object)
.context("Key is bound with an unsupported method")?;
let inner = super::binding_decrypt(
sigul_binding.clone(),
pkcs11_binding.value.as_bytes().to_vec(),
)?;
bound_key_password = String::from_utf8(inner)?;
} else {
return Err(anyhow::anyhow!(
"Sigul key is hardware-bound but no PKCS#11 unbinding key was provided"
));
}
}
bound_list if bound_key_password.starts_with("[") => {
if let Some(sigul_binding) = sigul_binding.as_ref() {
let binding_entries: Vec<serde_json::Value> =
serde_json::from_str(bound_list)?;
for binding in binding_entries {
if let Ok(pkcs11_binding) =
serde_json::from_value::<SigulPkcs11BoundPassword>(binding)
{
let inner = super::binding_decrypt(
sigul_binding.clone(),
pkcs11_binding.value.as_bytes().to_vec(),
);
if let Ok(inner) = inner {
bound_key_password = String::from_utf8(inner)?;
break;
} else {
tracing::debug!(
binding_token = pkcs11_binding.token,
"Failed to decrypt the password"
);
}
} else {
tracing::debug!("Unknown binding format found, skipping");
}
}
} else {
return Err(anyhow::anyhow!(
"Sigul key is hardware-bound but no PKCS#11 unbinding key was provided"
));
}
}
unbound => break Ok::<_, anyhow::Error>(unbound),
};
}?;
tracing::debug!("Key password successfully unbound");
Ok(key_password.into())
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use tempfile::NamedTempFile;
use super::*;
use crate::server::crypto::test_utils::setup_hsm;
#[test]
fn encrypt_decrypt() -> Result<()> {
let user_passphrase = Password::from("this grants a user access to the key passphrase");
let data = "this encrypts the private key";
let encrypted_data = symmetric_encrypt(user_passphrase.clone(), data.as_bytes())?;
let decrypted_data = symmetric_decrypt(user_passphrase, &encrypted_data)?;
assert_eq!(data.as_bytes(), decrypted_data);
Ok(())
}
#[test]
fn encrypt_with_sq_decrypt() -> Result<()> {
let user_passphrase = "this grants a user access to the key passphrase".to_string();
let data = "this encrypts the private key";
let mut password_file = NamedTempFile::new()?;
let mut message = NamedTempFile::new()?;
password_file.write_all(user_passphrase.as_bytes())?;
message.write_all(data.as_bytes())?;
let mut command = std::process::Command::new("sq");
let result = command
.arg("encrypt")
.arg(format!(
"--with-password-file={}",
password_file.path().display()
))
.arg("--without-signature")
.arg(message.path())
.output()?;
let retrieved_key_passphrase =
symmetric_decrypt(Password::from(user_passphrase), &result.stdout)?;
assert_eq!(data.as_bytes(), retrieved_key_passphrase);
Ok(())
}
#[test]
fn encrypt_decrypt_with_sq() -> Result<()> {
let user_passphrase = "this grants a user access to the key passphrase".to_string();
let data = "this encrypts the private key";
let encrypted_passphrase =
symmetric_encrypt(Password::from(user_passphrase.as_bytes()), data.as_bytes())?;
let mut password_file = NamedTempFile::new()?;
let mut encrypted_message = NamedTempFile::new()?;
password_file.write_all(user_passphrase.as_bytes())?;
encrypted_message.write_all(&encrypted_passphrase)?;
let mut command = std::process::Command::new("sq");
let result = command
.arg(format!(
"--password-file={}",
password_file.path().display()
))
.arg("decrypt")
.arg(encrypted_message.path())
.output()?;
assert_eq!(data.as_bytes(), result.stdout);
Ok(())
}
#[tokio::test]
async fn encrypt_decrypt_binding() -> Result<()> {
let softhsm = setup_hsm()?;
let bound_secrets = BoundSecret::bind(&softhsm.bindings, b"some data")?;
let bound_password = bound_secrets.first().unwrap();
assert!(
matches!(
&bound_password,
&BoundSecret::Pkcs11WithCMS {
fingerprint: _,
secret: _
}
),
"Expected PKCS11 binding"
);
let decrypted_data = bound_password
.unbind(&softhsm.bindings)?
.map(|p| p.to_vec());
assert_eq!(b"some data".as_slice(), decrypted_data);
Ok(())
}
#[tokio::test]
async fn encrypt_decrypt_key_password() -> Result<()> {
let softhsm = setup_hsm()?;
let key_password = Password::from("a secret that never leaves the server");
let user_password = Password::from("some long password clients provide");
let blob = encrypt_key_password(
&softhsm.bindings,
user_password.clone(),
key_password.clone(),
)?;
let roundtrip_key_password =
decrypt_key_password(&softhsm.bindings, user_password, &blob).await?;
assert_eq!(key_password, roundtrip_key_password);
Ok(())
}
#[tokio::test]
async fn encrypt_decrypt_key_password_binding_no_key() -> Result<()> {
let softhsm = setup_hsm()?;
let key_password = Password::from("a secret that never leaves the server");
let user_password = Password::from("some long password clients provide");
let blob = encrypt_key_password(
&softhsm.bindings,
user_password.clone(),
key_password.clone(),
)?;
let result =
decrypt_key_password(softhsm.bindings.get(1..).unwrap(), user_password, &blob).await;
assert!(result.is_err_and(|err| err.to_string().contains("Unable to unbind key password")));
Ok(())
}
#[tokio::test]
async fn encrypt_decrypt_key_password_wrong_user_password() -> Result<()> {
let softhsm = setup_hsm()?;
let key_password = Password::from("a secret that never leaves the server");
let user_password = Password::from("some long password clients provide");
let blob = encrypt_key_password(&softhsm.bindings, user_password, key_password.clone())?;
let user_password = Password::from("the wrong password");
let result =
decrypt_key_password(softhsm.bindings.get(1..).unwrap(), user_password, &blob).await;
let err_str = result.map_err(|e| e.to_string()).err().unwrap();
assert_eq!(err_str, "User password is invalid");
Ok(())
}
#[tokio::test]
async fn encrypt_decrypt_key_password_no_bindings() -> Result<()> {
let key_password = Password::from("a secret that never leaves the server");
let user_password = Password::from("some long password clients provide");
let blob = encrypt_key_password(&[], user_password.clone(), key_password.clone())?;
let roundtrip_key_password = decrypt_key_password(&[], user_password, &blob).await?;
assert_eq!(key_password, roundtrip_key_password);
Ok(())
}
}