This gist contains my notes about how the FIPS selftest signature check works in OpenSSL 1.x. Assumes basic awareness of the OpenSSL FOM (FIPS Object Module). My focus for this case is Solaris on SPARC.
The goal is to examine some of the inner workings of FIPS signature verification.
The FOM is built as fipscanister.o using designated code and linked into common OpenSSL build.
When the libcrypto.so library is loaded, the FOM will perform self test. If the self test fails,
the running program is abort()ed.
The overall build scheme is pretty well described on https://github.com/google/boringssl/blob/master/crypto/fipsmodule/FIPS.md , notably this part:
- OpenSSL depends on the link order and inserts two object files, fips_start.o and fips_end.o, in order to establish the module_start and module_end values. BoringCrypto adds labels at the correct places in the assembly for the static build, or uses a linker script for the shared build.
- OpenSSL calculates the hash after the final link and either injects it into the binary or recompiles with the value of the hash passed in as a #define.
- OpenSSL references read-write data directly, since it can know the offsets to it.
The fips_start.o and fips_end.o is created from fips/fips_canister.c, using #defines. Then it is linked into the fipscanister.o so that the list of files passed to the link editor (ld) starts with fips_start.o and fips_end.o. This way (at least in the commonly used link editors), the contents of these objects will be located at the start or at the end of the fipscanister.o, respectively. This is visible in the FOM Makefile - the list of objects in $objs starts with fips_start.o and ends with fips_end.o:
88 fipscanister.o: fips_start.o $(LIBOBJ) $(FIPS_OBJ_LISTS) fips_end.o
...
97 objs="fips_start.o $(LIBOBJ) $(FIPS_EX_OBJ) $$CPUID $$FIPS_ASM"; \
98 for i in $(FIPS_OBJ_LISTS); do \
99 dir=`dirname $$i`; script="s|^|$$dir/|;s| | $$dir/|g"; \
100 objs="$$objs `sed "$$script" $$i`"; \
101 done; \
102 objs="$$objs fips_end.o" ; \
103 os="`(uname -s) 2>/dev/null`"; cflags="$(CFLAGS)"; \
...
112 else case "$$os" in \
113 OSF1|SunOS) set -x; /usr/ccs/bin/ld -r -o $@ $$objs ;; \
114 *) set -x; $(CC) $$cflags -r -o $@ $$objs ;; \
115 esac fi
116 ./fips_standalone_sha1$(EXE_EXT) fipscanister.o > fipscanister.o.sha1
Now, fips_canister.c contains two arrays and two functions (in this order; The order is important.):
const unsigned int FIPS_rodata_start[]= { 0x46495053, 0x5f726f64, 0x6174615f, 0x73746172 };- defined for
fips_start.oonly
- defined for
const unsigned int FIPS_rodata_end[]= { 0x46495053, 0x5f726f64, 0x6174615f, 0x656e645b };- defined for
fips_end.oonly
- defined for
static void *instruction_pointer(void) { return NULL; }- in the case of Solaris/SPARC
const FIPS_ref_point()- this gets redefined using compilation defines asFIPS_text_startorFIPS_text_startto createfips_start.oandfips_end.o, respectively
194 /*
195 * This function returns pointer to an instruction in the vicinity of
196 * its entry point, but not outside this object module. This guarantees
197 * that sequestered code is covered...
198 */
199 const void *FIPS_ref_point() {
...
251 return (void *)instruction_pointer;
...
255 }For other architectures the source code contains some inline assembly. FIPS_text_start() will return the address of instruction_pointer() which is the address of its first instruction. The address returned from FIPS_text_start() is used during the self test as a base address for signature computation. Because instruction_pointer resides in the resulting object file before FIPS_text_start() (a result of the ordering in the source code file - assuming compiler will not reorder), the base address will cover the text of FIPS_text_start() itself. Similarly for FIPS_text_end() (except the cover).
To examine how this works in practice, write program that prints the addresses:
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
// from openssl/openssl-fips-140/fipscanister/build/i86/fips/fips.c
extern const void *FIPS_text_start(), *FIPS_text_end();
extern const unsigned char FIPS_rodata_start[], FIPS_rodata_end[];
int
main(void)
{
// OPENSSL_init();
printf("FIPS_text_start = %p\n", FIPS_text_start());
printf("FIPS_text_start = %p\n", FIPS_text_end());
// linker issues a warning for these symbols
// (relocation bound to a symbol with STV_PROTECTED visibility)
// so use dynamic linker to get the addresses.
void *p = dlsym(RTLD_NEXT, "FIPS_rodata_start");
if (p != NULL)
printf("FIPS_rodata_start = %p\n", p);
p = dlsym(RTLD_NEXT, "FIPS_rodata_end");
if (p != NULL)
printf("FIPS_rodata_end = %p\n", p);
pause();
}Compile:
gcc -m64 -o fips -I/usr/openssl/fips-140/include fips.c \
-Xlinker -R/lib/openssl/fips-140/64 \
-L/lib/openssl/fips-140/64 -lcrypto
Start the program:
./fips
FIPS_text_start = 7ff8318d3b20
FIPS_text_end = 7ff831947b30
FIPS_rodata_start = 7ff83189df40
FIPS_rodata_end = 7ff8318a64f0
^Z
attach debugger and display the address space layout:
$ bg
[1]+ ./fips &
$ mdb -p $!
Loading modules: [ ld.so.1 libc.so.1 ]
fips:14478*> ::mappings
BASE LIMIT RWX SIZE NAME
100000000 100002000 r-x 2000 [ text ] openssl-FIPS-experiment/fips
100100000 100102000 rwx 2000 [ data ] openssl-FIPS-experiment/fips
100102000 100110000 rwx e000 [ heap ]
7ff831800000 7ff831a50000 r-x 250000 [ text ] /lib/openssl/fips-140/sparcv9/libcrypto.so.1.0.0
7ff831b50000 7ff831b70000 rwx 20000 [ data ] /lib/openssl/fips-140/sparcv9/libcrypto.so.1.0.0
7ff831b70000 7ff831b7c000 rwx c000 [ data ] /lib/openssl/fips-140/sparcv9/libcrypto.so.1.0.0
7ff831c00000 7ff831c04000 rw- 4000 [ anon ]
7ffffef00000 7fffff120000 r-x 220000 [ text ] /lib/sparcv9/libc.so.1
7fffff120000 7fffff124000 r-x 4000 [ text ] /lib/sparcv9/libc.so.1
7fffff224000 7fffff238000 rwx 14000 [ data ] /lib/sparcv9/libc.so.1
7fffff238000 7fffff240000 rwx 8000 [ anon ]
ffffffff7f300000 ffffffff7f340000 r-x 40000 [ text ] /lib/sparcv9/ld.so.1
ffffffff7f340000 ffffffff7f344000 r-x 4000 [ text ] /lib/sparcv9/ld.so.1
ffffffff7f444000 ffffffff7f446000 r-- 2000 [ dtrace ] /lib/sparcv9/ld.so.1
ffffffff7f546000 ffffffff7f54c000 rwx 6000 [ data ] /lib/sparcv9/ld.so.1
ffffffff7f54c000 ffffffff7f54e000 rwx 2000 [ anon ]
ffffffff7f5c0000 ffffffff7f5c6000 rw- 6000 [ anon ]
ffffffff7f5d0000 ffffffff7f5e0000 rw- 10000 [ anon ]
ffffffff7f5ec000 ffffffff7f5ee000 rw- 2000 [ anon ]
ffffffff7f5f0000 ffffffff7f5f2000 r-- 2000 [ anon ]
ffffffff7f5f4000 ffffffff7f5f6000 r-- 2000 [ anon ]
ffffffff7f5f8000 ffffffff7f5fa000 r-- 2000 [ anon ]
ffffffff7f5fc000 ffffffff7f5fe000 r-x 2000 [ anon ]
ffffffff7fff0000 ffffffff80000000 rw- 10000 [ stack ]
Verify how FIPS_text_{start,end} works:
Looking at the object file (same thing will be in fipscanister.o:
$ dis -n -F FIPS_text_start fips_start.o
disassembly for fips_start.o
FIPS_text_start()
0x50: 93 41 40 00 rd %pc, %o1
0x54: 03 00 00 00 sethi %hi(0x0), %g1
0x58: 17 00 00 00 sethi %hi(0x0), %o3
0x5c: 82 00 60 00 add %g1, 0x0, %g1
0x60: 94 1a e0 00 xor %o3, 0x0, %o2
0x64: 92 00 40 09 add %g1, %o1, %o1
0x68: 81 c3 e0 08 retl
0x6c: d0 5a 40 0a ldx [%o1 + %o2], %o0
$ elfdump -r fips_start.o
Relocation Section: .rela.text
index type offset value addend section symbol
[0] R_SPARC_PC22 0x54 0 0x4 .text _GLOBAL_OFFSET_TABLE_
[1] R_SPARC_GOTDATA_OP_HIX22 0x58 0 0x20 .text .text (section)
[2] R_SPARC_PC10 0x5c 0 0xc .text _GLOBAL_OFFSET_TABLE_
[3] R_SPARC_GOTDATA_OP_LOX10 0x60 0 0x20 .text .text (section)
[4] R_SPARC_GOTDATA_OP 0x6c 0 0x20 .text .text (section)
...
Looking at the function after the runtime relocations were performed:
fips:14478*> FIPS_text_start::dis -a
0x7ff8318d3b50 rd %pc, %o1
0x7ff8318d3b54 sethi %hi(0x27c400), %g1
0x7ff8318d3b58 sethi %hi(0x27c400), %o3
0x7ff8318d3b5c add %g1, 0xb0, %g1
0x7ff8318d3b60 xor %o3, -0xe0, %o2
0x7ff8318d3b64 add %g1, %o1, %o1
0x7ff8318d3b68 retl
0x7ff8318d3b6c add %o1, %o2, %o0
in this very case it goes like this:
- saves 0x7ff8318d3b50 to o1
- sets g1 and o3 to 0x27c4000000000000 - https://arcb.csc.ncsu.edu/~mueller/codeopt/codeopt00/notes/sparc.html has useful explanation of how sethi works with %hi
- ADDs g1 and 0xb0 to g1
- XORs o3 with -0xe to o2
- ADDs g1 with o1 to o1
- ADDs o1 with o2 to the return value
Let's verify for FIPS_text_start by hand (could have set a breakpoint, single step and print the registers but this is more fun):
fips:14478*> 0x27c4000000000000
fips:14478*> 0x27c4000000000000+0xb0=K
27c40000000000b0
fips:14478*> 0x27c4000000000000^-0xe0=K
d83bffffffffff20
fips:14478*> 27c40000000000b0+0x7ff8318d3b50=K
27c47ff8318d3c00
fips:14478*> 27c47ff8318d3c00+d83bffffffffff20=K
7ff8318d3b20
and that's indeed the address of the first location of instruction_pointer:
fips:14478*> ::nm ! grep instruction_pointer
0x00007ff8318d3b20|0x0000000000000008|FUNC |LOCL |0x0 |17 |instruction_pointer
0x00007ff831947b30|0x0000000000000008|FUNC |LOCL |0x0 |17 |instruction_pointer
See the text area layout for these functions:
fips:14478*> FIPS_text_start-8::dis -a
0x7ff8318d3b20 retl
0x7ff8318d3b24 clr %o0
0x7ff8318d3b28 illtrap 0x10000
0x7ff8318d3b2c illtrap 0x10000
0x7ff8318d3b30 illtrap 0x10000
0x7ff8318d3b34 illtrap 0x10000
0x7ff8318d3b38 illtrap 0x10000
0x7ff8318d3b3c illtrap 0x10000
0x7ff8318d3b40 illtrap 0x10000
0x7ff8318d3b44 illtrap 0x10000
0x7ff8318d3b48 illtrap 0x10000
0x7ff8318d3b4c illtrap 0x10000
0x7ff8318d3b50 rd %pc, %o1
0x7ff8318d3b54 sethi %hi(0x27c400), %g1
0x7ff8318d3b58 sethi %hi(0x27c400), %o3
0x7ff8318d3b5c add %g1, 0xb0, %g1
0x7ff8318d3b60 xor %o3, -0xe0, %o2
0x7ff8318d3b64 add %g1, %o1, %o1
0x7ff8318d3b68 retl
0x7ff8318d3b6c add %o1, %o2, %o0
0x7ff8318d3b70 illtrap 0x10000
fips:14478*> instruction_pointer::dis
libcrypto.so.1.0.0`instruction_pointer: retl
libcrypto.so.1.0.0`instruction_pointer+4: clr %o0
Save the text range to a file:
fips:14478*> 7ff831947b30-7ff8318d3b20>text_size
fips:14478*> 7ff8318d3b20/$[<text_size]B ! tee > /tmp/text
fips:14478*> !less /tmp/text
Get the address of the signature array:
fips:14478*> FIPS_signature=K
7ff831b52018
and get its contents (HMAC-SHA1 is 160 bits, i.e. 20 bytes):
fips:14478*> FIPS_signature::dump -n 20 -g 1
0 1 2 3 4 5 6 7 \/ 9 a b c d e f 01234567v9abcdef
7ff831b52010: 00 00 00 01 00 00 00 00 02 b7 25 c0 64 42 81 03 ..........%.dB..
7ff831b52020: 67 89 5f c7 27 4f 2e 02 c3 49 f5 3c 00 00 00 00 g._.'O...I.<....
7ff831b52030: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 08 ................
Now the data part:
fips:14478*> FIPS_rodata_start=K
7ff83189df40
fips:14478*> FIPS_rodata_end=K
7ff8318a64f0
fips:14478*> FIPS_rodata_start::dump -n 16
\/ 1 2 3 4 5 6 7 8 9 a b c d e f v123456789abcdef
7ff83189df40: 46495053 5f726f64 6174615f 73746172 FIPS_rodata_star
7ff83189df50: 6574616f 6e726973 68646c63 7570666d etaonrishdlcupfm
fips:14478*> FIPS_rodata_end::dump -n 16
\/ 1 2 3 4 5 6 7 8 9 a b c d e f v123456789abcdef
7ff8318a64f0: 46495053 5f726f64 6174615f 656e645b FIPS_rodata_end[
7ff8318a6500: 30326237 32356330 36343432 38313033 02b725c064428103
This matches the comment in fips_canister.c:
66 /* Some compilers put string literals into a separate segment. As we
67 * are mostly interested to hash AES tables in .rodata, we declare
68 * reference points accordingly. In case you wonder, the values are
69 * big-endian encoded variable names, just to prevent these arrays
70 * from being merged by linker. */Save the data part to a file:
FIPS_rodata_end-FIPS_rodata_start>rodata_size
FIPS_rodata_start/$[<rodata_size]B ! tee > /tmp/rodata
fips/fips.c#FIPS_incore_fingerprint() that computes the signature checks for text/rodata overlaps and also punches a hole for the FIPS_signature array in case it overlaps (also note the comma operators to avoid curly brackets):
155 unsigned char FIPS_signature [20] = { 0, 0xff };
156 __fips_constseg
157 static const char FIPS_hmac_key[]="etaonrishdlcupfm";
158
159 unsigned int FIPS_incore_fingerprint(unsigned char *sig,unsigned int len)
160 {
161 const unsigned char *p1 = FIPS_text_start();
162 const unsigned char *p2 = FIPS_text_end();
163 const unsigned char *p3 = FIPS_rodata_start;
164 const unsigned char *p4 = FIPS_rodata_end;
165 HMAC_CTX c;
166
167 HMAC_CTX_init(&c);
168 HMAC_Init(&c,FIPS_hmac_key,strlen(FIPS_hmac_key),EVP_sha1());
169
170 /* detect overlapping regions */
171 if (p1<=p3 && p2>=p3)
172 p3=p1, p4=p2>p4?p2:p4, p1=NULL, p2=NULL;
173 else if (p3<=p1 && p4>=p1)
174 p3=p3, p4=p2>p4?p2:p4, p1=NULL, p2=NULL;
175
176 if (p1)
177 HMAC_Update(&c,p1,(size_t)p2-(size_t)p1);
178
179 if (FIPS_signature>=p3 && FIPS_signature<p4)
180 {
181 /* "punch" hole */
182 HMAC_Update(&c,p3,(size_t)FIPS_signature-(size_t)p3);
183 p3 = FIPS_signature+sizeof(FIPS_signature);
184 if (p3<p4)
185 HMAC_Update(&c,p3,(size_t)p4-(size_t)p3);
186 }
187 else
188 HMAC_Update(&c,p3,(size_t)p4-(size_t)p3);
189
190 if (!fips_post_corrupt(FIPS_TEST_INTEGRITY, 0, NULL))
191 HMAC_Update(&c, (unsigned char *)FIPS_hmac_key, 1);
192
193 HMAC_Final(&c,sig,&len);
194 HMAC_CTX_cleanup(&c);
195
196 return len;
197 }In this case there is no overlap so the function can be simplified and changed to accept external pointers:
unsigned int FIPS_incore_fingerprint(unsigned char *sig,unsigned int len)
{
const unsigned char *p1 = FIPS_text_start;
const unsigned char *p2 = FIPS_text_end;
const unsigned char *p3 = FIPS_rodata_start;
const unsigned char *p4 = FIPS_rodata_end;
HMAC_CTX c;
HMAC_CTX_init(&c);
HMAC_Init(&c,FIPS_hmac_key,strlen(FIPS_hmac_key),EVP_sha1());
HMAC_Update(&c,p1,(size_t)p2-(size_t)p1);
HMAC_Update(&c,p3,(size_t)p4-(size_t)p3);
HMAC_Final(&c,sig,&len);
HMAC_CTX_cleanup(&c);
return len;
}Running a program that mmap()s the saved rodata/text regions into memory and passes them to the modified function results in the identical signature as embedded in the binary.
Note: In the FOM source code there is util/incore Perl program that contains funky SHA-1 implementation and a twin FIPS_incore_fingerprint() that reads the contents from a ELF file and is used to embed the signature into ELF file.
Here's the Python program to convert the data extracted from mdb to binary file:
#!/usr/bin/env python3
def get_mdb_bytes(fp):
"""
Assumes text file with contents from something like this:
mdb> 7ff831947b30-7ff8318d3b20>text_size
mdb> 7ff8318d3b20/$[<text_size]B ! tee > /tmp/text
The bytes returned from the function can be then written to a file
(opened with "wb")
"""
lines = fp.readlines()
_, line = lines[1].split(":")
hexbytes = line.split()
data_list = list(map(lambda x : int(x, 16), hexbytes))
data_bytes = bytes(data_list)
return data_bytes
with open("/tmp/text") as file_in:
out = get_mdb_bytes(file_in)
with open("/tmp/text.bytes", "wb+") as file_out:
file_out.write(out)
with open("/tmp/rodata") as file_in:
out = get_mdb_bytes(file_in)
with open("/tmp/rodata.bytes", "wb+") as file_out:
file_out.write(out)and here's the C program that assumes the modified FIPS_incore_fingerprint() as shown above:
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <err.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <openssl/crypto.h>
#include <openssl/rand.h>
#include <openssl/err.h>
#include <openssl/bio.h>
#include <openssl/hmac.h>
// from fips/fips.c
unsigned char FIPS_signature [20] = { 0, 0xff };
static const char FIPS_hmac_key[]="etaonrishdlcupfm";
void *FIPS_text_start;
void *FIPS_text_end;
void *FIPS_rodata_start;
void *FIPS_rodata_end;
int
main(int argc, char *argv[])
{
if (argc != 3)
errx(1, "usage: <text_file> <rodata_file>");
OPENSSL_init();
int text_fd;
if ((text_fd = open(argv[1], O_RDONLY)) == -1)
err(1, "open %s", argv[1]);
off_t text_size = lseek(text_fd, 0, SEEK_END);
printf("%d\n", text_size);
FIPS_text_start = mmap(0, text_size, PROT_READ, MAP_SHARED, text_fd, 0);
FIPS_text_end = FIPS_text_start + text_size;
int rodata_fd;
if ((rodata_fd = open(argv[2], O_RDONLY)) == -1)
err(1, "open %s", argv[2]);
off_t rodata_size = lseek(rodata_fd, 0, SEEK_END);
printf("%d\n", rodata_size);
FIPS_rodata_start = mmap(0, rodata_size, PROT_READ, MAP_SHARED, rodata_fd, 0);
FIPS_rodata_end = FIPS_rodata_start + rodata_size;
FIPS_incore_fingerprint(FIPS_signature, sizeof (FIPS_signature));
for (int i = 0; i < sizeof FIPS_signature; i++)
printf("%02hhx ", FIPS_signature[i]);
printf("\n");
}