Skip to content

Commit

Permalink
feat(sign): Add sign command
Browse files Browse the repository at this point in the history
  • Loading branch information
passcod committed Feb 10, 2024
1 parent 87e5b6d commit 1957417
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 2 deletions.
85 changes: 85 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ clap_complete_nushell = "4.5.0"
dirs = "5.0.1"
duct = "0.13.7"
glob = "0.3.1"
hex = { version = "0.4.3", optional = true }
humantime = "2.1.0"
indicatif = { version = "0.17.7", features = ["tokio"] }
ip_network = { version = "0.4.1", optional = true }
Expand All @@ -36,6 +37,7 @@ leon-macros = { version = "1.0.0", optional = true }
local-ip-address = { version = "0.5.7", optional = true }
miette = { version = "5.10.0", features = ["fancy"] }
mimalloc = "0.1.39"
minisign = { version = "0.7.6", optional = true }
node-semver = "2.1.0"
regex = "1.10.3"
serde = { version = "1.0.195", features = ["derive"] }
Expand All @@ -53,14 +55,14 @@ build-data = "0.1.5"
windows_exe_info = { version = "0.4.1", features = ["manifest"] }

[features]
default = ["dyndns", "tamanu", "upload"]
default = ["dyndns", "sign", "tamanu", "upload"]

## Common dep groups (not meant to be used directly)
aws = ["dep:aws-config", "dep:aws-credential-types", "dep:aws-sdk-route53", "dep:aws-sdk-s3", "dep:aws-sdk-sts"]

## Subcommands
dyndns = ["aws", "dep:local-ip-address", "dep:ip_network"]
tamanu = []
sign = ["dep:hex", "dep:leon", "dep:minisign"]
tamanu = ["dep:leon", "dep:leon-macros"]
upload = ["aws"]
wifisetup = ["dep:networkmanager"]
Expand Down
6 changes: 6 additions & 0 deletions src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub mod completions;
pub mod context;
#[cfg(feature = "dyndns")]
pub mod dyndns;
#[cfg(feature = "sign")]
pub mod sign;
#[cfg(feature = "tamanu")]
pub mod tamanu;
#[cfg(feature = "upload")]
Expand All @@ -23,6 +25,8 @@ pub enum Action {
Completions(completions::CompletionsArgs),
#[cfg(feature = "dyndns")]
Dyndns(dyndns::DyndnsArgs),
#[cfg(feature = "sign")]
Sign(sign::SignArgs),
#[cfg(feature = "tamanu")]
Tamanu(tamanu::TamanuArgs),
#[cfg(feature = "upload")]
Expand All @@ -40,6 +44,8 @@ pub async fn run() -> Result<()> {
(Action::Completions(args), ctx) => completions::run(ctx.with_top(args)).await,
#[cfg(feature = "dyndns")]
(Action::Dyndns(args), ctx) => dyndns::run(ctx.with_top(args)).await,
#[cfg(feature = "sign")]
(Action::Sign(args), ctx) => sign::run(ctx.with_top(args)).await,
#[cfg(feature = "tamanu")]
(Action::Tamanu(args), ctx) => tamanu::run(ctx.with_top(args)).await,
#[cfg(feature = "upload")]
Expand Down
126 changes: 126 additions & 0 deletions src/actions/sign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use std::{fs::read_to_string, path::PathBuf};

use base64ct::{Base64, Encoding};
use clap::{Parser, Subcommand};
use miette::{bail, IntoDiagnostic, Result};
use minisign::{SecretKey, SecretKeyBox};

use super::Context;

pub mod files;

/// Sign and verify files.
#[derive(Debug, Clone, Parser)]
pub struct SignArgs {
/// Sign subcommand
#[command(subcommand)]
pub action: SignAction,
}

#[derive(Debug, Clone, Subcommand)]
pub enum SignAction {
Files(files::FilesArgs),
}

pub async fn run(ctx: Context<SignArgs>) -> Result<()> {
match ctx.args_top.action.clone() {
SignAction::Files(subargs) => files::run(ctx.with_sub(subargs)).await,
}
}

#[derive(Debug, Clone, Parser)]
pub(crate) struct KeyArgs {
/// The secret key to sign with.
///
/// Prefer to use `--key-file` or `--key-env` instead of this.
#[arg(long, value_name = "KEY", required_unless_present_any = &["key_file", "key_env"])]
pub key: Option<String>,

/// The secret key to sign with, read from a file.
#[arg(long, value_name = "FILE", required_unless_present_any = &["key", "key_env"])]
pub key_file: Option<PathBuf>,

/// The secret key to sign with, read from an environment variable.
#[arg(long, value_name = "ENVVAR", required_unless_present_any = &["key", "key_file"])]
pub key_env: Option<String>,

/// The password in plain text to decrypt the secret key, if it's encrypted.
///
/// Prefer to use `--password-file` or `--password-env` instead of this.
#[arg(long, value_name = "KEY", conflicts_with_all = &["password_file", "password_env"])]
pub password: Option<String>,

/// The secret key's password, read from a file.
#[arg(long, value_name = "FILE", conflicts_with_all = &["password", "password_env"])]
pub password_file: Option<PathBuf>,

/// The secret key's password, read from an environment variable.
#[arg(long, value_name = "ENVVAR", conflicts_with_all = &["password", "password_file"])]
pub password_env: Option<String>,

/// Prompt for the password interactively.
///
/// Do not use this in scripts or CI.
#[arg(long)]
pub password_prompt: bool,
}

impl KeyArgs {
fn read(&self) -> Result<SecretKey> {
let password = self.read_password()?;
self.read_key(password)
}

fn read_password(&self) -> Result<Option<String>> {
// TODO: zero-box the password to avoid it lingering in memory
match &self {
Self {
password_prompt: true,
..
} => Ok(None),
Self {
password: Some(pass),
..
} => Ok(Some(pass.into())),
Self {
password_env: Some(env),
..
} => std::env::var(env).into_diagnostic().map(Some),
Self {
password_file: Some(file),
..
} => read_to_string(file).into_diagnostic().map(Some),
_ => Ok(Some("".into())), // no password
}
}

fn read_key(&self, password: Option<String>) -> Result<SecretKey> {
match &self {
Self { key: Some(key), .. } => Self::from_string(key, password),
Self {
key_env: Some(env), ..
} => Self::from_string(&std::env::var(env).into_diagnostic()?, password),
Self {
key_file: Some(file),
..
} => {
// we'll always assume it's a full minisign key file
SecretKey::from_file(file, password).into_diagnostic()
}
_ => bail!("exactly one of --key, --key-file, or --key-env must be provided"),
}
}

fn from_string(s: &str, password: Option<String>) -> Result<SecretKey> {
// try parsing as the raw key as base64 first
if let Ok(key) = Base64::decode_vec(s) {
return Ok(SecretKey::from_bytes(&key).into_diagnostic()?);
}

// then as the full minisign key file
Ok(SecretKeyBox::from_string(s)
.into_diagnostic()?
.into_secret_key(password)
.into_diagnostic()?)
}
}
Loading

0 comments on commit 1957417

Please sign in to comment.