jwt_compact/alg/
es256k.rs

1//! `ES256K` algorithm implementation using the `secp256k1` crate.
2
3use core::{marker::PhantomData, num::NonZeroUsize};
4use std::sync::LazyLock;
5
6use secp256k1::{
7    All, Message, PublicKey, Secp256k1, SecretKey,
8    constants::{
9        COMPACT_SIGNATURE_SIZE, FIELD_SIZE, SECRET_KEY_SIZE, UNCOMPRESSED_PUBLIC_KEY_SIZE,
10    },
11    ecdsa::Signature,
12};
13use sha2::{
14    Digest, Sha256,
15    digest::{
16        FixedOutputReset, HashMarker,
17        crypto_common::{BlockSizeUser, typenum::U32},
18    },
19};
20
21use crate::{
22    Algorithm, AlgorithmSignature,
23    alg::{SecretBytes, SigningKey, VerifyingKey},
24    alloc::Cow,
25    jwk::{JsonWebKey, JwkError, KeyType},
26};
27
28/// Byte size of a serialized EC coordinate.
29const COORDINATE_SIZE: usize = FIELD_SIZE.len();
30
31impl AlgorithmSignature for Signature {
32    const LENGTH: Option<NonZeroUsize> = NonZeroUsize::new(COMPACT_SIGNATURE_SIZE);
33
34    fn try_from_slice(slice: &[u8]) -> anyhow::Result<Self> {
35        Signature::from_compact(slice).map_err(Into::into)
36    }
37
38    fn as_bytes(&self) -> Cow<'_, [u8]> {
39        Cow::Owned(self.serialize_compact()[..].to_vec())
40    }
41}
42
43/// Algorithm implementing elliptic curve digital signatures (ECDSA) on the secp256k1 curve.
44///
45/// The algorithm does not fix the choice of the message digest algorithm; instead,
46/// it is provided as a type parameter. SHA-256 is the default parameter value,
47/// but it can be set to any cryptographically secure hash function with 32-byte output
48/// (e.g., SHA3-256).
49#[derive(Debug)]
50#[cfg_attr(docsrs, doc(cfg(any(feature = "es256k", feature = "k256"))))]
51pub struct Es256k<D = Sha256> {
52    context: Secp256k1<All>,
53    _digest: PhantomData<D>,
54}
55
56impl<D> Default for Es256k<D>
57where
58    D: FixedOutputReset<OutputSize = U32> + BlockSizeUser + Clone + Default + HashMarker,
59{
60    fn default() -> Self {
61        Es256k {
62            context: Secp256k1::new(),
63            _digest: PhantomData,
64        }
65    }
66}
67
68impl<D> Es256k<D>
69where
70    D: FixedOutputReset<OutputSize = U32> + BlockSizeUser + Clone + Default + HashMarker,
71{
72    /// Creates a new algorithm instance.
73    /// This is a (moderately) expensive operation, so if necessary, the algorithm should
74    /// be `clone()`d rather than created anew.
75    #[cfg_attr(docsrs, doc(cfg(feature = "es256k")))]
76    pub fn new(context: Secp256k1<All>) -> Self {
77        Es256k {
78            context,
79            _digest: PhantomData,
80        }
81    }
82}
83
84impl<D> Algorithm for Es256k<D>
85where
86    D: FixedOutputReset<OutputSize = U32> + BlockSizeUser + Clone + Default + HashMarker,
87{
88    type SigningKey = SecretKey;
89    type VerifyingKey = PublicKey;
90    type Signature = Signature;
91
92    fn name(&self) -> Cow<'static, str> {
93        Cow::Borrowed("ES256K")
94    }
95
96    fn sign(&self, signing_key: &Self::SigningKey, message: &[u8]) -> Self::Signature {
97        let mut digest = D::default();
98        digest.update(message);
99        let message = Message::from_digest(digest.finalize().into());
100
101        self.context.sign_ecdsa(message, signing_key)
102    }
103
104    fn verify_signature(
105        &self,
106        signature: &Self::Signature,
107        verifying_key: &Self::VerifyingKey,
108        message: &[u8],
109    ) -> bool {
110        let mut digest = D::default();
111        digest.update(message);
112        let message = Message::from_digest(digest.finalize().into());
113
114        // Some implementations (e.g., OpenSSL) produce high-S signatures, which
115        // are considered invalid by this implementation. Hence, we perform normalization here.
116        //
117        // See also: https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki
118        let mut normalized_signature = *signature;
119        normalized_signature.normalize_s();
120
121        self.context
122            .verify_ecdsa(message, &normalized_signature, verifying_key)
123            .is_ok()
124    }
125}
126
127/// This implementation initializes a `libsecp256k1` context once on the first call to
128/// `to_verifying_key` if it was not initialized previously.
129impl SigningKey<Es256k> for SecretKey {
130    fn from_slice(raw: &[u8]) -> anyhow::Result<Self> {
131        let raw = <[u8; SECRET_KEY_SIZE]>::try_from(raw)?;
132        Self::from_byte_array(raw).map_err(From::from)
133    }
134
135    fn to_verifying_key(&self) -> PublicKey {
136        static CONTEXT: LazyLock<Secp256k1<All>> = LazyLock::new(Secp256k1::new);
137
138        PublicKey::from_secret_key(&CONTEXT, self)
139    }
140
141    fn as_bytes(&self) -> SecretBytes<'_> {
142        SecretBytes::borrowed(&self[..])
143    }
144}
145
146impl VerifyingKey<Es256k> for PublicKey {
147    fn from_slice(raw: &[u8]) -> anyhow::Result<Self> {
148        Self::from_slice(raw).map_err(From::from)
149    }
150
151    /// Serializes the key as a 33-byte compressed form, as per [`Self::serialize()`].
152    fn as_bytes(&self) -> Cow<'_, [u8]> {
153        Cow::Owned(self.serialize().to_vec())
154    }
155}
156
157fn create_jwk<'a>(pk: &PublicKey, sk: Option<&'a SecretKey>) -> JsonWebKey<'a> {
158    let uncompressed = pk.serialize_uncompressed();
159    JsonWebKey::EllipticCurve {
160        curve: "secp256k1".into(),
161        x: Cow::Owned(uncompressed[1..=COORDINATE_SIZE].to_vec()),
162        y: Cow::Owned(uncompressed[(1 + COORDINATE_SIZE)..].to_vec()),
163        secret: sk.map(|sk| SecretBytes::borrowed(&sk.as_ref()[..])),
164    }
165}
166
167impl<'a> From<&'a PublicKey> for JsonWebKey<'a> {
168    fn from(key: &'a PublicKey) -> JsonWebKey<'a> {
169        create_jwk(key, None)
170    }
171}
172
173impl TryFrom<&JsonWebKey<'_>> for PublicKey {
174    type Error = JwkError;
175
176    fn try_from(jwk: &JsonWebKey<'_>) -> Result<Self, Self::Error> {
177        let JsonWebKey::EllipticCurve { curve, x, y, .. } = jwk else {
178            return Err(JwkError::key_type(jwk, KeyType::EllipticCurve));
179        };
180        JsonWebKey::ensure_curve(curve, "secp256k1")?;
181        JsonWebKey::ensure_len("x", x, COORDINATE_SIZE)?;
182        JsonWebKey::ensure_len("y", y, COORDINATE_SIZE)?;
183
184        let mut key_bytes = [0_u8; UNCOMPRESSED_PUBLIC_KEY_SIZE];
185        key_bytes[0] = 4; // uncompressed key marker
186        key_bytes[1..=COORDINATE_SIZE].copy_from_slice(x);
187        key_bytes[(1 + COORDINATE_SIZE)..].copy_from_slice(y);
188        PublicKey::from_slice(&key_bytes[..]).map_err(JwkError::custom)
189    }
190}
191
192impl<'a> From<&'a SecretKey> for JsonWebKey<'a> {
193    fn from(key: &'a SecretKey) -> JsonWebKey<'a> {
194        create_jwk(&key.to_verifying_key(), Some(key))
195    }
196}
197
198impl TryFrom<&JsonWebKey<'_>> for SecretKey {
199    type Error = JwkError;
200
201    fn try_from(jwk: &JsonWebKey<'_>) -> Result<Self, Self::Error> {
202        let JsonWebKey::EllipticCurve { secret, .. } = jwk else {
203            return Err(JwkError::key_type(jwk, KeyType::EllipticCurve));
204        };
205        let sk_bytes = secret.as_deref();
206        let sk_bytes = sk_bytes.ok_or_else(|| JwkError::NoField("d".into()))?;
207        let sk_bytes: [u8; SECRET_KEY_SIZE] =
208            sk_bytes.try_into().map_err(|_| JwkError::UnexpectedLen {
209                field: "d".to_owned(),
210                expected: SECRET_KEY_SIZE,
211                actual: sk_bytes.len(),
212            })?;
213
214        let sk = SecretKey::from_byte_array(sk_bytes).map_err(JwkError::custom)?;
215        jwk.ensure_key_match(sk)
216    }
217}