use biscuit::{
jwa::SignatureAlgorithm,
jws::{Compact, Header, RegisteredHeader, Secret},
};
use clap::ValueEnum;
use ring::signature::KeyPair;
use serde::{Deserialize, Serialize};
use strum_macros::{EnumString, IntoStaticStr, VariantNames};
use crate::{Assertion, box_err::BoxResult, process::Error};
#[derive(Debug, Eq, PartialEq, Clone, Copy, Default)]
pub enum AlgorithmType {
#[default]
None,
RSA,
ECDSA,
}
#[derive(
Debug, ValueEnum, EnumString, VariantNames, IntoStaticStr, Eq, PartialEq, Clone, Copy, Default,
)]
pub enum Algorithm {
#[default]
None,
RS256,
RS384,
RS512,
ES256,
ES384,
}
impl Algorithm {
#[must_use]
pub const fn to_sig_alg(self) -> SignatureAlgorithm {
match self {
Self::None => SignatureAlgorithm::None,
Self::RS256 => SignatureAlgorithm::RS256,
Self::RS384 => SignatureAlgorithm::RS384,
Self::RS512 => SignatureAlgorithm::RS512,
Self::ES256 => SignatureAlgorithm::ES256,
Self::ES384 => SignatureAlgorithm::ES384,
}
}
#[must_use]
pub const fn r#type(self) -> AlgorithmType {
match self {
Self::None => AlgorithmType::None,
Self::RS256 | Self::RS384 | Self::RS512 => AlgorithmType::RSA,
Self::ES256 | Self::ES384 => AlgorithmType::ECDSA,
}
}
}
fn biscuit_to_process_err(err: &biscuit::errors::Error, key_file: &str) -> Error {
let hint = if let biscuit::errors::Error::KeyRejected(_key_rejected) = &err {
Some(
"
Often, keys generated for use in OpenSSL-based software are
encoded in PEM format, which is not supported by *ring*. PEM-encoded
keys can be re-encoded into DER using an OpenSSL command like this:
```sh
# RSA
openssl rsa -in private_key.pem -outform DER -out private_key.der
# ECDSA
openssl ec -in private_key.pem -outform DER -out private_key.der
```
",
)
} else {
None
};
Error::InvalidSigningPrivateKey {
msg: format!(
"Failed to decode a valid crypto key from '{key_file}': {err:#?}{}",
hint.unwrap_or_default(),
),
}
}
pub fn load_private_key_pair(alg: Algorithm, key_file: impl AsRef<str>) -> BoxResult<Secret> {
match alg.r#type() {
AlgorithmType::None => Err(
"If you try to sign (indicated by suplying a private key), you also have to specify the signing algorithm explicitly (for security reasons)",
)?,
AlgorithmType::RSA => {
log::info!(
"Trying to read RSA private key from file {} ...",
key_file.as_ref()
);
Secret::rsa_keypair_from_file(key_file.as_ref())
.map_err(|err| biscuit_to_process_err(&err, key_file.as_ref()).into())
}
AlgorithmType::ECDSA => {
log::info!(
"Trying to read ECDSA private key from file {} ...",
key_file.as_ref()
);
Secret::ecdsa_keypair_from_file(alg.to_sig_alg(), key_file.as_ref())
.map_err(|err| biscuit_to_process_err(&err, key_file.as_ref()).into())
}
}
}
fn extract_public_key(alg: Algorithm, key_pair_priv: &Secret) -> BoxResult<Secret> {
Ok(match alg.r#type() {
AlgorithmType::None => todo!(),
AlgorithmType::RSA => {
if let Secret::RsaKeyPair(key_pair_priv_inner) = key_pair_priv {
Secret::PublicKey(key_pair_priv_inner.public_key().as_ref().to_owned())
} else {
return Err("Given secret is not an RSA private key(-pair)!")?;
}
}
AlgorithmType::ECDSA => {
if let Secret::EcdsaKeyPair(key_pair_priv_inner) = key_pair_priv {
Secret::PublicKey(key_pair_priv_inner.public_key().as_ref().to_owned())
} else {
return Err("Given secret is not an ECDSA private key(-pair)!")?;
}
}
})
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct HeaderExtensions {
#[serde(rename = "kty", skip_serializing_if = "Option::is_none")]
key_type: Option<String>,
#[serde(rename = "use", skip_serializing_if = "Option::is_none")]
r#use: Option<String>,
}
pub fn sign_with_cert(
badge_assertion: Assertion,
alg: Algorithm,
secret_key: &Secret,
x509_chain: Option<Vec<String>>,
) -> BoxResult<String> {
let r#use = x509_chain.as_ref().map(|_| "sig".to_string());
let header = RegisteredHeader {
algorithm: alg.to_sig_alg(),
media_type: Some("JOSE+JSON".to_string()),
x509_chain,
..Default::default()
};
let header_ext = HeaderExtensions {
key_type: Some("RSA".to_string()),
r#use,
};
let header = Header {
registered: header,
private: header_ext,
};
let compact = Compact::new_decoded(header, badge_assertion);
let encoded = compact.into_encoded(secret_key)?;
Ok(match encoded {
Compact::Decoded {
header: _,
payload: _,
} => panic!("This can never happen"),
Compact::Encoded(parts) => parts.encode(),
})
}
pub fn sign(badge_assertion: Assertion, alg: Algorithm, secret_key: &Secret) -> BoxResult<String> {
sign_with_cert(badge_assertion, alg, secret_key, None)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Assertion;
use crate::Identity;
use crate::IdentityType;
use crate::Verification;
use crate::VerificationType;
use crate::{box_err::BoxResult, constants};
use chrono::DateTime;
fn sign_and_verify(
badge_assertion: Assertion,
alg: Algorithm,
secret_key: &Secret,
) -> BoxResult<()> {
let encoded = sign(badge_assertion, alg, secret_key)?;
let encoded_parsed: Compact<Assertion, biscuit::Empty> = Compact::new_encoded(&encoded);
let public_key = extract_public_key(alg, secret_key)?;
let _decoded = encoded_parsed.decode(&public_key, alg.to_sig_alg())?;
Ok(())
}
#[test]
fn test_sign() -> BoxResult<()> {
let mut badge_assert = Assertion::new(
constants::BADGE_ASSERTION_WITH_KEY_ID,
constants::BADGE_DEFINITION_WITH_KEY_ID,
Identity {
r#type: IdentityType::EMail,
hashed: true,
identity: constants::BADGE_ASSERTION_RECIPIENT_EMAIL_HASH_SALTED.clone(),
salt: Some(constants::BADGE_ASSERTION_RECIPIENT_SALT.to_string()),
},
Verification {
r#type: VerificationType::SignedBadge {
creator: Some(constants::ISSUER_KEY_ID.to_string()),
},
verification_property: None,
starts_with: None,
allowed_origins: None,
},
DateTime::parse_from_rfc3339(constants::DT_PAST)?,
);
badge_assert.expires = Some(DateTime::parse_from_rfc3339(constants::DT_FAR_FUTURE)?.into());
let alg = Algorithm::ES256;
let key_pair_priv = load_private_key_pair(alg, constants::ISSUER_KEY_PATH_PRIV)?;
sign_and_verify(badge_assert, alg, &key_pair_priv)?;
Ok(())
}
}