| // Copyright 2024 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| /* |
| FIPS-140 Verification Support |
| |
| See ../../../internal/obj/fips.go for a basic overview. |
| This file is concerned with computing the hash of the FIPS code+data. |
| Package obj has taken care of marking the FIPS symbols with the |
| special types STEXTFIPS, SRODATAFIPS, SNOPTRDATAFIPS, and SDATAFIPS. |
| |
| # FIPS Symbol Layout |
| |
| The first order of business is collecting the FIPS symbols into |
| contiguous sections of the final binary and identifying the start and |
| end of those sections. The linker already tracks the start and end of |
| the text section as runtime.text and runtime.etext, and similarly for |
| other sections, but the implementation of those symbols is tricky and |
| platform-specific. The problem is that they are zero-length |
| pseudo-symbols that share addresses with other symbols, which makes |
| everything harder. For the FIPS sections, we avoid that subtlety by |
| defining actual non-zero-length symbols bracketing each section and |
| use those symbols as the boundaries. |
| |
| Specifically, we define a 1-byte symbol go:textfipsstart of type |
| STEXTFIPSSTART and a 1-byte symbol go:textfipsend of type STEXTFIPSEND, |
| and we place those two symbols immediately before and after the |
| STEXTFIPS symbols. We do the same for SRODATAFIPS, SNOPTRDATAFIPS, |
| and SDATAFIPS. Because the symbols are real (but otherwise unused) data, |
| they can be treated as normal symbols for symbol table purposes and |
| don't need the same kind of special handling that runtime.text and |
| friends do. |
| |
| Note that treating the FIPS text as starting at &go:textfipsstart and |
| ending at &go:textfipsend means that go:textfipsstart is included in |
| the verified data while go:textfipsend is not. That's fine: they are |
| only framing and neither strictly needs to be in the hash. |
| |
| The new special symbols are created by [loadfips]. |
| |
| # FIPS Info Layout |
| |
| Having collated the FIPS symbols, we need to compute the hash |
| and then leave both the expected hash and the FIPS address ranges |
| for the run-time check in crypto/internal/fips140/check. |
| We do that by creating a special symbol named go:fipsinfo of the form |
| |
| struct { |
| sum [32]byte |
| self uintptr // points to start of struct |
| sects [4]struct{ |
| start uintptr |
| end uintptr |
| } |
| } |
| |
| The crypto/internal/fips140/check uses linkname to access this symbol, |
| which is of course not included in the hash. |
| |
| # FIPS Info Calculation |
| |
| When using internal linking, [asmbfips] runs after writing the output |
| binary but before code-signing it. It reads the relevant sections |
| back from the output file, hashes them, and then writes the go:fipsinfo |
| content into the output file. |
| |
| When using external linking, especially with -buildmode=pie, we cannot |
| predict the specific PLT index references that the linker will insert |
| into the FIPS code sections, so we must read the final linked executable |
| after external linking, compute the hash, and then write it back to the |
| executable in the go:fipsinfo sum field. [hostlinkfips] does this. |
| It finds go:fipsinfo easily because that symbol is given its own section |
| (.go.fipsinfo on ELF, __go_fipsinfo on Mach-O), and then it can use the |
| sections field to find the relevant parts of the executable, hash them, |
| and fill in sum. |
| |
| Both [asmbfips] and [hostlinkfips] need the same hash calculation code. |
| The [fipsObj] type provides that calculation. |
| |
| # Debugging |
| |
| It is of course impossible to debug a mismatched hash directly: |
| two random 32-byte strings differ. For debugging, the linker flag |
| -fipso can be set to the name of a file (such as /tmp/fips.o) |
| where the linker will write the “FIPS object” that is being hashed. |
| |
| There is also commented-out code in crypto/internal/fips140/check that |
| will write /tmp/fipscheck.o during the run-time verification. |
| |
| When the hashes differ, the first step is to uncomment the |
| /tmp/fipscheck.o-writing code and then rebuild with |
| -ldflags=-fipso=/tmp/fips.o. Then when the hash check fails, |
| compare /tmp/fips.o and /tmp/fipscheck.o to find the differences. |
| */ |
| |
| package ld |
| |
| import ( |
| "bufio" |
| "bytes" |
| "cmd/internal/obj" |
| "cmd/internal/objabi" |
| "cmd/link/internal/loader" |
| "cmd/link/internal/sym" |
| "crypto/hmac" |
| "crypto/sha256" |
| "debug/elf" |
| "debug/macho" |
| "debug/pe" |
| "encoding/binary" |
| "fmt" |
| "hash" |
| "io" |
| "os" |
| ) |
| |
| const enableFIPS = true |
| |
| // fipsSyms are the special FIPS section bracketing symbols. |
| var fipsSyms = []struct { |
| name string |
| kind sym.SymKind |
| sym loader.Sym |
| seg *sym.Segment |
| }{ |
| {name: "go:textfipsstart", kind: sym.STEXTFIPSSTART, seg: &Segtext}, |
| {name: "go:textfipsend", kind: sym.STEXTFIPSEND}, |
| {name: "go:rodatafipsstart", kind: sym.SRODATAFIPSSTART, seg: &Segrodata}, |
| {name: "go:rodatafipsend", kind: sym.SRODATAFIPSEND}, |
| {name: "go:noptrdatafipsstart", kind: sym.SNOPTRDATAFIPSSTART, seg: &Segdata}, |
| {name: "go:noptrdatafipsend", kind: sym.SNOPTRDATAFIPSEND}, |
| {name: "go:datafipsstart", kind: sym.SDATAFIPSSTART, seg: &Segdata}, |
| {name: "go:datafipsend", kind: sym.SDATAFIPSEND}, |
| } |
| |
| // fipsinfo is the loader symbol for go:fipsinfo. |
| var fipsinfo loader.Sym |
| |
| const ( |
| fipsMagic = "\xff Go fipsinfo \xff\x00" |
| fipsMagicLen = 16 |
| fipsSumLen = 32 |
| ) |
| |
| // loadfips creates the special bracketing symbols and go:fipsinfo. |
| func loadfips(ctxt *Link) { |
| if !obj.EnableFIPS() { |
| return |
| } |
| if ctxt.BuildMode == BuildModePlugin { // not sure why this doesn't work |
| return |
| } |
| // Write the fipsinfo symbol, which crypto/internal/fips140/check uses. |
| ldr := ctxt.loader |
| // TODO lock down linkname |
| info := ldr.CreateSymForUpdate("go:fipsinfo", 0) |
| info.SetType(sym.SFIPSINFO) |
| |
| data := make([]byte, fipsMagicLen+fipsSumLen) |
| copy(data, fipsMagic) |
| info.SetData(data) |
| info.SetSize(int64(len(data))) // magic + checksum, to be filled in |
| info.AddAddr(ctxt.Arch, info.Sym()) // self-reference |
| |
| for i := range fipsSyms { |
| s := &fipsSyms[i] |
| sb := ldr.CreateSymForUpdate(s.name, 0) |
| sb.SetType(s.kind) |
| sb.SetLocal(true) |
| sb.SetSize(1) |
| s.sym = sb.Sym() |
| info.AddAddr(ctxt.Arch, s.sym) |
| if s.kind == sym.STEXTFIPSSTART || s.kind == sym.STEXTFIPSEND { |
| ctxt.Textp = append(ctxt.Textp, s.sym) |
| } |
| } |
| |
| fipsinfo = info.Sym() |
| } |
| |
| // fipsObj calculates the fips object hash and optionally writes |
| // the hashed content to a file for debugging. |
| type fipsObj struct { |
| r io.ReaderAt |
| w io.Writer |
| wf *os.File |
| h hash.Hash |
| tmp [8]byte |
| } |
| |
| // newFipsObj creates a fipsObj reading from r and writing to fipso |
| // (unless fipso is the empty string, in which case it writes nowhere |
| // and only computes the hash). |
| func newFipsObj(r io.ReaderAt, fipso string) (*fipsObj, error) { |
| f := &fipsObj{r: r} |
| f.h = hmac.New(sha256.New, make([]byte, 32)) |
| f.w = f.h |
| if fipso != "" { |
| wf, err := os.Create(fipso) |
| if err != nil { |
| return nil, err |
| } |
| f.wf = wf |
| f.w = io.MultiWriter(f.h, wf) |
| } |
| |
| if _, err := f.w.Write([]byte("go fips object v1\n")); err != nil { |
| f.Close() |
| return nil, err |
| } |
| return f, nil |
| } |
| |
| // addSection adds the section of r (passed to newFipsObj) |
| // starting at byte offset start and ending before byte offset end |
| // to the fips object file. |
| func (f *fipsObj) addSection(start, end int64) error { |
| n := end - start |
| binary.BigEndian.PutUint64(f.tmp[:], uint64(n)) |
| f.w.Write(f.tmp[:]) |
| _, err := io.Copy(f.w, io.NewSectionReader(f.r, start, n)) |
| return err |
| } |
| |
| // sum returns the hash of the fips object file. |
| func (f *fipsObj) sum() []byte { |
| return f.h.Sum(nil) |
| } |
| |
| // Close closes the fipsObj. In particular it closes the output |
| // object file specified by fipso in the call to [newFipsObj]. |
| func (f *fipsObj) Close() error { |
| if f.wf != nil { |
| return f.wf.Close() |
| } |
| return nil |
| } |
| |
| // asmbfips is called from [asmb] to update go:fipsinfo |
| // when using internal linking. |
| // See [hostlinkfips] for external linking. |
| func asmbfips(ctxt *Link, fipso string) { |
| if !obj.EnableFIPS() { |
| return |
| } |
| if ctxt.LinkMode == LinkExternal { |
| return |
| } |
| if ctxt.BuildMode == BuildModePlugin { // not sure why this doesn't work |
| return |
| } |
| |
| // Create a new FIPS object with data read from our output file. |
| f, err := newFipsObj(bytes.NewReader(ctxt.Out.Data()), fipso) |
| if err != nil { |
| Errorf("asmbfips: %v", err) |
| return |
| } |
| defer f.Close() |
| |
| // Add the FIPS sections to the FIPS object. |
| ldr := ctxt.loader |
| for i := 0; i < len(fipsSyms); i += 2 { |
| start := &fipsSyms[i] |
| end := &fipsSyms[i+1] |
| startAddr := ldr.SymValue(start.sym) |
| endAddr := ldr.SymValue(end.sym) |
| seg := start.seg |
| if seg.Vaddr == 0 && seg == &Segrodata { // some systems use text instead of separate rodata |
| seg = &Segtext |
| } |
| base := int64(seg.Fileoff - seg.Vaddr) |
| if !(seg.Vaddr <= uint64(startAddr) && startAddr <= endAddr && uint64(endAddr) <= seg.Vaddr+seg.Filelen) { |
| Errorf("asmbfips: %s not in expected segment (%#x..%#x not in %#x..%#x)", start.name, startAddr, endAddr, seg.Vaddr, seg.Vaddr+seg.Filelen) |
| return |
| } |
| |
| if err := f.addSection(startAddr+base, endAddr+base); err != nil { |
| Errorf("asmbfips: %v", err) |
| return |
| } |
| } |
| |
| // Overwrite the go:fipsinfo sum field with the calculated sum. |
| addr := uint64(ldr.SymValue(fipsinfo)) |
| seg := &Segdata |
| if !(seg.Vaddr <= addr && addr+32 < seg.Vaddr+seg.Filelen) { |
| Errorf("asmbfips: fipsinfo not in expected segment (%#x..%#x not in %#x..%#x)", addr, addr+32, seg.Vaddr, seg.Vaddr+seg.Filelen) |
| return |
| } |
| ctxt.Out.SeekSet(int64(seg.Fileoff + addr - seg.Vaddr + fipsMagicLen)) |
| ctxt.Out.Write(f.sum()) |
| |
| if err := f.Close(); err != nil { |
| Errorf("asmbfips: %v", err) |
| return |
| } |
| } |
| |
| // hostlinkfips is called from [hostlink] to update go:fipsinfo |
| // when using external linking. |
| // See [asmbfips] for internal linking. |
| func hostlinkfips(ctxt *Link, exe, fipso string) error { |
| if !obj.EnableFIPS() { |
| return nil |
| } |
| if ctxt.BuildMode == BuildModePlugin { // not sure why this doesn't work |
| return nil |
| } |
| switch { |
| case ctxt.IsElf(): |
| return elffips(ctxt, exe, fipso) |
| case ctxt.HeadType == objabi.Hdarwin: |
| return machofips(ctxt, exe, fipso) |
| case ctxt.HeadType == objabi.Hwindows: |
| return pefips(ctxt, exe, fipso) |
| } |
| |
| // If we can't do FIPS, leave the output binary alone. |
| // If people enable FIPS the init-time check will fail, |
| // but the binaries will work otherwise. |
| return fmt.Errorf("fips unsupported on %s", ctxt.HeadType) |
| } |
| |
| // machofips updates go:fipsinfo after external linking |
| // on systems using Mach-O (GOOS=darwin, GOOS=ios). |
| func machofips(ctxt *Link, exe, fipso string) error { |
| // Open executable both for reading Mach-O and for the fipsObj. |
| mf, err := macho.Open(exe) |
| if err != nil { |
| return err |
| } |
| defer mf.Close() |
| |
| wf, err := os.OpenFile(exe, os.O_RDWR, 0) |
| if err != nil { |
| return err |
| } |
| defer wf.Close() |
| |
| f, err := newFipsObj(wf, fipso) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| |
| // Find the go:fipsinfo symbol. |
| sect := mf.Section("__go_fipsinfo") |
| if sect == nil { |
| return fmt.Errorf("cannot find __go_fipsinfo") |
| } |
| data, err := sect.Data() |
| if err != nil { |
| return err |
| } |
| |
| uptr := ctxt.Arch.ByteOrder.Uint64 |
| if ctxt.Arch.PtrSize == 4 { |
| uptr = func(x []byte) uint64 { |
| return uint64(ctxt.Arch.ByteOrder.Uint32(x)) |
| } |
| } |
| |
| // Add the sections listed in go:fipsinfo to the FIPS object. |
| // On Mac, the debug/macho package is not reporting any relocations, |
| // but the addends are all in the data already, all relative to |
| // the same base. |
| // Determine the base used for the self pointer, and then apply |
| // that base to the other uintptrs. |
| // The very high bits of the uint64s seem to be relocation metadata, |
| // so clear them. |
| // For non-pie builds, there are no relocations at all: |
| // the data holds the actual pointers. |
| // This code handles both pie and non-pie binaries. |
| const addendMask = 1<<48 - 1 |
| data = data[fipsMagicLen+fipsSumLen:] |
| self := int64(uptr(data)) & addendMask |
| base := int64(sect.Offset) - self |
| data = data[ctxt.Arch.PtrSize:] |
| |
| for i := 0; i < 4; i++ { |
| start := int64(uptr(data[0:]))&addendMask + base |
| end := int64(uptr(data[ctxt.Arch.PtrSize:]))&addendMask + base |
| data = data[2*ctxt.Arch.PtrSize:] |
| if err := f.addSection(start, end); err != nil { |
| return err |
| } |
| } |
| |
| // Overwrite the go:fipsinfo sum field with the calculated sum. |
| if _, err := wf.WriteAt(f.sum(), int64(sect.Offset)+fipsMagicLen); err != nil { |
| return err |
| } |
| if err := wf.Close(); err != nil { |
| return err |
| } |
| return f.Close() |
| } |
| |
| // elffips updates go:fipsinfo after external linking |
| // on systems using ELF (most Unix systems). |
| func elffips(ctxt *Link, exe, fipso string) error { |
| // Open executable both for reading ELF and for the fipsObj. |
| ef, err := elf.Open(exe) |
| if err != nil { |
| return err |
| } |
| defer ef.Close() |
| |
| wf, err := os.OpenFile(exe, os.O_RDWR, 0) |
| if err != nil { |
| return err |
| } |
| defer wf.Close() |
| |
| f, err := newFipsObj(wf, fipso) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| |
| // Find the go:fipsinfo symbol. |
| sect := ef.Section(".go.fipsinfo") |
| if sect == nil { |
| return fmt.Errorf("cannot find .go.fipsinfo") |
| } |
| |
| data, err := sect.Data() |
| if err != nil { |
| return err |
| } |
| |
| uptr := ctxt.Arch.ByteOrder.Uint64 |
| if ctxt.Arch.PtrSize == 4 { |
| uptr = func(x []byte) uint64 { |
| return uint64(ctxt.Arch.ByteOrder.Uint32(x)) |
| } |
| } |
| |
| // Add the sections listed in go:fipsinfo to the FIPS object. |
| // We expect R_zzz_RELATIVE relocations where the zero-based |
| // values are already stored in the data. That is, the addend |
| // is in the data itself in addition to being in the relocation tables. |
| // So no need to parse the relocation tables unless we find a |
| // toolchain that doesn't initialize the data this way. |
| // For non-pie builds, there are no relocations at all: |
| // the data holds the actual pointers. |
| // This code handles both pie and non-pie binaries. |
| data = data[fipsMagicLen+fipsSumLen:] |
| data = data[ctxt.Arch.PtrSize:] |
| |
| Addrs: |
| for i := 0; i < 4; i++ { |
| start := uptr(data[0:]) |
| end := uptr(data[ctxt.Arch.PtrSize:]) |
| data = data[2*ctxt.Arch.PtrSize:] |
| for _, prog := range ef.Progs { |
| if prog.Type == elf.PT_LOAD && prog.Vaddr <= start && start <= end && end <= prog.Vaddr+prog.Filesz { |
| if err := f.addSection(int64(start+prog.Off-prog.Vaddr), int64(end+prog.Off-prog.Vaddr)); err != nil { |
| return err |
| } |
| continue Addrs |
| } |
| } |
| return fmt.Errorf("invalid pointers found in .go.fipsinfo") |
| } |
| |
| // Overwrite the go:fipsinfo sum field with the calculated sum. |
| if _, err := wf.WriteAt(f.sum(), int64(sect.Offset)+fipsMagicLen); err != nil { |
| return err |
| } |
| if err := wf.Close(); err != nil { |
| return err |
| } |
| return f.Close() |
| } |
| |
| // pefips updates go:fipsinfo after external linking |
| // on systems using PE (GOOS=windows). |
| func pefips(ctxt *Link, exe, fipso string) error { |
| // Open executable both for reading Mach-O and for the fipsObj. |
| pf, err := pe.Open(exe) |
| if err != nil { |
| return err |
| } |
| defer pf.Close() |
| |
| wf, err := os.OpenFile(exe, os.O_RDWR, 0) |
| if err != nil { |
| return err |
| } |
| defer wf.Close() |
| |
| f, err := newFipsObj(wf, fipso) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| |
| // Find the go:fipsinfo symbol. |
| // PE does not put it in its own section, so we have to scan for it. |
| // It is near the start of the data segment, right after go:buildinfo, |
| // so we should not have to scan too far. |
| const maxScan = 16 << 20 |
| sect := pf.Section(".data") |
| if sect == nil { |
| return fmt.Errorf("cannot find .data") |
| } |
| b := bufio.NewReader(sect.Open()) |
| off := int64(0) |
| data := make([]byte, fipsMagicLen+fipsSumLen+9*ctxt.Arch.PtrSize) |
| for ; ; off += 16 { |
| if off >= maxScan { |
| break |
| } |
| if _, err := io.ReadFull(b, data[:fipsMagicLen]); err != nil { |
| return fmt.Errorf("scanning PE for FIPS magic: %v", err) |
| } |
| if string(data[:fipsMagicLen]) == fipsMagic { |
| if _, err := io.ReadFull(b, data[fipsMagicLen:]); err != nil { |
| return fmt.Errorf("scanning PE for FIPS magic: %v", err) |
| } |
| break |
| } |
| } |
| |
| uptr := ctxt.Arch.ByteOrder.Uint64 |
| if ctxt.Arch.PtrSize == 4 { |
| uptr = func(x []byte) uint64 { |
| return uint64(ctxt.Arch.ByteOrder.Uint32(x)) |
| } |
| } |
| |
| // Add the sections listed in go:fipsinfo to the FIPS object. |
| // Determine the base used for the self pointer, and then apply |
| // that base to the other uintptrs. |
| // For pie builds, the addends are in the data. |
| // For non-pie builds, there are no relocations at all: |
| // the data holds the actual pointers. |
| // This code handles both pie and non-pie binaries. |
| data = data[fipsMagicLen+fipsSumLen:] |
| self := int64(uptr(data)) |
| data = data[ctxt.Arch.PtrSize:] |
| |
| // On 64-bit binaries the pointers have extra bits set |
| // that don't appear in the actual section headers. |
| // For example, one generated test binary looks like: |
| // |
| // .data VirtualAddress = 0x2af000 |
| // .data (file) Offset = 0x2ac400 |
| // .data (file) Size = 0x1fc00 |
| // go:fipsinfo found at offset 0x2ac5e0 (off=0x1e0) |
| // go:fipsinfo self pointer = 0x01402af1e0 |
| // |
| // From the section headers, the address of the go:fipsinfo symbol |
| // should be 0x2af000 + (0x2ac5e0 - 0x2ac400) = 0x2af1e0, |
| // yet in this case its pointer is 0x1402af1e0, meaning the |
| // data section's VirtualAddress is really 0x1402af000. |
| // This is not (only) a 32-bit truncation problem, since the uint32 |
| // truncation of that address would be 0x402af000, not 0x2af000. |
| // Perhaps there is some 64-bit extension that debug/pe is not |
| // reading or is misreading. In any event, we can derive the delta |
| // between computed VirtualAddress and listed VirtualAddress |
| // and apply it to the rest of the pointers. |
| // As a sanity check, the low 12 bits (virtual page offset) |
| // must match between our computed address and the actual one. |
| peself := int64(sect.VirtualAddress) + off |
| if self&0xfff != off&0xfff { |
| return fmt.Errorf("corrupt pointer found in go:fipsinfo") |
| } |
| delta := peself - self |
| |
| Addrs: |
| for i := 0; i < 4; i++ { |
| start := int64(uptr(data[0:])) + delta |
| end := int64(uptr(data[ctxt.Arch.PtrSize:])) + delta |
| data = data[2*ctxt.Arch.PtrSize:] |
| for _, sect := range pf.Sections { |
| if int64(sect.VirtualAddress) <= start && start <= end && end <= int64(sect.VirtualAddress)+int64(sect.Size) { |
| off := int64(sect.Offset) - int64(sect.VirtualAddress) |
| if err := f.addSection(start+off, end+off); err != nil { |
| return err |
| } |
| continue Addrs |
| } |
| } |
| return fmt.Errorf("invalid pointers found in go:fipsinfo") |
| } |
| |
| // Overwrite the go:fipsinfo sum field with the calculated sum. |
| if _, err := wf.WriteAt(f.sum(), int64(sect.Offset)+off+fipsMagicLen); err != nil { |
| return err |
| } |
| if err := wf.Close(); err != nil { |
| return err |
| } |
| return f.Close() |
| } |