Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

elliptic-curve: serde support for scalar and PublicKey types #818

Merged
merged 1 commit into from
Nov 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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