Skip to content

Commit 7ceb7c3

Browse files
author
Willy Zhang
committed
[hsmtool] Add MLDSA support
Adds commands to generate, export, sign, and verify using the MLDSA algorithm (defaulting to ML-DSA-87). Includes support for exporting CSRs with the correct ML-DSA-87 OID. Signed-off-by: Willy Zhang <[email protected]>
1 parent cbedf7a commit 7ceb7c3

File tree

12 files changed

+817
-8
lines changed

12 files changed

+817
-8
lines changed

sw/host/hsmtool/BUILD

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ rust_library(
109109
"src/commands/kdf/generate.rs",
110110
"src/commands/kdf/import.rs",
111111
"src/commands/kdf/mod.rs",
112+
"src/commands/mldsa/export.rs",
113+
"src/commands/mldsa/export_csr.rs",
114+
"src/commands/mldsa/generate.rs",
115+
"src/commands/mldsa/import.rs",
116+
"src/commands/mldsa/mod.rs",
117+
"src/commands/mldsa/sign.rs",
118+
"src/commands/mldsa/verify.rs",
112119
"src/commands/mod.rs",
113120
"src/commands/object/destroy.rs",
114121
"src/commands/object/list.rs",
@@ -177,6 +184,7 @@ rust_library(
177184
"@crate_index//:anyhow",
178185
"@crate_index//:base64ct",
179186
"@crate_index//:clap",
187+
"@crate_index//:const-oid",
180188
"@crate_index//:cryptoki",
181189
"@crate_index//:cryptoki-sys",
182190
"@crate_index//:der",
@@ -197,9 +205,12 @@ rust_library(
197205
"@crate_index//:serde_bytes",
198206
"@crate_index//:serde_json",
199207
"@crate_index//:sha2",
208+
"@crate_index//:signature",
209+
"@crate_index//:spki",
200210
"@crate_index//:strum",
201211
"@crate_index//:thiserror",
202212
"@crate_index//:typetag",
213+
"@crate_index//:x509-cert",
203214
"@crate_index//:zeroize",
204215
"@lowrisc_serde_annotate//serde_annotate",
205216
],
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright lowRISC contributors (OpenTitan project).
2+
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
use anyhow::{Result, anyhow};
6+
use cryptoki::object::{Attribute, ObjectHandle};
7+
use cryptoki::session::Session;
8+
use serde::{Deserialize, Serialize};
9+
use std::any::Any;
10+
use std::fs;
11+
use std::path::PathBuf;
12+
13+
use crate::commands::{BasicResult, Dispatch};
14+
use crate::error::HsmError;
15+
use crate::module::Module;
16+
use crate::util::attribute::{AttributeMap, AttributeType, KeyType, ObjectClass};
17+
use crate::util::helper;
18+
use crate::util::key::KeyEncoding;
19+
use crate::util::wrap::{Wrap, WrapPrivateKey};
20+
21+
#[derive(clap::Args, Debug, Serialize, Deserialize)]
22+
pub struct Export {
23+
#[arg(long)]
24+
id: Option<String>,
25+
#[arg(short, long)]
26+
label: Option<String>,
27+
/// Export the private key.
28+
#[arg(long)]
29+
private: bool,
30+
/// Wrap the exported key a wrapping key.
31+
#[arg(long)]
32+
wrap: Option<String>,
33+
// Wrapping key mechanism. Required when wrap is specified.
34+
#[arg(long, default_value = "aes-key-wrap-pad")]
35+
wrap_mechanism: Option<WrapPrivateKey>,
36+
#[arg(short, long, value_enum, default_value = "der")]
37+
format: KeyEncoding,
38+
filename: PathBuf,
39+
}
40+
41+
impl Export {
42+
fn export(&self, session: &Session, object: ObjectHandle) -> Result<()> {
43+
let map = AttributeMap::from_object(session, object)?;
44+
let val = map.get(&AttributeType::Value).ok_or(anyhow!("Key does not contain a value"))?;
45+
let key_value: Vec<u8> = val.try_into()?;
46+
47+
match self.format {
48+
KeyEncoding::Der | KeyEncoding::Pkcs8Der => {
49+
fs::write(&self.filename, &key_value)?;
50+
}
51+
KeyEncoding::Pem | KeyEncoding::Pkcs8Pem => {
52+
let label = if self.private { "PRIVATE KEY" } else { "PUBLIC KEY" };
53+
let pem = pem_rfc7468::encode_string(label, pem_rfc7468::LineEnding::LF, &key_value)?;
54+
fs::write(&self.filename, pem.as_bytes())?;
55+
}
56+
_ => return Err(anyhow!("Unsupported format for MLDSA export")),
57+
}
58+
Ok(())
59+
}
60+
61+
fn wrap_key(&self, session: &Session, object: ObjectHandle) -> Result<()> {
62+
let wrapper: Wrap = self
63+
.wrap_mechanism
64+
.ok_or(anyhow!("wrap_mechanism is required when wrap is specified"))?
65+
.into();
66+
let wrapped = wrapper.wrap(session, object, self.wrap.as_deref())?;
67+
fs::write(&self.filename, &wrapped)?;
68+
Ok(())
69+
}
70+
}
71+
72+
#[typetag::serde(name = "mldsa-export")]
73+
impl Dispatch for Export {
74+
fn run(
75+
&self,
76+
_context: &dyn Any,
77+
_hsm: &Module,
78+
session: Option<&Session>,
79+
) -> Result<Box<dyn erased_serde::Serialize>> {
80+
let session = session.ok_or(HsmError::SessionRequired)?;
81+
let mut attrs = helper::search_spec(self.id.as_deref(), self.label.as_deref())?;
82+
attrs.push(Attribute::KeyType(KeyType::MlDsa.try_into()?));
83+
if self.private {
84+
attrs.push(Attribute::Class(ObjectClass::PrivateKey.try_into()?));
85+
} else {
86+
attrs.push(Attribute::Class(ObjectClass::PublicKey.try_into()?));
87+
}
88+
let object = helper::find_one_object(session, &attrs)?;
89+
90+
if self.wrap.is_some() {
91+
self.wrap_key(session, object)?;
92+
} else {
93+
self.export(session, object)?;
94+
}
95+
Ok(Box::<BasicResult>::default())
96+
}
97+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright lowRISC contributors (OpenTitan project).
2+
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
use anyhow::{Result, anyhow};
6+
use const_oid::ObjectIdentifier;
7+
use cryptoki::mechanism::vendor_defined::VendorDefinedMechanism;
8+
use cryptoki::mechanism::Mechanism;
9+
use cryptoki::object::Attribute;
10+
use cryptoki::session::Session;
11+
use der::{Encode, EncodePem};
12+
use serde::{Deserialize, Serialize};
13+
use std::any::Any;
14+
use std::fs;
15+
use std::path::PathBuf;
16+
use std::str::FromStr;
17+
use x509_cert::name::Name;
18+
use x509_cert::request::{CertReq, CertReqInfo};
19+
use x509_cert::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned};
20+
21+
use crate::commands::{BasicResult, Dispatch};
22+
use crate::error::HsmError;
23+
use crate::module::Module;
24+
use crate::util::attribute::{AttributeMap, AttributeType, KeyType, MechanismType, ObjectClass};
25+
use crate::util::helper;
26+
27+
// ML-DSA-87 OID: 2.16.840.1.101.3.4.3.19
28+
const OID_MLDSA_87: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.3.19");
29+
30+
#[derive(clap::Args, Debug, Serialize, Deserialize)]
31+
pub struct ExportCsr {
32+
#[arg(long)]
33+
id: Option<String>,
34+
#[arg(short, long)]
35+
label: Option<String>,
36+
#[arg(long)]
37+
subject: String,
38+
#[arg(short, long)]
39+
output: PathBuf,
40+
}
41+
42+
impl ExportCsr {
43+
fn run_command(&self, session: &Session) -> Result<()> {
44+
// Find the private key
45+
let mut attrs = helper::search_spec(self.id.as_deref(), self.label.as_deref())?;
46+
attrs.push(Attribute::Class(ObjectClass::PrivateKey.try_into()?));
47+
attrs.push(Attribute::KeyType(KeyType::MlDsa.try_into()?));
48+
let private_key = helper::find_one_object(session, &attrs)?;
49+
50+
// Determine public key label
51+
let pub_label_string = if let Some(l) = self.label.as_deref() {
52+
if l.ends_with(".priv") {
53+
Some(l.replace(".priv", ".pub"))
54+
} else {
55+
Some(l.to_string())
56+
}
57+
} else {
58+
None
59+
};
60+
let pub_label = pub_label_string.as_deref();
61+
62+
// Find the public key (needed for CSR)
63+
let mut pub_attrs = helper::search_spec(self.id.as_deref(), pub_label)?;
64+
pub_attrs.push(Attribute::Class(ObjectClass::PublicKey.try_into()?));
65+
pub_attrs.push(Attribute::KeyType(KeyType::MlDsa.try_into()?));
66+
let public_key = helper::find_one_object(session, &pub_attrs)?;
67+
68+
// Get public key value
69+
let map = AttributeMap::from_object(session, public_key)?;
70+
let val = map.get(&AttributeType::Value).ok_or(anyhow!("Public key does not contain a value"))?;
71+
let pub_key_bytes: Vec<u8> = val.try_into()?;
72+
73+
// Construct Subject Name
74+
let subject = Name::from_str(&self.subject).map_err(|e| anyhow!("Invalid subject: {}", e))?;
75+
76+
// Create CertReqInfo
77+
let algorithm = AlgorithmIdentifierOwned {
78+
oid: OID_MLDSA_87,
79+
parameters: None,
80+
};
81+
let subject_public_key_info = SubjectPublicKeyInfoOwned {
82+
algorithm: algorithm.clone(),
83+
subject_public_key: x509_cert::der::asn1::BitString::from_bytes(&pub_key_bytes)
84+
.map_err(|e| anyhow!("Invalid public key bytes: {}", e))?,
85+
};
86+
87+
let info = CertReqInfo {
88+
version: x509_cert::request::Version::V1,
89+
subject,
90+
public_key: subject_public_key_info,
91+
attributes: Default::default(),
92+
};
93+
94+
// Serialize Info to sign
95+
let tbs_bytes = info.to_der().map_err(|e| anyhow!("Failed to encode CertReqInfo: {}", e))?;
96+
97+
// Sign the request using HSM
98+
// Using VendorDefinedMechanism for MLDSA signature generation
99+
// to avoid type mismatch with native Mechanism::MlDsa if params are tricky.
100+
let mechanism = Mechanism::VendorDefined(VendorDefinedMechanism::new::<()>(
101+
MechanismType::MlDsa.try_into()?,
102+
None,
103+
));
104+
105+
let signature_bytes = session.sign(&mechanism, private_key, &tbs_bytes)
106+
.map_err(|e| anyhow!("HSM signing failed: {}", e))?;
107+
108+
let signature = x509_cert::der::asn1::BitString::from_bytes(&signature_bytes)
109+
.map_err(|e| anyhow!("Invalid signature bytes: {}", e))?;
110+
111+
let cert_req = CertReq {
112+
info,
113+
algorithm,
114+
signature,
115+
};
116+
117+
// Encode to PEM
118+
let pem = cert_req.to_pem(Default::default())
119+
.map_err(|e| anyhow!("Failed to encode CSR to PEM: {}", e))?;
120+
121+
fs::write(&self.output, pem.as_bytes())?;
122+
123+
Ok(())
124+
}
125+
}
126+
127+
#[typetag::serde(name = "mldsa-export-csr")]
128+
impl Dispatch for ExportCsr {
129+
fn run(
130+
&self,
131+
_context: &dyn Any,
132+
_hsm: &Module,
133+
session: Option<&Session>,
134+
) -> Result<Box<dyn erased_serde::Serialize>> {
135+
let session = session.ok_or(HsmError::SessionRequired)?;
136+
self.run_command(session)?;
137+
Ok(Box::<BasicResult>::default())
138+
}
139+
}

0 commit comments

Comments
 (0)