Skip to content

Conversation

@mtrmac
Copy link
Collaborator

@mtrmac mtrmac commented Jun 24, 2025

  • In signature/internal/sequoia, add Go bindings for https://github.com/ueno/podman-sequoia . (Parts of the middle C code are auto-generated by that project.)
  • With the containers_image_sequoia build tag:
    • Implement the previously-included but stub-only signature/simplesequoia API for creating simple-signing signatures using Sequoia-PGP
    • Do all simple signing verification using the Sequoia-PGP backend, instead of GnuPG via GPGME. Hopefully that will be transparent to users as of now.
    • Keep the traditional simple-signing API as is, backed by GnuPG via GPGME. That way, existing workflows that generate/import keys to GnuPG, or rely on GNUPGHOME, are unaffected.
  • Add a new set of CI tests, which run with the containers_image_sequoia build tag; currently that requires Rawhide.

Based on original contribution in #2569, with 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
Contributor

@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
Contributor

@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
Contributor

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
@@ -0,0 +1,200 @@
/*
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

signature/internal/sequoia/*.[ch] are auto-generated in https://github.com/ueno/podman-sequoia/tree/main/go/sequoia , only copied into this repo.

@mtrmac
Copy link
Collaborator Author

mtrmac commented Aug 25, 2025

Ready for review, PTAL. I have preserved the original work from #2569 and this is structured as a series of commits on top, but in this case, it’s probably easier to just review the final diff as a single unit.

Cc: @giuseppe @Luap99 @lsm5

@mtrmac mtrmac marked this pull request as ready for review August 25, 2025 11:39
@mtrmac mtrmac changed the title WIP: Add containers_image_sequoia build tag to do simple signing verification using Sequoia-PGP, and add a signature/simplesequoia implementation Add containers_image_sequoia build tag to do simple signing verification using Sequoia-PGP, and add a signature/simplesequoia implementation Aug 25, 2025
mtrmac added 16 commits August 25, 2025 13:46
- 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>
CI images come from containers/automation_images#411 .

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
@mtrmac mtrmac force-pushed the signature-sequoia branch from cb2c674 to 8cec77c Compare August 25, 2025 11:48
@lsm5
Copy link
Member

lsm5 commented Aug 25, 2025

Ready for review, PTAL. I have preserved the original work from #2569 and this is structured as a series of commits on top, but in this case, it’s probably easier to just review the final diff as a single unit.

Cc: @giuseppe @Luap99 @lsm5

Should this PR be moved to the monorepo?

@mtrmac
Copy link
Collaborator Author

mtrmac commented Aug 25, 2025

Should this PR be moved to the monorepo?

Sure, thanks for the reminder. I’ll move things after the dust settles.

Copy link
Member

@Luap99 Luap99 left a comment

Choose a reason for hiding this comment

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

LGTM, I rather get this merged by EOD before the monorepo move to avoid having to redo all commits against the new repo structure.

Automation wise it means we must port the rawhide tests to the new repo but we can do this after the initial repo creation.
cc @jankaluza

Copy link
Member

@lsm5 lsm5 left a comment

Choose a reason for hiding this comment

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

LGTM, I rather get this merged by EOD before the monorepo move to avoid having to redo all commits against the new repo structure.

LGTM in that case

@lsm5 lsm5 merged commit 58e4209 into containers:main Aug 25, 2025
13 checks passed
@mtrmac mtrmac deleted the signature-sequoia branch August 26, 2025 13:34
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.

4 participants