Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions pkg/verify/configmap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package verify

import (
"bytes"
"fmt"
"net/url"
"strings"

"sigs.k8s.io/yaml"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/pkg/errors"
"golang.org/x/crypto/openpgp"
"k8s.io/klog"
)

// ReleaseAnnotationConfigMapVerifier is an annotation set on a config map in the
// release payload to indicate that this config map controls signing for the payload.
// Only the first config map within the payload should be used, regardless of whether
// it has data. See NewFromConfigMapData for more.
const ReleaseAnnotationConfigMapVerifier = "release.openshift.io/verification-config-map"

// NamespaceLabelConfigMap is the Namespace label applied to a configmap
// containing signatures.
const NamespaceLabelConfigMap = "openshift-config-managed"

// ReleaseLabelConfigMap is a label applied to a configmap inside the
// NamespaceLabelConfigMap namespace that indicates it contains signatures
// for release image digests. Any binaryData key that starts with the digest
// is added to the list of signatures checked.
const ReleaseLabelConfigMap = "release.openshift.io/verification-signatures"

// digestToKeyPrefix changes digest to use '-' in place of ':',
// {algo}-{hash} instead of {algo}:{hash}, because colons are not
// allowed in ConfigMap keys.
func digestToKeyPrefix(digest string) (string, error) {
parts := strings.SplitN(digest, ":", 3)
if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
return "", fmt.Errorf("the provided digest must be of the form ALGO:HASH")
}
algo, hash := parts[0], parts[1]
return fmt.Sprintf("%s-%s", algo, hash), nil
}

// GetSignaturesAsConfigmap returns the given signatures in a configmap. Uses
// digestToKeyPrefix to replace colon with dash when saving digest to configmap.
func GetSignaturesAsConfigmap(digest string, signatures [][]byte) (*corev1.ConfigMap, error) {
cm := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1.SchemeGroupVersion.String(),
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: NamespaceLabelConfigMap,
Labels: map[string]string{
ReleaseLabelConfigMap: "",
},
},
BinaryData: make(map[string][]byte),
}
prefix, err := digestToKeyPrefix(digest)
if err != nil {
return nil, err
}
cm.Name = prefix
for i := 0; i < len(signatures); i++ {
cm.BinaryData[fmt.Sprintf("%s-%d", prefix, i+1)] = signatures[i]
}
return cm, nil
}

// GetSignaturesAsConfigmapBytes stores the given signatures as a configmap and
// and returns configmap as bytes. Uses digestToKeyPrefix to replace colon with
// dash when saving digest to configmap.
func GetSignaturesAsConfigmapBytes(digest string, signatures [][]byte) ([]byte, error) {
var cm *corev1.ConfigMap
cm, err := GetSignaturesAsConfigmap(digest, signatures)
if err != nil {
return nil, err
}
if cmData, err := yaml.Marshal(cm); err == nil {
return cmData, nil
} else {
return nil, fmt.Errorf("failed to create %s manifest: %v", digest, err)
}
}

// NewFromConfigMapData expects to receive the data field of the first config map in the release
// image payload with the annotation "release.openshift.io/verification-config-map". Only the
// first payload item in lexographic order will be considered - all others are ignored. The
// verifier returned by this method
//
// The presence of one or more config maps instructs the CVO to verify updates before they are
// downloaded.
//
// The keys within the config map in the data field define how verification is performed:
//
// verifier-public-key-*: One or more GPG public keys in ASCII form that must have signed the
// release image by digest.
//
// store-*: A URL (scheme file://, http://, or https://) location that contains signatures. These
// signatures are in the atomic container signature format. The URL will have the digest
// of the image appended to it as "<STORE>/<ALGO>=<DIGEST>/signature-<NUMBER>" as described
// in the container image signing format. The docker-image-manifest section of the
// signature must match the release image digest. Signatures are searched starting at
// NUMBER 1 and incrementing if the signature exists but is not valid. The signature is a
// GPG signed and encrypted JSON message. The file store is provided for testing only at
// the current time, although future versions of the CVO might allow host mounting of
// signatures.
//
// See https://github.com/containers/image/blob/ab49b0a48428c623a8f03b41b9083d48966b34a9/docs/signature-protocols.md
// for a description of the signature store
//
// The returned verifier will require that any new release image will only be considered verified
// if each provided public key has signed the release image digest. The signature may be in any
// store and the lookup order is internally defined.
func NewFromConfigMapData(src string, data map[string]string, clientBuilder ClientBuilder) (*ReleaseVerifier, error) {
verifiers := make(map[string]openpgp.EntityList)
var stores []*url.URL
for k, v := range data {
switch {
case strings.HasPrefix(k, "verifier-public-key-"):
keyring, err := loadArmoredOrUnarmoredGPGKeyRing([]byte(v))
if err != nil {
return nil, errors.Wrapf(err, "%s has an invalid key %q that must be a GPG public key: %v", src, k, err)
}
verifiers[k] = keyring
case strings.HasPrefix(k, "store-"):
v = strings.TrimSpace(v)
u, err := url.Parse(v)
if err != nil || (u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "file") {
return nil, fmt.Errorf("%s has an invalid key %q: must be a valid URL with scheme file://, http://, or https://", src, k)
}
stores = append(stores, u)
default:
klog.Warningf("An unexpected key was found in %s and will be ignored (expected store-* or verifier-public-key-*): %s", src, k)
}
}
if len(stores) == 0 {
return nil, fmt.Errorf("%s did not provide any signature stores to read from and cannot be used", src)
}
if len(verifiers) == 0 {
return nil, fmt.Errorf("%s did not provide any GPG public keys to verify signatures from and cannot be used", src)
}

return NewReleaseVerifier(verifiers, stores, clientBuilder), nil
}

func loadArmoredOrUnarmoredGPGKeyRing(data []byte) (openpgp.EntityList, error) {
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(data))
if err == nil {
return keyring, nil
}
return openpgp.ReadKeyRing(bytes.NewReader(data))
}
78 changes: 78 additions & 0 deletions pkg/verify/configmap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package verify

import (
"io/ioutil"
"path/filepath"
"testing"

"golang.org/x/crypto/openpgp"
)

type VerifierAccessor interface {
Verifiers() map[string]openpgp.EntityList
}

func Test_loadReleaseVerifierFromConfigMap(t *testing.T) {
redhatData, err := ioutil.ReadFile(filepath.Join("testdata", "keyrings", "redhat.txt"))
if err != nil {
t.Fatal(err)
}

tests := []struct {
name string
data map[string]string
want bool
wantErr bool
wantVerifiers int
}{
{
name: "requires data",
data: nil,
wantErr: true,
},
{
name: "requires stores",
data: map[string]string{
"verifier-public-key-redhat": string(redhatData),
},
wantErr: true,
},
{
name: "requires verifiers",
data: map[string]string{
"store-local": "file://../testdata/signatures",
},
wantErr: true,
},
{
name: "loads valid configuration",
data: map[string]string{
"verifier-public-key-redhat": string(redhatData),
"store-local": "file://../testdata/signatures",
},
want: true,
wantVerifiers: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewFromConfigMapData("from_test", tt.data, DefaultClient)
if (err != nil) != tt.wantErr {
t.Fatalf("loadReleaseVerifierFromPayload() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil != tt.want {
t.Fatal(got)
}
if err != nil {
return
}
if got == nil {
return
}
if len(got.Verifiers()) != tt.wantVerifiers {
t.Fatalf("unexpected release verifier: %#v", got)
}
})
}
}
56 changes: 56 additions & 0 deletions pkg/verify/testdata/keyrings/combined.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2.0.22 (GNU/Linux)
Comment: Use "gpg --dearmor" for unpacking

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
8+4iI63dvDLJWLtu
=8lmj
-----END PGP PUBLIC KEY BLOCK-----
34 changes: 34 additions & 0 deletions pkg/verify/testdata/keyrings/redhat.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
pub 4096R/FD431D51 2009-10-22
Key fingerprint = 567E 347A D004 4ADE 55BA 8A5F 199E 2F91 FD43 1D51
uid Red Hat, Inc. (release key 2) <security@redhat.com>

-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.5 (GNU/Linux)

mQINBErgSTsBEACh2A4b0O9t+vzC9VrVtL1AKvUWi9OPCjkvR7Xd8DtJxeeMZ5eF
0HtzIG58qDRybwUe89FZprB1ffuUKzdE+HcL3FbNWSSOXVjZIersdXyH3NvnLLLF
0DNRB2ix3bXG9Rh/RXpFsNxDp2CEMdUvbYCzE79K1EnUTVh1L0Of023FtPSZXX0c
u7Pb5DI5lX5YeoXO6RoodrIGYJsVBQWnrWw4xNTconUfNPk0EGZtEnzvH2zyPoJh
XGF+Ncu9XwbalnYde10OCvSWAZ5zTCpoLMTvQjWpbCdWXJzCm6G+/hx9upke546H
5IjtYm4dTIVTnc3wvDiODgBKRzOl9rEOCIgOuGtDxRxcQkjrC+xvg5Vkqn7vBUyW
9pHedOU+PoF3DGOM+dqv+eNKBvh9YF9ugFAQBkcG7viZgvGEMGGUpzNgN7XnS1gj
/DPo9mZESOYnKceve2tIC87p2hqjrxOHuI7fkZYeNIcAoa83rBltFXaBDYhWAKS1
PcXS1/7JzP0ky7d0L6Xbu/If5kqWQpKwUInXtySRkuraVfuK3Bpa+X1XecWi24JY
HVtlNX025xx1ewVzGNCTlWn1skQN2OOoQTV4C8/qFpTW6DTWYurd4+fE0OJFJZQF
buhfXYwmRlVOgN5i77NTIJZJQfYFj38c/Iv5vZBPokO6mffrOTv3MHWVgQARAQAB
tDNSZWQgSGF0LCBJbmMuIChyZWxlYXNlIGtleSAyKSA8c2VjdXJpdHlAcmVkaGF0
LmNvbT6JAjYEEwECACAFAkrgSTsCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAK
CRAZni+R/UMdUWzpD/9s5SFR/ZF3yjY5VLUFLMXIKUztNN3oc45fyLdTI3+UClKC
2tEruzYjqNHhqAEXa2sN1fMrsuKec61Ll2NfvJjkLKDvgVIh7kM7aslNYVOP6BTf
C/JJ7/ufz3UZmyViH/WDl+AYdgk3JqCIO5w5ryrC9IyBzYv2m0HqYbWfphY3uHw5
un3ndLJcu8+BGP5F+ONQEGl+DRH58Il9Jp3HwbRa7dvkPgEhfFR+1hI+Btta2C7E
0/2NKzCxZw7Lx3PBRcU92YKyaEihfy/aQKZCAuyfKiMvsmzs+4poIX7I9NQCJpyE
IGfINoZ7VxqHwRn/d5mw2MZTJjbzSf+Um9YJyA0iEEyD6qjriWQRbuxpQXmlAJbh
8okZ4gbVFv1F8MzK+4R8VvWJ0XxgtikSo72fHjwha7MAjqFnOq6eo6fEC/75g3NL
Ght5VdpGuHk0vbdENHMC8wS99e5qXGNDued3hlTavDMlEAHl34q2H9nakTGRF5Ki
JUfNh3DVRGhg8cMIti21njiRh7gyFI2OccATY7bBSr79JhuNwelHuxLrCFpY7V25
OFktl15jZJaMxuQBqYdBgSay2G0U6D1+7VsWufpzd/Abx1/c3oi9ZaJvW22kAggq
dzdA27UUYjWvx42w9menJwh/0jeQcTecIUd0d0rFcw/c1pvgMMl/Q73yzKgKYw==
=zbHE
-----END PGP PUBLIC KEY BLOCK-----

30 changes: 30 additions & 0 deletions pkg/verify/testdata/keyrings/simple.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBFy9Q6sBCAD1MvcwX9f1Vu/M/dh+SJYbuAP4urtZZ7YoZOlzo6lw/xDF9z0E
ef8BXAtO7YMStfbxn5Rqb3kPnA20CRXraW4PqA5mB37ubDGThxb8catCTeWpd/5o
mrbjLMrKCpg0ODfTgNZYj9gRDyDTKPjlW2xjX9Cmj/lmmGPYDG4qdrNpeicmMjpY
XyYDVxFTRFMdifxTjHQRT5R9Pdq8WDFLrd3ZZWo4fN5Rb+ByWh8MusHj+FyHxA4J
fD/G6VHyn19T7xT/g53JfPobKLdaoXKdSaorCYKWuyGaGyLStAn1MXgchswcBZcU
92EegcoZY8K3cYhRbw7rQUUkx3p0yviS1DrPABEBAAG0DG9wZW5zaGlmdC1jaYkB
VAQTAQgAPhYhBNBHYbEWIDsMCFm2Fii3bgW5I4iOBQJcvUOrAhsDBQkDwmcABQsJ
CAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJECi3bgW5I4iOqwUH+wTRXXZkB6PdksQ2
tF/x3vT3YAp1Fvm+aBt0L3+nUyI4W4wWmCvQ9mYqkXMDSx8rgSwMtwwJe7xJBkA1
fK8CoPeCqHc/omoLUS6/BjcbsXyS/ns6d5Zv0fKVHumZ23V2qVJwPpmNdpkdfBhw
HFKm0HLPaCyKM38fOPhrUwEW8OceVdHfBnkkAyYXA9+9qGF3gHC3MXMLkaH6pDYY
Nfx2P4+qYnMnTMSOOvKsJWY7t8Tnv1Qotag/uW8yWlIBSnvg1BQ7u1ZJs1EKSwhw
QbIrYj+eS+e8ddN7qSHJToMzHstTjSYQThA1iCVU6S+KHaLFeynf1d6PqkyeH/GD
bk+E+hu5AQ0EXL1DqwEIANhU5FczwquEAcjhA+kf+ni0Ul9Q2aq+rAL31dg+sGMZ
awcDu5aocwolXeBIkVl235GFfJSdYRzIbk5lSqVK+Wt5Yj4yOIO+QEAk5I51dzOC
5i3APTqOM0UPQ168ubcoT5LY/aWLJqnVAjgY/Sn2vXAwsYvkuJZMpeOPoNgocAWw
wGxXkPEy//OA3rwyy6PER2U7xLWL5SOH8oxjnsnHA98nF4iuOQqbwPTwfyWN7xr7
HAY6KiawHmD0T3ywswR1bEZ1CYn8KxpNMuHf7tbaMPONvawVEqM1xc9+4tB3ImdM
UB9eIiwIspq68mdE43eyUeM9f2foNR67Kj6F7hvBwDsAEQEAAYkBNgQYAQgAIBYh
BNBHYbEWIDsMCFm2Fii3bgW5I4iOBQJcvUOrAhsMAAoJECi3bgW5I4iOLCAIANNd
BwFFJpTaEZhOvDEsfOmHDFE+xG2fBq+SO53A4M/4xfJ6BVnpRvAgPvEu/ED8LMIB
buaMUpXjAwULIOnNEBsYem+m3IKcrZAIhfXAjI8EqzprjciUiVEx0+XR6eIbsFm2
gm61vHfbviKSyQg3hpKG8/g2sFgQ9CNi5DFghIYesp+7NwCC+UOVGBu90O4SIq+I
Ms2n3OTR2GIEz0LgEvC/3R7pkBNjLNTccExBNqOShJy3XnwntvYflxVwEBVsyEbK
LvLU2xtlIE/IdGssKQR8UFFsgFmGiX3t1TcahFnLlr6Et+vB4J02Xr+uvZ81v/Zq
1OHz7iIjrd28MslYu24=
=xMCa
-----END PGP PUBLIC KEY BLOCK-----
Empty file.
Binary file not shown.
Binary file not shown.
Loading