Skip to content

Commit

Permalink
feat: add ability to revoke certificates (#15)
Browse files Browse the repository at this point in the history
Introduce a new flag -revoke to revoke certificate
  • Loading branch information
nothinux authored Sep 3, 2023
1 parent 81f090f commit 38e4eb1
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 22 deletions.
42 changes: 42 additions & 0 deletions cmd/certify/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,48 @@ func createCertificate(args []string) error {
return nil
}

func revokeCertificate(args []string) (string, error) {
if len(args) < 4 {
return "", fmt.Errorf("you need to provide cert file and crl file")
}

certByte, err := os.ReadFile(args[2])
if err != nil {
return "", err
}

crlBytes, err := os.ReadFile(args[3])
if err != nil {
return "", err
}

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

caCert, err := getCACert(caPath)
if err != nil {
return "", err
}

pkey, err := getCAPrivateKey(caKeyPath)
if err != nil {
return "", err
}

fmt.Printf("revoking certificate cn=%s o=%s with serial number %s\n", cert.Subject.CommonName, cert.Subject.Organization, cert.SerialNumber)
crl, crlNum, err := certify.RevokeCertificate(crlBytes, cert, caCert, pkey)
if err != nil {
return "", err
}

path := fmt.Sprintf("ca-crl-%s.pem", crlNum)

fmt.Printf("CA CRL file generated %s\n", path)
return path, store(crl.String(), path)
}

// createIntermediateCertificate generate intermediate certificate and signed with existing root CA
func createIntermediateCertificate(args []string) error {
pkey, err := generatePrivateKey(caInterKeyPath)
Expand Down
65 changes: 65 additions & 0 deletions cmd/certify/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,71 @@ func TestReadCRL(t *testing.T) {
}
}

func TestRevokeCertificate(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 revoke certificate",
Args: []string{"certify", "-revoke", "nothinux.local.pem", "ca-crl.pem"},
Stdin: nil,
},
{
Name: "Test revoke certificate with not enough argument",
Args: []string{"certify", "-revoke", "ca-crl.pem"},
Stdin: nil,
expectedError: "you need to provide cert file and crl file",
},
{
Name: "Test revoke certificate with wrong crl file",
Args: []string{"certify", "-revoke", "nothinux.local.pem", "ca-cert.pem"},
Stdin: nil,
expectedError: "x509: unsupported crl version",
},
{
Name: "Test revoke certificate with wrong cert file",
Args: []string{"certify", "-revoke", "ca-crl.pem", "ca-crl.pem"},
Stdin: nil,
expectedError: "x509: malformed validity",
},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
if err := initCA([]string{"certify", "-init"}); err != nil {
t.Fatal(err)
}

if err := createCertificate([]string{"certify", "nothinux.local"}); err != nil {
t.Fatal(err)
}

crlPath, err := revokeCertificate(tt.Args)
if err != nil {
if !strings.Contains(err.Error(), tt.expectedError) {
t.Fatalf("got %v, want %v", err, tt.expectedError)
}
cleanupfiles([]string{caPath, caKeyPath, caCRLPath, "nothinux.local.pem", "nothinux.local-key.pem"})
return
}

_, err = readCRL([]string{"certify", "-read-crl", crlPath}, nil)
if err != nil {
t.Fatal(err)
}

t.Cleanup(func() {
cleanupfiles([]string{caPath, caKeyPath, caCRLPath, crlPath, "nothinux.local.pem", "nothinux.local-key.pem"})
})
})
}
}

func TestReadRemoteCertificate(t *testing.T) {
tests := []struct {
Name string
Expand Down
12 changes: 1 addition & 11 deletions cmd/certify/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func generateCA(pkey *ecdsa.PrivateKey, args []string, path string) (*certify.Re
}

func generateCRL(pkey *ecdsa.PrivateKey, caCert *x509.Certificate) error {
crl, err := certify.CreateCRL(pkey, caCert)
crl, _, err := certify.CreateCRL(pkey, caCert, nil)
if err != nil {
return err
}
Expand Down Expand Up @@ -447,16 +447,6 @@ 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
8 changes: 8 additions & 0 deletions cmd/certify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Flags:
Verify cert-key.pem and cert.pem has same public key
-interactive
Run certify interactively
-revoke <ca-cert> <ca-private-key>
Revoke certificate, the certificate will be added to CRL
-version
print certify version
`
Expand Down Expand Up @@ -68,6 +70,7 @@ func runMain() error {
connect = flag.Bool("connect", false, "show information about certificate on remote host")
epkcs12 = flag.Bool("export-p12", false, "export certificate and key to pkcs12 format")
interactive = flag.Bool("interactive", false, "run certify interactively")
revoke = flag.Bool("revoke", false, "Revoke certificate, the certificate will be added to CRL")
)

flag.Usage = func() {
Expand Down Expand Up @@ -126,6 +129,11 @@ func runMain() error {
return runWizard()
}

if *revoke {
_, err := revokeCertificate(os.Args)
return err
}

if *epkcs12 {
if len(os.Args) < 5 {
fmt.Println("you must provide [key-path] [cert-path] and [ca-path]")
Expand Down
38 changes: 29 additions & 9 deletions crl.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,26 @@ type CertRevocationList struct {
}

// CreateCRL Create certificate revocation list
func CreateCRL(pkey *ecdsa.PrivateKey, caCert *x509.Certificate) (*CertRevocationList, error) {
func CreateCRL(pkey *ecdsa.PrivateKey, caCert *x509.Certificate, crl *x509.RevocationList) (*CertRevocationList, *big.Int, error) {
crlNumber := time.Now().UTC().Format("20060102150405")
num, _ := big.NewInt(0).SetString(crlNumber, 10)

crl, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
RevokedCertificates: []pkix.RevokedCertificate{},
Number: num,
ThisUpdate: time.Now(),
NextUpdate: time.Now().Add(time.Hour * 48),
}, caCert, pkey)
if crl == nil {
crl = &x509.RevocationList{
RevokedCertificates: []pkix.RevokedCertificate{},
}
}

crl.Number = num
crl.ThisUpdate = time.Now()
crl.NextUpdate = time.Now().Add(96 * time.Hour)

crlByte, err := x509.CreateRevocationList(rand.Reader, crl, caCert, pkey)
if err != nil {
return nil, err
return nil, nil, err
}

return &CertRevocationList{Byte: crl}, nil
return &CertRevocationList{Byte: crlByte}, num, nil
}

// String return string of certificate revocation list in pem encoded format
Expand All @@ -58,6 +63,21 @@ func ParseCRL(crl []byte) (*x509.RevocationList, error) {
return x509.ParseRevocationList(c.Bytes)
}

func RevokeCertificate(crl []byte, cert *x509.Certificate, caCert *x509.Certificate, pkey *ecdsa.PrivateKey) (*CertRevocationList, *big.Int, error) {
crlF, err := ParseCRL(crl)
if err != nil {
return nil, nil, err
}

crlF.RevokedCertificateEntries = append(crlF.RevokedCertificateEntries, x509.RevocationListEntry{
SerialNumber: cert.SerialNumber,
RevocationTime: time.Now(),
})

return CreateCRL(pkey, caCert, crlF)

}

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

Expand Down
4 changes: 2 additions & 2 deletions crl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestCreateCRL(t *testing.T) {
t.Fatal(err)
}

_, err = CreateCRL(pkey.PrivateKey, caCert.Cert)
_, _, err = CreateCRL(pkey.PrivateKey, caCert.Cert, nil)
if err == nil {
t.Fatalf("this should be error, because the cert doesn't have keyUsage")
}
Expand All @@ -88,7 +88,7 @@ func TestCreateCRL(t *testing.T) {
t.Fatal(err)
}

_, err = CreateCRL(pkey.PrivateKey, caCert.Cert)
_, _, err = CreateCRL(pkey.PrivateKey, caCert.Cert, nil)
if err != nil {
t.Fatal(err)
}
Expand Down

0 comments on commit 38e4eb1

Please sign in to comment.