From 1480d1001d04f5e9f80f9aed86d84c229e15d704 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Fri, 5 Jun 2026 10:41:45 -0400 Subject: [PATCH 1/2] Add OTP key object factory and attributes Introduce the `OTPKeyFactory` trait and implement handling for `CKO_OTP_KEY` objects. This adds support for One-Time Password keys by defining their specific PKCS#11 attributes and applying standard key protections (e.g., sensitivity and extractability defaults) similar to private and secret keys. Assisted-by: Gemini Signed-off-by: Simo Sorce --- src/object/factory.rs | 14 ++++++++++--- src/object/key.rs | 6 +++--- src/object/mod.rs | 5 +++-- src/object/otp.rs | 48 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 src/object/otp.rs diff --git a/src/object/factory.rs b/src/object/factory.rs index 7d8479ba..84dd76dc 100644 --- a/src/object/factory.rs +++ b/src/object/factory.rs @@ -19,6 +19,7 @@ use super::key::{ GenericSecretKeyFactory, GenericSecretKeyMechanism, KeyFactory, PubKeyFactory, SecretKeyFactory, }; +use super::otp::OTPKeyFactory; use super::Object; #[cfg(feature = "nssdb")] @@ -562,6 +563,13 @@ pub trait ObjectFactory: Debug + Send + Sync { fn as_secret_key_factory(&self) -> Result<&dyn SecretKeyFactory> { Err(CKR_GENERAL_ERROR)? } + + /// Helper to access traits that are only available for objects of + /// class CKO_OTP_KEY. Other key type factories should not + /// implement this method. + fn as_otp_key_factory(&self) -> Result<&dyn OTPKeyFactory> { + Err(CKR_GENERAL_ERROR)? + } } /// This is a specialized factory for objects of class CKO_DATA @@ -854,14 +862,14 @@ impl ObjectFactories { None => return Err(CKR_TEMPLATE_INCOMPLETE)?, } } - CKO_SECRET_KEY => { + CKO_SECRET_KEY | CKO_OTP_KEY => { match template.iter().find(|a| a.type_ == CKA_KEY_TYPE) { Some(k) => k.to_ulong()?, None => return Err(CKR_TEMPLATE_INCOMPLETE)?, } } /* TODO: - * CKO_HW_FEATURE, CKO_DOMAIN_PARAMETERS, CKO_OTP_KEY, + * CKO_HW_FEATURE, CKO_DOMAIN_PARAMETERS, * CKO_VENDOR_DEFINED. * Builtin objects cannot be created so they always return * this error. Unsupported objects alaso return the same. @@ -880,7 +888,7 @@ impl ObjectFactories { let class = obj.get_attr_as_ulong(CKA_CLASS)?; let type_ = match class { CKO_CERTIFICATE => obj.get_attr_as_ulong(CKA_CERTIFICATE_TYPE)?, - CKO_PUBLIC_KEY | CKO_PRIVATE_KEY | CKO_SECRET_KEY => { + CKO_PUBLIC_KEY | CKO_PRIVATE_KEY | CKO_SECRET_KEY | CKO_OTP_KEY => { obj.get_attr_as_ulong(CKA_KEY_TYPE)? } _ => 0, diff --git a/src/object/key.rs b/src/object/key.rs index 0086e5ff..ade2b24d 100644 --- a/src/object/key.rs +++ b/src/object/key.rs @@ -79,7 +79,7 @@ pub trait KeyFactory: ObjectFactory { )?; match obj.get_class() { - CKO_PUBLIC_KEY | CKO_PRIVATE_KEY | CKO_SECRET_KEY => { + CKO_PUBLIC_KEY | CKO_PRIVATE_KEY | CKO_SECRET_KEY | CKO_OTP_KEY => { // default key attributes on CreateObject obj.set_attr(Attribute::from_bool(CKA_LOCAL, false))?; } @@ -87,8 +87,8 @@ pub trait KeyFactory: ObjectFactory { } match obj.get_class() { - CKO_PRIVATE_KEY | CKO_SECRET_KEY => { - // default key attributes on CreateObject for PRIVATE/SECRET keys + CKO_PRIVATE_KEY | CKO_SECRET_KEY | CKO_OTP_KEY => { + // default key attributes on CreateObject for PRIVATE/SECRET/OTP keys obj.set_attr(Attribute::from_bool( CKA_ALWAYS_SENSITIVE, false, diff --git a/src/object/mod.rs b/src/object/mod.rs index 7756a7ee..54a3f20f 100644 --- a/src/object/mod.rs +++ b/src/object/mod.rs @@ -20,6 +20,7 @@ use uuid::Uuid; pub mod certs; pub mod factory; pub mod key; +pub mod otp; pub use factory::{ attr_element, OAFlags, ObjectFactories, ObjectFactory, ObjectFactoryData, @@ -212,7 +213,7 @@ impl Object { /// Report if the object is sensitive with a sensible default pub fn is_sensitive(&self) -> bool { match self.class { - CKO_PRIVATE_KEY | CKO_SECRET_KEY => { + CKO_PRIVATE_KEY | CKO_SECRET_KEY | CKO_OTP_KEY => { for a in &self.attributes { if a.get_type() == CKA_SENSITIVE { return a.to_bool().unwrap_or(true); @@ -227,7 +228,7 @@ impl Object { /// Report is the object is extractable with a sensible default pub fn is_extractable(&self) -> bool { match self.class { - CKO_PRIVATE_KEY | CKO_SECRET_KEY => { + CKO_PRIVATE_KEY | CKO_SECRET_KEY | CKO_OTP_KEY => { for a in &self.attributes { if a.get_type() == CKA_EXTRACTABLE { return a.to_bool().unwrap_or(false); diff --git a/src/object/otp.rs b/src/object/otp.rs new file mode 100644 index 00000000..bbd8519c --- /dev/null +++ b/src/object/otp.rs @@ -0,0 +1,48 @@ +// Copyright 2023-2026 Simo Sorce +// See LICENSE.txt file for terms + +use crate::attribute::Attribute; +use crate::object::factory::{attr_element, OAFlags}; +use crate::object::key::SecretKeyFactory; +use crate::pkcs11::*; + +/// This is a common trait to define factories for key objects of class +/// CKO_OTP_KEY +/// +/// [OTP key objects](https://docs.oasis-open.org/pkcs11/pkcs11-spec/v3.2/pkcs11-spec-v3.2.html#_Toc195693644) +/// (Version 3.2) +pub trait OTPKeyFactory: SecretKeyFactory { + /// Adds the OTP key attributes defined in the spec + fn add_common_otp_key_attrs(&mut self) { + self.add_common_secret_key_attrs(); + let attrs = self.get_data_mut().get_attributes_mut(); + attrs.push(attr_element!( + CKA_OTP_FORMAT; OAFlags::Defval | OAFlags::SettableOnlyOnCreate; Attribute::from_ulong; val CK_OTP_FORMAT_DECIMAL)); + attrs.push(attr_element!( + CKA_OTP_LENGTH; OAFlags::Defval | OAFlags::SettableOnlyOnCreate; Attribute::from_ulong; val 6)); + attrs.push(attr_element!( + CKA_OTP_TIME_INTERVAL; OAFlags::SettableOnlyOnCreate; Attribute::from_ulong; val CK_UNAVAILABLE_INFORMATION)); + attrs.push(attr_element!( + CKA_OTP_USER_FRIENDLY_MODE; OAFlags::Defval; Attribute::from_bool; val false)); + attrs.push(attr_element!( + CKA_OTP_CHALLENGE_REQUIREMENT; OAFlags::Defval | OAFlags::SettableOnlyOnCreate; Attribute::from_ulong; val CK_OTP_PARAM_IGNORED)); + attrs.push(attr_element!( + CKA_OTP_TIME_REQUIREMENT; OAFlags::Defval | OAFlags::SettableOnlyOnCreate; Attribute::from_ulong; val CK_OTP_PARAM_IGNORED)); + attrs.push(attr_element!( + CKA_OTP_COUNTER_REQUIREMENT; OAFlags::Defval | OAFlags::SettableOnlyOnCreate; Attribute::from_ulong; val CK_OTP_PARAM_IGNORED)); + attrs.push(attr_element!( + CKA_OTP_PIN_REQUIREMENT; OAFlags::Defval | OAFlags::SettableOnlyOnCreate; Attribute::from_ulong; val CK_OTP_PARAM_IGNORED)); + attrs.push(attr_element!( + CKA_OTP_COUNTER; OAFlags::Defval; Attribute::from_bytes; val vec![0u8; 8])); + attrs.push(attr_element!( + CKA_OTP_TIME; OAFlags::Defval; Attribute::from_string; val String::new())); + attrs.push(attr_element!( + CKA_OTP_USER_IDENTIFIER; OAFlags::empty(); Attribute::from_string; val String::new())); + attrs.push(attr_element!( + CKA_OTP_SERVICE_IDENTIFIER; OAFlags::empty(); Attribute::from_string; val String::new())); + attrs.push(attr_element!( + CKA_OTP_SERVICE_LOGO; OAFlags::empty(); Attribute::from_bytes; val Vec::new())); + attrs.push(attr_element!( + CKA_OTP_SERVICE_LOGO_TYPE; OAFlags::empty(); Attribute::from_string; val String::new())); + } +} From 46501ee79e953fb613ab08ba3e8a3a99bead02b6 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Tue, 16 Jun 2026 14:03:40 -0400 Subject: [PATCH 2/2] Implement HOTP mechanism This introduces support for the HMAC-based One-Time Password (HOTP) mechanism behind a new `hotp` feature flag. It implements `CKM_HOTP_KEY_GEN` for key generation and `CKM_HOTP` for generating and verifying OTP signatures. To support the stateful nature of HOTP keys, which require incrementing a counter (`CKA_OTP_COUNTER`) upon usage, a new `updates_object` method is added to the `Sign` and `Verify` traits. This allows mechanisms to request attribute updates on the underlying token objects after an operation completes. Integration tests are also added to ensure correct generation, verification, and look-ahead functionality. Assisted-by: Gemini Signed-off-by: Simo Sorce --- Cargo.toml | 2 + src/enabled.rs | 6 + src/fns/signing.rs | 50 +++ src/hotp.rs | 699 ++++++++++++++++++++++++++++++++++++++++++ src/mechanism.rs | 25 ++ src/object/factory.rs | 7 - src/tests/hotp.rs | 150 +++++++++ src/tests/mod.rs | 3 + src/tests/util.rs | 12 +- src/token.rs | 2 +- 10 files changed, 943 insertions(+), 13 deletions(-) create mode 100644 src/hotp.rs create mode 100644 src/tests/hotp.rs diff --git a/Cargo.toml b/Cargo.toml index d1a1bd8f..38e000d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,3 +126,5 @@ minimal = ["sqlitedb", "aes", "ecc_min", "hash_all", "rsa"] ossl400 = ["ossl/ossl400"] vsprintf = ["dep:vsprintf"] + +hotp = [] diff --git a/src/enabled.rs b/src/enabled.rs index 727a04b6..2e24f6a1 100644 --- a/src/enabled.rs +++ b/src/enabled.rs @@ -21,6 +21,9 @@ mod hkdf; #[cfg(feature = "hmac")] mod hmac; +#[cfg(feature = "hotp")] +mod hotp; + #[cfg(feature = "pbkdf2")] mod pbkdf2; @@ -88,6 +91,9 @@ fn register_all(mechs: &mut Mechanisms, ot: &mut ObjectFactories) { #[cfg(feature = "hmac")] hmac::register(mechs, ot); + #[cfg(feature = "hotp")] + hotp::register(mechs, ot); + #[cfg(feature = "pbkdf2")] pbkdf2::register(mechs, ot); diff --git a/src/fns/signing.rs b/src/fns/signing.rs index f6685845..023c0bc7 100644 --- a/src/fns/signing.rs +++ b/src/fns/signing.rs @@ -91,6 +91,7 @@ fn sign( } let rstate = STATE.rlock()?; let mut session = rstate.get_session_mut(s_handle)?; + let slot_id = session.get_slot_id(); let operation = session.get_operation::()?; let signature_len = operation.signature_len()?; let sig_len = @@ -112,6 +113,14 @@ fn sign( unsafe { std::slice::from_raw_parts_mut(psignature, signature_len) }; operation.sign(data, signature)?; + + if let Some((handle, attrs)) = operation.updates_object() { + let mut token = rstate.get_token_from_slot_mut(slot_id)?; + if token.set_object_attrs(handle, attrs.as_slice()).is_err() { + return Err(CKR_GENERAL_ERROR)?; + } + } + unsafe { *pul_signature_len = sig_len; } @@ -213,6 +222,7 @@ fn sign_final( } let rstate = STATE.rlock()?; let mut session = rstate.get_session_mut(s_handle)?; + let slot_id = session.get_slot_id(); let operation = session.get_operation::()?; let signature_len = operation.signature_len()?; let sig_len = @@ -231,6 +241,14 @@ fn sign_final( let signature: &mut [u8] = unsafe { std::slice::from_raw_parts_mut(psignature, signature_len) }; operation.sign_final(signature)?; + + if let Some((handle, attrs)) = operation.updates_object() { + let mut token = rstate.get_token_from_slot_mut(slot_id)?; + if token.set_object_attrs(handle, attrs.as_slice()).is_err() { + return Err(CKR_GENERAL_ERROR)?; + } + } + unsafe { *pul_signature_len = sig_len; } @@ -363,6 +381,7 @@ fn verify( } let rstate = STATE.rlock()?; let mut session = rstate.get_session_mut(s_handle)?; + let slot_id = session.get_slot_id(); let operation = session.get_operation::()?; let signature_len = operation.signature_len()?; let sig_len = @@ -376,6 +395,13 @@ fn verify( unsafe { std::slice::from_raw_parts(psignature, signature_len) }; operation.verify(data, signature)?; + if let Some((handle, attrs)) = operation.updates_object() { + let mut token = rstate.get_token_from_slot_mut(slot_id)?; + if token.set_object_attrs(handle, attrs.as_slice()).is_err() { + return Err(CKR_GENERAL_ERROR)?; + } + } + #[cfg(feature = "fips")] { let approved = operation.fips_approved(); @@ -473,6 +499,7 @@ fn verify_final( } let rstate = STATE.rlock()?; let mut session = rstate.get_session_mut(s_handle)?; + let slot_id = session.get_slot_id(); let operation = session.get_operation::()?; let signature_len = operation.signature_len()?; let sig_len = @@ -484,6 +511,13 @@ fn verify_final( unsafe { std::slice::from_raw_parts_mut(psignature, signature_len) }; operation.verify_final(signature)?; + if let Some((handle, attrs)) = operation.updates_object() { + let mut token = rstate.get_token_from_slot_mut(slot_id)?; + if token.set_object_attrs(handle, attrs.as_slice()).is_err() { + return Err(CKR_GENERAL_ERROR)?; + } + } + #[cfg(feature = "fips")] { let approved = operation.fips_approved(); @@ -757,11 +791,19 @@ fn verify_signature( } let rstate = STATE.rlock()?; let mut session = rstate.get_session_mut(s_handle)?; + let slot_id = session.get_slot_id(); let operation = session.get_operation::()?; let dlen = usize::try_from(data_len).map_err(|_| CKR_ARGUMENTS_BAD)?; let data: &[u8] = unsafe { std::slice::from_raw_parts(pdata, dlen) }; operation.verify(data)?; + if let Some((handle, attrs)) = operation.updates_object() { + let mut token = rstate.get_token_from_slot_mut(slot_id)?; + if token.set_object_attrs(handle, attrs.as_slice()).is_err() { + return Err(CKR_GENERAL_ERROR)?; + } + } + #[cfg(feature = "fips")] { let approved = operation.fips_approved(); @@ -837,9 +879,17 @@ pub extern "C" fn fn_verify_signature_update( fn verify_signature_final(s_handle: CK_SESSION_HANDLE) -> Result<()> { let rstate = STATE.rlock()?; let mut session = rstate.get_session_mut(s_handle)?; + let slot_id = session.get_slot_id(); let operation = session.get_operation::()?; operation.verify_final()?; + if let Some((handle, attrs)) = operation.updates_object() { + let mut token = rstate.get_token_from_slot_mut(slot_id)?; + if token.set_object_attrs(handle, attrs.as_slice()).is_err() { + return Err(CKR_GENERAL_ERROR)?; + } + } + #[cfg(feature = "fips")] { let approved = operation.fips_approved(); diff --git a/src/hotp.rs b/src/hotp.rs new file mode 100644 index 00000000..7c59b1a5 --- /dev/null +++ b/src/hotp.rs @@ -0,0 +1,699 @@ +// Copyright 2023-2026 Simo Sorce +// See LICENSE.txt file for terms + +use std::fmt::Debug; +use std::sync::LazyLock; + +use crate::attribute::{Attribute, CkAttrs}; +use crate::error::Result; +use crate::mechanism::{ + Mac, MechOperation, Mechanism, Mechanisms, Sign, Verify, +}; +use crate::native::hmac::HMACOperation; +use crate::object::factory::{ + attr_element, OAFlags, ObjectFactories, ObjectFactory, ObjectFactoryData, +}; +use crate::object::key::{ + default_key_attributes, default_secret_key_generate, KeyFactory, + SecretKeyFactory, +}; +use crate::object::otp::OTPKeyFactory; +use crate::object::{Object, ObjectType}; +use crate::pkcs11::*; + +/// Object that holds HOTP Mechanisms +pub(crate) static HOTP_MECHS: LazyLock<[Box; 2]> = + LazyLock::new(|| { + [ + Box::new(HOTPKeyMechanism::new()), + Box::new(HOTPMechanism::new()), + ] + }); + +/// The HOTP Key Factory facility. +static HOTP_KEY_FACTORY: LazyLock> = + LazyLock::new(|| Box::new(HOTPKeyFactory::new())); + +/// Registers all implemented HOTP Mechanisms and Factories +pub fn register(mechs: &mut Mechanisms, ot: &mut ObjectFactories) { + mechs.add_mechanism(CKM_HOTP_KEY_GEN, &(*HOTP_MECHS)[0]); + mechs.add_mechanism(CKM_HOTP, &(*HOTP_MECHS)[1]); + + ot.add_factory( + ObjectType::new(CKO_OTP_KEY, CKK_HOTP), + &(*HOTP_KEY_FACTORY), + ); +} + +/// This is a specialized factory for objects of class CKO_OTP_KEY +/// and CKA_KEY_TYPE of value CKK_HOTP +/// +/// [HOTP secret key objects](https://docs.oasis-open.org/pkcs11/pkcs11-spec/v3.2/pkcs11-spec-v3.2.html#_Toc195693654) +#[derive(Debug)] +pub struct HOTPKeyFactory { + data: ObjectFactoryData, +} + +impl HOTPKeyFactory { + /// Initializes a new HOTPKeyFactory object + pub fn new() -> HOTPKeyFactory { + let mut factory = HOTPKeyFactory { + data: ObjectFactoryData::new(CKO_OTP_KEY), + }; + + factory.add_common_otp_key_attrs(); + + let attributes = factory.data.get_attributes_mut(); + + attributes.push(attr_element!( + CKA_VALUE; OAFlags::Sensitive | OAFlags::RequiredOnCreate + | OAFlags::SettableOnlyOnCreate; Attribute::from_bytes; + val Vec::new())); + attributes.push(attr_element!( + CKA_VALUE_LEN; OAFlags::Defval; Attribute::from_ulong; + val 20)); + + /* default to true CKA_PRIVATE, CKA_SIGN, CKA_VERIFY */ + let private = attr_element!( + CKA_PRIVATE; OAFlags::Defval | OAFlags::ChangeOnCopy; Attribute::from_bool; val true); + match attributes.iter().position(|x| x.get_type() == CKA_PRIVATE) { + Some(idx) => attributes[idx] = private, + None => attributes.push(private), + } + let sign = attr_element!( + CKA_SIGN; OAFlags::Defval; Attribute::from_bool; val true); + match attributes.iter().position(|x| x.get_type() == CKA_SIGN) { + Some(idx) => attributes[idx] = sign, + None => attributes.push(sign), + } + let verify = attr_element!( + CKA_VERIFY; OAFlags::Defval; Attribute::from_bool; val true); + match attributes.iter().position(|x| x.get_type() == CKA_VERIFY) { + Some(idx) => attributes[idx] = verify, + None => attributes.push(verify), + } + + /* override CKA_OTP_COUNTER to have 8 bytes of 0s as defval */ + let counter = attr_element!( + CKA_OTP_COUNTER; OAFlags::Defval | OAFlags::SettableOnlyOnCreate; Attribute::from_bytes; + val vec![0; 8]); + match attributes + .iter() + .position(|x| x.get_type() == CKA_OTP_COUNTER) + { + Some(idx) => attributes[idx] = counter, + None => attributes.push(counter), + } + + /* override CKA_OTP_FORMAT to be settable only on creation */ + let format = attr_element!( + CKA_OTP_FORMAT; OAFlags::Defval | OAFlags::SettableOnlyOnCreate; Attribute::from_ulong; + val CK_OTP_FORMAT_DECIMAL); + match attributes + .iter() + .position(|x| x.get_type() == CKA_OTP_FORMAT) + { + Some(idx) => attributes[idx] = format, + None => attributes.push(format), + } + + /* override CKA_OTP_USER_FRIENDLY_MODE to be true by default */ + let user_friendly = attr_element!( + CKA_OTP_USER_FRIENDLY_MODE; OAFlags::Defval; Attribute::from_bool; + val true); + match attributes + .iter() + .position(|x| x.get_type() == CKA_OTP_USER_FRIENDLY_MODE) + { + Some(idx) => attributes[idx] = user_friendly, + None => attributes.push(user_friendly), + } + + factory.data.finalize(); + + factory + } + + pub fn validate_object(&self, obj: &mut Object) -> Result<()> { + obj.ensure_ulong(CKA_CLASS, CKO_OTP_KEY) + .map_err(|_| CKR_TEMPLATE_INCONSISTENT)?; + obj.ensure_ulong(CKA_KEY_TYPE, CKK_HOTP) + .map_err(|_| CKR_TEMPLATE_INCONSISTENT)?; + + for attr in [CKA_ENCRYPT, CKA_DECRYPT, CKA_WRAP, CKA_UNWRAP] { + if let Ok(true) = obj.get_attr_as_bool(attr) { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } + } + + let set_mech = match obj.get_attr_as_bytes(CKA_ALLOWED_MECHANISMS) { + Ok(mechs) => { + if mechs.is_empty() { + true + } else if mechs != CKM_HOTP.to_ne_bytes().as_slice() { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } else { + false + } + } + Err(_) => true, + }; + if set_mech { + obj.set_attr(Attribute::from_bytes( + CKA_ALLOWED_MECHANISMS, + CKM_HOTP.to_ne_bytes().to_vec(), + ))?; + } + + if let Ok(c) = obj.get_attr_as_bytes(CKA_OTP_COUNTER) { + if c.len() != 8 { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } + } + + if let Ok(format) = obj.get_attr_as_ulong(CKA_OTP_FORMAT) { + if format != CK_OTP_FORMAT_DECIMAL { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } + } + + if let Ok(len) = obj.get_attr_as_ulong(CKA_VALUE_LEN) { + if len != 20 && len != 32 && len != 64 { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } + } + + if let Ok(otp_len) = obj.get_attr_as_ulong(CKA_OTP_LENGTH) { + if otp_len < 6 || otp_len > 8 { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } + } + + if let Ok(time) = obj.get_attr_as_string(CKA_OTP_TIME) { + if !time.is_empty() { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } + } + + if let Ok(req) = obj.get_attr_as_ulong(CKA_OTP_TIME_REQUIREMENT) { + if req != CK_OTP_PARAM_IGNORED { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } + } + + if let Ok(req) = obj.get_attr_as_ulong(CKA_OTP_CHALLENGE_REQUIREMENT) { + if req != CK_OTP_PARAM_IGNORED { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } + } + + if let Ok(interval) = obj.get_attr_as_ulong(CKA_OTP_TIME_INTERVAL) { + if interval != CK_UNAVAILABLE_INFORMATION { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } + } + + if let Ok(req) = obj.get_attr_as_ulong(CKA_OTP_PIN_REQUIREMENT) { + if req != CK_OTP_PARAM_IGNORED { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } + } + + Ok(()) + } +} + +impl ObjectFactory for HOTPKeyFactory { + fn create(&self, template: &[CK_ATTRIBUTE]) -> Result { + let mut obj = self.key_create(template)?; + let len = self.get_key_buffer_len(&obj)?; + if len == 0 { + return Err(CKR_ATTRIBUTE_VALUE_INVALID)?; + } + /* By default CKA_VALUE_LEN is set to 20 by the object's factory, + * ensure we set it to the actual buffer value passed in via + * template instead */ + obj.set_attr(Attribute::from_ulong(CKA_VALUE_LEN, len as CK_ULONG))?; + + self.validate_object(&mut obj)?; + + Ok(obj) + } + + fn get_data(&self) -> &ObjectFactoryData { + &self.data + } + fn get_data_mut(&mut self) -> &mut ObjectFactoryData { + &mut self.data + } + + fn as_key_factory(&self) -> Result<&dyn KeyFactory> { + Ok(self) + } + fn as_secret_key_factory(&self) -> Result<&dyn SecretKeyFactory> { + Ok(self) + } +} + +impl KeyFactory for HOTPKeyFactory { + fn export_for_wrapping(&self, key: &Object) -> Result> { + SecretKeyFactory::default_export_for_wrapping(self, key) + } + + fn import_from_wrapped( + &self, + data: Vec, + template: &[CK_ATTRIBUTE], + ) -> Result { + SecretKeyFactory::default_import_from_wrapped(self, data, template) + } +} + +impl SecretKeyFactory for HOTPKeyFactory { + fn recommend_key_size(&self, _default: usize) -> Result { + Ok(32) + } +} + +impl OTPKeyFactory for HOTPKeyFactory {} + +const HOTP_MIN_KEY_SIZE: CK_ULONG = 20; +const HOTP_MAX_KEY_SIZE: CK_ULONG = 64; + +/// Generic reusable object to represent mechanisms associated +/// with HOTP key objects +#[derive(Debug)] +pub struct HOTPKeyMechanism { + info: CK_MECHANISM_INFO, +} + +impl HOTPKeyMechanism { + /// Instantiates a mechanism info for HOTP key type + pub fn new() -> HOTPKeyMechanism { + HOTPKeyMechanism { + info: CK_MECHANISM_INFO { + ulMinKeySize: HOTP_MIN_KEY_SIZE, + ulMaxKeySize: HOTP_MAX_KEY_SIZE, + flags: CKF_GENERATE, + }, + } + } +} + +impl Mechanism for HOTPKeyMechanism { + fn info(&self) -> &CK_MECHANISM_INFO { + &self.info + } + + fn generate_key( + &self, + mech: &CK_MECHANISM, + template: &[CK_ATTRIBUTE], + _: &Mechanisms, + _: &ObjectFactories, + ) -> Result { + let factory = HOTPKeyFactory::new(); + let mut key = factory.key_generate(template)?; + + factory.validate_object(&mut key)?; + + default_secret_key_generate(&mut key)?; + default_key_attributes(&mut key, mech.mechanism)?; + + if key.get_attr_as_bytes(CKA_OTP_COUNTER).is_err() { + key.set_attr(Attribute::from_bytes(CKA_OTP_COUNTER, vec![0; 8]))?; + } + + Ok(key) + } +} + +/// Generic reusable object to represent HOTP signature and verification mechanism +#[derive(Debug)] +pub struct HOTPMechanism { + info: CK_MECHANISM_INFO, +} + +impl HOTPMechanism { + /// Instantiates a mechanism info for HOTP mechanism + pub fn new() -> HOTPMechanism { + HOTPMechanism { + info: CK_MECHANISM_INFO { + ulMinKeySize: HOTP_MIN_KEY_SIZE, + ulMaxKeySize: HOTP_MAX_KEY_SIZE, + flags: CKF_SIGN | CKF_VERIFY, + }, + } + } +} + +impl Mechanism for HOTPMechanism { + fn info(&self) -> &CK_MECHANISM_INFO { + &self.info + } + + fn sign_new( + &self, + mech: &CK_MECHANISM, + key: &Object, + ) -> Result> { + if key.get_attr_as_ulong(CKA_CLASS)? != CKO_OTP_KEY + || key.get_attr_as_ulong(CKA_KEY_TYPE)? != CKK_HOTP + { + return Err(CKR_KEY_TYPE_INCONSISTENT)?; + } + let can_sign = key + .get_attr_as_bool(CKA_SIGN) + .map_err(|_| CKR_GENERAL_ERROR)?; + if !can_sign { + return Err(CKR_KEY_FUNCTION_NOT_PERMITTED)?; + } + Ok(Box::new(HOTPOperation::new(mech, key, true)?)) + } + + fn verify_new( + &self, + mech: &CK_MECHANISM, + key: &Object, + ) -> Result> { + if key.get_attr_as_ulong(CKA_CLASS)? != CKO_OTP_KEY + || key.get_attr_as_ulong(CKA_KEY_TYPE)? != CKK_HOTP + { + return Err(CKR_KEY_TYPE_INCONSISTENT)?; + } + let can_verify = key + .get_attr_as_bool(CKA_VERIFY) + .map_err(|_| CKR_GENERAL_ERROR)?; + if !can_verify { + return Err(CKR_KEY_FUNCTION_NOT_PERMITTED)?; + } + Ok(Box::new(HOTPOperation::new(mech, key, false)?)) + } +} + +#[derive(Debug)] +pub struct HOTPOperation { + mech_type: CK_MECHANISM_TYPE, + hmac: HMACOperation, + counter: [u8; 8], + key_handle: CK_OBJECT_HANDLE, + length: usize, + mac_len: usize, + finalized: bool, + update: Option, +} + +impl HOTPOperation { + pub fn new( + mech: &CK_MECHANISM, + key: &Object, + _is_sign: bool, + ) -> Result { + let mut override_counter = None; + let update: Option; + + if mech.ulParameterLen != 0 && !mech.pParameter.is_null() { + if mech.ulParameterLen as usize + != std::mem::size_of::() + { + return Err(CKR_MECHANISM_PARAM_INVALID)?; + } + let params = unsafe { &*(mech.pParameter as *const CK_OTP_PARAMS) }; + if params.ulCount > 0 && params.pParams.is_null() { + return Err(CKR_MECHANISM_PARAM_INVALID)?; + } + if params.ulCount > 0 { + let param_slice = unsafe { + std::slice::from_raw_parts( + params.pParams, + params.ulCount as usize, + ) + }; + for p in param_slice { + match p.type_ { + CK_OTP_FLAGS => { + if p.ulValueLen as usize + != std::mem::size_of::() + || p.pValue.is_null() + { + return Err(CKR_MECHANISM_PARAM_INVALID)?; + } + let flags = + unsafe { *(p.pValue as *const CK_ULONG) }; + if (flags & !CKF_USER_FRIENDLY_OTP) != 0 { + return Err(CKR_MECHANISM_PARAM_INVALID)?; + } + } + CK_OTP_COUNTER => { + if p.ulValueLen != 8 || p.pValue.is_null() { + return Err(CKR_MECHANISM_PARAM_INVALID)?; + } + let c_slice = unsafe { + std::slice::from_raw_parts( + p.pValue as *const u8, + 8, + ) + }; + override_counter = Some(c_slice.to_vec()); + } + _ => return Err(CKR_MECHANISM_PARAM_INVALID)?, + } + } + } + } + + let req = key + .get_attr_as_ulong(CKA_OTP_COUNTER_REQUIREMENT) + .map_err(|_| CKR_GENERAL_ERROR)?; + + let c_vec = if let Some(oc) = override_counter { + if req == CK_OTP_PARAM_OPTIONAL || req == CK_OTP_PARAM_MANDATORY { + update = None; + oc + } else { + update = Some(false); + key.get_attr_as_bytes(CKA_OTP_COUNTER) + .map_err(|_| CKR_GENERAL_ERROR)? + .to_vec() + } + } else { + if req == CK_OTP_PARAM_MANDATORY { + return Err(CKR_MECHANISM_PARAM_INVALID)?; + } + update = Some(false); + key.get_attr_as_bytes(CKA_OTP_COUNTER) + .map_err(|_| CKR_GENERAL_ERROR)? + .to_vec() + }; + + if c_vec.len() != 8 { + return Err(CKR_GENERAL_ERROR)?; + } + let mut counter = [0u8; 8]; + counter.copy_from_slice(&c_vec); + + let length = key + .get_attr_as_ulong(CKA_OTP_LENGTH) + .map_err(|_| CKR_GENERAL_ERROR)? as usize; + + let key_val = key.get_attr_as_bytes(CKA_VALUE)?.clone(); + let (hmac_mech, mac_len) = match key_val.len() { + 20 => (CKM_SHA_1_HMAC, 20), + 32 => (CKM_SHA256_HMAC, 32), + 64 => (CKM_SHA512_HMAC, 64), + _ => return Err(CKR_GENERAL_ERROR)?, + }; + let hmac = HMACOperation::internal(hmac_mech, key_val, mac_len)?; + + Ok(HOTPOperation { + mech_type: mech.mechanism, + hmac, + counter, + key_handle: key.get_handle(), + length, + mac_len, + finalized: false, + update: update, + }) + } + + fn generate_otp( + &mut self, + counter: &[u8; 8], + output: &mut [u8], + ) -> Result<()> { + if output.len() != self.length { + return Err(CKR_GENERAL_ERROR)?; + } + + let mut mac = [0u8; HOTP_MAX_KEY_SIZE as usize]; + self.hmac.mac(counter, &mut mac[0..self.mac_len])?; + + let offset = (mac[self.mac_len - 1] & 0x0f) as usize; + mac[offset] &= 0x7f; + let value = u32::from_be_bytes(mac[offset..(offset + 4)].try_into()?); + let otp = value + % match self.length { + 6 => 1000000, + 7 => 10000000, + 8 => 100000000, + _ => return Err(CKR_GENERAL_ERROR)?, + }; + let otp_str = format!("{:0width$}", otp, width = self.length); + output.copy_from_slice(otp_str.as_bytes()); + Ok(()) + } +} + +impl MechOperation for HOTPOperation { + fn mechanism(&self) -> Result { + Ok(self.mech_type) + } + + fn finalized(&self) -> bool { + self.finalized + } +} + +impl Sign for HOTPOperation { + fn sign(&mut self, data: &[u8], signature: &mut [u8]) -> Result<()> { + if data.len() != 0 { + return Err(CKR_DATA_INVALID)?; + } + self.sign_final(signature) + } + + fn sign_update(&mut self, _data: &[u8]) -> Result<()> { + Err(CKR_DATA_INVALID)? + } + + fn sign_final(&mut self, signature: &mut [u8]) -> Result<()> { + if self.finalized { + return Err(CKR_OPERATION_NOT_INITIALIZED)?; + } + + let struct_size = std::mem::size_of::(); + let param_size = std::mem::size_of::(); + let total_size = struct_size + param_size + self.length; + + if signature.len() < total_size { + return Err(CKR_BUFFER_TOO_SMALL)?; + } + + self.finalized = true; + + let mut otp = vec![0u8; self.length]; + let counter = self.counter; + self.generate_otp(&counter, &mut otp)?; + + unsafe { + let sig_info = signature.as_mut_ptr() as *mut CK_OTP_SIGNATURE_INFO; + let p_params = + signature.as_mut_ptr().add(struct_size) as *mut CK_OTP_PARAM; + let p_value = signature.as_mut_ptr().add(struct_size + param_size); + + std::ptr::write_unaligned(&mut (*sig_info).pParams, p_params); + std::ptr::write_unaligned(&mut (*sig_info).ulCount, 1); + + std::ptr::write_unaligned(&mut (*p_params).type_, CK_OTP_VALUE); + std::ptr::write_unaligned( + &mut (*p_params).pValue, + p_value as *mut std::ffi::c_void, + ); + std::ptr::write_unaligned( + &mut (*p_params).ulValueLen, + self.length as CK_ULONG, + ); + + std::ptr::copy_nonoverlapping(otp.as_ptr(), p_value, self.length); + } + + if self.update.is_some() { + let mut c_val = u64::from_be_bytes(self.counter); + c_val += 1; + self.counter = c_val.to_be_bytes(); + self.update = Some(true); + } + + Ok(()) + } + + fn signature_len(&self) -> Result { + let struct_size = std::mem::size_of::(); + let param_size = std::mem::size_of::(); + Ok(struct_size + param_size + self.length) + } + + fn updates_object(&self) -> Option<(CK_OBJECT_HANDLE, CkAttrs<'_>)> { + if self.update == Some(true) { + let mut attrs = CkAttrs::new(); + if attrs + .add_owned_slice(CKA_OTP_COUNTER, &self.counter) + .is_ok() + { + return Some((self.key_handle, attrs)); + } + } + None + } +} + +impl Verify for HOTPOperation { + fn verify(&mut self, data: &[u8], signature: &[u8]) -> Result<()> { + if data.len() != 0 { + return Err(CKR_DATA_INVALID)?; + } + self.verify_final(signature) + } + + fn verify_update(&mut self, _data: &[u8]) -> Result<()> { + Err(CKR_DATA_INVALID)? + } + + fn verify_final(&mut self, signature: &[u8]) -> Result<()> { + if self.finalized { + return Err(CKR_OPERATION_NOT_INITIALIZED)?; + } + self.finalized = true; + + if signature.len() != self.length { + return Err(CKR_SIGNATURE_LEN_RANGE)?; + } + + let mut c_val = u64::from_be_bytes(self.counter); + for i in 0..5 { + if i > 0 { + self.hmac.reset()?; + } + let current_counter = c_val.to_be_bytes(); + let mut otp = vec![0u8; self.length]; + self.generate_otp(¤t_counter, &mut otp)?; + if signature == otp { + if self.update.is_some() { + self.counter = (c_val + 1).to_be_bytes(); + self.update = Some(true); + } + return Ok(()); + } + c_val += 1; + } + Err(CKR_SIGNATURE_INVALID)? + } + + fn signature_len(&self) -> Result { + Ok(self.length) + } + + fn updates_object(&self) -> Option<(CK_OBJECT_HANDLE, CkAttrs<'_>)> { + if self.update == Some(true) { + let mut attrs = CkAttrs::new(); + if attrs + .add_owned_slice(CKA_OTP_COUNTER, &self.counter) + .is_ok() + { + return Some((self.key_handle, attrs)); + } + } + None + } +} diff --git a/src/mechanism.rs b/src/mechanism.rs index 92542a1e..130eeb26 100644 --- a/src/mechanism.rs +++ b/src/mechanism.rs @@ -13,6 +13,7 @@ use std::collections::BTreeMap; use std::fmt::Debug; +use crate::attribute::CkAttrs; use crate::error::Result; use crate::object::{Object, ObjectFactories, ObjectFactory}; use crate::pkcs11::*; @@ -562,6 +563,14 @@ pub trait Sign: MechOperation { fn signature_len(&self) -> Result { Err(CKR_GENERAL_ERROR)? } + + /// Returns updated attributes for an object if the operation modifies its state. + /// + /// This can be used by mechanisms to request the update of object + /// attributes, such as an OTP counter, after an operation completes. + fn updates_object(&self) -> Option<(CK_OBJECT_HANDLE, CkAttrs<'_>)> { + None + } } pub trait Verify: MechOperation { @@ -600,6 +609,14 @@ pub trait Verify: MechOperation { fn signature_len(&self) -> Result { Err(CKR_GENERAL_ERROR)? } + + /// Returns updated attributes for an object if the operation modifies its state. + /// + /// This can be used by mechanisms to request the update of object + /// attributes, such as an OTP counter, after an operation completes. + fn updates_object(&self) -> Option<(CK_OBJECT_HANDLE, CkAttrs<'_>)> { + None + } } pub trait Derive: MechOperation { @@ -825,4 +842,12 @@ pub trait VerifySignature: MechOperation { fn verify_final(&mut self) -> Result<()> { Err(CKR_GENERAL_ERROR)? } + + /// Returns updated attributes for an object if the operation modifies its state. + /// + /// This can be used by mechanisms to request the update of object + /// attributes, such as an OTP counter, after an operation completes. + fn updates_object(&self) -> Option<(CK_OBJECT_HANDLE, CkAttrs<'_>)> { + None + } } diff --git a/src/object/factory.rs b/src/object/factory.rs index 84dd76dc..4d741462 100644 --- a/src/object/factory.rs +++ b/src/object/factory.rs @@ -563,13 +563,6 @@ pub trait ObjectFactory: Debug + Send + Sync { fn as_secret_key_factory(&self) -> Result<&dyn SecretKeyFactory> { Err(CKR_GENERAL_ERROR)? } - - /// Helper to access traits that are only available for objects of - /// class CKO_OTP_KEY. Other key type factories should not - /// implement this method. - fn as_otp_key_factory(&self) -> Result<&dyn OTPKeyFactory> { - Err(CKR_GENERAL_ERROR)? - } } /// This is a specialized factory for objects of class CKO_DATA diff --git a/src/tests/hotp.rs b/src/tests/hotp.rs new file mode 100644 index 00000000..e90ca396 --- /dev/null +++ b/src/tests/hotp.rs @@ -0,0 +1,150 @@ +// Copyright 2026 Simo Sorce +// See LICENSE.txt file for terms + +use crate::tests::*; + +use serial_test::parallel; + +fn generate_otp(session: CK_SESSION_HANDLE, key: CK_OBJECT_HANDLE) -> Vec { + let mech = CK_MECHANISM { + mechanism: CKM_HOTP, + pParameter: std::ptr::null_mut(), + ulParameterLen: 0, + }; + let otp_buf = + sig_gen(session, key, &[], &mech).expect("HOTP generation failed"); + + let sig_info = + unsafe { &*(otp_buf.as_ptr() as *const CK_OTP_SIGNATURE_INFO) }; + let p_params = sig_info.pParams; + let p_value = unsafe { (*p_params).pValue }; + let ul_value_len = unsafe { (*p_params).ulValueLen }; + unsafe { + std::slice::from_raw_parts(p_value as *const u8, ul_value_len as usize) + } + .to_vec() +} + +fn verify_otp( + session: CK_SESSION_HANDLE, + key: CK_OBJECT_HANDLE, + otp: &[u8], +) -> CK_RV { + let mech = CK_MECHANISM { + mechanism: CKM_HOTP, + pParameter: std::ptr::null_mut(), + ulParameterLen: 0, + }; + sig_verify(session, key, &[], otp, &mech) +} + +#[test] +#[parallel] +fn test_hotp() { + let mut testtokn = TestToken::initialized("test_hotp", None); + let session = testtokn.get_session(true); + + testtokn.login(); + + // 1. Generate HOTP key + let hotp_key_client = generate_key( + session, + CKM_HOTP_KEY_GEN, + std::ptr::null_mut(), + 0, + &[ + (CKA_CLASS, CKO_OTP_KEY), + (CKA_KEY_TYPE, CKK_HOTP), + (CKA_VALUE_LEN, 32), + ], + &[], + &[ + (CKA_EXTRACTABLE, true), + (CKA_SENSITIVE, false), + (CKA_SIGN, true), + (CKA_VERIFY, true), + ], + ) + .expect("HOTP key generation failed"); + + // 2. Export HOTP key + let exported_key = extract_key_value(session, hotp_key_client) + .expect("HOTP key extraction failed"); + + // 3. Generate some OTPs (tokens) with hotp_key_client + let otp1 = generate_otp(session, hotp_key_client); + let otp2 = generate_otp(session, hotp_key_client); + let otp3 = generate_otp(session, hotp_key_client); + let otp4 = generate_otp(session, hotp_key_client); // skipped for look-ahead + let otp5 = generate_otp(session, hotp_key_client); + + let mut otps = vec![&otp1, &otp2, &otp3, &otp4, &otp5]; + otps.sort(); + otps.dedup(); + assert_eq!( + otps.len(), + 5, + "All OTPs returned by the client key must be different" + ); + + // 4. Create a new token using the saved one + let hotp_key_server = import_object( + session, + CKO_OTP_KEY, + &[(CKA_KEY_TYPE, CKK_HOTP)], + &[(CKA_VALUE, &exported_key)], + &[(CKA_SIGN, true), (CKA_VERIFY, true)], + ) + .expect("Importing HOTP key failed"); + + // 6. Verify the tokens generated on the first one + let ret = verify_otp(session, hotp_key_server, &otp1); + assert_eq!(ret, CKR_OK, "Verification of OTP1 failed"); + + let ret = verify_otp(session, hotp_key_server, &otp2); + assert_eq!(ret, CKR_OK, "Verification of OTP2 failed"); + + // 7. Look-ahead verification (leaving gaps) + let ret = verify_otp(session, hotp_key_server, &otp5); + assert_eq!(ret, CKR_OK, "Look-ahead verification of OTP5 failed"); + + // Trying to reuse an older OTP should fail + let ret = verify_otp(session, hotp_key_server, &otp3); + assert_eq!(ret, CKR_SIGNATURE_INVALID, "Reused OTP should be invalid"); + + testtokn.finalize(); +} + +#[test] +#[parallel] +fn test_hotp_vectors() { + let mut testtokn = TestToken::initialized("test_hotp_vectors", None); + let session = testtokn.get_session(true); + + testtokn.login(); + + // Test vectors from RFC 4226, Appendix D + let secret = b"12345678901234567890"; + + let hotp_key = import_object( + session, + CKO_OTP_KEY, + &[(CKA_KEY_TYPE, CKK_HOTP)], + &[(CKA_VALUE, secret)], + &[(CKA_SIGN, true), (CKA_VERIFY, true)], + ) + .expect("Importing HOTP key failed"); + + let expected_otps = vec![ + "755224", "287082", "359152", "969429", "338314", "254676", "287922", + "162583", "399871", "520489", + ]; + + for expected in expected_otps { + let otp = generate_otp(session, hotp_key); + let otp_str = std::str::from_utf8(&otp).unwrap(); + assert_eq!(otp_str, expected); + } + + testtokn.finalize(); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 000779e3..5f4e5767 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -391,3 +391,6 @@ mod mldsa; mod slhdsa; mod pkcs11; + +#[cfg(feature = "hotp")] +mod hotp; diff --git a/src/tests/util.rs b/src/tests/util.rs index 0ecfd177..3d0d7343 100644 --- a/src/tests/util.rs +++ b/src/tests/util.rs @@ -522,11 +522,13 @@ pub fn generate_key( ) -> Result { let class = CKO_SECRET_KEY; let mut template = make_attr_template(ulongs, bytes, bools); - template.push(make_attribute!( - CKA_CLASS, - &class as *const _, - CK_ULONG_SIZE - )); + if !ulongs.iter().any(|&(t, _)| t == CKA_CLASS) { + template.push(make_attribute!( + CKA_CLASS, + &class as *const _, + CK_ULONG_SIZE + )); + } let mut mechanism: CK_MECHANISM = CK_MECHANISM { mechanism: mech, diff --git a/src/token.rs b/src/token.rs index a31e4564..8c7ce836 100644 --- a/src/token.rs +++ b/src/token.rs @@ -820,7 +820,7 @@ impl Token { pub fn set_object_attrs( &mut self, o_handle: CK_OBJECT_HANDLE, - template: &mut [CK_ATTRIBUTE], + template: &[CK_ATTRIBUTE], ) -> Result<()> { match self.session_objects.get_mut(&o_handle) { Some(mut obj) => self