1
0
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:
Chaosoffire
2026-01-07 16:42:12 +08:00
parent 7b7cb2f34b
commit 64b60db96c
11 changed files with 226 additions and 97 deletions

View File

@@ -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))
}
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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)