├── .gitignore ├── API_AddUpdateIntermediateCert └── README.md ├── EVChecker ├── Dockerfile ├── Makefile ├── README.md ├── app.yaml ├── go.mod ├── go.sum ├── main.go └── run.sh ├── LICENSE ├── README.md ├── cacheck ├── .gcloudignore ├── .gitignore ├── README.md ├── app.yaml ├── ccadb │ ├── __init__.py │ └── db.py ├── config.py ├── index.yaml ├── lint_dict.py ├── main.py ├── main_test.py ├── requirements.txt ├── static │ ├── css │ │ └── style.css │ ├── icon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ ├── js │ │ ├── script.js │ │ └── summary.js │ └── site.webmanifest └── templates │ ├── index.html │ ├── lint.html │ └── summary.html ├── cacompliance ├── README.md ├── app.yaml ├── config.py ├── index.yaml ├── main.py ├── main_test.py ├── requirements.txt ├── static │ └── css │ │ └── style.css └── templates │ └── index.html ├── capi ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app.yaml ├── capi.go ├── go.mod ├── go.sum ├── lib │ ├── ccadb │ │ ├── ccabd_test.go │ │ └── ccadb.go │ ├── certificateUtils │ │ └── parseChain.go │ ├── expiration │ │ ├── certutil │ │ │ └── certutil.go │ │ ├── expiration.go │ │ └── expiration_test.go │ ├── lint │ │ ├── certlint │ │ │ └── certlint.go │ │ └── x509lint │ │ │ ├── x509lint.go │ │ │ └── x509lint_test.go │ ├── model │ │ ├── ccadb_record.go │ │ ├── ccadb_record_test.go │ │ ├── lint_result.go │ │ └── result.go │ ├── revocation │ │ ├── crl │ │ │ ├── crl.go │ │ │ └── crl_test.go │ │ └── ocsp │ │ │ ├── ocsp.go │ │ │ └── ocsp_test.go │ └── service │ │ ├── interpretation.go │ │ └── verifyChain.go └── run.sh ├── certViewer ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app.yaml ├── cmd │ └── web │ │ ├── certificates.go │ │ ├── constraints.go │ │ ├── handlers.go │ │ ├── helpers.go │ │ ├── main.go │ │ ├── middleware.go │ │ ├── routes.go │ │ └── templates.go ├── go.mod ├── go.sum ├── internal │ └── validator │ │ └── validator.go ├── run.sh └── ui │ ├── html │ ├── base.tmpl │ ├── pages │ │ └── home.tmpl │ └── partials │ │ ├── basicInfo.tmpl │ │ ├── certExt.tmpl │ │ └── san.tmpl │ └── static │ ├── css │ └── main.css │ └── img │ └── favicon.ico ├── certdataDiffCCADB ├── Dockerfile ├── Makefile ├── README.md ├── app.yaml ├── ccadb │ └── ccadb.go ├── certdata │ ├── certdata.go │ └── certdata_test.go ├── go.mod ├── go.sum ├── main.go └── utils │ ├── entry.go │ └── pair.go ├── certificate ├── Dockerfile ├── Makefile ├── README.md ├── app.yaml ├── certificate.go ├── certificate_test.go ├── constraints_v25.go ├── constraints_v29.go ├── go.mod ├── go.sum ├── main.go └── run.sh ├── crlVerification ├── Dockerfile ├── Makefile ├── README.md ├── app.yaml ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── run.sh └── utils │ ├── crl.go │ ├── reason.go │ ├── revocationDate.go │ └── serial.go ├── evReady ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app.yaml ├── cmd │ └── web │ │ ├── certificates.go │ │ ├── handlers.go │ │ ├── helpers.go │ │ ├── main.go │ │ ├── middleware.go │ │ ├── routes.go │ │ └── templates.go ├── go.mod ├── go.sum ├── internal │ └── validator │ │ └── validator.go ├── run.sh ├── src │ ├── EVCheckerTrustDomain.cpp │ ├── EVCheckerTrustDomain.h │ ├── Makefile │ ├── README.md │ ├── Util.cpp │ ├── Util.h │ ├── ev-checker.cpp │ ├── pkix-import │ ├── pkix │ │ ├── include │ │ │ └── pkix │ │ │ │ ├── Input.h │ │ │ │ ├── Result.h │ │ │ │ ├── ScopedPtr.h │ │ │ │ ├── Time.h │ │ │ │ ├── pkix.h │ │ │ │ ├── pkixnss.h │ │ │ │ ├── pkixtypes.h │ │ │ │ └── stdkeywords.h │ │ ├── lib │ │ │ ├── pkixbuild.cpp │ │ │ ├── pkixcert.cpp │ │ │ ├── pkixcheck.cpp │ │ │ ├── pkixcheck.h │ │ │ ├── pkixder.cpp │ │ │ ├── pkixder.h │ │ │ ├── pkixnames.cpp │ │ │ ├── pkixnss.cpp │ │ │ ├── pkixocsp.cpp │ │ │ ├── pkixresult.cpp │ │ │ ├── pkixtime.cpp │ │ │ ├── pkixutil.h │ │ │ └── pkixverify.cpp │ │ ├── moz.build │ │ ├── test │ │ │ ├── gtest │ │ │ │ ├── README.txt │ │ │ │ ├── moz.build │ │ │ │ ├── pkixbuild_tests.cpp │ │ │ │ ├── pkixcert_extension_tests.cpp │ │ │ │ ├── pkixcert_signature_algorithm_tests.cpp │ │ │ │ ├── pkixcheck_CheckKeyUsage_tests.cpp │ │ │ │ ├── pkixcheck_CheckSignatureAlgorithm_tests.cpp │ │ │ │ ├── pkixcheck_CheckValidity_tests.cpp │ │ │ │ ├── pkixder_input_tests.cpp │ │ │ │ ├── pkixder_pki_types_tests.cpp │ │ │ │ ├── pkixder_universal_types_tests.cpp │ │ │ │ ├── pkixgtest.cpp │ │ │ │ ├── pkixgtest.h │ │ │ │ ├── pkixnames_tests.cpp │ │ │ │ ├── pkixocsp_CreateEncodedOCSPRequest_tests.cpp │ │ │ │ └── pkixocsp_VerifyEncodedOCSPResponse.cpp │ │ │ └── lib │ │ │ │ ├── moz.build │ │ │ │ ├── pkixtestnss.cpp │ │ │ │ ├── pkixtestutil.cpp │ │ │ │ └── pkixtestutil.h │ │ ├── tools │ │ │ └── DottedOIDToCode.py │ │ └── warnings.mozbuild │ └── test │ │ ├── ev-ca.cnf │ │ ├── ev-int.cnf │ │ ├── ev.cnf │ │ └── generate-certs.sh └── ui │ ├── html │ ├── base.tmpl │ └── pages │ │ └── home.tmpl │ └── static │ ├── css │ └── main.css │ └── img │ └── favicon.ico └── oneCRLDiffCCADB ├── Dockerfile ├── Makefile ├── README.md ├── app.yaml ├── ccadb ├── ccadb.go └── ccadb_test.go ├── go.mod ├── go.sum ├── main.go ├── normalized ├── normalize_test.go └── normalized.go ├── oneCRL ├── oneCRL.go └── oneCRL_test.go └── run.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .idea* 2 | target* 3 | **/*.rs.bk 4 | go.work* 5 | .DS_Store 6 | **/.DS_Store 7 | -------------------------------------------------------------------------------- /EVChecker/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 | # This is necessary to statically compile all 10 | # C libraries into the executable. Otherwise 11 | # the Alpine installation will fail out with 12 | # a "no such directory" when attempting to execute 13 | # the binary (it can't find the shared libs). 14 | ENV CGO_ENABLED=0 15 | RUN go test ./... && go build main.go 16 | 17 | FROM alpine:latest 18 | 19 | COPY --from=buildStage /opt/ /opt/ 20 | 21 | CMD ["/opt/main"] -------------------------------------------------------------------------------- /EVChecker/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 evchecker 7 | -docker rm evchecker 8 | -docker rmi evchecker 9 | -docker image prune -f 10 | -docker image prune -f --filter label=stage=intermediate 11 | 12 | build: 13 | docker build --rm -t evchecker:latest . 14 | docker image prune -f 15 | docker image prune -f --filter label=stage=intermediate 16 | 17 | run: 18 | ./run.sh -------------------------------------------------------------------------------- /EVChecker/README.md: -------------------------------------------------------------------------------- 1 | EVChecker 2 | ----------------- 3 | 4 | ### Use Cases 5 | 6 | EVChecker gets the EV Policy OIDs for each root cert listed in ExtendedValidation.cpp 7 | 8 | This tool is used to fill in the “ExtendedValidation.cpp OIDs” fields on root certs in thee CCADB. There is a CCADB home page report that alerts when that field does not match the published “Mozilla EV Policy OID(s)” field. 9 | 10 | The files in question are from [nightly](https://hg.mozilla.org/mozilla-central/raw-file/tip/security/certverifier/ExtendedValidation.cpp), [beta](https://hg.mozilla.org/releases/mozilla-beta/raw-file/tip/security/certverifier/ExtendedValidation.cpp), and [release](https://hg.mozilla.org/releases/mozilla-release/raw-file/tip/security/certverifier/ExtendedValidation.cpp) of Firefox. 11 | 12 | ### Deployment 13 | 14 | #### Locally 15 | When running `evChecker` locally: 16 | 17 | $ go build -o evChecker . 18 | $ PORT=8080 ./evChecker 19 | 20 | #### Using Docker 21 | Alternatively, one may use the provided `Dockerfile` and `Makefile`: 22 | 23 | $ make clean build run 24 | 25 | ### Usage 26 | 27 | EVChecker offers four endpoints - `/release`, `/beta`, `nightly`, and `/?url=` 28 | 29 | For example: 30 | 31 | ```bash 32 | # Parse Firefox release. 33 | curl http://localhost:8080/release 34 | 35 | # Parse beta Firefox. 36 | curl http://localhost:8080/beta 37 | 38 | # Parse nightly Firefox 39 | curl http://localhost:8080/nightly 40 | 41 | # Parse niightly Firefox again, but with an explicit (read: arbirtrary) url parameter. 42 | curl http://localhost:8080/?url=https://hg.mozilla.org/mozilla-central/raw-file/tip/security/certverifier/ExtendedValidation.cpp 43 | ``` 44 | 45 | Example output: 46 | 47 | ```json 48 | { 49 | "Error": null, 50 | "EVInfos": [ 51 | { 52 | "DottedOID": "1.3.6.1.4.1.6334.1.100.1", 53 | "OIDName": "Cybertrust EV OID", 54 | "SHA256Fingerprint": "960ADF0063E96356750C2965DD0A0867DA0B9CBD6E77714AEAFB2349AB393DA3", 55 | "Issuer": "CN=Cybertrust Global Root,O=Cybertrust\\, Inc", 56 | "Serial": "00000000000400000000010F85AA2D48" 57 | }, 58 | { 59 | "DottedOID": "2.16.756.1.89.1.2.1.1", 60 | "OIDName": "SwissSign EV OID", 61 | "SHA256Fingerprint": "62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", 62 | "Issuer": "CN=SwissSign Gold CA - G2,O=SwissSign AG,C=CH", 63 | "Serial": "0000000000000000BB401C43F55E4FB0" 64 | }, 65 | { 66 | "DottedOID": "2.16.840.1.114404.1.1.2.4.1", 67 | "OIDName": "Trustwave EV OID", 68 | "SHA256Fingerprint": "CECDDC905099D8DADFC5B1D209B737CBE2C18CFB2C10C0FF0BCF0D3286FC1AA2", 69 | "Issuer": "CN=XRamp Global Certification Authority,O=XRamp Security Services Inc,OU=www.xrampsecurity.com,C=US", 70 | "Serial": "50946CEC18EAD59C4DD597EF758FA0AD" 71 | }, 72 | ... 73 | ... 74 | ... 75 | { 76 | "DottedOID": "2.23.140.1.1", 77 | "OIDName": "CA/Browser Forum EV OID", 78 | "SHA256Fingerprint": "657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", 79 | "Issuer": "OU=certSIGN ROOT CA G2,O=CERTSIGN SA,C=RO", 80 | "Serial": "00000000000000110034B64EC6362D36" 81 | } 82 | ] 83 | } 84 | ``` -------------------------------------------------------------------------------- /EVChecker/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: custom 2 | env: flex 3 | service: evchecker 4 | -------------------------------------------------------------------------------- /EVChecker/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mozilla/CCADB-Tools 2 | 3 | go 1.21 4 | 5 | require github.com/pkg/errors v0.9.1 6 | -------------------------------------------------------------------------------- /EVChecker/go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 2 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | -------------------------------------------------------------------------------- /EVChecker/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 evchecker \ 13 | -d \ 14 | -e "PORT=$CONTAINER_PORT" \ 15 | -p ${HOST_PORT}:${CONTAINER_PORT} \ 16 | evchecker 17 | 18 | -------------------------------------------------------------------------------- /cacheck/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Python pycache: 17 | __pycache__/ 18 | # Ignored by the build system 19 | /setup.cfg -------------------------------------------------------------------------------- /cacheck/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/* 2 | __pycache__ 3 | **/__pycache__ 4 | -------------------------------------------------------------------------------- /cacheck/README.md: -------------------------------------------------------------------------------- 1 | cacheck 2 | ----------------- 3 | Web UI for viewing crt.sh's cached certificate linting results 4 | 5 | 6 | ## Deployment 7 | 8 | Make sure Python is in your PATH 9 | ```sh 10 | echo $PATH 11 | export PATH=$PATH:~/Library/Python/3.9/bin 12 | ``` 13 | Depends on a few other things: 14 | ```sh 15 | pip3 install psycopg2-binary 16 | pip3 install flask 17 | ``` 18 | Build 19 | ```sh 20 | python3 main.py 21 | ``` 22 | 23 | Navigate to http://127.0.0.1:8080 24 | -------------------------------------------------------------------------------- /cacheck/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python311 2 | service: cachecker 3 | 4 | #entrypoint: python main.py --port $PORT 5 | #env: flex 6 | 7 | handlers: 8 | # This configures Google App Engine to serve the files in the app's static 9 | # directory. 10 | - url: /static 11 | static_dir: static 12 | 13 | # This handler routes all requests not caught above to your main app. It is 14 | # required when static routes are defined, but can be omitted (along with 15 | # the entire handlers section) when there are no static files defined. 16 | #- url: /.* 17 | #script: auto 18 | -------------------------------------------------------------------------------- /cacheck/ccadb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/CCADB-Tools/ea10e50c76c4cc331efd7afaaf6f66589414a0d7/cacheck/ccadb/__init__.py -------------------------------------------------------------------------------- /cacheck/config.py: -------------------------------------------------------------------------------- 1 | class Config(object): 2 | DEBUG = True 3 | DB_HOST = 'crt.sh' 4 | DB_PORT = 5432 5 | DB_USER = 'guest' 6 | DB_PASS = '' 7 | DB_DATABASE = 'certwatch' 8 | -------------------------------------------------------------------------------- /cacheck/index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | - kind: visit 4 | ancestor: yes 5 | properties: 6 | - name: timestamp 7 | direction: desc 8 | -------------------------------------------------------------------------------- /cacheck/lint_dict.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | class LintDict(dict): 4 | def __init__(self,*args,**kwargs): 5 | dict.__init__(self,*args,**kwargs) 6 | 7 | def __getitem__(self, key): 8 | if key in self: 9 | return super().__getitem__(key) 10 | return copy.deepcopy([]) 11 | -------------------------------------------------------------------------------- /cacheck/main_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import main 16 | 17 | 18 | def test_index(): 19 | main.app.testing = True 20 | client = main.app.test_client() 21 | 22 | r = client.get('/') 23 | assert r.status_code == 200 24 | -------------------------------------------------------------------------------- /cacheck/requirements.txt: -------------------------------------------------------------------------------- 1 | Click>=7.0 2 | Flask>=2.3.2 3 | itsdangerous>=1.1.0 4 | Jinja2>=2.11.3 5 | MarkupSafe>=1.1.1 6 | psycopg2>=2.8.3 7 | Werkzeug>=2.3.7 -------------------------------------------------------------------------------- /cacheck/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "helvetica", sans-serif; 3 | text-align: center; 4 | } 5 | .tool { 6 | margin-top: 1em; 7 | margin-bottom: 1em; 8 | } 9 | 10 | .cert_info_label { padding-left:2em; float:left; padding-right:0.5em; max-width:20%;} 11 | .cert_info_value { float:right; max-width:80%;} 12 | 13 | .F { color: #fb0a06; font-weight:bold;} 14 | .E { color: #cc3300; font-weight:bold;} 15 | .W { color: #ffcc00; font-weight:bold;} 16 | -------------------------------------------------------------------------------- /cacheck/static/icon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/CCADB-Tools/ea10e50c76c4cc331efd7afaaf6f66589414a0d7/cacheck/static/icon/android-chrome-192x192.png -------------------------------------------------------------------------------- /cacheck/static/icon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/CCADB-Tools/ea10e50c76c4cc331efd7afaaf6f66589414a0d7/cacheck/static/icon/android-chrome-512x512.png -------------------------------------------------------------------------------- /cacheck/static/icon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/CCADB-Tools/ea10e50c76c4cc331efd7afaaf6f66589414a0d7/cacheck/static/icon/apple-touch-icon.png -------------------------------------------------------------------------------- /cacheck/static/icon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/CCADB-Tools/ea10e50c76c4cc331efd7afaaf6f66589414a0d7/cacheck/static/icon/favicon-16x16.png -------------------------------------------------------------------------------- /cacheck/static/icon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/CCADB-Tools/ea10e50c76c4cc331efd7afaaf6f66589414a0d7/cacheck/static/icon/favicon-32x32.png -------------------------------------------------------------------------------- /cacheck/static/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/CCADB-Tools/ea10e50c76c4cc331efd7afaaf6f66589414a0d7/cacheck/static/icon/favicon.ico -------------------------------------------------------------------------------- /cacheck/static/js/script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function focus_caid(){ 4 | $('#ca_id').focus(); 5 | $('#ca_id').effect('highlight'); 6 | } 7 | 8 | function getCACertInfo(cert_id){ 9 | $.get("/ca_cert/" + parseInt(cert_id).toString(), function(cert_info){ 10 | console.log(cert_info); 11 | var ts = Date.parse(cert_info.notbefore); 12 | console.log(ts); 13 | $('#start_date').val( new Date(ts).toISOString().split('T')[0]); 14 | $('#start_date').effect('highlight'); 15 | }).fail(function(){ 16 | alert('Error! Could not get certification information.'); 17 | }); 18 | } 19 | 20 | function getIssuerCAID(){ 21 | var fingerprint_256 = $('#sha256_fingerprint').val().toLowerCase() 22 | var fingerprint_1 = $('#sha1_fingerprint').val().toLowerCase() 23 | 24 | console.log("SHA-1 fingerprint:" + fingerprint_1); 25 | console.log("SHA-256 fingerprint:" + fingerprint_256); 26 | 27 | var fpdata = { "sha256_fingerprint" : fingerprint_256 }; 28 | if(fingerprint_1.length > 0){ 29 | fpdata = { "sha1_fingerprint" : fingerprint_1 }; 30 | } 31 | 32 | $.get("/ca_id", fpdata, function(data){ 33 | console.log("ca id: " + data); 34 | $('#ca_id').val(data); 35 | scrollToID('ca_id', focus_caid); 36 | getCACertInfo(data); 37 | 38 | //fail 39 | }).fail(function(){ 40 | alert('Error! Could not find issuing CA ID'); 41 | }); 42 | } 43 | 44 | function scrollToID(id, f) { 45 | // Scroll 46 | $('html,body').animate({ 47 | scrollTop: $("#" + id).offset().top, 48 | }, { 49 | complete: f, 50 | duration: 400 51 | }); 52 | } 53 | 54 | function fixLintIssueURL(){ 55 | //$('#lint-button').unbind('click'); 56 | $('#lint-button').on('click', function(){ 57 | var caid = $('#ca_id').val(); 58 | var action_url = "/lint_issues/" + parseInt(caid); 59 | $('#ca_lint_form').attr('action', action_url); 60 | if (caid.length == 0){ 61 | alert('Please enter a CA ID or use the fingerprint tool.'); 62 | focus_caid(); 63 | return false; 64 | } 65 | }); 66 | } 67 | 68 | function fixLintSummaryURL(){ 69 | //$('#lint-summary-button').unbind('click'); 70 | $('#lint-summary-button').on('click', function(){ 71 | var caid = $('#ca_id').val(); 72 | var action_url = "/summary/" + parseInt(caid); 73 | $('#ca_lint_form').attr('action', action_url); 74 | if (caid.length == 0){ 75 | alert('Please enter a CA ID or use the fingerprint tool.'); 76 | focus_caid(); 77 | return false; 78 | } 79 | }); 80 | } 81 | 82 | window.addEventListener('load', function () { 83 | 84 | /* 85 | * $('#ca_id').on('input', function(){ 86 | fixLintIssueURL(); 87 | fixLintSummaryURL(); 88 | }); 89 | */ 90 | 91 | fixLintIssueURL(); 92 | fixLintSummaryURL(); 93 | 94 | $('#sha1_fingerprint').on('input', function(){ 95 | $('#sha256_fingerprint').val(''); 96 | //trim whietspace 97 | $('#sha1_fingerprint').val( $('#sha1_fingerprint').val().replace(/[\s:]+/g,'') ); 98 | 99 | }); 100 | 101 | $('#sha256_fingerprint').on('input', function(){ 102 | $('#sha1_fingerprint').val(''); 103 | $('#sha256_fingerprint').val( $('#sha256_fingerprint').val().replace(/[\s:]+/g,'') ); 104 | }); 105 | 106 | //add todays date as the default end date 107 | var now = new Date(); 108 | var decade = new Date(2000, 0, 1); 109 | $('#end_date').val( now.toISOString().split('T')[0]); 110 | $('#start_date').val( decade.toISOString().split('T')[0] ); 111 | 112 | 113 | //JS handle enter press top form 114 | $('#issuer_ca_id_form .fingerprint-input').keypress(function (e) { 115 | if (e.which == 13) { 116 | $('#fingerprint-to-cert-id-btn').click(); 117 | return false; 118 | } 119 | }); 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /cacheck/static/js/summary.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018, Google LLC 3 | * Licensed under the Apache License, Version 2.0 (the `License`); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an `AS IS` BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 'use strict'; 17 | 18 | function fixSummaryLinkURLParams( jq_obj, base_params ){ 19 | //jq_obj.attr('href', jq_obj.attr('href') + '?' + base_params); 20 | jq_obj.href = jq_obj.href + '&' + base_params; 21 | } 22 | 23 | window.addEventListener('load', function () { 24 | 25 | console.log( "Fixing Summary link" ); 26 | var win_params = location.search.split('?')[1]; 27 | $('.ca_summary_link').each(function( index ){ 28 | fixSummaryLinkURLParams(this, win_params); 29 | }); 30 | 31 | $('.lint_issue_link').each(function( index ){ 32 | fixSummaryLinkURLParams(this, win_params); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /cacheck/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /cacheck/templates/lint.html: -------------------------------------------------------------------------------- 1 | {% macro print_lint_issues(lints) %} 2 | {% for issue in lints %} 3 | 4 | {{ issue['certificate_id'] }} 5 | {{ issue['not_before_date'] }} 6 | {{ issue['not_after_date'] }} 7 | {{ issue['subject_cn'] }} 8 | {{ issue['onecrl_revoked'] }} 9 | {{ issue['google_revoked'] }} 10 | {{ issue['microsoft_revoked'] }} 11 | {{ issue['sha256_fingerprint'] }} 12 | 13 | {% endfor %} 14 | {%- endmacro %} 15 | 16 | 17 | 18 | 19 | CA Misissuance Checker 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 |
40 | 41 | 42 | {% for issuer, issues in issuers.items() %} 43 |
Issuer Name: {{ issues[0]['issuer_cn'] }}
44 |
Linter: {{issues[0]['linter']}}
45 |
Issue: {{ issues[0]['issue_text'] }}
46 |

{{ issues|length }} certificates with this lint issue under this CA.

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {{ print_lint_issues(issues) }} 64 | 65 | 66 |
Cert IDNot BeforeNot AfterSubject CNOneCRL RevokedGoogle RevokedMicrosoft RevokedSHA-256 fingerprint
67 | {% endfor %} 68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /cacompliance/README.md: -------------------------------------------------------------------------------- 1 | # cacompliencereport 2 | -------------------------------------------------------------------------------- /cacompliance/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python311 2 | service: cacompliance 3 | 4 | #entrypoint: python main.py --port $PORT 5 | #env: flex 6 | 7 | handlers: 8 | # This configures Google App Engine to serve the files in the app's static 9 | # directory. 10 | - url: /static 11 | static_dir: static 12 | 13 | # This handler routes all requests not caught above to your main app. It is 14 | # required when static routes are defined, but can be omitted (along with 15 | # the entire handlers section) when there are no static files defined. 16 | #- url: /.* 17 | #script: auto 18 | -------------------------------------------------------------------------------- /cacompliance/config.py: -------------------------------------------------------------------------------- 1 | class Config(object): 2 | DEBUG = True 3 | BZ_URL = 'https://bugzilla.mozilla.org' 4 | #BZ_URL = 'https://bugzilla.allizom.org' 5 | BUG_COMPONENT = 'CA Certificate Compliance' 6 | BUG_PRODUCT = 'NSS' 7 | BUG_VERSION = 'unspecified' 8 | BZ_API_KEY = 'H2F3EETohIKDBfxjWg6snBk5xYKesohFTi1ugDYx' 9 | 10 | -------------------------------------------------------------------------------- /cacompliance/index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | - kind: visit 4 | ancestor: yes 5 | properties: 6 | - name: timestamp 7 | direction: desc 8 | -------------------------------------------------------------------------------- /cacompliance/main.py: -------------------------------------------------------------------------------- 1 | # [START gae_python311_app] 2 | from flask import Flask, render_template, request, jsonify, make_response 3 | from collections import Counter 4 | import datetime 5 | from datetime import datetime, timedelta, date 6 | import bugzilla 7 | 8 | app = Flask(__name__) 9 | app.config.from_object('config.Config') 10 | 11 | bzapi = bugzilla.Bugzilla(app.config['BZ_URL']) 12 | 13 | def query_bugs(): 14 | #no query params to search based on time 15 | 16 | fields = [ 'id', 'weburl', 'summary', 'status', 'resolution', 'creator', 'last_change_time', 'is_open', 'is_confirmed', 'creation_time', 17 | 'assigned_to' ] 18 | 19 | #version=app.config['BUG_VERSION'], 20 | #status="NEW" 21 | query = bzapi.build_query( 22 | product=app.config['BUG_PRODUCT'], 23 | component=app.config['BUG_COMPONENT'], 24 | include_fields=fields) 25 | 26 | print("Fetching bugs...") 27 | bugs = bzapi.query(query) 28 | print("Got {} bugs form bugzilla".format(len(bugs))) 29 | return bugs 30 | 31 | def extract_bug_info(bugs): 32 | time_delta = datetime.today() - timedelta(weeks=1) 33 | 34 | #unresolved_bugs = list(filter(lambda x: x.status != 'RESOLVED' or x.status != 'FIXED', bugs)) 35 | unresolved_bugs = list(filter(lambda x: x.is_open, bugs)) 36 | recent_bugs = list(filter(lambda x: x.creation_time > time_delta, bugs)) 37 | updated_bugs = list(filter(lambda x: x.last_change_time > time_delta and x not in recent_bugs, bugs)) 38 | 39 | return unresolved_bugs, recent_bugs, updated_bugs 40 | 41 | 42 | 43 | @app.template_filter('parse_user') 44 | def parse_user(email_str): 45 | return email_str.split('@')[0] 46 | 47 | @app.template_filter('parse_timedelta') 48 | def parse_timedelta(ts): 49 | ts_datetime = datetime.strptime(ts.value, "%Y%m%dT%H:%M:%S") 50 | now = datetime.now() 51 | now.replace(microsecond=0) 52 | timediff = now - ts_datetime 53 | return "{} days, {} hours, and {} minutes ago".format(timediff.days, timediff.seconds // 3600, (timediff.seconds // 60)% 60) 54 | 55 | @app.route('/') 56 | def root(): 57 | """ 58 | Renders CAChecker index page. 59 | """ 60 | today = datetime.today() 61 | last_week = today - timedelta(weeks=1) 62 | un_b, re_b, up_b = extract_bug_info( query_bugs() ) 63 | resp = make_response( render_template( 'index.html', unresolved_bugs=un_b, 64 | recent_bugs=re_b, 65 | updated_bugs=up_b, 66 | date_start=last_week.date(), 67 | date_end=today.date() ) ) 68 | return resp 69 | 70 | if __name__ == '__main__': 71 | app.run(host='127.0.0.1', port=8080, debug=True) 72 | # [END gae_python311_app] 73 | -------------------------------------------------------------------------------- /cacompliance/main_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import main 16 | 17 | 18 | def test_index(): 19 | main.app.testing = True 20 | client = main.app.test_client() 21 | 22 | r = client.get('/') 23 | assert r.status_code == 200 24 | -------------------------------------------------------------------------------- /cacompliance/requirements.txt: -------------------------------------------------------------------------------- 1 | Click==7.0 2 | Flask==2.3.2 3 | itsdangerous==1.1.0 4 | Jinja2==3.1.3 5 | MarkupSafe==1.1.1 6 | Werkzeug==3.0.1 7 | requests 8 | python-bugzilla==2.3.0 9 | -------------------------------------------------------------------------------- /cacompliance/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "helvetica", sans-serif; 3 | text-align: center; 4 | } 5 | .tool { 6 | margin-top: 1em; 7 | margin-bottom: 1em; 8 | } 9 | 10 | .cert_info_label { padding-left:2em; float:left; padding-right:0.5em; max-width:20%;} 11 | .cert_info_value { float:right; max-width:80%;} 12 | 13 | .F { color: #fb0a06; font-weight:bold;} 14 | .E { color: #cc3300; font-weight:bold;} 15 | .W { color: #ffcc00; font-weight:bold;} 16 | -------------------------------------------------------------------------------- /cacompliance/templates/index.html: -------------------------------------------------------------------------------- 1 | {% macro print_bug(bug) %} 2 | 3 | {{bug.status}} 4 | {{bug.summary}} 5 | {{bug.creator | parse_user }} 6 | 7 | {% if bug.is_confirmed %} 8 | 9 | {% else %} 10 | 11 | {% endif %} 12 | 13 | {{bug.last_change_time | parse_timedelta}} 14 | {{bug.assigned_to | parse_user }} 15 | link 16 | 17 | {%- endmacro %} 18 | 19 | {# 20 | {% macro print_bug(bug) -%} 21 |
  • 22 |

    Summary: {{bug.summary}}, Status: {{bug.status}}, Creator: {{bug.creator}}, Resolution: {{bug.resolution}}, Open: {{bug.is_open}}, Last Modified: {{bug.last_change_time}}, Confirmed: {{bug.is_confirmed}} link

    23 |
  • 24 | {%- endmacro %} 25 | #} 26 | 27 | {% macro table_footer() %} 28 | 29 | 30 | {% endmacro %} 31 | 32 | {% macro table_header() %} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {%endmacro %} 47 | 48 | 49 | 50 | 51 | 52 | 53 | CA Misissuance Checker 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
    69 |
    70 | 71 |

    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 |

    Certificate Viewer

    15 |
    16 |
    17 | {{template "main" .}} 18 |
    19 | 20 | 21 | {{end}} -------------------------------------------------------------------------------- /certViewer/ui/html/pages/home.tmpl: -------------------------------------------------------------------------------- 1 | {{define "title"}}Certificate Viewer{{end}} 2 | 3 | {{define "main"}} 4 | {{if .Certificate.Serial}} 5 | {{template "basicInfo" .}} 6 | {{template "certExt" .}} 7 | {{end}} 8 | {{if .Certificate.X509v3Extensions.SubjectAlternativeName}} 9 | {{template "san" .}} 10 | {{end}} 11 | 12 | 13 | {{with .Form.FieldErrors.rootCertUpload}} 14 | 15 | {{end}} 16 |
    17 | 18 |
    19 | {{with .Form.FieldErrors.rootCert}} 20 | 21 | {{end}} 22 |
    23 | 24 |
    25 |
    26 | 27 |
    28 | 29 | {{end}} -------------------------------------------------------------------------------- /certViewer/ui/html/partials/basicInfo.tmpl: -------------------------------------------------------------------------------- 1 | {{define "basicInfo"}} 2 | {{with .Certificate}} 3 |

    Basic Info

    4 |
    StatusSummaryCreatorConfirmedModifiedAssignedLink
    5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
    Version{{.Version}}
    Serial Number{{.Serial}}
    Subject{{.Subject}}
    Issuer{{.Issuer}}
    Not Before{{.Validity.NotBefore}}
    Not After{{.Validity.NotAfter}}
    Signature Algorithm{{.SignatureAlgorithm}}
    Key Size{{.Key.Size}}
    Exponent{{.Key.Exponent}}
    SHA1 Hash{{.Hashes.SHA1}}
    SHA256 Hash{{.Hashes.SHA256}}
    SPKI SHA256{{.Hashes.SPKISHA256}}
    Subject SPKI SHA256{{.Hashes.SubjectSPKISHA256}}
    HPKP PIN-SHA256{{.Hashes.PKPSHA256}}
    62 | {{end}} 63 | {{end}} 64 | -------------------------------------------------------------------------------- /certViewer/ui/html/partials/certExt.tmpl: -------------------------------------------------------------------------------- 1 | {{define "certExt"}} 2 | {{with .Certificate}} 3 |

    Certificate Extensions

    4 | 5 | {{if .X509v3Extensions.AuthorityKeyId}} 6 | 7 | 8 | 9 | 10 | {{end}} 11 | {{if .X509v3Extensions.SubjectKeyId}} 12 | 13 | 14 | 15 | 16 | {{end}} 17 | {{if .X509v3Extensions.KeyUsage}} 18 | 19 | 20 | 21 | 22 | {{end}} 23 | {{if .X509v3Extensions.ExtendedKeyUsage}} 24 | 25 | 26 | 27 | 28 | {{end}} 29 | {{if .X509v3Extensions.ExtendedKeyUsageOID}} 30 | 31 | 32 | 33 | 34 | {{end}} 35 | {{if .X509v3Extensions.PolicyIdentifiers}} 36 | 37 | 38 | 39 | 40 | {{end}} 41 | {{if .X509v3Extensions.CRLDistributionPoints}} 42 | 43 | 44 | 45 | 46 | {{end}} 47 | {{if .X509v3Extensions.PermittedDNSDomains}} 48 | 49 | 50 | 51 | 52 | {{end}} 53 | {{if .X509v3Extensions.ExcludedDNSDomains}} 54 | 55 | 56 | 57 | 58 | {{end}} 59 | {{if .X509v3Extensions.PermittedIPAddresses}} 60 | 61 | 62 | 63 | 64 | {{end}} 65 | {{if .X509v3Extensions.ExcludedIPAddresses}} 66 | 67 | 68 | 69 | 70 | {{end}} 71 | {{if .X509v3Extensions.InhibitAnyPolicy}} 72 | 73 | 74 | 75 | 76 | {{end}} 77 | 78 | 79 | 80 | 81 |
    AuthorityKeyID{{.X509v3Extensions.AuthorityKeyId}}
    SubjectKeyId{{.X509v3Extensions.SubjectKeyId}}
    KeyUsage{{.X509v3Extensions.KeyUsage}}
    ExtendedKeyUsage{{.X509v3Extensions.ExtendedKeyUsage}}
    ExtendedKeyUsageOID{{.X509v3Extensions.ExtendedKeyUsageOID}}
    PolicyIdentifiers{{.X509v3Extensions.PolicyIdentifiers}}
    CRLDistributionPoints{{.X509v3Extensions.CRLDistributionPoints}}
    PermittedDNSDomains{{.X509v3Extensions.PermittedDNSDomains}}
    ExcludedDNSDomains{{.X509v3Extensions.ExcludedDNSDomains}}
    PermittedIPAddresses{{.X509v3Extensions.PermittedIPAddresses}}
    ExcludedIPAddresses{{.X509v3Extensions.ExcludedIPAddresses}}
    InhibitAnyPolicy{{.X509v3Extensions.InhibitAnyPolicy}}
    BasicConstraintsCA: {{.CA}}
    82 | {{end}} 83 | {{end}} 84 | -------------------------------------------------------------------------------- /certViewer/ui/html/partials/san.tmpl: -------------------------------------------------------------------------------- 1 | {{define "san"}} 2 | {{with .Certificate}} 3 |

    Subject Alternative Names

    4 | 5 | {{range .X509v3Extensions.SubjectAlternativeName}} 6 | 7 | 8 | 9 | {{end}} 10 |
    {{.}}
    11 | {{end}} 12 | {{end}} 13 | -------------------------------------------------------------------------------- /certViewer/ui/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/CCADB-Tools/ea10e50c76c4cc331efd7afaaf6f66589414a0d7/certViewer/ui/static/img/favicon.ico -------------------------------------------------------------------------------- /certdataDiffCCADB/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 | ENV CGO_ENABLED=0 10 | RUN go build -o certdataDiffCCADB main.go 11 | 12 | FROM alpine:latest 13 | RUN apk --update add ca-certificates 14 | COPY --from=buildStage /opt/ /opt/ 15 | 16 | CMD ["/opt/certdataDiffCCADB", "--serve"] -------------------------------------------------------------------------------- /certdataDiffCCADB/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 | .PHONY: clean 6 | clean: 7 | -docker stop certdatadiffccadb 8 | -docker rm certdatadiffccadb 9 | -docker rmi certdatadiffccadb 10 | -docker image prune -f 11 | -docker image prune -f --filter label=stage=intermediate 12 | 13 | .PHONY: build 14 | build: 15 | docker build --rm -t certdatadiffccadb:latest . 16 | docker image prune -f 17 | docker image prune -f --filter label=stage=intermediate 18 | 19 | .PHONY: run 20 | run: 21 | # If the container exists, then; 22 | # Check to see if it is running 23 | # If running; then no-op 24 | # Else; start 25 | # If it does not exist, then; 26 | # Start the container with the default port mapping of 8080 -> 80 and name of certdatadiffccadb 27 | ( \ 28 | docker container inspect certdatadiffccadb > /dev/null 2>&1 && ( \ 29 | docker top certdatadiffccadb > /dev/null 2>&1 || docker start certdatadiffccadb) \ 30 | ) \ 31 | || docker run --name certdatadiffccadb -d -e "PORT=80" -p 8080:80 certdatadiffccadb -------------------------------------------------------------------------------- /certdataDiffCCADB/README.md: -------------------------------------------------------------------------------- 1 | certdataDiffCCADB 2 | ------------------------------- 3 | This tool finds the differences between a given `certdata.txt` file and the contents of a CCADB PEM report. 4 | 5 | The three "buckets" of output are: 6 | 7 | * Matched, wherein the entries are both in `certdata.txt` as well as the CCADB 8 | * Unmatched and Trusted, wherein the entries are either in `certdata.txt` or in the CCADB but NOT in both AND their trust bits are set to `true`. 9 | * Unmatched and Untrusted, wherein the entries are either in `certdata.txt` or in the CCADB but NOT in both AND their trust bits are set to `false`. 10 | 11 | 12 | Usage 13 | ------------------------------- 14 | ``` 15 | Usage of ./certdataDiffCCADB: 16 | -ccadb string 17 | Path to CCADB report file. 18 | -ccadburl string 19 | URL to CCADB report file. (default "https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV") 20 | -cd string 21 | Path to certdata.txt 22 | -cdurl string 23 | URL to certdata.txt (default "https://hg.mozilla.org/releases/mozilla-beta/raw-file/tip/security/nss/lib/ckfw/builtins/certdata.txt") 24 | -o string 25 | Path to the output directory. 26 | -serve 27 | Start in server mode. While in server mode the /certdata endpoint is available which downloads a copy of certdata.txt from the default URL and returns a simplified JSON representation. This option requires that the PORT environment variable be set. 28 | ``` 29 | 30 | __Examples:__ 31 | 32 | * Output three JSON files (matched.json, unmatchedTrusted.json, and unmatchedUntrusted.json) to the current directory using the default URLs to pull the CCADB report and certdata.txt 33 | 34 | $ ./certdataDiffCCADB 35 | 36 | * Output three JSON files (matched.json, unmatchedTrusted.json, and unmatchedUntrusted.json) to /tmp using the default URLs 37 | 38 | $ ./certdataDiffCCADB -o /tmp 39 | 40 | * Output three JSON files (matched.json, unmatchedTrusted.json, and unmatchedUntrusted.json) to the current directory using the default CCADB URL and a local copy of certdata.txt 41 | 42 | $ ./certdataDiffCCADB -cd ~/Downloads/certdata.txt 43 | 44 | * Output three JSON files (matched.json, unmatchedTrusted.json, and unmatchedUntrusted.json) to the current directory using the default certdata.txt URL and a local copy of a CCADB report. 45 | 46 | $ ./certdataDiffCCADB -cd ~/Downloads/IncludedCACertificateReportPEMCSV.csv 47 | 48 | * Output three JSON files (matched.json, unmatchedTrusted.json, and unmatchedUntrusted.json) to the current directory using the default CCADB URL and a different remote URL for certdata.txt 49 | 50 | $ ./certdataDiffCCADB -cdurl https://some.other.domain/raw/certdata.txt 51 | 52 | * Output three JSON files (matched.json, unmatchedTrusted.json, and unmatchedUntrusted.json) to the current directory using the default certdata.txt URL and a local copy of a CCADB report. 53 | 54 | $ ./certdataDiffCCADB -ccadburl https://some.other.domain/raw/ncludedCACertificateReportPEMCSV.csv 55 | 56 | * Start the tool in `serve` mode. 57 | 58 | $ PORT=8080 ./certdataDiffCCADB --serve 59 | -------------------------------------------------------------------------------- /certdataDiffCCADB/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: custom 2 | env: flex 3 | service: certdatadiff 4 | -------------------------------------------------------------------------------- /certdataDiffCCADB/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mozilla/CCADB-Tools/certdataDiffCCADB 2 | 3 | go 1.21 4 | 5 | require github.com/throttled/throttled v2.2.5+incompatible 6 | 7 | require ( 8 | github.com/gomodule/redigo v1.8.2 // indirect 9 | github.com/hashicorp/golang-lru v1.0.2 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /certdataDiffCCADB/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= 3 | github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= 4 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 5 | github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 9 | github.com/throttled/throttled v2.2.5+incompatible h1:65UB52X0qNTYiT0Sohp8qLYVFwZQPDw85uSa65OljjQ= 10 | github.com/throttled/throttled v2.2.5+incompatible/go.mod h1:0BjlrEGQmvxps+HuXLsyRdqpSRvJpq0PNIsOtqP9Nos= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 13 | -------------------------------------------------------------------------------- /certdataDiffCCADB/utils/entry.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 utils 6 | 7 | import ( 8 | "fmt" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | // Certificate normalization constants. 14 | const ( 15 | BEGIN = "-----BEGIN CERTIFICATE-----\n" 16 | END = "-----END CERTIFICATE-----" 17 | WIDTH = 64 // Columns per line https://tools.ietf.org/html/rfc1421 18 | ) 19 | 20 | var stripper *regexp.Regexp 21 | 22 | func init() { 23 | stripper = regexp.MustCompile("('|'|\n|-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----)") 24 | } 25 | 26 | // Entry is a normalized form of a Certificate Authority found 27 | // in either certdata.txt or from a CCADB report CSV. 28 | type Entry struct { 29 | OrganizationName string `json:"organizationName"` 30 | OrganizationalUnitName string `json:"organizationalUnitName"` 31 | CommonName string `json:"commonName"` 32 | SerialNumber string `json:"serialNumber"` 33 | PEM string `json:"-"` 34 | Fingerprint string `json:"sha256"` 35 | TrustWeb bool `json:"trustWeb"` 36 | TrustEmail bool `json:"trustEmail"` 37 | LineNumber int `json:"lineNumber"` 38 | Origin string `json:"origin"` 39 | } 40 | 41 | // UniqueID returns the issuer distinguished name and the serial (noralized with no leading zeroes) 42 | // contatenated together. 43 | func (e *Entry) UniqueID() string { 44 | return fmt.Sprintf("%v%v", e.DistinguishedName(), e.NormalizedSerial()) 45 | } 46 | 47 | // DistinguishedName builds a hierarchical string of Organization, Orgizational Unit, and Common Name. 48 | func (e *Entry) DistinguishedName() string { 49 | return fmt.Sprintf("O=%v/OU=%v/CN=%v", e.OrganizationName, e.OrganizationalUnitName, e.CommonName) 50 | } 51 | 52 | // NormalizedSerial returns the serial number with any leading zeroes stripped off. 53 | func (e *Entry) NormalizedSerial() string { 54 | return strings.TrimLeft(e.SerialNumber, "0") 55 | } 56 | 57 | // NewEntry constructs a new Entry with a normalized PEM. 58 | func NewEntry(org, orgUnit, commonName, serial, pem, fingerprint string, trustWeb, trustEmail bool, line int, origin string) *Entry { 59 | return &Entry{org, orgUnit, commonName, serial, NormalizePEM(pem), fingerprint, trustWeb, trustEmail, line, origin} 60 | } 61 | 62 | // normalizePEM ignores any formatting or string artifacts that the PEM may have had 63 | // and applies https://tools.ietf.org/html/rfc1421 64 | // 65 | // This stemmed from noticing that CCADB reports were fully formed while certdata 66 | // PEMS had no formatting nor BEGIN/END fields. This is simply avoiding any surprises 67 | // in individual formatting choices by forcing both to strip all formatting and conform 68 | // to the one, chosen, way. 69 | func NormalizePEM(pem string) string { 70 | if len(pem) == 0 { 71 | return "" 72 | } 73 | pem = stripper.ReplaceAllString(pem, "") 74 | p := []byte(pem) 75 | fmted := []byte(BEGIN) 76 | width := WIDTH 77 | for len(p) > 0 { 78 | if len(p) < WIDTH { 79 | width = len(p) 80 | } 81 | fmted = append(fmted, p[:width]...) 82 | fmted = append(fmted, '\n') 83 | p = p[width:] 84 | } 85 | fmted = append(fmted, END...) 86 | return string(fmted) 87 | } 88 | -------------------------------------------------------------------------------- /certdataDiffCCADB/utils/pair.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 utils 6 | 7 | // Pair holds a normalized certdata.txt entry, it's sister CCADB, 8 | // as well a slice of what fields between the two are differnt. 9 | type Pair struct { 10 | Certdata *Entry 11 | CCADB *Entry 12 | Diffs []string 13 | } 14 | 15 | // MapPairs map all of the entries in certdata.txt to entries in the CCADB 16 | // report. Entries are matched together if they: 17 | // 18 | // 1. Have the same serial number (normalized for leading zeroes) 19 | // or 20 | // 2. They have the exact same PEM. 21 | // or 22 | // 3. They have the exact same Common Name. 23 | // 24 | // Any entries for which a mapping could not be made are returned 25 | // in the 'rest' slice. 26 | func MapPairs(cd, ccadb []*Entry) (pairs []Pair, unmatchedT []*Entry, unmatchedUT []*Entry) { 27 | idMap := make(map[string]*Entry) // DN || serial 28 | ftocd := make(map[string]*Entry) // fingerprint 29 | for _, e := range cd { 30 | idMap[e.UniqueID()] = e 31 | ftocd[e.Fingerprint] = e 32 | } 33 | var pair Pair 34 | var match *Entry 35 | var ok bool 36 | for _, e := range ccadb { 37 | id := e.UniqueID() 38 | if match, ok = idMap[id]; ok { 39 | pair = NewPair(match, e) 40 | } else if match, ok = ftocd[e.Fingerprint]; ok { 41 | pair = NewPair(match, e) 42 | } else { 43 | if !(e.TrustWeb || e.TrustEmail) { 44 | unmatchedUT = append(unmatchedUT, e) 45 | } else { 46 | unmatchedT = append(unmatchedT, e) 47 | } 48 | continue 49 | } 50 | pairs = append(pairs, pair) 51 | // Avoid matching duplicates 52 | delete(idMap, match.UniqueID()) 53 | delete(ftocd, match.Fingerprint) 54 | } 55 | for _, e := range idMap { 56 | if !(e.TrustWeb || e.TrustEmail) { 57 | unmatchedUT = append(unmatchedUT, e) 58 | } else { 59 | unmatchedT = append(unmatchedT, e) 60 | } 61 | } 62 | return 63 | } 64 | 65 | // NewPair discovers the difference between a matched certdata.txt entry 66 | // and a CCADB report entry and constructs a new Pair. 67 | func NewPair(cd, ccadb *Entry) (p Pair) { 68 | p.Certdata = cd 69 | p.CCADB = ccadb 70 | p.Diffs = make([]string, 0) 71 | if cd.OrganizationName != ccadb.OrganizationName { 72 | p.Diffs = append(p.Diffs, "OrganizationName") 73 | } 74 | if cd.OrganizationalUnitName != ccadb.OrganizationalUnitName { 75 | p.Diffs = append(p.Diffs, "OrganizationalUnitName") 76 | } 77 | if cd.CommonName != ccadb.CommonName { 78 | p.Diffs = append(p.Diffs, "CommonName") 79 | } 80 | if cd.SerialNumber != ccadb.SerialNumber { 81 | p.Diffs = append(p.Diffs, "SerialNumber") 82 | } 83 | if cd.PEM != ccadb.PEM { 84 | p.Diffs = append(p.Diffs, "PEM") 85 | } 86 | if cd.Fingerprint != ccadb.Fingerprint { 87 | p.Diffs = append(p.Diffs, "Fingerprint") 88 | } 89 | if cd.TrustWeb != ccadb.TrustWeb { 90 | p.Diffs = append(p.Diffs, "TrustWeb") 91 | } 92 | if cd.TrustEmail != ccadb.TrustEmail { 93 | p.Diffs = append(p.Diffs, "TrustEmail") 94 | } 95 | return 96 | } 97 | -------------------------------------------------------------------------------- /certificate/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:bookworm AS buildStage 6 | 7 | WORKDIR /opt 8 | COPY . . 9 | RUN go build . 10 | 11 | FROM debian:bookworm-slim 12 | 13 | COPY --from=buildStage /opt/ /opt/ 14 | 15 | CMD ["/opt/certificate"] -------------------------------------------------------------------------------- /certificate/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 certificate 7 | -docker rm certificate 8 | -docker rmi certificate 9 | -docker image prune -f 10 | -docker image prune -f --filter label=stage=intermediate 11 | 12 | build: 13 | docker build --rm -t certificate:latest . 14 | docker image prune -f 15 | docker image prune -f --filter label=stage=intermediate 16 | 17 | run: 18 | ./run.sh -------------------------------------------------------------------------------- /certificate/README.md: -------------------------------------------------------------------------------- 1 | Certificate (JSON from PEM) 2 | ----------------- 3 | 4 | ### Use Cases 5 | 6 | Certificate takes a PEM-encoded certificate and outputs JSON containing the parsed certificate and its raw X509 version encoded with base64. 7 | 8 | ### Deployment 9 | 10 | #### Locally 11 | When running `certificate` locally: 12 | 13 | ```sh 14 | $ go build -o certificate . 15 | $ PORT=8080 ./certificate 16 | ``` 17 | 18 | #### Using Docker 19 | Alternatively, one may use the provided `Dockerfile` and `Makefile`: 20 | 21 | ```sh 22 | $ make clean build run 23 | ``` 24 | 25 | ### Usage 26 | 27 | Certificate offers one endpoint - `/certificate` 28 | 29 | For example: 30 | 31 | ```sh 32 | # Submit a PEM file and get back JSON output. 33 | $ curl -X POST -F certificate=@example.pem http://localhost:8080/certificate 34 | ``` 35 | -------------------------------------------------------------------------------- /certificate/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: custom 2 | env: flex 3 | service: certificate 4 | -------------------------------------------------------------------------------- /certificate/go.mod: -------------------------------------------------------------------------------- 1 | module CCADB-Tools/certificate 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gin-contrib/logger v0.3.0 7 | github.com/gin-gonic/gin v1.9.1 8 | github.com/rs/zerolog v1.31.0 9 | golang.org/x/crypto v0.17.0 10 | ) 11 | 12 | require ( 13 | github.com/bytedance/sonic v1.10.2 // indirect 14 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 15 | github.com/chenzhuoyu/iasm v0.9.1 // indirect 16 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 17 | github.com/gin-contrib/sse v0.1.0 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/go-playground/validator/v10 v10.16.0 // indirect 21 | github.com/goccy/go-json v0.10.2 // indirect 22 | github.com/json-iterator/go v1.1.12 // indirect 23 | github.com/klauspost/cpuid/v2 v2.2.6 // indirect 24 | github.com/kr/text v0.2.0 // indirect 25 | github.com/leodido/go-urn v1.2.4 // indirect 26 | github.com/mattn/go-colorable v0.1.13 // indirect 27 | github.com/mattn/go-isatty v0.0.20 // indirect 28 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 29 | github.com/modern-go/reflect2 v1.0.2 // indirect 30 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 31 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 32 | github.com/ugorji/go/codec v1.2.12 // indirect 33 | golang.org/x/arch v0.6.0 // indirect 34 | golang.org/x/net v0.19.0 // indirect 35 | golang.org/x/sys v0.15.0 // indirect 36 | golang.org/x/text v0.14.0 // indirect 37 | google.golang.org/protobuf v1.31.0 // indirect 38 | gopkg.in/yaml.v3 v3.0.1 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /certificate/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 | /* The following contains some adapted code from: 6 | * https://github.com/mozilla/tls-observatory/blob/7bc42856d2e5594614b56c2f55baf42bb9751b3d/tlsobs-api/handlers.go */ 7 | 8 | package main 9 | 10 | import ( 11 | "crypto/x509" 12 | "encoding/pem" 13 | "io" 14 | "net/http" 15 | "os" 16 | 17 | "github.com/gin-contrib/logger" 18 | "github.com/gin-gonic/gin" 19 | "github.com/rs/zerolog" 20 | "github.com/rs/zerolog/log" 21 | ) 22 | 23 | func main() { 24 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) 25 | 26 | // ReleaseMode is for production -- no debugging 27 | gin.SetMode(gin.ReleaseMode) 28 | 29 | // Default to port 8080 if PORT env var is not set 30 | port := getPortEnv("PORT", "8080") 31 | 32 | router := gin.Default() 33 | // Use zerolog for gin's logging 34 | router.Use(logger.SetLogger()) 35 | router.POST("/certificate", postCertificate) 36 | err := router.Run(":" + port) 37 | if err != nil { 38 | return 39 | } 40 | } 41 | 42 | // getPortEnv looks for the PORT env var and uses fallback if not set 43 | func getPortEnv(port, fallback string) string { 44 | if value, ok := os.LookupEnv(port); ok { 45 | return value 46 | } 47 | return fallback 48 | } 49 | 50 | // postCertificate does all of the certificate parsing on POST 51 | func postCertificate(c *gin.Context) { 52 | logger.SetLogger() 53 | 54 | certHeader, err := c.FormFile("certificate") 55 | if err != nil { 56 | log.Error().Err(err).Msg("Could not read certificate from request") 57 | c.String(http.StatusBadRequest, "Could not read certificate from request: %s", err.Error()) 58 | return 59 | } 60 | 61 | certReader, err := certHeader.Open() 62 | if err != nil { 63 | log.Error().Err(err).Msg("Could not open certificate from form data") 64 | c.String(http.StatusBadRequest, "Could not open certificate from form data: %s", err.Error()) 65 | return 66 | } 67 | 68 | certPEM, err := io.ReadAll(certReader) 69 | if err != nil { 70 | log.Error().Err(err).Msg("Could not read certificate from form data") 71 | c.String(http.StatusBadRequest, "Could not read certificate from form data: %s", err.Error()) 72 | return 73 | } 74 | 75 | block, _ := pem.Decode(certPEM) 76 | if block == nil { 77 | log.Error().Err(err).Msg("Failed to parse certificate PEM") 78 | c.String(http.StatusBadRequest, "Failed to parse certificate PEM") 79 | return 80 | } 81 | 82 | certX509, err := x509.ParseCertificate(block.Bytes) 83 | if err != nil { 84 | log.Error().Err(err).Msg("Could not parse X.509 certificate") 85 | c.String(http.StatusBadRequest, "Could not parse X.509 certificate: %s", err.Error()) 86 | return 87 | } 88 | 89 | cert := CertToJSON(certX509) 90 | 91 | c.JSON(http.StatusCreated, cert) 92 | } 93 | -------------------------------------------------------------------------------- /certificate/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 certificate \ 13 | -d \ 14 | -e "PORT=$CONTAINER_PORT" \ 15 | -p ${HOST_PORT}:${CONTAINER_PORT} \ 16 | certificate 17 | 18 | -------------------------------------------------------------------------------- /crlVerification/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 | # This is necessary to statically compile all 10 | # C libraries into the executable. Otherwise 11 | # the Alpine installation will fail out with 12 | # a "no such directory" when attempting to execute 13 | # the binary (it can't find the shared libs). 14 | ENV CGO_ENABLED=0 15 | RUN go test ./... && go build main.go 16 | 17 | FROM alpine:latest 18 | 19 | COPY --from=buildStage /opt/ /opt/ 20 | 21 | CMD ["/opt/main"] -------------------------------------------------------------------------------- /crlVerification/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 crlverification 7 | -docker rm crlverification 8 | -docker rmi crlverification 9 | -docker image prune -f 10 | -docker image prune -f --filter label=stage=intermediate 11 | 12 | build: 13 | docker build --rm -t crlverification:latest . 14 | docker image prune -f 15 | docker image prune -f --filter label=stage=intermediate 16 | 17 | run: 18 | ./run.sh -------------------------------------------------------------------------------- /crlVerification/README.md: -------------------------------------------------------------------------------- 1 | crlVerification 2 | ------------------- 3 | 4 | ### Use Cases 5 | 6 | Checks that the revoked certificate’s entry for a given CRL is as expected. That is: 7 | 8 | * It’s there (I.E. there is an entry in the given CRL whose serial matches the given serial), 9 | * Its revocation date matches that which is given to this tool. 10 | * Its revocation reason matches that which is given to this tool. 11 | 12 | This tool is used by “Verify Revocation” in CCADB. 13 | 14 | ### Deployment 15 | 16 | #### Locally 17 | When running `crlVerification` locally: 18 | 19 | $ go build . 20 | $ PORT=8080 ./crlVerification 21 | 22 | #### Using Docker 23 | Alternatively, one may use the provided `Dockerfile` and `Makefile`: 24 | 25 | $ make clean build run 26 | 27 | ### Usage 28 | 29 | The following is the expected (pseudo-code) JSON input 30 | ```json 31 | struct Input { 32 | "crl": Optional, 33 | "serial": String(Hex), 34 | "revocationDate": String(YYYY/MM/DD), 35 | "revocationReason": Optional 36 | } 37 | 38 | ReasonCode enum { 39 | "(0) unspecified" 40 | "(1) keyCompromise" 41 | "(2) cACompromise" 42 | "(3) affiliationChanged" 43 | "(4) superseded" 44 | "(5) cessationOfOperation" 45 | "(6) certificateHold" 46 | "(8) removeFromCRL" 47 | "(9) privilegeWithdrawn" 48 | "(10) aACompromise" 49 | } 50 | ``` 51 | 52 | 53 | An example call is the following cURL invocation. This application serves only one endpoint, so no resource is necessary in the URL. 54 | 55 | ```bash 56 | $ curl -d '{"crl": "http://crl.ws.symantec.com/pca1-g3.crl","serial": "fc788d52d4441678243b9882cb15b4","revocationDate": "2019/05/07"}' http://crlVerification.example.org 57 | ``` 58 | 59 | The following are a series of example inputs alonside their results. 60 | 61 | ```json 62 | // PASS case 63 | input = { 64 | "crl": "http://crl.ws.symantec.com/pca1-g3.crl", 65 | "serial": "fc788d52d4441678243b9882cb15b4", 66 | "revocationDate": "2019/05/07" 67 | } 68 | 69 | // Note that "Errors" is an array of strings, as multiple errors may be detected. 70 | output = { 71 | "Errors": [], 72 | "Result": "PASS" 73 | } 74 | ``` 75 | 76 | ```json 77 | // Wrong date and/or revocation reason. If both are wrong, then both will be provided. 78 | input = { 79 | "crl": "http://crl.ws.symantec.com/pca1-g3.crl", 80 | "serial": "fc788d52d4441678243b9882cb15b4", 81 | "revocationDate": "2019/12/13", 82 | "revocationReason": "(10) aACompromise" 83 | } 84 | 85 | output = { 86 | "Errors": [ 87 | "Revocation dates did not match. We wanted 2019/12/13, but got 2019/05/07", 88 | "Revocation reasons did not match. We wanted (10) aACompromise, but got no reason given" 89 | ], 90 | "Result": "FAIL" 91 | } 92 | ``` 93 | 94 | ```json 95 | // Missing CRL URL 96 | input = { 97 | "serial": "fc788d52d4441678243b9882cb15b4", 98 | "revocationDate": "2019/12/13", 99 | "revocationReason": "(10) aACompromise" 100 | } 101 | 102 | output = 103 | "Errors": [ 104 | "No CRL URL was provided" 105 | ], 106 | "Result": "FAIL" 107 | } 108 | ``` 109 | 110 | ```json 111 | // Serial number not found in CRL 112 | input = { 113 | "crl": "http://crl.ws.symantec.com/pca1-g3.crl", 114 | "serial": "1", 115 | "revocationDate": "2019/12/13", 116 | "revocationReason": "(10) aACompromise" 117 | } 118 | 119 | output = { 120 | "Errors": [ 121 | "\"01\" was not found in the given CRL" 122 | ], 123 | "Result": "FAIL" 124 | } 125 | ``` 126 | -------------------------------------------------------------------------------- /crlVerification/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: custom 2 | env: flex 3 | service: crlverification 4 | -------------------------------------------------------------------------------- /crlVerification/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mozilla/CCADB-Tools/crlVerification 2 | 3 | go 1.21 4 | 5 | require github.com/pkg/errors v0.9.1 6 | -------------------------------------------------------------------------------- /crlVerification/go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 2 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | -------------------------------------------------------------------------------- /crlVerification/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 crlverification \ 13 | -d \ 14 | -e "PORT=$CONTAINER_PORT" \ 15 | -p ${HOST_PORT}:${CONTAINER_PORT} \ 16 | crlverification 17 | 18 | -------------------------------------------------------------------------------- /crlVerification/utils/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 utils 6 | 7 | import ( 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "errors" 11 | "fmt" 12 | "io/ioutil" 13 | "log" 14 | "net/http" 15 | ) 16 | 17 | type CRLNotGiven struct{} 18 | 19 | func (c CRLNotGiven) Error() string { 20 | return "No CRL URL was provided" 21 | } 22 | 23 | type CRLDownloadFailed struct { 24 | url string 25 | err error 26 | } 27 | 28 | func (c CRLDownloadFailed) Error() string { 29 | // @TODO test this fmting 30 | return fmt.Sprintf("%s failed to download. error: %v", c.url, c.err) 31 | } 32 | 33 | type CRLFailedToParse struct { 34 | url string 35 | err error 36 | } 37 | 38 | func (c CRLFailedToParse) Error() string { 39 | // @TODO test this fmting 40 | return fmt.Sprintf("%s failed to parse. error: %v", c.url, c.err) 41 | } 42 | 43 | func CRLFromURL(crlUrl string) (*pkix.CertificateList, error) { 44 | resp, err := http.Get(crlUrl) 45 | if err != nil { 46 | return nil, CRLDownloadFailed{crlUrl, err} 47 | } 48 | if resp.StatusCode != http.StatusOK { 49 | return nil, CRLDownloadFailed{crlUrl, errors.New(fmt.Sprintf("recieved status code %v", resp.StatusCode))} 50 | } 51 | defer func() { 52 | if err := resp.Body.Close(); err != nil { 53 | log.Printf("%v\n", err) 54 | } 55 | }() 56 | body, err := ioutil.ReadAll(resp.Body) 57 | if err != nil { 58 | return nil, CRLDownloadFailed{crlUrl, err} 59 | } 60 | crl, err := x509.ParseCRL(body) 61 | if err != nil { 62 | return nil, CRLFailedToParse{crlUrl, err} 63 | } 64 | return crl, nil 65 | } 66 | -------------------------------------------------------------------------------- /crlVerification/utils/revocationDate.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 utils 6 | 7 | import ( 8 | "crypto/x509/pkix" 9 | "fmt" 10 | "time" 11 | ) 12 | 13 | const TimeFormat = "2006/01/02" 14 | 15 | func TimeFromString(date string) (time.Time, error) { 16 | return time.Parse(TimeFormat, date) 17 | } 18 | 19 | func ValidateRevocationDate(cert pkix.RevokedCertificate, ourRevocationDate time.Time) error { 20 | theirRevocationDate := cert.RevocationTime 21 | if theirRevocationDate.Year() != ourRevocationDate.Year() || theirRevocationDate.Month() != ourRevocationDate.Month() || theirRevocationDate.Day() != ourRevocationDate.Day() { 22 | return RevocationtimeError{ourRevocationDate, theirRevocationDate} 23 | } 24 | return nil 25 | } 26 | 27 | type RevocationtimeError struct { 28 | wanted time.Time 29 | got time.Time 30 | } 31 | 32 | func (r RevocationtimeError) Error() string { 33 | return fmt.Sprintf("Revocation dates did not match. We wanted %s, but got %s", r.wanted.Format(TimeFormat), r.got.Format(TimeFormat)) 34 | } 35 | -------------------------------------------------------------------------------- /crlVerification/utils/serial.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 utils 6 | 7 | import ( 8 | "crypto/x509/pkix" 9 | "encoding/hex" 10 | "fmt" 11 | "math/big" 12 | "reflect" 13 | ) 14 | 15 | type HexDecodeFailed struct { 16 | given string 17 | err error 18 | } 19 | 20 | type SerialNotFound struct { 21 | wanted *big.Int 22 | } 23 | 24 | func (s SerialNotFound) Error() string { 25 | return fmt.Sprintf(`"%s" was not found in the given CRL`, hex.EncodeToString(s.wanted.Bytes())) 26 | } 27 | 28 | func (h HexDecodeFailed) Error() string { 29 | // @TODO test this fmting 30 | return fmt.Sprintf(`The serial number "%s" failed to parse from hex. error: %v`, h.given, h.err) 31 | } 32 | 33 | func BigIntFromHexString(serial string) (*big.Int, error) { 34 | s, err := hex.DecodeString(serial) 35 | if err != nil { 36 | return nil, HexDecodeFailed{serial, err} 37 | } 38 | return new(big.Int).SetBytes(s), nil 39 | } 40 | 41 | func FindSerial(crl *pkix.CertificateList, serial *big.Int) (pkix.RevokedCertificate, error) { 42 | for _, cert := range crl.TBSCertList.RevokedCertificates { 43 | if reflect.DeepEqual(cert.SerialNumber, serial) { 44 | return cert, nil 45 | } 46 | } 47 | return pkix.RevokedCertificate{}, SerialNotFound{serial} 48 | } 49 | -------------------------------------------------------------------------------- /evReady/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/evReady/ 8 | COPY . . 9 | RUN go build -o evReady ./cmd/web 10 | 11 | # Build ev-checker 12 | RUN apt update && \ 13 | apt install -y clang libcurl4-nss-dev libnspr4-dev libnss3-dev gnutls-bin && \ 14 | cd src && \ 15 | make 16 | 17 | # Final image 18 | FROM debian:bookworm-slim 19 | WORKDIR /app/ 20 | 21 | RUN apt update && \ 22 | apt install -y libcurl4-nss-dev libnss3-dev 23 | 24 | COPY --from=builder /go/src/github.com/mozilla/CCADB-Tools/evReady/evReady ./ 25 | COPY --from=builder /go/src/github.com/mozilla/CCADB-Tools/evReady/src/ev-checker ./ 26 | COPY ./ui ./ui 27 | CMD ["/app/evReady"] -------------------------------------------------------------------------------- /evReady/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 evready 7 | -docker rm evready 8 | -docker rmi evready 9 | -docker image prune -f 10 | -docker image prune -f --filter label=stage=intermediate 11 | 12 | build: 13 | docker build --rm -t evready:latest . 14 | docker image prune -f 15 | docker image prune -f --filter label=stage=intermediate 16 | 17 | run: 18 | ./run.sh 19 | -------------------------------------------------------------------------------- /evReady/README.md: -------------------------------------------------------------------------------- 1 | EV Certificate Readiness Tool 2 | ----------------- 3 | 4 | ## Use Cases 5 | 6 | Test tool for Certificate Authorities (CAs) who request to have a root certificate enabled for Extended Validation (EV) treatment. 7 | 8 | ## Deployment 9 | 10 | ### Locally 11 | When running `evReadiness` locally: 12 | 13 | ```sh 14 | $ docker build -t evready . 15 | $ docker run -p 8080:8080 evready 16 | ``` 17 | Navigate to http://127.0.0.1:8080/evready in your web browser. 18 | 19 | ### Production 20 | When running `evReadiness` in production: 21 | 22 | ```sh 23 | $ make clean build run 24 | ``` -------------------------------------------------------------------------------- /evReady/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: custom 2 | env: flex 3 | service: evready 4 | -------------------------------------------------------------------------------- /evReady/cmd/web/certificates.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "encoding/base64" 7 | "errors" 8 | "github.com/rs/xid" 9 | "net" 10 | "os" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type CertChain struct { 16 | Hostname string 17 | IP string 18 | Certs []string 19 | } 20 | 21 | // getCertFromHost reaches out to the submitted hostname and retrieves its certificates 22 | func getCertFromHost(hostname, port string, skipVerify bool) ([]*x509.Certificate, string, error) { 23 | config := tls.Config{InsecureSkipVerify: skipVerify} 24 | canonicalName := hostname + ":" + port 25 | ip := "" 26 | dialer := &net.Dialer{ 27 | Timeout: 10 * time.Second, 28 | } 29 | 30 | conn, err := tls.DialWithDialer(dialer, "tcp", canonicalName, &config) 31 | if err != nil { 32 | return nil, ip, err 33 | } 34 | defer conn.Close() 35 | 36 | ip = strings.TrimSuffix(conn.RemoteAddr().String(), ":443") 37 | 38 | certs := conn.ConnectionState().PeerCertificates 39 | if certs == nil { 40 | return nil, ip, errors.New("could not get server's certificate from the TLS connection") 41 | } 42 | 43 | return certs, ip, nil 44 | } 45 | 46 | // certConvert is used to convert raw bytes into a string for cert creation 47 | func certConvert(rawCert []byte) string { 48 | certString := base64.StdEncoding.EncodeToString(rawCert) 49 | lineLength := 64 50 | runes := []rune(certString) 51 | prefix := "-----BEGIN CERTIFICATE-----" 52 | suffix := "-----END CERTIFICATE-----\n" 53 | 54 | pem := make([]string, 0) 55 | pem = append(pem, prefix) 56 | 57 | for i := 0; i < len(runes); i += lineLength { 58 | if i+lineLength < len(runes) { 59 | pem = append(pem, string(runes[i:(i+lineLength)])) 60 | } else { 61 | pem = append(pem, string(runes[i:])) 62 | } 63 | } 64 | pem = append(pem, suffix) 65 | 66 | return strings.Join(pem, "\n") 67 | } 68 | 69 | // pemCreator creates a valid PEM file which is used for ev-checker 70 | func (app *application) pemCreator(hostname, rootCert string) (string, error) { 71 | certs, ip, err := getCertFromHost(hostname, "443", true) 72 | if err != nil || certs == nil { 73 | app.logger.Error("Unable to retrieve cert from host.", "hostname", hostname, "error", err.Error()) 74 | } 75 | 76 | certChain := CertChain{ 77 | Hostname: hostname, 78 | IP: ip, 79 | } 80 | 81 | f, err := os.Create("/tmp/" + xid.New().String() + ".pem") 82 | if err != nil { 83 | app.logger.Error("Unable to create certs file.", "error", err.Error()) 84 | } 85 | defer f.Close() 86 | 87 | for _, cert := range certs { 88 | _, err := f.WriteString(certConvert(cert.Raw)) 89 | if err != nil { 90 | app.logger.Error("Unable to write certs to file.", "error", err.Error()) 91 | } 92 | certChain.Certs = append(certChain.Certs, base64.StdEncoding.EncodeToString(cert.Raw)) 93 | 94 | } 95 | _, err = f.WriteString(rootCert) 96 | if err != nil { 97 | app.logger.Error("Unable to write cert to PEM chain file.", "error", err.Error()) 98 | } 99 | 100 | return f.Name(), f.Sync() 101 | } 102 | 103 | // certCleanup removes the cert submitted after ev-checker runs to keep things tidy 104 | func (app *application) certCleanup(pemFile string) { 105 | err := os.RemoveAll(pemFile) 106 | if err != nil { 107 | app.logger.Error("Unable to delete PEM files or directories", "Error", err.Error()) 108 | } else { 109 | app.logger.Info("Removed unused PEM file", "File", pemFile) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /evReady/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 | "mime/multipart" 9 | "net/http" 10 | "os/exec" 11 | "strings" 12 | 13 | "github.com/mozilla/CCADB-Tools/evReady/internal/validator" 14 | ) 15 | 16 | type evForm struct { 17 | Hostname string 18 | OID string 19 | RootCert string 20 | RootCertUpload *multipart.FileHeader 21 | Status string 22 | validator.Validator 23 | } 24 | 25 | // home handles the default endpoint GET request, "/evready" 26 | func (app *application) home(w http.ResponseWriter, r *http.Request) { 27 | data := app.newTemplateData(r) 28 | data.Form = evForm{} 29 | 30 | app.render(w, r, http.StatusOK, "home.tmpl", data) 31 | 32 | } 33 | 34 | // evcheck handles the form POST request from the "/evready" endpoint 35 | func (app *application) evcheck(w http.ResponseWriter, r *http.Request) { 36 | err := r.ParseMultipartForm(1 << 20) // 10MB 37 | if err != nil { 38 | app.clientError(w, http.StatusBadRequest) 39 | return 40 | } 41 | 42 | form := evForm{ 43 | Hostname: r.PostFormValue("hostname"), 44 | OID: r.PostFormValue("oid"), 45 | RootCert: r.PostFormValue("rootCert"), 46 | } 47 | 48 | _, header, _ := r.FormFile("rootCertUpload") 49 | form.RootCertUpload = header 50 | 51 | form.CheckField(validator.NotBlank(form.Hostname), "hostname", "Hostname field is required") 52 | form.CheckField(validator.MaxChars(form.Hostname, 253), "hostname", "Hostname cannot be more than 253 characters") 53 | form.CheckField(validator.ValidURL(form.Hostname), "hostname", "Hostname must be in a valid URL format with no spaces") 54 | form.CheckField(validator.NotBlank(form.OID), "oid", "OID field is required") 55 | form.CheckField(validator.MaxChars(form.OID, 253), "oid", "OID cannot be more than 253 characters") 56 | form.CheckField(validator.ValidOID(form.OID), "oid", "OID must be a valid OID format") 57 | form.CheckField(validator.NoPEMs(form.RootCert, form.RootCertUpload), "rootCertUpload", "Please upload or paste the contents of a PEM file") 58 | form.CheckField(validator.BothPEMs(form.RootCert, form.RootCertUpload), "rootCertUpload", "Please only submit a pasted PEM file OR upload a file") 59 | 60 | hostname := app.urlCleaner(form.Hostname) 61 | oid := strings.TrimSpace(form.OID) 62 | 63 | var pemFile string 64 | 65 | if form.RootCert != "" { 66 | form.CheckField(validator.ValidPEM(form.RootCert), "rootCert", "Invalid certificate format. Certificate must be PEM-encoded") 67 | pemFile, err = app.pemCreator(hostname, form.RootCert) 68 | if err != nil { 69 | app.logger.Error("Unable to create PEM file from pasted contents", "Error", err.Error()) 70 | } 71 | } else if form.RootCertUpload != nil { 72 | pemUploadFile := app.pemReader(app.uploadSave(r)) 73 | form.CheckField(validator.ValidPEM(pemUploadFile), "rootCertUpload", "Invalid certificate format. Certificate must be PEM-encoded") 74 | pemFile, err = app.pemCreator(hostname, pemUploadFile) 75 | if err != nil { 76 | app.logger.Error("Unable to create PEM file from upload", "Error", err.Error()) 77 | } 78 | } 79 | 80 | if !form.Valid() { 81 | data := app.newTemplateData(r) 82 | data.Form = form 83 | app.render(w, r, http.StatusUnprocessableEntity, "home.tmpl", data) 84 | return 85 | } 86 | 87 | out, err := exec.Command(evReadyExec, "-h", hostname, "-o", oid, "-c", pemFile).CombinedOutput() 88 | if err != nil { 89 | app.logger.Error("ev-ready exec failed", "Error", err.Error()) 90 | } 91 | 92 | app.logger.Info("Ran ev-checker", "Status", string(out)) 93 | flash := string(out) 94 | 95 | data := app.newTemplateData(r) 96 | data.Form = form 97 | data.Flash = flash 98 | app.render(w, r, http.StatusOK, "home.tmpl", data) 99 | 100 | // remove the cert written to the file system 101 | app.certCleanup(pemFile) 102 | } 103 | -------------------------------------------------------------------------------- /evReady/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 | "net/url" 13 | "os" 14 | "os/exec" 15 | "runtime/debug" 16 | "strings" 17 | ) 18 | 19 | func (app *application) serverError(w http.ResponseWriter, r *http.Request, err error) { 20 | var ( 21 | method = r.Method 22 | uri = r.URL.RequestURI() 23 | trace = string(debug.Stack()) 24 | ) 25 | 26 | app.logger.Error(err.Error(), "method", method, "uri", uri, "trace", trace) 27 | 28 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 29 | } 30 | 31 | func (app *application) clientError(w http.ResponseWriter, status int) { 32 | http.Error(w, http.StatusText(status), status) 33 | } 34 | 35 | func (app *application) notFound(w http.ResponseWriter) { 36 | app.clientError(w, http.StatusNotFound) 37 | } 38 | 39 | func (app *application) render(w http.ResponseWriter, r *http.Request, status int, page string, data templateData) { 40 | ts, ok := app.templateCache[page] 41 | if !ok { 42 | err := fmt.Errorf("the template %s does not exist", page) 43 | app.serverError(w, r, err) 44 | return 45 | } 46 | 47 | buf := new(bytes.Buffer) 48 | 49 | err := ts.ExecuteTemplate(buf, "base", data) 50 | if err != nil { 51 | app.serverError(w, r, err) 52 | return 53 | } 54 | 55 | w.WriteHeader(status) 56 | 57 | buf.WriteTo(w) 58 | } 59 | 60 | func (app *application) newTemplateData(r *http.Request) templateData { 61 | return templateData{} 62 | } 63 | 64 | // checkEvReadyExecExists checks if the executable is present -- if it's not, exit, 65 | // because it's the brains behind everything 66 | func checkEvReadyExecExists(path string) { 67 | path, err := exec.LookPath(path) 68 | if err != nil { 69 | os.Exit(127) 70 | } 71 | } 72 | 73 | // urlCleaner cleans up the provided hostname 74 | func (app *application) urlCleaner(hostname string) string { 75 | u, err := url.Parse(strings.TrimSpace(hostname)) 76 | if err != nil { 77 | app.logger.Error("Unable to parse hostname url", "Error", err.Error()) 78 | } 79 | if u.IsAbs() { 80 | return u.Hostname() 81 | } else { 82 | return strings.TrimSuffix(u.Path, "/") 83 | } 84 | } 85 | 86 | // uploadSave handles the process of saving an uploaded file to the file system 87 | func (app *application) uploadSave(r *http.Request) string { 88 | err := r.ParseMultipartForm(1 << 20) 89 | if err != nil { 90 | app.logger.Error("Unable to parse form", "error", err.Error()) 91 | } 92 | 93 | file, fileHeader, err := r.FormFile("rootCertUpload") 94 | if err != nil { 95 | app.logger.Error("Unable to parse form", "error", err.Error()) 96 | } 97 | defer file.Close() 98 | 99 | err = os.MkdirAll("/tmp", os.ModePerm) 100 | if err != nil { 101 | app.logger.Error("Unable to create /tmp directory", "error", err.Error()) 102 | } 103 | 104 | pemFile := "/tmp/" + fileHeader.Filename 105 | dst, err := os.Create(pemFile) 106 | if err != nil { 107 | app.logger.Error("Unable to create file", "error", err.Error()) 108 | } 109 | 110 | defer dst.Close() 111 | 112 | if _, err := io.Copy(dst, file); err != nil { 113 | app.logger.Error("Unable to save file", "error", err.Error()) 114 | } 115 | 116 | return pemFile 117 | } 118 | 119 | // pemReader reads the contents of a PEM file 120 | func (app *application) pemReader(pemUpload string) string { 121 | content, err := os.ReadFile(pemUpload) 122 | if err != nil { 123 | app.logger.Error("Unable to read contents of uploaded file", "error", err.Error()) 124 | } 125 | 126 | return string(content) 127 | } 128 | -------------------------------------------------------------------------------- /evReady/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 | // executable built during the docker build step 17 | const evReadyExec = "/app/ev-checker" 18 | 19 | type application struct { 20 | logger *slog.Logger 21 | pemFile string 22 | templateCache map[string]*template.Template 23 | Request *http.Request 24 | } 25 | 26 | func main() { 27 | // Default to port 8080 if PORT env var is not set 28 | port := getPortEnv("PORT", "8080") 29 | 30 | addr := flag.String("addr", ":"+port, "HTTP network address") 31 | flag.Parse() 32 | 33 | logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) 34 | 35 | templateCache, err := newTemplateCache() 36 | if err != nil { 37 | logger.Error(err.Error()) 38 | os.Exit(1) 39 | } 40 | 41 | app := &application{ 42 | logger: logger, 43 | templateCache: templateCache, 44 | } 45 | 46 | srv := &http.Server{ 47 | Addr: *addr, 48 | Handler: app.routes(), 49 | ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), 50 | IdleTimeout: time.Minute, 51 | ReadTimeout: 5 * time.Second, 52 | WriteTimeout: 10 * time.Second, 53 | } 54 | 55 | logger.Info("Starting server", "addr", srv.Addr) 56 | 57 | // Check for ev-checker binary 58 | checkEvReadyExecExists(evReadyExec) 59 | 60 | err = srv.ListenAndServe() 61 | logger.Error(err.Error()) 62 | os.Exit(1) 63 | } 64 | 65 | // getPortEnv looks for the PORT env var and uses fallback if not set 66 | func getPortEnv(port, fallback string) string { 67 | if value, ok := os.LookupEnv(port); ok { 68 | return value 69 | } 70 | return fallback 71 | } 72 | -------------------------------------------------------------------------------- /evReady/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 | -------------------------------------------------------------------------------- /evReady/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, "/evready", app.home) 26 | router.HandlerFunc(http.MethodPost, "/evready", app.evcheck) 27 | 28 | standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders) 29 | 30 | return standard.Then(router) 31 | } 32 | -------------------------------------------------------------------------------- /evReady/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 | Form any 14 | Flash string 15 | } 16 | 17 | // newTemplateCache caches template files 18 | func newTemplateCache() (map[string]*template.Template, error) { 19 | cache := map[string]*template.Template{} 20 | 21 | pages, err := filepath.Glob("./ui/html/pages/*tmpl") 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | for _, page := range pages { 27 | name := filepath.Base(page) 28 | 29 | ts, err := template.ParseFiles("./ui/html/base.tmpl") 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | ts, err = ts.ParseFiles(page) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | cache[name] = ts 40 | } 41 | 42 | return cache, nil 43 | } 44 | -------------------------------------------------------------------------------- /evReady/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mozilla/CCADB-Tools/evReady 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 | github.com/rs/xid v1.5.0 9 | ) 10 | -------------------------------------------------------------------------------- /evReady/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 | github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= 6 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 7 | -------------------------------------------------------------------------------- /evReady/internal/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "mime/multipart" 5 | "net/url" 6 | "regexp" 7 | "strings" 8 | "unicode/utf8" 9 | ) 10 | 11 | type Validator struct { 12 | FieldErrors map[string]string 13 | } 14 | 15 | func (v *Validator) Valid() bool { 16 | return len(v.FieldErrors) == 0 17 | } 18 | 19 | func (v *Validator) AddFieldError(key, message string) { 20 | if v.FieldErrors == nil { 21 | v.FieldErrors = make(map[string]string) 22 | } 23 | 24 | if _, exists := v.FieldErrors[key]; !exists { 25 | v.FieldErrors[key] = message 26 | } 27 | } 28 | 29 | func (v *Validator) CheckField(ok bool, key, message string) { 30 | if !ok { 31 | v.AddFieldError(key, message) 32 | } 33 | } 34 | 35 | // NotBlank checks to make sure the field isn't blank 36 | func NotBlank(value string) bool { 37 | return strings.TrimSpace(value) != "" 38 | } 39 | 40 | // MaxChars checks to make sure the field isn't over the specified number of characters 41 | func MaxChars(value string, n int) bool { 42 | return utf8.RuneCountInString(value) <= n 43 | } 44 | 45 | // ValidURL validates the provided hostname 46 | func ValidURL(value string) bool { 47 | _, err := url.Parse(value) 48 | if err != nil { 49 | return false 50 | } else { 51 | return true 52 | } 53 | } 54 | 55 | // ValidOID validates the provided OID 56 | func ValidOID(value string) bool { 57 | re := regexp.MustCompile(`^([0-2])((\.0)|(\.[1-9][0-9]*))*$`) 58 | 59 | return re.MatchString(strings.TrimSpace(value)) 60 | } 61 | 62 | // NoPEMs validates that there is at least a pasted PEM or an uploaded PEM file 63 | func NoPEMs(pemPaste string, pemUpload *multipart.FileHeader) bool { 64 | if pemPaste == "" && pemUpload == nil { 65 | return false 66 | } else { 67 | return true 68 | } 69 | } 70 | 71 | // BothPEMs validates that only one or the other types of PEM are submitted 72 | func BothPEMs(pemPaste string, pemUpload *multipart.FileHeader) bool { 73 | if pemPaste != "" && pemUpload != nil { 74 | return false 75 | } else { 76 | return true 77 | } 78 | } 79 | 80 | // ValidPEM validates PEM content - pasted or uploaded 81 | func ValidPEM(value string) bool { 82 | value = strings.TrimSpace(value) 83 | return strings.HasPrefix(value, "-----BEGIN CERTIFICATE-----") && 84 | strings.HasSuffix(value, "-----END CERTIFICATE-----") 85 | } 86 | -------------------------------------------------------------------------------- /evReady/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 evready \ 13 | -d \ 14 | -e "PORT=$CONTAINER_PORT" \ 15 | -p ${HOST_PORT}:${CONTAINER_PORT} \ 16 | evready 17 | 18 | -------------------------------------------------------------------------------- /evReady/src/EVCheckerTrustDomain.h: -------------------------------------------------------------------------------- 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 | #ifndef EVCheckerTrustDomain_h 6 | #define EVCheckerTrustDomain_h 7 | 8 | #include "pkix/pkix.h" 9 | #include "pkix/pkixnss.h" 10 | #include "pkix/pkixtypes.h" 11 | 12 | #include "cert.h" 13 | 14 | #include "Util.h" 15 | 16 | class EVCheckerTrustDomain : public mozilla::pkix::TrustDomain 17 | { 18 | public: 19 | explicit EVCheckerTrustDomain(CERTCertificate* root); 20 | 21 | SECStatus Init(const char* dottedEVPolicyOID, const char* evPolicyName); 22 | 23 | SECStatus GetFirstEVPolicyForCert(const CERTCertificate* cert, 24 | /*out*/ mozilla::pkix::CertPolicyId& policy); 25 | 26 | mozilla::pkix::Result GetCertTrust(mozilla::pkix::EndEntityOrCA endEntityOrCA, 27 | const mozilla::pkix::CertPolicyId& policy, 28 | mozilla::pkix::Input candidateCertDER, 29 | /*out*/ mozilla::pkix::TrustLevel& trustLevel); 30 | 31 | mozilla::pkix::Result FindIssuer(mozilla::pkix::Input encodedIssuerName, 32 | mozilla::pkix::TrustDomain::IssuerChecker& checker, 33 | mozilla::pkix::Time time); 34 | 35 | mozilla::pkix::Result CheckRevocation(mozilla::pkix::EndEntityOrCA endEntityOrCA, 36 | const mozilla::pkix::CertID& certID, 37 | mozilla::pkix::Time time, 38 | const mozilla::pkix::Input* stapledOCSPResponse, 39 | const mozilla::pkix::Input* aiaExtension); 40 | 41 | mozilla::pkix::Result IsChainValid(const mozilla::pkix::DERArray& certChain, 42 | mozilla::pkix::Time time); 43 | 44 | mozilla::pkix::Result VerifyRSAPKCS1SignedDigest( 45 | const mozilla::pkix::SignedDigest& signedDigest, 46 | mozilla::pkix::Input subjectPublicKeyInfo) 47 | { 48 | return mozilla::pkix::VerifyRSAPKCS1SignedDigestNSS(signedDigest, 49 | subjectPublicKeyInfo, 50 | nullptr); 51 | } 52 | 53 | mozilla::pkix::Result DigestBuf(mozilla::pkix::Input item, 54 | mozilla::pkix::DigestAlgorithm digestAlg, 55 | /*out*/ uint8_t* digestBuf, size_t digestBufLen) 56 | { 57 | return mozilla::pkix::DigestBufNSS(item, digestAlg, digestBuf, 58 | digestBufLen); 59 | } 60 | 61 | mozilla::pkix::Result CheckSignatureDigestAlgorithm( 62 | mozilla::pkix::DigestAlgorithm digestAlg) 63 | { 64 | return mozilla::pkix::Success; 65 | } 66 | 67 | mozilla::pkix::Result CheckRSAPublicKeyModulusSizeInBits( 68 | mozilla::pkix::EndEntityOrCA endEntityOrCA, unsigned int modulusSizeInBits) 69 | { 70 | return mozilla::pkix::Success; 71 | } 72 | 73 | mozilla::pkix::Result CheckECDSACurveIsAcceptable( 74 | mozilla::pkix::EndEntityOrCA endEntityOrCA, 75 | mozilla::pkix::NamedCurve curve) 76 | { 77 | return mozilla::pkix::Success; 78 | } 79 | 80 | mozilla::pkix::Result VerifyECDSASignedDigest( 81 | const mozilla::pkix::SignedDigest& signedDigest, 82 | mozilla::pkix::Input subjectPublicKeyInfo) 83 | { 84 | return mozilla::pkix::VerifyECDSASignedDigestNSS(signedDigest, 85 | subjectPublicKeyInfo, 86 | nullptr); 87 | } 88 | 89 | private: 90 | ScopedCERTCertificate mRoot; 91 | SECOidTag mEVPolicyOIDTag; 92 | }; 93 | 94 | #endif // EVCheckerTrustDomain_h 95 | -------------------------------------------------------------------------------- /evReady/src/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 | CC=clang++ 6 | CFLAGS=-Ipkix/include -I/usr/include/nspr4 -I/usr/include/nspr -I/usr/local/include/nspr \ 7 | -I/usr/include/nss3 -I/usr/local/include/nss -I/usr/include/nss \ 8 | -g -Wall -c -std=c++11 9 | LDFLAGS=-lnss3 -lnssutil3 -lnspr4 -lplc4 -lcurl -Wl,-L/usr/local/lib 10 | SOURCES=pkix/lib/pkixbuild.cpp pkix/lib/pkixcert.cpp pkix/lib/pkixcheck.cpp \ 11 | pkix/lib/pkixder.cpp pkix/lib/pkixnames.cpp pkix/lib/pkixnss.cpp \ 12 | pkix/lib/pkixocsp.cpp pkix/lib/pkixresult.cpp pkix/lib/pkixtime.cpp \ 13 | pkix/lib/pkixverify.cpp ev-checker.cpp \ 14 | EVCheckerTrustDomain.cpp Util.cpp 15 | OBJECTS=$(SOURCES:.cpp=.o) 16 | EXECUTABLE=ev-checker 17 | 18 | all: $(SOURCES) $(EXECUTABLE) 19 | 20 | $(EXECUTABLE): $(OBJECTS) 21 | $(CC) $(LDFLAGS) $(OBJECTS) -o $@ 22 | 23 | .cpp.o: 24 | $(CC) $(CFLAGS) $< -o $@ 25 | 26 | deps-ubuntu: 27 | sudo apt -y install clang make libcurl4-nss-dev libnspr4-dev libnss3-dev gnutls-bin 28 | 29 | deps-rhel: 30 | sudo dnf install -y clang make nss-devel nspr-devel libcurl-devel gnutls-utils 31 | 32 | clean: 33 | rm -f $(EXECUTABLE) $(OBJECTS) test/*.pem test/*.key test/*.req \ 34 | test/run-tests.sh 35 | -------------------------------------------------------------------------------- /evReady/src/README.md: -------------------------------------------------------------------------------- 1 | # ev-checker # 2 | ****** 3 | ## *Note (2023-08-18) ## 4 | *This source code was copied from the `mozilla-services/ev-checker` repo here: 5 | https://github.com/mozilla-services/ev-checker/tree/04a7d03ff8ba4a3965bbd50c1d494100d99b8138 6 | This was done to prevent upstream dependency issues. The original source code is under the MPL 2.0 license.* 7 | 8 | --- 9 | 10 | ## What ## 11 | `ev-checker` is a standalone command-line utility for determining if a given EV 12 | policy fulfills the requirements of Mozilla's Root CA program and may thus be 13 | enabled. 14 | 15 | ## How ## 16 | `ev-checker` depends on the libraries 17 | [NSS](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS) and 18 | [NSPR](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSPR). It 19 | additionally makes use of 20 | [mozilla::pkix](https://wiki.mozilla.org/SecurityEngineering/Certificate_Verification). 21 | Since mozilla::pkix has not been released as a stand-alone library yet, this 22 | project imports a snapshot of the implementation. (See the file `pkix-import`.) 23 | `ev-checker` implements a `mozilla::pkix::TrustDomain` and uses 24 | `mozilla::pkix::BuildCertChain` to determine if a given EV policy meets the 25 | requirements to be enabled in Firefox. 26 | 27 | ## Example ## 28 | First, compile with `make`. There is no guarantee of portability, so feel free 29 | to file issues if this does not work as expected. 30 | 31 | Then, given the file `cert-chain.pem`, the dotted OID of the EV policy, and a 32 | hostname to validate against, run `ev-checker` like so: 33 | 34 | `./ev-checker -c cert-chain.pem -o dotted.OID -h hostname` 35 | 36 | `-c` specifies the file containing a sequence of PEM-encoded certificates. The 37 | first certificate is the end-entity certificate intended to be tested for EV 38 | treatment. The last certificate is the root certificate that is authoritative 39 | for the given EV policy. Any certificates in between are intermediate 40 | certificates. 41 | 42 | If run with the flag `-d` and a description of the EV OID, `ev-checker` will 43 | output a blob of text that must be added to 44 | [security/certverifier/ExtendedValidation.cpp](https://dxr.mozilla.org/mozilla-central/source/security/certverifier/ExtendedValidation.cpp) 45 | in the mozilla-central tree for Firefox to consider this a valid EV policy. 46 | It will also validate the end-entity certificate. If it succeeds, the EV policy 47 | is ready to be enabled. If not, something needs to be fixed. 48 | Hopefully `ev-checker` emitted a helpful error message pointing to the problem. 49 | 50 | ```bash 51 | $ ev-checker -c chain.pem -o 2.16.840.1.114412.2.1 -d "Digicert EV OID" -h addons.mozilla.org 52 | 53 | // CN=DigiCert High Assurance EV Root CA,OU=www.digicert.com,O=DigiCert Inc,C=US 54 | "2.16.840.1.114412.2.1", 55 | "Digicert EV OID", 56 | SEC_OID_UNKNOWN, 57 | { 0x74, 0x31, 0xE5, 0xF4, 0xC3, 0xC1, 0xCE, 0x46, 0x90, 0x77, 0x4F, 58 | 0x0B, 0x61, 0xE0, 0x54, 0x40, 0x88, 0x3B, 0xA9, 0xA0, 0x1E, 0xD0, 59 | 0x0B, 0xA6, 0xAB, 0xD7, 0x80, 0x6E, 0xD3, 0xB1, 0x18, 0xCF }, 60 | "MGwxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsT" 61 | "EHd3dy5kaWdpY2VydC5jb20xKzApBgNVBAMTIkRpZ2lDZXJ0IEhpZ2ggQXNzdXJh" 62 | "bmNlIEVWIFJvb3QgQ0E=", 63 | "AqxcJmoLQJuPC3nyrkYldw==", 64 | Success! 65 | ``` 66 | 67 | ## TODO Items ## 68 | * Do OCSP fetching 69 | * Other policy issues 70 | * More helpful error messages 71 | -------------------------------------------------------------------------------- /evReady/src/Util.cpp: -------------------------------------------------------------------------------- 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 | #include 6 | 7 | #include "Util.h" 8 | 9 | #include "prerror.h" 10 | 11 | void 12 | PrintPRError(const char* message) 13 | { 14 | const char* err = PR_ErrorToName(PR_GetError()); 15 | if (err) { 16 | std::cerr << message << ": " << err << std::endl; 17 | } else { 18 | std::cerr << message << std::endl; 19 | } 20 | } 21 | 22 | void 23 | PrintPRErrorString() 24 | { 25 | std::cerr << PR_ErrorToString(PR_GetError(), 0) << std::endl; 26 | } 27 | 28 | 29 | void 30 | PrintEVError(const char* message) 31 | { 32 | std::cerr << message << std::endl; 33 | } 34 | -------------------------------------------------------------------------------- /evReady/src/Util.h: -------------------------------------------------------------------------------- 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 | #ifndef Util_h 6 | #define Util_h 7 | 8 | #include "pkix/pkixtypes.h" 9 | #include "pkix/ScopedPtr.h" 10 | 11 | #include "cert.h" 12 | 13 | inline void 14 | PORT_FreeArena_false(PLArenaPool* arena) 15 | { 16 | return PORT_FreeArena(arena, false); 17 | } 18 | 19 | typedef mozilla::pkix::ScopedPtr 20 | ScopedCERTCertificate; 21 | typedef mozilla::pkix::ScopedPtr 22 | ScopedCERTCertList; 23 | typedef mozilla::pkix::ScopedPtr 24 | ScopedPLArenaPool; 25 | 26 | void PrintPRError(const char* message); 27 | void PrintPRErrorString(); 28 | void PrintEVError(const char* message); 29 | 30 | inline void 31 | PortFreeString(const char* ptr) 32 | { 33 | PORT_Free((void*)ptr); 34 | } 35 | typedef mozilla::pkix::ScopedPtr ScopedString; 36 | 37 | #endif // Util_h 38 | -------------------------------------------------------------------------------- /evReady/src/pkix-import: -------------------------------------------------------------------------------- 1 | mozilla::pkix imported from mozilla-central rev. 436686833af0 2 | -------------------------------------------------------------------------------- /evReady/src/pkix/include/pkix/ScopedPtr.h: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This code is made available to you under your choice of the following sets 4 | * of licensing terms: 5 | */ 6 | /* This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | /* Copyright 2013 Mozilla Contributors 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | */ 24 | 25 | #ifndef mozilla_pkix_ScopedPtr_h 26 | #define mozilla_pkix_ScopedPtr_h 27 | 28 | #include "pkix/stdkeywords.h" 29 | 30 | namespace mozilla { namespace pkix { 31 | 32 | // Similar to boost::scoped_ptr and std::unique_ptr. Does not support copying 33 | // or assignment. 34 | template 35 | class ScopedPtr final 36 | { 37 | public: 38 | explicit ScopedPtr(T* value = nullptr) : mValue(value) { } 39 | ~ScopedPtr() 40 | { 41 | if (mValue) { 42 | Destroyer(mValue); 43 | } 44 | } 45 | 46 | void operator=(T* newValue) 47 | { 48 | if (mValue) { 49 | Destroyer(mValue); 50 | } 51 | mValue = newValue; 52 | } 53 | 54 | T& operator*() const { return *mValue; } 55 | T* operator->() const { return mValue; } 56 | operator bool() const { return mValue; } 57 | 58 | T* get() const { return mValue; } 59 | 60 | T* release() 61 | { 62 | T* result = mValue; 63 | mValue = nullptr; 64 | return result; 65 | } 66 | 67 | void reset() { *this = nullptr; } 68 | 69 | protected: 70 | T* mValue; 71 | 72 | ScopedPtr(const ScopedPtr&) = delete; 73 | void operator=(const ScopedPtr&) = delete; 74 | }; 75 | 76 | template 77 | inline bool 78 | operator==(T* a, const ScopedPtr& b) 79 | { 80 | return a == b.get(); 81 | } 82 | 83 | template 84 | inline bool 85 | operator==(const ScopedPtr& a, T* b) 86 | { 87 | return a.get() == b; 88 | } 89 | 90 | template 91 | inline bool 92 | operator!=(T* a, const ScopedPtr& b) 93 | { 94 | return a != b.get(); 95 | } 96 | 97 | template 98 | inline bool 99 | operator!=(const ScopedPtr& a, T* b) 100 | { 101 | return a.get() != b; 102 | } 103 | 104 | } } // namespace mozilla::pkix 105 | 106 | #endif // mozilla_pkix_ScopedPtr_h 107 | -------------------------------------------------------------------------------- /evReady/src/pkix/include/pkix/Time.h: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This code is made available to you under your choice of the following sets 4 | * of licensing terms: 5 | */ 6 | /* This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | /* Copyright 2014 Mozilla Contributors 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | */ 24 | 25 | #ifndef mozilla_pkix_Time_h 26 | #define mozilla_pkix_Time_h 27 | 28 | #include 29 | #include 30 | #include 31 | 32 | #include "pkix/Result.h" 33 | #include "pkix/stdkeywords.h" 34 | 35 | namespace mozilla { namespace pkix { 36 | 37 | // Time with a range from the first second of year 0 (AD) through at least the 38 | // last second of year 9999, which is the range of legal times in X.509 and 39 | // OCSP. This type has second-level precision. The time zone is always UTC. 40 | // 41 | // Pass by value, not by reference. 42 | class Time final 43 | { 44 | public: 45 | // Construct an uninitilized instance. 46 | // 47 | // This will fail to compile because there is no default constructor: 48 | // Time x; 49 | // 50 | // This will succeed, leaving the time uninitialized: 51 | // Time x(Time::uninitialized); 52 | enum Uninitialized { uninitialized }; 53 | explicit Time(Uninitialized) { } 54 | 55 | bool operator==(const Time& other) const 56 | { 57 | return elapsedSecondsAD == other.elapsedSecondsAD; 58 | } 59 | bool operator>(const Time& other) const 60 | { 61 | return elapsedSecondsAD > other.elapsedSecondsAD; 62 | } 63 | bool operator>=(const Time& other) const 64 | { 65 | return elapsedSecondsAD >= other.elapsedSecondsAD; 66 | } 67 | bool operator<(const Time& other) const 68 | { 69 | return elapsedSecondsAD < other.elapsedSecondsAD; 70 | } 71 | bool operator<=(const Time& other) const 72 | { 73 | return elapsedSecondsAD <= other.elapsedSecondsAD; 74 | } 75 | 76 | Result AddSeconds(uint64_t seconds) 77 | { 78 | if (std::numeric_limits::max() - elapsedSecondsAD 79 | < seconds) { 80 | return Result::FATAL_ERROR_INVALID_ARGS; // integer overflow 81 | } 82 | elapsedSecondsAD += seconds; 83 | return Success; 84 | } 85 | 86 | Result SubtractSeconds(uint64_t seconds) 87 | { 88 | if (seconds > elapsedSecondsAD) { 89 | return Result::FATAL_ERROR_INVALID_ARGS; // integer overflow 90 | } 91 | elapsedSecondsAD -= seconds; 92 | return Success; 93 | } 94 | 95 | static const uint64_t ONE_DAY_IN_SECONDS 96 | = UINT64_C(24) * UINT64_C(60) * UINT64_C(60); 97 | 98 | private: 99 | // This constructor is hidden to prevent accidents like this: 100 | // 101 | // Time foo(time_t t) 102 | // { 103 | // // WRONG! 1970-01-01-00:00:00 == time_t(0), but not Time(0)! 104 | // return Time(t); 105 | // } 106 | explicit Time(uint64_t elapsedSecondsAD) 107 | : elapsedSecondsAD(elapsedSecondsAD) 108 | { 109 | } 110 | friend Time TimeFromElapsedSecondsAD(uint64_t); 111 | 112 | uint64_t elapsedSecondsAD; 113 | }; 114 | 115 | inline Time TimeFromElapsedSecondsAD(uint64_t elapsedSecondsAD) 116 | { 117 | return Time(elapsedSecondsAD); 118 | } 119 | 120 | Time Now(); 121 | 122 | // Note the epoch is the unix epoch (ie 00:00:00 UTC, 1 January 1970) 123 | Time TimeFromEpochInSeconds(uint64_t secondsSinceEpoch); 124 | 125 | } } // namespace mozilla::pkix 126 | 127 | #endif // mozilla_pkix_Time_h 128 | -------------------------------------------------------------------------------- /evReady/src/pkix/include/pkix/pkixnss.h: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This code is made available to you under your choice of the following sets 4 | * of licensing terms: 5 | */ 6 | /* This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | /* Copyright 2013 Mozilla Contributors 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | */ 24 | 25 | #ifndef mozilla_pkix_pkixnss_h 26 | #define mozilla_pkix_pkixnss_h 27 | 28 | #include "pkixtypes.h" 29 | #include "prerror.h" 30 | #include "seccomon.h" 31 | 32 | namespace mozilla { namespace pkix { 33 | 34 | // Verifies the PKCS#1.5 signature on the given data using the given RSA public 35 | // key. 36 | Result VerifyRSAPKCS1SignedDigestNSS(const SignedDigest& sd, 37 | Input subjectPublicKeyInfo, 38 | void* pkcs11PinArg); 39 | 40 | // Verifies the ECDSA signature on the given data using the given ECC public 41 | // key. 42 | Result VerifyECDSASignedDigestNSS(const SignedDigest& sd, 43 | Input subjectPublicKeyInfo, 44 | void* pkcs11PinArg); 45 | 46 | // Computes the digest of the given data using the given digest algorithm. 47 | // 48 | // item contains the data to hash. 49 | // digestBuf must point to a buffer to where the digest will be written. 50 | // digestBufLen must be the size of the buffer, which must be exactly equal 51 | // to the size of the digest output (20 for SHA-1, 32 for SHA-256, 52 | // etc.) 53 | // 54 | // TODO: Taking the output buffer as (uint8_t*, size_t) is counter to our 55 | // other, extensive, memory safety efforts in mozilla::pkix, and we should find 56 | // a way to provide a more-obviously-safe interface. 57 | Result DigestBufNSS(Input item, 58 | DigestAlgorithm digestAlg, 59 | /*out*/ uint8_t* digestBuf, 60 | size_t digestBufLen); 61 | 62 | Result MapPRErrorCodeToResult(PRErrorCode errorCode); 63 | PRErrorCode MapResultToPRErrorCode(Result result); 64 | 65 | // The error codes within each module must fit in 16 bits. We want these 66 | // errors to fit in the same module as the NSS errors but not overlap with 67 | // any of them. Converting an NSS SEC, NSS SSL, or PSM error to an NS error 68 | // involves negating the value of the error and then synthesizing an error 69 | // in the NS_ERROR_MODULE_SECURITY module. Hence, PSM errors will start at 70 | // a negative value that both doesn't overlap with the current value 71 | // ranges for NSS errors and that will fit in 16 bits when negated. 72 | static const PRErrorCode ERROR_BASE = -0x4000; 73 | static const PRErrorCode ERROR_LIMIT = ERROR_BASE + 1000; 74 | 75 | enum ErrorCode 76 | { 77 | MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE = ERROR_BASE + 0, 78 | MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY = ERROR_BASE + 1, 79 | MOZILLA_PKIX_ERROR_INADEQUATE_KEY_SIZE = ERROR_BASE + 2, 80 | MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA = ERROR_BASE + 3, 81 | MOZILLA_PKIX_ERROR_NO_RFC822NAME_MATCH = ERROR_BASE + 4, 82 | MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE = ERROR_BASE + 5, 83 | MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE = ERROR_BASE + 6, 84 | MOZILLA_PKIX_ERROR_SIGNATURE_ALGORITHM_MISMATCH = ERROR_BASE + 7, 85 | }; 86 | 87 | void RegisterErrorTable(); 88 | 89 | inline SECItem UnsafeMapInputToSECItem(Input input) 90 | { 91 | SECItem result = { 92 | siBuffer, 93 | const_cast(input.UnsafeGetData()), 94 | input.GetLength() 95 | }; 96 | static_assert(sizeof(decltype(input.GetLength())) <= sizeof(result.len), 97 | "input.GetLength() must fit in a SECItem"); 98 | return result; 99 | } 100 | 101 | } } // namespace mozilla::pkix 102 | 103 | #endif // mozilla_pkix_pkixnss_h 104 | -------------------------------------------------------------------------------- /evReady/src/pkix/include/pkix/stdkeywords.h: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This code is made available to you under your choice of the following sets 4 | * of licensing terms: 5 | */ 6 | /* This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | /* Copyright 2013 Mozilla Contributors 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | */ 24 | 25 | #ifndef mozilla_pkix_stdkeywords_h 26 | #define mozilla_pkix_stdkeywords_h 27 | 28 | #if defined(__GNUC__) && !defined(__clang__) 29 | 30 | // GCC does not understand final/override until 4.7 31 | #if __GNUC__ * 100 + __GNUC_MINOR__ < 407 32 | #define final 33 | #define override 34 | #endif 35 | 36 | #endif // defined(__GNUC__) && !defined(__clang__) 37 | 38 | #endif // mozilla_pkix_stdkeywords_h 39 | -------------------------------------------------------------------------------- /evReady/src/pkix/lib/pkixcheck.h: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This code is made available to you under your choice of the following sets 4 | * of licensing terms: 5 | */ 6 | /* This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | /* Copyright 2013 Mozilla Contributors 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | */ 24 | 25 | #ifndef mozilla_pkix_pkixcheck_h 26 | #define mozilla_pkix_pkixcheck_h 27 | 28 | #include "pkix/pkixtypes.h" 29 | 30 | namespace mozilla { namespace pkix { 31 | 32 | class BackCert; 33 | 34 | Result CheckIssuerIndependentProperties( 35 | TrustDomain& trustDomain, 36 | const BackCert& cert, 37 | Time time, 38 | KeyUsage requiredKeyUsageIfPresent, 39 | KeyPurposeId requiredEKUIfPresent, 40 | const CertPolicyId& requiredPolicy, 41 | unsigned int subCACount, 42 | /*out*/ TrustLevel& trustLevel); 43 | 44 | Result CheckNameConstraints(Input encodedNameConstraints, 45 | const BackCert& firstChild, 46 | KeyPurposeId requiredEKUIfPresent); 47 | 48 | } } // namespace mozilla::pkix 49 | 50 | #endif // mozilla_pkix_pkixcheck_h 51 | -------------------------------------------------------------------------------- /evReady/src/pkix/lib/pkixresult.cpp: -------------------------------------------------------------------------------- 1 | /*- *- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This code is made available to you under your choice of the following sets 4 | * of licensing terms: 5 | */ 6 | /* This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | /* Copyright 2013 Mozilla Contributors 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | */ 24 | 25 | #include "pkix/Result.h" 26 | #include "pkixutil.h" 27 | 28 | namespace mozilla { namespace pkix { 29 | 30 | const char* 31 | MapResultToName(Result result) 32 | { 33 | switch (result) 34 | { 35 | #define MOZILLA_PKIX_MAP(mozilla_pkix_result, value, nss_result) \ 36 | case Result::mozilla_pkix_result: return "Result::" #mozilla_pkix_result; 37 | 38 | MOZILLA_PKIX_MAP_LIST 39 | 40 | #undef MOZILLA_PKIX_MAP 41 | 42 | MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM 43 | } 44 | } 45 | 46 | } } // namespace mozilla::pkix 47 | -------------------------------------------------------------------------------- /evReady/src/pkix/lib/pkixtime.cpp: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This code is made available to you under your choice of the following sets 4 | * of licensing terms: 5 | */ 6 | /* This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | /* Copyright 2014 Mozilla Contributors 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | */ 24 | 25 | #include "pkix/Time.h" 26 | #include "pkixutil.h" 27 | 28 | #ifdef WIN32 29 | #ifdef _MSC_VER 30 | #pragma warning(push, 3) 31 | #endif 32 | #include "windows.h" 33 | #ifdef _MSC_VER 34 | #pragma warning(pop) 35 | #endif 36 | #else 37 | #include "sys/time.h" 38 | #endif 39 | 40 | namespace mozilla { namespace pkix { 41 | 42 | Time 43 | Now() 44 | { 45 | uint64_t seconds; 46 | 47 | #ifdef WIN32 48 | // "Contains a 64-bit value representing the number of 100-nanosecond 49 | // intervals since January 1, 1601 (UTC)." 50 | // - http://msdn.microsoft.com/en-us/library/windows/desktop/ms724284(v=vs.85).aspx 51 | FILETIME ft; 52 | GetSystemTimeAsFileTime(&ft); 53 | uint64_t ft64 = (static_cast(ft.dwHighDateTime) << 32) | 54 | ft.dwLowDateTime; 55 | seconds = (DaysBeforeYear(1601) * Time::ONE_DAY_IN_SECONDS) + 56 | ft64 / (1000u * 1000u * 1000u / 100u); 57 | #else 58 | // "The gettimeofday() function shall obtain the current time, expressed as 59 | // seconds and microseconds since the Epoch." 60 | // - http://pubs.opengroup.org/onlinepubs/009695399/functions/gettimeofday.html 61 | timeval tv; 62 | (void) gettimeofday(&tv, nullptr); 63 | seconds = (DaysBeforeYear(1970) * Time::ONE_DAY_IN_SECONDS) + 64 | static_cast(tv.tv_sec); 65 | #endif 66 | 67 | return TimeFromElapsedSecondsAD(seconds); 68 | } 69 | 70 | Time 71 | TimeFromEpochInSeconds(uint64_t secondsSinceEpoch) 72 | { 73 | uint64_t seconds = (DaysBeforeYear(1970) * Time::ONE_DAY_IN_SECONDS) + 74 | secondsSinceEpoch; 75 | return TimeFromElapsedSecondsAD(seconds); 76 | } 77 | 78 | } } // namespace mozilla::pkix 79 | -------------------------------------------------------------------------------- /evReady/src/pkix/lib/pkixverify.cpp: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This code is made available to you under your choice of the following sets 4 | * of licensing terms: 5 | */ 6 | /* This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | /* Copyright 2015 Mozilla Contributors 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | */ 24 | 25 | #include "pkixutil.h" 26 | 27 | namespace mozilla { namespace pkix { 28 | 29 | Result 30 | DigestSignedData(TrustDomain& trustDomain, 31 | const der::SignedDataWithSignature& signedData, 32 | /*out*/ uint8_t(&digestBuf)[MAX_DIGEST_SIZE_IN_BYTES], 33 | /*out*/ der::PublicKeyAlgorithm& publicKeyAlg, 34 | /*out*/ SignedDigest& signedDigest) 35 | { 36 | Reader signatureAlg(signedData.algorithm); 37 | Result rv = der::SignatureAlgorithmIdentifierValue( 38 | signatureAlg, publicKeyAlg, signedDigest.digestAlgorithm); 39 | if (rv != Success) { 40 | return rv; 41 | } 42 | if (!signatureAlg.AtEnd()) { 43 | return Result::ERROR_BAD_DER; 44 | } 45 | 46 | size_t digestLen; 47 | switch (signedDigest.digestAlgorithm) { 48 | case DigestAlgorithm::sha512: digestLen = 512 / 8; break; 49 | case DigestAlgorithm::sha384: digestLen = 384 / 8; break; 50 | case DigestAlgorithm::sha256: digestLen = 256 / 8; break; 51 | case DigestAlgorithm::sha1: digestLen = 160 / 8; break; 52 | MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM 53 | } 54 | assert(digestLen <= sizeof(digestBuf)); 55 | 56 | rv = trustDomain.DigestBuf(signedData.data, signedDigest.digestAlgorithm, 57 | digestBuf, digestLen); 58 | if (rv != Success) { 59 | return rv; 60 | } 61 | rv = signedDigest.digest.Init(digestBuf, digestLen); 62 | if (rv != Success) { 63 | return rv; 64 | } 65 | 66 | return signedDigest.signature.Init(signedData.signature); 67 | } 68 | 69 | Result 70 | VerifySignedDigest(TrustDomain& trustDomain, 71 | der::PublicKeyAlgorithm publicKeyAlg, 72 | const SignedDigest& signedDigest, 73 | Input signerSubjectPublicKeyInfo) 74 | { 75 | switch (publicKeyAlg) { 76 | case der::PublicKeyAlgorithm::ECDSA: 77 | return trustDomain.VerifyECDSASignedDigest(signedDigest, 78 | signerSubjectPublicKeyInfo); 79 | case der::PublicKeyAlgorithm::RSA_PKCS1: 80 | return trustDomain.VerifyRSAPKCS1SignedDigest(signedDigest, 81 | signerSubjectPublicKeyInfo); 82 | MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM 83 | } 84 | } 85 | 86 | Result 87 | VerifySignedData(TrustDomain& trustDomain, 88 | const der::SignedDataWithSignature& signedData, 89 | Input signerSubjectPublicKeyInfo) 90 | { 91 | uint8_t digestBuf[MAX_DIGEST_SIZE_IN_BYTES]; 92 | der::PublicKeyAlgorithm publicKeyAlg; 93 | SignedDigest signedDigest; 94 | Result rv = DigestSignedData(trustDomain, signedData, digestBuf, 95 | publicKeyAlg, signedDigest); 96 | if (rv != Success) { 97 | return rv; 98 | } 99 | return VerifySignedDigest(trustDomain, publicKeyAlg, signedDigest, 100 | signerSubjectPublicKeyInfo); 101 | } 102 | 103 | } } // namespace mozilla::pkix 104 | -------------------------------------------------------------------------------- /evReady/src/pkix/moz.build: -------------------------------------------------------------------------------- 1 | # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- 2 | # vim: set filetype=python: 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 | SOURCES += [ 8 | 'lib/pkixbuild.cpp', 9 | 'lib/pkixcert.cpp', 10 | 'lib/pkixcheck.cpp', 11 | 'lib/pkixder.cpp', 12 | 'lib/pkixnames.cpp', 13 | 'lib/pkixnss.cpp', 14 | 'lib/pkixocsp.cpp', 15 | 'lib/pkixresult.cpp', 16 | 'lib/pkixtime.cpp', 17 | 'lib/pkixverify.cpp', 18 | ] 19 | 20 | LOCAL_INCLUDES += [ 21 | 'include', 22 | ] 23 | 24 | TEST_DIRS += [ 25 | 'test/gtest', 26 | 'test/lib', 27 | ] 28 | 29 | include('warnings.mozbuild') 30 | 31 | Library('mozillapkix') 32 | 33 | FINAL_LIBRARY = 'xul' 34 | -------------------------------------------------------------------------------- /evReady/src/pkix/test/gtest/README.txt: -------------------------------------------------------------------------------- 1 | ------------- 2 | Running Tests 3 | ------------- 4 | 5 | Because of the rules below, you can run all the unit tests in this directory, 6 | and only these tests, with: 7 | 8 | mach gtest "pkix*" 9 | 10 | You can run just the tests for functions defined in filename pkixfoo.cpp with: 11 | 12 | mach gtest "pkixfoo*" 13 | 14 | If you run "mach gtest" then you'll end up running every gtest in Gecko. 15 | 16 | 17 | 18 | ------------ 19 | Naming Files 20 | ------------ 21 | 22 | Name files containing tests according to one of the following patterns: 23 | 24 | * _tests.cpp 25 | * __tests.cpp 26 | * __tests.cpp 27 | 28 | is the name of the file containing the definitions of the 29 | function(s) being tested by every test. 30 | is the name of the function that is being tested by every 31 | test. 32 | describes the group of related functions that are being 33 | tested by every test. 34 | 35 | 36 | 37 | ------------------------------------------------ 38 | Always Use a Fixture Class: TEST_F(), not TEST() 39 | ------------------------------------------------ 40 | 41 | Many tests don't technically need a fixture, and so TEST() could technically 42 | be used to define the test. However, when you use TEST_F() instead of TEST(), 43 | the compiler will not allow you to make any typos in the test case name, but 44 | if you use TEST() then the name of the test case is not checked. 45 | 46 | See https://code.google.com/p/googletest/wiki/Primer#Test_Fixtures:_Using_the_Same_Data_Configuration_for_Multiple_Te 47 | to learn more about test fixtures. 48 | 49 | --------------- 50 | Naming Fixtures 51 | --------------- 52 | 53 | When all tests in a file use the same fixture, use the base name of the file 54 | without the "_tests" suffix as the name of the fixture class; e.g. tests in 55 | "pkixocsp.cpp" should use a fixture "class pkixocsp" by default. 56 | 57 | Sometimes tests in a file need separate fixtures. In this case, name the 58 | fixture class according to the pattern _, where 59 | is the base name of the file without the "_tests" suffix, and 60 | is a descriptive name for the fixture class, e.g. 61 | "class pkixocsp_DelegatedResponder". 62 | -------------------------------------------------------------------------------- /evReady/src/pkix/test/gtest/moz.build: -------------------------------------------------------------------------------- 1 | # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- 2 | # vim: set filetype=python: 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 | SOURCES += [ 8 | 'pkixbuild_tests.cpp', 9 | 'pkixcert_extension_tests.cpp', 10 | 'pkixcert_signature_algorithm_tests.cpp', 11 | 'pkixcheck_CheckKeyUsage_tests.cpp', 12 | 'pkixcheck_CheckSignatureAlgorithm_tests.cpp', 13 | 'pkixcheck_CheckValidity_tests.cpp', 14 | 15 | # The naming conventions are described in ./README.txt. 16 | 17 | 'pkixder_input_tests.cpp', 18 | 'pkixder_pki_types_tests.cpp', 19 | 'pkixder_universal_types_tests.cpp', 20 | 'pkixgtest.cpp', 21 | 'pkixnames_tests.cpp', 22 | 'pkixocsp_CreateEncodedOCSPRequest_tests.cpp', 23 | 'pkixocsp_VerifyEncodedOCSPResponse.cpp', 24 | ] 25 | 26 | LOCAL_INCLUDES += [ 27 | '../../include', 28 | '../../lib', 29 | '../lib', 30 | ] 31 | 32 | FINAL_LIBRARY = 'xul-gtest' 33 | 34 | include('../../warnings.mozbuild') 35 | 36 | # These warnings are disabled in order to minimize the amount of boilerplate 37 | # required to implement tests, and/or because they originate in the GTest 38 | # framework in a way we cannot otherwise work around. 39 | if CONFIG['CLANG_CXX']: 40 | CXXFLAGS += [ 41 | '-Wno-exit-time-destructors', 42 | '-Wno-global-constructors', 43 | '-Wno-old-style-cast', 44 | '-Wno-used-but-marked-unused', 45 | ] 46 | elif CONFIG['_MSC_VER']: 47 | CXXFLAGS += [ 48 | '-wd4350', # behavior change: 'std::_Wrap_alloc>::... 49 | '-wd4275', # non dll-interface class used as base for dll-interface class 50 | '-wd4548', # Expression before comma has no effect 51 | '-wd4625', # copy constructor could not be generated. 52 | '-wd4626', # assugment operator could not be generated. 53 | '-wd4640', # construction of local static object is not thread safe. 54 | ] 55 | -------------------------------------------------------------------------------- /evReady/src/pkix/test/gtest/pkixgtest.cpp: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This code is made available to you under your choice of the following sets 4 | * of licensing terms: 5 | */ 6 | /* This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | /* Copyright 2013 Mozilla Contributors 11 | * 12 | * Licensed under the Apache License, Version 2.0 (the "License"); 13 | * you may not use this file except in compliance with the License. 14 | * You may obtain a copy of the License at 15 | * 16 | * http://www.apache.org/licenses/LICENSE-2.0 17 | * 18 | * Unless required by applicable law or agreed to in writing, software 19 | * distributed under the License is distributed on an "AS IS" BASIS, 20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | * See the License for the specific language governing permissions and 22 | * limitations under the License. 23 | */ 24 | 25 | #include "pkixgtest.h" 26 | 27 | #include 28 | 29 | #include "pkix/Time.h" 30 | 31 | namespace mozilla { namespace pkix { namespace test { 32 | 33 | const std::time_t ONE_DAY_IN_SECONDS_AS_TIME_T = 34 | static_cast(Time::ONE_DAY_IN_SECONDS); 35 | 36 | // This assumes that time/time_t are POSIX-compliant in that time() returns 37 | // the number of seconds since the Unix epoch. 38 | const std::time_t now(time(nullptr)); 39 | const std::time_t oneDayBeforeNow(time(nullptr) - 40 | ONE_DAY_IN_SECONDS_AS_TIME_T); 41 | const std::time_t oneDayAfterNow(time(nullptr) + 42 | ONE_DAY_IN_SECONDS_AS_TIME_T); 43 | 44 | } } } // namespace mozilla::pkix::test 45 | -------------------------------------------------------------------------------- /evReady/src/pkix/test/lib/moz.build: -------------------------------------------------------------------------------- 1 | # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- 2 | # This code is made available to you under your choice of the following sets 3 | # of licensing terms: 4 | # 5 | # This Source Code Form is subject to the terms of the Mozilla Public 6 | # License, v. 2.0. If a copy of the MPL was not distributed with this 7 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | # 9 | # Copyright 2013 Mozilla Contributors 10 | # 11 | # Licensed under the Apache License, Version 2.0 (the "License"); 12 | # you may not use this file except in compliance with the License. 13 | # You may obtain a copy of the License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the License is distributed on an "AS IS" BASIS, 19 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | # See the License for the specific language governing permissions and 21 | # limitations under the License. 22 | 23 | SOURCES += [ 24 | 'pkixtestnss.cpp', 25 | 'pkixtestutil.cpp', 26 | ] 27 | 28 | Library('pkixtestutil') 29 | 30 | LOCAL_INCLUDES += [ 31 | '../../include', 32 | '../../lib', 33 | ] 34 | 35 | FINAL_LIBRARY = 'xul-gtest' 36 | 37 | FAIL_ON_WARNINGS = True 38 | -------------------------------------------------------------------------------- /evReady/src/pkix/warnings.mozbuild: -------------------------------------------------------------------------------- 1 | FAIL_ON_WARNINGS = True 2 | 3 | if CONFIG['CLANG_CXX']: 4 | CXXFLAGS += [ 5 | '-Weverything', 6 | 7 | '-Wno-c++98-compat', 8 | '-Wno-c++98-compat-pedantic', 9 | '-Wno-missing-prototypes', 10 | '-Wno-missing-variable-declarations', 11 | '-Wno-padded', 12 | '-Wno-reserved-id-macro', # NSPR and NSS use reserved IDs in their include guards. 13 | '-Wno-shadow', # XXX: Clang's rules are too strict for constructors. 14 | '-Wno-weak-vtables', # We rely on the linker to merge the duplicate vtables. 15 | ] 16 | elif CONFIG['_MSC_VER']: 17 | CXXFLAGS += [ 18 | '-sdl', # Enable additional security checks based on Microsoft's SDL. 19 | 20 | '-Wall', 21 | 22 | '-wd4514', # 'function': unreferenced inline function has been removed 23 | '-wd4668', # warning C4668: 'X' is not defined as a preprocessor macro, 24 | # replacing with '0' for '#if/#elif'. 25 | '-wd4710', # 'function': function not inlined 26 | '-wd4711', # function 'function' selected for inline expansion 27 | '-wd4800', # forcing value to bool 'true' or 'false' 28 | '-wd4820', # 'bytes' bytes padding added after construct 'member_name' 29 | 30 | # XXX: We cannot use /Za (Disable Microsoft Extensions) because windows.h 31 | # won't copmile with it. 32 | '-Zc:forScope', # Standard C++ rules for variable scope in for loops. 33 | '-Zc:inline', # Standard C++ rules requiring definition inline functions. 34 | '-Zc:rvalueCast', # Standard C++ rules for result of cast being an rvalue. 35 | '-Zc:strictStrings', # Standard C++ rule that string literals are const. 36 | ] 37 | else: 38 | CXXFLAGS += [ 39 | '-Wall', 40 | '-Wextra', 41 | '-pedantic-errors', 42 | ] 43 | -------------------------------------------------------------------------------- /evReady/src/test/ev-ca.cnf: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 4096 3 | default_keyfile = CA.key 4 | distinguished_name = ca_distinguished_name 5 | x509_extensions = v3_ca 6 | prompt = no 7 | 8 | [ ca_distinguished_name ] 9 | C = US 10 | ST = California 11 | L = Mountain View 12 | O = Mozilla Corporation 13 | OU = EV Testing 14 | CN = EV Test Root 15 | 16 | [ v3_ca ] 17 | basicConstraints = critical,CA:true 18 | keyUsage = critical,cRLSign,keyCertSign 19 | -------------------------------------------------------------------------------- /evReady/src/test/ev-int.cnf: -------------------------------------------------------------------------------- 1 | # Extra OBJECT IDENTIFIER info: 2 | oid_section = new_oids 3 | 4 | [ new_oids ] 5 | # for EV distinguished name stuff 6 | businessCategory = 2.5.4.15 7 | jurisdictionOfIncorporationLocalityName = 1.3.6.1.4.1.311.60.2.1.1 8 | jurisdictionOfIncorporationStateOrProvinceName = 1.3.6.1.4.1.311.60.2.1.2 9 | jurisdictionOfIncorporationCountryName = 1.3.6.1.4.1.311.60.2.1.3 10 | 11 | [ req ] 12 | default_bits = 4096 13 | default_keyfile = int.key 14 | distinguished_name = int_distinguished_name 15 | x509_extensions = v3_int 16 | prompt = no 17 | 18 | [ int_distinguished_name ] 19 | C = US 20 | ST = California 21 | L = Mountain View 22 | O = Mozilla Corporation 23 | OU = EV Testing 24 | 25 | # OID 2.5.4.15 26 | businessCategory = V1.0, Clause 5.(d) 27 | 28 | # OID 1.3.6.1.4.1.311.60.2.1.1 29 | jurisdictionOfIncorporationLocalityName = Mountain View 30 | 31 | # OID 1.3.6.1.4.1.311.60.2.1.2 32 | jurisdictionOfIncorporationStateOrProvinceName = CA 33 | 34 | # OID 1.3.6.1.4.1.311.60.2.1.3 35 | jurisdictionOfIncorporationCountryName = US 36 | CN = EV Test Intermediate 37 | 38 | [ v3_int ] 39 | basicConstraints = critical,CA:true 40 | keyUsage = critical,cRLSign,keyCertSign 41 | authorityInfoAccess = OCSP;URI:http://ev-test.mozilla.example.com:8081/ 42 | certificatePolicies=@v3_req_ev_cp 43 | 44 | [ v3_req_ev_cp ] 45 | policyIdentifier = 1.3.6.1.4.1.13769.666.666.666.1.500.9.1 46 | CPS.1="http://ev-test.mozilla.example.com/cps"; 47 | -------------------------------------------------------------------------------- /evReady/src/test/ev.cnf: -------------------------------------------------------------------------------- 1 | # Copyright 2009 Sid Stamm 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # Author: 16 | # Sid Stamm 17 | 18 | # Extra OBJECT IDENTIFIER info: 19 | oid_section = new_oids 20 | 21 | [ new_oids ] 22 | # for EV distinguished name stuff 23 | businessCategory = 2.5.4.15 24 | jurisdictionOfIncorporationLocalityName = 1.3.6.1.4.1.311.60.2.1.1 25 | jurisdictionOfIncorporationStateOrProvinceName = 1.3.6.1.4.1.311.60.2.1.2 26 | jurisdictionOfIncorporationCountryName = 1.3.6.1.4.1.311.60.2.1.3 27 | 28 | [ req ] 29 | default_bits = 2048 30 | default_keyfile = ee.key 31 | distinguished_name = req_distinguished_name 32 | req_extensions = v3_req # The extensions to add to a certificate request 33 | prompt = no 34 | 35 | [ req_distinguished_name ] 36 | C = US 37 | ST = California 38 | L = Mountain View 39 | O = Mozilla Corporation 40 | OU = EV Testing 41 | 42 | # OID 2.5.4.15 43 | businessCategory = V1.0, Clause 5.(d) 44 | 45 | # OID 1.3.6.1.4.1.311.60.2.1.1 46 | jurisdictionOfIncorporationLocalityName = Mountain View 47 | 48 | # OID 1.3.6.1.4.1.311.60.2.1.2 49 | jurisdictionOfIncorporationStateOrProvinceName = CA 50 | 51 | # OID 1.3.6.1.4.1.311.60.2.1.3 52 | jurisdictionOfIncorporationCountryName = US 53 | 54 | CN = ev-test.mozilla.example.com 55 | 56 | [ v3_req ] 57 | # Extensions to add to a certificate request 58 | basicConstraints = CA:FALSE 59 | keyUsage = digitalSignature, keyEncipherment 60 | authorityInfoAccess = OCSP;URI:http://ev-test.mozilla.example.com:8080/ 61 | certificatePolicies=@v3_req_ev_cp 62 | 63 | [ v3_req_ev_cp ] 64 | policyIdentifier = 1.3.6.1.4.1.13769.666.666.666.1.500.9.1 65 | CPS.1="http://ev-test.mozilla.example.com/cps"; 66 | -------------------------------------------------------------------------------- /evReady/src/test/generate-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #echo "../ev-checker -c github.com.pem -o 2.16.840.1.114412.2.1 -d 'DigiCert'" > run-tests.sh 4 | 5 | echo "V 401212121212Z 401212121212Z 2 unknown /C=US/ST=California/L=Mountain View/O=Mozilla Corporation/OU=EV Testing/businessCategory=V1.0, Clause 5.(d)/1.3.6.1.4.1.311.60.2.1.1=Mountain View/1.3.6.1.4.1.311.60.2.1.2=CA/1.3.6.1.4.1.311.60.2.1.3=US/CN=EV Test Intermediate" > index.txt 6 | echo "V 401212121212Z 401212121212Z 1 unknown /C=US/ST=California/L=Mountain View/O=Mozilla Corporation, OU=EV Testing/CN=EV Test Root" >> index.txt 7 | 8 | echo "#!/bin/bash" > run-tests.sh 9 | chmod u+x run-tests.sh 10 | 11 | #XXX need some way to stop these when the test is done... 12 | echo "openssl ocsp -index index.txt -rsigner int.pem -rkey int.key -port 8080 -CA int.pem &" >> run-tests.sh 13 | echo "openssl ocsp -index index.txt -CA CA.pem -rsigner CA.pem -rkey CA.key -port 8081 &" >> run-tests.sh 14 | 15 | openssl req -new -x509 -days 1825 -nodes -out CA.pem -config ev-ca.cnf 16 | openssl req -new -days 365 -nodes -out int.req -config ev-int.cnf 17 | openssl x509 -req -in int.req -CA CA.pem -CAkey CA.key -extensions v3_int -out int.pem -set_serial 1 -extfile ev-int.cnf 18 | #openssl req -new -out non-ev-cert.req -days 365 -nodes 19 | #openssl x509 -req -in non-ev-cert.req -CA int.pem -CAkey int.key -extensions usr_cert -out non-ev-cert.pem -set_serial 1 20 | #cat CA.pem int.pem non-ev-cert.pem > non-ev-chain.pem 21 | openssl req -new -out ev-cert.req -days 365 -nodes -config ev.cnf 22 | openssl x509 -req -in ev-cert.req -CA int.pem -CAkey int.key -out ev-cert.pem -extfile ev.cnf -set_serial 2 -extensions v3_req 23 | cat ev-cert.pem int.pem CA.pem > ev-chain.pem 24 | echo "../ev-checker -c ev-chain.pem -o 1.3.6.1.4.1.13769.666.666.666.1.500.9.1 -d 'Test EV Policy' -h ev-test.mozilla.example.com" >> run-tests.sh 25 | openssl req -new -out ev-cert-no-int.req -days 365 -key ee.key -config ev.cnf 26 | openssl x509 -req -in ev-cert-no-int.req -CA CA.pem -CAkey CA.key -out ev-cert-no-int.pem -extfile ev.cnf -set_serial 3 -extensions v3_req 27 | cat ev-cert-no-int.pem CA.pem > ev-chain-no-int.pem 28 | echo "../ev-checker -c ev-chain-no-int.pem -o 1.3.6.1.4.1.13769.666.666.666.1.500.9.1 -d 'Test EV Policy' -h ev-test.mozilla.example.com" >> run-tests.sh 29 | -------------------------------------------------------------------------------- /evReady/ui/html/base.tmpl: -------------------------------------------------------------------------------- 1 | {{define "base"}} 2 | 3 | 4 | 5 | 6 | {{template "title" .}} 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |

    EV Certificate Readiness Tool

    15 |
    16 |
    17 |
    18 | Documentation: Wiki - PSM:EV_Testing_Easy_Version 19 |
    20 | {{with .Flash}} 21 |
    {{.}}
    22 | {{end}} 23 | {{template "main" .}} 24 |
    25 | 26 | 27 | {{end}} -------------------------------------------------------------------------------- /evReady/ui/html/pages/home.tmpl: -------------------------------------------------------------------------------- 1 | {{define "title"}}EV Readiness{{end}} 2 | 3 | {{define "main"}} 4 |
    5 |
    6 | 7 | {{with .Form.FieldErrors.hostname}} 8 | 9 | {{end}} 10 | 11 |
    12 |
    13 | 14 | {{with .Form.FieldErrors.oid}} 15 | 16 | {{end}} 17 | 18 |
    19 |
    20 | 21 | {{with .Form.FieldErrors.rootCertUpload}} 22 | 23 | {{end}} 24 |
    25 | 26 |
    27 | {{with .Form.FieldErrors.rootCert}} 28 | 29 | {{end}} 30 |
    31 | 32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 | {{end}} -------------------------------------------------------------------------------- /evReady/ui/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/CCADB-Tools/ea10e50c76c4cc331efd7afaaf6f66589414a0d7/evReady/ui/static/img/favicon.ico -------------------------------------------------------------------------------- /oneCRLDiffCCADB/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 | # This is necessary to statically compile all 10 | # C libraries into the executable. Otherwise 11 | # the Alpine installation will fail out with 12 | # a "no such directory" when attempting to execute 13 | # the binary (it can't find the shared libs). 14 | ENV CGO_ENABLED=0 15 | RUN go build main.go 16 | 17 | FROM alpine:latest 18 | 19 | RUN apk add ca-certificates 20 | COPY --from=buildStage /opt/ /opt/ 21 | 22 | CMD ["/opt/main"] -------------------------------------------------------------------------------- /oneCRLDiffCCADB/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 onecrldiffccadb 7 | -docker rm onecrldiffccadb 8 | -docker rmi onecrldiffccadb 9 | -docker image prune -f 10 | -docker image prune -f --filter label=stage=intermediate 11 | 12 | build: 13 | docker build --rm -t onecrldiffccadb:latest . 14 | docker image prune -f 15 | docker image prune -f --filter label=stage=intermediate 16 | 17 | run: 18 | ./run.sh -------------------------------------------------------------------------------- /oneCRLDiffCCADB/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: custom 2 | env: flex 3 | service: onecrldiff 4 | -------------------------------------------------------------------------------- /oneCRLDiffCCADB/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 | "encoding/hex" 9 | "fmt" 10 | "github.com/gocarina/gocsv" 11 | "math/big" 12 | "net/http" 13 | ) 14 | 15 | const report = "https://ccadb.my.salesforce-sites.com/mozilla/PublicIntermediateCertsRevokedWithPEMCSV" 16 | 17 | const ( 18 | Added = "Added to OneCRL" 19 | ReadyToAdd = "Ready to Add" 20 | Expired = "Cert Expired" 21 | ) 22 | 23 | type Entry struct { 24 | IssuerCommonName string `csv:"Certificate Issuer Common Name" json:"issuerCN"` 25 | Serial string `csv:"Certificate Serial Number" json:"serial"` 26 | Fingerprint string `csv:"SHA-256 Fingerprint" json:"sha_256"` 27 | RevocationStatus string `csv:"OneCRL Status" json:"revocationStatus"` 28 | IssuerOrganizationName string `csv:"Certificate Issuer Organization" json:"issuerON"` 29 | } 30 | 31 | // Key constructs a string that is the concatenation of the certificate serial (decoded from hex to an decimal value) 32 | // the issuer common name, and the issuer organization name. This key is used to join the results of the CCADB 33 | // with OneCRL. 34 | func (e *Entry) Key() string { 35 | return fmt.Sprintf("%s%s%s", e.decodeSerial(), e.IssuerCommonName, e.IssuerOrganizationName) 36 | } 37 | 38 | // Retrieve downloads the CCADB report located at 39 | // https://ccadb.my.salesforce-sites.com/mozilla/PublicIntermediateCertsRevokedWithPEMCSV 40 | // and returns a mapping "key"s to entries. 41 | // 42 | // The "key" in this case is the string concatenation of the decimal value of the certificate serial number, 43 | // the issuer common name, and the issuer organization name. 44 | func Retrieve() (map[string]*Entry, error) { 45 | result := make(map[string]*Entry, 0) 46 | resp, err := http.DefaultClient.Get(report) 47 | if err != nil { 48 | return result, err 49 | } 50 | defer resp.Body.Close() 51 | var e []*Entry 52 | if err := gocsv.Unmarshal(resp.Body, &e); err != nil { 53 | return result, err 54 | } 55 | for _, cert := range e { 56 | result[cert.Key()] = cert 57 | } 58 | return result, err 59 | } 60 | 61 | func (e *Entry) decodeSerial() string { 62 | s, err := hex.DecodeString(e.Serial) 63 | if err != nil { 64 | panic(err) 65 | } 66 | return big.NewInt(0).SetBytes(s).String() 67 | } 68 | -------------------------------------------------------------------------------- /oneCRLDiffCCADB/ccadb/ccadb_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 ( 8 | "testing" 9 | ) 10 | 11 | func TestGet(t *testing.T) { 12 | certs, err := Retrieve() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | t.Log(certs) 17 | } 18 | -------------------------------------------------------------------------------- /oneCRLDiffCCADB/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mozilla/CCADB-Tools/oneCRLDiffCCADB 2 | 3 | go 1.21 4 | 5 | require github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d 6 | -------------------------------------------------------------------------------- /oneCRLDiffCCADB/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d h1:KbPOUXFUDJxwZ04vbmDOc3yuruGvVO+LOa7cVER3yWw= 2 | github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= 3 | -------------------------------------------------------------------------------- /oneCRLDiffCCADB/normalized/normalize_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 normalized 6 | 7 | import ( 8 | "github.com/mozilla/CCADB-Tools/oneCRLDiffCCADB/ccadb" 9 | "github.com/mozilla/CCADB-Tools/oneCRLDiffCCADB/oneCRL" 10 | "testing" 11 | ) 12 | 13 | func TestNormalize(t *testing.T) { 14 | c, err := ccadb.Retrieve() 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | o, err := oneCRL.Retrieve() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | n := Join(c, o) 23 | t.Log(len(c)) 24 | t.Log(len(o)) 25 | t.Log(len(n)) 26 | } 27 | -------------------------------------------------------------------------------- /oneCRLDiffCCADB/normalized/normalized.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 normalized 6 | 7 | import ( 8 | "encoding/json" 9 | "github.com/mozilla/CCADB-Tools/oneCRLDiffCCADB/ccadb" 10 | "github.com/mozilla/CCADB-Tools/oneCRLDiffCCADB/oneCRL" 11 | "strings" 12 | ) 13 | 14 | // Join performs a join on the entries from the CCADB and OneCRL using the "Key" constructed by those entities. 15 | func Join(c map[string]*ccadb.Entry, o map[string]*oneCRL.OneCRLIntermediate) []*Normalized { 16 | intermediate := make(map[string]*Normalized, len(c)) 17 | for key, cert := range c { 18 | n := new(Normalized) 19 | n.Entry = cert 20 | intermediate[key] = n 21 | } 22 | for key, cert := range o { 23 | n := intermediate[key] 24 | if n == nil { 25 | n = new(Normalized) 26 | } 27 | n.OneCRLIntermediate = cert 28 | } 29 | flat := make([]*Normalized, 0) 30 | for _, v := range intermediate { 31 | flat = append(flat, v) 32 | } 33 | return flat 34 | } 35 | 36 | type Normalized struct { 37 | *ccadb.Entry 38 | *oneCRL.OneCRLIntermediate 39 | } 40 | 41 | func (n Normalized) MarshalJSON() ([]byte, error) { 42 | return json.Marshal(n.Entry) 43 | } 44 | 45 | func New(c *ccadb.Entry, o *oneCRL.OneCRLIntermediate) *Normalized { 46 | return &Normalized{c, o} 47 | } 48 | 49 | // The consequent grouping of methods encode the following truth table. 50 | // 51 | // "Added to OneCRL" "Cert Expired" "Ready to Add" Absent from Report 52 | // Present in OneCRL ✅ ❌ ❌ ❌ 53 | // Absent from OneCRL ❌ ✅ ✅ ✅ 54 | // 55 | // ...where a ✅ is typically considered fine and a ❌ is considered an error case, although 56 | // this tool does not do any deeper interpretation than merely providing the results 57 | // of building this table. 58 | 59 | func (n *Normalized) AddedAndPresent() bool { 60 | return n.Entry != nil && n.OneCRLIntermediate != nil && n.Entry.RevocationStatus == ccadb.Added 61 | } 62 | 63 | func (n *Normalized) ExpiredAndPresent() bool { 64 | return n.Entry != nil && n.OneCRLIntermediate != nil && n.Entry.RevocationStatus == ccadb.Expired 65 | } 66 | 67 | func (n *Normalized) ReadyAndPresent() bool { 68 | return n.Entry != nil && n.OneCRLIntermediate != nil && n.Entry.RevocationStatus == ccadb.ReadyToAdd 69 | } 70 | 71 | func (n *Normalized) AbsentAndPresent() bool { 72 | return n.Entry == nil && n.OneCRLIntermediate != nil 73 | } 74 | 75 | func (n *Normalized) AddedAndAbsent() bool { 76 | return n.Entry != nil && n.OneCRLIntermediate == nil && n.Entry.RevocationStatus == ccadb.Added 77 | } 78 | 79 | func (n *Normalized) ExpiredAndAbsent() bool { 80 | return n.Entry != nil && n.OneCRLIntermediate == nil && n.Entry.RevocationStatus == ccadb.Expired 81 | } 82 | 83 | func (n *Normalized) ReadyAndAbsent() bool { 84 | return n.Entry != nil && n.OneCRLIntermediate == nil && n.Entry.RevocationStatus == ccadb.ReadyToAdd 85 | } 86 | 87 | func (n *Normalized) NoRevocationStatus() bool { 88 | return n.Entry != nil && strings.Trim(n.RevocationStatus, " ") == "" 89 | } 90 | 91 | func (n *Normalized) AbsentAndAbsent() bool { 92 | // unknowable? 93 | return false 94 | } 95 | -------------------------------------------------------------------------------- /oneCRLDiffCCADB/oneCRL/oneCRL.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 oneCRL 6 | 7 | import ( 8 | "bytes" 9 | "crypto/x509/pkix" 10 | "encoding/asn1" 11 | "encoding/base64" 12 | "encoding/json" 13 | "fmt" 14 | "math/big" 15 | "net/http" 16 | ) 17 | 18 | const OneCRLEndpoint = "https://firefox.settings.services.mozilla.com/v1/buckets/blocklists/collections/certificates/records" 19 | 20 | type OneCRLIntermediates struct { 21 | Data []*OneCRLIntermediate `json:"data"` 22 | } 23 | 24 | type OneCRLIntermediate struct { 25 | Schema int `json:"schema"` 26 | Details struct { 27 | Bug string `json:"bug"` 28 | Who string `json:"who"` 29 | Why string `json:"why"` 30 | Name string `json:"name"` 31 | Created string `json:"created"` 32 | } `json:"details"` 33 | Enabled bool `json:"enabled"` 34 | IssuerName Name `json:"issuerName"` 35 | SerialNumber string `json:"serialNumber"` 36 | Id string `json:"id"` 37 | LastModified int `json:"last_modified"` 38 | } 39 | 40 | // Key constructs a string that is the concatenation of the certificate serial (decoded from base64 to an decimal value) 41 | // the issuer common name, and the issuer organization name. This key is used to join the results of OneCRL with the 42 | // CCADB. 43 | func (o *OneCRLIntermediate) Key() string { 44 | cn, org := o.IssuerName.Key() 45 | return fmt.Sprintf("%s%s%s", o.decodeSerial(), cn, org) 46 | } 47 | 48 | // Retrieve downloads the OneCRL report located at 49 | // https://firefox.settings.services.mozilla.com/v1/buckets/blocklists/collections/certificates/records 50 | // and returns a mapping "key"s to entries. 51 | // 52 | // The "key" in this case is the string concatenation of the decimal value of the certificate serial number, 53 | // the issuer common name, and the issuer organization name. 54 | func Retrieve() (map[string]*OneCRLIntermediate, error) { 55 | result := make(map[string]*OneCRLIntermediate) 56 | var intermediates OneCRLIntermediates 57 | resp, err := http.DefaultClient.Get(OneCRLEndpoint) 58 | if err != nil { 59 | return result, err 60 | } 61 | defer resp.Body.Close() 62 | err = json.NewDecoder(resp.Body).Decode(&intermediates) 63 | if err != nil { 64 | return result, err 65 | } 66 | for _, cert := range intermediates.Data { 67 | result[cert.Key()] = cert 68 | } 69 | return result, nil 70 | } 71 | 72 | func (o *OneCRLIntermediate) decodeSerial() string { 73 | s, err := base64.StdEncoding.DecodeString(o.SerialNumber) 74 | if err != nil { 75 | panic(err) 76 | } 77 | return big.NewInt(0).SetBytes(s).String() 78 | } 79 | 80 | // Name wraps a a vanilla RDN so that we can attach further methods for deserialization from JSON and extraction 81 | // of the issuer Common Name and Organization Name. 82 | type Name struct { 83 | // https://tools.ietf.org/html/rfc5280#section-4.1.2.4 84 | pkix.RDNSequence 85 | } 86 | 87 | func (n *Name) Key() (string, string) { 88 | cn := "" 89 | on := "" 90 | for _, i := range n.RDNSequence { 91 | for _, j := range i { 92 | switch j.Type.String() { 93 | // CN http://oidref.com/2.5.4.3 94 | case "2.5.4.3": 95 | cn = fmt.Sprint(j.Value) 96 | // ON http://oidref.com/2.5.4.10 97 | case "2.5.4.10": 98 | on = fmt.Sprint(j.Value) 99 | } 100 | } 101 | } 102 | return cn, on 103 | } 104 | 105 | func (n *Name) UnmarshalJSON(raw []byte) error { 106 | // As it comes in, this buffer is just a JSON string, which 107 | // includes double quotes that we do not want or need. 108 | raw = bytes.Trim(raw, `"`) 109 | dst := make([]byte, len(raw)) 110 | _, err := base64.StdEncoding.Decode(dst, raw) 111 | if err != nil { 112 | return err 113 | } 114 | _, err = asn1.Unmarshal(dst, &n.RDNSequence) 115 | if err != nil { 116 | return err 117 | } 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /oneCRLDiffCCADB/oneCRL/oneCRL_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 oneCRL 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestGet(t *testing.T) { 12 | r, err := Retrieve() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | t.Log(r) 17 | } 18 | -------------------------------------------------------------------------------- /oneCRLDiffCCADB/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 onecrldiffccadb \ 13 | -d \ 14 | -e "PORT=$CONTAINER_PORT" \ 15 | -p ${HOST_PORT}:${CONTAINER_PORT} \ 16 | onecrldiffccadb 17 | 18 | --------------------------------------------------------------------------------