1
0
Fork 0
mirror of https://codeberg.org/tyy/aspm synced 2024-12-22 20:39:29 -07:00

Allow key imports from asp-tool

This commit is contained in:
Tyler Beckman 2024-03-10 21:54:30 -06:00
parent 8af322b2ad
commit 00ffbdce8d
Signed by: Ty
GPG key ID: 2813440C772555A4
6 changed files with 96 additions and 23 deletions

View file

@ -72,7 +72,10 @@ mod tests {
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).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!(
decoded.is_ok(),
"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())
.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!(
decoded.is_ok(),
"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())
.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!(
decoded.is_ok(),
"ASPE request JWS should verify and decode successfully"

View file

@ -45,8 +45,8 @@ mod tests {
use hex_color::HexColor;
use josekit::jwk::Jwk;
use serde_email::Email;
use url::Url;
use serde_email::Email;
use url::Url;
use crate::{
keys::AspKey,
@ -83,8 +83,10 @@ use url::Url;
let key = TryInto::<AspKey>::try_into(Jwk::from_bytes(crate::test_constants::KEY).unwrap())
.unwrap();
let profile =
AriadneSignatureProfile::decode_and_verify(crate::test_constants::PROFILE, Some(&key.fingerprint));
let profile = AriadneSignatureProfile::decode_and_verify(
crate::test_constants::PROFILE,
Some(&key.fingerprint),
);
assert!(profile.is_ok(), "Profile should parse and verify correctly");
let (_, profile) = profile.unwrap();
assert_eq!(

View file

@ -20,7 +20,10 @@ pub trait JwtSerializable {}
pub trait JwtSerialize {
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
Self: for<'de> Deserialize<'de> + JwtSerializable,
S: AsRef<str>;
@ -80,10 +83,13 @@ impl<O: JwtSerializable + Serialize + for<'de> Deserialize<'de>> JwtSerialize fo
.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
Self: for<'de> serde::Deserialize<'de> + JwtSerializable,
S: AsRef<str>
S: AsRef<str>,
{
// Decode the header and check if the key id is correct
let header = jwt::decode_header(jwt).or(Err(JwtDeserializationError::HeaderDecodeError))?;

View file

@ -10,32 +10,32 @@ use clap::{Parser, ValueEnum};
use data_encoding::{BASE64, BASE64_NOPAD};
use dialoguer::{theme::ColorfulTheme, Password};
use josekit::jwk::Jwk;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use crate::{
commands::{AspmSubcommand, KeysEntityExt, KeysQueryResult},
entities::prelude::*,
};
#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
#[serde(tag = "alg", rename = "scrypt")]
pub struct ASPToolScryptExport {
#[serde(rename = "prm")]
parameters: ASPToolScryptExportParam,
pub parameters: ASPToolScryptExportParam,
#[serde(rename = "slt")]
salt: String,
pub salt: String,
#[serde(rename = "key")]
encrypted_key: String,
pub encrypted_key: String,
}
#[derive(Serialize)]
#[derive(Serialize, Deserialize)]
pub struct ASPToolScryptExportParam {
#[serde(rename = "N")]
cost: u64,
pub cost: u64,
#[serde(rename = "r")]
block_size: u32,
pub block_size: u32,
#[serde(rename = "p")]
parallelism: u32,
pub parallelism: u32,
}
#[derive(ValueEnum, Debug, Clone)]

View file

@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use asp::keys::AspKey;
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 aes_gcm::{
@ -13,7 +13,10 @@ use indoc::printdoc;
use josekit::jwk::Jwk;
use sea_orm::{ActiveValue, EntityTrait};
use crate::{commands::AspmSubcommand, entities::keys};
use crate::{
commands::{keys::export::ASPToolScryptExport, AspmSubcommand},
entities::keys,
};
#[derive(ValueEnum, Debug, Clone)]
pub enum KeyImportFormat {
@ -22,13 +25,15 @@ pub enum KeyImportFormat {
// Imports a PGP key from a local GPG store
#[cfg(feature = "gpg-compat")]
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.
#[derive(Parser, Debug)]
pub struct KeysImportCommand {
/// The format of key to import.
///
///
format: KeyImportFormat,
/// The key to import, as a string.
key: String,
@ -236,6 +241,53 @@ impl AspmSubcommand for KeysImportCommand {
.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(

View file

@ -143,7 +143,11 @@ fn cli(parsed: AspmCommand) -> Result<(), anyhow::Error> {
.context("Unable to check database for keys table")?);
// 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)
})?;