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

add windows support for root CA cert stores #84

Merged
merged 10 commits into from
May 5, 2022
7 changes: 3 additions & 4 deletions pkg/prober/prober.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package prober

import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"net/http"
"net/url"
Expand Down Expand Up @@ -58,13 +57,13 @@ func DoProbe(probe Probe, probeStatus *ProbeStatus, initial bool) error {
tlsConfig.Certificates = []tls.Certificate{clientCert}
}

caCertPool, err := x509.SystemCertPool()
if err != nil {
caCertPool = x509.NewCertPool()
caCertPool, err := GetSystemCertPool(probe.Name)
thedadams marked this conversation as resolved.
Show resolved Hide resolved
if err != nil || caCertPool == nil {
logrus.Errorf("error loading system cert pool for probe (%s): %v", probe.Name, err)
}

if probe.HTTPGetAction.CACert != "" {
logrus.Debugf("[DoProbe] adding CA certificate [%s] for probe (%s)", probe.HTTPGetAction.CACert, probe.Name)
caCert, err := ioutil.ReadFile(probe.HTTPGetAction.CACert)
if err != nil {
logrus.Errorf("error loading CA cert for probe (%s) %s: %v", probe.Name, probe.HTTPGetAction.CACert, err)
Expand Down
25 changes: 25 additions & 0 deletions pkg/prober/prober_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//go:build !windows
// +build !windows

package prober

import (
"crypto/x509"
"fmt"

"github.com/sirupsen/logrus"
)

// GetSystemCertPool returns a x509.CertPool that contains the
// root CA certificates if they are present at runtime
func GetSystemCertPool(probeName string) (*x509.CertPool, error) {
caCertPool, err := x509.SystemCertPool()
if err != nil {
caCertPool = x509.NewCertPool()
logrus.Errorf("[GetSystemCertPoolUnix] error loading system cert pool for probe (%s): %v", probeName, err)
}
if caCertPool == nil {
return nil, fmt.Errorf("[GetSystemCertPoolWindows] x509 returned a nil certpool for probe (%s)", probeName)
}
Comment on lines +21 to +23
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added the nil check inside each of the functions for linux/windows as it allows the function to fail faster for Windows and avoid a situation where a nil pointer causes a failure before we could check it in the main prober.go. Waiting to check until after the caCertPool is returned could result in a nil pointer being returned when we try to append the certs from the syscall to the caCertPool.

return caCertPool, nil
}
110 changes: 110 additions & 0 deletions pkg/prober/prober_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//go:build windows
// +build windows

package prober

import (
"crypto/x509"
"fmt"
"syscall"
"unsafe"

"github.com/sirupsen/logrus"
)

const (
CRYPT_E_NOT_FOUND = 0x80092004
maxEncodedCertLen = 1 << 20
)

// GetSystemCertPool is a workaround to Windows not having x509.SystemCertPool implemented in < go1.18
// it leverages syscalls to extract system certificates and load them into a new x509.CertPool
// workaround adapted from: https://github.com/golang/go/issues/16736#issuecomment-540373689
// ref: https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certgetissuercertificatefromstore
// TODO: Test and remove after system-agent is bumped to go1.18+
func GetSystemCertPool(probeName string) (*x509.CertPool, error) {
logrus.Tracef("[GetSystemCertPoolWindows] building system certContext pool for probe (%s)", probeName)
root, err := syscall.UTF16PtrFromString("Root")
if err != nil {
return nil, fmt.Errorf("[GetSystemCertPoolWindows] unable to return UTF16 pointer: %v", syscall.GetLastError())
}
if root == nil {
return nil, fmt.Errorf("[GetSystemCertPoolWindows] UTF16 pointer for Root returned nil: %v", syscall.GetLastError())

}
// win32 reference: https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certopensystemstorea
// If the function succeeds, it returns a handle to the specified certificate store.
storeHandle, err := syscall.CertOpenSystemStore(0, root)
if err != nil {
return nil, fmt.Errorf("[GetSystemCertPoolWindows] unable to open system certContext store: %v", syscall.GetLastError())
}

var certs []*x509.Certificate
var certContext *syscall.CertContext

// ref for why flags value is 0: https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certclosestore
defer func(store syscall.Handle, flags uint32) {
_ = syscall.CertCloseStore(store, flags)
}(certContext.Store, 0)

// this for loop will iterate through all available certificates in the specified certificate store
// and build an array of each x509.Certificate that is returned
for {
// CertEnumCertificatesInStore returns a single certContext containing the initial/next certificate in the cert store
certContext, err = syscall.CertEnumCertificatesInStore(storeHandle, certContext)
if err != nil {
if errno, ok := err.(syscall.Errno); ok {
if errno == CRYPT_E_NOT_FOUND {
// if the error returned here is CRYPTO_E_NOT_FOUND, that indicates no certificates were found.
// This happens if the store is empty or if the function reached the end of the store's list.
// ref: https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certenumcertificatesinstore#return-value
logrus.Debugf("[GetSystemCertPoolWindows] no certificates were returned from the root CA store for probe (%s)", probeName)
break
}
}
logrus.Errorf("[GetSystemCertPoolWindows] unable to enumerate certs in system certContext store for probe (%s): %v", probeName, syscall.GetLastError())
}
if certContext == nil {
logrus.Errorf("[GetSystemCertPoolWindows] certificate context returned from syscall is nil for probe (%s)", probeName)
break
}

// buf is a ~1048 kilobyte array that serves as a buffer holding the encoded value
// of a single CA certificate returned from the Windows root CA store
// equal to the length of the certContext pointer which contains a certificate from the Root CA store
//
// we are sizing for a single context (certificate) and not the whole store in buf
// using a binary shift to create a ~1048 Kb buffer (slightly larger than 1 megabyte)
// [1 << 20]byte -> (1*2)^20 = 1048576 bytes
// stating for reference but not related to this code
// the maximum size of a Windows certificate store is 16 kilobytes and is not related to number of certificates
if certContext.Length > maxEncodedCertLen {
return nil, fmt.Errorf("invalid CertContext length %d", certContext.Length)
}
buf := (*[maxEncodedCertLen]byte)(unsafe.Pointer(certContext.EncodedCert))[:certContext.Length]

// validate the root CA certificate and return a x509.Certificate pointer
// that is appended into our array of x509 certificates
c, err := x509.ParseCertificate(buf)
if err != nil {
return nil, fmt.Errorf("[GetSystemCertPoolWindows] unable to parse x509 certificate for probe (%s): %v", probeName, err)
}
certs = append(certs, c)
logrus.Debugf("[GetSystemCertPoolWindows] Successfully loaded %d certificates from system certContext store for probe (%s)", len(certs), probeName)
}

caCertPool := x509.NewCertPool()
if caCertPool == nil {
return nil, fmt.Errorf("[GetSystemCertPoolWindows] x509 returned a nil certpool for probe (%s)", probeName)
}

for _, certificate := range certs {
if !caCertPool.AppendCertsFromPEM(certificate.RawTBSCertificate) {
return nil, fmt.Errorf("[GetSystemCertPoolWindows] unable to append certContext with CN [%s] to system certContext pool for probe (%s)", certificate.Subject.CommonName, probeName)
}
logrus.Tracef("[GetSystemCertPoolWindows] successfully appended certContext with CN [%s] to system certContext pool for probe (%s)", certificate.Subject.CommonName, probeName)
}
logrus.Infof("[GetSystemCertPoolWindows] Successfully loaded %d certificates into system certContext pool for probe (%s)", len(certs), probeName)

return caCertPool, nil
}