Skip to content

Commit

Permalink
feat(ca): generate and sign cert with intermediate ca
Browse files Browse the repository at this point in the history
* add support for generate intermediate ca

* remove unneeded file

* add unit test

* update readme
  • Loading branch information
nothinux authored May 1, 2022
1 parent ab63a10 commit 6bc304c
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 70 deletions.
62 changes: 27 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Certify can be used for creating a private CA (Certificate Authority) and issuin
Certify is easy to use and can be used as an alternative to OpenSSL.

## Feature
+ Create a certificate authorities
+ Create a certificate authorities and intermediate CA
+ Issue certificate with custom common name, ip san, dns san, expiry date, and extended key usage
+ Show certificate information from file or remote host
+ Export certificate to PKCS12 format
Expand All @@ -17,40 +17,32 @@ Download in the [release page](https://github.com/nothinux/certify/releases)

## Usage
```
certify [flag] [ip-or-dns-san] [cn:default certify] [expiry: s,m,h,d]
$ certify -init
⚡️ Initialize new CA Certificate and Key
You must create new CA by run -init before you can create certificate.
$ certify server.local 172.17.0.1
⚡️ Generate certificate with alt name server.local and 172.17.0.1
$ certify cn:web-server
⚡️ Generate certificate with common name web-server
$ certify server.local expiry:1d
⚡️ Generate certificate expiry within 1 day
$ certify server.local eku:serverAuth,clientAuth
⚡️ Generate certificate with extended key usage Server Auth and Client Auth
Also, you can see information from certificate
$ certify -read server.local.pem
⚡️ Read certificate information from file server.local.pem
$ certify -connect google.com:443
⚡️ Show certificate information from remote host
Export certificate and private key file to pkcs12 format
$ certify -export-p12 cert.pem cert-key.pem ca-cert.pem
⚡️ Generate client.p12 pem file containing certificate, private key and ca certificate
Verify private key matches a certificate
$ certify -match cert-key.pem cert.pem
⚡️ verify cert-key.pem and cert.pem has same public key
_ _ ___
___ ___ ___| |_|_| _|_ _
| _| -_| _| _| | _| | |
|___|___|_| |_| |_|_| |_ |
|___| Certify v1.5
Usage of certify:
certify [flag] [ip-or-dns-san] [cn:default certify] [eku:default serverAuth,clientAuth] [expiry:default 8766h s,m,h,d]
$ certify server.local 172.17.0.1 cn:web-server eku:serverAuth expiry:1d
Flags:
-init
Initialize new root CA Certificate and Key
-intermediate
Generate intermediate certificate
-read <filename>
Read certificate information from file server.local.pem
-connect <host:443>
Show certificate information from remote host
-export-p12 <cert> <private-key> <ca-cert>
Generate client.p12 pem file containing certificate, private key and ca certificate
-match <private-key> <cert>
Verify cert-key.pem and cert.pem has same public key
-version
print certify version
```

## Use Certify as library
Expand Down
57 changes: 54 additions & 3 deletions certify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,46 @@ package certify

import (
"crypto/x509/pkix"
"fmt"
"net"
"strings"
"testing"
"time"
)

var (
RSATestCert = `-----BEGIN CERTIFICATE-----
MIIFUTCCBDmgAwIBAgIRAKXhAWONgQR0CqU9N56GVkcwDQYJKoZIhvcNAQELBQAw
RjELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBM
TEMxEzARBgNVBAMTCkdUUyBDQSAxRDQwHhcNMjIwNDA5MTgxNzQ1WhcNMjIwNzA4
MTgxNzQ0WjARMQ8wDQYDVQQDEwZnby5kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQC+++2A2RSZe0t8HrdKME2l8fsRtdBm83NDrFjI+ljGxh+fFoxp
szy4nyseUpQFFthlns/9Z0LJSwRTdbxLDNQdiDxAyMsnt20Je1bsaUP4g1jDZ00e
UhsMOsIApiCs6DRFqHydBLZVeWMraGa4e2g8q/x7LD3G7sYoXfOb3/yYJeghPuPE
tEdYssVPzZmdB0zJYBQZTVCSH4ceiOrnfrV7tbXKYzhN/ZUhaKOA07y3Yu9WtgHK
+drf4rnLxXALUxXOn73KFxrT5V7CYsnCcgtoc2v7dAtXORwd/cyD1OkfiL+8y5L3
Ix/AxfahGrYoM5GwuUerrLJ9l0Jio40dyArNAgMBAAGjggJtMIICaTAOBgNVHQ8B
Af8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNV
HQ4EFgQUoHUzYU6hibyjmKXIgVEib4VVoF0wHwYDVR0jBBgwFoAUJeIYDrJXkZQq
5dRdhpCD3lOzuJIweAYIKwYBBQUHAQEEbDBqMDUGCCsGAQUFBzABhilodHRwOi8v
b2NzcC5wa2kuZ29vZy9zL2d0czFkNC9KYUk3amVIU3hkQTAxBggrBgEFBQcwAoYl
aHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzMWQ0LmRlcjARBgNVHREECjAI
ggZnby5kZXYwIQYDVR0gBBowGDAIBgZngQwBAgEwDAYKKwYBBAHWeQIFAzA8BgNV
HR8ENTAzMDGgL6AthitodHRwOi8vY3Jscy5wa2kuZ29vZy9ndHMxZDQvRFlDVzlo
TnpyWHcuY3JsMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDwAHYAUaOw9f0BeZxWbbg3
eI8MpHrMGyfL956IQpoN/tSLBeUAAAGAD8z3swAABAMARzBFAiEAon8amRM09Pdm
mTr8RhSUljNjDyh2HktHIHksuMqP9XkCIDJ0vmMjT8AAtODewy1CQfKY6MBLRzOc
MX3pcNREwk9JAHYARqVV63X6kSAwtaKJafTzfREsQXS+/Um4havy/HD+bUcAAAGA
D8z35gAABAMARzBFAiA1ylRik7z+2AOIdV+WNKjm4ui5/O3jmOAf2KCofz9SAgIh
AMGoUjsi2x/ODEvJ5qG/NLgtNwjVzMUZ6cCuUOsAECyHMA0GCSqGSIb3DQEBCwUA
A4IBAQA9kJTuv18L6fXMZwysP4tf5R7Wzu4tUhzVVQqnakLXt6lE4WuQGSRJGg+j
JvC+MLkTBXJidmSUwOwofQVVWLKSgnMaF2CnvO+zpoWQ9j/xjM+UeDJTsOWJDqJr
u7brL9iz0L3zopxmj2OT76rAjpnKVim/Dcw77pO0SA6Y6T68HaDxyx/xQG35U4ko
g0J3x484NSLqNjnU4aGP/C8XKe4gLQR6k0OWm0fktd7pCEakrklyswsgoDG7rB50
VvjDmr0mWlzsr1CfdnA1TysPFiULaRCFYaWhA71Sa/doNd5nrtuMzNetmmYFtpzq
pAkvSpiE1H6RLeKYTqyIAGcui/Ah
-----END CERTIFICATE-----`
)

func TestGetCertificate(t *testing.T) {
pkey, err := GetPrivateKey()
if err != nil {
Expand Down Expand Up @@ -99,6 +133,23 @@ func TestCertInfo(t *testing.T) {
t.Fatal(err)
}

s := CertInfo(cert)
fmt.Println(s)
t.Log(CertInfo(cert))
}

func TestCertInfoRSA(t *testing.T) {
cert, err := ParseCertificate([]byte(RSATestCert))
if err != nil {
t.Fatal(err)
}

t.Log(CertInfo(cert))
}

func TestCertInEmptyFile(t *testing.T) {
_, err := ParseCertificate([]byte(""))
if err != nil {
if !strings.Contains(err.Error(), "can't decode CA cert file") {
t.Fatal("error must be contain can't decode CA cert file")
}
}
}
16 changes: 16 additions & 0 deletions cmd/certify/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,19 @@ func createCertificate(args []string) error {

return nil
}

// createIntermediateCertificate generate intermediate certificate and signed with existing root CA
func createIntermediateCertificate(args []string) error {
pkey, err := generatePrivateKey(caInterKeyPath)
if err != nil {
return err
}

fmt.Println("Private key file generated", caInterKeyPath)

if err := generateIntermediateCert(pkey.PrivateKey, args); err != nil {
return err
}

return nil
}
21 changes: 20 additions & 1 deletion cmd/certify/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestInitCA(t *testing.T) {
}

t.Run("Test parse certificate", func(t *testing.T) {
cert, err := getCACert()
cert, err := getCACert(caPath)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -248,3 +248,22 @@ func TestCreateCertificate(t *testing.T) {
os.Remove("nothinux.local-key.pem")
})
}

func TestCreateIntermediateCertificate(t *testing.T) {
t.Run("Test create intermediate certificate", func(t *testing.T) {
if err := initCA([]string{"certify", "-init"}); err != nil {
t.Fatal(err)
}

if err := createIntermediateCertificate([]string{"certify", "-intermediate", "cn:nothinux"}); err != nil {
t.Fatal(err)
}
})

t.Cleanup(func() {
os.Remove(caPath)
os.Remove(caKeyPath)
os.Remove(caInterPath)
os.Remove(caInterKeyPath)
})
}
89 changes: 79 additions & 10 deletions cmd/certify/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func generateCA(pkey *ecdsa.PrivateKey, cn string, path string) error {
CommonName: parseCN(cn),
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(8766 * time.Hour),
NotAfter: time.Now().Add(87660 * time.Hour),
IsCA: true,
}

Expand All @@ -46,17 +46,38 @@ func generateCA(pkey *ecdsa.PrivateKey, cn string, path string) error {
return store(caCert.String(), path)
}

func generateCert(pkey *ecdsa.PrivateKey, args []string) error {
func generateCert(pkey *ecdsa.PrivateKey, args []string) (err error) {
iplist, dnsnames, cn, expiry, ekus := parseArgs(args)

parentKey, err := getCAPrivateKey()
var parent *x509.Certificate
var parentKey *ecdsa.PrivateKey

// By default, If Intermediate CA exists the generated certificate
// will be signed with intermediate CA. If not, it will be signed
// with rootCA

parentKey, err = getCAPrivateKey(caInterKeyPath)
if err != nil {
return err
if errors.Is(err, os.ErrNotExist) {
parentKey, err = getCAPrivateKey(caKeyPath)
if err != nil {
return err
}
} else {
return err
}
}

parent, err := getCACert()
parent, err = getCACert(caInterPath)
if err != nil {
return err
if errors.Is(err, os.ErrNotExist) {
parent, err = getCACert(caPath)
if err != nil {
return err
}
} else {
return err
}
}

template := certify.Certificate{
Expand Down Expand Up @@ -89,6 +110,54 @@ func generateCert(pkey *ecdsa.PrivateKey, args []string) error {
return err
}

func generateIntermediateCert(pkey *ecdsa.PrivateKey, args []string) error {
_, _, cn, expiry, _ := parseArgs(args)

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

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

if cn == "" {
cn = "certify"
}

newCN := fmt.Sprintf("%s Intermediate", cn)

if expiry.Unix() > parent.NotAfter.Unix() {
return fmt.Errorf("intermediate certificate expiry date can't longer than root CA")
}

template := certify.Certificate{
Subject: pkix.Name{
Organization: []string{"certify"},
CommonName: newCN,
},
NotBefore: time.Now(),
NotAfter: expiry,
IsCA: true,
Parent: parent,
ParentPrivateKey: parentKey,
}

cert, err := template.GetCertificate(pkey)
if err != nil {
return err
}

err = store(cert.String(), caInterPath)
if err == nil {
fmt.Println("Certificate file generated", caInterPath)
}

return err
}

// getFilename returns path based on given args
// first it will check dnsnames, if nil, then check iplist, if iplist nil too
// it will check common name
Expand All @@ -115,8 +184,8 @@ func getFilename(args []string, key bool) string {
return path
}

func getCAPrivateKey() (*ecdsa.PrivateKey, error) {
pkey, err := readPrivateKeyFile("ca-key.pem")
func getCAPrivateKey(path string) (*ecdsa.PrivateKey, error) {
pkey, err := readPrivateKeyFile(path)
if err != nil {
return nil, err
}
Expand All @@ -138,8 +207,8 @@ func readPrivateKeyFile(path string) (*ecdsa.PrivateKey, error) {
return pkey, nil
}

func getCACert() (*x509.Certificate, error) {
c, err := readCertificateFile("ca-cert.pem")
func getCACert(path string) (*x509.Certificate, error) {
c, err := readCertificateFile(path)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit 6bc304c

Please sign in to comment.