Signing service

Some packages need to be signed for the benefit of UEFI Secure Boot and other similar firmware arrangements that ensure the authenticity of code executed by the firmware. See SecureBoot/Discussion for an outline of the current implementation in Debian.

Requirements

We must support Debian’s system of template packages that specify what is to be signed and provide a structure for uploading the results back to the archive. (It is not necessary for the package manipulation itself to be done on the same system that has access to the necessary private keys.)

It must be possible to restrict the use of signing keys to particular archives, suites, and/or source packages.

For parity with Debian’s existing system, the signing service must support at least UEFI and kmod signatures. It should be straightforward to extend it with additional signing methods: in particular, GPG.

It must be possible to sign using a YubiKey. It may be acceptable to support private key material stored on the signing system (this is likely to be convenient for development, and may be reasonable for lower-security keys), though if so they must at least be encrypted at rest.

All requests to the signing service must be encrypted and authenticated.

The signing service must retain an audit log of all signatures, and must provide a way for debusine to retrieve and present information from that log.

Prior art

  • code-signing is Debian’s current implementation. Most of it is in a single secure-boot-code-sign.py script, with a small attached database. It operates on a polling model: every so often it polls a configured URL for each archive that contains a GPG-signed list of requests for it. It downloads and unpacks template binary packages from an archive, downloads and unpacks binary packages containing objects to sign, constructs a new source package, and submits it back to the archive using dput. It has policy configuration for which packages should be signed using which keys.

  • lp-signing is used in Ubuntu. It is structured as a web service with an attached database, taking NaCl-boxed HTTP requests to generate keys, sign data, or inject keys. It does not interact with source or binary packages directly; that is left to the client of that service. The policy for which packages should be signed using which keys is also largely left to the client, although only authorized clients may use any given key. (Ubuntu uses a different and incompatible mechanism for indicating that packages should be signed.)

code-signing’s polling interface would be cumbersome to use in the context of a debusine task, and lp-signing’s interface is largely friendlier here, although it does make the mistake of using synchronous HTTP requests; signing can be quite slow and so timeouts are difficult to manage.

code-signing unpacks template binary packages and binary packages containing to-be-signed objects using dpkg -x, and builds source packages containing signed output using dpkg-source -b. By contrast, the design of lp-signing is careful to avoid needing to manipulate packages itself, which seems important for a security-critical service: while dpkg-source -x is probably the weakest point in dpkg’s security, having had at least 11 CVEs all by itself from 2010 to 2022, even dpkg -x has had problems in the past (e.g. CVE-2015-0860) and running dpkg-source -b on an untrusted directory tree also seems unwise.

lp-signing uses JSON as its message serialization format. This is simple, but inefficient since the messages often contain large binary blobs to be signed (e.g. kernel images), which thus have to be base64-encoded. A binary serialization format would be better here.

Signing workflows

Handling Debian’s template package system requires three basic steps: extracting input binary packages, signing files, and building the output source package. To avoid risk from vulnerabilities such as those listed above, unpacking binary packages and building source packages must always be run on an external worker within some kind of container. The simplest approach would be to have a task for each step and link them together using a workflow, although an optimization is possible here: the Sbuild task can do the initial extraction step itself, saving a considerable amount of overhead.

We define three new tasks for this (GenerateKey, Sign, and AssembleSignedSource) and three new artifact categories to mediate these tasks (Category debusine:signing-key, Category debusine:signing-input, and Category debusine:signing-output).

Signing key management

We define a debian:suite-signing-keys collection category, allowing signing keys to be associated with suites and optionally with specific source package names in that suite.

debian:suite collections gain a reference to a debian:suite-signing-keys collection suitable for that suite.

There will be API and UI actions to generate new signing keys for a package in a suite. It is not possible to set the signing key for a package to a particular known fingerprint via the API or the UI; doing that requires administrative access to the database.

(We might consider having additional controls on the signing service for the use of particular high-value keys. However, in general, debusine can build things for any key controlled by its signing service; the protections that the signing service provides are (a) the inability of debusine to extract plaintext of private keys, and (b) an audit log that a compromised debusine server cannot tamper with.)

Backend overview

There is a debusine.signing application, which is a separate Django application under the debusine project with its database models routed to a separate database, such that it can run on a separate system from the debusine server and workers. It uses Django primarily for its database facilities: there will initially be no need for it to have its own server component, although it may eventually be useful to add one for things like reading audit logs.

This application has a worker, reusing most of the existing debusine.worker code. It sends metadata to the server indicating that it has a new worker type (Signing), and it requires HTTPS connectivity to the server. debusine-admin list_workers gains an extra field for the worker type, and debusine-admin manage_worker requires a special option to manage signing workers, to avoid enabling them by accident.

There is a new Signing task type; tasks of this type are scheduled similarly to worker tasks, and are required to use the public API to interact with artifacts in the same way that worker tasks do, but they only execute on signing workers. Signing workers do not take tasks of any other type.

Todo

Specify how to configure keys to be used with a YubiKey.

Each successful generate and sign operation adds a row to an append-only audit log table.

Database models

Each key has a row with the following fields:

  • purpose: the purpose of this key (e.g. uefi for UEFI Secure Boot) different key purposes typically require different tools to generate them or sign data using them

  • fingerprint: the key fingerprint; keys are unique by purpose and fingerprint

  • private_key: a protected representation of the private key

  • public_key: the public key, as binary data

  • created_at, updated_at: timestamps for creation and update

Key protection

Private key material is never stored in the clear. If it is stored directly in the database, it is encrypted at rest using a NaCl SealedBox, with a configured key. The encrypted form includes both the public key and the ciphertext to allow for key rotation.

Alternatively, private keys may be stored as a handle referring to an attached hardware security module (HSM). The details required are implementation-dependent; for example, they may include a PKCS#11 URI.

These details are stored as a JSON object using a discriminated union to allow for easy extension.

HSM key availability

If keys are stored in a hardware security module such as a YubiKey, then they may not be available to all signing workers. A worker can add the list of such keys it supports to its dynamic metadata, and then the can_run_on method of the relevant tasks can check that metadata to avoid dispatching requests to workers that do not have access to the relevant keys.

Todo

Add more precise details of how this is recorded.