Skip to content

Commit

Permalink
[hsmtool] Support sign/verify over input slices
Browse files Browse the repository at this point in the history
To facilitate signing of ownership-related requests (e.g. signing the
owner config and the unlock/activate commands), give hsmtool the ability
to act on slices of the input.

1. Add the capability to sign and verify over a slice of the input file.
2. Add the capability to update the input file in place.

Example: Sign an owner config.
- The `format` is a plain-text slice from bytes 0..0x7a0 of the input
  file.
- The signature should be updated in place at bytes 0x7a0..0x7e0 of the
  input file.
```
hsmtool -t token -u user -p 123456 \
        ecdsa sign --label owner_key \
        --format slice:0..0x7a0 \
        --update-in-place 0x7a0..0x7e0 \
        owner_config.bin
```

Example: Verify the previously signed owner config.
```
hsmtool -t token -u user -p 123456 \
        ecdsa verify --label owner_key \
        --format slice:0..0x7a0 \
        --signature-at 0x7a0..0x7e0 \
        owner_config.bin
```

Signed-off-by: Chris Frantz <[email protected]>
(cherry picked from commit 60a2517)
  • Loading branch information
cfrantz committed Oct 25, 2024
1 parent ccbdca0 commit 10d6333
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 15 deletions.
17 changes: 15 additions & 2 deletions sw/host/hsmtool/src/commands/ecdsa/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

use anyhow::Result;
use anyhow::{anyhow, Result};
use cryptoki::object::Attribute;
use cryptoki::session::Session;
use serde::{Deserialize, Serialize};
use serde_annotate::Annotate;
use std::any::Any;
use std::ops::Range;
use std::path::PathBuf;

use crate::commands::{Dispatch, SignResult};
Expand All @@ -23,13 +24,16 @@ pub struct Sign {
id: Option<String>,
#[arg(short, long)]
label: Option<String>,
#[arg(short, long, value_enum, default_value = "sha256-hash")]
#[arg(short, long, default_value = "sha256-hash", help=SignData::HELP)]
format: SignData,
/// Reverse the input data and result (for little-endian targets).
#[arg(short = 'r', long)]
little_endian: bool,
#[arg(short, long)]
output: Option<PathBuf>,
/// Update the given byte range in the input file.
#[arg(short, long, value_parser=helper::parse_range)]
update_in_place: Option<Range<usize>>,
input: PathBuf,
}

Expand Down Expand Up @@ -66,6 +70,15 @@ impl Dispatch for Sign {
if let Some(output) = &self.output {
helper::write_file(output, &result)?;
}
if let Some(range) = &self.update_in_place {
let mut data = helper::read_file(&self.input)?;
if let Some(slice) = data.get_mut(range.clone()) {
slice.copy_from_slice(&result);
} else {
return Err(anyhow!("Invalid range on input file: {range:?}"));
}
helper::write_file(&self.input, &data)?;
}
Ok(Box::new(SignResult {
digest: data,
signature: result,
Expand Down
23 changes: 19 additions & 4 deletions sw/host/hsmtool/src/commands/ecdsa/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

use anyhow::Result;
use anyhow::{anyhow, Result};
use cryptoki::object::Attribute;
use cryptoki::session::Session;
use serde::{Deserialize, Serialize};
use serde_annotate::Annotate;
use std::any::Any;
use std::ops::Range;
use std::path::PathBuf;

use crate::commands::{BasicResult, Dispatch};
Expand All @@ -23,13 +24,17 @@ pub struct Verify {
id: Option<String>,
#[arg(short, long)]
label: Option<String>,
#[arg(short, long, value_enum, default_value = "sha256-hash")]
#[arg(short, long, default_value = "sha256-hash", help=SignData::HELP)]
format: SignData,
/// Reverse the input data and result (for little-endian targets).
#[arg(short = 'r', long)]
little_endian: bool,
/// The signature is at the given byte range of the input file.
#[arg(short, long, value_parser=helper::parse_range, conflicts_with="signature")]
signature_at: Option<Range<usize>>,
input: PathBuf,
signature: PathBuf,
#[arg(conflicts_with = "signature_at")]
signature: Option<PathBuf>,
}

#[typetag::serde(name = "ecdsa-verify")]
Expand All @@ -56,7 +61,17 @@ impl Dispatch for Verify {
}
let data = self.format.prepare(KeyType::Ec, &data)?;
let mechanism = self.format.mechanism(KeyType::Ec)?;
let mut signature = helper::read_file(&self.signature)?;
let mut signature = if let Some(filename) = &self.signature {
helper::read_file(filename)?
} else if let Some(range) = &self.signature_at {
let input = helper::read_file(&self.input)?;
input
.get(range.clone())
.ok_or_else(|| anyhow!("Invalid range on input file: {range:?}"))?
.to_vec()
} else {
unreachable!();
};
if self.little_endian {
let half = signature.len() / 2;
signature[..half].reverse();
Expand Down
24 changes: 20 additions & 4 deletions sw/host/hsmtool/src/commands/rsa/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

use anyhow::Result;
use anyhow::{anyhow, Result};
use cryptoki::object::Attribute;
use cryptoki::session::Session;
use serde::{Deserialize, Serialize};
use serde_annotate::Annotate;
use std::any::Any;
use std::ops::Range;
use std::path::PathBuf;

use crate::commands::{BasicResult, Dispatch};
use crate::commands::{Dispatch, SignResult};
use crate::error::HsmError;
use crate::module::Module;
use crate::util::attribute::KeyType;
Expand All @@ -23,13 +24,16 @@ pub struct Sign {
id: Option<String>,
#[arg(short, long)]
label: Option<String>,
#[arg(short, long, value_enum, default_value = "sha256-hash")]
#[arg(short, long, default_value = "sha256-hash", help=SignData::HELP)]
format: SignData,
/// Reverse the input data and result (for little-endian targets).
#[arg(short = 'r', long)]
little_endian: bool,
#[arg(short, long)]
output: Option<PathBuf>,
/// Update the given byte range in the input file.
#[arg(short, long, value_parser=helper::parse_range)]
update_in_place: Option<Range<usize>>,
input: PathBuf,
}

Expand Down Expand Up @@ -60,6 +64,18 @@ impl Dispatch for Sign {
if let Some(output) = &self.output {
helper::write_file(output, &result)?;
}
Ok(Box::<BasicResult>::default())
if let Some(range) = &self.update_in_place {
let mut data = helper::read_file(&self.input)?;
if let Some(slice) = data.get_mut(range.clone()) {
slice.copy_from_slice(&result);
} else {
return Err(anyhow!("Invalid range on input file: {range:?}"));
}
helper::write_file(&self.input, &data)?;
}
Ok(Box::new(SignResult {
digest: data,
signature: result,
}))
}
}
23 changes: 19 additions & 4 deletions sw/host/hsmtool/src/commands/rsa/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

use anyhow::Result;
use anyhow::{anyhow, Result};
use cryptoki::object::Attribute;
use cryptoki::session::Session;
use serde::{Deserialize, Serialize};
use serde_annotate::Annotate;
use std::any::Any;
use std::ops::Range;
use std::path::PathBuf;

use crate::commands::{BasicResult, Dispatch};
Expand All @@ -23,13 +24,17 @@ pub struct Verify {
id: Option<String>,
#[arg(short, long)]
label: Option<String>,
#[arg(short, long, value_enum, default_value = "sha256-hash")]
#[arg(short, long, default_value = "sha256-hash", help=SignData::HELP)]
format: SignData,
/// Reverse the input data and result (for little-endian targets).
#[arg(short = 'r', long)]
little_endian: bool,
/// The signature is at the given byte range of the input file.
#[arg(short, long, value_parser=helper::parse_range, conflicts_with="signature")]
signature_at: Option<Range<usize>>,
input: PathBuf,
signature: PathBuf,
#[arg(conflicts_with = "signature_at")]
signature: Option<PathBuf>,
}

#[typetag::serde(name = "rsa-verify")]
Expand All @@ -52,7 +57,17 @@ impl Dispatch for Verify {
}
let data = self.format.prepare(KeyType::Rsa, &data)?;
let mechanism = self.format.mechanism(KeyType::Rsa)?;
let mut signature = helper::read_file(&self.signature)?;
let mut signature = if let Some(filename) = &self.signature {
helper::read_file(filename)?
} else if let Some(range) = &self.signature_at {
let input = helper::read_file(&self.input)?;
input
.get(range.clone())
.ok_or_else(|| anyhow!("Invalid range on input file: {range:?}"))?
.to_vec()
} else {
unreachable!();
};
if self.little_endian {
signature.reverse();
}
Expand Down
25 changes: 25 additions & 0 deletions sw/host/hsmtool/src/util/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use rand::prelude::*;
use std::convert::AsRef;
use std::fs::File;
use std::io::{Read, Write};
use std::ops::Range;
use std::path::Path;

use crate::error::HsmError;
Expand Down Expand Up @@ -89,3 +90,27 @@ pub fn lockfile<P: AsRef<Path>>(path: P) -> Result<File> {
log::info!("Lock acquired");
Ok(lf)
}

fn parse_usize(s: &str) -> Result<usize> {
if let Some(hex) = s.strip_prefix("0x") {
Ok(usize::from_str_radix(hex, 16)?)
} else {
Ok(s.parse::<usize>()?)
}
}

/// Parse a range from a string (e.g. "10..20"). The integers in the range may be expressed in
/// either decimal or hexadecimal.
pub fn parse_range(s: &str) -> Result<Range<usize>> {
if let Some((a, b)) = s.split_once("..") {
let start = parse_usize(a)?;
let end = parse_usize(b)?;
if start < end {
Ok(Range { start, end })
} else {
Err(HsmError::Unsupported(format!("bad range: {s:?}")).into())
}
} else {
Err(HsmError::Unsupported(format!("bad range: {s:?}")).into())
}
}
54 changes: 53 additions & 1 deletion sw/host/hsmtool/src/util/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ use serde::{Deserialize, Serialize};
use sha2::digest::const_oid::AssociatedOid;
use sha2::digest::Digest;
use sha2::Sha256;
use std::str::FromStr;

use crate::error::HsmError;
use crate::util::attribute::KeyType;
use crate::util::helper::parse_range;

/// Specify the type of data being signed or verified.
#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SignData {
/// The data to be signed is plain text.
/// The data will be hashed and padded as specified by the signing algorithm.
Expand All @@ -27,9 +29,32 @@ pub enum SignData {
/// The data is raw and will be passed directly to the signing functions.
#[serde(alias = "raw")]
Raw,
/// The data to be signed is plain text.
/// A slice of the data will be hashed and padded as specified by the signing algorithm.
#[serde(alias = "slice")]
Slice(usize, usize),
}

impl FromStr for SignData {
type Err = anyhow::Error;
fn from_str(input: &str) -> Result<Self> {
if input.eq_ignore_ascii_case("plain-text") {
Ok(SignData::PlainText)
} else if input.eq_ignore_ascii_case("sha256-hash") {
Ok(SignData::Sha256Hash)
} else if input.eq_ignore_ascii_case("raw") {
Ok(SignData::Raw)
} else if input[..6].eq_ignore_ascii_case("slice:") {
let r = parse_range(&input[6..])?;
Ok(SignData::Slice(r.start, r.end))
} else {
Err(HsmError::Unsupported(format!("invalid variant: {input}")).into())
}
}
}

impl SignData {
pub const HELP: &'static str = "[allowed values: plain-text, sha256-hash, raw, slice:m..n]";
/// Prepare `input` data for signing or verification.
pub fn prepare(&self, keytype: KeyType, input: &[u8]) -> Result<Vec<u8>> {
match keytype {
Expand All @@ -40,6 +65,10 @@ impl SignData {
SignData::Sha256Hash => Self::pkcs15sign::<Sha256>(input),
// Raw data requires no transformation.
SignData::Raw => Self::data_raw(input),
// Data is a slice of plaintext: hash, then add PKCSv1.5 padding.
SignData::Slice(a, b) => {
Self::pkcs15sign::<Sha256>(&Self::data_plain_text(&input[*a..*b])?)
}
},
KeyType::Ec => match self {
// Data is plaintext: hash.
Expand All @@ -48,6 +77,8 @@ impl SignData {
SignData::Sha256Hash => Self::data_raw(input),
// Raw data requires no transformation.
SignData::Raw => Self::data_raw(input),
// Data is a slice of plaintext: hash.
SignData::Slice(a, b) => Self::data_plain_text(&input[*a..*b]),
},
_ => Err(HsmError::Unsupported("SignData prepare for {keytype:?}".into()).into()),
}
Expand All @@ -63,11 +94,13 @@ impl SignData {
"rust-cryptoki Mechanism doesn't include RSA_X_509".into(),
)
.into()),
SignData::Slice(_, _) => Ok(Mechanism::RsaPkcs),
},
KeyType::Ec => match self {
SignData::PlainText => Ok(Mechanism::Ecdsa),
SignData::Sha256Hash => Ok(Mechanism::Ecdsa),
SignData::Raw => Ok(Mechanism::Ecdsa),
SignData::Slice(_, _) => Ok(Mechanism::Ecdsa),
},
_ => Err(HsmError::Unsupported("No mechanism for {keytype:?}".into()).into()),
}
Expand Down Expand Up @@ -119,6 +152,13 @@ mod tests {
assert_eq!(hex::encode(result),
"3031300d0609608648016503040201050004207d38b5cd25a2baf85ad3bb5b9311383e671a8a142eb302b324d4a5fba8748c69"
);

let result = SignData::PlainText
.prepare(KeyType::Ec, b"The quick brown fox jumped over the lazy dog")?;
assert_eq!(
hex::encode(result),
"7d38b5cd25a2baf85ad3bb5b9311383e671a8a142eb302b324d4a5fba8748c69",
);
Ok(())
}

Expand All @@ -134,4 +174,16 @@ mod tests {
assert!(SignData::Sha256Hash.prepare(KeyType::Rsa, b"").is_err());
Ok(())
}

#[test]
fn test_slice() -> Result<()> {
let result = SignData::Slice(0, 3)
.prepare(KeyType::Ec, b"The quick brown fox jumped over the lazy dog")?;
assert_eq!(
hex::encode(result),
// Hash of "The".
"b344d80e24a3679999fa964450b34bc24d1578a35509f934c1418b0a20d21a67",
);
Ok(())
}
}

0 comments on commit 10d6333

Please sign in to comment.