Skip to main content

midnight_circuits/utils/
ecdsa.rs

1// This file is part of MIDNIGHT-ZK.
2// Copyright (C) Midnight Foundation
3// SPDX-License-Identifier: Apache-2.0
4// Licensed under the Apache License, Version 2.0 (the "License");
5// You may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7// http://www.apache.org/licenses/LICENSE-2.0
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14//! Helper module for cpu ECDSA signatures over secp256k1 (k256).
15use base64::DecodeError;
16use ff::Field;
17use group::{Curve, GroupEncoding};
18use midnight_curves::k256::{Fp as K256Base, Fq as K256Scalar, K256Affine, K256};
19use rand::RngCore;
20
21use crate::CircuitField;
22
23#[derive(Clone, Debug)]
24/// ECDSA implemented over secp256k1 (k256).
25pub struct Ecdsa;
26
27/// ECDSA public key.
28pub type PublicKey = K256;
29
30/// ECDSA secret key.
31pub type SecretKey = K256Scalar;
32
33#[derive(Clone, Copy, Debug)]
34
35/// ECDSA signature.
36pub struct ECDSASig {
37    // LE encoded r.
38    r: [u8; 32],
39    s: K256Scalar,
40}
41
42impl ECDSASig {
43    /// Return `r` scalar as bytes.
44    pub fn get_r(&self) -> [u8; 32] {
45        self.r
46    }
47
48    /// Return `s` scalar.
49    pub fn get_s(&self) -> K256Scalar {
50        self.s
51    }
52
53    /// Create ECDSASig from a slice where:
54    ///  - First 32 bytes represent BE-encoded r.
55    ///  - Next 32 bytes represent BE-encoded s.
56    pub fn from_bytes_be(bytes: &[u8]) -> Self {
57        assert_eq!(bytes.len(), 64);
58
59        let mut r = [0u8; 32];
60        r.copy_from_slice(&bytes[..32]);
61        r.reverse();
62
63        let s =
64            K256Scalar::from_bytes_be(&bytes[32..]).expect("Valid secp256k1 scalar in signature");
65        ECDSASig { r, s }
66    }
67}
68
69impl Ecdsa {
70    /// Generate keypair.
71    pub fn keygen<R: RngCore>(rng: &mut R) -> (PublicKey, SecretKey) {
72        let sk = K256Scalar::random(rng);
73        let pk = K256::generator() * sk;
74        (pk, sk)
75    }
76
77    /// Produce a signature for `msg_hash`.
78    pub fn sign<R: RngCore>(sk: &SecretKey, msg_hash: &K256Scalar, rng: &mut R) -> ECDSASig {
79        let k = K256Scalar::random(rng);
80        let k_point: K256 = K256::generator() * k;
81
82        let r_as_base = k_point.to_affine().x();
83        let r = r_as_base.to_bytes_le();
84        let r_as_scalar = K256Scalar::from_bytes_le(&r).unwrap();
85
86        let s = k.invert().unwrap() * (msg_hash + r_as_scalar * sk);
87        ECDSASig { r, s }
88    }
89
90    /// Verify a `signature` for `msg_hash` over key `pk`.
91    pub fn verify(pk: &PublicKey, msg_hash: &K256Scalar, signature: &ECDSASig) -> bool {
92        let g = K256::generator();
93        let r_as_scalar = K256Scalar::from_bytes_le(&signature.r).unwrap();
94        let r_as_base = K256Base::from_bytes_le(&signature.r).unwrap();
95
96        let s_inv = signature.s.invert().unwrap();
97        let k_point = g * (s_inv * msg_hash) + *pk * (s_inv * r_as_scalar);
98
99        k_point.to_affine().x() == r_as_base
100    }
101}
102
103// Note: Does this trait makes sense? If so, should it be moved somewhere else?
104/// This represents an object that can be obtained from a base64 encoding.
105pub trait FromBase64: Sized {
106    /// Returns an element from its base64 encoding.
107    fn from_base64(base64_bytes: &[u8]) -> Result<Self, DecodeError>;
108}
109
110impl FromBase64 for ECDSASig {
111    /// Create ECDSASig from a JWT Base64 encoded blob of bytes.
112    fn from_base64(base64_bytes: &[u8]) -> Result<Self, DecodeError> {
113        let bytes = base64::decode_config(base64_bytes, base64::URL_SAFE_NO_PAD)?;
114        Ok(ECDSASig::from_bytes_be(&bytes))
115    }
116}
117
118impl FromBase64 for PublicKey {
119    /// Input must be base64 encoding of the compressed point in BE.
120    fn from_base64(base64_bytes: &[u8]) -> Result<Self, DecodeError> {
121        let input_len = base64_bytes.len();
122
123        match input_len {
124            // Compressed format: standard SEC1 (0x02/0x03 prefix + 32 BE x-bytes).
125            44 => {
126                let bytes = base64::decode_config(base64_bytes, base64::STANDARD_NO_PAD)?;
127                assert_eq!(bytes.len(), 33);
128                let repr: [u8; 33] = bytes.try_into().expect("33 bytes");
129
130                let ret = K256Affine::from_bytes(&repr.into())
131                    .expect("Valid compressed secp256k1 point.");
132                Ok(ret.into())
133            }
134
135            // Uncompressed format (JWK: two base64-encoded coordinates).
136            86 => from_jwk(&base64_bytes[..43], &base64_bytes[43..]),
137            _ => Err(DecodeError::InvalidLength),
138        }
139    }
140}
141
142/// Receives a public key in JWK format: (x, y) coordinates base64 encoded.
143/// Returns the public key as a curve point.
144fn from_jwk(x: &[u8], y: &[u8]) -> Result<PublicKey, DecodeError> {
145    let x_bytes = base64::decode_config(x, base64::URL_SAFE)?;
146    let y_bytes = base64::decode_config(y, base64::URL_SAFE)?;
147    assert_eq!(x_bytes.len(), 32);
148    assert_eq!(y_bytes.len(), 32);
149
150    // JWK coordinates are in BE.
151    let x_fp = K256Base::from_bytes_be(&x_bytes).expect("Valid x coordinate");
152    let y_fp = K256Base::from_bytes_be(&y_bytes).expect("Valid y coordinate");
153
154    let ret = K256Affine::from_xy(x_fp, y_fp).expect("Valid point on curve");
155    Ok(ret.into())
156}