Proof Maintenance Exercises: s2n HMAC¶
The evolution of a program is accompanied by the evolution of its specifications. A key part of using SAW and Cryptol to verify a software system is the ongoing maintenance of proof artifacts through the software development lifecycle.
Proof maintenance is the process of preserving the correspondence between a program, its specification, and its proof of correctness as requirements change over time. This section poses as an exercise an extended proof-maintenance task, adapted from these changes to the HMAC implementation in Amazon’s s2n. The code’s file structure has been reorganized slightly, but the code itself is untouched.
This task will be approached as if the changes to the implementation are given, and the goal will be to evolve the relevant specifications to match. While completing the exercises, take note of the correspondences between the changes to the code and the changes to the specifications.
Background: The Updates to the Implementation¶
This section provides an overview of the changes to the implementation that form the basis of the proof maintenance task to be completed.
The s2n HMAC implementation needed to be updated to make use of an additional
piece of hashing state, outer_just_key
, for the implementation of TLS.
At its core, this change is captured by the addition of
a new field to the s2n_hmac_state
structure as it is defined in
s2n_hmac_old.h
. The resulting structure looks like this:
struct s2n_hmac_state {
s2n_hmac_algorithm alg;
uint16_t hash_block_size;
uint32_t currently_in_hash_block;
uint16_t xor_pad_size;
uint8_t digest_size;
struct s2n_hash_state inner;
struct s2n_hash_state inner_just_key;
struct s2n_hash_state outer;
struct s2n_hash_state outer_just_key;
/* key needs to be as large as the biggest block size */
uint8_t xor_pad[128];
/* For storing the inner digest */
uint8_t digest_pad[SHA512_DIGEST_LENGTH];
};
The addition of this new field saw corresponding changes to the implementation
code, which can be found in s2n_hmac_new.c
. These changes included memory
allocations, initializations, updates, and frees. The following code sample
gives a good sense of the types of changes involved:
static int s2n_tls_hmac_init(struct s2n_hmac_state *state, s2n_hmac_algorithm alg, const void *key, uint32_t klen)
{
s2n_hash_algorithm hash_alg;
GUARD(s2n_hmac_hash_alg(alg, &hash_alg));
GUARD(s2n_hash_init(&state->inner, hash_alg));
GUARD(s2n_hash_init(&state->inner_just_key, hash_alg));
GUARD(s2n_hash_init(&state->outer, hash_alg));
GUARD(s2n_hash_init(&state->outer_just_key, hash_alg));
uint32_t copied = klen;
if (klen > state->xor_pad_size) {
GUARD(s2n_hash_update(&state->outer, key, klen));
GUARD(s2n_hash_digest(&state->outer, state->digest_pad, state->digest_size));
memcpy_check(state->xor_pad, state->digest_pad, state->digest_size);
copied = state->digest_size;
} else {
memcpy_check(state->xor_pad, key, klen);
}
for (int i = 0; i < copied; i++) {
state->xor_pad[i] ^= 0x36;
}
for (int i = copied; i < state->xor_pad_size; i++) {
state->xor_pad[i] = 0x36;
}
GUARD(s2n_hash_update(&state->inner_just_key, state->xor_pad, state->xor_pad_size));
/* 0x36 xor 0x5c == 0x6a */
for (int i = 0; i < state->xor_pad_size; i++) {
state->xor_pad[i] ^= 0x6a;
}
GUARD(s2n_hash_update(&state->outer_just_key, state->xor_pad, state->xor_pad_size));
memset(&state->xor_pad, 0, sizeof(state->xor_pad));
return s2n_hmac_reset(state);
}
The complete diff between s2n_hmac_old.c
and s2n_hmac_new.c
shows a
number of updates similar to that above:
--- /Users/atomb/galois/saw-training/downloads/examples/hmac/s2n_hmac_old.c
+++ /Users/atomb/galois/saw-training/downloads/examples/hmac/s2n_hmac_new.c
@@ -18,7 +18,7 @@
#include "s2n_errno.h"
-#include "s2n_hmac_old.h"
+#include "s2n_hmac_new.h"
#include "s2n_hash.h"
#include "s2n_fips.h"
@@ -94,11 +94,15 @@
hash_alg = S2N_HASH_SHA1;
}
+ GUARD(s2n_hash_init(&state->inner, hash_alg));
+ GUARD(s2n_hash_init(&state->inner_just_key, hash_alg));
+ GUARD(s2n_hash_init(&state->outer, hash_alg));
+ GUARD(s2n_hash_init(&state->outer_just_key, hash_alg));
+
for (int i = 0; i < state->xor_pad_size; i++) {
state->xor_pad[i] = 0x36;
}
- GUARD(s2n_hash_init(&state->inner_just_key, hash_alg));
GUARD(s2n_hash_update(&state->inner_just_key, key, klen));
GUARD(s2n_hash_update(&state->inner_just_key, state->xor_pad, state->xor_pad_size));
@@ -106,9 +110,9 @@
state->xor_pad[i] = 0x5c;
}
- GUARD(s2n_hash_init(&state->outer, hash_alg));
GUARD(s2n_hash_update(&state->outer, key, klen));
GUARD(s2n_hash_update(&state->outer, state->xor_pad, state->xor_pad_size));
+ memset(&state->xor_pad, 0, sizeof(state->xor_pad));
/* Copy inner_just_key to inner */
return s2n_hmac_reset(state);
@@ -123,14 +127,15 @@
return s2n_hash_digest(&state->inner, out, size);
}
-
static int s2n_tls_hmac_init(struct s2n_hmac_state *state, s2n_hmac_algorithm alg, const void *key, uint32_t klen)
{
s2n_hash_algorithm hash_alg;
GUARD(s2n_hmac_hash_alg(alg, &hash_alg));
+ GUARD(s2n_hash_init(&state->inner, hash_alg));
GUARD(s2n_hash_init(&state->inner_just_key, hash_alg));
GUARD(s2n_hash_init(&state->outer, hash_alg));
+ GUARD(s2n_hash_init(&state->outer_just_key, hash_alg));
uint32_t copied = klen;
if (klen > state->xor_pad_size) {
@@ -156,6 +161,9 @@
for (int i = 0; i < state->xor_pad_size; i++) {
state->xor_pad[i] ^= 0x6a;
}
+
+ GUARD(s2n_hash_update(&state->outer_just_key, state->xor_pad, state->xor_pad_size));
+ memset(&state->xor_pad, 0, sizeof(state->xor_pad));
return s2n_hmac_reset(state);
}
@@ -201,6 +209,7 @@
GUARD(s2n_hash_new(&state->inner));
GUARD(s2n_hash_new(&state->inner_just_key));
GUARD(s2n_hash_new(&state->outer));
+ GUARD(s2n_hash_new(&state->outer_just_key));
return 0;
}
@@ -269,8 +278,7 @@
}
GUARD(s2n_hash_digest(&state->inner, state->digest_pad, state->digest_size));
- GUARD(s2n_hash_reset(&state->outer));
- GUARD(s2n_hash_update(&state->outer, state->xor_pad, state->xor_pad_size));
+ GUARD(s2n_hash_copy(&state->outer, &state->outer_just_key));
GUARD(s2n_hash_update(&state->outer, state->digest_pad, state->digest_size));
return s2n_hash_digest(&state->outer, out, size);
@@ -303,6 +311,7 @@
GUARD(s2n_hash_free(&state->inner));
GUARD(s2n_hash_free(&state->inner_just_key));
GUARD(s2n_hash_free(&state->outer));
+ GUARD(s2n_hash_free(&state->outer_just_key));
return 0;
}
@@ -333,6 +342,8 @@
GUARD(s2n_hash_copy(&to->inner, &from->inner));
GUARD(s2n_hash_copy(&to->inner_just_key, &from->inner_just_key));
GUARD(s2n_hash_copy(&to->outer, &from->outer));
+ GUARD(s2n_hash_copy(&to->outer_just_key, &from->outer_just_key));
+
memcpy_check(to->xor_pad, from->xor_pad, sizeof(to->xor_pad));
memcpy_check(to->digest_pad, from->digest_pad, sizeof(to->digest_pad));
From these changes alone, the work needed to keep the proofs up-to-date with the implementation can be very reasonably estimated. In this case, it will be necessary to complete the following tasks:
Add the new field to the correct type(s) in the Cryptol reference implementation
Add the relevant implementation details to the function(s) using the changed type
Update the SAWScript to reflect new memory layouts, initializations, etc implied by the updated type
Take note of the similarities to the rotr3
example in
Exercise: Swapping and Rotating; these kinds of update are ubiquitous when working
on proof maintenance tasks. It will help to review that section before
completing these exercises.
Exercise: Update the Cryptol Specification¶
In order for verification to go through, the Cryptol specification (that is, the implementation trusted to be correct) must be updated to reflect the existence of the new state field introduced above.
Your task is to perform these updates in
HMAC_iterative_old.cry
.
Use the bullet points above as a rough guide, and if you get stuck, there is a
complete solution presented on the next page.
Exercise: Update the SAW Specifications¶
The final step to proof maintenance is updating the SAW portion of the specification. This can range in difficulty from simply updating memory layouts to changing what the specification actually asserts about the program. For the HMAC updates, the necessary changes are closer to the former rather than the latter, since the implementation change was the addition of a data field rather than overall changes to the control flow.
In this exercise, you will edit the file
HMAC_old.saw
to add the
memory layout information for the state field added to the C implementation.
Hint: A reliable strategy for updating HMAC_old.saw
to account for
outer_just_key
is a simple search for the names of other fields already
present in the structure; these will likely appear where memory layouts and
initializations that need to be augmented are specified.
Note
HMAC_old.saw
does not use the helpers.saw
file
as the previous examples did. Feel free to consult helpers.saw
to
help understand what the various functions do, and perhaps even
rewrite HMAC_old.saw
to use the helper functions.
As before, if you get stuck, there is a complete solution presented on the next page.