mirror of
https://github.com/drone-plugins/drone-docker.git
synced 2026-07-16 16:21:18 +08:00
[CI-23743]: Support loading hosted certs in windows (#518)
This commit is contained in:
@@ -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
|
||||
}
|
||||
+114
@@ -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")
|
||||
}
|
||||
}
|
||||
+59
-27
@@ -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 <file>`.
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user