├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── certify.go ├── certify_test.go ├── cmd └── certify │ ├── command.go │ ├── command_test.go │ ├── helper.go │ ├── helper_test.go │ ├── interactive.go │ ├── main.go │ ├── main_test.go │ └── testdata │ ├── ca-cert.pem │ ├── ca-crl.pem │ ├── ca-key.pem │ ├── empty │ ├── nothinux.pem │ ├── server-key.pem │ └── server.pem ├── crl.go ├── crl_test.go ├── go.mod ├── go.sum ├── helper.go ├── helper_test.go ├── key.go └── key_test.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.21 23 | - name: Get tag 24 | id: tag 25 | uses: devops-actions/action-get-tag@v1.0.2 26 | with: 27 | strip_v: true 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v2 30 | with: 31 | distribution: goreleaser 32 | version: latest 33 | args: release --rm-dist 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | TAP_TOKEN: ${{ secrets.TAP_TOKEN }} 37 | RELEASE_VERSION: ${{ steps.tag.outputs.tag }} 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | 8 | jobs: 9 | build: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set up Go 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: 1.21 17 | 18 | - name: Check out code 19 | uses: actions/checkout@v2 20 | 21 | - name: Test 22 | run: go test -v -count=1 ./... -race -covermode=atomic -coverprofile=coverage.out 23 | 24 | - name: upload coverage to codecov 25 | uses: codecov/codecov-action@v2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | # You may remove this if you don't use go modules. 4 | - go mod tidy 5 | # you may remove this if you don't need go generate 6 | - go generate ./... 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - darwin 13 | - freebsd 14 | - windows 15 | goarch: 16 | - amd64 17 | - arm 18 | - arm64 19 | goarm: 20 | - 6 21 | - 7 22 | ldflags: 23 | - -X main.Version={{.Version}} 24 | main: ./cmd/certify/ 25 | brews: 26 | - name: certify 27 | homepage: "https://github.com/nothinux/homebrew-tools" 28 | folder: Formula 29 | description: "Certify is an easy-to-use certificate manager and can be used as an alternative to OpenSSL. With Certify you can create your own private CA (Certificate Authority) and issue certificates with your own CA" 30 | license: "MIT" 31 | repository: 32 | owner: nothinux 33 | name: homebrew-tools 34 | branch: main 35 | token: "{{ .Env.TAP_TOKEN }}" 36 | commit_msg_template: "formula update for {{ .ProjectName }} version {{ .Tag }}" 37 | - name: "certify@{{ .Env.RELEASE_VERSION }}" 38 | homepage: "https://github.com/nothinux/homebrew-tools" 39 | folder: Formula 40 | description: "Certify is an easy-to-use certificate manager and can be used as an alternative to OpenSSL. With Certify you can create your own private CA (Certificate Authority) and issue certificates with your own CA" 41 | license: "MIT" 42 | repository: 43 | owner: nothinux 44 | name: homebrew-tools 45 | branch: main 46 | token: "{{ .Env.TAP_TOKEN }}" 47 | commit_msg_template: "formula update for {{ .ProjectName }} version {{ .Tag }}" 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Taufik Mulyana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :lock: Certify 2 | Certify is an easy-to-use certificate manager and can be used as an alternative to OpenSSL. With Certify you can create your own private CA (Certificate Authority) and issue certificates with your own CA. 3 | 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/nothinux/certify.svg)](https://pkg.go.dev/github.com/nothinux/certify) [![Go Report Card](https://goreportcard.com/badge/github.com/nothinux/certify)](https://goreportcard.com/report/github.com/nothinux/certify) ![test status](https://github.com/nothinux/certify/actions/workflows/test.yml/badge.svg?branch=master) [![codecov](https://codecov.io/gh/nothinux/certify/branch/master/graph/badge.svg?token=iR3c5Zwo3F)](https://codecov.io/gh/nothinux/certify) 5 | 6 | ## Feature 7 | + Create a CA and intermediate CA 8 | + Issue certificate with custom common name, ip san, dns san, expiry date, and extended key usage 9 | + Show certificate information from file or remote host 10 | + Export certificate to PKCS12 format 11 | + Verify private key matches with certificate 12 | + Revoke certificate 13 | 14 | 15 | ## Installation 16 | Download in the [release page](https://github.com/nothinux/certify/releases) 17 | 18 | ## Usage 19 | ``` 20 | _ _ ___ 21 | ___ ___ ___| |_|_| _|_ _ 22 | | _| -_| _| _| | _| | | 23 | |___|___|_| |_| |_|_| |_ | 24 | |___| Certify v1.x 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 | $ certify -init cn:web-server o:nothinux crl-nextupdate:100d 31 | 32 | Flags: 33 | -init 34 | Initialize new root CA Certificate and Key 35 | -intermediate 36 | Generate intermediate certificate 37 | -read 38 | Read certificate information from file or stdin 39 | -read-crl 40 | Read certificate revocation list from file or stdin 41 | -connect 42 | Show certificate information from remote host, use tlsver to set spesific tls version 43 | -export-p12 44 | Generate client.p12 pem file containing certificate, private key and ca certificate 45 | -match 46 | Verify cert-key.pem and cert.pem has same public key 47 | -interactive 48 | Run certify interactively 49 | -revoke 50 | Revoke certificate, the certificate will be added to CRL 51 | -verify-crl 52 | Check if the certificate was revoked 53 | -version 54 | print certify version 55 | ``` 56 | 57 | Create Certificate with CN nothinux and expiry 30 days 58 | ``` 59 | # create CA 60 | $ certify -init cn:nothinux o:nothinux 61 | 62 | # create Certificate 63 | $ certify cn:nothinux expiry:30d 64 | ``` 65 | 66 | Create Certificate interactively 67 | ``` 68 | $ certify -interactive 69 | ``` 70 | 71 | Read Certificate 72 | ``` 73 | $ certify -read ca-cert.pem 74 | or 75 | $ cat ca-cert.pem | certify -read 76 | ``` 77 | 78 | ## Use Certify as library 79 | You can also use certify as library for your Go application 80 | 81 | ### Installation 82 | ``` 83 | go get github.com/nothinux/certify 84 | ``` 85 | ### Documentation 86 | see [pkg.go.dev](https://pkg.go.dev/github.com/nothinux/certify) 87 | ### Example 88 | #### Create Private Key and CA Certificates 89 | ``` go 90 | package main 91 | 92 | import ( 93 | "crypto/x509/pkix" 94 | "log" 95 | "os" 96 | "time" 97 | 98 | "github.com/nothinux/certify" 99 | ) 100 | 101 | func main() { 102 | p, err := certify.GetPrivateKey() 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | 107 | if err := os.WriteFile("CA-key.pem", []byte(p.String()), 0640); err != nil { 108 | log.Fatal(err) 109 | } 110 | 111 | // create ca 112 | template := certify.Certificate{ 113 | Subject: pkix.Name{ 114 | Organization: []string{"certify"}, 115 | }, 116 | NotBefore: time.Now(), 117 | NotAfter: time.Now().Add(8766 * time.Hour), 118 | IsCA: true, 119 | } 120 | 121 | caCert, err := template.GetCertificate(p.PrivateKey) 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | 126 | if err := os.WriteFile("CA-cert.pem", []byte(caCert.String()), 0640); err != nil { 127 | log.Fatal(err) 128 | } 129 | 130 | } 131 | 132 | ``` 133 | 134 | ## License 135 | [MIT](https://github.com/nothinux/certify/blob/master/LICENSE) 136 | -------------------------------------------------------------------------------- /certify.go: -------------------------------------------------------------------------------- 1 | package certify 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | "fmt" 12 | "math/big" 13 | "net" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // Certificate hold certificate information 19 | type Certificate struct { 20 | SerialNumber *big.Int 21 | Subject pkix.Name 22 | NotBefore time.Time 23 | NotAfter time.Time 24 | IPAddress []net.IP 25 | DNSNames []string 26 | IsCA bool 27 | Parent *x509.Certificate 28 | ParentPrivateKey interface{} 29 | KeyUsage x509.KeyUsage 30 | ExtentedKeyUsage []x509.ExtKeyUsage 31 | SubjectKeyId []byte 32 | AuthorityKeyId []byte 33 | } 34 | 35 | // Result hold created certificate in []byte format 36 | type Result struct { 37 | ByteCert []byte 38 | Cert *x509.Certificate 39 | } 40 | 41 | // GetSerial returns serial and an error 42 | func GetSerial() (*big.Int, error) { 43 | serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return serial, nil 49 | } 50 | 51 | // SetTemplate set template for x509.Certificate from given Certificate struct 52 | func (c *Certificate) SetTemplate() x509.Certificate { 53 | return x509.Certificate{ 54 | SerialNumber: c.SerialNumber, 55 | Subject: c.Subject, 56 | NotBefore: c.NotBefore, 57 | NotAfter: c.NotAfter, 58 | ExtKeyUsage: c.ExtentedKeyUsage, 59 | KeyUsage: c.KeyUsage, 60 | IsCA: c.IsCA, 61 | IPAddresses: c.IPAddress, 62 | DNSNames: c.DNSNames, 63 | BasicConstraintsValid: true, 64 | SubjectKeyId: c.SubjectKeyId, 65 | AuthorityKeyId: c.AuthorityKeyId, 66 | } 67 | } 68 | 69 | // GetCertificate generate certificate and returns it in Result struct 70 | func (c *Certificate) GetCertificate(pkey *ecdsa.PrivateKey) (*Result, error) { 71 | serial, err := GetSerial() 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | c.SerialNumber = serial 77 | template := c.SetTemplate() 78 | 79 | if c.Parent == nil { 80 | c.Parent = &template 81 | } 82 | 83 | if c.ParentPrivateKey == nil { 84 | c.ParentPrivateKey = pkey 85 | } 86 | 87 | der, err := x509.CreateCertificate(rand.Reader, &template, c.Parent, &pkey.PublicKey, c.ParentPrivateKey) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | return &Result{ByteCert: der, Cert: c.Parent}, nil 93 | } 94 | 95 | // String returns certificate in string format 96 | func (r *Result) String() string { 97 | var w bytes.Buffer 98 | 99 | if err := pem.Encode(&w, &pem.Block{ 100 | Type: "CERTIFICATE", 101 | Bytes: r.ByteCert, 102 | }); err != nil { 103 | return "" 104 | } 105 | 106 | return w.String() 107 | } 108 | 109 | // ParseCertificate returns parsed certificate and error 110 | func ParseCertificate(cert []byte) (*x509.Certificate, error) { 111 | p, _ := pem.Decode(cert) 112 | if p == nil { 113 | return nil, fmt.Errorf("can't decode CA cert file") 114 | } 115 | 116 | c, err := x509.ParseCertificate(p.Bytes) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | return c, nil 122 | } 123 | 124 | // CertInfo returns certificate information 125 | func CertInfo(cert *x509.Certificate) string { 126 | var buf bytes.Buffer 127 | 128 | buf.WriteString("Certificate\n") 129 | buf.WriteString(fmt.Sprintf("%4sData:\n", "")) 130 | buf.WriteString(fmt.Sprintf("%8sVersion: %d\n", "", cert.Version)) 131 | buf.WriteString(fmt.Sprintf("%8sSerial Number:\n%12s%v\n", "", "", formatKeyIDWithColon(cert.SerialNumber.Bytes()))) 132 | buf.WriteString(fmt.Sprintf("%8sSignature Algorithm: %v\n", "", cert.SignatureAlgorithm)) 133 | 134 | buf.WriteString(fmt.Sprintf("%8sIssuer: %v\n", "", strings.Replace(cert.Issuer.String(), ",", ", ", -1))) 135 | 136 | buf.WriteString(fmt.Sprintf("%8sValidity:\n", "")) 137 | buf.WriteString(fmt.Sprintf("%12sNotBefore: %v\n", "", cert.NotBefore.Format("Jan 2 15:04:05 2006 GMT"))) 138 | buf.WriteString(fmt.Sprintf("%12sNotAfter : %v\n", "", cert.NotAfter.Format("Jan 2 15:04:05 2006 GMT"))) 139 | 140 | buf.WriteString(fmt.Sprintf("%8sSubject: %v\n", "", strings.Replace(cert.Subject.String(), ",", ", ", -1))) 141 | 142 | buf.WriteString(fmt.Sprintf("%8sSubject Public Key Info:\n", "")) 143 | buf.WriteString(fmt.Sprintf("%12sPublic Key Algorithm: %v\n", "", cert.PublicKeyAlgorithm)) 144 | if cert.PublicKeyAlgorithm == x509.ECDSA { 145 | if ecdsakey, ok := cert.PublicKey.(*ecdsa.PublicKey); ok { 146 | buf.WriteString(fmt.Sprintf("%16sPublic Key: (%d bit)\n", "", ecdsakey.Params().BitSize)) 147 | buf.WriteString(fmt.Sprintf("%16sNIST Curve: %s\n", "", ecdsakey.Params().Name)) 148 | } 149 | } 150 | 151 | if cert.PublicKeyAlgorithm == x509.RSA { 152 | if rsakey, ok := cert.PublicKey.(*rsa.PublicKey); ok { 153 | buf.WriteString(fmt.Sprintf("%16sRSA Public-Key: (%d bit)\n", "", rsakey.N.BitLen())) 154 | buf.WriteString(fmt.Sprintf("%16sExponent: %d (%#x)\n", "", rsakey.E, rsakey.E)) 155 | } 156 | } 157 | 158 | buf.WriteString(fmt.Sprintf("%8sX509v3 extensions:\n", "")) 159 | if len(parseExtKeyUsage(cert.ExtKeyUsage)) != 0 { 160 | buf.WriteString(fmt.Sprintf("%12sX509v3 Extended Key Usage:\n", "")) 161 | buf.WriteString(fmt.Sprintf("%16s%v\n", "", parseExtKeyUsage(cert.ExtKeyUsage))) 162 | } 163 | 164 | if cert.KeyUsage != 0 { 165 | buf.WriteString(fmt.Sprintf("%12sX509v3 Key Usage:\n", "")) 166 | buf.WriteString(fmt.Sprintf("%16s%v\n", "", strings.Join(parseKeyUsage(cert.KeyUsage), ", "))) 167 | } 168 | 169 | buf.WriteString(fmt.Sprintf("%12sX509v3 Basic Constraints:\n", "")) 170 | buf.WriteString(fmt.Sprintf("%16sCA: %v\n", "", cert.IsCA)) 171 | 172 | if cert.SubjectKeyId != nil { 173 | buf.WriteString(fmt.Sprintf("%12sX509v3 Subject Key Identifier:\n", "")) 174 | buf.WriteString(fmt.Sprintf("%16s%v\n", "", formatKeyIDWithColon(cert.SubjectKeyId))) 175 | } 176 | 177 | if cert.AuthorityKeyId != nil { 178 | buf.WriteString(fmt.Sprintf("%12sX509v3 Authority Key Identifier:\n", "")) 179 | buf.WriteString(fmt.Sprintf("%16s%v\n", "", formatKeyIDWithColon(cert.AuthorityKeyId))) 180 | } 181 | 182 | if len(cert.IPAddresses) != 0 || len(cert.DNSNames) != 0 { 183 | buf.WriteString(fmt.Sprintf("%12sX509v3 Subject Alternative Name:\n", "")) 184 | if len(cert.IPAddresses) != 0 { 185 | var ips []string 186 | for _, ip := range cert.IPAddresses { 187 | ips = append(ips, ip.String()) 188 | } 189 | buf.WriteString(fmt.Sprintf("%16sIP Address: %v\n", "", strings.Join(ips, ", "))) 190 | } 191 | if len(cert.DNSNames) != 0 { 192 | buf.WriteString(fmt.Sprintf("%16sDNS: %v\n", "", strings.Join(cert.DNSNames, ", "))) 193 | } 194 | } 195 | 196 | buf.WriteString(fmt.Sprintf("%4sSignature Algorithm: %v\n", "", cert.SignatureAlgorithm)) 197 | 198 | return buf.String() 199 | } 200 | -------------------------------------------------------------------------------- /certify_test.go: -------------------------------------------------------------------------------- 1 | package certify 2 | 3 | import ( 4 | "crypto/x509/pkix" 5 | "net" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 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 | 45 | func TestGetCertificate(t *testing.T) { 46 | pkey, err := GetPrivateKey() 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | template := Certificate{ 52 | Subject: pkix.Name{ 53 | Organization: []string{"certify"}, 54 | CommonName: "certify", 55 | }, 56 | NotBefore: time.Now(), 57 | NotAfter: time.Now().Add(24 * time.Hour), 58 | IsCA: true, 59 | DNSNames: []string{"github.com"}, 60 | IPAddress: []net.IP{ 61 | net.ParseIP("127.0.0.1"), 62 | }, 63 | } 64 | 65 | cert, err := template.GetCertificate(pkey.PrivateKey) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | t.Run("Test created certificate match with given information", func(t *testing.T) { 71 | c, err := ParseCertificate([]byte(cert.String())) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | if c.Subject.CommonName != "certify" { 77 | t.Fatalf("got %v, want certify", c.Subject.CommonName) 78 | } 79 | 80 | if c.Subject.Organization[0] != "certify" { 81 | t.Fatalf("got %v, want certify", c.Subject.Organization[0]) 82 | } 83 | 84 | if !c.IsCA { 85 | t.Fatal("IsCA must be true") 86 | } 87 | 88 | if c.DNSNames[0] != "github.com" { 89 | t.Fatalf("got %v, want github.com", c.DNSNames[0]) 90 | } 91 | 92 | if c.IPAddresses[0].String() != "127.0.0.1" { 93 | t.Fatalf("got %v, want 127.0.0.1", c.IPAddresses[0].String()) 94 | } 95 | 96 | tomorrow := time.Now().Add(24 * time.Hour) 97 | 98 | if c.NotAfter.Day() != tomorrow.Day() { 99 | t.Fatalf("got %v, want %v", c.NotAfter.Day(), tomorrow.Day()) 100 | } 101 | 102 | }) 103 | 104 | } 105 | 106 | func TestCertInfo(t *testing.T) { 107 | pkey, err := GetPrivateKey() 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | 112 | template := Certificate{ 113 | Subject: pkix.Name{ 114 | Organization: []string{"certify"}, 115 | CommonName: "certify", 116 | }, 117 | NotBefore: time.Now(), 118 | NotAfter: time.Now().Add(24 * time.Hour), 119 | IsCA: true, 120 | DNSNames: []string{"github.com"}, 121 | IPAddress: []net.IP{ 122 | net.ParseIP("127.0.0.1"), 123 | }, 124 | } 125 | 126 | res, err := template.GetCertificate(pkey.PrivateKey) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | 131 | cert, err := ParseCertificate([]byte(res.String())) 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | 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 | } 155 | } 156 | -------------------------------------------------------------------------------- /cmd/certify/command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | 11 | "github.com/nothinux/certify" 12 | ) 13 | 14 | // initCA create private key and certificate for certificate authority 15 | func initCA(args []string) error { 16 | pkey, err := generatePrivateKey(caKeyPath) 17 | if err != nil { 18 | return err 19 | } 20 | fmt.Println("CA private key file generated", caKeyPath) 21 | 22 | caCert, err := generateCA(pkey.PrivateKey, args, caPath) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | fmt.Println("CA certificate file generated", caPath) 28 | 29 | if err := generateCRL(args, pkey.PrivateKey, caCert.Cert); err != nil { 30 | return err 31 | } 32 | fmt.Println("CRL file generated", caCRLPath) 33 | 34 | return nil 35 | } 36 | 37 | // readCertificate read certificate from stdin or from file 38 | func readCertificate(args []string, stdin *os.File) (string, error) { 39 | var certByte []byte 40 | var err error 41 | 42 | if len(args) < 3 { 43 | certByte, err = io.ReadAll(stdin) 44 | if err != nil { 45 | return "", err 46 | } 47 | } else { 48 | certByte, err = os.ReadFile(args[2]) 49 | if err != nil { 50 | return "", err 51 | } 52 | } 53 | 54 | cert, err := certify.ParseCertificate(certByte) 55 | if err != nil { 56 | return "", err 57 | } 58 | 59 | return certify.CertInfo(cert), nil 60 | } 61 | 62 | // readCRL read crl from stdin or from file 63 | func readCRL(args []string, stdin *os.File) (string, error) { 64 | var certByte []byte 65 | var err error 66 | 67 | if len(args) < 3 { 68 | certByte, err = io.ReadAll(stdin) 69 | if err != nil { 70 | return "", err 71 | } 72 | } else { 73 | certByte, err = os.ReadFile(args[2]) 74 | if err != nil { 75 | return "", err 76 | } 77 | } 78 | 79 | crl, err := certify.ParseCRL(certByte) 80 | if err != nil { 81 | return "", err 82 | } 83 | 84 | return certify.CRLInfo(crl), nil 85 | } 86 | 87 | // readRemoteCertificate read certificate from remote host 88 | func readRemoteCertificate(args []string) (string, error) { 89 | if len(args) < 3 { 90 | return "", fmt.Errorf("you must provide remote host") 91 | } 92 | 93 | tlsConfig := &tls.Config{} 94 | 95 | tlsVer := parseTLSVersion(args) 96 | tlsConfig.MinVersion = tlsVer 97 | tlsConfig.MaxVersion = tlsVer 98 | tlsConfig.InsecureSkipVerify = parseInsecureArg(args) 99 | 100 | caPath := parseCAarg(args) 101 | if caPath != "" { 102 | caCert, err := os.ReadFile(caPath) 103 | if err != nil { 104 | log.Printf("ca-cert error %v, ignoring the ca-cert\n", err) 105 | } 106 | 107 | if err == nil { 108 | caCertPool := x509.NewCertPool() 109 | caCertPool.AppendCertsFromPEM(caCert) 110 | 111 | tlsConfig.RootCAs = caCertPool 112 | } 113 | } 114 | 115 | result, err := tlsDial(args[2], tlsConfig) 116 | if err != nil { 117 | return "", err 118 | } 119 | 120 | return certify.CertInfo(result), nil 121 | } 122 | 123 | // matchCertificate math certificate with private key 124 | func matchCertificate(args []string) error { 125 | if len(args) < 4 { 126 | return fmt.Errorf("you must provide pkey and cert") 127 | } 128 | 129 | pubkey, pubcert, err := matcher(args[2], args[3]) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | fmt.Printf( 135 | "pubkey from %s:\n%s\n\npubkey from %s:\n%s\n✅ certificate and private key match\n", 136 | args[2], 137 | pubkey, 138 | args[3], 139 | pubcert, 140 | ) 141 | 142 | return nil 143 | } 144 | 145 | // exportCertificate export certificate to pkcs12 format 146 | func exportCertificate(args []string, bytePass []byte) error { 147 | // verify if cert and key has same public key 148 | _, _, err := matcher(args[2], args[3]) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | pfxData, err := getPfxData( 154 | args[2], 155 | args[3], 156 | args[4], 157 | string(bytePass), 158 | ) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | if err := os.WriteFile("client.p12", pfxData, 0644); err != nil { 164 | return err 165 | } 166 | fmt.Println("\ncertificate exported to client.p12") 167 | return nil 168 | } 169 | 170 | // createCertificate generate certificate and signed with existing CA 171 | func createCertificate(args []string) error { 172 | keyPath := getFilename(args, true) 173 | 174 | pkey, err := generatePrivateKey(keyPath) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | fmt.Println("Private key file generated", keyPath) 180 | 181 | if err := generateCert(pkey.PrivateKey, args); err != nil { 182 | return err 183 | } 184 | 185 | return nil 186 | } 187 | 188 | func verifyCertificate(args []string) error { 189 | if len(args) < 4 { 190 | return fmt.Errorf("you need to provide cert file and crl file") 191 | } 192 | 193 | cert, err := readCertificateFile(args[2]) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | crl, err := readCRLFile(args[3]) 199 | if err != nil { 200 | return err 201 | } 202 | 203 | for _, sn := range crl.RevokedCertificateEntries { 204 | if sn.SerialNumber.Cmp(cert.SerialNumber) == 0 { 205 | fmt.Printf("%s\ncode: %d\ncertificate revoked at %v\n", cert.Subject.String(), sn.ReasonCode, sn.RevocationTime.Format("2006-01-02 15:04:05")) 206 | return fmt.Errorf("error %s verification failed", args[2]) 207 | } 208 | } 209 | 210 | fmt.Printf("%s: OK\n", args[2]) 211 | return nil 212 | } 213 | 214 | func revokeCertificate(args []string) (string, error) { 215 | if len(args) < 4 { 216 | return "", fmt.Errorf("you need to provide cert file and crl file") 217 | } 218 | 219 | crlBytes, err := os.ReadFile(args[3]) 220 | if err != nil { 221 | return "", err 222 | } 223 | 224 | cert, err := readCertificateFile(args[2]) 225 | if err != nil { 226 | return "", err 227 | } 228 | 229 | caCert, err := getCACert(caPath) 230 | if err != nil { 231 | return "", err 232 | } 233 | 234 | pkey, err := getCAPrivateKey(caKeyPath) 235 | if err != nil { 236 | return "", err 237 | } 238 | 239 | _, _, _, _, _, _, nextUpdate := parseArgs(args) 240 | 241 | fmt.Printf("revoking certificate cn=%s o=%s with serial number %s\n", cert.Subject.CommonName, cert.Subject.Organization, cert.SerialNumber) 242 | crl, crlNum, err := certify.RevokeCertificate(crlBytes, cert, caCert, pkey, nextUpdate) 243 | if err != nil { 244 | return "", err 245 | } 246 | 247 | path := fmt.Sprintf("ca-crl-%s.pem", crlNum) 248 | 249 | fmt.Printf("CA CRL file generated %s\n", path) 250 | return path, store(crl.String(), path) 251 | } 252 | 253 | // createIntermediateCertificate generate intermediate certificate and signed with existing root CA 254 | func createIntermediateCertificate(args []string) error { 255 | pkey, err := generatePrivateKey(caInterKeyPath) 256 | if err != nil { 257 | return err 258 | } 259 | 260 | fmt.Println("Private key file generated", caInterKeyPath) 261 | 262 | if err := generateIntermediateCert(pkey.PrivateKey, args); err != nil { 263 | return err 264 | } 265 | 266 | return nil 267 | } 268 | -------------------------------------------------------------------------------- /cmd/certify/command_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | var TestCertificate = `-----BEGIN CERTIFICATE----- 11 | MIIBmDCCAT2gAwIBAgIQUjIMhHGW4CreYEIQOnPDdDAKBggqhkjOPQQDAjAkMRAw 12 | DgYDVQQKEwdjZXJ0aWZ5MRAwDgYDVQQDEwdjZXJ0aWZ5MB4XDTIyMDMxNzA4NDQx 13 | MloXDTIzMDMxNzE0NDQxMlowJDEQMA4GA1UEChMHY2VydGlmeTEQMA4GA1UEAxMH 14 | Y2VydGlmeTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIPmsrI8hCLHryeWc0wz 15 | zrrbAXhohqMfFnZS95qM83p/EHHUO4yoi4LSZhZnvPhPYG+St4KBZj2mqZYs6nf8 16 | sTSjUTBPMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8E 17 | BTADAQH/MB0GA1UdDgQWBBTuUKyfBpn78BTa2fodsucBYuApejAKBggqhkjOPQQD 18 | AgNJADBGAiEAlYCxixkXh6eI1nHBAhaUHajYF6ZWpK4tiDCWR5lHIA0CIQCpgqUp 19 | +R8a3HBTIcrpgdoI2g11HmV9+qOysbuWNpTnMw== 20 | -----END CERTIFICATE-----` 21 | 22 | func TestInitCA(t *testing.T) { 23 | tests := []struct { 24 | Name string 25 | Args []string 26 | expectedCN string 27 | }{ 28 | { 29 | Name: "Test run -init without cn", 30 | Args: []string{"certify", "-init"}, 31 | expectedCN: "certify", 32 | }, 33 | { 34 | Name: "Test run -init with wrong cn format", 35 | Args: []string{"certify", "-init", "cn-nothinux"}, 36 | expectedCN: "certify", 37 | }, 38 | { 39 | Name: "Test run -init with other argument", 40 | Args: []string{"certify", "-init", "cert"}, 41 | expectedCN: "certify", 42 | }, 43 | { 44 | Name: "Test run -init with 4 argument", 45 | Args: []string{"certify", "-init", "cert", "cn:aaa"}, 46 | expectedCN: "aaa", 47 | }, 48 | { 49 | Name: "Test run -init with cn", 50 | Args: []string{"certify", "-init", "cn:nothinux"}, 51 | expectedCN: "nothinux", 52 | }, 53 | } 54 | 55 | for _, tt := range tests { 56 | t.Run(tt.Name, func(t *testing.T) { 57 | if err := initCA(tt.Args); err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | t.Run("Test parse certificate", func(t *testing.T) { 62 | cert, err := getCACert(caPath) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | if cert.Subject.CommonName != tt.expectedCN { 68 | t.Fatalf("got %v, want %v", cert.Subject.CommonName, tt.expectedCN) 69 | } 70 | }) 71 | 72 | t.Cleanup(func() { 73 | os.Remove(caPath) 74 | os.Remove(caKeyPath) 75 | os.Remove(caCRLPath) 76 | }) 77 | }) 78 | } 79 | } 80 | 81 | func getTestCertificate(filename string) *os.File { 82 | f, err := os.Open(filename) 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | 87 | _, err = f.Seek(0, 0) 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | os.Stdin = f 93 | 94 | return os.Stdin 95 | } 96 | 97 | func TestReadCertificate(t *testing.T) { 98 | // TODO: add test reading certificate from stdin 99 | tests := []struct { 100 | Name string 101 | Args []string 102 | Stdin *os.File 103 | expectedOutput string 104 | expectedError string 105 | }{ 106 | { 107 | Name: "Test read certificate from file", 108 | Args: []string{"certify", "-read", "testdata/ca-cert.pem"}, 109 | Stdin: nil, 110 | expectedOutput: "Issuer: CN=certify, O=certify", 111 | }, 112 | { 113 | Name: "Test read not exists certificate", 114 | Args: []string{"certify", "-read", "ca-cert.pem"}, 115 | Stdin: nil, 116 | expectedError: "open ca-cert.pem: no such file or directory", 117 | }, 118 | { 119 | Name: "Test read content from stdin", 120 | Args: []string{"certify", "-read"}, 121 | Stdin: getTestCertificate("testdata/empty"), 122 | expectedError: "can't decode CA cert file", 123 | }, 124 | } 125 | 126 | for _, tt := range tests { 127 | t.Run(tt.Name, func(t *testing.T) { 128 | cert, err := readCertificate(tt.Args, tt.Stdin) 129 | 130 | if err != nil { 131 | if !strings.Contains(err.Error(), tt.expectedError) { 132 | t.Fatalf("got %v, want %v", err, tt.expectedError) 133 | } 134 | } 135 | 136 | if !strings.Contains(cert, tt.expectedOutput) { 137 | t.Fatalf("error, want output %s", tt.expectedOutput) 138 | } 139 | 140 | if tt.Stdin != nil { 141 | tt.Stdin.Close() 142 | } 143 | }) 144 | } 145 | } 146 | 147 | func TestReadCRL(t *testing.T) { 148 | // TODO: add test reading certificate from stdin 149 | tests := []struct { 150 | Name string 151 | Args []string 152 | Stdin *os.File 153 | expectedOutput string 154 | expectedError string 155 | }{ 156 | { 157 | Name: "Test read crl from file", 158 | Args: []string{"certify", "-read-crl", "testdata/ca-crl.pem"}, 159 | Stdin: nil, 160 | expectedOutput: "Issuer: CN=certify, O=certify", 161 | }, 162 | { 163 | Name: "Test read not exists crl", 164 | Args: []string{"certify", "-read", "ca-crl.pem"}, 165 | Stdin: nil, 166 | expectedError: "open ca-crl.pem: no such file or directory", 167 | }, 168 | { 169 | Name: "Test read content from stdin", 170 | Args: []string{"certify", "-read"}, 171 | Stdin: getTestCertificate("testdata/empty"), 172 | expectedError: "no pem data", 173 | }, 174 | } 175 | 176 | for _, tt := range tests { 177 | t.Run(tt.Name, func(t *testing.T) { 178 | crl, err := readCRL(tt.Args, tt.Stdin) 179 | 180 | if err != nil { 181 | if !strings.Contains(err.Error(), tt.expectedError) { 182 | t.Fatalf("got %v, want %v", err, tt.expectedError) 183 | } 184 | } 185 | 186 | if !strings.Contains(crl, tt.expectedOutput) { 187 | t.Fatalf("error, want output %s", tt.expectedOutput) 188 | } 189 | 190 | if tt.Stdin != nil { 191 | tt.Stdin.Close() 192 | } 193 | }) 194 | } 195 | } 196 | 197 | func TestRevokeCertificate(t *testing.T) { 198 | // TODO: add test reading certificate from stdin 199 | tests := []struct { 200 | Name string 201 | Args []string 202 | Stdin *os.File 203 | expectedOutput string 204 | expectedError string 205 | }{ 206 | { 207 | Name: "Test revoke certificate", 208 | Args: []string{"certify", "-revoke", "nothinux.local.pem", "ca-crl.pem"}, 209 | Stdin: nil, 210 | }, 211 | { 212 | Name: "Test revoke certificate with not enough argument", 213 | Args: []string{"certify", "-revoke", "ca-crl.pem"}, 214 | Stdin: nil, 215 | expectedError: "you need to provide cert file and crl file", 216 | }, 217 | { 218 | Name: "Test revoke certificate with wrong crl file", 219 | Args: []string{"certify", "-revoke", "nothinux.local.pem", "ca-cert.pem"}, 220 | Stdin: nil, 221 | expectedError: "x509: unsupported crl version", 222 | }, 223 | { 224 | Name: "Test revoke certificate with wrong cert file", 225 | Args: []string{"certify", "-revoke", "ca-crl.pem", "ca-crl.pem"}, 226 | Stdin: nil, 227 | expectedError: "x509: malformed validity", 228 | }, 229 | } 230 | 231 | for _, tt := range tests { 232 | t.Run(tt.Name, func(t *testing.T) { 233 | if err := initCA([]string{"certify", "-init"}); err != nil { 234 | t.Fatal(err) 235 | } 236 | 237 | if err := createCertificate([]string{"certify", "nothinux.local"}); err != nil { 238 | t.Fatal(err) 239 | } 240 | 241 | crlPath, err := revokeCertificate(tt.Args) 242 | if err != nil { 243 | if !strings.Contains(err.Error(), tt.expectedError) { 244 | t.Fatalf("got %v, want %v", err, tt.expectedError) 245 | } 246 | cleanupfiles([]string{caPath, caKeyPath, caCRLPath, "nothinux.local.pem", "nothinux.local-key.pem"}) 247 | return 248 | } 249 | 250 | _, err = readCRL([]string{"certify", "-read-crl", crlPath}, nil) 251 | if err != nil { 252 | t.Fatal(err) 253 | } 254 | 255 | t.Cleanup(func() { 256 | cleanupfiles([]string{caPath, caKeyPath, caCRLPath, crlPath, "nothinux.local.pem", "nothinux.local-key.pem"}) 257 | }) 258 | }) 259 | } 260 | } 261 | 262 | func TestVerifyCertificate(t *testing.T) { 263 | // TODO: add test reading certificate from stdin 264 | tests := []struct { 265 | Name string 266 | Args []string 267 | expectedOutput string 268 | expectedError string 269 | }{ 270 | { 271 | Name: "Test verify certificate", 272 | Args: []string{"certify", "-verify", "nothinux.local.pem", "ca-crl.pem"}, 273 | expectedOutput: "nothinux.local.pem: OK", 274 | }, 275 | { 276 | Name: "Test verify certificate with not enough argument", 277 | Args: []string{"certify", "-verify", "ca-crl.pem"}, 278 | expectedError: "you need to provide cert file and crl file", 279 | }, 280 | { 281 | Name: "Test verify certificate with wrong crl file", 282 | Args: []string{"certify", "-verify", "nothinux.local.pem", "ca-cert.pem"}, 283 | expectedError: "x509: unsupported crl version", 284 | }, 285 | { 286 | Name: "Test verify certificate with wrong cert file", 287 | Args: []string{"certify", "-revoke", "ca-crl.pem", "ca-crl.pem"}, 288 | expectedError: "x509: malformed validity", 289 | }, 290 | { 291 | Name: "Test verify certificate with revoked cert file", 292 | Args: []string{"certify", "-revoke", "nothinux.local.pem", "ca-crl.pem"}, 293 | expectedError: "error nothinux.local.pem verification failed", 294 | }, 295 | } 296 | 297 | for _, tt := range tests { 298 | t.Run(tt.Name, func(t *testing.T) { 299 | if err := initCA([]string{"certify", "-init"}); err != nil { 300 | t.Fatal(err) 301 | } 302 | 303 | if err := createCertificate([]string{"certify", "nothinux.local"}); err != nil { 304 | t.Fatal(err) 305 | } 306 | 307 | if tt.expectedOutput != "" { 308 | err := verifyCertificate(tt.Args) 309 | if err != nil { 310 | t.Fatal(err) 311 | } 312 | cleanupfiles([]string{caPath, caKeyPath, caCRLPath, "nothinux.local.pem", "nothinux.local-key.pem"}) 313 | return 314 | } 315 | 316 | crlPath, err := revokeCertificate(tt.Args) 317 | if err != nil { 318 | if !strings.Contains(err.Error(), tt.expectedError) { 319 | t.Fatalf("got %v, want %v", err, tt.expectedError) 320 | } 321 | cleanupfiles([]string{caPath, caKeyPath, caCRLPath, "nothinux.local.pem", "nothinux.local-key.pem"}) 322 | return 323 | } 324 | 325 | // replace last element 326 | tt.Args = tt.Args[:len(tt.Args)-1] 327 | tt.Args = append(tt.Args, crlPath) 328 | 329 | // verify revoked cert 330 | err = verifyCertificate(tt.Args) 331 | if err != nil { 332 | if !strings.Contains(err.Error(), tt.expectedError) { 333 | t.Fatalf("got %v, want %v", err, tt.expectedError) 334 | } 335 | cleanupfiles([]string{caPath, caKeyPath, caCRLPath, crlPath, "nothinux.local.pem", "nothinux.local-key.pem"}) 336 | return 337 | } 338 | 339 | t.Cleanup(func() { 340 | cleanupfiles([]string{caPath, caKeyPath, caCRLPath, crlPath, "nothinux.local.pem", "nothinux.local-key.pem"}) 341 | }) 342 | }) 343 | } 344 | } 345 | 346 | func TestReadRemoteCertificate(t *testing.T) { 347 | tests := []struct { 348 | Name string 349 | Args []string 350 | ExpectedOutput string 351 | ExpectedError string 352 | }{ 353 | { 354 | Name: "Test valid Host", 355 | Args: []string{"certify", "-connect", "google.com:443"}, 356 | ExpectedOutput: "Subject: CN=*.google.com", 357 | }, 358 | { 359 | Name: "Test invalid Host", 360 | Args: []string{"certify", "-connect", "google.com"}, 361 | ExpectedError: "missing port in address", 362 | }, 363 | { 364 | Name: "Test invalid Host", 365 | Args: []string{"certify", "-connect", "google"}, 366 | ExpectedError: "missing port in address", 367 | }, 368 | { 369 | Name: "Test invalid Host", 370 | Args: []string{"certify", "-connect", "1.1.1.1"}, 371 | ExpectedError: "missing port in address", 372 | }, 373 | { 374 | Name: "Test no Host", 375 | Args: []string{"certify", "-connect"}, 376 | ExpectedError: "you must provide remote host", 377 | }, 378 | } 379 | 380 | for _, tt := range tests { 381 | result, err := readRemoteCertificate(tt.Args) 382 | if err != nil { 383 | if !strings.Contains(err.Error(), tt.ExpectedError) { 384 | t.Fatalf("got %v want %v", err.Error(), tt.ExpectedError) 385 | } 386 | } 387 | 388 | if !strings.Contains(result, tt.ExpectedOutput) { 389 | t.Fatalf("certificate doesn't containing %s", tt.ExpectedOutput) 390 | } 391 | } 392 | } 393 | 394 | func TestMatchCertificate(t *testing.T) { 395 | tests := []struct { 396 | Name string 397 | Args []string 398 | expectedError string 399 | }{ 400 | { 401 | Name: "Test when certificate and private key match", 402 | Args: []string{"certify", "-match", "testdata/ca-key.pem", "testdata/ca-cert.pem"}, 403 | }, 404 | { 405 | Name: "Test when certificate and private key doesnt match", 406 | Args: []string{"certify", "-match", "testdata/ca-key.pem", "testdata/nothinux.pem"}, 407 | expectedError: "private key doesn't match with given certificate", 408 | }, 409 | { 410 | Name: "Test when no private key", 411 | Args: []string{"certify", "-match", "testdata/ca-cert.pem"}, 412 | expectedError: "you must provide pkey and cert", 413 | }, 414 | } 415 | 416 | for _, tt := range tests { 417 | if err := matchCertificate(tt.Args); err != nil { 418 | if !strings.Contains(err.Error(), tt.expectedError) { 419 | t.Fatalf("got %v, want %v", err.Error(), tt.expectedError) 420 | } 421 | } 422 | } 423 | } 424 | 425 | func TestCreateCertificate(t *testing.T) { 426 | t.Run("Test create certificate with no existing CA", func(t *testing.T) { 427 | if err := createCertificate([]string{"certify", "127.0.0.1", "nothinux.local"}); err != nil { 428 | if !strings.Contains(err.Error(), "open ca-key.pem: no such file or directory") { 429 | t.Fatalf("got %v, want open ca-key.pem: no such file or directory", err.Error()) 430 | } 431 | } 432 | 433 | os.Remove("nothinux.local-key.pem") 434 | }) 435 | 436 | t.Run("Test create certificate", func(t *testing.T) { 437 | if err := initCA([]string{"certify", "-init"}); err != nil { 438 | t.Fatal(err) 439 | } 440 | 441 | if err := createCertificate([]string{"certify", "127.0.0.1", "nothinux.local"}); err != nil { 442 | t.Fatal(err) 443 | } 444 | }) 445 | 446 | t.Cleanup(func() { 447 | os.Remove(caPath) 448 | os.Remove(caKeyPath) 449 | os.Remove(caCRLPath) 450 | os.Remove("nothinux.local.pem") 451 | os.Remove("nothinux.local-key.pem") 452 | }) 453 | } 454 | 455 | func TestCreateIntermediateCertificate(t *testing.T) { 456 | t.Run("Test create intermediate certificate", func(t *testing.T) { 457 | if err := initCA([]string{"certify", "-init"}); err != nil { 458 | t.Fatal(err) 459 | } 460 | 461 | if err := createIntermediateCertificate([]string{"certify", "-intermediate", "cn:nothinux"}); err != nil { 462 | t.Fatal(err) 463 | } 464 | }) 465 | 466 | t.Cleanup(func() { 467 | os.Remove(caPath) 468 | os.Remove(caKeyPath) 469 | os.Remove(caCRLPath) 470 | os.Remove(caInterPath) 471 | os.Remove(caInterKeyPath) 472 | }) 473 | } 474 | 475 | func TestExportCertificate(t *testing.T) { 476 | tests := []struct { 477 | Name string 478 | Args []string 479 | Password string 480 | expectedError string 481 | }{ 482 | { 483 | Name: "Test export certificate", 484 | Args: []string{"certify", "-match", "testdata/server-key.pem", "testdata/server.pem", "testdata/ca-cert.pem"}, 485 | Password: "password", 486 | }, 487 | { 488 | Name: "Test export certificate with invalid private key", 489 | Args: []string{"certify", "-match", "testdata/key.pem", "testdata/server.pem", "testdata/ca-cert.pem"}, 490 | Password: "password", 491 | expectedError: "no such file or directory", 492 | }, 493 | { 494 | Name: "Test export certificate doesn't match with private key", 495 | Args: []string{"certify", "-match", "testdata/server-key.pem", "testdata/nothinux.pem", "testdata/ca-cert.pem"}, 496 | Password: "password", 497 | expectedError: "private key doesn't match with given certificate", 498 | }, 499 | { 500 | Name: "Test export with wrong argument", 501 | Args: []string{"certify", "-match", "testdata/server.pem", "testdata/server-key.pem", "testdata/ca-cert.pem"}, 502 | Password: "password", 503 | expectedError: "failed to parse EC private key", 504 | }, 505 | } 506 | 507 | for _, tt := range tests { 508 | t.Run(tt.Name, func(t *testing.T) { 509 | if err := exportCertificate(tt.Args, []byte(tt.Password)); err != nil { 510 | if !strings.Contains(err.Error(), tt.expectedError) { 511 | t.Fatalf("the error must be contain %s, got %v", tt.expectedError, err) 512 | } 513 | } 514 | }) 515 | 516 | t.Cleanup(func() { 517 | os.Remove("client.p12") 518 | }) 519 | } 520 | 521 | } 522 | -------------------------------------------------------------------------------- /cmd/certify/helper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/rand" 6 | "crypto/sha1" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "errors" 11 | "fmt" 12 | "log" 13 | "net" 14 | "os" 15 | "strconv" 16 | "strings" 17 | "time" 18 | 19 | "github.com/nothinux/certify" 20 | "software.sslmate.com/src/go-pkcs12" 21 | ) 22 | 23 | func generatePrivateKey(path string) (*certify.PrivateKey, error) { 24 | p, err := certify.GetPrivateKey() 25 | if err != nil { 26 | return &certify.PrivateKey{}, err 27 | } 28 | 29 | return p, store(p.String(), path) 30 | } 31 | 32 | func getKeyIdentifier(publicKey *ecdsa.PublicKey) ([]byte, error) { 33 | b, err := x509.MarshalPKIXPublicKey(publicKey) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | ki := sha1.Sum(b) 39 | return ki[:], nil 40 | } 41 | 42 | func generateCA(pkey *ecdsa.PrivateKey, args []string, path string) (*certify.Result, error) { 43 | _, _, cn, org, expiry, _, _ := parseArgs(args) 44 | 45 | ski, err := getKeyIdentifier(&pkey.PublicKey) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | template := certify.Certificate{ 51 | Subject: pkix.Name{ 52 | Organization: []string{org}, 53 | CommonName: cn, 54 | }, 55 | NotBefore: time.Now(), 56 | NotAfter: expiry, 57 | IsCA: true, 58 | KeyUsage: x509.KeyUsageCRLSign | x509.KeyUsageCertSign, 59 | SubjectKeyId: ski, 60 | } 61 | 62 | caCert, err := template.GetCertificate(pkey) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return caCert, store(caCert.String(), path) 68 | } 69 | 70 | func generateCRL(args []string, pkey *ecdsa.PrivateKey, caCert *x509.Certificate) error { 71 | _, _, _, _, _, _, nextUpdate := parseArgs(args) 72 | 73 | crl, _, err := certify.CreateCRL(pkey, caCert, nil, nextUpdate) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return store(crl.String(), caCRLPath) 79 | } 80 | 81 | func generateCert(pkey *ecdsa.PrivateKey, args []string) (err error) { 82 | iplist, dnsnames, cn, org, expiry, ekus, _ := parseArgs(args) 83 | 84 | var parent *x509.Certificate 85 | var parentKey *ecdsa.PrivateKey 86 | 87 | // By default, If Intermediate CA exists the generated certificate 88 | // will be signed with intermediate CA. If not, it will be signed 89 | // with rootCA 90 | 91 | parentKey, err = getCAPrivateKey(caInterKeyPath) 92 | if err != nil { 93 | if errors.Is(err, os.ErrNotExist) { 94 | parentKey, err = getCAPrivateKey(caKeyPath) 95 | if err != nil { 96 | return err 97 | } 98 | } else { 99 | return err 100 | } 101 | } 102 | 103 | parent, err = getCACert(caInterPath) 104 | if err != nil { 105 | if errors.Is(err, os.ErrNotExist) { 106 | parent, err = getCACert(caPath) 107 | if err != nil { 108 | return err 109 | } 110 | } else { 111 | return err 112 | } 113 | } 114 | 115 | aki, err := getKeyIdentifier(&parentKey.PublicKey) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | ski, err := getKeyIdentifier(&pkey.PublicKey) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | template := certify.Certificate{ 126 | Subject: pkix.Name{ 127 | Organization: []string{org}, 128 | CommonName: cn, 129 | }, 130 | NotBefore: time.Now(), 131 | NotAfter: expiry, 132 | IPAddress: iplist, 133 | DNSNames: dnsnames, 134 | IsCA: false, 135 | Parent: parent, 136 | ParentPrivateKey: parentKey, 137 | ExtentedKeyUsage: ekus, 138 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment | x509.KeyUsageKeyAgreement, 139 | AuthorityKeyId: aki, 140 | SubjectKeyId: ski, 141 | } 142 | 143 | cert, err := template.GetCertificate(pkey) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | certPath := getFilename(args, false) 149 | 150 | err = store(cert.String(), certPath) 151 | if err == nil { 152 | fmt.Println("Certificate file generated", certPath) 153 | } 154 | 155 | return err 156 | } 157 | 158 | func generateIntermediateCert(pkey *ecdsa.PrivateKey, args []string) error { 159 | _, _, cn, org, expiry, _, _ := parseArgs(args) 160 | 161 | parentKey, err := getCAPrivateKey(caKeyPath) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | parent, err := getCACert(caPath) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | newCN := fmt.Sprintf("%s Intermediate", cn) 172 | 173 | if expiry.Unix() > parent.NotAfter.Unix() { 174 | return fmt.Errorf("intermediate certificate expiry date can't longer than root CA") 175 | } 176 | 177 | template := certify.Certificate{ 178 | Subject: pkix.Name{ 179 | Organization: []string{org}, 180 | CommonName: newCN, 181 | }, 182 | NotBefore: time.Now(), 183 | NotAfter: expiry, 184 | IsCA: true, 185 | Parent: parent, 186 | ParentPrivateKey: parentKey, 187 | } 188 | 189 | cert, err := template.GetCertificate(pkey) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | err = store(cert.String(), caInterPath) 195 | if err == nil { 196 | fmt.Println("Certificate file generated", caInterPath) 197 | } 198 | 199 | return err 200 | } 201 | 202 | // getFilename returns path based on given args 203 | // first it will check dnsnames, if nil, then check iplist, if iplist nil too 204 | // it will check common name 205 | func getFilename(args []string, key bool) string { 206 | iplist, dnsnames, cn, _, _, _, _ := parseArgs(args) 207 | 208 | var ext string 209 | var path string 210 | 211 | if key { 212 | ext = "-key.pem" 213 | } else { 214 | ext = ".pem" 215 | } 216 | 217 | if len(dnsnames) != 0 { 218 | path = fmt.Sprintf("%s%s", dnsnames[0], ext) 219 | } else if len(iplist) != 0 { 220 | path = fmt.Sprintf("%s%s", iplist[0], ext) 221 | } else { 222 | path = fmt.Sprintf("%s%s", cn, ext) 223 | } 224 | 225 | return path 226 | } 227 | 228 | func getCAPrivateKey(path string) (*ecdsa.PrivateKey, error) { 229 | pkey, err := readPrivateKeyFile(path) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | return pkey, nil 235 | } 236 | 237 | func readPrivateKeyFile(path string) (*ecdsa.PrivateKey, error) { 238 | f, err := os.ReadFile(path) 239 | if err != nil { 240 | return nil, err 241 | } 242 | 243 | pkey, err := certify.ParsePrivateKey(f) 244 | if err != nil { 245 | return nil, err 246 | } 247 | 248 | return pkey, nil 249 | } 250 | 251 | func getCACert(path string) (*x509.Certificate, error) { 252 | c, err := readCertificateFile(path) 253 | if err != nil { 254 | return nil, err 255 | } 256 | 257 | return c, nil 258 | } 259 | 260 | func readCRLFile(path string) (*x509.RevocationList, error) { 261 | f, err := os.ReadFile(path) 262 | if err != nil { 263 | return nil, err 264 | } 265 | 266 | return certify.ParseCRL(f) 267 | } 268 | 269 | func readCertificateFile(path string) (*x509.Certificate, error) { 270 | f, err := os.ReadFile(path) 271 | if err != nil { 272 | return nil, err 273 | } 274 | 275 | return certify.ParseCertificate(f) 276 | } 277 | 278 | func getPfxData(pkey, cert, caCert, password string) ([]byte, error) { 279 | p, err := readPrivateKeyFile(pkey) 280 | if err != nil { 281 | return nil, err 282 | } 283 | 284 | c, err := readCertificateFile(cert) 285 | if err != nil { 286 | return nil, err 287 | } 288 | 289 | ca, err := readCertificateFile(caCert) 290 | if err != nil { 291 | return nil, err 292 | } 293 | 294 | pfxData, err := pkcs12.Encode( 295 | rand.Reader, p, c, []*x509.Certificate{ca}, password, 296 | ) 297 | 298 | if err != nil { 299 | return nil, err 300 | } 301 | 302 | return pfxData, nil 303 | } 304 | 305 | // comparePublicKey returns an error if public key from certificate and public 306 | // key from private key doesn't match 307 | func comparePublicKey(key *ecdsa.PrivateKey, cert *x509.Certificate) (string, string, error) { 308 | pubkey, err := certify.GetPublicKey(&key.PublicKey) 309 | if err != nil { 310 | return "", "", err 311 | } 312 | 313 | pubcert, err := certify.GetPublicKey(cert.PublicKey) 314 | if err != nil { 315 | return "", "", err 316 | } 317 | 318 | if pubkey != pubcert { 319 | return "", "", errors.New("private key doesn't match with given certificate") 320 | } 321 | 322 | return pubkey, pubcert, nil 323 | } 324 | 325 | func matcher(key, cert string) (string, string, error) { 326 | k, err := readPrivateKeyFile(key) 327 | if err != nil { 328 | return "", "", err 329 | } 330 | 331 | c, err := readCertificateFile(cert) 332 | if err != nil { 333 | return "", "", err 334 | } 335 | 336 | return comparePublicKey(k, c) 337 | } 338 | 339 | // parseAltNames returns parsed net.IP, DNS, Common Name, Organization, expiry date, EKU and crl.Nextupdate in slice format 340 | func parseArgs(args []string) ([]net.IP, []string, string, string, time.Time, []x509.ExtKeyUsage, time.Time) { 341 | var iplist []net.IP 342 | var dnsnames []string 343 | var cn, organization string 344 | var expiry time.Time 345 | var crlNextUpdate time.Time 346 | var ekus []x509.ExtKeyUsage 347 | 348 | for _, arg := range args[1:] { 349 | if net.ParseIP(arg) != nil { 350 | iplist = append(iplist, net.ParseIP(arg)) 351 | } else if strings.Contains(arg, "cn:") { 352 | if cn == "" { 353 | cn = parseString(arg) 354 | } 355 | } else if strings.Contains(arg, "o:") { 356 | if organization == "" { 357 | organization = parseString(arg) 358 | } 359 | } else if strings.Contains(arg, "expiry:") { 360 | expiry = parseExpiry(arg) 361 | } else if strings.Contains(arg, "eku:") { 362 | ekus = parseEKU(arg) 363 | } else if strings.Contains(arg, "crl-nextupdate:") { 364 | crlNextUpdate = parseExpiry(arg) 365 | } else { 366 | dnsnames = append(dnsnames, arg) 367 | } 368 | } 369 | 370 | if expiry.IsZero() { 371 | expiry = parseExpiry("expiry:") 372 | } 373 | 374 | if crlNextUpdate.IsZero() { 375 | crlNextUpdate = parseExpiry("crl-nextupdate:10d") 376 | } 377 | 378 | if cn == "" { 379 | cn = "certify" 380 | } 381 | 382 | if organization == "" { 383 | organization = "certify" 384 | } 385 | 386 | if len(ekus) == 0 { 387 | ekus = []x509.ExtKeyUsage{ 388 | x509.ExtKeyUsageClientAuth, 389 | x509.ExtKeyUsageServerAuth, 390 | } 391 | } 392 | 393 | return iplist, dnsnames, cn, organization, expiry, ekus, crlNextUpdate 394 | } 395 | 396 | func parseString(ss string) string { 397 | s := strings.Split(ss, ":") 398 | if s[1] != "" { 399 | return s[1] 400 | } 401 | 402 | return "certify" 403 | } 404 | 405 | func parseEKU(ekus string) []x509.ExtKeyUsage { 406 | var ExtKeyUsage []x509.ExtKeyUsage 407 | 408 | parsedEku := strings.Split(strings.TrimLeft(ekus, "eku:"), ",") 409 | 410 | for _, eku := range parsedEku { 411 | e := strings.ToLower(eku) 412 | if e == "serverauth" { 413 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageServerAuth) 414 | } else if e == "clientauth" { 415 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageClientAuth) 416 | } else if e == "any" { 417 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageAny) 418 | } else if e == "codesigning" { 419 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageCodeSigning) 420 | } else if e == "emailprotection" { 421 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageEmailProtection) 422 | } else if e == "ipsecendsystem" { 423 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageIPSECEndSystem) 424 | } else if e == "ipsectunnel" { 425 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageIPSECTunnel) 426 | } else if e == "ipsecuser" { 427 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageIPSECUser) 428 | } else if e == "timestamping" { 429 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageTimeStamping) 430 | } else if e == "ocspsigning" { 431 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageOCSPSigning) 432 | } else if e == "microsoftservergatedcrypto" { 433 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageMicrosoftServerGatedCrypto) 434 | } else if e == "netscapeservergatedcrypto" { 435 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageNetscapeServerGatedCrypto) 436 | } else if e == "microsoftcommercialcodesigning" { 437 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageMicrosoftCommercialCodeSigning) 438 | } else if e == "microsoftkernelcodesigning" { 439 | ExtKeyUsage = append(ExtKeyUsage, x509.ExtKeyUsageMicrosoftKernelCodeSigning) 440 | } 441 | } 442 | 443 | return ExtKeyUsage 444 | } 445 | 446 | func parseExpiry(expiry string) time.Time { 447 | format := make(map[string]time.Duration) 448 | format["s"] = time.Second 449 | format["m"] = time.Minute 450 | format["h"] = time.Hour 451 | format["d"] = 24 * time.Hour 452 | 453 | s := strings.Split(expiry, ":") 454 | 455 | for f, d := range format { 456 | if strings.HasSuffix(s[1], f) { 457 | i, err := strconv.Atoi(strings.TrimSuffix(s[1], f)) 458 | if err != nil { 459 | return time.Now().Add(8766 * time.Hour) 460 | } 461 | 462 | return time.Now().Add(time.Duration(i) * d) 463 | } 464 | } 465 | 466 | return time.Now().Add(8766 * time.Hour) 467 | } 468 | 469 | // store write content to given path and returns an error 470 | func store(c, path string) error { 471 | if isExist(path) { 472 | return fmt.Errorf("file %s already exists", path) 473 | } 474 | 475 | return os.WriteFile(path, []byte(c), 0640) 476 | } 477 | 478 | func isExist(path string) bool { 479 | _, err := os.Stat(path) 480 | 481 | return !errors.Is(err, os.ErrNotExist) 482 | } 483 | 484 | func parseTLSVersion(args []string) uint16 { 485 | for _, arg := range args[1:] { 486 | if strings.Contains(arg, "tlsver:") { 487 | ver := strings.Split(arg, ":")[1] 488 | return getTLSVersion(ver) 489 | } 490 | } 491 | 492 | log.Println("use default settings ...") 493 | return tls.VersionTLS12 494 | } 495 | 496 | func parseInsecureArg(args []string) bool { 497 | for _, arg := range args[1:] { 498 | if strings.Contains(arg, "insecure") { 499 | return true 500 | } 501 | } 502 | 503 | return false 504 | } 505 | 506 | func parseCAarg(args []string) string { 507 | for _, arg := range args[1:] { 508 | if strings.Contains(arg, "with-ca:") { 509 | // return ca path 510 | return strings.Split(arg, ":")[1] 511 | } 512 | } 513 | 514 | return "" 515 | } 516 | 517 | func getTLSVersion(ver string) uint16 { 518 | if ver == "1.0" { 519 | return tls.VersionTLS10 520 | } else if ver == "1.1" { 521 | return tls.VersionTLS11 522 | } else if ver == "1.2" { 523 | return tls.VersionTLS12 524 | } else if ver == "1.3" { 525 | return tls.VersionTLS13 526 | } 527 | 528 | return tls.VersionTLS12 529 | } 530 | 531 | func tlsDial(host string, tlsConfig *tls.Config) (*x509.Certificate, error) { 532 | dialer := &net.Dialer{ 533 | Timeout: 5 * time.Second, 534 | } 535 | 536 | net, err := tls.DialWithDialer(dialer, "tcp", host, tlsConfig) 537 | if err != nil { 538 | return nil, err 539 | } 540 | defer net.Close() 541 | 542 | certChain := net.ConnectionState().PeerCertificates 543 | cert := certChain[0] 544 | 545 | return cert, nil 546 | } 547 | -------------------------------------------------------------------------------- /cmd/certify/helper_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "net" 7 | "os" 8 | "reflect" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestGeneratePrivateKeyAndCA(t *testing.T) { 14 | pkey, err := generatePrivateKey(caKeyPath) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | if _, err := generateCA(pkey.PrivateKey, []string{"cn:local"}, caPath); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | t.Run("Test create certificate", func(t *testing.T) { 24 | cpkey, err := generatePrivateKey("/tmp/pkey.pem") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | if err := generateCert(cpkey.PrivateKey, []string{"127.0.0.1", "local.dev", "cn:server", "expiry:1d", "eku:serverauth"}); err != nil { 30 | t.Fatal(err) 31 | } 32 | }) 33 | 34 | t.Run("Test create intermediate certificate with attribute", func(t *testing.T) { 35 | ikey, err := generatePrivateKey(caInterKeyPath) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if err := generateIntermediateCert(ikey.PrivateKey, []string{"cn:nothinux", "expiry:100d"}); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | t.Cleanup(func() { 45 | cleanupfiles([]string{ 46 | caInterPath, caInterKeyPath, 47 | }) 48 | }) 49 | }) 50 | 51 | t.Run("Test create intermediate certificate and certificate", func(t *testing.T) { 52 | ikey, err := generatePrivateKey(caInterKeyPath) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | if err := generateIntermediateCert(ikey.PrivateKey, []string{""}); err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | pkey, err := generatePrivateKey("/tmp/pkey-2.pem") 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | if err := generateCert(pkey.PrivateKey, []string{"127.0.0.1", "local-2.dev", "cn:server-2", "expiry:1d", "eku:serverauth"}); err != nil { 67 | t.Fatal(err) 68 | } 69 | }) 70 | 71 | t.Run("Test export certificate to pkcs12", func(t *testing.T) { 72 | _, err := getPfxData("/tmp/pkey.pem", "local.dev.pem", "ca-cert.pem", "p4ssw0rd") 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | }) 77 | 78 | t.Cleanup(func() { 79 | cleanupfiles([]string{ 80 | caPath, caKeyPath, caInterPath, caInterKeyPath, caKeyPath, "local.dev.pem", "/tmp/pkey.pem", "local-2.dev.pem", "/tmp/pkey-2.pem", 81 | }) 82 | }) 83 | } 84 | 85 | func cleanupfiles(paths []string) { 86 | for _, path := range paths { 87 | os.Remove(path) 88 | } 89 | } 90 | 91 | func TestMatcher(t *testing.T) { 92 | t.Run("Test valid certificate and private key", func(t *testing.T) { 93 | pubkey, privkey, err := matcher("testdata/ca-key.pem", "testdata/ca-cert.pem") 94 | 95 | if err != nil { 96 | t.Fatalf("the private key and certificate must be match\n%v\n%v", pubkey, privkey) 97 | } 98 | }) 99 | t.Run("Test invalid certificate and private key path", func(t *testing.T) { 100 | _, _, err := matcher("ca-key.pem", "ca-cert.pem") 101 | 102 | if err == nil { 103 | t.Fatalf("the matcher must be error, because the path is invalid") 104 | } 105 | }) 106 | } 107 | 108 | func TestParseArgs(t *testing.T) { 109 | tests := []struct { 110 | Name string 111 | Args []string 112 | expectedIP []net.IP 113 | expectedDNS []string 114 | expectedCN string 115 | expectedOrganization string 116 | expectedExpiry time.Time 117 | expectedEku []x509.ExtKeyUsage 118 | expectedNextUpdate time.Time 119 | }{ 120 | { 121 | Name: "Test with ip and dns names", 122 | Args: []string{"certify", "127.0.0.1", "172.16.0.1", "example.com"}, 123 | expectedIP: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("172.16.0.1")}, 124 | expectedDNS: []string{"example.com"}, 125 | expectedCN: "certify", 126 | expectedOrganization: "certify", 127 | }, 128 | { 129 | Name: "Test only dns names", 130 | Args: []string{"certify", "example.com"}, 131 | expectedDNS: []string{"example.com"}, 132 | expectedCN: "certify", 133 | expectedOrganization: "certify", 134 | }, 135 | { 136 | Name: "test with ip, dns and common name", 137 | Args: []string{"certify", "cn:manager", "172.16.0.1", "example.com"}, 138 | expectedIP: []net.IP{net.ParseIP("172.16.0.1")}, 139 | expectedDNS: []string{"example.com"}, 140 | expectedCN: "manager", 141 | expectedOrganization: "certify", 142 | }, 143 | { 144 | Name: "test with multiple ip", 145 | Args: []string{"certify", "172.16.0.1", "192.168.0.1"}, 146 | expectedIP: []net.IP{net.ParseIP("172.16.0.1"), net.ParseIP("192.168.0.1")}, 147 | expectedCN: "certify", 148 | expectedOrganization: "certify", 149 | }, 150 | { 151 | Name: "test with multiple dns", 152 | Args: []string{"certify", "sub.example.com", "srv.example.com", "example.com"}, 153 | expectedDNS: []string{"sub.example.com", "srv.example.com", "example.com"}, 154 | expectedCN: "certify", 155 | expectedOrganization: "certify", 156 | }, 157 | { 158 | Name: "test with common name", 159 | Args: []string{"certify", "cn:example.com"}, 160 | expectedCN: "example.com", 161 | expectedOrganization: "certify", 162 | }, 163 | { 164 | Name: "test with organization", 165 | Args: []string{"certify", "o:nothinux"}, 166 | expectedCN: "certify", 167 | expectedOrganization: "nothinux", 168 | }, 169 | { 170 | Name: "test with common name and organization", 171 | Args: []string{"certify", "cn:server", "o:nothinux"}, 172 | expectedCN: "server", 173 | expectedOrganization: "nothinux", 174 | }, 175 | { 176 | Name: "test with multiple common name", 177 | Args: []string{"certify", "cn:srv.example.com", "cn:example.com"}, 178 | expectedCN: "srv.example.com", 179 | expectedOrganization: "certify", 180 | }, 181 | { 182 | Name: "Test with expiry 12 hours", 183 | Args: []string{"certify", "sub.example.local", "expiry:12h"}, 184 | expectedExpiry: time.Now().Add(12 * time.Hour), 185 | expectedCN: "certify", 186 | expectedOrganization: "certify", 187 | }, 188 | { 189 | Name: "Test with expiry 30 days", 190 | Args: []string{"certify", "cn:server", "expiry:30d"}, 191 | expectedExpiry: time.Now().Add(30 * 24 * time.Hour), 192 | expectedCN: "server", 193 | expectedOrganization: "certify", 194 | }, 195 | { 196 | Name: "Test with custom ekus", 197 | Args: []string{"certify", "cn:client", "eku:serverauth,codesigning"}, 198 | expectedEku: []x509.ExtKeyUsage{ 199 | x509.ExtKeyUsageServerAuth, 200 | x509.ExtKeyUsageCodeSigning, 201 | }, 202 | expectedCN: "client", 203 | expectedOrganization: "certify", 204 | }, 205 | { 206 | Name: "Test with crl-nextupdate 100d", 207 | Args: []string{"certify", "crl-nextupdate:100d"}, 208 | expectedNextUpdate: time.Now().Add(2400 * time.Hour), 209 | expectedCN: "certify", 210 | expectedOrganization: "certify", 211 | }, 212 | } 213 | 214 | for _, tt := range tests { 215 | t.Run(tt.Name, func(t *testing.T) { 216 | ips, dns, cn, o, expiry, ekus, nextUpdate := parseArgs(tt.Args) 217 | 218 | if len(tt.expectedIP) != 0 { 219 | for i, ip := range ips { 220 | if !ip.Equal(tt.expectedIP[i]) { 221 | t.Fatalf("got %v, want %v", ip, tt.expectedIP[i]) 222 | } 223 | } 224 | } 225 | 226 | if len(tt.expectedDNS) != 0 { 227 | for i, d := range dns { 228 | if d != tt.expectedDNS[i] { 229 | t.Fatalf("got %v, want %v", d, tt.expectedDNS[i]) 230 | } 231 | } 232 | } 233 | 234 | if cn != tt.expectedCN { 235 | t.Fatalf("got %v, want %v", cn, tt.expectedCN) 236 | } 237 | 238 | if o != tt.expectedOrganization { 239 | t.Fatalf("got %v, want %v", o, tt.expectedOrganization) 240 | } 241 | 242 | if !tt.expectedExpiry.IsZero() { 243 | if expiry.Unix() != tt.expectedExpiry.Unix() { 244 | t.Fatalf("got %v, want %v", expiry.Unix(), tt.expectedExpiry.Unix()) 245 | } 246 | } else { 247 | if expiry.Unix() != time.Now().Add(8766*time.Hour).Unix() { 248 | t.Fatalf("got %v, want %v", expiry.Unix(), time.Now().Add(8766*time.Hour).Unix()) 249 | } 250 | } 251 | 252 | if len(tt.expectedEku) != 0 { 253 | if !reflect.DeepEqual(ekus, tt.expectedEku) { 254 | t.Fatalf("fot %v, want %v", ekus, tt.expectedEku) 255 | } 256 | } else { 257 | defaultEku := []x509.ExtKeyUsage{ 258 | x509.ExtKeyUsageClientAuth, 259 | x509.ExtKeyUsageServerAuth, 260 | } 261 | 262 | if !reflect.DeepEqual(ekus, defaultEku) { 263 | t.Fatalf("got %v, want %v", ekus, defaultEku) 264 | } 265 | } 266 | 267 | if !tt.expectedNextUpdate.IsZero() { 268 | if nextUpdate.Unix() != tt.expectedNextUpdate.Unix() { 269 | t.Fatalf("got %v, want %v", nextUpdate.Unix(), tt.expectedNextUpdate.Unix()) 270 | } 271 | } else { 272 | if nextUpdate.Unix() != time.Now().Add(240*time.Hour).Unix() { 273 | t.Fatalf("got %v, want %v", nextUpdate.Unix(), time.Now().Add(240*time.Hour).Unix()) 274 | } 275 | } 276 | }) 277 | } 278 | } 279 | 280 | func TestGetFilename(t *testing.T) { 281 | tests := []struct { 282 | Name string 283 | Args []string 284 | Key bool 285 | expectedPath string 286 | }{ 287 | { 288 | Name: "private key with ip", 289 | Args: []string{"certify", "127.0.0.1"}, 290 | Key: true, 291 | expectedPath: "127.0.0.1-key.pem", 292 | }, 293 | { 294 | Name: "private key with multiple ip", 295 | Args: []string{"certify", "127.0.0.1", "182.0.0.1"}, 296 | Key: true, 297 | expectedPath: "127.0.0.1-key.pem", 298 | }, 299 | { 300 | Name: "certificate with multiple ip", 301 | Args: []string{"certify", "127.0.0.1", "182.0.0.1"}, 302 | Key: false, 303 | expectedPath: "127.0.0.1.pem", 304 | }, 305 | { 306 | Name: "certificate with dns", 307 | Args: []string{"certify", "example.com"}, 308 | Key: false, 309 | expectedPath: "example.com.pem", 310 | }, 311 | { 312 | Name: "certificate with dns and ip", 313 | Args: []string{"certify", "example.com", "127.0.0.1"}, 314 | Key: false, 315 | expectedPath: "example.com.pem", 316 | }, 317 | { 318 | Name: "certificate with ip and dns", 319 | Args: []string{"certify", "127.0.0.1", "example.com"}, 320 | Key: false, 321 | expectedPath: "example.com.pem", 322 | }, 323 | { 324 | Name: "certificate with ip and dns and common name", 325 | Args: []string{"certify", "127.0.0.1", "example.com", "cn:web"}, 326 | Key: false, 327 | expectedPath: "example.com.pem", 328 | }, 329 | { 330 | Name: "certificate with common name and ip", 331 | Args: []string{"certify", "cn:web", "127.0.0.1"}, 332 | Key: false, 333 | expectedPath: "127.0.0.1.pem", 334 | }, 335 | { 336 | Name: "certificate with common name", 337 | Args: []string{"certify", "cn:web"}, 338 | Key: false, 339 | expectedPath: "web.pem", 340 | }, 341 | } 342 | 343 | for _, tt := range tests { 344 | t.Run(tt.Name, func(t *testing.T) { 345 | path := getFilename(tt.Args, tt.Key) 346 | 347 | if path != tt.expectedPath { 348 | t.Fatalf("got %v, want %v", path, tt.expectedPath) 349 | } 350 | }) 351 | } 352 | } 353 | 354 | func TestParseString(t *testing.T) { 355 | tests := []struct { 356 | Name string 357 | CN string 358 | ExpectedCN string 359 | Organization string 360 | ExpectedOrganization string 361 | }{ 362 | { 363 | Name: "Test valid common name", 364 | CN: "cn:server", 365 | ExpectedCN: "server", 366 | }, 367 | { 368 | Name: "Test empty common name", 369 | CN: "cn:", 370 | ExpectedCN: "certify", 371 | }, 372 | { 373 | Name: "Test valid organization", 374 | Organization: "o:nothinux", 375 | ExpectedOrganization: "nothinux", 376 | }, 377 | } 378 | 379 | for _, tt := range tests { 380 | t.Run(tt.Name, func(t *testing.T) { 381 | if tt.CN != "" { 382 | parsedCN := parseString(tt.CN) 383 | 384 | if parsedCN != tt.ExpectedCN { 385 | t.Fatalf("got %v, want %v", parsedCN, tt.ExpectedCN) 386 | } 387 | } 388 | 389 | if tt.Organization != "" { 390 | parsedOrganization := parseString(tt.Organization) 391 | 392 | if parsedOrganization != tt.ExpectedOrganization { 393 | t.Fatalf("got %v, want %v", parsedOrganization, tt.ExpectedOrganization) 394 | } 395 | } 396 | }) 397 | } 398 | } 399 | 400 | func TestParseEKU(t *testing.T) { 401 | tests := []struct { 402 | Name string 403 | Eku string 404 | ExpectedEku []x509.ExtKeyUsage 405 | }{ 406 | { 407 | Name: "Test eku serverauth", 408 | Eku: "eku:serverAuth", 409 | ExpectedEku: []x509.ExtKeyUsage{ 410 | x509.ExtKeyUsageServerAuth, 411 | }, 412 | }, 413 | { 414 | Name: "Test eku client auth and code signing", 415 | Eku: "eku:clientAuth,codesigning", 416 | ExpectedEku: []x509.ExtKeyUsage{ 417 | x509.ExtKeyUsageClientAuth, 418 | x509.ExtKeyUsageCodeSigning, 419 | }, 420 | }, 421 | { 422 | Name: "Test all eku", 423 | Eku: "eku:serverauth,clientauth,any,codesigning,emailprotection,ipsecendsystem,ipsectunnel,ipsecuser,timestamping,ocspsigning,microsoftservergatedcrypto,netscapeservergatedcrypto,microsoftcommercialcodesigning,microsoftkernelcodesigning", 424 | ExpectedEku: []x509.ExtKeyUsage{ 425 | x509.ExtKeyUsageServerAuth, 426 | x509.ExtKeyUsageClientAuth, 427 | x509.ExtKeyUsageAny, 428 | x509.ExtKeyUsageCodeSigning, 429 | x509.ExtKeyUsageEmailProtection, 430 | x509.ExtKeyUsageIPSECEndSystem, 431 | x509.ExtKeyUsageIPSECTunnel, 432 | x509.ExtKeyUsageIPSECUser, 433 | x509.ExtKeyUsageTimeStamping, 434 | x509.ExtKeyUsageOCSPSigning, 435 | x509.ExtKeyUsageMicrosoftServerGatedCrypto, 436 | x509.ExtKeyUsageNetscapeServerGatedCrypto, 437 | x509.ExtKeyUsageMicrosoftCommercialCodeSigning, 438 | x509.ExtKeyUsageMicrosoftKernelCodeSigning, 439 | }, 440 | }, 441 | { 442 | Name: "Test empty eku", 443 | Eku: "eku:", 444 | ExpectedEku: []x509.ExtKeyUsage{}, 445 | }, 446 | } 447 | 448 | for _, tt := range tests { 449 | t.Run(tt.Name, func(t *testing.T) { 450 | parsedEku := parseEKU(tt.Eku) 451 | 452 | if len(parsedEku) == 0 { 453 | if len(parsedEku) != len(tt.ExpectedEku) { 454 | t.Fatalf("got %v, want %v", len(parsedEku), len(tt.ExpectedEku)) 455 | } 456 | return 457 | } 458 | 459 | if !reflect.DeepEqual(parsedEku, tt.ExpectedEku) { 460 | t.Fatalf("got %v, want %v", parsedEku, tt.ExpectedEku) 461 | } 462 | }) 463 | } 464 | } 465 | 466 | func TestParseExpiry(t *testing.T) { 467 | tests := []struct { 468 | Name string 469 | Time string 470 | ExpectedTime time.Time 471 | }{ 472 | { 473 | Name: "Test 5 seconds", 474 | Time: "expiry:5s", 475 | ExpectedTime: time.Now().Add(5 * time.Second), 476 | }, 477 | { 478 | Name: "Test 10 minutes", 479 | Time: "expiry:10m", 480 | ExpectedTime: time.Now().Add(10 * time.Minute), 481 | }, 482 | { 483 | Name: "Test 5 hours", 484 | Time: "expiry:5h", 485 | ExpectedTime: time.Now().Add(5 * time.Hour), 486 | }, 487 | { 488 | Name: "Test 7 days", 489 | Time: "expiry:7d", 490 | ExpectedTime: time.Now().Add(7 * 24 * time.Hour), 491 | }, 492 | { 493 | Name: "Test 2 years", 494 | Time: "expiry:2y", 495 | ExpectedTime: time.Now().Add(8766 * time.Hour), 496 | }, 497 | { 498 | Name: "Test no time", 499 | Time: "expiry:", 500 | ExpectedTime: time.Now().Add(8766 * time.Hour), 501 | }, 502 | { 503 | Name: "Test wrong format", 504 | Time: "expiry:od", 505 | ExpectedTime: time.Now().Add(8766 * time.Hour), 506 | }, 507 | } 508 | 509 | for _, tt := range tests { 510 | t.Run(tt.Name, func(t *testing.T) { 511 | result := parseExpiry(tt.Time) 512 | 513 | if result.Unix() != tt.ExpectedTime.Unix() { 514 | t.Fatalf("got %v, want %v", result.Unix(), tt.ExpectedTime.Unix()) 515 | } 516 | }) 517 | 518 | } 519 | } 520 | 521 | func TestIsExist(t *testing.T) { 522 | t.Run("Test if path is exists", func(t *testing.T) { 523 | if err := os.Mkdir("/tmp/randpath", 0755); err != nil { 524 | t.Fatal(err) 525 | } 526 | 527 | if !isExist("/tmp/randpath") { 528 | t.Fatalf("path must be exists") 529 | } 530 | 531 | if err := os.Remove("/tmp/randpath"); err != nil { 532 | t.Fatal(err) 533 | } 534 | }) 535 | t.Run("Test if path is not exists", func(t *testing.T) { 536 | if isExist("/tmp/xxx/yyy/zzz") { 537 | t.Fatalf("path must be doesn't exists") 538 | } 539 | }) 540 | } 541 | 542 | func TestTlsDial(t *testing.T) { 543 | t.Run("Test valid host", func(t *testing.T) { 544 | _, err := tlsDial("google.com:443", &tls.Config{}) 545 | if err != nil { 546 | t.Fatalf("the dial must be success %v", err) 547 | } 548 | }) 549 | 550 | t.Run("Test Invalid host", func(t *testing.T) { 551 | _, err := tlsDial("google.com", &tls.Config{}) 552 | if err == nil { 553 | t.Fatalf("the dial must be error") 554 | } 555 | }) 556 | } 557 | 558 | func TestParseTLSVersion(t *testing.T) { 559 | tests := []struct { 560 | Name string 561 | Args []string 562 | ExpectedConfig uint16 563 | ExpectedErr error 564 | }{ 565 | { 566 | Name: "Test using tls version 1.0", 567 | Args: []string{"certify", "-connect", "google.com:443", "tlsver:1.0"}, 568 | ExpectedConfig: tls.VersionTLS10, 569 | ExpectedErr: nil, 570 | }, 571 | { 572 | Name: "Test using tls version 1.1", 573 | Args: []string{"certify", "-connect", "google.com:443", "tlsver:1.1"}, 574 | ExpectedConfig: tls.VersionTLS11, 575 | ExpectedErr: nil, 576 | }, 577 | { 578 | Name: "Test using tls version 1.2", 579 | Args: []string{"certify", "-connect", "google.com:443", "tlsver:1.2"}, 580 | ExpectedConfig: tls.VersionTLS12, 581 | ExpectedErr: nil, 582 | }, 583 | { 584 | Name: "Test using tls version 1.3", 585 | Args: []string{"certify", "-connect", "google.com:443", "tlsver:1.3"}, 586 | ExpectedConfig: tls.VersionTLS13, 587 | ExpectedErr: nil, 588 | }, 589 | { 590 | Name: "Test using not available tls version", 591 | Args: []string{"certify", "-connect", "google.com:443", "tlsver:1.4"}, 592 | ExpectedConfig: tls.VersionTLS12, 593 | ExpectedErr: nil, 594 | }, 595 | { 596 | Name: "Test using not available tls version", 597 | Args: []string{"certify", "-connect", "google.com:443", "tlsver:sslv3"}, 598 | ExpectedConfig: tls.VersionTLS12, 599 | ExpectedErr: nil, 600 | }, 601 | { 602 | Name: "Test without provide tls version", 603 | Args: []string{"certify", "-connect", "google.com:443"}, 604 | ExpectedConfig: tls.VersionTLS12, 605 | ExpectedErr: nil, 606 | }, 607 | } 608 | 609 | for _, tt := range tests { 610 | t.Run(tt.Name, func(t *testing.T) { 611 | config := parseTLSVersion(tt.Args) 612 | 613 | if !reflect.DeepEqual(config, tt.ExpectedConfig) { 614 | t.Fatalf("got %v, want %v", config, tt.ExpectedConfig) 615 | } 616 | }) 617 | } 618 | } 619 | 620 | func TestParseInsecureArg(t *testing.T) { 621 | tests := []struct { 622 | Name string 623 | Args []string 624 | Expected bool 625 | }{ 626 | { 627 | Name: "Test using insecure arg enabled", 628 | Args: []string{"certify", "-connect", "google.com:443", "insecure"}, 629 | Expected: true, 630 | }, 631 | { 632 | Name: "Test without insecure flag", 633 | Args: []string{"certify", "-connect", "google.com:443", "tlsver:1.1"}, 634 | Expected: false, 635 | }, 636 | } 637 | 638 | for _, tt := range tests { 639 | t.Run(tt.Name, func(t *testing.T) { 640 | config := parseInsecureArg(tt.Args) 641 | 642 | if !reflect.DeepEqual(config, tt.Expected) { 643 | t.Fatalf("got %v, want %v", config, tt.Expected) 644 | } 645 | }) 646 | } 647 | } 648 | 649 | func TestParseCAArg(t *testing.T) { 650 | tests := []struct { 651 | Name string 652 | Args []string 653 | Expected string 654 | }{ 655 | { 656 | Name: "Test with ca arg", 657 | Args: []string{"certify", "-connect", "google.com:443", "with-ca:/tmp/ca-cert.pem"}, 658 | Expected: "/tmp/ca-cert.pem", 659 | }, 660 | { 661 | Name: "Test with ca arg without value", 662 | Args: []string{"certify", "-connect", "google.com:443", "with-ca:"}, 663 | Expected: "", 664 | }, 665 | { 666 | Name: "Test without ca arg", 667 | Args: []string{"certify", "-connect", "google.com:443"}, 668 | Expected: "", 669 | }, 670 | } 671 | 672 | for _, tt := range tests { 673 | t.Run(tt.Name, func(t *testing.T) { 674 | config := parseCAarg(tt.Args) 675 | 676 | if !reflect.DeepEqual(config, tt.Expected) { 677 | t.Fatalf("got %v, want %v", config, tt.Expected) 678 | } 679 | }) 680 | } 681 | } 682 | -------------------------------------------------------------------------------- /cmd/certify/interactive.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/manifoldco/promptui" 8 | ) 9 | 10 | const ( 11 | optCreateCA = "Create CA" 12 | optCreateIntCert = "Create Intermediate CA" 13 | optCreateCert = "Create Certificate" 14 | ) 15 | 16 | var ( 17 | confirmSignPrompt = promptui.Prompt{Label: "Certificate will be signed with CA or Intermediate CA in current directory", IsConfirm: true} 18 | commonNamePrompt = promptui.Prompt{Label: "Common Name", Default: "Certify"} 19 | organizationPrompt = promptui.Prompt{Label: "Organization", Default: "Certify"} 20 | sanPrompt = promptui.Prompt{Label: "Subject Alternative Names", Default: "Certify"} 21 | startPrompt = promptui.Select{Label: "Certify", Items: []string{optCreateCA, optCreateCert, optCreateIntCert}} 22 | expiryPrompt = promptui.Select{Label: "Expiry", Items: []string{"30d", "90d", "365d", "3650d"}, CursorPos: 2} 23 | ) 24 | 25 | func promptErr(err error) { 26 | if err != nil { 27 | fmt.Printf("%v", err) 28 | os.Exit(1) 29 | } 30 | } 31 | 32 | func setCertifyFlag(flagKey, flagVal string) string { 33 | return fmt.Sprintf("%s:%s", flagKey, flagVal) 34 | } 35 | 36 | func runWizard() error { 37 | _, result, err := startPrompt.Run() 38 | promptErr(err) 39 | 40 | switch result { 41 | case optCreateCA: 42 | cn, err := commonNamePrompt.Run() 43 | promptErr(err) 44 | 45 | og, err := organizationPrompt.Run() 46 | promptErr(err) 47 | 48 | _, expiry, err := expiryPrompt.Run() 49 | promptErr(err) 50 | 51 | if err := initCA([]string{setCertifyFlag("cn", cn), setCertifyFlag("o", og), setCertifyFlag("expiry", expiry)}); err != nil { 52 | return err 53 | } 54 | 55 | case optCreateIntCert: 56 | cn, err := commonNamePrompt.Run() 57 | promptErr(err) 58 | 59 | og, err := organizationPrompt.Run() 60 | promptErr(err) 61 | 62 | _, expiry, err := expiryPrompt.Run() 63 | promptErr(err) 64 | 65 | _, err = confirmSignPrompt.Run() 66 | promptErr(err) 67 | 68 | if err := createIntermediateCertificate([]string{setCertifyFlag("cn", cn), setCertifyFlag("o", og), setCertifyFlag("expiry", expiry)}); err != nil { 69 | return err 70 | } 71 | 72 | case optCreateCert: 73 | cn, err := commonNamePrompt.Run() 74 | promptErr(err) 75 | 76 | og, err := organizationPrompt.Run() 77 | promptErr(err) 78 | 79 | san, err := sanPrompt.Run() 80 | promptErr(err) 81 | 82 | _, expiry, err := expiryPrompt.Run() 83 | promptErr(err) 84 | 85 | _, err = confirmSignPrompt.Run() 86 | promptErr(err) 87 | 88 | if err := createCertificate([]string{ 89 | setCertifyFlag("cn", cn), 90 | setCertifyFlag("o", og), 91 | setCertifyFlag("expiry", expiry), 92 | san, 93 | }); err != nil { 94 | return err 95 | } 96 | } 97 | 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /cmd/certify/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "syscall" 9 | 10 | "golang.org/x/term" 11 | ) 12 | 13 | var usage = ` _ _ ___ 14 | ___ ___ ___| |_|_| _|_ _ 15 | | _| -_| _| _| | _| | | 16 | |___|___|_| |_| |_|_| |_ | 17 | |___| Certify v%s 18 | 19 | Usage of certify: 20 | certify [flag] [ip-or-dns-san] [cn:default certify] [o: default certify] [eku:default serverAuth,clientAuth] [expiry:default 8766h s,m,h,d] 21 | 22 | $ certify server.local 172.17.0.1 cn:web-server eku:serverAuth expiry:1d 23 | $ certify -init cn:web-server o:nothinux crl-nextupdate:100d 24 | 25 | Flags: 26 | -init 27 | Initialize new root CA Certificate and Key 28 | -intermediate 29 | Generate intermediate certificate 30 | -read 31 | Read certificate information from file or stdin 32 | -read-crl 33 | Read certificate revocation list from file or stdin 34 | -connect 35 | Show certificate information from remote host, use tlsver to set spesific tls version 36 | -export-p12 37 | Generate client.p12 pem file containing certificate, private key and ca certificate 38 | -match 39 | Verify cert-key.pem and cert.pem has same public key 40 | -interactive 41 | Run certify interactively 42 | -revoke 43 | Revoke certificate, the certificate will be added to CRL 44 | -verify-crl 45 | Check if the certificate was revoked 46 | -version 47 | print certify version 48 | ` 49 | 50 | var ( 51 | caPath = "ca-cert.pem" 52 | caKeyPath = "ca-key.pem" 53 | caCRLPath = "ca-crl.pem" 54 | caInterPath = "ca-intermediate.pem" 55 | caInterKeyPath = "ca-intermediate-key.pem" 56 | Version = "No version provided" 57 | ) 58 | 59 | func main() { 60 | if err := runMain(); err != nil { 61 | log.Fatal(err) 62 | } 63 | } 64 | 65 | func runMain() error { 66 | var ( 67 | initialize = flag.Bool("init", false, "initialize new root CA Certificate and Key") 68 | intermediate = flag.Bool("intermediate", false, "create intermediate certificate") 69 | read = flag.Bool("read", false, "read information from certificate") 70 | readcrl = flag.Bool("read-crl", false, "read information from certificate revocation list") 71 | match = flag.Bool("match", false, "check if private key match with certificate") 72 | ver = flag.Bool("version", false, "see program version") 73 | connect = flag.Bool("connect", false, "show information about certificate on remote host") 74 | epkcs12 = flag.Bool("export-p12", false, "export certificate and key to pkcs12 format") 75 | interactive = flag.Bool("interactive", false, "run certify interactively") 76 | revoke = flag.Bool("revoke", false, "revoke certificate, the certificate will be added to CRL") 77 | verifycrl = flag.Bool("verify-crl", false, "check if the certificate was revoked") 78 | ) 79 | 80 | flag.Usage = func() { 81 | showUsage := fmt.Sprintf(usage, Version) 82 | fmt.Fprint(flag.CommandLine.Output(), showUsage) 83 | } 84 | flag.Parse() 85 | 86 | if *ver { 87 | fmt.Printf("Certify version v%s\n", Version) 88 | return nil 89 | } 90 | 91 | if *initialize { 92 | if err := initCA(os.Args); err != nil { 93 | return err 94 | } 95 | return nil 96 | } 97 | 98 | if *read { 99 | cert, err := readCertificate(os.Args, os.Stdin) 100 | if err != nil { 101 | return err 102 | } 103 | fmt.Printf("%s", cert) 104 | return nil 105 | } 106 | 107 | if *readcrl { 108 | crl, err := readCRL(os.Args, os.Stdin) 109 | if err != nil { 110 | return err 111 | } 112 | fmt.Printf("%s", crl) 113 | return nil 114 | } 115 | 116 | if *connect { 117 | result, err := readRemoteCertificate(os.Args) 118 | if err != nil { 119 | return err 120 | } 121 | fmt.Println(result) 122 | return nil 123 | } 124 | 125 | if *match { 126 | if err := matchCertificate(os.Args); err != nil { 127 | return err 128 | } 129 | return nil 130 | } 131 | 132 | if *interactive { 133 | return runWizard() 134 | } 135 | 136 | if *revoke { 137 | _, err := revokeCertificate(os.Args) 138 | return err 139 | } 140 | 141 | if *verifycrl { 142 | return verifyCertificate(os.Args) 143 | } 144 | 145 | if *epkcs12 { 146 | if len(os.Args) < 5 { 147 | fmt.Println("you must provide [key-path] [cert-path] and [ca-path]") 148 | os.Exit(1) 149 | } 150 | 151 | fmt.Print("enter password: ") 152 | bytePass, err := term.ReadPassword(int(syscall.Stdin)) 153 | if err != nil { 154 | log.Fatal(err) 155 | } 156 | 157 | if err := exportCertificate(os.Args, bytePass); err != nil { 158 | return err 159 | } 160 | 161 | return nil 162 | } 163 | 164 | if len(os.Args) < 2 { 165 | fmt.Fprint(flag.CommandLine.Output(), usage) 166 | return fmt.Errorf("you must provide at least two argument") 167 | } 168 | 169 | if !isExist(caPath) || !isExist(caKeyPath) { 170 | return fmt.Errorf("error CA Certificate or Key is not exists, run -init to create it") 171 | } 172 | 173 | if *intermediate { 174 | if err := createIntermediateCertificate(os.Args); err != nil { 175 | return err 176 | } 177 | return nil 178 | } 179 | 180 | if err := createCertificate(os.Args); err != nil { 181 | return err 182 | } 183 | 184 | return nil 185 | } 186 | -------------------------------------------------------------------------------- /cmd/certify/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestRunMain(t *testing.T) { 11 | oldArgs := os.Args 12 | defer func() { os.Args = oldArgs }() 13 | 14 | tests := []struct { 15 | Name string 16 | Args []string 17 | expectedError string 18 | PreRun func() 19 | RunCheck func() 20 | }{ 21 | { 22 | Name: "Test -version flag", 23 | Args: []string{"-version"}, 24 | PreRun: func() {}, 25 | RunCheck: func() {}, 26 | }, 27 | { 28 | Name: "Test -init flag", 29 | Args: []string{"-init"}, 30 | PreRun: func() {}, 31 | RunCheck: func() { 32 | if !isExist(caPath) { 33 | t.Fatalf("%s must be exists", caPath) 34 | } 35 | if !isExist(caKeyPath) { 36 | t.Fatalf("%s must be exists", caKeyPath) 37 | } 38 | 39 | os.Remove(caPath) 40 | os.Remove(caKeyPath) 41 | os.Remove(caCRLPath) 42 | }, 43 | }, 44 | { 45 | Name: "Test -init flag with cn", 46 | Args: []string{"-init", "cn:nothinux"}, 47 | PreRun: func() {}, 48 | RunCheck: func() { 49 | if !isExist(caPath) { 50 | t.Fatalf("%s must be exists", caPath) 51 | } 52 | if !isExist(caKeyPath) { 53 | t.Fatalf("%s must be exists", caKeyPath) 54 | } 55 | 56 | os.Remove(caPath) 57 | os.Remove(caKeyPath) 58 | os.Remove(caCRLPath) 59 | }, 60 | }, 61 | { 62 | Name: "Test -read flag with filename", 63 | Args: []string{"-read", "testdata/nothinux.pem"}, 64 | PreRun: func() {}, 65 | RunCheck: func() { 66 | cert, err := readCertificate([]string{"certify", "-read", "testdata/nothinux.pem"}, nil) 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | if !strings.Contains(cert, "Subject: CN=nothinux, O=certify") { 72 | t.Fatalf("certificate doesn't contain Subject: CN=nothinux, O=certify") 73 | } 74 | }, 75 | }, 76 | { 77 | Name: "Test -read flag with doesn't exist file", 78 | Args: []string{"-read", "nothinux.pem"}, 79 | PreRun: func() {}, 80 | RunCheck: func() {}, 81 | expectedError: "no such file or directory", 82 | }, 83 | { 84 | Name: "Test -connect flag with valid host", 85 | Args: []string{"-connect", "google.com:443"}, 86 | PreRun: func() {}, 87 | RunCheck: func() {}, 88 | }, 89 | { 90 | Name: "Test -connect flag with invalid host", 91 | Args: []string{"-connect", "google.com"}, 92 | PreRun: func() {}, 93 | RunCheck: func() {}, 94 | expectedError: "missing port in address", 95 | }, 96 | { 97 | Name: "Test -match flag", 98 | Args: []string{"-match", "testdata/ca-key.pem", "testdata/ca-cert.pem"}, 99 | PreRun: func() {}, 100 | RunCheck: func() {}, 101 | }, 102 | { 103 | Name: "Test -match flag with wrong certificate", 104 | Args: []string{"-match", "testdata/ca-key.pem", "testdata/nothinux.pem"}, 105 | PreRun: func() {}, 106 | RunCheck: func() {}, 107 | expectedError: "private key doesn't match with given certificate", 108 | }, 109 | { 110 | Name: "Test no argument", 111 | Args: []string{}, 112 | PreRun: func() {}, 113 | RunCheck: func() {}, 114 | expectedError: "you must provide at least two argument", 115 | }, 116 | { 117 | Name: "Test create certificate when existing CA doesn't exists", 118 | Args: []string{"127.0.0.1", "cn:nothinux"}, 119 | PreRun: func() {}, 120 | RunCheck: func() {}, 121 | expectedError: "error CA Certificate or Key is not exists, run -init to create it", 122 | }, 123 | { 124 | Name: "Test create intermediate certificate", 125 | Args: []string{"-intermediate", "cn:nothinux"}, 126 | PreRun: func() { 127 | if err := initCA([]string{"certify", "-init"}); err != nil { 128 | t.Fatal(err) 129 | } 130 | }, 131 | RunCheck: func() { 132 | cert, err := readCertificate([]string{"certify", "-read", caInterPath}, nil) 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | 137 | if !strings.Contains(cert, "Subject: CN=nothinux Intermediate, O=certify") { 138 | t.Fatalf("certificate doesn't contain Subject: CN=nothinux Intermediate, O=certify got %v", cert) 139 | } 140 | 141 | os.Remove(caPath) 142 | os.Remove(caKeyPath) 143 | os.Remove(caCRLPath) 144 | os.Remove(caInterPath) 145 | os.Remove(caInterKeyPath) 146 | }, 147 | }, 148 | { 149 | Name: "Test create certificate", 150 | Args: []string{"127.0.0.1", "nothinux", "cn:nothinux"}, 151 | PreRun: func() { 152 | if err := initCA([]string{"certify", "-init"}); err != nil { 153 | t.Fatal(err) 154 | } 155 | }, 156 | RunCheck: func() { 157 | cert, err := readCertificate([]string{"certify", "-read", "nothinux.pem"}, nil) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | 162 | if !strings.Contains(cert, "Subject: CN=nothinux, O=certify") { 163 | t.Fatalf("certificate doesn't contain Subject: CN=nothinux, O=certify") 164 | } 165 | 166 | os.Remove(caPath) 167 | os.Remove(caKeyPath) 168 | os.Remove(caCRLPath) 169 | os.Remove("nothinux.pem") 170 | os.Remove("nothinux-key.pem") 171 | }, 172 | }, 173 | } 174 | 175 | for _, tt := range tests { 176 | t.Run(tt.Name, func(t *testing.T) { 177 | flag.CommandLine = flag.NewFlagSet("certify", flag.ExitOnError) 178 | 179 | tt.PreRun() 180 | 181 | args := make([]string, len(tt.Args)+1) 182 | 183 | args[0] = "certify" 184 | copy(args[1:], tt.Args) 185 | 186 | os.Args = args 187 | 188 | if err := runMain(); err != nil { 189 | if !strings.Contains(err.Error(), tt.expectedError) { 190 | t.Fatalf("got %v, want %v", err.Error(), tt.expectedError) 191 | } 192 | } 193 | 194 | tt.RunCheck() 195 | }) 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /cmd/certify/testdata/ca-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBmDCCAT2gAwIBAgIQUjIMhHGW4CreYEIQOnPDdDAKBggqhkjOPQQDAjAkMRAw 3 | DgYDVQQKEwdjZXJ0aWZ5MRAwDgYDVQQDEwdjZXJ0aWZ5MB4XDTIyMDMxNzA4NDQx 4 | MloXDTIzMDMxNzE0NDQxMlowJDEQMA4GA1UEChMHY2VydGlmeTEQMA4GA1UEAxMH 5 | Y2VydGlmeTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIPmsrI8hCLHryeWc0wz 6 | zrrbAXhohqMfFnZS95qM83p/EHHUO4yoi4LSZhZnvPhPYG+St4KBZj2mqZYs6nf8 7 | sTSjUTBPMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8E 8 | BTADAQH/MB0GA1UdDgQWBBTuUKyfBpn78BTa2fodsucBYuApejAKBggqhkjOPQQD 9 | AgNJADBGAiEAlYCxixkXh6eI1nHBAhaUHajYF6ZWpK4tiDCWR5lHIA0CIQCpgqUp 10 | +R8a3HBTIcrpgdoI2g11HmV9+qOysbuWNpTnMw== 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /cmd/certify/testdata/ca-crl.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN X509 CRL----- 2 | MIHiMIGJAgEBMAoGCCqGSM49BAMCMCQxEDAOBgNVBAoTB2NlcnRpZnkxEDAOBgNV 3 | BAMTB2NlcnRpZnkXDTIzMDkwMjE1NDAyNFoXDTIzMDkwNDE1NDAyNFqgNDAyMB8G 4 | A1UdIwQYMBaAFB/nlGRBJw24im6iHRMrXXmExBnxMA8GA1UdFAQIAgYSZl+8gygw 5 | CgYIKoZIzj0EAwIDSAAwRQIgah2RIGIppWkG2GJoYk+V+imapbQbmuq6ZtMqIcYw 6 | s8wCIQD7qx8oS5eE8Zhwe7Sc3rUvZn1o0NNYrc6kkvwoXAzHwQ== 7 | -----END X509 CRL----- -------------------------------------------------------------------------------- /cmd/certify/testdata/ca-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIOgIRqRHosbtIPpHON1XY8TSVg/U9K9tiw/xexfrGRJwoAoGCCqGSM49 3 | AwEHoUQDQgAEg+aysjyEIsevJ5ZzTDPOutsBeGiGox8WdlL3mozzen8QcdQ7jKiL 4 | gtJmFme8+E9gb5K3goFmPaaplizqd/yxNA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /cmd/certify/testdata/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nothinux/certify/ad72452c661a02fe138d663abd81949eaced5442/cmd/certify/testdata/empty -------------------------------------------------------------------------------- /cmd/certify/testdata/nothinux.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBlDCCATqgAwIBAgIQO/OgP7myWY17hJKsJNj72jAKBggqhkjOPQQDAjAhMRAw 3 | DgYDVQQKEwdjZXJ0aWZ5MQ0wCwYDVQQDEwRzYXlhMB4XDTIyMDQzMDE0MDI1NVoX 4 | DTIzMDQzMDIwMDI1NVowJTEQMA4GA1UEChMHY2VydGlmeTERMA8GA1UEAxMIbm90 5 | aGludXgwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASXbA1GfHG/O1Xr+wESBbde 6 | zrtRNfBB+3axMRV69lzZoEQrgbxDSlxZS+H7MW/pPEe/4wOLrk0NiubOV6nl2a2A 7 | o1AwTjAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIw 8 | ADAfBgNVHSMEGDAWgBQe3V/CQc9PpcXWl4FPqF4A4V2QcDAKBggqhkjOPQQDAgNI 9 | ADBFAiEA2v4pDfFdtp8fX5thZtJNVQUELfDOUuVfItHuSFN2BIcCIAU5Asjr1vvm 10 | 1XpLNN1xCzZrb/GauMb6AvDkpKV6hX89 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /cmd/certify/testdata/server-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIO+BHvmxk6M7bAS8TTzzZI5XSQZJmuXi2CddrpDL4uzcoAoGCCqGSM49 3 | AwEHoUQDQgAE3p4hOn31nKC/MHoMN03mkmJfMMh8n2Dpv+GNdJXaGJ2ILFnhiqTV 4 | fnBJ8ZZCDbTBbB7LuQkoKkWP37Cxx1dK0g== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /cmd/certify/testdata/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBmDCCAT2gAwIBAgIQT8HhCBzd7EOfY3QglPianzAKBggqhkjOPQQDAjAkMRAw 3 | DgYDVQQKEwdjZXJ0aWZ5MRAwDgYDVQQDEwdjZXJ0aWZ5MB4XDTIyMDUwMTEzNDM0 4 | NFoXDTIzMDUwMTE5NDM0NFowEjEQMA4GA1UEChMHY2VydGlmeTBZMBMGByqGSM49 5 | AgEGCCqGSM49AwEHA0IABN6eITp99ZygvzB6DDdN5pJiXzDIfJ9g6b/hjXSV2hid 6 | iCxZ4Yqk1X5wSfGWQg20wWwey7kJKCpFj9+wscdXStKjYzBhMB0GA1UdJQQWMBQG 7 | CCsGAQUFBwMCBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFO5Q 8 | rJ8GmfvwFNrZ+h2y5wFi4Cl6MBEGA1UdEQQKMAiCBnNlcnZlcjAKBggqhkjOPQQD 9 | AgNJADBGAiEA5UhXkn+GRa0zoRZqY4cmGNQSduQmmJWN7bKvMDIl6aACIQCw+kfg 10 | xZftkNs1hC0YE+tWl7W0O6/mG1oKxIjfurHuUA== 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /crl.go: -------------------------------------------------------------------------------- 1 | package certify 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/rand" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/pem" 10 | "fmt" 11 | "math/big" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // CertRevocationList hold certificate revocation list 17 | type CertRevocationList struct { 18 | Byte []byte 19 | } 20 | 21 | // CreateCRL Create certificate revocation list 22 | func CreateCRL(pkey *ecdsa.PrivateKey, caCert *x509.Certificate, crl *x509.RevocationList, nextUpdate time.Time) (*CertRevocationList, *big.Int, error) { 23 | crlNumber := time.Now().UTC().Format("20060102150405") 24 | num, _ := big.NewInt(0).SetString(crlNumber, 10) 25 | 26 | if crl == nil { 27 | crl = &x509.RevocationList{ 28 | RevokedCertificates: []pkix.RevokedCertificate{}, 29 | } 30 | } 31 | 32 | crl.Number = num 33 | crl.ThisUpdate = time.Now() 34 | crl.NextUpdate = nextUpdate 35 | 36 | crlByte, err := x509.CreateRevocationList(rand.Reader, crl, caCert, pkey) 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | 41 | return &CertRevocationList{Byte: crlByte}, num, nil 42 | } 43 | 44 | // String return string of certificate revocation list in pem encoded format 45 | func (c *CertRevocationList) String() string { 46 | var w bytes.Buffer 47 | if err := pem.Encode(&w, &pem.Block{ 48 | Type: "X509 CRL", 49 | Bytes: c.Byte, 50 | }); err != nil { 51 | return "" 52 | } 53 | 54 | return w.String() 55 | } 56 | 57 | func ParseCRL(crl []byte) (*x509.RevocationList, error) { 58 | c, _ := pem.Decode(crl) 59 | if c == nil { 60 | return nil, fmt.Errorf("no pem data") 61 | } 62 | 63 | return x509.ParseRevocationList(c.Bytes) 64 | } 65 | 66 | func RevokeCertificate(crl []byte, cert *x509.Certificate, caCert *x509.Certificate, pkey *ecdsa.PrivateKey, nextUpdate time.Time) (*CertRevocationList, *big.Int, error) { 67 | crlF, err := ParseCRL(crl) 68 | if err != nil { 69 | return nil, nil, err 70 | } 71 | 72 | crlF.RevokedCertificateEntries = append(crlF.RevokedCertificateEntries, x509.RevocationListEntry{ 73 | SerialNumber: cert.SerialNumber, 74 | RevocationTime: time.Now(), 75 | }) 76 | 77 | return CreateCRL(pkey, caCert, crlF, nextUpdate) 78 | 79 | } 80 | 81 | func CRLInfo(rl *x509.RevocationList) string { 82 | var buf bytes.Buffer 83 | 84 | buf.WriteString("Certificate Revocation List (CRL):\n") 85 | buf.WriteString(fmt.Sprintf("%4sVersion \n", "")) 86 | buf.WriteString(fmt.Sprintf("%4sSignature Algorithm: %v\n", "", rl.SignatureAlgorithm)) 87 | 88 | buf.WriteString(fmt.Sprintf("%4sIssuer: %v\n", "", strings.Replace(rl.Issuer.String(), ",", ", ", -1))) 89 | buf.WriteString(fmt.Sprintf("%8sLastUpdate: %v\n", "", rl.ThisUpdate.Format("Jan 2 15:04:05 2006 GMT"))) 90 | buf.WriteString(fmt.Sprintf("%8sNextUpdate: %v\n", "", rl.NextUpdate.Format("Jan 2 15:04:05 2006 GMT"))) 91 | 92 | buf.WriteString(fmt.Sprintf("%8sCRL Extensions:\n", "")) 93 | buf.WriteString(fmt.Sprintf("%12sX509v3 Authority Key Identifier:\n", "")) 94 | buf.WriteString(fmt.Sprintf("%16s%s\n", "", formatKeyIDWithColon(rl.AuthorityKeyId))) 95 | buf.WriteString(fmt.Sprintf("%12sX509v3 CRL Number:\n", "")) 96 | buf.WriteString(fmt.Sprintf("%16s%s\n", "", rl.Number)) 97 | 98 | if len(rl.RevokedCertificateEntries) == 0 { 99 | buf.WriteString("No Revoked Certificates\n") 100 | return buf.String() 101 | } 102 | 103 | buf.WriteString("Revoked Certificates:\n") 104 | for _, rc := range rl.RevokedCertificateEntries { 105 | buf.WriteString(fmt.Sprintf("%4sSerial Number: %s\n", "", formatKeyIDWithColon(rc.SerialNumber.Bytes()))) 106 | buf.WriteString(fmt.Sprintf("%8sRevocation Date: %s\n", "", rc.RevocationTime.Format("Jan 2 15:04:05 2006 GMT"))) 107 | } 108 | 109 | return buf.String() 110 | } 111 | -------------------------------------------------------------------------------- /crl_test.go: -------------------------------------------------------------------------------- 1 | package certify 2 | 3 | import ( 4 | "crypto/sha1" 5 | "crypto/x509" 6 | "crypto/x509/pkix" 7 | "net" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | var ( 13 | CRLDATA = `-----BEGIN X509 CRL----- 14 | MIHiMIGJAgEBMAoGCCqGSM49BAMCMCQxEDAOBgNVBAoTB2NlcnRpZnkxEDAOBgNV 15 | BAMTB2NlcnRpZnkXDTIzMDkwMjE1NDAyNFoXDTIzMDkwNDE1NDAyNFqgNDAyMB8G 16 | A1UdIwQYMBaAFB/nlGRBJw24im6iHRMrXXmExBnxMA8GA1UdFAQIAgYSZl+8gygw 17 | CgYIKoZIzj0EAwIDSAAwRQIgah2RIGIppWkG2GJoYk+V+imapbQbmuq6ZtMqIcYw 18 | s8wCIQD7qx8oS5eE8Zhwe7Sc3rUvZn1o0NNYrc6kkvwoXAzHwQ== 19 | -----END X509 CRL----- 20 | ` 21 | CRLDATAWITHREVOCATIONCERT = `-----BEGIN X509 CRL----- 22 | MIICyzCBtAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhub3RoaW51eBcN 23 | MjMwOTAyMTYyODQ2WhcNMjUwOTAxMTYyODQ2WjBIMCICEQCV3atQTTX3B6j9MM5p 24 | JKwAFw0yMzA5MDIxMTE5MDZaMCICEQCTD91VR4f01/4pCuo+3AfIFw0yMzA5MDIx 25 | NjI4NDZaoCMwITAfBgNVHSMEGDAWgBRwJ198tLFEwrc/2hvRwVgiBFGM6zANBgkq 26 | hkiG9w0BAQsFAAOCAgEAeGiqXjlTE3Kgm6dwgZpQk5AhvX1raM+IC/4bgAeAQBJ5 27 | 6iGboGbW4mOaLFPPdawFJbmtRtyW2/RxQX2QW+/DwR/pPQ5IcYyNJ8gsyn/vpBZe 28 | 0zTOCg0ILBtWxa+YL4EYPk8EqiOMXVZG73qaJBUR4snRCwph3f1CI5PZvsgZaPmd 29 | Q9X+tHYqHKruS/fu3uzAKgRUz27DCKgJ2kmPxF9AZ61J3PywykE8/A3ccLBHOzj+ 30 | IETKvuyWaATse2Q9qa1eDmjMDbZSpA4gQmSoCqOFc9M1exrb4zxT1YEwkosyzMvM 31 | /6BbDdWd178Vjxlzy1MakOU+4IRV6X+n74zXRbaERypLJMWIy1ndHMsfDDYn0Hrc 32 | 1XXoEIkuc4wvFihFkN9PjEJBEo1Mraew9xe3x7NY7AD8fYW2JOgd+Z2vxlbqXQ9Z 33 | nc4yytlA/P6hFJSbrigVAcUwQYPjS84DwLDFvJSmv0PpLiw0Enqdta4WcSOp/rr+ 34 | s3hycfohM1EtWm2GpmukKdJ6GkP3YitXnZo/FQOt6+0chmec3QYCrSiY1QHBEvX3 35 | ty4YcAH77NN3m1GMbC62GfjWd70V/SK43kzBtIwGE+kkoWIPoZQj0tf/1ZrNFGKS 36 | gkSyWaYiSbs9Xyl4ilVsENNxKsN5RUwv91ZisniYV5COfrJAsye3Jbzeb/AEGNM= 37 | -----END X509 CRL-----` 38 | ) 39 | 40 | func TestCreateCRL(t *testing.T) { 41 | pkey, err := GetPrivateKey() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | b, err := x509.MarshalPKIXPublicKey(&pkey.PublicKey) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | ski := sha1.Sum(b) 52 | 53 | template := Certificate{ 54 | Subject: pkix.Name{ 55 | Organization: []string{"certify"}, 56 | CommonName: "certify", 57 | }, 58 | NotBefore: time.Now(), 59 | NotAfter: time.Now().Add(24 * time.Hour), 60 | IsCA: true, 61 | SubjectKeyId: ski[:], 62 | DNSNames: []string{"github.com"}, 63 | IPAddress: []net.IP{ 64 | net.ParseIP("127.0.0.1"), 65 | }, 66 | } 67 | 68 | t.Run("Test Create CRL with cert that doesn't have keyUsage", func(t *testing.T) { 69 | template1 := template 70 | 71 | caCert, err := template1.GetCertificate(pkey.PrivateKey) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | _, _, err = CreateCRL(pkey.PrivateKey, caCert.Cert, nil, time.Now().Add(48*time.Hour)) 77 | if err == nil { 78 | t.Fatalf("this should be error, because the cert doesn't have keyUsage") 79 | } 80 | }) 81 | 82 | t.Run("Test Create CRL", func(t *testing.T) { 83 | template2 := template 84 | template2.KeyUsage = x509.KeyUsageCRLSign 85 | 86 | caCert, err := template2.GetCertificate(pkey.PrivateKey) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | 91 | _, _, err = CreateCRL(pkey.PrivateKey, caCert.Cert, nil, time.Now().Add(48*time.Hour)) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | }) 96 | } 97 | 98 | func TestParseCRL(t *testing.T) { 99 | rl, err := ParseCRL([]byte(CRLDATA)) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | t.Run("Test CRL number output", func(t *testing.T) { 105 | if rl.Number.String() != "20230902154024" { 106 | t.Fatal("The CRL number is different from what we expect") 107 | } 108 | }) 109 | } 110 | 111 | func TestCRLInfo(t *testing.T) { 112 | t.Run("Test CRL info with empty revocation list", func(t *testing.T) { 113 | rl, err := ParseCRL([]byte(CRLDATA)) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | 118 | CRLInfo(rl) 119 | }) 120 | 121 | t.Run("Test CRL info with 2 revocation list", func(t *testing.T) { 122 | rl, err := ParseCRL([]byte(CRLDATAWITHREVOCATIONCERT)) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | 127 | CRLInfo(rl) 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nothinux/certify 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/manifoldco/promptui v0.9.0 7 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 8 | software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 9 | ) 10 | 11 | require ( 12 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 13 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect 14 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 2 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 3 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 4 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 5 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 6 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 10 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 11 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 12 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= 13 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 14 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 15 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 16 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 17 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 19 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 21 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 22 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 23 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 24 | software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4= 25 | software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78/go.mod h1:B7Wf0Ya4DHF9Yw+qfZuJijQYkWicqDa+79Ytmmq3Kjg= 26 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package certify 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | // GetPublicKey returns string of pem encoded structure from given public key 12 | func GetPublicKey(pub interface{}) (string, error) { 13 | b, err := x509.MarshalPKIXPublicKey(pub) 14 | if err != nil { 15 | return "", err 16 | } 17 | 18 | var w bytes.Buffer 19 | if err := pem.Encode(&w, &pem.Block{ 20 | Type: "PUBLIC KEY", 21 | Bytes: b, 22 | }); err != nil { 23 | return "", err 24 | } 25 | 26 | return w.String(), err 27 | } 28 | 29 | func parseKeyUsage(ku x509.KeyUsage) []string { 30 | usages := []string{} 31 | 32 | if ku&x509.KeyUsageDigitalSignature > 0 { 33 | usages = append(usages, "Digital Signature") 34 | } 35 | if ku&x509.KeyUsageContentCommitment > 0 { 36 | usages = append(usages, "Content Commitment") 37 | } 38 | if ku&x509.KeyUsageDataEncipherment > 0 { 39 | usages = append(usages, "Key Encipherment") 40 | } 41 | if ku&x509.KeyUsageDataEncipherment > 0 { 42 | usages = append(usages, "Data Encipherment") 43 | } 44 | if ku&x509.KeyUsageKeyAgreement > 0 { 45 | usages = append(usages, "Key Agreement") 46 | } 47 | if ku&x509.KeyUsageCertSign > 0 { 48 | usages = append(usages, "Cert Sign") 49 | } 50 | if ku&x509.KeyUsageCRLSign > 0 { 51 | usages = append(usages, "CRL Sign") 52 | } 53 | if ku&x509.KeyUsageEncipherOnly > 0 { 54 | usages = append(usages, "Enchiper Only") 55 | } 56 | if ku&x509.KeyUsageDecipherOnly > 0 { 57 | usages = append(usages, "Dechiper Only") 58 | } 59 | 60 | return usages 61 | } 62 | 63 | func parseExtKeyUsage(ekus []x509.ExtKeyUsage) string { 64 | var extku []string 65 | 66 | for _, eku := range ekus { 67 | if eku == x509.ExtKeyUsageAny { 68 | extku = append(extku, "Any Extended Key Usage") 69 | } else if eku == x509.ExtKeyUsageClientAuth { 70 | extku = append(extku, "TLS Web Client Authentication") 71 | } else if eku == x509.ExtKeyUsageServerAuth { 72 | extku = append(extku, "TLS Web Server Authentication") 73 | } else if eku == x509.ExtKeyUsageCodeSigning { 74 | extku = append(extku, "Code Signing") 75 | } else if eku == x509.ExtKeyUsageEmailProtection { 76 | extku = append(extku, "E-mail Protection") 77 | } else if eku == x509.ExtKeyUsageIPSECEndSystem { 78 | extku = append(extku, "IPSec End System") 79 | } else if eku == x509.ExtKeyUsageIPSECTunnel { 80 | extku = append(extku, "IPSec Tunnel") 81 | } else if eku == x509.ExtKeyUsageIPSECUser { 82 | extku = append(extku, "IPSec User") 83 | } else if eku == x509.ExtKeyUsageTimeStamping { 84 | extku = append(extku, "Time Stamping") 85 | } else if eku == x509.ExtKeyUsageOCSPSigning { 86 | extku = append(extku, "OCSP Signing") 87 | } else if eku == x509.ExtKeyUsageMicrosoftServerGatedCrypto { 88 | extku = append(extku, "Microsoft Server Gated Crypto") 89 | } else if eku == x509.ExtKeyUsageNetscapeServerGatedCrypto { 90 | extku = append(extku, "Netscape Server Gated Crypto") 91 | } else if eku == x509.ExtKeyUsageMicrosoftCommercialCodeSigning { 92 | extku = append(extku, "Microsoft Commercial Code Signing") 93 | } else if eku == x509.ExtKeyUsageMicrosoftKernelCodeSigning { 94 | extku = append(extku, "1.3.6.1.4.1.311.61.1.1") 95 | } 96 | } 97 | 98 | return strings.Join(extku, ", ") 99 | } 100 | 101 | func formatKeyIDWithColon(id []byte) string { 102 | var s string 103 | 104 | for i, c := range id { 105 | if i > 0 { 106 | s += ":" 107 | } 108 | s += fmt.Sprintf("%02x", c) 109 | } 110 | 111 | return s 112 | } 113 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | package certify 2 | 3 | import ( 4 | "crypto/x509" 5 | "os" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestGetPublicKey(t *testing.T) { 11 | expectedPubKey := `-----BEGIN PUBLIC KEY----- 12 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEg+aysjyEIsevJ5ZzTDPOutsBeGiG 13 | ox8WdlL3mozzen8QcdQ7jKiLgtJmFme8+E9gb5K3goFmPaaplizqd/yxNA== 14 | -----END PUBLIC KEY----- 15 | ` 16 | 17 | cert, err := readCertificateFile("./cmd/certify/testdata/ca-cert.pem") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | pubkey, err := GetPublicKey(cert.PublicKey) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | if pubkey != expectedPubKey { 28 | t.Fatalf("got %v, want %v", pubkey, expectedPubKey) 29 | } 30 | } 31 | 32 | func TestParseKeyUsage(t *testing.T) { 33 | tests := []struct { 34 | Name string 35 | KeyUsage x509.KeyUsage 36 | Expected []string 37 | }{ 38 | { 39 | Name: "Test Cert Sign and CRL Sign Key Usage", 40 | KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, 41 | Expected: []string{"Cert Sign", "CRL Sign"}, 42 | }, 43 | { 44 | Name: "Test CRL Sign Key Usage", 45 | KeyUsage: x509.KeyUsageCRLSign, 46 | Expected: []string{"CRL Sign"}, 47 | }, 48 | { 49 | Name: "Test Digital Signature Key Usage", 50 | KeyUsage: x509.KeyUsageDigitalSignature, 51 | Expected: []string{"Digital Signature"}, 52 | }, 53 | { 54 | Name: "Test other Key Usage", 55 | KeyUsage: x509.KeyUsage(0), 56 | Expected: []string{}, 57 | }, 58 | } 59 | 60 | for _, tt := range tests { 61 | t.Run(tt.Name, func(t *testing.T) { 62 | got := parseKeyUsage(tt.KeyUsage) 63 | if !reflect.DeepEqual(got, tt.Expected) { 64 | t.Fatalf("got %v, want %v", got, tt.Expected) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func TestParseExtKeyUsage(t *testing.T) { 71 | t.Run("Test single eku", func(t *testing.T) { 72 | result := parseExtKeyUsage([]x509.ExtKeyUsage{ 73 | x509.ExtKeyUsageServerAuth, 74 | }) 75 | 76 | expectedResult := "TLS Web Server Authentication" 77 | 78 | if result != expectedResult { 79 | t.Fatalf("got %v, eant %v", result, expectedResult) 80 | } 81 | }) 82 | 83 | t.Run("Test multiple eku", func(t *testing.T) { 84 | result := parseExtKeyUsage([]x509.ExtKeyUsage{ 85 | x509.ExtKeyUsageServerAuth, 86 | x509.ExtKeyUsageClientAuth, 87 | }) 88 | 89 | expectedResult := "TLS Web Server Authentication, TLS Web Client Authentication" 90 | 91 | if result != expectedResult { 92 | t.Fatalf("got %v, eant %v", result, expectedResult) 93 | } 94 | }) 95 | 96 | t.Run("Test all Eku", func(t *testing.T) { 97 | result := parseExtKeyUsage([]x509.ExtKeyUsage{ 98 | x509.ExtKeyUsageServerAuth, 99 | x509.ExtKeyUsageClientAuth, 100 | x509.ExtKeyUsageAny, 101 | x509.ExtKeyUsageCodeSigning, 102 | x509.ExtKeyUsageEmailProtection, 103 | x509.ExtKeyUsageIPSECEndSystem, 104 | x509.ExtKeyUsageIPSECTunnel, 105 | x509.ExtKeyUsageIPSECUser, 106 | x509.ExtKeyUsageTimeStamping, 107 | x509.ExtKeyUsageOCSPSigning, 108 | x509.ExtKeyUsageMicrosoftServerGatedCrypto, 109 | x509.ExtKeyUsageNetscapeServerGatedCrypto, 110 | x509.ExtKeyUsageMicrosoftCommercialCodeSigning, 111 | x509.ExtKeyUsageMicrosoftKernelCodeSigning, 112 | }) 113 | 114 | expectedResult := "TLS Web Server Authentication, TLS Web Client Authentication, Any Extended Key Usage, Code Signing, E-mail Protection, IPSec End System, IPSec Tunnel, IPSec User, Time Stamping, OCSP Signing, Microsoft Server Gated Crypto, Netscape Server Gated Crypto, Microsoft Commercial Code Signing, 1.3.6.1.4.1.311.61.1.1" 115 | 116 | if result != expectedResult { 117 | t.Fatalf("got %v, eant %v", result, expectedResult) 118 | } 119 | }) 120 | } 121 | 122 | func TestFormatKeyIDWithColon(t *testing.T) { 123 | result := formatKeyIDWithColon([]byte{36, 44, 106, 165, 22, 233, 173, 100, 28, 6, 69, 211, 74, 214, 212, 162}) 124 | expectedResult := "24:2c:6a:a5:16:e9:ad:64:1c:06:45:d3:4a:d6:d4:a2" 125 | 126 | if result != expectedResult { 127 | t.Fatalf("got %v, want %v", result, expectedResult) 128 | } 129 | } 130 | 131 | func readCertificateFile(path string) (*x509.Certificate, error) { 132 | f, err := os.ReadFile(path) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | c, err := ParseCertificate(f) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | return c, nil 143 | } 144 | -------------------------------------------------------------------------------- /key.go: -------------------------------------------------------------------------------- 1 | package certify 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "fmt" 11 | ) 12 | 13 | // PrivateKey hold private key 14 | type PrivateKey struct { 15 | *ecdsa.PrivateKey 16 | } 17 | 18 | // GetPrivateKey returns struct PrivateKey containing the private key 19 | func GetPrivateKey() (*PrivateKey, error) { 20 | pkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 21 | if err != nil { 22 | return &PrivateKey{}, err 23 | } 24 | 25 | return &PrivateKey{ 26 | pkey, 27 | }, nil 28 | } 29 | 30 | // String returns string of private key in pem encoded format 31 | func (p *PrivateKey) String() string { 32 | b, err := x509.MarshalECPrivateKey(p.PrivateKey) 33 | if err != nil { 34 | return "" 35 | } 36 | 37 | var w bytes.Buffer 38 | if err := pem.Encode(&w, &pem.Block{ 39 | Type: "EC PRIVATE KEY", 40 | Bytes: b, 41 | }); err != nil { 42 | return "" 43 | } 44 | 45 | return w.String() 46 | } 47 | 48 | // ParsePrivatekey parse given []byte private key to struct *ecdsa.PrivateKey 49 | func ParsePrivateKey(pkey []byte) (*ecdsa.PrivateKey, error) { 50 | b, _ := pem.Decode(pkey) 51 | if b == nil { 52 | return &ecdsa.PrivateKey{}, fmt.Errorf("no pem data found") 53 | } 54 | 55 | u, err := x509.ParseECPrivateKey(b.Bytes) 56 | if err != nil { 57 | return &ecdsa.PrivateKey{}, err 58 | } 59 | 60 | return u, nil 61 | } 62 | -------------------------------------------------------------------------------- /key_test.go: -------------------------------------------------------------------------------- 1 | package certify 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | var ( 10 | PKEYDATA = `-----BEGIN EC PRIVATE KEY----- 11 | MHcCAQEEIL66D9bK8UInoN0xfbQ3/usWXWzHSb8cq+e2RfO6usYpoAoGCCqGSM49 12 | AwEHoUQDQgAE8aYzn9wIIS+K4/lX6aoUQR18rGLsjKmQgIa+vtc3jwWclNJhh3tT 13 | AsroDcdnN6haaQt3fmI56TphJF5PXCAhIQ== 14 | -----END EC PRIVATE KEY----- 15 | ` 16 | RSAPKEYDATA = `-----BEGIN RSA PRIVATE KEY----- 17 | MIIBOQIBAAJBAMqARHoSpBvmYR92JAfSf4roUoyLB9D6e/nNoIK7yjw5PvUGEHM+ 18 | uMOiIQjlqui020aj5TeuWs09ljGKhcF0nGkCAwEAAQJAZiBiaJ5WHawGd3OBoGBM 19 | 6qVYXIERpBdvxwApX0WOLOhcAJ5nYSboyppHEYTk4NgK7YuoZy61KswAU+qmy/Jw 20 | AQIhAPHWn5ghX+VhTG/J1ZY/y13hOpj4+9Eki+MJNr7pXqXpAiEA1lvxHLYEDOev 21 | rj4iN5/bvF6Dbl1QYrwMa582C2LPsoECIAuPpA+EwO3ZSesqLfDB2foB82gutvMX 22 | mSxgW2KjC2hJAiA2xQ0pIdSNG5GGurdxcPXq/lckltEYOSYPRYHAjQG2gQIgZdwE 23 | QfCCn+yOvP+oeXatjlGliCnVL95G6fA1icn4AnE= 24 | -----END RSA PRIVATE KEY----- 25 | ` 26 | ) 27 | 28 | func TestGetPrivateKey(t *testing.T) { 29 | p, err := GetPrivateKey() 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | t.Run("Test created private key can be parsed", func(t *testing.T) { 35 | _, err := ParsePrivateKey([]byte(p.String())) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | }) 40 | } 41 | 42 | func TestParsePrivateKey(t *testing.T) { 43 | t.Run("Test compare parsed valid private key", func(t *testing.T) { 44 | p, err := ParsePrivateKey([]byte(PKEYDATA)) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | pkey := &PrivateKey{p} 50 | 51 | if !reflect.DeepEqual(pkey.String(), PKEYDATA) { 52 | t.Fatalf("\ngot %v\nwant %v\n", pkey.String(), PKEYDATA) 53 | } 54 | }) 55 | 56 | t.Run("Test parsing unsuported rsa private key", func(t *testing.T) { 57 | _, err := ParsePrivateKey([]byte(RSAPKEYDATA)) 58 | if err == nil { 59 | t.Fatalf("got no error, want error contains x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format") 60 | } 61 | }) 62 | } 63 | 64 | func TestParseEmptyPrivateKeyFile(t *testing.T) { 65 | _, err := ParsePrivateKey([]byte("")) 66 | if err != nil { 67 | if !strings.Contains(err.Error(), "no pem data found") { 68 | t.Fatal("the error must contain no pem data found") 69 | } 70 | } 71 | } 72 | --------------------------------------------------------------------------------