implementation of encrypt/decrypt push/pull/bud/from

Signed-off-by: Brandon Lum <lumjjb@gmail.com>
This commit is contained in:
Brandon Lum 2020-04-01 18:15:56 +00:00
parent e895bd022d
commit 37df2b9690
28 changed files with 569 additions and 10 deletions

View File

@ -13,6 +13,7 @@ import (
"github.com/containers/buildah/docker"
"github.com/containers/image/v5/types"
encconfig "github.com/containers/ocicrypt/config"
"github.com/containers/storage"
"github.com/containers/storage/pkg/ioutils"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
@ -413,6 +414,9 @@ type BuilderOptions struct {
MaxPullRetries int
// PullRetryDelay is how long to wait before retrying a pull attempt.
PullRetryDelay time.Duration
// OciDecryptConfig contains the config that can be used to decrypt an image if it is
// encrypted if non-nil. If nil, it does not attempt to decrypt an image.
OciDecryptConfig *encconfig.DecryptConfig
}
// ImportOptions are used to initialize a Builder from an existing container

View File

@ -67,6 +67,7 @@ func init() {
// BUD is a all common flags
budFlags := buildahcli.GetBudFlags(&budFlagResults)
budFlags.StringVar(&budFlagResults.Runtime, "runtime", util.Runtime(), "`path` to an alternate runtime. Use BUILDAH_RUNTIME environment variable to override.")
flags.StringSliceVar(&budFlagResults.DecryptionKeys, "decryption-key", nil, "key needed to decrypt the image")
layerFlags := buildahcli.GetLayerFlags(&layerFlagsResults)
fromAndBudFlags, err := buildahcli.GetFromAndBudFlags(&fromAndBudResults, &userNSResults, &namespaceResults)
@ -295,6 +296,11 @@ func budCmd(c *cobra.Command, inputArgs []string, iopts budOptions) error {
return err
}
decConfig, err := getDecryptConfig(iopts.DecryptionKeys)
if err != nil {
return errors.Wrapf(err, "unable to obtain decrypt config")
}
options := imagebuildah.BuildOptions{
AddCapabilities: iopts.CapAdd,
AdditionalTags: tags,
@ -339,6 +345,7 @@ func budCmd(c *cobra.Command, inputArgs []string, iopts budOptions) error {
SystemContext: systemContext,
Target: iopts.Target,
TransientMounts: iopts.Volumes,
OciDecryptConfig: decConfig,
}
if iopts.Quiet {

View File

@ -11,6 +11,8 @@ import (
"github.com/containers/image/v5/manifest"
is "github.com/containers/image/v5/storage"
"github.com/containers/image/v5/types"
encconfig "github.com/containers/ocicrypt/config"
enchelpers "github.com/containers/ocicrypt/helpers"
"github.com/containers/storage"
"github.com/containers/storage/pkg/unshare"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
@ -411,6 +413,38 @@ func getFormat(format string) (string, error) {
}
}
func getDecryptConfig(decryptionKeys []string) (*encconfig.DecryptConfig, error) {
decConfig := &encconfig.DecryptConfig{}
if len(decryptionKeys) > 0 {
// decryption
dcc, err := enchelpers.CreateCryptoConfig([]string{}, decryptionKeys)
if err != nil {
return nil, errors.Wrapf(err, "invalid decryption keys")
}
cc := encconfig.CombineCryptoConfigs([]encconfig.CryptoConfig{dcc})
decConfig = cc.DecryptConfig
}
return decConfig, nil
}
func getEncryptConfig(encryptionKeys []string, encryptLayers []int) (*encconfig.EncryptConfig, *[]int, error) {
var encLayers *[]int
var encConfig *encconfig.EncryptConfig
if len(encryptionKeys) > 0 {
// encryption
encLayers = &encryptLayers
ecc, err := enchelpers.CreateCryptoConfig(encryptionKeys, []string{})
if err != nil {
return nil, nil, errors.Wrapf(err, "invalid encryption keys")
}
cc := encconfig.CombineCryptoConfigs([]encconfig.CryptoConfig{ecc})
encConfig = cc.EncryptConfig
}
return encConfig, encLayers, nil
}
// Tail returns a string slice after the first element unless there are
// not enough elements, then it returns an empty slice. This is to replace
// the urfavecli Tail method for args

View File

@ -31,6 +31,7 @@ type fromReply struct {
quiet bool
signaturePolicy string
tlsVerify bool
decryptionKeys []string
*buildahcli.FromAndBudResults
*buildahcli.UserNSResults
*buildahcli.NameSpaceResults
@ -73,6 +74,7 @@ func init() {
flags.BoolVar(&opts.pullAlways, "pull-always", false, "pull the image even if the named image is present in store")
flags.BoolVar(&opts.pullNever, "pull-never", false, "do not pull the image, use the image present in store if available")
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "don't output progress information when pulling images")
flags.StringSliceVar(&opts.decryptionKeys, "decryption-key", nil, "key needed to decrypt the image")
flags.StringVar(&opts.signaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)")
if err := flags.MarkHidden("signature-policy"); err != nil {
panic(fmt.Sprintf("error marking signature-policy as hidden: %v", err))
@ -267,6 +269,12 @@ func fromCmd(c *cobra.Command, args []string, iopts fromReply) error {
}
commonOpts.Ulimit = append(defaultContainerConfig.Containers.DefaultUlimits, commonOpts.Ulimit...)
decConfig, err := getDecryptConfig(iopts.decryptionKeys)
if err != nil {
return errors.Wrapf(err, "unable to obtain decrypt config")
}
options := buildah.BuilderOptions{
FromImage: args[0],
Container: iopts.name,
@ -288,6 +296,7 @@ func fromCmd(c *cobra.Command, args []string, iopts fromReply) error {
DefaultEnv: defaultContainerConfig.GetDefaultEnv(),
MaxPullRetries: maxPullPushRetries,
PullRetryDelay: pullPushRetryDelay,
OciDecryptConfig: decConfig,
}
if !iopts.quiet {

View File

@ -25,6 +25,7 @@ type pullOptions struct {
quiet bool
removeSignatures bool
tlsVerify bool
decryptionKeys []string
}
func init() {
@ -58,6 +59,7 @@ func init() {
flags.StringVar(&opts.creds, "creds", "", "use `[username[:password]]` for accessing the registry")
flags.BoolVarP(&opts.removeSignatures, "remove-signatures", "", false, "don't copy signatures when pulling image")
flags.StringVar(&opts.signaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)")
flags.StringSliceVar(&opts.decryptionKeys, "decryption-key", nil, "key needed to decrypt the image")
if err := flags.MarkHidden("signature-policy"); err != nil {
panic(fmt.Sprintf("error marking signature-policy as hidden: %v", err))
}
@ -102,6 +104,11 @@ func pullCmd(c *cobra.Command, args []string, iopts pullOptions) error {
return err
}
decConfig, err := getDecryptConfig(iopts.decryptionKeys)
if err != nil {
return errors.Wrapf(err, "unable to obtain decrypt config")
}
options := buildah.PullOptions{
SignaturePolicyPath: iopts.signaturePolicy,
Store: store,
@ -112,6 +119,7 @@ func pullCmd(c *cobra.Command, args []string, iopts pullOptions) error {
RemoveSignatures: iopts.removeSignatures,
MaxRetries: maxPullPushRetries,
RetryDelay: pullPushRetryDelay,
OciDecryptConfig: decConfig,
}
if iopts.quiet {

View File

@ -34,6 +34,8 @@ type pushOptions struct {
signaturePolicy string
signBy string
tlsVerify bool
encryptionKeys []string
encryptLayers []int
}
func init() {
@ -77,6 +79,9 @@ func init() {
flags.BoolVarP(&opts.removeSignatures, "remove-signatures", "", false, "don't copy signatures when pushing image")
flags.StringVar(&opts.signBy, "sign-by", "", "sign the image using a GPG key with the specified `FINGERPRINT`")
flags.StringVar(&opts.signaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)")
flags.StringSliceVar(&opts.encryptionKeys, "encryption-key", nil, "key with the encryption protocol to use needed to encrypt the image (e.g. jwe:/path/to/key.pem)")
flags.IntSliceVar(&opts.encryptLayers, "encrypt-layer", nil, "layers to encrypt, 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified")
if err := flags.MarkHidden("signature-policy"); err != nil {
panic(fmt.Sprintf("error marking signature-policy as hidden: %v", err))
}
@ -165,6 +170,11 @@ func pushCmd(c *cobra.Command, args []string, iopts pushOptions) error {
}
}
encConfig, encLayers, err := getEncryptConfig(iopts.encryptionKeys, iopts.encryptLayers)
if err != nil {
return errors.Wrapf(err, "unable to obtain encryption config")
}
options := buildah.PushOptions{
Compression: compress,
ManifestType: manifestType,
@ -176,6 +186,8 @@ func pushCmd(c *cobra.Command, args []string, iopts pushOptions) error {
SignBy: iopts.signBy,
MaxRetries: maxPullPushRetries,
RetryDelay: pullPushRetryDelay,
OciEncryptConfig: encConfig,
OciEncryptLayers: encLayers,
}
if !iopts.quiet {
options.ReportWriter = os.Stderr

View File

@ -19,6 +19,7 @@ import (
is "github.com/containers/image/v5/storage"
"github.com/containers/image/v5/transports"
"github.com/containers/image/v5/types"
encconfig "github.com/containers/ocicrypt/config"
"github.com/containers/storage"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/stringid"
@ -132,6 +133,19 @@ type PushOptions struct {
MaxRetries int
// RetryDelay is how long to wait before retrying a push attempt.
RetryDelay time.Duration
// OciEncryptConfig when non-nil indicates that an image should be encrypted.
// The encryption options is derived from the construction of EncryptConfig object.
// Note: During initial encryption process of a layer, the resultant digest is not known
// during creation, so newDigestingReader has to be set with validateDigest = false
OciEncryptConfig *encconfig.EncryptConfig
// OciEncryptLayers represents the list of layers to encrypt.
// If nil, don't encrypt any layers.
// If non-nil and len==0, denotes encrypt all layers.
// integers in the slice represent 0-indexed layer indices, with support for negative
// indexing. i.e. 0 is the first layer, -1 is the last (top-most) layer.
OciEncryptLayers *[]int
}
var (
@ -319,7 +333,7 @@ func (b *Builder) Commit(ctx context.Context, dest types.ImageReference, options
}
var manifestBytes []byte
if manifestBytes, err = retryCopyImage(ctx, policyContext, maybeCachedDest, maybeCachedSrc, dest, "push", getCopyOptions(b.store, options.ReportWriter, nil, systemContext, "", false, options.SignBy), options.MaxRetries, options.RetryDelay); err != nil {
if manifestBytes, err = retryCopyImage(ctx, policyContext, maybeCachedDest, maybeCachedSrc, dest, "push", getCopyOptions(b.store, options.ReportWriter, nil, systemContext, "", false, options.SignBy, nil, nil, nil), options.MaxRetries, options.RetryDelay); err != nil {
return imgID, nil, "", errors.Wrapf(err, "error copying layers and metadata for container %q", b.ContainerID)
}
// If we've got more names to attach, and we know how to do that for
@ -451,7 +465,7 @@ func Push(ctx context.Context, image string, dest types.ImageReference, options
systemContext.DirForceCompress = true
}
var manifestBytes []byte
if manifestBytes, err = retryCopyImage(ctx, policyContext, dest, maybeCachedSrc, dest, "push", getCopyOptions(options.Store, options.ReportWriter, nil, systemContext, options.ManifestType, options.RemoveSignatures, options.SignBy), options.MaxRetries, options.RetryDelay); err != nil {
if manifestBytes, err = retryCopyImage(ctx, policyContext, dest, maybeCachedSrc, dest, "push", getCopyOptions(options.Store, options.ReportWriter, nil, systemContext, options.ManifestType, options.RemoveSignatures, options.SignBy, options.OciEncryptLayers, options.OciEncryptConfig, nil), options.MaxRetries, options.RetryDelay); err != nil {
return nil, "", errors.Wrapf(err, "error copying layers and metadata from %q to %q", transports.ImageName(maybeCachedSrc), transports.ImageName(dest))
}
if options.ReportWriter != nil {

View File

@ -14,6 +14,7 @@ import (
"github.com/containers/image/v5/docker"
"github.com/containers/image/v5/signature"
"github.com/containers/image/v5/types"
encconfig "github.com/containers/ocicrypt/config"
"github.com/containers/storage"
"github.com/containers/storage/pkg/unshare"
"github.com/docker/distribution/registry/api/errcode"
@ -30,7 +31,7 @@ const (
DOCKER = "docker"
)
func getCopyOptions(store storage.Store, reportWriter io.Writer, sourceSystemContext *types.SystemContext, destinationSystemContext *types.SystemContext, manifestType string, removeSignatures bool, addSigner string) *cp.Options {
func getCopyOptions(store storage.Store, reportWriter io.Writer, sourceSystemContext *types.SystemContext, destinationSystemContext *types.SystemContext, manifestType string, removeSignatures bool, addSigner string, ociEncryptLayers *[]int, ociEncryptConfig *encconfig.EncryptConfig, ociDecryptConfig *encconfig.DecryptConfig) *cp.Options {
sourceCtx := getSystemContext(store, nil, "")
if sourceSystemContext != nil {
*sourceCtx = *sourceSystemContext
@ -47,6 +48,9 @@ func getCopyOptions(store storage.Store, reportWriter io.Writer, sourceSystemCon
ForceManifestMIMEType: manifestType,
RemoveSignatures: removeSignatures,
SignBy: addSigner,
OciEncryptConfig: ociEncryptConfig,
OciDecryptConfig: ociDecryptConfig,
OciEncryptLayers: ociEncryptLayers,
}
}

View File

@ -392,6 +392,7 @@ return 1
--cpuset-cpus
--cpuset-mems
--creds
--decryption-key
--device
--dns-search
--dns
@ -588,6 +589,7 @@ return 1
--authfile
--cert-dir
--creds
--decryption-key
"
local all_options="$options_with_args $boolean_options"
@ -615,6 +617,8 @@ return 1
--authfile
--cert-dir
--creds
--encrypt-layer
--encryption-key
--format
-f
--sign-by

View File

@ -178,6 +178,10 @@ The [username[:password]] to use to authenticate with the registry if required.
If one or both values are not supplied, a command line prompt will appear and the
value can be entered. The password is entered without echo.
**--decryption-key** *key*
A reference required to perform decryption of container images. This should point to files which represent keys and/or certificates that can be used for decryption. Decryption will be tried with all keys. This feature is still *experimental*.
**--device**=*device*
Add a host device or devices under a directory to the container. The format is `<device-on-host>[:<device-on-container>][:<permissions>]` (e.g. --device=/dev/sdc:/dev/xvdc:rwm)

View File

@ -168,6 +168,10 @@ The [username[:password]] to use to authenticate with the registry if required.
If one or both values are not supplied, a command line prompt will appear and the
value can be entered. The password is entered without echo.
**--decryption-key** *key*
A reference required to perform decryption of container images. This should point to files which represent keys and/or certificates that can be used for decryption. Decryption will be tried with all keys. This feature is still *experimental*.
**--device**=*device*
Add a host device or devices under a directory to the container. The format is `<device-on-host>[:<device-on-container>][:<permissions>]` (e.g. --device=/dev/sdc:/dev/xvdc:rwm)

View File

@ -62,6 +62,10 @@ The [username[:password]] to use to authenticate with the registry if required.
If one or both values are not supplied, a command line prompt will appear and the
value can be entered. The password is entered without echo.
**--decryption-key** *key*
A reference required to perform decryption of container images. This should point to files which represent keys and/or certificates that can be used for decryption. Decryption will be tried with all keys. This feature is still *experimental*.
**--quiet, -q**
If an image needs to be pulled from the registry, suppress progress output.

View File

@ -66,6 +66,10 @@ After copying the image, write the digest of the resulting image to the file.
Don't compress copies of filesystem layers which will be pushed.
**--encryption-key** *key*
A reference prefixed with the encryption protocol to use. The supported protocols are JWE, PGP and PKCS7. For instance, jwe:/path/to/key.pem or pgp:admin@example.com or pkcs7:/path/to/x509-file. This feature is still *experimental*.
**--format, -f**
Manifest Type (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is manifest type of source)

1
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/containernetworking/cni v0.7.2-0.20190904153231-83439463f784
github.com/containers/common v0.11.2
github.com/containers/image/v5 v5.4.4
github.com/containers/ocicrypt v1.0.2
github.com/containers/storage v1.19.2
github.com/cyphar/filepath-securejoin v0.2.2
github.com/docker/distribution v2.7.1+incompatible

2
go.sum
View File

@ -308,11 +308,13 @@ github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/seccomp/containers-golang v0.0.0-20190312124753-8ca8945ccf5f h1:OtU/w6sBKmXYaw2KEODxjcYi3oPSyyslhgGFgIJVGAI=
github.com/seccomp/containers-golang v0.0.0-20190312124753-8ca8945ccf5f/go.mod h1:f/98/SnvAzhAEFQJ3u836FePXvcbE8BS0YGMQNn4mhA=
github.com/seccomp/libseccomp-golang v0.9.1 h1:NJjM5DNFOs0s3kYE1WUOr6G8V97sdt46rlXTMfXGWBo=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=

View File

@ -16,6 +16,7 @@ import (
"github.com/containers/common/pkg/config"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/types"
encconfig "github.com/containers/ocicrypt/config"
"github.com/containers/storage"
"github.com/containers/storage/pkg/archive"
specs "github.com/opencontainers/runtime-spec/specs-go"
@ -171,6 +172,9 @@ type BuildOptions struct {
MaxPullPushRetries int
// PullPushRetryDelay is how long to wait before retrying a pull or push attempt.
PullPushRetryDelay time.Duration
// OciDecryptConfig contains the config that can be used to decrypt an image if it is
// encrypted if non-nil. If nil, it does not attempt to decrypt an image.
OciDecryptConfig *encconfig.DecryptConfig
}
// BuildDockerfiles parses a set of one or more Dockerfiles (which may be

View File

@ -20,6 +20,7 @@ import (
"github.com/containers/image/v5/transports"
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
encconfig "github.com/containers/ocicrypt/config"
"github.com/containers/storage"
"github.com/containers/storage/pkg/archive"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
@ -100,6 +101,7 @@ type Executor struct {
os string
maxPullPushRetries int
retryPullPushDelay time.Duration
ociDecryptConfig *encconfig.DecryptConfig
}
// NewExecutor creates a new instance of the imagebuilder.Executor interface.
@ -188,6 +190,7 @@ func NewExecutor(store storage.Store, options BuildOptions, mainNode *parser.Nod
os: options.OS,
maxPullPushRetries: options.MaxPullPushRetries,
retryPullPushDelay: options.PullPushRetryDelay,
ociDecryptConfig: options.OciDecryptConfig,
}
if exec.err == nil {
exec.err = os.Stderr

View File

@ -633,6 +633,7 @@ func (s *StageExecutor) prepare(ctx context.Context, from string, initializeIBCo
Devices: s.executor.devices,
MaxPullRetries: s.executor.maxPullPushRetries,
PullRetryDelay: s.executor.retryPullPushDelay,
OciDecryptConfig: s.executor.ociDecryptConfig,
}
// Check and see if the image is a pseudonym for the end result of a

13
new.go
View File

@ -30,12 +30,13 @@ const (
func pullAndFindImage(ctx context.Context, store storage.Store, srcRef types.ImageReference, options BuilderOptions, sc *types.SystemContext) (*storage.Image, types.ImageReference, error) {
pullOptions := PullOptions{
ReportWriter: options.ReportWriter,
Store: store,
SystemContext: options.SystemContext,
BlobDirectory: options.BlobDirectory,
MaxRetries: options.MaxPullRetries,
RetryDelay: options.PullRetryDelay,
ReportWriter: options.ReportWriter,
Store: store,
SystemContext: options.SystemContext,
BlobDirectory: options.BlobDirectory,
MaxRetries: options.MaxPullRetries,
RetryDelay: options.PullRetryDelay,
OciDecryptConfig: options.OciDecryptConfig,
}
ref, err := pullImage(ctx, store, srcRef, pullOptions, sc)
if err != nil {

View File

@ -57,6 +57,7 @@ type BudResults struct {
Creds string
DisableCompression bool
DisableContentTrust bool
DecryptionKeys []string
File []string
Format string
Iidfile string

View File

@ -19,6 +19,7 @@ import (
is "github.com/containers/image/v5/storage"
"github.com/containers/image/v5/transports"
"github.com/containers/image/v5/types"
encconfig "github.com/containers/ocicrypt/config"
"github.com/containers/storage"
multierror "github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
@ -56,6 +57,9 @@ type PullOptions struct {
MaxRetries int
// RetryDelay is how long to wait before retrying a pull attempt.
RetryDelay time.Duration
// OciDecryptConfig contains the config that can be used to decrypt an image if it is
// encrypted if non-nil. If nil, it does not attempt to decrypt an image.
OciDecryptConfig *encconfig.DecryptConfig
}
func localImageNameForReference(ctx context.Context, store storage.Store, srcRef types.ImageReference) (string, error) {
@ -164,6 +168,7 @@ func Pull(ctx context.Context, imageName string, options PullOptions) (imageID s
ReportWriter: options.ReportWriter,
MaxPullRetries: options.MaxRetries,
PullRetryDelay: options.RetryDelay,
OciDecryptConfig: options.OciDecryptConfig,
}
storageRef, transport, img, err := resolveImage(ctx, systemContext, options.Store, boptions)
@ -275,7 +280,7 @@ func pullImage(ctx context.Context, store storage.Store, srcRef types.ImageRefer
}()
logrus.Debugf("copying %q to %q", transports.ImageName(srcRef), destName)
if _, err := retryCopyImage(ctx, policyContext, maybeCachedDestRef, srcRef, srcRef, "pull", getCopyOptions(store, options.ReportWriter, sc, nil, "", options.RemoveSignatures, ""), options.MaxRetries, options.RetryDelay); err != nil {
if _, err := retryCopyImage(ctx, policyContext, maybeCachedDestRef, srcRef, srcRef, "pull", getCopyOptions(store, options.ReportWriter, sc, nil, "", options.RemoveSignatures, "", nil, nil, options.OciDecryptConfig), options.MaxRetries, options.RetryDelay); err != nil {
logrus.Debugf("error copying src image [%q] to dest image [%q] err: %v", transports.ImageName(srcRef), destName, err)
return nil, err
}

View File

@ -2079,3 +2079,22 @@ EOM
run grep "secretthings" <<< "$output"
expect_output ""
}
@test "bud with encrypted FROM image" {
_prefetch busybox
mkdir ${TESTDIR}/tmp
openssl genrsa -out ${TESTDIR}/tmp/mykey.pem 1024
openssl genrsa -out ${TESTDIR}/tmp/mykey2.pem 1024
openssl rsa -in ${TESTDIR}/tmp/mykey.pem -pubout > ${TESTDIR}/tmp/mykey.pub
run_buildah push --signature-policy ${TESTSDIR}/policy.json --tls-verify=false --creds testuser:testpassword --encryption-key jwe:${TESTDIR}/tmp/mykey.pub busybox docker://localhost:5000/buildah/busybox_encrypted:latest
target=busybox-image
# Try to build from encrypted image without key
run_buildah 1 bud --signature-policy ${TESTSDIR}/policy.json --tls-verify=false --creds testuser:testpassword -t ${target} -f ${TESTSDIR}/bud/from-encrypted-image/Dockerfile
# Try to build from encrypted image with wrong key
run_buildah 1 bud --signature-policy ${TESTSDIR}/policy.json --tls-verify=false --creds testuser:testpassword --decryption-key ${TESTDIR}/tmp/mykey2.pem -t ${target} -f ${TESTSDIR}/bud/from-encrypted-image/Dockerfile
# Try to build with the correct key
run_buildah bud --signature-policy ${TESTSDIR}/policy.json --tls-verify=false --creds testuser:testpassword --decryption-key ${TESTDIR}/tmp/mykey.pem -t ${target} -f ${TESTSDIR}/bud/from-encrypted-image/Dockerfile
rm -rf ${TESTDIR}/tmp
}

View File

@ -0,0 +1 @@
FROM localhost:5000/buildah/busybox_encrypted:latest

View File

@ -389,3 +389,40 @@ load helpers
run_buildah from --signature-policy ${TESTSDIR}/policy.json --quiet docker.io/busybox
expect_output "busybox-working-container"
}
@test "from encrypted local image" {
_prefetch busybox
mkdir ${TESTDIR}/tmp
openssl genrsa -out ${TESTDIR}/tmp/mykey.pem 1024
openssl genrsa -out ${TESTDIR}/tmp/mykey2.pem 1024
openssl rsa -in ${TESTDIR}/tmp/mykey.pem -pubout > ${TESTDIR}/tmp/mykey.pub
run_buildah push --signature-policy ${TESTSDIR}/policy.json --tls-verify=false --creds testuser:testpassword --encryption-key jwe:${TESTDIR}/tmp/mykey.pub busybox oci:${TESTDIR}/tmp/busybox_enc
# Try encrypted image without key should fail
run_buildah 1 from oci:${TESTDIR}/tmp/busybox_enc
# Try encrypted image with wrong key should fail
run_buildah 1 from --decryption-key ${TESTDIR}/tmp/mykey2.pem oci:${TESTDIR}/tmp/busybox_enc
# Providing the right key should succeed
run_buildah from --decryption-key ${TESTDIR}/tmp/mykey.pem oci:${TESTDIR}/tmp/busybox_enc
rm -rf ${TESTDIR}/tmp
}
@test "from encrypted registry image" {
_prefetch busybox
mkdir ${TESTDIR}/tmp
openssl genrsa -out ${TESTDIR}/tmp/mykey.pem 1024
openssl genrsa -out ${TESTDIR}/tmp/mykey2.pem 1024
openssl rsa -in ${TESTDIR}/tmp/mykey.pem -pubout > ${TESTDIR}/tmp/mykey.pub
run_buildah push --signature-policy ${TESTSDIR}/policy.json --tls-verify=false --creds testuser:testpassword --encryption-key jwe:${TESTDIR}/tmp/mykey.pub busybox docker://localhost:5000/buildah/busybox_encrypted:latest
# Try encrypted image without key should fail
run_buildah 1 from --tls-verify=false --creds testuser:testpassword docker://localhost:5000/buildah/busybox_encrypted:latest
# Try encrypted image with wrong key should fail
run_buildah 1 from --tls-verify=false --creds testuser:testpassword --decryption-key ${TESTDIR}/tmp/mykey2.pem docker://localhost:5000/buildah/busybox_encrypted:latest
# Providing the right key should succeed
run_buildah from --tls-verify=false --creds testuser:testpassword --decryption-key ${TESTDIR}/tmp/mykey.pem docker://localhost:5000/buildah/busybox_encrypted:latest
run_buildah rmi localhost:5000/buildah/busybox_encrypted:latest
rm -rf ${TESTDIR}/tmp
}

View File

@ -167,3 +167,40 @@ load helpers
@test "pull should fail with nonexist authfile" {
run_buildah 125 pull --authfile /tmp/nonexist --signature-policy ${TESTSDIR}/policy.json alpine
}
@test "pull encrypted local image" {
_prefetch busybox
mkdir ${TESTDIR}/tmp
openssl genrsa -out ${TESTDIR}/tmp/mykey.pem 1024
openssl genrsa -out ${TESTDIR}/tmp/mykey2.pem 1024
openssl rsa -in ${TESTDIR}/tmp/mykey.pem -pubout > ${TESTDIR}/tmp/mykey.pub
run_buildah push --signature-policy ${TESTSDIR}/policy.json --encryption-key jwe:${TESTDIR}/tmp/mykey.pub busybox oci:${TESTDIR}/tmp/busybox_enc
# Try to pull encrypted image without key should fail
run_buildah 1 pull --signature-policy ${TESTSDIR}/policy.json oci:${TESTDIR}/tmp/busybox_enc
# Try to pull encrypted image with wrong key should fail
run_buildah 1 pull --signature-policy ${TESTSDIR}/policy.json --decryption-key ${TESTDIR}/tmp/mykey2.pem oci:${TESTDIR}/tmp/busybox_enc
# Providing the right key should succeed
run_buildah pull --signature-policy ${TESTSDIR}/policy.json --decryption-key ${TESTDIR}/tmp/mykey.pem oci:${TESTDIR}/tmp/busybox_enc
rm -rf ${TESTDIR}/tmp
}
@test "pull encrypted registry image" {
_prefetch busybox
mkdir ${TESTDIR}/tmp
openssl genrsa -out ${TESTDIR}/tmp/mykey.pem 1024
openssl genrsa -out ${TESTDIR}/tmp/mykey2.pem 1024
openssl rsa -in ${TESTDIR}/tmp/mykey.pem -pubout > ${TESTDIR}/tmp/mykey.pub
run_buildah push --signature-policy ${TESTSDIR}/policy.json --tls-verify=false --creds testuser:testpassword --encryption-key jwe:${TESTDIR}/tmp/mykey.pub busybox docker://localhost:5000/buildah/busybox_encrypted:latest
# Try to pull encrypted image without key should fail
run_buildah 1 pull --signature-policy ${TESTSDIR}/policy.json --tls-verify=false --creds testuser:testpassword docker://localhost:5000/buildah/busybox_encrypted:latest
# Try to pull encrypted image with wrong key should fail
run_buildah 1 pull --signature-policy ${TESTSDIR}/policy.json --tls-verify=false --creds testuser:testpassword --decryption-key ${TESTDIR}/tmp/mykey2.pem docker://localhost:5000/buildah/busybox_encrypted:latest
# Providing the right key should succeed
run_buildah pull --signature-policy ${TESTSDIR}/policy.json --tls-verify=false --creds testuser:testpassword --decryption-key ${TESTDIR}/tmp/mykey.pem docker://localhost:5000/buildah/busybox_encrypted:latest
run_buildah rmi localhost:5000/buildah/busybox_encrypted:latest
rm -rf ${TESTDIR}/tmp
}

View File

@ -159,3 +159,24 @@ load helpers
docker rmi localhost:5000/buildah/busybox:latest
docker logout localhost:5000
}
@test "buildah oci encrypt and push local oci" {
_prefetch busybox
mkdir ${TESTDIR}/tmp
openssl genrsa -out ${TESTDIR}/tmp/mykey.pem 1024
openssl rsa -in ${TESTDIR}/tmp/mykey.pem -pubout > ${TESTDIR}/tmp/mykey.pub
run_buildah push --signature-policy ${TESTSDIR}/policy.json --encryption-key jwe:${TESTDIR}/tmp/mykey.pub busybox oci:${TESTDIR}/tmp/busybox_enc
imgtype -show-manifest oci:${TESTDIR}/tmp/busybox_enc | grep "+encrypted"
rm -rf ${TESTDIR}/tmp
}
@test "buildah oci encrypt and push registry" {
_prefetch busybox
mkdir ${TESTDIR}/tmp
openssl genrsa -out ${TESTDIR}/tmp/mykey.pem 1024
openssl rsa -in ${TESTDIR}/tmp/mykey.pem -pubout > ${TESTDIR}/tmp/mykey.pub
run_buildah push --signature-policy ${TESTSDIR}/policy.json --tls-verify=false --creds testuser:testpassword --encryption-key jwe:${TESTDIR}/tmp/mykey.pub busybox docker://localhost:5000/buildah/busybox_encrypted:latest
# this test, just checks the ability to push an image
# there is no good way to test the details of the image unless with ./buildah pull, test will be in pull.bats
rm -rf ${TESTDIR}/tmp
}

View File

@ -0,0 +1,313 @@
package helpers
import (
"context"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/containerd/containerd/platforms"
"github.com/containers/ocicrypt"
encconfig "github.com/containers/ocicrypt/config"
encutils "github.com/containers/ocicrypt/utils"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// processRecipientKeys sorts the array of recipients by type. Recipients may be either
// x509 certificates, public keys, or PGP public keys identified by email address or name
func processRecipientKeys(recipients []string) ([][]byte, [][]byte, [][]byte, error) {
var (
gpgRecipients [][]byte
pubkeys [][]byte
x509s [][]byte
)
for _, recipient := range recipients {
idx := strings.Index(recipient, ":")
if idx < 0 {
return nil, nil, nil, errors.New("Invalid recipient format")
}
protocol := recipient[:idx]
value := recipient[idx+1:]
switch protocol {
case "pgp":
gpgRecipients = append(gpgRecipients, []byte(value))
case "jwe":
tmp, err := ioutil.ReadFile(value)
if err != nil {
return nil, nil, nil, errors.Wrap(err, "Unable to read file")
}
if !encutils.IsPublicKey(tmp) {
return nil, nil, nil, errors.New("File provided is not a public key")
}
pubkeys = append(pubkeys, tmp)
case "pkcs7":
tmp, err := ioutil.ReadFile(value)
if err != nil {
return nil, nil, nil, errors.Wrap(err, "Unable to read file")
}
if !encutils.IsCertificate(tmp) {
return nil, nil, nil, errors.New("File provided is not an x509 cert")
}
x509s = append(x509s, tmp)
default:
return nil, nil, nil, errors.New("Provided protocol not recognized")
}
}
return gpgRecipients, pubkeys, x509s, nil
}
// processPwdString process a password that may be in any of the following formats:
// - file=<passwordfile>
// - pass=<password>
// - fd=<filedescriptor>
// - <password>
func processPwdString(pwdString string) ([]byte, error) {
if strings.HasPrefix(pwdString, "file=") {
return ioutil.ReadFile(pwdString[5:])
} else if strings.HasPrefix(pwdString, "pass=") {
return []byte(pwdString[5:]), nil
} else if strings.HasPrefix(pwdString, "fd=") {
fdStr := pwdString[3:]
fd, err := strconv.Atoi(fdStr)
if err != nil {
return nil, errors.Wrapf(err, "could not parse file descriptor %s", fdStr)
}
f := os.NewFile(uintptr(fd), "pwdfile")
if f == nil {
return nil, fmt.Errorf("%s is not a valid file descriptor", fdStr)
}
defer f.Close()
pwd := make([]byte, 64)
n, err := f.Read(pwd)
if err != nil {
return nil, errors.Wrapf(err, "could not read from file descriptor")
}
return pwd[:n], nil
}
return []byte(pwdString), nil
}
// processPrivateKeyFiles sorts the different types of private key files; private key files may either be
// private keys or GPG private key ring files. The private key files may include the password for the
// private key and take any of the following forms:
// - <filename>
// - <filename>:file=<passwordfile>
// - <filename>:pass=<password>
// - <filename>:fd=<filedescriptor>
// - <filename>:<password>
func processPrivateKeyFiles(keyFilesAndPwds []string) ([][]byte, [][]byte, [][]byte, [][]byte, error) {
var (
gpgSecretKeyRingFiles [][]byte
gpgSecretKeyPasswords [][]byte
privkeys [][]byte
privkeysPasswords [][]byte
err error
)
// keys needed for decryption in case of adding a recipient
for _, keyfileAndPwd := range keyFilesAndPwds {
var password []byte
parts := strings.Split(keyfileAndPwd, ":")
if len(parts) == 2 {
password, err = processPwdString(parts[1])
if err != nil {
return nil, nil, nil, nil, err
}
}
keyfile := parts[0]
tmp, err := ioutil.ReadFile(keyfile)
if err != nil {
return nil, nil, nil, nil, err
}
isPrivKey, err := encutils.IsPrivateKey(tmp, password)
if encutils.IsPasswordError(err) {
return nil, nil, nil, nil, err
}
if isPrivKey {
privkeys = append(privkeys, tmp)
privkeysPasswords = append(privkeysPasswords, password)
} else if encutils.IsGPGPrivateKeyRing(tmp) {
gpgSecretKeyRingFiles = append(gpgSecretKeyRingFiles, tmp)
gpgSecretKeyPasswords = append(gpgSecretKeyPasswords, password)
} else {
return nil, nil, nil, nil, fmt.Errorf("unidentified private key in file %s (password=%s)", keyfile, string(password))
}
}
return gpgSecretKeyRingFiles, gpgSecretKeyPasswords, privkeys, privkeysPasswords, nil
}
func createGPGClient(context context.Context) (ocicrypt.GPGClient, error) {
return ocicrypt.NewGPGClient(context.Value("gpg-version").(string), context.Value("gpg-homedir").(string))
}
func getGPGPrivateKeys(context context.Context, gpgSecretKeyRingFiles [][]byte, descs []ocispec.Descriptor, mustFindKey bool) (gpgPrivKeys [][]byte, gpgPrivKeysPwds [][]byte, err error) {
gpgClient, err := createGPGClient(context)
if err != nil {
return nil, nil, err
}
var gpgVault ocicrypt.GPGVault
if len(gpgSecretKeyRingFiles) > 0 {
gpgVault = ocicrypt.NewGPGVault()
err = gpgVault.AddSecretKeyRingDataArray(gpgSecretKeyRingFiles)
if err != nil {
return nil, nil, err
}
}
return ocicrypt.GPGGetPrivateKey(descs, gpgClient, gpgVault, mustFindKey)
}
// CreateDecryptCryptoConfig creates the CryptoConfig object that contains the necessary
// information to perform decryption from command line options and possibly
// LayerInfos describing the image and helping us to query for the PGP decryption keys
func CreateDecryptCryptoConfig(keys []string, decRecipients []string) (encconfig.CryptoConfig, error) {
ccs := []encconfig.CryptoConfig{}
// x509 cert is needed for PKCS7 decryption
_, _, x509s, err := processRecipientKeys(decRecipients)
if err != nil {
return encconfig.CryptoConfig{}, err
}
gpgSecretKeyRingFiles, gpgSecretKeyPasswords, privKeys, privKeysPasswords, err := processPrivateKeyFiles(keys)
if err != nil {
return encconfig.CryptoConfig{}, err
}
if len(gpgSecretKeyRingFiles) > 0 {
gpgCc, err := encconfig.DecryptWithGpgPrivKeys(gpgSecretKeyRingFiles, gpgSecretKeyPasswords)
if err != nil {
return encconfig.CryptoConfig{}, err
}
ccs = append(ccs, gpgCc)
}
/* TODO: Add in GPG client query for secret keys in the future.
_, err = createGPGClient(context)
gpgInstalled := err == nil
if gpgInstalled {
if len(gpgSecretKeyRingFiles) == 0 && len(privKeys) == 0 && descs != nil {
// Get pgp private keys from keyring only if no private key was passed
gpgPrivKeys, gpgPrivKeyPasswords, err := getGPGPrivateKeys(context, gpgSecretKeyRingFiles, descs, true)
if err != nil {
return encconfig.CryptoConfig{}, err
}
gpgCc, err := encconfig.DecryptWithGpgPrivKeys(gpgPrivKeys, gpgPrivKeyPasswords)
if err != nil {
return encconfig.CryptoConfig{}, err
}
ccs = append(ccs, gpgCc)
} else if len(gpgSecretKeyRingFiles) > 0 {
gpgCc, err := encconfig.DecryptWithGpgPrivKeys(gpgSecretKeyRingFiles, gpgSecretKeyPasswords)
if err != nil {
return encconfig.CryptoConfig{}, err
}
ccs = append(ccs, gpgCc)
}
}
*/
x509sCc, err := encconfig.DecryptWithX509s(x509s)
if err != nil {
return encconfig.CryptoConfig{}, err
}
ccs = append(ccs, x509sCc)
privKeysCc, err := encconfig.DecryptWithPrivKeys(privKeys, privKeysPasswords)
if err != nil {
return encconfig.CryptoConfig{}, err
}
ccs = append(ccs, privKeysCc)
return encconfig.CombineCryptoConfigs(ccs), nil
}
// parsePlatformArray parses an array of specifiers and converts them into an array of specs.Platform
func parsePlatformArray(specifiers []string) ([]ocispec.Platform, error) {
var speclist []ocispec.Platform
for _, specifier := range specifiers {
spec, err := platforms.Parse(specifier)
if err != nil {
return []ocispec.Platform{}, err
}
speclist = append(speclist, spec)
}
return speclist, nil
}
// CreateCryptoConfig from the list of recipient strings and list of key paths of private keys
func CreateCryptoConfig(recipients []string, keys []string) (encconfig.CryptoConfig, error) {
var decryptCc *encconfig.CryptoConfig
ccs := []encconfig.CryptoConfig{}
if len(keys) > 0 {
dcc, err := CreateDecryptCryptoConfig(keys, []string{})
if err != nil {
return encconfig.CryptoConfig{}, err
}
decryptCc = &dcc
ccs = append(ccs, dcc)
}
if len(recipients) > 0 {
gpgRecipients, pubKeys, x509s, err := processRecipientKeys(recipients)
if err != nil {
return encconfig.CryptoConfig{}, err
}
encryptCcs := []encconfig.CryptoConfig{}
// Create GPG client with guessed GPG version and default homedir
gpgClient, err := ocicrypt.NewGPGClient("", "")
gpgInstalled := err == nil
if len(gpgRecipients) > 0 && gpgInstalled {
gpgPubRingFile, err := gpgClient.ReadGPGPubRingFile()
if err != nil {
return encconfig.CryptoConfig{}, err
}
gpgCc, err := encconfig.EncryptWithGpg(gpgRecipients, gpgPubRingFile)
if err != nil {
return encconfig.CryptoConfig{}, err
}
encryptCcs = append(encryptCcs, gpgCc)
}
// Create Encryption Crypto Config
pkcs7Cc, err := encconfig.EncryptWithPkcs7(x509s)
if err != nil {
return encconfig.CryptoConfig{}, err
}
encryptCcs = append(encryptCcs, pkcs7Cc)
jweCc, err := encconfig.EncryptWithJwe(pubKeys)
if err != nil {
return encconfig.CryptoConfig{}, err
}
encryptCcs = append(encryptCcs, jweCc)
ecc := encconfig.CombineCryptoConfigs(encryptCcs)
if decryptCc != nil {
ecc.EncryptConfig.AttachDecryptConfig(decryptCc.DecryptConfig)
}
ccs = append(ccs, ecc)
}
if len(ccs) > 0 {
return encconfig.CombineCryptoConfigs(ccs), nil
} else {
return encconfig.CryptoConfig{}, nil
}
}

1
vendor/modules.txt vendored
View File

@ -108,6 +108,7 @@ github.com/containers/libtrust
github.com/containers/ocicrypt
github.com/containers/ocicrypt/blockcipher
github.com/containers/ocicrypt/config
github.com/containers/ocicrypt/helpers
github.com/containers/ocicrypt/keywrap
github.com/containers/ocicrypt/keywrap/jwe
github.com/containers/ocicrypt/keywrap/pgp