Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify default bundles downloaded from mirror.openshift.com #3605

Merged
merged 3 commits into from
May 5, 2023
Merged
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
35 changes: 17 additions & 18 deletions pkg/crc/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
DefaultAdminHelperURLBase = "https://github.com/crc-org/admin-helper/releases/download/v%s/%s"
CRCMacTrayDownloadURL = "https://github.com/crc-org/tray-electron/releases/download/%s/crc-tray-macos.tar.gz"
CRCWindowsTrayDownloadURL = "https://github.com/crc-org/tray-electron/releases/download/%s/crc-tray-windows.zip"
DefaultBundleURLBase = "https://mirror.openshift.com/pub/openshift-v4/clients/crc/bundles/%s/%s/%s"
DefaultContext = "admin"
DaemonHTTPEndpoint = "http://unix/api"
DaemonVsockPort = 1024
Expand All @@ -45,24 +46,6 @@ const (

OpenShiftIngressHTTPPort = 80
OpenShiftIngressHTTPSPort = 443

// This public key is owned by the CRC team (crc@crc.dev), and is used
// to sign bundles uploaded to an image registry.
// It can be fetched with: `gpg --recv-key DC7EAC400A1BFDFB`
GPGPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----

mDMEYrvgDRYJKwYBBAHaRw8BAQdAoW+hjSRYpTAdLEE1u6ZuYNER1g97e8ygT4ic
mvo1AKi0MmNyYyAoS2V5IHRvIHNpZ24gYnVuZGxlIHVzZWQgYnkgY3JjKSA8Y3Jj
QGNyYy5kZXY+iJkEExYKAEEWIQS4RlW/rByOBn/ZyofcfqxAChv9+wUCYrvgDQIb
AwUJEswDAAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRDcfqxAChv9+/ep
APwISi03R7npwimqdL7NYKDGMO8ikOwmmPkqh9CKwt4CdwD8Cc6HNcZumHDpJ4gH
x7FXxIS9KLwDihpm1Gxr4t1t5Qy4OARiu+ANEgorBgEEAZdVAQUBAQdA/w7pM7hf
bxZ2qwSuoBuhcA1sAlPSb3NrIZf3CceoqzQDAQgHiH4EGBYKACYWIQS4RlW/rByO
Bn/ZyofcfqxAChv9+wUCYrvgDQIbDAUJEswDAAAKCRDcfqxAChv9+2UkAQCNCdaf
vnhbvfPHDltmwDZ3aD4l3jjSKpeySeKQocgjQAD6A7kawst/50k4wb+vUDUnEoYo
9Ix7lKfKWCXil/z0vg4=
=lmb/
-----END PGP PUBLIC KEY BLOCK-----`
)

var adminHelperExecutableForOs = map[string]string{
Expand Down Expand Up @@ -133,6 +116,22 @@ func GetDefaultBundlePath(preset crcpreset.Preset) string {
return filepath.Join(MachineCacheDir, GetDefaultBundle(preset))
}

func GetDefaultBundleDownloadURL(preset crcpreset.Preset) string {
return fmt.Sprintf(DefaultBundleURLBase,
preset.String(),
version.GetBundleVersion(preset),
GetDefaultBundle(preset),
)
}

func GetDefaultBundleSignedHashURL(preset crcpreset.Preset) string {
return fmt.Sprintf(DefaultBundleURLBase,
preset.String(),
version.GetBundleVersion(preset),
"sha256sum.txt.sig",
)
}

func ResolveHelperPath(executableName string) string {
if version.IsInstaller() {
return filepath.Join(version.InstallPath(), executableName)
Expand Down
50 changes: 50 additions & 0 deletions pkg/crc/constants/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package constants

const (
// This public key is owned by the CRC team (crc@crc.dev), and is used
// to sign bundles uploaded to an image registry.
// It can be fetched with: `gpg --recv-key DC7EAC400A1BFDFB`
CrcOrgPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----

mDMEYrvgDRYJKwYBBAHaRw8BAQdAoW+hjSRYpTAdLEE1u6ZuYNER1g97e8ygT4ic
mvo1AKi0MmNyYyAoS2V5IHRvIHNpZ24gYnVuZGxlIHVzZWQgYnkgY3JjKSA8Y3Jj
QGNyYy5kZXY+iJkEExYKAEEWIQS4RlW/rByOBn/ZyofcfqxAChv9+wUCYrvgDQIb
AwUJEswDAAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRDcfqxAChv9+/ep
APwISi03R7npwimqdL7NYKDGMO8ikOwmmPkqh9CKwt4CdwD8Cc6HNcZumHDpJ4gH
x7FXxIS9KLwDihpm1Gxr4t1t5Qy4OARiu+ANEgorBgEEAZdVAQUBAQdA/w7pM7hf
bxZ2qwSuoBuhcA1sAlPSb3NrIZf3CceoqzQDAQgHiH4EGBYKACYWIQS4RlW/rByO
Bn/ZyofcfqxAChv9+wUCYrvgDQIbDAUJEswDAAAKCRDcfqxAChv9+2UkAQCNCdaf
vnhbvfPHDltmwDZ3aD4l3jjSKpeySeKQocgjQAD6A7kawst/50k4wb+vUDUnEoYo
9Ix7lKfKWCXil/z0vg4=
=lmb/
-----END PGP PUBLIC KEY BLOCK-----`

RedHatReleaseKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----

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-----`
)
52 changes: 51 additions & 1 deletion pkg/crc/gpg/gpg.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ package gpg
import (
"bytes"
"fmt"
"io"
"os"
"strings"

"github.com/ProtonMail/go-crypto/openpgp"
"github.com/crc-org/crc/pkg/crc/constants"
"github.com/crc-org/crc/pkg/crc/logging"
goOpenpgp "golang.org/x/crypto/openpgp" //nolint
goClearsign "golang.org/x/crypto/openpgp/clearsign" //nolint
)

func Verify(filePath, signatureFilePath string) error {
Expand All @@ -22,7 +27,7 @@ func Verify(filePath, signatureFilePath string) error {
}
defer signature.Close()

keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(constants.GPGPublicKey))
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(constants.CrcOrgPublicKey))
if err != nil {
return fmt.Errorf("failed to parse public key: %s", err)
}
Expand All @@ -32,3 +37,48 @@ func Verify(filePath, signatureFilePath string) error {
}
return nil
}

func GetVerifiedClearsignedMsgV3(pubkey, clearSignedMsg string) (string, error) {
k, err := goOpenpgp.ReadArmoredKeyRing(bytes.NewBufferString(pubkey))
if err != nil {
return "", fmt.Errorf("Unable to read pubkey: %w", err)
}
block, rest := goClearsign.Decode([]byte(clearSignedMsg))
if len(rest) != 0 {
return "", fmt.Errorf("Error decoding clear signed message: %w", err)
}
sig, err := io.ReadAll(block.ArmoredSignature.Body)
if err != nil {
return "", fmt.Errorf("Error reading signature: %w", err)
}

// CheckDetachedSignature method expects the clear text msg
// in the canonical format as defined in the pgp spec which
// says that each line of the text needs to end with \r\n
clearTextMsg := make([]byte, len(block.Bytes))
copy(clearTextMsg, block.Bytes)
canonicalizedMsgText := canonicalize(trimEachLine(string(clearTextMsg)))

id, err := goOpenpgp.CheckDetachedSignature(k, bytes.NewBufferString(canonicalizedMsgText), bytes.NewBuffer(sig))
if err != nil {
return "", fmt.Errorf("Invalid signature: %w", err)
}
logging.Debugf("Got valid signature from key id: %s", id.PrimaryKey.KeyIdString())
return trimEachLine(string(clearTextMsg)), nil
Copy link
Contributor

Choose a reason for hiding this comment

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

return canonicalizedMsgText, nil maybe?

Copy link
Member Author

Choose a reason for hiding this comment

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

canonicalizedMsgText will have \r\n line endings
it feels a bit easier to deal with lines ending in \n especially in the unit test

}

// https://github.com/ProtonMail/gopenpgp/blob/5aebf6a366fd8b81e80c337186fdaa0793597354/internal/common.go#L10-L12
func canonicalize(text string) string {
return strings.ReplaceAll(strings.ReplaceAll(text, "\r\n", "\n"), "\n", "\r\n")
}

// https://github.com/ProtonMail/gopenpgp/blob/5aebf6a366fd8b81e80c337186fdaa0793597354/internal/common.go#L14-L22
func trimEachLine(text string) string {
lines := strings.Split(text, "\n")

for i := range lines {
lines[i] = strings.TrimRight(lines[i], " \t\r")
}

return strings.Join(lines, "\n")
}
45 changes: 45 additions & 0 deletions pkg/crc/gpg/gpg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package gpg

import (
"testing"

"github.com/crc-org/crc/pkg/crc/constants"
"github.com/stretchr/testify/assert"
)

const (
testMsg = `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

a8267d09eac58e3c7f0db093f3cba83091390e5ac623b0a0282f6f55102b7681 crc_hyperv_4.12.9_amd64.crcbundle
f57ab331ad092d8cb1f354b4308046c5ffd15bd143b19f841cb64b0fda89db67 crc_libvirt_4.12.9_amd64.crcbundle
7e83b6a4c4da6766b6b4981655d4bb38fd8f9da36ef1a5d16d017cd07d6ee7e9 crc_vfkit_4.12.9_amd64.crcbundle
412d20e4969e872c24b14e55cbaa892848a1657b95a20f4af8ad4629ffdf73ab crc_vfkit_4.12.9_arm64.crcbundle
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1

iQIVAwUBZCQdgxmeL5H9Qx1RAQhUrxAAiLMQqfTZliHzSTmLsgTYj5YOoRly8ax1
lEGxJZ6jTmrkDaC60S76wAoobD7zbVUifNp0XDYs8f7x6VVANpARoKp/p7NDtWP5
K19NhlG6Hip7/RmzUnEfEMH6sYcrNr0mbNQpsTJpje13u7HuwEXEpiceyU3GcDaA
VcdARo6hcP7bZIieHwJtpWrb8knc/OE2lCMKjanBbeC4+vZtj0xU1kpZTsBq8q84
WFdkR7C76XXhdRnuQqBBTbTuXGTI0OjB963Rx0+3Ej1liiDokH7JWvlUVaX7MKS3
IRf4X7q/Q3acsBtfJ9aNjrTCZJoMNg+F/eCbj+iVpXUK5rdOEzDKQitkMrB8VYgF
SCMe+FMyNqDS6MewR+wJzzKoVZDf6SDuAlSej1FthJ4QZAljuXlpRrDbFq2SRiFz
WgVipg15Bl6vTftjOnCkwHg/GQt1bQuzudGyulRlTu/Nnezvf9/sej++91z2aA86
nJnofMLLw3KkA1HCCgIiE5upMvjzLUobKAFxYyaDal6gNPG9S/l8N91UHujhKCpg
otaQ9Awtg7Z/F8pCLH08Nen4J/CqYaG+JHRORm/i17eD3qBEc+EgIZ7m0sLvm1aV
1Tg7G/6pu+LdPIYvJQKSgGuI8eP/p1zc8zzgHtaSWd2AVL3M/iOjteaba8eU5VCd
uaTo5yjgVLY=
=x4q3
-----END PGP SIGNATURE-----`
expectedMsg = `a8267d09eac58e3c7f0db093f3cba83091390e5ac623b0a0282f6f55102b7681 crc_hyperv_4.12.9_amd64.crcbundle
f57ab331ad092d8cb1f354b4308046c5ffd15bd143b19f841cb64b0fda89db67 crc_libvirt_4.12.9_amd64.crcbundle
7e83b6a4c4da6766b6b4981655d4bb38fd8f9da36ef1a5d16d017cd07d6ee7e9 crc_vfkit_4.12.9_amd64.crcbundle
412d20e4969e872c24b14e55cbaa892848a1657b95a20f4af8ad4629ffdf73ab crc_vfkit_4.12.9_arm64.crcbundle`
)

func TestVerify(t *testing.T) {
msg, err := GetVerifiedClearsignedMsgV3(constants.RedHatReleaseKey, testMsg)
assert.NoError(t, err)
assert.Equal(t, expectedMsg, msg)
}
37 changes: 0 additions & 37 deletions pkg/crc/machine/bundle/constants.go

This file was deleted.

34 changes: 0 additions & 34 deletions pkg/crc/machine/bundle/constants_test.go

This file was deleted.

59 changes: 45 additions & 14 deletions pkg/crc/machine/bundle/metadata.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
package bundle

import (
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"

"github.com/Masterminds/semver/v3"
"github.com/crc-org/crc/pkg/crc/constants"
"github.com/crc-org/crc/pkg/crc/gpg"
"github.com/crc-org/crc/pkg/crc/image"
"github.com/crc-org/crc/pkg/crc/logging"
"github.com/crc-org/crc/pkg/crc/network"
crcPreset "github.com/crc-org/crc/pkg/crc/preset"
"github.com/crc-org/crc/pkg/download"
)
Expand Down Expand Up @@ -280,24 +285,50 @@ func GetBundleNameFromURI(bundleURI string) string {
}
}

type presetDownloadInfo map[crcPreset.Preset]*download.RemoteFile
type bundlesDownloadInfo map[string]presetDownloadInfo

func getBundleDownloadInfo(preset crcPreset.Preset) (*download.RemoteFile, error) {
bundles, ok := bundleLocations[runtime.GOARCH]
if !ok {
return nil, fmt.Errorf("Unsupported architecture: %s", runtime.GOARCH)
sha256sum, err := getDefaultBundleVerifiedHash(preset)
if err != nil {
return nil, fmt.Errorf("unable to get verified hash for default bundle: %w", err)
}
downloadInfo := download.NewRemoteFile(constants.GetDefaultBundleDownloadURL(preset), sha256sum)
return downloadInfo, nil
}

// getDefaultBundleVerifiedHash downloads the sha256sum.txt.sig file from mirror.openshift.com
// then verifies it is signed by redhat release key, if signature is valid it returns the hash
// for the default bundle of preset from the file
func getDefaultBundleVerifiedHash(preset crcPreset.Preset) (string, error) {
client := &http.Client{
Timeout: 5 * time.Second,
Transport: network.HTTPTransport(),
}
res, err := client.Get(constants.GetDefaultBundleSignedHashURL(preset))
anjannath marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", err
}
presetdownloadInfo, ok := bundles[runtime.GOOS]
if !ok {
return nil, fmt.Errorf("Unknown GOOS: %s", runtime.GOOS)
signedHashes, err := io.ReadAll(res.Body)
if err != nil {
return "", err
}
downloadInfo, ok := presetdownloadInfo[preset]
if !ok {
return nil, fmt.Errorf("Unknown preset: %s", preset)
if err := res.Body.Close(); err != nil {
logging.Debug(err)
}

return downloadInfo, nil
verifiedHashes, err := gpg.GetVerifiedClearsignedMsgV3(constants.RedHatReleaseKey, string(signedHashes))
if err != nil {
return "", fmt.Errorf("Invalid signature: %w", err)
}

logging.Debugf("Verified bundle hashes:\n%s", verifiedHashes)

lines := strings.Split(verifiedHashes, "\n")
for _, line := range lines {
if strings.HasSuffix(line, constants.GetDefaultBundle(preset)) {
sha256sum := strings.TrimSuffix(line, " "+constants.GetDefaultBundle(preset))
return sha256sum, nil
}
anjannath marked this conversation as resolved.
Show resolved Hide resolved
}
return "", errors.New("default bundle's hash is missing or shasums are malformed")
}

func DownloadDefault(preset crcPreset.Preset) (string, error) {
Expand Down
Loading