diff --git a/htmltest/check-link.go b/htmltest/check-link.go index 6458d3e..1ce03ca 100644 --- a/htmltest/check-link.go +++ b/htmltest/check-link.go @@ -1,12 +1,14 @@ package htmltest import ( + "crypto/x509" "fmt" "github.com/wjdp/htmltest/htmldoc" "github.com/wjdp/htmltest/issues" "github.com/wjdp/htmltest/output" "golang.org/x/net/html" "net/http" + "net/url" "os" "path" "strings" @@ -197,6 +199,18 @@ func (hT *HTMLTest) checkExternal(ref *htmldoc.Reference) { return } + if certErr, ok := err.(*url.Error).Err.(x509.UnknownAuthorityError); ok { + err = validateCertChain(certErr.Cert) + if err == nil { + hT.issueStore.AddIssue(issues.Issue{ + Level: issues.LevelWarning, + Reference: ref, + Message: "incomplete certificate chain", + }) + return + } + } + // Unhandled client error, return generic error hT.issueStore.AddIssue(issues.Issue{ Level: issues.LevelError, diff --git a/htmltest/check-link_test.go b/htmltest/check-link_test.go index 1978e8a..3e6de37 100644 --- a/htmltest/check-link_test.go +++ b/htmltest/check-link_test.go @@ -108,7 +108,7 @@ func TestAnchorExternalInsecureOptionIgnored(t *testing.T) { hT := tTestFileOpts("fixtures/links/issues/94.html", map[string]interface{}{ "EnforceHTTPS": true, - "IgnoreURLs": []interface{}{"plantuml.com", "plantuml.net", "forum.plantuml.net"}, + "IgnoreURLs": []interface{}{"plantuml.com", "plantuml.net", "forum.plantuml.net"}, }) tExpectIssueCount(t, hT, 0) } @@ -158,6 +158,14 @@ func TestAnchorExternalHTTPSInvalid(t *testing.T) { tExpectIssueCount(t, hT, 6) } +func TestAnchorExternalHTTPSMissingChain(t *testing.T) { + // should support https aia + // see issue #130 + hT := tTestFileOpts("fixtures/links/https-incomplete-chain.html", + map[string]interface{}{"VCREnable": true}) + tExpectIssue(t, hT, "incomplete certificate chain", 1) +} + func TestAnchorExternalHTTPSBadH2(t *testing.T) { // should connect to servers with bad http/2 support // See issue #49 diff --git a/htmltest/fixtures/links/https-incomplete-chain.html b/htmltest/fixtures/links/https-incomplete-chain.html new file mode 100644 index 0000000..ce1ac0c --- /dev/null +++ b/htmltest/fixtures/links/https-incomplete-chain.html @@ -0,0 +1,12 @@ + + +
+ +Blah blah blah. An HTTPS link!
+ ++ incomplete-chain +
+ + + diff --git a/htmltest/util.go b/htmltest/util.go index f29157d..ab5b553 100644 --- a/htmltest/util.go +++ b/htmltest/util.go @@ -1,7 +1,78 @@ package htmltest -import "net/http" +import ( + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" +) + +type CertChainErr struct { + cert *x509.Certificate + chain *x509.CertPool + hintErr error +} + +func (e CertChainErr) Error() string { + s := "x509: could not validate certificate chain" + if e.hintErr != nil { + s += fmt.Sprintf(" (possibly because of %q)", e.hintErr) + } + return s +} func statusCodeValid(code int) bool { return code == http.StatusPartialContent || code == http.StatusOK } + +func validateCertChain(cert *x509.Certificate) (err error) { + if cert.IssuingCertificateURL == nil { + return CertChainErr{cert: cert} + } + + intermediates := x509.NewCertPool() + //roots, err := x509.SystemCertPool() + if err != nil { + return CertChainErr{cert: cert} + } + var certsToFetch []string = cert.IssuingCertificateURL + + for i := 0; i < len(certsToFetch); i++ { + url := certsToFetch[i] + + resp, err := http.Get(url) + if err != nil { + return CertChainErr{cert: cert, chain: intermediates, hintErr: err} + } + + if resp.StatusCode != 200 { + return CertChainErr{cert: cert, chain: intermediates, hintErr: fmt.Errorf("could not fetch certificate at %s (status %d)", url, resp.StatusCode)} + } + + certBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return CertChainErr{cert: cert, chain: intermediates, hintErr: err} + } + + newCert, err := x509.ParseCertificate(certBytes) + if err != nil { + return CertChainErr{cert: cert, chain: intermediates, hintErr: err} + } + + if newCert.CheckSignatureFrom(cert) == nil { + // we have out root + break + } + + intermediates.AddCert(newCert) + + if newCert.IssuingCertificateURL != nil { + certsToFetch = append(certsToFetch, newCert.IssuingCertificateURL...) + } + } + + _, err = cert.Verify(x509.VerifyOptions{ + Intermediates: intermediates, + }) + return +}