OpenSSL content verification in Clear Linux* OS

11 Aug, 2016

Client updates: verification of the signed MoM

Before we can begin to determine whether content is secure and verifiable, we must consider an important first step: starting from a known point of trust.

A client running Clear Linux boots into a system that has established trust before it ever reaches out to the web. This is accomplished by including the public certificate with each image we release during image creation. The certificate cannot be modified by anyone other than root, and this ensures that all future content is trusted before installing it on the file system.

The client implements OpenSSL verification as explained above; however, the process is much more complex than verifying via command line. The client-updater code follows this general process:

  • Initializes OpenSSL and certificate

  • Extracts the public key from the certificate file

  • Validates the certificate before it can be used

  • Sets up the proper certificate stores

  • Verifies content signature

    • First checks the local file system for the signature and the manifest to verify
    • Downloads the signature and manifest if they fail verification or if they do not exist
  • Returns result & terminates signatures (cleanup)

Initializing OpenSSL

The OpenSSL context must be set up correctly for any verification to work; thus, the very first lines called are:

ERR_load_crypto_strings(); ERR_load_PKCS7_strings(); EVP_add_digest(EVP_sha256());

The first two lines are fairly self-explanatory: they load the error strings for all of the libcrypto and PKCS7 functions that we may call. The important call here is EVP_add_digest() because it defines exactly what hashing algorithm we want the verification to use (SHA256), as opposed to loading all the digests and cipher algorithms. It provides added security by only supporting certificates and signatures that use that specific algorithm, while ignoring weaker cipher suites such as SHA1.

From there we open the certificate as read only and then read it in to an X509 struct with the following:

fp_pubkey = fopen(CERTNAME, "r"); cert = PEM_read_X509(fp_pubkey, NULL, NULL, NULL);

Creating a certificate store

The cert is added to the certificate store and is used as part of the certificate validation (as well as the signature verification) since the store is passed into the verify function.

At this point, we have a loaded certificate, but the certificate store must be set up so we can pass it to verify and to validate the certificate chain (if there is one). The relevant portions to do this are:

/* create the cert store and set the verify callback */ if (!(store = X509_STORE_new())) { fprintf(stderr, "Failed X509_STORE_new() for %s\n", CERTNAME); goto error; } /* Add the certificates to be verified to the store */ if (!(lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file()))) { fprintf(stderr, "Failed X509_STORE_add_lookup() for %s\n", CERTNAME); goto error; } /* Load our Root cert, which can be in either DER or PEM format */ if (!X509_load_cert_file(lookup, CERTNAME, X509_FILETYPE_PEM)) { fprintf(stderr, "Failed X509_load_cert_file() for %s\n", CERTNAME); goto error; } /* create a verification context and initialize it */ if (!(verify_ctx = X509_STORE_CTX_new())) { fprintf(stderr, "Failed X509_STORE_CTX_new() for %s\n", CERTNAME); goto error; } /* Initialize the verification context withthe store and certificate */ if (X509_STORE_CTX_init(verify_ctx, store, cert, NULL) != 1) { fprintf(stderr, "Failed X509_STORE_CTX_init() for %s\n", CERTNAME); goto error; } /* Specify which cert to validate in the verify context. * This is required because we may add multiple certs to the X509 store, * but we want to validate a specific one out of the group/chain. */ X509_STORE_CTX_set_cert(verify_ctx, cert); /* verify the certificate */ if (X509_verify_cert(verify_ctx) != 1) { fprintf(stderr, "Failed X509_verify_cert() for %s\n", CERTNAME); goto error; }

A self-signed certificate should always verify as "valid"; however, if a chain and CRL are provided, then that chain will be validated if passed into the verify context. The certificate store acts as a container for the entire chain, and we choose which cert out of them needs to be verified. Later in the signature verification, the store is also used by the PKCS7 verify function.

Verifying the signature

With the certificate loaded and the store initialized, the Manifest.MoM is finally able to be verified against the signature. The following code shows the proper way to intialize all the required data structures for performing signature verification, as well as the call to PKCS7_verify().

/* get the signature */ sig_fd = open(sig_filename, O_RDONLY); if (sig_fd == -1) { string_or_die(&errorstr, "Failed open %s: %s\n", sig_filename, strerror(errno)); goto error; } if (fstat(sig_fd, &st) != 0) { string_or_die(&errorstr, "Failed to stat %s file\n", sig_filename); goto error; } sig_len = st.st_size; sig = mmap(NULL, sig_len, PROT_READ, MAP_PRIVATE, sig_fd, 0); if (sig == MAP_FAILED) { string_or_die(&errorstr, "Failed to mmap %s signature\n", sig_filename); goto error; } sig_BIO = BIO_new_mem_buf(sig, sig_len); if (!sig_BIO) { string_or_die(&errorstr, "Failed to read %s signature into BIO\n", sig_filename); goto error; } /* the signature is in DER format, so d2i it into verification pkcs7 form */ p7 = d2i_PKCS7_bio(sig_BIO, NULL); if (p7 == NULL) { string_or_die(&errorstr, "NULL PKCS7 File\n"); goto error; } /* get the data to be verified */ data_fd = open(data_filename, O_RDONLY); if (data_fd == -1) { string_or_die(&errorstr, "Failed open %s\n", data_filename); goto error; } if (fstat(data_fd, &st) != 0) { string_or_die(&errorstr, "Failed to stat %s\n", data_filename); goto error; } data_len = st.st_size; data = mmap(NULL, data_len, PROT_READ, MAP_PRIVATE, data_fd, 0); if (data == MAP_FAILED) { string_or_die(&errorstr, "Failed to mmap %s\n", data_filename); goto error; } data_BIO = BIO_new_mem_buf(data, data_len); if (!data_BIO) { string_or_die(&errorstr, "Failed to read %s into BIO\n", data_filename); goto error; } /* munge the signature and data into a verifiable format */ verify_BIO = PKCS7_dataInit(p7, data_BIO); if (!verify_BIO) { string_or_die(&errorstr, "Failed PKCS7_dataInit()\n"); goto error; } /* Verify the signature, outdata can be NULL because we don't use it */ ret = PKCS7_verify(p7, x509_stack, store, verify_BIO, NULL, 0);

The Manifest.MoM (the data to be verified), and the Manifest.MoM.sig (the provided signature file) are both loaded into memory and are then used to initialize some BIO memory buffers; the data must be added into these BIO structs for the PKCS7 functions to be able to use them.

An important call to note is:

p7 = d2i_PKCS7_bio(sig_BIO, NULL);

This reads the signature from the BIO into a PKCS7 object, specifically in DER format - which can be confirmed using the OpenSSL command line. If the signature is opened with the wrong format, the verification will fail. Once we have the PKCS7 object, we create a verification BIO that contains it and the data BIO (Manifest.MoM), and pass it into PKCS7_verify(), which handles all of the verification behind the scenes. The verification function does all the hashing and checking of the data and signature, returning one if it successfully verifies the signature.

If the signature verifies correctly, the Manifest.MoM is confirmed to be legitimate, and the rest of the software update process continues. The manifest and signature are first verified on disk if they exist. If that check fails, they are removed, and a new manifest and signature file are downloaded and verified again. Should the verification fail, no content can be trusted, and the system's trust is said to be broken. Because of all the content branching from the Manifest.MoM, it is the only file we need to verify to ensure all other content is also trusted, keeping verification very minimal.