Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion certinfo.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package certinfo

import (
"bufio"
"bytes"
"crypto/dsa"
"crypto/ecdsa"
Expand All @@ -12,6 +13,7 @@ import (
"fmt"
"math/big"
"net"
"strings"
"time"
)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()))
}
Expand Down
7 changes: 4 additions & 3 deletions certinfo_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package certinfo

import (
"bytes"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"testing"

"github.com/google/go-cmp/cmp"
)

type InputType int
Expand Down Expand Up @@ -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)
}
}

Expand Down
53 changes: 53 additions & 0 deletions formatter_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/grantae/certinfo

go 1.18

require github.com/google/go-cmp v0.5.8 // indirect
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
26 changes: 26 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion test_certs/leaf2.csr.text
Original file line number Diff line number Diff line change
Expand Up @@ -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
37 changes: 37 additions & 0 deletions test_certs/root1.cert.customfield.text
Original file line number Diff line number Diff line change
@@ -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