diff --git a/mod/tigron/utils/testca/ca.go b/mod/tigron/utils/testca/ca.go new file mode 100644 index 00000000000..662be0c810c --- /dev/null +++ b/mod/tigron/utils/testca/ca.go @@ -0,0 +1,170 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package testca provides helpers to create a self-signed CA certificate, and the ability to generate +// signed certificates from it. +// PLEASE NOTE THIS IS NOT A PRODUCTION SAFE NOR VERIFIED WAY TO MANAGE CERTIFICATES FOR SERVERS. +package testca + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io" + "math/big" + "net" + "time" + + "github.com/containerd/nerdctl/mod/tigron/internal/assertive" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" +) + +const ( + keyLength = 4096 + caRoot = "ca" + certsRoot = "certs" + organization = "tigron volatile testing organization" + lifetime = 24 * time.Hour + serialSize = 60 +) + +// NewX509 creates a new, self-signed, signing certificate under data.Temp()/ca +// From that Cert as a CA, you can then generate signed certificates. +// Note that the common name of the cert will be set to the test name. +func NewX509(data test.Data, helpers test.Helpers) *Cert { + template := &x509.Certificate{ + Subject: pkix.Name{ + Organization: []string{organization}, + CommonName: helpers.T().Name(), + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(lifetime), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + return (&Cert{}).GenerateCustomX509(data, helpers, caRoot, template) +} + +// Cert allows the consumer to retrieve the cert and key path, to be used by other processes, like servers for example. +type Cert struct { + KeyPath string + CertPath string + key *rsa.PrivateKey + cert *x509.Certificate +} + +// GenerateServerX509 produces a certificate usable by a server. +// additional can be used to provide additional ips to be added to the certificate. +func (ca *Cert) GenerateServerX509(data test.Data, helpers test.Helpers, host string, additional ...string) *Cert { + template := &x509.Certificate{ + Subject: pkix.Name{ + Organization: []string{organization}, + CommonName: host, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(lifetime), + KeyUsage: x509.KeyUsageCRLSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: additional, + } + + additional = append([]string{host}, additional...) + for _, h := range additional { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } + } + + return ca.GenerateCustomX509(data, helpers, certsRoot, template) +} + +// GenerateCustomX509 signs a random x509 certificate template. +// Note that if SerialNumber is specified, it must be safe to use on the filesystem as this will be used in the name +// of the certificate file. +func (ca *Cert) GenerateCustomX509( + data test.Data, + helpers test.Helpers, + underDirectory string, + template *x509.Certificate, +) *Cert { + silentT := assertive.WithSilentSuccess(helpers.T()) + key, certPath, keyPath := createCert(silentT, data, underDirectory, template, ca.cert, ca.key) + + return &Cert{ + CertPath: certPath, + KeyPath: keyPath, + key: key, + cert: template, + } +} + +func createCert( + testing tig.T, + data test.Data, + dir string, + template, caCert *x509.Certificate, + caKey *rsa.PrivateKey, +) (key *rsa.PrivateKey, certPath, keyPath string) { + if caCert == nil { + caCert = template + } + + if caKey == nil { + caKey = key + } + + key, err := rsa.GenerateKey(rand.Reader, keyLength) + assertive.ErrorIsNil(testing, err, "key generation should succeed") + + signedCert, err := x509.CreateCertificate(rand.Reader, template, caCert, &key.PublicKey, caKey) + assertive.ErrorIsNil(testing, err, "certificate creation should succeed") + + serial := template.SerialNumber + if serial == nil { + serial = serialNumber() + } + + data.Temp().Dir(dir) + certPath = data.Temp().Path(dir, serial.String()+".cert") + keyPath = data.Temp().Path(dir, serial.String()+".key") + + data.Temp().SaveToWriter(func(writer io.Writer) error { + return pem.Encode(writer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + }, keyPath) + + data.Temp().SaveToWriter(func(writer io.Writer) error { + return pem.Encode(writer, &pem.Block{Type: "CERTIFICATE", Bytes: signedCert}) + }, keyPath) + + return key, certPath, keyPath +} + +func serialNumber() *big.Int { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), serialSize) + + serial, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + panic(err) + } + + return serial +} diff --git a/pkg/containerdutil/image_store.go b/pkg/containerdutil/image_store.go new file mode 100644 index 00000000000..571f42c69c2 --- /dev/null +++ b/pkg/containerdutil/image_store.go @@ -0,0 +1,20 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package containerdutil + +type ImageStore struct { +} diff --git a/pkg/testutil/nerdtest/ca/ca.go b/pkg/testutil/nerdtest/ca/ca.go deleted file mode 100644 index 49d69f81b4f..00000000000 --- a/pkg/testutil/nerdtest/ca/ca.go +++ /dev/null @@ -1,161 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package ca - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "fmt" - "math/big" - "net" - "os" - "path/filepath" - "testing" - "time" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/mod/tigron/test" -) - -type CA struct { - KeyPath string - CertPath string - - t *testing.T - key *rsa.PrivateKey - cert *x509.Certificate - closeF func() error -} - -func (ca *CA) Close() error { - return ca.closeF() -} - -const keyLength = 4096 - -func New(data test.Data, t *testing.T) *CA { - key, err := rsa.GenerateKey(rand.Reader, keyLength) - assert.NilError(t, err) - - cert := &x509.Certificate{ - SerialNumber: serialNumber(t), - Subject: pkix.Name{ - Organization: []string{"nerdctl test organization"}, - CommonName: fmt.Sprintf("nerdctl CA (%s)", t.Name()), - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(24 * time.Hour), - IsCA: true, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - BasicConstraintsValid: true, - } - - dir := data.Temp().Dir("ca") - keyPath := filepath.Join(dir, "ca.key") - certPath := filepath.Join(dir, "ca.cert") - writePair(t, keyPath, certPath, cert, cert, key, key) - - return &CA{ - KeyPath: keyPath, - CertPath: certPath, - t: t, - key: key, - cert: cert, - closeF: func() error { - return os.RemoveAll(dir) - }, - } -} - -type Cert struct { - KeyPath string - CertPath string - closeF func() error -} - -func (c *Cert) Close() error { - return c.closeF() -} - -func (ca *CA) NewCert(host string, additional ...string) *Cert { - t := ca.t - - key, err := rsa.GenerateKey(rand.Reader, keyLength) - assert.NilError(t, err) - - additional = append([]string{host}, additional...) - - cert := &x509.Certificate{ - SerialNumber: serialNumber(t), - Subject: pkix.Name{ - Organization: []string{"nerdctl test organization"}, - CommonName: fmt.Sprintf("nerdctl %s (%s)", host, t.Name()), - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(24 * time.Hour), - KeyUsage: x509.KeyUsageCRLSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - DNSNames: additional, - } - for _, h := range additional { - if ip := net.ParseIP(h); ip != nil { - cert.IPAddresses = append(cert.IPAddresses, ip) - } - } - - dir, err := os.MkdirTemp(t.TempDir(), "cert") - assert.NilError(t, err) - certPath := filepath.Join(dir, "a.cert") - keyPath := filepath.Join(dir, "a.key") - writePair(t, keyPath, certPath, cert, ca.cert, key, ca.key) - - return &Cert{ - CertPath: certPath, - KeyPath: keyPath, - closeF: func() error { - return os.RemoveAll(dir) - }, - } -} - -func writePair(t *testing.T, keyPath, certPath string, cert, caCert *x509.Certificate, key, caKey *rsa.PrivateKey) { - keyF, err := os.Create(keyPath) - assert.NilError(t, err) - defer keyF.Close() - assert.NilError(t, pem.Encode(keyF, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) - assert.NilError(t, keyF.Close()) - - certB, err := x509.CreateCertificate(rand.Reader, cert, caCert, &key.PublicKey, caKey) - assert.NilError(t, err) - certF, err := os.Create(certPath) - assert.NilError(t, err) - defer certF.Close() - assert.NilError(t, pem.Encode(certF, &pem.Block{Type: "CERTIFICATE", Bytes: certB})) - assert.NilError(t, certF.Close()) -} - -func serialNumber(t *testing.T) *big.Int { - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 60) - sn, err := rand.Int(rand.Reader, serialNumberLimit) - assert.NilError(t, err) - return sn -} diff --git a/pkg/testutil/nerdtest/registry/cesanta.go b/pkg/testutil/nerdtest/registry/cesanta.go index 111286e3c6b..1a83f73dfcb 100644 --- a/pkg/testutil/nerdtest/registry/cesanta.go +++ b/pkg/testutil/nerdtest/registry/cesanta.go @@ -31,9 +31,9 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/utils/testca" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" - "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" @@ -119,7 +119,7 @@ func ensureContainerStarted(helpers test.Helpers, con string) { } } -func NewCesantaAuthServer(data test.Data, helpers test.Helpers, ca *ca.CA, port int, user, pass string, tls bool) *TokenAuthServer { +func NewCesantaAuthServer(data test.Data, helpers test.Helpers, ca *testca.Cert, port int, user, pass string, tls bool) *TokenAuthServer { // listen on 0.0.0.0 to enable 127.0.0.1 listenIP := net.ParseIP("0.0.0.0") hostIP, err := nettestutil.NonLoopbackIPv4() @@ -165,7 +165,7 @@ func NewCesantaAuthServer(data test.Data, helpers test.Helpers, ca *ca.CA, port err = cc.Save(configFileName) assert.NilError(helpers.T(), err, fmt.Errorf("failed writing configuration: %w", err)) - cert := ca.NewCert(hostIP.String()) + cert := ca.GenerateServerX509(data, helpers, hostIP.String()) // FIXME: this will fail in many circumstances. Review strategy on how to acquire a free port. // We probably have better code for that already somewhere. port, err = portlock.Acquire(port) @@ -177,13 +177,9 @@ func NewCesantaAuthServer(data test.Data, helpers test.Helpers, ca *ca.CA, port cleanup := func(data test.Data, helpers test.Helpers) { helpers.Ensure("rm", "-f", containerName) errPortRelease := portlock.Release(port) - errCertClose := cert.Close() if errPortRelease != nil { helpers.T().Error(errPortRelease.Error()) } - if errCertClose != nil { - helpers.T().Error(errCertClose.Error()) - } } setup := func(data test.Data, helpers test.Helpers) { diff --git a/pkg/testutil/nerdtest/registry/docker.go b/pkg/testutil/nerdtest/registry/docker.go index 82cde7f21d6..6e90cdfcfc9 100644 --- a/pkg/testutil/nerdtest/registry/docker.go +++ b/pkg/testutil/nerdtest/registry/docker.go @@ -25,15 +25,15 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/utils/testca" - "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/hoststoml" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" ) -func NewDockerRegistry(data test.Data, helpers test.Helpers, currentCA *ca.CA, port int, auth Auth) *Server { +func NewDockerRegistry(data test.Data, helpers test.Helpers, currentCA *testca.Cert, port int, auth Auth) *Server { // listen on 0.0.0.0 to enable 127.0.0.1 listenIP := net.ParseIP("0.0.0.0") hostIP, err := nettestutil.NonLoopbackIPv4() @@ -56,10 +56,10 @@ func NewDockerRegistry(data test.Data, helpers test.Helpers, currentCA *ca.CA, p "--name", containerName, } scheme := "http" - var cert *ca.Cert + var cert *testca.Cert if currentCA != nil { scheme = "https" - cert = currentCA.NewCert(hostIP.String(), "127.0.0.1", "localhost", "::1") + cert = currentCA.GenerateServerX509(data, helpers, hostIP.String(), "127.0.0.1", "localhost", "::1") args = append(args, "--env", "REGISTRY_HTTP_TLS_CERTIFICATE=/registry/domain.crt", "--env", "REGISTRY_HTTP_TLS_KEY=/registry/domain.key", @@ -86,10 +86,6 @@ func NewDockerRegistry(data test.Data, helpers test.Helpers, currentCA *ca.CA, p helpers.Anyhow("rm", "-f", containerName) errPortRelease := portlock.Release(port) - if cert != nil { - assert.NilError(helpers.T(), cert.Close(), fmt.Errorf("failed cleaning certificates: %w", err)) - } - assert.NilError(helpers.T(), errPortRelease, fmt.Errorf("failed releasing port: %w", err)) } diff --git a/pkg/testutil/nerdtest/registry/kubo.go b/pkg/testutil/nerdtest/registry/kubo.go index 1a9a3ca2db8..40c0f67f798 100644 --- a/pkg/testutil/nerdtest/registry/kubo.go +++ b/pkg/testutil/nerdtest/registry/kubo.go @@ -25,14 +25,14 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/utils/testca" - "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" ) -func NewKuboRegistry(data test.Data, helpers test.Helpers, t *testing.T, currentCA *ca.CA, port int, auth Auth) *Server { +func NewKuboRegistry(data test.Data, helpers test.Helpers, t *testing.T, currentCA *testca.Cert, port int, auth Auth) *Server { // listen on 0.0.0.0 to enable 127.0.0.1 listenIP := net.ParseIP("0.0.0.0") hostIP, err := nettestutil.NonLoopbackIPv4() diff --git a/pkg/testutil/nerdtest/third-party.go b/pkg/testutil/nerdtest/third-party.go index 0a0906599e9..f845a61f84e 100644 --- a/pkg/testutil/nerdtest/third-party.go +++ b/pkg/testutil/nerdtest/third-party.go @@ -22,8 +22,8 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/utils/testca" - "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) @@ -45,16 +45,16 @@ func KubeCtlCommand(helpers test.Helpers, args ...string) test.TestableCommand { } func RegistryWithTokenAuth(data test.Data, helpers test.Helpers, user, pass string, port int, tls bool) (*registry.Server, *registry.TokenAuthServer) { - rca := ca.New(data, helpers.T()) + rca := testca.NewX509(data, helpers) as := registry.NewCesantaAuthServer(data, helpers, rca, 0, user, pass, tls) re := registry.NewDockerRegistry(data, helpers, rca, port, as.Auth) return re, as } func RegistryWithNoAuth(data test.Data, helpers test.Helpers, port int, tls bool) *registry.Server { - var rca *ca.CA + var rca *testca.Cert if tls { - rca = ca.New(data, helpers.T()) + rca = testca.NewX509(data, helpers) } return registry.NewDockerRegistry(data, helpers, rca, port, ®istry.NoAuth{}) } @@ -64,9 +64,9 @@ func RegistryWithBasicAuth(data test.Data, helpers test.Helpers, user, pass stri Username: user, Password: pass, } - var rca *ca.CA + var rca *testca.Cert if tls { - rca = ca.New(data, helpers.T()) + rca = testca.NewX509(data, helpers) } return registry.NewDockerRegistry(data, helpers, rca, port, auth) }