Tutorial: Code Signing and Verification with OpenSSL

September 7, 2016 | 7 min Read

Code signing and verification is the process of digitally signing executables or scripts to ensure that the software you are executing has not been altered since it was signed. Code signing helps protect against corrupt artifacts, process breakdown (accidentally delivering the wrong thing) and even malicious intents. We have recently started implementing code verification in J2V8. Code verification has been implemented in the native code using OpenSSL.

Code signing and verification works as follows. In addition to writing the code, the author executes a hash function with the code as the input, producing a digest. The digest is signed with the author’s private key, producing the signature. The code, signature and hash function are then delivered to the verifier. The verifier produces the digest from the code using the same hash function, and then uses the public key to decrypt the signature. If both digests match, then the verifier can be confident that the code has not been tampered with.

In this tutorial we will demonstrate how you can use OpenSSL to sign and verify a script. This tutorial will describe both the OpenSSL command line, and the C++ APIs.

Key Generation

Before you can begin the process of code signing and verification, you must first create a public/private key pair. The ssh-keygen -t rsa can be used to generate key pairs.

$ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/irbull/.ssh/id_rsa): ./example_rsa
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in ./example_rsa.
Your public key has been saved in ./example_rsa.pub.
The key fingerprint is:
35:26:61:ae:23:11:6c:e1:88:39:31:c5:0f:06:f7:71 me@mymachine
The key randomart image is:
+--[ RSA 2048]----+
|++o.+.E o        |
| *++o+ o .       |
|+..++   o +      |
| .  .. . + .     |
|    . o S        |
|     . .         |
|                 |
|                 |
|                 |
+-----------------+

If you’re interested in what randomart is, checkout the answer on StackExchange.

Converting to PEM format

The standard file format for OpenSSL is the PEM format. The PEM format is intended to be readable in ASCII and safe for ASCII editors and text documents. The PEM format is a container format and can include public certificates, or certificate chains including the public key, private key and root certificate. PEM files can be recognized by the BEGIN and END headers. To export a public key in PEM format use the following OpenSSL command.

openssl rsa -in example_rsa -pubout -out public.key.pem

Code Signing

OpenSSL makes it relatively easy to compute the digest and signature from a plaintext using a single API. However, before you begin you must first create an RSA object from your private key:

RSA* createPrivateRSA(std::string key) {
  RSA *rsa = NULL;
  const char* c_string = key.c_str();
  BIO * keybio = BIO_new_mem_buf((void*)c_string, -1);
  if (keybio==NULL) {
      return 0;
  }
  rsa = PEM_read_bio_RSAPrivateKey(keybio, &rsa,NULL, NULL);
  return rsa;
}

With an RSA object and plaintext you can create the digest and digital signature:

bool RSASign( RSA* rsa, 
              const unsigned char* Msg, 
              size_t MsgLen,
              unsigned char** EncMsg, 
              size_t* MsgLenEnc) {
  EVP_MD_CTX* m_RSASignCtx = EVP_MD_CTX_create();
  EVP_PKEY* priKey  = EVP_PKEY_new();
  EVP_PKEY_assign_RSA(priKey, rsa);
  if (EVP_DigestSignInit(m_RSASignCtx,NULL, EVP_sha256(), NULL,priKey)<=0) {
      return false;
  }
  if (EVP_DigestSignUpdate(m_RSASignCtx, Msg, MsgLen) <= 0) {
      return false;
  }
  if (EVP_DigestSignFinal(m_RSASignCtx, NULL, MsgLenEnc) <=0) {
      return false;
  }
  *EncMsg = (unsigned char*)malloc(*MsgLenEnc);
  if (EVP_DigestSignFinal(m_RSASignCtx, *EncMsg, MsgLenEnc) <= 0) {
      return false;
  }
  EVP_MD_CTX_cleanup(m_RSASignCtx);
  return true;
}

This works by first creating a signing context, and then initializing the context with the hash function (SHA-256 in our case) and the private key. The message is then added to the context, and finally the signature length is computed. Space for the signature is then allocated and finally the signature (signed digest) computed. EncMsg will hold the signature and MsgLenEnc will hold the length of the signature. The signature should not be treated as a string.

If you need to print the signature or write it to non-binary file, you should Base64 encode it. OpenSSL provides an API to help with this. Barry Steyn has put together a simple example that shows how to use this API. Below is a slightly modified version of his code:

void Base64Encode( const unsigned char* buffer, 
                   size_t length, 
                   char** base64Text) { 
  BIO *bio, *b64;
  BUF_MEM *bufferPtr;

  b64 = BIO_new(BIO_f_base64());
  bio = BIO_new(BIO_s_mem());
  bio = BIO_push(b64, bio);

  BIO_write(bio, buffer, length);
  BIO_flush(bio);
  BIO_get_mem_ptr(bio, &bufferPtr);
  BIO_set_close(bio, BIO_NOCLOSE);
  BIO_free_all(bio);

  *base64Text=(*bufferPtr).data;
}

Putting this all together you can create a signed digest in a Base64 encoded string:

char* signMessage(std::string privateKey, std::string plainText) {
  RSA* privateRSA = createPrivateRSA(privateKey);
  unsigned char* encMessage;
  char* base64Text;
  size_t encMessageLength;
  RSASign(privateRSA, (unsigned char*) plainText.c_str(), plainText.length(), &encMessage, &encMessageLength);
  Base64Encode(encMessage, encMessageLength, &base64Text);
  free(encMessage);
  return base64Text;
}

The character array base64Text will hold the result.

OpenSSL Command Line

You can also create a digest and digital signature using the following OpenSSL commands. The first command will create the digest and signature. The signature will be written to sign.txt.sha256 as binary. The second command Base64 encodes the signature.

openssl dgst -sha256 -sign my_private.key -out sign.txt.sha256 codeToSign.txt
openssl enc -base64 -in sign.txt.sha256 -out sign.txt.sha256.base64

Signature Verification

Signature verification ensures that the signature matches the original code. If the code was altered at all (even the addition of a single newline character) then a different signature will be produced and the verification will fail.

Signature verification works in the opposite direction. In order to verify that the signature is correct, you must first compute the digest using the same algorithm as the author. Then, using the public key, you decrypt the author’s signature and verify that the digests match.

Again, OpenSSL has an API for computing the digest and verifying the signature. Since we wrote the signature with a Base64 encoding, we must first decode it. Again, Barry Steyn has a detailed example of how to do this on his blog. A copy of his code can be found below.

size_t calcDecodeLength(const char* b64input) {
  size_t len = strlen(b64input), padding = 0;

  if (b64input[len-1] == '=' && b64input[len-2] == '=') //last two chars are =
    padding = 2;
  else if (b64input[len-1] == '=') //last char is =
    padding = 1;
  return (len*3)/4 - padding;
}

void Base64Decode(const char* b64message, unsigned char** buffer, size_t* length) {
  BIO *bio, *b64;

  int decodeLen = calcDecodeLength(b64message);
  *buffer = (unsigned char*)malloc(decodeLen + 1);
  (*buffer)[decodeLen] = '\0';

  bio = BIO_new_mem_buf(b64message, -1);
  b64 = BIO_new(BIO_f_base64());
  bio = BIO_push(b64, bio);

  *length = BIO_read(bio, *buffer, strlen(b64message));
  BIO_free_all(bio);
}

In addition to decoding the Base64 encoded signature, you must also create an RSA object from the public key. This is similar to how the RSA object was created from the private key when the signature was computed.

RSA* createPublicRSA(std::string key) {
  RSA *rsa = NULL;
  BIO *keybio;
  const char* c_string = key.c_str();
  keybio = BIO_new_mem_buf((void*)c_string, -1);
  if (keybio==NULL) {
      return 0;
  }
  rsa = PEM_read_bio_RSA_PUBKEY(keybio, &rsa,NULL, NULL);
  return rsa;
}

Finally, with the RSA object, original message and binary encoded signature, you can verify that the signature matches the plain text.

bool RSAVerifySignature( RSA* rsa, 
                         unsigned char* MsgHash, 
                         size_t MsgHashLen, 
                         const char* Msg, 
                         size_t MsgLen, 
                         bool* Authentic) {
  *Authentic = false;
  EVP_PKEY* pubKey  = EVP_PKEY_new();
  EVP_PKEY_assign_RSA(pubKey, rsa);
  EVP_MD_CTX* m_RSAVerifyCtx = EVP_MD_CTX_create();

  if (EVP_DigestVerifyInit(m_RSAVerifyCtx,NULL, EVP_sha256(),NULL,pubKey)<=0) {
    return false;
  }
  if (EVP_DigestVerifyUpdate(m_RSAVerifyCtx, Msg, MsgLen) <= 0) {
    return false;
  }
  int AuthStatus = EVP_DigestVerifyFinal(m_RSAVerifyCtx, MsgHash, MsgHashLen);
  if (AuthStatus==1) {
    *Authentic = true;
    EVP_MD_CTX_cleanup(m_RSAVerifyCtx);
    return true;
  } else if(AuthStatus==0){
    *Authentic = false;
    EVP_MD_CTX_cleanup(m_RSAVerifyCtx);
    return true;
  } else{
    *Authentic = false;
    EVP_MD_CTX_cleanup(m_RSAVerifyCtx);
    return false;
  }
}

The output variable Authentic holds the result of the verification. The verification works by first creating a verification context. The context is initialized with the hash function used (SHA-256 in our case) and the public key. The original message is then provided and finally the verification is performed.

Putting this all together, you can verify a signature given the original text, the signature and public key as follows:

bool verifySignature(std::string publicKey, std::string plainText, char* signatureBase64) {
  RSA* publicRSA = createPublicRSA(publicKey);
  unsigned char* encMessage;
  size_t encMessageLength;
  bool authentic;
  Base64Decode(signatureBase64, &encMessage, &encMessageLength);
  bool result = RSAVerifySignature(publicRSA, encMessage, encMessageLength, plainText.c_str(), plainText.length(), &authentic);
  return result & authentic;
}

OpenSSL Command Line

Finally, the OpenSSL command line tool can also be used to decode and verify a digital signature.

openssl enc -base64 -d -in sign.txt.sha256.base64 -out sign.txt.sha256 
openssl dgst -sha256 -verify public.key.pem -signature sign.txt.sha256 codeToSign.txt

Conclusion

So that’s it, with either the OpenSSL API or the command line you can sign and verify a code fragment to ensure that it has not been altered since it was authored. You can even mix & match the command line tools with the API, so you can generate the signatures during a build and verify them during program execution. All the code for this example can be found on GitHub.

We will be including a code verification API in the upcoming version of J2V8. For more news about J2V8 and other things I find interesting, follow me on Twitter.

Ian Bull

Ian Bull

Ian is an Eclipse committer and EclipseSource Distinguished Engineer with a passion for developer productivity.

He leads the J2V8 project and has served on several …