cherry/codesign: add an ad-hoc code signing tool
Add an ad-hoc code signing tool, trying to do what darwin linker
does. It seems to make the "codesign" command line tool happy.
Change-Id: I643368ea5cbcfbb933c83ff9ced46820542f3681
Reviewed-on: https://go-review.googlesource.com/c/scratch/+/271866
Trust: Cherry Zhang <cherryyz@google.com>
Reviewed-by: Cherry Zhang <cherryyz@google.com>
diff --git a/cherry/codesign.go b/cherry/codesign.go
new file mode 100644
index 0000000..eff1acb
--- /dev/null
+++ b/cherry/codesign.go
@@ -0,0 +1,356 @@
+// Copyright 2020 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.
+
+// This programs does ad-hoc code signing fo Mach-O files.
+// It tries to do what darwin linker does.
+
+package main
+
+import (
+ "crypto/sha256"
+ "debug/macho"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "os"
+ "unsafe"
+)
+
+const (
+ pageSizeBits = 12
+ pageSize = 1 << pageSizeBits
+)
+
+const LC_CODE_SIGNATURE = 0x1d
+
+const fileHeaderSize64 = 8 * 4
+
+const (
+ CSMAGIC_REQUIREMENT = 0xfade0c00 // single Requirement blob
+ CSMAGIC_REQUIREMENTS = 0xfade0c01 // Requirements vector (internal requirements)
+ CSMAGIC_CODEDIRECTORY = 0xfade0c02 // CodeDirectory blob
+ CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0 // embedded form of signature data
+ CSMAGIC_DETACHED_SIGNATURE = 0xfade0cc1 // multi-arch collection of embedded signatures
+
+ CSSLOT_CODEDIRECTORY = 0 // slot index for CodeDirectory
+)
+
+const (
+ kSecCodeSignatureNoHash = 0 // null value
+ kSecCodeSignatureHashSHA1 = 1 // SHA-1
+ kSecCodeSignatureHashSHA256 = 2 // SHA-256
+ kSecCodeSignatureHashSHA256Truncated = 3 // SHA-256 truncated to first 20 bytes
+ kSecCodeSignatureHashSHA384 = 4 // SHA-384
+ kSecCodeSignatureHashSHA512 = 5 // SHA-512
+)
+
+const (
+ CS_EXECSEG_MAIN_BINARY = 0x1 // executable segment denotes main binary
+ CS_EXECSEG_ALLOW_UNSIGNED = 0x10 // allow unsigned pages (for debugging)
+ CS_EXECSEG_DEBUGGER = 0x20 // main binary is debugger
+ CS_EXECSEG_JIT = 0x40 // JIT enabled
+ CS_EXECSEG_SKIP_LV = 0x80 // skip library validation
+ CS_EXECSEG_CAN_LOAD_CDHASH = 0x100 // can bless cdhash for execution
+ CS_EXECSEG_CAN_EXEC_CDHASH = 0x200 // can execute blessed cdhash
+)
+
+type Blob struct {
+ typ uint32 // type of entry
+ offset uint32 // offset of entry
+ // data follows
+}
+
+func (b *Blob) put(out []byte) []byte {
+ out = put32be(out, b.typ)
+ out = put32be(out, b.offset)
+ return out
+}
+
+type SuperBlob struct {
+ magic uint32 // magic number
+ length uint32 // total length of SuperBlob
+ count uint32 // number of index entries following
+ // blobs []Blob
+}
+
+func (s *SuperBlob) put(out []byte) []byte {
+ out = put32be(out, s.magic)
+ out = put32be(out, s.length)
+ out = put32be(out, s.count)
+ return out
+}
+
+type CodeDirectory struct {
+ magic uint32 // magic number (CSMAGIC_CODEDIRECTORY)
+ length uint32 // total length of CodeDirectory blob
+ version uint32 // compatibility version
+ flags uint32 // setup and mode flags
+ hashOffset uint32 // offset of hash slot element at index zero
+ identOffset uint32 // offset of identifier string
+ nSpecialSlots uint32 // number of special hash slots
+ nCodeSlots uint32 // number of ordinary (code) hash slots
+ codeLimit uint32 // limit to main image signature range
+ hashSize uint8 // size of each hash in bytes
+ hashType uint8 // type of hash (cdHashType* constants)
+ _pad1 uint8 // unused (must be zero)
+ pageSize uint8 // log2(page size in bytes); 0 => infinite
+ _pad2 uint32 // unused (must be zero)
+ scatterOffset uint32
+ teamOffset uint32
+ _pad3 uint32
+ codeLimit64 uint64
+ execSegBase uint64
+ execSegLimit uint64
+ execSegFlags uint64
+ // data follows
+}
+
+func (c *CodeDirectory) put(out []byte) []byte {
+ out = put32be(out, c.magic)
+ out = put32be(out, c.length)
+ out = put32be(out, c.version)
+ out = put32be(out, c.flags)
+ out = put32be(out, c.hashOffset)
+ out = put32be(out, c.identOffset)
+ out = put32be(out, c.nSpecialSlots)
+ out = put32be(out, c.nCodeSlots)
+ out = put32be(out, c.codeLimit)
+ out = put8(out, c.hashSize)
+ out = put8(out, c.hashType)
+ out = put8(out, c._pad1)
+ out = put8(out, c.pageSize)
+ out = put32be(out, c._pad2)
+ out = put32be(out, c.scatterOffset)
+ out = put32be(out, c.teamOffset)
+ out = put32be(out, c._pad3)
+ out = put64be(out, c.codeLimit64)
+ out = put64be(out, c.execSegBase)
+ out = put64be(out, c.execSegLimit)
+ out = put64be(out, c.execSegFlags)
+ return out
+}
+
+type linkeditDataCmd struct {
+ cmd uint32
+ cmdsize uint32 // sizeof(struct linkedit_data_command)
+ dataoff uint32 // file offset of data in __LINKEDIT segment
+ datasize uint32 // file size of data in __LINKEDIT segment
+}
+
+func (l *linkeditDataCmd) put(out []byte) []byte {
+ // load command is little endian
+ out = put32le(out, l.cmd)
+ out = put32le(out, l.cmdsize)
+ out = put32le(out, l.dataoff)
+ out = put32le(out, l.datasize)
+ return out
+}
+
+func get32le(b []byte) uint32 { return binary.LittleEndian.Uint32(b) }
+func put32le(b []byte, x uint32) []byte { binary.LittleEndian.PutUint32(b, x); return b[4:] }
+func put32be(b []byte, x uint32) []byte { binary.BigEndian.PutUint32(b, x); return b[4:] }
+func put64le(b []byte, x uint64) []byte { binary.LittleEndian.PutUint64(b, x); return b[8:] }
+func put64be(b []byte, x uint64) []byte { binary.BigEndian.PutUint64(b, x); return b[8:] }
+func put8(b []byte, x uint8) []byte { b[0] = x; return b[1:] }
+func puts(b, s []byte) []byte { n := copy(b, s); return b[n:] }
+
+// round x up to a multiple of n. n must be a power of 2.
+func roundUp(x, n int) int { return (x + n - 1) &^ (n - 1) }
+
+const verbose = false
+
+func main() {
+ if len(os.Args) != 2 {
+ fmt.Println("usage: codesign <binary>")
+ os.Exit(1)
+ }
+
+ fname := os.Args[1]
+ f, err := os.OpenFile(fname, os.O_RDWR, 0)
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+
+ mf, err := macho.NewFile(f)
+ if err != nil {
+ panic(err)
+ }
+ if mf.Magic != macho.Magic64 {
+ panic("not 64-bit")
+ }
+ if mf.ByteOrder != binary.LittleEndian {
+ panic("not little endian")
+ }
+
+ // find existing LC_CODE_SIGNATURE and __LINKEDIT segment
+ var sigOff, sigSz, linkeditOff int
+ var linkeditSeg, textSeg *macho.Segment
+ loadOff := fileHeaderSize64
+ for _, l := range mf.Loads {
+ data := l.Raw()
+ cmd, sz := get32le(data), get32le(data[4:])
+ if cmd == LC_CODE_SIGNATURE {
+ sigOff = int(get32le(data[8:]))
+ sigSz = int(get32le(data[12:]))
+ }
+ if seg, ok := l.(*macho.Segment); ok {
+ switch seg.Name {
+ case "__LINKEDIT":
+ linkeditSeg = seg
+ linkeditOff = loadOff
+ case "__TEXT":
+ textSeg = seg
+ }
+ }
+ loadOff += int(sz)
+ }
+
+ if sigOff == 0 {
+ st, err := f.Stat()
+ if err != nil {
+ panic(err)
+ }
+ sigOff = int(st.Size())
+ sigOff = roundUp(sigOff, 16) // round up to 16 bytes ???
+ err = f.Truncate(int64(sigOff))
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ // compute sizes
+ id := "a.out\000"
+ nhashes := (sigOff + pageSize - 1) / pageSize
+ idOff := int(unsafe.Sizeof(CodeDirectory{}))
+ hashOff := idOff + len(id)
+ cdirSz := hashOff + nhashes*sha256.Size
+ sz := int(unsafe.Sizeof(SuperBlob{})+unsafe.Sizeof(Blob{})) + cdirSz
+ if sigSz != 0 && sz != sigSz {
+ println(sz, sigSz)
+ panic("LC_CODE_SIGNATURE exists but with a different size. already signed?")
+ }
+
+ if sigSz == 0 { // LC_CODE_SIGNATURE does not exist. Add one.
+ csCmdSz := int(unsafe.Sizeof(linkeditDataCmd{}))
+ csCmd := linkeditDataCmd{
+ cmd: LC_CODE_SIGNATURE,
+ cmdsize: uint32(csCmdSz),
+ dataoff: uint32(sigOff),
+ datasize: uint32(sz),
+ }
+ if loadOff+csCmdSz > int(mf.Sections[0].Offset) {
+ panic("no space for adding LC_CODE_SIGNATURE")
+ }
+ out := make([]byte, csCmdSz)
+ csCmd.put(out)
+ _, err = f.WriteAt(out, int64(loadOff))
+ if err != nil {
+ panic(err)
+ }
+
+ // fix up header: update Ncmd and Cmdsz
+ var tmp [8]byte
+ put32le(tmp[:4], mf.FileHeader.Ncmd+1)
+ _, err = f.WriteAt(tmp[:4], int64(unsafe.Offsetof(mf.FileHeader.Ncmd)))
+ if err != nil {
+ panic(err)
+ }
+ put32le(tmp[:4], mf.FileHeader.Cmdsz+uint32(csCmdSz))
+ _, err = f.WriteAt(tmp[:4], int64(unsafe.Offsetof(mf.FileHeader.Cmdsz)))
+ if err != nil {
+ panic(err)
+ }
+
+ // fix up LINKEDIT segment: update Memsz and Filesz
+ segSz := sigOff + sz - int(linkeditSeg.Offset)
+ put64le(tmp[:8], uint64(roundUp(segSz, 0x4000))) // round up to physical page size
+ _, err = f.WriteAt(tmp[:8], int64(linkeditOff)+int64(unsafe.Offsetof(macho.Segment64{}.Memsz)))
+ if err != nil {
+ panic(err)
+ }
+ put64le(tmp[:8], uint64(segSz))
+ _, err = f.WriteAt(tmp[:8], int64(linkeditOff)+int64(unsafe.Offsetof(macho.Segment64{}.Filesz)))
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ // emit blob headers
+ sb := SuperBlob{
+ magic: CSMAGIC_EMBEDDED_SIGNATURE,
+ length: uint32(sz),
+ count: 1,
+ }
+ blob := Blob{
+ typ: CSSLOT_CODEDIRECTORY,
+ offset: uint32(unsafe.Sizeof(SuperBlob{}) + unsafe.Sizeof(Blob{})),
+ }
+ cdir := CodeDirectory{
+ magic: CSMAGIC_CODEDIRECTORY,
+ length: uint32(sz) - uint32(unsafe.Sizeof(SuperBlob{})+unsafe.Sizeof(Blob{})),
+ version: 0x20400,
+ flags: 0x20002, // adhoc | linkerSigned
+ hashOffset: uint32(hashOff),
+ identOffset: uint32(idOff),
+ nCodeSlots: uint32(nhashes),
+ codeLimit: uint32(sigOff),
+ hashSize: sha256.Size,
+ hashType: kSecCodeSignatureHashSHA256,
+ pageSize: uint8(pageSizeBits),
+ execSegBase: textSeg.Offset,
+ execSegLimit: textSeg.Filesz,
+ }
+ if mf.Type == macho.TypeExec {
+ cdir.execSegFlags = CS_EXECSEG_MAIN_BINARY
+ }
+
+ out := make([]byte, sz)
+ outp := out
+
+ outp = sb.put(outp)
+ outp = blob.put(outp)
+ outp = cdir.put(outp)
+ outp = puts(outp, []byte(id))
+
+ // emit hashes
+ _, err = f.Seek(0, os.SEEK_SET)
+ if err != nil {
+ panic(err)
+ }
+ var buf [pageSize]byte
+ fileOff := 0
+ for fileOff < sigOff {
+ n, err := io.ReadFull(f, buf[:])
+ if err == io.EOF {
+ break
+ }
+ if err != nil && err != io.ErrUnexpectedEOF {
+ panic(err)
+ }
+ if fileOff+n > sigOff {
+ n = sigOff - fileOff
+ }
+ h := sha256.New()
+ h.Write(buf[:n])
+ b := h.Sum(nil)
+ outp = puts(outp, b[:])
+ fileOff += n
+ }
+
+ if verbose {
+ for i := 0; i < len(out); i += 16 {
+ end := i + 16
+ if end > len(out) {
+ end = len(out)
+ }
+ fmt.Printf("% x\n", out[i:end])
+ }
+ }
+
+ _, err = f.WriteAt(out, int64(sigOff))
+ if err != nil {
+ panic(err)
+ }
+}