Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -145,26 +145,19 @@ crate.annotation(
)
crate.annotation(
crate = "cryptoki",
patch_args = ["-p2"],
patches = [
"@lowrisc_opentitan//third_party/rust/patches:cryptoki-vendor-defined-mechanism-type.patch",
"@lowrisc_opentitan//third_party/rust/patches:cryptoki-profile.patch",
],
repositories = ["crate_index"],
)
crate.annotation(
additive_build_file_content = """
filegroup(
name = "binding_srcs",
srcs = [
"src/lib.rs",
"src/bindings/x86_64-unknown-linux-gnu.rs",
],
name = "cryptoki-sys-binding-srcs",
srcs = glob(["src/bindings/*.rs"]),
visibility = ["//visibility:public"],
)
""",
crate = "cryptoki-sys",
extra_aliased_targets = {
"cryptoki-sys-binding-srcs": "binding_srcs",
"cryptoki-sys-binding-srcs": "cryptoki-sys-binding-srcs",
},
repositories = ["crate_index"],
)
Expand Down
293 changes: 208 additions & 85 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions sw/host/hsmtool/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ rust_library(
"src/commands/kdf/generate.rs",
"src/commands/kdf/import.rs",
"src/commands/kdf/mod.rs",
"src/commands/mldsa/export.rs",
"src/commands/mldsa/export_csr.rs",
"src/commands/mldsa/generate.rs",
"src/commands/mldsa/import.rs",
"src/commands/mldsa/mod.rs",
"src/commands/mldsa/sign.rs",
"src/commands/mldsa/verify.rs",
"src/commands/mod.rs",
"src/commands/object/destroy.rs",
"src/commands/object/list.rs",
Expand Down Expand Up @@ -177,6 +184,7 @@ rust_library(
"@crate_index//:anyhow",
"@crate_index//:base64ct",
"@crate_index//:clap",
"@crate_index//:const-oid",
"@crate_index//:cryptoki",
"@crate_index//:cryptoki-sys",
"@crate_index//:der",
Expand All @@ -197,9 +205,12 @@ rust_library(
"@crate_index//:serde_bytes",
"@crate_index//:serde_json",
"@crate_index//:sha2",
"@crate_index//:signature",
"@crate_index//:spki",
"@crate_index//:strum",
"@crate_index//:thiserror",
"@crate_index//:typetag",
"@crate_index//:x509-cert",
"@crate_index//:zeroize",
"@lowrisc_serde_annotate//serde_annotate",
],
Expand Down
3 changes: 2 additions & 1 deletion sw/host/hsmtool/scripts/pkcs11_consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ def get_names(self):
raise Exception(
'Expected objects to all be of uniform type',
name, tp, m.group(2))
names.append(name)
if name not in names:
names.append(name)
return (names, tp)

@staticmethod
Expand Down
97 changes: 97 additions & 0 deletions sw/host/hsmtool/src/commands/mldsa/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// 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, anyhow};
use cryptoki::object::{Attribute, ObjectHandle};
use cryptoki::session::Session;
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::fs;
use std::path::PathBuf;

use crate::commands::{BasicResult, Dispatch};
use crate::error::HsmError;
use crate::module::Module;
use crate::util::attribute::{AttributeMap, AttributeType, KeyType, ObjectClass};
use crate::util::helper;
use crate::util::key::KeyEncoding;
use crate::util::wrap::{Wrap, WrapPrivateKey};

#[derive(clap::Args, Debug, Serialize, Deserialize)]
pub struct Export {
#[arg(long)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you provide doc comments for each parameter?

id: Option<String>,
#[arg(short, long)]
label: Option<String>,
/// Export the private key.
#[arg(long)]
private: bool,
/// Wrap the exported key a wrapping key.
#[arg(long)]
wrap: Option<String>,
// Wrapping key mechanism. Required when wrap is specified.
#[arg(long, default_value = "aes-key-wrap-pad")]
wrap_mechanism: Option<WrapPrivateKey>,
#[arg(short, long, value_enum, default_value = "der")]
format: KeyEncoding,
filename: PathBuf,
}

impl Export {
fn export(&self, session: &Session, object: ObjectHandle) -> Result<()> {
let map = AttributeMap::from_object(session, object)?;
let val = map.get(&AttributeType::Value).ok_or(anyhow!("Key does not contain a value"))?;
let key_value: Vec<u8> = val.try_into()?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What (if anything) does the PKCS#11 spec say about the encoding of the Value field?

I see that we're using this as-is later to save the key as either DER or PEM form. This is ok as long as the Value field is already DER encoded.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good catch this is wrong, I was just writing the raw keys to a file which is wrong.

I imported the ml_dsa rust library to perform the encoding to PEM/DER for export

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This encode/decode stuff maybe belongs in //sw/host/hsmtool/src/util/key/.... The existing code for ecdsa and rsa has TryFrom implementations that convert between the RustCrypto forms and AttributeMap. This is meant to hide the ugliness of the conversions from the code that implements the command hierarchy.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved the encode/decode logic to a seperate file under //sw/host/hsmtool/src/util/key/mldsa.rs


match self.format {
KeyEncoding::Der | KeyEncoding::Pkcs8Der => {
fs::write(&self.filename, &key_value)?;
}
KeyEncoding::Pem | KeyEncoding::Pkcs8Pem => {
let label = if self.private { "PRIVATE KEY" } else { "PUBLIC KEY" };
let pem = pem_rfc7468::encode_string(label, pem_rfc7468::LineEnding::LF, &key_value)?;
fs::write(&self.filename, pem.as_bytes())?;
}
_ => return Err(anyhow!("Unsupported format for MLDSA export")),
}
Ok(())
}

fn wrap_key(&self, session: &Session, object: ObjectHandle) -> Result<()> {
let wrapper: Wrap = self
.wrap_mechanism
.ok_or(anyhow!("wrap_mechanism is required when wrap is specified"))?
.into();
let wrapped = wrapper.wrap(session, object, self.wrap.as_deref())?;
fs::write(&self.filename, &wrapped)?;
Ok(())
}
}

#[typetag::serde(name = "mldsa-export")]
impl Dispatch for Export {
fn run(
&self,
_context: &dyn Any,
_hsm: &Module,
session: Option<&Session>,
) -> Result<Box<dyn erased_serde::Serialize>> {
let session = session.ok_or(HsmError::SessionRequired)?;
let mut attrs = helper::search_spec(self.id.as_deref(), self.label.as_deref())?;
attrs.push(Attribute::KeyType(KeyType::MlDsa.try_into()?));
if self.private {
attrs.push(Attribute::Class(ObjectClass::PrivateKey.try_into()?));
} else {
attrs.push(Attribute::Class(ObjectClass::PublicKey.try_into()?));
}
let object = helper::find_one_object(session, &attrs)?;

if self.wrap.is_some() {
self.wrap_key(session, object)?;
} else {
self.export(session, object)?;
}
Ok(Box::<BasicResult>::default())
}
}
139 changes: 139 additions & 0 deletions sw/host/hsmtool/src/commands/mldsa/export_csr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// 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, anyhow};
use const_oid::ObjectIdentifier;
use cryptoki::mechanism::vendor_defined::VendorDefinedMechanism;
use cryptoki::mechanism::Mechanism;
use cryptoki::object::Attribute;
use cryptoki::session::Session;
use der::{Encode, EncodePem};
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use x509_cert::name::Name;
use x509_cert::request::{CertReq, CertReqInfo};
use x509_cert::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned};

use crate::commands::{BasicResult, Dispatch};
use crate::error::HsmError;
use crate::module::Module;
use crate::util::attribute::{AttributeMap, AttributeType, KeyType, MechanismType, ObjectClass};
use crate::util::helper;

// ML-DSA-87 OID: 2.16.840.1.101.3.4.3.19
const OID_MLDSA_87: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.3.19");

#[derive(clap::Args, Debug, Serialize, Deserialize)]
pub struct ExportCsr {
#[arg(long)]
id: Option<String>,
#[arg(short, long)]
label: Option<String>,
#[arg(long)]
subject: String,
#[arg(short, long)]
output: PathBuf,
}

impl ExportCsr {
fn run_command(&self, session: &Session) -> Result<()> {
// Find the private key
let mut attrs = helper::search_spec(self.id.as_deref(), self.label.as_deref())?;
attrs.push(Attribute::Class(ObjectClass::PrivateKey.try_into()?));
attrs.push(Attribute::KeyType(KeyType::MlDsa.try_into()?));
let private_key = helper::find_one_object(session, &attrs)?;

// Determine public key label
let pub_label_string = if let Some(l) = self.label.as_deref() {
if l.ends_with(".priv") {
Some(l.replace(".priv", ".pub"))
} else {
Some(l.to_string())
}
} else {
None
};
let pub_label = pub_label_string.as_deref();

// Find the public key (needed for CSR)
let mut pub_attrs = helper::search_spec(self.id.as_deref(), pub_label)?;
pub_attrs.push(Attribute::Class(ObjectClass::PublicKey.try_into()?));
pub_attrs.push(Attribute::KeyType(KeyType::MlDsa.try_into()?));
let public_key = helper::find_one_object(session, &pub_attrs)?;

// Get public key value
let map = AttributeMap::from_object(session, public_key)?;
let val = map.get(&AttributeType::Value).ok_or(anyhow!("Public key does not contain a value"))?;
let pub_key_bytes: Vec<u8> = val.try_into()?;

// Construct Subject Name
let subject = Name::from_str(&self.subject).map_err(|e| anyhow!("Invalid subject: {}", e))?;

// Create CertReqInfo
let algorithm = AlgorithmIdentifierOwned {
oid: OID_MLDSA_87,
parameters: None,
};
let subject_public_key_info = SubjectPublicKeyInfoOwned {
algorithm: algorithm.clone(),
subject_public_key: x509_cert::der::asn1::BitString::from_bytes(&pub_key_bytes)
.map_err(|e| anyhow!("Invalid public key bytes: {}", e))?,
};

let info = CertReqInfo {
version: x509_cert::request::Version::V1,
subject,
public_key: subject_public_key_info,
attributes: Default::default(),
};

// Serialize Info to sign
let tbs_bytes = info.to_der().map_err(|e| anyhow!("Failed to encode CertReqInfo: {}", e))?;

// Sign the request using HSM
// Using VendorDefinedMechanism for MLDSA signature generation
// to avoid type mismatch with native Mechanism::MlDsa if params are tricky.
let mechanism = Mechanism::VendorDefined(VendorDefinedMechanism::new::<()>(
MechanismType::MlDsa.try_into()?,
None,
));

let signature_bytes = session.sign(&mechanism, private_key, &tbs_bytes)
.map_err(|e| anyhow!("HSM signing failed: {}", e))?;

let signature = x509_cert::der::asn1::BitString::from_bytes(&signature_bytes)
.map_err(|e| anyhow!("Invalid signature bytes: {}", e))?;

let cert_req = CertReq {
info,
algorithm,
signature,
};

// Encode to PEM
let pem = cert_req.to_pem(Default::default())
.map_err(|e| anyhow!("Failed to encode CSR to PEM: {}", e))?;

fs::write(&self.output, pem.as_bytes())?;

Ok(())
}
}

#[typetag::serde(name = "mldsa-export-csr")]
impl Dispatch for ExportCsr {
fn run(
&self,
_context: &dyn Any,
_hsm: &Module,
session: Option<&Session>,
) -> Result<Box<dyn erased_serde::Serialize>> {
let session = session.ok_or(HsmError::SessionRequired)?;
self.run_command(session)?;
Ok(Box::<BasicResult>::default())
}
}
Loading