diff --git a/sw/host/hsmtool/BUILD b/sw/host/hsmtool/BUILD index 15805127339a27..306722f59abfb9 100644 --- a/sw/host/hsmtool/BUILD +++ b/sw/host/hsmtool/BUILD @@ -30,6 +30,13 @@ rust_library( "src/commands/rsa/mod.rs", "src/commands/rsa/sign.rs", "src/commands/rsa/verify.rs", + "src/commands/spx/export.rs", + "src/commands/spx/generate.rs", + "src/commands/spx/import.rs", + "src/commands/spx/list.rs", + "src/commands/spx/mod.rs", + "src/commands/spx/sign.rs", + "src/commands/spx/verify.rs", "src/commands/token.rs", "src/error.rs", "src/lib.rs", @@ -55,6 +62,8 @@ rust_library( ], crate_name = "hsmtool", deps = [ + "//sw/host/hsmtool/acorn", + "//sw/host/sphincsplus", "@crate_index//:anyhow", "@crate_index//:clap", "@crate_index//:cryptoki", @@ -68,6 +77,7 @@ rust_library( "@crate_index//:num_enum", "@crate_index//:once_cell", "@crate_index//:p256", + "@crate_index//:pem-rfc7468", "@crate_index//:rand", "@crate_index//:regex", "@crate_index//:rsa", diff --git a/sw/host/hsmtool/src/commands/mod.rs b/sw/host/hsmtool/src/commands/mod.rs index 536c56f43fbf07..f98b822dd3033d 100644 --- a/sw/host/hsmtool/src/commands/mod.rs +++ b/sw/host/hsmtool/src/commands/mod.rs @@ -16,6 +16,7 @@ mod ecdsa; mod exec; mod object; mod rsa; +mod spx; mod token; #[typetag::serde(tag = "command")] @@ -45,6 +46,8 @@ pub enum Commands { #[command(subcommand)] Rsa(rsa::Rsa), #[command(subcommand)] + Spx(spx::Spx), + #[command(subcommand)] Token(token::Token), } @@ -60,6 +63,7 @@ impl Dispatch for Commands { Commands::Ecdsa(x) => x.run(context, hsm, session), Commands::Exec(x) => x.run(context, hsm, session), Commands::Object(x) => x.run(context, hsm, session), + Commands::Spx(x) => x.run(context, hsm, session), Commands::Rsa(x) => x.run(context, hsm, session), Commands::Token(x) => x.run(context, hsm, session), } @@ -73,6 +77,7 @@ impl Dispatch for Commands { Commands::Ecdsa(x) => x.leaf(), Commands::Exec(x) => x.leaf(), Commands::Object(x) => x.leaf(), + Commands::Spx(x) => x.leaf(), Commands::Rsa(x) => x.leaf(), Commands::Token(x) => x.leaf(), } diff --git a/sw/host/hsmtool/src/commands/spx/export.rs b/sw/host/hsmtool/src/commands/spx/export.rs new file mode 100644 index 00000000000000..67d7a0b669ad9c --- /dev/null +++ b/sw/host/hsmtool/src/commands/spx/export.rs @@ -0,0 +1,49 @@ +// 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 serde::{Deserialize, Serialize}; +use serde_annotate::Annotate; +use std::any::Any; +use std::path::PathBuf; +use std::str::FromStr; + +use crate::commands::{BasicResult, Dispatch}; +use crate::error::HsmError; +use crate::module::Module; +use acorn::Acorn; +use sphincsplus::{EncodeKey, SphincsPlus, SpxPublicKey}; + +#[derive(clap::Args, Debug, Serialize, Deserialize)] +pub struct Export { + #[arg(short, long)] + label: String, + filename: PathBuf, +} + +impl Export { + fn export(&self, acorn: &Acorn) -> Result<()> { + let key = acorn.get_key_info(&self.label)?; + let algorithm = SphincsPlus::from_str(&key.algorithm)?; + let pk = SpxPublicKey::from_bytes(algorithm, &key.public_key)?; + pk.write_pem_file(&self.filename)?; + Ok(()) + } +} + +#[typetag::serde(name = "spx-export")] +impl Dispatch for Export { + fn run( + &self, + _context: &dyn Any, + hsm: &Module, + _session: Option<&Session>, + ) -> Result> { + let acorn = hsm.acorn.as_ref().ok_or(HsmError::AcornUnavailable)?; + let _token = hsm.token.as_deref().ok_or(HsmError::SessionRequired)?; + self.export(acorn)?; + Ok(Box::::default()) + } +} diff --git a/sw/host/hsmtool/src/commands/spx/generate.rs b/sw/host/hsmtool/src/commands/spx/generate.rs new file mode 100644 index 00000000000000..90b92d955066af --- /dev/null +++ b/sw/host/hsmtool/src/commands/spx/generate.rs @@ -0,0 +1,61 @@ +// 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 serde::{Deserialize, Serialize}; +use serde_annotate::Annotate; +use std::any::Any; +use std::path::PathBuf; + +use crate::commands::{BasicResult, Dispatch}; +use crate::error::HsmError; +use crate::module::Module; +use crate::util::attribute::AttrData; +use acorn::GenerateFlags; +use sphincsplus::{EncodeKey, SphincsPlus, SpxSecretKey}; + +#[derive(clap::Args, Debug, Serialize, Deserialize)] +pub struct Generate { + #[arg(short, long)] + label: String, + #[arg(short, long, default_value = "SHA2-128s-simple")] + algorithm: SphincsPlus, + #[arg(short, long, help = "Overwrite an existing key with the same label")] + overwrite: bool, + #[arg(short, long, help = "Export the private key material to a file")] + export: Option, +} + +#[typetag::serde(name = "spx-generate")] +impl Dispatch for Generate { + fn run( + &self, + _context: &dyn Any, + hsm: &Module, + _session: Option<&Session>, + ) -> Result> { + let acorn = hsm.acorn.as_ref().ok_or(HsmError::AcornUnavailable)?; + let token = hsm.token.as_deref().ok_or(HsmError::SessionRequired)?; + + #[rustfmt::skip] + let flags = + if self.overwrite { GenerateFlags::OVERWRITE } else { GenerateFlags::NONE } + | if self.export.is_some() { GenerateFlags::EXPORT_PRIVATE } else { GenerateFlags::NONE }; + + let key = acorn.generate_key(&self.label, &self.algorithm.to_string(), token, flags)?; + + if let Some(path) = &self.export { + let sk = SpxSecretKey::from_bytes(self.algorithm, &key.private_key)?; + sk.write_pem_file(path)?; + } + + Ok(Box::new(BasicResult { + success: true, + id: AttrData::Str(key.hash.expect("key hash")), + label: AttrData::Str(key.alias), + error: None, + })) + } +} diff --git a/sw/host/hsmtool/src/commands/spx/import.rs b/sw/host/hsmtool/src/commands/spx/import.rs new file mode 100644 index 00000000000000..b32630c66367a5 --- /dev/null +++ b/sw/host/hsmtool/src/commands/spx/import.rs @@ -0,0 +1,56 @@ +// 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 serde::{Deserialize, Serialize}; +use serde_annotate::Annotate; +use std::any::Any; +use std::path::PathBuf; + +use crate::commands::{BasicResult, Dispatch}; +use crate::error::HsmError; +use crate::module::Module; +use crate::util::attribute::AttrData; +use sphincsplus::{DecodeKey, SpxPublicKey, SpxSecretKey}; + +#[derive(clap::Args, Debug, Serialize, Deserialize)] +pub struct Import { + #[arg(short, long)] + label: String, + #[arg(short, long, help = "Overwrite an existing key with the same label")] + overwrite: bool, + filename: PathBuf, +} + +#[typetag::serde(name = "spx-import")] +impl Dispatch for Import { + fn run( + &self, + _context: &dyn Any, + hsm: &Module, + _session: Option<&Session>, + ) -> Result> { + let acorn = hsm.acorn.as_ref().ok_or(HsmError::AcornUnavailable)?; + let token = hsm.token.as_deref().ok_or(HsmError::SessionRequired)?; + + let sk = SpxSecretKey::read_pem_file(&self.filename)?; + let pk = SpxPublicKey::from(&sk); + + let key = acorn.import_keypair( + &self.label, + &sk.algorithm().to_string(), + token, + self.overwrite, + pk.as_bytes(), + sk.as_bytes(), + )?; + Ok(Box::new(BasicResult { + success: true, + id: AttrData::Str(key.hash.expect("key hash")), + label: AttrData::Str(key.alias), + error: None, + })) + } +} diff --git a/sw/host/hsmtool/src/commands/spx/list.rs b/sw/host/hsmtool/src/commands/spx/list.rs new file mode 100644 index 00000000000000..d13c3f68adced0 --- /dev/null +++ b/sw/host/hsmtool/src/commands/spx/list.rs @@ -0,0 +1,59 @@ +// 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 serde::{Deserialize, Serialize}; +use serde_annotate::Annotate; +use std::any::Any; + +use crate::commands::Dispatch; +use crate::error::HsmError; +use crate::module::Module; + +#[derive(clap::Args, Debug, Serialize, Deserialize)] +pub struct List {} + +#[derive(Default, Debug, Serialize)] +pub struct Key { + pub id: String, + pub label: String, + pub algorithm: String, +} + +#[derive(Default, Debug, Serialize)] +pub struct ListResult { + host_version: String, + see_version: String, + objects: Vec, +} + +#[typetag::serde(name = "spx-list")] +impl Dispatch for List { + fn run( + &self, + _context: &dyn Any, + hsm: &Module, + _session: Option<&Session>, + ) -> Result> { + let acorn = hsm.acorn.as_ref().ok_or(HsmError::AcornUnavailable)?; + let _token = hsm.token.as_deref().ok_or(HsmError::SessionRequired)?; + + let mut result = Box::new(ListResult { + host_version: acorn.get_version()?, + see_version: acorn.get_see_version()?, + ..Default::default() + }); + let keys = acorn.list_keys()?; + for key in keys { + let info = acorn.get_key_info(&key.alias)?; + result.objects.push(Key { + id: info.hash, + label: key.alias, + algorithm: info.algorithm, + }); + } + Ok(result) + } +} diff --git a/sw/host/hsmtool/src/commands/spx/mod.rs b/sw/host/hsmtool/src/commands/spx/mod.rs new file mode 100644 index 00000000000000..d048b10c013ec3 --- /dev/null +++ b/sw/host/hsmtool/src/commands/spx/mod.rs @@ -0,0 +1,61 @@ +// 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 serde::{Deserialize, Serialize}; +use serde_annotate::Annotate; +use std::any::Any; + +use crate::commands::Dispatch; +use crate::module::Module; + +pub mod export; +pub mod generate; +pub mod import; +pub mod list; +pub mod sign; +pub mod verify; + +#[derive(clap::Subcommand, Debug, Serialize, Deserialize)] +pub enum Spx { + Generate(generate::Generate), + Export(export::Export), + Import(import::Import), + List(list::List), + Sign(sign::Sign), + Verify(verify::Verify), +} + +#[typetag::serde(name = "__spx__")] +impl Dispatch for Spx { + fn run( + &self, + context: &dyn Any, + hsm: &Module, + session: Option<&Session>, + ) -> Result> { + match self { + Spx::Generate(x) => x.run(context, hsm, session), + Spx::Export(x) => x.run(context, hsm, session), + Spx::Import(x) => x.run(context, hsm, session), + Spx::List(x) => x.run(context, hsm, session), + Spx::Sign(x) => x.run(context, hsm, session), + Spx::Verify(x) => x.run(context, hsm, session), + } + } + fn leaf(&self) -> &dyn Dispatch + where + Self: Sized, + { + match self { + Spx::Generate(x) => x.leaf(), + Spx::Export(x) => x.leaf(), + Spx::Import(x) => x.leaf(), + Spx::List(x) => x.leaf(), + Spx::Sign(x) => x.leaf(), + Spx::Verify(x) => x.leaf(), + } + } +} diff --git a/sw/host/hsmtool/src/commands/spx/sign.rs b/sw/host/hsmtool/src/commands/spx/sign.rs new file mode 100644 index 00000000000000..7a1dc0b024f7c1 --- /dev/null +++ b/sw/host/hsmtool/src/commands/spx/sign.rs @@ -0,0 +1,44 @@ +// 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 serde::{Deserialize, Serialize}; +use serde_annotate::Annotate; +use std::any::Any; +use std::path::PathBuf; + +use crate::commands::{BasicResult, Dispatch}; +use crate::error::HsmError; +use crate::module::Module; +use crate::util::helper; + +#[derive(clap::Args, Debug, Serialize, Deserialize)] +pub struct Sign { + #[arg(long)] + id: Option, + #[arg(short, long)] + label: Option, + #[arg(short, long)] + output: PathBuf, + input: PathBuf, +} + +#[typetag::serde(name = "spx-sign")] +impl Dispatch for Sign { + fn run( + &self, + _context: &dyn Any, + hsm: &Module, + _session: Option<&Session>, + ) -> Result> { + let acorn = hsm.acorn.as_ref().ok_or(HsmError::AcornUnavailable)?; + let _token = hsm.token.as_deref().ok_or(HsmError::SessionRequired)?; + + let data = helper::read_file(&self.input)?; + let result = acorn.sign(self.label.as_deref(), self.id.as_deref(), &data)?; + helper::write_file(&self.output, &result)?; + Ok(Box::::default()) + } +} diff --git a/sw/host/hsmtool/src/commands/spx/verify.rs b/sw/host/hsmtool/src/commands/spx/verify.rs new file mode 100644 index 00000000000000..e7d9b60f12677b --- /dev/null +++ b/sw/host/hsmtool/src/commands/spx/verify.rs @@ -0,0 +1,51 @@ +// 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 serde::{Deserialize, Serialize}; +use serde_annotate::Annotate; +use std::any::Any; +use std::path::PathBuf; + +use crate::commands::{BasicResult, Dispatch}; +use crate::error::HsmError; +use crate::module::Module; +use crate::util::helper; + +#[derive(clap::Args, Debug, Serialize, Deserialize)] +pub struct Verify { + #[arg(long)] + id: Option, + #[arg(short, long)] + label: Option, + input: PathBuf, + signature: PathBuf, +} + +#[typetag::serde(name = "rsa-verify")] +impl Dispatch for Verify { + fn run( + &self, + _context: &dyn Any, + hsm: &Module, + _session: Option<&Session>, + ) -> Result> { + let acorn = hsm.acorn.as_ref().ok_or(HsmError::AcornUnavailable)?; + let _token = hsm.token.as_deref().ok_or(HsmError::SessionRequired)?; + + let data = helper::read_file(&self.input)?; + let signature = helper::read_file(&self.signature)?; + let result = acorn.verify(self.label.as_deref(), self.id.as_deref(), &data, &signature)?; + Ok(Box::new(BasicResult { + success: result, + error: if result { + None + } else { + Some("SPX Verify Failed".into()) + }, + ..Default::default() + })) + } +} diff --git a/sw/host/hsmtool/src/error.rs b/sw/host/hsmtool/src/error.rs index ab10e76a04e324..0162cde2984dc1 100644 --- a/sw/host/hsmtool/src/error.rs +++ b/sw/host/hsmtool/src/error.rs @@ -34,4 +34,6 @@ pub enum HsmError { FilePermissionError(u32), #[error("DER error: {0}")] DerError(String), + #[error("This operation requires the acorn library")] + AcornUnavailable, } diff --git a/sw/host/hsmtool/src/hsmtool.rs b/sw/host/hsmtool/src/hsmtool.rs index eb22b949c33d16..88de8199035b94 100644 --- a/sw/host/hsmtool/src/hsmtool.rs +++ b/sw/host/hsmtool/src/hsmtool.rs @@ -47,6 +47,10 @@ struct Args { #[arg(long, env = "HSMTOOL_MODULE")] module: String, + /// Path to the `acorn` shared library. + #[arg(long, env = "HSMTOOL_ACORN")] + acorn: Option, + /// HSM Token to use. #[arg(short, long, env = "HSMTOOL_TOKEN")] token: Option, @@ -79,7 +83,9 @@ fn main() -> Result<()> { .as_ref() .map(hsmtool::util::helper::lockfile) .transpose()?; - let hsm = Module::initialize(&args.module).context( + + // Initialize the HSM module interface. + let mut hsm = Module::initialize(&args.module, args.acorn.as_deref()).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 diff --git a/sw/host/hsmtool/src/module.rs b/sw/host/hsmtool/src/module.rs index 0c58eaa13ddcbc..b9baae8f487697 100644 --- a/sw/host/hsmtool/src/module.rs +++ b/sw/host/hsmtool/src/module.rs @@ -12,16 +12,24 @@ use cryptoki::types::AuthPin; use serde::de::{Deserialize, Deserializer}; use crate::error::HsmError; +use acorn::Acorn; pub struct Module { pub pkcs11: Pkcs11, + pub acorn: Option, + pub token: Option, } impl Module { - pub fn initialize(module: &str) -> Result { + pub fn initialize(module: &str, acorn: Option<&str>) -> Result { let pkcs11 = Pkcs11::new(module)?; pkcs11.initialize(CInitializeArgs::OsThreads)?; - Ok(Module { pkcs11 }) + let acorn = acorn.map(Acorn::new).transpose()?; + Ok(Module { + pkcs11, + acorn, + token: None, + }) } pub fn get_token(&self, label: &str) -> Result { @@ -36,7 +44,7 @@ impl Module { } pub fn connect( - &self, + &mut self, token: &str, user: Option, pin: Option<&str>, @@ -49,6 +57,7 @@ impl Module { .login(user, pin.as_ref()) .context("Failed HSM Login")?; } + self.token = Some(token.into()); Ok(session) } }