Skip to content

Commit 6bc304c

Browse files
authored
feat(ca): generate and sign cert with intermediate ca
* add support for generate intermediate ca * remove unneeded file * add unit test * update readme
1 parent ab63a10 commit 6bc304c

9 files changed

+291
-70
lines changed

README.md

+27-35
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Certify can be used for creating a private CA (Certificate Authority) and issuin
55
Certify is easy to use and can be used as an alternative to OpenSSL.
66

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

1818
## Usage
1919
```
20-
certify [flag] [ip-or-dns-san] [cn:default certify] [expiry: s,m,h,d]
21-
22-
$ certify -init
23-
⚡️ Initialize new CA Certificate and Key
24-
25-
You must create new CA by run -init before you can create certificate.
26-
27-
$ certify server.local 172.17.0.1
28-
⚡️ Generate certificate with alt name server.local and 172.17.0.1
29-
30-
$ certify cn:web-server
31-
⚡️ Generate certificate with common name web-server
32-
33-
$ certify server.local expiry:1d
34-
⚡️ Generate certificate expiry within 1 day
35-
36-
$ certify server.local eku:serverAuth,clientAuth
37-
⚡️ Generate certificate with extended key usage Server Auth and Client Auth
38-
39-
Also, you can see information from certificate
40-
41-
$ certify -read server.local.pem
42-
⚡️ Read certificate information from file server.local.pem
43-
44-
$ certify -connect google.com:443
45-
⚡️ Show certificate information from remote host
46-
47-
Export certificate and private key file to pkcs12 format
48-
$ certify -export-p12 cert.pem cert-key.pem ca-cert.pem
49-
⚡️ Generate client.p12 pem file containing certificate, private key and ca certificate
50-
51-
Verify private key matches a certificate
52-
$ certify -match cert-key.pem cert.pem
53-
⚡️ verify cert-key.pem and cert.pem has same public key
20+
_ _ ___
21+
___ ___ ___| |_|_| _|_ _
22+
| _| -_| _| _| | _| | |
23+
|___|___|_| |_| |_|_| |_ |
24+
|___| Certify v1.5
25+
26+
Usage of certify:
27+
certify [flag] [ip-or-dns-san] [cn:default certify] [eku:default serverAuth,clientAuth] [expiry:default 8766h s,m,h,d]
28+
29+
$ certify server.local 172.17.0.1 cn:web-server eku:serverAuth expiry:1d
30+
31+
Flags:
32+
-init
33+
Initialize new root CA Certificate and Key
34+
-intermediate
35+
Generate intermediate certificate
36+
-read <filename>
37+
Read certificate information from file server.local.pem
38+
-connect <host:443>
39+
Show certificate information from remote host
40+
-export-p12 <cert> <private-key> <ca-cert>
41+
Generate client.p12 pem file containing certificate, private key and ca certificate
42+
-match <private-key> <cert>
43+
Verify cert-key.pem and cert.pem has same public key
44+
-version
45+
print certify version
5446
```
5547

5648
## Use Certify as library

certify_test.go

+54-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,46 @@ package certify
22

33
import (
44
"crypto/x509/pkix"
5-
"fmt"
65
"net"
6+
"strings"
77
"testing"
88
"time"
99
)
1010

11+
var (
12+
RSATestCert = `-----BEGIN CERTIFICATE-----
13+
MIIFUTCCBDmgAwIBAgIRAKXhAWONgQR0CqU9N56GVkcwDQYJKoZIhvcNAQELBQAw
14+
RjELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBM
15+
TEMxEzARBgNVBAMTCkdUUyBDQSAxRDQwHhcNMjIwNDA5MTgxNzQ1WhcNMjIwNzA4
16+
MTgxNzQ0WjARMQ8wDQYDVQQDEwZnby5kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
17+
DwAwggEKAoIBAQC+++2A2RSZe0t8HrdKME2l8fsRtdBm83NDrFjI+ljGxh+fFoxp
18+
szy4nyseUpQFFthlns/9Z0LJSwRTdbxLDNQdiDxAyMsnt20Je1bsaUP4g1jDZ00e
19+
UhsMOsIApiCs6DRFqHydBLZVeWMraGa4e2g8q/x7LD3G7sYoXfOb3/yYJeghPuPE
20+
tEdYssVPzZmdB0zJYBQZTVCSH4ceiOrnfrV7tbXKYzhN/ZUhaKOA07y3Yu9WtgHK
21+
+drf4rnLxXALUxXOn73KFxrT5V7CYsnCcgtoc2v7dAtXORwd/cyD1OkfiL+8y5L3
22+
Ix/AxfahGrYoM5GwuUerrLJ9l0Jio40dyArNAgMBAAGjggJtMIICaTAOBgNVHQ8B
23+
Af8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNV
24+
HQ4EFgQUoHUzYU6hibyjmKXIgVEib4VVoF0wHwYDVR0jBBgwFoAUJeIYDrJXkZQq
25+
5dRdhpCD3lOzuJIweAYIKwYBBQUHAQEEbDBqMDUGCCsGAQUFBzABhilodHRwOi8v
26+
b2NzcC5wa2kuZ29vZy9zL2d0czFkNC9KYUk3amVIU3hkQTAxBggrBgEFBQcwAoYl
27+
aHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzMWQ0LmRlcjARBgNVHREECjAI
28+
ggZnby5kZXYwIQYDVR0gBBowGDAIBgZngQwBAgEwDAYKKwYBBAHWeQIFAzA8BgNV
29+
HR8ENTAzMDGgL6AthitodHRwOi8vY3Jscy5wa2kuZ29vZy9ndHMxZDQvRFlDVzlo
30+
TnpyWHcuY3JsMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDwAHYAUaOw9f0BeZxWbbg3
31+
eI8MpHrMGyfL956IQpoN/tSLBeUAAAGAD8z3swAABAMARzBFAiEAon8amRM09Pdm
32+
mTr8RhSUljNjDyh2HktHIHksuMqP9XkCIDJ0vmMjT8AAtODewy1CQfKY6MBLRzOc
33+
MX3pcNREwk9JAHYARqVV63X6kSAwtaKJafTzfREsQXS+/Um4havy/HD+bUcAAAGA
34+
D8z35gAABAMARzBFAiA1ylRik7z+2AOIdV+WNKjm4ui5/O3jmOAf2KCofz9SAgIh
35+
AMGoUjsi2x/ODEvJ5qG/NLgtNwjVzMUZ6cCuUOsAECyHMA0GCSqGSIb3DQEBCwUA
36+
A4IBAQA9kJTuv18L6fXMZwysP4tf5R7Wzu4tUhzVVQqnakLXt6lE4WuQGSRJGg+j
37+
JvC+MLkTBXJidmSUwOwofQVVWLKSgnMaF2CnvO+zpoWQ9j/xjM+UeDJTsOWJDqJr
38+
u7brL9iz0L3zopxmj2OT76rAjpnKVim/Dcw77pO0SA6Y6T68HaDxyx/xQG35U4ko
39+
g0J3x484NSLqNjnU4aGP/C8XKe4gLQR6k0OWm0fktd7pCEakrklyswsgoDG7rB50
40+
VvjDmr0mWlzsr1CfdnA1TysPFiULaRCFYaWhA71Sa/doNd5nrtuMzNetmmYFtpzq
41+
pAkvSpiE1H6RLeKYTqyIAGcui/Ah
42+
-----END CERTIFICATE-----`
43+
)
44+
1145
func TestGetCertificate(t *testing.T) {
1246
pkey, err := GetPrivateKey()
1347
if err != nil {
@@ -99,6 +133,23 @@ func TestCertInfo(t *testing.T) {
99133
t.Fatal(err)
100134
}
101135

102-
s := CertInfo(cert)
103-
fmt.Println(s)
136+
t.Log(CertInfo(cert))
137+
}
138+
139+
func TestCertInfoRSA(t *testing.T) {
140+
cert, err := ParseCertificate([]byte(RSATestCert))
141+
if err != nil {
142+
t.Fatal(err)
143+
}
144+
145+
t.Log(CertInfo(cert))
146+
}
147+
148+
func TestCertInEmptyFile(t *testing.T) {
149+
_, err := ParseCertificate([]byte(""))
150+
if err != nil {
151+
if !strings.Contains(err.Error(), "can't decode CA cert file") {
152+
t.Fatal("error must be contain can't decode CA cert file")
153+
}
154+
}
104155
}

cmd/certify/command.go

+16
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,19 @@ func createCertificate(args []string) error {
157157

158158
return nil
159159
}
160+
161+
// createIntermediateCertificate generate intermediate certificate and signed with existing root CA
162+
func createIntermediateCertificate(args []string) error {
163+
pkey, err := generatePrivateKey(caInterKeyPath)
164+
if err != nil {
165+
return err
166+
}
167+
168+
fmt.Println("Private key file generated", caInterKeyPath)
169+
170+
if err := generateIntermediateCert(pkey.PrivateKey, args); err != nil {
171+
return err
172+
}
173+
174+
return nil
175+
}

cmd/certify/command_test.go

+20-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func TestInitCA(t *testing.T) {
5959
}
6060

6161
t.Run("Test parse certificate", func(t *testing.T) {
62-
cert, err := getCACert()
62+
cert, err := getCACert(caPath)
6363
if err != nil {
6464
t.Fatal(err)
6565
}
@@ -248,3 +248,22 @@ func TestCreateCertificate(t *testing.T) {
248248
os.Remove("nothinux.local-key.pem")
249249
})
250250
}
251+
252+
func TestCreateIntermediateCertificate(t *testing.T) {
253+
t.Run("Test create intermediate certificate", func(t *testing.T) {
254+
if err := initCA([]string{"certify", "-init"}); err != nil {
255+
t.Fatal(err)
256+
}
257+
258+
if err := createIntermediateCertificate([]string{"certify", "-intermediate", "cn:nothinux"}); err != nil {
259+
t.Fatal(err)
260+
}
261+
})
262+
263+
t.Cleanup(func() {
264+
os.Remove(caPath)
265+
os.Remove(caKeyPath)
266+
os.Remove(caInterPath)
267+
os.Remove(caInterKeyPath)
268+
})
269+
}

cmd/certify/helper.go

+79-10
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func generateCA(pkey *ecdsa.PrivateKey, cn string, path string) error {
3434
CommonName: parseCN(cn),
3535
},
3636
NotBefore: time.Now(),
37-
NotAfter: time.Now().Add(8766 * time.Hour),
37+
NotAfter: time.Now().Add(87660 * time.Hour),
3838
IsCA: true,
3939
}
4040

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

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

52-
parentKey, err := getCAPrivateKey()
52+
var parent *x509.Certificate
53+
var parentKey *ecdsa.PrivateKey
54+
55+
// By default, If Intermediate CA exists the generated certificate
56+
// will be signed with intermediate CA. If not, it will be signed
57+
// with rootCA
58+
59+
parentKey, err = getCAPrivateKey(caInterKeyPath)
5360
if err != nil {
54-
return err
61+
if errors.Is(err, os.ErrNotExist) {
62+
parentKey, err = getCAPrivateKey(caKeyPath)
63+
if err != nil {
64+
return err
65+
}
66+
} else {
67+
return err
68+
}
5569
}
5670

57-
parent, err := getCACert()
71+
parent, err = getCACert(caInterPath)
5872
if err != nil {
59-
return err
73+
if errors.Is(err, os.ErrNotExist) {
74+
parent, err = getCACert(caPath)
75+
if err != nil {
76+
return err
77+
}
78+
} else {
79+
return err
80+
}
6081
}
6182

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

113+
func generateIntermediateCert(pkey *ecdsa.PrivateKey, args []string) error {
114+
_, _, cn, expiry, _ := parseArgs(args)
115+
116+
parentKey, err := getCAPrivateKey(caKeyPath)
117+
if err != nil {
118+
return err
119+
}
120+
121+
parent, err := getCACert(caPath)
122+
if err != nil {
123+
return err
124+
}
125+
126+
if cn == "" {
127+
cn = "certify"
128+
}
129+
130+
newCN := fmt.Sprintf("%s Intermediate", cn)
131+
132+
if expiry.Unix() > parent.NotAfter.Unix() {
133+
return fmt.Errorf("intermediate certificate expiry date can't longer than root CA")
134+
}
135+
136+
template := certify.Certificate{
137+
Subject: pkix.Name{
138+
Organization: []string{"certify"},
139+
CommonName: newCN,
140+
},
141+
NotBefore: time.Now(),
142+
NotAfter: expiry,
143+
IsCA: true,
144+
Parent: parent,
145+
ParentPrivateKey: parentKey,
146+
}
147+
148+
cert, err := template.GetCertificate(pkey)
149+
if err != nil {
150+
return err
151+
}
152+
153+
err = store(cert.String(), caInterPath)
154+
if err == nil {
155+
fmt.Println("Certificate file generated", caInterPath)
156+
}
157+
158+
return err
159+
}
160+
92161
// getFilename returns path based on given args
93162
// first it will check dnsnames, if nil, then check iplist, if iplist nil too
94163
// it will check common name
@@ -115,8 +184,8 @@ func getFilename(args []string, key bool) string {
115184
return path
116185
}
117186

118-
func getCAPrivateKey() (*ecdsa.PrivateKey, error) {
119-
pkey, err := readPrivateKeyFile("ca-key.pem")
187+
func getCAPrivateKey(path string) (*ecdsa.PrivateKey, error) {
188+
pkey, err := readPrivateKeyFile(path)
120189
if err != nil {
121190
return nil, err
122191
}
@@ -138,8 +207,8 @@ func readPrivateKeyFile(path string) (*ecdsa.PrivateKey, error) {
138207
return pkey, nil
139208
}
140209

141-
func getCACert() (*x509.Certificate, error) {
142-
c, err := readCertificateFile("ca-cert.pem")
210+
func getCACert(path string) (*x509.Certificate, error) {
211+
c, err := readCertificateFile(path)
143212
if err != nil {
144213
return nil, err
145214
}

0 commit comments

Comments
 (0)