Skip to content

WIP: Add containers_image_sequoia build tag to do simple signing verification using Sequoia-PGP, and add a signature/simplesequoia implementation #2876

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from

Conversation

mtrmac
Copy link
Collaborator

@mtrmac mtrmac commented Jun 24, 2025

Sharing this to possibly help collaboration; unlikely to be useful for any real-world use, anything finished will probably be split into separate PRs.

Based on #2569 , many thanks to @ueno .

DO NOT MERGE: Includes UNMERGED #2905 .

This adds a new OpenPGP signing mechanism based Sequoia-PGP[1]. As
Sequoia-PGP is written in Rust and doesn't provide a stable C FFI,
this integration includes a minimal shared library as a "point
solution".

To build, first follow the instruction in signature/sequoia/README.md
and install `libimage_sequoia.so*` into the library path, and then
build with the following command from the top-level directory:

  $ make BUILDTAGS="btrfs_noversion containers_image_sequoia"

Note also that, for testing on Fedora, crypto-policies settings might
need to be modified to explictly allow SHA-1 and 1024 bit RSA, as the
testing keys in signature/fixtures are using those legacy algorithms.

1. https://sequoia-pgp.org/

Signed-off-by: Daiki Ueno <dueno@redhat.com>
return Ok(());
}
Err(verification_error) => {
signature_errors.push(verification_error.to_string());
Copy link

@ueno ueno Jun 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a matter of taste, but instead of storing errors as a string, I would keep the error types and let the caller format them:

diff --git a/signature/internal/sequoia/rust/src/signature.rs b/signature/internal/sequoia/rust/src/signature.rs
index 014d4f5f..d0ba6cd3 100644
--- a/signature/internal/sequoia/rust/src/signature.rs
+++ b/signature/internal/sequoia/rust/src/signature.rs
@@ -169,6 +169,7 @@ impl<'a> SequoiaMechanism<'a> {
         let h = Helper {
             certstore: self.certstore.clone(),
             signer: Default::default(),
+            errors: Default::default(),
         };
 
         let mut v = VerifierBuilder::from_bytes(signature)?.with_policy(&self.policy, None, h)?;
@@ -190,6 +191,7 @@ impl<'a> SequoiaMechanism<'a> {
 struct Helper<'a> {
     certstore: Arc<sequoia_cert_store::CertStore<'a>>,
     signer: Option<openpgp::Cert>,
+    errors: Vec<openpgp::Error>,
 }
 
 impl<'a> VerificationHelper for Helper<'a> {
@@ -205,7 +207,6 @@ impl<'a> VerificationHelper for Helper<'a> {
     }
 
     fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> {
-        let mut signature_errors: Vec<String> = Vec::new();
         for layer in structure {
             match layer {
                 MessageLayer::Compression { algo } => log::info!("Compressed using {}", algo),
@@ -219,7 +220,7 @@ impl<'a> VerificationHelper for Helper<'a> {
                         log::info!("Encrypted using {}", sym_algo);
                     }
                 }
-                MessageLayer::SignatureGroup { ref results } => {
+                MessageLayer::SignatureGroup { results } => {
                     for result in results {
                         match result {
                             Ok(good_checksum) => {
@@ -227,19 +228,14 @@ impl<'a> VerificationHelper for Helper<'a> {
                                 return Ok(());
                             }
                             Err(verification_error) => {
-                                signature_errors.push(verification_error.to_string());
+                                self.errors.push(openpgp::Error::from(verification_error));
                             }
                         }
                     }
                 }
             }
         }
-        let err = match signature_errors.len() {
-        0 => anyhow::anyhow!("No valid signature"),
-        1 => anyhow::anyhow!("{}", &signature_errors[0]),
-        _ => anyhow::anyhow!("Multiple signature errors: [{}]", signature_errors.join(", ")),
-        };
-        Err(err)
+        Err(anyhow::anyhow!("No valid signature"))
     }
 }

I also wonder if we might want to record all signers if there are multiple signatures.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep the error types and let the caller format them:

Thanks, that might very well be cleaner. It would require a somewhat tight coupling with the caller — the caller would specifically need to know that it should ignore the returned “no valid signature” error.


I also wonder if we might want to record all signers if there are multiple signatures.

For myself, I think I need to study / understand what is the precise semantics of MessageStructure, whether it is possible to have multiple signatures, and what does that mean.

… and whether it is relevant to designing a stable C API, which is the ~most urgent work right now.

Copy link

@ueno ueno Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep the error types and let the caller format them:

Thanks, that might very well be cleaner. It would require a somewhat tight coupling with the caller — the caller would specifically need to know that it should ignore the returned “no valid signature” error.

Actually that's the intention behind this suggestion: if the errors are converted into a string at the callee, useful information that could be retrieved through VerificationError may be lost, or it would require parsing of the error strings later.

I also wonder if we might want to record all signers if there are multiple signatures.

For myself, I think I need to study / understand what is the precise semantics of MessageStructure, whether it is possible to have multiple signatures, and what does that mean.

… and whether it is relevant to designing a stable C API, which is the ~most urgent work right now.

MessageStructure can certainly have multiple signatures, though the current c/image API seems to only expect a single signature to be valid. For future extension with PQ/T multiple signatures, it might make sense to provide a control on which signature has to be valid and which can be ignored.

As for the C API, GPGME provides a similar abstraction with gpgme_verify_result_t, which also supports multiple signatures.

@mtrmac mtrmac force-pushed the signature-sequoia branch 3 times, most recently from 23e7645 to 7e16175 Compare June 25, 2025 19:11
@mtrmac mtrmac force-pushed the signature-sequoia branch from efcf53d to 461beea Compare June 27, 2025 17:06
func WithPassphrase(passphrase string) Option {
return func(s *simpleSequoiaSigner) error {
// The gpgme implementation can’t use passphrase with \n; reject it here for consistent behavior.
// FIXME: We don’t need it in this API at all, but the "\n" check exists in the current call stack. That should go away.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Branch sequoia-refactor-SignDockerManifest, but that can wait.

return nil, fmt.Errorf("Signing not supported: %w", err)
}

// Ideally, we should look up (and unlock?) the key at this point already. FIXME: is that possible? Anyway, low-priority.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if needed we could reorganize the Rust API to allow that.

@mtrmac mtrmac force-pushed the signature-sequoia branch 3 times, most recently from 83975b0 to e19a9d0 Compare July 3, 2025 18:38
Comment on lines 77 to 100
sig := C.go_sequoia_sign(
m.mechanism,
cKeyIdentity,
cPassphrase,
(*C.uchar)(unsafe.Pointer(unsafe.SliceData(input))),
C.size_t(len(input)),
&cerr,
)
if sig == nil {
defer C.go_sequoia_error_free(cerr)
return nil, errors.New(C.GoString(cerr.message))
}
defer C.go_sequoia_signature_free(sig)
var size C.size_t
cData := C.go_sequoia_signature_get_data(sig, &size)
if size > C.size_t(C.INT_MAX) {
return nil, errors.New("overflow")
}
return C.GoBytes(unsafe.Pointer(cData), C.int(size)), nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mention it because we have been burned by this many times before and I haven't looked into the sequoia code to know for sure but is the sequoia library fully thread safe. And by that I mean is it using any thread local memory or other things that depend on the current thread? Because the go runtime can schedule us around in any thread at any time between these C calls which means go_sequoia_sign() and go_sequoia_signature_get_data() may get called from different threads so the underlying library must be able to handle this correctly

i.e. a simple example on how things can go wrong easily:
coreos/go-systemd@d1df97f

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the Go side, this PR now uses a sync.Once to load the library. And afterwards, everything is supposed to go through a Go *SigningMechanism / Rust SequoiaMechanism, where serialization of operations on a single mechanism is up to the caller.

I have no idea whether Sequoia is internally consistently thread-safe; I do see some uses of OnceLock on static variables, so it is at least not naively unaware.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serialization of operations on a single mechanism is up to the caller.

My concern was not so much about serialization but more about the use of stuff like thread local storage or other per thread things like a namespace for example. Between each C.somefunc call the go runtime can schedule us the code onto another thread. But because that is super unlikely normal testing will not uncover this most likely. It could end up being a very hard to find race condition later on. See the linked commit where the use of dlerror() causes some bugs because it stores the error in thread local memory.

I am not saying there is a problem here but just that we should check. I am sure @ueno or the other sequoia folks would know this

@mtrmac mtrmac force-pushed the signature-sequoia branch 15 times, most recently from f7dbbbe to 7871386 Compare July 4, 2025 16:28
@mtrmac mtrmac force-pushed the signature-sequoia branch from 47d405a to a5bc6cc Compare July 25, 2025 19:42
@mtrmac mtrmac force-pushed the signature-sequoia branch 2 times, most recently from a1bf189 to 9bb1701 Compare August 7, 2025 18:35
mtrmac added 25 commits August 7, 2025 21:54
Encapsulate the wanted/recorded tuples a bit more, to prepare for
changes to the signatureAcceptanceRules type.

Should not change (test) behavior.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
…ryKeyFingerprints

Move more code into the centralized function, to allow us to change
the implementation in the future.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
Document how the key identities from NewEphemeralGPGSigningMechanism
and SigningMechanism should relate.

For the GPGME implementation where the relationship isn't as desired, introduce
signingMechanismWithVerificationIdentityLookup .

For the openpgp implementation, just fix the returned value to match
new expecations.

Currently, the simple signing verifyAndExtractSignature rejects
subkey signatures for both mechanism implementations, so this does not
change behavior for any previously-accepted signatures (but it might
affect theoretical external direct callers of SigningMechanism).

We will actually use this in the next commit.

WARNING: Adding support for subkeys makes subkey revocation relevant.
Revocation is always messy - to support long-term signature validity
(which is very relevant for container image signing), smooth
handling of revocation might require trusted timestamps (not natively
supported in OpenPGP).

In practice, this library (partially out of necessity) lets the underlying
OpenPGP implementations impose their own policy, and that handling DIFFERS
between various SigningMechanism implementations: it seems that GnuPG
and refuses to use a revoked subkey regardless of revocation time, while
Sequoia-PGP decides based on the revocation reason whether the revocation
applies to all signatures or only to signatures (supposedly!!) made after the
revocation happened.

It is STRONGLY recommended to avoid all of this complexity, and to plan
for workflows where subkey revocation never happens. Just revoke and reissue
the whole key - either way you'll need to distribute an updated key to all
consumers.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
This does not change behavior for mechanisms which already always return the
primary key fingerprint (after a recent change, openpgp); for the others,
add support for signingMechanismWithVerificationIdentityLookup .

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
- Use the correct library name
- Allow specifying a library path at compile time

Example usage:
> make BUILDTAGS="btrfs_noversion containers_image_sequoia" SEQUOIA_SONAME_DIR=$(pwd)/signature/internal/sequoia/rust/target/release

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
... to match the recent more specific subkey validation
semantics.

Should not change behavior.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
Should not change test behavior, merely to be more similar
to the rest of the codebase.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
…Mechanism

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
It can be handled in callers - and it's better to do based on .Init()
outcomes.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
- Allow using c/image without loading sequoia if
  GO_SEQUOIA_ENABLE_DLOPEN is used, and nothing uses
  the Sequoia code at runtime.
- Allow adding ~independent users of signature/internal/sequoia,
  without having to coordinate to call Init at least once.
  We will add signature/simplesequoia to benefit from this.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
We can't reasonably unit-test the $HOME variant; tested manually.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
By default, the Rust logging output is silently discarded.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
Add missing tests, or add comments where that's not practical.

Should not change behavior.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
…ner API

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
Should not change behavior.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
For now only local tests, we should also do this for test_skopeo,
but that ~requires a corresponding Skopeo update.

WIP: We want to use distribution packages instead of compiling
Rust code ourselves.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
Requires an unmerged Skopeo PR.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
... to save on irrelevant cloud costs and noise, for now.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
@mtrmac mtrmac force-pushed the signature-sequoia branch from 9bb1701 to 55d1eaf Compare August 7, 2025 20:17
- Set VM_IMAGE_NAME on the currently-deleted tests as well

DO NOT MERGE: Depends on UNMERGED containers/automation_images#411 .

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants