CA Compliance Summary for {{date_start}} - {{date_end}}
72 |
73 |
74 |
Summary
75 |
76 |
77 |
{{recent_bugs|length}} new bug reports were filed, of which
78 | {{recent_bugs|selectattr('is_open', 'equalto', false) | list | length}}
79 | are resolved.
80 |
{{updated_bugs|selectattr('is_open', 'equalto', false) | list | length}} old bug reports were closed.
81 |
{{updated_bugs|selectattr('is_open') | list | length}} old bug reports were updated.
82 |
There are {{unresolved_bugs|length}} total unresolved bug reports for all time.
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
New bugs
91 | Bugs created in this time period.
92 | {{ table_header() }}
93 | {% for bug in recent_bugs %}
94 | {{ print_bug(bug) }}
95 | {% endfor %}
96 | {{ table_footer() }}
97 |
98 |
99 |
100 |
Resolved bugs
101 | Bugs resolved in this time period.
102 | {{ table_header() }}
103 | {% for bug in updated_bugs|selectattr('is_open', 'equalto', false) | list + recent_bugs|selectattr('is_open', 'equalto', false) | list %}
104 | {{ print_bug(bug) }}
105 | {% endfor %}
106 | {{ table_footer() }}
107 |
108 |
109 |
110 |
111 |
112 |
Updated bugs
113 | Bugs updated but not resolved in this time period.
114 | {{ table_header() }}
115 | {% for bug in updated_bugs %}
116 | {{ print_bug(bug) }}
117 | {% endfor %}
118 | {{ table_footer() }}
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/capi/Dockerfile:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | FROM golang:latest AS buildStage
6 |
7 | WORKDIR /opt
8 | COPY . .
9 | RUN apt update
10 | RUN apt install -y libnss3-tools libssl-dev ruby-dev zlib1g-dev
11 | RUN ln -s /usr/lib/x86_64-linux-gnu/libcrypto.a /usr/lib64/libcrypto.a
12 | RUN go build capi.go
13 |
14 | FROM debian:latest
15 |
16 | RUN apt update
17 | RUN apt install -y gcc g++ git libffi-dev libnss3-tools make ruby-dev ruby-sdoc
18 |
19 | RUN gem install public_suffix simpleidn
20 | RUN cd /tmp && git clone https://github.com/certlint/certlint.git && \
21 | cd certlint/ext && \
22 | ruby extconf.rb && \
23 | make
24 |
25 | RUN apt purge -y gcc g++ git libffi-dev make ruby-dev ruby-sdoc
26 |
27 | COPY --from=buildStage /opt/ /tmp/
28 | RUN mv /tmp/capi /opt/
29 | RUN mv /tmp/certlint /opt/
30 | RUN rm -rf /tmp/*
31 |
32 | CMD ["/opt/capi"]
33 |
--------------------------------------------------------------------------------
/capi/Makefile:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | clean:
6 | -docker stop capi
7 | -docker rm capi
8 | -docker rmi capi
9 | -docker image prune -f
10 | -docker image prune -f --filter label=stage=intermediate
11 |
12 | build:
13 | docker build --rm -t capi:latest .
14 | docker image prune -f
15 | docker image prune -f --filter label=stage=intermediate
16 |
17 | run:
18 | ./run.sh
--------------------------------------------------------------------------------
/capi/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: custom
2 | env: flex
3 | service: default
4 |
--------------------------------------------------------------------------------
/capi/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mozilla/CCADB-Tools/capi
2 |
3 | require (
4 | github.com/natefinch/lumberjack v2.0.0+incompatible
5 | github.com/pkg/errors v0.9.1
6 | github.com/sirupsen/logrus v1.9.3
7 | github.com/throttled/throttled v2.2.5+incompatible
8 | golang.org/x/crypto v0.17.0
9 | )
10 |
11 | require (
12 | github.com/BurntSushi/toml v1.2.1 // indirect
13 | github.com/crtsh/go-x509lint v1.0.1 // indirect
14 | github.com/gomodule/redigo v1.8.9 // indirect
15 | github.com/hashicorp/golang-lru v1.0.2 // indirect
16 | golang.org/x/sys v0.15.0 // indirect
17 | golang.org/x/term v0.15.0 // indirect
18 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
19 | gopkg.in/yaml.v2 v2.4.0 // indirect
20 | )
21 |
22 | go 1.21
23 |
--------------------------------------------------------------------------------
/capi/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
2 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
3 | github.com/crtsh/go-x509lint v1.0.1 h1:mAHYab9s5Sz0QEbR6ihKV3FdpDoYVRk+dwlag0KoZLc=
4 | github.com/crtsh/go-x509lint v1.0.1/go.mod h1:cFOsh7E+UiN7Fkvh6ydSm3DSHj93EMEN5fkzw9cLR1E=
5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
9 | github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
10 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
11 | github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
12 | github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
13 | github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
14 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
15 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
18 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
19 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
21 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
22 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
23 | github.com/throttled/throttled v2.2.5+incompatible h1:65UB52X0qNTYiT0Sohp8qLYVFwZQPDw85uSa65OljjQ=
24 | github.com/throttled/throttled v2.2.5+incompatible/go.mod h1:0BjlrEGQmvxps+HuXLsyRdqpSRvJpq0PNIsOtqP9Nos=
25 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
26 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
27 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
28 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
29 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
30 | golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
31 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
33 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
34 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
35 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
36 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
37 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
38 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
39 |
--------------------------------------------------------------------------------
/capi/lib/ccadb/ccabd_test.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package ccadb
6 |
7 | import "testing"
8 |
9 | func TestGetHeader(t *testing.T) {
10 | report, err := NewReport()
11 | if err != nil {
12 | t.Fatal(err)
13 | }
14 | for _, record := range report.Records {
15 | t.Log(record.TestWebsiteRevoked())
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/capi/lib/ccadb/ccadb.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package ccadb
6 |
7 | import (
8 | "crypto/tls"
9 | "crypto/x509"
10 | "encoding/csv"
11 | "encoding/pem"
12 | "fmt"
13 | "github.com/mozilla/CCADB-Tools/capi/lib/certificateUtils"
14 | "github.com/pkg/errors"
15 | "github.com/sirupsen/logrus"
16 | "net/http"
17 | )
18 |
19 | const ReportURL = "https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV"
20 |
21 | var headers = []string{"Owner", "Certificate Issuer Organization", "Certificate Issuer Organizational Unit", "Common Name or Certificate Name", "Certificate Serial Number", "SHA-256 Fingerprint", "Subject + SPKI SHA256", "Valid From [GMT]", "Valid To [GMT]", "Public Key Algorithm", "Signature Hash Algorithm", "Trust Bits", "Distrust for TLS After Date", "Distrust for S/MIME After Date", "EV Policy OID(s)", "Approval Bug", "NSS Release When First Included", "Firefox Release When First Included", "Test Website - Valid", "Test Website - Expired", "Test Website - Revoked", "Mozilla Applied Constraints", "Company Website", "Geographic Focus", "Certificate Policy (CP)", "Certification Practice Statement (CPS)", "Standard Audit", "BR Audit", "EV Audit", "Auditor", "Standard Audit Type", "Standard Audit Statement Dt", "PEM Info"}
22 |
23 | const (
24 | Owner = iota
25 | CertificateIssuerOrganization
26 | CertificateIssuerOrganizationalUnit
27 | CommonNameorCertificateName
28 | CertificateSerialNumber
29 | SHA256Fingerprint
30 | SubjectSPKISHA256
31 | ValidFromGMT
32 | ValidToGMT
33 | PublicKeyAlgorithm
34 | SignatureHashAlgorithm
35 | TrustBits
36 | DistrustForTLSAfterDate
37 | DistrustForSMIMEAfterDate
38 | EVPolicyOIDs
39 | ApprovalBug
40 | NSSReleaseWhenFirstIncluded
41 | FirefoxReleaseWhenFirstIncluded
42 | TestWebsiteValid
43 | TestWebsiteExpired
44 | TestWebsiteRevoked
45 | MozillaAppliedConstraints
46 | CompanyWebsite
47 | GeographicFocus
48 | CertificatePolicyCP
49 | CertificationPracticeStatementCPS
50 | StandardAudit
51 | BRAudit
52 | EVAudit
53 | Auditor
54 | StandardAuditType
55 | StandardAuditStatementDt
56 | PEMInfo
57 | )
58 |
59 | type Report struct {
60 | Records []Record
61 | }
62 |
63 | type Record []string
64 |
65 | func (r Record) Root() *x509.Certificate {
66 | block, _ := pem.Decode([]byte(r.RootPEM()))
67 | cert, err := x509.ParseCertificate(block.Bytes)
68 | if err != nil {
69 | logrus.Panic(err)
70 | }
71 | return cert
72 | }
73 |
74 | func (r Record) RootPEM() string {
75 | pem, err := certificateUtils.NormalizePEM([]byte(r[PEMInfo]))
76 | if err != nil {
77 | logrus.Panic(err)
78 | }
79 | return string(pem)
80 | }
81 |
82 | func (r Record) TestWebsiteValid() string {
83 | return r[TestWebsiteValid]
84 | }
85 |
86 | func (r Record) TestWebsiteExpired() string {
87 | return r[TestWebsiteExpired]
88 | }
89 |
90 | func (r Record) TestWebsiteRevoked() string {
91 | return r[TestWebsiteRevoked]
92 | }
93 |
94 | func (r Record) Fingerprint() string {
95 | return r[SHA256Fingerprint]
96 | }
97 |
98 | func NewReport() (Report, error) {
99 | return NewReportFrom(ReportURL)
100 | }
101 |
102 | func NewReportFrom(url string) (report Report, err error) {
103 | transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
104 | client := &http.Client{Transport: transport}
105 | req, err := http.NewRequest("GET", url, nil)
106 | if err != nil {
107 | return
108 | }
109 | req.Header.Add("X-TOOL", "github.com/mozilla/CCADB-Tools/tree/master/capi")
110 | resp, err := client.Do(req)
111 | if err != nil {
112 | return
113 | }
114 | defer func() {
115 | if err := resp.Body.Close(); err != nil {
116 | logrus.Warn(err)
117 | }
118 | }()
119 | c := csv.NewReader(resp.Body)
120 | all, err := c.ReadAll()
121 | if err != nil {
122 | return
123 | }
124 | err = assertHeader(all[0])
125 | if err != nil {
126 | return
127 | }
128 | report.Records = make([]Record, len(all[1:]))
129 | for i, r := range all[1:] {
130 | report.Records[i] = r
131 | }
132 | return
133 | }
134 |
135 | func assertHeader(header []string) error {
136 | for i, field := range header {
137 | if field != headers[i] {
138 | return errors.New(fmt.Sprintf("Unexpected CSV header. Wanted %s, got %s", headers, header))
139 | }
140 | }
141 | return nil
142 | }
143 |
--------------------------------------------------------------------------------
/capi/lib/expiration/expiration.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package expiration
6 |
7 | import (
8 | "crypto/x509"
9 | "github.com/mozilla/CCADB-Tools/capi/lib/expiration/certutil"
10 | "github.com/pkg/errors"
11 | )
12 |
13 | type Status string
14 |
15 | const (
16 | Valid Status = "valid"
17 | Expired = "expired"
18 | IssuerUnknown = "issuerUnknown"
19 | UnexpectedResponse = "unexpectedResponse"
20 | )
21 |
22 | func toStatus(nssResponse string) (Status, bool) {
23 | status, ok := map[string]Status{
24 | certutil.VALID: Valid,
25 | certutil.EXPIRED: Expired,
26 | certutil.ISSUER_UNKOWN: IssuerUnknown,
27 | }[nssResponse]
28 | return status, ok
29 | }
30 |
31 | type ExpirationStatus struct {
32 | Raw string `json:"-"`
33 | Error string
34 | Status Status
35 | }
36 |
37 | func VerifyChain(chain []*x509.Certificate) ([]ExpirationStatus, error) {
38 | statuses := make([]ExpirationStatus, len(chain))
39 | c, err := certutil.NewCertutil()
40 | if err != nil {
41 | return statuses, errors.Wrap(err, "failed to initialize a new NSS certificate database")
42 | }
43 | defer c.Delete()
44 | for _, cert := range chain {
45 | out, err := c.Install(cert)
46 | o := string(out)
47 | if err != nil {
48 | return statuses, errors.Wrapf(err, "failed to install certificate, %v", o)
49 | }
50 | }
51 | for i, cert := range chain {
52 | statuses[i] = queryExpiration(cert, c)
53 | }
54 | return statuses, nil
55 | }
56 |
57 | func queryExpiration(certificate *x509.Certificate, c certutil.Certutil) (exps ExpirationStatus) {
58 | // @TODO try to figure certutil's error codes. It uses non zero codes when the answer is
59 | // anything other than just "valid", so it's not a reliable way to know whether or not
60 | // the tool was fundamentally used wrong or if the cert is just expired or what.
61 | resp, _ := c.Verify(certificate)
62 | response := string(resp)
63 | exps.Raw = response
64 | switch status, ok := toStatus(response); ok {
65 | case true:
66 | exps.Status = status
67 | case false:
68 | exps.Error = response
69 | exps.Status = UnexpectedResponse
70 | }
71 | return
72 | }
73 |
--------------------------------------------------------------------------------
/capi/lib/lint/certlint/certlint.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package certlint
6 |
7 | import (
8 | "bytes"
9 | "crypto/x509"
10 | "io/ioutil"
11 | "log"
12 | "os"
13 | "os/exec"
14 | )
15 |
16 | const LIB = `/opt/certlint/lib/:/opt/certlint/ext`
17 | const CERTLINT = `/opt/certlint/bin/certlint`
18 | const CABLINT = `/opt/certlint/bin/cablint`
19 |
20 | type Certlint struct {
21 | Certlint certlint
22 | Cablint certlint
23 | }
24 |
25 | func LintCerts(certificates []*x509.Certificate) ([]Certlint, error) {
26 | lints := make([]Certlint, len(certificates))
27 | for i, cert := range certificates {
28 | l, err := Lint(cert)
29 | if err != nil {
30 | return lints, err
31 | }
32 | lints[i] = l
33 | }
34 | return lints, nil
35 | }
36 |
37 | func Lint(certificate *x509.Certificate) (Certlint, error) {
38 | var result Certlint
39 | f, err := ioutil.TempFile("", "certlint")
40 | if err != nil {
41 | return result, err
42 | }
43 | defer os.Remove(f.Name())
44 | defer f.Close()
45 | err = ioutil.WriteFile(f.Name(), certificate.Raw, 066)
46 | if err != nil {
47 | return result, err
48 | }
49 | result.Certlint = lint(f.Name(), CERTLINT)
50 | result.Cablint = lint(f.Name(), CABLINT)
51 | return result, nil
52 | }
53 |
54 | type certlint struct {
55 | Bug []string
56 | Info []string
57 | Notices []string
58 | Warnings []string
59 | Errors []string
60 | Fatal []string
61 | CmdError *string
62 | }
63 |
64 | func NewCertlint() certlint {
65 | return certlint{
66 | Bug: make([]string, 0),
67 | Info: make([]string, 0),
68 | Notices: make([]string, 0),
69 | Warnings: make([]string, 0),
70 | Errors: make([]string, 0),
71 | Fatal: make([]string, 0),
72 | CmdError: nil,
73 | }
74 | }
75 |
76 | func lint(fname, tool string) certlint {
77 | result := NewCertlint()
78 | cmd := exec.Command("ruby", "-I", LIB, tool, fname)
79 | stdout := bytes.NewBuffer([]byte{})
80 | stderr := bytes.NewBuffer([]byte{})
81 | cmd.Stdout = stdout
82 | cmd.Stderr = stderr
83 | err := cmd.Run()
84 | if err != nil {
85 | errStr := err.Error()
86 | result.CmdError = &errStr
87 | return result
88 | }
89 | output, err := ioutil.ReadAll(stdout)
90 | if err != nil {
91 | errStr := err.Error()
92 | result.CmdError = &errStr
93 | return result
94 | }
95 | errors, err := ioutil.ReadAll(stderr)
96 | if err != nil {
97 | errStr := err.Error()
98 | result.CmdError = &errStr
99 | return result
100 | }
101 | if string(errors) != "" {
102 | errStr := string(errors)
103 | result.CmdError = &errStr
104 | return result
105 | }
106 | parseOutput(output, &result)
107 | return result
108 | }
109 |
110 | //
111 | // B: Bug. Your certificate has a feature not handled by certlint.
112 | // I: Information. These are purely informational; no action is needed.
113 | // N: Notice. These are items known to cause issues with one or more implementations of certificate processing but are not errors according to the standard.
114 | // W: Warning. These are issues where a standard recommends differently but the standard uses terms such as "SHOULD" or "MAY".
115 | // E: Error. These are issues where the certificate is not compliant with the standard.
116 | // F: Fatal Error. These errors are fatal to the checks and prevent most further checks from being executed. These are extremely bad errors.
117 | func parseOutput(output []byte, result *certlint) {
118 | for _, line := range bytes.Split(output, []byte{'\n'}) {
119 | if bytes.HasPrefix(line, []byte("B: ")) {
120 | result.Bug = append(result.Bug, string(line[3:]))
121 | } else if bytes.HasPrefix(line, []byte("I: ")) {
122 | result.Info = append(result.Info, string(line[3:]))
123 | } else if bytes.HasPrefix(line, []byte("N: ")) {
124 | result.Notices = append(result.Notices, string(line[3:]))
125 | } else if bytes.HasPrefix(line, []byte("W: ")) {
126 | result.Warnings = append(result.Warnings, string(line[3:]))
127 | } else if bytes.HasPrefix(line, []byte("E: ")) {
128 | result.Errors = append(result.Errors, string(line[3:]))
129 | } else if bytes.HasPrefix(line, []byte("F: ")) {
130 | result.Fatal = append(result.Fatal, string(line[3:]))
131 | } else if bytes.Equal(line, []byte("")) {
132 | //
133 | } else {
134 | log.Printf(`unexpected certlint output: "%s"`, string(output))
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/capi/lib/lint/x509lint/x509lint.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package x509lint
6 |
7 | import (
8 | "crypto/x509"
9 | go_x509lint "github.com/crtsh/go-x509lint"
10 | "log"
11 | "reflect"
12 | "strings"
13 | "sync"
14 | )
15 |
16 | type X509Lint struct {
17 | Errors []string
18 | Warnings []string
19 | Info []string
20 | CmdError *string
21 | }
22 |
23 | type certType int
24 |
25 | const (
26 | subscriber certType = iota
27 | intermediate
28 | ca
29 | )
30 |
31 | var certTypeToStr = map[certType]string{
32 | subscriber: "subscriber",
33 | intermediate: "intermediate",
34 | ca: "ca",
35 | }
36 |
37 | func LintChain(certificates []*x509.Certificate) ([]X509Lint, error) {
38 | results := make([]X509Lint, len(certificates))
39 | for i, cert := range certificates {
40 | var ct certType
41 | switch {
42 | case i == 0:
43 | ct = subscriber
44 | case reflect.DeepEqual(cert.Subject, cert.Issuer):
45 | ct = ca
46 | default:
47 | ct = intermediate
48 | }
49 | results[i] = Lint(cert, ct)
50 | }
51 | return results, nil
52 | }
53 |
54 | // go_x509lint.Init() and go_x509lint.Finish() both mutate global state.
55 | //
56 | // Concurrent read/writes MAY be safe for some roundabout reason that I cannot see,
57 | // but given that there are no docs on the matter it seems prudent to simply
58 | // lock the library for a given certificate check.
59 | var x509LintLock = sync.Mutex{}
60 |
61 | func Lint(certificate *x509.Certificate, ctype certType) X509Lint {
62 | x509LintLock.Lock()
63 | defer x509LintLock.Unlock()
64 | go_x509lint.Init()
65 | defer go_x509lint.Finish()
66 | got := go_x509lint.Check(certificate.Raw, int(ctype))
67 | return parseOutput(got)
68 | }
69 |
70 | func NewX509Lint() X509Lint {
71 | return X509Lint{
72 | Errors: make([]string, 0),
73 | Warnings: make([]string, 0),
74 | Info: make([]string, 0),
75 | }
76 | }
77 |
78 | func parseOutput(output string) X509Lint {
79 | result := NewX509Lint()
80 | for _, line := range strings.Split(output, "\n") {
81 | if len(line) == 0 {
82 | continue
83 | }
84 | if strings.HasPrefix(line, "E: ") {
85 | if strings.Contains(line, "Fails decoding the characterset") {
86 | // @TODO We currently have no notion as why this happens, so we are ignoring it for now.
87 | continue
88 | }
89 | result.Errors = append(result.Errors, line[3:])
90 | } else if strings.HasPrefix(line, "W: ") {
91 | result.Warnings = append(result.Warnings, line[3:])
92 | } else if strings.HasPrefix(line, "I: ") {
93 | result.Info = append(result.Info, line[3:])
94 | } else {
95 | log.Printf(`unexpected x509Lint output: "%s"`, line)
96 | }
97 | }
98 | return result
99 | }
100 |
--------------------------------------------------------------------------------
/capi/lib/lint/x509lint/x509lint_test.go:
--------------------------------------------------------------------------------
1 | package x509lint
2 |
3 | import (
4 | "crypto/x509"
5 | "encoding/pem"
6 | "reflect"
7 | "testing"
8 | )
9 |
10 | // SHA-256: 92a37fbd5e21a53a95c716e1144f442f582b94d0fafc673eb6717a4eb51a88a7
11 | var GITHUB_LEAF = []byte(`
12 | -----BEGIN CERTIFICATE-----
13 | MIIFajCCBPGgAwIBAgIQDNCovsYyz+ZF7KCpsIT7HDAKBggqhkjOPQQDAzBWMQsw
14 | CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTAwLgYDVQQDEydEaWdp
15 | Q2VydCBUTFMgSHlicmlkIEVDQyBTSEEzODQgMjAyMCBDQTEwHhcNMjMwMjE0MDAw
16 | MDAwWhcNMjQwMzE0MjM1OTU5WjBmMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2Fs
17 | aWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHVi
18 | LCBJbmMuMRMwEQYDVQQDEwpnaXRodWIuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D
19 | AQcDQgAEo6QDRgPfRlFWy8k5qyLN52xZlnqToPu5QByQMog2xgl2nFD1Vfd2Xmgg
20 | nO4i7YMMFTAQQUReMqyQodWq8uVDs6OCA48wggOLMB8GA1UdIwQYMBaAFAq8CCkX
21 | jKU5bXoOzjPHLrPt+8N6MB0GA1UdDgQWBBTHByd4hfKdM8lMXlZ9XNaOcmfr3jAl
22 | BgNVHREEHjAcggpnaXRodWIuY29tgg53d3cuZ2l0aHViLmNvbTAOBgNVHQ8BAf8E
23 | BAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMIGbBgNVHR8EgZMw
24 | gZAwRqBEoEKGQGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRMU0h5
25 | YnJpZEVDQ1NIQTM4NDIwMjBDQTEtMS5jcmwwRqBEoEKGQGh0dHA6Ly9jcmw0LmRp
26 | Z2ljZXJ0LmNvbS9EaWdpQ2VydFRMU0h5YnJpZEVDQ1NIQTM4NDIwMjBDQTEtMS5j
27 | cmwwPgYDVR0gBDcwNTAzBgZngQwBAgIwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3
28 | dy5kaWdpY2VydC5jb20vQ1BTMIGFBggrBgEFBQcBAQR5MHcwJAYIKwYBBQUHMAGG
29 | GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0cDovL2Nh
30 | Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VExTSHlicmlkRUNDU0hBMzg0MjAy
31 | MENBMS0xLmNydDAJBgNVHRMEAjAAMIIBgAYKKwYBBAHWeQIEAgSCAXAEggFsAWoA
32 | dwDuzdBk1dsazsVct520zROiModGfLzs3sNRSFlGcR+1mwAAAYZQ3Rv6AAAEAwBI
33 | MEYCIQDkFq7T4iy6gp+pefJLxpRS7U3gh8xQymmxtI8FdzqU6wIhALWfw/nLD63Q
34 | YPIwG3EFchINvWUfB6mcU0t2lRIEpr8uAHYASLDja9qmRzQP5WoC+p0w6xxSActW
35 | 3SyB2bu/qznYhHMAAAGGUN0cKwAABAMARzBFAiAePGAyfiBR9dbhr31N9ZfESC5G
36 | V2uGBTcyTyUENrH3twIhAPwJfsB8A4MmNr2nW+sdE1n2YiCObW+3DTHr2/UR7lvU
37 | AHcAO1N3dT4tuYBOizBbBv5AO2fYT8P0x70ADS1yb+H61BcAAAGGUN0cOgAABAMA
38 | SDBGAiEAzOBr9OZ0+6OSZyFTiywN64PysN0FLeLRyL5jmEsYrDYCIQDu0jtgWiMI
39 | KU6CM0dKcqUWLkaFE23c2iWAhYAHqrFRRzAKBggqhkjOPQQDAwNnADBkAjAE3A3U
40 | 3jSZCpwfqOHBdlxi9ASgKTU+wg0qw3FqtfQ31OwLYFdxh0MlNk/HwkjRSWgCMFbQ
41 | vMkXEPvNvv4t30K6xtpG26qmZ+6OiISBIIXMljWnsiYR1gyZnTzIg3AQSw4Vmw==
42 | -----END CERTIFICATE-----
43 | `)
44 |
45 | func TestInfoLevelSubscriber(t *testing.T) {
46 | b, _ := pem.Decode(GITHUB_LEAF)
47 | cert, err := x509.ParseCertificate(b.Bytes)
48 | if err != nil {
49 | t.Fatal(err)
50 | }
51 | got := Lint(cert, subscriber)
52 | want := NewX509Lint()
53 | want.Info = []string{
54 | "Subject has a deprecated CommonName",
55 | "Checking as leaf certificate",
56 | }
57 | if !reflect.DeepEqual(want, got) {
58 | t.Errorf("expected %v, got %v", want, got)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/capi/lib/model/ccadb_record.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package model
6 |
7 | import (
8 | "crypto/x509"
9 | "encoding/json"
10 | "encoding/pem"
11 | "github.com/mozilla/CCADB-Tools/capi/lib/certificateUtils"
12 | )
13 |
14 | type CCADBRecords struct {
15 | CertificateDetails []CCADBRecord
16 | }
17 |
18 | type CCADBRecord struct {
19 | RecordID string
20 | Name string
21 | PEM *x509.Certificate
22 | TestWebsiteValid string
23 | TestWebsiteRevoked string
24 | TestWebsiteExpired string
25 | }
26 |
27 | type intermediateRepresentation struct {
28 | RecordID string
29 | Name string
30 | PEM string
31 | TestWebsiteValid string
32 | TestWebsiteRevoked string
33 | TestWebsiteExpired string
34 | }
35 |
36 | func (c *CCADBRecord) UnmarshalJSON(data []byte) (err error) {
37 | var i intermediateRepresentation
38 | err = json.Unmarshal(data, &i)
39 | if err != nil {
40 | return
41 | }
42 | p, err := certificateUtils.NormalizePEM([]byte(i.PEM))
43 | if err != nil {
44 | return
45 | }
46 | block, _ := pem.Decode(p)
47 | root, err := x509.ParseCertificate(block.Bytes)
48 | if err != nil {
49 | return
50 | }
51 | c.RecordID = i.RecordID
52 | c.Name = i.Name
53 | c.PEM = root
54 | c.TestWebsiteValid = i.TestWebsiteValid
55 | c.TestWebsiteRevoked = i.TestWebsiteRevoked
56 | c.TestWebsiteExpired = i.TestWebsiteExpired
57 | return
58 | }
59 |
--------------------------------------------------------------------------------
/capi/lib/model/lint_result.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "crypto/x509"
5 | "github.com/mozilla/CCADB-Tools/capi/lib/certificateUtils"
6 | "github.com/mozilla/CCADB-Tools/capi/lib/lint/certlint"
7 | "github.com/mozilla/CCADB-Tools/capi/lib/lint/x509lint"
8 | )
9 |
10 | type ChainLintResult struct {
11 | Subject string
12 | Leaf CertificateLintResult
13 | Intermediates []CertificateLintResult
14 | Opinion Opinion
15 | Error string
16 | }
17 |
18 | func NewChainLintResult(subject string) ChainLintResult {
19 | return ChainLintResult{
20 | Subject: subject,
21 | }
22 | }
23 |
24 | func (c *ChainLintResult) Finalize(leaf CertificateLintResult, intermediates []CertificateLintResult) {
25 | c.Leaf = leaf
26 | c.Intermediates = intermediates
27 | c.Opinion = NewOpinion()
28 | c.Opinion.Result = PASS
29 | interpretLint(c.Leaf, &c.Opinion)
30 | for _, intermediate := range intermediates {
31 | interpretLint(intermediate, &c.Opinion)
32 | }
33 | }
34 |
35 | func interpretLint(c CertificateLintResult, opinion *Opinion) {
36 | for _, err := range c.X509Lint.Errors {
37 | opinion.Result = FAIL
38 | opinion.Errors = append(opinion.Errors, Concern{
39 | Raw: err,
40 | Interpretation: "",
41 | Advise: "",
42 | })
43 | }
44 | if err := c.X509Lint.CmdError; err != nil {
45 | opinion.Result = FAIL
46 | opinion.Errors = append(opinion.Errors, Concern{
47 | Raw: *err,
48 | Interpretation: "",
49 | Advise: "",
50 | })
51 | }
52 | for _, err := range c.Certlint.Certlint.Errors {
53 | opinion.Result = FAIL
54 | opinion.Errors = append(opinion.Errors, Concern{
55 | Raw: err,
56 | Interpretation: "",
57 | Advise: "",
58 | })
59 | }
60 | if err := c.Certlint.Certlint.CmdError; err != nil {
61 | opinion.Result = FAIL
62 | opinion.Errors = append(opinion.Errors, Concern{
63 | Raw: *err,
64 | Interpretation: "",
65 | Advise: "",
66 | })
67 | }
68 | for _, err := range c.Certlint.Cablint.Errors {
69 | opinion.Result = FAIL
70 | opinion.Errors = append(opinion.Errors, Concern{
71 | Raw: err,
72 | Interpretation: "",
73 | Advise: "",
74 | })
75 | }
76 | for _, err := range c.Certlint.Cablint.Fatal {
77 | opinion.Result = FAIL
78 | opinion.Errors = append(opinion.Errors, Concern{
79 | Raw: err,
80 | Interpretation: "",
81 | Advise: "",
82 | })
83 | }
84 | for _, err := range c.Certlint.Cablint.Bug {
85 | opinion.Result = FAIL
86 | opinion.Errors = append(opinion.Errors, Concern{
87 | Raw: err,
88 | Interpretation: "",
89 | Advise: "",
90 | })
91 | }
92 | if err := c.Certlint.Cablint.CmdError; err != nil {
93 | opinion.Result = FAIL
94 | opinion.Errors = append(opinion.Errors, Concern{
95 | Raw: *err,
96 | Interpretation: "",
97 | Advise: "",
98 | })
99 | }
100 | }
101 |
102 | type CertificateLintResult struct {
103 | X509Lint x509lint.X509Lint
104 | Certlint certlint.Certlint
105 | CrtSh string
106 | }
107 |
108 | func NewCertificateLintResult(original *x509.Certificate, X509 x509lint.X509Lint, clint certlint.Certlint) CertificateLintResult {
109 | return CertificateLintResult{
110 | X509Lint: X509,
111 | Certlint: clint,
112 | CrtSh: "https://crt.sh/?q=" + certificateUtils.FingerprintOf(original),
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/capi/lib/model/result.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package model
6 |
7 | import (
8 | "crypto/x509"
9 | "encoding/json"
10 | "github.com/mozilla/CCADB-Tools/capi/lib/certificateUtils"
11 | "github.com/mozilla/CCADB-Tools/capi/lib/expiration"
12 | "github.com/mozilla/CCADB-Tools/capi/lib/revocation/crl"
13 | "github.com/mozilla/CCADB-Tools/capi/lib/revocation/ocsp"
14 | )
15 |
16 | type TestWebsiteResult struct {
17 | SubjectURL string
18 | RecordID string `json:"RecordID,omitempty"`
19 | Expectation string
20 | Chain ChainResult
21 | Opinion Opinion
22 | Error string
23 | }
24 |
25 | func NewTestWebsiteResult(subject, expectation string) TestWebsiteResult {
26 | return TestWebsiteResult{
27 | SubjectURL: subject,
28 | Expectation: expectation,
29 | Opinion: NewOpinion(),
30 | }
31 | }
32 |
33 | func (t TestWebsiteResult) SetRecordID(id string) TestWebsiteResult {
34 | t.RecordID = id
35 | return t
36 | }
37 |
38 | type ChainResult struct {
39 | Leaf CertificateResult
40 | Intermediates []CertificateResult
41 | Root CertificateResult
42 | }
43 |
44 | type OpinionResult = bool
45 |
46 | const (
47 | PASS OpinionResult = true
48 | FAIL OpinionResult = false
49 | )
50 |
51 | type Opinion struct {
52 | Result OpinionResult // Whether this opinion thinks the run is bad in some way.
53 | Errors []Concern
54 | }
55 |
56 | func (o Opinion) MarshalJSON() ([]byte, error) {
57 | var result string
58 | switch o.Result {
59 | case PASS:
60 | result = "PASS"
61 | case FAIL:
62 | result = "FAIL"
63 | }
64 | return json.Marshal(struct {
65 | Result string
66 | Errors []Concern
67 | }{
68 | Result: result,
69 | Errors: o.Errors,
70 | })
71 | }
72 |
73 | func NewOpinion() Opinion {
74 | return Opinion{
75 | Result: FAIL,
76 | Errors: make([]Concern, 0),
77 | }
78 | }
79 |
80 | func (o *Opinion) Append(other Opinion) {
81 | o.Errors = append(o.Errors, other.Errors...)
82 | }
83 |
84 | type Concern struct {
85 | Raw string // The raw response from, say, the OCSP or certutil tools
86 | Interpretation string // What this tool thinks is wrong.
87 | Advise string // Any advise for troubleshooting
88 | }
89 |
90 | type CertificateResult struct {
91 | *x509.Certificate `json:"-"`
92 | Fingerprint string
93 | CrtSh string
94 | CommonName string
95 | OCSP []ocsp.OCSP
96 | CRL []crl.CRL
97 | Expiration expiration.ExpirationStatus
98 | }
99 |
100 | func NewCeritifcateResult(certificate *x509.Certificate, ocspResonse []ocsp.OCSP, crlStatus []crl.CRL, expirationStatus expiration.ExpirationStatus) CertificateResult {
101 | return CertificateResult{
102 | certificate,
103 | certificateUtils.FingerprintOf(certificate),
104 | "https://crt.sh/?q=" + certificateUtils.FingerprintOf(certificate),
105 | certificate.Subject.CommonName,
106 | ocspResonse,
107 | crlStatus,
108 | expirationStatus,
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/capi/lib/revocation/crl/crl.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package crl
6 |
7 | import (
8 | "crypto/x509"
9 | "fmt"
10 | "github.com/pkg/errors"
11 | "io/ioutil"
12 | "math/big"
13 | "net/http"
14 | "strings"
15 | "time"
16 | )
17 |
18 | type CRLStatus string
19 |
20 | const (
21 | Good CRLStatus = "good"
22 | Revoked = "revoked"
23 | Unchecked = "unchecked"
24 | BadResponse = "badResponse"
25 | )
26 |
27 | type CRL struct {
28 | Error string
29 | Endpoint string
30 | Status CRLStatus
31 | }
32 |
33 | func VerifyChain(chain []*x509.Certificate) [][]CRL {
34 | crls := make([][]CRL, len(chain))
35 | if len(chain) == 1 {
36 | return crls
37 | }
38 | for i, cert := range chain[:len(chain)-1] {
39 | crls[i] = queryCRLs(cert)
40 | }
41 | crls[len(crls)-1] = make([]CRL, 0)
42 | return crls
43 | }
44 |
45 | func queryCRLs(certificate *x509.Certificate) []CRL {
46 | statuses := make([]CRL, len(certificate.CRLDistributionPoints))
47 | for i, url := range certificate.CRLDistributionPoints {
48 | statuses[i] = newCRL(certificate.SerialNumber, url)
49 | }
50 | if disagreement := allAgree(statuses); disagreement != nil {
51 | for _, status := range statuses {
52 | status.Error = disagreement.Error()
53 | }
54 | }
55 | return statuses
56 | }
57 |
58 | func allAgree(statuses []CRL) error {
59 | if len(statuses) <= 1 {
60 | return nil
61 | }
62 | checkedCRLs := make([]CRL, 0)
63 | for _, s := range statuses {
64 | if s.Status == Unchecked {
65 | continue
66 | }
67 | checkedCRLs = append(checkedCRLs, s)
68 | }
69 | firstAnswer := checkedCRLs[0]
70 | for _, otherAnswer := range checkedCRLs[1:] {
71 | if otherAnswer.Status != firstAnswer.Status {
72 | return errors.New("The listed CRLs disagree with each other")
73 | }
74 | }
75 | return nil
76 | }
77 |
78 | func newCRL(serialNumber *big.Int, distributionPoint string) (crl CRL) {
79 | crl.Endpoint = distributionPoint
80 | if strings.HasPrefix(distributionPoint, "ldap") {
81 | crl.Status = Unchecked
82 | return
83 | }
84 | req, err := http.NewRequest("GET", distributionPoint, nil)
85 | req.Header.Add("X-Automated-Tool", "https://github.com/mozilla/CCADB-Tools/capi CCADB test website verification tool")
86 | client := http.Client{}
87 | client.Timeout = time.Duration(20 * time.Second)
88 | raw, err := client.Do(req)
89 | if err != nil {
90 | crl.Error = errors.Wrapf(err, "failed to retrieve CRL from distribution point %v", distributionPoint).Error()
91 | crl.Status = BadResponse
92 | return
93 | }
94 | defer raw.Body.Close()
95 | if raw.StatusCode != http.StatusOK {
96 | crl.Error = errors.New(fmt.Sprintf("wanted 200 response, got %d", raw.StatusCode)).Error()
97 | crl.Status = BadResponse
98 | return
99 | }
100 | b, err := ioutil.ReadAll(raw.Body)
101 | if err != nil {
102 | crl.Error = errors.Wrapf(err, "failed to read response from CRL distribution point %v", distributionPoint).Error()
103 | crl.Status = BadResponse
104 | return
105 | }
106 | c, err := x509.ParseCRL(b)
107 | if err != nil {
108 | crl.Error = errors.Wrapf(err, "failed to parse provided CRL\n%v", raw).Error()
109 | crl.Status = BadResponse
110 | return
111 | }
112 | if c.TBSCertList.RevokedCertificates == nil {
113 | crl.Status = Good
114 | return
115 | }
116 | for _, revoked := range c.TBSCertList.RevokedCertificates {
117 | if revoked.SerialNumber.Cmp(serialNumber) == 0 {
118 | crl.Status = Revoked
119 | return
120 | }
121 | }
122 | crl.Status = Good
123 | return
124 | }
125 |
--------------------------------------------------------------------------------
/capi/lib/service/verifyChain.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package service
6 |
7 | import (
8 | "crypto/x509"
9 | "fmt"
10 | "github.com/mozilla/CCADB-Tools/capi/lib/certificateUtils"
11 | "github.com/mozilla/CCADB-Tools/capi/lib/expiration"
12 | "github.com/mozilla/CCADB-Tools/capi/lib/model"
13 | "github.com/mozilla/CCADB-Tools/capi/lib/revocation/crl"
14 | "github.com/mozilla/CCADB-Tools/capi/lib/revocation/ocsp"
15 | log "github.com/sirupsen/logrus"
16 | "time"
17 | )
18 |
19 | func VerifyChain(chain []*x509.Certificate) model.ChainResult {
20 | result := model.ChainResult{}
21 | if len(chain) == 0 {
22 | return result
23 | }
24 | expirations, err := expiration.VerifyChain(chain)
25 | if err != nil {
26 | // @TODO richer conveyance back over HTTP to the client
27 | log.WithError(err)
28 | log.WithTime(time.Now())
29 | for i, cert := range chain {
30 | log.WithField(fmt.Sprintf("certificate %d", i), certificateUtils.FingerprintOf(cert))
31 | }
32 | log.Error("A query to NSS for expiration status failed")
33 | }
34 | ocsps := ocsp.VerifyChain(chain)
35 | crls := crl.VerifyChain(chain)
36 | result.Leaf = model.NewCeritifcateResult(chain[0], ocsps[0], crls[0], expirations[0])
37 |
38 | ca := len(chain) - 1
39 | result.Root = model.NewCeritifcateResult(chain[ca], ocsps[ca], crls[ca], expirations[ca])
40 |
41 | // Just a leaf and its root, no intermediates.
42 | if len(chain) <= 2 {
43 | return result
44 | }
45 |
46 | result.Intermediates = make([]model.CertificateResult, len(chain[1:len(chain)-1]))
47 | for i := 1; i < len(chain)-1; i++ {
48 | result.Intermediates[i-1] = model.NewCeritifcateResult(chain[i], ocsps[i], crls[i], expirations[i])
49 | }
50 |
51 | return result
52 | }
53 |
54 | func VerifySubject(subject string, root *x509.Certificate) model.ChainResult {
55 | chain, err := certificateUtils.GatherCertificateChain(subject)
56 | if err != nil {
57 | log.WithField("URL", subject)
58 | log.WithError(err)
59 | log.Error("failed to retrieve a certificate chain from the remote host")
60 | return model.ChainResult{}
61 | }
62 | chain = certificateUtils.EmplaceRoot(chain, root)
63 | return VerifyChain(chain)
64 | }
65 |
--------------------------------------------------------------------------------
/capi/run.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This Source Code Form is subject to the terms of the Mozilla Public
4 | # License, v. 2.0. If a copy of the MPL was not distributed with this
5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | # Mappings from the host port to the container port.
8 | HOST_PORT=8080
9 | CONTAINER_PORT=80
10 |
11 | # Mappings from host directory to container directory
12 | # for persisting logs.
13 | HOST_LOG_DIR=$(pwd)/logs/capi
14 | CONTAINER_LOG_DIR=/var/logs/capi/capi.log
15 | mkdir -p ${HOST_LOG_DIR}
16 |
17 | # Valid log levels are...
18 | # panic
19 | # fatal
20 | # error
21 | # warn OR warning
22 | # info
23 | # debug
24 | # trace
25 | # Default is info.
26 | LOGLEVEL=info
27 |
28 | # Lumberjack configurations
29 | # In megabytes
30 | MAX_LOG_SIZE=12
31 | # In days
32 | MAX_LOG_AGE=31
33 | MAX_LOG_BACKUPS=12
34 |
35 | docker run \
36 | --name capi \
37 | -d \
38 | -e "PORT=$CONTAINER_PORT" \
39 | -p ${HOST_PORT}:${CONTAINER_PORT} \
40 | -e "LOG_DIR=$CONTAINER_LOG_DIR" \
41 | --mount type=bind,source=${HOST_LOG_DIR},target=${CONTAINER_LOG_DIR} \
42 | -e "MAX_LOG_SIZE=${MAX_LOG_SIZE}" \
43 | -e "MAX_LOG_AGE=${MAX_LOG_AGE}" \
44 | -e "MAX_LOG_BACKUPS=${MAX_LOG_BACKUPS}" \
45 | capi
46 |
47 |
--------------------------------------------------------------------------------
/certViewer/Dockerfile:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | # Build stage
6 | FROM golang:bookworm AS builder
7 | WORKDIR /go/src/github.com/mozilla/CCADB-Tools/certViewer/
8 | COPY . .
9 | RUN go build -o certViewer ./cmd/web
10 |
11 | # Final image
12 | FROM debian:bookworm-slim
13 | WORKDIR /app/
14 |
15 | COPY --from=builder /go/src/github.com/mozilla/CCADB-Tools/certViewer/certViewer ./
16 | COPY ./ui ./ui
17 | CMD ["/app/certViewer"]
--------------------------------------------------------------------------------
/certViewer/Makefile:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | clean:
6 | -docker stop certviewer
7 | -docker rm certviewer
8 | -docker rmi certviewer
9 | -docker image prune -f
10 | -docker image prune -f --filter label=stage=intermediate
11 |
12 | build:
13 | docker build --rm -t certviewer:latest .
14 | docker image prune -f
15 | docker image prune -f --filter label=stage=intermediate
16 |
17 | run:
18 | ./run.sh
19 |
--------------------------------------------------------------------------------
/certViewer/README.md:
--------------------------------------------------------------------------------
1 | Certificate Viewing Tool
2 | -----------------
3 |
4 | ## Deployment
5 |
6 | ### Locally
7 | When running `certViewer` locally:
8 |
9 | ```sh
10 | $ docker build -t certviewer .
11 | $ docker run -p 8080:8080 certviewer
12 | ```
13 | Navigate to http://127.0.0.1:8080/certviewer in your web browser.
14 |
15 | ### Production
16 | When running `certViewer` in production:
17 |
18 | ```sh
19 | $ make clean build run
20 | ```
--------------------------------------------------------------------------------
/certViewer/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: custom
2 | env: flex
3 | service: certviewer
4 |
--------------------------------------------------------------------------------
/certViewer/cmd/web/handlers.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package main
6 |
7 | import (
8 | "crypto/x509"
9 | "encoding/pem"
10 | "github.com/mozilla/CCADB-Tools/certViewer/internal/validator"
11 | "mime/multipart"
12 | "net/http"
13 | )
14 |
15 | type certForm struct {
16 | RootCert string
17 | RootCertUpload *multipart.FileHeader
18 | validator.Validator
19 | }
20 |
21 | // home handles the default endpoint GET request, "/certviewer"
22 | func (app *application) home(w http.ResponseWriter, r *http.Request) {
23 | data := app.newTemplateData(r)
24 | data.Certificate = Certificate{
25 | Serial: "",
26 | }
27 | data.Form = certForm{}
28 |
29 | app.render(w, r, http.StatusOK, "home.tmpl", data)
30 |
31 | }
32 |
33 | // certPost handles the form POST request from the "/certviewer" endpoint
34 | func (app *application) certPost(w http.ResponseWriter, r *http.Request) {
35 | err := r.ParseMultipartForm(1 << 20) // 10MB
36 | if err != nil {
37 | app.clientError(w, http.StatusBadRequest)
38 | return
39 | }
40 |
41 | form := certForm{
42 | RootCert: r.PostFormValue("rootCert"),
43 | }
44 |
45 | _, header, _ := r.FormFile("rootCertUpload")
46 | form.RootCertUpload = header
47 |
48 | form.CheckField(validator.NoPEMs(form.RootCert, form.RootCertUpload), "rootCertUpload", "Please upload or paste the contents of a PEM file")
49 | form.CheckField(validator.BothPEMs(form.RootCert, form.RootCertUpload), "rootCertUpload", "Please only submit a pasted PEM file OR upload a file")
50 |
51 | var pemFile string
52 |
53 | if form.RootCert != "" {
54 | form.CheckField(validator.ValidPEM(form.RootCert), "rootCert", "Invalid certificate format. Certificate must be PEM-encoded")
55 | pemFile = form.RootCert
56 | } else if form.RootCertUpload != nil {
57 | pemUploadFile := app.uploadSave(r)
58 | pemContents := app.pemReader(pemUploadFile)
59 | form.CheckField(validator.ValidPEM(pemContents), "rootCertUpload", "Invalid certificate format. Certificate must be PEM-encoded")
60 | pemFile = pemContents
61 |
62 | // remove the cert written to the file system
63 | app.certCleanup(pemUploadFile)
64 | }
65 |
66 | if !form.Valid() {
67 | data := app.newTemplateData(r)
68 | data.Form = form
69 | app.render(w, r, http.StatusUnprocessableEntity, "home.tmpl", data)
70 | return
71 | }
72 |
73 | block, _ := pem.Decode([]byte(pemFile))
74 | if block == nil {
75 | app.clientError(w, http.StatusBadRequest)
76 | return
77 | }
78 |
79 | certX509, err := x509.ParseCertificate(block.Bytes)
80 | if err != nil {
81 | app.clientError(w, http.StatusBadRequest)
82 | return
83 | }
84 |
85 | certData := certInfo(certX509)
86 | data := app.newTemplateData(r)
87 | data.Certificate = certData
88 | data.Form = form
89 |
90 | app.render(w, r, http.StatusOK, "home.tmpl", data)
91 | }
92 |
--------------------------------------------------------------------------------
/certViewer/cmd/web/helpers.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package main
6 |
7 | import (
8 | "bytes"
9 | "fmt"
10 | "io"
11 | "net/http"
12 | "os"
13 | "runtime/debug"
14 | )
15 |
16 | func (app *application) serverError(w http.ResponseWriter, r *http.Request, err error) {
17 | var (
18 | method = r.Method
19 | uri = r.URL.RequestURI()
20 | trace = string(debug.Stack())
21 | )
22 |
23 | app.logger.Error(err.Error(), "method", method, "uri", uri, "trace", trace)
24 |
25 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
26 | }
27 |
28 | func (app *application) clientError(w http.ResponseWriter, status int) {
29 | http.Error(w, http.StatusText(status), status)
30 | }
31 |
32 | func (app *application) notFound(w http.ResponseWriter) {
33 | app.clientError(w, http.StatusNotFound)
34 | }
35 |
36 | func (app *application) render(w http.ResponseWriter, r *http.Request, status int, page string, data templateData) {
37 | ts, ok := app.templateCache[page]
38 | if !ok {
39 | err := fmt.Errorf("the template %s does not exist", page)
40 | app.serverError(w, r, err)
41 | return
42 | }
43 |
44 | buf := new(bytes.Buffer)
45 |
46 | err := ts.ExecuteTemplate(buf, "base", data)
47 | if err != nil {
48 | app.serverError(w, r, err)
49 | return
50 | }
51 |
52 | w.WriteHeader(status)
53 |
54 | buf.WriteTo(w)
55 | }
56 |
57 | func (app *application) newTemplateData(r *http.Request) templateData {
58 | return templateData{}
59 | }
60 |
61 | // uploadSave handles the process of saving an uploaded file to the file system
62 | func (app *application) uploadSave(r *http.Request) string {
63 | err := r.ParseMultipartForm(1 << 20)
64 | if err != nil {
65 | app.logger.Error("Unable to parse form", "error", err.Error())
66 | }
67 |
68 | file, fileHeader, err := r.FormFile("rootCertUpload")
69 | if err != nil {
70 | app.logger.Error("Unable to parse form", "error", err.Error())
71 | }
72 | defer file.Close()
73 |
74 | err = os.MkdirAll("/tmp", os.ModePerm)
75 | if err != nil {
76 | app.logger.Error("Unable to create /tmp directory", "error", err.Error())
77 | }
78 |
79 | pemFile := "/tmp/" + fileHeader.Filename
80 | dst, err := os.Create(pemFile)
81 | if err != nil {
82 | app.logger.Error("Unable to create file", "error", err.Error())
83 | }
84 |
85 | defer dst.Close()
86 |
87 | if _, err := io.Copy(dst, file); err != nil {
88 | app.logger.Error("Unable to save file", "error", err.Error())
89 | }
90 |
91 | return pemFile
92 | }
93 |
94 | // pemReader reads the contents of a PEM file
95 | func (app *application) pemReader(pemUpload string) string {
96 | content, err := os.ReadFile(pemUpload)
97 | if err != nil {
98 | app.logger.Error("Unable to read contents of uploaded file", "error", err.Error())
99 | }
100 |
101 | return string(content)
102 | }
103 |
--------------------------------------------------------------------------------
/certViewer/cmd/web/main.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package main
6 |
7 | import (
8 | "flag"
9 | "html/template"
10 | "log/slog"
11 | "net/http"
12 | "os"
13 | "time"
14 | )
15 |
16 | type application struct {
17 | certificate *Certificate
18 | logger *slog.Logger
19 | pemFile string
20 | templateCache map[string]*template.Template
21 | Request *http.Request
22 | }
23 |
24 | func main() {
25 | // Default to port 8080 if PORT env var is not set
26 | port := getPortEnv("PORT", "8080")
27 |
28 | addr := flag.String("addr", ":"+port, "HTTP network address")
29 | flag.Parse()
30 |
31 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
32 |
33 | templateCache, err := newTemplateCache()
34 | if err != nil {
35 | logger.Error(err.Error())
36 | os.Exit(1)
37 | }
38 |
39 | app := &application{
40 | logger: logger,
41 | templateCache: templateCache,
42 | }
43 |
44 | srv := &http.Server{
45 | Addr: *addr,
46 | Handler: app.routes(),
47 | ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
48 | IdleTimeout: time.Minute,
49 | ReadTimeout: 5 * time.Second,
50 | WriteTimeout: 10 * time.Second,
51 | }
52 |
53 | logger.Info("Starting server", "addr", srv.Addr)
54 |
55 | err = srv.ListenAndServe()
56 | logger.Error(err.Error())
57 | os.Exit(1)
58 | }
59 |
60 | // getPortEnv looks for the PORT env var and uses fallback if not set
61 | func getPortEnv(port, fallback string) string {
62 | if value, ok := os.LookupEnv(port); ok {
63 | return value
64 | }
65 | return fallback
66 | }
67 |
--------------------------------------------------------------------------------
/certViewer/cmd/web/middleware.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package main
6 |
7 | import (
8 | "fmt"
9 | "net/http"
10 | )
11 |
12 | // secureHeaders follows some security best practices
13 | func secureHeaders(next http.Handler) http.Handler {
14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15 | w.Header().Set("Content-Security-Policy",
16 | "default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com")
17 |
18 | w.Header().Set("Referrer-Policy", "origin-when-cross-origin")
19 | w.Header().Set("X-Content-Type-Options", "nosniff")
20 | w.Header().Set("X-Frame-Options", "deny")
21 | w.Header().Set("X-XSS-Protection", "0")
22 |
23 | next.ServeHTTP(w, r)
24 | })
25 | }
26 |
27 | // logRequest handles logging of requests to server
28 | func (app *application) logRequest(next http.Handler) http.Handler {
29 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
30 | var (
31 | ip = r.RemoteAddr
32 | proto = r.Proto
33 | method = r.Method
34 | uri = r.URL.RequestURI()
35 | )
36 |
37 | app.logger.Info("Received request", "ip", ip, "proto", proto, "method", method, "uri", uri)
38 |
39 | next.ServeHTTP(w, r)
40 | })
41 | }
42 |
43 | // recoverPanic recovers from server panics and returns an error instead
44 | func (app *application) recoverPanic(next http.Handler) http.Handler {
45 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46 | defer func() {
47 | if err := recover(); err != nil {
48 | w.Header().Set("Connection", "close")
49 | app.serverError(w, r, fmt.Errorf("%s", err))
50 | }
51 | }()
52 |
53 | next.ServeHTTP(w, r)
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/certViewer/cmd/web/routes.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package main
6 |
7 | import (
8 | "net/http"
9 |
10 | "github.com/julienschmidt/httprouter"
11 | "github.com/justinas/alice"
12 | )
13 |
14 | // routes handles the routing for /evready
15 | func (app *application) routes() http.Handler {
16 | router := httprouter.New()
17 |
18 | router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19 | app.notFound(w)
20 | })
21 |
22 | fileServer := http.FileServer(http.Dir("./ui/static/"))
23 | router.Handler(http.MethodGet, "/static/*filepath", http.StripPrefix("/static", fileServer))
24 |
25 | router.HandlerFunc(http.MethodGet, "/certviewer", app.home)
26 | router.HandlerFunc(http.MethodPost, "/certviewer", app.certPost)
27 |
28 | standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
29 |
30 | return standard.Then(router)
31 | }
32 |
--------------------------------------------------------------------------------
/certViewer/cmd/web/templates.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package main
6 |
7 | import (
8 | "html/template"
9 | "path/filepath"
10 | )
11 |
12 | type templateData struct {
13 | Certificate Certificate
14 | Form any
15 | Flash string
16 | }
17 |
18 | // newTemplateCache caches template files
19 | func newTemplateCache() (map[string]*template.Template, error) {
20 | cache := map[string]*template.Template{}
21 |
22 | pages, err := filepath.Glob("./ui/html/pages/*tmpl")
23 | if err != nil {
24 | return nil, err
25 | }
26 |
27 | for _, page := range pages {
28 | name := filepath.Base(page)
29 |
30 | ts, err := template.ParseFiles("./ui/html/base.tmpl")
31 | if err != nil {
32 | return nil, err
33 | }
34 |
35 | ts, err = ts.ParseGlob("./ui/html/partials/*.tmpl")
36 | if err != nil {
37 | return nil, err
38 | }
39 |
40 | ts, err = ts.ParseFiles(page)
41 | if err != nil {
42 | return nil, err
43 | }
44 |
45 | cache[name] = ts
46 | }
47 |
48 | return cache, nil
49 | }
50 |
--------------------------------------------------------------------------------
/certViewer/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mozilla/CCADB-Tools/certViewer
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/julienschmidt/httprouter v1.3.0
7 | github.com/justinas/alice v1.2.0
8 | )
9 |
--------------------------------------------------------------------------------
/certViewer/go.sum:
--------------------------------------------------------------------------------
1 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
2 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
3 | github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
4 | github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
5 |
--------------------------------------------------------------------------------
/certViewer/internal/validator/validator.go:
--------------------------------------------------------------------------------
1 | /* This Source Code Form is subject to the terms of the Mozilla Public
2 | * License, v. 2.0. If a copy of the MPL was not distributed with this
3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 |
5 | package validator
6 |
7 | import (
8 | "mime/multipart"
9 | "net/url"
10 | "regexp"
11 | "strings"
12 | "unicode/utf8"
13 | )
14 |
15 | type Validator struct {
16 | FieldErrors map[string]string
17 | }
18 |
19 | func (v *Validator) Valid() bool {
20 | return len(v.FieldErrors) == 0
21 | }
22 |
23 | func (v *Validator) AddFieldError(key, message string) {
24 | if v.FieldErrors == nil {
25 | v.FieldErrors = make(map[string]string)
26 | }
27 |
28 | if _, exists := v.FieldErrors[key]; !exists {
29 | v.FieldErrors[key] = message
30 | }
31 | }
32 |
33 | func (v *Validator) CheckField(ok bool, key, message string) {
34 | if !ok {
35 | v.AddFieldError(key, message)
36 | }
37 | }
38 |
39 | // NotBlank checks to make sure the field isn't blank
40 | func NotBlank(value string) bool {
41 | return strings.TrimSpace(value) != ""
42 | }
43 |
44 | // MaxChars checks to make sure the field isn't over the specified number of characters
45 | func MaxChars(value string, n int) bool {
46 | return utf8.RuneCountInString(value) <= n
47 | }
48 |
49 | // ValidURL validates the provided hostname
50 | func ValidURL(value string) bool {
51 | _, err := url.Parse(value)
52 | if err != nil {
53 | return false
54 | } else {
55 | return true
56 | }
57 | }
58 |
59 | // ValidOID validates the provided OID
60 | func ValidOID(value string) bool {
61 | re := regexp.MustCompile(`^([0-2])((\.0)|(\.[1-9][0-9]*))*$`)
62 |
63 | return re.MatchString(strings.TrimSpace(value))
64 | }
65 |
66 | // NoPEMs validates that there is at least a pasted PEM or an uploaded PEM file
67 | func NoPEMs(pemPaste string, pemUpload *multipart.FileHeader) bool {
68 | if pemPaste == "" && pemUpload == nil {
69 | return false
70 | } else {
71 | return true
72 | }
73 | }
74 |
75 | // BothPEMs validates that only one or the other types of PEM are submitted
76 | func BothPEMs(pemPaste string, pemUpload *multipart.FileHeader) bool {
77 | if pemPaste != "" && pemUpload != nil {
78 | return false
79 | } else {
80 | return true
81 | }
82 | }
83 |
84 | // ValidPEM validates PEM content - pasted or uploaded
85 | func ValidPEM(value string) bool {
86 | value = strings.TrimSpace(value)
87 | return strings.HasPrefix(value, "-----BEGIN CERTIFICATE-----") &&
88 | strings.HasSuffix(value, "-----END CERTIFICATE-----")
89 | }
90 |
--------------------------------------------------------------------------------
/certViewer/run.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This Source Code Form is subject to the terms of the Mozilla Public
4 | # License, v. 2.0. If a copy of the MPL was not distributed with this
5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 |
7 | # Mappings from the host port to the container port.
8 | HOST_PORT=8080
9 | CONTAINER_PORT=80
10 |
11 | docker run \
12 | --name certviewer \
13 | -d \
14 | -e "PORT=$CONTAINER_PORT" \
15 | -p ${HOST_PORT}:${CONTAINER_PORT} \
16 | certviewer
17 |
18 |
--------------------------------------------------------------------------------
/certViewer/ui/html/base.tmpl:
--------------------------------------------------------------------------------
1 | {{define "base"}}
2 |
3 |
4 |
5 |
6 | {{template "title" .}}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |