Add FIPS shared mode.

This change adds a FIPS integrity check using shared libraries. Unlike
with the static case, a shared build can take advantage of the linker
resolving relocations and thus doesn't need delocation. It does mean
that both .text and .rodata sections need to be handled, however, so the
hashing format is slightly different. inject-hash.go is updated to be
able to rewrite shared libraries to inject the correct hash value.

Change-Id: I9a71910cd6df3a85e4efac896b0913e65b5f875b
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/36024
Commit-Queue: Adam Langley <agl@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4cfbcc5..67c1145 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -369,9 +369,13 @@
   if(FIPS_BREAK_TEST)
     add_definitions("-DBORINGSSL_FIPS_BREAK_${FIPS_BREAK_TEST}=1")
   endif()
-  # Delocate does not work for ASan and MSan builds.
+  # The FIPS integrity check does not work for ASan and MSan builds.
   if(NOT ASAN AND NOT MSAN)
-    set(FIPS_DELOCATE "1")
+    if(BUILD_SHARED_LIBS)
+      set(FIPS_SHARED "1")
+    else()
+      set(FIPS_DELOCATE "1")
+    endif()
   endif()
 endif()
 
diff --git a/crypto/CMakeLists.txt b/crypto/CMakeLists.txt
index 1c505bc..017a06a 100644
--- a/crypto/CMakeLists.txt
+++ b/crypto/CMakeLists.txt
@@ -87,7 +87,7 @@
 add_subdirectory(fipsmodule)
 add_subdirectory(test)
 
-if(FIPS_DELOCATE)
+if(FIPS_DELOCATE OR FIPS_SHARED)
   SET_SOURCE_FILES_PROPERTIES(fipsmodule/bcm.o PROPERTIES EXTERNAL_OBJECT true)
   SET_SOURCE_FILES_PROPERTIES(fipsmodule/bcm.o PROPERTIES GENERATED true)
 
@@ -420,9 +420,25 @@
   ${CRYPTO_FIPS_OBJECTS}
 )
 
+if(FIPS_SHARED)
+  # Rewrite libcrypto.so to inject the correct module hash value. This assumes
+  # UNIX-style library naming, but we only support FIPS mode on Linux anyway.
+  add_custom_command(
+    TARGET crypto POST_BUILD
+    COMMAND ${GO_EXECUTABLE} run
+    ${CMAKE_CURRENT_SOURCE_DIR}/../util/fipstools/inject_hash/inject_hash.go
+    -o libcrypto.so -in-object libcrypto.so
+    # The DEPENDS argument to a POST_BUILD rule appears to be ignored. Thus
+    # go_executable isn't used (as it doesn't get built), but we list this
+    # dependency anyway in case it starts working in some CMake version.
+    DEPENDS ../util/fipstools/inject_hash/inject_hash.go
+    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+  )
+endif()
+
 add_dependencies(crypto global_target)
 
-if(FIPS_DELOCATE)
+if(FIPS_DELOCATE OR FIPS_SHARED)
   add_dependencies(crypto bcm_o_target)
 endif()
 
diff --git a/crypto/fipsmodule/CMakeLists.txt b/crypto/fipsmodule/CMakeLists.txt
index d1e2cb9..2fd55fd 100644
--- a/crypto/fipsmodule/CMakeLists.txt
+++ b/crypto/fipsmodule/CMakeLists.txt
@@ -129,6 +129,10 @@
 perlasm(x86-mont.${ASM_EXT} bn/asm/x86-mont.pl)
 
 if(FIPS_DELOCATE)
+  if(FIPS_SHARED)
+    error("Can't set both delocate and shared mode for FIPS build")
+  endif()
+
   if(OPENSSL_NO_ASM)
     # If OPENSSL_NO_ASM was defined then ASM will not have been enabled, but in
     # FIPS mode we have to have it because the module build requires going via
@@ -195,6 +199,42 @@
   add_dependencies(fipsmodule global_target)
 
   set_target_properties(fipsmodule PROPERTIES LINKER_LANGUAGE C)
+elseif(FIPS_SHARED)
+  if(NOT BUILD_SHARED_LIBS)
+    error("FIPS_SHARED set but not BUILD_SHARED_LIBS")
+  endif()
+
+  add_library(
+    fipsmodule
+
+    OBJECT
+
+    is_fips.c
+    fips_shared_support.c
+  )
+
+  add_dependencies(fipsmodule global_target)
+
+  add_library(
+    bcm_library
+
+    STATIC
+
+    bcm.c
+
+    ${BCM_ASM_SOURCES}
+  )
+
+  add_dependencies(bcm_library global_target)
+
+  add_custom_command(
+    OUTPUT bcm.o
+    COMMAND ld -r -T ${CMAKE_CURRENT_SOURCE_DIR}/fips_shared.lds -o bcm.o --whole-archive $<TARGET_FILE:bcm_library>
+    DEPENDS bcm_library fips_shared.lds
+    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+  )
+
+  add_custom_target(bcm_o_target DEPENDS bcm.o)
 else()
   add_library(
     fipsmodule
diff --git a/crypto/fipsmodule/FIPS.md b/crypto/fipsmodule/FIPS.md
index a60e2bf..8e6fa67 100644
--- a/crypto/fipsmodule/FIPS.md
+++ b/crypto/fipsmodule/FIPS.md
@@ -62,9 +62,25 @@
 
 BoringCrypto is linked (often statically) into a large number of binaries. It would be a significant cost if each of these binaries had to be post-processed in order to calculate the known-good HMAC value. We would much prefer if the value could be calculated, once, when BoringCrypto itself is compiled.
 
-In order for the value to be calculated before the final link, there can be no relocations in the hashed code and data. This document describes how we build C and assembly code in order to produce an object file containing all the code and data for the FIPS module without that code having any relocations.
+In order for the value to be calculated before the final link, there can be no relocations in the hashed code and data. This document describes how we build C and assembly code in order to produce a binary file containing all the code and data for the FIPS module without that code having any relocations.
 
-First, all the C source files for the module are compiled as a single unit by compiling a single source file that `#include`s them all (this is `bcm.c`). The `-fPIC` flag is used to cause the compiler to use IP-relative addressing in many (but not all) cases. Also the `-S` flag is used to instruct the compiler to produce a textual assembly file rather than a binary object file.
+There are two build configurations supported: static and shared. The shared build produces `libcrypto.so`, which includes the FIPS module and is significantly more straightforward and so is described first:
+
+### Shared build
+
+First, all the C source files for the module are compiled as a single unit by compiling a single source file that `#include`s them all (this is `bcm.c`). This, along with some assembly sources, comprise the FIPS module.
+
+The object files resulting from compiling (or assembling) those files is linked in partial-linking mode with a linker script that causes the linker to insert symbols marking the beginning and end of the text and rodata sections. The linker script also discards other types of data sections to ensure that no unhashed data is used by the module.
+
+One source of such data are `rel.ro` sections, which contain data that includes function pointers. Since these function pointers are absolute, they are written by the dynamic linker at run-time and so we must eliminate them. The pattern that causes them is when we have a static `EVP_MD` or `EVP_CIPHER` object thus, inside the module, this pattern is changed to instead reserve space in the BSS for the object, and to add a `CRYPTO_once_t` to protect its initialisation.
+
+Once the partially-linked result is linked again, with other parts of libcrypto, to produce `libcrypto.so`, the contents of the module are fixed, as required. The module code uses the linker-added symbols to find the its code and data at run-time and hashes them upon initialisation. The result is compared against a value stored inside `libcrypto.so`, but outside of the module. That value will, initially, be incorrect, but `inject-hash.go` can inject the correct value.
+
+### Static build
+
+The static build cannot depend on the shared-object link to resolve relocations and thus must take another path.
+
+As with the shared build, all the C sources are build in a single compilation unit. The `-fPIC` flag is used to cause the compiler to use IP-relative addressing in many (but not all) cases. Also the `-S` flag is used to instruct the compiler to produce a textual assembly file rather than a binary object file.
 
 The textual assembly file is then processed by a script to merge in assembly implementations of some primitives and to eliminate the remaining sources of relocations.
 
@@ -80,9 +96,9 @@
 
 ##### Read-only data
 
-Normally read-only data is placed in a `.data` segment that doesn't get mapped into memory with execute permissions. However, the offset of the data segment from the text segment is another thing that isn't determined until the final link. In order to fix data offsets before the link, read-only data is simply placed in the module's `.text` segment. This might make building ROP chains easier for an attacker, but so it goes.
+Normally read-only data is placed in an `.rodata` segment that doesn't get mapped into memory with execute permissions. However, the offset of the data segment from the text segment is another thing that isn't determined until the final link. In order to fix data offsets before the link, read-only data is simply placed in the module's `.text` segment. This might make building ROP chains easier for an attacker, but so it goes.
 
-One special case is `rel.ro` data, which is data that contains function pointers. Since these function pointers are absolute, they are written by the dynamic linker at run-time and so we must eliminate them. The pattern that causes them is when we have a static `EVP_MD` or `EVP_CIPHER` object thus, inside the module, we'll change this pattern to instead to reserve space in the BSS for the object, and add a `CRYPTO_once_t` to protect its initialisation. The script will generate functions outside of the module that return pointers to these areas of memory—they effectively act like a special-purpose malloc calls that cannot fail.
+Data containing function pointers remains an issue. The source-code changes described above for the shared build apply here too, but no direct references to a BSS section are possible because the offset to that section is not known at compile time. Instead, the script generates functions outside of the module that return pointers to these areas of memory—they effectively act like special-purpose malloc calls that cannot fail.
 
 ##### Read-write data
 
@@ -92,7 +108,7 @@
 
 ##### Other transforms
 
-The script performs a number of other transformations which are worth noting but do not warrant their own sections:
+The script performs a number of other transformations which are worth noting but do not warrant their own discussions:
 
 1.  It duplicates each global symbol with a local symbol that has `_local_target` appended to the name. References to the global symbols are rewritten to use these duplicates instead. Otherwise, although the generated code uses IP-relative references, relocations are emitted for global symbols in case they are overridden by a different object file during the link.
 1.  Various sections, notably `.rodata`, are moved to the `.text` section, inside the module, so module code may reference it without relocations.
@@ -112,15 +128,15 @@
 
 (This is based on reading OpenSSL's [user guide](https://www.openssl.org/docs/fips/UserGuide-2.0.pdf) and inspecting the code of OpenSSL FIPS 2.0.12.)
 
-OpenSSL's solution to this problem is broadly similar but has a number of differences:
+OpenSSL's solution to this problem is very similar to our shared build, with just a few differences:
 
 1.  OpenSSL deals with run-time relocations by not hashing parts of the module's data.
-1.  OpenSSL uses `ld -r` (the partial linking mode) to merge a number of object files into their `fipscanister.o`. For BoringCrypto, we merge all the C source files by building a single C file that #includes all the others, and we merge the assembly sources by appending them to the assembly output from the C compiler.
-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.
+1.  OpenSSL uses `ld -r` (the partial linking mode) to merge a number of object files into their `fipscanister.o`. For BoringCrypto's static build, we merge all the C source files by building a single C file that #includes all the others, and we merge the assembly sources by appending them to the assembly output from the C compiler.
+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.
 1.  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. BoringCrypto calculates it prior to the final link and injects it into the object file.
 1.  OpenSSL references read-write data directly, since it can know the offsets to it. BoringCrypto indirects these loads and stores.
 1.  OpenSSL doesn't run the power-on test until `FIPS_module_mode_set` is called. BoringCrypto does it in a constructor function. Failure of the test is non-fatal in OpenSSL, BoringCrypto will crash.
-1.  Since the contents of OpenSSL's module change between compilation and use, OpenSSL generates `fipscanister.o.sha1` to check that the compiled object doesn't change before linking. Since BoringCrypto's module is fixed after compilation, the final integrity check is unbroken through the linking process.
+1.  Since the contents of OpenSSL's module change between compilation and use, OpenSSL generates `fipscanister.o.sha1` to check that the compiled object doesn't change before linking. Since BoringCrypto's module is fixed after compilation (in the static case), the final integrity check is unbroken through the linking process.
 
 Some of the similarities are worth noting:
 
diff --git a/crypto/fipsmodule/bcm.c b/crypto/fipsmodule/bcm.c
index e15ecb8..64a320d 100644
--- a/crypto/fipsmodule/bcm.c
+++ b/crypto/fipsmodule/bcm.c
@@ -100,11 +100,16 @@
 #if defined(BORINGSSL_FIPS)
 
 #if !defined(OPENSSL_ASAN)
-// These symbols are filled in by delocate.go. They point to the start and end
-// of the module, and the location of the integrity hash, respectively.
+// These symbols are filled in by delocate.go (in static builds) or a linker
+// script (in shared builds). They point to the start and end of the module, and
+// the location of the integrity hash, respectively.
 extern const uint8_t BORINGSSL_bcm_text_start[];
 extern const uint8_t BORINGSSL_bcm_text_end[];
 extern const uint8_t BORINGSSL_bcm_text_hash[];
+#if defined(BORINGSSL_SHARED_LIBRARY)
+extern const uint8_t BORINGSSL_bcm_rodata_start[];
+extern const uint8_t BORINGSSL_bcm_rodata_end[];
+#endif
 #endif
 
 static void __attribute__((constructor))
@@ -116,13 +121,34 @@
   // .text section, which triggers the global-buffer overflow detection.
   const uint8_t *const start = BORINGSSL_bcm_text_start;
   const uint8_t *const end = BORINGSSL_bcm_text_end;
+#if defined(BORINGSSL_SHARED_LIBRARY)
+  const uint8_t *const rodata_start = BORINGSSL_bcm_rodata_start;
+  const uint8_t *const rodata_end = BORINGSSL_bcm_rodata_end;
+#endif
 
   static const uint8_t kHMACKey[64] = {0};
   uint8_t result[SHA512_DIGEST_LENGTH];
 
   unsigned result_len;
-  if (!HMAC(EVP_sha512(), kHMACKey, sizeof(kHMACKey), start, end - start,
-            result, &result_len) ||
+  HMAC_CTX hmac_ctx;
+  HMAC_CTX_init(&hmac_ctx);
+  if (!HMAC_Init_ex(&hmac_ctx, kHMACKey, sizeof(kHMACKey), EVP_sha512(),
+                    NULL /* no ENGINE */)) {
+    fprintf(stderr, "HMAC_Init_ex failed.\n");
+    goto err;
+  }
+#if defined(BORINGSSL_SHARED_LIBRARY)
+  uint64_t length = end - start;
+  HMAC_Update(&hmac_ctx, (const uint8_t *) &length, sizeof(length));
+  HMAC_Update(&hmac_ctx, start, length);
+
+  length = rodata_end - rodata_start;
+  HMAC_Update(&hmac_ctx, (const uint8_t *) &length, sizeof(length));
+  HMAC_Update(&hmac_ctx, rodata_start, length);
+#else
+  HMAC_Update(&hmac_ctx, start, end - start);
+#endif
+  if (!HMAC_Final(&hmac_ctx, result, &result_len) ||
       result_len != sizeof(result)) {
     fprintf(stderr, "HMAC failed.\n");
     goto err;
diff --git a/crypto/fipsmodule/delocate.h b/crypto/fipsmodule/delocate.h
index 59effde..d6564e4 100644
--- a/crypto/fipsmodule/delocate.h
+++ b/crypto/fipsmodule/delocate.h
@@ -20,7 +20,8 @@
 #include "../internal.h"
 
 
-#if defined(BORINGSSL_FIPS) && !defined(OPENSSL_ASAN) && !defined(OPENSSL_MSAN)
+#if !defined(BORINGSSL_SHARED_LIBRARY) && defined(BORINGSSL_FIPS) && \
+    !defined(OPENSSL_ASAN) && !defined(OPENSSL_MSAN)
 #define DEFINE_BSS_GET(type, name)        \
   static type name __attribute__((used)); \
   type *name##_bss_get(void) __attribute__((const));
diff --git a/crypto/fipsmodule/fips_shared.lds b/crypto/fipsmodule/fips_shared.lds
new file mode 100644
index 0000000..6d44abc
--- /dev/null
+++ b/crypto/fipsmodule/fips_shared.lds
@@ -0,0 +1,19 @@
+SECTIONS
+{
+  .text : {
+    BORINGSSL_bcm_text_start = .;
+    *(.text)
+    BORINGSSL_bcm_text_end = .;
+  }
+  .rodata : {
+    BORINGSSL_bcm_rodata_start = .;
+    *(.rodata)
+    BORINGSSL_bcm_rodata_end = .;
+  }
+
+  /DISCARD/ : {
+    *(.rela.dyn)
+    *(.data)
+    *(.rel.ro)
+  }
+}
diff --git a/crypto/fipsmodule/fips_shared_support.c b/crypto/fipsmodule/fips_shared_support.c
new file mode 100644
index 0000000..07d5945
--- /dev/null
+++ b/crypto/fipsmodule/fips_shared_support.c
@@ -0,0 +1,34 @@
+/* Copyright (c) 2019, Google Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
+
+#include <stdint.h>
+
+
+#if !defined(BORINGSSL_FIPS) || !defined(BORINGSSL_SHARED_LIBRARY)
+#error "This file should only be built in a shared-object FIPS build"
+#endif
+
+// BORINGSSL_bcm_text_hash is is default hash value for the FIPS integrity check
+// that must be replaced with the real value during the build process. This
+// value need only be distinct, i.e. so that we can safely search-and-replace it
+// in an object file.
+const uint8_t BORINGSSL_bcm_text_hash[64];
+const uint8_t BORINGSSL_bcm_text_hash[64] = {
+    0xae, 0x2c, 0xea, 0x2a, 0xbd, 0xa6, 0xf3, 0xec, 0x97, 0x7f, 0x9b,
+    0xf6, 0x94, 0x9a, 0xfc, 0x83, 0x68, 0x27, 0xcb, 0xa0, 0xa0, 0x9f,
+    0x6b, 0x6f, 0xde, 0x52, 0xcd, 0xe2, 0xcd, 0xff, 0x31, 0x80, 0xa2,
+    0xd4, 0xc3, 0x66, 0x0f, 0xc2, 0x6a, 0x7b, 0xf4, 0xbe, 0x39, 0xa2,
+    0xd7, 0x25, 0xdb, 0x21, 0x98, 0xe9, 0xd5, 0x53, 0xbf, 0x5c, 0x32,
+    0x06, 0x83, 0x34, 0x0c, 0x65, 0x89, 0x52, 0xbd, 0x1f,
+};
diff --git a/include/openssl/cpu.h b/include/openssl/cpu.h
index ad5fc94..eb36a57 100644
--- a/include/openssl/cpu.h
+++ b/include/openssl/cpu.h
@@ -93,7 +93,7 @@
 // bits in XCR0, so it is not necessary to check those.
 extern uint32_t OPENSSL_ia32cap_P[4];
 
-#if defined(BORINGSSL_FIPS)
+#if defined(BORINGSSL_FIPS) && !defined(BORINGSSL_SHARED_LIBRARY)
 const uint32_t *OPENSSL_ia32cap_get(void);
 #else
 OPENSSL_INLINE const uint32_t *OPENSSL_ia32cap_get(void) {
diff --git a/util/fipstools/inject_hash/inject_hash.go b/util/fipstools/inject_hash/inject_hash.go
index 29307c0..8cb9e3d 100644
--- a/util/fipstools/inject_hash/inject_hash.go
+++ b/util/fipstools/inject_hash/inject_hash.go
@@ -22,12 +22,14 @@
 	"crypto/hmac"
 	"crypto/sha512"
 	"debug/elf"
+	"encoding/binary"
 	"errors"
 	"flag"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
+	"strings"
 
 	"boringssl.googlesource.com/boringssl/util/ar"
 	"boringssl.googlesource.com/boringssl/util/fipstools/fipscommon"
@@ -35,7 +37,10 @@
 
 func do(outPath, oInput string, arInput string) error {
 	var objectBytes []byte
+	var isStatic bool
 	if len(arInput) > 0 {
+		isStatic = true
+
 		if len(oInput) > 0 {
 			return fmt.Errorf("-in-archive and -in-object are mutually exclusive")
 		}
@@ -63,6 +68,7 @@
 		if objectBytes, err = ioutil.ReadFile(oInput); err != nil {
 			return err
 		}
+		isStatic = strings.HasSuffix(oInput, ".o")
 	} else {
 		return fmt.Errorf("exactly one of -in-archive or -in-object is required")
 	}
@@ -72,15 +78,18 @@
 		return errors.New("failed to parse object: " + err.Error())
 	}
 
-	// Find the .text section.
+	// Find the .text and, optionally, .data sections.
 
-	var textSection *elf.Section
-	var textSectionIndex elf.SectionIndex
+	var textSection, rodataSection *elf.Section
+	var textSectionIndex, rodataSectionIndex elf.SectionIndex
 	for i, section := range object.Sections {
-		if section.Name == ".text" {
+		switch section.Name {
+		case ".text":
 			textSectionIndex = elf.SectionIndex(i)
 			textSection = section
-			break
+		case ".rodata":
+			rodataSectionIndex = elf.SectionIndex(i)
+			rodataSection = section
 		}
 	}
 
@@ -90,8 +99,7 @@
 
 	// Find the starting and ending symbols for the module.
 
-	var startSeen, endSeen bool
-	var start, end uint64
+	var textStart, textEnd, rodataStart, rodataEnd *uint64
 
 	symbols, err := object.Symbols()
 	if err != nil {
@@ -99,50 +107,115 @@
 	}
 
 	for _, symbol := range symbols {
-		if symbol.Section != textSectionIndex {
+		var base uint64
+		switch symbol.Section {
+		case textSectionIndex:
+			base = textSection.Addr
+		case rodataSectionIndex:
+			if rodataSection == nil {
+				continue
+			}
+			base = rodataSection.Addr
+		default:
 			continue
 		}
 
+		if isStatic {
+			// Static objects appear to have different symantics about whether symbol
+			// values are relative to their section or not.
+			base = 0
+		} else if symbol.Value < base {
+			return fmt.Errorf("symbol %q at %x, which is below base of %x", symbol.Name, symbol.Value, base)
+		}
+
+		value := symbol.Value - base
 		switch symbol.Name {
 		case "BORINGSSL_bcm_text_start":
-			if startSeen {
+			if textStart != nil {
 				return errors.New("duplicate start symbol found")
 			}
-			startSeen = true
-			start = symbol.Value
+			textStart = &value
 		case "BORINGSSL_bcm_text_end":
-			if endSeen {
+			if textEnd != nil {
 				return errors.New("duplicate end symbol found")
 			}
-			endSeen = true
-			end = symbol.Value
+			textEnd = &value
+		case "BORINGSSL_bcm_rodata_start":
+			if rodataStart != nil {
+				return errors.New("duplicate rodata start symbol found")
+			}
+			rodataStart = &value
+		case "BORINGSSL_bcm_rodata_end":
+			if rodataEnd != nil {
+				return errors.New("duplicate rodata end symbol found")
+			}
+			rodataEnd = &value
 		default:
 			continue
 		}
 	}
 
-	if !startSeen || !endSeen {
-		return errors.New("could not find module boundaries in object")
+	if textStart == nil || textEnd == nil {
+		return errors.New("could not find .text module boundaries in object")
 	}
 
-	if max := textSection.Size; start > max || start > end || end > max {
-		return fmt.Errorf("invalid module boundaries: start: %x, end: %x, max: %x", start, end, max)
+	if (rodataStart == nil) != (rodataSection == nil) {
+		return errors.New("rodata start marker inconsistent with rodata section presence")
+	}
+
+	if (rodataStart != nil) != (rodataEnd != nil) {
+		return errors.New("rodata marker presence inconsistent")
+	}
+
+	if max := textSection.Size; *textStart > max || *textStart > *textEnd || *textEnd > max {
+		return fmt.Errorf("invalid module .text boundaries: start: %x, end: %x, max: %x", *textStart, *textEnd, max)
+	}
+
+	if rodataSection != nil {
+		if max := rodataSection.Size; *rodataStart > max || *rodataStart > *rodataEnd || *rodataEnd > max {
+			return fmt.Errorf("invalid module .rodata boundaries: start: %x, end: %x, max: %x", *rodataStart, *rodataEnd, max)
+		}
 	}
 
 	// Extract the module from the .text section and hash it.
 
 	text := textSection.Open()
-	if _, err := text.Seek(int64(start), 0); err != nil {
+	if _, err := text.Seek(int64(*textStart), 0); err != nil {
 		return errors.New("failed to seek to module start in .text: " + err.Error())
 	}
-	moduleText := make([]byte, end-start)
+	moduleText := make([]byte, *textEnd-*textStart)
 	if _, err := io.ReadFull(text, moduleText); err != nil {
 		return errors.New("failed to read .text: " + err.Error())
 	}
 
+	// Maybe extract the module's read-only data too
+	var moduleROData []byte
+	if rodataSection != nil {
+		rodata := rodataSection.Open()
+		if _, err := rodata.Seek(int64(*rodataStart), 0); err != nil {
+			return errors.New("failed to seek to module start in .rodata: " + err.Error())
+		}
+		moduleROData = make([]byte, *rodataEnd-*rodataStart)
+		if _, err := io.ReadFull(rodata, moduleROData); err != nil {
+			return errors.New("failed to read .rodata: " + err.Error())
+		}
+	}
+
 	var zeroKey [64]byte
 	mac := hmac.New(sha512.New, zeroKey[:])
-	mac.Write(moduleText)
+
+	if moduleROData != nil {
+		var lengthBytes [8]byte
+		binary.LittleEndian.PutUint64(lengthBytes[:], uint64(len(moduleText)))
+		mac.Write(lengthBytes[:])
+		mac.Write(moduleText)
+
+		binary.LittleEndian.PutUint64(lengthBytes[:], uint64(len(moduleROData)))
+		mac.Write(lengthBytes[:])
+		mac.Write(moduleROData)
+	} else {
+		mac.Write(moduleText)
+	}
 	calculated := mac.Sum(nil)
 
 	// Replace the default hash value in the object with the calculated