Skip to content

Commit

Permalink
elliptic-curve: serde support for scalar and PublicKey types (#818)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tarcieri authored Nov 19, 2021
1 parent 7d81302 commit 88d5ea3
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 5 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/elliptic-curve.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
target: ${{ matrix.target }}
override: true
- run: cargo build --target ${{ matrix.target }} --release --no-default-features
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features alloc
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features arithmetic
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features bits
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features dev
Expand All @@ -45,9 +46,10 @@ jobs:
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pem
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pkcs8
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features sec1
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features serde
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pkcs8,sec1
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pem,pkcs8,sec1
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features ecdh,hazmat,jwk,pem,pkcs8,sec1
- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features alloc,ecdh,hazmat,jwk,pem,pkcs8,sec1,serde

test:
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions elliptic-curve/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 127 additions & 0 deletions elliptic-curve/src/hex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//! Hexadecimal encoding helpers
use crate::{Error, Result};
use core::{fmt, str};

/// Write the provided slice to the formatter as lower case hexadecimal
#[inline]
pub(crate) fn write_lower(slice: &[u8], formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in slice {
write!(formatter, "{:02x}", byte)?;
}
Ok(())
}

/// Write the provided slice to the formatter as upper case hexadecimal
#[inline]
pub(crate) fn write_upper(slice: &[u8], formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in slice {
write!(formatter, "{:02X}", byte)?;
}
Ok(())
}

/// Decode the provided hexadecimal string into the provided buffer.
///
/// Accepts either lower case or upper case hexadecimal, but not mixed.
// TODO(tarcieri): constant-time hex decoder?
pub(crate) fn decode(hex: &str, out: &mut [u8]) -> Result<()> {
if hex.as_bytes().len() != out.len() * 2 {
return Err(Error);
}

let mut upper_case = None;

// Ensure all characters are valid and case is not mixed
for &byte in hex.as_bytes() {
match byte {
b'0'..=b'9' => (),
b'a'..=b'z' => match upper_case {
Some(true) => return Err(Error),
Some(false) => (),
None => upper_case = Some(false),
},
b'A'..=b'Z' => match upper_case {
Some(true) => (),
Some(false) => return Err(Error),
None => upper_case = Some(true),
},
_ => return Err(Error),
}
}

for (digit, byte) in hex.as_bytes().chunks_exact(2).zip(out.iter_mut()) {
*byte = str::from_utf8(digit)
.ok()
.and_then(|s| u8::from_str_radix(s, 16).ok())
.ok_or(Error)?;
}

Ok(())
}

#[cfg(all(test, feature = "std"))]
mod tests {
use core::fmt;
use hex_literal::hex;

const EXAMPLE_DATA: &[u8] = &hex!("0123456789ABCDEF");
const EXAMPLE_HEX_LOWER: &str = "0123456789abcdef";
const EXAMPLE_HEX_UPPER: &str = "0123456789ABCDEF";

struct Wrapper<'a>(&'a [u8]);

impl fmt::LowerHex for Wrapper<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
super::write_lower(self.0, f)
}
}

impl fmt::UpperHex for Wrapper<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
super::write_upper(self.0, f)
}
}

#[test]
fn decode_lower() {
let mut buf = [0u8; 8];
super::decode(EXAMPLE_HEX_LOWER, &mut buf).unwrap();
assert_eq!(buf, EXAMPLE_DATA);
}

#[test]
fn decode_upper() {
let mut buf = [0u8; 8];
super::decode(EXAMPLE_HEX_LOWER, &mut buf).unwrap();
assert_eq!(buf, EXAMPLE_DATA);
}

#[test]
fn decode_rejects_mixed_case() {
let mut buf = [0u8; 8];
assert!(super::decode("0123456789abcDEF", &mut buf).is_err());
}

#[test]
fn decode_rejects_too_short() {
let mut buf = [0u8; 9];
assert!(super::decode(EXAMPLE_HEX_LOWER, &mut buf).is_err());
}

#[test]
fn decode_rejects_too_long() {
let mut buf = [0u8; 7];
assert!(super::decode(EXAMPLE_HEX_LOWER, &mut buf).is_err());
}

#[test]
fn encode_lower() {
assert_eq!(format!("{:x}", Wrapper(EXAMPLE_DATA)), EXAMPLE_HEX_LOWER);
}

#[test]
fn encode_upper() {
assert_eq!(format!("{:X}", Wrapper(EXAMPLE_DATA)), EXAMPLE_HEX_UPPER);
}
}
4 changes: 4 additions & 0 deletions elliptic-curve/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub mod ops;
pub mod sec1;

mod error;
mod hex;
mod point;
mod scalar;
mod secret_key;
Expand Down Expand Up @@ -112,6 +113,9 @@ pub use crate::jwk::{JwkEcKey, JwkParameters};
#[cfg(feature = "pkcs8")]
pub use ::sec1::pkcs8;

#[cfg(feature = "serde")]
pub use serde;

use core::fmt::Debug;
use generic_array::GenericArray;

Expand Down
42 changes: 42 additions & 0 deletions elliptic-curve/src/public_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ use {
#[cfg(any(feature = "jwk", feature = "pem"))]
use alloc::string::{String, ToString};

#[cfg(all(feature = "pem", feature = "serde"))]
#[cfg_attr(docsrs, doc(all(feature = "pem", feature = "serde")))]
use serde::{de, ser, Deserialize, Serialize};

/// Elliptic curve public keys.
///
/// This is a wrapper type for [`AffinePoint`] which ensures an inner
Expand Down Expand Up @@ -339,6 +343,44 @@ where
}
}

#[cfg(all(feature = "pem", feature = "serde"))]
#[cfg_attr(docsrs, doc(all(feature = "pem", feature = "serde")))]
impl<C> Serialize for PublicKey<C>
where
C: Curve + AlgorithmParameters + ProjectiveArithmetic,
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
FieldSize<C>: ModulusSize,
{
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
self.to_public_key_der()
.map_err(ser::Error::custom)?
.as_ref()
.serialize(serializer)
}
}

#[cfg(all(feature = "pem", feature = "serde"))]
#[cfg_attr(docsrs, doc(all(feature = "pem", feature = "serde")))]
impl<'de, C> Deserialize<'de> for PublicKey<C>
where
C: Curve + AlgorithmParameters + ProjectiveArithmetic,
AffinePoint<C>: FromEncodedPoint<C> + ToEncodedPoint<C>,
FieldSize<C>: ModulusSize,
{
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
use de::Error;

<&[u8]>::deserialize(deserializer)
.and_then(|bytes| Self::from_public_key_der(bytes).map_err(D::Error::custom))
}
}

#[cfg(all(feature = "dev", test))]
mod tests {
use crate::{dev::MockCurve, sec1::FromEncodedPoint};
Expand Down
109 changes: 108 additions & 1 deletion elliptic-curve/src/scalar/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use crate::{
bigint::{prelude::*, Limb, NonZero},
hex,
rand_core::{CryptoRng, RngCore},
subtle::{
Choice, ConditionallySelectable, ConstantTimeEq, ConstantTimeGreater, ConstantTimeLess,
Expand All @@ -11,7 +12,9 @@ use crate::{
};
use core::{
cmp::Ordering,
fmt,
ops::{Add, AddAssign, Neg, Sub, SubAssign},
str,
};
use generic_array::GenericArray;
use zeroize::DefaultIsZeroes;
Expand All @@ -22,6 +25,9 @@ use {
group::ff::PrimeField,
};

#[cfg(feature = "serde")]
use serde::{de, ser, Deserialize, Serialize};

/// Generic scalar type with core functionality.
///
/// This type provides a baseline level of scalar arithmetic functionality
Expand Down Expand Up @@ -123,7 +129,7 @@ where
}

/// Encode [`ScalarCore`] as little endian bytes.
pub fn to_bytes_le(self) -> FieldBytes<C> {
pub fn to_le_bytes(self) -> FieldBytes<C> {
self.inner.to_le_byte_array()
}
}
Expand Down Expand Up @@ -347,3 +353,104 @@ where
self.inner.ct_gt(&n_2)
}
}

impl<C> fmt::Display for ScalarCore<C>
where
C: Curve,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:X}", self)
}
}

impl<C> fmt::LowerHex for ScalarCore<C>
where
C: Curve,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
hex::write_lower(&self.to_be_bytes(), f)
}
}

impl<C> fmt::UpperHex for ScalarCore<C>
where
C: Curve,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
hex::write_upper(&self.to_be_bytes(), f)
}
}

impl<C> str::FromStr for ScalarCore<C>
where
C: Curve,
{
type Err = Error;

fn from_str(hex: &str) -> Result<Self> {
let mut bytes = FieldBytes::<C>::default();
hex::decode(hex, &mut bytes)?;
Option::from(Self::from_be_bytes(bytes)).ok_or(Error)
}
}

#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl<C> Serialize for ScalarCore<C>
where
C: Curve,
{
#[cfg(not(feature = "alloc"))]
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
self.to_be_bytes().as_slice().serialize(serializer)
}

#[cfg(feature = "alloc")]
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
use alloc::string::ToString;
if serializer.is_human_readable() {
self.to_string().serialize(serializer)
} else {
self.to_be_bytes().as_slice().serialize(serializer)
}
}
}

#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl<'de, C> Deserialize<'de> for ScalarCore<C>
where
C: Curve,
{
#[cfg(not(feature = "alloc"))]
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
use de::Error;
<&[u8]>::deserialize(deserializer)
.and_then(|slice| Self::from_be_slice(slice).map_err(D::Error::custom))
}

#[cfg(feature = "alloc")]
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
use de::Error;
if deserializer.is_human_readable() {
<&str>::deserialize(deserializer)?
.parse()
.map_err(D::Error::custom)
} else {
<&[u8]>::deserialize(deserializer)
.and_then(|slice| Self::from_be_slice(slice).map_err(D::Error::custom))
}
}
}
Loading

0 comments on commit 88d5ea3

Please sign in to comment.