Skip to content

Instantly share code, notes, and snippets.

@vladak
Last active October 19, 2021 12:25
Show Gist options
  • Select an option

  • Save vladak/5332130bd6bf6650d809c6165e811d6d to your computer and use it in GitHub Desktop.

Select an option

Save vladak/5332130bd6bf6650d809c6165e811d6d to your computer and use it in GitHub Desktop.
OpenSSL 1.x FIPS signature

OpenSSL 1.x FIPS embedded signature test

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:

  1. 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.
  2. 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.
  3. 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.o only
  • const unsigned int FIPS_rodata_end[]= { 0x46495053, 0x5f726f64, 0x6174615f, 0x656e645b };
    • defined for fips_end.o only
  • static void *instruction_pointer(void) { return NULL; }
    • in the case of Solaris/SPARC
  • const FIPS_ref_point() - this gets redefined using compilation defines as FIPS_text_start or FIPS_text_start to create fips_start.o and fips_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 ]

Text

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:

  1. saves 0x7ff8318d3b50 to o1
  2. 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
  3. ADDs g1 and 0xb0 to g1
  4. XORs o3 with -0xe to o2
  5. ADDs g1 with o1 to o1
  6. 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

Signature

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  ................

Data

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

Verifying the signature

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");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment