Skip to content

Commit 858fa06

Browse files
CFusion-zzFiloSottile
authored andcommitted
crypto/x509: return additional chains from Verify on Windows
Previously windows only returned the certificate-chain with the highest quality. This change makes it so chains with a potentially lower quality originating from other root certificates are also returned by verify. Tests in verify_test flagged with systemLax are now allowed to pass if the system returns additional chains Fixes #40604 Change-Id: I66edc233219f581039d47a15f2200ff627154691 Reviewed-on: https://go-review.googlesource.com/c/go/+/257257 Reviewed-by: Tobias Klauser <[email protected]> Reviewed-by: Filippo Valsorda <[email protected]> Trust: Tobias Klauser <[email protected]> Run-TryBot: Tobias Klauser <[email protected]> TryBot-Result: Go Bot <[email protected]>
1 parent 8a368c6 commit 858fa06

File tree

2 files changed

+104
-71
lines changed

2 files changed

+104
-71
lines changed

src/crypto/x509/root_windows.go

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,44 @@ func init() {
155155
}
156156
}
157157

158+
func verifyChain(c *Certificate, chainCtx *syscall.CertChainContext, opts *VerifyOptions) (chain []*Certificate, err error) {
159+
err = checkChainTrustStatus(c, chainCtx)
160+
if err != nil {
161+
return nil, err
162+
}
163+
164+
if opts != nil && len(opts.DNSName) > 0 {
165+
err = checkChainSSLServerPolicy(c, chainCtx, opts)
166+
if err != nil {
167+
return nil, err
168+
}
169+
}
170+
171+
chain, err = extractSimpleChain(chainCtx.Chains, int(chainCtx.ChainCount))
172+
if err != nil {
173+
return nil, err
174+
}
175+
if len(chain) == 0 {
176+
return nil, errors.New("x509: internal error: system verifier returned an empty chain")
177+
}
178+
179+
// Mitigate CVE-2020-0601, where the Windows system verifier might be
180+
// tricked into using custom curve parameters for a trusted root, by
181+
// double-checking all ECDSA signatures. If the system was tricked into
182+
// using spoofed parameters, the signature will be invalid for the correct
183+
// ones we parsed. (We don't support custom curves ourselves.)
184+
for i, parent := range chain[1:] {
185+
if parent.PublicKeyAlgorithm != ECDSA {
186+
continue
187+
}
188+
if err := parent.CheckSignature(chain[i].SignatureAlgorithm,
189+
chain[i].RawTBSCertificate, chain[i].Signature); err != nil {
190+
return nil, err
191+
}
192+
}
193+
return chain, nil
194+
}
195+
158196
// systemVerify is like Verify, except that it uses CryptoAPI calls
159197
// to build certificate chains and verify them.
160198
func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate, err error) {
@@ -202,67 +240,41 @@ func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate
202240
verifyTime = &ft
203241
}
204242

205-
// CertGetCertificateChain will traverse Windows's root stores
206-
// in an attempt to build a verified certificate chain. Once
207-
// it has found a verified chain, it stops. MSDN docs on
208-
// CERT_CHAIN_CONTEXT:
209-
//
210-
// When a CERT_CHAIN_CONTEXT is built, the first simple chain
211-
// begins with an end certificate and ends with a self-signed
212-
// certificate. If that self-signed certificate is not a root
213-
// or otherwise trusted certificate, an attempt is made to
214-
// build a new chain. CTLs are used to create the new chain
215-
// beginning with the self-signed certificate from the original
216-
// chain as the end certificate of the new chain. This process
217-
// continues building additional simple chains until the first
218-
// self-signed certificate is a trusted certificate or until
219-
// an additional simple chain cannot be built.
220-
//
221-
// The result is that we'll only get a single trusted chain to
222-
// return to our caller.
223-
var chainCtx *syscall.CertChainContext
224-
err = syscall.CertGetCertificateChain(syscall.Handle(0), storeCtx, verifyTime, storeCtx.Store, para, 0, 0, &chainCtx)
225-
if err != nil {
226-
return nil, err
227-
}
228-
defer syscall.CertFreeCertificateChain(chainCtx)
243+
// The default is to return only the highest quality chain,
244+
// setting this flag will add additional lower quality contexts.
245+
// These are returned in the LowerQualityChains field.
246+
const CERT_CHAIN_RETURN_LOWER_QUALITY_CONTEXTS = 0x00000080
229247

230-
err = checkChainTrustStatus(c, chainCtx)
248+
// CertGetCertificateChain will traverse Windows's root stores in an attempt to build a verified certificate chain
249+
var topCtx *syscall.CertChainContext
250+
err = syscall.CertGetCertificateChain(syscall.Handle(0), storeCtx, verifyTime, storeCtx.Store, para, CERT_CHAIN_RETURN_LOWER_QUALITY_CONTEXTS, 0, &topCtx)
231251
if err != nil {
232252
return nil, err
233253
}
254+
defer syscall.CertFreeCertificateChain(topCtx)
234255

235-
if opts != nil && len(opts.DNSName) > 0 {
236-
err = checkChainSSLServerPolicy(c, chainCtx, opts)
237-
if err != nil {
238-
return nil, err
239-
}
256+
chain, topErr := verifyChain(c, topCtx, opts)
257+
if topErr == nil {
258+
chains = append(chains, chain)
240259
}
241260

242-
chain, err := extractSimpleChain(chainCtx.Chains, int(chainCtx.ChainCount))
243-
if err != nil {
244-
return nil, err
245-
}
246-
if len(chain) < 1 {
247-
return nil, errors.New("x509: internal error: system verifier returned an empty chain")
248-
}
261+
if lqCtxCount := topCtx.LowerQualityChainCount; lqCtxCount > 0 {
262+
lqCtxs := (*[1 << 20]*syscall.CertChainContext)(unsafe.Pointer(topCtx.LowerQualityChains))[:lqCtxCount:lqCtxCount]
249263

250-
// Mitigate CVE-2020-0601, where the Windows system verifier might be
251-
// tricked into using custom curve parameters for a trusted root, by
252-
// double-checking all ECDSA signatures. If the system was tricked into
253-
// using spoofed parameters, the signature will be invalid for the correct
254-
// ones we parsed. (We don't support custom curves ourselves.)
255-
for i, parent := range chain[1:] {
256-
if parent.PublicKeyAlgorithm != ECDSA {
257-
continue
258-
}
259-
if err := parent.CheckSignature(chain[i].SignatureAlgorithm,
260-
chain[i].RawTBSCertificate, chain[i].Signature); err != nil {
261-
return nil, err
264+
for _, ctx := range lqCtxs {
265+
chain, err := verifyChain(c, ctx, opts)
266+
if err == nil {
267+
chains = append(chains, chain)
268+
}
262269
}
263270
}
264271

265-
return [][]*Certificate{chain}, nil
272+
if len(chains) == 0 {
273+
// Return the error from the highest quality context.
274+
return nil, topErr
275+
}
276+
277+
return chains, nil
266278
}
267279

268280
func loadSystemRoots() (*CertPool, error) {

src/crypto/x509/verify_test.go

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -550,34 +550,55 @@ func testVerify(t *testing.T, test verifyTest, useSystemRoots bool) {
550550
}
551551
}
552552

553-
if len(chains) != len(test.expectedChains) {
554-
t.Errorf("wanted %d chains, got %d", len(test.expectedChains), len(chains))
553+
doesMatch := func(expectedChain []string, chain []*Certificate) bool {
554+
if len(chain) != len(expectedChain) {
555+
return false
556+
}
557+
558+
for k, cert := range chain {
559+
if !strings.Contains(nameToKey(&cert.Subject), expectedChain[k]) {
560+
return false
561+
}
562+
}
563+
return true
555564
}
556565

557-
// We check that each returned chain matches a chain from
558-
// expectedChains but an entry in expectedChains can't match
559-
// two chains.
560-
seenChains := make([]bool, len(chains))
561-
NextOutputChain:
562-
for _, chain := range chains {
563-
TryNextExpected:
564-
for j, expectedChain := range test.expectedChains {
565-
if seenChains[j] {
566-
continue
566+
// Every expected chain should match 1 returned chain
567+
for _, expectedChain := range test.expectedChains {
568+
nChainMatched := 0
569+
for _, chain := range chains {
570+
if doesMatch(expectedChain, chain) {
571+
nChainMatched++
572+
}
573+
}
574+
575+
if nChainMatched != 1 {
576+
t.Errorf("Got %v matches instead of %v for expected chain %v", nChainMatched, 1, expectedChain)
577+
for _, chain := range chains {
578+
if doesMatch(expectedChain, chain) {
579+
t.Errorf("\t matched %v", chainToDebugString(chain))
580+
}
567581
}
568-
if len(chain) != len(expectedChain) {
569-
continue
582+
}
583+
}
584+
585+
// Every returned chain should match 1 expected chain (or <2 if testing against the system)
586+
for _, chain := range chains {
587+
nMatched := 0
588+
for _, expectedChain := range test.expectedChains {
589+
if doesMatch(expectedChain, chain) {
590+
nMatched++
570591
}
571-
for k, cert := range chain {
572-
if !strings.Contains(nameToKey(&cert.Subject), expectedChain[k]) {
573-
continue TryNextExpected
592+
}
593+
// Allow additional unknown chains if systemLax is set
594+
if nMatched == 0 && test.systemLax == false || nMatched > 1 {
595+
t.Errorf("Got %v matches for chain %v", nMatched, chainToDebugString(chain))
596+
for _, expectedChain := range test.expectedChains {
597+
if doesMatch(expectedChain, chain) {
598+
t.Errorf("\t matched %v", expectedChain)
574599
}
575600
}
576-
// we matched
577-
seenChains[j] = true
578-
continue NextOutputChain
579601
}
580-
t.Errorf("no expected chain matched %s", chainToDebugString(chain))
581602
}
582603
}
583604

0 commit comments

Comments
 (0)