mirror of
https://github.com/lxc/distrobuilder.git
synced 2026-02-06 00:46:16 +01:00
sources: enforce GPG verification across multiple distros
This commit introduces a centralized GPG verification requirement logic in `sources/common.go` via the `validateGPGRequirements` method. It ensures consistent security constraints across multiple supported distributions. Specific security fixes included: - Rocky Linux: Fixed an issue where the `CHECKSUM` file was downloaded but not GPG verified. - CentOS: Fixed an issue where 'SHA256SUM' and 'CHECKSUM' files were downloaded but not GPG verified. - Gentoo: Added GPG requirement validation for the portage snapshot download URL. Fixes: https://github.com/lxc/distrobuilder/issues/963 Signed-off-by: Chaosoffire <81634128+chaosoffire@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,6 @@ package sources
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -58,40 +57,40 @@ func (s *almalinux) Run() error {
|
||||
return fmt.Errorf("Failed to parse URL %q: %w", baseURL, err)
|
||||
}
|
||||
|
||||
skip, err := s.validateGPGRequirements(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to validate GPG requirements: %w", err)
|
||||
}
|
||||
|
||||
s.definition.Source.SkipVerification = skip
|
||||
|
||||
checksumFile := ""
|
||||
if !s.definition.Source.SkipVerification {
|
||||
// Force gpg checks when using http
|
||||
if url.Scheme != "https" {
|
||||
if len(s.definition.Source.Keys) == 0 {
|
||||
return errors.New("GPG keys are required if downloading from HTTP")
|
||||
if s.definition.Image.ArchitectureMapped == "armhfp" {
|
||||
checksumFile = "sha256sum.txt"
|
||||
} else {
|
||||
switch s.majorVersion {
|
||||
case "8", "9", "10":
|
||||
checksumFile = "CHECKSUM"
|
||||
default:
|
||||
checksumFile = "sha256sum.txt.asc"
|
||||
}
|
||||
}
|
||||
|
||||
if s.definition.Image.ArchitectureMapped == "armhfp" {
|
||||
checksumFile = "sha256sum.txt"
|
||||
} else {
|
||||
switch s.majorVersion {
|
||||
case "8", "9", "10":
|
||||
checksumFile = "CHECKSUM"
|
||||
default:
|
||||
checksumFile = "sha256sum.txt.asc"
|
||||
}
|
||||
}
|
||||
fpath, err := s.DownloadHash(s.definition.Image, baseURL+checksumFile, "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download %q: %w", baseURL+checksumFile, err)
|
||||
}
|
||||
|
||||
fpath, err := s.DownloadHash(s.definition.Image, baseURL+checksumFile, "", nil)
|
||||
// Only verify file if possible.
|
||||
if strings.HasSuffix(checksumFile, ".asc") || checksumFile == "CHECKSUM" {
|
||||
valid, err := s.VerifyFile(filepath.Join(fpath, checksumFile), "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download %q: %w", baseURL+checksumFile, err)
|
||||
return fmt.Errorf("Failed to verify %q: %w", checksumFile, err)
|
||||
}
|
||||
|
||||
// Only verify file if possible.
|
||||
if strings.HasSuffix(checksumFile, ".asc") || checksumFile == "CHECKSUM" {
|
||||
valid, err := s.VerifyFile(filepath.Join(fpath, checksumFile), "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to verify %q: %w", checksumFile, err)
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("Invalid signature for %q", filepath.Join(fpath, checksumFile))
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("Invalid signature for %q", filepath.Join(fpath, checksumFile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,11 +61,13 @@ func (s *alpineLinux) Run() error {
|
||||
return fmt.Errorf("Failed to parse URL %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" &&
|
||||
len(s.definition.Source.Keys) == 0 {
|
||||
return errors.New("GPG keys are required if downloading from HTTP")
|
||||
skip, err := s.validateGPGRequirements(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to validate GPG requirements: %w", err)
|
||||
}
|
||||
|
||||
s.definition.Source.SkipVerification = skip
|
||||
|
||||
var fpath string
|
||||
|
||||
if s.definition.Source.SkipVerification {
|
||||
@@ -78,8 +80,7 @@ func (s *alpineLinux) Run() error {
|
||||
return fmt.Errorf("Failed to download %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
// Force gpg checks when using http
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" {
|
||||
if !s.definition.Source.SkipVerification {
|
||||
_, err = s.DownloadHash(s.definition.Image, tarball+".asc", "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed downloading %q: %w", tarball+".asc", err)
|
||||
|
||||
@@ -59,18 +59,19 @@ func (s *archlinux) Run() error {
|
||||
return fmt.Errorf("Failed to parse URL %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" &&
|
||||
len(s.definition.Source.Keys) == 0 {
|
||||
return errors.New("GPG keys are required if downloading from HTTP")
|
||||
skip, err := s.validateGPGRequirements(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to validate GPG requirements: %w", err)
|
||||
}
|
||||
|
||||
s.definition.Source.SkipVerification = skip
|
||||
|
||||
fpath, err := s.DownloadHash(s.definition.Image, tarball, "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
// Force gpg checks when using http
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" {
|
||||
if !s.definition.Source.SkipVerification {
|
||||
_, err = s.DownloadHash(s.definition.Image, tarball+".sig", "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed downloading %q: %w", tarball+".sig", err)
|
||||
|
||||
@@ -76,40 +76,40 @@ func (s *centOS) Run() error {
|
||||
return fmt.Errorf("Failed to parse URL %q: %w", baseURL, err)
|
||||
}
|
||||
|
||||
skip, err := s.validateGPGRequirements(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to validate GPG requirements: %w", err)
|
||||
}
|
||||
|
||||
s.definition.Source.SkipVerification = skip
|
||||
|
||||
checksumFile := ""
|
||||
if !s.definition.Source.SkipVerification {
|
||||
// Force gpg checks when using http
|
||||
if url.Scheme != "https" {
|
||||
if len(s.definition.Source.Keys) == 0 {
|
||||
return errors.New("GPG keys are required if downloading from HTTP")
|
||||
if s.definition.Image.ArchitectureMapped == "armhfp" {
|
||||
checksumFile = "sha256sum.txt"
|
||||
} else {
|
||||
checksumFile = "sha256sum.txt.asc"
|
||||
if strings.HasPrefix(s.definition.Image.Release, "9") {
|
||||
checksumFile = "SHA256SUM"
|
||||
} else if strings.HasPrefix(s.definition.Image.Release, "8") {
|
||||
checksumFile = "CHECKSUM"
|
||||
}
|
||||
}
|
||||
|
||||
if s.definition.Image.ArchitectureMapped == "armhfp" {
|
||||
checksumFile = "sha256sum.txt"
|
||||
} else {
|
||||
checksumFile = "sha256sum.txt.asc"
|
||||
if strings.HasPrefix(s.definition.Image.Release, "9") {
|
||||
checksumFile = "SHA256SUM"
|
||||
} else if strings.HasPrefix(s.definition.Image.Release, "8") {
|
||||
checksumFile = "CHECKSUM"
|
||||
}
|
||||
}
|
||||
fpath, err := s.DownloadHash(s.definition.Image, baseURL+checksumFile, "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download %q: %w", baseURL+checksumFile, err)
|
||||
}
|
||||
|
||||
fpath, err := s.DownloadHash(s.definition.Image, baseURL+checksumFile, "", nil)
|
||||
// Only verify file if possible.
|
||||
if strings.HasSuffix(checksumFile, ".asc") || checksumFile == "SHA256SUM" || checksumFile == "CHECKSUM" {
|
||||
valid, err := s.VerifyFile(filepath.Join(fpath, checksumFile), "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download %q: %w", baseURL+checksumFile, err)
|
||||
return fmt.Errorf("Failed to verify %q: %w", checksumFile, err)
|
||||
}
|
||||
|
||||
// Only verify file if possible.
|
||||
if strings.HasSuffix(checksumFile, ".asc") {
|
||||
valid, err := s.VerifyFile(filepath.Join(fpath, checksumFile), "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to verify %q: %w", checksumFile, err)
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("Invalid signature for %q", checksumFile)
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("Invalid signature for %q", checksumFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"hash"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -284,3 +285,22 @@ func (s *common) CreateGPGKeyring() (string, error) {
|
||||
|
||||
return filepath.Join(gpgDir, "distrobuilder.gpg"), nil
|
||||
}
|
||||
|
||||
// Checks GPG key requirements.
|
||||
func (s *common) validateGPGRequirements(u *url.URL) (bool, error) {
|
||||
hasKeys := len(s.definition.Source.Keys) != 0
|
||||
|
||||
if hasKeys {
|
||||
// GPG keys provided, always verify regardless of protocol
|
||||
return false, nil
|
||||
} else if u.Scheme != "https" {
|
||||
// Force gpg checks when using http
|
||||
return false, fmt.Errorf("GPG keys are required if downloading from %s", u.Scheme)
|
||||
} else if !s.definition.Source.SkipVerification {
|
||||
// HTTPS without keys: warn but allow
|
||||
s.logger.Warnf("Downloading from %s without GPG keys as no keys were specified", u.Scheme)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return s.definition.Source.SkipVerification, nil
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ package sources
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
incus "github.com/lxc/incus/v6/shared/util"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/lxc/distrobuilder/shared"
|
||||
@@ -140,3 +142,85 @@ func TestCreateGPGKeyring(t *testing.T) {
|
||||
require.False(t, incus.PathExists(keyring), "File should not exist")
|
||||
os.RemoveAll(path.Dir(keyring))
|
||||
}
|
||||
|
||||
func TestValidateGPGRequirements(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
keys []string
|
||||
skipVerification bool
|
||||
expectError bool
|
||||
expectSkipVerify bool
|
||||
}{
|
||||
{
|
||||
name: "keys provided with https",
|
||||
url: "https://example.com/file",
|
||||
keys: []string{"0x12345678"},
|
||||
skipVerification: false,
|
||||
expectError: false,
|
||||
expectSkipVerify: false, // Keys provided, always verify
|
||||
},
|
||||
{
|
||||
name: "keys provided with http",
|
||||
url: "http://example.com/file",
|
||||
keys: []string{"0x12345678"},
|
||||
skipVerification: false,
|
||||
expectError: false,
|
||||
expectSkipVerify: false, // Keys provided, always verify
|
||||
},
|
||||
{
|
||||
name: "http without keys",
|
||||
url: "http://example.com/file",
|
||||
keys: []string{},
|
||||
skipVerification: false,
|
||||
expectError: true, // HTTP requires GPG keys
|
||||
expectSkipVerify: false,
|
||||
},
|
||||
{
|
||||
name: "https without keys and skip false",
|
||||
url: "https://example.com/file",
|
||||
keys: []string{},
|
||||
skipVerification: false,
|
||||
expectError: false,
|
||||
expectSkipVerify: true, // Should be set to true with warning
|
||||
},
|
||||
{
|
||||
name: "https without keys and skip true",
|
||||
url: "https://example.com/file",
|
||||
keys: []string{},
|
||||
skipVerification: true,
|
||||
expectError: false,
|
||||
expectSkipVerify: true, // Remains true
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(os.Stdout)
|
||||
|
||||
c := common{
|
||||
logger: logger,
|
||||
definition: shared.Definition{
|
||||
Source: shared.DefinitionSource{
|
||||
Keys: tt.keys,
|
||||
SkipVerification: tt.skipVerification,
|
||||
},
|
||||
},
|
||||
ctx: context.TODO(),
|
||||
}
|
||||
|
||||
u, err := url.Parse(tt.url)
|
||||
require.NoError(t, err)
|
||||
|
||||
skip, err := c.validateGPGRequirements(u)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectSkipVerify, skip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package sources
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
@@ -47,11 +46,13 @@ func (s *funtoo) Run() error {
|
||||
return fmt.Errorf("Failed to parse URL %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" &&
|
||||
len(s.definition.Source.Keys) == 0 {
|
||||
return errors.New("GPG keys are required if downloading from HTTP")
|
||||
skip, err := s.validateGPGRequirements(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to validate GPG requirements: %w", err)
|
||||
}
|
||||
|
||||
s.definition.Source.SkipVerification = skip
|
||||
|
||||
var fpath string
|
||||
|
||||
fpath, err = s.DownloadHash(s.definition.Image, tarball, "", nil)
|
||||
@@ -59,8 +60,7 @@ func (s *funtoo) Run() error {
|
||||
return fmt.Errorf("Failed to download %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
// Force gpg checks when using http
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" {
|
||||
if !s.definition.Source.SkipVerification {
|
||||
_, err = s.DownloadHash(s.definition.Image, tarball+".gpg", "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download %q: %w", tarball+".gpg", err)
|
||||
|
||||
@@ -56,11 +56,13 @@ func (s *gentoo) Run() error {
|
||||
return fmt.Errorf("Failed to parse %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" &&
|
||||
len(s.definition.Source.Keys) == 0 {
|
||||
return errors.New("GPG keys are required if downloading from HTTP")
|
||||
skip, err := s.validateGPGRequirements(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to validate GPG requirements: %w", err)
|
||||
}
|
||||
|
||||
s.definition.Source.SkipVerification = skip
|
||||
|
||||
var fpath string
|
||||
|
||||
if s.definition.Source.SkipVerification {
|
||||
@@ -73,8 +75,7 @@ func (s *gentoo) Run() error {
|
||||
return fmt.Errorf("Failed to download %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
// Force gpg checks when using http
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" {
|
||||
if !s.definition.Source.SkipVerification {
|
||||
_, err = s.DownloadHash(s.definition.Image, tarball+".DIGESTS.asc", "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download %q: %w", tarball+".DIGESTS.asc", err)
|
||||
@@ -111,8 +112,19 @@ func (s *gentoo) Run() error {
|
||||
return fmt.Errorf("Failed to download %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
// Force gpg checks when using http
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" {
|
||||
url, err = url.Parse(tarball)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
skip, err = s.validateGPGRequirements(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to validate GPG requirements: %w", err)
|
||||
}
|
||||
|
||||
s.definition.Source.SkipVerification = skip
|
||||
|
||||
if !s.definition.Source.SkipVerification {
|
||||
_, err = s.DownloadHash(s.definition.Image, tarball+".gpgsig", "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download %q: %w", tarball+".gpgsig", err)
|
||||
|
||||
@@ -41,20 +41,29 @@ func (s *rockylinux) Run() error {
|
||||
return fmt.Errorf("Failed to parse URL %q: %w", baseURL, err)
|
||||
}
|
||||
|
||||
skip, err := s.validateGPGRequirements(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to validate GPG requirements: %w", err)
|
||||
}
|
||||
|
||||
s.definition.Source.SkipVerification = skip
|
||||
|
||||
checksumFile := ""
|
||||
if !s.definition.Source.SkipVerification {
|
||||
// Force gpg checks when using http
|
||||
if url.Scheme != "https" {
|
||||
if len(s.definition.Source.Keys) == 0 {
|
||||
return errors.New("GPG keys are required if downloading from HTTP")
|
||||
}
|
||||
checksumFile = "CHECKSUM"
|
||||
|
||||
checksumFile = "CHECKSUM"
|
||||
fpath, err := s.DownloadHash(s.definition.Image, baseURL+checksumFile, "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download %q: %w", baseURL+checksumFile, err)
|
||||
}
|
||||
|
||||
_, err := s.DownloadHash(s.definition.Image, baseURL+checksumFile, "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download %q: %w", baseURL+checksumFile, err)
|
||||
}
|
||||
valid, err := s.VerifyFile(filepath.Join(fpath, checksumFile), "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to verify %q: %w", checksumFile, err)
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("Invalid signature for %q", checksumFile)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,15 +64,17 @@ func (s *ubuntu) downloadImage(definition shared.Definition) error {
|
||||
return fmt.Errorf("Failed to parse URL %q: %w", baseURL, err)
|
||||
}
|
||||
|
||||
skip, err := s.validateGPGRequirements(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to validate GPG requirements: %w", err)
|
||||
}
|
||||
|
||||
s.definition.Source.SkipVerification = skip
|
||||
|
||||
var fpath string
|
||||
|
||||
checksumFile := ""
|
||||
// Force gpg checks when using http
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" {
|
||||
if len(s.definition.Source.Keys) == 0 {
|
||||
return errors.New("GPG keys are required if downloading from HTTP")
|
||||
}
|
||||
|
||||
if !s.definition.Source.SkipVerification {
|
||||
checksumFile = baseURL + "SHA256SUMS"
|
||||
fpath, err = s.DownloadHash(s.definition.Image, baseURL+"SHA256SUMS.gpg", "", nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -39,11 +39,13 @@ func (s *voidlinux) Run() error {
|
||||
return fmt.Errorf("Failed to parse URL %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" &&
|
||||
len(s.definition.Source.Keys) == 0 {
|
||||
return errors.New("GPG keys are required if downloading from HTTP")
|
||||
skip, err := s.validateGPGRequirements(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to validate GPG requirements: %w", err)
|
||||
}
|
||||
|
||||
s.definition.Source.SkipVerification = skip
|
||||
|
||||
var fpath string
|
||||
|
||||
if s.definition.Source.SkipVerification {
|
||||
@@ -56,8 +58,7 @@ func (s *voidlinux) Run() error {
|
||||
return fmt.Errorf("Failed to download %q: %w", tarball, err)
|
||||
}
|
||||
|
||||
// Force gpg checks when using http
|
||||
if !s.definition.Source.SkipVerification && url.Scheme != "https" {
|
||||
if !s.definition.Source.SkipVerification {
|
||||
_, err = s.DownloadHash(s.definition.Image, digests, "", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to download %q: %w", digests, err)
|
||||
|
||||
Reference in New Issue
Block a user