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

ADDED windows certificate store cert_skip_invalid options #4384

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/certstore/certstore_other.go
Expand Up @@ -26,7 +26,7 @@ var _ = MATCHBYEMPTY
// otherKey implements crypto.Signer and crypto.Decrypter to satisfy linter on platforms that don't implement certstore
type otherKey struct{}

func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, config *tls.Config) error {
func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, certMatchSkipInvalid bool, config *tls.Config) error {
_, _, _, _ = certStore, certMatchBy, certMatch, config
return ErrOSNotCompatCertStore
}
Expand Down
66 changes: 43 additions & 23 deletions server/certstore/certstore_windows.go
Expand Up @@ -119,6 +119,7 @@ var (
winNCrypt = windows.MustLoadDLL("ncrypt.dll")

winCertFindCertificateInStore = winCrypt32.MustFindProc("CertFindCertificateInStore")
winCertVerifyTimeValidity = winCrypt32.MustFindProc("CertVerifyTimeValidity")
winCryptAcquireCertificatePrivateKey = winCrypt32.MustFindProc("CryptAcquireCertificatePrivateKey")
winNCryptExportKey = winNCrypt.MustFindProc("NCryptExportKey")
winNCryptOpenStorageProvider = winNCrypt.MustFindProc("NCryptOpenStorageProvider")
Expand All @@ -139,7 +140,7 @@ type winPSSPaddingInfo struct {

// TLSConfig fulfills the same function as reading cert and key pair from pem files but
// sources the Windows certificate store instead
func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, config *tls.Config) error {
func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, skipInvalid bool, config *tls.Config) error {
var (
leaf *x509.Certificate
leafCtx *windows.CertContext
Expand All @@ -166,9 +167,9 @@ func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, c

// certByIssuer or certBySubject
if certMatchBy == matchBySubject || certMatchBy == MATCHBYEMPTY {
leaf, leafCtx, err = cs.certBySubject(certMatch, scope)
leaf, leafCtx, err = cs.certBySubject(certMatch, scope, skipInvalid)
} else if certMatchBy == matchByIssuer {
leaf, leafCtx, err = cs.certByIssuer(certMatch, scope)
leaf, leafCtx, err = cs.certByIssuer(certMatch, scope, skipInvalid)
} else {
return ErrBadMatchByType
}
Expand Down Expand Up @@ -268,6 +269,16 @@ func winFindCert(store windows.Handle, enc, findFlags, findType uint32, para *ui
return (*windows.CertContext)(unsafe.Pointer(h)), nil
}

// winVerifyCertValid wraps the CertVerifyTimeValidity and simply returns true if the certificate is valid
func winVerifyCertValid(timeToVerify *windows.Filetime, certInfo *windows.CertInfo) bool {
// this function does not document returning errors / setting lasterror
r, _, _ := winCertVerifyTimeValidity.Call(
uintptr(unsafe.Pointer(timeToVerify)),
uintptr(unsafe.Pointer(certInfo)),
)
return r == 0
}

// winCertStore is a store implementation for the Windows Certificate Store
type winCertStore struct {
Prov uintptr
Expand Down Expand Up @@ -307,21 +318,21 @@ func winCertContextToX509(ctx *windows.CertContext) (*x509.Certificate, error) {
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
// current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_ISSUER_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) certByIssuer(issuer string, storeType uint32) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindIssuerStr, issuer, winMyStore, storeType)
func (w *winCertStore) certByIssuer(issuer string, storeType uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindIssuerStr, issuer, winMyStore, storeType, skipInvalid)
}

// certBySubject matches and returns the first certificate found by passed subject field.
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
// current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) certBySubject(subject string, storeType uint32) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindSubjectStr, subject, winMyStore, storeType)
func (w *winCertStore) certBySubject(subject string, storeType uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindSubjectStr, subject, winMyStore, storeType, skipInvalid)
}

// certSearch is a helper function to lookup certificates based on search type and match value.
// store is used to specify which store to perform the lookup in (system or user).
func (w *winCertStore) certSearch(searchType uint32, matchValue string, searchRoot *uint16, store uint32) (*x509.Certificate, *windows.CertContext, error) {
func (w *winCertStore) certSearch(searchType uint32, matchValue string, searchRoot *uint16, store uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
// store handle to "MY" store
h, err := w.storeHandle(store, searchRoot)
if err != nil {
Expand All @@ -338,23 +349,32 @@ func (w *winCertStore) certSearch(searchType uint32, matchValue string, searchRo

// pass 0 as the third parameter because it is not used
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa376064(v=vs.85).aspx
nc, err := winFindCert(h, winEncodingX509ASN|winEncodingPKCS7, 0, searchType, i, prev)
if err != nil {
return nil, nil, err
}
if nc != nil {
// certificate found
prev = nc

// Extract the DER-encoded certificate from the cert context
xc, err := winCertContextToX509(nc)
if err == nil {
cert = xc
for {
nc, err := winFindCert(h, winEncodingX509ASN|winEncodingPKCS7, 0, searchType, i, prev)
if err != nil {
return nil, nil, err
}
if nc != nil {
// certificate found
prev = nc

var now *windows.Filetime
if skipInvalid && !winVerifyCertValid(now, nc.CertInfo) {
continue
}

// Extract the DER-encoded certificate from the cert context
xc, err := winCertContextToX509(nc)
if err == nil {
cert = xc
break
} else {
return nil, nil, ErrFailedX509Extract
}
} else {
return nil, nil, ErrFailedX509Extract
return nil, nil, ErrFailedCertSearch
}
} else {
return nil, nil, ErrFailedCertSearch
}

if cert == nil {
Expand Down Expand Up @@ -550,7 +570,7 @@ func winSignRSAPSSPadding(kh uintptr, digest []byte, algID *uint16) ([]byte, err
return sig[:size], nil
}

// certKey wraps CryptAcquireCertificatePrivateKey. It obtains the CNG private
// certKey wraps CryptAcquireCFertificatePrivateKey. It obtains the CNG private
// key of a known certificate and returns a pointer to a winKey which implements
// both crypto.Signer. When a nil cert context is passed
// a nil key is intentionally returned, to model the expected behavior of a
Expand Down
3 changes: 3 additions & 0 deletions server/certstore/errors.go
Expand Up @@ -68,6 +68,9 @@ var (
// ErrBadCertMatchField represents malformed cert_match option
ErrBadCertMatchField = errors.New("expected 'cert_match' to be a valid non-empty string")

// ErrBadCertMatchSkipInvalidField represents malformed cert_match_skip_invalid option
ErrBadCertMatchSkipInvalidField = errors.New("expected 'cert_match_skip_invalid' to be a boolean")

// ErrOSNotCompatCertStore represents cert_store passed that exists but is not valid on current OS
ErrOSNotCompatCertStore = errors.New("cert_store not compatible with current operating system")
)
57 changes: 56 additions & 1 deletion server/certstore_windows_test.go
Expand Up @@ -28,9 +28,12 @@ import (
)

func runPowershellScript(scriptFile string, args []string) error {
_ = args
psExec, _ := exec.LookPath("powershell.exe")

execArgs := []string{psExec, "-command", fmt.Sprintf("& '%s'", scriptFile)}
if len(args) > 0 {
execArgs = append(execArgs, args...)
}

cmdImport := &exec.Cmd{
Path: psExec,
Expand Down Expand Up @@ -228,3 +231,55 @@ func TestServerTLSWindowsCertStore(t *testing.T) {
})
}
}

// TestServerIgnoreExpiredCerts tests if the server skips expired certificates in configuration, and finds non-expired ones
func TestServerIgnoreExpiredCerts(t *testing.T) {

// Server Identities: expired.pem; not-expired.pem
// Issuer: OU = NATS.io, CN = localhost
// Subject: OU = NATS.io Operators, CN = localhost

testCases := []struct {
certFile string
expect bool
}{
{"expired.p12", false},
{"not-expired.p12", true},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("Server certificate: %s", tc.certFile), func(t *testing.T) {
// Make sure windows cert store is reset to avoid conflict with other tests
err := runPowershellScript("../test/configs/certs/tlsauth/certstore/delete-cert-from-store.ps1", nil)
if err != nil {
t.Fatalf("expected powershell cert delete to succeed: %s", err.Error())
}

// Provision Windows cert store with server cert and secret
err = runPowershellScript("../test/configs/certs/tlsauth/certstore/import-p12-server.ps1", []string{tc.certFile})
if err != nil {
t.Fatalf("expected powershell provision to succeed: %s", err.Error())
}
// Fire up the server
srvConfig := createConfFile(t, []byte(`
listen: "localhost:-1"
tls {
cert_store: "WindowsCurrentUser"
cert_match_by: "Subject"
cert_match: "NATS.io Operators"
cert_match_skip_invalid: true
timeout: 5
}
`))
defer removeFile(t, srvConfig)
cfg, _ := ProcessConfigFile(srvConfig)
if (cfg != nil) == tc.expect {
return
}
if tc.expect == false {
t.Fatalf("expected server start to fail with expired certificate")
} else {
t.Fatalf("expected server to start with non expired certificate")
}
})
}
}
41 changes: 24 additions & 17 deletions server/opts.go
Expand Up @@ -573,22 +573,23 @@ type authorization struct {
// TLSConfigOpts holds the parsed tls config information,
// used with flag parsing
type TLSConfigOpts struct {
CertFile string
KeyFile string
CaFile string
Verify bool
Insecure bool
Map bool
TLSCheckKnownURLs bool
Timeout float64
RateLimit int64
Ciphers []uint16
CurvePreferences []tls.CurveID
PinnedCerts PinnedCertSet
CertStore certstore.StoreType
CertMatchBy certstore.MatchByType
CertMatch string
OCSPPeerConfig *certidp.OCSPPeerConfig
CertFile string
KeyFile string
CaFile string
Verify bool
Insecure bool
Map bool
TLSCheckKnownURLs bool
Timeout float64
RateLimit int64
Ciphers []uint16
CurvePreferences []tls.CurveID
PinnedCerts PinnedCertSet
CertStore certstore.StoreType
CertMatchBy certstore.MatchByType
CertMatch string
CertMatchSkipInvalid bool
OCSPPeerConfig *certidp.OCSPPeerConfig
}

// OCSPConfig represents the options of OCSP stapling options.
Expand Down Expand Up @@ -4089,6 +4090,12 @@ func parseTLS(v interface{}, isClientCtx bool) (t *TLSConfigOpts, retErr error)
return nil, &configErr{tk, certstore.ErrBadCertMatchField.Error()}
}
tc.CertMatch = certMatch
case "cert_match_skip_invalid":
certMatchSkipInvalid, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, certstore.ErrBadCertMatchSkipInvalidField.Error()}
}
tc.CertMatchSkipInvalid = certMatchSkipInvalid
case "ocsp_peer":
switch vv := mv.(type) {
case bool:
Expand Down Expand Up @@ -4417,7 +4424,7 @@ func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) {
}
config.Certificates = []tls.Certificate{cert}
case tc.CertStore != certstore.STOREEMPTY:
err := certstore.TLSConfig(tc.CertStore, tc.CertMatchBy, tc.CertMatch, &config)
err := certstore.TLSConfig(tc.CertStore, tc.CertMatchBy, tc.CertMatch, tc.CertMatchSkipInvalid, &config)
if err != nil {
return nil, err
}
Expand Down
@@ -1,2 +1,2 @@
$issuer="Synadia Communications Inc."
Get-ChildItem Cert:\CurrentUser\My | Where-Object {$_.Issuer -match $issuer} | Remove-Item
Get-ChildItem Cert:\CurrentUser\My | Where-Object {$_.Issuer -match "Synadia Communications Inc."} | Remove-Item
Get-ChildItem Cert:\CurrentUser\My | Where-Object {$_.Issuer -match "NATS.io Operators"} | Remove-Item
Binary file not shown.
@@ -1,4 +1,7 @@
$fileLocale = $PSScriptRoot + "\server.p12"
$file=$args[0]
if (!$file) { $file="server.p12 "}
$fileLocale = $PSScriptRoot + "\" + $file
echo "Installing certificate $fileLocale"
$Pass = ConvertTo-SecureString -String 's3cr3t' -Force -AsPlainText
$User = "whatever"
$Cred = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $User, $Pass
Expand Down
Binary file not shown.