0

I am stuck on this error when verifying ECDSA signatures with the ring crate.

I a minimal reproductible example, have a working version in Python. I could verify that the input bytes (after parsing the cert etc.) are exactly the same in both.

import base64

try:
    import ecdsa.util
    from cryptography import x509
    from cryptography.hazmat.primitives import serialization
    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives.asymmetric import ec as cryptography_ec
    from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
    from cryptography.hazmat.primitives.hashes import SHA384
except ImportError:
    print("Missing deps, install:\n\n\tpip install ecdsa cryptography")


CERT = """-----BEGIN CERTIFICATE-----
MIIDBjCCAougAwIBAgIIFiJLFfdxFlYwCgYIKoZIzj0EAwMwgaMxCzAJBgNVBAYT
AlVTMRwwGgYDVQQKExNNb3ppbGxhIENvcnBvcmF0aW9uMS8wLQYDVQQLEyZNb3pp
bGxhIEFNTyBQcm9kdWN0aW9uIFNpZ25pbmcgU2VydmljZTFFMEMGA1UEAww8Q29u
dGVudCBTaWduaW5nIEludGVybWVkaWF0ZS9lbWFpbEFkZHJlc3M9Zm94c2VjQG1v
emlsbGEuY29tMB4XDTIwMDYxNjE3MTYxNVoXDTIwMDkwNDE3MTYxNVowgakxCzAJ
BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
biBWaWV3MRwwGgYDVQQKExNNb3ppbGxhIENvcnBvcmF0aW9uMRcwFQYDVQQLEw5D
bG91ZCBTZXJ2aWNlczE2MDQGA1UEAxMtcmVtb3RlLXNldHRpbmdzLmNvbnRlbnQt
c2lnbmF0dXJlLm1vemlsbGEub3JnMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEDmOX
N5IGlUqCvu6xkOKr020Eo3kY2uPdJO0ZihVUoglk1ktQPss184OajFOMKm/BJX4W
IsZUzQoRL8NgGfZDwBjT95Q87lhOWEWs5AU/nMXIYwDp7rpUPaUqw0QLMikdo4GD
MIGAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSME
GDAWgBSgHUoXT4zCKzVF8WPx2nBwp8744TA4BgNVHREEMTAvgi1yZW1vdGUtc2V0
dGluZ3MuY29udGVudC1zaWduYXR1cmUubW96aWxsYS5vcmcwCgYIKoZIzj0EAwMD
aQAwZgIxAJvyynyPqRmRMqf95FPH5xfcoT3jb/2LOkUifGDtjtZ338ScpT2glUK8
HszKVANqXQIxAIygMaeTiD9figEusmHMthBdFoIoHk31x4MHukAy+TWZ863X6/V2
6/ZrZMp6Wq/0ow==
-----END CERTIFICATE-----
"""

SIG = "oPRadsg_5wnnUXlRIjamXKPWyyGe4VLt-KR4-PJTK2hq4hF196L3nbvne1_7-HfpoVRR4BLsHWtnnt6700CTt5kNgwvrE8aJ3nXFa0vJBoOvIRco-vCt-rJ7acEu0IFG"

DATA = b"""Content-Signature:\x00{"data":[],"last_modified":"1594998798350"}"""

# Parse X509 certificate
cert = x509.load_pem_x509_certificate(CERT.encode("utf8"), backend=default_backend())


signature = base64.urlsafe_b64decode(SIG)
r, s = ecdsa.util.sigdecode_string(
    signature, order=ecdsa.curves.NIST384p.order
)
signature_dss = encode_dss_signature(r, s)

algorithm = cryptography_ec.ECDSA(SHA384())

cert.public_key().verify(signature_dss, DATA, algorithm)

print("SUCCESS!")
print("PK:\t", cert.public_key().public_bytes(encoding=serialization.Encoding.X962, format=serialization.PublicFormat.UncompressedPoint).hex())
print("Sig:\t", signature.hex())
print("Data:\t", DATA.hex())

With ring (and x509-parser), it looks like this and the call to verify() fails with Unspecified error, which is not very helpful (ring API docs):

use base64;
use ring::signature;
use x509_parser;

const CERTIFICATE: &str = r#"-----BEGIN CERTIFICATE-----
MIIDBjCCAougAwIBAgIIFiJLFfdxFlYwCgYIKoZIzj0EAwMwgaMxCzAJBgNVBAYT
AlVTMRwwGgYDVQQKExNNb3ppbGxhIENvcnBvcmF0aW9uMS8wLQYDVQQLEyZNb3pp
bGxhIEFNTyBQcm9kdWN0aW9uIFNpZ25pbmcgU2VydmljZTFFMEMGA1UEAww8Q29u
dGVudCBTaWduaW5nIEludGVybWVkaWF0ZS9lbWFpbEFkZHJlc3M9Zm94c2VjQG1v
emlsbGEuY29tMB4XDTIwMDYxNjE3MTYxNVoXDTIwMDkwNDE3MTYxNVowgakxCzAJ
BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
biBWaWV3MRwwGgYDVQQKExNNb3ppbGxhIENvcnBvcmF0aW9uMRcwFQYDVQQLEw5D
bG91ZCBTZXJ2aWNlczE2MDQGA1UEAxMtcmVtb3RlLXNldHRpbmdzLmNvbnRlbnQt
c2lnbmF0dXJlLm1vemlsbGEub3JnMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEDmOX
N5IGlUqCvu6xkOKr020Eo3kY2uPdJO0ZihVUoglk1ktQPss184OajFOMKm/BJX4W
IsZUzQoRL8NgGfZDwBjT95Q87lhOWEWs5AU/nMXIYwDp7rpUPaUqw0QLMikdo4GD
MIGAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSME
GDAWgBSgHUoXT4zCKzVF8WPx2nBwp8744TA4BgNVHREEMTAvgi1yZW1vdGUtc2V0
dGluZ3MuY29udGVudC1zaWduYXR1cmUubW96aWxsYS5vcmcwCgYIKoZIzj0EAwMD
aQAwZgIxAJvyynyPqRmRMqf95FPH5xfcoT3jb/2LOkUifGDtjtZ338ScpT2glUK8
HszKVANqXQIxAIygMaeTiD9figEusmHMthBdFoIoHk31x4MHukAy+TWZ863X6/V2
6/ZrZMp6Wq/0ow==
-----END CERTIFICATE-----"#;

const SIGNATURE: &str = r#"oPRadsg_5wnnUXlRIjamXKPWyyGe4VLt-KR4-PJTK2hq4hF196L3nbvne1_7-HfpoVRR4BLsHWtnnt6700CTt5kNgwvrE8aJ3nXFa0vJBoOvIRco-vCt-rJ7acEu0IFG"#;

const DATA: &str = r#"Content-Signature:\x00{"data":[],"last_modified":"1594998798350"}"#;

macro_rules! hex {
    ($b: expr) => {
        {
            let ss: Vec<String> = $b.iter()
                .map(|b| format!("{:02x}", b))
                .collect();
            ss.join("")
        }
    };
}

fn main() {
    let pem = match x509_parser::pem::parse_x509_pem(CERTIFICATE.as_bytes()) {
        Ok((rem, pem)) => {
            assert!(rem.is_empty());
            //
            assert_eq!(pem.label, String::from("CERTIFICATE"));
            //
            pem
        },
        err => panic!("PEM parsing failed: {:?}", err),
    };

    let x509 = match x509_parser::parse_x509_certificate(&pem.contents) {
        Ok((rem, x509)) => {
            assert!(rem.is_empty());
            x509
        },
        err => panic!("X509 parsing failed: {:?}", err),
    };

    let spki = x509.tbs_certificate.subject_pki;
    let public_key = signature::UnparsedPublicKey::new(
        &signature::ECDSA_P384_SHA384_ASN1,
        &spki.subject_public_key.data,
    );

    let signature_bytes = base64::decode_config(&SIGNATURE, base64::URL_SAFE).unwrap();

    println!("PK:\t{}", hex!(spki.subject_public_key.data));
    println!("Sig:\t{}", hex!(signature_bytes));
    println!("Data:\t{}", hex!(DATA.as_bytes()));

    match public_key.verify(&DATA.as_bytes(), &signature_bytes) {
        Ok(_) => (),
        Err(err) => println!("ERROR {:?}", err),
    }
}

Using openssl x509 -inform PEM -in cert.pem -text I could also make sure that the public key is ASN1, and the bytes match too.

Is there something obvious that I missed?

What could I do to have more insights about the error?

Thank you!

leplatrem
  • 1,005
  • 13
  • 25

1 Answers1

0

I figured it out.

First, in the python example, the printed signature is not the one passed to verify(). It should be print("Sig:\t", signature_dss.hex()).

Second, in Rust, the Content-Signature:\x00 part should not a be a raw string in DATA. It could be let payload = format!("Content-Signature:\x00{}", r#"{"data":[],"last_modified":"1594998798350"}"#); for example.

And last, most importantly, we have to encode the signature point values as DER in Rust too (the equivalent of encode_dss_signature() on the Python side). Since the der_writer module is not public in ring, I ended up implementing a quick specific one:

use ring::io::der;

fn encode_dss_signature(signature_bytes: Vec<u8>) -> Vec<u8> {
    // Split the signature in two integers (48 bytes each)
    let sig_len = signature_bytes.len();
    let r_bytes = &signature_bytes[0..sig_len / 2];
    let s_bytes = &signature_bytes[sig_len / 2..];

    // Encode the two integer points.
    // See https://github.com/briansmith/ring/blob/3b1ece4/src/io/der_writer.rs
    let mut tuple_der: Vec<u8> = Vec::new();
    for val in [r_bytes, s_bytes].iter() {
        tuple_der.push(der::Tag::Integer as u8);
        tuple_der.push((val.len() + 1) as u8);
        if (val[0] & 0x80) != 0 {
            // Disambiguate negative number.
            tuple_der.push(0x00);
        }
        tuple_der.extend(*val);
    }

    // Sequence tag followed by content length and bytes.
    let mut signature_der: Vec<u8> = Vec::new();
    signature_der.push(der::Tag::Sequence as u8);
    signature_der.push(tuple_der.len() as u8);
    signature_der.extend(tuple_der);

    signature_der
}

Signature can now be verified successfully!

leplatrem
  • 1,005
  • 13
  • 25