diff --git a/cmd/quill/cli/commands/sign.go b/cmd/quill/cli/commands/sign.go index cfbad6fa..b143eb42 100644 --- a/cmd/quill/cli/commands/sign.go +++ b/cmd/quill/cli/commands/sign.go @@ -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/options/signing.go b/cmd/quill/cli/options/signing.go index db512711..49972b19 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/sign.go b/quill/sign.go index 3d4cbb3d..a034131a 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/entitlements.go b/quill/sign/entitlements.go new file mode 100644 index 00000000..4eb8d57b --- /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/signing_super_blob.go b/quill/sign/signing_super_blob.go index f138d02d..e91e3537 100644 --- a/quill/sign/signing_super_blob.go +++ b/quill/sign/signing_super_blob.go @@ -16,7 +16,7 @@ type SpecialSlot struct { HashBytes []byte } -func GenerateSigningSuperBlob(id string, m *macho.File, signingMaterial pki.SigningMaterial, paddingTarget int) (int, []byte, error) { +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) @@ -29,6 +29,14 @@ func GenerateSigningSuperBlob(id string, m *macho.File, signingMaterial pki.Sign specialSlots := []SpecialSlot{} + entitlements, err := generateEntitlements(sha256.New(), entitlementsData) + if err != nil { + return 0, nil, fmt.Errorf("unable to create entitlements: %w", err) + } + if entitlements != nil { + specialSlots = append(specialSlots, *entitlements) + } + requirements, err := generateRequirements(id, sha256.New(), signingMaterial) if err != nil { return 0, nil, fmt.Errorf("unable to create requirements: %w", err) @@ -37,8 +45,6 @@ func GenerateSigningSuperBlob(id string, m *macho.File, signingMaterial pki.Sign specialSlots = append(specialSlots, *requirements) } - // TODO: add entitlements, for the meantime, don't include it - cdBlob, err := generateCodeDirectory(id, sha256.New(), m, cdFlags, specialSlots) if err != nil { return 0, nil, fmt.Errorf("unable to create code directory: %w", err)