mirror of
https://codeberg.org/tyy/aspm
synced 2024-12-23 01:19:28 -07:00
Allow key imports from asp-tool
This commit is contained in:
parent
8af322b2ad
commit
00ffbdce8d
6 changed files with 96 additions and 23 deletions
|
@ -72,7 +72,10 @@ mod tests {
|
||||||
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
|
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let decoded = AspeRequest::decode_and_verify(crate::test_constants::CREATE_REQUEST, Some(&key.fingerprint));
|
let decoded = AspeRequest::decode_and_verify(
|
||||||
|
crate::test_constants::CREATE_REQUEST,
|
||||||
|
Some(&key.fingerprint),
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
decoded.is_ok(),
|
decoded.is_ok(),
|
||||||
"ASPE request JWS should verify and decode successfully"
|
"ASPE request JWS should verify and decode successfully"
|
||||||
|
@ -119,7 +122,10 @@ mod tests {
|
||||||
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
|
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let decoded = AspeRequest::decode_and_verify(crate::test_constants::UPDATE_REQUEST, Some(&key.fingerprint));
|
let decoded = AspeRequest::decode_and_verify(
|
||||||
|
crate::test_constants::UPDATE_REQUEST,
|
||||||
|
Some(&key.fingerprint),
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
decoded.is_ok(),
|
decoded.is_ok(),
|
||||||
"ASPE request JWS should verify and decode successfully"
|
"ASPE request JWS should verify and decode successfully"
|
||||||
|
@ -166,7 +172,10 @@ mod tests {
|
||||||
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
|
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let decoded = AspeRequest::decode_and_verify(crate::test_constants::DELETE_REQUEST, Some(&key.fingerprint));
|
let decoded = AspeRequest::decode_and_verify(
|
||||||
|
crate::test_constants::DELETE_REQUEST,
|
||||||
|
Some(&key.fingerprint),
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
decoded.is_ok(),
|
decoded.is_ok(),
|
||||||
"ASPE request JWS should verify and decode successfully"
|
"ASPE request JWS should verify and decode successfully"
|
||||||
|
|
|
@ -45,8 +45,8 @@ mod tests {
|
||||||
|
|
||||||
use hex_color::HexColor;
|
use hex_color::HexColor;
|
||||||
use josekit::jwk::Jwk;
|
use josekit::jwk::Jwk;
|
||||||
use serde_email::Email;
|
use serde_email::Email;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
keys::AspKey,
|
keys::AspKey,
|
||||||
|
@ -83,8 +83,10 @@ use url::Url;
|
||||||
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
|
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let profile =
|
let profile = AriadneSignatureProfile::decode_and_verify(
|
||||||
AriadneSignatureProfile::decode_and_verify(crate::test_constants::PROFILE, Some(&key.fingerprint));
|
crate::test_constants::PROFILE,
|
||||||
|
Some(&key.fingerprint),
|
||||||
|
);
|
||||||
assert!(profile.is_ok(), "Profile should parse and verify correctly");
|
assert!(profile.is_ok(), "Profile should parse and verify correctly");
|
||||||
let (_, profile) = profile.unwrap();
|
let (_, profile) = profile.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -20,7 +20,10 @@ pub trait JwtSerializable {}
|
||||||
|
|
||||||
pub trait JwtSerialize {
|
pub trait JwtSerialize {
|
||||||
fn encode_and_sign(&self, key: &AspKey) -> Result<String, JwtSerializationError>;
|
fn encode_and_sign(&self, key: &AspKey) -> Result<String, JwtSerializationError>;
|
||||||
fn decode_and_verify<S>(jwt: &str, expected_fingerprint: Option<S>) -> Result<(AspKey, Box<Self>), JwtDeserializationError>
|
fn decode_and_verify<S>(
|
||||||
|
jwt: &str,
|
||||||
|
expected_fingerprint: Option<S>,
|
||||||
|
) -> Result<(AspKey, Box<Self>), JwtDeserializationError>
|
||||||
where
|
where
|
||||||
Self: for<'de> Deserialize<'de> + JwtSerializable,
|
Self: for<'de> Deserialize<'de> + JwtSerializable,
|
||||||
S: AsRef<str>;
|
S: AsRef<str>;
|
||||||
|
@ -80,10 +83,13 @@ impl<O: JwtSerializable + Serialize + for<'de> Deserialize<'de>> JwtSerialize fo
|
||||||
.or(Err(JwtSerializationError::SerializationError))
|
.or(Err(JwtSerializationError::SerializationError))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_and_verify<S>(jwt: &str, expected_fingerprint: Option<S>) -> Result<(AspKey, Box<Self>), JwtDeserializationError>
|
fn decode_and_verify<S>(
|
||||||
|
jwt: &str,
|
||||||
|
expected_fingerprint: Option<S>,
|
||||||
|
) -> Result<(AspKey, Box<Self>), JwtDeserializationError>
|
||||||
where
|
where
|
||||||
Self: for<'de> serde::Deserialize<'de> + JwtSerializable,
|
Self: for<'de> serde::Deserialize<'de> + JwtSerializable,
|
||||||
S: AsRef<str>
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
// Decode the header and check if the key id is correct
|
// Decode the header and check if the key id is correct
|
||||||
let header = jwt::decode_header(jwt).or(Err(JwtDeserializationError::HeaderDecodeError))?;
|
let header = jwt::decode_header(jwt).or(Err(JwtDeserializationError::HeaderDecodeError))?;
|
||||||
|
|
|
@ -10,32 +10,32 @@ use clap::{Parser, ValueEnum};
|
||||||
use data_encoding::{BASE64, BASE64_NOPAD};
|
use data_encoding::{BASE64, BASE64_NOPAD};
|
||||||
use dialoguer::{theme::ColorfulTheme, Password};
|
use dialoguer::{theme::ColorfulTheme, Password};
|
||||||
use josekit::jwk::Jwk;
|
use josekit::jwk::Jwk;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
|
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
|
||||||
entities::prelude::*,
|
entities::prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[serde(tag = "alg", rename = "scrypt")]
|
#[serde(tag = "alg", rename = "scrypt")]
|
||||||
pub struct ASPToolScryptExport {
|
pub struct ASPToolScryptExport {
|
||||||
#[serde(rename = "prm")]
|
#[serde(rename = "prm")]
|
||||||
parameters: ASPToolScryptExportParam,
|
pub parameters: ASPToolScryptExportParam,
|
||||||
#[serde(rename = "slt")]
|
#[serde(rename = "slt")]
|
||||||
salt: String,
|
pub salt: String,
|
||||||
#[serde(rename = "key")]
|
#[serde(rename = "key")]
|
||||||
encrypted_key: String,
|
pub encrypted_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ASPToolScryptExportParam {
|
pub struct ASPToolScryptExportParam {
|
||||||
#[serde(rename = "N")]
|
#[serde(rename = "N")]
|
||||||
cost: u64,
|
pub cost: u64,
|
||||||
#[serde(rename = "r")]
|
#[serde(rename = "r")]
|
||||||
block_size: u32,
|
pub block_size: u32,
|
||||||
#[serde(rename = "p")]
|
#[serde(rename = "p")]
|
||||||
parallelism: u32,
|
pub parallelism: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ValueEnum, Debug, Clone)]
|
#[derive(ValueEnum, Debug, Clone)]
|
||||||
|
|
|
@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context};
|
||||||
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||||
use asp::keys::AspKey;
|
use asp::keys::AspKey;
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
use data_encoding::{BASE64URL_NOPAD, BASE64_NOPAD};
|
use data_encoding::{BASE64, BASE64URL_NOPAD, BASE64_NOPAD};
|
||||||
use dialoguer::{theme::ColorfulTheme, Input, Password};
|
use dialoguer::{theme::ColorfulTheme, Input, Password};
|
||||||
|
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
|
@ -13,7 +13,10 @@ use indoc::printdoc;
|
||||||
use josekit::jwk::Jwk;
|
use josekit::jwk::Jwk;
|
||||||
use sea_orm::{ActiveValue, EntityTrait};
|
use sea_orm::{ActiveValue, EntityTrait};
|
||||||
|
|
||||||
use crate::{commands::AspmSubcommand, entities::keys};
|
use crate::{
|
||||||
|
commands::{keys::export::ASPToolScryptExport, AspmSubcommand},
|
||||||
|
entities::keys,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(ValueEnum, Debug, Clone)]
|
#[derive(ValueEnum, Debug, Clone)]
|
||||||
pub enum KeyImportFormat {
|
pub enum KeyImportFormat {
|
||||||
|
@ -22,6 +25,8 @@ pub enum KeyImportFormat {
|
||||||
// Imports a PGP key from a local GPG store
|
// Imports a PGP key from a local GPG store
|
||||||
#[cfg(feature = "gpg-compat")]
|
#[cfg(feature = "gpg-compat")]
|
||||||
Gpg,
|
Gpg,
|
||||||
|
// Imports a secret key in the format used by https://asp.keyoxide.org
|
||||||
|
AspTool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Imports an ASP from raw JWK format. This only will import JWKs that have supported curves.
|
/// Imports an ASP from raw JWK format. This only will import JWKs that have supported curves.
|
||||||
|
@ -236,6 +241,53 @@ impl AspmSubcommand for KeysImportCommand {
|
||||||
.context("Unable to construct Jwk representation of PGP key")?,
|
.context("Unable to construct Jwk representation of PGP key")?,
|
||||||
)?
|
)?
|
||||||
}
|
}
|
||||||
|
KeyImportFormat::AspTool => {
|
||||||
|
let parsed: ASPToolScryptExport = serde_json::from_slice(
|
||||||
|
BASE64
|
||||||
|
.decode(self.key.as_bytes())
|
||||||
|
.context("Unable to base64 decode asp-tool secret key")?
|
||||||
|
.as_slice(),
|
||||||
|
)
|
||||||
|
.context("Unable to parse base64-decoded asp-tool secret key as JSON")?;
|
||||||
|
|
||||||
|
let mut derived_key = vec![0u8; parsed.encrypted_key.len()];
|
||||||
|
scrypt::scrypt(
|
||||||
|
key_password.as_bytes(),
|
||||||
|
&BASE64
|
||||||
|
.decode(parsed.salt.as_bytes())
|
||||||
|
.context("Unable to base64 decode salt from parsed asp-tool secret key")?,
|
||||||
|
&scrypt::Params::new(
|
||||||
|
parsed
|
||||||
|
.parameters
|
||||||
|
.cost
|
||||||
|
.ilog2()
|
||||||
|
.try_into()
|
||||||
|
.context("log_2(N) from parsed asp-tool secret key was not a u8")?,
|
||||||
|
parsed.parameters.block_size,
|
||||||
|
parsed.parameters.parallelism,
|
||||||
|
// This length is unused in the scrypt function itself, so the recommended works as a placeholder
|
||||||
|
scrypt::Params::RECOMMENDED_LEN,
|
||||||
|
)
|
||||||
|
.context(
|
||||||
|
"Unable to assemble scrypt parameters from parsed asp-tool secret key",
|
||||||
|
)?,
|
||||||
|
&mut derived_key,
|
||||||
|
)
|
||||||
|
.context("Unable to derive PKCS#8 encryption key with scrypt")?;
|
||||||
|
|
||||||
|
AspKey::from_pkcs8(
|
||||||
|
&BASE64_NOPAD.encode(
|
||||||
|
BASE64.decode(parsed.encrypted_key.as_bytes())
|
||||||
|
.context("Unable to base64 decode encrypted key from parsed asp-tool secret key")?
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, byte)| byte ^ derived_key[i])
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.as_slice(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.context("Unable to decode decrypted asp-tool key as PKCS#8")?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let argon_salt = SaltString::from_b64(
|
let argon_salt = SaltString::from_b64(
|
||||||
|
|
|
@ -143,7 +143,11 @@ fn cli(parsed: AspmCommand) -> Result<(), anyhow::Error> {
|
||||||
.context("Unable to check database for keys table")?);
|
.context("Unable to check database for keys table")?);
|
||||||
|
|
||||||
// Make the state
|
// Make the state
|
||||||
let state = AspmState { data_dir, db, verbose: parsed.verbose };
|
let state = AspmState {
|
||||||
|
data_dir,
|
||||||
|
db,
|
||||||
|
verbose: parsed.verbose,
|
||||||
|
};
|
||||||
|
|
||||||
Ok::<AspmState, anyhow::Error>(state)
|
Ok::<AspmState, anyhow::Error>(state)
|
||||||
})?;
|
})?;
|
||||||
|
|
Loading…
Reference in a new issue