From 48f8235649c910bf852e8546d148800e5de8c831 Mon Sep 17 00:00:00 2001 From: Raghav Date: Wed, 15 Jul 2026 15:55:16 +0530 Subject: [PATCH] [CI-23743]: Support loading hosted certs in windows (#518) --- ca_pem.go | 29 +++++++++++++ ca_pem_test.go | 114 +++++++++++++++++++++++++++++++++++++++++++++++++ ca_windows.go | 86 +++++++++++++++++++++++++------------ go.mod | 2 +- 4 files changed, 203 insertions(+), 28 deletions(-) create mode 100644 ca_pem.go create mode 100644 ca_pem_test.go diff --git a/ca_pem.go b/ca_pem.go new file mode 100644 index 0000000..d098424 --- /dev/null +++ b/ca_pem.go @@ -0,0 +1,29 @@ +package docker + +import ( + "encoding/pem" + "fmt" +) + +// decodePEMCertificates extracts DER-encoded CERTIFICATE blocks from a PEM +// (or PEM bundle). Non-certificate blocks are skipped. Returns an error when +// no certificate blocks are present. +func decodePEMCertificates(pemBytes []byte) ([][]byte, error) { + var ders [][]byte + rest := pemBytes + for { + var block *pem.Block + block, rest = pem.Decode(rest) + if block == nil { + break + } + if block.Type != "CERTIFICATE" { + continue + } + ders = append(ders, block.Bytes) + } + if len(ders) == 0 { + return nil, fmt.Errorf("no CERTIFICATE PEM blocks found") + } + return ders, nil +} diff --git a/ca_pem_test.go b/ca_pem_test.go new file mode 100644 index 0000000..5273505 --- /dev/null +++ b/ca_pem_test.go @@ -0,0 +1,114 @@ +package docker + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "testing" +) + +// Real self-signed CAs generated for these tests (CN=HarnessTestCA / HarnessTestCA2). +const harnessTestCAPEM = `-----BEGIN CERTIFICATE----- +MIIDETCCAfmgAwIBAgIUUnMVT6JwuvsR8lPanhZxYJnqUPYwDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNSGFybmVzc1Rlc3RDQTAeFw0yNjA3MTUwNzMxMDBaFw0z +NjA3MTIwNzMxMDBaMBgxFjAUBgNVBAMMDUhhcm5lc3NUZXN0Q0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJOssgNxmGP8itq7IxObLY2igTK3/YeMIJ +9gvjWzUGe/RRrWqWJvfplKScNaIbyGaABYbDaGURnL3uGr5QQWEUbqvOMlEYaoNq +21TmVbEl6Sponc+4aFfDPP9CFAOn2tqyrZaU3wZIjSUWbGo2wwseWULzaQxZ8lZ6 +a+kgjipymolfTZeHbJQAk/hwXVhbrz6xBbOhue/K/tFJxFBuawvwgXEi+2ywzXgy +fn80wYWdjeFEpFAtNKyjg+bu36DtHikwDS9XqZzgSZnGhDKEGh3cj0hd3/nhr59h +FCDm56XsBvWQVHmNmiuuxCn3svzSa9qdyk6tcIqZN8wKz1J7lFRfAgMBAAGjUzBR +MB0GA1UdDgQWBBSrLR7fG/bwSozrDipyfT5MtiuYjTAfBgNVHSMEGDAWgBSrLR7f +G/bwSozrDipyfT5MtiuYjTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA +A4IBAQAq9+CxKqe2vhBTz+5i9NBpxzxFy3fZTKcakuB98hafnNw29D4VggjV7sgo +99En87gkTIPsiOsRa0YH3wl5aa5EUp/Dx0ZfXasjzS5BFSZKZDYcuLuuUEeK9N6j +UBxaWDxNRjENevkX8SWpbxyNkcDGZM2IfJvVA/g7h8ERZLjlu9iOTqb6jt3PDe4q +CNoMbU5L3md3gk8mfoEZAX/IIUmbw7hAy7zE9dEfbKHeXl3FrcXPL0WmLePcK9BF +8XEpsOUSoq4sMPdVxaPF7l+SKDbdsR4gKtwlEMLefiiLqk0k4HhygjzzRgdsMOc3 +S8dL7zv0BrAFy//XsfmsVeVFVhRN +-----END CERTIFICATE----- +` + +const harnessTestCA2PEM = `-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIUR5AfE7MTe0ucAk1JzlqvOJd3Z7AwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOSGFybmVzc1Rlc3RDQTIwHhcNMjYwNzE1MDczMTAwWhcN +MzYwNzEyMDczMTAwWjAZMRcwFQYDVQQDDA5IYXJuZXNzVGVzdENBMjCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAI2EDM2Ac+jwhP8OaWFFb+DlVwzKSkEI +LoaZ41RQGHZaCckTLtaCtIBaNQHLWqj8q6I0fIz+8mmaXuxRKDjnhGl3AFIJ2wql +Hbyywo2MQcu+6wfs2Ewd887NDb5wrNJs/gl1GYRilSAd8n53T5IJUHHA0IGxtq4g +9XwS6OTmfBdQ3ycEsi9Yexd5Oz79sLMDhBnt21nYWrEO8Kumgyfd7gfQUBcGc1nH +NhoBrZffLmo+cljGqPNEFyAlWEnsmiqZnYh/EOUXGdXxsZMvyMWt02kLe0BlGmJr +PrI8iJCMElj86UhKK2oIjWLMA6cviYSkw9jyiihhodSUChhmqOABoe8CAwEAAaNT +MFEwHQYDVR0OBBYEFKK9RP95NSP4xmxN+hojPXlxVVatMB8GA1UdIwQYMBaAFKK9 +RP95NSP4xmxN+hojPXlxVVatMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBABlrORwXmhmO+eIdTHZMuad72JMEJeHxiRCGbwH6vAVb03YZAXLmIf31 +sBKkUCDV+Vm0MaTYmYncK3eZZBn6teEWcx7+L9XlF7ckVKBqj9xkwFUdfVipCimD +cjBl/H+2PEY5Hzoy/uf2/sE1czrfHA2HeEIEacVyht9t0HTtqXQqrn44ijESkNtb +SqqaVYCcceS85ID2oJYYKQycMnFJd0VwSHlD+Lu4ZubXbrg+pzSqw6olTOwCE7Q2 +LhLxQRB4f8Xw807aIuFK2mKbPs+4dmPkhClM3rbFzzldQJ4lKLnvZHcFtXJUXg2a +UzwP8JQn8xuz7gIZxMO3vGqTweP48yc= +-----END CERTIFICATE----- +` + +func TestDecodePEMCertificates_Single(t *testing.T) { + ders, err := decodePEMCertificates([]byte(harnessTestCAPEM)) + if err != nil { + t.Fatalf("decodePEMCertificates: %v", err) + } + if len(ders) != 1 { + t.Fatalf("got %d certs, want 1", len(ders)) + } + cert, err := x509.ParseCertificate(ders[0]) + if err != nil { + t.Fatalf("ParseCertificate: %v", err) + } + if cert.Subject.CommonName != "HarnessTestCA" { + t.Fatalf("CN = %q, want HarnessTestCA", cert.Subject.CommonName) + } +} + +func TestDecodePEMCertificates_Bundle(t *testing.T) { + bundle := harnessTestCAPEM + "\n" + harnessTestCA2PEM + ders, err := decodePEMCertificates([]byte(bundle)) + if err != nil { + t.Fatalf("decodePEMCertificates: %v", err) + } + if len(ders) != 2 { + t.Fatalf("got %d certs, want 2", len(ders)) + } + for i, der := range ders { + if _, err := x509.ParseCertificate(der); err != nil { + t.Fatalf("cert %d: ParseCertificate: %v", i, err) + } + } +} + +func TestDecodePEMCertificates_SkipsNonCertificateBlocks(t *testing.T) { + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: []byte("not-a-key")}) + mixed := append(append([]byte{}, keyPEM...), []byte(harnessTestCAPEM)...) + ders, err := decodePEMCertificates(mixed) + if err != nil { + t.Fatalf("decodePEMCertificates: %v", err) + } + if len(ders) != 1 { + t.Fatalf("got %d certs, want 1 (private key block skipped)", len(ders)) + } +} + +func TestDecodePEMCertificates_Empty(t *testing.T) { + _, err := decodePEMCertificates([]byte("not a pem\n")) + if err == nil { + t.Fatal("expected error for input with no CERTIFICATE blocks") + } +} + +func TestDecodePEMCertificates_MatchesOpenSSLDER(t *testing.T) { + ders, err := decodePEMCertificates([]byte(harnessTestCAPEM)) + if err != nil { + t.Fatal(err) + } + origBlock, _ := pem.Decode([]byte(harnessTestCAPEM)) + if !bytes.Equal(origBlock.Bytes, ders[0]) { + t.Fatal("decoded DER does not match original PEM payload") + } +} diff --git a/ca_windows.go b/ca_windows.go index d4db1e2..93a4696 100644 --- a/ca_windows.go +++ b/ca_windows.go @@ -5,8 +5,9 @@ package docker import ( "fmt" - "os" - "os/exec" + "unsafe" + + "golang.org/x/sys/windows" ) // installHarnessCA imports the Harness egress-proxy CA into the Windows @@ -14,38 +15,69 @@ import ( // Docker daemon and Go's crypto/x509 consult). Best-effort: failures are logged, // never fatal. // -// certutil needs a file path, so the (already-validated) CA bytes are written to -// a temp .crt first, then added with `certutil -addstore -f Root `. +// Uses CryptoAPI (crypt32.dll) directly so it works on Nano Server images that +// lack certutil and PowerShell. CERT_STORE_ADD_REPLACE_EXISTING keeps the step +// idempotent across retries. func installHarnessCA(caPEM []byte) { - tmp, err := os.CreateTemp("", "harness-egress-ca-*.crt") + ders, err := decodePEMCertificates(caPEM) if err != nil { - fmt.Printf("Could not create temp file for Harness CA: %s\n", err) - return - } - tmpPath := tmp.Name() - defer os.Remove(tmpPath) - - if _, err := tmp.Write(caPEM); err != nil { - tmp.Close() - fmt.Printf("Could not write temp Harness CA: %s\n", err) - return - } - tmp.Close() - - if _, err := exec.LookPath("certutil"); err != nil { - fmt.Printf("certutil not found on PATH; cannot install Harness egress CA: %s\n", err) + fmt.Printf("Could not decode Harness CA PEM: %s\n", err) fmt.Println("Base-image pulls through the egress proxy may fail with x509 errors.") return } - // -f overwrites an existing entry, keeping the step idempotent across retries. - cmd := exec.Command("certutil", "-addstore", "-f", "Root", tmpPath) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - fmt.Printf("Warning: certutil -addstore Root failed: %s\n", err) + storeName, err := windows.UTF16PtrFromString("ROOT") + if err != nil { + fmt.Printf("Could not prepare Windows root store name: %s\n", err) + return + } + + store, err := windows.CertOpenStore( + windows.CERT_STORE_PROV_SYSTEM, + 0, + 0, + windows.CERT_SYSTEM_STORE_LOCAL_MACHINE, + uintptr(unsafe.Pointer(storeName)), + ) + if err != nil { + fmt.Printf("Could not open LocalMachine\\Root store: %s\n", err) fmt.Println("Base-image pulls through the egress proxy may fail with x509 errors.") return } - fmt.Println("Installed Harness egress CA into the Windows LocalMachine\\Root store.") + defer windows.CertCloseStore(store, 0) + + for i, der := range ders { + if err := addCertToStore(store, der); err != nil { + fmt.Printf("Warning: failed to add Harness CA cert %d to LocalMachine\\Root: %s\n", i+1, err) + fmt.Println("Base-image pulls through the egress proxy may fail with x509 errors.") + return + } + } + + fmt.Printf("Installed %d Harness egress CA certificate(s) into the Windows LocalMachine\\Root store.\n", len(ders)) +} + +func addCertToStore(store windows.Handle, der []byte) error { + if len(der) == 0 { + return fmt.Errorf("empty DER certificate") + } + ctx, err := windows.CertCreateCertificateContext( + windows.X509_ASN_ENCODING|windows.PKCS_7_ASN_ENCODING, + &der[0], + uint32(len(der)), + ) + if err != nil { + return fmt.Errorf("CertCreateCertificateContext: %w", err) + } + defer windows.CertFreeCertificateContext(ctx) + + if err := windows.CertAddCertificateContextToStore( + store, + ctx, + windows.CERT_STORE_ADD_REPLACE_EXISTING, + nil, + ); err != nil { + return fmt.Errorf("CertAddCertificateContextToStore: %w", err) + } + return nil } diff --git a/go.mod b/go.mod index 9b07f39..31f1815 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/urfave/cli v1.22.2 golang.org/x/oauth2 v0.34.0 + golang.org/x/sys v0.39.0 google.golang.org/api v0.187.0 ) @@ -61,7 +62,6 @@ require ( go.opentelemetry.io/otel/trace v1.39.0 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/grpc v1.79.3 // indirect