diff options
Diffstat (limited to 'internal')
25 files changed, 1948 insertions, 0 deletions
diff --git a/internal/signer/darwin/go.mod b/internal/signer/darwin/go.mod new file mode 100644 index 0000000..5f52caa --- /dev/null +++ b/internal/signer/darwin/go.mod @@ -0,0 +1,3 @@ +module signer + +go 1.19
\ No newline at end of file diff --git a/internal/signer/darwin/keychain/keychain.go b/internal/signer/darwin/keychain/keychain.go new file mode 100644 index 0000000..6759904 --- /dev/null +++ b/internal/signer/darwin/keychain/keychain.go @@ -0,0 +1,407 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build darwin && cgo +// +build darwin,cgo + +// Package keychain contains functions for retrieving certificates from the Darwin Keychain. +package keychain + +/* +#cgo CFLAGS: -mmacosx-version-min=10.12 +#cgo LDFLAGS: -framework CoreFoundation -framework Security + +#include <CoreFoundation/CoreFoundation.h> +#include <Security/Security.h> +*/ +import "C" + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "runtime" + "sync" + "time" + "unsafe" +) + +// Maps for translating from crypto.Hash to SecKeyAlgorithm. +// https://developer.apple.com/documentation/security/seckeyalgorithm +var ( + ecdsaAlgorithms = map[crypto.Hash]C.CFStringRef{ + crypto.SHA256: C.kSecKeyAlgorithmECDSASignatureDigestX962SHA256, + crypto.SHA384: C.kSecKeyAlgorithmECDSASignatureDigestX962SHA384, + crypto.SHA512: C.kSecKeyAlgorithmECDSASignatureDigestX962SHA512, + } + rsaPKCS1v15Algorithms = map[crypto.Hash]C.CFStringRef{ + crypto.SHA256: C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA256, + crypto.SHA384: C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA384, + crypto.SHA512: C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA512, + } + rsaPSSAlgorithms = map[crypto.Hash]C.CFStringRef{ + crypto.SHA256: C.kSecKeyAlgorithmRSASignatureDigestPSSSHA256, + crypto.SHA384: C.kSecKeyAlgorithmRSASignatureDigestPSSSHA384, + crypto.SHA512: C.kSecKeyAlgorithmRSASignatureDigestPSSSHA512, + } +) + +// cfStringToString returns a Go string given a CFString. +func cfStringToString(cfStr C.CFStringRef) string { + s := C.CFStringGetCStringPtr(cfStr, C.kCFStringEncodingUTF8) + if s != nil { + return C.GoString(s) + } + glyphLength := C.CFStringGetLength(cfStr) + 1 + utf8Length := C.CFStringGetMaximumSizeForEncoding(glyphLength, C.kCFStringEncodingUTF8) + if s = (*C.char)(C.malloc(C.size_t(utf8Length))); s == nil { + panic("unable to allocate memory") + } + defer C.free(unsafe.Pointer(s)) + if C.CFStringGetCString(cfStr, s, utf8Length, C.kCFStringEncodingUTF8) == 0 { + panic("unable to convert cfStringref to string") + } + return C.GoString(s) +} + +func cfRelease(x unsafe.Pointer) { + C.CFRelease(C.CFTypeRef(x)) +} + +// cfError is an error type that owns a CFErrorRef, and obtains the error string +// by using CFErrorCopyDescription. +type cfError struct { + e C.CFErrorRef +} + +// cfErrorFromRef converts a C.CFErrorRef to a cfError, taking ownership of the +// reference and releasing when the value is finalized. +func cfErrorFromRef(cfErr C.CFErrorRef) *cfError { + if cfErr == 0 { + return nil + } + c := &cfError{e: cfErr} + runtime.SetFinalizer(c, func(x interface{}) { + C.CFRelease(C.CFTypeRef(x.(*cfError).e)) + }) + return c +} + +func (e *cfError) Error() string { + s := C.CFErrorCopyDescription(C.CFErrorRef(e.e)) + defer C.CFRelease(C.CFTypeRef(s)) + return cfStringToString(s) +} + +// keychainError is an error type that is based on an OSStatus return code, and +// obtains the error string with SecCopyErrorMessageString. +type keychainError C.OSStatus + +func (e keychainError) Error() string { + s := C.SecCopyErrorMessageString(C.OSStatus(e), nil) + defer C.CFRelease(C.CFTypeRef(s)) + return cfStringToString(s) +} + +// cfDataToBytes turns a CFDataRef into a byte slice. +func cfDataToBytes(cfData C.CFDataRef) []byte { + return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(cfData)), C.int(C.CFDataGetLength(cfData))) +} + +// bytesToCFData turns a byte slice into a CFDataRef. Caller then "owns" the +// CFDataRef and must CFRelease the CFDataRef when done. +func bytesToCFData(buf []byte) C.CFDataRef { + return C.CFDataCreate(C.kCFAllocatorDefault, (*C.UInt8)(unsafe.Pointer(&buf[0])), C.CFIndex(len(buf))) +} + +// int32ToCFNumber turns an int32 into a CFNumberRef. Caller then "owns" +// the CFNumberRef and must CFRelease the CFNumberRef when done. +func int32ToCFNumber(n int32) C.CFNumberRef { + return C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberSInt32Type, unsafe.Pointer(&n)) +} + +// Key is a wrapper around the Keychain reference that uses it to +// implement signing-related methods with Keychain functionality. +type Key struct { + privateKeyRef C.SecKeyRef + certs []*x509.Certificate + once sync.Once +} + +// newKey makes a new Key wrapper around the key reference, +// takes ownership of the reference, and sets up a finalizer to handle releasing +// the reference. +func newKey(privateKeyRef C.SecKeyRef, certs []*x509.Certificate) (*Key, error) { + k := &Key{ + privateKeyRef: privateKeyRef, + certs: certs, + } + + // This struct now owns the key reference. Retain now and release on + // finalise in case the credential gets forgotten about. + C.CFRetain(C.CFTypeRef(privateKeyRef)) + runtime.SetFinalizer(k, func(x interface{}) { + x.(*Key).Close() + }) + return k, nil +} + +// CertificateChain returns the credential as a raw X509 cert chain. This +// contains the public key. +func (k *Key) CertificateChain() [][]byte { + rv := make([][]byte, len(k.certs)) + for i, c := range k.certs { + rv[i] = c.Raw + } + return rv +} + +// Close releases resources held by the credential. +func (k *Key) Close() error { + // Don't double-release references. + k.once.Do(func() { + C.CFRelease(C.CFTypeRef(k.privateKeyRef)) + }) + return nil +} + +// Public returns the corresponding public key for this Key. Good +// thing we extracted it when we created it. +func (k *Key) Public() crypto.PublicKey { + return k.certs[0].PublicKey +} + +// Sign signs a message digest. Here, we pass off the signing to Keychain library. +func (k *Key) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + // Map the signing algorithm and hash function to a SecKeyAlgorithm constant. + var algorithms map[crypto.Hash]C.CFStringRef + switch pub := k.Public().(type) { + case *ecdsa.PublicKey: + algorithms = ecdsaAlgorithms + case *rsa.PublicKey: + if _, ok := opts.(*rsa.PSSOptions); ok { + algorithms = rsaPSSAlgorithms + break + } + algorithms = rsaPKCS1v15Algorithms + default: + return nil, fmt.Errorf("unsupported algorithm %T", pub) + } + algorithm, ok := algorithms[opts.HashFunc()] + if !ok { + return nil, fmt.Errorf("unsupported hash function %T", opts.HashFunc()) + } + + // Copy input over into CF-land. + cfDigest := bytesToCFData(digest) + defer C.CFRelease(C.CFTypeRef(cfDigest)) + + var cfErr C.CFErrorRef + sig := C.SecKeyCreateSignature(C.SecKeyRef(k.privateKeyRef), algorithm, C.CFDataRef(cfDigest), &cfErr) + if cfErr != 0 { + return nil, cfErrorFromRef(cfErr) + } + defer C.CFRelease(C.CFTypeRef(sig)) + + return cfDataToBytes(C.CFDataRef(sig)), nil +} + +// Cred gets the first Credential (filtering on issuer) corresponding to +// available certificate and private key pairs (i.e. identities) available in +// the Keychain. This includes both the current login keychain for the user, +// and the system keychain. +func Cred(issuerCN string) (*Key, error) { + leafSearch := C.CFDictionaryCreateMutable(C.kCFAllocatorDefault, 5, &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) + defer C.CFRelease(C.CFTypeRef(unsafe.Pointer(leafSearch))) + // Get identities (certificate + private key pairs). + C.CFDictionaryAddValue(leafSearch, unsafe.Pointer(C.kSecClass), unsafe.Pointer(C.kSecClassIdentity)) + // Get identities that are signing capable. + C.CFDictionaryAddValue(leafSearch, unsafe.Pointer(C.kSecAttrCanSign), unsafe.Pointer(C.kCFBooleanTrue)) + // For each identity, give us the reference to it. + C.CFDictionaryAddValue(leafSearch, unsafe.Pointer(C.kSecReturnRef), unsafe.Pointer(C.kCFBooleanTrue)) + // Be sure to list out all the matches. + C.CFDictionaryAddValue(leafSearch, unsafe.Pointer(C.kSecMatchLimit), unsafe.Pointer(C.kSecMatchLimitAll)) + // Do the matching-item copy. + var leafMatches C.CFTypeRef + if errno := C.SecItemCopyMatching((C.CFDictionaryRef)(leafSearch), &leafMatches); errno != C.errSecSuccess { + return nil, keychainError(errno) + } + defer C.CFRelease(leafMatches) + signingIdents := C.CFArrayRef(leafMatches) + // Dump the certs into golang x509 Certificates. + var ( + leafIdent C.SecIdentityRef + leaf *x509.Certificate + ) + // Find the first valid leaf whose issuer (CA) matches the name in filter. + // Validation in identityToX509 covers Not Before, Not After and key alg. + for i := 0; i < int(C.CFArrayGetCount(signingIdents)) && leaf == nil; i++ { + identDict := C.CFArrayGetValueAtIndex(signingIdents, C.CFIndex(i)) + xc, err := identityToX509(C.SecIdentityRef(identDict)) + if err != nil { + continue + } + if xc.Issuer.CommonName == issuerCN { + leaf = xc + leafIdent = C.SecIdentityRef(identDict) + } + } + + caSearch := C.CFDictionaryCreateMutable(C.kCFAllocatorDefault, 0, &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) + defer C.CFRelease(C.CFTypeRef(unsafe.Pointer(caSearch))) + // Get identities (certificates). + C.CFDictionaryAddValue(caSearch, unsafe.Pointer(C.kSecClass), unsafe.Pointer(C.kSecClassCertificate)) + // For each identity, give us the reference to it. + C.CFDictionaryAddValue(caSearch, unsafe.Pointer(C.kSecReturnRef), unsafe.Pointer(C.kCFBooleanTrue)) + // Be sure to list out all the matches. + C.CFDictionaryAddValue(caSearch, unsafe.Pointer(C.kSecMatchLimit), unsafe.Pointer(C.kSecMatchLimitAll)) + // Do the matching-item copy. + var caMatches C.CFTypeRef + if errno := C.SecItemCopyMatching((C.CFDictionaryRef)(caSearch), &caMatches); errno != C.errSecSuccess { + return nil, keychainError(errno) + } + defer C.CFRelease(caMatches) + certRefs := C.CFArrayRef(caMatches) + // Validate and dump the certs into golang x509 Certificates. + var allCerts []*x509.Certificate + for i := 0; i < int(C.CFArrayGetCount(certRefs)); i++ { + refDict := C.CFArrayGetValueAtIndex(certRefs, C.CFIndex(i)) + if xc, err := certRefToX509(C.SecCertificateRef(refDict)); err == nil { + allCerts = append(allCerts, xc) + } + } + + // Build a certificate chain from leaf by matching prev.RawIssuer to + // next.RawSubject across all valid certificates in the keychain. + var ( + certs []*x509.Certificate + prev, next *x509.Certificate + ) + for prev = leaf; prev != nil; prev, next = next, nil { + certs = append(certs, prev) + for _, xc := range allCerts { + if certIn(xc, certs) { + continue // finite chains only, mmmmkay. + } + if bytes.Equal(prev.RawIssuer, xc.RawSubject) && prev.CheckSignatureFrom(xc) == nil { + // Prefer certificates with later expirations. + if next == nil || xc.NotAfter.After(next.NotAfter) { + next = xc + } + } + } + } + if len(certs) == 0 { + return nil, fmt.Errorf("no key found with issuer common name %q", issuerCN) + } + + skr, err := identityToSecKeyRef(leafIdent) + if err != nil { + return nil, err + } + defer C.CFRelease(C.CFTypeRef(skr)) + return newKey(skr, certs) +} + +// identityToX509 converts a single CFDictionary that contains the item ref and +// attribute dictionary into an x509.Certificate. +func identityToX509(ident C.SecIdentityRef) (*x509.Certificate, error) { + var certRef C.SecCertificateRef + if errno := C.SecIdentityCopyCertificate(ident, &certRef); errno != 0 { + return nil, keychainError(errno) + } + defer C.CFRelease(C.CFTypeRef(certRef)) + + return certRefToX509(certRef) +} + +// certRefToX509 converts a single C.SecCertificateRef into an *x509.Certificate. +func certRefToX509(certRef C.SecCertificateRef) (*x509.Certificate, error) { + // Export the PEM-encoded certificate to a CFDataRef. + var certPEMData C.CFDataRef + if errno := C.SecItemExport(C.CFTypeRef(certRef), C.kSecFormatUnknown, C.kSecItemPemArmour, nil, &certPEMData); errno != 0 { + return nil, keychainError(errno) + } + defer C.CFRelease(C.CFTypeRef(certPEMData)) + certPEM := cfDataToBytes(certPEMData) + + // This part based on crypto/tls. + var certDERBlock *pem.Block + for { + certDERBlock, certPEM = pem.Decode(certPEM) + if certDERBlock == nil { + return nil, fmt.Errorf("failed to parse certificate PEM data") + } + if certDERBlock.Type == "CERTIFICATE" { + // found it + break + } + } + + // Check the certificate is OK by the x509 library, and obtain the + // public key algorithm (which I assume is the same as the private key + // algorithm). This also filters out certs missing critical extensions. + xc, err := x509.ParseCertificate(certDERBlock.Bytes) + if err != nil { + return nil, err + } + switch xc.PublicKey.(type) { + case *rsa.PublicKey, *ecdsa.PublicKey: + default: + return nil, fmt.Errorf("unsupported key type %T", xc.PublicKey) + } + + // Check the certificate is valid + if n := time.Now(); n.Before(xc.NotBefore) || n.After(xc.NotAfter) { + return nil, fmt.Errorf("certificate not valid") + } + + return xc, nil +} + +// identityToSecKeyRef converts a single CFDictionary that contains the item ref and +// attribute dictionary into a SecKeyRef for its private key. +func identityToSecKeyRef(ident C.SecIdentityRef) (C.SecKeyRef, error) { + // Get the private key (ref). Note that "Copy" in "CopyPrivateKey" + // refers to "the create rule" of CoreFoundation memory management, and + // does not actually copy the private key---it gives us a copy of the + // reference that we now own. + var ref C.SecKeyRef + if errno := C.SecIdentityCopyPrivateKey(C.SecIdentityRef(ident), &ref); errno != 0 { + return 0, keychainError(errno) + } + return ref, nil +} + +func stringIn(s string, ss []string) bool { + for _, s2 := range ss { + if s == s2 { + return true + } + } + return false +} + +func certIn(xc *x509.Certificate, xcs []*x509.Certificate) bool { + for _, xc2 := range xcs { + if xc.Equal(xc2) { + return true + } + } + return false +} diff --git a/internal/signer/darwin/keychain/keychain_test.go b/internal/signer/darwin/keychain/keychain_test.go new file mode 100644 index 0000000..946ba9b --- /dev/null +++ b/internal/signer/darwin/keychain/keychain_test.go @@ -0,0 +1,48 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build darwin && cgo +// +build darwin,cgo + +package keychain + +import ( + "bytes" + "testing" + "unsafe" +) + +func TestKeychainError(t *testing.T) { + tests := []struct { + e keychainError + want string + }{ + {e: keychainError(0), want: "No error."}, + {e: keychainError(-4), want: "Function or operation not implemented."}, + } + + for i, test := range tests { + if got := test.e.Error(); got != test.want { + t.Errorf("test %d: %#v.Error() = %q, want %q", i, test.e, got, test.want) + } + } +} + +func TestBytesToCFDataRoundTrip(t *testing.T) { + want := []byte("an arbitrary and yet coherent byte slice!") + d := bytesToCFData(want) + defer cfRelease(unsafe.Pointer(d)) + if got := cfDataToBytes(d); !bytes.Equal(got, want) { + t.Errorf("bytesToCFData -> cfDataToBytes\ngot %x\nwant %x", got, want) + } +} diff --git a/internal/signer/darwin/signer.go b/internal/signer/darwin/signer.go new file mode 100644 index 0000000..3eac7db --- /dev/null +++ b/internal/signer/darwin/signer.go @@ -0,0 +1,132 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Signer.go is a net/rpc server that listens on stdin/stdout, exposing +// methods that perform device certificate signing for Mac OS using keychain utils. +// This server is intended to be launched as a subprocess by the signer client, +// and should not be launched manually as a stand-alone process. +package main + +import ( + "crypto" + "crypto/rsa" + "crypto/x509" + "encoding/gob" + "io" + "log" + "net/rpc" + "os" + "signer/keychain" + "signer/util" + "time" +) + +// If ECP Logging is enabled return true +// Otherwise return false +func enableECPLogging() bool { + if os.Getenv("ENABLE_ENTERPRISE_CERTIFICATE_LOGS") != "" { + return true + } + + log.SetOutput(io.Discard) + return false +} + +func init() { + gob.Register(crypto.SHA256) + gob.Register(crypto.SHA384) + gob.Register(crypto.SHA512) + gob.Register(&rsa.PSSOptions{}) +} + +// SignArgs contains arguments to a crypto Signer.Sign method. +type SignArgs struct { + Digest []byte // The content to sign. + Opts crypto.SignerOpts // Options for signing, such as Hash identifier. +} + +// A EnterpriseCertSigner exports RPC methods for signing. +type EnterpriseCertSigner struct { + key *keychain.Key +} + +// A Connection wraps a pair of unidirectional streams as an io.ReadWriteCloser. +type Connection struct { + io.ReadCloser + io.WriteCloser +} + +// Close closes c's underlying ReadCloser and WriteCloser. +func (c *Connection) Close() error { + rerr := c.ReadCloser.Close() + werr := c.WriteCloser.Close() + if rerr != nil { + return rerr + } + return werr +} + +// CertificateChain returns the credential as a raw X509 cert chain. This +// contains the public key. +func (k *EnterpriseCertSigner) CertificateChain(ignored struct{}, certificateChain *[][]byte) error { + *certificateChain = k.key.CertificateChain() + return nil +} + +// Public returns the corresponding public key for this Key, in ASN.1 DER form. +func (k *EnterpriseCertSigner) Public(ignored struct{}, publicKey *[]byte) (err error) { + *publicKey, err = x509.MarshalPKIXPublicKey(k.key.Public()) + return +} + +// Sign signs a message digest. +func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) { + *resp, err = k.key.Sign(nil, args.Digest, args.Opts) + return +} + +func main() { + enableECPLogging() + if len(os.Args) != 2 { + log.Fatalln("Signer is not meant to be invoked manually, exiting...") + } + configFilePath := os.Args[1] + config, err := util.LoadConfig(configFilePath) + if err != nil { + log.Fatalf("Failed to load enterprise cert config: %v", err) + } + + enterpriseCertSigner := new(EnterpriseCertSigner) + enterpriseCertSigner.key, err = keychain.Cred(config.CertConfigs.MacOSKeychain.Issuer) + if err != nil { + log.Fatalf("Failed to initialize enterprise cert signer using keychain: %v", err) + } + + if err := rpc.Register(enterpriseCertSigner); err != nil { + log.Fatalf("Failed to register enterprise cert signer with net/rpc: %v", err) + } + + // If the parent process dies, we should exit. + // We can detect this by periodically checking if the PID of the parent + // process is 1 (https://stackoverflow.com/a/2035683). + go func() { + for { + if os.Getppid() == 1 { + log.Fatalln("Enterprise cert signer's parent process died, exiting...") + } + time.Sleep(time.Second) + } + }() + + rpc.ServeConn(&Connection{os.Stdin, os.Stdout}) +} diff --git a/internal/signer/darwin/util/test_data/certificate_config.json b/internal/signer/darwin/util/test_data/certificate_config.json new file mode 100644 index 0000000..a4f0edf --- /dev/null +++ b/internal/signer/darwin/util/test_data/certificate_config.json @@ -0,0 +1,8 @@ +{ + "cert_configs": { + "macos_keychain": { + "issuer": "Google Endpoint Verification" + } + } +} + diff --git a/internal/signer/darwin/util/util.go b/internal/signer/darwin/util/util.go new file mode 100644 index 0000000..b8019d8 --- /dev/null +++ b/internal/signer/darwin/util/util.go @@ -0,0 +1,55 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package util provides helper functions for the signer. +package util + +import ( + "encoding/json" + "io" + "os" +) + +// EnterpriseCertificateConfig contains parameters for initializing signer. +type EnterpriseCertificateConfig struct { + CertConfigs CertConfigs `json:"cert_configs"` +} + +// CertConfigs is a container for various ECP Configs. +type CertConfigs struct { + MacOSKeychain MacOSKeychain `json:"macos_keychain"` +} + +// MacOSKeychain contains parameters describing the certificate to use. +type MacOSKeychain struct { + Issuer string `json:"issuer"` +} + +// LoadConfig retrieves the ECP config file. +func LoadConfig(configFilePath string) (config EnterpriseCertificateConfig, err error) { + jsonFile, err := os.Open(configFilePath) + if err != nil { + return EnterpriseCertificateConfig{}, err + } + + byteValue, err := io.ReadAll(jsonFile) + if err != nil { + return EnterpriseCertificateConfig{}, err + } + err = json.Unmarshal(byteValue, &config) + if err != nil { + return EnterpriseCertificateConfig{}, err + } + return config, nil + +} diff --git a/internal/signer/darwin/util/util_test.go b/internal/signer/darwin/util/util_test.go new file mode 100644 index 0000000..372ef7e --- /dev/null +++ b/internal/signer/darwin/util/util_test.go @@ -0,0 +1,29 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "testing" +) + +func TestLoadConfig(t *testing.T) { + config, err := LoadConfig("./test_data/certificate_config.json") + if err != nil { + t.Errorf("LoadConfig error: %q", err) + } + want := "Google Endpoint Verification" + if config.CertConfigs.MacOSKeychain.Issuer != want { + t.Errorf("Expected issuer is %q, got: %q", want, config.CertConfigs.MacOSKeychain.Issuer) + } +} diff --git a/internal/signer/linux/go.mod b/internal/signer/linux/go.mod new file mode 100644 index 0000000..96aab0c --- /dev/null +++ b/internal/signer/linux/go.mod @@ -0,0 +1,5 @@ +module signer + +go 1.19 + +require github.com/google/go-pkcs11 v0.2.0 diff --git a/internal/signer/linux/go.sum b/internal/signer/linux/go.sum new file mode 100644 index 0000000..d01e7f0 --- /dev/null +++ b/internal/signer/linux/go.sum @@ -0,0 +1,4 @@ +github.com/google/go-pkcs11 v0.1.1-0.20220804004530-aced8594bb2e h1:y7UBq7yC0nK2b4h9uisyrhYVd21Ju/2GyzRve8dOvtk= +github.com/google/go-pkcs11 v0.1.1-0.20220804004530-aced8594bb2e/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/go-pkcs11 v0.2.0 h1:5meDPB26aJ98f+K9G21f0AqZwo/S5BJMJh8nuhMbdsI= +github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= diff --git a/internal/signer/linux/signer.go b/internal/signer/linux/signer.go new file mode 100644 index 0000000..ac2bb25 --- /dev/null +++ b/internal/signer/linux/signer.go @@ -0,0 +1,132 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Signer.go is a net/rpc server that listens on stdin/stdout, exposing +// methods that perform device certificate signing for Linux using PKCS11 +// shared library. +// This server is intended to be launched as a subprocess by the signer client, +// and should not be launched manually as a stand-alone process. +package main + +import ( + "crypto" + "crypto/rsa" + "crypto/x509" + "encoding/gob" + "io" + "log" + "net/rpc" + "os" + "signer/util" + "time" +) + +// If ECP Logging is enabled return true +// Otherwise return false +func enableECPLogging() bool { + if os.Getenv("ENABLE_ENTERPRISE_CERTIFICATE_LOGS") != "" { + return true + } + + log.SetOutput(io.Discard) + return false +} + +func init() { + gob.Register(crypto.SHA256) + gob.Register(crypto.SHA384) + gob.Register(crypto.SHA512) + gob.Register(&rsa.PSSOptions{}) +} + +// SignArgs contains arguments to a crypto Signer.Sign method. +type SignArgs struct { + Digest []byte // The content to sign. + Opts crypto.SignerOpts // Options for signing, such as Hash identifier. +} + +// A EnterpriseCertSigner exports RPC methods for signing. +type EnterpriseCertSigner struct { + key *util.Key +} + +// A Connection wraps a pair of unidirectional streams as an io.ReadWriteCloser. +type Connection struct { + io.ReadCloser + io.WriteCloser +} + +// Close closes c's underlying ReadCloser and WriteCloser. +func (c *Connection) Close() error { + rerr := c.ReadCloser.Close() + werr := c.WriteCloser.Close() + if rerr != nil { + return rerr + } + return werr +} + +// CertificateChain returns the credential as a raw X509 cert chain. This +// contains the public key. +func (k *EnterpriseCertSigner) CertificateChain(ignored struct{}, certificateChain *[][]byte) (err error) { + *certificateChain = k.key.CertificateChain() + return nil +} + +// Public returns the corresponding public key for this Key, in ASN.1 DER form. +func (k *EnterpriseCertSigner) Public(ignored struct{}, publicKey *[]byte) (err error) { + *publicKey, err = x509.MarshalPKIXPublicKey(k.key.Public()) + return +} + +// Sign signs a message digest. +func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) { + *resp, err = k.key.Sign(nil, args.Digest, args.Opts) + return +} + +func main() { + enableECPLogging() + if len(os.Args) != 2 { + log.Fatalln("Signer is not meant to be invoked manually, exiting...") + } + configFilePath := os.Args[1] + config, err := util.LoadConfig(configFilePath) + if err != nil { + log.Fatalf("Failed to load enterprise cert config: %v", err) + } + + enterpriseCertSigner := new(EnterpriseCertSigner) + enterpriseCertSigner.key, err = util.Cred(config.CertConfigs.PKCS11.PKCS11Module, config.CertConfigs.PKCS11.Slot, config.CertConfigs.PKCS11.Label, config.CertConfigs.PKCS11.UserPin) + if err != nil { + log.Fatalf("Failed to initialize enterprise cert signer using pkcs11: %v", err) + } + + if err := rpc.Register(enterpriseCertSigner); err != nil { + log.Fatalf("Failed to register enterprise cert signer with net/rpc: %v", err) + } + + // If the parent process dies, we should exit. + // We can detect this by periodically checking if the PID of the parent + // process is 1 (https://stackoverflow.com/a/2035683). + go func() { + for { + if os.Getppid() == 1 { + log.Fatalln("Enterprise cert signer's parent process died, exiting...") + } + time.Sleep(time.Second) + } + }() + + rpc.ServeConn(&Connection{os.Stdin, os.Stdout}) +} diff --git a/internal/signer/linux/util/cert_util.go b/internal/signer/linux/util/cert_util.go new file mode 100644 index 0000000..07a1449 --- /dev/null +++ b/internal/signer/linux/util/cert_util.go @@ -0,0 +1,112 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Cert_util provides helpers for working with certificates via PKCS11 +package util + +import ( + "crypto" + "errors" + "io" + + "github.com/google/go-pkcs11/pkcs11" +) + +// Cred returns a Key wrapping the first valid certificate in the pkcs11 module +// matching a given slot and label. +func Cred(pkcs11Module string, slotUint32Str string, label string, userPin string) (*Key, error) { + module, err := pkcs11.Open(pkcs11Module) + if err != nil { + return nil, err + } + slotUint32, err := ParseHexString(slotUint32Str) + if err != nil { + return nil, err + } + kslot, err := module.Slot(slotUint32, pkcs11.Options{PIN: userPin}) + if err != nil { + return nil, err + } + + certs, err := kslot.Objects(pkcs11.Filter{Class: pkcs11.ClassCertificate, Label: label}) + if err != nil { + return nil, err + } + cert, err := certs[0].Certificate() + if err != nil { + return nil, err + } + x509, err := cert.X509() + if err != nil { + return nil, err + } + var kchain [][]byte + kchain = append(kchain, x509.Raw) + + pubKeys, err := kslot.Objects(pkcs11.Filter{Class: pkcs11.ClassPublicKey, Label: label}) + if err != nil { + return nil, err + } + pubKey, err := pubKeys[0].PublicKey() + if err != nil { + return nil, err + } + + privkeys, err := kslot.Objects(pkcs11.Filter{Class: pkcs11.ClassPrivateKey, Label: label}) + if err != nil { + return nil, err + } + privKey, err := privkeys[0].PrivateKey(pubKey) + if err != nil { + return nil, err + } + ksigner, ok := privKey.(crypto.Signer) + if !ok { + return nil, errors.New("PrivateKey does not implement crypto.Signer") + } + + return &Key{ + slot: kslot, + signer: ksigner, + chain: kchain, + }, nil +} + +// Key is a wrapper around the pkcs11 module and uses it to +// implement signing-related methods. +type Key struct { + slot *pkcs11.Slot + signer crypto.Signer + chain [][]byte +} + +// CertificateChain returns the credential as a raw X509 cert chain. This +// contains the public key. +func (k *Key) CertificateChain() [][]byte { + return k.chain +} + +// Close releases resources held by the credential. +func (k *Key) Close() { + k.slot.Close() +} + +// Public returns the corresponding public key for this Key. +func (k *Key) Public() crypto.PublicKey { + return k.signer.Public() +} + +// Sign signs a message. +func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + return k.signer.Sign(nil, digest, opts) +} diff --git a/internal/signer/linux/util/test_data/certificate_config.json b/internal/signer/linux/util/test_data/certificate_config.json new file mode 100644 index 0000000..64ed1c2 --- /dev/null +++ b/internal/signer/linux/util/test_data/certificate_config.json @@ -0,0 +1,10 @@ +{ + "cert_configs": { + "pkcs11": { + "slot": "0x1739427", + "label": "gecc", + "user_pin": "0000", + "module": "pkcs11_module.so" + } + } +} diff --git a/internal/signer/linux/util/util.go b/internal/signer/linux/util/util.go new file mode 100644 index 0000000..630840a --- /dev/null +++ b/internal/signer/linux/util/util.go @@ -0,0 +1,70 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package util provides helper functions for the signer. +package util + +import ( + "encoding/json" + "io" + "os" + "strconv" + "strings" +) + +// ParseHexString parses hexadecimal string into uint32 +func ParseHexString(str string) (i uint32, err error) { + stripped := strings.Replace(str, "0x", "", -1) + resultUint64, err := strconv.ParseUint(stripped, 16, 32) + if err != nil { + return 0, err + } + return uint32(resultUint64), nil +} + +// EnterpriseCertificateConfig contains parameters for initializing signer. +type EnterpriseCertificateConfig struct { + CertConfigs CertConfigs `json:"cert_configs"` +} + +// CertConfigs is a container for various ECP Configs. +type CertConfigs struct { + PKCS11 PKCS11 `json:"pkcs11"` +} + +// PKCS11 contains parameters describing the certificate to use. +type PKCS11 struct { + Slot string `json:"slot"` // The hexadecimal representation of the uint36 slot ID. (ex:0x1739427) + Label string `json:"label"` // The token label (ex: gecc) + PKCS11Module string `json:"module"` // The path to the pkcs11 module (shared lib) + UserPin string `json:"user_pin"` // Optional user pin to unlock the PKCS #11 module. If it is not defined or empty C_Login will not be called. +} + +// LoadConfig retrieves the ECP config file. +func LoadConfig(configFilePath string) (config EnterpriseCertificateConfig, err error) { + jsonFile, err := os.Open(configFilePath) + if err != nil { + return EnterpriseCertificateConfig{}, err + } + + byteValue, err := io.ReadAll(jsonFile) + if err != nil { + return EnterpriseCertificateConfig{}, err + } + err = json.Unmarshal(byteValue, &config) + if err != nil { + return EnterpriseCertificateConfig{}, err + } + return config, nil + +} diff --git a/internal/signer/linux/util/util_test.go b/internal/signer/linux/util/util_test.go new file mode 100644 index 0000000..86f2b64 --- /dev/null +++ b/internal/signer/linux/util/util_test.go @@ -0,0 +1,66 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "testing" +) + +func TestLoadConfig(t *testing.T) { + config, err := LoadConfig("./test_data/certificate_config.json") + if err != nil { + t.Fatalf("LoadConfig error: %v", err) + } + want := "0x1739427" + if config.CertConfigs.PKCS11.Slot != want { + t.Errorf("Expected slot is %v, got: %v", want, config.CertConfigs.PKCS11.Slot) + } + want = "gecc" + if config.CertConfigs.PKCS11.Label != want { + t.Errorf("Expected label is %v, got: %v", want, config.CertConfigs.PKCS11.Label) + } + want = "pkcs11_module.so" + if config.CertConfigs.PKCS11.PKCS11Module != want { + t.Errorf("Expected pkcs11_module is %v, got: %v", want, config.CertConfigs.PKCS11.PKCS11Module) + } + want = "0000" + if config.CertConfigs.PKCS11.UserPin != want { + t.Errorf("Expected user pin is %v, got: %v", want, config.CertConfigs.PKCS11.UserPin) + } +} + +func TestLoadConfigMissing(t *testing.T) { + _, err := LoadConfig("./test_data/certificate_config_missing.json") + if err == nil { + t.Error("Expected error but got nil") + } +} + +func TestParseHexString(t *testing.T) { + got, err := ParseHexString("0x1739427") + if err != nil { + t.Fatalf("ParseHexString error: %v", err) + } + want := uint32(0x1739427) + if got != want { + t.Errorf("Expected result is %v, got: %v", want, got) + } +} + +func TestParseHexStringFailure(t *testing.T) { + _, err := ParseHexString("abcdefgh") + if err == nil { + t.Error("Expected error but got nil") + } +} diff --git a/internal/signer/test/signer.go b/internal/signer/test/signer.go new file mode 100644 index 0000000..c34fc14 --- /dev/null +++ b/internal/signer/test/signer.go @@ -0,0 +1,110 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// signer.go is a net/rpc server that listens on stdin/stdout, exposing +// mock methods for testing client.go. +package main + +import ( + "crypto" + "crypto/tls" + "crypto/x509" + "io" + "log" + "net/rpc" + "os" + "time" +) + +// SignArgs encapsulate the parameters for the Sign method. +type SignArgs struct { + Digest []byte + Opts crypto.SignerOpts +} + +// EnterpriseCertSigner exports RPC methods for signing. +type EnterpriseCertSigner struct { + cert *tls.Certificate +} + +// Connection wraps a pair of unidirectional streams as an io.ReadWriteCloser. +type Connection struct { + io.ReadCloser + io.WriteCloser +} + +// Close closes c's underlying ReadCloser and WriteCloser. +func (c *Connection) Close() error { + rerr := c.ReadCloser.Close() + werr := c.WriteCloser.Close() + if rerr != nil { + return rerr + } + return werr +} + +// CertificateChain returns the credential as a raw X509 cert chain. This +// contains the public key. +func (k *EnterpriseCertSigner) CertificateChain(ignored struct{}, certificateChain *[][]byte) error { + *certificateChain = k.cert.Certificate + return nil +} + +// Public returns the first public key for this Key, in ASN.1 DER form. +func (k *EnterpriseCertSigner) Public(ignored struct{}, publicKey *[]byte) (err error) { + if len(k.cert.Certificate) == 0 { + return nil + } + cert, err := x509.ParseCertificate(k.cert.Certificate[0]) + if err != nil { + return err + } + *publicKey, err = x509.MarshalPKIXPublicKey(cert.PublicKey) + return err +} + +// Sign signs a message digest. +func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) { + *resp = args.Digest + return nil +} + +func main() { + enterpriseCertSigner := new(EnterpriseCertSigner) + + data, err := os.ReadFile(os.Args[1]) + if err != nil { + log.Fatalf("Error reading certificate: %v", err) + } + cert, _ := tls.X509KeyPair(data, data) + + enterpriseCertSigner.cert = &cert + + if err := rpc.Register(enterpriseCertSigner); err != nil { + log.Fatalf("Error registering net/rpc: %v", err) + } + + // If the parent process dies, we should exit. + // We can detect this by periodically checking if the PID of the parent + // process is 1 (https://stackoverflow.com/a/2035683). + go func() { + for { + if os.Getppid() == 1 { + log.Fatalln("Parent process died, exiting...") + } + time.Sleep(time.Second) + } + }() + + rpc.ServeConn(&Connection{os.Stdin, os.Stdout}) +} diff --git a/internal/signer/windows/.gitattributes b/internal/signer/windows/.gitattributes new file mode 100644 index 0000000..a0717e4 --- /dev/null +++ b/internal/signer/windows/.gitattributes @@ -0,0 +1 @@ +*.go text eol=lf
\ No newline at end of file diff --git a/internal/signer/windows/go.mod b/internal/signer/windows/go.mod new file mode 100644 index 0000000..b6b5b16 --- /dev/null +++ b/internal/signer/windows/go.mod @@ -0,0 +1,8 @@ +module signer + +go 1.19 + +require ( + golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect +) diff --git a/internal/signer/windows/go.sum b/internal/signer/windows/go.sum new file mode 100644 index 0000000..c085ca2 --- /dev/null +++ b/internal/signer/windows/go.sum @@ -0,0 +1,11 @@ +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/signer/windows/ncrypt/cert_util.go b/internal/signer/windows/ncrypt/cert_util.go new file mode 100644 index 0000000..f2f078a --- /dev/null +++ b/internal/signer/windows/ncrypt/cert_util.go @@ -0,0 +1,300 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows +// +build windows + +// Cert_util provides helpers for working with Windows certificates via crypt32.dll + +package ncrypt + +import ( + "crypto" + "crypto/x509" + "errors" + "fmt" + "io" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + // wincrypt.h constants + encodingX509ASN = 1 // X509_ASN_ENCODING + certStoreCurrentUserID = 1 // CERT_SYSTEM_STORE_CURRENT_USER_ID + certStoreLocalMachineID = 2 // CERT_SYSTEM_STORE_LOCAL_MACHINE_ID + infoIssuerFlag = 4 // CERT_INFO_ISSUER_FLAG + compareNameStrW = 8 // CERT_COMPARE_NAME_STR_A + certStoreProvSystem = 10 // CERT_STORE_PROV_SYSTEM + compareShift = 16 // CERT_COMPARE_SHIFT + locationShift = 16 // CERT_SYSTEM_STORE_LOCATION_SHIFT + findIssuerStr = compareNameStrW<<compareShift | infoIssuerFlag // CERT_FIND_ISSUER_STR_W + certStoreLocalMachine = certStoreLocalMachineID << locationShift // CERT_SYSTEM_STORE_LOCAL_MACHINE + certStoreCurrentUser = certStoreCurrentUserID << locationShift // CERT_SYSTEM_STORE_CURRENT_USER + signatureKeyUsage = 0x80 // CERT_DIGITAL_SIGNATURE_KEY_USAGE + acquireCached = 0x1 // CRYPT_ACQUIRE_CACHE_FLAG + acquireSilent = 0x40 // CRYPT_ACQUIRE_SILENT_FLAG + acquireOnlyNCryptKey = 0x40000 // CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG + ncryptKeySpec = 0xFFFFFFFF // CERT_NCRYPT_KEY_SPEC + certChainCacheOnlyURLRetrieval = 0x00000004 // CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL + certChainDisableAIA = 0x00002000 // CERT_CHAIN_DISABLE_AIA + certChainRevocationCheckCacheOnly = 0x80000000 // CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY + + hcceLocalMachine = windows.Handle(0x01) // HCCE_LOCAL_MACHINE + + // winerror.h constants + cryptENotFound = 0x80092004 // CRYPT_E_NOT_FOUND +) + +var ( + null = uintptr(unsafe.Pointer(nil)) + + crypt32 = windows.MustLoadDLL("crypt32.dll") + + certFindCertificateInStore = crypt32.MustFindProc("CertFindCertificateInStore") + certGetIntendedKeyUsage = crypt32.MustFindProc("CertGetIntendedKeyUsage") + cryptAcquireCertificatePrivateKey = crypt32.MustFindProc("CryptAcquireCertificatePrivateKey") +) + +// findCert wraps the CertFindCertificateInStore call. Note that any cert context passed +// into prev will be freed. If no certificate was found, nil will be returned. +func findCert(store windows.Handle, enc uint32, findFlags uint32, findType uint32, para *uint16, prev *windows.CertContext) (*windows.CertContext, error) { + h, _, err := certFindCertificateInStore.Call( + uintptr(store), + uintptr(enc), + uintptr(findFlags), + uintptr(findType), + uintptr(unsafe.Pointer(para)), + uintptr(unsafe.Pointer(prev)), + ) + if h == 0 { + // Actual error, or simply not found? + errno, ok := err.(syscall.Errno) + if !ok { + return nil, err + } + if errno == cryptENotFound { + return nil, nil + } + return nil, err + } + return (*windows.CertContext)(unsafe.Pointer(h)), nil +} + +// extractSimpleChain extracts the final certificate chain from a CertSimpleChain. +// Adapted from crypto.x509.root_windows +func extractSimpleChain(simpleChain **windows.CertSimpleChain, chainCount int) ([]*x509.Certificate, error) { + if simpleChain == nil || chainCount == 0 { + return nil, errors.New("invalid simple chain") + } + // Convert the simpleChain array to a huge slice and slice it to the length we want. + // https://github.com/golang/go/wiki/cgo#turning-c-arrays-into-go-slices + simpleChains := (*[1 << 20]*windows.CertSimpleChain)(unsafe.Pointer(simpleChain))[:chainCount:chainCount] + // Each simple chain contains the chain of certificates, summary trust information + // about the chain, and trust information about each certificate element in the chain. + // Select the last chain since only expect to encounter one chain. + lastChain := simpleChains[chainCount-1] + chainLen := int(lastChain.NumElements) + elements := (*[1 << 20]*windows.CertChainElement)(unsafe.Pointer(lastChain.Elements))[:chainLen:chainLen] + chain := make([]*x509.Certificate, 0, chainLen) + for _, element := range elements { + xc, err := certContextToX509(element.CertContext) + if err != nil { + return nil, err + } + chain = append(chain, xc) + } + return chain, nil +} + +// findCertChain builds a chain from a given certificate using the local machine store. +func findCertChain(cert *windows.CertContext) ([]*x509.Certificate, error) { + var ( + chainPara windows.CertChainPara + chainCtx *windows.CertChainContext + ) + + // Search the system for candidate certificate chains. + // Because we are using unsafe pointers here, we CANNOT directly call + // CertGetCertificateChain and MUST either use the windows or syscall library + // to validly use unsafe pointers. + // See https://golang.org/pkg/unsafe/#Pointer for valid unsafe package patterns. + chainPara.Size = uint32(unsafe.Sizeof(chainPara)) + err := windows.CertGetCertificateChain( + hcceLocalMachine, + cert, + nil, + cert.Store, + &chainPara, + certChainRevocationCheckCacheOnly|certChainCacheOnlyURLRetrieval|certChainDisableAIA, + 0, + &chainCtx) + + if err != nil { + return nil, fmt.Errorf("getCertificateChain: %w", err) + } + defer windows.CertFreeCertificateChain(chainCtx) + + x509Certs, err := extractSimpleChain(chainCtx.Chains, int(chainCtx.ChainCount)) + if err != nil { + return nil, fmt.Errorf("getCertificateChain extractSimpleChain: %w", err) + } + return x509Certs, nil +} + +// intendedKeyUsage wraps CertGetIntendedKeyUsage. If there are key usage bytes they will be returned, +// otherwise 0 will be returned. +func intendedKeyUsage(enc uint32, cert *windows.CertContext) (usage uint16) { + _, _, _ = certGetIntendedKeyUsage.Call(uintptr(enc), uintptr(unsafe.Pointer(cert.CertInfo)), uintptr(unsafe.Pointer(&usage)), 2) + return +} + +// acquirePrivateKey wraps CryptAcquireCertificatePrivateKey. +func acquirePrivateKey(cert *windows.CertContext) (windows.Handle, error) { + var ( + key windows.Handle + keySpec uint32 + mustFree int + ) + r, _, err := cryptAcquireCertificatePrivateKey.Call( + uintptr(unsafe.Pointer(cert)), + acquireCached|acquireSilent|acquireOnlyNCryptKey, + null, + uintptr(unsafe.Pointer(&key)), + uintptr(unsafe.Pointer(&keySpec)), + uintptr(unsafe.Pointer(&mustFree)), + ) + if r == 0 { + return 0, fmt.Errorf("acquiring private key: %x %w", r, err) + } + if mustFree != 0 { + return 0, fmt.Errorf("wrong mustFree [%d != 0]", mustFree) + } + if keySpec != ncryptKeySpec { + return 0, fmt.Errorf("wrong keySpec [%d != %d]", keySpec, ncryptKeySpec) + } + return key, nil +} + +// certContextToX509 extracts the x509 certificate from the cert context. +func certContextToX509(ctx *windows.CertContext) (*x509.Certificate, error) { + // To ensure we don't mess with the cert context's memory, use a copy of it. + src := (*[1 << 20]byte)(unsafe.Pointer(ctx.EncodedCert))[:ctx.Length:ctx.Length] + der := make([]byte, int(ctx.Length)) + copy(der, src) + + xc, err := x509.ParseCertificate(der) + if err != nil { + return xc, err + } + return xc, nil +} + +// Cred returns a Key wrapping the first valid certificate in the system store +// matching a given issuer string. +func Cred(issuer string, storeName string, provider string) (*Key, error) { + var certStore uint32 + if provider == "local_machine" { + certStore = uint32(certStoreLocalMachine) + } else if provider == "current_user" { + certStore = uint32(certStoreCurrentUser) + } else { + return nil, errors.New("provider must be local_machine or current_user") + } + storeNamePtr, err := windows.UTF16PtrFromString(storeName) + if err != nil { + return nil, err + } + store, err := windows.CertOpenStore(certStoreProvSystem, 0, null, certStore, uintptr(unsafe.Pointer(storeNamePtr))) + if err != nil { + return nil, fmt.Errorf("opening certificate store: %w", err) + } + i, err := windows.UTF16PtrFromString(issuer) + if err != nil { + return nil, err + } + var prev *windows.CertContext + for { + nc, err := findCert(store, encodingX509ASN, 0, findIssuerStr, i, prev) + if err != nil { + return nil, fmt.Errorf("finding certificates: %w", err) + } + if nc == nil { + return nil, errors.New("no certificate found") + } + prev = nc + if (intendedKeyUsage(encodingX509ASN, nc) & signatureKeyUsage) == 0 { + continue + } + + xc, err := certContextToX509(nc) + if err != nil { + continue + } + + machineChain, err := findCertChain(nc) + if err != nil { + continue + } + return &Key{ + cert: xc, + ctx: nc, + store: store, + chain: machineChain, + }, nil + } +} + +// Key is a wrapper around the certificate store and context that uses it to +// implement signing-related methods with CryptoNG functionality. +type Key struct { + cert *x509.Certificate + ctx *windows.CertContext + store windows.Handle + chain []*x509.Certificate +} + +// CertificateChain returns the credential as a raw X509 cert chain. This +// contains the public key. +func (k *Key) CertificateChain() [][]byte { + // Convert the certificates to a list of encoded certificate bytes. + chain := make([][]byte, len(k.chain)) + for i, xc := range k.chain { + chain[i] = xc.Raw + } + return chain +} + +// Close releases resources held by the credential. +func (k *Key) Close() error { + if err := windows.CertFreeCertificateContext(k.ctx); err != nil { + return err + } + return windows.CertCloseStore(k.store, 0) +} + +// Public returns the corresponding public key for this Key. +func (k *Key) Public() crypto.PublicKey { + return k.cert.PublicKey +} + +// Sign signs a message digest. Here, we pass off the signing to the Windows CryptoNG library. +func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + key, err := acquirePrivateKey(k.ctx) + if err != nil { + return nil, fmt.Errorf("cannot acquire private key handle: %w", err) + } + return SignHash(key, k.Public(), digest, opts) +} diff --git a/internal/signer/windows/ncrypt/cert_util_test.go b/internal/signer/windows/ncrypt/cert_util_test.go new file mode 100644 index 0000000..96ef40a --- /dev/null +++ b/internal/signer/windows/ncrypt/cert_util_test.go @@ -0,0 +1,32 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows +// +build windows + +package ncrypt + +import ( + "testing" +) + +func TestCredProviderNotSupported(t *testing.T) { + _, err := Cred("issuer", "store", "unsupported_provider") + if err == nil { + t.Errorf("Expected error, but got nil.") + } + want := "provider must be local_machine or current_user" + if err.Error() != want { + t.Errorf("Expected error is %q, got: %q", want, err.Error()) + } +} diff --git a/internal/signer/windows/ncrypt/ncrypt.go b/internal/signer/windows/ncrypt/ncrypt.go new file mode 100644 index 0000000..e8997d2 --- /dev/null +++ b/internal/signer/windows/ncrypt/ncrypt.go @@ -0,0 +1,170 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows +// +build windows + +// Package ncrypt provides wrappers around ncrypt.h functions. +// https://docs.microsoft.com/en-us/windows/win32/api/ncrypt/ +package ncrypt + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "fmt" + "math/big" + "unsafe" + + "golang.org/x/crypto/cryptobyte" + "golang.org/x/crypto/cryptobyte/asn1" + "golang.org/x/sys/windows" +) + +const ( + // bcrypt.h constants + bcryptPadPKCS1 = 0x00000002 // BCRYPT_PAD_PKCS1 + bcryptPadPSS = 0x00000008 // BCRYPT_PAD_PSS + + // ncrypt.h constants + nCryptSilentFlag = 0x00000040 // NCRYPT_SILENT_FLAG +) + +var ( + nCrypt = windows.MustLoadDLL("ncrypt.dll") + nCryptSignHash = nCrypt.MustFindProc("NCryptSignHash") +) + +// bcypt.h structs. +type pkcs1PaddingInfo struct { + algID *uint16 +} +type pssPaddingInfo struct { + algID *uint16 + saltLength uint32 +} + +func algID(hashFunc crypto.Hash) (*uint16, bool) { + algID, ok := map[crypto.Hash][]uint16{ + crypto.SHA256: {'S', 'H', 'A', '2', '5', '6', 0}, // BCRYPT_SHA256_ALGORITHM + }[hashFunc] + return &algID[0], ok +} + +func rsaPadding(opts crypto.SignerOpts, flags *int) (paddingInfo unsafe.Pointer, err error) { + if o, ok := opts.(*rsa.PSSOptions); ok { + algID, ok := algID(o.HashFunc()) + if !ok { + err = fmt.Errorf("unsupported hash function %T", o.HashFunc()) + return + } + saltLength := o.SaltLength + switch saltLength { + case rsa.PSSSaltLengthAuto: + err = fmt.Errorf("rsa.PSSSaltLengthAuto is not supported") + return + case rsa.PSSSaltLengthEqualsHash: + saltLength = o.HashFunc().Size() + } + paddingInfo = unsafe.Pointer(&pssPaddingInfo{ + algID: algID, + saltLength: uint32(saltLength), + }) + *flags |= bcryptPadPSS + return + } + + algID, ok := algID(opts.HashFunc()) + if !ok { + err = fmt.Errorf("unsupported hash function %T", opts.HashFunc()) + return + } + paddingInfo = unsafe.Pointer(&pkcs1PaddingInfo{ + algID: algID, + }) + *flags |= bcryptPadPKCS1 + return +} + +func signHashInternal(priv windows.Handle, pub crypto.PublicKey, digest []byte, flags int, paddingInfo unsafe.Pointer) ([]byte, error) { + var size uint32 + r, _, _ := nCryptSignHash.Call( + /* hKey */ uintptr(priv), + /* *pPaddingInfo */ uintptr(paddingInfo), + /* pbHashValue */ uintptr(unsafe.Pointer(&digest[0])), + /* cbHashValue */ uintptr(len(digest)), + /* pbSignature */ 0, + /* cbSignature */ 0, + /* *pcbResult */ uintptr(unsafe.Pointer(&size)), + /* dwFlagss */ uintptr(flags)) + if r != 0 { + return nil, fmt.Errorf("NCryptSignHash: failed to get signature length: %#x", r) + } + + sig := make([]byte, size) + r, _, _ = nCryptSignHash.Call( + /* hKey */ uintptr(priv), + /* *pPaddingInfo */ uintptr(paddingInfo), + /* pbHashValue */ uintptr(unsafe.Pointer(&digest[0])), + /* cbHashValue */ uintptr(len(digest)), + /* pbSignature */ uintptr(unsafe.Pointer(&sig[0])), + /* cbSignature */ uintptr(size), + /* *pcbResult */ uintptr(unsafe.Pointer(&size)), + /* dwFlagss */ uintptr(flags)) + if r != 0 { + return nil, fmt.Errorf("NCryptSignHash: failed to get signature: %#x", r) + } + if len(sig) != int(size) { + return nil, fmt.Errorf("invalid length sig = %d, size = %d", sig, size) + } + + switch pub := pub.(type) { + case *ecdsa.PublicKey: + var b cryptobyte.Builder + b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) { + b.AddASN1BigInt(new(big.Int).SetBytes(sig[:len(sig)/2])) + b.AddASN1BigInt(new(big.Int).SetBytes(sig[len(sig)/2:])) + }) + return b.Bytes() + case *rsa.PublicKey: + return sig, nil + default: + return nil, fmt.Errorf("unsupported public key type %T", pub) + } +} + +// SignHash is a wrapper for the NCryptSignHash function that supports only a +// subset of well-supported cryptographic primitives. +// +// Signature algorithms: ECDSA, RSA. +// Hash functions: SHA-256. +// RSA schemes: RSASSA-PKCS1 and RSASSA-PSS. +// +// https://docs.microsoft.com/en-us/windows/win32/api/ncrypt/nf-ncrypt-ncryptsignhash +func SignHash(priv windows.Handle, pub crypto.PublicKey, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + var paddingInfo unsafe.Pointer + flags := nCryptSilentFlag + switch pub := pub.(type) { + case *ecdsa.PublicKey: + case *rsa.PublicKey: + var err error + paddingInfo, err = rsaPadding(opts, &flags) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unsupported public key type %T", pub) + } + + return signHashInternal(priv, pub, digest, flags, paddingInfo) +} diff --git a/internal/signer/windows/signer.go b/internal/signer/windows/signer.go new file mode 100644 index 0000000..9ef64ab --- /dev/null +++ b/internal/signer/windows/signer.go @@ -0,0 +1,132 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Signer.go is a net/rpc server that listens on stdin/stdout, exposing +// methods that perform device certificate signing for Windows OS using ncrypt utils. +// This server is intended to be launched as a subprocess by the signer client, +// and should not be launched manually as a stand-alone process. +package main + +import ( + "crypto" + "crypto/rsa" + "crypto/x509" + "encoding/gob" + "io" + "log" + "net/rpc" + "os" + "signer/ncrypt" + "signer/util" + "time" +) + +// If ECP Logging is enabled return true +// Otherwise return false +func enableECPLogging() bool { + if os.Getenv("ENABLE_ENTERPRISE_CERTIFICATE_LOGS") != "" { + return true + } + + log.SetOutput(io.Discard) + return false +} + +func init() { + gob.Register(crypto.SHA256) + gob.Register(crypto.SHA384) + gob.Register(crypto.SHA512) + gob.Register(&rsa.PSSOptions{}) +} + +// SignArgs contains arguments to a crypto Signer.Sign method. +type SignArgs struct { + Digest []byte // The content to sign. + Opts crypto.SignerOpts // Options for signing, such as Hash identifier. +} + +// A EnterpriseCertSigner exports RPC methods for signing. +type EnterpriseCertSigner struct { + key *ncrypt.Key +} + +// A Connection wraps a pair of unidirectional streams as an io.ReadWriteCloser. +type Connection struct { + io.ReadCloser + io.WriteCloser +} + +// Close closes c's underlying ReadCloser and WriteCloser. +func (c *Connection) Close() error { + rerr := c.ReadCloser.Close() + werr := c.WriteCloser.Close() + if rerr != nil { + return rerr + } + return werr +} + +// CertificateChain returns the credential as a raw X509 cert chain. This +// contains the public key. +func (k *EnterpriseCertSigner) CertificateChain(ignored struct{}, certificateChain *[][]byte) error { + *certificateChain = k.key.CertificateChain() + return nil +} + +// Public returns the corresponding public key for this Key, in ASN.1 DER form. +func (k *EnterpriseCertSigner) Public(ignored struct{}, publicKey *[]byte) (err error) { + *publicKey, err = x509.MarshalPKIXPublicKey(k.key.Public()) + return +} + +// Sign signs a message digest specified by args and writes the output to resp. +func (k *EnterpriseCertSigner) Sign(args SignArgs, resp *[]byte) (err error) { + *resp, err = k.key.Sign(nil, args.Digest, args.Opts) + return +} + +func main() { + enableECPLogging() + if len(os.Args) != 2 { + log.Fatalln("Signer is not meant to be invoked manually, exiting...") + } + configFilePath := os.Args[1] + config, err := util.LoadConfig(configFilePath) + if err != nil { + log.Fatalf("Failed to load enterprise cert config: %v", err) + } + + enterpriseCertSigner := new(EnterpriseCertSigner) + enterpriseCertSigner.key, err = ncrypt.Cred(config.CertConfigs.WindowsStore.Issuer, config.CertConfigs.WindowsStore.Store, config.CertConfigs.WindowsStore.Provider) + if err != nil { + log.Fatalf("Failed to initialize enterprise cert signer using ncrypt: %v", err) + } + + if err := rpc.Register(enterpriseCertSigner); err != nil { + log.Fatalf("Failed to register enterprise cert signer with net/rpc: %v", err) + } + + // If the parent process dies, we should exit. + // We can detect this by periodically checking if the PID of the parent + // process is 1 (https://stackoverflow.com/a/2035683). + go func() { + for { + if os.Getppid() == 1 { + log.Fatalln("Enterprise cert signer's parent process died, exiting...") + } + time.Sleep(time.Second) + } + }() + + rpc.ServeConn(&Connection{os.Stdin, os.Stdout}) +} diff --git a/internal/signer/windows/util/test_data/certificate_config.json b/internal/signer/windows/util/test_data/certificate_config.json new file mode 100644 index 0000000..567f719 --- /dev/null +++ b/internal/signer/windows/util/test_data/certificate_config.json @@ -0,0 +1,9 @@ +{ + "cert_configs": { + "windows_store": { + "issuer": "enterprise_v1_corp_client", + "store": "MY", + "provider": "current_user" + } + } +} diff --git a/internal/signer/windows/util/util.go b/internal/signer/windows/util/util.go new file mode 100644 index 0000000..a2bb1bd --- /dev/null +++ b/internal/signer/windows/util/util.go @@ -0,0 +1,57 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package util provides helper functions for the signer. +package util + +import ( + "encoding/json" + "io" + "os" +) + +// EnterpriseCertificateConfig contains parameters for initializing signer. +type EnterpriseCertificateConfig struct { + CertConfigs CertConfigs `json:"cert_configs"` +} + +// CertConfigs is a container for various ECP Configs. +type CertConfigs struct { + WindowsStore WindowsStore `json:"windows_store"` +} + +// WindowsStore contains parameters describing the certificate to use. +type WindowsStore struct { + Issuer string `json:"issuer"` + Store string `json:"store"` + Provider string `json:"provider"` +} + +// LoadConfig retrieves the ECP config file. +func LoadConfig(configFilePath string) (config EnterpriseCertificateConfig, err error) { + jsonFile, err := os.Open(configFilePath) + if err != nil { + return EnterpriseCertificateConfig{}, err + } + + byteValue, err := io.ReadAll(jsonFile) + if err != nil { + return EnterpriseCertificateConfig{}, err + } + err = json.Unmarshal(byteValue, &config) + if err != nil { + return EnterpriseCertificateConfig{}, err + } + return config, nil + +} diff --git a/internal/signer/windows/util/util_test.go b/internal/signer/windows/util/util_test.go new file mode 100644 index 0000000..89ad6e6 --- /dev/null +++ b/internal/signer/windows/util/util_test.go @@ -0,0 +1,37 @@ +// Copyright 2022 Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "testing" +) + +func TestLoadConfig(t *testing.T) { + config, err := LoadConfig("./test_data/certificate_config.json") + if err != nil { + t.Errorf("LoadConfig error: %q", err) + } + want := "enterprise_v1_corp_client" + if config.CertConfigs.WindowsStore.Issuer != want { + t.Errorf("Expected issuer is %q, got: %q", want, config.CertConfigs.WindowsStore.Issuer) + } + want = "MY" + if config.CertConfigs.WindowsStore.Store != want { + t.Errorf("Expected store is %q, got: %q", want, config.CertConfigs.WindowsStore.Store) + } + want = "current_user" + if config.CertConfigs.WindowsStore.Provider != want { + t.Errorf("Expected provider is %q, got: %q", want, config.CertConfigs.WindowsStore.Provider) + } +} |