EVChecker/main.go (348 lines of code) (raw):
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package main // import "github.com/mozilla/CCADB-Tools"
import (
"bytes"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"strings"
)
const nightly = "https://hg.mozilla.org/mozilla-central/raw-file/tip/security/certverifier/ExtendedValidation.cpp"
const beta = "https://hg.mozilla.org/releases/mozilla-beta/raw-file/tip/security/certverifier/ExtendedValidation.cpp"
const release = "https://hg.mozilla.org/releases/mozilla-release/raw-file/tip/security/certverifier/ExtendedValidation.cpp"
func get(url string) ([]byte, error) {
client := http.Client{}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Add("X-Automated-Tool", "https://github.com/mozilla/CCADB-Tools/EVChecker")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return b, nil
}
// Yanks out the array literal of EVInfos.
var canary = regexp.MustCompile(`static const struct EVInfo kEVInfos.*\n(.*\n)*};`)
// Strips away all comments and DEBUG pragma blocks.
var commentsAndDebugPragmas = regexp.MustCompile(`(//.*\n|#ifdef DEBUG.*\n(.*\n)*#endif)`)
// Finds instances of multiline strings, E.G. any string literal this is followed by a raw newline.
var multlineString = regexp.MustCompile(`"\s*\n`)
// After putting all entries of a multiline string onto the same line, we need to strip away
// any spaces between the string entries as well as their internal quotation marks.
var joinMultiLine = regexp.MustCompile(`"\s*"`)
func extract(src []byte) ([]*EVInfo, error) {
b := canary.Find(src)
b = commentsAndDebugPragmas.ReplaceAll(b, []byte{})
b = multlineString.ReplaceAll(b, []byte{'"'})
b = joinMultiLine.ReplaceAll(b, []byte{})
return deser(b)
}
type EVInfo struct {
DottedOID string
OIDName string
SHA256Fingerprint string
Issuer string
Serial string
}
func deser(src []byte) ([]*EVInfo, error) {
kEVinfos := make([]*EVInfo, 0)
r := bytes.NewReader(src)
var b rune
var err error
// consumes all declaration info up to the opening brace that starts the array literal.
// I.E. static const struct .... {
for b, _, err = r.ReadRune(); err == nil && b != '{'; b, _, err = r.ReadRune() {
}
if err != nil {
return kEVinfos, errors.Wrap(err, "failed to begin reading the kEVinfos array")
}
for {
b, err := consumeWhiteSpace(r)
if err != nil {
return kEVinfos, err
}
switch b {
case '{':
// Begin an EVInfo object boundary.
evinfo, err := NewEVInfo(r)
if err != nil {
return kEVinfos, err
}
kEVinfos = append(kEVinfos, evinfo)
case '}':
// The end of the kEVinfo array
return kEVinfos, nil
default:
return kEVinfos, errors.New(fmt.Sprintf(`received an unexpected character while parsing the kEVInfos array, got ""%s"`, string(b)))
}
}
}
func NewEVInfo(r io.RuneReader) (*EVInfo, error) {
dottedOid, err := extractStringField(r)
if err != nil {
return nil, err
}
oidName, err := extractStringField(r)
if err != nil {
return nil, err
}
fp, err := extractFingerprint(r)
if err != nil {
return nil, err
}
issuer, err := extractStringField(r)
if err != nil {
return nil, err
}
issuer, err = decodeIssuer(issuer)
if err != nil {
return nil, err
}
serial, err := extractStringField(r)
if err != nil {
return nil, err
}
s, err := base64.StdEncoding.DecodeString(serial)
if err != nil {
return nil, err
}
// hex Serial number left padded with 0
serial = fmt.Sprintf("%032X", s)
brace, err := consumeWhiteSpace(r)
if err != nil {
return nil, err
}
if brace != '}' {
return nil, errors.New(fmt.Sprintf("expected a closing brace for an EVInfo boundary, but got %s", string(brace)))
}
comma, err := consumeWhiteSpace(r)
if err != nil {
return nil, errors.Wrap(err, "failed to consume the delimiting comma between EVInfos")
}
// consume up to the , that delimits struct literal fields.
// This does not in any way honor the final field omitting this comma.
if comma != ',' {
return nil, errors.New(fmt.Sprintf(`expected the character "," while decoding the delimiter for an EVInfo array, got "%s"`, string(comma)))
}
return &EVInfo{
DottedOID: dottedOid,
OIDName: oidName,
SHA256Fingerprint: fp,
Issuer: issuer,
Serial: serial,
}, nil
}
func decodeIssuer(i string) (string, error) {
b, err := base64.StdEncoding.DecodeString(i)
if err != nil {
return "", err
}
var issuer pkix.RDNSequence
_, err = asn1.Unmarshal(b, &issuer)
if err != nil {
return "", err
}
return issuer.String(), nil
}
func extractFingerprint(r io.RuneReader) (string, error) {
_, err := consumeWhiteSpace(r)
if err != nil {
return "", errors.Wrap(err, "failed to consume the whitespace prefixing a fingerprint")
}
str := strings.Builder{}
for i := 0; i < 32; i++ {
zero, err := consumeWhiteSpace(r)
if err != nil {
return "", errors.Wrap(err, "failed to consume whitespace while extracting fingerprint")
}
if zero != '0' {
return "", errors.New(fmt.Sprintf(`Expected the character "0" while decoding hex, got "%s"`, string(zero)))
}
x, _, err := r.ReadRune() // read the x in 0xAA
if err != nil {
return "", errors.Wrap(err, `failed to read the "x" in a hex literal`)
}
if x != 'x' {
return "", errors.New(fmt.Sprintf(`Expected the character "x" while decoding hex, got "%s"`, string(x)))
}
b, _, err := r.ReadRune() // get the upper byte of 0xAA
if err != nil {
return "", errors.Wrap(err, `failed to read the upper byte in a hex literal`)
}
str.WriteRune(b)
b, _, err = r.ReadRune() // get the lower byte of 0xAA
if err != nil {
return "", errors.Wrap(err, `failed to read the lower byte in a hex literal`)
}
str.WriteRune(b)
comma, _, err := r.ReadRune() // read the , in the array literal
if err != nil {
return "", errors.Wrap(err, `failed to read the hex literal array delimiter`)
}
if comma != ',' && comma != ' ' {
return "", errors.New(fmt.Sprintf(`Expected the character "," or " " while decoding the internals of a hex array, got "%s"`, string(x)))
}
}
brace, err := consumeWhiteSpace(r)
if err != nil {
return "", errors.Wrap(err, "failed to consume the closing brace while extracting a fingerprint")
}
if brace != '}' {
return "", errors.New(fmt.Sprintf(`Expected the character "}" while decoding a hex array, got "%s"`, string(brace)))
}
comma, err := consumeWhiteSpace(r)
if comma != ',' {
return "", errors.New(fmt.Sprintf(`Expected the character "," while decoding the end of a hex array, got "%s"`, string(comma)))
}
return str.String(), nil
}
func extractStringField(r io.RuneReader) (string, error) {
// consume whitespace up to the opening quote
b, err := consumeWhiteSpace(r)
if err != nil {
return "", errors.Wrap(err, "failed to consume the leading whitespace before a string literal")
}
if b != '"' {
return "", errors.New(fmt.Sprintf(`expected the beginning byte of a string to be an opening quote, but we got a "%s"`, string(b)))
}
str := strings.Builder{}
// consume the string literal
// This loop does not in any way honor escaped quotes.
for b, _, err = r.ReadRune(); err == nil && b != '"'; b, _, err = r.ReadRune() {
str.WriteRune(b) // always returns a nil error
}
if err != nil {
return "", errors.Wrap(err, "failure occurred while consuming a string literal")
}
// consume up to the , that delimits struct literal fields.
// This does not in any way honor the final field omitting this comma.
b, err = consumeWhiteSpace(r)
if err != nil {
return "", errors.Wrap(err, "failed to consume the leading whitespace before the string field delimiter (comma)")
}
if b != ',' {
return "", errors.New(fmt.Sprintf(`expected the final byte of a string field to be a comma, but we got a "%s"`, string(b)))
}
return str.String(), nil
}
// consumes all whitespace up to the next non-whitespace run and returns that rune.
func consumeWhiteSpace(r io.RuneReader) (rune, error) {
var b rune
var err error
for b, _, err = r.ReadRune(); err == nil; b, _, err = r.ReadRune() {
switch b {
case ' ', '\n':
default:
return b, err
}
}
return b, err
}
type Response struct {
Error *string
EVInfos []*EVInfo
}
func nightlyHandler(w http.ResponseWriter, r *http.Request) {
corehandler(w, r, nightly)
}
func betaHandler(w http.ResponseWriter, r *http.Request) {
corehandler(w, r, beta)
}
func releaseHandler(w http.ResponseWriter, r *http.Request) {
corehandler(w, r, release)
}
func givenHandler(w http.ResponseWriter, r *http.Request) {
u, ok := r.URL.Query()["url"]
if !ok {
w.WriteHeader(400)
_, err := w.Write([]byte("'url' is a required query parameter"))
if err != nil {
log.Println(err)
}
return
}
if len(u) == 0 {
w.WriteHeader(400)
_, err := w.Write([]byte("'url' query parameter may not be empty"))
if err != nil {
log.Println(err)
}
return
}
target, err := url.QueryUnescape(u[0])
if err != nil {
w.WriteHeader(400)
_, err = w.Write([]byte(fmt.Sprintf("failed to decode `url` query parameter, err: %s", err)))
if err != nil {
log.Println(err)
}
return
}
corehandler(w, r, target)
}
func corehandler(w http.ResponseWriter, r *http.Request, target string) {
resp := Response{}
code := 500
defer func() {
w.WriteHeader(code)
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
if err := encoder.Encode(resp); err != nil {
log.Print(err)
}
}()
file, err := get(target)
if err != nil {
code = 500
errStr := err.Error()
resp.Error = &errStr
return
}
kEVInfos, err := extract(file)
if err != nil {
code = 500
errStr := err.Error()
resp.Error = &errStr
return
}
resp.EVInfos = kEVInfos
}
func main() {
http.HandleFunc("/nightly", nightlyHandler)
http.HandleFunc("/beta", betaHandler)
http.HandleFunc("/release", releaseHandler)
http.HandleFunc("/", givenHandler)
port := Port()
addr := BindingAddress()
if err := http.ListenAndServe(addr+":"+port, nil); err != nil {
log.Panicln(err)
}
}
func Port() string {
return fmt.Sprintf("%d", parseIntFromEnvOrDie("PORT", 8080))
}
func BindingAddress() string {
switch addr := os.Getenv("ADDR"); addr {
case "":
return "0.0.0.0"
default:
_, _, err := net.ParseCIDR(addr)
if err != nil {
panic("failed to parse the provided ADDR to a valid CIDR")
}
return addr
}
}
func parseIntFromEnvOrDie(key string, defaultVal int) int {
switch val := os.Getenv(key); val {
case "":
return defaultVal
default:
i, err := strconv.ParseUint(val, 10, 32)
if err != nil {
fmt.Printf("%s (%s) could not be parsed to an integer", val, key)
os.Exit(1)
}
return int(i)
}
}