Files
certigo/lib/display.go
T

366 lines
10 KiB
Go

/*-
* Copyright 2016 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lib
import (
"bufio"
"bytes"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"text/template"
"time"
"crypto/x509/pkix"
"github.com/Masterminds/sprig"
"github.com/fatih/color"
)
var verboseLayout = `
{{- define "PkixName" -}}
{{- range .Names}}
{{ .Type | oidName }}: {{ .Value }}
{{- end -}}
{{end -}}
{{- if .Alias}}{{.Alias}}
{{end}}Serial: {{.SerialNumber}}
Valid: {{.NotBefore | certStart}} to {{.NotAfter | certEnd}}
Signature: {{.SignatureAlgorithm | highlightAlgorithm}}{{if .IsSelfSigned}} (self-signed){{end}}
Subject Info:
{{- template "PkixName" .Subject.Name}}
Issuer Info:
{{- template "PkixName" .Issuer.Name}}
{{- if .Subject.KeyID}}
Subject Key ID: {{.Subject.KeyID | hexify}}
{{- end}}
{{- if .Issuer.KeyID}}
Authority Key ID: {{.Issuer.KeyID | hexify}}
{{- end}}
{{- if .BasicConstraints}}
Basic Constraints: CA:{{.BasicConstraints.IsCA}}{{if .BasicConstraints.MaxPathLen}}, pathlen:{{.BasicConstraints.MaxPathLen}}{{end}}{{end}}
{{- if .NameConstraints}}
Name Constraints{{if .NameConstraints.Critical}} (critical){{end}}:
{{- if .NameConstraints.PermittedDNSDomains}}
Permitted DNS domains:
{{wrapWith .Width "\n\t" (join ", " .NameConstraints.PermittedDNSDomains)}}
{{- end -}}
{{- if .NameConstraints.PermittedEmailAddresses}}
Permitted email addresses:
{{wrapWith .Width "\n\t" (join ", " .NameConstraints.PermittedEmailAddresses)}}
{{- end -}}
{{- if .NameConstraints.PermittedIPRanges}}
Permitted IP ranges:
{{wrapWith .Width "\n\t" (join ", " .NameConstraints.PermittedIPRanges)}}
{{- end -}}
{{- if .NameConstraints.PermittedURIDomains}}
Permitted URI domains:
{{wrapWith .Width "\n\t" (join ", " .NameConstraints.PermittedURIDomains)}}
{{- end}}
{{- if .NameConstraints.ExcludedDNSDomains}}
Excluded DNS domains:
{{wrapWith .Width "\n\t" (join ", " .NameConstraints.ExcludedDNSDomains)}}
{{- end}}
{{- if .NameConstraints.ExcludedEmailAddresses}}
Excluded email addresses:
{{wrapWith .Width "\n\t" (join ", " .NameConstraints.ExcludedEmailAddresses)}}
{{- end}}
{{- if .NameConstraints.ExcludedIPRanges}}
Excluded IP ranges:
{{wrapWith .Width "\n\t" (join ", " .NameConstraints.ExcludedIPRanges)}}
{{- end}}
{{- if .NameConstraints.ExcludedURIDomains}}
Excluded URI domains:
{{wrapWith .Width "\n\t" (join ", " .NameConstraints.ExcludedURIDomains)}}
{{- end}}
{{- end}}
{{- if .OCSPServer}}
OCSP Server(s):
{{wrapWith .Width "\n\t" (join ", " .OCSPServer)}}
{{- end}}
{{- if .IssuingCertificateURL}}
Issuing Certificate URL(s):
{{wrapWith .Width "\n\t" (join ", " .IssuingCertificateURL)}}
{{- end}}
{{- if .KeyUsage}}
Key Usage:
{{- range .KeyUsage | keyUsage}}
{{.}}
{{- end}}
{{- end}}
{{- if .ExtKeyUsage}}
Extended Key Usage:
{{- range .ExtKeyUsage}}
{{. | extKeyUsage}}{{end}}
{{- end}}
{{- if .AltDNSNames}}
DNS Names:
{{wrapWith .Width "\n\t" (join ", " .AltDNSNames)}}
{{- end}}
{{- if .AltIPAddresses}}
IP Addresses:
{{wrapWith .Width "\n\t" (join ", " .AltIPAddresses)}}
{{- end}}
{{- if .URINames}}
URI Names:
{{wrapWith .Width "\n\t" (join ", " .URINames)}}
{{- end}}
{{- if .EmailAddresses}}
Email Addresses:
{{wrapWith .Width "\n\t" (join ", " .EmailAddresses)}}
{{- end}}
{{- if .Warnings}}
Warnings:
{{- range .Warnings}}
{{. | redify}}
{{- end}}
{{- end}}`
var layout = `
{{- if .Alias}}{{.Alias}}
{{end -}}
Valid: {{.NotBefore | certStart}} to {{.NotAfter | certEnd}}
Subject:
{{wrapWith .Width "\n\t" (.Subject.Name | printShortName)}}
Issuer:
{{wrapWith .Width "\n\t" (.Issuer.Name | printShortName)}}
{{- if .AltDNSNames}}
DNS Names:
{{wrapWith .Width "\n\t" (join ", " .AltDNSNames)}}{{end}}
{{- if .AltIPAddresses}}
IP Addresses:
{{wrapWith .Width "\n\t" (join ", " .AltIPAddresses)}}{{end}}
{{- if .URINames}}
URI Names:
{{wrapWith .Width "\n\t" (join ", " .URINames)}}{{end}}
{{- if .EmailAddresses}}
Email Addresses:
{{wrapWith .Width "\n\t" (join ", " .EmailAddresses)}}{{end}}
{{- if .Warnings}}
Warnings:{{range .Warnings}}
{{. | redify}}{{end}}{{end}}`
type certWithName struct {
name string
file string
cert *x509.Certificate
}
func (c certWithName) MarshalJSON() ([]byte, error) {
out := createSimpleCertificate(c.name, c.cert)
return json.Marshal(out)
}
func createSimpleCertificateFromX509(block *pem.Block) (simpleCertificate, error) {
raw, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return simpleCertificate{}, fmt.Errorf("error reading cert: %s", err)
}
cert := certWithName{cert: raw}
if val, ok := block.Headers[nameHeader]; ok {
cert.name = val
}
if val, ok := block.Headers[fileHeader]; ok {
cert.file = val
}
return createSimpleCertificate(cert.name, cert.cert), nil
}
// EncodeX509ToJSON encodes an X.509 certificate into a JSON string.
func EncodeX509ToJSON(cert *x509.Certificate) []byte {
out := createSimpleCertificate("", cert)
raw, err := json.Marshal(out)
if err != nil {
panic(err)
}
return raw
}
// EncodeX509ToObject encodes an X.509 certificate into a JSON-serializable object.
func EncodeX509ToObject(cert *x509.Certificate) interface{} {
return createSimpleCertificate("", cert)
}
// EncodeX509ToText encodes an X.509 certificate into human-readable text.
func EncodeX509ToText(cert *x509.Certificate, terminalWidth int, verbose bool) []byte {
c := createSimpleCertificate("", cert)
c.Width = terminalWidth - 8 /* Need some margin for tab */
return displayCert(c, verbose)
}
// displayCert takes in a parsed certificate object
// (for jceks certs, blank otherwise), and prints out relevant
// information. Start and end dates are colored based on whether or not
// the certificate is expired, not expired, or close to expiring.
func displayCert(cert simpleCertificate, verbose bool) []byte {
// Use template functions from sprig, but add some extras
funcMap := sprig.TxtFuncMap()
extras := template.FuncMap{
"certStart": certStart,
"certEnd": certEnd,
"redify": redify,
"highlightAlgorithm": highlightAlgorithm,
"hexify": hexify,
"keyUsage": keyUsage,
"extKeyUsage": extKeyUsage,
"oidName": oidName,
"oidShort": oidShort,
"printShortName": PrintShortName,
"printCommonName": PrintCommonName,
}
for k, v := range extras {
funcMap[k] = v
}
t := template.New("Cert template").Funcs(funcMap)
var err error
if verbose {
t, err = t.Parse(verboseLayout)
} else {
t, err = t.Parse(layout)
}
if err != nil {
// Should never happen
panic(err)
}
var buffer bytes.Buffer
w := bufio.NewWriter(&buffer)
err = t.Execute(w, cert)
if err != nil {
// Should never happen
panic(err)
}
w.Flush()
return buffer.Bytes()
}
var (
green = color.New(color.Bold, color.FgGreen)
yellow = color.New(color.Bold, color.FgYellow)
red = color.New(color.Bold, color.FgRed)
)
var algorithmColors = map[x509.SignatureAlgorithm]*color.Color{
x509.MD2WithRSA: red,
x509.MD5WithRSA: red,
x509.SHA1WithRSA: red,
x509.SHA256WithRSA: green,
x509.SHA384WithRSA: green,
x509.SHA512WithRSA: green,
x509.DSAWithSHA1: red,
x509.DSAWithSHA256: red,
x509.ECDSAWithSHA1: red,
x509.ECDSAWithSHA256: green,
x509.ECDSAWithSHA384: green,
x509.ECDSAWithSHA512: green,
}
// highlightAlgorithm changes the color of the signing algorithm
// based on a set color map, e.g. to make SHA-1 show up red.
func highlightAlgorithm(sigAlg simpleSigAlg) string {
sig := x509.SignatureAlgorithm(sigAlg)
color, ok := algorithmColors[sig]
if !ok {
return algString(sig)
}
return color.SprintFunc()(algString(sig))
}
// timeString formats a time in UTC with minute precision, in the given color.
func timeString(t time.Time, c *color.Color) string {
return c.SprintfFunc()(t.Format("2006-01-02 15:04 MST"))
}
// certStart takes a given start time for the validity of
// a certificate and returns that time colored properly
// based on how close it is to expiry. If it's more than
// a day after the certificate became valid the string will
// be green. If it has been less than a day the string will
// be yellow. If the certificate is not yet valid, the string
// will be red.
func certStart(start time.Time) string {
now := time.Now()
day, _ := time.ParseDuration("24h")
threshold := start.Add(day)
if now.After(threshold) {
return timeString(start, green)
} else if now.After(start) {
return timeString(start, yellow)
} else {
return timeString(start, red)
}
}
// certEnd takes a given end time for the validity of
// a certificate and returns that time colored properly
// based on how close it is to expiry. If the certificate
// is more than a month away from expiring it returns a
// green string. If the certificate is less than a month
// from expiry it returns a yellow string. If the certificate
// is expired it returns a red string.
func certEnd(end time.Time) string {
now := time.Now()
month, _ := time.ParseDuration("720h")
threshold := now.Add(month)
if threshold.Before(end) {
return timeString(end, green)
} else if now.Before(end) {
return timeString(end, yellow)
} else {
return timeString(end, red)
}
}
func redify(text string) string {
return red.SprintfFunc()("%s", text)
}
func greenify(text string) string {
return green.SprintfFunc()("%s", text)
}
// PrintCommonName prints the CN from a pkix.Name, or falls back to PrintShortName if CN is missing.
func PrintCommonName(name pkix.Name) (out string) {
if name.CommonName != "" {
return fmt.Sprintf("CN=%s", name.CommonName)
}
return PrintShortName(name)
}
// PrintShortName turns a pkix.Name into a string of RDN tuples.
func PrintShortName(name pkix.Name) (out string) {
printed := false
for _, name := range name.Names {
short := oidShort(name.Type)
if short != "" {
if printed {
out += ", "
}
out += fmt.Sprintf("%s=%v", short, name.Value)
printed = true
}
}
return
}