diff --git a/.golangci.yaml b/.golangci.yaml index f2dd296..8727ae8 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -21,7 +21,6 @@ linters: enable: - asciicheck - bodyclose - - depguard - dogsled - dupl - errcheck @@ -69,4 +68,5 @@ linters: # - varcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. # - deadcode # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. # - structcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. -# - rowserrcheck # we're not using sql.Rows at all in the codebase \ No newline at end of file +# - rowserrcheck # we're not using sql.Rows at all in the codebase +# - depguard # this requires additional configuration https://github.com/golangci/golangci-lint/issues/3877#issuecomment-1573760321 diff --git a/Makefile b/Makefile index b6f47dd..2662c3c 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ GLOW_CMD = $(TEMP_DIR)/glow # Tool versions ################################# QUILL_VERSION = latest -GOLANG_CI_VERSION = v1.52.2 +GOLANG_CI_VERSION = v1.57.2 GOBOUNCER_VERSION = v0.4.0 GORELEASER_VERSION = v1.17.0 GOSIMPORTS_VERSION = v0.3.8 diff --git a/cmd/quill/cli/commands/describe.go b/cmd/quill/cli/commands/describe.go index dce8b27..4546cd4 100644 --- a/cmd/quill/cli/commands/describe.go +++ b/cmd/quill/cli/commands/describe.go @@ -41,7 +41,7 @@ func Describe(app clio.Application) *cobra.Command { return nil }, ), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { defer bus.Exit() var err error diff --git a/cmd/quill/cli/commands/embedded_certificates.go b/cmd/quill/cli/commands/embedded_certificates.go index 356777b..2eba434 100644 --- a/cmd/quill/cli/commands/embedded_certificates.go +++ b/cmd/quill/cli/commands/embedded_certificates.go @@ -20,7 +20,7 @@ func EmbeddedCerts(app clio.Application) *cobra.Command { Use: "embedded-certificates", Short: "show the certificates embedded into quill (typically the Apple root and intermediate certs)", Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { defer bus.Exit() var err error diff --git a/cmd/quill/cli/commands/extract_certificates.go b/cmd/quill/cli/commands/extract_certificates.go index cea7906..fb5eeb3 100644 --- a/cmd/quill/cli/commands/extract_certificates.go +++ b/cmd/quill/cli/commands/extract_certificates.go @@ -40,7 +40,7 @@ func ExtractCertificates(app clio.Application) *cobra.Command { return nil }, ), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { defer bus.Exit() certs, err := extractCertificates(opts.Path, opts.Leaf) diff --git a/cmd/quill/cli/commands/notarize.go b/cmd/quill/cli/commands/notarize.go index 55b73b3..ff90e78 100644 --- a/cmd/quill/cli/commands/notarize.go +++ b/cmd/quill/cli/commands/notarize.go @@ -47,7 +47,7 @@ func Notarize(app clio.Application) *cobra.Command { return nil }, ), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { defer bus.Exit() // TODO: verify path is a signed darwin binary diff --git a/cmd/quill/cli/commands/p12_attach_chain.go b/cmd/quill/cli/commands/p12_attach_chain.go index 5dc35b5..c4570e8 100644 --- a/cmd/quill/cli/commands/p12_attach_chain.go +++ b/cmd/quill/cli/commands/p12_attach_chain.go @@ -52,7 +52,7 @@ func P12AttachChain(app clio.Application) *cobra.Command { return nil }, ), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { defer bus.Exit() newFilename, err := writeP12WithChain(opts.Path, opts.P12.Password, opts.Keychain.Path, true) diff --git a/cmd/quill/cli/commands/p12_describe.go b/cmd/quill/cli/commands/p12_describe.go index cd00b00..3797bfb 100644 --- a/cmd/quill/cli/commands/p12_describe.go +++ b/cmd/quill/cli/commands/p12_describe.go @@ -36,7 +36,7 @@ func P12Describe(app clio.Application) *cobra.Command { return nil }, ), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { defer bus.Exit() description, err := describeP12(opts.Path, opts.Password) diff --git a/cmd/quill/cli/commands/sign.go b/cmd/quill/cli/commands/sign.go index 3f7f0b7..b143eb4 100644 --- a/cmd/quill/cli/commands/sign.go +++ b/cmd/quill/cli/commands/sign.go @@ -37,7 +37,7 @@ func Sign(app clio.Application) *cobra.Command { return nil }, ), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { defer bus.Exit() return sign(opts.Path, opts.Signing) @@ -72,6 +72,7 @@ func sign(binPath string, opts options.Signing) error { cfg.WithIdentity(opts.Identity) cfg.WithTimestampServer(opts.TimestampServer) + cfg.WithEntitlements(opts.Entitlements) return quill.Sign(cfg) } diff --git a/cmd/quill/cli/commands/sign_and_notarize.go b/cmd/quill/cli/commands/sign_and_notarize.go index 822ab20..1ead33c 100644 --- a/cmd/quill/cli/commands/sign_and_notarize.go +++ b/cmd/quill/cli/commands/sign_and_notarize.go @@ -47,7 +47,7 @@ func SignAndNotarize(app clio.Application) *cobra.Command { return nil }, ), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { defer bus.Exit() err := sign(opts.Path, opts.Signing) diff --git a/cmd/quill/cli/commands/submission_list.go b/cmd/quill/cli/commands/submission_list.go index 439d2e2..daa9ca4 100644 --- a/cmd/quill/cli/commands/submission_list.go +++ b/cmd/quill/cli/commands/submission_list.go @@ -25,7 +25,7 @@ func SubmissionList(app clio.Application) *cobra.Command { Use: "list", Short: "list previous submissions to Apple's Notary service", Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, _ []string) error { defer bus.Exit() log.Info("fetching previous submissions") diff --git a/cmd/quill/cli/commands/submission_logs.go b/cmd/quill/cli/commands/submission_logs.go index cbc06c5..51b2cc4 100644 --- a/cmd/quill/cli/commands/submission_logs.go +++ b/cmd/quill/cli/commands/submission_logs.go @@ -34,7 +34,7 @@ func SubmissionLogs(app clio.Application) *cobra.Command { return nil }, ), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { defer bus.Exit() log.Infof("fetching submission logs for %q", opts.ID) diff --git a/cmd/quill/cli/commands/submission_status.go b/cmd/quill/cli/commands/submission_status.go index 0b52549..0555beb 100644 --- a/cmd/quill/cli/commands/submission_status.go +++ b/cmd/quill/cli/commands/submission_status.go @@ -41,7 +41,7 @@ func SubmissionStatus(app clio.Application) *cobra.Command { return nil }, ), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { defer bus.Exit() log.Infof("checking submission status for %q", opts.ID) diff --git a/cmd/quill/cli/options/signing.go b/cmd/quill/cli/options/signing.go index db51271..49972b1 100644 --- a/cmd/quill/cli/options/signing.go +++ b/cmd/quill/cli/options/signing.go @@ -20,7 +20,8 @@ type Signing struct { FailWithoutFullChain bool `yaml:"fail-without-full-chain" json:"fail-without-full-chain" mapstructure:"fail-without-full-chain"` // unbound options - Password string `yaml:"password" json:"password" mapstructure:"password"` + Password string `yaml:"password" json:"password" mapstructure:"password"` + Entitlements string `yaml:"entitlements" json:"entitlements" mapstructure:"entitlements"` } func DefaultSigning() Signing { @@ -60,6 +61,12 @@ func (o *Signing) AddFlags(flags fangs.FlagSet) { "ad-hoc", "", "perform ad-hoc signing. No cryptographic signature is included and --p12 key and certificate input are not needed. Do NOT use this option for production builds.", ) + + flags.StringVarP( + &o.Entitlements, + "entitlements", "", + "path to an XML file containing the entitlements for the binary being signed", + ) } func (o *Signing) DescribeFields(d fangs.FieldDescriptionSet) { diff --git a/quill/extract/details.go b/quill/extract/details.go index 364d2eb..ad50324 100644 --- a/quill/extract/details.go +++ b/quill/extract/details.go @@ -35,10 +35,11 @@ func (d Details) String(hideVerboseData bool) (r string) { for idx, req := range d.SuperBlob.Requirements { r += fmt.Sprintf("\nRequirements (block %d):\n", idx+1) + doIndent(req.String(), " ") } + if d.SuperBlob.Entitlements != nil { + r += "\nEntitlements:\n" + doIndent(d.SuperBlob.Entitlements.String(), " ") + } } - // TODO: add entitlements - return r } diff --git a/quill/extract/entitlements.go b/quill/extract/entitlements.go index e462aeb..830b8a3 100644 --- a/quill/extract/entitlements.go +++ b/quill/extract/entitlements.go @@ -1,10 +1,23 @@ package extract type EntitlementDetails struct { - Blob BlobDetails `json:"blob"` + Blob BlobDetails `json:"blob"` + Entitlements string `json:"entitlements,omitempty"` + EntitlementsDER []byte `json:"entitlements_der,omitempty"` } -func getEntitlements(_ File) []EntitlementDetails { - // TODO - return nil +func getEntitlements(m File) *EntitlementDetails { + entitlements := m.blacktopFile.CodeSignature().Entitlements + entitlementsDER := m.blacktopFile.CodeSignature().EntitlementsDER + if entitlements == "" && entitlementsDER == nil { + return nil + } + return &EntitlementDetails{ + Entitlements: entitlements, + EntitlementsDER: entitlementsDER, + } +} + +func (e EntitlementDetails) String() string { + return e.Entitlements } diff --git a/quill/extract/super_blob.go b/quill/extract/super_blob.go index 077eb64..37d4eff 100644 --- a/quill/extract/super_blob.go +++ b/quill/extract/super_blob.go @@ -5,7 +5,7 @@ type SuperBlobDetails struct { Size uint32 `json:"size"` CodeDirectories []CodeDirectoryDetails `json:"codeDirectories"` Requirements []RequirementDetails `json:"requirements"` - Entitlements []EntitlementDetails `json:"entitlements"` + Entitlements *EntitlementDetails `json:"entitlements"` Signatures []SignatureDetails `json:"signatures"` } diff --git a/quill/pki/apple/internal/generate/main.go b/quill/pki/apple/internal/generate/main.go index 6de0a7b..856bbed 100644 --- a/quill/pki/apple/internal/generate/main.go +++ b/quill/pki/apple/internal/generate/main.go @@ -191,7 +191,7 @@ func extractCertLinks(selection *goquery.Selection, u string) []Link { baseURL := parsedURL.Scheme + "://" + parsedURL.Host // Find all elements in the selection - selection.Find("li").Each(func(i int, row *goquery.Selection) { + selection.Find("li").Each(func(_ int, row *goquery.Selection) { // Find the element in the first in the row link := row.Find("a") diff --git a/quill/sign.go b/quill/sign.go index 3d4cbb3..a034131 100644 --- a/quill/sign.go +++ b/quill/sign.go @@ -21,6 +21,7 @@ type SigningConfig struct { SigningMaterial pki.SigningMaterial Identity string Path string + Entitlements string } func NewSigningConfigFromPEMs(binaryPath, certificate, privateKey, password string, failWithoutFullChain bool) (*SigningConfig, error) { @@ -66,6 +67,11 @@ func (c *SigningConfig) WithTimestampServer(url string) *SigningConfig { return c } +func (c *SigningConfig) WithEntitlements(path string) *SigningConfig { + c.Entitlements = path + return c +} + func Sign(cfg SigningConfig) error { f, err := os.Open(cfg.Path) if err != nil { @@ -212,6 +218,16 @@ func signSingleBinary(cfg SigningConfig) error { log.Warnf("only ad-hoc signing, which means that anyone can alter the binary contents without you knowing (there is no cryptographic signature)") } + entitlementsXML := "" + if cfg.Entitlements != "" { + log.Infof("Loading entitlements from %s", cfg.Entitlements) + data, err := os.ReadFile(cfg.Entitlements) + if err != nil { + return err + } + entitlementsXML = string(data) + } + // (patch) add empty LcCodeSignature loader (offset and size references are not set) if err = m.AddEmptyCodeSigningCmd(); err != nil { return err @@ -219,7 +235,7 @@ func signSingleBinary(cfg SigningConfig) error { // first pass: add the signed data with the dummy loader log.Debugf("estimating signing material size") - superBlobSize, sbBytes, err := sign.GenerateSigningSuperBlob(cfg.Identity, m, cfg.SigningMaterial, 0) + superBlobSize, sbBytes, err := sign.GenerateSigningSuperBlob(cfg.Identity, m, cfg.SigningMaterial, entitlementsXML, 0) if err != nil { return fmt.Errorf("failed to add signing data on pass=1: %w", err) } @@ -232,7 +248,7 @@ func signSingleBinary(cfg SigningConfig) error { // second pass: now that all of the sizing is right, let's do it again with the final contents (replacing the hashes and signature) log.Debug("creating signature for binary") - _, sbBytes, err = sign.GenerateSigningSuperBlob(cfg.Identity, m, cfg.SigningMaterial, superBlobSize) + _, sbBytes, err = sign.GenerateSigningSuperBlob(cfg.Identity, m, cfg.SigningMaterial, entitlementsXML, superBlobSize) if err != nil { return fmt.Errorf("failed to add signing data on pass=2: %w", err) } diff --git a/quill/sign/code_directory.go b/quill/sign/code_directory.go index f8359c9..93b3b1b 100644 --- a/quill/sign/code_directory.go +++ b/quill/sign/code_directory.go @@ -11,11 +11,12 @@ import ( "github.com/go-restruct/restruct" + "github.com/anchore/quill/internal/log" "github.com/anchore/quill/quill/macho" ) -func generateCodeDirectory(id string, hasher hash.Hash, m *macho.File, flags macho.CdFlag, requirementsHashBytes, entitlementsHashBytes []byte) (*macho.Blob, error) { - cd, err := newCodeDirectoryFromMacho(id, hasher, m, flags, requirementsHashBytes, entitlementsHashBytes) +func generateCodeDirectory(id string, hasher hash.Hash, m *macho.File, flags macho.CdFlag, specialSlots []SpecialSlot) (*macho.Blob, error) { + cd, err := newCodeDirectoryFromMacho(id, hasher, m, flags, specialSlots) if err != nil { return nil, err } @@ -38,7 +39,7 @@ func packCodeDirectory(cd *macho.CodeDirectory, order binary.ByteOrder) (*macho. return &blob, nil } -func newCodeDirectoryFromMacho(id string, hasher hash.Hash, m *macho.File, flags macho.CdFlag, requirementsHashBytes, entitlementsHashBytes []byte) (*macho.CodeDirectory, error) { +func newCodeDirectoryFromMacho(id string, hasher hash.Hash, m *macho.File, flags macho.CdFlag, specialSlots []SpecialSlot) (*macho.CodeDirectory, error) { textSeg := m.Segment("__TEXT") var codeSize uint32 @@ -58,14 +59,83 @@ func newCodeDirectoryFromMacho(id string, hasher hash.Hash, m *macho.File, flags return nil, err } - return newCodeDirectory(id, hasher, textSeg.Offset, textSeg.Filesz, codeSize, hashes, flags, requirementsHashBytes, entitlementsHashBytes) + return newCodeDirectory(id, hasher, textSeg.Offset, textSeg.Filesz, codeSize, hashes, flags, specialSlots) } -func newCodeDirectory(id string, hasher hash.Hash, execOffset, execSize uint64, codeSize uint32, hashes [][]byte, flags macho.CdFlag, requirementsHashBytes, entitlementsHashBytes []byte) (*macho.CodeDirectory, error) { +// SpecialSlotHashWriter writes the special slots in the right order and with the right content. +// Special slots have a type defined in quill/macho/blob_index.go, their hashes must be written from higher +// type to lower type. +// All slot types between CsSlotInfoslot (1) and the higher valued type must be written to the file. +// The hashes for the missing slots must be filled with 0s. +// +// newCodeDirectory() also needs to know how many slots are present (including +// the 0-filled ones), and the total number of bytes which were written (to +// maintain an offset). It can use maxSlotType and totalBytesWritten for this. +type SpecialSlotHashWriter struct { + totalBytesWritten int // total number of bytes written by the Write method + // slot type with the higher int value. This corresponds to the number of slots which will be written + maxSlotType int + // used to detect inconsistencies in the provided hashes - they must all have the same size + hashSize int + // SpecialSlot map keyed by their type to easily detect which slot types are missing + slots map[int]SpecialSlot +} + +// newSpecialSlotHashWriter creates a new SpecialSlotHashWriter for the slots defined in specialSlots. +func newSpecialSlotHashWriter(specialSlots []SpecialSlot) (*SpecialSlotHashWriter, error) { + w := SpecialSlotHashWriter{} + w.slots = map[int]SpecialSlot{} + + for _, slot := range specialSlots { + switch w.hashSize { + case 0: + w.hashSize = len(slot.HashBytes) + case len(slot.HashBytes): + // w.hashSize was set previously and has the right value, nothing to do + default: + return nil, fmt.Errorf("inconsistent hash size: %d != %d", w.hashSize, len(slot.HashBytes)) + } + + slotType := int(slot.Type) + if slotType > w.maxSlotType { + w.maxSlotType = slotType + } + w.slots[slotType] = slot + } + + log.Debugf("SpecialSlotHashWriter: %d special slots", w.maxSlotType) + + return &w, nil +} + +// Write will write all the special slots hashes to w.buffer. +func (w *SpecialSlotHashWriter) Write(buffer *bytes.Buffer) error { + nullHashBytes := bytes.Repeat([]byte{0}, w.hashSize) + w.totalBytesWritten = 0 + + for i := w.maxSlotType; i > 0; i-- { + log.Debugf("SpecialSlotHashWriter: writing slot %d", i) + hashBytes := nullHashBytes + slot, hasSlot := w.slots[i] + if hasSlot { + hashBytes = slot.HashBytes + } else { + log.Debugf("SpecialSlotHashWriter: slot %d is empty", i) + } + written, err := buffer.Write(hashBytes) + if err != nil { + return fmt.Errorf("unable to write plist hash to code directory: %w", err) + } + w.totalBytesWritten += written + } + + return nil +} + +func newCodeDirectory(id string, hasher hash.Hash, execOffset, execSize uint64, codeSize uint32, hashes [][]byte, flags macho.CdFlag, specialSlots []SpecialSlot) (*macho.CodeDirectory, error) { cdSize := unsafe.Sizeof(macho.BlobHeader{}) + unsafe.Sizeof(macho.CodeDirectoryHeader{}) idOff := int32(cdSize) // 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. - hashOff := idOff + int32(len(id)+1) + int32(len(requirementsHashBytes)) + int32(len(entitlementsHashBytes)) var ht macho.HashType switch hasher.Size() { @@ -80,17 +150,25 @@ func newCodeDirectory(id string, hasher hash.Hash, execOffset, execSize uint64, buff := bytes.Buffer{} // write the identifier - if _, err := buff.Write([]byte(id + "\000")); err != nil { + hashOff := int(idOff) + var ( + written int + err error + ) + if written, err = buff.Write([]byte(id + "\000")); err != nil { return nil, fmt.Errorf("unable to write ID to code directory: %w", err) } + hashOff += written // write hashes - if _, err := buff.Write(requirementsHashBytes); err != nil { - return nil, fmt.Errorf("unable to write requirements hash to code directory: %w", err) + specialSlotHashWriter, err := newSpecialSlotHashWriter(specialSlots) + if err != nil { + return nil, err } - if _, err := buff.Write(entitlementsHashBytes); err != nil { - return nil, fmt.Errorf("unable to write plist hash to code directory: %w", err) + if err := specialSlotHashWriter.Write(&buff); err != nil { + return nil, err } + hashOff += specialSlotHashWriter.totalBytesWritten for idx, hBytes := range hashes { _, err := buff.Write(hBytes) @@ -105,7 +183,7 @@ func newCodeDirectory(id string, hasher hash.Hash, execOffset, execSize uint64, Flags: flags, HashOffset: uint32(hashOff), IdentOffset: uint32(idOff), - NSpecialSlots: uint32(2), // requirements + plist + NSpecialSlots: uint32(specialSlotHashWriter.maxSlotType), NCodeSlots: uint32(len(hashes)), CodeLimit: codeSize, HashSize: uint8(hasher.Size()), diff --git a/quill/sign/code_directory_test.go b/quill/sign/code_directory_test.go index 9ba4a53..88ef548 100644 --- a/quill/sign/code_directory_test.go +++ b/quill/sign/code_directory_test.go @@ -87,7 +87,16 @@ func Test_newCodeDirectoryFromMacho(t *testing.T) { pListBytes, err := hex.DecodeString(tt.pListHash) require.NoError(t, err) - actualCD, err := newCodeDirectoryFromMacho(tt.id, tt.hasher, m, tt.flags, reqBytes, pListBytes) + reqSlot := SpecialSlot{ + Type: macho.CsSlotRequirements, + HashBytes: reqBytes, + } + plistSlot := SpecialSlot{ + Type: macho.CsSlotInfoslot, + HashBytes: pListBytes, + } + + actualCD, err := newCodeDirectoryFromMacho(tt.id, tt.hasher, m, tt.flags, []SpecialSlot{reqSlot, plistSlot}) require.NoError(t, err) // make certain the headers match @@ -184,7 +193,16 @@ func Test_generateCodeDirectory(t *testing.T) { pListBytes, err := hex.DecodeString(tt.pListHash) require.NoError(t, err) - cdBlob, err := generateCodeDirectory(tt.id, tt.hasher, m, tt.flags, reqBytes, pListBytes) + reqSlot := SpecialSlot{ + Type: macho.CsSlotRequirements, + HashBytes: reqBytes, + } + plistSlot := SpecialSlot{ + Type: macho.CsSlotInfoslot, + HashBytes: pListBytes, + } + + cdBlob, err := generateCodeDirectory(tt.id, tt.hasher, m, tt.flags, []SpecialSlot{reqSlot, plistSlot}) require.NoError(t, err) cdBytes, err := cdBlob.Pack() diff --git a/quill/sign/entitlements.go b/quill/sign/entitlements.go new file mode 100644 index 0000000..4eb8d57 --- /dev/null +++ b/quill/sign/entitlements.go @@ -0,0 +1,30 @@ +package sign + +import ( + "fmt" + "hash" + + "github.com/go-restruct/restruct" + + "github.com/anchore/quill/quill/macho" +) + +func generateEntitlements(h hash.Hash, entitlementsXML string) (*SpecialSlot, error) { + if entitlementsXML == "" { + return nil, nil + } + entitlementsBytes := []byte(entitlementsXML) + blob := macho.NewBlob(macho.MagicEmbeddedEntitlements, entitlementsBytes) + blobBytes, err := restruct.Pack(macho.SigningOrder, &blob) + if err != nil { + return nil, fmt.Errorf("unable to encode entitlements blob: %w", err) + } + + // the requirements hash is against the entire blob, not just the payload + h.Write(blobBytes) + if err != nil { + return nil, err + } + + return &SpecialSlot{macho.CsSlotEntitlements, &blob, h.Sum(nil)}, nil +} diff --git a/quill/sign/requirements.go b/quill/sign/requirements.go index 646790d..74d2c56 100644 --- a/quill/sign/requirements.go +++ b/quill/sign/requirements.go @@ -61,7 +61,7 @@ const ( anchorCertIndex = ^uint32(0) // index for anchor (last in chain), equiv to -1 ) -func generateRequirements(id string, h hash.Hash, signingMaterial pki.SigningMaterial) (*macho.Blob, []byte, error) { +func generateRequirements(id string, h hash.Hash, signingMaterial pki.SigningMaterial) (*SpecialSlot, error) { var reqBytes []byte if signingMaterial.Signer == nil { log.Trace("skipping adding designated requirement because no signer was found") @@ -69,12 +69,12 @@ func generateRequirements(id string, h hash.Hash, signingMaterial pki.SigningMat } else { req, err := newRequirements(id, signingMaterial) if err != nil { - return nil, nil, err + return nil, err } reqBytes, err = restruct.Pack(macho.SigningOrder, req) if err != nil { - return nil, nil, fmt.Errorf("unable to encode requirement: %w", err) + return nil, fmt.Errorf("unable to encode requirement: %w", err) } } @@ -82,16 +82,16 @@ func generateRequirements(id string, h hash.Hash, signingMaterial pki.SigningMat blobBytes, err := restruct.Pack(macho.SigningOrder, &blob) if err != nil { - return nil, nil, fmt.Errorf("unable to encode requirements blob: %w", err) + return nil, fmt.Errorf("unable to encode requirements blob: %w", err) } // the requirements hash is against the entire blob, not just the payload _, err = h.Write(blobBytes) if err != nil { - return nil, nil, err + return nil, err } - return &blob, h.Sum(nil), nil + return &SpecialSlot{macho.CsSlotRequirements, &blob, h.Sum(nil)}, nil } func newRequirements(id string, signingMaterial pki.SigningMaterial) (*macho.Requirements, error) { diff --git a/quill/sign/signing_super_blob.go b/quill/sign/signing_super_blob.go index 552f6de..e91e353 100644 --- a/quill/sign/signing_super_blob.go +++ b/quill/sign/signing_super_blob.go @@ -1,7 +1,6 @@ package sign import ( - "bytes" "crypto/sha256" "fmt" @@ -11,7 +10,13 @@ import ( "github.com/anchore/quill/quill/pki" ) -func GenerateSigningSuperBlob(id string, m *macho.File, signingMaterial pki.SigningMaterial, paddingTarget int) (int, []byte, error) { +type SpecialSlot struct { + Type macho.SlotType + Blob *macho.Blob + HashBytes []byte +} + +func GenerateSigningSuperBlob(id string, m *macho.File, signingMaterial pki.SigningMaterial, entitlementsData string, paddingTarget int) (int, []byte, error) { var cdFlags macho.CdFlag if signingMaterial.Signer != nil { // TODO: add options to enable more strict rules (such as macho.Hard) @@ -22,15 +27,25 @@ func GenerateSigningSuperBlob(id string, m *macho.File, signingMaterial pki.Sign cdFlags = macho.Adhoc } - requirementsBlob, requirementsHashBytes, err := generateRequirements(id, sha256.New(), signingMaterial) + specialSlots := []SpecialSlot{} + + entitlements, err := generateEntitlements(sha256.New(), entitlementsData) if err != nil { - return 0, nil, fmt.Errorf("unable to create requirements: %w", err) + return 0, nil, fmt.Errorf("unable to create entitlements: %w", err) + } + if entitlements != nil { + specialSlots = append(specialSlots, *entitlements) } - // TODO: add entitlements, for the meantime, don't include it - entitlementsHashBytes := bytes.Repeat([]byte{0}, sha256.New().Size()) + requirements, err := generateRequirements(id, sha256.New(), signingMaterial) + if err != nil { + return 0, nil, fmt.Errorf("unable to create requirements: %w", err) + } + if requirements != nil { + specialSlots = append(specialSlots, *requirements) + } - cdBlob, err := generateCodeDirectory(id, sha256.New(), m, cdFlags, requirementsHashBytes, entitlementsHashBytes) + cdBlob, err := generateCodeDirectory(id, sha256.New(), m, cdFlags, specialSlots) if err != nil { return 0, nil, fmt.Errorf("unable to create code directory: %w", err) } @@ -41,9 +56,11 @@ func GenerateSigningSuperBlob(id string, m *macho.File, signingMaterial pki.Sign } sb := macho.NewSuperBlob(macho.MagicEmbeddedSignature) - sb.Add(macho.CsSlotCodedirectory, cdBlob) - sb.Add(macho.CsSlotRequirements, requirementsBlob) + for _, slot := range specialSlots { + sb.Add(slot.Type, slot.Blob) + } + sb.Add(macho.CsSlotCmsSignature, cmsBlob) sb.Finalize(paddingTarget)