schnorrkel/
points.rs

1// -*- mode: rust; -*-
2//
3// This file is part of schnorrkel.
4// Copyright (c) 2019 Web 3 Foundation
5// See LICENSE for licensing information.
6//
7// Authors:
8// - Jeff Burdges <jeff@web3.foundation>
9
10//! ### Ristretto point tooling
11//!
12//! We provide a `RistrettoBoth` type that contains both an uncompressed
13//! `RistrettoPoint` alongside its matching `CompressedRistretto`,
14//! which helps several protocols avoid duplicate ristretto compressions
15//! and/or decompressions.
16
17// We're discussing including some variant in curve25519-dalek directly in
18// https://github.com/dalek-cryptography/curve25519-dalek/pull/220
19
20
21use core::fmt::{Debug};
22
23use curve25519_dalek::ristretto::{CompressedRistretto,RistrettoPoint};
24use subtle::{ConstantTimeEq,Choice};
25// use curve25519_dalek::scalar::Scalar;
26
27use crate::errors::{SignatureError,SignatureResult};
28
29
30/// Compressed Ristretto point length
31pub const RISTRETTO_POINT_LENGTH: usize = 32;
32
33
34/// A `RistrettoBoth` contains both an uncompressed `RistrettoPoint`
35/// as well as the corresponding `CompressedRistretto`.  It provides
36/// a convenient middle ground for protocols that both hash compressed
37/// points to derive scalars for use with uncompressed points.
38#[derive(Copy, Clone, Default, Eq)]  // PartialEq optimized below
39pub struct RistrettoBoth {
40    compressed: CompressedRistretto,
41    point: RistrettoPoint,
42}
43
44impl Debug for RistrettoBoth {
45    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
46        write!(f, "RistrettoPoint( {:?} )", self.compressed)
47    }
48}
49
50impl ConstantTimeEq for RistrettoBoth {
51    fn ct_eq(&self, other: &RistrettoBoth) -> Choice {
52       self.compressed.ct_eq(&other.compressed)
53    }
54}
55
56impl RistrettoBoth {
57    const DESCRIPTION : &'static str = "A ristretto point represented as a 32-byte compressed point";
58
59    // I dislike getter methods, and prefer direct field access, but doing
60    // getters here permits the fields being private, and gives us faster
61    // equality comparisons.
62
63    /// Access the compressed Ristretto form
64    pub fn as_compressed(&self) -> &CompressedRistretto { &self.compressed }
65
66    /// Extract the compressed Ristretto form
67    pub fn into_compressed(self) -> CompressedRistretto { self.compressed }
68
69    /// Access the point form
70    pub fn as_point(&self) -> &RistrettoPoint { &self.point }
71
72    /// Extract the point form
73    pub fn into_point(self) -> RistrettoPoint { self.point }
74
75    /// Decompress into the `RistrettoBoth` format that also retains the
76    /// compressed form.
77    pub fn from_compressed(compressed: CompressedRistretto) -> SignatureResult<RistrettoBoth> {
78        Ok(RistrettoBoth {
79            point: compressed.decompress().ok_or(SignatureError::PointDecompressionError) ?,
80            compressed,
81        })
82    }
83
84    /// Compress into the `RistrettoBoth` format that also retains the
85    /// uncompressed form.
86    pub fn from_point(point: RistrettoPoint) -> RistrettoBoth {
87        RistrettoBoth {
88            compressed: point.compress(),
89            point,
90        }
91    }
92
93    /// Convert this public key to a byte array.
94    #[inline]
95    pub fn to_bytes(&self) -> [u8; RISTRETTO_POINT_LENGTH] {
96        self.compressed.to_bytes()
97    }
98
99    /// Construct a `RistrettoBoth` from a slice of bytes.
100    ///
101    /// # Example
102    ///
103    /// ```
104    /// use schnorrkel::points::RistrettoBoth;
105    /// use schnorrkel::PUBLIC_KEY_LENGTH;
106    /// use schnorrkel::SignatureError;
107    ///
108    /// # fn doctest() -> Result<RistrettoBoth, SignatureError> {
109    /// let public_key_bytes: [u8; PUBLIC_KEY_LENGTH] = [
110    ///    215,  90, 152,   1, 130, 177,  10, 183, 213,  75, 254, 211, 201, 100,   7,  58,
111    ///     14, 225, 114, 243, 218, 166,  35,  37, 175,   2,  26, 104, 247,   7,   81, 26];
112    ///
113    /// let public_key = RistrettoBoth::from_bytes(&public_key_bytes)?;
114    /// #
115    /// # Ok(public_key)
116    /// # }
117    /// #
118    /// # fn main() {
119    /// #     doctest();
120    /// # }
121    /// ```
122    ///
123    /// # Returns
124    ///
125    /// A `Result` whose okay value is an EdDSA `RistrettoBoth` or whose error value
126    /// is an `SignatureError` describing the error that occurred.
127    #[inline]
128    pub fn from_bytes(bytes: &[u8]) -> SignatureResult<RistrettoBoth> {
129        RistrettoBoth::from_bytes_ser("RistrettoPoint",RistrettoBoth::DESCRIPTION,bytes)
130    }
131
132    /// Variant of `RistrettoBoth::from_bytes` that propagates more informative errors.
133    #[inline]
134    pub fn from_bytes_ser(name: &'static str, description: &'static str, bytes: &[u8]) -> SignatureResult<RistrettoBoth> {
135        if bytes.len() != RISTRETTO_POINT_LENGTH {
136            return Err(SignatureError::BytesLengthError{
137                name, description, length: RISTRETTO_POINT_LENGTH,
138            });
139        }
140        let mut compressed = CompressedRistretto([0u8; RISTRETTO_POINT_LENGTH]);
141        compressed.0.copy_from_slice(&bytes[..32]);
142        RistrettoBoth::from_compressed(compressed)
143    }
144}
145
146serde_boilerplate!(RistrettoBoth);
147
148/// We hide fields largely so that only comparing the compressed forms works.
149impl PartialEq<Self> for RistrettoBoth {
150    fn eq(&self, other: &Self) -> bool {
151        let r = self.compressed.eq(&other.compressed);
152        debug_assert_eq!(r, self.point.eq(&other.point));
153        r
154    }
155}
156
157impl PartialOrd<RistrettoBoth> for RistrettoBoth {
158    fn partial_cmp(&self, other: &RistrettoBoth) -> Option<::core::cmp::Ordering> {
159        Some(self.cmp(other))
160    }
161}
162
163impl Ord for RistrettoBoth {
164    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
165        self.compressed.0.cmp(&other.compressed.0)
166    }
167}
168
169impl core::hash::Hash for RistrettoBoth {
170    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
171        self.compressed.0.hash(state);
172    }
173}