bc_components/signing/
signature.rs

1use anyhow::{Result, bail};
2use bc_crypto::{
3    ECDSA_SIGNATURE_SIZE, ED25519_SIGNATURE_SIZE, SCHNORR_SIGNATURE_SIZE,
4};
5use bc_ur::prelude::*;
6use ssh_key::{LineEnding, SshSig};
7
8use super::SignatureScheme;
9use crate::{MLDSASignature, tags};
10
11/// A digital signature created with various signature algorithms.
12///
13/// `Signature` is an enum representing different types of digital signatures:
14///
15/// - `Schnorr`: A BIP-340 Schnorr signature (64 bytes)
16/// - `ECDSA`: An ECDSA signature using the secp256k1 curve (64 bytes)
17/// - `Ed25519`: An Ed25519 signature (64 bytes)
18/// - `SSH`: An SSH signature in various formats
19/// - `MLDSA`: A post-quantum ML-DSA signature
20///
21/// Signatures can be serialized to and from CBOR with appropriate tags.
22///
23/// # Examples
24///
25/// ```
26/// use bc_components::{SignatureScheme, Signer, Verifier};
27///
28/// // Create a key pair using Schnorr
29/// let (private_key, public_key) = SignatureScheme::Schnorr.keypair();
30///
31/// // Sign a message
32/// let message = b"Hello, world!";
33/// let signature = private_key.sign(&message).unwrap();
34///
35/// // The signature can be verified with the corresponding public key
36/// assert!(public_key.verify(&signature, &message));
37/// ```
38///
39/// Converting to and from CBOR:
40///
41/// ```
42/// use bc_components::{SignatureScheme, Signer};
43/// use dcbor::prelude::*;
44///
45/// // Create a signature
46/// let (private_key, _) = SignatureScheme::Schnorr.keypair();
47/// let message = b"Hello, world!";
48/// let signature = private_key.sign(&message).unwrap();
49///
50/// // Convert to CBOR
51/// let cbor: CBOR = signature.clone().into();
52/// let data = cbor.to_cbor_data();
53///
54/// // Convert back from CBOR
55/// let recovered =
56///     bc_components::Signature::from_tagged_cbor_data(&data).unwrap();
57///
58/// // The signatures should be identical
59/// assert_eq!(signature, recovered);
60/// ```
61#[derive(Clone)]
62pub enum Signature {
63    /// A BIP-340 Schnorr signature (64 bytes)
64    Schnorr([u8; SCHNORR_SIGNATURE_SIZE]),
65
66    /// An ECDSA signature using the secp256k1 curve (64 bytes)
67    ECDSA([u8; ECDSA_SIGNATURE_SIZE]),
68
69    /// An Ed25519 signature (64 bytes)
70    Ed25519([u8; ED25519_SIGNATURE_SIZE]),
71
72    /// An SSH signature
73    SSH(SshSig),
74
75    /// A post-quantum ML-DSA signature
76    MLDSA(MLDSASignature),
77}
78
79/// Implementation of equality comparison for Signature
80impl PartialEq for Signature {
81    /// Compares two signatures for equality.
82    ///
83    /// Signatures are equal if they have the same type and the same signature
84    /// data. Signatures of different types (e.g., Schnorr vs ECDSA) are
85    /// never equal.
86    fn eq(&self, other: &Self) -> bool {
87        match (self, other) {
88            (Self::Schnorr(a), Self::Schnorr(b)) => a == b,
89            (Self::ECDSA(a), Self::ECDSA(b)) => a == b,
90            (Self::Ed25519(a), Self::Ed25519(b)) => a == b,
91            (Self::SSH(a), Self::SSH(b)) => a == b,
92            (Self::MLDSA(a), Self::MLDSA(b)) => a.as_bytes() == b.as_bytes(),
93            _ => false,
94        }
95    }
96}
97
98impl Signature {
99    /// Creates a Schnorr signature from a 64-byte array.
100    ///
101    /// # Arguments
102    ///
103    /// * `data` - The 64-byte signature data
104    ///
105    /// # Returns
106    ///
107    /// A new Schnorr signature
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use bc_components::Signature;
113    ///
114    /// let data = [0u8; 64]; // In practice, this would be a real signature
115    /// let signature = Signature::schnorr_from_data(data);
116    /// ```
117    pub fn schnorr_from_data(data: [u8; SCHNORR_SIGNATURE_SIZE]) -> Self {
118        Self::Schnorr(data)
119    }
120
121    /// Creates a Schnorr signature from a byte slice.
122    ///
123    /// # Arguments
124    ///
125    /// * `data` - A byte slice containing the signature data
126    ///
127    /// # Returns
128    ///
129    /// A `Result` containing the signature or an error if the data is not
130    /// exactly 64 bytes in length.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use bc_components::Signature;
136    ///
137    /// let data = vec![0u8; 64]; // In practice, this would be a real signature
138    /// let signature = Signature::schnorr_from_data_ref(&data).unwrap();
139    /// ```
140    pub fn schnorr_from_data_ref(data: impl AsRef<[u8]>) -> Result<Self> {
141        let data = data.as_ref();
142        if data.len() != SCHNORR_SIGNATURE_SIZE {
143            bail!("Invalid Schnorr signature size");
144        }
145        let mut arr = [0u8; SCHNORR_SIGNATURE_SIZE];
146        arr.copy_from_slice(data);
147        Ok(Self::schnorr_from_data(arr))
148    }
149
150    /// Creates an ECDSA signature from a 64-byte array.
151    ///
152    /// # Arguments
153    ///
154    /// * `data` - The 64-byte signature data
155    ///
156    /// # Returns
157    ///
158    /// A new ECDSA signature
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use bc_components::Signature;
164    ///
165    /// let data = [0u8; 64]; // In practice, this would be a real signature
166    /// let signature = Signature::ecdsa_from_data(data);
167    /// ```
168    pub fn ecdsa_from_data(data: [u8; ECDSA_SIGNATURE_SIZE]) -> Self {
169        Self::ECDSA(data)
170    }
171
172    /// Creates an ECDSA signature from a byte slice.
173    ///
174    /// # Arguments
175    ///
176    /// * `data` - A byte slice containing the signature data
177    ///
178    /// # Returns
179    ///
180    /// A `Result` containing the signature or an error if the data is not
181    /// exactly 64 bytes in length.
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// use bc_components::Signature;
187    ///
188    /// let data = vec![0u8; 64]; // In practice, this would be a real signature
189    /// let signature = Signature::ecdsa_from_data_ref(&data).unwrap();
190    /// ```
191    pub fn ecdsa_from_data_ref(data: impl AsRef<[u8]>) -> Result<Self> {
192        let data = data.as_ref();
193        if data.len() != ECDSA_SIGNATURE_SIZE {
194            bail!("Invalid ECDSA signature size");
195        }
196        let mut arr = [0u8; ECDSA_SIGNATURE_SIZE];
197        arr.copy_from_slice(data);
198        Ok(Self::ecdsa_from_data(arr))
199    }
200
201    /// Creates an Ed25519 signature from a 64-byte array.
202    ///
203    /// # Arguments
204    ///
205    /// * `data` - The 64-byte signature data
206    ///
207    /// # Returns
208    ///
209    /// A new Ed25519 signature
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// use bc_components::Signature;
215    ///
216    /// let data = [0u8; 64]; // In practice, this would be a real signature
217    /// let signature = Signature::ed25519_from_data(data);
218    /// ```
219    pub fn ed25519_from_data(data: [u8; ED25519_SIGNATURE_SIZE]) -> Self {
220        Self::Ed25519(data)
221    }
222
223    /// Creates an Ed25519 signature from a byte slice.
224    ///
225    /// # Arguments
226    ///
227    /// * `data` - A byte slice containing the signature data
228    ///
229    /// # Returns
230    ///
231    /// A `Result` containing the signature or an error if the data is not
232    /// exactly 64 bytes in length.
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use bc_components::Signature;
238    ///
239    /// let data = vec![0u8; 64]; // In practice, this would be a real signature
240    /// let signature = Signature::ed25519_from_data_ref(&data).unwrap();
241    /// ```
242    pub fn ed25519_from_data_ref(data: impl AsRef<[u8]>) -> Result<Self> {
243        let data = data.as_ref();
244        if data.len() != ED25519_SIGNATURE_SIZE {
245            bail!("Invalid Ed25519 signature size");
246        }
247        let mut arr = [0u8; ED25519_SIGNATURE_SIZE];
248        arr.copy_from_slice(data);
249        Ok(Self::Ed25519(arr))
250    }
251
252    /// Creates an SSH signature from an `SshSig` object.
253    ///
254    /// # Arguments
255    ///
256    /// * `sig` - The SSH signature object
257    ///
258    /// # Returns
259    ///
260    /// A new SSH signature
261    pub fn from_ssh(sig: SshSig) -> Self { Self::SSH(sig) }
262
263    /// Returns the Schnorr signature data if this is a Schnorr signature.
264    ///
265    /// # Returns
266    ///
267    /// Some reference to the 64-byte signature data if this is a Schnorr
268    /// signature, or None if it's a different signature type.
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// use bc_components::{SignatureScheme, Signer};
274    ///
275    /// // Create a Schnorr signature
276    /// let (private_key, _) = SignatureScheme::Schnorr.keypair();
277    /// let message = b"Hello, world!";
278    /// let signature = private_key.sign(&message).unwrap();
279    ///
280    /// // We can access the Schnorr signature data
281    /// assert!(signature.to_schnorr().is_some());
282    ///
283    /// // Create an ECDSA signature
284    /// let (ecdsa_key, _) = SignatureScheme::Ecdsa.keypair();
285    /// let ecdsa_sig = ecdsa_key.sign(&message).unwrap();
286    ///
287    /// // This will return None since it's not a Schnorr signature
288    /// assert!(ecdsa_sig.to_schnorr().is_none());
289    /// ```
290    pub fn to_schnorr(&self) -> Option<&[u8; SCHNORR_SIGNATURE_SIZE]> {
291        match self {
292            Self::Schnorr(sig) => Some(sig),
293            _ => None,
294        }
295    }
296
297    /// Returns the ECDSA signature data if this is an ECDSA signature.
298    ///
299    /// # Returns
300    ///
301    /// Some reference to the 64-byte signature data if this is an ECDSA
302    /// signature, or None if it's a different signature type.
303    pub fn to_ecdsa(&self) -> Option<&[u8; ECDSA_SIGNATURE_SIZE]> {
304        match self {
305            Self::ECDSA(sig) => Some(sig),
306            _ => None,
307        }
308    }
309
310    /// Returns the SSH signature if this is an SSH signature.
311    ///
312    /// # Returns
313    ///
314    /// Some reference to the SSH signature if this is an SSH signature,
315    /// or None if it's a different signature type.
316    pub fn to_ssh(&self) -> Option<&SshSig> {
317        match self {
318            Self::SSH(sig) => Some(sig),
319            _ => None,
320        }
321    }
322
323    /// Determines the signature scheme used to create this signature.
324    ///
325    /// # Returns
326    ///
327    /// A `Result` containing the signature scheme, or an error if the
328    /// signature scheme cannot be determined (e.g., for unsupported SSH
329    /// algorithms).
330    ///
331    /// # Examples
332    ///
333    /// ```
334    /// use bc_components::{SignatureScheme, Signer};
335    ///
336    /// // Create a signature with ECDSA
337    /// let (private_key, _) = SignatureScheme::Ecdsa.keypair();
338    /// let message = b"Hello, world!";
339    /// let signature = private_key.sign(&message).unwrap();
340    ///
341    /// // Get the signature scheme
342    /// let scheme = signature.scheme().unwrap();
343    /// assert_eq!(scheme, SignatureScheme::Ecdsa);
344    /// ```
345    pub fn scheme(&self) -> Result<SignatureScheme> {
346        match self {
347            Self::Schnorr(_) => Ok(SignatureScheme::Schnorr),
348            Self::ECDSA(_) => Ok(SignatureScheme::Ecdsa),
349            Self::Ed25519(_) => Ok(SignatureScheme::Ed25519),
350            Self::SSH(sig) => match sig.algorithm() {
351                ssh_key::Algorithm::Dsa => Ok(SignatureScheme::SshDsa),
352                ssh_key::Algorithm::Ecdsa { curve } => match curve {
353                    ssh_key::EcdsaCurve::NistP256 => {
354                        Ok(SignatureScheme::SshEcdsaP256)
355                    }
356                    ssh_key::EcdsaCurve::NistP384 => {
357                        Ok(SignatureScheme::SshEcdsaP384)
358                    }
359                    _ => bail!("Unsupported SSH ECDSA curve"),
360                },
361                ssh_key::Algorithm::Ed25519 => Ok(SignatureScheme::SshEd25519),
362                _ => bail!("Unsupported SSH signature algorithm"),
363            },
364            Self::MLDSA(sig) => match sig.level() {
365                crate::MLDSA::MLDSA44 => Ok(SignatureScheme::MLDSA44),
366                crate::MLDSA::MLDSA65 => Ok(SignatureScheme::MLDSA65),
367                crate::MLDSA::MLDSA87 => Ok(SignatureScheme::MLDSA87),
368            },
369        }
370    }
371}
372
373/// Debug implementation for Signature
374impl std::fmt::Debug for Signature {
375    /// Formats the signature for display.
376    ///
377    /// For binary signatures (Schnorr, ECDSA, Ed25519), displays the
378    /// hex-encoded signature data. For SSH and ML-DSA signatures, displays
379    /// the signature object.
380    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381        match self {
382            Signature::Schnorr(data) => f
383                .debug_struct("Schnorr")
384                .field("data", &hex::encode(data))
385                .finish(),
386            Signature::ECDSA(data) => f
387                .debug_struct("ECDSA")
388                .field("data", &hex::encode(data))
389                .finish(),
390            Signature::Ed25519(data) => f
391                .debug_struct("Ed25519")
392                .field("data", &hex::encode(data))
393                .finish(),
394            Signature::SSH(sig) => {
395                f.debug_struct("SSH").field("sig", sig).finish()
396            }
397            Signature::MLDSA(sig) => {
398                f.debug_struct("MLDSA").field("sig", sig).finish()
399            }
400        }
401    }
402}
403
404/// Implementation of AsRef for Signature
405impl AsRef<Signature> for Signature {
406    /// Returns a reference to self.
407    fn as_ref(&self) -> &Signature { self }
408}
409
410/// Implementation of the CBORTagged trait for Signature
411impl CBORTagged for Signature {
412    /// Returns the CBOR tags used for this type.
413    ///
414    /// For Signature, the tag is 40020.
415    fn cbor_tags() -> Vec<dcbor::Tag> {
416        tags_for_values(&[tags::TAG_SIGNATURE])
417    }
418}
419
420/// Conversion from Signature to CBOR
421impl From<Signature> for CBOR {
422    /// Converts a Signature to a tagged CBOR value.
423    fn from(value: Signature) -> Self { value.tagged_cbor() }
424}
425
426/// Implementation of the CBORTaggedEncodable trait for Signature
427impl CBORTaggedEncodable for Signature {
428    /// Converts the Signature to an untagged CBOR value.
429    ///
430    /// The CBOR encoding depends on the signature type:
431    ///
432    /// - Schnorr: A byte string containing the 64-byte signature
433    /// - ECDSA: An array containing the discriminator 1 and the 64-byte
434    ///   signature
435    /// - Ed25519: An array containing the discriminator 2 and the 64-byte
436    ///   signature
437    /// - SSH: A tagged text string containing the PEM-encoded signature
438    /// - ML-DSA: Delegates to the MLDSASignature implementation
439    fn untagged_cbor(&self) -> CBOR {
440        match self {
441            Signature::Schnorr(data) => CBOR::to_byte_string(data),
442            Signature::ECDSA(data) => {
443                vec![(1).into(), CBOR::to_byte_string(data)].into()
444            }
445            Signature::Ed25519(data) => {
446                vec![(2).into(), CBOR::to_byte_string(data)].into()
447            }
448            Signature::SSH(sig) => {
449                let pem = sig.to_pem(LineEnding::LF).unwrap();
450                CBOR::to_tagged_value(tags::TAG_SSH_TEXT_SIGNATURE, pem)
451            }
452            Signature::MLDSA(sig) => sig.clone().into(),
453        }
454    }
455}
456
457/// TryFrom implementation for converting CBOR to Signature
458impl TryFrom<CBOR> for Signature {
459    type Error = dcbor::Error;
460
461    /// Tries to convert a CBOR value to a Signature.
462    ///
463    /// This is a convenience method that calls from_tagged_cbor.
464    fn try_from(cbor: CBOR) -> dcbor::Result<Self> {
465        Self::from_tagged_cbor(cbor)
466    }
467}
468
469/// Implementation of the CBORTaggedDecodable trait for Signature
470impl CBORTaggedDecodable for Signature {
471    /// Creates a Signature from an untagged CBOR value.
472    ///
473    /// # Arguments
474    ///
475    /// * `cbor` - The CBOR value to decode
476    ///
477    /// # Returns
478    ///
479    /// A Result containing the decoded Signature or an error if decoding fails.
480    ///
481    /// # Format
482    ///
483    /// The CBOR value must be one of:
484    /// - A byte string (interpreted as a Schnorr signature)
485    /// - An array of length 2, where the first element is 1 (ECDSA) or 2
486    ///   (Ed25519) and the second element is a byte string containing the
487    ///   signature data
488    /// - A tagged value with a tag for MLDSA or SSH signatures
489    fn from_untagged_cbor(cbor: CBOR) -> dcbor::Result<Self> {
490        match cbor.clone().into_case() {
491            CBORCase::ByteString(bytes) => {
492                Ok(Self::schnorr_from_data_ref(bytes)?)
493            }
494            CBORCase::Array(mut elements) => {
495                if elements.len() == 2 {
496                    let mut drain = elements.drain(0..);
497                    let ele_0 = drain.next().unwrap().into_case();
498                    let ele_1 = drain.next().unwrap().into_case();
499                    match ele_0 {
500                        CBORCase::ByteString(data) => {
501                            return Ok(Self::schnorr_from_data_ref(data)?);
502                        }
503                        CBORCase::Unsigned(1) => {
504                            if let CBORCase::ByteString(data) = ele_1 {
505                                return Ok(Self::ecdsa_from_data_ref(data)?);
506                            }
507                        }
508                        CBORCase::Unsigned(2) => {
509                            if let CBORCase::ByteString(data) = ele_1 {
510                                return Ok(Self::ed25519_from_data_ref(data)?);
511                            }
512                        }
513                        _ => (),
514                    }
515                }
516                Err("Invalid signature format".into())
517            }
518            CBORCase::Tagged(tag, item) => match tag.value() {
519                tags::TAG_MLDSA_SIGNATURE => {
520                    let sig = MLDSASignature::try_from(cbor)?;
521                    Ok(Self::MLDSA(sig))
522                }
523                tags::TAG_SSH_TEXT_SIGNATURE => {
524                    let string = item.try_into_text()?;
525                    let pem = SshSig::from_pem(string)
526                        .map_err(|_| "Invalid PEM format")?;
527                    Ok(Self::SSH(pem))
528                }
529                _ => Err("Invalid signature format".into()),
530            },
531            _ => Err("Invalid signature format".into()),
532        }
533    }
534}