From a3ead16faf7c1df840c44044369172f561ea6495 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 26 Sep 2017 15:59:39 -0700 Subject: [PATCH] Add ski utility. --- cmd/ski/README | 30 ++++++++ cmd/ski/main.go | 187 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 cmd/ski/README create mode 100644 cmd/ski/main.go diff --git a/cmd/ski/README b/cmd/ski/README new file mode 100644 index 0000000..892be3b --- /dev/null +++ b/cmd/ski/README @@ -0,0 +1,30 @@ +ski: print subject public key info + +Usage: + ski [-hm] files... + +Flags: + -h Print a help message and exit. + -m All SKIs should match. + +Examples: + + Printing the SKI of a private key and certificate: + + $ ski * + server.key 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA private key) + [ski] trailing data in PEM file + server.pem 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA certificate) + + Making sure the SKIs match: + + $ ski -m * + tyrfingr.key 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA private key) + [ski] trailing data in PEM file + tyrfingr.pem 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA certificate) + + Making sure the SKIs match with a bad certificate: + $ ski -m server.key bad.pem + server.key 3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E (RSA private key) + [ski] bad.pem: SKI mismatch (3A:AB:D1:B2:E5:7A:F2:5A:D5:8E:8B:7B:25:D9:41:90:F8:6B:A3:5E != 90:AF:6A:3A:94:5A:0B:D8:90:EA:12:56:73:DF:43:B4:3A:28:DA:E7) + bad.pem 90:AF:6A:3A:94:5A:0B:D8:90:EA:12:56:73:DF:43:B4:3A:28:DA:E7 (RSA certificate) diff --git a/cmd/ski/main.go b/cmd/ski/main.go new file mode 100644 index 0000000..3e794bf --- /dev/null +++ b/cmd/ski/main.go @@ -0,0 +1,187 @@ +package main + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + + "github.com/kisom/goutils/die" + "github.com/kisom/goutils/lib" +) + +func usage(w io.Writer) { + fmt.Fprintf(w, `ski: print subject key info for PEM-encoded files + +Usage: + ski [-hm] files... + +Flags: + -h Print this help message. + -m All SKIs should match; as soon as an SKI mismatch is found, + it is reported. + +`) +} + +func init() { + flag.Usage = func() { usage(os.Stderr) } +} + +func parse(path string) (public []byte, kt, ft string) { + data, err := ioutil.ReadFile(path) + die.If(err) + + data = bytes.TrimSpace(data) + p, rest := pem.Decode(data) + if len(rest) > 0 { + lib.Warnx("trailing data in PEM file") + } + + data = p.Bytes + + switch p.Type { + case "PRIVATE KEY", "RSA PRIVATE KEY", "EC PRIVATE KEY": + public, kt = parseKey(data) + ft = "private key" + case "CERTIFICATE": + public, kt = parseCertificate(data) + ft = "certificate" + case "CERTIFICATE REQUEST": + public, kt = parseCSR(data) + ft = "certificate request" + default: + die.With("unknown PEM type %s", p.Type) + } + + return +} + +func parseKey(data []byte) (public []byte, kt string) { + privInterface, err := x509.ParsePKCS8PrivateKey(data) + if err != nil { + privInterface, err = x509.ParsePKCS1PrivateKey(data) + if err != nil { + privInterface, err = x509.ParseECPrivateKey(data) + if err != nil { + die.With("couldn't parse private key.") + } + } + } + + var priv crypto.Signer + switch privInterface.(type) { + case *rsa.PrivateKey: + priv = privInterface.(*rsa.PrivateKey) + kt = "RSA" + case *ecdsa.PrivateKey: + priv = privInterface.(*ecdsa.PrivateKey) + kt = "ECDSA" + default: + die.With("unknown private key type %T", privInterface) + } + + public, err = x509.MarshalPKIXPublicKey(priv.Public()) + die.If(err) + + return +} + +func parseCertificate(data []byte) (public []byte, kt string) { + cert, err := x509.ParseCertificate(data) + die.If(err) + + pub := cert.PublicKey + switch pub.(type) { + case *rsa.PublicKey: + kt = "RSA" + case *ecdsa.PublicKey: + kt = "ECDSA" + default: + die.With("unknown public key type %T", pub) + } + + public, err = x509.MarshalPKIXPublicKey(pub) + die.If(err) + return +} + +func parseCSR(data []byte) (public []byte, kt string) { + csr, err := x509.ParseCertificateRequest(data) + die.If(err) + + pub := csr.PublicKey + switch pub.(type) { + case *rsa.PublicKey: + kt = "RSA" + case *ecdsa.PublicKey: + kt = "ECDSA" + default: + die.With("unknown public key type %T", pub) + } + + public, err = x509.MarshalPKIXPublicKey(pub) + die.If(err) + return +} + +func dumpHex(in []byte) string { + var s string + for i := range in { + s += fmt.Sprintf("%02X:", in[i]) + } + + return strings.Trim(s, ":") +} + +type subjectPublicKeyInfo struct { + Algorithm pkix.AlgorithmIdentifier + SubjectPublicKey asn1.BitString +} + +func main() { + var help, shouldMatch bool + flag.BoolVar(&help, "h", false, "print a help message and exit") + flag.BoolVar(&shouldMatch, "m", false, "all SKIs should match") + flag.Parse() + + if help { + usage(os.Stdout) + os.Exit(0) + } + + var ski string + for _, path := range flag.Args() { + public, kt, ft := parse(path) + + var subPKI subjectPublicKeyInfo + _, err := asn1.Unmarshal(public, &subPKI) + if err != nil { + lib.Warn(err, "failed to get subject PKI") + continue + } + + pubHash := sha1.Sum(subPKI.SubjectPublicKey.Bytes) + pubHashString := dumpHex(pubHash[:]) + if ski == "" { + ski = pubHashString + } + + if shouldMatch && ski != pubHashString { + lib.Warnx("%s: SKI mismatch (%s != %s)", + path, ski, pubHashString) + } + fmt.Printf("%s %s (%s %s)\n", path, pubHashString, kt, ft) + } +}