From 88d5ea3ad633d13c9ac3b064c084f645830d7713 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Thu, 18 Nov 2021 18:18:03 -0700 Subject: [PATCH] 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. --- .github/workflows/elliptic-curve.yml | 4 +- elliptic-curve/Cargo.lock | 4 +- elliptic-curve/src/hex.rs | 127 ++++++++++++++++++++++++++ elliptic-curve/src/lib.rs | 4 + elliptic-curve/src/public_key.rs | 42 +++++++++ elliptic-curve/src/scalar/core.rs | 109 +++++++++++++++++++++- elliptic-curve/src/scalar/non_zero.rs | 47 +++++++++- 7 files changed, 332 insertions(+), 5 deletions(-) create mode 100644 elliptic-curve/src/hex.rs diff --git a/.github/workflows/elliptic-curve.yml b/.github/workflows/elliptic-curve.yml index 621579ce6..2448a6627 100644 --- a/.github/workflows/elliptic-curve.yml +++ b/.github/workflows/elliptic-curve.yml @@ -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 @@ -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 diff --git a/elliptic-curve/Cargo.lock b/elliptic-curve/Cargo.lock index 5ffaab435..ebeec275a 100644 --- a/elliptic-curve/Cargo.lock +++ b/elliptic-curve/Cargo.lock @@ -193,9 +193,9 @@ checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" [[package]] name = "sec1" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2503adcd8c83e7081b3f9a3173f328f4d6cd50e116aaa324389b46f2d771d595" +checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" dependencies = [ "der", "generic-array", diff --git a/elliptic-curve/src/hex.rs b/elliptic-curve/src/hex.rs new file mode 100644 index 000000000..4c0f37fd1 --- /dev/null +++ b/elliptic-curve/src/hex.rs @@ -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); + } +} diff --git a/elliptic-curve/src/lib.rs b/elliptic-curve/src/lib.rs index 02ba070a7..4fd6ce390 100644 --- a/elliptic-curve/src/lib.rs +++ b/elliptic-curve/src/lib.rs @@ -56,6 +56,7 @@ pub mod ops; pub mod sec1; mod error; +mod hex; mod point; mod scalar; mod secret_key; @@ -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; diff --git a/elliptic-curve/src/public_key.rs b/elliptic-curve/src/public_key.rs index 59382c0f9..4ff04aae7 100644 --- a/elliptic-curve/src/public_key.rs +++ b/elliptic-curve/src/public_key.rs @@ -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 @@ -339,6 +343,44 @@ where } } +#[cfg(all(feature = "pem", feature = "serde"))] +#[cfg_attr(docsrs, doc(all(feature = "pem", feature = "serde")))] +impl Serialize for PublicKey +where + C: Curve + AlgorithmParameters + ProjectiveArithmetic, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldSize: ModulusSize, +{ + fn serialize(&self, serializer: S) -> core::result::Result + 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 +where + C: Curve + AlgorithmParameters + ProjectiveArithmetic, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldSize: ModulusSize, +{ + fn deserialize(deserializer: D) -> core::result::Result + 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}; diff --git a/elliptic-curve/src/scalar/core.rs b/elliptic-curve/src/scalar/core.rs index 6b6533c71..5d0bf9e92 100644 --- a/elliptic-curve/src/scalar/core.rs +++ b/elliptic-curve/src/scalar/core.rs @@ -2,6 +2,7 @@ use crate::{ bigint::{prelude::*, Limb, NonZero}, + hex, rand_core::{CryptoRng, RngCore}, subtle::{ Choice, ConditionallySelectable, ConstantTimeEq, ConstantTimeGreater, ConstantTimeLess, @@ -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; @@ -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 @@ -123,7 +129,7 @@ where } /// Encode [`ScalarCore`] as little endian bytes. - pub fn to_bytes_le(self) -> FieldBytes { + pub fn to_le_bytes(self) -> FieldBytes { self.inner.to_le_byte_array() } } @@ -347,3 +353,104 @@ where self.inner.ct_gt(&n_2) } } + +impl fmt::Display for ScalarCore +where + C: Curve, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:X}", self) + } +} + +impl fmt::LowerHex for ScalarCore +where + C: Curve, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + hex::write_lower(&self.to_be_bytes(), f) + } +} + +impl fmt::UpperHex for ScalarCore +where + C: Curve, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + hex::write_upper(&self.to_be_bytes(), f) + } +} + +impl str::FromStr for ScalarCore +where + C: Curve, +{ + type Err = Error; + + fn from_str(hex: &str) -> Result { + let mut bytes = FieldBytes::::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 Serialize for ScalarCore +where + C: Curve, +{ + #[cfg(not(feature = "alloc"))] + fn serialize(&self, serializer: S) -> core::result::Result + where + S: ser::Serializer, + { + self.to_be_bytes().as_slice().serialize(serializer) + } + + #[cfg(feature = "alloc")] + fn serialize(&self, serializer: S) -> core::result::Result + 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 +where + C: Curve, +{ + #[cfg(not(feature = "alloc"))] + fn deserialize(deserializer: D) -> core::result::Result + 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(deserializer: D) -> core::result::Result + 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)) + } + } +} diff --git a/elliptic-curve/src/scalar/non_zero.rs b/elliptic-curve/src/scalar/non_zero.rs index 818f5d101..498e6ac1c 100644 --- a/elliptic-curve/src/scalar/non_zero.rs +++ b/elliptic-curve/src/scalar/non_zero.rs @@ -2,11 +2,16 @@ use crate::{ bigint::Encoding as _, + hex, ops::Invert, rand_core::{CryptoRng, RngCore}, Curve, Error, FieldBytes, IsHigh, Result, Scalar, ScalarArithmetic, ScalarCore, SecretKey, }; -use core::ops::{Deref, Neg}; +use core::{ + fmt, + ops::{Deref, Neg}, + str, +}; use ff::{Field, PrimeField}; use generic_array::GenericArray; use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; @@ -220,6 +225,46 @@ where } } +impl fmt::Display for NonZeroScalar +where + C: Curve + ScalarArithmetic, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:X}", self) + } +} + +impl fmt::LowerHex for NonZeroScalar +where + C: Curve + ScalarArithmetic, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + hex::write_lower(&self.to_repr(), f) + } +} + +impl fmt::UpperHex for NonZeroScalar +where + C: Curve + ScalarArithmetic, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + hex::write_upper(&self.to_repr(), f) + } +} + +impl str::FromStr for NonZeroScalar +where + C: Curve + ScalarArithmetic, +{ + type Err = Error; + + fn from_str(hex: &str) -> Result { + let mut bytes = FieldBytes::::default(); + hex::decode(hex, &mut bytes)?; + Option::from(Self::from_repr(bytes)).ok_or(Error) + } +} + #[cfg(all(test, feature = "dev"))] mod tests { use crate::dev::{NonZeroScalar, Scalar};