use base64ct::LineEnding;
use crate::{Algorithm, Header, JwsSigner, JwsVerifier, RecommendHeaderParams, SigningAlgorithm, ValidateHeaderParams};
use ecdsa::elliptic_curve::pkcs8::{
DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey,
};
use ecdsa::{Signature, SigningKey, VerifyingKey};
use signature::{Signer, Verifier};
macro_rules! impl_es {
(
$( #[$main_attrs:meta] )*
main: $main_ident:ident,
$( #[$public_attrs:meta] )*
public: $public_ident:ident,
curve: $curve_ty:ty
) => {
$( #[$main_attrs] )*
pub struct $main_ident {
key: SigningKey<$curve_ty>
}
$( #[$public_attrs] )*
pub struct $public_ident {
key: VerifyingKey<$curve_ty>
}
impl $main_ident {
pub fn from(key: SigningKey<$curve_ty>) -> Self {
Self {
key
}
}
pub fn parse_pem(key: &str) -> ecdsa::elliptic_curve::pkcs8::Result<Self> {
SigningKey::from_pkcs8_pem(key).map(Self::from)
}
pub fn encode_as_pkcs8_pem(&self) -> ecdsa::elliptic_curve::pkcs8::Result<ecdsa::elliptic_curve::pkcs8::der::zeroize::Zeroizing<String>> {
EncodePrivateKey::to_pkcs8_pem(&self.key, LineEnding::default())
}
#[cfg(feature = "rand")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
pub fn new_rand<R>(rng: &mut R) -> Self
where
R: rand_core::CryptoRngCore
{
Self {
key: SigningKey::random(rng)
}
}
pub fn public(&self) -> $public_ident {
$public_ident::from(self.key.verifying_key().clone())
}
}
impl $public_ident {
pub fn from(key: VerifyingKey<$curve_ty>) -> Self {
Self {
key
}
}
pub fn parse_pem(key: &str) -> ecdsa::elliptic_curve::pkcs8::spki::Result<Self> {
VerifyingKey::from_public_key_pem(key).map(Self::from)
}
pub fn encode_as_pkcs8_pem(&self) -> ecdsa::elliptic_curve::pkcs8::spki::Result<String> {
EncodePublicKey::to_public_key_pem(&self.key, LineEnding::default())
}
}
impl RecommendHeaderParams for $main_ident {
fn alg(&self) -> Algorithm {
Algorithm::Signing(SigningAlgorithm::$main_ident)
}
}
impl JwsSigner for $main_ident {
fn sign(&self, data: &[u8]) -> Vec<u8> {
let signature: Signature<$curve_ty> = Signer::sign(&self.key, data);
signature.to_vec()
}
}
impl ValidateHeaderParams for $main_ident {
fn validate_header(&self, header: &Header) -> bool {
header.algorithm == SigningAlgorithm::$main_ident
}
}
impl JwsVerifier for $main_ident {
fn verify_signature(&self, data: &[u8], signature: &[u8]) -> bool {
let Ok(signature) = Signature::<$curve_ty>::from_slice(signature) else { return false };
Verifier::verify(self.key.verifying_key(), data, &signature).is_ok()
}
}
impl ValidateHeaderParams for $public_ident {
fn validate_header(&self, header: &Header) -> bool {
header.algorithm == SigningAlgorithm::$main_ident
}
}
impl JwsVerifier for $public_ident {
fn verify_signature(&self, data: &[u8], signature: &[u8]) -> bool {
let Ok(signature) = Signature::<$curve_ty>::from_slice(signature) else { return false };
Verifier::verify(&self.key, data, &signature).is_ok()
}
}
};
}
impl_es!(
main: ES256,
public: ES256Public,
curve: p256::NistP256
);
impl_es!(
main: ES384,
public: ES384Public,
curve: p384::NistP384
);
#[cfg(test)]
mod tests {
use super::*;
use crate::repr;
use ecdsa::elliptic_curve::pkcs8::{DecodePrivateKey, DecodePublicKey};
macro_rules! test_ecdsa {
(
$private_ty:path = $private_key:expr,
$public_ty:path = $public_key:expr =>
data: $data:expr,
sig: $signature:expr
) => {{
{
let data: &[u8] = { $data }.as_ref();
let signature =
repr::decode_bytes_from_base64url($signature).expect("invalid signature");
let private_instance = <$private_ty>::from($private_key);
let created_signature = private_instance.sign(data);
let public_instance = <$public_ty>::from($public_key);
assert!(
public_instance.verify_signature(data, &signature),
"Precalculated signature does not match"
);
assert!(
public_instance.verify_signature(data, &created_signature),
"Precalculated signature does not match"
);
}
}};
}
#[test]
fn es256() {
let jwtio_private_key: SigningKey<p256::NistP256> =
SigningKey::from_pkcs8_pem(JWTIO_PRIVATE_KEY_ES256)
.expect("Could not decode signing key");
let jwtio_public_key: VerifyingKey<p256::NistP256> =
VerifyingKey::from_public_key_pem(JWTIO_PUBLIC_KEY_ES256)
.expect("Could not decode verifying key");
test_ecdsa!(
ES256 = jwtio_private_key.clone(), ES256Public = jwtio_public_key.clone() =>
data: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0",
sig: "tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA"
);
}
#[test]
fn es384() {
let jwtio_private_key: SigningKey<p384::NistP384> =
SigningKey::from_pkcs8_pem(JWTIO_PRIVATE_KEY_ES384)
.expect("Could not decode signing key");
let jwtio_public_key: VerifyingKey<p384::NistP384> =
VerifyingKey::from_public_key_pem(JWTIO_PUBLIC_KEY_ES384)
.expect("Could not decode verifying key");
test_ecdsa!(
ES384 = jwtio_private_key.clone(), ES384Public = jwtio_public_key.clone() =>
data: "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0",
sig: "VUPWQZuClnkFbaEKCsPy7CZVMh5wxbCSpaAWFLpnTe9J0--PzHNeTFNXCrVHysAa3eFbuzD8_bLSsgTKC8SzHxRVSj5eN86vBPo_1fNfE7SHTYhWowjY4E_wuiC13yoj"
);
}
const JWTIO_PRIVATE_KEY_ES256: &str = "-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
-----END PRIVATE KEY-----";
const JWTIO_PUBLIC_KEY_ES256: &str = "-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----";
const JWTIO_PRIVATE_KEY_ES384: &str = "-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCAHpFQ62QnGCEvYh/p
E9QmR1C9aLcDItRbslbmhen/h1tt8AyMhskeenT+rAyyPhGhZANiAAQLW5ZJePZz
MIPAxMtZXkEWbDF0zo9f2n4+T1h/2sh/fviblc/VTyrv10GEtIi5qiOy85Pf1RRw
8lE5IPUWpgu553SteKigiKLUPeNpbqmYZUkWGh3MLfVzLmx85ii2vMU=
-----END PRIVATE KEY-----";
const JWTIO_PUBLIC_KEY_ES384: &str = "-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEC1uWSXj2czCDwMTLWV5BFmwxdM6PX9p+
Pk9Yf9rIf374m5XP1U8q79dBhLSIuaojsvOT39UUcPJROSD1FqYLued0rXiooIii
1D3jaW6pmGVJFhodzC31cy5sfOYotrzF
-----END PUBLIC KEY-----";
}