# SHA1 computation with Modules This document demonstrates the use of Modules in Concrete through a SHA1 computation example. SHA1 is a deprecated and broken hash function; we use it here for pedagogical purposes, not for its security. The SHA1 code is available [here](sha1.py). Execution times for the different functions are provided in the final section. We created our example by forking [python-sha1](https://github.com/ajalt/python-sha1) by [AJ Alt](https://github.com/ajalt) and made extensive modifications to implement SHA1 in FHE and corresponding tests. ## SHA1 overview SHA1 is a deprecated broken hash function defined by NIST in 1995: You can find detailed information on SHA1 on its [Wikipedia page](https://en.wikipedia.org/wiki/SHA-1) or in its [official description](https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.180-4.pdf). We follow the structure of its [pseudo-code](https://en.wikipedia.org/wiki/SHA-1#SHA-1_pseudocode) in our implementation. ## Our FHE implementation In our implementation, only the compression function is implemented in FHE, corresponding to the `_process_encrypted_chunk_server_side function`. The rest is done client-side in the clear, including the message expansion. While more of the process could be done in FHE, this tutorial focuses on demonstrating the use of [Modules](https://docs.zama.ai/concrete/compilation/modules). Our Module contains 7 functions which can be combined together: - `xor3` XORs three values together - `iftern` computes the ternary IF, i.e., c ? t : f - `maj` computes the majority function - `rotate30` rotates a 32-bit word by 30 positions - `rotate5` rotates a 32-bit word by 5 positions - `add2` adds two values together - `add5` adds five values together ``` @fhe.module() class MyModule: @fhe.function({"x": "encrypted", "y": "encrypted", "z": "encrypted"}) def xor3(x, y, z): return x ^ y ^ z @fhe.function({"x": "encrypted", "y": "encrypted", "z": "encrypted"}) def iftern(x, y, z): return z ^ (x & (y ^ z)) @fhe.function({"x": "encrypted", "y": "encrypted", "z": "encrypted"}) def maj(x, y, z): return (x & y) | (z & (x | y)) @fhe.function({"x": "encrypted"}) def rotate30(x): ans = fhe.zeros((32,)) ans[30:32] = x[0:2] ans[0:30] = x[2:32] return ans @fhe.function({"x": "encrypted"}) def rotate5(x): ans = fhe.zeros((32,)) ans[5:32] = x[0:27] ans[0:5] = x[27:32] return ans @fhe.function({"x": "encrypted", "y": "encrypted"}) def add2(x, y): return fhe.bits(add_chunked_number(x, y))[0] @fhe.function( {"x": "encrypted", "y": "encrypted", "u": "encrypted", "v": "encrypted", "w": "encrypted"} ) def add5(x, y, u, v, w): result = add_chunked_number(x, y) result = add_chunked_number(result, u) result = add_chunked_number(result, v) result = add_chunked_number(result, w) return fhe.bits(result)[0] ``` We then compile this Module, setting `p_error=10**-8` as a very small value to avoid computation errors. The Module feature allows the combination of all these functions so that the outputs of some to be used as inputs for others. This makes it convenient to create larger functions with some control flow (conditions, branches, loops) handled in the clear while using these smaller functions. In our case, this is done in the `_process_encrypted_chunk_server_side function.` ## Details of `_process_encrypted_chunk_server_side` `_process_encrypted_chunk_server_side` uses encrypted inputs and returns encrypted values. In the clear, all variables are 32-bit words, but here they are represented as 32 encrypted bits to simplify and accelerate the non-linear operations in SHA1. Then, we have the main loop of the compression function: ``` for i in range(80): if 0 <= i <= 19: # Do f = d ^ (b & (c ^ d)) fsplit_enc = my_module.iftern.run(bsplit_enc, csplit_enc, dsplit_enc) ksplit = split(0x5A827999) elif 20 <= i <= 39: # Do f = b ^ c ^ d fsplit_enc = my_module.xor3.run(bsplit_enc, csplit_enc, dsplit_enc) ksplit = split(0x6ED9EBA1) elif 40 <= i <= 59: # Do f = (b & c) | (b & d) | (c & d) fsplit_enc = my_module.maj.run(bsplit_enc, csplit_enc, dsplit_enc) ksplit = split(0x8F1BBCDC) elif 60 <= i <= 79: # Do f = b ^ c ^ d fsplit_enc = my_module.xor3.run(bsplit_enc, csplit_enc, dsplit_enc) ksplit = split(0xCA62C1D6) ``` In this main loop, we take the right choice of `f` and `k`. Here, we can see a first use of the different functions in the Module. Then we continue with other functions in the Module, to compute `arot5 = _left_rotate(a, 5)` and `arot5 + f + e + k + w[i]`: ``` # Do arot5 = _left_rotate(a, 5) arot5split_enc = my_module.rotate5.run(asplit_enc) # Do arot5 + f + e + k + w[i] ssplit_enc = my_module.add5.run( arot5split_enc, fsplit_enc, esplit_enc, wsplit_enc[i], my_module.rotate5.encrypt(ksplit), # BCM: later remove the encryption on k ) ``` Finally, we update the different `a, b, c, d, e` values as in the clear implementation but with the encrypted forms: ``` # Final update of the a, b, c, d and e registers newasplit_enc = ssplit_enc esplit_enc = dsplit_enc dsplit_enc = csplit_enc # Do c = _left_rotate(b, 30) csplit_enc = my_module.rotate30.run(bsplit_enc) bsplit_enc = asplit_enc asplit_enc = newasplit_enc ``` You can see that we compiled the Module's different functions on inputset made with bits. Under the hood, Concrete adds a few programmable bootstrappings to compute the correct functions in FHE. ## MLIR code Compiling with `show_mlir = True` allows to see the different MLIR implementations. ## Testing or using This tutorial focuses on the use of Modules rather than a production-ready implementation. For a full client-server API, you might want to perform more operations in FHE, including message expansion, and function optimizations. You can verify the implementation in FHE by running `python sha1.py --autotest`: it will pick a certain number of random inputs, hash them in FHE and compare the result with the `hashlib` standard implementation. You can also hash a given value with `echo -n "The quick brown fox jumps over the lazy dog" | python sha1.py`, and it will print something like: ``` sha1-digest: 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12 computed in: 320.265383 seconds ``` ## Benchmarks We have executed our implementation on an HPC7a machine with Concrete 2.7. `python sha1.py --autotest` typically returns: ``` Checking SHA1(yozulCBAPuFosqTBMwPTVmvQvmfhGFJjdtSSiemdytn) for an input length 43 sha1-digest: aa1871c2d560221e14f18b43d559aafc4920d9bc computed in: 93.844131 seconds Checking SHA1(HwXFZxXUGckiuWysDtrpIijiRwRGPJZPGaNpJMlfbPptfNhzKOXZMiZnoLlaRCXqK) for an input length 65 sha1-digest: 8555e05fc2396a3b291e983901bdfa02cb454a72 computed in: 181.812173 seconds Checking SHA1(am) for an input length 2 sha1-digest: 96e8155732e8324ae26f64d4516eb6fe696ac84f computed in: 91.206739 seconds Checking SHA1(OTzaWtYfzqKyTHIgBSlmI) for an input length 21 sha1-digest: c76426dbecb3afd015b132e0e44a1b4d0fc664cb computed in: 91.182635 seconds Checking SHA1(MBwAxKkvLOUzXkHILdVchwjfcUTlofyQdSqaonqcXvRVVwEJpmaGKOsNDCUGkt) for an input length 62 sha1-digest: 2231cecd803a9000c117a2f2d3ea35f70ecf6fce computed in: 179.300580 seconds Checking SHA1(iqObpuNHZXKztrUZYrpmAPjFflNLacYyUTBLZdbjbPcjbLOseIKqZNYCbsoaDuwvgbvfWE) for an input length 70 sha1-digest: 87cdf7a7f984ef5843d0cfc95a69eaef3e82e31b computed in: 182.332336 seconds Checking SHA1(nPIZWXYUXOerncJAeBQrcPhuHsXbYcQkKMRoAGzxFZjBXWvproNcHlHIFNNGzQChEVjZvsEmSOpQuoihPgudlqizwAfXzgU) for an input length 95 sha1-digest: 601db1e338e8b817f5309c7c3bf89450d70425a1 computed in: 182.233933 seconds Checking SHA1(SfUDDjhLqcmifCpLlqnUZKFjwtPjfCrpdRChUWypdrhdDTjizF) for an input length 50 sha1-digest: 8c57e70e92af96c6df5aecb4f3740e68b0686883 computed in: 91.378782 seconds Checking SHA1(VePVJXHljlLpkThrANZaEkkbYoFZVdSFFsvVkQPDlrwyisOIZqAUhGwHYYhxnFjOrUgFV) for an input length 69 sha1-digest: b54a98dc0f74b50669a5e0c81199a2819e26d3e9 computed in: 182.419465 seconds Checking SHA1(hCOuPvLrBXYhaWLeSnyRxpJZmXyEeTCBdfkgvDwvFIgoVaXNPzHoHkitSvZLosTMEcDpuoLGqeCkuwLmQQLbSibizfPwxp) for an input length 94 sha1-digest: 19432e37fe0bbc7c5a5c9379ed38c4298d796fe6 computed in: 182.285430 seconds Checking SHA1(IRZuLcbBXJAfgYMuFwGWUXiKgfoWwSAOTOoQiLaZHKwuTbGGsTChMLEdlaRXkIOYUFJRxRyAVHhQbFKyKOFDNairicMm) for an input length 92 sha1-digest: 4307a80365937a3ff6703763a8f1a759362c35e5 computed in: 182.162872 seconds Checking SHA1(hsTWqDAwjdLiAhmLsnBJJozejydFLcksQmSQVcojeAhdZizXMzjKSAGjQUjRPGZlwGaKnE) for an input length 70 sha1-digest: 3889e4448d11e9d801608ab3a94378ee8d41aaa0 computed in: 182.288777 seconds Checking SHA1(GihWwpolRavMdWHiNGVlOVsziPqZcYkPEyTmgfbBtwMowfKMipRxADcqqzMBlpCrWEOJpnZQcqAwt) for an input length 77 sha1-digest: 4b048588c8ec5d1602487124b5232889668f6b6a computed in: 182.184901 seconds Checking SHA1(GiSbjYHQyEozGliG) for an input length 16 sha1-digest: dc7a31c2e17ae503c6c808a02c05f8d1ad4b346f computed in: 91.260812 seconds Checking SHA1(boVGLdzvbKOTYUSErbeSyoiJQMox) for an input length 28 sha1-digest: 50a40fb6d0362087ed9b6dceda5378c14b96a743 computed in: 91.119592 seconds Checking SHA1(kiaAjFJrzBaRFvSgIzIQdlJZokXGPBpjNRPqEcVDyCFGkxNzeiRNHuSmD) for an input length 57 sha1-digest: 44e423eaa7616d2a93b235d99b61f76a6f58e236 computed in: 179.129039 seconds Checking SHA1(IylnzdjqldIJGkolzRlJhFz) for an input length 23 sha1-digest: 463137090865b283801d49d1546ce0dea45f2529 computed in: 91.211171 seconds Checking SHA1(QEyszfMlKJjKIf) for an input length 14 sha1-digest: 937bfdff3bb97e9037a23bca4055302f97a90eb3 computed in: 91.124824 seconds Checking SHA1(IOTanBIVaq) for an input length 10 sha1-digest: b8dca6ad5df447549f5a95d4c195b889f5be6069 computed in: 91.110348 seconds Checking SHA1(FiWWroKJVGeMPfmaNKmXdalAyIRLpYJdFgCfBIEMXPDfWR) for an input length 46 sha1-digest: 84775d2c31df2ab191a0417cf26028f989d2cd9c computed in: 91.031444 seconds Checking SHA1() sha1-digest: da39a3ee5e6b4b0d3255bfef95601890afd80709 computed in: 91.044046 seconds Checking SHA1(The quick brown fox jumps over the lazy dog) sha1-digest: 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12 computed in: 90.971023 seconds ``` These results mean that: - one block of compression takes about 92 seconds - two blocks of compression take about 181 seconds