Skip to content

Commit 88d5ea3

Browse files
authored
elliptic-curve: serde support for scalar and PublicKey types (#818)
Adds support for serializing/deserializing the following types using serde: - `ScalarCore` - `NonZeroScalar` - `PublicKey` While this crate had `serde` support previously, it was entirely limited to the JWK implementation. This commit expands it to support more types, and provides the underpinnings for better leveraging `serde` in all of the dependent crates.
1 parent 7d81302 commit 88d5ea3

File tree

7 files changed

+332
-5
lines changed

7 files changed

+332
-5
lines changed

.github/workflows/elliptic-curve.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
target: ${{ matrix.target }}
3737
override: true
3838
- run: cargo build --target ${{ matrix.target }} --release --no-default-features
39+
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features alloc
3940
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features arithmetic
4041
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features bits
4142
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features dev
@@ -45,9 +46,10 @@ jobs:
4546
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pem
4647
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pkcs8
4748
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features sec1
49+
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features serde
4850
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pkcs8,sec1
4951
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pem,pkcs8,sec1
50-
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features ecdh,hazmat,jwk,pem,pkcs8,sec1
52+
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features alloc,ecdh,hazmat,jwk,pem,pkcs8,sec1,serde
5153

5254
test:
5355
runs-on: ubuntu-latest

elliptic-curve/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

elliptic-curve/src/hex.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//! Hexadecimal encoding helpers
2+
3+
use crate::{Error, Result};
4+
use core::{fmt, str};
5+
6+
/// Write the provided slice to the formatter as lower case hexadecimal
7+
#[inline]
8+
pub(crate) fn write_lower(slice: &[u8], formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
9+
for byte in slice {
10+
write!(formatter, "{:02x}", byte)?;
11+
}
12+
Ok(())
13+
}
14+
15+
/// Write the provided slice to the formatter as upper case hexadecimal
16+
#[inline]
17+
pub(crate) fn write_upper(slice: &[u8], formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18+
for byte in slice {
19+
write!(formatter, "{:02X}", byte)?;
20+
}
21+
Ok(())
22+
}
23+
24+
/// Decode the provided hexadecimal string into the provided buffer.
25+
///
26+
/// Accepts either lower case or upper case hexadecimal, but not mixed.
27+
// TODO(tarcieri): constant-time hex decoder?
28+
pub(crate) fn decode(hex: &str, out: &mut [u8]) -> Result<()> {
29+
if hex.as_bytes().len() != out.len() * 2 {
30+
return Err(Error);
31+
}
32+
33+
let mut upper_case = None;
34+
35+
// Ensure all characters are valid and case is not mixed
36+
for &byte in hex.as_bytes() {
37+
match byte {
38+
b'0'..=b'9' => (),
39+
b'a'..=b'z' => match upper_case {
40+
Some(true) => return Err(Error),
41+
Some(false) => (),
42+
None => upper_case = Some(false),
43+
},
44+
b'A'..=b'Z' => match upper_case {
45+
Some(true) => (),
46+
Some(false) => return Err(Error),
47+
None => upper_case = Some(true),
48+
},
49+
_ => return Err(Error),
50+
}
51+
}
52+
53+
for (digit, byte) in hex.as_bytes().chunks_exact(2).zip(out.iter_mut()) {
54+
*byte = str::from_utf8(digit)
55+
.ok()
56+
.and_then(|s| u8::from_str_radix(s, 16).ok())
57+
.ok_or(Error)?;
58+
}
59+
60+
Ok(())
61+
}
62+
63+
#[cfg(all(test, feature = "std"))]
64+
mod tests {
65+
use core::fmt;
66+
use hex_literal::hex;
67+
68+
const EXAMPLE_DATA: &[u8] = &hex!("0123456789ABCDEF");
69+
const EXAMPLE_HEX_LOWER: &str = "0123456789abcdef";
70+
const EXAMPLE_HEX_UPPER: &str = "0123456789ABCDEF";
71+
72+
struct Wrapper<'a>(&'a [u8]);
73+
74+
impl fmt::LowerHex for Wrapper<'_> {
75+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76+
super::write_lower(self.0, f)
77+
}
78+
}
79+
80+
impl fmt::UpperHex for Wrapper<'_> {
81+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82+
super::write_upper(self.0, f)
83+
}
84+
}
85+
86+
#[test]
87+
fn decode_lower() {
88+
let mut buf = [0u8; 8];
89+
super::decode(EXAMPLE_HEX_LOWER, &mut buf).unwrap();
90+
assert_eq!(buf, EXAMPLE_DATA);
91+
}
92+
93+
#[test]
94+
fn decode_upper() {
95+
let mut buf = [0u8; 8];
96+
super::decode(EXAMPLE_HEX_LOWER, &mut buf).unwrap();
97+
assert_eq!(buf, EXAMPLE_DATA);
98+
}
99+
100+
#[test]
101+
fn decode_rejects_mixed_case() {
102+
let mut buf = [0u8; 8];
103+
assert!(super::decode("0123456789abcDEF", &mut buf).is_err());
104+
}
105+
106+
#[test]
107+
fn decode_rejects_too_short() {
108+
let mut buf = [0u8; 9];
109+
assert!(super::decode(EXAMPLE_HEX_LOWER, &mut buf).is_err());
110+
}
111+
112+
#[test]
113+
fn decode_rejects_too_long() {
114+
let mut buf = [0u8; 7];
115+
assert!(super::decode(EXAMPLE_HEX_LOWER, &mut buf).is_err());
116+
}
117+
118+
#[test]
119+
fn encode_lower() {
120+
assert_eq!(format!("{:x}", Wrapper(EXAMPLE_DATA)), EXAMPLE_HEX_LOWER);
121+
}
122+
123+
#[test]
124+
fn encode_upper() {
125+
assert_eq!(format!("{:X}", Wrapper(EXAMPLE_DATA)), EXAMPLE_HEX_UPPER);
126+
}
127+
}

elliptic-curve/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub mod ops;
5656
pub mod sec1;
5757

5858
mod error;
59+
mod hex;
5960
mod point;
6061
mod scalar;
6162
mod secret_key;
@@ -112,6 +113,9 @@ pub use crate::jwk::{JwkEcKey, JwkParameters};
112113
#[cfg(feature = "pkcs8")]
113114
pub use ::sec1::pkcs8;
114115

116+
#[cfg(feature = "serde")]
117+
pub use serde;
118+
115119
use core::fmt::Debug;
116120
use generic_array::GenericArray;
117121

elliptic-curve/src/public_key.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ use {
3131
#[cfg(any(feature = "jwk", feature = "pem"))]
3232
use alloc::string::{String, ToString};
3333

34+
#[cfg(all(feature = "pem", feature = "serde"))]
35+
#[cfg_attr(docsrs, doc(all(feature = "pem", feature = "serde")))]
36+
use serde::{de, ser, Deserialize, Serialize};
37+
3438
/// Elliptic curve public keys.
3539
///
3640
/// This is a wrapper type for [`AffinePoint`] which ensures an inner
@@ -339,6 +343,44 @@ where
339343
}
340344
}
341345

346+
#[cfg(all(feature = "pem", feature = "serde"))]
347+
#[cfg_attr(docsrs, doc(all(feature = "pem", feature = "serde")))]
348+
impl<C> Serialize for PublicKey<C>
349+
where
350+
C: Curve + AlgorithmParameters + ProjectiveArithmetic,
351+
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
352+
FieldSize<C>: ModulusSize,
353+
{
354+
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
355+
where
356+
S: ser::Serializer,
357+
{
358+
self.to_public_key_der()
359+
.map_err(ser::Error::custom)?
360+
.as_ref()
361+
.serialize(serializer)
362+
}
363+
}
364+
365+
#[cfg(all(feature = "pem", feature = "serde"))]
366+
#[cfg_attr(docsrs, doc(all(feature = "pem", feature = "serde")))]
367+
impl<'de, C> Deserialize<'de> for PublicKey<C>
368+
where
369+
C: Curve + AlgorithmParameters + ProjectiveArithmetic,
370+
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
371+
FieldSize<C>: ModulusSize,
372+
{
373+
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
374+
where
375+
D: de::Deserializer<'de>,
376+
{
377+
use de::Error;
378+
379+
<&[u8]>::deserialize(deserializer)
380+
.and_then(|bytes| Self::from_public_key_der(bytes).map_err(D::Error::custom))
381+
}
382+
}
383+
342384
#[cfg(all(feature = "dev", test))]
343385
mod tests {
344386
use crate::{dev::MockCurve, sec1::FromEncodedPoint};

elliptic-curve/src/scalar/core.rs

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use crate::{
44
bigint::{prelude::*, Limb, NonZero},
5+
hex,
56
rand_core::{CryptoRng, RngCore},
67
subtle::{
78
Choice, ConditionallySelectable, ConstantTimeEq, ConstantTimeGreater, ConstantTimeLess,
@@ -11,7 +12,9 @@ use crate::{
1112
};
1213
use core::{
1314
cmp::Ordering,
15+
fmt,
1416
ops::{Add, AddAssign, Neg, Sub, SubAssign},
17+
str,
1518
};
1619
use generic_array::GenericArray;
1720
use zeroize::DefaultIsZeroes;
@@ -22,6 +25,9 @@ use {
2225
group::ff::PrimeField,
2326
};
2427

28+
#[cfg(feature = "serde")]
29+
use serde::{de, ser, Deserialize, Serialize};
30+
2531
/// Generic scalar type with core functionality.
2632
///
2733
/// This type provides a baseline level of scalar arithmetic functionality
@@ -123,7 +129,7 @@ where
123129
}
124130

125131
/// Encode [`ScalarCore`] as little endian bytes.
126-
pub fn to_bytes_le(self) -> FieldBytes<C> {
132+
pub fn to_le_bytes(self) -> FieldBytes<C> {
127133
self.inner.to_le_byte_array()
128134
}
129135
}
@@ -347,3 +353,104 @@ where
347353
self.inner.ct_gt(&n_2)
348354
}
349355
}
356+
357+
impl<C> fmt::Display for ScalarCore<C>
358+
where
359+
C: Curve,
360+
{
361+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362+
write!(f, "{:X}", self)
363+
}
364+
}
365+
366+
impl<C> fmt::LowerHex for ScalarCore<C>
367+
where
368+
C: Curve,
369+
{
370+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371+
hex::write_lower(&self.to_be_bytes(), f)
372+
}
373+
}
374+
375+
impl<C> fmt::UpperHex for ScalarCore<C>
376+
where
377+
C: Curve,
378+
{
379+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380+
hex::write_upper(&self.to_be_bytes(), f)
381+
}
382+
}
383+
384+
impl<C> str::FromStr for ScalarCore<C>
385+
where
386+
C: Curve,
387+
{
388+
type Err = Error;
389+
390+
fn from_str(hex: &str) -> Result<Self> {
391+
let mut bytes = FieldBytes::<C>::default();
392+
hex::decode(hex, &mut bytes)?;
393+
Option::from(Self::from_be_bytes(bytes)).ok_or(Error)
394+
}
395+
}
396+
397+
#[cfg(feature = "serde")]
398+
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
399+
impl<C> Serialize for ScalarCore<C>
400+
where
401+
C: Curve,
402+
{
403+
#[cfg(not(feature = "alloc"))]
404+
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
405+
where
406+
S: ser::Serializer,
407+
{
408+
self.to_be_bytes().as_slice().serialize(serializer)
409+
}
410+
411+
#[cfg(feature = "alloc")]
412+
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
413+
where
414+
S: ser::Serializer,
415+
{
416+
use alloc::string::ToString;
417+
if serializer.is_human_readable() {
418+
self.to_string().serialize(serializer)
419+
} else {
420+
self.to_be_bytes().as_slice().serialize(serializer)
421+
}
422+
}
423+
}
424+
425+
#[cfg(feature = "serde")]
426+
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
427+
impl<'de, C> Deserialize<'de> for ScalarCore<C>
428+
where
429+
C: Curve,
430+
{
431+
#[cfg(not(feature = "alloc"))]
432+
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
433+
where
434+
D: de::Deserializer<'de>,
435+
{
436+
use de::Error;
437+
<&[u8]>::deserialize(deserializer)
438+
.and_then(|slice| Self::from_be_slice(slice).map_err(D::Error::custom))
439+
}
440+
441+
#[cfg(feature = "alloc")]
442+
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
443+
where
444+
D: de::Deserializer<'de>,
445+
{
446+
use de::Error;
447+
if deserializer.is_human_readable() {
448+
<&str>::deserialize(deserializer)?
449+
.parse()
450+
.map_err(D::Error::custom)
451+
} else {
452+
<&[u8]>::deserialize(deserializer)
453+
.and_then(|slice| Self::from_be_slice(slice).map_err(D::Error::custom))
454+
}
455+
}
456+
}

0 commit comments

Comments
 (0)