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}