Version 5.22 introduced a new option to /etc/containers/policy.json called
keyPaths, see
https://github.com/containers/image/pull/1609
EL9 immediately took advantage of this new feature and started using it, see
04645c4a84
This quickly became an issue in our code: The go library (containers/image)
parses the configuration file very strictly and refuses to create a client
when policy.json with an unknown key is present on the filesystem. As we
used 5.21.1 that doesn't know the new key, our unit tests started to
failing when containers-common was present.
Reproducer:
podman run --pull=always --rm -it centos:stream9
dnf install -y dnf-plugins-core
dnf config-manager --set-enabled crb
dnf install -y gpgme-devel libassuan-devel krb5-devel golang git-core
git clone https://github.com/osbuild/osbuild-composer
cd osbuild-composer
# install the new containers-common and run the test
dnf install -y https://kojihub.stream.centos.org/kojifiles/packages/containers-common/1/44.el9/x86_64/containers-common-1-44.el9.x86_64.rpm
go test -count 1 ./...
# this returns:
--- FAIL: TestClientResolve (0.00s)
client_test.go:31:
Error Trace: client_test.go:31
Error: Received unexpected error:
Unknown key "keyPaths"
invalid policy in "/etc/containers/policy.json"
github.com/containers/image/v5/signature.NewPolicyFromFile
/osbuild-composer/vendor/github.com/containers/image/v5/signature/policy_config.go:88
github.com/osbuild/osbuild-composer/internal/container.NewClient
/osbuild-composer/internal/container/client.go:123
github.com/osbuild/osbuild-composer/internal/container_test.TestClientResolve
/osbuild-composer/internal/container/client_test.go:29
testing.tRunner
/usr/lib/golang/src/testing/testing.go:1439
runtime.goexit
/usr/lib/golang/src/runtime/asm_amd64.s:1571
Test: TestClientResolve
client_test.go:32:
Error Trace: client_test.go:32
Error: Expected value not to be nil.
Test: TestClientResolve
When run with an older containers-common, it succeeds:
dnf install -y https://kojihub.stream.centos.org/kojifiles/packages/containers-common/1/40.el9/x86_64/containers-common-1-40.el9.x86_64.rpm
go test -count 1 ./...
PASS
To sum it up, I had to upgrade github.com/containers/image/v5 to v5.22.0.
Unfortunately, this wasn't so simple, see
go get github.com/containers/image/v5@latest
go: github.com/containers/image/v5@v5.22.0 requires
github.com/letsencrypt/boulder@v0.0.0-20220331220046-b23ab962616e requires
github.com/honeycombio/beeline-go@v1.1.1 requires
github.com/gobuffalo/pop/v5@v5.3.1 requires
github.com/mattn/go-sqlite3@v2.0.3+incompatible: reading github.com/mattn/go-sqlite3/go.mod at revision v2.0.3: unknown revision v2.0.3
It turns out that github.com/mattn/go-sqlite3@v2.0.3+incompatible has been
recently retracted https://github.com/mattn/go-sqlite3/pull/998 and this
broke a ton of packages depending on it. I was able to fix it by adding
exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
to our go.mod, see
https://github.com/mattn/go-sqlite3/issues/975#issuecomment-955661657
After adding it,
go get github.com/containers/image/v5@latest
succeeded and tools/prepare-source.sh took care of the rest.
Signed-off-by: Ondřej Budai <ondrej@budai.cz>
129 lines
4 KiB
Go
129 lines
4 KiB
Go
package copy
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/containers/image/v5/types"
|
|
"github.com/containers/ocicrypt"
|
|
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
// isOciEncrypted returns a bool indicating if a mediatype is encrypted
|
|
// This function will be moved to be part of OCI spec when adopted.
|
|
func isOciEncrypted(mediatype string) bool {
|
|
return strings.HasSuffix(mediatype, "+encrypted")
|
|
}
|
|
|
|
// isEncrypted checks if an image is encrypted
|
|
func isEncrypted(i types.Image) bool {
|
|
layers := i.LayerInfos()
|
|
for _, l := range layers {
|
|
if isOciEncrypted(l.MediaType) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// bpDecryptionStepData contains data that the copy pipeline needs about the decryption step.
|
|
type bpDecryptionStepData struct {
|
|
decrypting bool // We are actually decrypting the stream
|
|
}
|
|
|
|
// blobPipelineDecryptionStep updates *stream to decrypt if, it necessary.
|
|
// srcInfo is only used for error messages.
|
|
// Returns data for other steps; the caller should eventually use updateCryptoOperation.
|
|
func (c *copier) blobPipelineDecryptionStep(stream *sourceStream, srcInfo types.BlobInfo) (*bpDecryptionStepData, error) {
|
|
if isOciEncrypted(stream.info.MediaType) && c.ociDecryptConfig != nil {
|
|
desc := imgspecv1.Descriptor{
|
|
Annotations: stream.info.Annotations,
|
|
}
|
|
reader, decryptedDigest, err := ocicrypt.DecryptLayer(c.ociDecryptConfig, stream.reader, desc, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decrypting layer %s: %w", srcInfo.Digest, err)
|
|
}
|
|
|
|
stream.reader = reader
|
|
stream.info.Digest = decryptedDigest
|
|
stream.info.Size = -1
|
|
for k := range stream.info.Annotations {
|
|
if strings.HasPrefix(k, "org.opencontainers.image.enc") {
|
|
delete(stream.info.Annotations, k)
|
|
}
|
|
}
|
|
return &bpDecryptionStepData{
|
|
decrypting: true,
|
|
}, nil
|
|
}
|
|
return &bpDecryptionStepData{
|
|
decrypting: false,
|
|
}, nil
|
|
}
|
|
|
|
// updateCryptoOperation sets *operation, if necessary.
|
|
func (d *bpDecryptionStepData) updateCryptoOperation(operation *types.LayerCrypto) {
|
|
if d.decrypting {
|
|
*operation = types.Decrypt
|
|
}
|
|
}
|
|
|
|
// bpdData contains data that the copy pipeline needs about the encryption step.
|
|
type bpEncryptionStepData struct {
|
|
encrypting bool // We are actually encrypting the stream
|
|
finalizer ocicrypt.EncryptLayerFinalizer
|
|
}
|
|
|
|
// blobPipelineEncryptionStep updates *stream to encrypt if, it required by toEncrypt.
|
|
// srcInfo is primarily used for error messages.
|
|
// Returns data for other steps; the caller should eventually call updateCryptoOperationAndAnnotations.
|
|
func (c *copier) blobPipelineEncryptionStep(stream *sourceStream, toEncrypt bool, srcInfo types.BlobInfo,
|
|
decryptionStep *bpDecryptionStepData) (*bpEncryptionStepData, error) {
|
|
if toEncrypt && !isOciEncrypted(srcInfo.MediaType) && c.ociEncryptConfig != nil {
|
|
var annotations map[string]string
|
|
if !decryptionStep.decrypting {
|
|
annotations = srcInfo.Annotations
|
|
}
|
|
desc := imgspecv1.Descriptor{
|
|
MediaType: srcInfo.MediaType,
|
|
Digest: srcInfo.Digest,
|
|
Size: srcInfo.Size,
|
|
Annotations: annotations,
|
|
}
|
|
reader, finalizer, err := ocicrypt.EncryptLayer(c.ociEncryptConfig, stream.reader, desc)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encrypting blob %s: %w", srcInfo.Digest, err)
|
|
}
|
|
|
|
stream.reader = reader
|
|
stream.info.Digest = ""
|
|
stream.info.Size = -1
|
|
return &bpEncryptionStepData{
|
|
encrypting: true,
|
|
finalizer: finalizer,
|
|
}, nil
|
|
}
|
|
return &bpEncryptionStepData{
|
|
encrypting: false,
|
|
}, nil
|
|
}
|
|
|
|
// updateCryptoOperationAndAnnotations sets *operation and updates *annotations, if necessary.
|
|
func (d *bpEncryptionStepData) updateCryptoOperationAndAnnotations(operation *types.LayerCrypto, annotations *map[string]string) error {
|
|
if !d.encrypting {
|
|
return nil
|
|
}
|
|
|
|
encryptAnnotations, err := d.finalizer()
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to finalize encryption: %w", err)
|
|
}
|
|
*operation = types.Encrypt
|
|
if *annotations == nil {
|
|
*annotations = map[string]string{}
|
|
}
|
|
for k, v := range encryptAnnotations {
|
|
(*annotations)[k] = v
|
|
}
|
|
return nil
|
|
}
|