January 28, 2020·5 min read·engineeringgosecurityx509

Go 1.13.7 repairs two certificate verification boundaries

Windows certificate verification and 32-bit ASN.1 parsing fail in different ways. Go 1.13.7 adds signature checks and rejects lengths that cannot fit in an int.

A certificate verifier depends on more than the signature algorithm it exposes to its caller. It must interpret untrusted bytes correctly, and it must preserve its security guarantees when it delegates work to an operating system. Go 1.13.7, released today, repairs failures at both boundaries. One permits certificate-chain spoofing through the Windows system verifier. The other lets malformed certificate input provoke a panic on 32-bit architectures.

The release history identifies two security fixes in crypto/x509. Their shared package name conceals different failure modes. The Windows fix checks a result returned by another verifier. The parsing fix prevents an unsigned length from becoming an invalid signed length before a slice operation. Both deserve attention from engineers who regard TLS as a solved dependency of an application.

I like Go because its ordinary interfaces leave relatively little room for private interpretation. Certificate verification has the same need for explicit boundaries, although its implementation must negotiate platform behavior and binary formats that application code rarely sees.

Checking the Windows result

Go can use Windows CryptoAPI to construct and verify certificate chains. The Go 1.13.7 Windows implementation creates an in-memory certificate store containing the leaf and supplied intermediates. It asks CertGetCertificateChain to build a chain, checks the returned trust status, and applies SSL server policy when the caller supplied a DNS name. The system call therefore participates directly in deciding which remote identities an application trusts.

The new mitigation addresses CVE-2020-0601. The system verifier can be deceived into accepting custom elliptic-curve parameters associated with a trusted root. A successful operating-system verification result is consequently insufficient. The Go code now examines the returned chain and independently checks signatures whose parent certificate has an ECDSA public key.

The implementation iterates over chain[1:]. Each parent verifies the signature on the certificate immediately below it, using that certificate's signature algorithm, signed certificate bytes, and signature. A failed check returns an error instead of a verified chain. This placement matters because Go still uses Windows to find and evaluate the chain. It adds a check after that process has returned its candidate result.

Go's own parser does not support custom curves. Its interpretation of the parent key therefore provides the check that the affected Windows path lacked. If Windows accepted spoofed parameters, the signature does not verify under the parameters Go parsed. This is a specific mitigation for the affected chain-validation behavior, not a replacement for Windows certificate policy.

That distinction determines the operational response. Updating a Go application changes the verification performed by that application. It cannot repair every program that uses the operating system's verifier. The operating-system security update remains necessary for protection beyond the rebuilt Go program. Treating either update as a substitute for the other would confuse the scope of the component being repaired.

Rejecting lengths before conversion

The second defect concerns ASN.1 parsing on 32-bit systems. The tagged cryptobyte parser shows why architecture enters the problem. Its internal readASN1 function stores the complete encoded element length in a uint32. The slice-reading operation accepts an int. Those types have different positive ranges on a 32-bit target.

ASN.1 lengths use either a short form or a long form. In the short form, the initial length byte carries the content length. In the long form, that byte says how many subsequent bytes encode the length. This implementation permits at most four length bytes. It also rejects encodings that use more bytes than necessary and checks for unsigned overflow when it adds the header size to the content size.

Those checks do not by themselves establish that the total fits in a signed integer. A value can fit in uint32 while converting to a negative int on a 32-bit system. The corrected parser tests the converted value before asking the underlying string reader to consume that many bytes. The relevant condition is short enough to inspect directly.

if int(length) < 0 || !s.ReadBytes((*[]byte)(out), int(length)) {
    return false
}

The first operand prevents the second from running when conversion produces a negative length. Returning false sends the malformed encoding through the parser's failure path. This is a useful example of a bounds check that belongs before the slice helper, because the helper's argument type already assumes a representable length.

The full function has another overflow check earlier in the long-form path. It rejects headerLen + len32 when that sum wraps below len32. These checks establish different properties. Unsigned addition needs the earlier check. The new one protects conversion into the signed type required by the read operation. Removing either argument from an explanation obscures why apparently careful length handling still needed a security patch.

Parsing happens before trust is established

An application cannot assume that certificate verification protects the parser from hostile certificates. Parsing supplies the structures that verification needs. Malformed data can therefore reach this code before the application has decided whether it trusts the remote party. That order explains why a parsing panic can matter even when certificate authentication is configured correctly.

The release announcement assigns the parsing issue CVE-2020-7919 and describes the affected TLS paths. An HTTPS server can crash an affected HTTP client. Servers accepting client certificates also encounter the input, although net/http recovers the relevant panic. The announcement also identifies a corrected version of the separately imported golang.org/x/crypto/cryptobyte package. Updating the toolchain and updating that module dependency address distinct copies of the parser.

This makes dependency ownership relevant to the repair. The tagged Go source contains a vendored parser used by the standard library. An application may also import the external package through its module dependencies. A toolchain update repairs the former when the program is rebuilt. It does not establish which external module revision the application selects.

I regard the conversion check as the more instructive part of this release for everyday code review. A parser can reject truncated input and nonminimal encodings while still accepting a length that its next function cannot represent. The useful question is whether every transition between numeric types preserves the bounds established so far.

The Windows mitigation suggests a related question about delegated results. A successful platform call does not remove the need to understand which guarantees the platform supplied. Here, Go retains the platform chain builder and adds an independent ECDSA check over the returned certificates. That division preserves Windows integration while rejecting the particular inconsistent result involved in the vulnerability.