aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/signer/darwin/go.mod3
-rw-r--r--internal/signer/darwin/keychain/keychain.go407
-rw-r--r--internal/signer/darwin/keychain/keychain_test.go48
-rw-r--r--internal/signer/darwin/signer.go132
-rw-r--r--internal/signer/darwin/util/test_data/certificate_config.json8
-rw-r--r--internal/signer/darwin/util/util.go55
-rw-r--r--internal/signer/darwin/util/util_test.go29
-rw-r--r--internal/signer/linux/go.mod5
-rw-r--r--internal/signer/linux/go.sum4
-rw-r--r--internal/signer/linux/signer.go132
-rw-r--r--internal/signer/linux/util/cert_util.go112
-rw-r--r--internal/signer/linux/util/test_data/certificate_config.json10
-rw-r--r--internal/signer/linux/util/util.go70
-rw-r--r--internal/signer/linux/util/util_test.go66
-rw-r--r--internal/signer/test/signer.go110
-rw-r--r--internal/signer/windows/.gitattributes1
-rw-r--r--internal/signer/windows/go.mod8
-rw-r--r--internal/signer/windows/go.sum11
-rw-r--r--internal/signer/windows/ncrypt/cert_util.go300
-rw-r--r--internal/signer/windows/ncrypt/cert_util_test.go32
-rw-r--r--internal/signer/windows/ncrypt/ncrypt.go170
-rw-r--r--internal/signer/windows/signer.go132
-rw-r--r--internal/signer/windows/util/test_data/certificate_config.json9
-rw-r--r--internal/signer/windows/util/util.go57
-rw-r--r--internal/signer/windows/util/util_test.go37
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)
+ }
+}