From 5f43bb6f41c092fead1b4daeef522c773bf67f61 Mon Sep 17 00:00:00 2001 From: txthinking Date: Thu, 25 Mar 2021 11:35:08 +0000 Subject: [PATCH] opensource --- LICENSE | 21 ++++++ ca.go | 112 ++++++++++++++++++++++++++++++ cert.go | 139 ++++++++++++++++++++++++++++++++++++++ cli/mad/build.sh | 26 +++++++ cli/mad/main.go | 165 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 8 +++ go.sum | 16 +++++ install.go | 10 +++ install_darwin.go | 11 +++ install_windows.go | 11 +++ 10 files changed, 519 insertions(+) create mode 100644 LICENSE create mode 100644 ca.go create mode 100644 cert.go create mode 100755 cli/mad/build.sh create mode 100644 cli/mad/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 install.go create mode 100644 install_darwin.go create mode 100644 install_windows.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..03aad45 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015-present Cloud https://www.txthinking.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ca.go b/ca.go new file mode 100644 index 0000000..280ddf1 --- /dev/null +++ b/ca.go @@ -0,0 +1,112 @@ +package mad + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "math/big" + "os" + "time" +) + +type Ca struct { + C *x509.Certificate + CaPEM []byte + KeyPEM []byte +} + +func NewCa(Organization, OrganizationalUnit, CommonName string) *Ca { + c := &x509.Certificate{ + Subject: pkix.Name{ + Organization: []string{Organization}, + OrganizationalUnit: []string{OrganizationalUnit}, + CommonName: CommonName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + MaxPathLenZero: true, + } + return &Ca{ + C: c, + } +} + +func (c *Ca) Create() error { + p, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return err + } + pub := p.Public() + + b, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return err + } + var spki struct { + Algorithm pkix.AlgorithmIdentifier + SubjectPublicKey asn1.BitString + } + if _, err := asn1.Unmarshal(b, &spki); err != nil { + return err + } + skid := sha1.Sum(spki.SubjectPublicKey.Bytes) + c.C.SubjectKeyId = skid[:] + + sn, err := rand.Int(rand.Reader, big.NewInt(0).Lsh(big.NewInt(1), 128)) + if err != nil { + return err + } + c.C.SerialNumber = sn + + b, err = x509.CreateCertificate(rand.Reader, c.C, c.C, pub, p) + if err != nil { + return err + } + c.CaPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: b}) + // c.KeyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(p)}) + b, err = x509.MarshalPKCS8PrivateKey(p) + if err != nil { + return err + } + c.KeyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: b}) + return nil +} + +func (c *Ca) Ca() []byte { + return c.CaPEM +} + +func (c *Ca) Key() []byte { + return c.KeyPEM +} + +func (c *Ca) SaveToFile(ca, key string) error { + f, err := os.Create(ca) + if err != nil { + return err + } + if _, err := f.Write(c.CaPEM); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + f, err = os.Create(key) + if err != nil { + return err + } + if _, err := f.Write(c.KeyPEM); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + return nil +} diff --git a/cert.go b/cert.go new file mode 100644 index 0000000..770a11d --- /dev/null +++ b/cert.go @@ -0,0 +1,139 @@ +package mad + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "math/big" + "net" + "os" + "time" +) + +type Cert struct { + CaPEM []byte + CaKeyPEM []byte + C *x509.Certificate + CertPEM []byte + KeyPEM []byte +} + +func NewCert(caPEM, caKeyPEM []byte, Organization, OrganizationalUnit string) *Cert { + c := &x509.Certificate{ + Subject: pkix.Name{ + Organization: []string{Organization}, + OrganizationalUnit: []string{OrganizationalUnit}, + }, + NotBefore: time.Date(2019, time.June, 1, 0, 0, 0, 0, time.UTC), + NotAfter: time.Now().AddDate(10, 0, 0), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + } + return &Cert{ + CaPEM: caPEM, + CaKeyPEM: caKeyPEM, + C: c, + } +} + +func (c *Cert) SetIPAddresses(ips []net.IP) { + c.C.IPAddresses = ips + if len(ips) > 0 { + c.C.Subject.CommonName = ips[0].String() + } +} + +func (c *Cert) SetDNSNames(domains []string) { + c.C.DNSNames = domains + if len(domains) > 0 { + c.C.Subject.CommonName = domains[0] + } +} + +func (c *Cert) Create() error { + tc, err := tls.X509KeyPair(c.CaPEM, c.CaKeyPEM) + if err != nil { + return err + } + ca, err := x509.ParseCertificate(tc.Certificate[0]) + if err != nil { + return err + } + + p, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return err + } + pub := p.Public() + + b, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return err + } + var spki struct { + Algorithm pkix.AlgorithmIdentifier + SubjectPublicKey asn1.BitString + } + if _, err := asn1.Unmarshal(b, &spki); err != nil { + return err + } + skid := sha1.Sum(spki.SubjectPublicKey.Bytes) + c.C.SubjectKeyId = skid[:] + + sn, err := rand.Int(rand.Reader, big.NewInt(0).Lsh(big.NewInt(1), 128)) + if err != nil { + return err + } + c.C.SerialNumber = sn + + b, err = x509.CreateCertificate(rand.Reader, c.C, ca, pub, tc.PrivateKey) + if err != nil { + return err + } + c.CertPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: b}) + // c.KeyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(p)}) + b, err = x509.MarshalPKCS8PrivateKey(p) + if err != nil { + return err + } + c.KeyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: b}) + return nil +} + +func (c *Cert) Cert() []byte { + return c.CertPEM +} + +func (c *Cert) Key() []byte { + return c.KeyPEM +} + +func (c *Cert) SaveToFile(cert, key string) error { + f, err := os.Create(cert) + if err != nil { + return err + } + if _, err := f.Write(c.CertPEM); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + f, err = os.Create(key) + if err != nil { + return err + } + if _, err := f.Write(c.KeyPEM); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + return nil +} diff --git a/cli/mad/build.sh b/cli/mad/build.sh new file mode 100755 index 0000000..3914eb7 --- /dev/null +++ b/cli/mad/build.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +if [ $# -ne 1 ]; then + echo "./build.sh version" + exit +fi + +mkdir _ + +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-w -s" -o _/mad_darwin_amd64 +CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags="-w -s" -o _/mad_freebsd_386 +CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags="-w -s" -o _/mad_freebsd_amd64 +CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags="-w -s" -o _/mad_linux_386 +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o _/mad_linux_amd64 +CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o _/mad_linux_arm64 +CGO_ENABLED=0 GOOS=netbsd GOARCH=386 go build -ldflags="-w -s" -o _/mad_netbsd_386 +CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 go build -ldflags="-w -s" -o _/mad_netbsd_amd64 +CGO_ENABLED=0 GOOS=openbsd GOARCH=386 go build -ldflags="-w -s" -o _/mad_openbsd_386 +CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -ldflags="-w -s" -o _/mad_openbsd_amd64 +CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build -ldflags="-w -s" -o _/mad_openbsd_arm64 +CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-w -s" -o _/mad_windows_amd64.exe +CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags="-w -s" -o _/mad_windows_386.exe + +mad release github.com/txthinking/mad $1 _ + +rm -rf _ diff --git a/cli/mad/main.go b/cli/mad/main.go new file mode 100644 index 0000000..5a84a0e --- /dev/null +++ b/cli/mad/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "errors" + "io/ioutil" + "log" + "net" + _ "net/http/pprof" + "os" + + "github.com/txthinking/mad" + "github.com/urfave/cli/v2" +) + +func main() { + app := cli.NewApp() + app.Name = "Mad" + app.Version = "20210401" + app.Usage = "Generate root CA and derivative certificate for any domains and any IPs" + app.Authors = []*cli.Author{ + { + Name: "Cloud", + Email: "cloud@txthinking.com", + }, + } + app.Commands = []*cli.Command{ + &cli.Command{ + Name: "ca", + Usage: "Generate CA", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "ca", + Usage: "CA file which will be created or overwritten", + Value: "ca.pem", + }, + &cli.StringFlag{ + Name: "key", + Usage: "Key file which will be created or overwritten", + Value: "ca_key.pem", + }, + &cli.StringFlag{ + Name: "organization", + Value: "github.com/txthinking/mad", + }, + &cli.StringFlag{ + Name: "organizationUnit", + Value: "github.com/txthinking/mad", + }, + &cli.StringFlag{ + Name: "commonName", + Value: "github.com/txthinking/mad", + }, + &cli.BoolFlag{ + Name: "install", + Usage: "Install CA", + }, + }, + Action: func(c *cli.Context) error { + ca := mad.NewCa(c.String("organization"), c.String("organizationUnit"), c.String("commonName")) + if err := ca.Create(); err != nil { + return err + } + if err := ca.SaveToFile(c.String("ca"), c.String("key")); err != nil { + return err + } + if c.Bool("install") { + if err := mad.Install(c.String("ca")); err != nil { + return err + } + } + return nil + }, + }, + &cli.Command{ + Name: "cert", + Usage: "Generate certificate", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "ca", + Usage: "ROOT CA file path", + Value: "ca.pem", + }, + &cli.StringFlag{ + Name: "ca_key", + Usage: "ROOT Key file path", + Value: "ca_key.pem", + }, + &cli.StringFlag{ + Name: "cert", + Usage: "Certificate file which will be created or overwritten", + Value: "cert.pem", + }, + &cli.StringFlag{ + Name: "key", + Usage: "Certificate key file which will be created or overwritten", + Value: "cert_key.pem", + }, + &cli.StringFlag{ + Name: "organization", + Value: "github.com/txthinking/mad", + }, + &cli.StringFlag{ + Name: "organizationUnit", + Value: "github.com/txthinking/mad", + }, + &cli.StringSliceFlag{ + Name: "ip", + Usage: "IP address. Repeated", + }, + &cli.StringSliceFlag{ + Name: "domain", + Usage: "Domain name. Repeated", + }, + }, + Action: func(c *cli.Context) error { + ca, err := ioutil.ReadFile(c.String("ca")) + if err != nil { + return err + } + caKey, err := ioutil.ReadFile(c.String("ca_key")) + if err != nil { + return err + } + cert := mad.NewCert(ca, caKey, c.String("organization"), c.String("organizationUnit")) + ips := make([]net.IP, 0) + for _, v := range c.StringSlice("ip") { + ip := net.ParseIP(v) + if ip == nil { + return errors.New(v + " is not an IP") + } + ips = append(ips, ip) + } + cert.SetIPAddresses(ips) + cert.SetDNSNames(c.StringSlice("domain")) + if err := cert.Create(); err != nil { + return err + } + if err := cert.SaveToFile(c.String("cert"), c.String("key")); err != nil { + return err + } + return nil + }, + }, + &cli.Command{ + Name: "install", + Usage: "Install ROOT CA", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "ca", + Usage: "CA file which will be installed", + Value: "ca.pem", + }, + }, + Action: func(c *cli.Context) error { + if err := mad.Install(c.String("ca")); err != nil { + return err + } + return nil + }, + }, + } + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6bedadc --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/txthinking/mad + +go 1.16 + +require ( + github.com/urfave/cli v1.22.5 + github.com/urfave/cli/v2 v2.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5302594 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/install.go b/install.go new file mode 100644 index 0000000..faa0377 --- /dev/null +++ b/install.go @@ -0,0 +1,10 @@ +// +build !darwin +// +build !windows + +package mad + +import "errors" + +func Install(ca string) error { + return errors.New("Unsupported your OS, PR welcome, https://github.com/txthinking/mad") +} diff --git a/install_darwin.go b/install_darwin.go new file mode 100644 index 0000000..093e368 --- /dev/null +++ b/install_darwin.go @@ -0,0 +1,11 @@ +package mad + +import "os/exec" + +func Install(ca string) error { + cmd := exec.Command("sh", "-c", "sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "+ca) + if err := cmd.Run(); err != nil { + return err + } + return nil +} diff --git a/install_windows.go b/install_windows.go new file mode 100644 index 0000000..0a20a62 --- /dev/null +++ b/install_windows.go @@ -0,0 +1,11 @@ +package mad + +import "os/exec" + +func Install(ca string) error { + cmd := exec.Command("sh", "-c", "certutil -addstore -f \"ROOT\" "+ca) + if err := cmd.Run(); err != nil { + return err + } + return nil +}