-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
301 additions
and
5 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
fn main() { | ||
orb_build_info::initialize().expect("failed to initialize"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters