README

XPI Signing
===========

.. sectnum::
.. contents:: Table of Contents

XPI are zip files that contain Firefox extensions and addons. XPI signing uses
the `JAR Signing`_ format to produce PKCS7 signatures protecting the integrity
of addons.

.. _`JAR Signing`: http://download.java.net/jdk7/archive/b125/docs/technotes/tools/solaris/jarsigner.html

A full description of how addon signing is implemented in Firefox can be found
`on the Mozilla wiki`_. This readme focuses on the autograph implementation.

.. _`on the Mozilla wiki`: https://wiki.mozilla.org/Add-ons/Extension_Signing

Configuration
-------------

The type of this signer is **xpi**.

The XPI signer in Autograph supports four types of addons. A signer is
configured to issue signatures for a given type using the `mode` parameter in
the autograph configuration:

* Regular addons use mode `add-on`
* Regular addons with recommendations use mode `add-on-with-recommendation`
* Mozilla Extensions use mode `extension`
* Mozilla Components (aka. System Addons) use mode `system add-on`
* Hotfixes use mode `hotfix`

Each signer must have a type, a mode and the certificate and private key of
an intermediate CA issued by either the staging or root PKIs of AMO (refer to
internal documentation to issue those, as they require access to private HSMs).

When a signature is requested, autograph will generate a private key
and issue an end-entity certificate specifically for the signature
request. The certificate is signed by the configured intermediate
CA. The private key is thrown away right after the signature is
issued. Note that if one XPI signer is including a recommendations
file the other XPI signers should reserve that file path.

.. code:: yaml

  signers:
    - id: webextensions-rsa
      type: xpi
      mode: add-on
      recommendation:
        path: "recommendation.json"
      certificate: |
          -----BEGIN CERTIFICATE-----
          MIIH0zCCBbugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBvDELMAkGA1UEBhMCVVMx
		  ...
          -----END CERTIFICATE-----
      privatekey: |
          -----BEGIN PRIVATE KEY-----
          MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDHV+bKFLr1p5FR
		  ...
          -----END PRIVATE KEY-----

The signer can also include optional config params for an RSA key
cache and recommendations file:

.. code:: yaml

  signers:
    - id: webextensions-rsa
      type: xpi
      mode: add-on
      recommendation:
        path: "recommendation.json"
        states:
          standard: true
          recommended: true
          partner: true
        relative_start: 0h
        duration: 26298h
      # RSA key gen is slow and CPU intensive, so we can optionally
      # pregenerate and cache keys with a worker pool
      rsacacheconfig:
        numkeys: 25
        numgenerators: 2
        generatorsleepduration: 1m
        fetchtimeout: 100ms
        statssamplerate: 1m
      certificate: |
          -----BEGIN CERTIFICATE-----
          MIIH0zCCBbugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBvDELMAkGA1UEBhMCVVMx
		  ...
          -----END CERTIFICATE-----
      privatekey: |
          -----BEGIN PRIVATE KEY-----
          MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDHV+bKFLr1p5FR
		  ...
          -----END PRIVATE KEY-----

Signature Request
-----------------

Supports the `/sign/data` and `/sign/file` endpoints for data and file signing respectively. `/sign/data` uses the request format:

.. code:: json

	[
		{
			"input": "Y2FyaWJvdW1hdXJpY2UK",
			"options": {
				"id": "myaddon@allizom.org"
			},
			"keyid": "some_xpi_signer"
		}
	]

and `/sign/file` uses the format:

.. code:: json

	[
		{
			"input": "Y2FyaWJvdW1hdXJpY2UK",
			"options": {
				"id": "myaddon@allizom.org",
				"cose_algorithms": [
					"ES256"
				],
				"recommendations": [
					"standard",
					"recommended"
				],
				"pkcs7_digest": "SHA256"
			},
			"keyid": "some_xpi_signer"
		}
	]


Where options includes the following fields:

* `id` is the **required** ID of the addon to sign for both data and
  file signing. It must be decided client side, and is generally a
  string that looks like an email address, but when longer than 64
  characters can be the hexadecimal encoding of a sha256 hash. This
  signer doesn't care about the content of the string, and uses it as
  received when generating the end-entity signing cert.

* `pkcs7_digest` is a **required** string representing a supported
  PKCS7 digest algorithm (`"SHA1"` or `"SHA256"`). Only `/sign/file`
  supports this field.

* `cose_algorithms` is an **optional** array of strings representing
  supported `COSE Algorithms`_ (as of 2018-06-20 one of `"ES256"`,
  `"ES384"`, `"ES512"`, or `"PS256"`) to sign the XPI with in addition
  to the PKCS7 signature. Only `/sign/file` supports this field.

* `recommendations` is an **optional** array of strings representing
  recommendation states to add to the recommendation file for XPI
  signers in `add-on-with-recommendation` mode. Only `/sign/file`
  supports this field.

The `/sign/file` endpoint takes a whole XPI encoded in base64. As
described in `Extension Signing Algorithm`_, it:

* unzips the XPI
* hashes each file to generate the manifest file `manifest.mf`
* then when one or more supported COSE algorithms are in the options `cose_algorithms` field
  * writes the manifest file to `cose.manifest`
  * creates a COSE Sign Message and for each COSE algorithm:
    * generates an end entity cert and key from the signer's intermediate
    * signs the manifest with the end entity key using the COSE algorithm
    * adds the detached signature to the Sign Message
  * writes the CBOR-encoded Sign Message to `cose.sig`
  * hashes `cose.manifest` and `cose.sig` and adds them to the manifest file `manifest.mf`
* hashes the manifest file to generate the signature file `mozilla.sf`
* generates an RSA end entity cert from the signer's intermediate
* uses the generated cert to sign the signature file and create a PKCS7 detached signature `mozilla.rsa` using the algorithm from `pkcs7_digest`
* adds the generated manifest, signature, and detached signature files to the XPI `META-INF/`
* repacks and returns the ZIP/XPI

The `/sign/data` endpoint generates the end entity cert and signs the
signature file. The `input` field must contain the base64 encoding of
a `mozilla.sf` signature file and returns the PKCS7 detached signature
`mozilla.rsa` in the response `signature` field. The caller is then
responsible for repacking the ZIP.

.. _`COSE Algorithms`: https://www.iana.org/assignments/cose/cose.xhtml#table-header-algorithm-parameters
.. _`Extension Signing Algorithm`: https://wiki.mozilla.org/Add-ons/Extension_Signing#Algorithm

Signature Response
------------------

Data Signing
~~~~~~~~~~~~

XPI signatures are binary files encoded using the PKCS7 format and stored in the
file called **mozilla.rsa** in the META-INF folder of XPI archives.

Autograph returns the base64 representation of the `mozilla.rsa` file in its
signature responses. Clients must decode the base64 from the autograph response
and write it to a `mozilla.rsa` file.

.. code:: json

	[
	  {
		"ref": "z4cfx4x6qymxsj9hiqbuqvn7",
		"type": "xpi",
		"signer_id": "webextensions-rsa",
		"public_key": "",
		"signature": "MIIRUQYJKoZIhvcNAQcCoIIRQjCCET4CAQExCTAHBgUr..."
	  }
	]

Note that the **public_key** field is empty in signature responses because PKCS7
files already contain the public certificate of the end-entity that issued the
signature.

File Signing
~~~~~~~~~~~~

Like the data signing except the signed XPI is returned in the
`signed_file` field. Clients must decode the base64 from the autograph
response and write it to a `signed_addon.xpi` file.

.. code:: json

	[
	  {
		"ref": "z4cfx4x6qymxsj9hiqbuqvn7",
		"type": "xpi",
		"signer_id": "webextensions-rsa",
		"public_key": "",
		"signed_file": "MIIRUQYJKoZIhvcNAQcCoIIRQjCCET4CAQExCTAHBgUr..."
	  }
	]

Appendix A: Firefox add-on signature verification
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This directed graphs represents the add-ons signature verification path in Firefox.

.. code::

	graph LR
	  Firefox-->loadManifest
	  loadManifest -->verifySignedState
	  verifySignedState-->OpenSignedAppFile
	  OpenSignedAppFile-->VerifyPK7Signature
	  OpenSignedAppFile-->VerifyCOSESignature
	  OpenSignedAppFile == return zip reader and signing cert ==> verifySignedState

	  subgraph extension_jsm
	  verifySignedState-->verifySignedStateForRoot
	  verifySignedStateForRoot == Get signing cert and add-on ID ==>getSignedStatus
	  getSignedStatus == if add-on ID != cert CN ==> SIGNEDSTATE_BROKEN
	  getSignedStatus == if cert OU is Mozilla Components ==> SIGNEDSTATE_SYSTEM
	  getSignedStatus == if cert OU is Mozilla Extensions==> SIGNEDSTATE_PRIVILEGED
	  getSignedStatus == if signature valid ==> SIGNEDSTATE_SIGNED
	  getSignedStatus == if signature invalid ==> NS_ERROR_SIGNED_JAR_*
	  end

	  subgraph pkcs7
	  VerifyPK7Signature == Extract RSA signature ==> VerifySignature
	  VerifyPK7Signature == Extract hash of SF signature file ==> VerifySignature
	  VerifySignature == Extract Signing Certificate ==> VerifyCertificate
	  VerifyCertificate == Get Trusted Root ==> BuildCertChain
	  BuildCertChain == ERROR_EXPIRED_CERTIFICATE ==> Success
	  Success --> VerifyPK7Signature
	  BuildCertChain == else ==> Error
	  Error --> VerifyPK7Signature
	  end

	  subgraph cose
	  VerifyCOSESignature == Extract Signature ==> verify_cose_signature_ffi
	  VerifyCOSESignature == Extract SF Signature file ==> verify_cose_signature_ffi
	  VerifyCOSESignature == Get Trusted Root ==> verify_cose_signature_ffi
	  end

	  subgraph verify_manifest
	  VerifyCOSESignature == List files to ignore==> VerifyAppManifest
	  VerifyPK7Signature == List files to ignore==> VerifyAppManifest
	  VerifyAppManifest--> ParseMF
	  ParseMF == for each manifest entry ==> VerifyEntryContentDigest
	  VerifyEntryContentDigest--> VerifyStreamContentDigest
	  VerifyStreamContentDigest == entry digest matches manifest ==> NS_OK
	  VerifyStreamContentDigest == else ==> NS_ERROR_SIGNED_JAR_something
	  end

Documentation

Index

Constants

View Source
const (
	// Type of this signer is "xpi"
	Type = "xpi"

	// ModeAddOn represents a signer that issues signatures for
	// regular firefox add-ons and web extensions developed by anyone
	ModeAddOn = "add-on"

	// ModeAddOnWithRecommendation represents a signer that issues
	// signatures for regular firefox add-ons and web extensions
	// developed by anyone including a recommendation file
	ModeAddOnWithRecommendation = "add-on-with-recommendation"

	// ModeExtension represents a signer that issues signatures for
	// internal extensions developed by Mozilla
	ModeExtension = "extension"

	// ModeSystemAddOn represents a signer that issues signatures for
	// System Add-Ons developed by Mozilla
	ModeSystemAddOn = "system add-on"

	// ModeHotFix represents a signer that issues signatures for
	// Firefox HotFixes
	ModeHotFix = "hotfix"
)

Variables

This section is empty.

Functions

func VerifySignedFile

func VerifySignedFile(signedFile signer.SignedFile, truststore *x509.CertPool, opts Options) error

VerifySignedFile checks the XPI's PKCS7 signature and COSE signatures if present

Types

type Metafile

type Metafile struct {
	Name string
	Body []byte
}

Metafile is a file to pack into a JAR at .Name with contents .Body

func (*Metafile) IsNameValid

func (m *Metafile) IsNameValid() bool

IsNameValid checks whether a Metafile.Name is non-nil and begins with "META-INF/" functions taking Metafile args should validate names before reading or writing them to JARs

type Options

type Options struct {
	// ID is the add-on ID which is stored in the end-entity subject CN
	ID string `json:"id"`

	// COSEAlgorithms is an optional list of strings referring to IANA algorithms to use for COSE signatures
	COSEAlgorithms []string `json:"cose_algorithms"`

	// PKCS7Digest is a string required for /sign/file referring to algorithm to use for the PKCS7 signature digest
	PKCS7Digest string `json:"pkcs7_digest"`

	// Recommendations is an optional list of strings referring to
	// recommended states to add to the recommendations file
	// for signers in ModeAddOnWithRecommendation
	Recommendations []string `json:"recommendations"`
}

Options contains specific parameters used to sign XPIs

func GetOptions

func GetOptions(input interface{}) (options Options, err error)

GetOptions takes a input interface and reflects it into a struct of options

func (*Options) Algorithms

func (o *Options) Algorithms() (algs []*cose.Algorithm, err error)

Algorithms validates and returns COSE algorithms

func (*Options) CN

func (o *Options) CN(s *XPISigner) (cn string, err error)

CN returns the common name

func (*Options) PK7Digest

func (o *Options) PK7Digest() (asn1.ObjectIdentifier, error)

PK7Digest validates and return an ASN OID for a PKCS7 digest algorithm or an error

func (*Options) RecommendationStates

func (o *Options) RecommendationStates(allowedRecommendationStates map[string]bool) (states []string, err error)

RecommendationStates validates and returns allowed recommendation states algorithms from the request

type Recommendation

type Recommendation struct {
	// AddOnID is the ID of the extension this recommendation is
	// for. Must match the ID in the extension’s manifest.json
	AddOnID string `json:"addon_id"`

	// States is a list of strings for each state of an addon that
	// firefox understands
	States []string `json:"states"`

	// Validity is a pair of timestamps to expire a recommendation
	// after an appropriate amount of time, since the
	// recommendation is for a given version of the addon and it
	// will need to be reissued for new versions.
	Validity map[string]time.Time `json:"validity"`

	// SchemaVersion is a uint to allow gradual upgrades of the
	// recommendation file
	SchemaVersion int `json:"schema_version"`
}

Recommendation represents an Addon Recommendation file

func Recommend

func Recommend(addonID string, states []string, notBefore, notAfter time.Time) *Recommendation

Recommend returns a Recommendation for param addonID with param states

func UnmarshalRecommendation

func UnmarshalRecommendation(input []byte) (r *Recommendation, err error)

UnmarshalRecommendation parses a recommendation file from JSON

func (*Recommendation) Marshal

func (r *Recommendation) Marshal() ([]byte, error)

Marshal serializes a Recommendation to JSON

func (*Recommendation) Validate

func (r *Recommendation) Validate(allowedRecommendationStates map[string]bool) error

Validate checks a Recommendation's validity fields and state is in the allowed states

type Signature

type Signature struct {
	Data     []byte
	Finished bool
	// contains filtered or unexported fields
}

Signature is a detached PKCS7 signature or COSE SignMessage

func Unmarshal

func Unmarshal(signature string, content []byte) (sig *Signature, err error)

Unmarshal parses a PKCS7 struct from the base64 representation of a PKCS7 detached and content of the signed data or it parses a COSE Sign Message struct from the base64 representation of a CBOR encoded Sign Message

func (*Signature) Marshal

func (sig *Signature) Marshal() (string, error)

Marshal returns the base64 representation of a detached PKCS7 signature or COSE Sign Message

func (*Signature) String

func (sig *Signature) String() string

String returns a PEM encoded PKCS7 block

func (*Signature) VerifyWithChain

func (sig *Signature) VerifyWithChain(truststore *x509.CertPool) error

VerifyWithChain verifies an xpi signature using the provided truststore

type XPISigner

type XPISigner struct {
	signer.Configuration

	// OU is the organizational unit of the end-entity certificate
	// generated for each operation performed by this signer
	OU string

	// EndEntityCN is the subject CN of the end-entity certificate generated
	// for each operation performed by this signer. Most of the time
	// the ID will be left blank and provided by the requester of the
	// signature, but for hotfix signers, it is set to a specific value.
	EndEntityCN string
	// contains filtered or unexported fields
}

An XPISigner is configured to issue detached PKCS7 and COSE signatures for Firefox Add-ons of various types.

func New

func New(conf signer.Configuration, stats *signer.StatsClient) (s *XPISigner, err error)

New initializes an XPI signer using a configuration

func (*XPISigner) Config

func (s *XPISigner) Config() signer.Configuration

Config returns the configuration of the current signer

func (*XPISigner) GetDefaultOptions

func (s *XPISigner) GetDefaultOptions() interface{}

GetDefaultOptions returns default options of the signer

func (*XPISigner) MakeEndEntity

func (s *XPISigner) MakeEndEntity(cn string, coseAlg *cose.Algorithm) (eeCert *x509.Certificate, eeKey crypto.PrivateKey, err error)

MakeEndEntity generates a private key and certificate ready to sign a given XPI.

The subject CN of the certificate is taken from the `cn` string argument.

The key type is identical to the key type of the signer that issues the certificate when the optional `coseAlg` argument is nil. For example, if the signer uses an RSA 2048 key, so will the end-entity. When `coseAlg` is not nil, a key type of the COSE algorithm is generated.

The signature expiration date is copied over from the issuer.

The signed x509 certificate and private key are returned.

func (*XPISigner) ReadAndVerifyRecommendationFile

func (s *XPISigner) ReadAndVerifyRecommendationFile(signedXPI []byte) (recFileBytes []byte, err error)

ReadAndVerifyRecommendationFile reads and verifies the recommendation file from an XPI for a signer's config and returns the file bytes and an error when verification fails

func (*XPISigner) SignData

func (s *XPISigner) SignData(sigfile []byte, options interface{}) (signer.Signature, error)

SignData takes an input signature file and returns a PKCS7 or COSE detached signature

func (*XPISigner) SignFile

func (s *XPISigner) SignFile(input []byte, options interface{}) (signedFile signer.SignedFile, err error)

SignFile takes an unsigned zipped XPI file and returns a signed XPI file