Recommendations for PKI signing and verifying?

I am looking for a crate for signing and verifying messages using a common PKI standard.

It seems a straight forward thing and there are many crates that can fulfill this, however some other requirements makes this somewhat more tricky:

  • It needs to be cross platform (x86_64-unknown-linux-gnu and x86_64-pc-windows-msvc at a minimum; preferably also x86_64-pc-windows-gnu, and x86_64-apple-darwin) with as minimal toolchain setup as possible.
  • Compatible with an established standard so that the other side might be implemented in C++ and/or Python.
  • Permissibly licensed (Apache, BSD, MIT, etc).
  • Simple API.

I have already spent many hours in lib.rs and crates.io but still unsure for what will be my best (or even a reasonable) choice.

Any advice will be greatly appreciated.

1 Like

Thanks @hellow.

I started by looking at ring:

  • I was able to trivially build and run the signature example. On x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc, and x86_64-pc-windows-gnu using cross :+1:
  • Next I tried interact with the generated key-pair using openssl:
let rng = rand::SystemRandom::new();
let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
let mut keyfile = File::create("keyfile").unwrap();
keyfile.write_all(bytes).unwrap();

This produced a file that looked like a DER file, and according to the docs should be a DER file. So I tried to:

openssl pkcs8 -inform DER -in keyfile -outform PEM -out keyfile.pem

But I got nothing but some error messages:

Error reading key
139753456247232:error:0D0680A8:asn1 encoding routines:asn1_check_tlen:wrong tag:../crypto/asn1/tasn_dec.c:1130:
139753456247232:error:0D07803A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error:../crypto/asn1/tasn_dec.c:290:Type=X509_SIG

I realize that ring is using BoringSSL but since:

Although BoringSSL is an open source project, it is not intended for general use, as OpenSSL is. We don’t recommend that third parties depend upon it. Doing so is likely to be frustrating because there are no guarantees of API or ABI stability.

I hoped that it can play nicely with openssl with regard to key format, and that a message signed by one could be verified by the other.

Is this assumption safe, and if so what am I missing with storing and/or interacting with my keys?

1 Like

Reported against openssl here. Seems like OpenSSL maybe doesn’t support ones containing the public key?

1 Like

As for rust_sodium:

I was able to build the signature example on x86_64-unknown-linux-gnu and x86_64-pc-windows-msvc, but using cross to build x86_64-pc-windows-gnu gave me an error message, that I haven’t dig into yet.

As for saving the PublicKey in DER, PEM or some other standard format, I am not sure how to go about it.

I was able to dump the content of the keys into files:

let (pk, sk) = sign::gen_keypair();
let mut pf = File::create("pubkey").unwrap();
pf.write_all(&pk[..]).unwrap();
let mut sf = File::create("seckey").unwrap();
sf.write_all(&sk[..]).unwrap();

but it doesn’t seem to be in any particular format I recognize.

I also realize that PublicKey implement the Serialize trait, but was not able to figure how to use it to save the key to a file in any standard format.

So the saga continues.

Ring almost do what I need, but I think I am missing some key piece (pun not intended).

AFAIK, In order to sign some data I need to

Generate a key pair
Sign the message with the private key
Export the signature in a standard format
Export the public key in a standard format

Notes:

  • The code below use Ring v0.15, but I have the same issues in v0.14
  • I use ECDSA over ED25519, since it has better support with openssl.

Generating a key pair

let rng = rand::SystemRandom::new();
let alg = &signature::ECDSA_P256_SHA256_ASN1_SIGNING;
let pkcs8_bytes: ring::pkcs8::Document = cdsaKeyPair::generate_pkcs8(alg, &rng)?;

I can then File::create("key_pair.der")?.write_all(&pkcs8_bytes)? So that
openssl pkey -inform DER -in key_pair.der -text will show me both private and public key.

Sign a message

let key_pair = EcdsaKeyPair::from_pkcs8(&alg, pkcs8_bytes.as_ref())?;
const MESSAGE: &[u8] = b"hello, world";
let sig = key_pair.sign(&rng, MESSAGE)?;

I can File::create("sig.der")?.write_all(&sig)? which is a valid DER file, although I am not sure how to process it with openssl.

Verify signature

This is not part of my workflow, since the verification will be done in non Rust code, but for completeness here how it is done:

let peer_public_key_bytes = key_pair.public_key().as_ref();
let peer_public_key = UnparsedPublicKey::new(
    &signature::ECDSA_P256_SHA256_ASN1, peer_public_key_bytes
);
let sig_bytes = sig.as_ref();
peer_public_key.verify(MESSAGE, &sig_bytes)?;

Exporting the public key

While I can File::create("pub_key.raw")?.write_all(&peer_public_key_bytes)?, it isn’t formatted as DER or any other format I recognize.


So, finally, my question is: How can I export only the public key in DER format or some other format that openssl can use?

2 Likes

@briansmith can you help here?

So I was able to make some progress:

I started with the key_pair.der and the pub_key.raw files, that I was able to create with:

File::create("key_pair.der")?.write_all(&pkcs8_bytes)?;
File::create("pub_key.raw")?.write_all(&peer_public_key_bytes)?;

Then I used openssl to view the public key part:

openssl pkey -inform DER -in key_pair.der -pubout -outform DER | hd

and compared it against the raw public key:

hd pub_key.raw

The delta is:

30 59 30 13 06 07 2a 86  48 ce 3d 02 01 06 08 2a
86 48 ce 3d 03 01 07 03  42 00

Which corresponds to:

SEQUENCE (2 elem)
  SEQUENCE (2 elem)
    OBJECT IDENTIFIER 1.2.840.10045.2.1 ecPublicKey (ANSI X9.62 public key type)
    OBJECT IDENTIFIER 1.2.840.10045.3.1.7 prime256v1 (ANSI X9.62 named elliptic curve)
  BIT STRING (520 bit) ...

So to create a proper DER file from the raw public key I wrote:

let der_head = &[
    0x30, 0x59, // SEQUENCE (2+89) 2 elem
    0x30, 0x13, // SEQUENCE (2+19) 2 elem
    0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, // OID 1.2.840.10045.2.1
    0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, // OID 1.2.840.10045.3.1.7
    0x03, 0x42, 0x00 // BIT STRING (2+66)
];
let mut der_file = File::create("pub_key.der").unwrap();
der_file.write_all(der_head).unwrap();
der_file.write_all(peer_public_key_bytes).unwrap();

And I verified that this file is indeed a valid DER file that looks similar to the one generated with openssl, and was able to read it with openssl:

openssl pkey -inform DER -pubin -in pub_key.der -text

Finally lets check that verify the signature with openssl:

Save the message and the signature with:

File::create("message")?.write_all(&MESSAGE)?;
File::create("message.sig")?.write_all(&sig_bytes)?;

Convert the pub_key.der into PEM format:

openssl pkey -inform DER -pubin -in pub_key.der -outform PEM -out pub_key.pem

And verify the signature:

openssl dgst -verify pub_key.pem -signature message.sig  message

If there is an easier way to do this, I will happily take it.

2 Likes

I always recommend using binary formats instead of the PEM ones (PEM is just the binary format, base64 encoded for no reason). I wrote https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b to document how to use OpenSSL’s command line tool to generate the keys. In particular, see https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b#extracting-the-public-key-from-the-private-key-as-a-subjectpublickeyinfo for how to extract the public key from the private key using OpenSSL’s command line tool.

1 Like

That’s the raw public key without the SubjectPublicKeyInfo (SPKI) structure wrapper around it. That’s the format of public keys in TLS, for example. For OpenSSL’s command line tool we usually need the SPKI structure around it. We need to extend ring’s API to add a function to generate the SPKI boilerplate around the key. File an issue in the ring repo about it if you need it.

2 Likes