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 +}