From 792999c0ae38cd1d120fb1f447cdf83cf4fc19e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Scha=CC=88fer?= <101886095+PeterSchafer@users.noreply.github.com> Date: Wed, 14 Dec 2022 09:14:47 +0100 Subject: [PATCH] fix: integrate support of NODE_EXTRA_CA_CERTS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * forwarding externally defined NODE_EXTRA_CA_CERTS * internally using NODE_EXTRA_CA_CERTS Signed-off-by: Peter Schäfer <101886095+PeterSchafer@users.noreply.github.com> --- cliv2/cmd/cliv2/main.go | 21 +++++- cliv2/cmd/make-cert/main.go | 2 +- cliv2/go.mod | 2 +- cliv2/go.sum | 4 +- cliv2/internal/certs/certs.go | 69 ------------------- cliv2/internal/proxy/proxy.go | 32 ++++++++- cliv2/internal/proxy/proxy_test.go | 24 +++++++ cliv2/pkg/basic_workflows/legacycli.go | 2 +- test/jest/acceptance/extra-certs.spec.ts | 86 ++++++++++++++++++++++++ 9 files changed, 164 insertions(+), 78 deletions(-) delete mode 100644 cliv2/internal/certs/certs.go create mode 100644 test/jest/acceptance/extra-certs.spec.ts diff --git a/cliv2/cmd/cliv2/main.go b/cliv2/cmd/cliv2/main.go index f00cd8dce6c..9f3ca199001 100644 --- a/cliv2/cmd/cliv2/main.go +++ b/cliv2/cmd/cliv2/main.go @@ -89,10 +89,17 @@ func sendAnalytics(analytics analytics.Analytics, debugLogger *log.Logger) { res, err := analytics.Send() errorCodeReceived := res != nil && 200 <= res.StatusCode && res.StatusCode < 300 - if err == nil && !errorCodeReceived { + if err == nil && errorCodeReceived { debugLogger.Println("Analytics sucessfully send") } else { - debugLogger.Println("Failed to send Analytics:", err) + var details string + if res != nil { + details = res.Status + } else if err != nil { + details = err.Error() + } + + debugLogger.Println("Failed to send Analytics:", details) } } @@ -233,6 +240,16 @@ func MainWithErrorCode() int { networkAccess := engine.GetNetworkAccess() networkAccess.AddHeaderField("x-snyk-cli-version", cliv2.GetFullVersion()) + extraCaCertFile := config.GetString(constants.SNYK_CA_CERTIFICATE_LOCATION_ENV) + if len(extraCaCertFile) > 0 { + err = networkAccess.AddRootCAs(extraCaCertFile) + if err != nil { + debugLogger.Printf("Failed to AddRootCAs from '%s' (%v)\n", extraCaCertFile, err) + } else { + debugLogger.Println("Using additional CAs from file:", extraCaCertFile) + } + } + // init Analytics cliAnalytics := engine.GetAnalytics() cliAnalytics.SetVersion(cliv2.GetFullVersion()) diff --git a/cliv2/cmd/make-cert/main.go b/cliv2/cmd/make-cert/main.go index 8c72f8baf53..fa42affabe2 100644 --- a/cliv2/cmd/make-cert/main.go +++ b/cliv2/cmd/make-cert/main.go @@ -7,8 +7,8 @@ import ( "path" "strings" - "github.com/snyk/cli/cliv2/internal/certs" "github.com/snyk/cli/cliv2/internal/utils" + "github.com/snyk/go-application-framework/pkg/networking/certs" ) func main() { diff --git a/cliv2/go.mod b/cliv2/go.mod index 0cd740a0e74..bca9be2876f 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 github.com/snyk/cli-extension-sbom v0.0.0-20221212093410-6b474ed1a42a - github.com/snyk/go-application-framework v0.0.0-20221213122015-81ad8dd6311d + github.com/snyk/go-application-framework v0.0.0-20221215182111-b2d10cf1e146 github.com/snyk/go-httpauth v0.0.0-20220915135832-0edf62cf8cdd github.com/spf13/cobra v1.6.0 github.com/spf13/pflag v1.0.5 diff --git a/cliv2/go.sum b/cliv2/go.sum index 6dca9c5e152..a8e7fb41e86 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -184,8 +184,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/snyk/cli-extension-sbom v0.0.0-20221212093410-6b474ed1a42a h1:kImXWA4kbwaREeC+kaJ8H0aOukWzpK8K/UzAsExj6MU= github.com/snyk/cli-extension-sbom v0.0.0-20221212093410-6b474ed1a42a/go.mod h1:ohrrgC94Gx82/cgSiac02JQrsMjFtggvhAvXGuGjDGU= -github.com/snyk/go-application-framework v0.0.0-20221213122015-81ad8dd6311d h1:5//WGQrFXri33xGuLgVEHOsBD0aU2ZHU8JFEGJBBc68= -github.com/snyk/go-application-framework v0.0.0-20221213122015-81ad8dd6311d/go.mod h1:5hLGqObbxLWnZkhn3Xc5PblESjQOfjN509ucQ4dtqz8= +github.com/snyk/go-application-framework v0.0.0-20221215182111-b2d10cf1e146 h1:V5kc8tSGVhyiPNuEXkZ9CVmwWiYlMmaQGpjRbORuqlU= +github.com/snyk/go-application-framework v0.0.0-20221215182111-b2d10cf1e146/go.mod h1:5hLGqObbxLWnZkhn3Xc5PblESjQOfjN509ucQ4dtqz8= github.com/snyk/go-httpauth v0.0.0-20220915135832-0edf62cf8cdd h1:zjDhcQ642rIVI8aIjfG5uVcw+OGotQtX2l9VHe7IqCQ= github.com/snyk/go-httpauth v0.0.0-20220915135832-0edf62cf8cdd/go.mod h1:v6t6wKizOcHXT3p4qKn6Bda7yNIjCQ54Xyl31NjgXkY= github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= diff --git a/cliv2/internal/certs/certs.go b/cliv2/internal/certs/certs.go deleted file mode 100644 index a57ff1e000e..00000000000 --- a/cliv2/internal/certs/certs.go +++ /dev/null @@ -1,69 +0,0 @@ -package certs - -import ( - "bytes" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "fmt" - "log" - "math/big" - "time" -) - -func MakeSelfSignedCert(certName string, dnsNames []string, debugLogger *log.Logger) (certPEMBlock []byte, keyPEMBlock []byte, err error) { - // create a key - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, err - } - - // create a self-signed cert using the key - template := x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - CommonName: certName, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 365), - - KeyUsage: x509.KeyUsageDigitalSignature | - x509.KeyUsageKeyEncipherment | - x509.KeyUsageKeyAgreement | - x509.KeyUsageCertSign, // needed for sure - - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - IsCA: true, - } - - for _, dnsName := range dnsNames { - template.DNSNames = append(template.DNSNames, dnsName) - debugLogger.Println("MakeSelfSignedCert added ", dnsName) - } - - certDERBytes, err_CreateCertificate := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) - if err_CreateCertificate != nil { - return nil, nil, err - } - - certPEMBytesBuffer := &bytes.Buffer{} - if err := pem.Encode(certPEMBytesBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: certDERBytes}); err != nil { - fmt.Println(err) - return nil, nil, err - } - - // make the key pem - keyDERBytes := x509.MarshalPKCS1PrivateKey(privateKey) - keyPEMBytesBuffer := &bytes.Buffer{} - if err := pem.Encode(keyPEMBytesBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDERBytes}); err != nil { - return nil, nil, err - } - - certPEMBlockBytes := certPEMBytesBuffer.Bytes() - keyPEMBlockBytes := keyPEMBytesBuffer.Bytes() - - return certPEMBlockBytes, keyPEMBlockBytes, nil -} diff --git a/cliv2/internal/proxy/proxy.go b/cliv2/internal/proxy/proxy.go index d5192d7c724..c0da976e98d 100644 --- a/cliv2/internal/proxy/proxy.go +++ b/cliv2/internal/proxy/proxy.go @@ -13,8 +13,9 @@ import ( "github.com/google/uuid" - "github.com/snyk/cli/cliv2/internal/certs" + "github.com/snyk/cli/cliv2/internal/constants" "github.com/snyk/cli/cliv2/internal/utils" + "github.com/snyk/go-application-framework/pkg/networking/certs" "github.com/snyk/go-httpauth/pkg/httpauth" "github.com/elazarl/goproxy" @@ -71,7 +72,33 @@ func NewWrapperProxy(insecureSkipVerify bool, cacheDirectory string, cliVersion defer certFile.Close() p.CertificateLocation = certFile.Name() // gives full path, not just the name - p.DebugLogger.Println("p.CertificateLocation:", p.CertificateLocation) + + rootCAs, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + + // append any given extra CA certificate to the internal PEM data before storing it to file + // this merges user provided CA certificates with the internal one + if extraCaCertFile, ok := os.LookupEnv(constants.SNYK_CA_CERTIFICATE_LOCATION_ENV); ok { + extraCertificateBytes, extraCertificateList, extraCertificateError := certs.GetExtraCaCert(extraCaCertFile) + if extraCertificateError == nil { + // add to pem data + certPEMBlock = append(certPEMBlock, '\n') + certPEMBlock = append(certPEMBlock, extraCertificateBytes...) + + // add to cert pool + for _, currentCert := range extraCertificateList { + if currentCert != nil { + rootCAs.AddCert(currentCert) + } + } + + p.DebugLogger.Println("Using additional CAs from file: ", extraCaCertFile) + } + } + + p.DebugLogger.Println("Temporary CertificateLocation:", p.CertificateLocation) certPEMString := string(certPEMBlock) err = utils.WriteToFile(p.CertificateLocation, certPEMString) @@ -88,6 +115,7 @@ func NewWrapperProxy(insecureSkipVerify bool, cacheDirectory string, cliVersion p.transport = &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: insecureSkipVerify, // goproxy defaults to true + RootCAs: rootCAs, }, } diff --git a/cliv2/internal/proxy/proxy_test.go b/cliv2/internal/proxy/proxy_test.go index cfc1c24765b..2a0232a4d04 100644 --- a/cliv2/internal/proxy/proxy_test.go +++ b/cliv2/internal/proxy/proxy_test.go @@ -13,7 +13,9 @@ import ( "os" "testing" + "github.com/snyk/cli/cliv2/internal/constants" "github.com/snyk/cli/cliv2/internal/proxy" + "github.com/snyk/go-application-framework/pkg/networking/certs" "github.com/snyk/go-httpauth/pkg/httpauth" "github.com/stretchr/testify/assert" @@ -218,3 +220,25 @@ func Test_SetUpstreamProxy(t *testing.T) { } } } + +func Test_appendExtraCaCert(t *testing.T) { + certPem, _, _ := certs.MakeSelfSignedCert("mycert", []string{"dns"}, debugLogger) + file, _ := os.CreateTemp("", "") + file.Write(certPem) + + os.Setenv(constants.SNYK_CA_CERTIFICATE_LOCATION_ENV, file.Name()) + + wp, err := proxy.NewWrapperProxy(false, "", "", debugLogger) + assert.Nil(t, err) + + certsPem, err := os.ReadFile(wp.CertificateLocation) + assert.Nil(t, err) + + certsList, err := certs.GetAllCerts(certsPem) + assert.Nil(t, err) + assert.Equal(t, 2, len(certsList)) + + // cleanup + os.Unsetenv(constants.SNYK_CA_CERTIFICATE_LOCATION_ENV) + os.Remove(file.Name()) +} diff --git a/cliv2/pkg/basic_workflows/legacycli.go b/cliv2/pkg/basic_workflows/legacycli.go index 750a92450bb..9d02ea0db0f 100644 --- a/cliv2/pkg/basic_workflows/legacycli.go +++ b/cliv2/pkg/basic_workflows/legacycli.go @@ -84,10 +84,10 @@ func legacycliWorkflow(invocation workflow.InvocationContext, input []workflow.D // init proxy object wrapperProxy, err := proxy.NewWrapperProxy(insecure, cacheDirectory, cliv2.GetFullVersion(), debugLogger) - defer wrapperProxy.Close() if err != nil { return output, errors.Wrap(err, "Failed to create proxy!") } + defer wrapperProxy.Close() wrapperProxy.SetUpstreamProxyAuthentication(proxyAuthenticationMechanism) diff --git a/test/jest/acceptance/extra-certs.spec.ts b/test/jest/acceptance/extra-certs.spec.ts new file mode 100644 index 00000000000..33361f93d91 --- /dev/null +++ b/test/jest/acceptance/extra-certs.spec.ts @@ -0,0 +1,86 @@ +import { runSnykCLI } from '../util/runSnykCLI'; +import * as fs from 'fs'; +import * as fsPromises from 'fs/promises'; +import { runCommand } from '../util/runCommand'; +import { fakeServer } from '../../../test/acceptance/fake-server'; +import { isCLIV2 } from '../util/isCLIV2'; + +if (isCLIV2()) { + console.debug("isCLIV2") +} + +jest.setTimeout(1000 * 60 * 1); +describe('Extra CA certificates specified with `NODE_EXTRA_CA_CERTS`', () => { + it('using a not existing file', async () => { + const { code } = await runSnykCLI(`woof --debug`, { + env: { + ...process.env, + NODE_EXTRA_CA_CERTS: "doesntexist.crt" + }, + }); + + expect(code).toBe(0); + }); + + it('using an invalid file', async () => { + const filename = "someotherfile.txt" + var writeStream = fs.createWriteStream(filename); + writeStream.write("Hello World"); + writeStream.end(); + + const { code, stdout } = await runSnykCLI(`woof --debug`, { + env: { + ...process.env, + NODE_EXTRA_CA_CERTS: filename + }, + }); + + expect(code).toBe(0); + fs.unlink(filename, () => {}) + }); + + it('using an valid cert file', async () => { + // generate certificate + const res = await runCommand("go", ["run", "cmd/make-cert/main.go", "mytestcert"], {cwd: "cliv2", env: {...process.env, SNYK_DNS_NAMES: "localhost"}}) + + console.debug(res.stderr) + expect(res.code).toBe(0) + + // setup https server + const port = 2132; + const token = '1234' + const baseApi = '/api/v1'; + const SNYK_API = 'https://localhost:' + port + baseApi; + var server = fakeServer(baseApi, token) + const certPem = await fsPromises.readFile("cliv2/mytestcert.pem", { encoding: 'utf8' }) + const keyPem = await fsPromises.readFile("cliv2/mytestcert.key", { encoding: 'utf8' }) + await server.listenWithHttps(port, {cert: certPem, key: keyPem}) + + // invoke WITHOUT additional certificate set => fails + const res1 = await runSnykCLI(`test --debug`, { + env: { + ...process.env, + SNYK_API: SNYK_API, + SNYK_TOKEN: token, + }, + }); + + // invoke WITH additional certificate set => succeeds + const res2 = await runSnykCLI(`test --debug`, { + env: { + ...process.env, + NODE_EXTRA_CA_CERTS: "cliv2/mytestcert.crt", + SNYK_API: SNYK_API, + SNYK_TOKEN: token, + }, + }); + + await server.closePromise() + + expect(res1.code).toBe(2); + expect(res2.code).toBe(0); + fs.unlink("cliv2/mytestcert.crt", () => {}) + fs.unlink("cliv2/mytestcert.key", () => {}) + fs.unlink("cliv2/mytestcert.pem", () => {}) + }); +});