From 511497ed0edf6aa408897f699dc799951d487f59 Mon Sep 17 00:00:00 2001 From: Christophe Fergeau Date: Fri, 1 Mar 2024 10:03:20 +0100 Subject: [PATCH] sign: Add support for entitlements With the abstraction work done in the previous commit, adding support for entitlements is now fairly straightforward, just need to build the entitlements blob and hashes using user-provided XML data. This fixes https://github.com/anchore/quill/issues/4 Signed-off-by: Christophe Fergeau --- cmd/quill/cli/commands/sign.go | 1 + cmd/quill/cli/options/signing.go | 9 ++++++++- quill/sign.go | 20 ++++++++++++++++++-- quill/sign/entitlements.go | 30 ++++++++++++++++++++++++++++++ quill/sign/signing_super_blob.go | 12 +++++++++--- 5 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 quill/sign/entitlements.go 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)