use crate::crypto::TrustAnchorPublicKey;
use crate::error::CellosError;
use crate::trust_keys::Signer;
use cryptoki::context::{CInitializeArgs, Pkcs11};
use cryptoki::mechanism::Mechanism;
use cryptoki::object::{Attribute, AttributeType, ObjectClass, ObjectHandle};
use cryptoki::session::{Session, UserType};
use cryptoki::slot::Slot;
use cryptoki::types::AuthPin;
use std::path::Path;
use zeroize::Zeroizing;
pub const PKCS11_MODULE_ENV: &str = "CELLOS_PKCS11_MODULE";
pub const PKCS11_TOKEN_LABEL_ENV: &str = "CELLOS_PKCS11_TOKEN_LABEL";
pub const PKCS11_KEY_LABEL_ENV: &str = "CELLOS_PKCS11_KEY_LABEL";
pub const PKCS11_PIN_ENV: &str = "CELLOS_PKCS11_PIN";
pub struct Pkcs11Signer {
kid: String,
ctx: Pkcs11,
slot: Slot,
key_label: Vec<u8>,
pin: Zeroizing<String>,
public: TrustAnchorPublicKey,
}
fn err(stage: &str, e: impl std::fmt::Display) -> CellosError {
CellosError::InvalidSpec(format!("pkcs11 {stage}: {e}"))
}
impl Pkcs11Signer {
pub fn open(
kid: impl Into<String>,
module_path: &Path,
token_label: &str,
key_label: &str,
pin: &str,
) -> Result<Self, CellosError> {
let ctx = Pkcs11::new(module_path).map_err(|e| err("load module", e))?;
ctx.initialize(CInitializeArgs::OsThreads)
.map_err(|e| err("initialize", e))?;
let slot = find_slot(&ctx, token_label)?;
let key_label = key_label.as_bytes().to_vec();
let pin = Zeroizing::new(pin.to_string());
let public = {
let session = open_login(&ctx, slot, &pin)?;
let pub_handle = find_one(&session, ObjectClass::PUBLIC_KEY, &key_label)?;
let point = ec_point(&session, pub_handle)?;
TrustAnchorPublicKey::from_bytes_unchecked(parse_ed25519_point(&point)?)
};
Ok(Self {
kid: kid.into(),
ctx,
slot,
key_label,
pin,
public,
})
}
}
impl Signer for Pkcs11Signer {
fn kid(&self) -> &str {
&self.kid
}
fn sign(&self, message: &[u8]) -> Result<[u8; 64], CellosError> {
let session = open_login(&self.ctx, self.slot, &self.pin)?;
let priv_handle = find_one(&session, ObjectClass::PRIVATE_KEY, &self.key_label)?;
let sig = session
.sign(&Mechanism::Eddsa, priv_handle, message)
.map_err(|e| err("sign", e))?;
sig.as_slice().try_into().map_err(|_| {
CellosError::InvalidSpec(format!(
"pkcs11 sign: expected a 64-byte Ed25519 signature, got {}",
sig.len()
))
})
}
fn verifying_key(&self) -> TrustAnchorPublicKey {
self.public
}
}
fn find_slot(ctx: &Pkcs11, token_label: &str) -> Result<Slot, CellosError> {
let slots = ctx
.get_slots_with_token()
.map_err(|e| err("list slots", e))?;
for slot in slots {
if let Ok(info) = ctx.get_token_info(slot) {
if info.label().trim() == token_label {
return Ok(slot);
}
}
}
Err(CellosError::InvalidSpec(format!(
"pkcs11: no token with label {token_label:?}"
)))
}
fn open_login(ctx: &Pkcs11, slot: Slot, pin: &str) -> Result<Session, CellosError> {
let session = ctx
.open_rw_session(slot)
.map_err(|e| err("open session", e))?;
session
.login(UserType::User, Some(&AuthPin::new(pin.to_string())))
.map_err(|e| err("login", e))?;
Ok(session)
}
fn find_one(
session: &Session,
class: ObjectClass,
label: &[u8],
) -> Result<ObjectHandle, CellosError> {
let template = vec![Attribute::Class(class), Attribute::Label(label.to_vec())];
let handles = session
.find_objects(&template)
.map_err(|e| err("find object", e))?;
handles
.into_iter()
.next()
.ok_or_else(|| CellosError::InvalidSpec("pkcs11: object not found for label".into()))
}
fn ec_point(session: &Session, handle: ObjectHandle) -> Result<Vec<u8>, CellosError> {
let attrs = session
.get_attributes(handle, &[AttributeType::EcPoint])
.map_err(|e| err("read EC point", e))?;
for a in attrs {
if let Attribute::EcPoint(bytes) = a {
return Ok(bytes);
}
}
Err(CellosError::InvalidSpec(
"pkcs11: public key has no EC point".into(),
))
}
fn parse_ed25519_point(point: &[u8]) -> Result<[u8; 32], CellosError> {
let raw: &[u8] = if point.len() == 34 && point[0] == 0x04 && point[1] == 0x20 {
&point[2..]
} else if point.len() == 32 {
point
} else {
return Err(CellosError::InvalidSpec(format!(
"pkcs11: unexpected EC point length {} (want 32 raw or 34 DER-wrapped)",
point.len()
)));
};
raw.try_into()
.map_err(|_| CellosError::InvalidSpec("pkcs11: EC point is not 32 bytes".into()))
}
#[cfg(test)]
mod tests {
use super::parse_ed25519_point;
#[test]
fn raw_32_byte_point_is_accepted_verbatim() {
let raw = [7u8; 32];
assert_eq!(parse_ed25519_point(&raw).unwrap(), raw);
}
#[test]
fn der_wrapped_point_is_unwrapped() {
let mut der = vec![0x04, 0x20];
der.extend_from_slice(&[9u8; 32]);
assert_eq!(parse_ed25519_point(&der).unwrap(), [9u8; 32]);
}
#[test]
fn der_prefix_must_be_exact() {
let mut bad = vec![0x03, 0x20];
bad.extend_from_slice(&[1u8; 32]);
assert!(parse_ed25519_point(&bad).is_err());
let mut badlen = vec![0x04, 0x21];
badlen.extend_from_slice(&[1u8; 32]);
assert!(parse_ed25519_point(&badlen).is_err());
}
#[test]
fn wrong_lengths_are_rejected_without_panic() {
for n in [0usize, 1, 31, 33, 35, 64] {
assert!(
parse_ed25519_point(&vec![0u8; n]).is_err(),
"len {n} must be rejected, not panic"
);
}
}
}