diff --git a/certinfo.go b/certinfo.go index 2aea8ca..449ba1d 100644 --- a/certinfo.go +++ b/certinfo.go @@ -1,6 +1,7 @@ package certinfo import ( + "bufio" "bytes" "crypto/dsa" "crypto/ecdsa" @@ -12,6 +13,7 @@ import ( "fmt" "math/big" "net" + "strings" "time" ) @@ -252,7 +254,14 @@ func printSignature(sigAlgo x509.SignatureAlgorithm, sig []byte, buf *bytes.Buff // CertificateText returns a human-readable string representation // of the certificate cert. The format is similar (but not identical) // to the OpenSSL way of printing certificates. -func CertificateText(cert *x509.Certificate) (string, error) { +func CertificateText(cert *x509.Certificate, opts ...Option) (string, error) { + o := &options{ + formatters: make(map[string]Formatter), + } + for _, fn := range opts { + fn(o) + } + var buf bytes.Buffer buf.Grow(4096) // 4KiB should be enough @@ -519,6 +528,14 @@ func CertificateText(cert *x509.Certificate) (string, error) { } else { buf.WriteString(fmt.Sprintf("%12sNetscape Comment:\n%16s%s\n", "", "", comment)) } + } else if format, ok := o.formatters[ext.Id.String()]; ok { + // If configured, use custom formatter. + scanner := bufio.NewScanner(strings.NewReader(format(ext))) + for scanner.Scan() { + // Prepend padding so that formatted string appears in-line + // with other content. + buf.WriteString(fmt.Sprintf("%12s%s\n", "", scanner.Text())) + } } else { buf.WriteString(fmt.Sprintf("%12sUnknown extension %s\n", "", ext.Id.String())) } diff --git a/certinfo_test.go b/certinfo_test.go index 9e802bb..70fe068 100644 --- a/certinfo_test.go +++ b/certinfo_test.go @@ -1,11 +1,12 @@ package certinfo import ( - "bytes" "crypto/x509" "encoding/pem" "io/ioutil" "testing" + + "github.com/google/go-cmp/cmp" ) type InputType int @@ -54,9 +55,9 @@ func testPair(t *testing.T, certFile, refFile string, inputType InputType) { if err != nil { t.Fatal(err) } - if !bytes.Equal(resultData, refData) { + if diff := cmp.Diff(resultData, refData); diff != "" { t.Logf("'%s' did not match reference '%s'\n", certFile, refFile) - t.Errorf("Dump follows:\n%s\n", result) + t.Errorf("Dump follows:\n%s\n", diff) } } diff --git a/formatter_test.go b/formatter_test.go new file mode 100644 index 0000000..489ef82 --- /dev/null +++ b/formatter_test.go @@ -0,0 +1,53 @@ +package certinfo + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestFormatter(t *testing.T) { + pemData, err := ioutil.ReadFile("test_certs/root1.cert.pem") + if err != nil { + t.Fatal(err) + } + block, rest := pem.Decode([]byte(pemData)) + if block == nil || len(rest) > 0 { + t.Fatal("Certificate decoding error") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatal(err) + } + + oid := asn1.ObjectIdentifier{1, 2, 3, 4} + + cert.Extensions = append(cert.Extensions, pkix.Extension{ + Id: oid, + Value: []byte("foo"), + }) + + got, err := CertificateText(cert, WithFormatter(oid, func(ext pkix.Extension) string { + return fmt.Sprintf("Custom:\n%4s%s", "", string(ext.Value)) + })) + if err != nil { + t.Fatal(err) + } + + want, err := os.ReadFile("test_certs/root1.cert.customfield.text") + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(string(want), got); diff != "" { + t.Log(got) + t.Error(diff) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d1582f8 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/grantae/certinfo + +go 1.18 + +require github.com/google/go-cmp v0.5.8 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e9b099c --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/option.go b/option.go new file mode 100644 index 0000000..4168160 --- /dev/null +++ b/option.go @@ -0,0 +1,26 @@ +package certinfo + +import ( + "crypto/x509/pkix" + "encoding/asn1" +) + +// Option provides configurable options to output formatting. +type Option func(*options) + +type options struct { + // formatters maps oid -> format funcs + formatters map[string]Formatter +} + +// WithFormatter configures a custom formatting function for the given OID. +func WithFormatter(oid asn1.ObjectIdentifier, fn Formatter) Option { + return func(opts *options) { + opts.formatters[oid.String()] = fn + } +} + +// Formatter returns a formatted string for a given pkix.Extension. +// Formatters should return relative strings - padding will be prepended +// automatically when the certificate is printed. +type Formatter func(ext pkix.Extension) string diff --git a/test_certs/leaf2.csr.text b/test_certs/leaf2.csr.text index 60aa811..cb19ad8 100644 --- a/test_certs/leaf2.csr.text +++ b/test_certs/leaf2.csr.text @@ -25,7 +25,7 @@ Certificate Request: 71:bc:10:a6:45:dd:a0:55:e1:77:02:28:84:58:09: da:f9:ad:16:6e:22:3f:13:f7:91:71:44:5b:5f:98: 8c:92:21:13 - Signature Algorithm: 0 + Signature Algorithm: DSA-SHA256 30:2c:02:14:6e:2f:4d:43:42:fe:ef:dd:d6:5d:82:ce:40:35: 0f:df:f6:03:d0:56:02:14:2c:af:8d:a5:7e:00:cb:18:c9:eb: 03:ee:9b:92:32:c0:15:73:6a:29 diff --git a/test_certs/root1.cert.customfield.text b/test_certs/root1.cert.customfield.text new file mode 100644 index 0000000..03798bb --- /dev/null +++ b/test_certs/root1.cert.customfield.text @@ -0,0 +1,37 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: SHA256-RSA + Issuer: C=US,ST=California,O=World Widget Authority,OU=Identity Affairs,CN=worldwidgetauthority.com,emailAddress=nobody@worldwidgetauthority.com + Validity + Not Before: Jul 23 18:56:47 2020 UTC + Not After : Jun 30 07:37:21 2040 UTC + Subject: C=US,ST=California,O=World Widget Authority,OU=Identity Affairs,CN=worldwidgetauthority.com,emailAddress=nobody@worldwidgetauthority.com + Subject Public Key Info: + Public Key Algorithm: RSA + Public-Key: (512 bit) + Modulus: + b5:d6:60:b9:f9:31:09:fe:97:34:c4:f7:6b:7b:06: + 01:f4:8b:fe:1a:e0:65:8f:fd:30:c0:82:30:3c:61: + f7:c2:1d:98:7c:3a:ed:9f:b4:5e:8f:15:ce:90:8b: + 45:de:db:23:0e:aa:4d:95:e9:af:3b:79:26:a5:ce: + 71:8a:3a:bd + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:TRUE + Netscape Comment: + This is a test certificate only + X509v3 Subject Key Identifier: + 7C:0F:26:9D:ED:C8:7A:C0:05:1E:99:A3:5D:A5:9E:8D:A6:A6:96:5E + X509v3 Authority Key Identifier: + keyid:7C:0F:26:9D:ED:C8:7A:C0:05:1E:99:A3:5D:A5:9E:8D:A6:A6:96:5E + Custom: + foo + + Signature Algorithm: SHA256-RSA + 60:bd:b4:c4:9a:09:0d:7a:d7:b4:6b:e2:85:3b:78:0b:97:de: + 57:47:34:19:37:2a:82:1a:79:c3:3f:0b:71:46:fe:9b:db:ce: + c7:41:42:2b:17:22:b4:d5:f1:fc:18:c3:31:af:c9:c4:4d:2d: + 92:16:f7:a6:6d:4f:5d:e0:8c:83