Skip to content
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ __pycache__
/rpmbuild
/test/data/manifests
/tools/appsre-ansible/inventory
/build
/check-host-config
dictionary.dic

*~
Expand Down
4 changes: 2 additions & 2 deletions Schutzfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"common": {
"rngseed": 10,
"rngseed": 12,
"bootc-image-builder": {
"ref": "quay.io/centos-bootc/bootc-image-builder@sha256:9893e7209e5f449b86ababfd2ee02a58cca2e5990f77b06c3539227531fc8120"
},
Expand Down Expand Up @@ -84,4 +84,4 @@
}
}
}
}
}
104 changes: 104 additions & 0 deletions cmd/check-host-config/check/cacerts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package check

import (
"crypto/x509"
"encoding/pem"
"fmt"
"log"
"strings"

"github.com/osbuild/images/internal/buildconfig"
)

func init() {
RegisterCheck(Metadata{
Name: "CA Certs Check",
ShortName: "cacerts",
RequiresBlueprint: true,
RequiresCustomizations: true,
}, cacertsCheck)
}

func cacertsCheck(meta *Metadata, config *buildconfig.BuildConfig) error {
cacerts := config.Blueprint.Customizations.CACerts
if cacerts == nil || len(cacerts.PEMCerts) == 0 {
return Skip("no CA certs to check")
}

// Check all CA certs
checkedCount := 0
for i, pemCert := range cacerts.PEMCerts {
if pemCert == "" {
log.Printf("Skipping empty CA cert at index %d\n", i)
continue
}
checkedCount++

log.Printf("Parsing CA cert %d\n", i+1)
block, _ := pem.Decode([]byte(pemCert))
if block == nil {
return Fail("failed to decode PEM certificate at index", fmt.Sprintf("%d", i))
}

if block.Type != "CERTIFICATE" {
return Fail("PEM block is not a CERTIFICATE at index", fmt.Sprintf("%d", i), "got:", block.Type)
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return Fail("failed to parse certificate at index", fmt.Sprintf("%d", i), "error:", err.Error())
}

// Extract serial number (format as hex, lowercase)
serial := strings.ToLower(cert.SerialNumber.Text(16))
log.Printf("Extracting serial from CA cert %d: %s\n", i+1, serial)

// Extract CN from certificate subject
cn := cert.Subject.CommonName
if cn == "" {
// Fallback: try to extract from Subject.String() if CommonName is empty
// Subject.String() format: "CN=value,OU=...,O=..."
subjectStr := cert.Subject.String()
if _, after, cnOk := strings.Cut(subjectStr, "CN="); cnOk {
cnPart := after
// CN value might be followed by , or end of string
if before, _, pOk := strings.Cut(cnPart, ","); pOk {
cn = before
} else {
cn = cnPart
}
cn = strings.TrimSpace(cn)
}
}

if cn == "" {
return Fail("failed to extract CN from CA cert subject at index", fmt.Sprintf("%d", i))
}

// Check anchor file
anchorPath := "/etc/pki/ca-trust/source/anchors/" + serial + ".pem"
log.Printf("Checking CA cert %d anchor file serial '%s'\n", i+1, serial)
if !Exists(anchorPath) {
return Fail("file missing for cert", fmt.Sprintf("%d", i+1), "at", anchorPath)
}

// Check extracted CA cert file
log.Printf("Checking extracted CA cert %d file named '%s'\n", i+1, cn)
bundlePath := "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
found, err := Grep(cn, bundlePath)
if err != nil {
return Fail("extracted CA cert not found in the bundle for cert", fmt.Sprintf("%d", i+1), "cn:", cn, "error:", err.Error())
}
if !found {
log.Printf("Pattern not found in file: %s\n", bundlePath)
return Fail("extracted CA cert not found in the bundle for cert", fmt.Sprintf("%d", i+1), "cn:", cn)
}
log.Printf("Pattern found in %s\n", bundlePath)
}

if checkedCount == 0 {
return Skip("all CA certs were empty")
}

return Pass()
}
182 changes: 182 additions & 0 deletions cmd/check-host-config/check/cacerts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package check_test

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"strings"
"testing"
"time"

"github.com/osbuild/blueprint/pkg/blueprint"
check "github.com/osbuild/images/cmd/check-host-config/check"
"github.com/osbuild/images/internal/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// generateTestCert creates a test X509 certificate and returns it as PEM
func generateTestCert(t *testing.T, cn string, serial *big.Int) string {
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)

template := x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: cn,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
require.NoError(t, err)

certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})

return string(certPEM)
}

func TestCACertsCheck(t *testing.T) {
// Generate a test certificate with a known serial number
serial := big.NewInt(1234567890)
cn := "Test CA Certificate"
pemCert := generateTestCert(t, cn, serial)

// Calculate expected serial (hex, lowercase)
expectedSerial := strings.ToLower(serial.Text(16))

test.MockGlobal(t, &check.Exists, func(name string) bool {
// Check for anchor file
if name == "/etc/pki/ca-trust/source/anchors/"+expectedSerial+".pem" {
return true
}
return false
})

test.MockGlobal(t, &check.Grep, func(pattern, filename string) (bool, error) {
// Mock grep to check if CN is in bundle
if filename == "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" && pattern == cn {
return true, nil
}
return false, nil
})

chk, found := check.FindCheckByName("CA Certs Check")
require.True(t, found, "CA Certs Check not found")
config := buildConfig(&blueprint.Customizations{
CACerts: &blueprint.CACustomization{
PEMCerts: []string{pemCert},
},
})

require.NoError(t, chk.Func(chk.Meta, config))
}

func TestCACertsCheckMultiple(t *testing.T) {
// Generate two test certificates
serial1 := big.NewInt(1111111111)
cn1 := "First CA Certificate"
pemCert1 := generateTestCert(t, cn1, serial1)
expectedSerial1 := strings.ToLower(serial1.Text(16))

serial2 := big.NewInt(2222222222)
cn2 := "Second CA Certificate"
pemCert2 := generateTestCert(t, cn2, serial2)
expectedSerial2 := strings.ToLower(serial2.Text(16))

test.MockGlobal(t, &check.Exists, func(name string) bool {
// Check for both anchor files
if name == "/etc/pki/ca-trust/source/anchors/"+expectedSerial1+".pem" {
return true
}
if name == "/etc/pki/ca-trust/source/anchors/"+expectedSerial2+".pem" {
return true
}
return false
})

test.MockGlobal(t, &check.Grep, func(pattern, filename string) (bool, error) {
// Mock grep to check if CN is in bundle
if filename == "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" && (pattern == cn1 || pattern == cn2) {
return true, nil
}
return false, nil
})

chk, found := check.FindCheckByName("CA Certs Check")
require.True(t, found, "CA Certs Check not found")
config := buildConfig(&blueprint.Customizations{
CACerts: &blueprint.CACustomization{
PEMCerts: []string{pemCert1, pemCert2},
},
})

require.NoError(t, chk.Func(chk.Meta, config))
}

func TestCACertsCheckSkip(t *testing.T) {
chk, found := check.FindCheckByName("CA Certs Check")
require.True(t, found, "CA Certs Check not found")
config := buildConfig(&blueprint.Customizations{
CACerts: &blueprint.CACustomization{
PEMCerts: []string{},
},
})

err := chk.Func(chk.Meta, config)
require.Error(t, err)
assert.True(t, check.IsSkip(err))
}

func TestCACertsCheckEmptyCert(t *testing.T) {
chk, found := check.FindCheckByName("CA Certs Check")
require.True(t, found, "CA Certs Check not found")
config := buildConfig(&blueprint.Customizations{
CACerts: &blueprint.CACustomization{
PEMCerts: []string{""},
},
})

err := chk.Func(chk.Meta, config)
require.Error(t, err)
assert.True(t, check.IsSkip(err))
}

func TestCACertsCheckMissingAnchor(t *testing.T) {
serial := big.NewInt(9999999999)
cn := "Missing Anchor Test"
pemCert := generateTestCert(t, cn, serial)

test.MockGlobal(t, &check.Exists, func(name string) bool {
// Anchor file does not exist
return false
})

test.MockGlobal(t, &check.Grep, func(pattern, filename string) (bool, error) {
// Return false to simulate CN not found
return false, nil
})

chk, found := check.FindCheckByName("CA Certs Check")
require.True(t, found, "CA Certs Check not found")
config := buildConfig(&blueprint.Customizations{
CACerts: &blueprint.CACustomization{
PEMCerts: []string{pemCert},
},
})

err := chk.Func(chk.Meta, config)
require.Error(t, err)
assert.False(t, check.IsSkip(err))
assert.True(t, check.IsFail(err))
}
56 changes: 56 additions & 0 deletions cmd/check-host-config/check/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package check

import (
"errors"
"fmt"
"strings"
)

var ErrCheckSkipped = errors.New("skip")
var ErrCheckFailed = errors.New("fail")
var ErrCheckWarning = errors.New("warn")

func Skip(reason string) error {
return fmt.Errorf("%w: %s", ErrCheckSkipped, reason)
}

func Pass() error {
return nil
}

func Fail(reason ...string) error {
msg := strings.Join(reason, " ")
return fmt.Errorf("%w: %s", ErrCheckFailed, msg)
}

func Warning(reason ...string) error {
msg := strings.Join(reason, " ")
return fmt.Errorf("%w: %s", ErrCheckWarning, msg)
}

func IsSkip(err error) bool {
return errors.Is(err, ErrCheckSkipped)
}

func IsFail(err error) bool {
return errors.Is(err, ErrCheckFailed)
}

func IsWarning(err error) bool {
return errors.Is(err, ErrCheckWarning)
}

func IconFor(err error) string {
switch {
case err == nil:
return "🟢"
case IsSkip(err):
return "🔵"
case IsWarning(err):
return "🟠"
case IsFail(err):
return "🔴"
default:
return "🔴"
}
}
Loading
Loading