diff mbox series

[v2] Document shared state signing

Message ID 20260421-sstate-signing-v2-1-7b572121f2fd@bootlin.com
State Under Review
Headers show
Series [v2] Document shared state signing | expand

Commit Message

Antonin Godard April 21, 2026, 7:19 a.m. UTC
Document the shared state signing feature. Add a new document in the
Security manual, and definitions for the variables involved in this
process in the variable glossary.

[YOCTO #15217]

Signed-off-by: Antonin Godard <antonin.godard@bootlin.com>
---
Changes in v2:
- Fix typos reported by Ulrich (thanks!)
- Link to v1: https://patch.msgid.link/20260417-sstate-signing-v1-1-5df11613249e@bootlin.com
---
 documentation/overview-manual/concepts.rst       |   6 +
 documentation/ref-manual/variables.rst           |  50 +++++
 documentation/security-manual/index.rst          |   1 +
 documentation/security-manual/sstate-signing.rst | 272 +++++++++++++++++++++++
 4 files changed, 329 insertions(+)


---
base-commit: 2c12ec7bf29aedeacf82970a9d2eb262fde4670e
change-id: 20260417-sstate-signing-f96c75ce1917

Comments

Quentin Schulz April 24, 2026, 3:18 p.m. UTC | #1
Hi Antonin,

On 4/21/26 9:19 AM, Antonin Godard via lists.yoctoproject.org wrote:
> Document the shared state signing feature. Add a new document in the
> Security manual, and definitions for the variables involved in this
> process in the variable glossary.
> 
> [YOCTO #15217]
> 
> Signed-off-by: Antonin Godard <antonin.godard@bootlin.com>
> ---
> Changes in v2:
> - Fix typos reported by Ulrich (thanks!)
> - Link to v1: https://patch.msgid.link/20260417-sstate-signing-v1-1-5df11613249e@bootlin.com
> ---
>   documentation/overview-manual/concepts.rst       |   6 +
>   documentation/ref-manual/variables.rst           |  50 +++++
>   documentation/security-manual/index.rst          |   1 +
>   documentation/security-manual/sstate-signing.rst | 272 +++++++++++++++++++++++
>   4 files changed, 329 insertions(+)
> 
> diff --git a/documentation/overview-manual/concepts.rst b/documentation/overview-manual/concepts.rst
> index ab723d7c3..f0b336226 100644
> --- a/documentation/overview-manual/concepts.rst
> +++ b/documentation/overview-manual/concepts.rst
> @@ -1240,6 +1240,12 @@ variable is the function that determines whether a given dependency
>   needs to be followed, and whether for any given relationship the
>   function needs to be passed. The function returns a True or False value.
>   
> +.. note::
> +
> +   Is is possible to sign these artifacts with :wikipedia:`GPG
> +   <GNU_Privacy_Guard>`. See :doc:`/security-manual/sstate-signing` in the Yocto
> +   Project Security Manual for more information.
> +
>   Images
>   ------
>   
> diff --git a/documentation/ref-manual/variables.rst b/documentation/ref-manual/variables.rst
> index 317b75913..90d7fc924 100644
> --- a/documentation/ref-manual/variables.rst
> +++ b/documentation/ref-manual/variables.rst
> @@ -10097,6 +10097,25 @@ system and gives an overview of their function and contents.
>   
>         For details on the process, see the :ref:`ref-classes-staging` class.
>   
> +   :term:`SSTATE_SIG_KEY`
> +      When signing :ref:`shared state <overview-manual/concepts:setscene tasks
> +      and shared state>` artifacts (when :term:`SSTATE_VERIFY_SIG` is set to
> +      "1"), the :term:`SSTATE_SIG_KEY` variable is the :wikipedia:`GPG
> +      <GNU_Privacy_Guard>` key identifier used to sign them (with the private
> +      key).
> +
> +      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
> +      Manual for more information.
> +
> +   :term:`SSTATE_SIG_PASSPHRASE`
> +      When signing :ref:`shared state <overview-manual/concepts:setscene tasks
> +      and shared state>` artifacts (when :term:`SSTATE_VERIFY_SIG` is set to
> +      "1"), the :term:`SSTATE_SIG_PASSPHRASE` variable is the passphrase used to
> +      protect the private key signing the artifacts.
> +
> +      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
> +      Manual for more information.
> +
>      :term:`SSTATE_SKIP_CREATION`
>         The :term:`SSTATE_SKIP_CREATION` variable can be used to skip the
>         creation of :ref:`shared state <overview-manual/concepts:shared state cache>`
> @@ -10117,6 +10136,37 @@ system and gives an overview of their function and contents.
>   
>            SSTATE_SKIP_CREATION = "1"
>   
> +   :term:`SSTATE_VALID_SIGS`
> +      When verifying :ref:`shared state <overview-manual/concepts:setscene tasks
> +      and shared state>` artifacts (when :term:`SSTATE_VERIFY_SIG` is set to
> +      "1"), the :term:`SSTATE_VALID_SIGS` variable is a space-separated list of
> +      :wikipedia:`GPG <GNU_Privacy_Guard>` key identifiers to use to verify their
> +      signature.
> +
> +      It must contain the short form identifier of the key pair. For example,
> +      when running the ``gpg --list-keys`` command:
> +
> +      .. parsed-literal::
> +

You could use

.. code-block:: console

    $ gpg --list-keys
    pub....

to avoid having to use a literal include.

> +         pub   ed25519 2026-04-17 [SC]
> +               \4049A47E3AAA99D0250966DC\ **5B97632FA7F4E942**
> +         uid           [ultimate] Antonin Godard (SState Signing) <antonin.godard\@bootlin.com>
> +         sub   cv25519 2026-04-17 [E]
> +
> +      The short form equals the last 16 characters of the identier. In the above

s/identier/identifier/

> +      example: ``5B97632FA7F4E942``.
> +
> +      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
> +      Manual for more information.
> +
> +   :term:`SSTATE_VERIFY_SIG`
> +      The :term:`SSTATE_VERIFY_SIG` variable controls whether to enable or
> +      disable the :ref:`shared state <overview-manual/concepts:setscene tasks
> +      and shared state>` artifacts signing feature.
> +
> +      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
> +      Manual for more information.
> +
>      :term:`STAGING_BASE_LIBDIR_NATIVE`
>         Specifies the path to the ``/lib`` subdirectory of the sysroot
>         directory for the build host.
> diff --git a/documentation/security-manual/index.rst b/documentation/security-manual/index.rst
> index 3453940f5..a767cd9c6 100644
> --- a/documentation/security-manual/index.rst
> +++ b/documentation/security-manual/index.rst
> @@ -14,6 +14,7 @@ Yocto Project Security Manual
>      securing-images
>      vulnerabilities
>      read-only-rootfs
> +   sstate-signing
>   
>   .. include:: /boilerplate.rst
>   
> diff --git a/documentation/security-manual/sstate-signing.rst b/documentation/security-manual/sstate-signing.rst
> new file mode 100644
> index 000000000..54ae92d50
> --- /dev/null
> +++ b/documentation/security-manual/sstate-signing.rst
> @@ -0,0 +1,272 @@
> +.. SPDX-License-Identifier: CC-BY-SA-2.0-UK
> +
> +Shared State Signing
> +********************
> +
> +The :term:`OpenEmbedded Build System` build system has a built-in mechanism
> +allowing to save execution time by re-using pre-built artifacts: the
> +:ref:`shared state cache (sstate cache) <overview-manual/concepts:shared state
> +cache>`. These artifacts are stored in a directory (:term:`SSTATE_DIR`) and are
> +not signed by default.
> +
> +This document goes through the steps to enable shared state signing.
> +This feature is fully dependent on :wikipedia:`GPG <GNU_Privacy_Guard>`, meaning
> +examples shown in this document will use the ``gpg`` command-line tool.
> +
> +Host Requirements
> +=================
> +
> +As :wikipedia:`GPG <GNU_Privacy_Guard>` is not part of the default :ref:`host
> +requirements <ref-manual/system-requirements:Required Packages for the Build
> +Host>`, you will need to install it on your host.
> +
> +For example, Debian based distributions provide it with the `gpg` package name.

Double tick around gpg maybe?

> +Install it as follows:
> +
> +.. code-block:: console
> +
> +   $ sudo apt install gpg
> +

apt-get

:D

> +Verify that your installation is successful by showing the version of GPG:
> +
> +.. code-block:: console
> +
> +   $ gpg --version
> +

What exactly does this depend on? Only the gpg binary or also libraries? 
It'd be nice to know because maybe other distros have different packages 
and need more/less. It seems that Debian's package installs the binary 
and depends on libgpg-error0 library.

Can be figured out later but it'd be nice.

> +Generating A Public And Private Key Pair With GPG
> +=================================================
> +
> +.. note::
> +
> +   This step is optional if you already have a pair of public and private keys.
> +
> +We need a pair of public and private keys for two independent tasks:
> +
> +-  Signing our sstate artifacts that the :term:`OpenEmbedded Build System`
> +   generates with our **private key**.
> +
> +-  Verifying our sstate artifacts with our **public key** when re-using them.
> +

You alternate between "we/us/our" and "you" and the use of the 
imperative mood ("Do this") throughout the document, it's a bit 
confusing. I think we've mostly use the second person and imperative 
mood so far, so maybe stick to that?

You also alternate between "sstate" and "shared state".

> +.. note::
> +
> +   For more information on public key cryptography, see
> +   :wikipedia:`Public-key_cryptography`.
> +
> +With the ``gpg`` command-line tool, generate a new pair of public and private
> +keys:
> +
> +.. code-block:: console
> +
> +   $ gpg --full-generate-key
> +
> +This will guide you through the steps of creating the key pair.
> +

s/This/It/

I initially was being confused why there was no step after "This will 
guide you" that would actually guide me through the steps :)

> +Once done, you should be able to list your new key with the following command:
> +
> +.. code-block:: console
> +
> +   $ gpg --list-keys
> +   pub   ed25519 2026-04-17 [SC]
> +         4049A47E3AAA99D0250966DC5B97632FA7F4E942
> +   uid           [ultimate] Antonin Godard (SState Signing) <antonin.godard@bootlin.com>
> +   sub   cv25519 2026-04-17 [E]
> +
> +In the above example, take note of the
> +``4049A47E3AAA99D0250966DC5B97632FA7F4E942`` key identifier. This will be used
> +to configure our build.
> +
> +Configuring Our Build System To Sign Sstate Artifacts
> +=====================================================
> +
> +Shared State Location
> +---------------------
> +
> +Our build system needs to be configured to sign new sstate artifacts when they
> +are generated. The generation of new artifacts is done once a task has finished
> +being executed.
> +
> +For the following sections let's assume that our build system has the shared
> +state directory location (:term:`SSTATE_DIR`) defined as follows::
> +
> +   SSTATE_DIR = "${TOPDIR}/sstate-cache"
> +
> +Assuming this directory and our temporary directory (:term:`TMPDIR`) are empty,
> +as an example throughout this document, let's run the ``create_recipe_spdx``

What part of the sentence does "as an example throughout this document" 
apply to? The wording is awkward.

> +task of the ``gettext-minimal-native`` recipe:
> +
> +.. code-block:: console
> +
> +   $ bitbake gettext-minimal-native -c create_recipe_spdx
> +
> +After execution, our shared state directory should be populated with new files:
> +
> +.. code-block:: console
> +
> +   $ find $BUILDDIR/sstate-cache/ -name "*gettext-minimal-native*create_recipe_spdx*"
> +   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst
> +   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst.siginfo
> +
> +These are the default shared state artifacts generated by the
> +:term:`OpenEmbedded Build System`. They are not signed with GPG by default, so
> +let's see how to add signing of these artifacts.
> +
> +.. note::
> +
> +   The `siginfo` file is not related to GPG signing.
> +

But the first file also not since we haven't enabled signing yet, what 
did you want to say here?

> +Enabling Shared State Signing
> +-----------------------------
> +
> +Create a new :term:`configuration file` on your host **in a safe location** and

What makes a location safe?

I would suggest to put it somewhere in $HOME/.config/ maybe?

Since it contains a secret, we should probably also recommend to do

chmod o-rwx <file>

no (and possibly chown $USER:$USER <file> ?)

> +add the two following statements::
> +
> +   SSTATE_VERIFY_SIG = "1"
> +   SSTATE_SIG_KEY = "80613045069236143C2EE1A3C8855DA51F042B42"
> +   SSTATE_SIG_PASSPHRASE = "3lvJGHo8HZIcaufWFRezFIYRFDMSDmmkxOiwQ67PeM8IZre90I"
> +

Those (the key and the passphrase) don't match anything in this file, 
it'd be best to reuse what you've highlighted in the previous section.

For the passphrase, maybe mention to "for example use that passphrase" 
when generating the key (and adding a big fat warning to not actually 
use that passphrase), so we know this here refers to that there. Just a 
suggestion, maybe it's easier to not give an example than having to 
remind people not to reuse it.

> +It is advised to put these statements in a separate file as those contain
> +secrets and should not be shared. For this example, let's assume this file is
> +``conf/sstate-sig-key.conf``.
> +
> +These statements define:
> +
> +-  :term:`SSTATE_VERIFY_SIG`: setting this variable to "1" enables the shared
> +   state signing feature.
> +
> +-  :term:`SSTATE_SIG_KEY`: the GPG key identifier for signing shared state
> +   artifacts with the private key.
> +
> +   In our example, this corresponds to the identifier printed with the ``gpg
> +   --list-keys`` command :ref:`above <security-manual/sstate-signing:Generating
> +   A Public And Private Key Pair With GPG>`.
> +
> +-  :term:`SSTATE_SIG_PASSPHRASE`: the passphrase used to protect your private
> +   key when creating the key, chosen when creating the key pair.
> +

Am I the only bothered this is an option? Is there no way to use a 
keyring or the gpg-agent instead?

> +Let's test our configuration:
> +
> +#. Continuing with our ``gettext-minimal-native`` example, let's first clean the
> +   existing shared state artifacts:
> +
> +   .. code-block:: console
> +
> +      $ bitbake gettext-minimal-native -c cleansstate
> +

Why do we need to clean the cache (I'm assuming because you want to make 
sure the artifacts you sign are the one you just generated and not 
malicious ones, but I think it'd be worth explaining this is not a 
limitation but a design decision (IFF I really guessed that right)).

> +#. Run the ``create_recipe_spdx`` task for ``gettext-minimal-native``, but this
> +   time pass our new ``sstate-sig-key.conf`` file to :term:`BitBake`:
> +
> +   .. code-block:: console
> +
> +      $ bitbake -R conf/sstate-sig-key.conf gettext-minimal-native -c create_recipe_spdx
> +
> +List the files in the shared state directory again:
> +
> +.. code-block:: console
> +
> +   $ find sstate-cache/ -name "*gettext-minimal-native*create_recipe_spdx*"
> +   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst
> +   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst.sig
> +   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst.siginfo
> +
> +A new ``.sig`` file was created: this means our artifact was successfully
> +signed, and the signature is stored in a separate ``.sig`` file.
> +
> +Verifying Signed Shared State Artifacts
> +=======================================
> +
> +Now that we have set up our build to sign shared state artifacts, let's see how
> +we can verify them with the public key counterpart of the private key.
> +
> +.. note::
> +
> +   Signature of shared state and its verification can happen on two different
> +   hosts, meaning one host can be in charge of the signature while another only
> +   verifies the artifacts. This is preferred as the private key should not be
> +   shared spread on multiple hosts.
> +

s/spread on/between/

> +From a :term:`configuration file` such as the :ref:`site configuration file
> +<structure-build-conf-site.conf>`, include the following statements::
> +
> +   SSTATE_VERIFY_SIG = "1"
> +   SSTATE_VALID_SIGS = "5B97632FA7F4E942"
> +
> +These statements define:
> +
> +-  :term:`SSTATE_VERIFY_SIG`: setting this variable to "1" enables the shared
> +   state signing feature.
> +
> +-  :term:`SSTATE_VALID_SIGS`: a space-separated list of key identifiers for
> +   which we can accept shared state artifacts.
> +
> +   This means that shared state will be reused **only if it was signed with the
> +   private key corresponding to key identifier**.
> +
> +   You'll notice the short-form of the key identifier here, which are the last 16
> +   characters of the long-form key identifier shown with ``gpg --list-keys``:
> +
> +   .. parsed-literal::
> +
> +      pub   ed25519 2026-04-17 [SC]
> +            \4049A47E3AAA99D0250966DC\ **5B97632FA7F4E942**
> +      uid           [ultimate] Antonin Godard (SState Signing) <antonin.godard\@bootlin.com>
> +      sub   cv25519 2026-04-17 [E]
> +

See comment in the first occurrence of this parsed-literal.

> +Let's verify that signature verification works:
> +
> +#. First, remove temporary outputs (:term:`TMPDIR`) from the previous builds, to
> +   make the :term:`OpenEmbedded Build System` use the shared state:

make <OE> rebuild everything using the shared state

?

> +
> +   .. code-block:: console
> +
> +      $ rm -r $BUILDDIR/tmp/
> +

You never tell the user what this variable should be set to. If they 
forget, they'll wreak havoc on their host system deleting /tmp/ recursively.

> +#. Then, run the task again:
> +
> +   .. code-block:: console
> +
> +      $ bitbake gettext-minimal-native -c create_recipe_spdx
> +
> +#. To make sure the shared state artifact was successfully used, look for the
> +   :ref:`setscene <overview-manual/concepts:setscene tasks and shared state>` task
> +   for ``create_recipe_spdx`` in the latest log file from :term:`BitBake`:
> +
> +   .. code-block:: console
> +
> +      $ grep create_recipe_spdx_setscene tmp/log/cooker/<machine>/console-latest.log
> +      NOTE: Running setscene task 1 of 1 (../layers/openembedded-core/meta/recipes-core/gettext/gettext-minimal-native_1.0.bb:do_create_recipe_spdx_setscene)
> +      NOTE: recipe gettext-minimal-native-1.0-r0: task do_create_recipe_spdx_setscene: Started
> +      NOTE: recipe gettext-minimal-native-1.0-r0: task do_create_recipe_spdx_setscene: Succeeded
> +
> +   Our shared state was successfully verified and used!

Is there any indication in the logs that verification was actually 
performed?

> +
> +.. note::
> +
> +   To make sure shared state verification is working, you can set a "fake"
> +   public key identifier in :term:`SSTATE_VALID_SIGS`::
> +
> +      SSTATE_VALID_SIGS = "CAFECAFECAFECAFE"
> +
> +   Remove the temporary outputs again:
> +
> +   .. code-block:: console
> +
> +      $ rm -r $BUILDDIR/tmp/
> +
> +   Now, try executing the task:
> +
> +   .. code-block:: console
> +
> +      $ bitbake gettext-minimal-native -c create_recipe_spdx
> +
> +   You should have warnings from :term:`BitBake`:
> +

Is this output in the console or in the log file you mentioned earlier?

> +   .. code-block:: text
> +
> +      WARNING: gettext-minimal-native-1.0-r0 do_create_recipe_spdx_setscene: No accepted signatures found. Good signatures found: 5B97632FA7F4E942.
> +      WARNING: gettext-minimal-native-1.0-r0 do_create_recipe_spdx_setscene: Cannot verify signature on sstate package ../build/sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst, skipping acceleration...
> +      WARNING: gettext-minimal-native-1.0-r0 do_create_recipe_spdx_setscene: No sstate archive obtainable, will run full task instead.
> +      WARNING: Logfile for failed setscene task is ../build/tmp/work/x86_64-linux/gettext-minimal-native/1.0/temp/log.do_create_recipe_spdx_setscene.6994
> +      WARNING: Setscene task (../layers/openembedded-core/meta/recipes-core/gettext/gettext-minimal-native_1.0.bb:do_create_recipe_spdx_setscene) failed with exit code '1' - real task will be run instead
> +
> +   As you can see, this does not prevent :term:`BitBake` from continuing, but
> +   the real task is executed instead of re-using the shared state.
> 

Cheers,
Quentin
Antonin Godard April 27, 2026, 2:43 p.m. UTC | #2
Hi,

On Fri Apr 24, 2026 at 5:18 PM CEST, Quentin Schulz via lists.yoctoproject.org wrote:
> Hi Antonin,
>
> On 4/21/26 9:19 AM, Antonin Godard via lists.yoctoproject.org wrote:
>> Document the shared state signing feature. Add a new document in the
>> Security manual, and definitions for the variables involved in this
>> process in the variable glossary.
>> 
>> [YOCTO #15217]
>> 
>> Signed-off-by: Antonin Godard <antonin.godard@bootlin.com>
>> ---
>> Changes in v2:
>> - Fix typos reported by Ulrich (thanks!)
>> - Link to v1: https://patch.msgid.link/20260417-sstate-signing-v1-1-5df11613249e@bootlin.com
>> ---
>>   documentation/overview-manual/concepts.rst       |   6 +
>>   documentation/ref-manual/variables.rst           |  50 +++++
>>   documentation/security-manual/index.rst          |   1 +
>>   documentation/security-manual/sstate-signing.rst | 272 +++++++++++++++++++++++
>>   4 files changed, 329 insertions(+)
>> 
>> diff --git a/documentation/overview-manual/concepts.rst b/documentation/overview-manual/concepts.rst
>> index ab723d7c3..f0b336226 100644
>> --- a/documentation/overview-manual/concepts.rst
>> +++ b/documentation/overview-manual/concepts.rst
>> @@ -1240,6 +1240,12 @@ variable is the function that determines whether a given dependency
>>   needs to be followed, and whether for any given relationship the
>>   function needs to be passed. The function returns a True or False value.
>>   
>> +.. note::
>> +
>> +   Is is possible to sign these artifacts with :wikipedia:`GPG
>> +   <GNU_Privacy_Guard>`. See :doc:`/security-manual/sstate-signing` in the Yocto
>> +   Project Security Manual for more information.
>> +
>>   Images
>>   ------
>>   
>> diff --git a/documentation/ref-manual/variables.rst b/documentation/ref-manual/variables.rst
>> index 317b75913..90d7fc924 100644
>> --- a/documentation/ref-manual/variables.rst
>> +++ b/documentation/ref-manual/variables.rst
>> @@ -10097,6 +10097,25 @@ system and gives an overview of their function and contents.
>>   
>>         For details on the process, see the :ref:`ref-classes-staging` class.
>>   
>> +   :term:`SSTATE_SIG_KEY`
>> +      When signing :ref:`shared state <overview-manual/concepts:setscene tasks
>> +      and shared state>` artifacts (when :term:`SSTATE_VERIFY_SIG` is set to
>> +      "1"), the :term:`SSTATE_SIG_KEY` variable is the :wikipedia:`GPG
>> +      <GNU_Privacy_Guard>` key identifier used to sign them (with the private
>> +      key).
>> +
>> +      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
>> +      Manual for more information.
>> +
>> +   :term:`SSTATE_SIG_PASSPHRASE`
>> +      When signing :ref:`shared state <overview-manual/concepts:setscene tasks
>> +      and shared state>` artifacts (when :term:`SSTATE_VERIFY_SIG` is set to
>> +      "1"), the :term:`SSTATE_SIG_PASSPHRASE` variable is the passphrase used to
>> +      protect the private key signing the artifacts.
>> +
>> +      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
>> +      Manual for more information.
>> +
>>      :term:`SSTATE_SKIP_CREATION`
>>         The :term:`SSTATE_SKIP_CREATION` variable can be used to skip the
>>         creation of :ref:`shared state <overview-manual/concepts:shared state cache>`
>> @@ -10117,6 +10136,37 @@ system and gives an overview of their function and contents.
>>   
>>            SSTATE_SKIP_CREATION = "1"
>>   
>> +   :term:`SSTATE_VALID_SIGS`
>> +      When verifying :ref:`shared state <overview-manual/concepts:setscene tasks
>> +      and shared state>` artifacts (when :term:`SSTATE_VERIFY_SIG` is set to
>> +      "1"), the :term:`SSTATE_VALID_SIGS` variable is a space-separated list of
>> +      :wikipedia:`GPG <GNU_Privacy_Guard>` key identifiers to use to verify their
>> +      signature.
>> +
>> +      It must contain the short form identifier of the key pair. For example,
>> +      when running the ``gpg --list-keys`` command:
>> +
>> +      .. parsed-literal::
>> +
>
> You could use
>
> .. code-block:: console
>
>     $ gpg --list-keys
>     pub....
>
> to avoid having to use a literal include.

This use of parsed-literal was intended, as it shows the last 16 characters in
bold. I'll add " (in bold text below)" before the colon.

>> +         pub   ed25519 2026-04-17 [SC]
>> +               \4049A47E3AAA99D0250966DC\ **5B97632FA7F4E942**
>> +         uid           [ultimate] Antonin Godard (SState Signing) <antonin.godard\@bootlin.com>
>> +         sub   cv25519 2026-04-17 [E]
>> +
>> +      The short form equals the last 16 characters of the identier. In the above
>
> s/identier/identifier/
>
>> +      example: ``5B97632FA7F4E942``.
>> +
>> +      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
>> +      Manual for more information.
>> +
>> +   :term:`SSTATE_VERIFY_SIG`
>> +      The :term:`SSTATE_VERIFY_SIG` variable controls whether to enable or
>> +      disable the :ref:`shared state <overview-manual/concepts:setscene tasks
>> +      and shared state>` artifacts signing feature.
>> +
>> +      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
>> +      Manual for more information.
>> +
>>      :term:`STAGING_BASE_LIBDIR_NATIVE`
>>         Specifies the path to the ``/lib`` subdirectory of the sysroot
>>         directory for the build host.
>> diff --git a/documentation/security-manual/index.rst b/documentation/security-manual/index.rst
>> index 3453940f5..a767cd9c6 100644
>> --- a/documentation/security-manual/index.rst
>> +++ b/documentation/security-manual/index.rst
>> @@ -14,6 +14,7 @@ Yocto Project Security Manual
>>      securing-images
>>      vulnerabilities
>>      read-only-rootfs
>> +   sstate-signing
>>   
>>   .. include:: /boilerplate.rst
>>   
>> diff --git a/documentation/security-manual/sstate-signing.rst b/documentation/security-manual/sstate-signing.rst
>> new file mode 100644
>> index 000000000..54ae92d50
>> --- /dev/null
>> +++ b/documentation/security-manual/sstate-signing.rst
>> @@ -0,0 +1,272 @@
>> +.. SPDX-License-Identifier: CC-BY-SA-2.0-UK
>> +
>> +Shared State Signing
>> +********************
>> +
>> +The :term:`OpenEmbedded Build System` build system has a built-in mechanism
>> +allowing to save execution time by re-using pre-built artifacts: the
>> +:ref:`shared state cache (sstate cache) <overview-manual/concepts:shared state
>> +cache>`. These artifacts are stored in a directory (:term:`SSTATE_DIR`) and are
>> +not signed by default.
>> +
>> +This document goes through the steps to enable shared state signing.
>> +This feature is fully dependent on :wikipedia:`GPG <GNU_Privacy_Guard>`, meaning
>> +examples shown in this document will use the ``gpg`` command-line tool.
>> +
>> +Host Requirements
>> +=================
>> +
>> +As :wikipedia:`GPG <GNU_Privacy_Guard>` is not part of the default :ref:`host
>> +requirements <ref-manual/system-requirements:Required Packages for the Build
>> +Host>`, you will need to install it on your host.
>> +
>> +For example, Debian based distributions provide it with the `gpg` package name.
>
> Double tick around gpg maybe?
>
>> +Install it as follows:
>> +
>> +.. code-block:: console
>> +
>> +   $ sudo apt install gpg
>> +
>
> apt-get
>
> :D
>
>> +Verify that your installation is successful by showing the version of GPG:
>> +
>> +.. code-block:: console
>> +
>> +   $ gpg --version
>> +
>
> What exactly does this depend on? Only the gpg binary or also libraries? 
> It'd be nice to know because maybe other distros have different packages 
> and need more/less. It seems that Debian's package installs the binary 
> and depends on libgpg-error0 library.
>
> Can be figured out later but it'd be nice.

This: https://pkgs.org/search/?q=%2Fusr%2Fbin%2Fgpg
tells me that you probably only need to install a single package to get the
``gpg`` command-line tool. However I'm not sure if there are other requirements,
but I don't think this document should go in length about it

>> +Generating A Public And Private Key Pair With GPG
>> +=================================================
>> +
>> +.. note::
>> +
>> +   This step is optional if you already have a pair of public and private keys.
>> +
>> +We need a pair of public and private keys for two independent tasks:
>> +
>> +-  Signing our sstate artifacts that the :term:`OpenEmbedded Build System`
>> +   generates with our **private key**.
>> +
>> +-  Verifying our sstate artifacts with our **public key** when re-using them.
>> +
>
> You alternate between "we/us/our" and "you" and the use of the 
> imperative mood ("Do this") throughout the document, it's a bit 
> confusing. I think we've mostly use the second person and imperative 
> mood so far, so maybe stick to that?
>
> You also alternate between "sstate" and "shared state".

I've harmonized the doc in v3, thanks

>> +.. note::
>> +
>> +   For more information on public key cryptography, see
>> +   :wikipedia:`Public-key_cryptography`.
>> +
>> +With the ``gpg`` command-line tool, generate a new pair of public and private
>> +keys:
>> +
>> +.. code-block:: console
>> +
>> +   $ gpg --full-generate-key
>> +
>> +This will guide you through the steps of creating the key pair.
>> +
>
> s/This/It/
>
> I initially was being confused why there was no step after "This will 
> guide you" that would actually guide me through the steps :)
>
>> +Once done, you should be able to list your new key with the following command:
>> +
>> +.. code-block:: console
>> +
>> +   $ gpg --list-keys
>> +   pub   ed25519 2026-04-17 [SC]
>> +         4049A47E3AAA99D0250966DC5B97632FA7F4E942
>> +   uid           [ultimate] Antonin Godard (SState Signing) <antonin.godard@bootlin.com>
>> +   sub   cv25519 2026-04-17 [E]
>> +
>> +In the above example, take note of the
>> +``4049A47E3AAA99D0250966DC5B97632FA7F4E942`` key identifier. This will be used
>> +to configure our build.
>> +
>> +Configuring Our Build System To Sign Sstate Artifacts
>> +=====================================================
>> +
>> +Shared State Location
>> +---------------------
>> +
>> +Our build system needs to be configured to sign new sstate artifacts when they
>> +are generated. The generation of new artifacts is done once a task has finished
>> +being executed.
>> +
>> +For the following sections let's assume that our build system has the shared
>> +state directory location (:term:`SSTATE_DIR`) defined as follows::
>> +
>> +   SSTATE_DIR = "${TOPDIR}/sstate-cache"
>> +
>> +Assuming this directory and our temporary directory (:term:`TMPDIR`) are empty,
>> +as an example throughout this document, let's run the ``create_recipe_spdx``
>
> What part of the sentence does "as an example throughout this document" 
> apply to? The wording is awkward.
>
>> +task of the ``gettext-minimal-native`` recipe:
>> +
>> +.. code-block:: console
>> +
>> +   $ bitbake gettext-minimal-native -c create_recipe_spdx
>> +
>> +After execution, our shared state directory should be populated with new files:
>> +
>> +.. code-block:: console
>> +
>> +   $ find $BUILDDIR/sstate-cache/ -name "*gettext-minimal-native*create_recipe_spdx*"
>> +   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst
>> +   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst.siginfo
>> +
>> +These are the default shared state artifacts generated by the
>> +:term:`OpenEmbedded Build System`. They are not signed with GPG by default, so
>> +let's see how to add signing of these artifacts.
>> +
>> +.. note::
>> +
>> +   The `siginfo` file is not related to GPG signing.
>> +
>
> But the first file also not since we haven't enabled signing yet, what 
> did you want to say here?

Avoid confusion as "siginfo" could make someone think of "signatures" and hence
signing/gpg. Is adding the note more confusing?

>
>> +Enabling Shared State Signing
>> +-----------------------------
>> +
>> +Create a new :term:`configuration file` on your host **in a safe location** and
>
> What makes a location safe?

> I would suggest to put it somewhere in $HOME/.config/ maybe?

Maybe, but I don't think that makes it safer?

> Since it contains a secret, we should probably also recommend to do
>
> chmod o-rwx <file>

Yes, I'll add that.

> no (and possibly chown $USER:$USER <file> ?)

This should be the default when creating the file but I can add that.

>> +add the two following statements::
>> +
>> +   SSTATE_VERIFY_SIG = "1"
>> +   SSTATE_SIG_KEY = "80613045069236143C2EE1A3C8855DA51F042B42"
>> +   SSTATE_SIG_PASSPHRASE = "3lvJGHo8HZIcaufWFRezFIYRFDMSDmmkxOiwQ67PeM8IZre90I"
>> +
>
> Those (the key and the passphrase) don't match anything in this file, 
> it'd be best to reuse what you've highlighted in the previous section.
>
> For the passphrase, maybe mention to "for example use that passphrase" 
> when generating the key (and adding a big fat warning to not actually 
> use that passphrase), so we know this here refers to that there. Just a 
> suggestion, maybe it's easier to not give an example than having to 
> remind people not to reuse it.
>
>> +It is advised to put these statements in a separate file as those contain
>> +secrets and should not be shared. For this example, let's assume this file is
>> +``conf/sstate-sig-key.conf``.
>> +
>> +These statements define:
>> +
>> +-  :term:`SSTATE_VERIFY_SIG`: setting this variable to "1" enables the shared
>> +   state signing feature.
>> +
>> +-  :term:`SSTATE_SIG_KEY`: the GPG key identifier for signing shared state
>> +   artifacts with the private key.
>> +
>> +   In our example, this corresponds to the identifier printed with the ``gpg
>> +   --list-keys`` command :ref:`above <security-manual/sstate-signing:Generating
>> +   A Public And Private Key Pair With GPG>`.
>> +
>> +-  :term:`SSTATE_SIG_PASSPHRASE`: the passphrase used to protect your private
>> +   key when creating the key, chosen when creating the key pair.
>> +
>
> Am I the only bothered this is an option? Is there no way to use a 
> keyring or the gpg-agent instead?

I don't think so, no.

>> +Let's test our configuration:
>> +
>> +#. Continuing with our ``gettext-minimal-native`` example, let's first clean the
>> +   existing shared state artifacts:
>> +
>> +   .. code-block:: console
>> +
>> +      $ bitbake gettext-minimal-native -c cleansstate
>> +
>
> Why do we need to clean the cache (I'm assuming because you want to make 
> sure the artifacts you sign are the one you just generated and not 
> malicious ones, but I think it'd be worth explaining this is not a 
> limitation but a design decision (IFF I really guessed that right)).

I'm giving an example here, and want to make sure my share state artifacts are
re-generated.

>> +#. Run the ``create_recipe_spdx`` task for ``gettext-minimal-native``, but this
>> +   time pass our new ``sstate-sig-key.conf`` file to :term:`BitBake`:
>> +
>> +   .. code-block:: console
>> +
>> +      $ bitbake -R conf/sstate-sig-key.conf gettext-minimal-native -c create_recipe_spdx
>> +
>> +List the files in the shared state directory again:
>> +
>> +.. code-block:: console
>> +
>> +   $ find sstate-cache/ -name "*gettext-minimal-native*create_recipe_spdx*"
>> +   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst
>> +   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst.sig
>> +   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst.siginfo
>> +
>> +A new ``.sig`` file was created: this means our artifact was successfully
>> +signed, and the signature is stored in a separate ``.sig`` file.
>> +
>> +Verifying Signed Shared State Artifacts
>> +=======================================
>> +
>> +Now that we have set up our build to sign shared state artifacts, let's see how
>> +we can verify them with the public key counterpart of the private key.
>> +
>> +.. note::
>> +
>> +   Signature of shared state and its verification can happen on two different
>> +   hosts, meaning one host can be in charge of the signature while another only
>> +   verifies the artifacts. This is preferred as the private key should not be
>> +   shared spread on multiple hosts.
>> +
>
> s/spread on/between/
>
>> +From a :term:`configuration file` such as the :ref:`site configuration file
>> +<structure-build-conf-site.conf>`, include the following statements::
>> +
>> +   SSTATE_VERIFY_SIG = "1"
>> +   SSTATE_VALID_SIGS = "5B97632FA7F4E942"
>> +
>> +These statements define:
>> +
>> +-  :term:`SSTATE_VERIFY_SIG`: setting this variable to "1" enables the shared
>> +   state signing feature.
>> +
>> +-  :term:`SSTATE_VALID_SIGS`: a space-separated list of key identifiers for
>> +   which we can accept shared state artifacts.
>> +
>> +   This means that shared state will be reused **only if it was signed with the
>> +   private key corresponding to key identifier**.
>> +
>> +   You'll notice the short-form of the key identifier here, which are the last 16
>> +   characters of the long-form key identifier shown with ``gpg --list-keys``:
>> +
>> +   .. parsed-literal::
>> +
>> +      pub   ed25519 2026-04-17 [SC]
>> +            \4049A47E3AAA99D0250966DC\ **5B97632FA7F4E942**
>> +      uid           [ultimate] Antonin Godard (SState Signing) <antonin.godard\@bootlin.com>
>> +      sub   cv25519 2026-04-17 [E]
>> +
>
> See comment in the first occurrence of this parsed-literal.
>
>> +Let's verify that signature verification works:
>> +
>> +#. First, remove temporary outputs (:term:`TMPDIR`) from the previous builds, to
>> +   make the :term:`OpenEmbedded Build System` use the shared state:
>
> make <OE> rebuild everything using the shared state
>
> ?
>
>> +
>> +   .. code-block:: console
>> +
>> +      $ rm -r $BUILDDIR/tmp/
>> +
>
> You never tell the user what this variable should be set to. If they 
> forget, they'll wreak havoc on their host system deleting /tmp/ recursively.

I'll remove BUILDDIR

>> +#. Then, run the task again:
>> +
>> +   .. code-block:: console
>> +
>> +      $ bitbake gettext-minimal-native -c create_recipe_spdx
>> +
>> +#. To make sure the shared state artifact was successfully used, look for the
>> +   :ref:`setscene <overview-manual/concepts:setscene tasks and shared state>` task
>> +   for ``create_recipe_spdx`` in the latest log file from :term:`BitBake`:
>> +
>> +   .. code-block:: console
>> +
>> +      $ grep create_recipe_spdx_setscene tmp/log/cooker/<machine>/console-latest.log
>> +      NOTE: Running setscene task 1 of 1 (../layers/openembedded-core/meta/recipes-core/gettext/gettext-minimal-native_1.0.bb:do_create_recipe_spdx_setscene)
>> +      NOTE: recipe gettext-minimal-native-1.0-r0: task do_create_recipe_spdx_setscene: Started
>> +      NOTE: recipe gettext-minimal-native-1.0-r0: task do_create_recipe_spdx_setscene: Succeeded
>> +
>> +   Our shared state was successfully verified and used!
>
> Is there any indication in the logs that verification was actually 
> performed?

No, I didn't find any in the code, or with any verbosity level

>> +
>> +.. note::
>> +
>> +   To make sure shared state verification is working, you can set a "fake"
>> +   public key identifier in :term:`SSTATE_VALID_SIGS`::
>> +
>> +      SSTATE_VALID_SIGS = "CAFECAFECAFECAFE"
>> +
>> +   Remove the temporary outputs again:
>> +
>> +   .. code-block:: console
>> +
>> +      $ rm -r $BUILDDIR/tmp/
>> +
>> +   Now, try executing the task:
>> +
>> +   .. code-block:: console
>> +
>> +      $ bitbake gettext-minimal-native -c create_recipe_spdx
>> +
>> +   You should have warnings from :term:`BitBake`:
>> +
>
> Is this output in the console or in the log file you mentioned earlier?

In the console. I'll mention it

>> +   .. code-block:: text
>> +
>> +      WARNING: gettext-minimal-native-1.0-r0 do_create_recipe_spdx_setscene: No accepted signatures found. Good signatures found: 5B97632FA7F4E942.
>> +      WARNING: gettext-minimal-native-1.0-r0 do_create_recipe_spdx_setscene: Cannot verify signature on sstate package ../build/sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst, skipping acceleration...
>> +      WARNING: gettext-minimal-native-1.0-r0 do_create_recipe_spdx_setscene: No sstate archive obtainable, will run full task instead.
>> +      WARNING: Logfile for failed setscene task is ../build/tmp/work/x86_64-linux/gettext-minimal-native/1.0/temp/log.do_create_recipe_spdx_setscene.6994
>> +      WARNING: Setscene task (../layers/openembedded-core/meta/recipes-core/gettext/gettext-minimal-native_1.0.bb:do_create_recipe_spdx_setscene) failed with exit code '1' - real task will be run instead
>> +
>> +   As you can see, this does not prevent :term:`BitBake` from continuing, but
>> +   the real task is executed instead of re-using the shared state.
>> 

Thanks!
Antonin
Quentin Schulz April 27, 2026, 3:08 p.m. UTC | #3
Hi Antonin,

On 4/27/26 4:43 PM, Antonin Godard wrote:
> Hi,
> 
> On Fri Apr 24, 2026 at 5:18 PM CEST, Quentin Schulz via lists.yoctoproject.org wrote:
>> Hi Antonin,
>>
>> On 4/21/26 9:19 AM, Antonin Godard via lists.yoctoproject.org wrote:
[...]
>>> +   :term:`SSTATE_VALID_SIGS`
>>> +      When verifying :ref:`shared state <overview-manual/concepts:setscene tasks
>>> +      and shared state>` artifacts (when :term:`SSTATE_VERIFY_SIG` is set to
>>> +      "1"), the :term:`SSTATE_VALID_SIGS` variable is a space-separated list of
>>> +      :wikipedia:`GPG <GNU_Privacy_Guard>` key identifiers to use to verify their
>>> +      signature.
>>> +
>>> +      It must contain the short form identifier of the key pair. For example,
>>> +      when running the ``gpg --list-keys`` command:
>>> +

What happens if this variable is empty? My reading of 
meta/lib/oe/gpg_sign.py:LocalSigner:verify() hints at *any* key that 
verifies the signature will do.

>>> +      .. parsed-literal::
>>> +
>>
>> You could use
>>
>> .. code-block:: console
>>
>>      $ gpg --list-keys
>>      pub....
>>
>> to avoid having to use a literal include.
> 
> This use of parsed-literal was intended, as it shows the last 16 characters in
> bold. I'll add " (in bold text below)" before the colon.
> 

We can also use

:emphasize-lines: <line-no>[, <line-no>...]

in code-blocks, see 
https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-option-code-block-emphasize-lines

[...]

>>> +.. note::
>>> +
>>> +   The `siginfo` file is not related to GPG signing.
>>> +
>>
>> But the first file also not since we haven't enabled signing yet, what
>> did you want to say here?
> 
> Avoid confusion as "siginfo" could make someone think of "signatures" and hence
> signing/gpg. Is adding the note more confusing?
> 

Ah, yes this makes sense to specify this.

What about

Despite its name, the `siginfo` file is unrelated to GPG signing.

?

>>
>>> +Enabling Shared State Signing
>>> +-----------------------------
>>> +
>>> +Create a new :term:`configuration file` on your host **in a safe location** and
>>
>> What makes a location safe?
> 
>> I would suggest to put it somewhere in $HOME/.config/ maybe?
> 
> Maybe, but I don't think that makes it safer?
> 

It puts it outside of a possibly versioned repository, but in itself, 
not better no.

>> Since it contains a secret, we should probably also recommend to do
>>
>> chmod o-rwx <file>
> 
> Yes, I'll add that.
> 
>> no (and possibly chown $USER:$USER <file> ?)
> 
> This should be the default when creating the file but I can add that.
> 

Fair enough.

[...]

>>> +It is advised to put these statements in a separate file as those contain
>>> +secrets and should not be shared. For this example, let's assume this file is
>>> +``conf/sstate-sig-key.conf``.
>>> +
>>> +These statements define:
>>> +
>>> +-  :term:`SSTATE_VERIFY_SIG`: setting this variable to "1" enables the shared
>>> +   state signing feature.
>>> +
>>> +-  :term:`SSTATE_SIG_KEY`: the GPG key identifier for signing shared state
>>> +   artifacts with the private key.
>>> +
>>> +   In our example, this corresponds to the identifier printed with the ``gpg
>>> +   --list-keys`` command :ref:`above <security-manual/sstate-signing:Generating
>>> +   A Public And Private Key Pair With GPG>`.
>>> +
>>> +-  :term:`SSTATE_SIG_PASSPHRASE`: the passphrase used to protect your private
>>> +   key when creating the key, chosen when creating the key pair.
>>> +
>>
>> Am I the only bothered this is an option? Is there no way to use a
>> keyring or the gpg-agent instead?
> 
> I don't think so, no.
> 

Terrible security practice :(

>>> +Let's test our configuration:
>>> +
>>> +#. Continuing with our ``gettext-minimal-native`` example, let's first clean the
>>> +   existing shared state artifacts:
>>> +
>>> +   .. code-block:: console
>>> +
>>> +      $ bitbake gettext-minimal-native -c cleansstate
>>> +
>>
>> Why do we need to clean the cache (I'm assuming because you want to make
>> sure the artifacts you sign are the one you just generated and not
>> malicious ones, but I think it'd be worth explaining this is not a
>> limitation but a design decision (IFF I really guessed that right)).
> 
> I'm giving an example here, and want to make sure my share state artifacts are
> re-generated.
> 

Then say this :)

[...]

>>> +#. Then, run the task again:
>>> +
>>> +   .. code-block:: console
>>> +
>>> +      $ bitbake gettext-minimal-native -c create_recipe_spdx
>>> +
>>> +#. To make sure the shared state artifact was successfully used, look for the
>>> +   :ref:`setscene <overview-manual/concepts:setscene tasks and shared state>` task
>>> +   for ``create_recipe_spdx`` in the latest log file from :term:`BitBake`:
>>> +
>>> +   .. code-block:: console
>>> +
>>> +      $ grep create_recipe_spdx_setscene tmp/log/cooker/<machine>/console-latest.log
>>> +      NOTE: Running setscene task 1 of 1 (../layers/openembedded-core/meta/recipes-core/gettext/gettext-minimal-native_1.0.bb:do_create_recipe_spdx_setscene)
>>> +      NOTE: recipe gettext-minimal-native-1.0-r0: task do_create_recipe_spdx_setscene: Started
>>> +      NOTE: recipe gettext-minimal-native-1.0-r0: task do_create_recipe_spdx_setscene: Succeeded
>>> +
>>> +   Our shared state was successfully verified and used!
>>
>> Is there any indication in the logs that verification was actually
>> performed?
> 
> No, I didn't find any in the code, or with any verbosity level
> 

Maybe something we should add? This seems like an important piece of 
information to have.

What about

diff --git a/meta/lib/oe/gpg_sign.py b/meta/lib/oe/gpg_sign.py
index ede6186c84..fa339644fd 100644
--- a/meta/lib/oe/gpg_sign.py
+++ b/meta/lib/oe/gpg_sign.py
@@ -132,8 +132,11 @@ class LocalSigner(object):
          status = subprocess.run(cmd, stdout=subprocess.PIPE, 
stderr=subprocess.PIPE)
          # Valid if any key matches if unspecified
          if not valid_sigs:
-            ret = False if status.returncode else True
-            return ret
+            if status.returncode:
+                return False
+
+            bb.note("Signature file %s successfully verified with 
unspecified key" % (sig_file))
+            return True

          import re
          goodsigs = []
@@ -145,6 +148,7 @@ class LocalSigner(object):

          for sig in valid_sigs.split():
              if sig in goodsigs:
+                bb.note("Signature file %s successfully verified with 
key %s" % (sig_file, sig))
                  return True
          if len(goodsigs):
              bb.warn('No accepted signatures found. Good signatures 
found: %s.' % ' '.join(goodsigs))


? I think we may have a few people resisting this as I'm guessing this 
is on the hot path so adding messages isn't the best (even if disabled 
most of the time).

Cheers,
Quentin
Antonin Godard April 27, 2026, 3:33 p.m. UTC | #4
Hi,

On Mon Apr 27, 2026 at 5:08 PM CEST, Quentin Schulz wrote:
> Hi Antonin,
>
> On 4/27/26 4:43 PM, Antonin Godard wrote:
>> Hi,
>> 
>> On Fri Apr 24, 2026 at 5:18 PM CEST, Quentin Schulz via lists.yoctoproject.org wrote:
>>> Hi Antonin,
>>>
>>> On 4/21/26 9:19 AM, Antonin Godard via lists.yoctoproject.org wrote:
> [...]
>>>> +   :term:`SSTATE_VALID_SIGS`
>>>> +      When verifying :ref:`shared state <overview-manual/concepts:setscene tasks
>>>> +      and shared state>` artifacts (when :term:`SSTATE_VERIFY_SIG` is set to
>>>> +      "1"), the :term:`SSTATE_VALID_SIGS` variable is a space-separated list of
>>>> +      :wikipedia:`GPG <GNU_Privacy_Guard>` key identifiers to use to verify their
>>>> +      signature.
>>>> +
>>>> +      It must contain the short form identifier of the key pair. For example,
>>>> +      when running the ``gpg --list-keys`` command:
>>>> +
>
> What happens if this variable is empty? My reading of 
> meta/lib/oe/gpg_sign.py:LocalSigner:verify() hints at *any* key that 
> verifies the signature will do.

I think that's what happens, when reading again. I'll add this to the variable's
description.

>>>> +      .. parsed-literal::
>>>> +
>>>
>>> You could use
>>>
>>> .. code-block:: console
>>>
>>>      $ gpg --list-keys
>>>      pub....
>>>
>>> to avoid having to use a literal include.
>> 
>> This use of parsed-literal was intended, as it shows the last 16 characters in
>> bold. I'll add " (in bold text below)" before the colon.
>> 
>
> We can also use
>
> :emphasize-lines: <line-no>[, <line-no>...]
>
> in code-blocks, see 
> https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-option-code-block-emphasize-lines

I want to highlight a specific portion of a line, I don't think this does that?

> [...]
>
>>>> +.. note::
>>>> +
>>>> +   The `siginfo` file is not related to GPG signing.
>>>> +
>>>
>>> But the first file also not since we haven't enabled signing yet, what
>>> did you want to say here?
>> 
>> Avoid confusion as "siginfo" could make someone think of "signatures" and hence
>> signing/gpg. Is adding the note more confusing?
>> 
>
> Ah, yes this makes sense to specify this.
>
> What about
>
> Despite its name, the `siginfo` file is unrelated to GPG signing.

Agreed, clearer

>>>
>>>> +Enabling Shared State Signing
>>>> +-----------------------------
>>>> +
>>>> +Create a new :term:`configuration file` on your host **in a safe location** and
>>>
>>> What makes a location safe?
>> 
>>> I would suggest to put it somewhere in $HOME/.config/ maybe?
>> 
>> Maybe, but I don't think that makes it safer?
>> 
>
> It puts it outside of a possibly versioned repository, but in itself, 
> not better no.
>
>>> Since it contains a secret, we should probably also recommend to do
>>>
>>> chmod o-rwx <file>
>> 
>> Yes, I'll add that.
>> 
>>> no (and possibly chown $USER:$USER <file> ?)
>> 
>> This should be the default when creating the file but I can add that.
>> 
>
> Fair enough.
>
> [...]
>
>>>> +It is advised to put these statements in a separate file as those contain
>>>> +secrets and should not be shared. For this example, let's assume this file is
>>>> +``conf/sstate-sig-key.conf``.
>>>> +
>>>> +These statements define:
>>>> +
>>>> +-  :term:`SSTATE_VERIFY_SIG`: setting this variable to "1" enables the shared
>>>> +   state signing feature.
>>>> +
>>>> +-  :term:`SSTATE_SIG_KEY`: the GPG key identifier for signing shared state
>>>> +   artifacts with the private key.
>>>> +
>>>> +   In our example, this corresponds to the identifier printed with the ``gpg
>>>> +   --list-keys`` command :ref:`above <security-manual/sstate-signing:Generating
>>>> +   A Public And Private Key Pair With GPG>`.
>>>> +
>>>> +-  :term:`SSTATE_SIG_PASSPHRASE`: the passphrase used to protect your private
>>>> +   key when creating the key, chosen when creating the key pair.
>>>> +
>>>
>>> Am I the only bothered this is an option? Is there no way to use a
>>> keyring or the gpg-agent instead?
>> 
>> I don't think so, no.
>> 
>
> Terrible security practice :(

Yeah I agree. Maybe it's worth opening an enhancement "bug" on Bugzilla for
this, what do you think?

>>>> +Let's test our configuration:
>>>> +
>>>> +#. Continuing with our ``gettext-minimal-native`` example, let's first clean the
>>>> +   existing shared state artifacts:
>>>> +
>>>> +   .. code-block:: console
>>>> +
>>>> +      $ bitbake gettext-minimal-native -c cleansstate
>>>> +
>>>
>>> Why do we need to clean the cache (I'm assuming because you want to make
>>> sure the artifacts you sign are the one you just generated and not
>>> malicious ones, but I think it'd be worth explaining this is not a
>>> limitation but a design decision (IFF I really guessed that right)).
>> 
>> I'm giving an example here, and want to make sure my share state artifacts are
>> re-generated.
>> 
>
> Then say this :)

Fair enough, I will!

> [...]
>
>>>> +#. Then, run the task again:
>>>> +
>>>> +   .. code-block:: console
>>>> +
>>>> +      $ bitbake gettext-minimal-native -c create_recipe_spdx
>>>> +
>>>> +#. To make sure the shared state artifact was successfully used, look for the
>>>> +   :ref:`setscene <overview-manual/concepts:setscene tasks and shared state>` task
>>>> +   for ``create_recipe_spdx`` in the latest log file from :term:`BitBake`:
>>>> +
>>>> +   .. code-block:: console
>>>> +
>>>> +      $ grep create_recipe_spdx_setscene tmp/log/cooker/<machine>/console-latest.log
>>>> +      NOTE: Running setscene task 1 of 1 (../layers/openembedded-core/meta/recipes-core/gettext/gettext-minimal-native_1.0.bb:do_create_recipe_spdx_setscene)
>>>> +      NOTE: recipe gettext-minimal-native-1.0-r0: task do_create_recipe_spdx_setscene: Started
>>>> +      NOTE: recipe gettext-minimal-native-1.0-r0: task do_create_recipe_spdx_setscene: Succeeded
>>>> +
>>>> +   Our shared state was successfully verified and used!
>>>
>>> Is there any indication in the logs that verification was actually
>>> performed?
>> 
>> No, I didn't find any in the code, or with any verbosity level
>> 
>
> Maybe something we should add? This seems like an important piece of 
> information to have.

Yes, I agree.

> What about
>
> diff --git a/meta/lib/oe/gpg_sign.py b/meta/lib/oe/gpg_sign.py
> index ede6186c84..fa339644fd 100644
> --- a/meta/lib/oe/gpg_sign.py
> +++ b/meta/lib/oe/gpg_sign.py
> @@ -132,8 +132,11 @@ class LocalSigner(object):
>           status = subprocess.run(cmd, stdout=subprocess.PIPE, 
> stderr=subprocess.PIPE)
>           # Valid if any key matches if unspecified
>           if not valid_sigs:
> -            ret = False if status.returncode else True
> -            return ret
> +            if status.returncode:
> +                return False
> +
> +            bb.note("Signature file %s successfully verified with unspecified key" % (sig_file))

Maybe `bb.debug`?

> +            return True
>
>           import re
>           goodsigs = []
> @@ -145,6 +148,7 @@ class LocalSigner(object):
>
>           for sig in valid_sigs.split():
>               if sig in goodsigs:
> +                bb.note("Signature file %s successfully verified with key %s" % (sig_file, sig))
>                   return True
>           if len(goodsigs):
>               bb.warn('No accepted signatures found. Good signatures 
> found: %s.' % ' '.join(goodsigs))
>
>
> ? I think we may have a few people resisting this as I'm guessing this 
> is on the hot path so adding messages isn't the best (even if disabled 
> most of the time).

I see what you mean. You might be a bit more convincing by passing them through
bb.debug?

Thanks,
Antonin
Quentin Schulz April 27, 2026, 4:44 p.m. UTC | #5
Hi Antonin,

On 4/27/26 5:33 PM, Antonin Godard wrote:
> Hi,
> 
> On Mon Apr 27, 2026 at 5:08 PM CEST, Quentin Schulz wrote:
>> Hi Antonin,
>>
>> On 4/27/26 4:43 PM, Antonin Godard wrote:
>>> Hi,
>>>
>>> On Fri Apr 24, 2026 at 5:18 PM CEST, Quentin Schulz via lists.yoctoproject.org wrote:
>>>> Hi Antonin,
>>>>
>>>> On 4/21/26 9:19 AM, Antonin Godard via lists.yoctoproject.org wrote:
[...]
>>>>> +      .. parsed-literal::
>>>>> +
>>>>
>>>> You could use
>>>>
>>>> .. code-block:: console
>>>>
>>>>       $ gpg --list-keys
>>>>       pub....
>>>>
>>>> to avoid having to use a literal include.
>>>
>>> This use of parsed-literal was intended, as it shows the last 16 characters in
>>> bold. I'll add " (in bold text below)" before the colon.
>>>
>>
>> We can also use
>>
>> :emphasize-lines: <line-no>[, <line-no>...]
>>
>> in code-blocks, see
>> https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-option-code-block-emphasize-lines
> 
> I want to highlight a specific portion of a line, I don't think this does that?
> 

No, but I vaguely remember there is an extension for that (there's 
always one for anything in Sphinx it seems like :) ). I don't think I 
was convinced when reading the code/README so wouldn't recommend.

Is it really necessary to highlight that specific part of the line? A 
small sentence after the code-block to say we are interested in 
DECAFBEBE in the highlighted output could be good enough.

But not a blocker, for sure!

[...]

>>>>> +-  :term:`SSTATE_SIG_PASSPHRASE`: the passphrase used to protect your private
>>>>> +   key when creating the key, chosen when creating the key pair.
>>>>> +
>>>>
>>>> Am I the only bothered this is an option? Is there no way to use a
>>>> keyring or the gpg-agent instead?
>>>
>>> I don't think so, no.
>>>
>>
>> Terrible security practice :(
> 
> Yeah I agree. Maybe it's worth opening an enhancement "bug" on Bugzilla for
> this, what do you think?
> 

I guess so. Considering nobody touched that code in the last three 
years, either it works really well or not many people use it :) Doesn't 
hurt, it's only one more bug in the Bugzilla :)

[...]

>> +            return True
>>
>>            import re
>>            goodsigs = []
>> @@ -145,6 +148,7 @@ class LocalSigner(object):
>>
>>            for sig in valid_sigs.split():
>>                if sig in goodsigs:
>> +                bb.note("Signature file %s successfully verified with key %s" % (sig_file, sig))
>>                    return True
>>            if len(goodsigs):
>>                bb.warn('No accepted signatures found. Good signatures
>> found: %s.' % ' '.join(goodsigs))
>>
>>
>> ? I think we may have a few people resisting this as I'm guessing this
>> is on the hot path so adding messages isn't the best (even if disabled
>> most of the time).
> 
> I see what you mean. You might be a bit more convincing by passing them through
> bb.debug?
> 

Sure, I randomly picked bb.note() but anything will do (though, does it 
appear in the logs, which is what we're after after all?). Even 
bb.debug() writes to the mainlogger, so I'm assuming there's *some* cost 
even if it's not printed. Did you try? Does it work? (Because I just 
wrote this without even running bitbake with it :D)

Cheers,
Quentin
Antonin Godard April 28, 2026, 8:27 a.m. UTC | #6
Hi,

On Mon Apr 27, 2026 at 6:44 PM CEST, Quentin Schulz wrote:
[...]
>
>>> +            return True
>>>
>>>            import re
>>>            goodsigs = []
>>> @@ -145,6 +148,7 @@ class LocalSigner(object):
>>>
>>>            for sig in valid_sigs.split():
>>>                if sig in goodsigs:
>>> +                bb.note("Signature file %s successfully verified with key %s" % (sig_file, sig))
>>>                    return True
>>>            if len(goodsigs):
>>>                bb.warn('No accepted signatures found. Good signatures
>>> found: %s.' % ' '.join(goodsigs))
>>>
>>>
>>> ? I think we may have a few people resisting this as I'm guessing this
>>> is on the hot path so adding messages isn't the best (even if disabled
>>> most of the time).
>> 
>> I see what you mean. You might be a bit more convincing by passing them through
>> bb.debug?
>> 
>
> Sure, I randomly picked bb.note() but anything will do (though, does it 
> appear in the logs, which is what we're after after all?). Even 
> bb.debug() writes to the mainlogger, so I'm assuming there's *some* cost 
> even if it's not printed. Did you try? Does it work? (Because I just 
> wrote this without even running bitbake with it :D)

I tested this and this works fine. The messages will be printed when passing -v
to bitbake (verbose), so I guess bb.note is fine.

Maybe I'd re-organize a bit and do:

diff --git a/meta/lib/oe/gpg_sign.py b/meta/lib/oe/gpg_sign.py
index ede6186c84f..9024b349ef2 100644
--- a/meta/lib/oe/gpg_sign.py
+++ b/meta/lib/oe/gpg_sign.py
@@ -130,10 +130,6 @@ class LocalSigner(object):
 
         cmd += [sig_file]
         status = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-        # Valid if any key matches if unspecified
-        if not valid_sigs:
-            ret = False if status.returncode else True
-            return ret
 
         import re
         goodsigs = []
@@ -143,8 +139,18 @@ class LocalSigner(object):
             if s:
                 goodsigs += [s.group(1)]
 
+        # Valid if any key matches if unspecified
+        if not valid_sigs:
+            if status.returncode:
+                return False
+
+            bb.note("Signature file %s successfully verified with key(s): %s" %
+                    (sig_file, goodsigs))
+            return True
+
         for sig in valid_sigs.split():
             if sig in goodsigs:
+                bb.note("Signature file %s successfully verified with key %s" % (sig_file, sig))
                 return True
         if len(goodsigs):
             bb.warn('No accepted signatures found. Good signatures found: %s.' % ' '.join(goodsigs))

To show which key on the host verified our file. What do you think?

Antonin
Quentin Schulz May 7, 2026, 9:11 a.m. UTC | #7
Hi Antonin,

On 4/28/26 10:27 AM, Antonin Godard wrote:
> Hi,
> 
> On Mon Apr 27, 2026 at 6:44 PM CEST, Quentin Schulz wrote:
> [...]
>>
>>>> +            return True
>>>>
>>>>             import re
>>>>             goodsigs = []
>>>> @@ -145,6 +148,7 @@ class LocalSigner(object):
>>>>
>>>>             for sig in valid_sigs.split():
>>>>                 if sig in goodsigs:
>>>> +                bb.note("Signature file %s successfully verified with key %s" % (sig_file, sig))
>>>>                     return True
>>>>             if len(goodsigs):
>>>>                 bb.warn('No accepted signatures found. Good signatures
>>>> found: %s.' % ' '.join(goodsigs))
>>>>
>>>>
>>>> ? I think we may have a few people resisting this as I'm guessing this
>>>> is on the hot path so adding messages isn't the best (even if disabled
>>>> most of the time).
>>>
>>> I see what you mean. You might be a bit more convincing by passing them through
>>> bb.debug?
>>>
>>
>> Sure, I randomly picked bb.note() but anything will do (though, does it
>> appear in the logs, which is what we're after after all?). Even
>> bb.debug() writes to the mainlogger, so I'm assuming there's *some* cost
>> even if it's not printed. Did you try? Does it work? (Because I just
>> wrote this without even running bitbake with it :D)
> 
> I tested this and this works fine. The messages will be printed when passing -v
> to bitbake (verbose), so I guess bb.note is fine.
> 
> Maybe I'd re-organize a bit and do:
> 
> diff --git a/meta/lib/oe/gpg_sign.py b/meta/lib/oe/gpg_sign.py
> index ede6186c84f..9024b349ef2 100644
> --- a/meta/lib/oe/gpg_sign.py
> +++ b/meta/lib/oe/gpg_sign.py
> @@ -130,10 +130,6 @@ class LocalSigner(object):
>   
>           cmd += [sig_file]
>           status = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
> -        # Valid if any key matches if unspecified
> -        if not valid_sigs:
> -            ret = False if status.returncode else True
> -            return ret
>   
>           import re
>           goodsigs = []
> @@ -143,8 +139,18 @@ class LocalSigner(object):
>               if s:
>                   goodsigs += [s.group(1)]
>   
> +        # Valid if any key matches if unspecified
> +        if not valid_sigs:
> +            if status.returncode:
> +                return False
> +
> +            bb.note("Signature file %s successfully verified with key(s): %s" %
> +                    (sig_file, goodsigs))

We probably want ", ".join(goodsigs) here?

> +            return True
> +
>           for sig in valid_sigs.split():
>               if sig in goodsigs:
> +                bb.note("Signature file %s successfully verified with key %s" % (sig_file, sig))
>                   return True
>           if len(goodsigs):
>               bb.warn('No accepted signatures found. Good signatures found: %s.' % ' '.join(goodsigs))
> 
> To show which key on the host verified our file. What do you think?
> 

Sounds good to me, but we probably want to split that in at least two steps?

- print matching keys whenever none are required,
- print good signature when a key is required,

Since you already have everything set up for testing this and I don't, 
do you mind sending the patches?

I'm wondering if "any key will do" is a security best-practice or if we 
should warn in that setup? (probably not in the hot path here though :) ).

Cheers,
Quentin
diff mbox series

Patch

diff --git a/documentation/overview-manual/concepts.rst b/documentation/overview-manual/concepts.rst
index ab723d7c3..f0b336226 100644
--- a/documentation/overview-manual/concepts.rst
+++ b/documentation/overview-manual/concepts.rst
@@ -1240,6 +1240,12 @@  variable is the function that determines whether a given dependency
 needs to be followed, and whether for any given relationship the
 function needs to be passed. The function returns a True or False value.
 
+.. note::
+
+   Is is possible to sign these artifacts with :wikipedia:`GPG
+   <GNU_Privacy_Guard>`. See :doc:`/security-manual/sstate-signing` in the Yocto
+   Project Security Manual for more information.
+
 Images
 ------
 
diff --git a/documentation/ref-manual/variables.rst b/documentation/ref-manual/variables.rst
index 317b75913..90d7fc924 100644
--- a/documentation/ref-manual/variables.rst
+++ b/documentation/ref-manual/variables.rst
@@ -10097,6 +10097,25 @@  system and gives an overview of their function and contents.
 
       For details on the process, see the :ref:`ref-classes-staging` class.
 
+   :term:`SSTATE_SIG_KEY`
+      When signing :ref:`shared state <overview-manual/concepts:setscene tasks
+      and shared state>` artifacts (when :term:`SSTATE_VERIFY_SIG` is set to
+      "1"), the :term:`SSTATE_SIG_KEY` variable is the :wikipedia:`GPG
+      <GNU_Privacy_Guard>` key identifier used to sign them (with the private
+      key).
+
+      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
+      Manual for more information.
+
+   :term:`SSTATE_SIG_PASSPHRASE`
+      When signing :ref:`shared state <overview-manual/concepts:setscene tasks
+      and shared state>` artifacts (when :term:`SSTATE_VERIFY_SIG` is set to
+      "1"), the :term:`SSTATE_SIG_PASSPHRASE` variable is the passphrase used to
+      protect the private key signing the artifacts.
+
+      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
+      Manual for more information.
+
    :term:`SSTATE_SKIP_CREATION`
       The :term:`SSTATE_SKIP_CREATION` variable can be used to skip the
       creation of :ref:`shared state <overview-manual/concepts:shared state cache>`
@@ -10117,6 +10136,37 @@  system and gives an overview of their function and contents.
 
          SSTATE_SKIP_CREATION = "1"
 
+   :term:`SSTATE_VALID_SIGS`
+      When verifying :ref:`shared state <overview-manual/concepts:setscene tasks
+      and shared state>` artifacts (when :term:`SSTATE_VERIFY_SIG` is set to
+      "1"), the :term:`SSTATE_VALID_SIGS` variable is a space-separated list of
+      :wikipedia:`GPG <GNU_Privacy_Guard>` key identifiers to use to verify their
+      signature.
+
+      It must contain the short form identifier of the key pair. For example,
+      when running the ``gpg --list-keys`` command:
+
+      .. parsed-literal::
+
+         pub   ed25519 2026-04-17 [SC]
+               \4049A47E3AAA99D0250966DC\ **5B97632FA7F4E942**
+         uid           [ultimate] Antonin Godard (SState Signing) <antonin.godard\@bootlin.com>
+         sub   cv25519 2026-04-17 [E]
+
+      The short form equals the last 16 characters of the identier. In the above
+      example: ``5B97632FA7F4E942``.
+
+      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
+      Manual for more information.
+
+   :term:`SSTATE_VERIFY_SIG`
+      The :term:`SSTATE_VERIFY_SIG` variable controls whether to enable or
+      disable the :ref:`shared state <overview-manual/concepts:setscene tasks
+      and shared state>` artifacts signing feature.
+
+      See :doc:`/security-manual/sstate-signing` in the Yocto Project Security
+      Manual for more information.
+
    :term:`STAGING_BASE_LIBDIR_NATIVE`
       Specifies the path to the ``/lib`` subdirectory of the sysroot
       directory for the build host.
diff --git a/documentation/security-manual/index.rst b/documentation/security-manual/index.rst
index 3453940f5..a767cd9c6 100644
--- a/documentation/security-manual/index.rst
+++ b/documentation/security-manual/index.rst
@@ -14,6 +14,7 @@  Yocto Project Security Manual
    securing-images
    vulnerabilities
    read-only-rootfs
+   sstate-signing
 
 .. include:: /boilerplate.rst
 
diff --git a/documentation/security-manual/sstate-signing.rst b/documentation/security-manual/sstate-signing.rst
new file mode 100644
index 000000000..54ae92d50
--- /dev/null
+++ b/documentation/security-manual/sstate-signing.rst
@@ -0,0 +1,272 @@ 
+.. SPDX-License-Identifier: CC-BY-SA-2.0-UK
+
+Shared State Signing
+********************
+
+The :term:`OpenEmbedded Build System` build system has a built-in mechanism
+allowing to save execution time by re-using pre-built artifacts: the
+:ref:`shared state cache (sstate cache) <overview-manual/concepts:shared state
+cache>`. These artifacts are stored in a directory (:term:`SSTATE_DIR`) and are
+not signed by default.
+
+This document goes through the steps to enable shared state signing.
+This feature is fully dependent on :wikipedia:`GPG <GNU_Privacy_Guard>`, meaning
+examples shown in this document will use the ``gpg`` command-line tool.
+
+Host Requirements
+=================
+
+As :wikipedia:`GPG <GNU_Privacy_Guard>` is not part of the default :ref:`host
+requirements <ref-manual/system-requirements:Required Packages for the Build
+Host>`, you will need to install it on your host.
+
+For example, Debian based distributions provide it with the `gpg` package name.
+Install it as follows:
+
+.. code-block:: console
+
+   $ sudo apt install gpg
+
+Verify that your installation is successful by showing the version of GPG:
+
+.. code-block:: console
+
+   $ gpg --version
+
+Generating A Public And Private Key Pair With GPG
+=================================================
+
+.. note::
+
+   This step is optional if you already have a pair of public and private keys.
+
+We need a pair of public and private keys for two independent tasks:
+
+-  Signing our sstate artifacts that the :term:`OpenEmbedded Build System`
+   generates with our **private key**.
+
+-  Verifying our sstate artifacts with our **public key** when re-using them.
+
+.. note::
+
+   For more information on public key cryptography, see
+   :wikipedia:`Public-key_cryptography`.
+
+With the ``gpg`` command-line tool, generate a new pair of public and private
+keys:
+
+.. code-block:: console
+
+   $ gpg --full-generate-key
+
+This will guide you through the steps of creating the key pair.
+
+Once done, you should be able to list your new key with the following command:
+
+.. code-block:: console
+
+   $ gpg --list-keys
+   pub   ed25519 2026-04-17 [SC]
+         4049A47E3AAA99D0250966DC5B97632FA7F4E942
+   uid           [ultimate] Antonin Godard (SState Signing) <antonin.godard@bootlin.com>
+   sub   cv25519 2026-04-17 [E]
+
+In the above example, take note of the
+``4049A47E3AAA99D0250966DC5B97632FA7F4E942`` key identifier. This will be used
+to configure our build.
+
+Configuring Our Build System To Sign Sstate Artifacts
+=====================================================
+
+Shared State Location
+---------------------
+
+Our build system needs to be configured to sign new sstate artifacts when they
+are generated. The generation of new artifacts is done once a task has finished
+being executed.
+
+For the following sections let's assume that our build system has the shared
+state directory location (:term:`SSTATE_DIR`) defined as follows::
+
+   SSTATE_DIR = "${TOPDIR}/sstate-cache"
+
+Assuming this directory and our temporary directory (:term:`TMPDIR`) are empty,
+as an example throughout this document, let's run the ``create_recipe_spdx``
+task of the ``gettext-minimal-native`` recipe:
+
+.. code-block:: console
+
+   $ bitbake gettext-minimal-native -c create_recipe_spdx
+
+After execution, our shared state directory should be populated with new files:
+
+.. code-block:: console
+
+   $ find $BUILDDIR/sstate-cache/ -name "*gettext-minimal-native*create_recipe_spdx*"
+   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst
+   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst.siginfo
+
+These are the default shared state artifacts generated by the
+:term:`OpenEmbedded Build System`. They are not signed with GPG by default, so
+let's see how to add signing of these artifacts.
+
+.. note::
+
+   The `siginfo` file is not related to GPG signing.
+
+Enabling Shared State Signing
+-----------------------------
+
+Create a new :term:`configuration file` on your host **in a safe location** and
+add the two following statements::
+
+   SSTATE_VERIFY_SIG = "1"
+   SSTATE_SIG_KEY = "80613045069236143C2EE1A3C8855DA51F042B42"
+   SSTATE_SIG_PASSPHRASE = "3lvJGHo8HZIcaufWFRezFIYRFDMSDmmkxOiwQ67PeM8IZre90I"
+
+It is advised to put these statements in a separate file as those contain
+secrets and should not be shared. For this example, let's assume this file is
+``conf/sstate-sig-key.conf``.
+
+These statements define:
+
+-  :term:`SSTATE_VERIFY_SIG`: setting this variable to "1" enables the shared
+   state signing feature.
+
+-  :term:`SSTATE_SIG_KEY`: the GPG key identifier for signing shared state
+   artifacts with the private key.
+
+   In our example, this corresponds to the identifier printed with the ``gpg
+   --list-keys`` command :ref:`above <security-manual/sstate-signing:Generating
+   A Public And Private Key Pair With GPG>`.
+
+-  :term:`SSTATE_SIG_PASSPHRASE`: the passphrase used to protect your private
+   key when creating the key, chosen when creating the key pair.
+
+Let's test our configuration:
+
+#. Continuing with our ``gettext-minimal-native`` example, let's first clean the
+   existing shared state artifacts:
+
+   .. code-block:: console
+
+      $ bitbake gettext-minimal-native -c cleansstate
+
+#. Run the ``create_recipe_spdx`` task for ``gettext-minimal-native``, but this
+   time pass our new ``sstate-sig-key.conf`` file to :term:`BitBake`:
+
+   .. code-block:: console
+
+      $ bitbake -R conf/sstate-sig-key.conf gettext-minimal-native -c create_recipe_spdx
+
+List the files in the shared state directory again:
+
+.. code-block:: console
+
+   $ find sstate-cache/ -name "*gettext-minimal-native*create_recipe_spdx*"
+   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst
+   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst.sig
+   sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst.siginfo
+
+A new ``.sig`` file was created: this means our artifact was successfully
+signed, and the signature is stored in a separate ``.sig`` file.
+
+Verifying Signed Shared State Artifacts
+=======================================
+
+Now that we have set up our build to sign shared state artifacts, let's see how
+we can verify them with the public key counterpart of the private key.
+
+.. note::
+
+   Signature of shared state and its verification can happen on two different
+   hosts, meaning one host can be in charge of the signature while another only
+   verifies the artifacts. This is preferred as the private key should not be
+   shared spread on multiple hosts.
+
+From a :term:`configuration file` such as the :ref:`site configuration file
+<structure-build-conf-site.conf>`, include the following statements::
+
+   SSTATE_VERIFY_SIG = "1"
+   SSTATE_VALID_SIGS = "5B97632FA7F4E942"
+
+These statements define:
+
+-  :term:`SSTATE_VERIFY_SIG`: setting this variable to "1" enables the shared
+   state signing feature.
+
+-  :term:`SSTATE_VALID_SIGS`: a space-separated list of key identifiers for
+   which we can accept shared state artifacts.
+
+   This means that shared state will be reused **only if it was signed with the
+   private key corresponding to key identifier**.
+
+   You'll notice the short-form of the key identifier here, which are the last 16
+   characters of the long-form key identifier shown with ``gpg --list-keys``:
+
+   .. parsed-literal::
+
+      pub   ed25519 2026-04-17 [SC]
+            \4049A47E3AAA99D0250966DC\ **5B97632FA7F4E942**
+      uid           [ultimate] Antonin Godard (SState Signing) <antonin.godard\@bootlin.com>
+      sub   cv25519 2026-04-17 [E]
+
+Let's verify that signature verification works:
+
+#. First, remove temporary outputs (:term:`TMPDIR`) from the previous builds, to
+   make the :term:`OpenEmbedded Build System` use the shared state:
+
+   .. code-block:: console
+
+      $ rm -r $BUILDDIR/tmp/
+
+#. Then, run the task again:
+
+   .. code-block:: console
+
+      $ bitbake gettext-minimal-native -c create_recipe_spdx
+
+#. To make sure the shared state artifact was successfully used, look for the
+   :ref:`setscene <overview-manual/concepts:setscene tasks and shared state>` task
+   for ``create_recipe_spdx`` in the latest log file from :term:`BitBake`:
+
+   .. code-block:: console
+
+      $ grep create_recipe_spdx_setscene tmp/log/cooker/<machine>/console-latest.log
+      NOTE: Running setscene task 1 of 1 (../layers/openembedded-core/meta/recipes-core/gettext/gettext-minimal-native_1.0.bb:do_create_recipe_spdx_setscene)
+      NOTE: recipe gettext-minimal-native-1.0-r0: task do_create_recipe_spdx_setscene: Started
+      NOTE: recipe gettext-minimal-native-1.0-r0: task do_create_recipe_spdx_setscene: Succeeded
+
+   Our shared state was successfully verified and used!
+
+.. note::
+
+   To make sure shared state verification is working, you can set a "fake"
+   public key identifier in :term:`SSTATE_VALID_SIGS`::
+
+      SSTATE_VALID_SIGS = "CAFECAFECAFECAFE"
+
+   Remove the temporary outputs again:
+
+   .. code-block:: console
+
+      $ rm -r $BUILDDIR/tmp/
+
+   Now, try executing the task:
+
+   .. code-block:: console
+
+      $ bitbake gettext-minimal-native -c create_recipe_spdx
+
+   You should have warnings from :term:`BitBake`:
+
+   .. code-block:: text
+
+      WARNING: gettext-minimal-native-1.0-r0 do_create_recipe_spdx_setscene: No accepted signatures found. Good signatures found: 5B97632FA7F4E942.
+      WARNING: gettext-minimal-native-1.0-r0 do_create_recipe_spdx_setscene: Cannot verify signature on sstate package ../build/sstate-cache/universal/a3/6e/sstate:gettext-minimal-native:x86_64-linux:1.0:r0:x86_64:14:a36ef66df6b8c0cb5a849bc70a99dcfd61e4bacd11cefe6bbaf4978b2b3b617a_create_recipe_spdx.tar.zst, skipping acceleration...
+      WARNING: gettext-minimal-native-1.0-r0 do_create_recipe_spdx_setscene: No sstate archive obtainable, will run full task instead.
+      WARNING: Logfile for failed setscene task is ../build/tmp/work/x86_64-linux/gettext-minimal-native/1.0/temp/log.do_create_recipe_spdx_setscene.6994
+      WARNING: Setscene task (../layers/openembedded-core/meta/recipes-core/gettext/gettext-minimal-native_1.0.bb:do_create_recipe_spdx_setscene) failed with exit code '1' - real task will be run instead
+
+   As you can see, this does not prevent :term:`BitBake` from continuing, but
+   the real task is executed instead of re-using the shared state.