Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

crypto/x509: make SystemCertPool work on Windows #26770

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions src/crypto/x509/cert_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ package x509

import (
"encoding/pem"
"errors"
"runtime"
)

// CertPool is a set of certificates.
Expand Down Expand Up @@ -53,11 +51,6 @@ func (s *CertPool) copy() *CertPool {
// New changes in the the system cert pool might not be reflected
// in subsequent calls.
func SystemCertPool() (*CertPool, error) {
if runtime.GOOS == "windows" {
// Issue 16736, 18609:
return nil, errors.New("crypto/x509: system root pool is not available on Windows")
}

if sysRoots := systemRootsPool(); sysRoots != nil {
return sysRoots.copy(), nil
}
Expand Down
179 changes: 155 additions & 24 deletions src/crypto/x509/root_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@
package x509

import (
"context"
"errors"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"unsafe"
)

Expand Down Expand Up @@ -226,41 +233,165 @@ func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate
}

func loadSystemRoots() (*CertPool, error) {
// TODO: restore this functionality on Windows. We tried to do
// it in Go 1.8 but had to revert it. See Issue 18609.
// Returning (nil, nil) was the old behavior, prior to CL 30578.
return nil, nil
roots := NewCertPool()
disallowedRoots := NewCertPool()

storeLocations := []string{"CurrentUser", "LocalMachine"}
storeNames := []string{"AddressBook", "AuthRoot", "CertificateAuthority", "My", "Root", "TrustedPeople", "TrustedPublisher"}

for _, l := range storeLocations {
disallowed, err := windowsX509Store("Disallowed", l)
if err != nil {
return nil, err
}

for _, c := range disallowed.certs {
disallowedRoots.AddCert(c)
}
}

for _, l := range storeLocations {
for _, n := range storeNames {
x509Store, err := windowsX509Store(n, l)
if err != nil {
return nil, err
}

for _, c := range x509Store.certs {
if !disallowedRoots.contains(c) {
roots.AddCert(c)
}
}
}
}

wuCerts, _, err := downloadWUCerts(time.Second * 15)
if err != nil {
return nil, err
}

for _, c := range wuCerts.certs {
if !disallowedRoots.contains(c) {
roots.AddCert(c)
}
}

return roots, nil
}

func windowsX509Store(storeName string, storeLocation string) (*CertPool, error) {
certStoreDir, err := ioutil.TempDir("", "go_x509_certs_"+storeName+storeLocation)
if err != nil {
return nil, err
}
defer os.RemoveAll(certStoreDir)

powershellScript := "$xs = New-Object System.Security.Cryptography.X509Certificates.X509Store('" + storeName + "', '" + storeLocation + "'); $xs.Open('OpenExistingOnly'); [io.file]::WriteAllBytes('" + filepath.Join(certStoreDir, "\\certs.sst") + "', $xs.Certificates.Export('SerializedStore')); $xs.Close();"

cmdOut, err := exec.Command("powershell", "-Command", powershellScript).Output()
if err != nil {
return nil, errors.New(string(cmdOut))
}

roots, err := certPoolFromSST(certStoreDir, "certs.sst")
if err != nil {
return nil, err
}

return roots, nil
}

const CRYPT_E_NOT_FOUND = 0x80092004
func downloadWUCerts(timeout time.Duration) (*CertPool, *CertPool, error) {
// directory for allowed certificates
wuAllowedDir, err := ioutil.TempDir("", "go_x509_certs_wuauth")
if err != nil {
return nil, nil, err
}
defer os.RemoveAll(wuAllowedDir)

// directory for disallowed certificates
wuDisallowedDir, err := ioutil.TempDir("", "go_x509_certs_wudisallowed")
if err != nil {
return nil, nil, err
}
defer os.RemoveAll(wuDisallowedDir)

// context to set timeout
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

// certutil -f -syncWithWU download certificates from Windows Update
cmdOut, err := exec.CommandContext(ctx, "certutil", "-f", "-syncWithWU", wuAllowedDir).Output()

// check if timed out
if ctx.Err() == context.DeadlineExceeded {
return nil, nil, errors.New("crypto/x509: could not download certificates, process timed out")
}

if err != nil {
return nil, nil, errors.New(string(cmdOut))
}

// move the disallowedcert.sst certificate store from wuAllowedDir to wuDisallowedDir
os.Rename(filepath.Join(wuAllowedDir, "disallowedcert.sst"), filepath.Join(wuDisallowedDir, "disallowedcert.sst"))

store, err := syscall.CertOpenSystemStore(0, syscall.StringToUTF16Ptr("ROOT"))
wuAllowedRoots, err := certPoolFromDir(wuAllowedDir)
if err != nil {
return nil, nil, err
}
wuDisallowedRoots, err := certPoolFromSST(wuDisallowedDir, "disallowedcert.sst")
if err != nil {
return nil, nil, err
}

return wuAllowedRoots, wuDisallowedRoots, nil
}

func certPoolFromDir(path string) (*CertPool, error) {
certs, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
defer syscall.CertCloseStore(store, 0)

roots := NewCertPool()
var cert *syscall.CertContext
for {
cert, err = syscall.CertEnumCertificatesInStore(store, cert)

for _, cert := range certs {
if !strings.HasSuffix(cert.Name(), ".crt") {
continue
}

certBytes, err := ioutil.ReadFile(filepath.Join(path, cert.Name()))
if err != nil {
if errno, ok := err.(syscall.Errno); ok {
if errno == CRYPT_E_NOT_FOUND {
break
}
}
return nil, err
}
if cert == nil {
break
}
// Copy the buf, since ParseCertificate does not create its own copy.
buf := (*[1 << 20]byte)(unsafe.Pointer(cert.EncodedCert))[:]
buf2 := make([]byte, cert.Length)
copy(buf2, buf)
if c, err := ParseCertificate(buf2); err == nil {
roots.AddCert(c)

c, err := ParseCertificate(certBytes)
if err != nil {
return nil, errors.New("crypto/x509: error while parsing " + cert.Name() + "\n" + err.Error())
}
roots.AddCert(c)
}

return roots, nil
}

func dumpSST(dir string, file string) error {
dumpCmd := "cd /d " + dir + " && certutil -f -split -dump " + file
cmdOut, err := exec.Command("cmd", "/K", dumpCmd).Output()
if err != nil {
return errors.New(string(cmdOut))
}
return nil
}

func certPoolFromSST(dir string, file string) (*CertPool, error) {
err := dumpSST(dir, file)
if err != nil {
return nil, err
}
certPool, err := certPoolFromDir(dir)
if err != nil {
return nil, err
}
return certPool, nil
}