Skip to content

Commit

Permalink
add windows support for root CA cert stores (#84)
Browse files Browse the repository at this point in the history
* add windows support for system cert stores

* fix comment on unix prober

* ensure we loop over every certificate, add nil check

* clean up buffer logic

* nil check for unix prober

* fix goimports

* better code comments

* additional nil check

* check certcontext length

* init new cert pool in case of nil in prober
  • Loading branch information
rosskirkpat authored May 5, 2022
1 parent 5710abb commit 46fbba3
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 2 deletions.
5 changes: 3 additions & 2 deletions pkg/prober/prober.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,14 @@ func DoProbe(probe Probe, probeStatus *ProbeStatus, initial bool) error {
tlsConfig.Certificates = []tls.Certificate{clientCert}
}

caCertPool, err := x509.SystemCertPool()
if err != nil {
caCertPool, err := GetSystemCertPool(probe.Name)
if err != nil || caCertPool == nil {
caCertPool = x509.NewCertPool()
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)
}
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
}

0 comments on commit 46fbba3

Please sign in to comment.