From 4f8a85cbc42ceee5b7d0fd95cf597356be7fc3ac Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz <wiktor@metacode.biz> Date: Mon, 17 Mar 2025 11:35:52 +0100 Subject: [PATCH 1/2] feat: make `openpgp_sign_state` always return an armored OpenPGP signature Signed-off-by: Wiktor Kwapisiewicz <wiktor@metacode.biz> --- nethsm-cli/README.md | 2 ++ nethsm-cli/src/main.rs | 2 +- nethsm/src/lib.rs | 4 ++-- nethsm/src/openpgp.rs | 17 ++++++++--------- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/nethsm-cli/README.md b/nethsm-cli/README.md index 205accaf..fe38a822 100644 --- a/nethsm-cli/README.md +++ b/nethsm-cli/README.md @@ -471,6 +471,8 @@ done signstar-request-signature "$NETHSM_OPENPGP_SIGNATURE_MESSAGE" | tee "${NETHSM_OPENPGP_SIGNATURE_MESSAGE}.json" nethsm openpgp sign-state --force "signing1" "${NETHSM_OPENPGP_SIGNATURE_MESSAGE}.json" +# the signature is always armored +grep -- "-----BEGIN PGP SIGNATURE-----" "$NETHSM_OPENPGP_SIGNATURE_OUTPUT_FILE" gpg --verify "$NETHSM_OPENPGP_SIGNATURE_OUTPUT_FILE" "$NETHSM_OPENPGP_SIGNATURE_MESSAGE" rpacket dump "$NETHSM_OPENPGP_SIGNATURE_OUTPUT_FILE" sha512sum "$NETHSM_OPENPGP_SIGNATURE_MESSAGE" diff --git a/nethsm-cli/src/main.rs b/nethsm-cli/src/main.rs index 83971709..3c59258a 100644 --- a/nethsm-cli/src/main.rs +++ b/nethsm-cli/src/main.rs @@ -893,7 +893,7 @@ fn main() -> Result<(), Error> { output.output().write_all( nethsm .openpgp_sign_state(&command.key_id, hasher)? - .as_slice(), + .as_bytes(), )?; } }, diff --git a/nethsm/src/lib.rs b/nethsm/src/lib.rs index e423d292..4ee4c1b0 100644 --- a/nethsm/src/lib.rs +++ b/nethsm/src/lib.rs @@ -6030,7 +6030,7 @@ impl NetHsm { openpgp::sign(self, key_id, message) } - /// Generates an OpenPGP signature based on provided hasher state. + /// Generates an armored OpenPGP signature based on provided hasher state. /// /// Signs the hasher `state` using the key identified by `key_id` /// and returns a binary [OpenPGP data signature]. @@ -6139,7 +6139,7 @@ impl NetHsm { &self, key_id: &KeyId, state: impl sha2::Digest + Clone + std::io::Write, - ) -> Result<Vec<u8>, crate::Error> { + ) -> Result<String, crate::Error> { openpgp::sign_hasher_state(self, key_id, state) } } diff --git a/nethsm/src/openpgp.rs b/nethsm/src/openpgp.rs index 5bfeba97..ffd003ef 100644 --- a/nethsm/src/openpgp.rs +++ b/nethsm/src/openpgp.rs @@ -11,10 +11,12 @@ use base64ct::{Base64, Encoding as _}; use chrono::{DateTime, Utc}; use email_address::{EmailAddress, Options}; use pgp::{ + ArmorOptions, Deserializable, KeyDetails, SignedPublicKey, SignedSecretKey, + StandaloneSignature, crypto::{ecc_curve::ECCCurve, hash::HashAlgorithm, public_key::PublicKeyAlgorithm}, packet::{ KeyFlags, @@ -813,7 +815,7 @@ pub fn sign( Ok(out) } -/// Generates an OpenPGP signature based on provided hasher state. +/// Generates an armored OpenPGP signature based on provided hasher state. /// /// Signs the hasher `state` using the key identified by `key_id` /// and returns a binary [OpenPGP data signature]. @@ -851,7 +853,7 @@ pub fn sign_hasher_state( nethsm: &NetHsm, key_id: &crate::KeyId, state: impl sha2::Digest + Clone + std::io::Write, -) -> Result<Vec<u8>, crate::Error> { +) -> Result<String, crate::Error> { let public_key = nethsm.get_key_certificate(key_id)?; let signer = HsmKey::new( @@ -903,13 +905,10 @@ pub fn sign_hasher_state( let signature = pgp::Signature::from_config(sig_config, signed_hash_value, raw_sig); - let out = { - let mut out = vec![]; - pgp::packet::write_packet(&mut out, &signature).map_err(Error::Pgp)?; - out - }; - - Ok(out) + let signature = StandaloneSignature { signature }; + Ok(signature + .to_armored_string(ArmorOptions::default()) + .map_err(Error::Pgp)?) } /// Converts NetHSM public key to OpenPGP public key. -- GitLab From 4b16cf539282f6952f471542c327eae35488ceb1 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz <wiktor@metacode.biz> Date: Fri, 7 Mar 2025 12:30:42 +0100 Subject: [PATCH 2/2] feat: Add `signstar-sign` Fixes: https://gitlab.archlinux.org/archlinux/signstar/-/issues/34 Signed-off-by: Wiktor Kwapisiewicz <wiktor@metacode.biz> --- Cargo.lock | 15 +++- Cargo.toml | 1 + justfile | 4 ++ signstar-sign/Cargo.toml | 16 +++++ signstar-sign/README.md | 86 +++++++++++++++++++++++ signstar-sign/src/main.rs | 143 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 signstar-sign/Cargo.toml create mode 100644 signstar-sign/README.md create mode 100644 signstar-sign/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 8a00f64c..04700c6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3023,9 +3023,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -3169,6 +3169,17 @@ dependencies = [ "thiserror 2.0.11", ] +[[package]] +name = "signstar-sign" +version = "0.1.0" +dependencies = [ + "clap", + "nethsm", + "nethsm-config", + "serde_json", + "signstar-request-signature", +] + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 6b970a36..edc77e30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "signstar-configure-build", "signstar-common", "signstar-request-signature", + "signstar-sign", ] [workspace.dependencies] diff --git a/justfile b/justfile index 2ee731f6..4cbdf194 100755 --- a/justfile +++ b/justfile @@ -310,6 +310,9 @@ test-readme project: nethsm-cli) cargo install --locked --path signstar-request-signature ;; + signstar-sign) + cargo install --locked --path nethsm-cli + ;; esac cargo install --locked --path {{ project }} } @@ -356,6 +359,7 @@ test-readme project: test-readmes: just test-readme nethsm-cli just test-readme signstar-configure-build + just test-readme signstar-sign # Adds pre-commit and pre-push git hooks add-hooks: diff --git a/signstar-sign/Cargo.toml b/signstar-sign/Cargo.toml new file mode 100644 index 00000000..4bf0e79a --- /dev/null +++ b/signstar-sign/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "signstar-sign" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +clap.workspace = true +nethsm.workspace = true +nethsm-config.workspace = true +serde_json = "1.0.140" +signstar-request-signature.workspace = true diff --git a/signstar-sign/README.md b/signstar-sign/README.md new file mode 100644 index 00000000..43c6cb9c --- /dev/null +++ b/signstar-sign/README.md @@ -0,0 +1,86 @@ +# Signstar Sign + +This crate offers an executable for processing signing requests. +Signing requests are created using the [`signstar-request-signature`] and specify everything that is needed for creating an artifact signature. +The signing response returned by this executable contains a raw, protocol-specific framing. +Currently, `signstar-sign` can created only OpenPGP signatures but the format is extensible and more could be implemented in the future. + +<!-- + +The following sets up a dummy NetHSM device, which serves as a backend for storing private parts of the signing key. + +```bash +# use a custom, temporary directory for all generated files +nethsm_tmpdir="$(mktemp --directory --suffix '.nethsm-test')" +# set a custom, temporary configuration file location +export NETHSM_CONFIG="$(mktemp --tmpdir="$nethsm_tmpdir" --suffix '-nethsm.toml' --dry-run)" +# add the container using unsafe TLS connection handling for testing +nethsm env add device --label test https://localhost:8443/api/v1 Unsafe +# prepare a temporary passphrase file for the initial admin user passphrase +nethsm_admin_passphrase_file="$(mktemp --tmpdir="$nethsm_tmpdir" --dry-run --suffix '-nethsm.admin-passphrase.txt')" +export NETHSM_PASSPHRASE_FILE="$nethsm_admin_passphrase_file" +printf 'my-very-unsafe-admin-passphrase' > "$NETHSM_PASSPHRASE_FILE" +# add the default admin user credentials +nethsm env add credentials admin Administrator +set +x +counter=0 + +while ! nethsm health state; do + printf "NetHSM is not ready, waiting (try %d)...\n" "$counter" + sleep 1 + counter=$(( counter + 1 )) + if (( counter > 30 )); then + printf "NetHSM is not up even after 30 tries. Aborting." + set -x + exit 2 + fi +done + +printf "NetHSM is ready for provisioning after %d seconds.\n" "$counter" +set -x +# prepare a temporary passphrase file for the initial unlock passphrase +export NETHSM_UNLOCK_PASSPHRASE_FILE="$(mktemp --tmpdir="$nethsm_tmpdir" --dry-run --suffix '-nethsm.unlock-passphrase.txt')" +printf 'my-very-unsafe-unlock-passphrase' > "$NETHSM_UNLOCK_PASSPHRASE_FILE" +# reuse the initial admin passphrase +export NETHSM_ADMIN_PASSPHRASE_FILE="$nethsm_admin_passphrase_file" +nethsm provision + +nethsm_admin1_passphrase_file="$(mktemp --tmpdir="$nethsm_tmpdir" --dry-run --suffix '-nethsm.admin1-passphrase.txt')" +printf 'my-very-unsafe-admin1-passphrase' > "$nethsm_admin1_passphrase_file" +nethsm_operator1_passphrase_file="$(mktemp --tmpdir="$nethsm_tmpdir" --dry-run --suffix '-nethsm.operator1-passphrase.txt')" +printf 'my-very-unsafe-operator1-passphrase' > "$nethsm_operator1_passphrase_file" + +# add users +export NETHSM_PASSPHRASE_FILE="$nethsm_admin1_passphrase_file" +nethsm user add "Some Admin1" Administrator admin1 +nethsm env add credentials admin1 Administrator +export NETHSM_PASSPHRASE_FILE="$nethsm_operator1_passphrase_file" +nethsm user add "Some Operator1" Operator operator1 +nethsm env add credentials operator1 Operator + +export NETHSM_KEY_CERT_OUTPUT_FILE="$(mktemp --tmpdir="$nethsm_tmpdir" --dry-run --suffix '-nethsm.openpgp-cert.pgp')" + +# create a signing key +nethsm --user admin1 key generate --key-id signing1 --tags tag1 Curve25519 EdDsaSignature + +# an R-Administrator can only modify system-wide users +nethsm --user admin1 user tag operator1 tag1 + +# add an openpgp certificate to the key +nethsm --user admin1 --user operator1 openpgp add --can-sign signing1 "Test signing1 key <test@example.org>" +nethsm --user operator1 key cert get --force signing1 +rpacket dump "$NETHSM_KEY_CERT_OUTPUT_FILE" | grep "Test signing1 key" + +# signing +export NETHSM_KEY_ID=signing1 +``` +--> + +## `signstar-sign` + +The following command takes a signing request, encoded in JSON, and produces a JSON response. +The JSON response contains a `signature` field, which is an armored OpenPGP signature. + +```bash +signstar-sign < ../signstar-request-signature/tests/sample-request.json | jq --raw-output .signature | rsop dearmor | rpacket dump +``` diff --git a/signstar-sign/src/main.rs b/signstar-sign/src/main.rs new file mode 100644 index 00000000..ac606844 --- /dev/null +++ b/signstar-sign/src/main.rs @@ -0,0 +1,143 @@ +use std::{ + collections::HashMap, + fs::read_to_string, + path::{Path, PathBuf}, + str::FromStr, +}; + +use clap::Parser; +use nethsm::{KeyId, Passphrase, UserId, UserRole}; +use nethsm_config::{Config, ConfigInteractivity, ConfigSettings}; +use signstar_request_signature::{Error, Request, Sha512}; + +#[derive(Debug, Parser)] +struct Cli { + #[arg( + env = "NETHSM_AUTH_PASSPHRASE_FILE", + global = true, + help = "The path to a file containing a passphrase for authentication", + long_help = "The path to a file containing a passphrase for authentication + +The passphrase provided in the file must be the one for the user chosen for the command. + +This option can be provided multiple times, which is needed for commands that require multiple roles at once. +With multiple passphrase files ordering matters, as the files are assigned to the respective user provided by the \"--user\" option.", + long, + short + )] + pub auth_passphrase_file: Vec<PassphraseFile>, + + #[arg( + env = "NETHSM_CONFIG", + global = true, + help = "The path to a custom configuration file", + long_help = "The path to a custom configuration file + +If specified, the custom configuration file is used instead of the default configuration file location.", + long, + short + )] + pub config: Option<PathBuf>, + + #[arg( + env = "NETHSM_USER", + global = true, + help = "A user name which is used for the command", + long_help = "A user name which is used for a command + +Can be provided, if no user name is setup in the configuration file for a device. +Must be provided, if several user names of the same target role are setup in the configuration file for a device. + +This option can be provided multiple times, which is needed for commands that require multiple roles at once. +", + long, + short + )] + pub user: Vec<UserId>, + + #[arg( + env = "NETHSM_LABEL", + global = true, + help = "A label uniquely identifying a device in the configuration file", + long_help = "A label uniquely identifying a device in the configuration file + +Must be provided if more than one device is setup in the configuration file.", + long, + short + )] + pub label: Option<String>, + + #[arg(env = "NETHSM_KEY_ID", help = "The ID of the key to use")] + pub key_id: KeyId, +} + +#[derive(Clone, Debug)] +pub struct PassphraseFile { + pub passphrase: Passphrase, +} + +impl PassphraseFile { + pub fn new(path: &Path) -> Result<Self, Error> { + Ok(Self { + passphrase: Passphrase::new(read_to_string(path).unwrap()), + }) + } +} + +impl FromStr for PassphraseFile { + type Err = Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + PassphraseFile::new(&PathBuf::from_str(s).unwrap()) + } +} + +fn main() -> Result<(), Box<dyn std::error::Error>> { + let cli = Cli::parse(); + + let config = Config::new( + ConfigSettings::new( + "nethsm".to_string(), + ConfigInteractivity::NonInteractive, + None, + ), + cli.config.as_deref(), + )?; + + let auth_passphrases: Vec<Passphrase> = cli + .auth_passphrase_file + .iter() + .map(|x| x.passphrase.clone()) + .collect(); + + let nethsm = config + .get_device(cli.label.as_deref())? + .nethsm_with_matching_creds(&[UserRole::Operator], &cli.user, &auth_passphrases)?; + + let req = Request::from_reader(std::io::stdin())?; + + if !req.required.output.is_openpgp_v4() { + Err(Error::InvalidContentSize)?; // FIXME: fix error variant + } + + if req.version.major != 1 { + Err(Error::InvalidContentSize)?; // FIXME: fix error variant + } + + let hasher: Sha512 = req.required.input.try_into()?; + + let signature = nethsm.openpgp_sign_state(&cli.key_id, hasher)?; + + // FIXME: use Response from !148 when it's merged + let response = [ + ("version".into(), "0.0.0".into()), + ("signature".into(), signature), + ] + .iter() + .cloned() + .collect::<HashMap<String, String>>(); + + serde_json::to_writer(std::io::stdout(), &response)?; + + Ok(()) +} -- GitLab