Skip to content
Blog » Signing & Verifying Messages with Digital Signatures in Rust using Ring

Signing & Verifying Messages with Digital Signatures in Rust using Ring

    The Ring cryptography library supports signing and verifying the integrity of messages using digital signatures. In this post I introduce digital signatures and explain how to use the sign and verify methods defined in the ring::signature module to create and then verify signatures for any given message.

    Introduction to Digital Signatures

    Digital signatures are similar to message authentication codes in that they provide a way to check the integrity and authenticity of messages (see my post on HMAC here) but they additionally provide non-repudiation. This is because MACs use a symmetric key as the secret where as digital signatures use a public/private key pair. The signature is produce by using the private key to sign a message and then later the signature can be checked against the message using the public key to determine if the signature is valid for that message.

    If a signature successfully verifies then we can be sure that the message has not been modified and that it originated from the owner of the private key which is associated with the public key. This means that the owner of the private key cannot deny sending the message because of the valid signature associated with the message and hence we have non-repudiation.

    The interface of a digital signature sign function looks something like this:

    signature = sign(private_key, message)

    The interface of a digital signature verify function looks something like this:

    result = verify(public_key, message, signature)

    Result is a boolean where true means that the signature is valid over the message and false means that the signature verification has failed.

    Internally digital signature algorithms use asymmetric cryptography where public/private key pairs are used for encryption and decryption. To generate a signature the message to be signed is first hashed using a cryptographically secure hash function and then the hash is encrypted using the private key. To verify the signature the encrypted hash is decrypted using the public key and the message is hashed to verify that it matches the decrypted hash value. Messages are hashed in this way because asymmetric encryption can only encrypt values of limited size and it is also much slower then symmetric encryption in terms of performance.

    Introduction to ECDSA & EdDSA

    In the code examples below I will show how to sign and verify messages using both ECDSA and EdDSA digital signature algorithms so lets start with a bit of background.

    Elliptic Curve Digital Signature Algorithm (ECDSA) is a cryptographically secure digital signature scheme based on elliptic-curve cryptography (ECC). It relies on the math of the cyclic groups of elliptic curves over finite fields and on the difficulty of the elliptic-curve discrete logarithm problem. The signature algorithm uses a random nonce as an input which must not be re-used and must be completely unpredictable. In order to get 128 bits of security (relative to symmetric cryptography) we can use 256 bit key pairs which will generate 64 byte signatures.

    Edwards-curve Digital Signature Algorithm (EdDSA) is a digital signature scheme that uses a variant of Schnorr signature based on twisted Edwards curves. It is designed to be faster than existing digital signature schemes without sacrificing security. EdDSA signatures use the Edwards form of elliptic curves and the algorithm relies on the difficulty of the elliptic-curve discrete logarithm problem. As was the case above, EdDSA provides 128 bits of security when used with 256 bit keys (such as Ed25519) which generate 64 byte signatures.

    Summary of Types

    The ring::signature module contains the following types and functions:

    trait KeyPair – Key pairs for signing messages (private key and public key). Each of the key pairs for the various digital signature algorithms implement this trait.

    trait VerificationAlgorithm – A signature verification algorithm. Each of the various signature verification algorithms implement this trait.

    struct Signature – A public key signature returned from a signing operation. This is basically a wrapper around the signature byte array.

    struct UnparsedPublicKey – An unparsed, possibly malformed, public key for signature verification. The message and signature is passed into the verify message which is a method on the public key type. This design enforces validation/parsing of the public key before verifying a signature which is an important security consideration to prevent certain vulnerabilities.

    For a full listing of all types see the module documentation here.

    Rust Imports

    Let’s start by importing the required types into our project.

    use ring::error::Unspecified;
    use ring::rand::SystemRandom;
    use ring::signature;
    use ring::signature::KeyPair;
    use ring::signature::UnparsedPublicKey;
    use ring::signature::EcdsaKeyPair;
    use ring::signature::Ed25519KeyPair;
    use ring::signature::ECDSA_P256_SHA256_ASN1;
    use ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING;
    use ring::signature::ED25519;

    Signing & Verifying a Message using ECDSA

    In the example below we use the ECDSA_P256_SHA256_ASN1_SIGNING algorithm for signing a message and the ECDSA_P256_SHA256_ASN1 algorithm for verifying the signature.

    First start by generating a new ECDSA key pair using an instance of SystemRandom as the source of entropy (for more background on generating random numbers in Rust see here). First create the key pair in pkcs8 document format which can be used to persist the key if needed and then we convert it into an EcdsaKeyPair.

    // generate a new ECDSA key pair
    let rand = SystemRandom::new();
    let pkcs8_bytes = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rand)?; // pkcs8 format used for persistent storage
    let key_pair = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8_bytes.as_ref(), &rand).map_err(|_| Unspecified)?;

    We then create a message and sign it by passing in the message to the sign method which returns the the signature on success. Note that the SystemRandom is used in the sign method to generate a random nonce for each signature. The nonce must be random and never re-used so you should make sure that only a single instance of SystemRandom is used throughout the program in order to prevent the possibility of using the same source of entropy multiple times.

    // create a message and sign using the key pair
    const MESSAGE: &[u8] = b"hello, world";
    let sig = key_pair.sign(&rand, MESSAGE)?;

    The public key is then passed into a new instance of UnparsedPublicKey. Next the verify method is called on the UnparsedPublicKey instance passing in the message and signature bytes. The verify method returns () on success and error::Unspecified otherwise.

    // get the public key as bytes
    let peer_public_key_bytes = key_pair.public_key().as_ref();
    
    // verify the signature using the public key and message
    let peer_public_key = UnparsedPublicKey::new(&ECDSA_P256_SHA256_ASN1, peer_public_key_bytes);
    peer_public_key.verify(MESSAGE, sig.as_ref())?;

    Signing & Verifying a Message using EdDSA

    Here is another example which is similar to the above scenario but instead this time we use EdDSA for signing and verification. Note that the signing method doesn’t require passing in the SystemRandom instance in this case. We use the default EdDSA signing algorithm and the ED25519 verification algorithm.

    // generate a new Ed25519 key pair
    let rand = SystemRandom::new();
    let pkcs8_bytes = Ed25519KeyPair::generate_pkcs8(&rand)?; // pkcs8 format used for persistent storage
    let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref()).map_err(|_| Unspecified)?;
    
    // create a message and sign using the key pair
    const MESSAGE: &[u8] = b"hello, world";
    let sig = key_pair.sign(MESSAGE);
    
    // get the public key as bytes
    let peer_public_key_bytes = key_pair.public_key().as_ref();
    
    // verify the signature using the public key and message
    let peer_public_key = UnparsedPublicKey::new(&ED25519, peer_public_key_bytes);
    peer_public_key.verify(MESSAGE, sig.as_ref())

    Full Sample Code

    Here is the full code sample for reference. 

    use ring::error::Unspecified;
    use ring::rand::SystemRandom;
    use ring::signature;
    use ring::signature::KeyPair;
    use ring::signature::UnparsedPublicKey;
    use ring::signature::EcdsaKeyPair;
    use ring::signature::Ed25519KeyPair;
    use ring::signature::ECDSA_P256_SHA256_ASN1;
    use ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING;
    use ring::signature::ED25519;
    
    fn main() -> Result<(), Unspecified> {
        // generate a new ECDSA key pair
        let rand = SystemRandom::new();
        let pkcs8_bytes = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING,&rand)?; // pkcs8 format used for persistent storage
        let key_pair = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8_bytes.as_ref(), &rand).map_err(|_| Unspecified)?;
    
        // create a message and sign using the key pair
        const MESSAGE: &[u8] = b"hello, world";
        let sig = key_pair.sign(&rand,MESSAGE)?;
    
        // get the public key as bytes
        let peer_public_key_bytes = key_pair.public_key().as_ref();
    
        // verify the signature using the public key and message
        let peer_public_key = UnparsedPublicKey::new(&ECDSA_P256_SHA256_ASN1, peer_public_key_bytes);
        peer_public_key.verify(MESSAGE, sig.as_ref())?;
    
        // generate a new Ed25519 key pair
        let pkcs8_bytes = Ed25519KeyPair::generate_pkcs8(&rand)?; // pkcs8 format used for persistent storage
        let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref()).map_err(|_| Unspecified)?;
    
        // create a message and sign using the key pair
        let sig = key_pair.sign(MESSAGE);
    
        // get the public key as bytes
        let peer_public_key_bytes = key_pair.public_key().as_ref();
    
        // verify the signature using the public key and message
        let peer_public_key = UnparsedPublicKey::new(&ED25519, peer_public_key_bytes);
        peer_public_key.verify(MESSAGE, sig.as_ref())
    }

    Conclusion

    In this post I gave a brief introduction to digital signatures, introduced the signature module in the Ring cryptography library and gave a few examples showing how to create and verify signatures. I demonstrated how to use the sign method defined on the key pair types to return an instance of Signature and then how to use the verify method defined on the UnparsedPublicKey type to check signatures for validity.

    Leave a Reply

    Your email address will not be published. Required fields are marked *