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)
+	}
+}