Skip to content

Commit 45bad50

Browse files
committed
sign: Add SpecialSlot data structure
Special slots (requirements, entitlements, ...) are handled in 2 places: in GenerateSigningSuperBlob and in newCodeDirectory. This handling mostly hardcodes that there's a macho.CsSlotRequirements slot, and nothing else. For examples, to add handling for a new slot type, newCodeDirectory needs changes in at least 3 non-obvious places (`hashOff` computation, writing of the hashes, and NSpecialSlots). This code abstracts special slots handling by: - adding a new SpecialSlot struct to describe a special slot - the rest of the code no longer needs to know it's dealing with CsSlotRequirements or a CsSlotEntitlements (which I want to add support for) - it adds a SpecialSlotHashWriter type for use in newCodeDirectory to count the number of special slots, compute the `hashOff` value accordingly, write the slots in the correct order, ... This will be useful in the commits which add support for entitlements. Signed-off-by: Christophe Fergeau <[email protected]>
1 parent 75b910e commit 45bad50

File tree

4 files changed

+133
-26
lines changed

4 files changed

+133
-26
lines changed

quill/sign/code_directory.go

+90-12
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import (
1111

1212
"github.com/go-restruct/restruct"
1313

14+
"github.com/anchore/quill/internal/log"
1415
"github.com/anchore/quill/quill/macho"
1516
)
1617

17-
func generateCodeDirectory(id string, hasher hash.Hash, m *macho.File, flags macho.CdFlag, requirementsHashBytes, entitlementsHashBytes []byte) (*macho.Blob, error) {
18-
cd, err := newCodeDirectoryFromMacho(id, hasher, m, flags, requirementsHashBytes, entitlementsHashBytes)
18+
func generateCodeDirectory(id string, hasher hash.Hash, m *macho.File, flags macho.CdFlag, specialSlots []SpecialSlot) (*macho.Blob, error) {
19+
cd, err := newCodeDirectoryFromMacho(id, hasher, m, flags, specialSlots)
1920
if err != nil {
2021
return nil, err
2122
}
@@ -38,7 +39,7 @@ func packCodeDirectory(cd *macho.CodeDirectory, order binary.ByteOrder) (*macho.
3839
return &blob, nil
3940
}
4041

41-
func newCodeDirectoryFromMacho(id string, hasher hash.Hash, m *macho.File, flags macho.CdFlag, requirementsHashBytes, entitlementsHashBytes []byte) (*macho.CodeDirectory, error) {
42+
func newCodeDirectoryFromMacho(id string, hasher hash.Hash, m *macho.File, flags macho.CdFlag, specialSlots []SpecialSlot) (*macho.CodeDirectory, error) {
4243
textSeg := m.Segment("__TEXT")
4344

4445
var codeSize uint32
@@ -58,14 +59,83 @@ func newCodeDirectoryFromMacho(id string, hasher hash.Hash, m *macho.File, flags
5859
return nil, err
5960
}
6061

61-
return newCodeDirectory(id, hasher, textSeg.Offset, textSeg.Filesz, codeSize, hashes, flags, requirementsHashBytes, entitlementsHashBytes)
62+
return newCodeDirectory(id, hasher, textSeg.Offset, textSeg.Filesz, codeSize, hashes, flags, specialSlots)
6263
}
6364

64-
func newCodeDirectory(id string, hasher hash.Hash, execOffset, execSize uint64, codeSize uint32, hashes [][]byte, flags macho.CdFlag, requirementsHashBytes, entitlementsHashBytes []byte) (*macho.CodeDirectory, error) {
65+
// SpecialSlotHashWriter writes the special slots in the right order and with the right content.
66+
// Special slots have a type defined in quill/macho/blob_index.go, their hashes must be written from higher
67+
// type to lower type.
68+
// All slot types between CsSlotInfoslot (1) and the higher valued type must be written to the file.
69+
// The hashes for the missing slots must be filled with 0s.
70+
//
71+
// newCodeDirectory() also needs to know how many slots are present (including
72+
// the 0-filled ones), and the total number of bytes which were written (to
73+
// maintain an offset). It can use maxSlotType and totalBytesWritten for this.
74+
type SpecialSlotHashWriter struct {
75+
totalBytesWritten int // total number of bytes written by the Write method
76+
// slot type with the higher int value. This corresponds to the number of slots which will be written
77+
maxSlotType int
78+
// used to detect inconsistencies in the provided hashes - they must all have the same size
79+
hashSize int
80+
// SpecialSlot map keyed by their type to easily detect which slot types are missing
81+
slots map[int]SpecialSlot
82+
}
83+
84+
// newSpecialSlotHashWriter creates a new SpecialSlotHashWriter for the slots defined in specialSlots.
85+
func newSpecialSlotHashWriter(specialSlots []SpecialSlot) (*SpecialSlotHashWriter, error) {
86+
w := SpecialSlotHashWriter{}
87+
w.slots = map[int]SpecialSlot{}
88+
89+
for _, slot := range specialSlots {
90+
switch w.hashSize {
91+
case 0:
92+
w.hashSize = len(slot.HashBytes)
93+
case len(slot.HashBytes):
94+
// w.hashSize was set previously and has the right value, nothing to do
95+
default:
96+
return nil, fmt.Errorf("inconsistent hash size: %d != %d", w.hashSize, len(slot.HashBytes))
97+
}
98+
99+
slotType := int(slot.Type)
100+
if slotType > w.maxSlotType {
101+
w.maxSlotType = slotType
102+
}
103+
w.slots[slotType] = slot
104+
}
105+
106+
log.Debugf("SpecialSlotHashWriter: %d special slots", w.maxSlotType)
107+
108+
return &w, nil
109+
}
110+
111+
// Write will write all the special slots hashes to w.buffer.
112+
func (w *SpecialSlotHashWriter) Write(buffer *bytes.Buffer) error {
113+
nullHashBytes := bytes.Repeat([]byte{0}, w.hashSize)
114+
w.totalBytesWritten = 0
115+
116+
for i := w.maxSlotType; i > 0; i-- {
117+
log.Debugf("SpecialSlotHashWriter: writing slot %d", i)
118+
hashBytes := nullHashBytes
119+
slot, hasSlot := w.slots[i]
120+
if hasSlot {
121+
hashBytes = slot.HashBytes
122+
} else {
123+
log.Debugf("SpecialSlotHashWriter: slot %d is empty", i)
124+
}
125+
written, err := buffer.Write(hashBytes)
126+
if err != nil {
127+
return fmt.Errorf("unable to write plist hash to code directory: %w", err)
128+
}
129+
w.totalBytesWritten += written
130+
}
131+
132+
return nil
133+
}
134+
135+
func newCodeDirectory(id string, hasher hash.Hash, execOffset, execSize uint64, codeSize uint32, hashes [][]byte, flags macho.CdFlag, specialSlots []SpecialSlot) (*macho.CodeDirectory, error) {
65136
cdSize := unsafe.Sizeof(macho.BlobHeader{}) + unsafe.Sizeof(macho.CodeDirectoryHeader{})
66137
idOff := int32(cdSize)
67138
// note: the hash offset starts at the first non-special hash (page hashes). Special hashes (e.g. requirements hash) are written before the page hashes.
68-
hashOff := idOff + int32(len(id)+1) + int32(len(requirementsHashBytes)) + int32(len(entitlementsHashBytes))
69139

70140
var ht macho.HashType
71141
switch hasher.Size() {
@@ -80,17 +150,25 @@ func newCodeDirectory(id string, hasher hash.Hash, execOffset, execSize uint64,
80150
buff := bytes.Buffer{}
81151

82152
// write the identifier
83-
if _, err := buff.Write([]byte(id + "\000")); err != nil {
153+
hashOff := int(idOff)
154+
var (
155+
written int
156+
err error
157+
)
158+
if written, err = buff.Write([]byte(id + "\000")); err != nil {
84159
return nil, fmt.Errorf("unable to write ID to code directory: %w", err)
85160
}
161+
hashOff += written
86162

87163
// write hashes
88-
if _, err := buff.Write(requirementsHashBytes); err != nil {
89-
return nil, fmt.Errorf("unable to write requirements hash to code directory: %w", err)
164+
specialSlotHashWriter, err := newSpecialSlotHashWriter(specialSlots)
165+
if err != nil {
166+
return nil, err
90167
}
91-
if _, err := buff.Write(entitlementsHashBytes); err != nil {
92-
return nil, fmt.Errorf("unable to write plist hash to code directory: %w", err)
168+
if err := specialSlotHashWriter.Write(&buff); err != nil {
169+
return nil, err
93170
}
171+
hashOff += specialSlotHashWriter.totalBytesWritten
94172

95173
for idx, hBytes := range hashes {
96174
_, err := buff.Write(hBytes)
@@ -105,7 +183,7 @@ func newCodeDirectory(id string, hasher hash.Hash, execOffset, execSize uint64,
105183
Flags: flags,
106184
HashOffset: uint32(hashOff),
107185
IdentOffset: uint32(idOff),
108-
NSpecialSlots: uint32(2), // requirements + plist
186+
NSpecialSlots: uint32(specialSlotHashWriter.maxSlotType),
109187
NCodeSlots: uint32(len(hashes)),
110188
CodeLimit: codeSize,
111189
HashSize: uint8(hasher.Size()),

quill/sign/code_directory_test.go

+20-2
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,16 @@ func Test_newCodeDirectoryFromMacho(t *testing.T) {
8787
pListBytes, err := hex.DecodeString(tt.pListHash)
8888
require.NoError(t, err)
8989

90-
actualCD, err := newCodeDirectoryFromMacho(tt.id, tt.hasher, m, tt.flags, reqBytes, pListBytes)
90+
reqSlot := SpecialSlot{
91+
Type: macho.CsSlotRequirements,
92+
HashBytes: reqBytes,
93+
}
94+
plistSlot := SpecialSlot{
95+
Type: macho.CsSlotInfoslot,
96+
HashBytes: pListBytes,
97+
}
98+
99+
actualCD, err := newCodeDirectoryFromMacho(tt.id, tt.hasher, m, tt.flags, []SpecialSlot{reqSlot, plistSlot})
91100
require.NoError(t, err)
92101

93102
// make certain the headers match
@@ -184,7 +193,16 @@ func Test_generateCodeDirectory(t *testing.T) {
184193
pListBytes, err := hex.DecodeString(tt.pListHash)
185194
require.NoError(t, err)
186195

187-
cdBlob, err := generateCodeDirectory(tt.id, tt.hasher, m, tt.flags, reqBytes, pListBytes)
196+
reqSlot := SpecialSlot{
197+
Type: macho.CsSlotRequirements,
198+
HashBytes: reqBytes,
199+
}
200+
plistSlot := SpecialSlot{
201+
Type: macho.CsSlotInfoslot,
202+
HashBytes: pListBytes,
203+
}
204+
205+
cdBlob, err := generateCodeDirectory(tt.id, tt.hasher, m, tt.flags, []SpecialSlot{reqSlot, plistSlot})
188206
require.NoError(t, err)
189207

190208
cdBytes, err := cdBlob.Pack()

quill/sign/requirements.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -61,37 +61,37 @@ const (
6161
anchorCertIndex = ^uint32(0) // index for anchor (last in chain), equiv to -1
6262
)
6363

64-
func generateRequirements(id string, h hash.Hash, signingMaterial pki.SigningMaterial) (*macho.Blob, []byte, error) {
64+
func generateRequirements(id string, h hash.Hash, signingMaterial pki.SigningMaterial) (*SpecialSlot, error) {
6565
var reqBytes []byte
6666
if signingMaterial.Signer == nil {
6767
log.Trace("skipping adding designated requirement because no signer was found")
6868
reqBytes = []byte{0, 0, 0, 0}
6969
} else {
7070
req, err := newRequirements(id, signingMaterial)
7171
if err != nil {
72-
return nil, nil, err
72+
return nil, err
7373
}
7474

7575
reqBytes, err = restruct.Pack(macho.SigningOrder, req)
7676
if err != nil {
77-
return nil, nil, fmt.Errorf("unable to encode requirement: %w", err)
77+
return nil, fmt.Errorf("unable to encode requirement: %w", err)
7878
}
7979
}
8080

8181
blob := macho.NewBlob(macho.MagicRequirements, reqBytes)
8282

8383
blobBytes, err := restruct.Pack(macho.SigningOrder, &blob)
8484
if err != nil {
85-
return nil, nil, fmt.Errorf("unable to encode requirements blob: %w", err)
85+
return nil, fmt.Errorf("unable to encode requirements blob: %w", err)
8686
}
8787

8888
// the requirements hash is against the entire blob, not just the payload
8989
_, err = h.Write(blobBytes)
9090
if err != nil {
91-
return nil, nil, err
91+
return nil, err
9292
}
9393

94-
return &blob, h.Sum(nil), nil
94+
return &SpecialSlot{macho.CsSlotRequirements, &blob, h.Sum(nil)}, nil
9595
}
9696

9797
func newRequirements(id string, signingMaterial pki.SigningMaterial) (*macho.Requirements, error) {

quill/sign/signing_super_blob.go

+17-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package sign
22

33
import (
4-
"bytes"
54
"crypto/sha256"
65
"fmt"
76

@@ -11,6 +10,12 @@ import (
1110
"github.com/anchore/quill/quill/pki"
1211
)
1312

13+
type SpecialSlot struct {
14+
Type macho.SlotType
15+
Blob *macho.Blob
16+
HashBytes []byte
17+
}
18+
1419
func GenerateSigningSuperBlob(id string, m *macho.File, signingMaterial pki.SigningMaterial, paddingTarget int) (int, []byte, error) {
1520
var cdFlags macho.CdFlag
1621
if signingMaterial.Signer != nil {
@@ -22,15 +27,19 @@ func GenerateSigningSuperBlob(id string, m *macho.File, signingMaterial pki.Sign
2227
cdFlags = macho.Adhoc
2328
}
2429

25-
requirementsBlob, requirementsHashBytes, err := generateRequirements(id, sha256.New(), signingMaterial)
30+
specialSlots := []SpecialSlot{}
31+
32+
requirements, err := generateRequirements(id, sha256.New(), signingMaterial)
2633
if err != nil {
2734
return 0, nil, fmt.Errorf("unable to create requirements: %w", err)
2835
}
36+
if requirements != nil {
37+
specialSlots = append(specialSlots, *requirements)
38+
}
2939

3040
// TODO: add entitlements, for the meantime, don't include it
31-
entitlementsHashBytes := bytes.Repeat([]byte{0}, sha256.New().Size())
3241

33-
cdBlob, err := generateCodeDirectory(id, sha256.New(), m, cdFlags, requirementsHashBytes, entitlementsHashBytes)
42+
cdBlob, err := generateCodeDirectory(id, sha256.New(), m, cdFlags, specialSlots)
3443
if err != nil {
3544
return 0, nil, fmt.Errorf("unable to create code directory: %w", err)
3645
}
@@ -41,9 +50,11 @@ func GenerateSigningSuperBlob(id string, m *macho.File, signingMaterial pki.Sign
4150
}
4251

4352
sb := macho.NewSuperBlob(macho.MagicEmbeddedSignature)
44-
4553
sb.Add(macho.CsSlotCodedirectory, cdBlob)
46-
sb.Add(macho.CsSlotRequirements, requirementsBlob)
54+
for _, slot := range specialSlots {
55+
sb.Add(slot.Type, slot.Blob)
56+
}
57+
4758
sb.Add(macho.CsSlotCmsSignature, cmsBlob)
4859

4960
sb.Finalize(paddingTarget)

0 commit comments

Comments
 (0)