use base64::{Engine as _, engine::general_purpose};
use ed25519_dalek::{VerifyingKey, ed25519};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
#[derive(Debug)]
pub enum KeyringError {
UnsupportedAlgorithm,
ParsingError(base64::DecodeError),
ConversionError(ed25519::Error),
KeyAlreadyExists,
}
pub type PublicKey = Vec<u8>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Algorithm {
Ed25519,
RsaPssSha512,
RsaV1_5Sha256,
HmacSha256,
EcdsaP256Sha256,
EcdsaP384Sha384,
}
impl std::fmt::Display for Algorithm {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Algorithm::Ed25519 => write!(f, "ed25519"),
Algorithm::RsaPssSha512 => write!(f, "rsa-pss-sha512"),
Algorithm::RsaV1_5Sha256 => write!(f, "rsa-pss-sha512"),
Algorithm::HmacSha256 => write!(f, "hmac-sha256"),
Algorithm::EcdsaP256Sha256 => write!(f, "ecdsa-p256-sha256"),
Algorithm::EcdsaP384Sha384 => write!(f, "ecdsa-p384-sha384"),
}
}
}
#[derive(Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kty")]
pub enum Thumbprintable {
EC {
crv: String,
x: String,
y: String,
},
OKP {
crv: String,
x: String,
},
RSA {
e: String,
n: String,
},
#[serde(rename = "oct")]
OCT {
k: String,
},
}
#[derive(Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
pub struct JSONWebKeySet {
pub keys: Vec<Thumbprintable>,
}
impl Thumbprintable {
pub fn b64_thumbprint(&self) -> String {
general_purpose::URL_SAFE_NO_PAD.encode(Sha256::digest(match self {
Thumbprintable::EC { crv, x, y } => {
format!("{{\"crv\":\"{crv}\",\"kty\":\"EC\",\"x\":\"{x}\",\"y\":\"{y}\"}}")
}
Thumbprintable::OKP { crv, x } => {
format!("{{\"crv\":\"{crv}\",\"kty\":\"OKP\",\"x\":\"{x}\"}}")
}
Thumbprintable::RSA { e, n } => {
format!("{{\"e\":\"{e}\",\"kty\":\"RSA\",\"n\":\"{n}\"}}")
}
Thumbprintable::OCT { k } => format!("{{\"k\":\"{k}\",\"kty\":\"oct\"}}"),
}))
}
pub fn public_key(&self) -> Result<Vec<u8>, KeyringError> {
match self {
Thumbprintable::OKP { crv, x } => match crv.as_str() {
"Ed25519" => {
let decoded = general_purpose::URL_SAFE_NO_PAD
.decode(x)
.map_err(KeyringError::ParsingError)?;
VerifyingKey::try_from(decoded.as_slice())
.map(|key| key.to_bytes().to_vec())
.map_err(KeyringError::ConversionError)
}
_ => Err(KeyringError::UnsupportedAlgorithm),
},
_ => Err(KeyringError::UnsupportedAlgorithm),
}
}
pub fn algorithm(&self) -> Result<Algorithm, KeyringError> {
match self {
Thumbprintable::OKP { crv, .. } => match crv.as_str() {
"Ed25519" => Ok(Algorithm::Ed25519),
_ => Err(KeyringError::UnsupportedAlgorithm),
},
_ => Err(KeyringError::UnsupportedAlgorithm),
}
}
}
#[derive(Default, Debug, Clone)]
pub struct KeyRing {
ring: HashMap<String, (Algorithm, PublicKey)>,
}
impl FromIterator<(String, (Algorithm, PublicKey))> for KeyRing {
fn from_iter<T: IntoIterator<Item = (String, (Algorithm, PublicKey))>>(iter: T) -> KeyRing {
KeyRing {
ring: HashMap::from_iter(iter),
}
}
}
impl KeyRing {
pub fn import_raw(
&mut self,
identifier: String,
algorithm: Algorithm,
public_key: Vec<u8>,
) -> bool {
!self.ring.contains_key(&identifier)
&& self
.ring
.insert(identifier, (algorithm, public_key))
.is_none()
}
pub fn rename_key(&mut self, old_identifier: String, new_identifier: String) -> bool {
match self.ring.remove(&old_identifier) {
Some(value) => self.ring.insert(new_identifier, value).is_none(),
None => false,
}
}
pub fn get(&self, identifier: &String) -> Option<&(Algorithm, Vec<u8>)> {
self.ring.get(identifier)
}
pub fn try_import_jwk(&mut self, jwk: &Thumbprintable) -> Result<(), KeyringError> {
let thumbprint = jwk.b64_thumbprint();
let public_key = jwk.public_key()?;
let algorithm = jwk.algorithm()?;
if !self.import_raw(thumbprint, algorithm, public_key) {
return Err(KeyringError::KeyAlreadyExists);
}
Ok(())
}
pub fn import_jwks(&mut self, jwks: JSONWebKeySet) -> Vec<Option<KeyringError>> {
jwks.keys
.iter()
.map(|jwk| self.try_import_jwk(jwk).err())
.collect::<Vec<_>>()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_importing_ed25519_key_from_jwks() {
let mut keyring = KeyRing::default();
let jwks: JSONWebKeySet = serde_json::from_str(r#"{"keys":[{"kty":"OKP","crv":"Ed25519","kid":"test-key-ed25519","d":"n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU","x":"JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"}]}"#).unwrap();
for (index, result) in keyring.import_jwks(jwks).into_iter().enumerate() {
assert_eq!(index, 0);
assert!(result.is_none());
}
assert!(
keyring
.get(&String::from("poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U"))
.is_some()
);
assert!(keyring.rename_key(
String::from("poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U"),
String::from("test-key-ed25519")
));
assert!(keyring.get(&String::from("test-key-ed25519")).is_some());
}
}