use anyhow::Context;
use cryptoki::{mechanism::Mechanism, session::Session};
use openssl::{pkey::PKey, pkey_ctx::PkeyCtx};
use crate::{
protocol::{self, DigestAlgorithm, KeyAlgorithm, SignaturePayload},
server::db,
};
pub fn sign_with_softkey(
key: &db::Key,
pkey: &PKey<openssl::pkey::Private>,
digests: Vec<(DigestAlgorithm, String)>,
) -> anyhow::Result<Vec<protocol::Signature>> {
let mut signatures = Vec::with_capacity(digests.len());
for (algorithm, hex_hash) in digests {
let hash = hex::decode(&hex_hash).context("The digest provided was not valid hex")?;
if hash.len() != algorithm.size() {
return Err(anyhow::anyhow!(
"The specified digest algorithm is {} bytes; payload was {}",
algorithm.size(),
hash.len()
));
}
let mut ctx = PkeyCtx::new(pkey)?;
ctx.sign_init()?;
ctx.set_signature_md(algorithm.into())?;
if key.key_algorithm == KeyAlgorithm::Rsa4K {
ctx.set_rsa_padding(openssl::rsa::Padding::PKCS1)?;
}
let mut signature = vec![];
ctx.sign_to_vec(&hash, &mut signature)?;
let signature = match key.key_algorithm {
KeyAlgorithm::Rsa2K | KeyAlgorithm::Rsa4K => protocol::SignaturePayload::RSA(signature),
KeyAlgorithm::P256 => protocol::SignaturePayload::P256(signature),
};
tracing::info!(digest_algorithm=%algorithm, digest=hex_hash, "Signature issued");
signatures.push(protocol::Signature {
signature,
digest: algorithm,
hash: hex_hash,
});
}
Ok(signatures)
}
pub fn sign_with_pkcs11(
key: &db::Key,
session: &Session,
digests: Vec<(DigestAlgorithm, String)>,
) -> anyhow::Result<Vec<protocol::Signature>> {
let private_key = key.get_pkcs11_private_key(session)?;
let mut signatures = Vec::with_capacity(digests.len());
for (algorithm, hex_hash) in digests {
let hash = hex::decode(&hex_hash).context("The digest provided was not valid hex")?;
if hash.len() != algorithm.size() {
return Err(anyhow::anyhow!(
"The specified digest algorithm is {} bytes; payload was {}",
algorithm.size(),
hash.len()
));
}
let (mechanism, data_to_sign) = match key.key_algorithm {
KeyAlgorithm::Rsa4K | KeyAlgorithm::Rsa2K => {
let digest_info = crate::der::encode_digest_info(algorithm, &hash)?;
(Mechanism::RsaPkcs, digest_info)
}
KeyAlgorithm::P256 => {
(Mechanism::Ecdsa, hash)
}
};
let signature = session
.sign(&mechanism, private_key, &data_to_sign)
.context("PKCS#11 signing operation failed")?;
let signature = match key.key_algorithm {
KeyAlgorithm::Rsa4K | KeyAlgorithm::Rsa2K => SignaturePayload::RSA(signature),
KeyAlgorithm::P256 => {
let r = signature
.get(..32)
.map(openssl::bn::BigNum::from_slice)
.expect("A P256 signature should be 64 bytes")?;
let s = signature
.get(32..)
.map(openssl::bn::BigNum::from_slice)
.expect("A P256 signature should be 64 bytes")?;
let ecdsa_sig = openssl::ecdsa::EcdsaSig::from_private_components(r, s)?;
SignaturePayload::P256(ecdsa_sig.to_der()?)
}
};
tracing::info!(digest_algorithm=%algorithm, digest=hex_hash, "Signature issued");
signatures.push(protocol::Signature {
signature,
digest: algorithm,
hash: hex_hash,
});
}
Ok(signatures)
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU32;
use std::path::PathBuf;
use std::process::Command;
use anyhow::Result;
use sequoia_openpgp::crypto::Password;
use tempfile::TempDir;
use zerocopy::IntoBytes;
use super::*;
use crate::der::{decode_digest_info, encode_digest_info};
use crate::protocol::DigestAlgorithm;
use crate::server::crypto;
use crate::server::crypto::binding::decrypt_private_key;
use crate::server::crypto::test_utils::setup_hsm;
use crate::server::crypto::token::import_pkcs11_token;
#[test]
fn encode_decode_digest_info() -> Result<()> {
let algorithm = DigestAlgorithm::Sha256;
let hash = openssl::hash::hash(algorithm.into(), b"data")?;
let encoded = encode_digest_info(algorithm, &hash)?;
let (decoded_algorithm, decoded_hash) = decode_digest_info(&encoded)?;
assert_eq!(
algorithm, decoded_algorithm,
"Digest algorithm should match"
);
assert_eq!(hash.as_bytes(), &decoded_hash, "Digest should match");
Ok(())
}
#[tokio::test]
async fn sign_with_pkcs11_rsa_key() -> Result<()> {
let hsm = setup_hsm()?;
let db_pool = db::pool("sqlite::memory:", false).await?;
db::migrate(&db_pool).await?;
let mut conn = db_pool.begin().await?;
let token = import_pkcs11_token(
&mut conn,
PathBuf::from("/usr/lib64/pkcs11/libkryoptic_pkcs11.so"),
None,
hsm.user_pin.clone(),
)
.await?;
let pkcs11 = token.intialize()?;
let slot = token.slot(&pkcs11)?;
let session = pkcs11.open_ro_session(slot)?;
session.login(cryptoki::session::UserType::User, Some(&hsm.user_pin))?;
let keys = db::Key::list(&mut conn).await?;
let rsa_key = keys
.iter()
.find(|k| k.key_algorithm == KeyAlgorithm::Rsa4K)
.expect("Should have an RSA key");
let data = b"test data";
let digest = openssl::hash::hash(openssl::hash::MessageDigest::sha256(), data)?;
let hex_hash = hex::encode(digest);
let signatures = super::sign_with_pkcs11(
rsa_key,
&session,
vec![(DigestAlgorithm::Sha256, hex_hash.clone())],
)?;
pkcs11.finalize()?;
assert_eq!(signatures.len(), 1);
assert_eq!(signatures.first().unwrap().digest, DigestAlgorithm::Sha256);
assert_eq!(signatures.first().unwrap().hash, hex_hash);
assert!(!signatures.first().unwrap().signature.is_empty());
let public_key = openssl::pkey::PKey::public_key_from_pem(rsa_key.public_key.as_bytes())?;
let mut ctx = openssl::pkey_ctx::PkeyCtx::new(&public_key)?;
ctx.verify_init()?;
ctx.set_signature_md(openssl::md::Md::sha256())?;
ctx.set_rsa_padding(openssl::rsa::Padding::PKCS1)?;
let signature = signatures.first().unwrap().signature.as_ref();
let result = ctx.verify(&digest, signature)?;
assert!(result, "Signature should be valid (OpenSSL bindings)");
let data_path = hsm.directory.path().join("unsigned_data");
let signature_path = hsm.directory.path().join("signature.bin");
let pubkey_path = hsm.directory.path().join("pubkey.pem");
std::fs::write(&data_path, data)?;
std::fs::write(&signature_path, signature)?;
std::fs::write(&pubkey_path, rsa_key.public_key.as_bytes())?;
let output = Command::new("openssl")
.args(["dgst", "-sha256", "-verify"])
.arg(&pubkey_path)
.arg("-signature")
.arg(&signature_path)
.arg(&data_path)
.output()?;
assert!(
output.status.success(),
"OpenSSL CLI verification failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
#[tokio::test]
async fn sign_with_pkcs11_ecc_key() -> Result<()> {
let hsm = setup_hsm()?;
let db_pool = db::pool("sqlite::memory:", false).await?;
db::migrate(&db_pool).await?;
let mut conn = db_pool.begin().await?;
let token = import_pkcs11_token(
&mut conn,
PathBuf::from("/usr/lib64/pkcs11/libkryoptic_pkcs11.so"),
None,
hsm.user_pin.clone(),
)
.await?;
let pkcs11 = token.intialize()?;
let slot = token.slot(&pkcs11)?;
let session = pkcs11.open_ro_session(slot)?;
session.login(cryptoki::session::UserType::User, Some(&hsm.user_pin))?;
let keys = db::Key::list(&mut conn).await?;
let ecc_key = keys
.iter()
.find(|k| k.key_algorithm == KeyAlgorithm::P256)
.expect("Should have an ECC key");
let data = b"test data";
let digest = openssl::hash::hash(openssl::hash::MessageDigest::sha256(), data)?;
let hex_hash = hex::encode(digest);
let signatures = super::sign_with_pkcs11(
ecc_key,
&session,
vec![(DigestAlgorithm::Sha256, hex_hash.clone())],
)?;
pkcs11.finalize()?;
assert_eq!(signatures.len(), 1);
assert_eq!(signatures.first().unwrap().digest, DigestAlgorithm::Sha256);
assert_eq!(signatures.first().unwrap().hash, hex_hash);
assert!(!signatures.first().unwrap().signature.is_empty());
let public_key = openssl::pkey::PKey::public_key_from_pem(ecc_key.public_key.as_bytes())?;
let ec_key = public_key.ec_key()?;
let signature = signatures.first().unwrap().signature.as_ref();
let ecdsa_sig = openssl::ecdsa::EcdsaSig::from_der(signature)?;
assert!(
ecdsa_sig.verify(&digest, &ec_key)?,
"ECDSA signature should be valid (OpenSSL bindings)"
);
let data_path = hsm.directory.path().join("unsigned_data");
let signature_path = hsm.directory.path().join("signature.bin");
let pubkey_path = hsm.directory.path().join("pubkey.pem");
std::fs::write(&data_path, data)?;
std::fs::write(&signature_path, signature)?;
std::fs::write(&pubkey_path, ecc_key.public_key.as_bytes())?;
let output = Command::new("openssl")
.args(["dgst", "-sha256", "-verify"])
.arg(&pubkey_path)
.arg("-signature")
.arg(&signature_path)
.arg(&data_path)
.output()?;
assert!(
output.status.success(),
"OpenSSL CLI verification failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
#[tokio::test]
async fn sign_with_softkey_rsa() -> Result<()> {
let temp_dir = TempDir::new()?;
let user_password = Password::from("test-key-password");
let key_algorithm = KeyAlgorithm::Rsa4K;
let encrypted_key = crypto::create_encrypted_key(
&crate::server::Config::default(),
user_password.clone(),
key_algorithm,
sequoia_openpgp::Profile::RFC4880,
crypto::KeyUsage::CodeSigning,
"test-key".to_string(),
NonZeroU32::new(100).unwrap(),
None,
)?;
let key = db::Key {
id: 1,
hybrid_pair_id: None,
name: "test-rsa-softkey".to_string(),
key_algorithm,
handle: encrypted_key.handle,
key_material: Some(encrypted_key.key_material),
public_key: encrypted_key.public_key_pem,
pkcs11_token_id: None,
pkcs11_key_id: None,
};
let data = b"test data";
let digest = openssl::hash::hash(openssl::hash::MessageDigest::sha256(), data)?;
let hex_hash = hex::encode(digest);
let pkey = decrypt_private_key(&key, &encrypted_key.encrypted_password, &[], user_password)
.await?;
let signatures = super::sign_with_softkey(
&key,
&pkey,
vec![(DigestAlgorithm::Sha256, hex_hash.clone())],
)?;
assert_eq!(signatures.len(), 1);
assert_eq!(signatures.first().unwrap().digest, DigestAlgorithm::Sha256);
assert_eq!(signatures.first().unwrap().hash, hex_hash);
assert!(!signatures.first().unwrap().signature.is_empty());
let public_key = openssl::pkey::PKey::public_key_from_pem(key.public_key.as_bytes())?;
let mut ctx = openssl::pkey_ctx::PkeyCtx::new(&public_key)?;
ctx.verify_init()?;
ctx.set_signature_md(openssl::md::Md::sha256())?;
ctx.set_rsa_padding(openssl::rsa::Padding::PKCS1)?;
let signature = signatures.first().unwrap().signature.as_ref();
let result = ctx.verify(&digest, signature)?;
assert!(result, "Signature should be valid (OpenSSL bindings)");
let data_path = temp_dir.path().join("unsigned_data");
let signature_path = temp_dir.path().join("signature.bin");
let pubkey_path = temp_dir.path().join("pubkey.pem");
std::fs::write(&data_path, data)?;
std::fs::write(&signature_path, signature)?;
std::fs::write(&pubkey_path, key.public_key.as_bytes())?;
let output = Command::new("openssl")
.args(["dgst", "-sha256", "-verify"])
.arg(&pubkey_path)
.arg("-signature")
.arg(&signature_path)
.arg(&data_path)
.output()?;
assert!(
output.status.success(),
"OpenSSL CLI verification failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
#[tokio::test]
async fn sign_with_softkey_ecc() -> Result<()> {
let temp_dir = TempDir::new()?;
let user_password = Password::from("test-key-password");
let key_algorithm = KeyAlgorithm::P256;
let encrypted_key = crypto::create_encrypted_key(
&crate::server::Config::default(),
user_password.clone(),
key_algorithm,
sequoia_openpgp::Profile::RFC4880,
crypto::KeyUsage::CodeSigning,
"test-key".to_string(),
NonZeroU32::new(100).unwrap(),
None,
)?;
let key = db::Key {
id: 1,
hybrid_pair_id: None,
name: "test-ecc-softkey".to_string(),
key_algorithm,
handle: encrypted_key.handle,
key_material: Some(encrypted_key.key_material),
public_key: encrypted_key.public_key_pem,
pkcs11_token_id: None,
pkcs11_key_id: None,
};
let data = b"test data for ECC softkey signing";
let digest = openssl::hash::hash(openssl::hash::MessageDigest::sha256(), data)?;
let hex_hash = hex::encode(digest);
let pkey = decrypt_private_key(&key, &encrypted_key.encrypted_password, &[], user_password)
.await?;
let signatures = super::sign_with_softkey(
&key,
&pkey,
vec![(DigestAlgorithm::Sha256, hex_hash.clone())],
)?;
assert_eq!(signatures.len(), 1);
assert_eq!(signatures.first().unwrap().digest, DigestAlgorithm::Sha256);
assert_eq!(signatures.first().unwrap().hash, hex_hash);
assert!(!signatures.first().unwrap().signature.is_empty());
let public_key = openssl::pkey::PKey::public_key_from_pem(key.public_key.as_bytes())?;
let ec_key = public_key.ec_key()?;
let signature = signatures.first().unwrap().signature.as_ref();
let ecdsa_sig = openssl::ecdsa::EcdsaSig::from_der(signature)?;
assert!(
ecdsa_sig.verify(&digest, &ec_key)?,
"ECDSA signature should be valid (OpenSSL bindings)"
);
let data_path = temp_dir.path().join("unsigned_data");
let signature_path = temp_dir.path().join("signature.bin");
let pubkey_path = temp_dir.path().join("pubkey.pem");
std::fs::write(&data_path, data)?;
std::fs::write(&signature_path, signature)?;
std::fs::write(&pubkey_path, key.public_key.as_bytes())?;
let output = Command::new("openssl")
.args(["dgst", "-sha256", "-verify"])
.arg(&pubkey_path)
.arg("-signature")
.arg(&signature_path)
.arg(&data_path)
.output()?;
assert!(
output.status.success(),
"OpenSSL CLI verification failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
}