Skip to content

Commit

Permalink
feat: Issue cmd and cert validities
Browse files Browse the repository at this point in the history
Implemented issue cmd to issue mTLS certs locally.

Cert validity parsing from timespec for notBefore and notAfter.
  • Loading branch information
ananthb committed Mar 7, 2024
1 parent 43959a1 commit c1ae993
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 91 deletions.
30 changes: 29 additions & 1 deletion cmd/bf/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,34 @@ var (
Destination: &caPrivKeyUri,
}

clientPrivKeyUri string
clientPrivKeyFlag = &cli.StringFlag{
Name: "client-private-key",
Usage: "read CA private key from `FILE`",
Aliases: []string{"client-key"},
EnvVars: []string{"CLIENT_PRIVKEY", "CLIENT_KEY"},
TakesFile: true,
Value: "client-key.pem",
Destination: &clientPrivKeyUri,
}

notBeforeTime string
notBeforeFlag = &cli.StringFlag{
Name: "not-before",
Usage: "certificate valid from `TIMESPEC` (default: \"now\")",
Aliases: []string{"before"},
EnvVars: []string{"NOT_BEFORE"},
Destination: &notBeforeTime,
}
notAfterTime string
notAfterFlag = &cli.StringFlag{
Name: "not-after",
Usage: "certificate valid until `TIMESPEC` (default: \"+1h\")",
Aliases: []string{"after"},
EnvVars: []string{"NOT_AFTER"},
Destination: &notAfterTime,
}

outputFile string
outputFlag = &cli.StringFlag{
Name: "output",
Expand All @@ -56,7 +84,7 @@ var (
)

func getOutputWriter() (io.Writer, error) {
if outputFile == "-" {
if outputFile == "" || outputFile == "-" {
return os.Stdout, nil
}
return os.Create(outputFile)
Expand Down
111 changes: 68 additions & 43 deletions cmd/bf/issue.go
Original file line number Diff line number Diff line change
@@ -1,54 +1,79 @@
package main

import (
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"time"

"github.com/RealImage/bifrost/cafiles"
"github.com/RealImage/bifrost/tinyca"
"github.com/urfave/cli/v2"
)

var (
notBefore cli.Timestamp
notAfter cli.Timestamp

issueCmd = &cli.Command{
Name: "issue",
Flags: []cli.Flag{
caCertFlag,
caPrivKeyFlag,
&cli.TimestampFlag{
Name: "not-before",
Usage: "issue certificates valid from `TIMESTAMP`",
Aliases: []string{"before"},
EnvVars: []string{"NOT_BEFORE"},
Value: cli.NewTimestamp(time.Now()),
Destination: &notBefore,
},
&cli.TimestampFlag{
Name: "not-after",
Usage: "issue certificates valid until `TIMESTAMP`",
Aliases: []string{"after"},
EnvVars: []string{"NOT_AFTER"},
Value: cli.NewTimestamp(time.Now().AddDate(0, 0, 1)),
Destination: &notAfter,
var issueCmd = &cli.Command{
Name: "issue",
Flags: []cli.Flag{
caCertFlag,
caPrivKeyFlag,
clientPrivKeyFlag,
notBeforeFlag,
notAfterFlag,
outputFlag,
},

Action: func(cliCtx *cli.Context) error {
ctx := cliCtx.Context
caCert, caKey, err := cafiles.GetCertKey(ctx, caCertUri, caPrivKeyUri)
if err != nil {
return cli.Exit(fmt.Sprintf("Error reading cert/key: %s", err), 1)
}

ca, err := tinyca.New(caCert, caKey)
if err != nil {
return cli.Exit(fmt.Sprintf("Error creating CA: %s", err), 1)
}

clientKey, err := cafiles.GetPrivateKey(ctx, clientPrivKeyUri)
if err != nil {
return cli.Exit(fmt.Sprintf("Error reading client key: %s", err), 1)
}

csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
Subject: pkix.Name{
Organization: []string{caCert.Namespace.String()},
CommonName: clientKey.UUID(caCert.Namespace).String(),
},
},

Action: func(cliCtx *cli.Context) error {
ctx := cliCtx.Context
cert, key, err := cafiles.GetCertKey(ctx, caCertUri, caPrivKeyUri)
if err != nil {
return cli.Exit(fmt.Sprintf("Error reading cert/key: %s", err), 1)
}

_, err = tinyca.New(cert, key)
if err != nil {
return cli.Exit(fmt.Sprintf("Error creating CA: %s", err), 1)
}

return nil
},
}
)
}, clientKey)
if err != nil {
return cli.Exit(fmt.Sprintf("Error creating certificate request: %s", err), 1)
}

notBefore, notAfter, err := tinyca.ParseValidity(notBeforeTime, notAfterTime)
if err != nil {
return cli.Exit(fmt.Sprintf("Error parsing validity: %s", err), 1)
}

template := tinyca.TLSClientCertTemplate(notBefore, notAfter)

cert, err := ca.IssueCertificate(csr, template)
if err != nil {
return cli.Exit(fmt.Sprintf("Error issuing certificate: %s", err), 1)
}

out, err := getOutputWriter()
if err != nil {
return cli.Exit(fmt.Sprintf("Error getting output writer: %s", err), 1)
}

block := &pem.Block{
Type: "CERTIFICATE",
Bytes: cert,
}

fmt.Fprint(out, string(pem.EncodeToMemory(block)))

return nil
},
}
87 changes: 60 additions & 27 deletions cmd/bf/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import (
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"time"

"github.com/RealImage/bifrost"
"github.com/RealImage/bifrost/cafiles"
"github.com/RealImage/bifrost/tinyca"
"github.com/google/uuid"
"github.com/urfave/cli/v2"
)
Expand Down Expand Up @@ -38,8 +37,8 @@ var newCmd = &cli.Command{
},
},
{
Name: "identity",
Aliases: []string{"id"},
Name: "private-key",
Aliases: []string{"key", "pk", "pkey"},
Usage: "Create a new identity",
Flags: []cli.Flag{
outputFlag,
Expand All @@ -66,48 +65,82 @@ var newCmd = &cli.Command{
return nil
},
},
{
Name: "certificate-request",
Aliases: []string{"csr", "req"},
Flags: []cli.Flag{
nsFlag,
clientPrivKeyFlag,
outputFlag,
},
Usage: "Create a new certificate request",
Action: func(c *cli.Context) error {
if namespace == uuid.Nil {
return fmt.Errorf("namespace is required")
}

key, err := cafiles.GetPrivateKey(c.Context, clientPrivKeyUri)
if err != nil {
return err
}

csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
Subject: pkix.Name{
Organization: []string{namespace.String()},
CommonName: key.UUID(namespace).String(),
},
}, key)
if err != nil {
return err
}

out, err := getOutputWriter()
if err != nil {
return err
}

block := &pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csr,
}
fmt.Fprint(out, string(pem.EncodeToMemory(block)))

return nil
},
},
{
Name: "ca-certificate",
Aliases: []string{"ca-cert", "ca"},
Flags: []cli.Flag{
nsFlag,
caPrivKeyFlag,
outputFlag,
&cli.DurationFlag{
Name: "validity",
Usage: "certificate `VALIDITY`",
Value: time.Hour * 24 * 365,
},
notBeforeFlag,
notAfterFlag,
},
Usage: "Create a new certificate authority signing certificate",
Action: func(c *cli.Context) error {
if namespace == uuid.Nil {
return fmt.Errorf("namespace is required")
}

key, err := cafiles.GetPrivateKey(c.Context, caPrivKeyUri)
if err != nil {
return err
}

notBefore := time.Now()
notAfter := notBefore.Add(c.Duration("validity"))

// Create root certificate.
template := x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{
CommonName: key.UUID(namespace).String(),
Organization: []string{namespace.String()},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLenZero: true,
id := key.UUID(namespace)
notBefore, notAfter, err := tinyca.ParseValidity(notBeforeTime, notAfterTime)
if err != nil {
return err
}

template := tinyca.CACertTemplate(notBefore, notAfter, namespace, id)

certDer, err := x509.CreateCertificate(
rand.Reader,
&template,
&template,
template,
template,
key.PublicKey().PublicKey,
key,
)
Expand Down
33 changes: 13 additions & 20 deletions tinyca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,19 @@ func (ca CA) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ca.requestsTotal.Inc()
startTime := time.Now()

notBefore := time.Now()
if nb := r.URL.Query().Get("not-before"); nb != "" {
var err error
if notBefore, err = time.Parse(time.RFC3339, nb); err != nil {
http.Error(w, "invalid not-before query parameter", http.StatusBadRequest)
return
}
nb := r.URL.Query().Get("not-before")
if nb == "" {
nb = "now"
}
na := r.URL.Query().Get("not-after")
if na == "" {
na = "+1h"
}

notAfter := notBefore.AddDate(0, 0, 1)
if na := r.URL.Query().Get("not-after"); na != "" {
var err error
if notAfter, err = time.Parse(time.RFC3339, na); err != nil {
http.Error(w, "invalid not-after query parameter", http.StatusBadRequest)
return
}
notBefore, notAfter, err := ParseValidity(nb, na)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

contentType, _, err := webapp.GetContentType(r.Header, webapp.MimeTypeText)
Expand All @@ -112,12 +109,8 @@ func (ca CA) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

template := &x509.Certificate{
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
NotBefore: notBefore,
NotAfter: notAfter,
}
template := TLSClientCertTemplate(notBefore, notAfter)

cert, err := ca.IssueCertificate(csr, template)
if err != nil {
statusCode := http.StatusInternalServerError
Expand Down
32 changes: 32 additions & 0 deletions tinyca/templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package tinyca

import (
"crypto/x509"
"crypto/x509/pkix"
"time"

"github.com/google/uuid"
)

func TLSClientCertTemplate(nb, na time.Time) *x509.Certificate {
return &x509.Certificate{
NotBefore: nb,
NotAfter: na,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
}

func CACertTemplate(nb, na time.Time, ns, id uuid.UUID) *x509.Certificate {
return &x509.Certificate{
Subject: pkix.Name{
Organization: []string{ns.String()},
CommonName: id.String(),
},
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLenZero: true,
}
}
Loading

0 comments on commit c1ae993

Please sign in to comment.