This article is part of a series on mTLS. Check out the previous articles:
- mTLS Hello World
- mTLS with macOS keychain
- mTLS Go client
- mTLS Go client with custom certificate signer
- mTLS Go client using macOS keychain
- mTLS with Windows certificate store
Why use the Windows certificate store?
Keeping the mTLS client private key on the filesystem is insecure and not recommended. In the mTLS Go client using macOS keychain, we demonstrated achieving greater mTLS security with macOS keychain. In this article, we reach a similar level of protection with the Windows certificate store.
The Windows certificate store is a secure location where certificates and keys can be stored. Many applications, such as Edge and Powershell, use it. The Windows certificate store is an excellent place to store mTLS client certificates and keys.
Building a custom tls.Certificate for the Windows certificate store
This work builds on the mTLS Go client with custom certificate signer article. We will use the CustomSigner
from that article to build a custom tls.Certificate
that uses the Windows certificate store.
However, before the application uses the Public
and Sign
methods of the CustomSigner,
we must retrieve the client certificate using Windows APIs.
Retrieving mTLS client certificate from Windows certificate store using Go
We will use the golang.org/x/sys/windows package to access the Windows APIs. We use the windows
package to call the CertOpenStore, CertFindCertificateInStore, and CryptAcquireCertificatePrivateKey functions from the crypt32
DLL (dynamic link library).
First, we open the MY
store, which is the personal store for the current user. This store contains our client mTLS certificate.
// Open the certificate store
storePtr, err := windows.UTF16PtrFromString(windowsStoreName)
if err != nil {
return nil, err
}
store, err := windows.CertOpenStore(
windows.CERT_STORE_PROV_SYSTEM,
0,
uintptr(0),
windows.CERT_SYSTEM_STORE_CURRENT_USER,
uintptr(unsafe.Pointer(storePtr)),
)
if err != nil {
return nil, err
}
Next, we find the certificate by the common name.
// Find the certificate
var pPrevCertContext *windows.CertContext
var certContext *windows.CertContext
commonNamePtr, err := windows.UTF16PtrFromString(commonName)
for {
certContext, err = windows.CertFindCertificateInStore(
store,
windows.X509_ASN_ENCODING,
0,
windows.CERT_FIND_SUBJECT_STR,
unsafe.Pointer(commonNamePtr),
pPrevCertContext,
)
if err != nil {
return nil, err
}
// We can extract the certificate chain and further filter the certificate
// we want here.
break
}
Converting the Windows certificate to a Go x509.Certificate
After retrieving the certificate from the Windows certificate store, we convert it to a Go x509.Certificate
.
// Copy the certificate data so that we have our own copy outside the windows context
encodedCert := unsafe.Slice(certContext.EncodedCert, certContext.Length)
buf := bytes.Clone(encodedCert)
foundCert, err := x509.ParseCertificate(buf)
if err != nil {
return nil, err
}
Building the custom tls.Certificate
Finally, we put together the custom tls.Certificate
using the x509.Certificate
. We hold on to the certContext
pointer to get the private key later.
customSigner := &CustomSigner{
store: store,
windowsCertContext: certContext,
}
customSigner.x509Cert = foundCert
certificate := tls.Certificate{
Certificate: [][]byte{foundCert.Raw},
PrivateKey: customSigner,
SupportedSignatureAlgorithms: []tls.SignatureScheme{supportedAlgorithm},
}
Our example only supports the tls.PSSWithSHA256
signature algorithm to keep the code simple.
Signing the mTLS digest with the Windows certificate store
As discussed in the previous mTLS Go client with custom certificate signer article, we must sign the CertificateVerify
message during the TLS handshake. We will use the CustomSigner
to sign the digest, which implements the crypto.Signer
interface as defined in the Go standard library’s crypto
package.
// CustomSigner is a crypto.Signer that uses the client certificate and key to sign
type CustomSigner struct {
store windows.Handle
windowsCertContext *windows.CertContext
x509Cert *x509.Certificate
}
func (k *CustomSigner) Public() crypto.PublicKey {
fmt.Printf("crypto.Signer.Public\n")
return k.x509Cert.PublicKey
}
func (k *CustomSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts
) (signature []byte, err error) {
...
Retrieve the private key reference from the Windows certificate store
We retrieve the private key reference from the Windows certificate store using the CryptAcquireCertificatePrivateKey
function.
// Get private key
var (
privateKey windows.Handle
pdwKeySpec uint32
pfCallerFreeProvOrNCryptKey bool
)
err = windows.CryptAcquireCertificatePrivateKey(
k.windowsCertContext,
windows.CRYPT_ACQUIRE_CACHE_FLAG|windows.CRYPT_ACQUIRE_SILENT_FLAG|
windows.CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG,
nil,
&privateKey,
&pdwKeySpec,
&pfCallerFreeProvOrNCryptKey,
)
if err != nil {
return nil, err
}
Signing the mTLS digest
We will use the NCryptSignHash function from ncrypt.dll
to sign the digest.
var (
nCrypt = windows.MustLoadDLL("ncrypt.dll")
nCryptSignHash = nCrypt.MustFindProc("NCryptSignHash")
)
But before we do that, we must create a BCRYPT_PSS_PADDING_INFO
structure for our supported RSA-PSS algorithm.
flags := nCryptSilentFlag | bCryptPadPss
pPaddingInfo, err := getRsaPssPadding(opts)
if err != nil {
return nil, err
}
Where getRsaPssPadding
is a helper function:
func getRsaPssPadding(opts crypto.SignerOpts) (unsafe.Pointer, error) {
pssOpts, ok := opts.(*rsa.PSSOptions)
if !ok || pssOpts.Hash != crypto.SHA256 {
return nil, fmt.Errorf("unsupported hash function %s", opts.HashFunc().String())
}
if pssOpts.SaltLength != rsa.PSSSaltLengthEqualsHash {
return nil, fmt.Errorf("unsupported salt length %d", pssOpts.SaltLength)
}
sha256, _ := windows.UTF16PtrFromString("SHA256")
// Create BCRYPT_PSS_PADDING_INFO structure:
// typedef struct _BCRYPT_PSS_PADDING_INFO {
// LPCWSTR pszAlgId;
// ULONG cbSalt;
// } BCRYPT_PSS_PADDING_INFO;
return unsafe.Pointer(
&struct {
pszAlgId *uint16
cbSalt uint32
}{
pszAlgId: sha256,
cbSalt: uint32(pssOpts.HashFunc().Size()),
},
), nil
}
Finally, we sign the digest using the NCryptSignHash
function.
// Sign the digest
// The first call to NCryptSignHash retrieves the size of the signature
var size uint32
success, _, _ := nCryptSignHash.Call(
uintptr(privateKey),
uintptr(pPaddingInfo),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
uintptr(0),
uintptr(0),
uintptr(unsafe.Pointer(&size)),
uintptr(flags),
)
if success != 0 {
return nil, fmt.Errorf("NCryptSignHash: failed to get signature length: %#x", success)
}
// The second call to NCryptSignHash retrieves the signature
signature = make([]byte, size)
success, _, _ = nCryptSignHash.Call(
uintptr(privateKey),
uintptr(pPaddingInfo),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
uintptr(unsafe.Pointer(&signature[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
uintptr(flags),
)
if success != 0 {
return nil, fmt.Errorf("NCryptSignHash: failed to generate signature: %#x", success)
}
return signature, nil
Putting it all together
With the above code, we can create our new Go mTLS client that uses the Windows certificate store.
func main() {
urlPath := flag.String("url", "", "URL to make request to")
flag.Parse()
if *urlPath == "" {
log.Fatalf("URL to make request to is required")
}
client := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
GetClientCertificate: signer.GetClientCertificate,
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
},
},
}
// Make a GET request to the URL
rsp, err := client.Get(*urlPath)
if err != nil {
log.Fatalf("error making get request: %v", err)
}
defer func() { _ = rsp.Body.Close() }()
// Read the response body
rspBytes, err := io.ReadAll(rsp.Body)
if err != nil {
log.Fatalf("error reading response: %v", err)
}
// Print the response body
fmt.Printf("%s\n", string(rspBytes))
}
We limit the scope of this example to TLS 1.3
Setting up the environment
The next step is to use the Windows certificate store to store the client certificate and private key. We will use the certificates and keys scripts from the previous mTLS with Windows certificate store article. If you still need to generate the certificates and keys, please follow the instructions in that article.
Finally, as in the mTLS with Windows certificate store article, we start two nginx servers:
- https://<your_host>:8888 for TLS
- https://<your_host>:8889 for mTLS
Running the Go mTLS client using the Windows certificate store
We can run our mTLS client without pointing to certificate/key files and retrieving everything from the Windows certificate store. Hitting the ordinary TLS server:
go run .\client-signer.go --url https://myhost:8888/hello-world.txt
Returns the expected:
TLS Hello World!
While hitting the mTLS server:
go run .\client-signer.go --url https://myhost:8889/hello-world.txt
Returns a more detailed message, including the print statements in our custom code:
Server requested certificate
Found certificate with common name testClientTLS
crypto.Signer.Public
crypto.Signer.Public
crypto.Signer.Sign with key type *rsa.PublicKey, opts type *rsa.PSSOptions, hash SHA-256
mTLS Hello World!
Example code on GitHub
The example code is available on GitHub at https://github.com/getvictor/mtls/tree/master/mtls-go-windows
mTLS Go client using Windows certificate store video
Note: If you want to comment on this article, please do so on the YouTube video.