DO NOT MERGE: vendor UNMERGED c/image

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
This commit is contained in:
Miloslav Trmač
2025-07-07 23:47:10 +02:00
parent bce0b233ac
commit 2434c04558
16 changed files with 823 additions and 99 deletions

2
go.mod
View File

@@ -114,3 +114,5 @@ require (
google.golang.org/grpc v1.72.2 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
replace github.com/containers/image/v5 => github.com/mtrmac/image/v5 v5.0.0-20250807201425-55d1eaf519bd

4
go.sum
View File

@@ -39,8 +39,6 @@ github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/containers/common v0.64.1 h1:E8vSiL+B84/UCsyVSb70GoxY9cu+0bseLujm4EKF6GE=
github.com/containers/common v0.64.1/go.mod h1:CtfQNHoCAZqWeXMwdShcsxmMJSeGRgKKMqAwRKmWrHE=
github.com/containers/image/v5 v5.36.2-0.20250807195248-f8cef395fcde h1:uUtFXNTs4bGwB1h/iJPadA9xYo1jcHCQG5K3gWc9cWM=
github.com/containers/image/v5 v5.36.2-0.20250807195248-f8cef395fcde/go.mod h1:x0jlG/5SvMUqUTD5H4f4ezuqU9E+PE6JnVfOJ+/vrPU=
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA=
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM=
@@ -204,6 +202,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mtrmac/image/v5 v5.0.0-20250807201425-55d1eaf519bd h1:IpH02DMIjamb3rC3dKBtq/StZ+UysGfZoNs761um3Aw=
github.com/mtrmac/image/v5 v5.0.0-20250807201425-55d1eaf519bd/go.mod h1:x0jlG/5SvMUqUTD5H4f4ezuqU9E+PE6JnVfOJ+/vrPU=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=

View File

@@ -5,7 +5,6 @@ package signature
import (
"errors"
"fmt"
"slices"
"strings"
"github.com/containers/image/v5/docker/reference"
@@ -64,15 +63,8 @@ func VerifyImageManifestSignatureUsingKeyIdentityList(unverifiedSignature, unver
if err != nil {
return nil, "", err
}
var matchedKeyIdentity string
sig, err := verifyAndExtractSignature(mech, unverifiedSignature, signatureAcceptanceRules{
validateKeyIdentity: func(keyIdentity string) error {
if !slices.Contains(expectedKeyIdentities, keyIdentity) {
return internal.NewInvalidSignatureError(fmt.Sprintf("Signature by %s does not match expected fingerprints %v", keyIdentity, expectedKeyIdentities))
}
matchedKeyIdentity = keyIdentity
return nil
},
sig, matchedKeyIdentity, err := verifyAndExtractSignature(mech, unverifiedSignature, signatureAcceptanceRules{
acceptedKeyIdentities: expectedKeyIdentities,
validateSignedDockerReference: func(signedDockerReference string) error {
signedRef, err := reference.ParseNormalizedNamed(signedDockerReference)
if err != nil {

View File

@@ -0,0 +1,200 @@
/*
* Copying and distribution of this file, with or without modification,
* are permitted in any medium without royalty provided the copyright
* notice and this notice are preserved. This file is offered as-is,
* without any warranty.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gosequoia.h"
#if defined(GO_SEQUOIA_ENABLE_DLOPEN) && GO_SEQUOIA_ENABLE_DLOPEN
#include <assert.h>
#include <dlfcn.h>
#include <errno.h>
#include <stdlib.h>
/* If SEQUOIA_SONAME is defined, dlopen handle can be automatically
* set; otherwise, the caller needs to call
* go_sequoia_ensure_library with soname determined at run time.
*/
#ifdef SEQUOIA_SONAME
static void
ensure_library (void)
{
if (go_sequoia_ensure_library (SEQUOIA_SONAME, RTLD_LAZY | RTLD_LOCAL) < 0)
abort ();
}
#if defined(GO_SEQUOIA_ENABLE_PTHREAD) && GO_SEQUOIA_ENABLE_PTHREAD
#include <pthread.h>
static pthread_once_t dlopen_once = PTHREAD_ONCE_INIT;
#define ENSURE_LIBRARY pthread_once(&dlopen_once, ensure_library)
#else /* GO_SEQUOIA_ENABLE_PTHREAD */
#define ENSURE_LIBRARY do { \
if (!go_sequoia_dlhandle) \
ensure_library(); \
} while (0)
#endif /* !GO_SEQUOIA_ENABLE_PTHREAD */
#else /* SEQUOIA_SONAME */
#define ENSURE_LIBRARY do {} while (0)
#endif /* !SEQUOIA_SONAME */
static void *go_sequoia_dlhandle;
/* Define redirection symbols */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-macros"
#if (2 <= __GNUC__ || (4 <= __clang_major__))
#define FUNC(ret, name, args, cargs) \
static __typeof__(name)(*go_sequoia_sym_##name);
#else
#define FUNC(ret, name, args, cargs) \
static ret(*go_sequoia_sym_##name)args;
#endif
#define VOID_FUNC FUNC
#include "gosequoiafuncs.h"
#undef VOID_FUNC
#undef FUNC
#pragma GCC diagnostic pop
/* Define redirection wrapper functions */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-macros"
#define FUNC(ret, name, args, cargs) \
ret go_##name args \
{ \
ENSURE_LIBRARY; \
assert (go_sequoia_sym_##name); \
return go_sequoia_sym_##name cargs; \
}
#define VOID_FUNC(ret, name, args, cargs) \
ret go_##name args \
{ \
ENSURE_LIBRARY; \
assert (go_sequoia_sym_##name); \
go_sequoia_sym_##name cargs; \
}
#include "gosequoiafuncs.h"
#undef VOID_FUNC
#undef FUNC
#pragma GCC diagnostic pop
static int
ensure_symbol (const char *name, void **symp)
{
if (!*symp)
{
void *sym = dlsym (go_sequoia_dlhandle, name);
if (!sym)
return -EINVAL;
*symp = sym;
}
return 0;
}
int
go_sequoia_ensure_library (const char *soname, int flags)
{
int err;
if (!go_sequoia_dlhandle)
{
go_sequoia_dlhandle = dlopen (soname, flags);
if (!go_sequoia_dlhandle)
return -EINVAL;
}
#define ENSURE_SYMBOL(name) \
ensure_symbol(#name, (void **)&go_sequoia_sym_##name)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-macros"
#define FUNC(ret, name, args, cargs) \
err = ENSURE_SYMBOL(name); \
if (err < 0) \
{ \
dlclose (go_sequoia_dlhandle); \
go_sequoia_dlhandle = NULL; \
return err; \
}
#define VOID_FUNC FUNC
#include "gosequoiafuncs.h"
#undef VOID_FUNC
#undef FUNC
#pragma GCC diagnostic pop
#undef ENSURE_SYMBOL
return 0;
}
void
go_sequoia_unload_library (void)
{
if (go_sequoia_dlhandle)
{
dlclose (go_sequoia_dlhandle);
go_sequoia_dlhandle = NULL;
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-macros"
#define FUNC(ret, name, args, cargs) \
go_sequoia_sym_##name = NULL;
#define VOID_FUNC FUNC
#include "gosequoiafuncs.h"
#undef VOID_FUNC
#undef FUNC
#pragma GCC diagnostic pop
}
unsigned
go_sequoia_is_usable (void)
{
return go_sequoia_dlhandle != NULL;
}
#else /* GO_SEQUOIA_ENABLE_DLOPEN */
int
go_sequoia_ensure_library (const char *soname, int flags)
{
(void) soname;
(void) flags;
return 0;
}
void
go_sequoia_unload_library (void)
{
}
unsigned
go_sequoia_is_usable (void)
{
/* The library is linked at build time, thus always usable */
return 1;
}
#endif /* !GO_SEQUOIA_ENABLE_DLOPEN */

View File

@@ -0,0 +1,54 @@
/*
* Copying and distribution of this file, with or without modification,
* are permitted in any medium without royalty provided the copyright
* notice and this notice are preserved. This file is offered as-is,
* without any warranty.
*/
#ifndef GO_SEQUOIA_H_
#define GO_SEQUOIA_H_
#include <sequoia.h>
#if defined(GO_SEQUOIA_ENABLE_DLOPEN) && GO_SEQUOIA_ENABLE_DLOPEN
#define FUNC(ret, name, args, cargs) \
ret go_##name args;
#define VOID_FUNC FUNC
#include "gosequoiafuncs.h"
#undef VOID_FUNC
#undef FUNC
#define GO_SEQUOIA_FUNC(name) go_##name
#else
#define GO_SEQUOIA_FUNC(name) name
#endif /* GO_SEQUOIA_ENABLE_DLOPEN */
/* Ensure SONAME to be loaded with dlopen FLAGS, and all the necessary
* symbols are resolved.
*
* Returns 0 on success; negative error code otherwise.
*
* Note that this function is NOT thread-safe; when calling it from
* multi-threaded programs, protect it with a locking mechanism.
*/
int go_sequoia_ensure_library (const char *soname, int flags);
/* Unload library and reset symbols.
*
* Note that this function is NOT thread-safe; when calling it from
* multi-threaded programs, protect it with a locking mechanism.
*/
void go_sequoia_unload_library (void);
/* Return 1 if the library is loaded and usable.
*
* Note that this function is NOT thread-safe; when calling it from
* multi-threaded programs, protect it with a locking mechanism.
*/
unsigned go_sequoia_is_usable (void);
#endif /* GO_SEQUOIA_H_ */

View File

@@ -0,0 +1,21 @@
/*
* This file was automatically generated from sequoia.h,
* which is covered by the following license:
* SPDX-License-Identifier: Apache-2.0
*/
VOID_FUNC(void, sequoia_error_free, (struct SequoiaError *err_ptr), (err_ptr))
FUNC(struct SequoiaMechanism *, sequoia_mechanism_new_from_directory, (const char *dir_ptr, struct SequoiaError **err_ptr), (dir_ptr, err_ptr))
FUNC(struct SequoiaMechanism *, sequoia_mechanism_new_ephemeral, (struct SequoiaError **err_ptr), (err_ptr))
VOID_FUNC(void, sequoia_mechanism_free, (struct SequoiaMechanism *mechanism_ptr), (mechanism_ptr))
VOID_FUNC(void, sequoia_signature_free, (struct SequoiaSignature *signature_ptr), (signature_ptr))
FUNC(const uint8_t *, sequoia_signature_get_data, (const struct SequoiaSignature *signature_ptr, size_t *data_len), (signature_ptr, data_len))
VOID_FUNC(void, sequoia_verification_result_free, (struct SequoiaVerificationResult *result_ptr), (result_ptr))
FUNC(const uint8_t *, sequoia_verification_result_get_content, (const struct SequoiaVerificationResult *result_ptr, size_t *data_len), (result_ptr, data_len))
FUNC(const char *, sequoia_verification_result_get_signer, (const struct SequoiaVerificationResult *result_ptr), (result_ptr))
FUNC(struct SequoiaSignature *, sequoia_sign, (struct SequoiaMechanism *mechanism_ptr, const char *key_handle_ptr, const char *password_ptr, const uint8_t *data_ptr, size_t data_len, struct SequoiaError **err_ptr), (mechanism_ptr, key_handle_ptr, password_ptr, data_ptr, data_len, err_ptr))
FUNC(struct SequoiaVerificationResult *, sequoia_verify, (struct SequoiaMechanism *mechanism_ptr, const uint8_t *signature_ptr, size_t signature_len, struct SequoiaError **err_ptr), (mechanism_ptr, signature_ptr, signature_len, err_ptr))
VOID_FUNC(void, sequoia_import_result_free, (struct SequoiaImportResult *result_ptr), (result_ptr))
FUNC(size_t, sequoia_import_result_get_count, (const struct SequoiaImportResult *result_ptr), (result_ptr))
FUNC(const char *, sequoia_import_result_get_content, (const struct SequoiaImportResult *result_ptr, size_t index, struct SequoiaError **err_ptr), (result_ptr, index, err_ptr))
FUNC(struct SequoiaImportResult *, sequoia_import_keys, (struct SequoiaMechanism *mechanism_ptr, const uint8_t *blob_ptr, size_t blob_len, struct SequoiaError **err_ptr), (mechanism_ptr, blob_ptr, blob_len, err_ptr))
FUNC(int, sequoia_set_logger_consumer, (void (*consumer)(enum SequoiaLogLevel, const char *), struct SequoiaError **err_ptr), (consumer, err_ptr))

View File

@@ -0,0 +1,223 @@
//go:build containers_image_sequoia
package sequoia
// #cgo CFLAGS: -I. -DGO_SEQUOIA_ENABLE_DLOPEN=1
// #include "gosequoia.h"
// #include <dlfcn.h>
// #include <limits.h>
// typedef void (*sequoia_logger_consumer_t) (enum SequoiaLogLevel level, char *message);
// extern void sequoia_logrus_logger (enum SequoiaLogLevel level, char *message);
import "C"
import (
"errors"
"fmt"
"path/filepath"
"runtime"
"sync"
"unsafe"
"github.com/sirupsen/logrus"
)
// sequoiaLibraryDir is the path to the directory where libpodman_sequoia is installed,
// if it is not in the platforms default library path.
// You can override this at build time with
// -ldflags '-X github.com/containers/image/v5/signature/sequoia.sequoiaLibraryDir=$your_path'
var sequoiaLibraryDir = ""
type SigningMechanism struct {
mechanism *C.SequoiaMechanism
}
// NewMechanismFromDirectory initializes a mechanism using (user-managed) Sequoia state
// in dir, which can be "" to indicate the default (using $SEQUOIA_HOME or the default home directory location).
func NewMechanismFromDirectory(
dir string,
) (*SigningMechanism, error) {
var cerr *C.SequoiaError
var cDir *C.char
if dir != "" {
cDir = C.CString(dir)
defer C.free(unsafe.Pointer(cDir))
}
cMechanism := C.go_sequoia_mechanism_new_from_directory(cDir, &cerr)
if cMechanism == nil {
defer C.go_sequoia_error_free(cerr)
return nil, errors.New(C.GoString(cerr.message))
}
return &SigningMechanism{
mechanism: cMechanism,
}, nil
}
func NewEphemeralMechanism() (*SigningMechanism, error) {
var cerr *C.SequoiaError
cMechanism := C.go_sequoia_mechanism_new_ephemeral(&cerr)
if cMechanism == nil {
defer C.go_sequoia_error_free(cerr)
return nil, errors.New(C.GoString(cerr.message))
}
return &SigningMechanism{
mechanism: cMechanism,
}, nil
}
func (m *SigningMechanism) SignWithPassphrase(
input []byte,
keyIdentity string,
passphrase string,
) ([]byte, error) {
var cerr *C.SequoiaError
var cPassphrase *C.char
if passphrase == "" {
cPassphrase = nil
} else {
cPassphrase = C.CString(passphrase)
defer C.free(unsafe.Pointer(cPassphrase))
}
cKeyIdentity := C.CString(keyIdentity)
defer C.free(unsafe.Pointer(cKeyIdentity))
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") // Coverage: This should not reasonably happen, and we dont want to generate gigabytes of input to test this.
}
return C.GoBytes(unsafe.Pointer(cData), C.int(size)), nil
}
func (m *SigningMechanism) Sign(
input []byte,
keyIdentity string,
) ([]byte, error) {
return m.SignWithPassphrase(input, keyIdentity, "")
}
func (m *SigningMechanism) Verify(
unverifiedSignature []byte,
) (contents []byte, keyIdentity string, err error) {
var cerr *C.SequoiaError
result := C.go_sequoia_verify(
m.mechanism,
(*C.uchar)(unsafe.Pointer(unsafe.SliceData(unverifiedSignature))),
C.size_t(len(unverifiedSignature)),
&cerr,
)
if result == nil {
defer C.go_sequoia_error_free(cerr)
return nil, "", errors.New(C.GoString(cerr.message))
}
defer C.go_sequoia_verification_result_free(result)
var size C.size_t
cContent := C.go_sequoia_verification_result_get_content(result, &size)
if size > C.size_t(C.INT_MAX) {
return nil, "", errors.New("overflow") // Coverage: This should not reasonably happen, and we dont want to generate gigabytes of input to test this.
}
contents = C.GoBytes(unsafe.Pointer(cContent), C.int(size))
cSigner := C.go_sequoia_verification_result_get_signer(result)
keyIdentity = C.GoString(cSigner)
return contents, keyIdentity, nil
}
func (m *SigningMechanism) ImportKeys(blob []byte) ([]string, error) {
var cerr *C.SequoiaError
result := C.go_sequoia_import_keys(
m.mechanism,
(*C.uchar)(unsafe.Pointer(unsafe.SliceData(blob))),
C.size_t(len(blob)),
&cerr,
)
if result == nil {
defer C.go_sequoia_error_free(cerr)
return nil, errors.New(C.GoString(cerr.message))
}
defer C.go_sequoia_import_result_free(result)
keyIdentities := []string{}
count := C.go_sequoia_import_result_get_count(result)
for i := C.size_t(0); i < count; i++ {
var cerr *C.SequoiaError
cKeyIdentity := C.go_sequoia_import_result_get_content(result, i, &cerr)
if cerr != nil {
defer C.go_sequoia_error_free(cerr) // Coverage: this can fail only if i is out of range.
return nil, errors.New(C.GoString(cerr.message))
}
keyIdentities = append(keyIdentities, C.GoString(cKeyIdentity))
}
return keyIdentities, nil
}
func (m *SigningMechanism) Close() error {
C.go_sequoia_mechanism_free(m.mechanism)
return nil
}
//export sequoia_logrus_logger
func sequoia_logrus_logger(level C.enum_SequoiaLogLevel, message *C.char) {
var logrusLevel logrus.Level
switch level { // Coverage: We are not in control of whether / how the Rust code chooses to log things.
case C.SEQUOIA_LOG_LEVEL_ERROR:
logrusLevel = logrus.ErrorLevel
case C.SEQUOIA_LOG_LEVEL_WARN:
logrusLevel = logrus.WarnLevel
case C.SEQUOIA_LOG_LEVEL_INFO:
logrusLevel = logrus.InfoLevel
case C.SEQUOIA_LOG_LEVEL_DEBUG:
logrusLevel = logrus.DebugLevel
case C.SEQUOIA_LOG_LEVEL_TRACE:
logrusLevel = logrus.TraceLevel
case C.SEQUOIA_LOG_LEVEL_UNKNOWN:
fallthrough
default:
logrusLevel = logrus.ErrorLevel // Should never happen
}
logrus.StandardLogger().Log(logrusLevel, C.GoString(message))
}
// initOnce should only be called by Init.
func initOnce() error {
var soName string
switch runtime.GOOS {
case "linux":
soName = "libpodman_sequoia.so.0"
case "darwin":
soName = "libpodman_sequoia.dylib"
default:
return fmt.Errorf("Unhandled OS %q in sequoia initialization", runtime.GOOS) // Coverage: This is ~by definition not reached in tests.
}
if sequoiaLibraryDir != "" {
soName = filepath.Join(sequoiaLibraryDir, soName)
}
cSOName := C.CString(soName)
defer C.free(unsafe.Pointer(cSOName))
if C.go_sequoia_ensure_library(cSOName,
C.RTLD_NOW|C.RTLD_GLOBAL) < 0 {
return fmt.Errorf("unable to load %q", soName) // Coverage: This is impractical to test in-process, with the static go_sequoia_dlhandle.
}
var cerr *C.SequoiaError
if C.go_sequoia_set_logger_consumer(C.sequoia_logger_consumer_t(C.sequoia_logrus_logger), &cerr) != 0 {
defer C.go_sequoia_error_free(cerr) // Coverage: This is impractical to test in-process, with the static go_sequoia_dlhandle.
return fmt.Errorf("initializing logging: %s", C.GoString(cerr.message))
}
return nil
}
// Init ensures the libpodman_sequoia library is available.
// It is safe to call from arbitrary goroutines.
var Init = sync.OnceValue(initOnce)

View File

@@ -0,0 +1,85 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
typedef enum SequoiaErrorKind {
SEQUOIA_ERROR_KIND_UNKNOWN,
SEQUOIA_ERROR_KIND_INVALID_ARGUMENT,
SEQUOIA_ERROR_KIND_IO_ERROR,
} SequoiaErrorKind;
typedef enum SequoiaLogLevel {
SEQUOIA_LOG_LEVEL_UNKNOWN,
SEQUOIA_LOG_LEVEL_ERROR,
SEQUOIA_LOG_LEVEL_WARN,
SEQUOIA_LOG_LEVEL_INFO,
SEQUOIA_LOG_LEVEL_DEBUG,
SEQUOIA_LOG_LEVEL_TRACE,
} SequoiaLogLevel;
typedef struct SequoiaImportResult SequoiaImportResult;
typedef struct SequoiaMechanism SequoiaMechanism;
typedef struct SequoiaSignature SequoiaSignature;
typedef struct SequoiaVerificationResult SequoiaVerificationResult;
typedef struct SequoiaError {
enum SequoiaErrorKind kind;
char *message;
} SequoiaError;
void sequoia_error_free(struct SequoiaError *err_ptr);
struct SequoiaMechanism *sequoia_mechanism_new_from_directory(const char *dir_ptr,
struct SequoiaError **err_ptr);
struct SequoiaMechanism *sequoia_mechanism_new_ephemeral(struct SequoiaError **err_ptr);
void sequoia_mechanism_free(struct SequoiaMechanism *mechanism_ptr);
void sequoia_signature_free(struct SequoiaSignature *signature_ptr);
const uint8_t *sequoia_signature_get_data(const struct SequoiaSignature *signature_ptr,
size_t *data_len);
void sequoia_verification_result_free(struct SequoiaVerificationResult *result_ptr);
const uint8_t *sequoia_verification_result_get_content(const struct SequoiaVerificationResult *result_ptr,
size_t *data_len);
const char *sequoia_verification_result_get_signer(const struct SequoiaVerificationResult *result_ptr);
struct SequoiaSignature *sequoia_sign(struct SequoiaMechanism *mechanism_ptr,
const char *key_handle_ptr,
const char *password_ptr,
const uint8_t *data_ptr,
size_t data_len,
struct SequoiaError **err_ptr);
struct SequoiaVerificationResult *sequoia_verify(struct SequoiaMechanism *mechanism_ptr,
const uint8_t *signature_ptr,
size_t signature_len,
struct SequoiaError **err_ptr);
void sequoia_import_result_free(struct SequoiaImportResult *result_ptr);
size_t sequoia_import_result_get_count(const struct SequoiaImportResult *result_ptr);
const char *sequoia_import_result_get_content(const struct SequoiaImportResult *result_ptr,
size_t index,
struct SequoiaError **err_ptr);
struct SequoiaImportResult *sequoia_import_keys(struct SequoiaMechanism *mechanism_ptr,
const uint8_t *blob_ptr,
size_t blob_len,
struct SequoiaError **err_ptr);
int sequoia_set_logger_consumer(void (*consumer)(enum SequoiaLogLevel level, const char *message),
struct SequoiaError **err_ptr);

View File

@@ -27,7 +27,10 @@ type SigningMechanism interface {
// Sign creates a (non-detached) signature of input using keyIdentity.
// Fails with a SigningNotSupportedError if the mechanism does not support signing.
Sign(input []byte, keyIdentity string) ([]byte, error)
// Verify parses unverifiedSignature and returns the content and the signer's identity
// Verify parses unverifiedSignature and returns the content and the signer's identity.
// For mechanisms created using NewEphemeralGPGSigningMechanism, the returned key identity
// is expected to be one of the values returned by NewEphemeralGPGSigningMechanism,
// or the mechanism should implement signingMechanismWithVerificationIdentityLookup.
Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error)
// UntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION,
// along with a short identifier of the key used for signing.
@@ -46,6 +49,16 @@ type signingMechanismWithPassphrase interface {
SignWithPassphrase(input []byte, keyIdentity string, passphrase string) ([]byte, error)
}
// signingMechanismWithVerificationIdentityLookup is an internal extension of SigningMechanism.
type signingMechanismWithVerificationIdentityLookup interface {
SigningMechanism
// keyIdentityForVerificationKeyIdentity re-checks the key identity returned by Verify
// if it doesn't match an identity returned by NewEphemeralGPGSigningMechanism, trying to match it.
// (To be more specific, for mechanisms which return a subkey fingerprint from Verify,
// this converts the subkey fingerprint into the corresponding primary key fingerprint.)
keyIdentityForVerificationKeyIdentity(keyIdentity string) (string, error)
}
// SigningNotSupportedError is returned when trying to sign using a mechanism which does not support that.
type SigningNotSupportedError string

View File

@@ -2,6 +2,9 @@
package signature
// This is shared by mechanism_gpgme_only.go and mechanism_sequoia.go; in both situations
// newGPGSigningMechanismInDirectory is implemented using GPGME.
import (
"bytes"
"errors"
@@ -18,6 +21,16 @@ type gpgmeSigningMechanism struct {
ephemeralDir string // If not "", a directory to be removed on Close()
}
// newGPGMESigningMechanism returns a new GPG/OpenPGP signing mechanism for ctx.
// The caller must call .Close() on the returned SigningMechanism; if ephemeralDir is set,
// the .Close() call will remove its contents.
func newGPGMESigningMechanism(ctx *gpgme.Context, ephemeralDir string) signingMechanismWithPassphrase {
return &gpgmeSigningMechanism{
ctx: ctx,
ephemeralDir: ephemeralDir,
}
}
// newGPGSigningMechanismInDirectory returns a new GPG/OpenPGP signing mechanism, using optionalDir if not empty.
// The caller must call .Close() on the returned SigningMechanism.
func newGPGSigningMechanismInDirectory(optionalDir string) (signingMechanismWithPassphrase, error) {
@@ -25,46 +38,7 @@ func newGPGSigningMechanismInDirectory(optionalDir string) (signingMechanismWith
if err != nil {
return nil, err
}
return &gpgmeSigningMechanism{
ctx: ctx,
ephemeralDir: "",
}, nil
}
// newEphemeralGPGSigningMechanism returns a new GPG/OpenPGP signing mechanism which
// recognizes _only_ public keys from the supplied blobs, and returns the identities
// of these keys.
// The caller must call .Close() on the returned SigningMechanism.
func newEphemeralGPGSigningMechanism(blobs [][]byte) (signingMechanismWithPassphrase, []string, error) {
dir, err := os.MkdirTemp("", "containers-ephemeral-gpg-")
if err != nil {
return nil, nil, err
}
removeDir := true
defer func() {
if removeDir {
os.RemoveAll(dir)
}
}()
ctx, err := newGPGMEContext(dir)
if err != nil {
return nil, nil, err
}
mech := &gpgmeSigningMechanism{
ctx: ctx,
ephemeralDir: dir,
}
keyIdentities := []string{}
for _, blob := range blobs {
ki, err := mech.importKeysFromBytes(blob)
if err != nil {
return nil, nil, err
}
keyIdentities = append(keyIdentities, ki...)
}
removeDir = false
return mech, keyIdentities, nil
return newGPGMESigningMechanism(ctx, ""), nil
}
// newGPGMEContext returns a new *gpgme.Context, using optionalDir if not empty.
@@ -94,28 +68,6 @@ func (m *gpgmeSigningMechanism) Close() error {
return nil
}
// importKeysFromBytes imports public keys from the supplied blob and returns their identities.
// The blob is assumed to have an appropriate format (the caller is expected to know which one).
// NOTE: This may modify long-term state (e.g. key storage in a directory underlying the mechanism);
// but we do not make this public, it can only be used through newEphemeralGPGSigningMechanism.
func (m *gpgmeSigningMechanism) importKeysFromBytes(blob []byte) ([]string, error) {
inputData, err := gpgme.NewDataBytes(blob)
if err != nil {
return nil, err
}
res, err := m.ctx.Import(inputData)
if err != nil {
return nil, err
}
keyIdentities := []string{}
for _, i := range res.Imports {
if i.Result == nil {
keyIdentities = append(keyIdentities, i.Fingerprint)
}
}
return keyIdentities, nil
}
// SupportsSigning returns nil if the mechanism supports signing, or a SigningNotSupportedError.
func (m *gpgmeSigningMechanism) SupportsSigning() error {
return nil
@@ -169,7 +121,10 @@ func (m *gpgmeSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte,
return m.SignWithPassphrase(input, keyIdentity, "")
}
// Verify parses unverifiedSignature and returns the content and the signer's identity
// Verify parses unverifiedSignature and returns the content and the signer's identity.
// For mechanisms created using NewEphemeralGPGSigningMechanism, the returned key identity
// is expected to be one of the values returned by NewEphemeralGPGSigningMechanism,
// or the mechanism should implement signingMechanismWithVerificationIdentityLookup.
func (m *gpgmeSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) {
signedBuffer := bytes.Buffer{}
signedData, err := gpgme.NewDataWriter(&signedBuffer)
@@ -196,6 +151,24 @@ func (m *gpgmeSigningMechanism) Verify(unverifiedSignature []byte) (contents []b
return signedBuffer.Bytes(), sig.Fingerprint, nil
}
// keyIdentityForVerificationKeyIdentity re-checks the key identity returned by Verify
// if it doesn't match an identity returned by NewEphemeralGPGSigningMechanism, trying to match it.
// (To be more specific, for mechanisms which return a subkey fingerprint from Verify,
// this converts the subkey fingerprint into the corresponding primary key fingerprint.)
func (m *gpgmeSigningMechanism) keyIdentityForVerificationKeyIdentity(keyIdentity string) (string, error) {
// In theory, if keyIdentity refers to a subkey, the same subkey could be attached to different primary keys;
// in that case, GetKey fails with “ambiguous name”.
// We _could_ handle that, by using KeyList* (GetKey is internally just a helper for KeyList*), but sharing
// a subkey that way is very unexpected, so, for now, prefer the much simpler implementation.
key, err := m.ctx.GetKey(keyIdentity, false)
if err != nil {
return "", err
}
// In theory this value could be nil if (gpg --list-keys --with-colons) misses a "pub:" line
// or a "fpr:" line, but gpg (in recent enough versions) prints that unconditionally. // codespell:ignore fpr
return key.Fingerprint(), nil
}
// UntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION,
// along with a short identifier of the key used for signing.
// WARNING: The short key identifier (which corresponds to "Key ID" for OpenPGP keys)

View File

@@ -0,0 +1,64 @@
//go:build !containers_image_openpgp && !containers_image_sequoia
package signature
import (
"os"
"github.com/proglottis/gpgme"
)
// newEphemeralGPGSigningMechanism returns a new GPG/OpenPGP signing mechanism which
// recognizes _only_ public keys from the supplied blobs, and returns the identities
// of these keys.
// The caller must call .Close() on the returned SigningMechanism.
func newEphemeralGPGSigningMechanism(blobs [][]byte) (signingMechanismWithPassphrase, []string, error) {
dir, err := os.MkdirTemp("", "containers-ephemeral-gpg-")
if err != nil {
return nil, nil, err
}
removeDir := true
defer func() {
if removeDir {
os.RemoveAll(dir)
}
}()
ctx, err := newGPGMEContext(dir)
if err != nil {
return nil, nil, err
}
keyIdentities := []string{}
for _, blob := range blobs {
ki, err := importKeysFromBytes(ctx, blob)
if err != nil {
return nil, nil, err
}
keyIdentities = append(keyIdentities, ki...)
}
mech := newGPGMESigningMechanism(ctx, dir)
removeDir = false
return mech, keyIdentities, nil
}
// importKeysFromBytes imports public keys from the supplied blob and returns their identities.
// The blob is assumed to have an appropriate format (the caller is expected to know which one).
// NOTE: This may modify long-term state (e.g. key storage in a directory underlying the mechanism);
// but we do not make this public, it can only be used through newEphemeralGPGSigningMechanism.
func importKeysFromBytes(ctx *gpgme.Context, blob []byte) ([]string, error) {
inputData, err := gpgme.NewDataBytes(blob)
if err != nil {
return nil, err
}
res, err := ctx.Import(inputData)
if err != nil {
return nil, err
}
keyIdentities := []string{}
for _, i := range res.Imports {
if i.Result == nil {
keyIdentities = append(keyIdentities, i.Fingerprint)
}
}
return keyIdentities, nil
}

View File

@@ -127,7 +127,10 @@ func (m *openpgpSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte
return m.SignWithPassphrase(input, keyIdentity, "")
}
// Verify parses unverifiedSignature and returns the content and the signer's identity
// Verify parses unverifiedSignature and returns the content and the signer's identity.
// For mechanisms created using NewEphemeralGPGSigningMechanism, the returned key identity
// is expected to be one of the values returned by NewEphemeralGPGSigningMechanism,
// or the mechanism should implement signingMechanismWithVerificationIdentityLookup.
func (m *openpgpSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) {
md, err := openpgp.ReadMessage(bytes.NewReader(unverifiedSignature), m.keyring, nil, nil)
if err != nil {
@@ -166,7 +169,7 @@ func (m *openpgpSigningMechanism) Verify(unverifiedSignature []byte) (contents [
}
// Uppercase the fingerprint to be compatible with gpgme
return content, strings.ToUpper(fmt.Sprintf("%x", md.SignedBy.PublicKey.Fingerprint)), nil
return content, strings.ToUpper(fmt.Sprintf("%x", md.SignedBy.Entity.PrimaryKey.Fingerprint)), nil
}
// UntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION,

View File

@@ -0,0 +1,84 @@
//go:build containers_image_sequoia
package signature
import (
"github.com/containers/image/v5/signature/internal/sequoia"
)
// A GPG/OpenPGP signing mechanism, implemented using Sequoia and only supporting verification.
// Legacy users who reach newGPGSigningMechanismInDirectory will use GPGME.
// Signing using Sequoia is preferable, but should happen via signature/simplesequoia.NewSigner, not using
// the legacy mechanism API.
type sequoiaEphemeralSigningMechanism struct {
inner *sequoia.SigningMechanism
}
// newEphemeralGPGSigningMechanism returns a new GPG/OpenPGP signing mechanism which
// recognizes _only_ public keys from the supplied blobs, and returns the identities
// of these keys.
// The caller must call .Close() on the returned SigningMechanism.
func newEphemeralGPGSigningMechanism(blobs [][]byte) (signingMechanismWithPassphrase, []string, error) {
if err := sequoia.Init(); err != nil {
return nil, nil, err // Coverage: This is impractical to test in-process, with the static go_sequoia_dlhandle.
}
mech, err := sequoia.NewEphemeralMechanism()
if err != nil {
return nil, nil, err
}
keyIdentities := []string{}
for _, blob := range blobs {
ki, err := mech.ImportKeys(blob)
if err != nil {
return nil, nil, err
}
keyIdentities = append(keyIdentities, ki...)
}
return &sequoiaEphemeralSigningMechanism{
inner: mech,
}, keyIdentities, nil
}
func (m *sequoiaEphemeralSigningMechanism) Close() error {
return m.inner.Close()
}
// SupportsSigning returns nil if the mechanism supports signing, or a SigningNotSupportedError.
func (m *sequoiaEphemeralSigningMechanism) SupportsSigning() error {
// This code is externally reachable via NewEphemeralGPGSigningMechanism(), but that API provides no way to
// import or generate a key.
return SigningNotSupportedError("caller error: Attempt to sign using a mechanism created via NewEphemeralGPGSigningMechanism().")
}
// Sign creates a (non-detached) signature of input using keyIdentity and passphrase.
// Fails with a SigningNotSupportedError if the mechanism does not support signing.
func (m *sequoiaEphemeralSigningMechanism) SignWithPassphrase(input []byte, keyIdentity string, passphrase string) ([]byte, error) {
// This code is externally reachable via NewEphemeralGPGSigningMechanism(), but that API provides no way to
// import or generate a key.
return nil, SigningNotSupportedError("caller error: Attempt to sign using a mechanism created via NewEphemeralGPGSigningMechanism().")
}
// Sign creates a (non-detached) signature of input using keyIdentity.
// Fails with a SigningNotSupportedError if the mechanism does not support signing.
func (m *sequoiaEphemeralSigningMechanism) Sign(input []byte, keyIdentity string) ([]byte, error) {
return m.SignWithPassphrase(input, keyIdentity, "")
}
// Verify parses unverifiedSignature and returns the content and the signer's identity.
// For mechanisms created using NewEphemeralGPGSigningMechanism, the returned key identity
// is expected to be one of the values returned by NewEphemeralGPGSigningMechanism,
// or the mechanism should implement signingMechanismWithVerificationIdentityLookup.
func (m *sequoiaEphemeralSigningMechanism) Verify(unverifiedSignature []byte) (contents []byte, keyIdentity string, err error) {
return m.inner.Verify(unverifiedSignature)
}
// UntrustedSignatureContents returns UNTRUSTED contents of the signature WITHOUT ANY VERIFICATION,
// along with a short identifier of the key used for signing.
// WARNING: The short key identifier (which corresponds to "Key ID" for OpenPGP keys)
// is NOT the same as a "key identity" used in other calls to this interface, and
// the values may have no recognizable relationship if the public key is not available.
func (m *sequoiaEphemeralSigningMechanism) UntrustedSignatureContents(untrustedSignature []byte) (untrustedContents []byte, shortKeyIdentifier string, err error) {
return gpgUntrustedSignatureContents(untrustedSignature)
}

View File

@@ -6,7 +6,6 @@ import (
"context"
"errors"
"fmt"
"slices"
"github.com/containers/image/v5/internal/multierr"
"github.com/containers/image/v5/internal/private"
@@ -50,15 +49,8 @@ func (pr *prSignedBy) isSignatureAuthorAccepted(ctx context.Context, image priva
return sarRejected, nil, PolicyRequirementError("No public keys imported")
}
signature, err := verifyAndExtractSignature(mech, sig, signatureAcceptanceRules{
validateKeyIdentity: func(keyIdentity string) error {
if slices.Contains(trustedIdentities, keyIdentity) {
return nil
}
// Coverage: We use a private GPG home directory and only import trusted keys, so this should
// not be reachable.
return PolicyRequirementError(fmt.Sprintf("Signature by key %s is not accepted", keyIdentity))
},
signature, _, err := verifyAndExtractSignature(mech, sig, signatureAcceptanceRules{
acceptedKeyIdentities: trustedIdentities,
validateSignedDockerReference: func(ref string) error {
if !pr.SignedIdentity.matchesDockerReference(image, ref) {
return PolicyRequirementError(fmt.Sprintf("Signature for identity %q is not accepted", ref))

View File

@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"time"
"github.com/containers/image/v5/signature/internal"
@@ -207,37 +208,52 @@ func (s untrustedSignature) sign(mech SigningMechanism, keyIdentity string, pass
// because the functions have the same or similar types, so there is a risk of exchanging the functions;
// named members of this struct are more explicit.
type signatureAcceptanceRules struct {
validateKeyIdentity func(string) error
acceptedKeyIdentities []string
validateSignedDockerReference func(string) error
validateSignedDockerManifestDigest func(digest.Digest) error
}
// verifyAndExtractSignature verifies that unverifiedSignature has been signed, and that its principal components
// match expected values, both as specified by rules, and returns it
func verifyAndExtractSignature(mech SigningMechanism, unverifiedSignature []byte, rules signatureAcceptanceRules) (*Signature, error) {
// match expected values, both as specified by rules.
// Returns the signature, and an identity of the key that signed it.
func verifyAndExtractSignature(mech SigningMechanism, unverifiedSignature []byte, rules signatureAcceptanceRules) (*Signature, string, error) {
signed, keyIdentity, err := mech.Verify(unverifiedSignature)
if err != nil {
return nil, err
return nil, "", err
}
if err := rules.validateKeyIdentity(keyIdentity); err != nil {
return nil, err
if !slices.Contains(rules.acceptedKeyIdentities, keyIdentity) {
withLookup, ok := mech.(signingMechanismWithVerificationIdentityLookup)
if !ok {
return nil, "", internal.NewInvalidSignatureError(fmt.Sprintf("signature by key %s is not accepted", keyIdentity))
}
primaryKey, err := withLookup.keyIdentityForVerificationKeyIdentity(keyIdentity)
if err != nil {
// Coverage: This only fails if lookup by keyIdentity fails, but we just found and used that key.
// Or maybe on some unexpected I/O error.
return nil, "", err
}
if !slices.Contains(rules.acceptedKeyIdentities, primaryKey) {
return nil, "", internal.NewInvalidSignatureError(fmt.Sprintf("signature by key %s of %s is not accepted", keyIdentity, primaryKey))
}
keyIdentity = primaryKey
}
var unmatchedSignature untrustedSignature
if err := json.Unmarshal(signed, &unmatchedSignature); err != nil {
return nil, internal.NewInvalidSignatureError(err.Error())
return nil, "", internal.NewInvalidSignatureError(err.Error())
}
if err := rules.validateSignedDockerManifestDigest(unmatchedSignature.untrustedDockerManifestDigest); err != nil {
return nil, err
return nil, "", err
}
if err := rules.validateSignedDockerReference(unmatchedSignature.untrustedDockerReference); err != nil {
return nil, err
return nil, "", err
}
// signatureAcceptanceRules have accepted this value.
return &Signature{
DockerManifestDigest: unmatchedSignature.untrustedDockerManifestDigest,
DockerReference: unmatchedSignature.untrustedDockerReference,
}, nil
}, keyIdentity, nil
}
// GetUntrustedSignatureInformationWithoutVerifying extracts information available in an untrusted signature,

4
vendor/modules.txt vendored
View File

@@ -79,7 +79,7 @@ github.com/containers/common/pkg/password
github.com/containers/common/pkg/report
github.com/containers/common/pkg/report/camelcase
github.com/containers/common/pkg/retry
# github.com/containers/image/v5 v5.36.2-0.20250807195248-f8cef395fcde
# github.com/containers/image/v5 v5.36.2-0.20250807195248-f8cef395fcde => github.com/mtrmac/image/v5 v5.0.0-20250807201425-55d1eaf519bd
## explicit; go 1.23.3
github.com/containers/image/v5/copy
github.com/containers/image/v5/directory
@@ -137,6 +137,7 @@ github.com/containers/image/v5/pkg/tlsclientconfig
github.com/containers/image/v5/sif
github.com/containers/image/v5/signature
github.com/containers/image/v5/signature/internal
github.com/containers/image/v5/signature/internal/sequoia
github.com/containers/image/v5/signature/signer
github.com/containers/image/v5/signature/sigstore
github.com/containers/image/v5/signature/sigstore/fulcio
@@ -711,3 +712,4 @@ google.golang.org/protobuf/types/known/timestamppb
# gopkg.in/yaml.v3 v3.0.1
## explicit
gopkg.in/yaml.v3
# github.com/containers/image/v5 => github.com/mtrmac/image/v5 v5.0.0-20250807201425-55d1eaf519bd