hare

The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit f0fcc878219cc034a9f110f6a2b36912db148238
parent fd6a3ec9675a5d98759f41d3c1d17f3e4752134f
Author: Drew DeVault <sir@cmpwn.com>
Date:   Tue, 23 Nov 2021 12:31:33 +0100

crypto: address secure erasure of secret data

Signed-off-by: Drew DeVault <sir@cmpwn.com>

Diffstat:
Abytes/zero.ha | 7+++++++
Mcrypto/aes/aes_ct64.ha | 35++++++++++++++++++++++-------------
Mcrypto/aes/cbc+test.ha | 10++++++++--
Mcrypto/aes/ct64+test.ha | 33+++++++++++++++++++++++++--------
Mcrypto/aes/ctr+test.ha | 29+++++++++++++++++++++++------
Mcrypto/cipher/block.ha | 5+++++
Mcrypto/cipher/ctr.ha | 10++++++++++
Acrypto/conventions.txt | 17+++++++++++++++++
Mcrypto/hmac/hmac.ha | 7+++++++
Mscripts/gen-stdlib | 7++++---
Mstdlib.mk | 12+++++++-----
11 files changed, 135 insertions(+), 37 deletions(-)

diff --git a/bytes/zero.ha b/bytes/zero.ha @@ -0,0 +1,7 @@ +// Sets all bytes in a slice to zero. This is suitable for erasing private data +// from a slice. +export fn zero(buf: []u8) void = { + for (let i = 0z; i < len(buf); i += 1) { + buf[i] = 0; + }; +}; diff --git a/crypto/aes/aes_ct64.ha b/crypto/aes/aes_ct64.ha @@ -46,21 +46,25 @@ export type ct64_block = struct { // Returns an AES [[crypto::cipher::block]] cipher implementation optimized for // constant time operation on 64-bit systems. -export fn ct64(key: []u8) ct64_block = { - let state = ct64_block { - blocksz = BLOCKSIZE, - nparallel = CT64_NPARALLEL, - encrypt = &aes_ct64_encrypt, - decrypt = &aes_ct64_decrypt, - ... - }; +// +// The caller must call [[ct64_init]] to add a key to the cipher before using +// the cipher, and must call [[crypto::cipher::finish]] when they are finished +// using the cipher to securely erase any secret data stored in the cipher +// state. +export fn ct64() ct64_block = ct64_block { + blocksz = BLOCKSIZE, + nparallel = CT64_NPARALLEL, + encrypt = &aes_ct64_encrypt, + decrypt = &aes_ct64_decrypt, + finish = &aes_ct64_finish, + ... +}; +// Initializes the ct64 AES implementation with an encryption key. +export fn ct64_init(cipher: *ct64_block, key: []u8) void = { let comp_skey: [30]u64 = [0...]; - state.rounds = br_aes_ct64_keysched(comp_skey[..], key, len(key)); - - br_aes_ct64_skey_expand(state.sk_exp, state.rounds, comp_skey[..]); - - return state; + cipher.rounds = br_aes_ct64_keysched(comp_skey[..], key, len(key)); + br_aes_ct64_skey_expand(cipher.sk_exp, cipher.rounds, comp_skey[..]); }; // Combines up to 4 blocks and encrypts them in one run @@ -118,6 +122,11 @@ fn aes_ct64_decrypt(block: *cipher::block, dest: []u8, src: []u8) void = { br_range_enc32le(dest, w); }; +fn aes_ct64_finish(block: *cipher::block) void = { + let b = block: *ct64_block; + b.sk_exp = [0...]; +}; + // see br_aes_ct64_ortho in src/inner.h of BearSSL fn br_aes_ct64_ortho(q: []u64) void = { swapn(0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 1, &q[0], &q[1]); diff --git a/crypto/aes/cbc+test.ha b/crypto/aes/cbc+test.ha @@ -37,7 +37,10 @@ use bytes; let result: [64]u8 = [0...]; let buf: [CBC_BUFSIZE]u8 = [0...]; - let b = ct64(key); + let b = ct64(); + ct64_init(&b, key); + defer cipher::finish(&b); + let cbc = cipher::cbc_encryptor(&b, iv[..], buf[..]); cipher::cbc_encrypt(&cbc, result, plain); @@ -85,7 +88,10 @@ use bytes; let result: [64]u8 = plain; let buf: [CBC_BUFSIZE]u8 = [0...]; - let b = ct64(key); + let b = ct64(); + ct64_init(&b, key); + defer cipher::finish(&b); + let cbc = cipher::cbc_encryptor(&b, iv[..], buf[..]); cipher::cbc_encrypt(&cbc, result, result); diff --git a/crypto/aes/ct64+test.ha b/crypto/aes/ct64+test.ha @@ -19,7 +19,9 @@ use bytes; let result: [16]u8 = [0...]; - let block = ct64(key[..]); + let block = ct64(); + ct64_init(&block, key); + defer cipher::finish(&block); cipher::encrypt(&block, result[..], plain[..]); assert(bytes::equal(cipher, result)); @@ -54,7 +56,9 @@ use bytes; ]; - let block = ct64(key[..]); + let block = ct64(); + ct64_init(&block, key[..]); + defer cipher::finish(&block); // test from 1 to 4 parallel blocks for (let i = 0z; i < 4; i += 1) { @@ -96,7 +100,9 @@ use bytes; ]; - let block = ct64(key[..]); + let block = ct64(); + ct64_init(&block, key[..]); + defer cipher::finish(&block); // test from 1 to 4 parallel blocks for (let i = 0z; i < 4; i += 1) { @@ -127,7 +133,10 @@ use bytes; let result: [16]u8 = [0...]; - let block = ct64(key[..]); + let block = ct64(); + ct64_init(&block, key[..]); + defer cipher::finish(&block); + cipher::decrypt(&block, result[..], cipher[..]); assert(bytes::equal(plain, result)); @@ -152,7 +161,9 @@ use bytes; let result: [16]u8 = [0...]; - let block = ct64(key[..]); + let block = ct64(); + ct64_init(&block, key[..]); + defer cipher::finish(&block); cipher::encrypt(&block, result[..], plain[..]); assert(bytes::equal(cipher, result)); @@ -179,7 +190,9 @@ use bytes; let result: [16]u8 = plain; - let block = ct64(key[..]); + let block = ct64(); + ct64_init(&block, key[..]); + defer cipher::finish(&block); cipher::encrypt(&block, result[..], result[..]); assert(bytes::equal(cipher, result)); @@ -207,7 +220,9 @@ use bytes; ]; let result: [16]u8 = [0...]; - let block = ct64(key[..]); + let block = ct64(); + ct64_init(&block, key[..]); + defer cipher::finish(&block); cipher::encrypt(&block, result[..], plain[..]); assert(bytes::equal(cipher, result)); @@ -236,7 +251,9 @@ use bytes; ]; let result: [16]u8 = [0...]; - let block = ct64(key[..]); + let block = ct64(); + ct64_init(&block, key[..]); + defer cipher::finish(&block); cipher::encrypt(&block, result[..], plain[..]); assert(bytes::equal(cipher, result)); diff --git a/crypto/aes/ctr+test.ha b/crypto/aes/ctr+test.ha @@ -25,7 +25,10 @@ use bytes; let result: [16]u8 = [0...]; let buf: [CTR_BUFSIZE]u8 = [0...]; - let b = ct64(key); + let b = ct64(); + ct64_init(&b, key); + defer cipher::finish(&b); + let ctr = cipher::ctr(&b, iv[..], buf[..]); cipher::stream_xor(&ctr, result, plain); @@ -58,7 +61,10 @@ use bytes; let result: [18]u8 = [0...]; let buf: [CTR_BUFSIZE]u8 = [0...]; - let b = ct64(key); + let b = ct64(); + ct64_init(&b, key); + defer cipher::finish(&b); + let ctr = cipher::ctr(&b, iv[..], buf[..]); cipher::stream_xor(&ctr, result, plain); @@ -105,7 +111,10 @@ use bytes; let result: [80]u8 = [0...]; let buf: [CTR_BUFSIZE]u8 = [0...]; - let b = ct64(key); + let b = ct64(); + ct64_init(&b, key); + defer cipher::finish(&b); + let ctr = cipher::ctr(&b, iv[..], buf[..]); cipher::stream_xor(&ctr, result, plain); @@ -152,7 +161,9 @@ use bytes; let result: [80]u8 = [0...]; let buf: [CTR_BUFSIZE]u8 = [0...]; - let b = ct64(key); + let b = ct64(); + ct64_init(&b, key); + defer cipher::finish(&b); let ctr = cipher::ctr(&b, iv[..], buf[..]); cipher::stream_xor(&ctr, result[0..32], plain[0..32]); @@ -189,7 +200,10 @@ use bytes; 0xb1, 0xa6, 0xd9, 0x40, 0xaf, 0x1b, 0x02, 0xe1, ]; - let b = ct64(key); + let b = ct64(); + ct64_init(&b, key); + defer cipher::finish(&b); + let buf: [CTR_BUFSIZE]u8 = [0...]; let ctr = cipher::ctr(&b, iv[..], buf[..]); @@ -220,7 +234,10 @@ use bytes; 0xb1, 0xa6, 0xd9, 0x40, 0xaf, 0x1b, 0x02, 0xe1, ]; - let b = ct64(key); + let b = ct64(); + ct64_init(&b, key); + defer cipher::finish(&b); + let buf: [64]u8 = [0...]; let ctr = cipher::ctr(&b, iv[..], buf[..]); diff --git a/crypto/cipher/block.ha b/crypto/cipher/block.ha @@ -6,6 +6,7 @@ export type block = struct { encrypt: *fn(b: *block, dest: []u8, src: []u8) void, decrypt: *fn(b: *block, dest: []u8, src: []u8) void, + finish: *fn(b: *block) void, }; // Returns the block size in bytes for a [[block]] cipher. @@ -22,3 +23,7 @@ export fn encrypt(b: *block, dest: []u8, src: []u8) void = // Decrypt up to [[nparallel]] blocks from 'src' and writes to 'dest'. export fn decrypt(b: *block, dest: []u8, src: []u8) void = b.decrypt(b, dest, src); + +// Discards any state associated with a block cipher algorithm, securely erasing +// secret data from memory. +export fn finish(b: *block) void = b.finish(b); diff --git a/crypto/cipher/ctr.ha b/crypto/cipher/ctr.ha @@ -1,3 +1,4 @@ +use bytes; use crypto::math; export type ctr_stream = struct { @@ -15,6 +16,9 @@ export type ctr_stream = struct { // whose size is equal to the block size times two. The module providing the // underlying block cipher usually provides constants which define the lengths // of these buffers for static allocation. +// +// The user must call [[ctr_finish]] when they are done using the stream to +// securely erase secret information stored in the stream state. export fn ctr(b: *block, iv: []u8, buf: []u8) ctr_stream = { assert(len(iv) == blocksz(b), "iv is of invalid block size"); assert(len(buf) >= blocksz(b) * 2, "buf must be at least 2 * blocksize"); @@ -51,6 +55,12 @@ export fn ctr(b: *block, iv: []u8, buf: []u8) ctr_stream = { }; }; +// Discards state associated with a CTR stream, securely erasing secret data +// from memory. +export fn ctr_finish(ctr: *ctr_stream) void = { + bytes::zero(ctr.xorbuf); +}; + fn ctr_stream_xor(s: *stream, dest: []u8, src: []u8) void = { let ctr = s: *ctr_stream; const bsz = blocksz(ctr.b); diff --git a/crypto/conventions.txt b/crypto/conventions.txt @@ -0,0 +1,17 @@ +This is a WIP document offering some advice on how to implement cryptographic +algorithms securely for Hare. + +All cryptographic algorithms must be constant time, such that an attacker cannot +learn any secret information by analysis of the time required to complete a +cryptographic operation. Not all of the math performed by cryptographic +algorithms in Hare needs to be constant-time: just math whose inputs include +secret information. + +It is important to know that secret data has been securely erased from memory +when it is no longer required. A few items to note about Hare: + +- Return-by-value will leave garbage on the stack which is copied into the + caller's stack frame and abandoned. You cannot return-by-value any objects + which contain secret information. +- Assignments are never optimized away. You can securely erase an array's + contents with array = [0...]; diff --git a/crypto/hmac/hmac.ha b/crypto/hmac/hmac.ha @@ -1,3 +1,4 @@ +use bytes; use crypto::math; use hash; use io; @@ -13,6 +14,10 @@ export type state = struct { // Creates a [[hash::hash]] that computes an HMAC using the provided hash // creation function, e.g. [[crypto::sha256::create]]. +// +// The caller must take extra care to call [[hash::close]] when they are +// finished using the hash, which, in addition to freeing state associated with +// the hash, will securely erase state which contains secret information. export fn hmac(create: *hash::createfunc, key: []u8) state = { let hinner = create(), houter = create(); let bsz = hash::bsz(hinner); @@ -57,6 +62,8 @@ export fn hmac(create: *hash::createfunc, key: []u8) state = { fn close(s: *io::stream) void = { let h: *state = s: *state; + bytes::zero(h.okeypad); + bytes::zero(h.ikeypad); free(h.okeypad); free(h.ikeypad); free(h.sumbuf); diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib @@ -164,7 +164,8 @@ bytes() { index.ha \ reverse.ha \ tokenize.ha \ - two_way.ha + two_way.ha \ + zero.ha gen_ssa bytes types } @@ -228,7 +229,7 @@ crypto_cipher() { cbc.ha \ ctr.ha \ stream.ha - gen_ssa crypto::cipher crypto::math + gen_ssa crypto::cipher crypto::math bytes } crypto_hmac() { @@ -236,7 +237,7 @@ crypto_hmac() { then gen_srcs crypto::hmac \ hmac.ha - gen_ssa crypto::hmac crypto::math hash io + gen_ssa crypto::hmac crypto::math hash io bytes else gen_srcs crypto::hmac \ hmac.ha \ diff --git a/stdlib.mk b/stdlib.mk @@ -628,7 +628,8 @@ stdlib_bytes_any_srcs= \ $(STDLIB)/bytes/index.ha \ $(STDLIB)/bytes/reverse.ha \ $(STDLIB)/bytes/tokenize.ha \ - $(STDLIB)/bytes/two_way.ha + $(STDLIB)/bytes/two_way.ha \ + $(STDLIB)/bytes/zero.ha $(HARECACHE)/bytes/bytes-any.ssa: $(stdlib_bytes_any_srcs) $(stdlib_rt) $(stdlib_types_$(PLATFORM)) @printf 'HAREC \t$@\n' @@ -683,7 +684,7 @@ stdlib_crypto_cipher_any_srcs= \ $(STDLIB)/crypto/cipher/ctr.ha \ $(STDLIB)/crypto/cipher/stream.ha -$(HARECACHE)/crypto/cipher/crypto_cipher-any.ssa: $(stdlib_crypto_cipher_any_srcs) $(stdlib_rt) $(stdlib_crypto_math_$(PLATFORM)) +$(HARECACHE)/crypto/cipher/crypto_cipher-any.ssa: $(stdlib_crypto_cipher_any_srcs) $(stdlib_rt) $(stdlib_crypto_math_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) @printf 'HAREC \t$@\n' @mkdir -p $(HARECACHE)/crypto/cipher @HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Ncrypto::cipher \ @@ -693,7 +694,7 @@ $(HARECACHE)/crypto/cipher/crypto_cipher-any.ssa: $(stdlib_crypto_cipher_any_src stdlib_crypto_hmac_any_srcs= \ $(STDLIB)/crypto/hmac/hmac.ha -$(HARECACHE)/crypto/hmac/crypto_hmac-any.ssa: $(stdlib_crypto_hmac_any_srcs) $(stdlib_rt) $(stdlib_crypto_math_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) +$(HARECACHE)/crypto/hmac/crypto_hmac-any.ssa: $(stdlib_crypto_hmac_any_srcs) $(stdlib_rt) $(stdlib_crypto_math_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) @printf 'HAREC \t$@\n' @mkdir -p $(HARECACHE)/crypto/hmac @HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Ncrypto::hmac \ @@ -2346,7 +2347,8 @@ testlib_bytes_any_srcs= \ $(STDLIB)/bytes/index.ha \ $(STDLIB)/bytes/reverse.ha \ $(STDLIB)/bytes/tokenize.ha \ - $(STDLIB)/bytes/two_way.ha + $(STDLIB)/bytes/two_way.ha \ + $(STDLIB)/bytes/zero.ha $(TESTCACHE)/bytes/bytes-any.ssa: $(testlib_bytes_any_srcs) $(testlib_rt) $(testlib_types_$(PLATFORM)) @printf 'HAREC \t$@\n' @@ -2407,7 +2409,7 @@ testlib_crypto_cipher_any_srcs= \ $(STDLIB)/crypto/cipher/ctr.ha \ $(STDLIB)/crypto/cipher/stream.ha -$(TESTCACHE)/crypto/cipher/crypto_cipher-any.ssa: $(testlib_crypto_cipher_any_srcs) $(testlib_rt) $(testlib_crypto_math_$(PLATFORM)) +$(TESTCACHE)/crypto/cipher/crypto_cipher-any.ssa: $(testlib_crypto_cipher_any_srcs) $(testlib_rt) $(testlib_crypto_math_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) @printf 'HAREC \t$@\n' @mkdir -p $(TESTCACHE)/crypto/cipher @HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::cipher \