util/fipstools/integrity_tool/main.go (337 lines of code) (raw):
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"debug/elf"
"encoding/binary"
"encoding/hex"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
const usageTemplate = `
Compare the fipsmodule hash of two shared libraries or two binaries statically linked AWS-LC:
%s <fileLeft> <fileRight>
Print the fipsmodule hash of a shared library or a binary statically linked AWS-LC.
Options:
-verify used to verify that the contents of the module match the recorded hash.
-extract used to extract the BCM .text and .rodata sections explicitly for inspection. This will dump in binary and hex.
%s [-verify] [-extract] <file>
`
const (
fipsModuleHashSymbol = "BORINGSSL_bcm_text_hash"
fipsModuleTextSection = "BORINGSSL_bcm_text"
fipsModuleReadOnlySection = "BORINGSSL_bcm_rodata"
)
var (
usageStr string
verify bool
extract bool
)
func init() {
programName := filepath.Base(os.Args[0])
usageStr = fmt.Sprintf(usageTemplate, programName, programName)
flag.BoolVar(&verify, "verify", false, "Verify the recorded fipsmodule hash of a shared library or a binary statically linked AWS-LC to it's contents")
flag.BoolVar(&extract, "extract", false, "Extract the BCM sections to output files")
flag.CommandLine.Usage = func() {
_, _ = fmt.Fprint(os.Stderr, usageStr)
// Exit handled by flag package
}
}
func printErrorAndExit(message string, args ...interface{}) {
_, _ = fmt.Fprintf(os.Stderr, message, args...)
os.Exit(1)
}
func printMessage(message string, args ...interface{}) {
_, _ = fmt.Fprintf(os.Stdout, message, args...)
}
func main() {
flag.Parse()
switch {
case len(flag.Args()) == 1:
printModuleHash(flag.Arg(0))
case len(flag.Args()) == 2:
diffModules(flag.Arg(0), flag.Arg(1))
default:
printErrorAndExit(usageStr)
}
}
func printModuleHash(filepath string) {
module, err := loadFIPSModule(filepath)
if err != nil {
printErrorAndExit("Error: %v\n", err)
}
defer module.Close()
hashBytes, err := getFIPSModuleIntegrityHash(&module)
if err != nil {
printErrorAndExit("Error: %v\n", err)
}
hexStr := hex.EncodeToString(hashBytes)
printMessage("%s\n", hexStr)
if verify {
computed, err := computeFIPSModuleIntegrityHash(&module)
if err != nil {
printErrorAndExit("Error Verifying Integrity Hash: %v\n", err)
}
if bytes.Equal(hashBytes, computed) {
printMessage("Integrity Hash: VERIFIED\n")
} else {
printErrorAndExit("Integrity Hash: MISMATCH\nComputed: %v\n", hex.EncodeToString(computed))
}
}
if extract {
textBinary, textHex, rodataBinary, rodataHex, err := extractModuleSections(&module)
if err != nil {
printErrorAndExit("Error: %v\n", err)
}
printMessage("Extracted %s: %s\n", fipsModuleTextSection, textBinary)
printMessage("Extracted %s: %s\n", fipsModuleTextSection, textHex)
if rodataBinary != "" && rodataHex != "" {
printMessage("Extracted %s: %s\n", fipsModuleReadOnlySection, rodataBinary)
printMessage("Extracted %s: %s\n", fipsModuleReadOnlySection, rodataHex)
}
}
}
func diffModules(left, right string) {
leftModule, err := loadFIPSModule(left)
if err != nil {
printErrorAndExit("Error: %v\n", err)
}
defer leftModule.Close()
leftHashBytes, err := getFIPSModuleIntegrityHash(&leftModule)
if err != nil {
printErrorAndExit("Error: %v\n", err)
}
rightModule, err := loadFIPSModule(right)
if err != nil {
printErrorAndExit("Error: %v\n", err)
}
defer rightModule.Close()
rightHashBytes, err := getFIPSModuleIntegrityHash(&rightModule)
if err != nil {
printErrorAndExit("Error: %v\n", err)
}
leftHexStr := hex.EncodeToString(leftHashBytes)
rightHexStr := hex.EncodeToString(rightHashBytes)
if leftHexStr != rightHexStr {
printErrorAndExit("MISMATCH: (%s != %s)\n", leftHexStr, rightHexStr)
}
printMessage("MATCH: (%s)\n", leftHexStr)
}
type FIPSModule struct {
file *elf.File
hash *elf.Symbol
textStart *elf.Symbol
textEnd *elf.Symbol
rodataStart *elf.Symbol
rodataEnd *elf.Symbol
}
func (m *FIPSModule) Close() error {
m.hash = nil
m.textStart = nil
m.textEnd = nil
m.rodataStart = nil
m.rodataEnd = nil
return m.file.Close()
}
func (m *FIPSModule) HasROData() bool {
return m.rodataStart != nil && m.rodataEnd != nil
}
func loadFIPSModule(filepath string) (FIPSModule, error) {
file, err := elf.Open(filepath)
if err != nil {
return FIPSModule{}, fmt.Errorf("failed to open file: %w", err)
}
symbols, err := file.Symbols()
if err != nil {
return FIPSModule{}, fmt.Errorf("error reading symbols: %w", err)
}
var hashSymbol, textStart, textEnd, rodataStart, rodataEnd *elf.Symbol
for _, symbol := range symbols {
symbol := symbol
if symbol.Name == fipsModuleHashSymbol {
hashSymbol = &symbol
} else if strings.HasPrefix(symbol.Name, fipsModuleTextSection) {
if strings.HasSuffix(symbol.Name, "_start") {
textStart = &symbol
} else if strings.HasSuffix(symbol.Name, "_end") {
textEnd = &symbol
} else {
return FIPSModule{}, fmt.Errorf("unexpected symbol name: %s", symbol.Name)
}
} else if strings.HasPrefix(symbol.Name, fipsModuleReadOnlySection) {
if strings.HasSuffix(symbol.Name, "_start") {
rodataStart = &symbol
} else if strings.HasSuffix(symbol.Name, "_end") {
rodataEnd = &symbol
} else {
return FIPSModule{}, fmt.Errorf("unexpected symbol name: %s", symbol.Name)
}
}
}
if hashSymbol == nil {
return FIPSModule{}, fmt.Errorf("failed to find fipsmodule hash symbol")
}
// We should always find the the .text section start and end markers. The .rodata section markers will only
// be present for shared library builds.
if textStart == nil || textEnd == nil {
return FIPSModule{}, fmt.Errorf("failed to find fipsmodule .text start and end markers")
}
if (rodataStart != nil) != (rodataEnd != nil) {
return FIPSModule{}, fmt.Errorf("inconsistent fipsmodule .rodata start and end markers")
}
return FIPSModule{
file: file,
hash: hashSymbol,
textStart: textStart,
textEnd: textEnd,
rodataStart: rodataStart,
rodataEnd: rodataEnd,
}, nil
}
func getFIPSModuleIntegrityHash(module *FIPSModule) ([]byte, error) {
hashSymbolSection := module.file.Sections[module.hash.Section]
if hashSymbolSection == nil {
return nil, fmt.Errorf("fipsmodule hash symbol's section not found")
}
hashSymbolOffset := module.hash.Value - hashSymbolSection.Addr
const expectedSize = 32
if module.hash.Size != expectedSize {
return nil, fmt.Errorf("the fipsmodule hash symbol does not match the expected size, expect %v got %v", expectedSize, module.hash.Size)
}
hashSymbolSectionData, err := hashSymbolSection.Data()
if err != nil {
return nil, fmt.Errorf("error reading section containing fipsmodule hash symbol: %w", err)
}
if int(hashSymbolOffset+module.hash.Size) > len(hashSymbolSectionData) {
return nil, fmt.Errorf("the fipsmodule hash extends out of bounds of the section?")
}
return hashSymbolSectionData[hashSymbolOffset : hashSymbolOffset+module.hash.Size], nil
}
func computeFIPSModuleIntegrityHash(module *FIPSModule) ([]byte, error) {
length, subReader, err := getSubSectionReader(module.file, module.textStart, module.textEnd)
if err != nil {
return nil, fmt.Errorf("failed to extract .text section: %w", err)
}
var lengthBytes [8]byte
var zeroKey [64]byte
hmacHash := hmac.New(sha256.New, zeroKey[:])
binary.LittleEndian.PutUint64(lengthBytes[:], uint64(length))
if _, err := hmacHash.Write(lengthBytes[:]); err != nil {
return nil, err
}
if _, err := io.Copy(hmacHash, subReader); err != nil {
return nil, err
}
if module.HasROData() {
length, subReader, err := getSubSectionReader(module.file, module.rodataStart, module.rodataEnd)
if err != nil {
return nil, fmt.Errorf("failed to extract .rodata section: %w", err)
}
binary.LittleEndian.PutUint64(lengthBytes[:], uint64(length))
if _, err := hmacHash.Write(lengthBytes[:]); err != nil {
return nil, err
}
if _, err := io.Copy(hmacHash, subReader); err != nil {
return nil, err
}
}
return hmacHash.Sum(nil), nil
}
func extractModuleSections(module *FIPSModule) (textBinary, textHex, rodataBinary, rodataHex string, err error) {
textBinary, err = dumpSubSectionBinaryToFile(module.file, fipsModuleTextSection, module.textStart, module.textEnd)
if err != nil {
return "", "", "", "", fmt.Errorf("failed to dump %s binary to file: %w", fipsModuleTextSection, err)
}
textHex, err = dumpSubSectionHexToFile(module.file, fipsModuleTextSection, module.textStart, module.textEnd)
if err != nil {
return "", "", "", "", fmt.Errorf("failed to dump %s hex to file: %w", fipsModuleTextSection, err)
}
if module.HasROData() {
rodataBinary, err = dumpSubSectionBinaryToFile(module.file, fipsModuleReadOnlySection, module.rodataStart, module.rodataEnd)
if err != nil {
return "", "", "", "", fmt.Errorf("failed to dump %s binary to file: %w", fipsModuleReadOnlySection, err)
}
rodataHex, err = dumpSubSectionHexToFile(module.file, fipsModuleReadOnlySection, module.rodataStart, module.rodataEnd)
if err != nil {
return "", "", "", "", fmt.Errorf("failed to dump %s hex to file: %w", fipsModuleReadOnlySection, err)
}
}
return textBinary, textHex, rodataBinary, rodataHex, nil
}
func dumpSubSectionBinaryToFile(file *elf.File, name string, start, end *elf.Symbol) (string, error) {
_, subReader, err := getSubSectionReader(file, start, end)
if err != nil {
return "", fmt.Errorf("failed to extract %s section: %w", name, err)
}
fileName, err := writeContentsToTempFile(subReader, name+".bin")
if err != nil {
return "", fmt.Errorf("failed to write %s binary to file: %w", name, err)
}
return fileName, nil
}
func dumpSubSectionHexToFile(file *elf.File, name string, start, end *elf.Symbol) (string, error) {
_, subReader, err := getSubSectionReader(file, start, end)
if err != nil {
return "", fmt.Errorf("failed to extract %s section: %w", name, err)
}
tempFile, err := getTempFile(name + ".hex")
if err != nil {
return "", err
}
hexDumper := hex.Dumper(tempFile)
if _, err := io.Copy(hexDumper, subReader); err != nil {
return "", fmt.Errorf("failed to write %s hex to file: %w", name, err)
}
if err := hexDumper.Close(); err != nil {
return "", fmt.Errorf("failed to close hex dumper: %w", err)
}
if err := tempFile.Close(); err != nil {
return "", fmt.Errorf("failed to close file: %w", err)
}
return tempFile.Name(), nil
}
func getSubSectionReader(file *elf.File, start, end *elf.Symbol) (int64, io.Reader, error) {
symbolSection := file.Sections[start.Section]
if symbolSection == nil {
return 0, nil, fmt.Errorf("section not found")
}
if start.Section != end.Section {
return 0, nil, fmt.Errorf("start and end symbols are in different sections")
}
startOffset := start.Value - symbolSection.Addr
if start.Size != 0 {
return 0, nil, fmt.Errorf("start symbol does not match the expected size, expect 0 got %v", start.Size)
}
endOffset := end.Value - symbolSection.Addr
if end.Size != 0 {
return 0, nil, fmt.Errorf("end symbol does not match the expected size, expect 0 got %v", end.Size)
}
sectionReader := symbolSection.Open()
if _, err := sectionReader.Seek(int64(startOffset), 0); err != nil {
return 0, nil, err
}
length := int64(endOffset - startOffset)
return length, io.LimitReader(sectionReader, length), nil
}
func writeContentsToTempFile(reader io.Reader, filename string) (string, error) {
file, err := getTempFile(filename)
if err != nil {
return "", err
}
if _, err := io.Copy(file, reader); err != nil {
return "", fmt.Errorf("failed to write file: %w", err)
}
if err := file.Close(); err != nil {
return "", fmt.Errorf("failed to close file: %w", err)
}
return file.Name(), nil
}
func getTempFile(filename string) (*os.File, error) {
file, err := os.CreateTemp("", fmt.Sprintf("*.%s", filename))
if err != nil {
return nil, fmt.Errorf("failed to create file: %w", err)
}
return file, nil
}