Skip to content

Commit

Permalink
feat: add support for reading crl files (#14)
Browse files Browse the repository at this point in the history
Introduce a new flag -read-crl to read crl files

Usage:
certify -read-crl ca-crl.pem
  • Loading branch information
nothinux authored Sep 2, 2023
1 parent bf64611 commit 81f090f
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
go-version: 1.21

- name: Check out code
uses: actions/checkout@v2
Expand Down
11 changes: 11 additions & 0 deletions certify.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,20 @@ func CertInfo(cert *x509.Certificate) string {
buf.WriteString(fmt.Sprintf("%12sX509v3 Extended Key Usage:\n", ""))
buf.WriteString(fmt.Sprintf("%16s%v\n", "", parseExtKeyUsage(cert.ExtKeyUsage)))
}

if cert.KeyUsage != 0 {
buf.WriteString(fmt.Sprintf("%12sX509v3 Key Usage:\n", ""))
buf.WriteString(fmt.Sprintf("%16s%v\n", "", parseKeyUsage(cert.KeyUsage)))
}

buf.WriteString(fmt.Sprintf("%12sX509v3 Basic Constraints:\n", ""))
buf.WriteString(fmt.Sprintf("%16sCA: %v\n", "", cert.IsCA))

if cert.SubjectKeyId != nil {
buf.WriteString(fmt.Sprintf("%12sX509v3 Subject Key Identifier:\n", ""))
buf.WriteString(fmt.Sprintf("%16s%v\n", "", formatKeyIDWithColon(cert.SubjectKeyId)))
}

if len(cert.IPAddresses) != 0 || len(cert.DNSNames) != 0 {
buf.WriteString(fmt.Sprintf("%12sX509v3 Subject Alternative Name:\n", ""))
if len(cert.IPAddresses) != 0 {
Expand Down
25 changes: 25 additions & 0 deletions cmd/certify/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,31 @@ func readCertificate(args []string, stdin *os.File) (string, error) {
return certify.CertInfo(cert), nil
}

// readCRL read crl from stdin or from file
func readCRL(args []string, stdin *os.File) (string, error) {
var certByte []byte
var err error

if len(args) < 3 {
certByte, err = io.ReadAll(stdin)
if err != nil {
return "", err
}
} else {
certByte, err = os.ReadFile(args[2])
if err != nil {
return "", err
}
}

crl, err := certify.ParseCRL(certByte)
if err != nil {
return "", err
}

return certify.CRLInfo(crl), nil
}

// readRemoteCertificate read certificate from remote host
func readRemoteCertificate(args []string) (string, error) {
if len(args) < 3 {
Expand Down
50 changes: 50 additions & 0 deletions cmd/certify/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,56 @@ func TestReadCertificate(t *testing.T) {
}
}

func TestReadCRL(t *testing.T) {
// TODO: add test reading certificate from stdin
tests := []struct {
Name string
Args []string
Stdin *os.File
expectedOutput string
expectedError string
}{
{
Name: "Test read crl from file",
Args: []string{"certify", "-read-crl", "testdata/ca-crl.pem"},
Stdin: nil,
expectedOutput: "Issuer: CN=certify, O=certify",
},
{
Name: "Test read not exists crl",
Args: []string{"certify", "-read", "ca-crl.pem"},
Stdin: nil,
expectedError: "open ca-crl.pem: no such file or directory",
},
{
Name: "Test read content from stdin",
Args: []string{"certify", "-read"},
Stdin: getTestCertificate("testdata/empty"),
expectedError: "no pem data",
},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
crl, err := readCRL(tt.Args, tt.Stdin)

if err != nil {
if !strings.Contains(err.Error(), tt.expectedError) {
t.Fatalf("got %v, want %v", err, tt.expectedError)
}
}

if !strings.Contains(crl, tt.expectedOutput) {
t.Fatalf("error, want output %s", tt.expectedOutput)
}

if tt.Stdin != nil {
tt.Stdin.Close()
}
})
}
}

func TestReadRemoteCertificate(t *testing.T) {
tests := []struct {
Name string
Expand Down
10 changes: 10 additions & 0 deletions cmd/certify/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,16 @@ func isExist(path string) bool {
return !errors.Is(err, os.ErrNotExist)
}

func isCRLFile(args []string) bool {
for _, arg := range args {
if strings.Contains(arg, "crl") {
return true
}
}

return false
}

func parseTLSVersion(args []string) uint16 {
for _, arg := range args[1:] {
if strings.Contains(arg, "tlsver:") {
Expand Down
14 changes: 13 additions & 1 deletion cmd/certify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ Flags:
-intermediate
Generate intermediate certificate
-read <filename>
Read certificate information from file server.local.pem
Read certificate information from file or stdin
-read-crl <filename>
Read certificate revocation list from file or stdin
-connect <host:443> <tlsver:1.2> <insecure> <with-ca:ca-path>
Show certificate information from remote host, use tlsver to set spesific tls version
-export-p12 <cert> <private-key> <ca-cert>
Expand Down Expand Up @@ -60,6 +62,7 @@ func runMain() error {
initialize = flag.Bool("init", false, "initialize new root CA Certificate and Key")
intermediate = flag.Bool("intermediate", false, "create intermediate certificate")
read = flag.Bool("read", false, "read information from certificate")
readcrl = flag.Bool("read-crl", false, "read information from certificate revocation list")
match = flag.Bool("match", false, "check if private key match with certificate")
ver = flag.Bool("version", false, "see program version")
connect = flag.Bool("connect", false, "show information about certificate on remote host")
Expand Down Expand Up @@ -94,6 +97,15 @@ func runMain() error {
return nil
}

if *readcrl {
crl, err := readCRL(os.Args, os.Stdin)
if err != nil {
return err
}
fmt.Printf("%s", crl)
return nil
}

if *connect {
result, err := readRemoteCertificate(os.Args)
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions cmd/certify/testdata/ca-crl.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN X509 CRL-----
MIHiMIGJAgEBMAoGCCqGSM49BAMCMCQxEDAOBgNVBAoTB2NlcnRpZnkxEDAOBgNV
BAMTB2NlcnRpZnkXDTIzMDkwMjE1NDAyNFoXDTIzMDkwNDE1NDAyNFqgNDAyMB8G
A1UdIwQYMBaAFB/nlGRBJw24im6iHRMrXXmExBnxMA8GA1UdFAQIAgYSZl+8gygw
CgYIKoZIzj0EAwIDSAAwRQIgah2RIGIppWkG2GJoYk+V+imapbQbmuq6ZtMqIcYw
s8wCIQD7qx8oS5eE8Zhwe7Sc3rUvZn1o0NNYrc6kkvwoXAzHwQ==
-----END X509 CRL-----
42 changes: 42 additions & 0 deletions crl.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"strings"
"time"
)

Expand Down Expand Up @@ -46,3 +48,43 @@ func (c *CertRevocationList) String() string {

return w.String()
}

func ParseCRL(crl []byte) (*x509.RevocationList, error) {
c, _ := pem.Decode(crl)
if c == nil {
return nil, fmt.Errorf("no pem data")
}

return x509.ParseRevocationList(c.Bytes)
}

func CRLInfo(rl *x509.RevocationList) string {
var buf bytes.Buffer

buf.WriteString("Certificate Revocation List (CRL):\n")
buf.WriteString(fmt.Sprintf("%4sVersion \n", ""))
buf.WriteString(fmt.Sprintf("%4sSignature Algorithm: %v\n", "", rl.SignatureAlgorithm))

buf.WriteString(fmt.Sprintf("%4sIssuer: %v\n", "", strings.Replace(rl.Issuer.String(), ",", ", ", -1)))
buf.WriteString(fmt.Sprintf("%8sLastUpdate: %v\n", "", rl.ThisUpdate.Format("Jan 2 15:04:05 2006 GMT")))
buf.WriteString(fmt.Sprintf("%8sNextUpdate: %v\n", "", rl.NextUpdate.Format("Jan 2 15:04:05 2006 GMT")))

buf.WriteString(fmt.Sprintf("%8sCRL Extensions:\n", ""))
buf.WriteString(fmt.Sprintf("%12sX509v3 Authority Key Identifier:\n", ""))
buf.WriteString(fmt.Sprintf("%16s%s\n", "", formatKeyIDWithColon(rl.AuthorityKeyId)))
buf.WriteString(fmt.Sprintf("%12sX509v3 CRL Number:\n", ""))
buf.WriteString(fmt.Sprintf("%16s%s\n", "", rl.Number))

if len(rl.RevokedCertificateEntries) == 0 {
buf.WriteString("No Revoked Certificates\n")
return buf.String()
}

buf.WriteString("Revoked Certificates:\n")
for _, rc := range rl.RevokedCertificateEntries {
buf.WriteString(fmt.Sprintf("%4sSerial Number: %s\n", "", formatKeyIDWithColon(rc.SerialNumber.Bytes())))
buf.WriteString(fmt.Sprintf("%8sRevocation Date: %s\n", "", rc.RevocationTime.Format("Jan 2 15:04:05 2006 GMT")))
}

return buf.String()
}
61 changes: 61 additions & 0 deletions crl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@ import (
"time"
)

var (
CRLDATA = `-----BEGIN X509 CRL-----
MIHiMIGJAgEBMAoGCCqGSM49BAMCMCQxEDAOBgNVBAoTB2NlcnRpZnkxEDAOBgNV
BAMTB2NlcnRpZnkXDTIzMDkwMjE1NDAyNFoXDTIzMDkwNDE1NDAyNFqgNDAyMB8G
A1UdIwQYMBaAFB/nlGRBJw24im6iHRMrXXmExBnxMA8GA1UdFAQIAgYSZl+8gygw
CgYIKoZIzj0EAwIDSAAwRQIgah2RIGIppWkG2GJoYk+V+imapbQbmuq6ZtMqIcYw
s8wCIQD7qx8oS5eE8Zhwe7Sc3rUvZn1o0NNYrc6kkvwoXAzHwQ==
-----END X509 CRL-----
`
CRLDATAWITHREVOCATIONCERT = `-----BEGIN X509 CRL-----
MIICyzCBtAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhub3RoaW51eBcN
MjMwOTAyMTYyODQ2WhcNMjUwOTAxMTYyODQ2WjBIMCICEQCV3atQTTX3B6j9MM5p
JKwAFw0yMzA5MDIxMTE5MDZaMCICEQCTD91VR4f01/4pCuo+3AfIFw0yMzA5MDIx
NjI4NDZaoCMwITAfBgNVHSMEGDAWgBRwJ198tLFEwrc/2hvRwVgiBFGM6zANBgkq
hkiG9w0BAQsFAAOCAgEAeGiqXjlTE3Kgm6dwgZpQk5AhvX1raM+IC/4bgAeAQBJ5
6iGboGbW4mOaLFPPdawFJbmtRtyW2/RxQX2QW+/DwR/pPQ5IcYyNJ8gsyn/vpBZe
0zTOCg0ILBtWxa+YL4EYPk8EqiOMXVZG73qaJBUR4snRCwph3f1CI5PZvsgZaPmd
Q9X+tHYqHKruS/fu3uzAKgRUz27DCKgJ2kmPxF9AZ61J3PywykE8/A3ccLBHOzj+
IETKvuyWaATse2Q9qa1eDmjMDbZSpA4gQmSoCqOFc9M1exrb4zxT1YEwkosyzMvM
/6BbDdWd178Vjxlzy1MakOU+4IRV6X+n74zXRbaERypLJMWIy1ndHMsfDDYn0Hrc
1XXoEIkuc4wvFihFkN9PjEJBEo1Mraew9xe3x7NY7AD8fYW2JOgd+Z2vxlbqXQ9Z
nc4yytlA/P6hFJSbrigVAcUwQYPjS84DwLDFvJSmv0PpLiw0Enqdta4WcSOp/rr+
s3hycfohM1EtWm2GpmukKdJ6GkP3YitXnZo/FQOt6+0chmec3QYCrSiY1QHBEvX3
ty4YcAH77NN3m1GMbC62GfjWd70V/SK43kzBtIwGE+kkoWIPoZQj0tf/1ZrNFGKS
gkSyWaYiSbs9Xyl4ilVsENNxKsN5RUwv91ZisniYV5COfrJAsye3Jbzeb/AEGNM=
-----END X509 CRL-----`
)

func TestCreateCRL(t *testing.T) {
pkey, err := GetPrivateKey()
if err != nil {
Expand Down Expand Up @@ -66,3 +94,36 @@ func TestCreateCRL(t *testing.T) {
}
})
}

func TestParseCRL(t *testing.T) {
rl, err := ParseCRL([]byte(CRLDATA))
if err != nil {
t.Fatal(err)
}

t.Run("Test CRL number output", func(t *testing.T) {
if rl.Number.String() != "20230902154024" {
t.Fatal("The CRL number is different from what we expect")
}
})
}

func TestCRLInfo(t *testing.T) {
t.Run("Test CRL info with empty revocation list", func(t *testing.T) {
rl, err := ParseCRL([]byte(CRLDATA))
if err != nil {
t.Fatal(err)
}

CRLInfo(rl)
})

t.Run("Test CRL info with 2 revocation list", func(t *testing.T) {
rl, err := ParseCRL([]byte(CRLDATAWITHREVOCATIONCERT))
if err != nil {
t.Fatal(err)
}

CRLInfo(rl)
})
}
13 changes: 13 additions & 0 deletions helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ func GetPublicKey(pub interface{}) (string, error) {
return w.String(), err
}

func parseKeyUsage(ku x509.KeyUsage) string {
switch ku {
case x509.KeyUsageCRLSign:
return "CRL Sign"
case x509.KeyUsageCertSign:
return "Cert Sign"
case x509.KeyUsageDigitalSignature:
return "Digital Signature"
default:
return ""
}
}

func parseExtKeyUsage(ekus []x509.ExtKeyUsage) string {
var extku []string

Expand Down
38 changes: 38 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,44 @@ ox8WdlL3mozzen8QcdQ7jKiLgtJmFme8+E9gb5K3goFmPaaplizqd/yxNA==
}
}

func TestParseKeyUsage(t *testing.T) {
tests := []struct {
Name string
KeyUsage x509.KeyUsage
Expected string
}{
{
Name: "Test Cert Sign Key Usage",
KeyUsage: x509.KeyUsageCertSign,
Expected: "Cert Sign",
},
{
Name: "Test CRL Sign Key Usage",
KeyUsage: x509.KeyUsageCRLSign,
Expected: "CRL Sign",
},
{
Name: "Test Digital Signature Key Usage",
KeyUsage: x509.KeyUsageDigitalSignature,
Expected: "Digital Signature",
},
{
Name: "Test other Key Usage",
KeyUsage: x509.KeyUsageEncipherOnly,
Expected: "",
},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
got := parseKeyUsage(tt.KeyUsage)
if got != tt.Expected {
t.Fatalf("got %v, want %v", got, tt.Expected)
}
})
}
}

func TestParseExtKeyUsage(t *testing.T) {
t.Run("Test single eku", func(t *testing.T) {
result := parseExtKeyUsage([]x509.ExtKeyUsage{
Expand Down

0 comments on commit 81f090f

Please sign in to comment.