From 7171f28421df6d7f0d33dc7453913afcff95fd78 Mon Sep 17 00:00:00 2001 From: Chris Frantz Date: Sat, 23 Nov 2024 14:37:53 -0800 Subject: [PATCH] [hsmtool] Add a SPX+ implementation for PKCS11 Elementary Files Create a SPHINCS+ implementation that uses key material from PKCS#11 Elementary Files (aka CKO_DATA objects). This implementation stores key material on a secure token, and loads the key material to perform the sphincs+ operations. This is not as secure as signing within an HSM security boundary, but it provides token-protected storage for key material when an HSM-based implementation is not available. Signed-off-by: Chris Frantz --- sw/host/hsmtool/BUILD | 5 +- sw/host/hsmtool/src/commands/mod.rs | 4 +- sw/host/hsmtool/src/error.rs | 4 + sw/host/hsmtool/src/hsmtool.rs | 24 +-- sw/host/hsmtool/src/lib.rs | 1 + sw/host/hsmtool/src/module.rs | 60 +++++- sw/host/hsmtool/src/spxef/mod.rs | 219 +++++++++++++++++++++ sw/host/hsmtool/src/util/attribute/data.rs | 7 + sw/host/hsmtool/src/util/ef.rs | 138 +++++++++++++ sw/host/hsmtool/src/util/mod.rs | 1 + sw/host/sphincsplus/BUILD | 1 + sw/host/sphincsplus/key.rs | 8 + 12 files changed, 452 insertions(+), 20 deletions(-) create mode 100644 sw/host/hsmtool/src/spxef/mod.rs create mode 100644 sw/host/hsmtool/src/util/ef.rs diff --git a/sw/host/hsmtool/BUILD b/sw/host/hsmtool/BUILD index 3dc55b331e678..8285793c5ad10 100644 --- a/sw/host/hsmtool/BUILD +++ b/sw/host/hsmtool/BUILD @@ -21,9 +21,9 @@ rust_library( "src/commands/object/list.rs", "src/commands/object/mod.rs", "src/commands/object/read.rs", - "src/commands/object/write.rs", "src/commands/object/show.rs", "src/commands/object/update.rs", + "src/commands/object/write.rs", "src/commands/rsa/decrypt.rs", "src/commands/rsa/encrypt.rs", "src/commands/rsa/export.rs", @@ -44,6 +44,7 @@ rust_library( "src/lib.rs", "src/module.rs", "src/profile.rs", + "src/spxef/mod.rs", "src/util/attribute/attr.rs", "src/util/attribute/attribute_type.rs", "src/util/attribute/certificate_type.rs", @@ -54,6 +55,7 @@ rust_library( "src/util/attribute/mechanism_type.rs", "src/util/attribute/mod.rs", "src/util/attribute/object_class.rs", + "src/util/ef.rs", "src/util/escape.rs", "src/util/helper.rs", "src/util/key/ecdsa.rs", @@ -91,6 +93,7 @@ rust_library( "@crate_index//:strum", "@crate_index//:thiserror", "@crate_index//:typetag", + "@crate_index//:zeroize", "@lowrisc_serde_annotate//serde_annotate", ], ) diff --git a/sw/host/hsmtool/src/commands/mod.rs b/sw/host/hsmtool/src/commands/mod.rs index 92690254f89e7..2cd4c9ffe04c7 100644 --- a/sw/host/hsmtool/src/commands/mod.rs +++ b/sw/host/hsmtool/src/commands/mod.rs @@ -84,7 +84,7 @@ impl Dispatch for Commands { } } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Annotate)] pub struct BasicResult { success: bool, #[serde(skip_serializing_if = "AttrData::is_none")] @@ -92,8 +92,10 @@ pub struct BasicResult { #[serde(skip_serializing_if = "AttrData::is_none")] label: AttrData, #[serde(skip_serializing_if = "Option::is_none")] + #[annotate(format = block)] value: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[annotate(format = block)] error: Option, } diff --git a/sw/host/hsmtool/src/error.rs b/sw/host/hsmtool/src/error.rs index 0162cde2984dc..066976ee7303f 100644 --- a/sw/host/hsmtool/src/error.rs +++ b/sw/host/hsmtool/src/error.rs @@ -36,4 +36,8 @@ pub enum HsmError { DerError(String), #[error("This operation requires the acorn library")] AcornUnavailable, + #[error("Parse error: {0}")] + ParseError(String), + #[error("Unknown application: {0}")] + UnknownApplication(String), } diff --git a/sw/host/hsmtool/src/hsmtool.rs b/sw/host/hsmtool/src/hsmtool.rs index 88de8199035b9..05f9517f9a7c1 100644 --- a/sw/host/hsmtool/src/hsmtool.rs +++ b/sw/host/hsmtool/src/hsmtool.rs @@ -10,7 +10,7 @@ use log::LevelFilter; use std::path::PathBuf; use hsmtool::commands::{print_command, print_result, Commands, Dispatch, Format}; -use hsmtool::module::{self, Module}; +use hsmtool::module::{self, Module, SpxModule}; use hsmtool::profile::Profile; use hsmtool::util::attribute::AttributeMap; @@ -47,9 +47,8 @@ struct Args { #[arg(long, env = "HSMTOOL_MODULE")] module: String, - /// Path to the `acorn` shared library. - #[arg(long, env = "HSMTOOL_ACORN")] - acorn: Option, + #[arg(long, env = "HSMTOOL_SPX_MODULE", help=SpxModule::HELP)] + spx_module: Option, /// HSM Token to use. #[arg(short, long, env = "HSMTOOL_TOKEN")] @@ -85,7 +84,7 @@ fn main() -> Result<()> { .transpose()?; // Initialize the HSM module interface. - let mut hsm = Module::initialize(&args.module, args.acorn.as_deref()).context( + let mut hsm = Module::initialize(&args.module).context( "Loading the PKCS11 module usually depends on several environent variables. Check HSMTOOL_MODULE, SOFTHSM2_CONF or your HSM's documentation.")?; // Initialize the list of all valid attribute types early. Disable logging @@ -99,18 +98,19 @@ fn main() -> Result<()> { return print_command(args.format, args.color, args.command.leaf()); } - let session = if let Some(profile) = &args.profile { + if let Some(profile) = &args.profile { let profiles = Profile::load(&args.profiles)?; let profile = profiles .get(profile) .ok_or_else(|| anyhow!("Profile {profile:?} not found."))?; - Some(hsm.connect(&profile.token, Some(profile.user), profile.pin.as_deref())?) + hsm.connect(&profile.token, Some(profile.user), profile.pin.as_deref())?; } else if let Some(token) = &args.token { - Some(hsm.connect(token, args.user, args.pin.as_deref())?) - } else { - None - }; + hsm.connect(token, args.user, args.pin.as_deref())?; + } + if let Some(spx_module) = &args.spx_module { + hsm.initialize_spx(spx_module)?; + } - let result = args.command.run(&(), &hsm, session.as_ref()); + let result = args.command.run(&(), &hsm, hsm.get_session()); print_result(args.format, args.color, args.quiet, result) } diff --git a/sw/host/hsmtool/src/lib.rs b/sw/host/hsmtool/src/lib.rs index ff0134171ffac..0e3e082cd39bf 100644 --- a/sw/host/hsmtool/src/lib.rs +++ b/sw/host/hsmtool/src/lib.rs @@ -7,4 +7,5 @@ pub mod commands; pub mod error; pub mod module; pub mod profile; +pub mod spxef; pub mod util; diff --git a/sw/host/hsmtool/src/module.rs b/sw/host/hsmtool/src/module.rs index 659d9a1dd9253..fc63670daa5b9 100644 --- a/sw/host/hsmtool/src/module.rs +++ b/sw/host/hsmtool/src/module.rs @@ -10,29 +10,76 @@ use cryptoki::session::UserType; use cryptoki::slot::Slot; use cryptoki::types::AuthPin; use serde::de::{Deserialize, Deserializer}; +use std::rc::Rc; +use std::str::FromStr; use crate::error::HsmError; +use crate::spxef::SpxEf; use acorn::{Acorn, SpxInterface}; +#[derive(Debug, Clone)] +pub enum SpxModule { + Acorn(String), + Pkcs11Ef, +} + +impl SpxModule { + pub const HELP: &'static str = + "Type of sphincs+ module [allowed values: acorn:, pkcs11-ef]"; +} + +impl FromStr for SpxModule { + type Err = HsmError; + fn from_str(s: &str) -> Result { + if s[..6].eq_ignore_ascii_case("acorn:") { + Ok(SpxModule::Acorn(s[6..].into())) + } else if s.eq_ignore_ascii_case("pkcs11-ef") { + Ok(SpxModule::Pkcs11Ef) + } else { + Err(HsmError::ParseError(format!("unknown SpxModule {s:?}"))) + } + } +} + pub struct Module { pub pkcs11: Pkcs11, + pub session: Option>, pub acorn: Option>, pub token: Option, } impl Module { - pub fn initialize(module: &str, acorn: Option<&str>) -> Result { + pub fn initialize(module: &str) -> Result { let pkcs11 = Pkcs11::new(module)?; pkcs11.initialize(CInitializeArgs::OsThreads)?; - let acorn = acorn.map(Acorn::new).transpose()?; - let acorn = acorn.map(|a| a as Box); Ok(Module { pkcs11, - acorn, + session: None, + acorn: None, token: None, }) } + pub fn initialize_spx(&mut self, module: &SpxModule) -> Result<()> { + let module = match module { + SpxModule::Acorn(libpath) => Acorn::new(libpath)? as Box, + SpxModule::Pkcs11Ef => { + let session = self + .session + .as_ref() + .map(Rc::clone) + .ok_or(HsmError::SessionRequired)?; + SpxEf::new(session) as Box + } + }; + self.acorn = Some(module); + Ok(()) + } + + pub fn get_session(&self) -> Option<&Session> { + self.session.as_ref().map(Rc::as_ref) + } + pub fn get_token(&self, label: &str) -> Result { let slots = self.pkcs11.get_slots_with_token()?; for slot in slots { @@ -49,7 +96,7 @@ impl Module { token: &str, user: Option, pin: Option<&str>, - ) -> Result { + ) -> Result<()> { let slot = self.get_token(token)?; let session = self.pkcs11.open_rw_session(slot)?; if let Some(user) = user { @@ -59,7 +106,8 @@ impl Module { .context("Failed HSM Login")?; } self.token = Some(token.into()); - Ok(session) + self.session = Some(Rc::new(session)); + Ok(()) } } diff --git a/sw/host/hsmtool/src/spxef/mod.rs b/sw/host/hsmtool/src/spxef/mod.rs new file mode 100644 index 0000000000000..d64c1fd3389d4 --- /dev/null +++ b/sw/host/hsmtool/src/spxef/mod.rs @@ -0,0 +1,219 @@ +// Copyright lowRISC contributors (OpenTitan project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use acorn::{GenerateFlags, KeyEntry, KeyInfo, SpxInterface}; +use anyhow::Result; +use cryptoki::session::Session; +use sphincsplus::{DecodeKey, EncodeKey}; +use sphincsplus::{SphincsPlus, SpxDomain, SpxError, SpxPublicKey, SpxSecretKey}; +use std::rc::Rc; +use std::str::FromStr; +use zeroize::Zeroizing; + +use crate::error::HsmError; +use crate::util::attribute::{AttrData, AttributeMap, AttributeType}; +use crate::util::ef::ElementaryFile; + +/// SpxEf implements host-based SPHINCS+ signing with elementary files stored +/// on a PKCS#11 token. +/// +/// This is not as secure as signing on an HSM, but allows secure storage of +/// the key material on a token. Every effort is made to destroy secret key +/// material loaded to host RAM after use to prevent unintentional leaking of +/// keys. +pub struct SpxEf { + session: Rc, +} + +impl SpxEf { + const APPLICATION: &'static str = "hsmtool-spx"; + + pub fn new(session: Rc) -> Box { + Box::new(Self { session }) + } + + fn load_key(&self, alias: &str) -> Result { + let mut search = AttributeMap::default(); + search.insert(AttributeType::Label, AttrData::Str(alias.into())); + let mut ef = ElementaryFile::find(&self.session, search)?; + if ef.is_empty() { + return Err(HsmError::ObjectNotFound(alias.into()).into()); + } else if ef.len() > 1 { + return Err(HsmError::TooManyObjects(ef.len(), alias.into()).into()); + } + let ef = ef.remove(0); + if let Some(app) = &ef.application { + match app.split_once(':') { + Some((Self::APPLICATION, _algo)) => { + let data = Zeroizing::new(String::from_utf8(ef.read(&self.session)?)?); + return Ok(SpxSecretKey::from_pem(data.as_str())?); + } + Some((_, _)) | None => { + return Err(HsmError::UnknownApplication(app.into()).into()); + } + } + } + Err(HsmError::UnknownApplication("".into()).into()) + } +} + +impl SpxInterface for SpxEf { + /// Get the version of the backend. + fn get_version(&self) -> Result { + Ok(String::from("PKCS#11 ElementaryFiles 0.0.1")) + } + + /// List keys known to the backend. + fn list_keys(&self) -> Result> { + let mut result = Vec::new(); + for file in ElementaryFile::list(&self.session)? { + if let Some(app) = file.application { + match app.split_once(':') { + Some((Self::APPLICATION, algo)) => { + result.push(KeyEntry { + alias: file.name.clone(), + hash: None, + algorithm: algo.into(), + ..Default::default() + }); + } + Some((_, _)) | None => {} + } + } + } + Ok(result) + } + + /// Get the public key info. + fn get_key_info(&self, alias: &str) -> Result { + let sk = self.load_key(alias)?; + let pk = SpxPublicKey::from(&sk); + + Ok(KeyInfo { + hash: "".into(), + algorithm: pk.algorithm().to_string(), + public_key: pk.as_bytes().to_vec(), + private_blob: Vec::new(), + }) + } + + /// Generate a key pair. + fn generate_key( + &self, + alias: &str, + algorithm: &str, + _token: &str, + flags: GenerateFlags, + ) -> Result { + let mut search = AttributeMap::default(); + search.insert(AttributeType::Label, AttrData::Str(alias.into())); + let ef = ElementaryFile::find(&self.session, search)?; + if flags.contains(GenerateFlags::OVERWRITE) { + if ef.len() <= 1 { + // delete files + } else { + return Err(HsmError::TooManyObjects(ef.len(), alias.into()).into()); + } + } else if !ef.is_empty() { + return Err(HsmError::ObjectExists("".into(), alias.into()).into()); + } + + let (sk, _) = SpxSecretKey::new_keypair(SphincsPlus::from_str(algorithm)?)?; + let app = format!("{}:{}", Self::APPLICATION, sk.algorithm()); + let skf = ElementaryFile::new(alias.into()) + .application(app) + .private(true); + let encoded = Zeroizing::new(sk.to_pem()?); + skf.write(&self.session, encoded.as_bytes())?; + + let private_key = if flags.contains(GenerateFlags::EXPORT_PRIVATE) { + sk.as_bytes().to_vec() + } else { + Vec::new() + }; + + Ok(KeyEntry { + alias: alias.into(), + hash: Some("".into()), + algorithm: sk.algorithm().to_string(), + private_blob: Vec::new(), + private_key, + }) + } + + /// Import a key pair. + fn import_keypair( + &self, + alias: &str, + algorithm: &str, + _token: &str, + overwrite: bool, + public_key: &[u8], + private_key: &[u8], + ) -> Result { + let mut search = AttributeMap::default(); + search.insert(AttributeType::Label, AttrData::Str(alias.into())); + let ef = ElementaryFile::find(&self.session, search)?; + if overwrite { + if ef.len() <= 1 { + // delete files + } else { + return Err(HsmError::TooManyObjects(ef.len(), alias.into()).into()); + } + } else if !ef.is_empty() { + return Err(HsmError::ObjectExists("".into(), alias.into()).into()); + } + + let sk = SpxSecretKey::from_bytes(SphincsPlus::from_str(algorithm)?, private_key)?; + let pk = SpxPublicKey::from(&sk); + if public_key != pk.as_bytes() { + return Err(HsmError::KeyError("secret/public key mismatch".into()).into()); + } + let app = format!("{}:{}", Self::APPLICATION, sk.algorithm()); + let skf = ElementaryFile::new(alias.into()) + .application(app) + .private(true); + let encoded = Zeroizing::new(sk.to_pem()?); + skf.write(&self.session, encoded.as_bytes())?; + + Ok(KeyEntry { + alias: alias.into(), + hash: None, + algorithm: sk.algorithm().to_string(), + private_blob: Vec::new(), + private_key: Vec::new(), + }) + } + + /// Sign a message. + fn sign(&self, alias: Option<&str>, key_hash: Option<&str>, message: &[u8]) -> Result> { + let alias = alias.ok_or(HsmError::NoSearchCriteria)?; + if key_hash.is_some() { + log::warn!("ignored key_hash {key_hash:?}"); + } + let sk = self.load_key(alias)?; + Ok(sk.sign(SpxDomain::None, message)?) + } + + /// Verify a message. + fn verify( + &self, + alias: Option<&str>, + key_hash: Option<&str>, + message: &[u8], + signature: &[u8], + ) -> Result { + let alias = alias.ok_or(HsmError::NoSearchCriteria)?; + if key_hash.is_some() { + log::warn!("ignored key_hash {key_hash:?}"); + } + let sk = self.load_key(alias)?; + let pk = SpxPublicKey::from(&sk); + match pk.verify(SpxDomain::None, signature, message) { + Ok(()) => Ok(true), + Err(SpxError::BadSignature) => Ok(false), + Err(e) => Err(e.into()), + } + } +} diff --git a/sw/host/hsmtool/src/util/attribute/data.rs b/sw/host/hsmtool/src/util/attribute/data.rs index 9b58c4a499c2a..89f47fe029062 100644 --- a/sw/host/hsmtool/src/util/attribute/data.rs +++ b/sw/host/hsmtool/src/util/attribute/data.rs @@ -138,6 +138,13 @@ impl AttrData { _ => Err(AttributeError::InvalidDataType), } } + + pub fn try_string(&self) -> Result { + match self { + AttrData::Str(v) => Ok(v.clone()), + _ => Err(AttributeError::InvalidDataType), + } + } } #[cfg(test)] diff --git a/sw/host/hsmtool/src/util/ef.rs b/sw/host/hsmtool/src/util/ef.rs new file mode 100644 index 0000000000000..b1d046d019970 --- /dev/null +++ b/sw/host/hsmtool/src/util/ef.rs @@ -0,0 +1,138 @@ +// Copyright lowRISC contributors (OpenTitan project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Result; +use cryptoki::session::Session; + +use crate::util::attribute::{AttrData, AttributeError, AttributeMap, AttributeType, ObjectClass}; +use crate::util::helper; + +#[derive(Clone, Debug, Default)] +pub struct ElementaryFile { + pub name: String, + pub application: Option, + pub private: bool, +} + +impl ElementaryFile { + pub fn new(name: String) -> Self { + Self { + name, + ..Default::default() + } + } + + pub fn application(mut self, app: String) -> Self { + self.application = Some(app); + self + } + + pub fn private(mut self, private: bool) -> Self { + self.private = private; + self + } + + pub fn find(session: &Session, search: AttributeMap) -> Result> { + let mut search = search; + search.insert( + AttributeType::Class, + AttrData::ObjectClass(ObjectClass::Data), + ); + let search = search.to_vec()?; + let attr = [ + AttributeType::Label, + AttributeType::Application, + AttributeType::Private, + ]; + let attr = attr + .iter() + .map(|&a| Ok(a.try_into()?)) + .collect::>>()?; + + let mut result = Vec::new(); + for object in session.find_objects(&search)? { + let data = session.get_attributes(object, &attr)?; + let data = AttributeMap::from(data.as_slice()); + result.push(Self { + name: data + .get(&AttributeType::Label) + .map(|x| x.try_string()) + .transpose()? + .unwrap_or_else(|| String::from("")), + application: data + .get(&AttributeType::Application) + .map(|x| x.try_string()) + .transpose()?, + private: data + .get(&AttributeType::Private) + .map(|x| x.try_into()) + .transpose()? + .unwrap_or(false), + }); + } + Ok(result) + } + + pub fn list(session: &Session) -> Result> { + Self::find(session, AttributeMap::default()) + } + + pub fn exists(self, session: &Session) -> Result { + let mut attr = AttributeMap::default(); + attr.insert( + AttributeType::Class, + AttrData::ObjectClass(ObjectClass::Data), + ); + attr.insert(AttributeType::Label, AttrData::Str(self.name.clone())); + if let Some(app) = &self.application { + attr.insert(AttributeType::Application, AttrData::Str(app.clone())); + } + let attr = attr.to_vec()?; + let objects = session.find_objects(&attr)?; + Ok(!objects.is_empty()) + } + + pub fn read(self, session: &Session) -> Result> { + let mut attr = AttributeMap::default(); + attr.insert( + AttributeType::Class, + AttrData::ObjectClass(ObjectClass::Data), + ); + attr.insert(AttributeType::Label, AttrData::Str(self.name.clone())); + if let Some(app) = &self.application { + attr.insert(AttributeType::Application, AttrData::Str(app.clone())); + } + let attr = attr.to_vec()?; + + let object = helper::find_one_object(session, &attr)?; + let data = AttributeMap::from_object(session, object)?; + let value = data + .get(&AttributeType::Value) + .ok_or(AttributeError::AttributeNotFound(AttributeType::Value))?; + let value = Vec::::try_from(value)?; + Ok(value) + } + + pub fn write(self, session: &Session, data: &[u8]) -> Result<()> { + let mut attr = AttributeMap::default(); + attr.insert( + AttributeType::Class, + AttrData::ObjectClass(ObjectClass::Data), + ); + attr.insert(AttributeType::Label, AttrData::Str(self.name.clone())); + if let Some(application) = &self.application { + // Is this a bug in opensc-pkcs11 or in the Nitrokey? + // It seems the application string needs a nul terminator. + let mut val = application.clone(); + val.push(0 as char); + attr.insert(AttributeType::Application, AttrData::Str(val)); + } + attr.insert(AttributeType::Token, AttrData::from(true)); + attr.insert(AttributeType::Private, AttrData::from(self.private)); + attr.insert(AttributeType::Value, AttrData::from(data)); + let attr = attr.to_vec()?; + session.create_object(&attr)?; + Ok(()) + } +} diff --git a/sw/host/hsmtool/src/util/mod.rs b/sw/host/hsmtool/src/util/mod.rs index c84cc4c72270e..9ef525f64df30 100644 --- a/sw/host/hsmtool/src/util/mod.rs +++ b/sw/host/hsmtool/src/util/mod.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod attribute; +pub mod ef; pub mod escape; pub mod helper; pub mod key; diff --git a/sw/host/sphincsplus/BUILD b/sw/host/sphincsplus/BUILD index aba44de7ea228..5ec209a36806f 100644 --- a/sw/host/sphincsplus/BUILD +++ b/sw/host/sphincsplus/BUILD @@ -64,6 +64,7 @@ rust_library( "@crate_index//:serde", "@crate_index//:strum", "@crate_index//:thiserror", + "@crate_index//:zeroize", ], ) diff --git a/sw/host/sphincsplus/key.rs b/sw/host/sphincsplus/key.rs index 900c156821e3a..8a8463ea191e8 100644 --- a/sw/host/sphincsplus/key.rs +++ b/sw/host/sphincsplus/key.rs @@ -8,6 +8,7 @@ use std::borrow::Cow; use std::fmt; use std::str::FromStr; use strum::{Display, EnumString}; +use zeroize::Zeroize; #[derive(Clone, PartialEq, Eq, Debug)] pub struct SpxPublicKey { @@ -104,6 +105,13 @@ impl SpxSecretKey { } } +impl Drop for SpxSecretKey { + fn drop(&mut self) { + // Destroy the secret key value upon drop. + self.key.zeroize(); + } +} + impl DecodeKey for SpxSecretKey { /// Decodes a SPHINCS+ secret key from a PEM encoded string. fn from_pem(s: &str) -> Result {