diff --git a/.gitignore b/.gitignore index 17a3443..e521273 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store cmd/scep/scep +build/ diff --git a/README.md b/README.md index af62956..331e573 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,45 @@ -SCEP server and Go library +`scep` is a Simple Certificate Enrollment Protocol server. -# Standalone server and client binaries -A standalone go server is available under `cmd/scep/main.go` +# Installation +A binary release is available on the releases page. + +# Usage + +The default flags configure and run the scep server. +depot must be the path to a folder with `ca.pem` and `ca.key` files. + +If you don't already have a CA to use, you can create one using the `scep ca` subcommand. ``` Usage of ./cmd/scep/scep: + -challenge string + enforce a challenge password -depot string path to ca folder (default "depot") -port string port to listen on (default "8080") + -version + prints version information +``` + +`scep ca -init` to create a new CA and private key. + +``` +Usage of ./cmd/scep/scep ca: + -country string + country for CA cert (default "US") + -depot string + path to ca folder (default "depot") + -init + create a new CA + -key-password string + password to store rsa key + -keySize int + rsa key size (default 4096) + -organization string + organization for CA cert (default "scep-ca") + -years int + default CA years (default 10) ``` # SCEP library diff --git a/cmd/scep/main.go b/cmd/scep/main.go index 49bb1e3..b1d9b01 100644 --- a/cmd/scep/main.go +++ b/cmd/scep/main.go @@ -1,12 +1,23 @@ package main import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "errors" "flag" "fmt" + "math/big" "net/http" "os" "os/signal" "syscall" + "time" "github.com/go-kit/kit/log" "github.com/micromdm/scep/server" @@ -20,13 +31,30 @@ var ( ) func main() { - // flags + var caCMD = flag.NewFlagSet("ca", flag.ExitOnError) + { + if len(os.Args) >= 1 { + if os.Args[1] == "ca" { + status := caMain(caCMD) + os.Exit(status) + } + } + } + + //main flags var ( flVersion = flag.Bool("version", false, "prints version information") flPort = flag.String("port", envString("SCEP_HTTP_LISTEN_PORT", "8080"), "port to listen on") flDepotPath = flag.String("depot", envString("SCEP_FILE_DEPOT", "depot"), "path to ca folder") flChallengePassword = flag.String("challenge", envString("SCEP_CHALLENGE_PASSWORD", ""), "enforce a challenge password") ) + flag.Usage = func() { + flag.PrintDefaults() + + fmt.Println("usage: scep [] []") + fmt.Println(" ca create/manage a CA") + fmt.Println("type --help to see usage for each subcommand") + } flag.Parse() // print version information @@ -88,6 +116,203 @@ func main() { logger.Log("terminated", <-errs) } +func caMain(cmd *flag.FlagSet) int { + var ( + flDepotPath = cmd.String("depot", "depot", "path to ca folder") + flInit = cmd.Bool("init", false, "create a new CA") + flYears = cmd.Int("years", 10, "default CA years") + flKeySize = cmd.Int("keySize", 4096, "rsa key size") + flOrg = cmd.String("organization", "scep-ca", "organization for CA cert") + flPassword = cmd.String("key-password", "", "password to store rsa key") + flCountry = cmd.String("country", "US", "country for CA cert") + ) + cmd.Parse(os.Args[2:]) + if *flInit { + fmt.Println("Initializing new CA") + key, err := createKey(*flKeySize, []byte(*flPassword), *flDepotPath) + if err != nil { + fmt.Println(err) + return 1 + } + if err := createCertificateAuthority(key, *flYears, *flOrg, *flCountry, *flDepotPath); err != nil { + fmt.Println(err) + return 1 + } + } + + return 0 +} + +// create a key, save it to depot and return it for further usage +func createKey(bits int, password []byte, depot string) (*rsa.PrivateKey, error) { + key, err := newRSAKey(bits) + if err != nil { + return nil, err + } + e, err := encryptedKey(key, password) + if err != nil { + return nil, err + } + + if err := os.MkdirAll(depot, 0755); err != nil { + return nil, err + } + + name := depot + "/" + "ca.key" + file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400) + if err != nil { + return nil, err + } + defer file.Close() + + if _, err := file.Write(e); err != nil { + file.Close() + os.Remove(name) + return nil, err + } + + return key, nil +} + +func createCertificateAuthority(key *rsa.PrivateKey, years int, organization string, country string, depot string) error { + var ( + authPkixName = pkix.Name{ + Country: nil, + Organization: nil, + OrganizationalUnit: []string{"SCEP CA"}, + Locality: nil, + Province: nil, + StreetAddress: nil, + PostalCode: nil, + SerialNumber: "", + CommonName: "", + } + // Build CA based on RFC5280 + authTemplate = x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: authPkixName, + // NotBefore is set to be 10min earlier to fix gap on time difference in cluster + NotBefore: time.Now().Add(-600).UTC(), + NotAfter: time.Time{}, + // Used for certificate signing only + KeyUsage: x509.KeyUsageCertSign, + + ExtKeyUsage: nil, + UnknownExtKeyUsage: nil, + + // activate CA + BasicConstraintsValid: true, + IsCA: true, + // Not allow any non-self-issued intermediate CA + MaxPathLen: 0, + + // 160-bit SHA-1 hash of the value of the BIT STRING subjectPublicKey + // (excluding the tag, length, and number of unused bits) + // **SHOULD** be filled in later + SubjectKeyId: nil, + + // Subject Alternative Name + DNSNames: nil, + + PermittedDNSDomainsCritical: false, + PermittedDNSDomains: nil, + } + ) + + subjectKeyID, err := generateSubjectKeyID(&key.PublicKey) + if err != nil { + return err + } + authTemplate.SubjectKeyId = subjectKeyID + authTemplate.NotAfter = time.Now().AddDate(years, 0, 0).UTC() + authTemplate.Subject.Country = []string{country} + authTemplate.Subject.Organization = []string{organization} + crtBytes, err := x509.CreateCertificate(rand.Reader, &authTemplate, &authTemplate, &key.PublicKey, key) + if err != nil { + return err + } + + name := depot + "/" + "ca.pem" + file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400) + if err != nil { + return err + } + defer file.Close() + + if _, err := file.Write(pemCert(crtBytes)); err != nil { + file.Close() + os.Remove(name) + return err + } + + return nil +} + +const ( + rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" + certificatePEMBlockType = "CERTIFICATE" +) + +// rsaPublicKey reflects the ASN.1 structure of a PKCS#1 public key. +type rsaPublicKey struct { + N *big.Int + E int +} + +// GenerateSubjectKeyID generates SubjectKeyId used in Certificate +// ID is 160-bit SHA-1 hash of the value of the BIT STRING subjectPublicKey +func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) { + var pubBytes []byte + var err error + switch pub := pub.(type) { + case *rsa.PublicKey: + pubBytes, err = asn1.Marshal(rsaPublicKey{ + N: pub.N, + E: pub.E, + }) + if err != nil { + return nil, err + } + default: + return nil, errors.New("only RSA public key is supported") + } + + hash := sha1.Sum(pubBytes) + + return hash[:], nil +} + +func pemCert(derBytes []byte) []byte { + pemBlock := &pem.Block{ + Type: certificatePEMBlockType, + Headers: nil, + Bytes: derBytes, + } + out := pem.EncodeToMemory(pemBlock) + return out +} + +// protect an rsa key with a password +func encryptedKey(key *rsa.PrivateKey, password []byte) ([]byte, error) { + privBytes := x509.MarshalPKCS1PrivateKey(key) + privPEMBlock, err := x509.EncryptPEMBlock(rand.Reader, rsaPrivateKeyPEMBlockType, privBytes, password, x509.PEMCipher3DES) + if err != nil { + return nil, err + } + + out := pem.EncodeToMemory(privPEMBlock) + return out, nil +} + +// create a new RSA private key +func newRSAKey(bits int) (*rsa.PrivateKey, error) { + private, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, err + } + return private, nil +} + func envString(key, def string) string { if env := os.Getenv(key); env != "" { return env diff --git a/cmd/scep/release.sh b/cmd/scep/release.sh new file mode 100755 index 0000000..069ecca --- /dev/null +++ b/cmd/scep/release.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +VERSION="0.1.0.0" +NAME=scep +OUTPUT=../../build + +echo "Building $NAME version $VERSION" + +mkdir -p ${OUTPUT} + +build() { + echo -n "=> $1-$2: " + GOOS=$1 GOARCH=$2 go build -o ${OUTPUT}/$NAME-$1-$2 -ldflags "-X main.version=$VERSION -X main.gitHash=`git rev-parse HEAD`" ./*.go + du -h ${OUTPUT}/${NAME}-$1-$2 +} + +build "darwin" "amd64" +build "linux" "amd64" diff --git a/server/service.go b/server/service.go index 92413f9..1dd3c57 100644 --- a/server/service.go +++ b/server/service.go @@ -86,6 +86,7 @@ func (svc service) PKIOperation(ctx context.Context, data []byte) ([]byte, error if err != nil { return nil, err } + // create cert template tmpl := &x509.Certificate{ SerialNumber: serial, @@ -93,8 +94,8 @@ func (svc service) PKIOperation(ctx context.Context, data []byte) ([]byte, error NotBefore: time.Now().Add(-600).UTC(), NotAfter: time.Now().AddDate(1, 0, 0).UTC(), SubjectKeyId: id, + KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{ - x509.ExtKeyUsageAny, x509.ExtKeyUsageClientAuth, }, }