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

osbuild: new stage 'cacert' #907

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

osbuild: new stage 'cacert' #907

wants to merge 1 commit into from

Conversation

lzap
Copy link
Contributor

@lzap lzap commented Sep 5, 2024

This pull request adds a new stage called 'cacert' to the osbuild package. It also includes file changes to support the CACustomization feature, which allows users to specify a list of certificates for the CA (Certificate Authority). The changes include updates to the Customizations struct, the osCustomizations function, and the OSCustomizations struct. Additionally, a new function called parseCerts is added to parse the certificate strings. The changes also include updates to the serialize and prependKernelCmdlineStage functions in the OS struct. Finally, a new file called ca_stage.go is added to the osbuild package, which contains the implementation of the NewCAStageStage function.

Needs: osbuild/osbuild#1854

if len(p.CACerts) > 0 {
for _, cc := range p.CACerts {
for _, c := range parseCerts(cc) {
path := filepath.Join("/etc/pki/ca-trust/source/anchors", filepath.Base(c.SerialNumber.Text(16)))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Missing the filename extension, will add during review.

Copy link
Member

@achilleas-k achilleas-k left a comment

Choose a reason for hiding this comment

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

Can we break down this PR into multiple commits for easier reviewing and history?

I like to have commits that introduce new things separate from the following ones that use them, so maybe:

  1. Add osbuild/..._stage.go
  2. Add manifest/os.go changes.
  3. Add blueprint customization.
  4. Add config option to all-customizations.json.

(though, 2 and 3 can be merged).

@@ -32,6 +32,7 @@ type Customizations struct {
Installer *InstallerCustomization `json:"installer,omitempty" toml:"installer,omitempty"`
RPM *RPMCustomization `json:"rpm,omitempty" toml:"rpm,omitempty"`
RHSM *RHSMCustomization `json:"rhsm,omitempty" toml:"rhsm,omitempty"`
CA *CACustomization `json:"ca,omitempty" toml:"ca,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

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

ca, while an accurate initialism and probably a very common term to refer to what we're doing, is probably not a great name for a configuration key in the soup of top-level blueprint customization names.

CertificateAuthority (certificate-authority) is, on the other hand, too long.

Maybe CACerts (toml:"ca-certs") would be a better name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, in the same struct I see a two-worded JSON field SSHKey as sshkey so I will stick with this theme and go for cacerts.

Comment on lines 177 to 178
"ca": [
"-----BEGIN CERTIFICATE-----\nMIIDszCCApugAwIBAgIUJ4lK+JfdJCNgcEVxZDinJfKKbQswDQYJKoZIhvcNAQEL\nBQAwaDELMAkGA1UEBhMCVVMxFzAVBgNVBAgMDk5vcnRoIENhcm9saW5hMRAwDgYD\nVQQHDAdSYWxlaWdoMRAwDgYDVQQKDAdSZWQgSGF0MRwwGgYDVQQDDBNUZXN0IENB\nIGZvciBvc2J1aWxkMCAXDTI0MDkwMzEzMjkyMFoYDzIyOTgwNjE4MTMyOTIwWjBo\nMQswCQYDVQQGEwJVUzEXMBUGA1UECAwOTm9ydGggQ2Fyb2xpbmExEDAOBgNVBAcM\nB1JhbGVpZ2gxEDAOBgNVBAoMB1JlZCBIYXQxHDAaBgNVBAMME1Rlc3QgQ0EgZm9y\nIG9zYnVpbGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDeA7OcWTrV\ngstoBsUaeJKm8nelg7Lc0WNXH6yOTLsr4td4yHs0YOvFGwgSf+ffV3RAG1mgqnMG\nMgkD2+z+7QhHbHHs3y0d0zfhA2bg0KVvfCWk7fNRPHY0UOePpXk245Bfw3D0VTpl\nF7nePk1I7ZY09snPWUeb2rjKXzYjKjzM0h27+ykV8I8+FbdyPk/pR8whyDqtHLUa\nXfFy2TFloDSYMkHKVd38BnL0bj91x5F+KsZkN4HzfbYwxLbCQfOSgy7q6TWce9kq\nLo6tya9vuvpWFm1dye7L+BodAQAq/dI/JMeCfyTb0eFb+tyzfr5aVIoqqDN+p9ft\ncw4OefpHbhtNAgMBAAGjUzBRMB0GA1UdDgQWBBRV2A9YmusekPzu5Yf08cV0oPL1\nwjAfBgNVHSMEGDAWgBRV2A9YmusekPzu5Yf08cV0oPL1wjAPBgNVHRMBAf8EBTAD\nAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCgQZ2Xfj+NxaKBZgn2KNxS0MTbhzHRz6Rn\nqJs+h8OUz2Crmaf6N+RHlmDRZXUrDjSHpxVT2LxFy7ofRrLYIezFDUYfb920VkkV\nSVcxh1YDFROJalfMoE6wdyR/LnK4MJZS9fUpeCJJc/A0J+9FK9CwcyUrHgJ8XbJh\nMKYyQ+cf6O7wzutuBpMyRqSKS+hVM7BQTmSFvv1eAJlo6klGAmmKiYmAEvcQadH1\ndjrujsA3Cn5vX2L+0yuiLB5/zoxqx5cEy97TuKUYB8OqMMujAXNzF4L3HJDUNba2\nAhEkFozMXwYX73TGbGZ0mawPS5D3v3tYTEmJFf6SnVCmUW1fs57g\n-----END CERTIFICATE-----\n"
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't match the customization struct in the code. The way the customization is written now this should be:

"ca": {
  "certs": [
    "..."
  ]
}

Copy link
Member

Choose a reason for hiding this comment

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

The stage is called pki.update-ca-trust so please name the file accordingly for easier navigation.
pki_update_stage.go for example.

Copy link
Member

@achilleas-k achilleas-k left a comment

Choose a reason for hiding this comment

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

Can we break down this PR into multiple commits for easier reviewing and history?

I like to have commits that introduce new things separate from the following ones that use them, so maybe:

  1. Add osbuild/..._stage.go
  2. Add manifest/os.go changes.
  3. Add blueprint customization.
  4. Add config option to all-customizations.json.

(though, 2 and 3 can be merged).

@lzap
Copy link
Contributor Author

lzap commented Sep 9, 2024

Sure, amended all remarks. If you don’t mind, I am keeping one commit for easier work from my side, multiple commits rebasing is a pain to work with this isn’t a big PR by any means. I will split it after the code is ready.

Copy link

This PR is stale because it has been open 30 days with no activity. Remove "Stale" label or comment or this will be closed in 7 days.

@github-actions github-actions bot added the Stale label Oct 10, 2024
@mvo5 mvo5 removed the Stale label Oct 10, 2024
Copy link
Contributor

@mvo5 mvo5 left a comment

Choose a reason for hiding this comment

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

Thank you for this PR! It looks good but I still have some ideas/suggestions inline for your consideration. But I'm happy to help with them if you have no enough time :)

for _, block := range blocks {
cert, err := x509.ParseCertificate(block)
if err != nil {
logrus.Warnf("failed to parse certificate, skipping: %s", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the behavior we want/expect? This will mean that pasing an invalid cert string will just result in an empty list and the cert not appearing in the manifest. As a user I would probably be surprised if I paste a cert with an error and then get an image (much later) that does not contain it. Sorry, I think this is a very wordy way of saysing: "it seems to me we should return an error here" (unless I miss something of course which is entriely possible)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call, explicit error it is.

@@ -22,6 +24,7 @@ import (
"github.com/osbuild/images/pkg/platform"
"github.com/osbuild/images/pkg/rhsm/facts"
"github.com/osbuild/images/pkg/rpmmd"
"github.com/sirupsen/logrus"
Copy link
Contributor

Choose a reason for hiding this comment

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

(nipick) in general our convention for imports is (in most go project btw):

  • stdlib
  • external third party
  • our own includes

we are not very strict about it but here I would suggest to move it between "strings" and github.com/osbuild/images/internal/common" (with \n before/after)


func (c *Customizations) GetCACerts() *CACustomization {
if c == nil {
return nil
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if it wouldn't be better to also validate here, this would avoid the panic for invalid certs later. Here we could follow the pattern from e.g. GetRepositories() and return (*CACustomization,error) and then the callers (in fedora for example) can report a meaningful error for invalid cert strings early. Wdyt?

Copy link
Contributor Author

@lzap lzap Oct 14, 2024

Choose a reason for hiding this comment

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

I can do that, just note this introduces a dependency on distro/manifest package. Also validating on a getter is something that can possibly cause performance issues later. Finally, getters are generally not recommended in Go communty.

Overall, I do not like this, but can do that we can discuss it over the code and I can revert the hunk if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah it is an import cycle actually, I cannot do that, do you want me to do it? I would suggest to create an explicit CheckCACerts function and extract the parsing X509 code into a separate package.

Copy link
Contributor

@mvo5 mvo5 Oct 16, 2024

Choose a reason for hiding this comment

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

Thanks for looking into this, I'm not sure I follow, maybe I'm missing something but I was thinking of something like this:

diff --git a/pkg/blueprint/customizations.go b/pkg/blueprint/customizations.go
index 0bb5bacbe..f0d800077 100644
--- a/pkg/blueprint/customizations.go
+++ b/pkg/blueprint/customizations.go
@@ -428,12 +428,12 @@ func (c *Customizations) GetRHSM() *RHSMCustomization {
        return c.RHSM
 }
 
-func (c *Customizations) CheckCACerts() error {
+func (c *Customizations) checkCACerts() error {
        if c == nil {
                return nil
        }
 
-       if c.CACerts == nil {
+       if c.CACerts != nil {
                for _, bundle := range c.CACerts.PEMCerts {
                        _, err := cert.ParseCerts(bundle)
                        if err != nil {
@@ -445,9 +445,14 @@ func (c *Customizations) CheckCACerts() error {
        return nil
 }
 
-func (c *Customizations) GetCACerts() *CACustomization {
+func (c *Customizations) GetCACerts() (*CACustomization, error) {
        if c == nil {
-               return nil
+               return nil, nil
        }
-       return c.CACerts
+
+       if err := c.checkCACerts(); err != nil {
+               return nil, err
+       }
+
+       return c.CACerts, nil
 }
diff --git a/pkg/distro/fedora/images.go b/pkg/distro/fedora/images.go
index b0e737468..4c04a06c4 100644
--- a/pkg/distro/fedora/images.go
+++ b/pkg/distro/fedora/images.go
@@ -226,10 +226,11 @@ func osCustomizations(
        osc.Files = append(osc.Files, imageConfig.Files...)
        osc.Directories = append(osc.Directories, imageConfig.Directories...)
 
-       if err := c.CheckCACerts(); err != nil {
+       ca, err := c.GetCACerts()
+       if err != nil {
                panic(fmt.Sprintf("unexpected error checking CA certs: %v", err))
        }
-       if ca := c.GetCACerts(); ca != nil {
+       if ca != nil {
                osc.CACerts = ca.PEMCerts
        }
 
diff --git a/pkg/distro/rhel/images.go b/pkg/distro/rhel/images.go
index 5e13afe7f..806463b92 100644
--- a/pkg/distro/rhel/images.go
+++ b/pkg/distro/rhel/images.go
@@ -271,10 +271,11 @@ func osCustomizations(
                osc.NoBLS = *imageConfig.NoBLS
        }
 
-       if err := c.CheckCACerts(); err != nil {
+       ca, err := c.GetCACerts()
+       if err != nil {
                panic(fmt.Sprintf("unexpected error checking CA certs: %v", err))
        }
-       if ca := c.GetCACerts(); ca != nil {
+       if ca != nil {
                osc.CACerts = ca.PEMCerts
        }
 

i.e. every time we call GetCACerts() we implicitly check as well - this way we will not forget and it's similar to the pattern of GetRepositories() ?

return pipeline
}

func parseCerts(cert string) []*x509.Certificate {
Copy link
Contributor

Choose a reason for hiding this comment

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

A simple unit test for this would be very nice, ideally both for a "positive" and "error" case, something like

// taken from osbuild:test/data/certs/cert1.pem
const exampleCert = `
-----BEGIN CERTIFICATE-----
MIIDhTCCAm2gAwIBAgIUVya7VJ3O8W8SqwuEa0BZ4HSsXvAwDQYJKoZIhvcNAQEL
BQAwUTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy
bGluMQwwCgYDVQQKDANPcmcxEjAQBgNVBAMMCWxvY2FsaG9zdDAgFw0yNDA4MjYx
MDQyNDBaGA8yMTI0MDgwMjEwNDI0MFowUTELMAkGA1UEBhMCREUxDzANBgNVBAgM
BkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMQwwCgYDVQQKDANPcmcxEjAQBgNVBAMM
CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnGjlvN
O3F/Z7Lr/r+6Xp2DosnNwoPHhG2e61KnFzgZfaxbklal5ORpuV/gLIg7lrbpdZe7
WvK+16RanL6fLitis/tYVFyvz1MXqBYYrEoFGvVg9fOiis7hjpdZcpNDH9SngoAN
O0Wvv4T6LQS0cC7ZAFZjvmJ+RiZEbzRkNG5pUddZXbotE6htNfLgA5L1wIBgllrM
4DVkG0yNKmzqPNzfPTbdUgWCfjaQShHy1GP8KNEwFxM31F2wvQxsEb77o1S44Out
mlsi83tti6P7KjDk7w2j2zZO1X0xI8pflv3TBkJT1Am8vnk6rVnNO4pCpop3+kma
pDUEzBQmSQA5R1ECAwEAAaNTMFEwHQYDVR0OBBYEFDxFcFgPEsgsDixfKxB0uYGN
aJmzMB8GA1UdIwQYMBaAFDxFcFgPEsgsDixfKxB0uYGNaJmzMA8GA1UdEwEB/wQF
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFih4lUbLlhKwIAV9x3/W7Mih8xUEdZr
olquZgaHedFet+ByAHvoES3pec7AVYTOD53mjgyZubD6INnVHzKyS4AG9ydD73o4
cmm3DKxBaesvlHeTn0MOKsoM8QCxeyFJmiUPpgDBok/PFnbGR9+JcsrlGJAnsSKD
vWpiwYcBauZ9nnK5yDe5M9XNFPkNDZzbKvWU7Sw3ziMT/+bRJse5vTrYcyOnNGgy
gZNz2nimKy1U8XZVAVwOV0rdGEFrfMln8DkRW86rGK/EncaVsl0SSP/rmjQgiX8Q
3CZraQGujJP932HSwUfdCX9yh+rTjE3MEnbqMoLzJa4BXB2aDQWtywU=
-----END CERTIFICATE-----
`

func TestParseCerts(t *testing.T) {
	certs := parseCerts(exampleCert)
	assert.Equal(t, len(certs), 1)
	assert.Equal(t, "localhost", certs[0].Subject.CommonName)
}

maybe?

@lzap
Copy link
Contributor Author

lzap commented Oct 14, 2024

I have extracted the checking code + test into separate package cert and implemented an explicit check method. No warnings but errors.

Copy link
Contributor

@mvo5 mvo5 left a comment

Choose a reason for hiding this comment

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

Thanks for the update, looks very nice now, I made a suggestion inline, hope it's useful.

return nil
}

if c.CACerts == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be !=, no? If so ideally that would have triggered a test failure :/


func (c *Customizations) GetCACerts() *CACustomization {
if c == nil {
return nil
Copy link
Contributor

@mvo5 mvo5 Oct 16, 2024

Choose a reason for hiding this comment

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

Thanks for looking into this, I'm not sure I follow, maybe I'm missing something but I was thinking of something like this:

diff --git a/pkg/blueprint/customizations.go b/pkg/blueprint/customizations.go
index 0bb5bacbe..f0d800077 100644
--- a/pkg/blueprint/customizations.go
+++ b/pkg/blueprint/customizations.go
@@ -428,12 +428,12 @@ func (c *Customizations) GetRHSM() *RHSMCustomization {
        return c.RHSM
 }
 
-func (c *Customizations) CheckCACerts() error {
+func (c *Customizations) checkCACerts() error {
        if c == nil {
                return nil
        }
 
-       if c.CACerts == nil {
+       if c.CACerts != nil {
                for _, bundle := range c.CACerts.PEMCerts {
                        _, err := cert.ParseCerts(bundle)
                        if err != nil {
@@ -445,9 +445,14 @@ func (c *Customizations) CheckCACerts() error {
        return nil
 }
 
-func (c *Customizations) GetCACerts() *CACustomization {
+func (c *Customizations) GetCACerts() (*CACustomization, error) {
        if c == nil {
-               return nil
+               return nil, nil
        }
-       return c.CACerts
+
+       if err := c.checkCACerts(); err != nil {
+               return nil, err
+       }
+
+       return c.CACerts, nil
 }
diff --git a/pkg/distro/fedora/images.go b/pkg/distro/fedora/images.go
index b0e737468..4c04a06c4 100644
--- a/pkg/distro/fedora/images.go
+++ b/pkg/distro/fedora/images.go
@@ -226,10 +226,11 @@ func osCustomizations(
        osc.Files = append(osc.Files, imageConfig.Files...)
        osc.Directories = append(osc.Directories, imageConfig.Directories...)
 
-       if err := c.CheckCACerts(); err != nil {
+       ca, err := c.GetCACerts()
+       if err != nil {
                panic(fmt.Sprintf("unexpected error checking CA certs: %v", err))
        }
-       if ca := c.GetCACerts(); ca != nil {
+       if ca != nil {
                osc.CACerts = ca.PEMCerts
        }
 
diff --git a/pkg/distro/rhel/images.go b/pkg/distro/rhel/images.go
index 5e13afe7f..806463b92 100644
--- a/pkg/distro/rhel/images.go
+++ b/pkg/distro/rhel/images.go
@@ -271,10 +271,11 @@ func osCustomizations(
                osc.NoBLS = *imageConfig.NoBLS
        }
 
-       if err := c.CheckCACerts(); err != nil {
+       ca, err := c.GetCACerts()
+       if err != nil {
                panic(fmt.Sprintf("unexpected error checking CA certs: %v", err))
        }
-       if ca := c.GetCACerts(); ca != nil {
+       if ca != nil {
                osc.CACerts = ca.PEMCerts
        }
 

i.e. every time we call GetCACerts() we implicitly check as well - this way we will not forget and it's similar to the pattern of GetRepositories() ?

@@ -822,6 +826,25 @@ func (p *OS) serialize() osbuild.Pipeline {
}
}

if len(p.CACerts) > 0 {
for _, cc := range p.CACerts {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would love to see a test for this but I understand this area is really difficult to test so fine to ignore that (but I also can't help to mention it)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants