summaryrefslogtreecommitdiff
path: root/bin-src
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@lukeshu.com>2018-03-14 18:18:31 -0400
committerLuke Shumaker <lukeshu@lukeshu.com>2018-03-17 13:49:41 -0400
commitb54a1c9686eec3c1114e9b58cb67679ba59c45bd (patch)
tree0bdb2f3ed51ff077a8c3e337e4bc556aacec108e /bin-src
parent54feeb027d6e5a760b49769dfe695ea2591dc6fe (diff)
directories
Diffstat (limited to 'bin-src')
-rwxr-xr-xbin-src/cron-daily5
-rwxr-xr-xbin-src/crtsh-getcerts28
-rw-r--r--bin-src/crtsh-pem2html.go150
-rw-r--r--bin-src/diff-pem2html.go109
-rw-r--r--bin-src/pem-diff.go140
-rw-r--r--bin-src/tls-getcerts.go192
-rw-r--r--bin-src/tls-pem2html.go160
-rw-r--r--bin-src/util/date.go51
-rw-r--r--bin-src/util/html.go14
9 files changed, 849 insertions, 0 deletions
diff --git a/bin-src/cron-daily b/bin-src/cron-daily
new file mode 100755
index 0000000..7b71669
--- /dev/null
+++ b/bin-src/cron-daily
@@ -0,0 +1,5 @@
+#!/bin/sh
+cd "$(dirname -- "$0")/.."
+date > NET-crtsh
+date > NET-tls
+make
diff --git a/bin-src/crtsh-getcerts b/bin-src/crtsh-getcerts
new file mode 100755
index 0000000..0191e2e
--- /dev/null
+++ b/bin-src/crtsh-getcerts
@@ -0,0 +1,28 @@
+#!/usr/bin/env ruby
+require 'nokogiri'
+require 'open-uri'
+
+certs = {}
+ARGV.each do |domain|
+ [ domain, "%.#{domain}" ].each do |pattern|
+ Nokogiri::XML(open("https://crt.sh/atom?identity=#{pattern}&exclude=expired")).css('feed > entry').each do |entry|
+ url = entry.css('id').first.text.split("#").first
+
+ updated = entry.css('updated').first.text
+
+ html = Nokogiri::HTML(entry.css('summary').first.text)
+ html.css('br').each{|br| br.replace("\n")}
+ pem = html.css('div').first.text
+
+ lines = pem.split("\n")
+ lines.insert(1, "X-Crt-Sh-Url: #{url}", "X-Crt-Sh-Updated: #{updated}")
+ pem = lines.join("\n")+"\n"
+
+ certs[url] = pem
+ end
+ end
+end
+
+certs.each do |url, pem|
+ print pem
+end
diff --git a/bin-src/crtsh-pem2html.go b/bin-src/crtsh-pem2html.go
new file mode 100644
index 0000000..109917c
--- /dev/null
+++ b/bin-src/crtsh-pem2html.go
@@ -0,0 +1,150 @@
+package main
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "html/template"
+ "io/ioutil"
+ "os"
+ "sort"
+ "time"
+
+ "./util"
+)
+
+func handleErr(err error, str string, a ...interface{}) {
+ a = append([]interface{}{err}, a...)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, str, a...)
+ os.Exit(1)
+ }
+}
+
+func handleBool(ok bool, str string, a ...interface{}) {
+ if !ok {
+ fmt.Fprintf(os.Stderr, str, a...)
+ os.Exit(1)
+ }
+}
+
+var tmpl = template.Must(template.New("pem2html").
+ Funcs(template.FuncMap{
+ "red": red,
+ "green": green,
+ "date": util.Date2HTML,
+ "datetime": util.DateTime2HTML,
+ "colorDatetime": util.DateTime2ColorHTML,
+ }).Parse(`<table class=sortable>
+ <caption>
+ <p>CT log (Updated {{.now | colorDatetime}})</p>
+ </caption>
+ <tr>
+ <th>Logged</th>
+ <th>NotBefore</th>
+ <th>NotAfter</th>
+ <th>Subject.CN</th>
+ <th>Issuer.O</th>
+ </tr>
+{{range $cert := .certs}}
+ <tr>
+ <td style="background-color: {{$cert.Updated | green}}"><a target="_blank" href="{{$cert.Url}}">{{$cert.Updated | date}}</a></td>
+ <td style="background-color: {{$cert.X509.NotBefore | green}}"><a target="_blank" href="{{$cert.Url}}">{{$cert.X509.NotBefore | date}}</a></td>
+ <td style="background-color: {{$cert.X509.NotAfter | red }}"><a target="_blank" href="{{$cert.Url}}">{{$cert.X509.NotAfter | date}}</a></td>
+ <td><a target="_blank" href="{{$cert.Url}}">{{$cert.X509.Subject.CommonName}}</a></td>
+ <td><a target="_blank" href="{{$cert.Url}}">{{$cert.X509.Issuer.Organization}}</a></td>
+ </tr>
+{{end}}
+</table>
+`))
+
+func getNow() time.Time {
+ stat, err := os.Stdin.Stat()
+ if err == nil {
+ return stat.ModTime()
+ } else {
+ return time.Now()
+ }
+}
+
+var now = getNow()
+
+func green(t time.Time) string {
+ max := byte(0xF3)
+ // When did we get the cert?
+ // - 30 days ago => 0 green
+ // - just now => max green
+ greenness := util.MapRange(
+ util.TimeRange{now.AddDate(0, 0, -30), now},
+ util.ByteRange{0, max},
+ t)
+ return fmt.Sprintf("#%02X%02X%02X", max-greenness, max, max-greenness)
+}
+
+func red(t time.Time) string {
+ max := byte(0xF3)
+ // When with the cert expire?
+ // - now => max red
+ // - 30 days from now => 0 red
+ redness := util.MapRange(
+ util.TimeRange{now, now.AddDate(0, 0, 30)},
+ util.ByteRange{max, 0},
+ t)
+ return fmt.Sprintf("#%02X%02X%02X", max, max-redness, max-redness)
+}
+
+type Cert struct {
+ Url string
+ Updated time.Time
+ X509 *x509.Certificate
+}
+
+type Certs []Cert
+
+// Len is the number of elements in the collection.
+func (l Certs) Len() int {
+ return len(l)
+}
+
+// Less reports whether the element with
+// index i should sort before the element with index j.
+func (l Certs) Less(i, j int) bool {
+ return l[i].Updated.After(l[j].Updated)
+}
+
+// Swap swaps the elements with indexes i and j.
+func (l Certs) Swap(i, j int) {
+ tmp := l[i]
+ l[i] = l[j]
+ l[j] = tmp
+}
+
+func main() {
+ data, err := ioutil.ReadAll(os.Stdin)
+ handleErr(err, "Error reading stdin: %v\n")
+
+ var certs Certs
+ for len(data) > 0 {
+ var certPem *pem.Block
+ certPem, data = pem.Decode(data)
+
+ var ok bool
+ var cert Cert
+
+ cert.Url, ok = certPem.Headers["X-Crt-Sh-Url"]
+ handleBool(ok, "Did not get X-Crt-Sh-Url\n")
+
+ str, ok := certPem.Headers["X-Crt-Sh-Updated"]
+ handleBool(ok, "Did not get X-Crt-Sh-Updated\n")
+ cert.Updated, err = time.Parse("2006-01-02T15:04:05Z", str)
+ handleErr(err, "Could not parse updated time")
+
+ cert.X509, err = x509.ParseCertificate(certPem.Bytes)
+ handleErr(err, "Error parsing cert: %v\n")
+
+ certs = append(certs, cert)
+ }
+
+ sort.Sort(certs)
+ handleErr(tmpl.Execute(os.Stdout, map[string]interface{}{"certs": certs, "now": now}), "Could not execute template: %v\n")
+}
diff --git a/bin-src/diff-pem2html.go b/bin-src/diff-pem2html.go
new file mode 100644
index 0000000..f3b25ff
--- /dev/null
+++ b/bin-src/diff-pem2html.go
@@ -0,0 +1,109 @@
+package main
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "html/template"
+ "io/ioutil"
+ "os"
+
+ "./util"
+)
+
+func handleErr(err error, str string, a ...interface{}) {
+ a = append([]interface{}{err}, a...)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, str, a...)
+ os.Exit(1)
+ }
+}
+
+func handleBool(ok bool, str string, a ...interface{}) {
+ if !ok {
+ fmt.Fprintf(os.Stderr, str, a...)
+ os.Exit(1)
+ }
+}
+
+var tmpl = template.Must(template.New("pem2html").
+ Funcs(template.FuncMap{
+ "htmlcell": util.HTMLCellEscapeString,
+ }).Parse(`<table class=diff>
+ <tr class="diff-del"><td colspan=4>--- tls.pem</td></tr>
+ <tr class="diff-add"><td colspan=4>+++ crtsh.pem</td></tr>
+ <tr class="diff-dat"><td colspan=4>@@ -1,{{.nTLS}} +1,{{.nCrtSh}} @@</td></tr>
+{{range $cert := .certs}}
+ <tr class={{$cert.Class}}>
+ <td><a href="{{$cert.Url}}">{{$cert.Pfix | htmlcell}}</a></td>
+ <td><a href="{{$cert.Url}}">{{$cert.X509.Subject.CommonName | htmlcell}}</a></td>
+ <td><a href="{{$cert.Url}}">{{$cert.X509.NotBefore.Local.Format "2006-01-02 15:04:05"}}</a></td>
+ <td><a href="{{$cert.Url}}">{{$cert.X509.NotAfter.Local.Format "2006-01-02 15:04:05"}}</a></td>
+ </tr>
+{{end}}
+</table>
+`))
+
+type Cert struct {
+ Url string
+ action string
+ X509 *x509.Certificate
+}
+
+func (cert Cert) Pfix() string {
+ return map[string]string{
+ "add": "+",
+ "del": "-",
+ "ctx": " ",
+ }[cert.action]
+}
+
+func (cert Cert) Class() string {
+ return "diff-" + cert.action
+}
+
+func main() {
+ data, err := ioutil.ReadAll(os.Stdin)
+ handleErr(err, "Error reading stdin: %v\n")
+
+ var certs []Cert
+ a := 0
+ b := 0
+ for len(data) > 0 {
+ var certPem *pem.Block
+ certPem, data = pem.Decode(data)
+
+ var ok bool
+ var cert Cert
+
+ cert.Url, ok = certPem.Headers["X-Crt-Sh-Url"]
+ handleBool(ok, "Did not get X-Crt-Sh-Url\n")
+
+ cert.action, ok = certPem.Headers["X-Diff-Action"]
+ handleBool(ok, "Did not get X-Diff-Action\n")
+ switch cert.action {
+ case "add":
+ b++
+ case "del":
+ a++
+ case "ctx":
+ a++
+ b++
+ default:
+ handleBool(false, "Unknown X-Diff-Action: %q\n", cert.action)
+ }
+
+ cert.X509, err = x509.ParseCertificate(certPem.Bytes)
+ if err != nil {
+ cert.X509 = new(x509.Certificate)
+ }
+
+ certs = append(certs, cert)
+ }
+
+ handleErr(tmpl.Execute(os.Stdout, map[string]interface{}{
+ "certs": certs,
+ "nTLS": a,
+ "nCrtSh": b,
+ }), "Could not execute template: %v\n")
+}
diff --git a/bin-src/pem-diff.go b/bin-src/pem-diff.go
new file mode 100644
index 0000000..da27a62
--- /dev/null
+++ b/bin-src/pem-diff.go
@@ -0,0 +1,140 @@
+package main
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/url"
+ "os"
+ "sort"
+ "strings"
+)
+
+func handleErr(err error, str string, a ...interface{}) {
+ a = append([]interface{}{err}, a...)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, str, a...)
+ os.Exit(1)
+ }
+}
+
+type Cert struct {
+ Url string
+ X509 *x509.Certificate
+}
+
+func (cert Cert) WriteTo(w io.Writer, action string) error {
+ block := pem.Block{
+ Type: "CERTIFICATE",
+ Headers: map[string]string{
+ "X-Crt-Sh-Url": cert.Url,
+ "X-Diff-Action": action,
+ },
+ Bytes: cert.X509.Raw,
+ }
+ return pem.Encode(w, &block)
+}
+
+func readTLS(filename string) (map[string]Cert, error) {
+ file, err := os.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+ data, err := ioutil.ReadAll(file)
+ if err != nil {
+ return nil, err
+ }
+
+ ret := make(map[string]Cert)
+ for len(data) > 0 {
+ var certPem *pem.Block
+ certPem, data = pem.Decode(data)
+ certX509, err := x509.ParseCertificate(certPem.Bytes)
+ if err != nil {
+ url, err2 := url.Parse(certPem.Headers["X-Socket"])
+ if err2 != nil {
+ fmt.Fprintf(os.Stderr, "Could not get cert or even parse URL:\ncert: %v\nurl: %v\n", err, err2)
+ os.Exit(1)
+ }
+ ret[strings.Split(url.Host, ":")[0]] = Cert{
+ X509: new(x509.Certificate),
+ }
+ } else {
+ ret[certX509.Subject.CommonName] = Cert{
+ Url: fmt.Sprintf("https://crt.sh/?serial=%036x", certX509.SerialNumber),
+ X509: certX509,
+ }
+ }
+ }
+ return ret, nil
+}
+
+func readCrtSh(filename string, hosts []string) (map[string]Cert, error) {
+ file, err := os.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+ data, err := ioutil.ReadAll(file)
+ if err != nil {
+ return nil, err
+ }
+
+ ret := make(map[string]Cert)
+ for len(data) > 0 {
+ var certPem *pem.Block
+ certPem, data = pem.Decode(data)
+ certX509, err := x509.ParseCertificate(certPem.Bytes)
+ if err != nil {
+ return nil, err
+ }
+ for _, host := range hosts {
+ if certX509.VerifyHostname(host) == nil {
+ if old, haveold := ret[host]; !haveold || certX509.NotBefore.After(old.X509.NotBefore) {
+ ret[host] = Cert{
+ Url: certPem.Headers["X-Crt-Sh-Url"],
+ X509: certX509,
+ }
+ }
+ }
+ }
+ }
+ return ret, nil
+}
+
+func keys(m map[string]Cert) []string {
+ ret := make([]string, len(m))
+ i := 0
+ for k := range m {
+ ret[i] = k
+ i++
+ }
+ sort.Strings(ret)
+ return ret
+}
+
+func main() {
+ if len(os.Args) != 3 {
+ fmt.Fprintf(os.Stderr, "Usage: %s TLS-file crt.sh-file\n", os.Args[0])
+ }
+ certsTLS, err := readTLS(os.Args[1])
+ handleErr(err, "Could load TLS file: %v\n")
+ hostsTLS := keys(certsTLS)
+ certsCrtSh, err := readCrtSh(os.Args[2], hostsTLS)
+ handleErr(err, "Could load crt.sh file: %v\n")
+
+ for _, host := range hostsTLS {
+ certTLS := certsTLS[host]
+ certCrtSh, haveCrtSh := certsCrtSh[host]
+
+ if !haveCrtSh {
+ handleErr(certTLS.WriteTo(os.Stdout, "del"), "Could not encode PEM: %v\n")
+ } else if !certTLS.X509.Equal(certCrtSh.X509) {
+ handleErr(certTLS.WriteTo(os.Stdout, "del"), "Could not encode PEM: %v\n")
+ handleErr(certCrtSh.WriteTo(os.Stdout, "add"), "Could not encode PEM: %v\n")
+ } else {
+ handleErr(certCrtSh.WriteTo(os.Stdout, "ctx"), "Could not encode PEM: %v\n")
+ }
+ }
+}
diff --git a/bin-src/tls-getcerts.go b/bin-src/tls-getcerts.go
new file mode 100644
index 0000000..34e25e5
--- /dev/null
+++ b/bin-src/tls-getcerts.go
@@ -0,0 +1,192 @@
+package main
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/pem"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net"
+ "net/textproto"
+ "net/url"
+ "os"
+ "strings"
+ "time"
+)
+
+type xmppStreamsFeatures struct {
+ XMLName xml.Name `xml:"http://etherx.jabber.org/streams features"`
+}
+
+type xmppTlsProceed struct {
+ XMLName xml.Name `xml:"urn:ietf:params:xml:ns:xmpp-tls proceed"`
+}
+
+func xmppStartTLS(connRaw net.Conn, host string) error {
+ decoder := xml.NewDecoder(connRaw)
+
+ // send <stream> start
+ _, err := fmt.Fprintf(connRaw, "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='%s' version='1.0'>", host)
+ if err != nil {
+ return err
+ }
+ // read <stream> start
+ for {
+ t, err := decoder.Token()
+ if err != nil || t == nil {
+ return err
+ }
+ if se, ok := t.(xml.StartElement); ok {
+ if se.Name.Local != "stream" {
+ return xml.UnmarshalError(fmt.Sprintf("expected element of type <%s> but have <%s>", "stream", se.Name.Local))
+ }
+ break
+ }
+ }
+ // read <features>
+ var features xmppStreamsFeatures
+ err = decoder.DecodeElement(&features, nil)
+ if err != nil {
+ return err
+ }
+ // send <starttls>
+ _, err = io.WriteString(connRaw, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
+ if err != nil {
+ return err
+ }
+ // read <proceed>
+ var proceed xmppTlsProceed
+ err = decoder.DecodeElement(&proceed, nil)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// smtpCmd is a convenience function that sends a command, and reads
+// (but discards) the response
+func smtpCmd(tp *textproto.Conn, expectCode int, format string, args ...interface{}) error {
+ id, err := tp.Cmd(format, args...)
+ if err != nil {
+ return err
+ }
+ tp.StartResponse(id)
+ defer tp.EndResponse(id)
+ _, _, err = tp.ReadResponse(expectCode)
+ return err
+}
+
+func smtpStartTLS(connRaw net.Conn, host string) error {
+ tp := textproto.NewConn(connRaw)
+
+ // let the server introduce itself
+ _, _, err := tp.ReadResponse(220)
+ if err != nil {
+ return err
+ }
+ // introduce ourself
+ localhost, err := os.Hostname()
+ if err != nil {
+ localhost = "localhost"
+ }
+ err = smtpCmd(tp, 250, "EHLO %s", localhost)
+ if err != nil {
+ err := smtpCmd(tp, 250, "HELO %s", localhost)
+ if err != nil {
+ return err
+ }
+ }
+ // starttls
+ err = smtpCmd(tp, 220, "STARTTLS")
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func getcert(socket string) (*x509.Certificate, error) {
+ u, err := url.Parse(socket)
+ if err != nil {
+ return nil, err
+ }
+ host, _, err := net.SplitHostPort(u.Host)
+ if err != nil {
+ return nil, err
+ }
+
+ connRaw, err := net.Dial(u.Scheme, u.Host)
+ if err != nil {
+ return nil, err
+ }
+ err = connRaw.SetDeadline(time.Now().Add(5 * time.Second))
+ if err != nil {
+ return nil, err
+ }
+
+ switch u.Path {
+ case "", "/":
+ // do nothing
+ case "/xmpp":
+ err = xmppStartTLS(connRaw, host)
+ if err != nil {
+ return nil, err
+ }
+ case "/smtp":
+ err = smtpStartTLS(connRaw, host)
+ if err != nil {
+ return nil, err
+ }
+ default:
+ return nil, fmt.Errorf("Unknown negotiation path: %q", u.Path)
+ }
+
+ connTLS := tls.Client(connRaw, &tls.Config{InsecureSkipVerify: true})
+ defer connTLS.Close()
+ err = connTLS.Handshake()
+ if err != nil {
+ return nil, err
+ }
+
+ cstate := connTLS.ConnectionState()
+
+ opts := x509.VerifyOptions{
+ DNSName: host,
+ Intermediates: x509.NewCertPool(),
+ }
+ for _, cert := range cstate.PeerCertificates[1:] {
+ opts.Intermediates.AddCert(cert)
+ }
+
+ cert := cstate.PeerCertificates[0]
+ _, err = cert.Verify(opts)
+ return cert, err
+}
+
+func split(socket string) (net, addr string) {
+ ary := strings.SplitN(socket, ":", 2)
+ if len(ary) == 1 {
+ return "tcp", ary[0]
+ }
+ return ary[0], ary[1]
+}
+
+func main() {
+ for _, socket := range os.Args[1:] {
+ fmt.Fprintf(os.Stderr, "Getting %q... ", socket)
+ block := pem.Block{
+ Type: "CERTIFICATE",
+ Headers: map[string]string{"X-Socket": socket},
+ Bytes: nil,
+ }
+ cert, err := getcert(socket)
+ if cert != nil {
+ block.Bytes = cert.Raw
+ }
+ if err != nil {
+ block.Headers["X-Error"] = err.Error()
+ }
+ pem.Encode(os.Stdout, &block)
+ fmt.Fprintln(os.Stderr, "[done]")
+ }
+}
diff --git a/bin-src/tls-pem2html.go b/bin-src/tls-pem2html.go
new file mode 100644
index 0000000..bc14f9a
--- /dev/null
+++ b/bin-src/tls-pem2html.go
@@ -0,0 +1,160 @@
+package main
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "html/template"
+ "io/ioutil"
+ "os"
+ "sort"
+ "time"
+
+ "./util"
+)
+
+func handleErr(err error, str string, a ...interface{}) {
+ a = append([]interface{}{err}, a...)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, str, a...)
+ os.Exit(1)
+ }
+}
+
+func handleBool(ok bool, str string, a ...interface{}) {
+ if !ok {
+ fmt.Fprintf(os.Stderr, str, a...)
+ os.Exit(1)
+ }
+}
+
+var tmpl = template.Must(template.New("pem2html").
+ Funcs(template.FuncMap{
+ "red": red,
+ "green": green,
+ "date": util.Date2HTML,
+ "datetime": util.DateTime2HTML,
+ "colorDatetime": util.DateTime2ColorHTML,
+ "htmlcell": util.HTMLCellEscapeString,
+ }).Parse(`<table class=sortable>
+ <caption>
+ <p>Live Certs (Updated {{.now | colorDatetime}})</p>
+ </caption>
+ <tr>
+ <th>NotBefore</th>
+ <th>NotAfter</th>
+ <th>Subject.CN</th>
+ <th>Socket</th>
+ </tr>
+{{range $cert := .certs}}
+ <tr class="{{$cert.Class}}">
+ <td style="background-color: {{$cert.X509.NotBefore | green}}"><a href="{{$cert.Url}}" title="{{$cert.Error}}">{{$cert.X509.NotBefore | date}}</a></td>
+ <td style="background-color: {{$cert.X509.NotAfter | red }}"><a href="{{$cert.Url}}" title="{{$cert.Error}}">{{$cert.X509.NotAfter | date}}</a></td>
+ <td><a href="{{$cert.Url}}" title="{{$cert.Error}}">{{$cert.X509.Subject.CommonName | htmlcell}}</a></td>
+ <td><a href="{{$cert.Url}}" title="{{$cert.Error}}">{{$cert.Socket | htmlcell}}</a></td>
+ </tr>
+{{end}}
+</table>
+`))
+
+func getNow() time.Time {
+ stat, err := os.Stdin.Stat()
+ if err == nil {
+ return stat.ModTime()
+ } else {
+ return time.Now()
+ }
+}
+
+var now = getNow()
+
+func green(t time.Time) string {
+ max := byte(0xF3)
+ // When did we get the cert?
+ // - 30 days ago => 0 green
+ // - just now => max green
+ greenness := util.MapRange(
+ util.TimeRange{now.AddDate(0, 0, -30), now},
+ util.ByteRange{0, max},
+ t)
+ return fmt.Sprintf("#%02X%02X%02X", max-greenness, max, max-greenness)
+}
+
+func red(t time.Time) string {
+ max := byte(0xF3)
+ // When with the cert expire?
+ // - now => max red
+ // - 30 days from now => 0 red
+ redness := util.MapRange(
+ util.TimeRange{now, now.AddDate(0, 0, 30)},
+ util.ByteRange{max, 0},
+ t)
+ return fmt.Sprintf("#%02X%02X%02X", max, max-redness, max-redness)
+}
+
+type Cert struct {
+ Socket string
+ Error string
+ X509 *x509.Certificate
+}
+
+func (cert Cert) Url() string {
+ return fmt.Sprintf("https://crt.sh/?serial=%036x", cert.X509.SerialNumber)
+}
+
+func (cert Cert) Class() string {
+ if cert.Error == "" {
+ return ""
+ } else {
+ return "invalid"
+ }
+}
+
+type Certs []Cert
+
+// Len is the number of elements in the collection.
+func (l Certs) Len() int {
+ return len(l)
+}
+
+// Less reports whether the element with
+// index i should sort before the element with index j.
+func (l Certs) Less(i, j int) bool {
+ return l[i].X509.NotAfter.After(l[j].X509.NotAfter)
+}
+
+// Swap swaps the elements with indexes i and j.
+func (l Certs) Swap(i, j int) {
+ tmp := l[i]
+ l[i] = l[j]
+ l[j] = tmp
+}
+
+func main() {
+ data, err := ioutil.ReadAll(os.Stdin)
+ handleErr(err, "Error reading stdin: %v\n")
+
+ var certs Certs
+ for len(data) > 0 {
+ var certPem *pem.Block
+ certPem, data = pem.Decode(data)
+
+ var ok bool
+ var cert Cert
+
+ cert.Socket, ok = certPem.Headers["X-Socket"]
+ handleBool(ok, "Did not get X-Socket\n")
+
+ cert.Error, ok = certPem.Headers["X-Error"]
+
+ cert.X509, err = x509.ParseCertificate(certPem.Bytes)
+ if err != nil {
+ cert.X509 = new(x509.Certificate)
+ }
+
+ certs = append(certs, cert)
+ }
+
+ sort.Sort(certs)
+ handleErr(tmpl.Execute(os.Stdout, map[string]interface{}{"certs": certs, "now": now}), "Could not execute template: %v\n")
+}
diff --git a/bin-src/util/date.go b/bin-src/util/date.go
new file mode 100644
index 0000000..3b5c457
--- /dev/null
+++ b/bin-src/util/date.go
@@ -0,0 +1,51 @@
+package util
+
+import (
+ "html/template"
+ "time"
+)
+
+func Date2HTML(t time.Time) template.HTML {
+ return template.HTML(t.Local().Format("<time datetime=\"2006-01-02 15:04:05\" title=\"2006-01-02 15:04:05\">2006-01-02 <span class=time>15:04:05</span></time>"))
+}
+
+func DateTime2HTML(t time.Time) template.HTML {
+ return template.HTML(t.Local().Format("<time datetime=\"2006-01-02 15:04:05\">2006-01-02 15:04:05</time>"))
+}
+
+func DateTime2ColorHTML(t time.Time) template.HTML {
+ return template.HTML(t.Local().Format("<time class=daily datetime=\"2006-01-02 15:04:05\">2006-01-02 15:04:05</time>"))
+}
+
+type TimeRange struct {
+ A, B time.Time
+}
+
+func (tr TimeRange) ToPct(point time.Time) float64 {
+ dur_ab := tr.B.Sub(tr.A)
+ dur_ap := point.Sub(tr.A)
+ return float64(dur_ap) / float64(dur_ab)
+}
+
+type ByteRange struct {
+ A, B byte
+}
+
+func (br ByteRange) FromPct(pct float64) byte {
+ ab := int16(br.B) - int16(br.A)
+ ap := int16(pct * float64(ab))
+ return byte(int16(br.A) + ap)
+}
+
+func PctCap(pct float64) float64 {
+ if pct < 0 {
+ pct = 0
+ } else if pct > 1 {
+ pct = 1
+ }
+ return pct
+}
+
+func MapRange(tr TimeRange, br ByteRange, t time.Time) byte {
+ return br.FromPct(PctCap(tr.ToPct(t)))
+}
diff --git a/bin-src/util/html.go b/bin-src/util/html.go
new file mode 100644
index 0000000..af2ce60
--- /dev/null
+++ b/bin-src/util/html.go
@@ -0,0 +1,14 @@
+package util
+
+import (
+ "html/template"
+ "strings"
+)
+
+func HTMLCellEscapeString(s string) template.HTML {
+ html := template.HTMLEscapeString(s)
+ if strings.TrimSpace(html) == "" {
+ html = "&nbsp;"
+ }
+ return template.HTML(html)
+}