From bbbb2afe0de2776426e64e40c75e0c2dc5bdc13d Mon Sep 17 00:00:00 2001 From: Adam Shannon Date: Wed, 22 Aug 2018 15:17:44 -0500 Subject: [PATCH 1/2] pkg/store: implement list on windows, cleanup GetInfo() Issue: https://github.com/adamdecaf/cert-manage/issues/8 --- pkg/store/windows.go | 183 ++++++++++++++------------------------ pkg/store/windows_test.go | 38 ++------ 2 files changed, 74 insertions(+), 147 deletions(-) diff --git a/pkg/store/windows.go b/pkg/store/windows.go index 5ead67e..ea43488 100644 --- a/pkg/store/windows.go +++ b/pkg/store/windows.go @@ -17,17 +17,16 @@ package store import ( - "bytes" "crypto/x509" "fmt" - "io/ioutil" - "os" "os/exec" "regexp" "strings" + "sync" + "syscall" + "unsafe" "github.com/adamdecaf/cert-manage/pkg/certutil" - "github.com/adamdecaf/cert-manage/pkg/file" "github.com/adamdecaf/cert-manage/pkg/whitelist" ) @@ -54,9 +53,23 @@ var ( "CA", // "Intermediate Certification Authorities" "AuthRoot", // "Third-Party Root Certification Authorities" } + + winRootStore *windowsRootStore ) -type windowsStore struct{} +type windowsStore struct { + once sync.Once +} + +func (s windowsStore) setup() { + s.once.Do(func() { + store, err := loadWindowsRootStore("Root") + if err != nil { + panic(fmt.Sprintf("problem loading windows store: %v", err)) + } + winRootStore = &store + }) +} func platform() Store { return windowsStore{} @@ -91,136 +104,76 @@ func (s windowsStore) GetInfo() *Info { version := strings.TrimPrefix(versionRegex.FindString(info), "OS Version:") return &Info{ - Name: strings.TrimSpace(name), - Version: strings.TrimSpace(version), + Name: strings.Split(strings.TrimSpace(name), "\r")[0], + Version: strings.Split(strings.TrimSpace(version), "\r")[0], } } func (s windowsStore) List(_ *ListOptions) ([]*x509.Certificate, error) { - pool := certutil.Pool{} - for i := range windowsStoreNames { - certs, err := s.certsFromStore(windowsStoreNames[i]) - if err != nil { - return nil, err - } - pool.AddCertificates(certs) - } - return pool.GetCertificates(), nil + s.setup() + return winRootStore.getCertificates() } -func (s windowsStore) certsFromStore(store string) ([]*x509.Certificate, error) { - serials, err := s.certSerialsFromStore(store) - if err != nil { - return nil, err - } - var accum []*x509.Certificate - for i := range serials { - cert, err := s.exportCertFromStore(serials[i], store) - if err != nil { - return nil, err - } - if cert != nil { - accum = append(accum, cert) - } - } - return accum, nil +func (s windowsStore) Remove(wh whitelist.Whitelist) error { + return nil } -func (s windowsStore) certSerialsFromStore(store string) ([]string, error) { - cmd := exec.Command("certutil", "-store", store) - var stdout bytes.Buffer - cmd.Stdout = &stdout - err := cmd.Run() - if err != nil { - return nil, fmt.Errorf("error reading serials from store %s err=%v", store, err) - } - return s.readCertSerials(string(stdout.Bytes())) +func (s windowsStore) Restore(where string) error { + return nil } var ( - // C:\Users>certutil -store Root - // Root "Trusted Root Certification Authorities" - // ================ Certificate 0 ================ - // Serial Number: 72696afcd5edce864658141cb588a3a8 - winSerialNumberPrefix = "Serial Number:" - winSerialNumberRegex = regexp.MustCompile(fmt.Sprintf(`%s ([0-9a-f]+)\r?\n?`, winSerialNumberPrefix)) + modcrypt32 = syscall.NewLazyDLL("crypt32.dll") + procCertCloseStore = modcrypt32.NewProc("CertCloseStore") + procCertDuplicateCertificateContext = modcrypt32.NewProc("CertDuplicateCertificateContext") + procCertEnumCertificatesInStore = modcrypt32.NewProc("CertEnumCertificatesInStore") + procCertOpenSystemStoreW = modcrypt32.NewProc("CertOpenSystemStoreW") ) -func (s windowsStore) readCertSerials(out string) ([]string, error) { - matches := winSerialNumberRegex.FindAllString(out, -1) - for i := range matches { - matches[i] = strings.TrimSpace(strings.Replace(matches[i], winSerialNumberPrefix, "", -1)) - } - return matches, nil -} - -var ( - pfxPassword = "password" -) +// windowsRootStore represents a pointer to a Root Certificate store on Windows +// This code is inspired from FiloSottile/mkcert's truststore_windows.go, but adapted +// for this projects usecase. +type windowsRootStore uintptr -func (s windowsStore) exportCertFromStore(serial, store string) (*x509.Certificate, error) { - tmp, err := ioutil.TempFile("", "cert-manage-windows") - if err != nil { - return nil, fmt.Errorf("error creating temp file, err=%v", err) +func loadWindowsRootStore(name string) (windowsRootStore, error) { + store, _, err := procCertOpenSystemStoreW.Call(0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name)))) + if store != 0 { + return windowsRootStore(store), nil } + return 0, fmt.Errorf("problem opening windows root store: %v", err) +} - // close and rm file, we got a unique name - // This avoids: "The process cannot access the file because it is being used by another process." - err = tmp.Close() - if err != nil { - return nil, err +func (w windowsRootStore) close() error { + ret, _, err := procCertCloseStore.Call(uintptr(w), 0) + if ret != 0 { + return nil } - err = os.Remove(tmp.Name()) - if err != nil { - return nil, err - } - defer func() { // then make sure it's gone afterwords - if file.Exists(tmp.Name()) { - os.Remove(tmp.Name()) - } - }() + return fmt.Errorf("problem closing windows root store: %v", err) +} - // export cert into PKCS #12 format - out, err := exec.Command("certutil", "-exportPFX", "-f", "-p", pfxPassword, store, serial, tmp.Name()).CombinedOutput() - if debug { - fmt.Printf("%q\n", string(out)) - } - if err != nil { - if debug && bytes.Contains(out, []byte("Keyset does not exist")) { - // TODO(adam): Issue repair? That might muck with the store(s) - return nil, nil - } - if debug && bytes.Contains(out, []byte("Cannot find object or property.")) { - // TODO(adam): uhh..? - // CertUtil: -exportPFX command FAILED: 0x80092004 (-2146885628 CRYPT_E_NOT_FOUND)\r\nCertUtil: Cannot find object or property. - return nil, nil +func (w windowsRootStore) getCertificates() ([]*x509.Certificate, error) { + var cert *syscall.CertContext + pool := certutil.Pool{} + for { + // Read next certificate + certPtr, _, err := procCertEnumCertificatesInStore.Call(uintptr(w), uintptr(unsafe.Pointer(cert))) + if cert = (*syscall.CertContext)(unsafe.Pointer(certPtr)); cert == nil { + // TODO(adam): figure out from FiloSottile/mkcert what "0x80092004" is exactly for.. + if errno, ok := err.(syscall.Errno); ok && errno == 0x80092004 { + break + } + return nil, fmt.Errorf("problem enumerating certs: %v", err) } - return nil, fmt.Errorf("error exporting cert %q (from %s) to PKCS #12 err=%q", serial, store, err) - } - bs, err := ioutil.ReadFile(tmp.Name()) - if err != nil { - return nil, fmt.Errorf("error reading temp file, err=%v", err) - } - cert, err := certutil.DecodePKCS12(bs, pfxPassword) - if err != nil { - if debug && strings.Contains(err.Error(), "expected exactly two items in the authenticated safe") { - // TODO(adam): https://github.com/golang/go/issues/23499 - return nil, nil - } - if debug && strings.Contains(err.Error(), "OID 1.3.6.1.4.1.311.17.2") { - // TODO(adam): http://oidref.com/1.3.6.1.4.1.311.17.2 - return nil, nil + // Parse cert + // Using C.GoBytes requires gcc, but crypto/x509 uses this trick too + // https://github.com/golang/go/blob/22e17d0ac7db5321a0f6e073bd0afb949f44dd70/src/crypto/x509/root_windows.go#L70 + certBytes := (*[1 << 20]byte)(unsafe.Pointer(cert.EncodedCert))[:cert.Length] + parsedCert, err := x509.ParseCertificate(certBytes) + if err != nil { + continue } - return nil, fmt.Errorf("error parsing PKCS #12 of serial %q from %s, err=%q", serial, store, err) + pool.Add(parsedCert) } - return cert, nil -} - -func (s windowsStore) Remove(wh whitelist.Whitelist) error { - return nil -} - -func (s windowsStore) Restore(where string) error { - return nil + return pool.GetCertificates(), nil } diff --git a/pkg/store/windows_test.go b/pkg/store/windows_test.go index 6752cf4..f0fe090 100644 --- a/pkg/store/windows_test.go +++ b/pkg/store/windows_test.go @@ -17,43 +17,17 @@ package store import ( - "reflect" "testing" ) -func TestStoreWindows__certSerialsFromStore(t *testing.T) { - win := windowsStore{} - - in := `Root "Trusted Root Certification Authorities" -================ Certificate 0 ================ -Serial Number: 72696afcd5edce864658141cb588a3a8 -Issuer: CN=WinDev1712Eval - NotBefore: 12/3/2017 9:55 PM - NotAfter: 4/5/3017 9:55 PM -Subject: CN=WinDev1712Eval -Signature matches Public Key -Root Certificate: Subject matches Issuer -Cert Hash(sha1): ef56cf1f0052b3ff7a1c834cfa3d71ea0d72c04e - Key Container = a3815d1a-0473-4a96-9375-cf18cc03b2f5 - Provider = Microsoft RSA SChannel Cryptographic Provider -Missing stored keyset - -Root "other" -================ Certificate 0 ================ -Serial Number: 214817429841748bbbcccca` - serials, err := win.readCertSerials(in) +func TestStoreWindows__list(t *testing.T) { + st := windowsStore{} + certs, err := st.List(&ListOptions{}) if err != nil { - t.Fatal(err) - } - if len(serials) != 2 { - t.Errorf("got %d serials", len(serials)) - } - ans := []string{ - "72696afcd5edce864658141cb588a3a8", - "214817429841748bbbcccca", + t.Fatalf("problem listing certs: %v", err) } - if !reflect.DeepEqual(serials, ans) { - t.Errorf("got %q", serials) + if len(certs) <= 0 { + t.Fatal("found no certificates") } } From edc57ca9a151ad9fde71d9818b8232fc058c3f08 Mon Sep 17 00:00:00 2001 From: Adam Shannon Date: Wed, 22 Aug 2018 15:35:45 -0500 Subject: [PATCH 2/2] pkg/store: share windows Once to avoid copying --- pkg/store/windows.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/store/windows.go b/pkg/store/windows.go index ea43488..1d9403b 100644 --- a/pkg/store/windows.go +++ b/pkg/store/windows.go @@ -54,15 +54,14 @@ var ( "AuthRoot", // "Third-Party Root Certification Authorities" } + winSetup sync.Once winRootStore *windowsRootStore ) -type windowsStore struct { - once sync.Once -} +type windowsStore struct {} func (s windowsStore) setup() { - s.once.Do(func() { + winSetup.Do(func() { store, err := loadWindowsRootStore("Root") if err != nil { panic(fmt.Sprintf("problem loading windows store: %v", err))