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