mirror of
https://github.com/RustCrypto/signatures
synced 2026-06-21 13:45:42 +00:00
ecdsa: Asn1Signature <-> FixedSignature translation
Adapts the following code from BearSSL to support translation between ASN.1 DER encoded and fixed size (a.k.a. "raw") signatures. This additionally allows eagerly checking that ASN.1 DER-encoded signatures are well-formed.
This commit is contained in:
+15
-11
@@ -1,6 +1,6 @@
|
||||
//! ASN.1 DER-encoded ECDSA signatures
|
||||
|
||||
use crate::curve::Curve;
|
||||
use crate::{curve::Curve, scalar_pair::ScalarPair};
|
||||
use core::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt::{self, Debug},
|
||||
@@ -23,7 +23,7 @@ use signature::Error;
|
||||
/// - 1-byte: ASN.1 `INTEGER` tag (0x02)
|
||||
/// - 1-byte: length
|
||||
/// - 1-byte: zero to indicate value is positive (`INTEGER` is signed)
|
||||
type MaxOverhead = generic_array::typenum::U9;
|
||||
pub type MaxOverhead = generic_array::typenum::U9;
|
||||
|
||||
/// Maximum size of an ASN.1 DER encoded signature for the given elliptic curve.
|
||||
// TODO(tarcieri): const generics
|
||||
@@ -32,20 +32,20 @@ pub type MaxSize<ScalarSize> = <<ScalarSize as Add>::Output as Add<MaxOverhead>>
|
||||
/// ASN.1 DER-encoded ECDSA signature generic over elliptic curves.
|
||||
pub struct Asn1Signature<C: Curve>
|
||||
where
|
||||
<C::ScalarSize as Add>::Output: Add<MaxOverhead>,
|
||||
MaxSize<C::ScalarSize>: ArrayLength<u8>,
|
||||
<C::ScalarSize as Add>::Output: ArrayLength<u8> + Add<MaxOverhead>,
|
||||
{
|
||||
/// ASN.1 DER-encoded signature data
|
||||
bytes: GenericArray<u8, MaxSize<C::ScalarSize>>,
|
||||
pub(crate) bytes: GenericArray<u8, MaxSize<C::ScalarSize>>,
|
||||
|
||||
/// Length of the signature in bytes (DER is variable-width)
|
||||
length: usize,
|
||||
pub(crate) length: usize,
|
||||
}
|
||||
|
||||
impl<C: Curve> signature::Signature for Asn1Signature<C>
|
||||
where
|
||||
<C::ScalarSize as Add>::Output: Add<MaxOverhead>,
|
||||
MaxSize<C::ScalarSize>: ArrayLength<u8>,
|
||||
<C::ScalarSize as Add>::Output: ArrayLength<u8> + Add<MaxOverhead>,
|
||||
{
|
||||
fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, Error> {
|
||||
bytes.as_ref().try_into()
|
||||
@@ -54,8 +54,8 @@ where
|
||||
|
||||
impl<C: Curve> AsRef<[u8]> for Asn1Signature<C>
|
||||
where
|
||||
<C::ScalarSize as Add>::Output: Add<MaxOverhead>,
|
||||
MaxSize<C::ScalarSize>: ArrayLength<u8>,
|
||||
<C::ScalarSize as Add>::Output: ArrayLength<u8> + Add<MaxOverhead>,
|
||||
{
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.bytes.as_slice()[..self.length]
|
||||
@@ -64,8 +64,8 @@ where
|
||||
|
||||
impl<C: Curve> Debug for Asn1Signature<C>
|
||||
where
|
||||
<C::ScalarSize as Add>::Output: Add<MaxOverhead>,
|
||||
MaxSize<C::ScalarSize>: ArrayLength<u8>,
|
||||
<C::ScalarSize as Add>::Output: ArrayLength<u8> + Add<MaxOverhead>,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
@@ -79,21 +79,25 @@ where
|
||||
|
||||
impl<'a, C: Curve> TryFrom<&'a [u8]> for Asn1Signature<C>
|
||||
where
|
||||
<C::ScalarSize as Add>::Output: Add<MaxOverhead>,
|
||||
MaxSize<C::ScalarSize>: ArrayLength<u8>,
|
||||
<C::ScalarSize as Add>::Output: ArrayLength<u8> + Add<MaxOverhead>,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(slice: &'a [u8]) -> Result<Self, Error> {
|
||||
let length = slice.len();
|
||||
|
||||
// TODO: better validate signature is well-formed ASN.1 DER
|
||||
if <MaxSize<C::ScalarSize>>::to_usize() < length {
|
||||
return Err(Error::new());
|
||||
}
|
||||
|
||||
let mut bytes = GenericArray::default();
|
||||
bytes.as_mut_slice()[..length].copy_from_slice(slice);
|
||||
Ok(Self { bytes, length })
|
||||
let result = Self { bytes, length };
|
||||
|
||||
// Ensure signature is well-formed ASN.1 DER
|
||||
ScalarPair::from_asn1_signature(&result).ok_or_else(Error::new)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -7,11 +7,12 @@ pub mod secp256k1;
|
||||
pub use self::{nistp256::NistP256, nistp384::NistP384, secp256k1::Secp256k1};
|
||||
|
||||
use core::{fmt::Debug, ops::Add};
|
||||
use generic_array::typenum::Unsigned;
|
||||
|
||||
/// Elliptic curve in short Weierstrass form suitable for use with ECDSA
|
||||
pub trait Curve: Debug + Default + Send + Sync {
|
||||
/// Size of an integer modulo p (i.e. the curve's order) when serialized
|
||||
/// as octets (i.e. bytes). This also describes the size of an ECDSA
|
||||
/// private key, as well as half the size of a fixed-width signature.
|
||||
type ScalarSize: Add; // `Add<Self>` impl from typenum used for doubling
|
||||
type ScalarSize: Unsigned + Add; // `Add<Self>` impl from typenum used for doubling
|
||||
}
|
||||
|
||||
@@ -73,3 +73,12 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Curve> From<GenericArray<u8, Size<C::ScalarSize>>> for FixedSignature<C>
|
||||
where
|
||||
Size<C::ScalarSize>: ArrayLength<u8>,
|
||||
{
|
||||
fn from(bytes: GenericArray<u8, Size<C::ScalarSize>>) -> Self {
|
||||
Self { bytes }
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -1,5 +1,18 @@
|
||||
//! Elliptic Curve Digital Signature Algorithm (ECDSA) as specified in
|
||||
//! [FIPS 186-4] (Digital Signature Standard)
|
||||
//! [FIPS 186-4][1] (Digital Signature Standard)
|
||||
//!
|
||||
//! This crate doesn't contain an implementation of ECDSA itself, but instead
|
||||
//! contains an [`ecdsa::Asn1Signature`] and [`ecdsa::FixedSignature`] type
|
||||
//! generic over an [`ecdsa::Curve`] type which other crates can use in
|
||||
//! conjunction with the [`signature::Signer`] and [`signature::Verifier`]
|
||||
//! traits.
|
||||
//!
|
||||
//! These traits allow crates which produce and consume ECDSA signatures
|
||||
//! to be written abstractly in such a way that different signer/verifier
|
||||
//! providers can be plugged in, enabling support for using different
|
||||
//! ECDSA implementations, including HSMs or Cloud KMS services.
|
||||
//!
|
||||
//! [1]: https://csrc.nist.gov/publications/detail/fips/186/4/final
|
||||
|
||||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -14,5 +27,6 @@ pub use signature;
|
||||
pub mod asn1_signature;
|
||||
pub mod curve;
|
||||
pub mod fixed_signature;
|
||||
mod scalar_pair;
|
||||
|
||||
pub use self::{asn1_signature::Asn1Signature, curve::Curve, fixed_signature::FixedSignature};
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
//! An ECDSA signature comprises 2 scalars: `r` and `s`. The scalars are
|
||||
//! the same size as the curve's modulus, i.e. for an elliptic curve over
|
||||
//! a ~256-bit prime field, they will also be 256-bit (i.e. the `ScalarSize`
|
||||
//! for a particular `WeierstrassCurve`)
|
||||
//!
|
||||
//! This type provides a convenient representation for converting between
|
||||
//! formats, i.e. all of the serialization code is in this module.
|
||||
|
||||
// Adapted from BearSSL. Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>.
|
||||
// Relicensed under Apache 2.0 + MIT (from original MIT) with permission.
|
||||
//
|
||||
// <https://www.bearssl.org/gitweb/?p=BearSSL;a=blob;f=src/ec/ecdsa_atr.c>
|
||||
// <https://www.bearssl.org/gitweb/?p=BearSSL;a=blob;f=src/ec/ecdsa_rta.c>
|
||||
|
||||
use crate::{
|
||||
asn1_signature::{self, Asn1Signature},
|
||||
Curve, FixedSignature,
|
||||
};
|
||||
use core::{marker::PhantomData, ops::Add};
|
||||
use generic_array::{typenum::Unsigned, ArrayLength, GenericArray};
|
||||
use signature::Signature;
|
||||
|
||||
/// ASN.1 tags
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub(crate) enum Asn1Tag {
|
||||
/// ASN.1 `INTEGER`
|
||||
Integer = 0x02,
|
||||
|
||||
/// ASN.1 `SEQUENCE`: lists of other elements
|
||||
Sequence = 0x30,
|
||||
}
|
||||
|
||||
/// ECDSA signature `r` and `s` values, represented as slices which are at
|
||||
/// most `C::ScalarSize` bytes (but *may* be smaller)
|
||||
pub struct ScalarPair<'a, C: Curve + 'a> {
|
||||
/// `r` scalar value
|
||||
r: &'a [u8],
|
||||
|
||||
/// `s` scalar value
|
||||
s: &'a [u8],
|
||||
|
||||
/// Placeholder for elliptic curve type
|
||||
curve: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<'a, C: Curve + 'a> ScalarPair<'a, C>
|
||||
where
|
||||
asn1_signature::MaxSize<C::ScalarSize>: ArrayLength<u8>,
|
||||
<C::ScalarSize as Add>::Output: ArrayLength<u8> + Add<asn1_signature::MaxOverhead>,
|
||||
{
|
||||
/// Parse the given ASN.1 DER-encoded ECDSA signature, obtaining the
|
||||
/// `r` and `s` scalar pair
|
||||
pub(crate) fn from_asn1_signature(signature: &'a Asn1Signature<C>) -> Option<Self> {
|
||||
// Signature format is a SEQUENCE of two INTEGER values. We
|
||||
// support only integers of less than 127 bytes each (signed
|
||||
// encoding) so the resulting raw signature will have length
|
||||
// at most 254 bytes.
|
||||
let mut bytes = signature.as_slice();
|
||||
|
||||
// First byte is SEQUENCE tag.
|
||||
if bytes[0] != Asn1Tag::Sequence as u8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// The SEQUENCE length will be encoded over one or two bytes. We
|
||||
// limit the total SEQUENCE contents to 255 bytes, because it
|
||||
// makes things simpler; this is enough for subgroup orders up
|
||||
// to 999 bits.
|
||||
let mut zlen = bytes[1] as usize;
|
||||
|
||||
if zlen > 0x80 {
|
||||
if zlen != 0x81 {
|
||||
return None;
|
||||
}
|
||||
|
||||
zlen = bytes[2] as usize;
|
||||
|
||||
if zlen != bytes.len().checked_sub(3).unwrap() {
|
||||
return None;
|
||||
}
|
||||
|
||||
bytes = &bytes[3..];
|
||||
} else {
|
||||
if zlen != bytes.len().checked_sub(2).unwrap() {
|
||||
return None;
|
||||
}
|
||||
|
||||
bytes = &bytes[2..];
|
||||
}
|
||||
|
||||
// First INTEGER (r)
|
||||
let (mut r, bytes) = Self::asn1_int_parse(bytes)?;
|
||||
|
||||
// Second INTEGER (s)
|
||||
let (mut s, bytes) = Self::asn1_int_parse(bytes)?;
|
||||
|
||||
if !bytes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let scalar_size = C::ScalarSize::to_usize();
|
||||
|
||||
if r.len() > scalar_size {
|
||||
if r.len() != scalar_size.checked_add(1).unwrap() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if r[0] != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
r = &r[1..];
|
||||
}
|
||||
|
||||
if s.len() > scalar_size {
|
||||
if s.len() != scalar_size.checked_add(1).unwrap() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if s[0] != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
s = &s[1..];
|
||||
}
|
||||
|
||||
// Removing leading zeros from r and s
|
||||
|
||||
while !r.is_empty() && r[0] == 0 {
|
||||
r = &r[1..];
|
||||
}
|
||||
|
||||
while !s.is_empty() && s[0] == 0 {
|
||||
s = &s[1..];
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
r,
|
||||
s,
|
||||
curve: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the given fixed-size ECDSA signature, obtaining the `r` and `s`
|
||||
/// scalar pair
|
||||
pub(crate) fn from_fixed_signature(signature: &'a FixedSignature<C>) -> Self {
|
||||
let scalar_size = C::ScalarSize::to_usize();
|
||||
|
||||
Self {
|
||||
r: &signature.as_ref()[..scalar_size],
|
||||
s: &signature.as_ref()[scalar_size..],
|
||||
curve: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize this ECDSA signature's `r` and `s` scalar pair as ASN.1 DER
|
||||
pub(crate) fn to_asn1_signature(&self) -> Asn1Signature<C> {
|
||||
let rlen = Self::asn1_int_length(self.r);
|
||||
let slen = Self::asn1_int_length(self.s);
|
||||
let mut bytes = GenericArray::default();
|
||||
|
||||
// SEQUENCE header
|
||||
bytes[0] = Asn1Tag::Sequence as u8;
|
||||
let zlen = rlen.checked_add(slen).unwrap().checked_add(4).unwrap();
|
||||
|
||||
let mut offset = if zlen >= 0x80 {
|
||||
bytes[1] = 0x81;
|
||||
bytes[2] = zlen as u8;
|
||||
3
|
||||
} else {
|
||||
bytes[1] = zlen as u8;
|
||||
2
|
||||
};
|
||||
|
||||
// First INTEGER (r)
|
||||
Self::asn1_int_serialize(self.r, &mut bytes[offset..], rlen);
|
||||
offset = offset.checked_add(2).unwrap().checked_add(rlen).unwrap();
|
||||
|
||||
// Second INTEGER (s)
|
||||
Self::asn1_int_serialize(self.s, &mut bytes[offset..], slen);
|
||||
|
||||
Asn1Signature {
|
||||
bytes,
|
||||
length: offset.checked_add(2).unwrap().checked_add(slen).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_fixed_signature(&self) -> FixedSignature<C> {
|
||||
let mut bytes = GenericArray::default();
|
||||
|
||||
let scalar_size = C::ScalarSize::to_usize();
|
||||
let rbegin = scalar_size.checked_sub(self.r.len()).unwrap();
|
||||
bytes.as_mut_slice()[rbegin..scalar_size].copy_from_slice(self.r);
|
||||
|
||||
let sbegin = bytes.len().checked_sub(self.s.len()).unwrap();
|
||||
bytes.as_mut_slice()[sbegin..].copy_from_slice(self.s);
|
||||
|
||||
FixedSignature::from(bytes)
|
||||
}
|
||||
|
||||
/// Compute ASN.1 DER encoded length for the provided scalar. The ASN.1
|
||||
/// encoding is signed, so its leading bit must have value 0; it must also be
|
||||
/// of minimal length (so leading bytes of value 0 must be removed, except if
|
||||
/// that would contradict the rule about the sign bit).
|
||||
fn asn1_int_length(mut x: &[u8]) -> usize {
|
||||
while !x.is_empty() && x[0] == 0 {
|
||||
x = &x[1..];
|
||||
}
|
||||
|
||||
if x.is_empty() || x[0] >= 0x80 {
|
||||
x.len().checked_add(1).unwrap()
|
||||
} else {
|
||||
x.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an integer from its ASN.1 DER serialization
|
||||
fn asn1_int_parse(bytes: &[u8]) -> Option<(&[u8], &[u8])> {
|
||||
if bytes.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if bytes[0] != Asn1Tag::Integer as u8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let len = bytes[1] as usize;
|
||||
|
||||
if len >= 0x80 || len.checked_add(2).unwrap() > bytes.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let integer = &bytes[2..len.checked_add(2).unwrap()];
|
||||
let remaining = &bytes[len.checked_add(2).unwrap()..];
|
||||
|
||||
Some((integer, remaining))
|
||||
}
|
||||
|
||||
/// Serialize scalar as ASN.1 DER
|
||||
fn asn1_int_serialize(scalar: &[u8], out: &mut [u8], len: usize) {
|
||||
out[0] = Asn1Tag::Integer as u8;
|
||||
out[1] = len as u8;
|
||||
|
||||
if len > C::ScalarSize::to_usize() {
|
||||
out[2] = 0x00;
|
||||
out[3..C::ScalarSize::to_usize().checked_add(3).unwrap()].copy_from_slice(scalar);
|
||||
} else {
|
||||
out[2..len.checked_add(2).unwrap()]
|
||||
.copy_from_slice(&scalar[C::ScalarSize::to_usize().checked_sub(len).unwrap()..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, C: Curve> From<&'a Asn1Signature<C>> for FixedSignature<C>
|
||||
where
|
||||
asn1_signature::MaxSize<C::ScalarSize>: ArrayLength<u8>,
|
||||
<C::ScalarSize as Add>::Output: ArrayLength<u8> + Add<asn1_signature::MaxOverhead>,
|
||||
{
|
||||
fn from(asn1_signature: &Asn1Signature<C>) -> FixedSignature<C> {
|
||||
// We always ensure `Asn1Signature`s parse successfully, so this should always work
|
||||
ScalarPair::from_asn1_signature(asn1_signature)
|
||||
.unwrap()
|
||||
.to_fixed_signature()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, C: Curve> From<&'a FixedSignature<C>> for Asn1Signature<C>
|
||||
where
|
||||
asn1_signature::MaxSize<C::ScalarSize>: ArrayLength<u8>,
|
||||
<C::ScalarSize as Add>::Output: ArrayLength<u8> + Add<asn1_signature::MaxOverhead>,
|
||||
{
|
||||
/// Parse `r` and `s` values from a fixed-width signature and reserialize
|
||||
/// them as ASN.1 DER.
|
||||
fn from(fixed_signature: &FixedSignature<C>) -> Self {
|
||||
ScalarPair::from_fixed_signature(fixed_signature).to_asn1_signature()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user