From 560626fd2a0966578a4c682102a1f40fc48aa78e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B8rn=20Snoen?=
Date: Sat, 13 Nov 2021 21:48:05 +0100
Subject: [PATCH 1/3] (feat) Factor out cert handling and add C binding
---
certutils/certutuils.go | 268 ++++++++++++++++++++++++++++++++++++++
main.go | 280 +++-------------------------------------
2 files changed, 286 insertions(+), 262 deletions(-)
create mode 100644 certutils/certutuils.go
diff --git a/certutils/certutuils.go b/certutils/certutuils.go
new file mode 100644
index 0000000..30986a2
--- /dev/null
+++ b/certutils/certutuils.go
@@ -0,0 +1,268 @@
+package certutils
+
+import (
+ "bytes"
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha1"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/asn1"
+ "encoding/hex"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "math"
+ "math/big"
+ "net"
+ "os"
+ "strings"
+ "time"
+)
+
+type issuer struct {
+ key crypto.Signer
+ cert *x509.Certificate
+}
+
+func GetIssuer(keyFile, certFile string) (*issuer, error) {
+ keyContents, keyErr := ioutil.ReadFile(keyFile)
+ certContents, certErr := ioutil.ReadFile(certFile)
+ if os.IsNotExist(keyErr) && os.IsNotExist(certErr) {
+ err := makeIssuer(keyFile, certFile)
+ if err != nil {
+ return nil, err
+ }
+ return GetIssuer(keyFile, certFile)
+ } else if keyErr != nil {
+ return nil, fmt.Errorf("%s (but %s exists)", keyErr, certFile)
+ } else if certErr != nil {
+ return nil, fmt.Errorf("%s (but %s exists)", certErr, keyFile)
+ }
+ key, err := readPrivateKey(keyContents)
+ if err != nil {
+ return nil, fmt.Errorf("reading private key from %s: %s", keyFile, err)
+ }
+
+ cert, err := readCert(certContents)
+ if err != nil {
+ return nil, fmt.Errorf("reading CA certificate from %s: %s", certFile, err)
+ }
+
+ equal, err := publicKeysEqual(key.Public(), cert.PublicKey)
+ if err != nil {
+ return nil, fmt.Errorf("comparing public keys: %s", err)
+ } else if !equal {
+ return nil, fmt.Errorf("public key in CA certificate %s doesn't match private key in %s",
+ certFile, keyFile)
+ }
+ return &issuer{key, cert}, nil
+}
+
+func readPrivateKey(keyContents []byte) (crypto.Signer, error) {
+ block, _ := pem.Decode(keyContents)
+ if block == nil {
+ return nil, fmt.Errorf("no PEM found")
+ } else if block.Type != "RSA PRIVATE KEY" && block.Type != "ECDSA PRIVATE KEY" {
+ return nil, fmt.Errorf("incorrect PEM type %s", block.Type)
+ }
+ return x509.ParsePKCS1PrivateKey(block.Bytes)
+}
+
+func readCert(certContents []byte) (*x509.Certificate, error) {
+ block, _ := pem.Decode(certContents)
+ if block == nil {
+ return nil, fmt.Errorf("no PEM found")
+ } else if block.Type != "CERTIFICATE" {
+ return nil, fmt.Errorf("incorrect PEM type %s", block.Type)
+ }
+ return x509.ParseCertificate(block.Bytes)
+}
+
+func makeIssuer(keyFile, certFile string) error {
+ key, err := makeKey(keyFile)
+ if err != nil {
+ return err
+ }
+ _, err = makeRootCert(key, certFile)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func makeKey(filename string) (*rsa.PrivateKey, error) {
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, err
+ }
+ der := x509.MarshalPKCS1PrivateKey(key)
+ if err != nil {
+ return nil, err
+ }
+ file, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ err = pem.Encode(file, &pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Bytes: der,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return key, nil
+}
+
+func makeRootCert(key crypto.Signer, filename string) (*x509.Certificate, error) {
+ serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
+ if err != nil {
+ return nil, err
+ }
+ skid, err := calculateSKID(key.Public())
+ if err != nil {
+ return nil, err
+ }
+ template := &x509.Certificate{
+ Subject: pkix.Name{
+ CommonName: "minica root ca " + hex.EncodeToString(serial.Bytes()[:3]),
+ },
+ SerialNumber: serial,
+ NotBefore: time.Now(),
+ NotAfter: time.Now().AddDate(100, 0, 0),
+
+ SubjectKeyId: skid,
+ AuthorityKeyId: skid,
+ KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
+ BasicConstraintsValid: true,
+ IsCA: true,
+ MaxPathLenZero: true,
+ }
+
+ der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
+ if err != nil {
+ return nil, err
+ }
+ file, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ err = pem.Encode(file, &pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: der,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return x509.ParseCertificate(der)
+}
+
+func parseIPs(ipAddresses []string) ([]net.IP, error) {
+ var parsed []net.IP
+ for _, s := range ipAddresses {
+ p := net.ParseIP(s)
+ if p == nil {
+ return nil, fmt.Errorf("invalid IP address %s", s)
+ }
+ parsed = append(parsed, p)
+ }
+ return parsed, nil
+}
+
+func publicKeysEqual(a, b interface{}) (bool, error) {
+ aBytes, err := x509.MarshalPKIXPublicKey(a)
+ if err != nil {
+ return false, err
+ }
+ bBytes, err := x509.MarshalPKIXPublicKey(b)
+ if err != nil {
+ return false, err
+ }
+ return bytes.Compare(aBytes, bBytes) == 0, nil
+}
+
+func calculateSKID(pubKey crypto.PublicKey) ([]byte, error) {
+ spkiASN1, err := x509.MarshalPKIXPublicKey(pubKey)
+ if err != nil {
+ return nil, err
+ }
+
+ var spki struct {
+ Algorithm pkix.AlgorithmIdentifier
+ SubjectPublicKey asn1.BitString
+ }
+ _, err = asn1.Unmarshal(spkiASN1, &spki)
+ if err != nil {
+ return nil, err
+ }
+ skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
+ return skid[:], nil
+}
+
+func Sign(iss *issuer, domains []string, ipAddresses []string) (*x509.Certificate, error) {
+ var cn string
+ if len(domains) > 0 {
+ cn = domains[0]
+ } else if len(ipAddresses) > 0 {
+ cn = ipAddresses[0]
+ } else {
+ return nil, fmt.Errorf("must specify at least one domain name or IP address")
+ }
+ var cnFolder = strings.Replace(cn, "*", "_", -1)
+ err := os.Mkdir(cnFolder, 0700)
+ if err != nil && !os.IsExist(err) {
+ return nil, err
+ }
+ key, err := makeKey(fmt.Sprintf("%s/key.pem", cnFolder))
+ if err != nil {
+ return nil, err
+ }
+ parsedIPs, err := parseIPs(ipAddresses)
+ if err != nil {
+ return nil, err
+ }
+ serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
+ if err != nil {
+ return nil, err
+ }
+ template := &x509.Certificate{
+ DNSNames: domains,
+ IPAddresses: parsedIPs,
+ Subject: pkix.Name{
+ CommonName: cn,
+ },
+ SerialNumber: serial,
+ NotBefore: time.Now(),
+ // Set the validity period to 2 years and 30 days, to satisfy the iOS and
+ // macOS requirements that all server certificates must have validity
+ // shorter than 825 days:
+ // https://derflounder.wordpress.com/2019/06/06/new-tls-security-requirements-for-ios-13-and-macos-catalina-10-15/
+ NotAfter: time.Now().AddDate(2, 0, 30),
+
+ KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
+ BasicConstraintsValid: true,
+ IsCA: false,
+ }
+ der, err := x509.CreateCertificate(rand.Reader, template, iss.cert, key.Public(), iss.key)
+ if err != nil {
+ return nil, err
+ }
+ file, err := os.OpenFile(fmt.Sprintf("%s/cert.pem", cnFolder), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ err = pem.Encode(file, &pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: der,
+ })
+ if err != nil {
+ return nil, err
+ }
+ return x509.ParseCertificate(der)
+}
diff --git a/main.go b/main.go
index 9734969..b6e5d80 100644
--- a/main.go
+++ b/main.go
@@ -1,27 +1,16 @@
package main
import (
- "bytes"
- "crypto"
- "crypto/rand"
- "crypto/rsa"
- "crypto/sha1"
- "crypto/x509"
- "crypto/x509/pkix"
- "encoding/asn1"
- "encoding/hex"
- "encoding/pem"
+ "C"
"flag"
"fmt"
- "io/ioutil"
"log"
- "math"
- "math/big"
"net"
"os"
"regexp"
"strings"
- "time"
+
+ "github.com/jsha/minica/certutils"
)
func main() {
@@ -31,252 +20,6 @@ func main() {
}
}
-type issuer struct {
- key crypto.Signer
- cert *x509.Certificate
-}
-
-func getIssuer(keyFile, certFile string) (*issuer, error) {
- keyContents, keyErr := ioutil.ReadFile(keyFile)
- certContents, certErr := ioutil.ReadFile(certFile)
- if os.IsNotExist(keyErr) && os.IsNotExist(certErr) {
- err := makeIssuer(keyFile, certFile)
- if err != nil {
- return nil, err
- }
- return getIssuer(keyFile, certFile)
- } else if keyErr != nil {
- return nil, fmt.Errorf("%s (but %s exists)", keyErr, certFile)
- } else if certErr != nil {
- return nil, fmt.Errorf("%s (but %s exists)", certErr, keyFile)
- }
- key, err := readPrivateKey(keyContents)
- if err != nil {
- return nil, fmt.Errorf("reading private key from %s: %s", keyFile, err)
- }
-
- cert, err := readCert(certContents)
- if err != nil {
- return nil, fmt.Errorf("reading CA certificate from %s: %s", certFile, err)
- }
-
- equal, err := publicKeysEqual(key.Public(), cert.PublicKey)
- if err != nil {
- return nil, fmt.Errorf("comparing public keys: %s", err)
- } else if !equal {
- return nil, fmt.Errorf("public key in CA certificate %s doesn't match private key in %s",
- certFile, keyFile)
- }
- return &issuer{key, cert}, nil
-}
-
-func readPrivateKey(keyContents []byte) (crypto.Signer, error) {
- block, _ := pem.Decode(keyContents)
- if block == nil {
- return nil, fmt.Errorf("no PEM found")
- } else if block.Type != "RSA PRIVATE KEY" && block.Type != "ECDSA PRIVATE KEY" {
- return nil, fmt.Errorf("incorrect PEM type %s", block.Type)
- }
- return x509.ParsePKCS1PrivateKey(block.Bytes)
-}
-
-func readCert(certContents []byte) (*x509.Certificate, error) {
- block, _ := pem.Decode(certContents)
- if block == nil {
- return nil, fmt.Errorf("no PEM found")
- } else if block.Type != "CERTIFICATE" {
- return nil, fmt.Errorf("incorrect PEM type %s", block.Type)
- }
- return x509.ParseCertificate(block.Bytes)
-}
-
-func makeIssuer(keyFile, certFile string) error {
- key, err := makeKey(keyFile)
- if err != nil {
- return err
- }
- _, err = makeRootCert(key, certFile)
- if err != nil {
- return err
- }
- return nil
-}
-
-func makeKey(filename string) (*rsa.PrivateKey, error) {
- key, err := rsa.GenerateKey(rand.Reader, 2048)
- if err != nil {
- return nil, err
- }
- der := x509.MarshalPKCS1PrivateKey(key)
- if err != nil {
- return nil, err
- }
- file, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
- if err != nil {
- return nil, err
- }
- defer file.Close()
- err = pem.Encode(file, &pem.Block{
- Type: "RSA PRIVATE KEY",
- Bytes: der,
- })
- if err != nil {
- return nil, err
- }
- return key, nil
-}
-
-func makeRootCert(key crypto.Signer, filename string) (*x509.Certificate, error) {
- serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
- if err != nil {
- return nil, err
- }
- skid, err := calculateSKID(key.Public())
- if err != nil {
- return nil, err
- }
- template := &x509.Certificate{
- Subject: pkix.Name{
- CommonName: "minica root ca " + hex.EncodeToString(serial.Bytes()[:3]),
- },
- SerialNumber: serial,
- NotBefore: time.Now(),
- NotAfter: time.Now().AddDate(100, 0, 0),
-
- SubjectKeyId: skid,
- AuthorityKeyId: skid,
- KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
- BasicConstraintsValid: true,
- IsCA: true,
- MaxPathLenZero: true,
- }
-
- der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
- if err != nil {
- return nil, err
- }
- file, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
- if err != nil {
- return nil, err
- }
- defer file.Close()
- err = pem.Encode(file, &pem.Block{
- Type: "CERTIFICATE",
- Bytes: der,
- })
- if err != nil {
- return nil, err
- }
- return x509.ParseCertificate(der)
-}
-
-func parseIPs(ipAddresses []string) ([]net.IP, error) {
- var parsed []net.IP
- for _, s := range ipAddresses {
- p := net.ParseIP(s)
- if p == nil {
- return nil, fmt.Errorf("invalid IP address %s", s)
- }
- parsed = append(parsed, p)
- }
- return parsed, nil
-}
-
-func publicKeysEqual(a, b interface{}) (bool, error) {
- aBytes, err := x509.MarshalPKIXPublicKey(a)
- if err != nil {
- return false, err
- }
- bBytes, err := x509.MarshalPKIXPublicKey(b)
- if err != nil {
- return false, err
- }
- return bytes.Compare(aBytes, bBytes) == 0, nil
-}
-
-func calculateSKID(pubKey crypto.PublicKey) ([]byte, error) {
- spkiASN1, err := x509.MarshalPKIXPublicKey(pubKey)
- if err != nil {
- return nil, err
- }
-
- var spki struct {
- Algorithm pkix.AlgorithmIdentifier
- SubjectPublicKey asn1.BitString
- }
- _, err = asn1.Unmarshal(spkiASN1, &spki)
- if err != nil {
- return nil, err
- }
- skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
- return skid[:], nil
-}
-
-func sign(iss *issuer, domains []string, ipAddresses []string) (*x509.Certificate, error) {
- var cn string
- if len(domains) > 0 {
- cn = domains[0]
- } else if len(ipAddresses) > 0 {
- cn = ipAddresses[0]
- } else {
- return nil, fmt.Errorf("must specify at least one domain name or IP address")
- }
- var cnFolder = strings.Replace(cn, "*", "_", -1)
- err := os.Mkdir(cnFolder, 0700)
- if err != nil && !os.IsExist(err) {
- return nil, err
- }
- key, err := makeKey(fmt.Sprintf("%s/key.pem", cnFolder))
- if err != nil {
- return nil, err
- }
- parsedIPs, err := parseIPs(ipAddresses)
- if err != nil {
- return nil, err
- }
- serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
- if err != nil {
- return nil, err
- }
- template := &x509.Certificate{
- DNSNames: domains,
- IPAddresses: parsedIPs,
- Subject: pkix.Name{
- CommonName: cn,
- },
- SerialNumber: serial,
- NotBefore: time.Now(),
- // Set the validity period to 2 years and 30 days, to satisfy the iOS and
- // macOS requirements that all server certificates must have validity
- // shorter than 825 days:
- // https://derflounder.wordpress.com/2019/06/06/new-tls-security-requirements-for-ios-13-and-macos-catalina-10-15/
- NotAfter: time.Now().AddDate(2, 0, 30),
-
- KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
- BasicConstraintsValid: true,
- IsCA: false,
- }
- der, err := x509.CreateCertificate(rand.Reader, template, iss.cert, key.Public(), iss.key)
- if err != nil {
- return nil, err
- }
- file, err := os.OpenFile(fmt.Sprintf("%s/cert.pem", cnFolder), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
- if err != nil {
- return nil, err
- }
- defer file.Close()
- err = pem.Encode(file, &pem.Block{
- Type: "CERTIFICATE",
- Bytes: der,
- })
- if err != nil {
- return nil, err
- }
- return x509.ParseCertificate(der)
-}
-
func split(s string) (results []string) {
if len(s) > 0 {
return strings.Split(s, ",")
@@ -336,10 +79,23 @@ will not overwrite existing keys or certificates.
os.Exit(1)
}
}
- issuer, err := getIssuer(*caKey, *caCert)
+ issuer, err := certutils.GetIssuer(*caKey, *caCert)
if err != nil {
return err
}
- _, err = sign(issuer, domainSlice, ipSlice)
+ _, err = certutils.Sign(issuer, domainSlice, ipSlice)
return err
}
+
+//export generateCertificate
+func generateCertificate(domain *C.char) C.int {
+ iss, err := certutils.GetIssuer("minica-key.pem", "minica.pem")
+ if err != nil {
+ return 1
+ }
+ _, err = certutils.Sign(iss, []string{C.GoString(domain)}, []string{})
+ if err != nil {
+ return 2
+ }
+ return 0
+}
From e3fac6ed16917fdb44bb076746b78536b33e64af Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B8rn=20Snoen?=
Date: Sat, 13 Nov 2021 21:55:56 +0100
Subject: [PATCH 2/3] (refactor) Move c-export to own file
---
api.go | 20 ++++++++++++++++++++
main.go | 14 --------------
2 files changed, 20 insertions(+), 14 deletions(-)
create mode 100644 api.go
diff --git a/api.go b/api.go
new file mode 100644
index 0000000..2c9222a
--- /dev/null
+++ b/api.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+ "C"
+
+ "github.com/jsha/minica/certutils"
+)
+
+//export generateCertificate
+func generateCertificate(domain *C.char) C.int {
+ iss, err := certutils.GetIssuer("minica-key.pem", "minica.pem")
+ if err != nil {
+ return 1
+ }
+ _, err = certutils.Sign(iss, []string{C.GoString(domain)}, []string{})
+ if err != nil {
+ return 2
+ }
+ return 0
+}
diff --git a/main.go b/main.go
index b6e5d80..40a49cf 100644
--- a/main.go
+++ b/main.go
@@ -1,7 +1,6 @@
package main
import (
- "C"
"flag"
"fmt"
"log"
@@ -86,16 +85,3 @@ will not overwrite existing keys or certificates.
_, err = certutils.Sign(issuer, domainSlice, ipSlice)
return err
}
-
-//export generateCertificate
-func generateCertificate(domain *C.char) C.int {
- iss, err := certutils.GetIssuer("minica-key.pem", "minica.pem")
- if err != nil {
- return 1
- }
- _, err = certutils.Sign(iss, []string{C.GoString(domain)}, []string{})
- if err != nil {
- return 2
- }
- return 0
-}
From e261bd769c23c20ada2d527a38d5a1469543c3f1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B8rn=20Snoen?=
Date: Sun, 14 Nov 2021 11:18:26 +0100
Subject: [PATCH 3/3] (feat) Another api function for IP certs
---
api.go | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/api.go b/api.go
index 2c9222a..8f89c36 100644
--- a/api.go
+++ b/api.go
@@ -18,3 +18,16 @@ func generateCertificate(domain *C.char) C.int {
}
return 0
}
+
+//export generateIPCertificate
+func generateIPCertificate(ipAddress *C.char) C.int {
+ iss, err := certutils.GetIssuer("minica-key.pem", "minica.pem")
+ if err != nil {
+ return 1
+ }
+ _, err = certutils.Sign(iss, []string{}, []string{C.GoString(ipAddress)})
+ if err != nil {
+ return 2
+ }
+ return 0
+}