Skip to content

Commit

Permalink
feat(jwk-util): Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
TheButlah committed Nov 18, 2024
1 parent b3dbbe4 commit c167f01
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 5 deletions.
26 changes: 26 additions & 0 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ members = [
"endpoints",
"header-parsing",
"hil",
"jwk-util",
"mcu-interface",
"mcu-util",
"qr-link",
Expand Down Expand Up @@ -47,15 +48,19 @@ rust-version = "1.81.0" # See rust-toolchain.toml
# prevent edge cases where CI doesn't catch build errors due to more features
# being present in a --all vs -p build.
[workspace.dependencies]
base64 = "0.22.1"
bytes = "1.7.1"
clap = { version = "4.5", features = ["derive"] }
color-eyre = "0.6.2"
console-subscriber = "0.4"
data-encoding = "2.3"
derive_more = { version = "0.99", default-features = false, features = ["display", "from"] }
ed25519-dalek = { version = "2.1.1", default-features = false }
eyre = "0.6.12"
ftdi-embedded-hal = { version = "0.22.0", features = ["libftd2xx", "libftd2xx-static"] }
futures = "0.3.30"
hex-literal = "0.4.1"
jose-jwk = { version = "0.1.2", default-features = false }
libc = "0.2.153"
nix = { version = "0.28", default-features = false, features = [] }
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "stream"] }
Expand Down
28 changes: 28 additions & 0 deletions jwk-util/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "orb-jwk-util"
version = "0.0.0"
authors = ["Ryan Butler <[email protected]>"]
description = "CLI utility for manipulating Json Web Keys"
publish = false

edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true

[dependencies]
clap = { workspace = true, features = ["derive"] }
color-eyre.workspace = true
ed25519-dalek = { workspace = true, features = ["pkcs8", "pem", "std"] }
jose-jwk.workspace = true
orb-build-info.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true

[build-dependencies]
orb-build-info = { workspace = true, features = ["build-script"] }

[dev-dependencies]
base64.workspace = true
hex-literal.workspace = true
pkcs8 = "0.10.2"
3 changes: 3 additions & 0 deletions jwk-util/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
orb_build_info::initialize().expect("failed to initialize");
}
139 changes: 139 additions & 0 deletions jwk-util/src/conversions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum ExportPrivKeys {
True,
False,
}

pub fn dalek_signing_key_to_jwk(
signing_key: &ed25519_dalek::SigningKey,
export_priv_keys: ExportPrivKeys,
) -> jose_jwk::Jwk {
let signing_key_bytes = signing_key.as_bytes().to_vec().into_boxed_slice();
assert_eq!(signing_key_bytes.len(), ed25519_dalek::SECRET_KEY_LENGTH);
let pub_key_bytes = signing_key
.verifying_key()
.as_bytes()
.to_vec()
.into_boxed_slice();
assert_eq!(pub_key_bytes.len(), ed25519_dalek::PUBLIC_KEY_LENGTH);

let okp = jose_jwk::Key::Okp(jose_jwk::Okp {
crv: jose_jwk::OkpCurves::Ed25519,
x: pub_key_bytes.into(),
d: (export_priv_keys == ExportPrivKeys::True)
.then_some(signing_key_bytes.into()),
});
jose_jwk::Jwk {
key: okp,
prm: jose_jwk::Parameters::default(),
}
}

#[cfg(test)]
mod test {
use super::*;
use base64::Engine as _;
use jose_jwk::Jwk;

use ed25519_dalek::pkcs8::DecodePublicKey as _;
use pkcs8::DecodePrivateKey as _;

#[test]
fn test_signing_key_to_jwk_with_rfc_test_vector() {
// arrange
// See https://datatracker.ietf.org/doc/html/rfc8037#appendix-A.2
let mut rfc_example = serde_json::json! ({
"kty": "OKP",
"crv": "Ed25519",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", // Public part
"d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", // Private part
});

// Sanity checks
let pubkey_bytes = hex_literal::hex!(
"d7 5a 98 01 82 b1 0a b7 d5 4b fe d3 c9 64 07 3a
0e e1 72 f3 da a6 23 25 af 02 1a 68 f7 07 51 1a"
);
let privkey_bytes = hex_literal::hex!(
"9d 61 b1 9d ef fd 5a 60 ba 84 4a f4 92 ec 2c c4
44 49 c5 69 7b 32 69 19 70 3b ac 03 1c ae 7f 60"
);
assert_eq!(
base64::prelude::BASE64_URL_SAFE_NO_PAD
.decode(rfc_example["x"].as_str().unwrap())
.unwrap(),
pubkey_bytes,
"sanity check: example pubkey bytes should match, they come from the RFC itself"
);
assert_eq!(
base64::prelude::BASE64_URL_SAFE_NO_PAD
.decode(rfc_example["d"].as_str().unwrap())
.unwrap(),
privkey_bytes,
"sanity check: example privkey bytes should match, they come from the RFC itself"
);
let expected_privkey: Jwk =
serde_json::from_value(rfc_example.clone()).unwrap();
rfc_example["d"].take(); // delete priv key
let expected_pubkey: Jwk = serde_json::from_value(rfc_example).unwrap();

// act + assert
let signing_key = ed25519_dalek::SigningKey::from_bytes(&privkey_bytes);
assert_eq!(
dalek_signing_key_to_jwk(&signing_key, ExportPrivKeys::True),
expected_privkey
);
assert_eq!(
dalek_signing_key_to_jwk(&signing_key, ExportPrivKeys::False),
expected_pubkey
);
}

// These keys were randomly generated by https://jwkset.com/generate
#[test]
fn test_known_pem_matches_jwk() {
let mut expected_jwk = serde_json::json! ({
"kty": "OKP",
"crv": "Ed25519",
"x": "qhVpW12CnO55bQ2625kaWNCz9Uh5SNk7bctS9ieVgL0",
"d": "hVtClEJp0nLXm-ToFB6WLUe0Pnj9A_lrhAky1lVXQ_k"
});
let expected_privkey: Jwk =
serde_json::from_value(expected_jwk.clone()).unwrap();
expected_jwk["d"].take(); // delete priv key
let expected_pubkey: Jwk = serde_json::from_value(expected_jwk).unwrap();

let expected_pem_pub = r#"
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAqhVpW12CnO55bQ2625kaWNCz9Uh5SNk7bctS9ieVgL0=
-----END PUBLIC KEY-----"#;

let expected_pem_priv = r#"
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIIVbQpRCadJy15vk6BQeli1HtD54/QP5a4QJMtZVV0P5
-----END PRIVATE KEY-----"#;

// PEM -> Dalek
let signing_key =
ed25519_dalek::SigningKey::from_pkcs8_pem(expected_pem_priv).unwrap();
let verifying_key =
ed25519_dalek::VerifyingKey::from_public_key_pem(expected_pem_pub).unwrap();
assert_eq!(
signing_key.verifying_key(),
verifying_key,
"sanity check: the pub and priv PEM should match"
);

// Dalek -> JWK
assert_eq!(
dalek_signing_key_to_jwk(&signing_key, ExportPrivKeys::True),
expected_privkey,
"converting dalek to jwk should match the expected result"
);
assert_eq!(
dalek_signing_key_to_jwk(&signing_key, ExportPrivKeys::False),
expected_pubkey,
"converting dalek to jwk should match the expected result"
);
}
}
95 changes: 95 additions & 0 deletions jwk-util/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
mod conversions;

use std::io::{Read as _, Write as _};

use clap::{
builder::{styling::AnsiColor, Styles},
command, Parser,
};
use color_eyre::eyre::{ensure, WrapErr as _};
use ed25519_dalek::pkcs8::DecodePrivateKey as _;

use crate::conversions::{dalek_signing_key_to_jwk, ExportPrivKeys};

const BUILD_INFO: orb_build_info::BuildInfo = orb_build_info::make_build_info!();

fn clap_v3_styles() -> Styles {
Styles::styled()
.header(AnsiColor::Yellow.on_default())
.usage(AnsiColor::Green.on_default())
.literal(AnsiColor::Green.on_default())
.placeholder(AnsiColor::Green.on_default())
}

#[derive(Debug, Parser)]
#[command(
author,
version = BUILD_INFO.version,
styles = clap_v3_styles(),
)]
struct Args {
/// The input format
#[clap(long)]
in_fmt: Format,
/// The output format
#[clap(long)]
out_fmt: Format,
/// The file to read as input.
#[clap(long)]
in_file: std::path::PathBuf,
/// The file to write as output.
#[clap(long)]
out_file: Option<std::path::PathBuf>,
/// If provided, the output format will include private keys. Will error if the
/// input format doesn't have any private keys
#[clap(long)]
export_priv_keys: bool,
}

#[derive(Debug, Eq, PartialEq, clap::ValueEnum, Clone, Copy)]
enum Format {
Pkcs8,
Jwk,
}

fn main() -> color_eyre::Result<()> {
let args = Args::parse();

ensure!(
args.in_fmt == Format::Pkcs8,
"todo: input formats other than PKCS8 are not yet supported"
);

let mut pem_contents = String::new();
std::fs::File::open(args.in_file)
.wrap_err("failed to open in_file")?
.read_to_string(&mut pem_contents)
.wrap_err("error while reading file")?;

// TODO: We should also support pem public keys
let signing_key = ed25519_dalek::SigningKey::from_pkcs8_pem(&pem_contents)
.wrap_err("failed to parse PEM contents into ed25519 signing key")?;

let jwk = dalek_signing_key_to_jwk(
&signing_key,
if args.export_priv_keys {
ExportPrivKeys::True
} else {
ExportPrivKeys::False
},
);
let jwk_string =
serde_json::to_string(&jwk).wrap_err("failed to serialize jwk to string")?;

if let Some(out_path) = args.out_file {
let mut out_file =
std::fs::File::create(out_path).wrap_err("failed to create output file")?;
out_file
.write_all(jwk_string.as_bytes())
.wrap_err("failed to write to file")?;
} else {
println!("{jwk_string}");
}

Ok(())
}
2 changes: 1 addition & 1 deletion security-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ reqwest = ["dep:reqwest"]

[dependencies]
eyre = "0.6"
hex-literal = "0.4.1"
hex-literal.workspace = true
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-manual-roots"], optional = true }
ring = "0.17.0"
8 changes: 4 additions & 4 deletions update-agent/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ rust-version.workspace = true
skip-manifest-signature-verification = []

[dependencies]
base64 = "0.22.1"
base64.workspace = true
dogstatsd = "0.11.1"
ed25519-dalek = "2.1.1"
ed25519-dalek.workspace = true
gpt.workspace = true
hex-literal = "0.4.1"
jose-jwk = { version = "0.1.2", default-features = false }
hex-literal.workspace = true
jose-jwk = { workspace = true, default-features = false }
once_cell = "1.18.0"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = [ "raw_value" ] }
Expand Down

0 comments on commit c167f01

Please sign in to comment.