Skip to content

Commit

Permalink
Clean up docs and Dockerfile
Browse files Browse the repository at this point in the history
Better docs. bfid handles files better.
  • Loading branch information
ananthb committed May 4, 2023
1 parent d888f76 commit 08d34ac
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 65 deletions.
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ FROM gcr.io/distroless/base-debian11 as issuer
# https://github.com/awslabs/aws-lambda-web-adapter
# for configuration see https://github.com/awslabs/aws-lambda-web-adapter#configurations
ARG AWS_LAMBDA_WEB_ADAPTER_VERSION=0.6.0
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:$AWS_LAMBDA_WEB_ADAPTER_VERSION /lambda-adapter /opt/extensions/lambda-adapter
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:$AWS_LAMBDA_WEB_ADAPTER_VERSION \
/lambda-adapter /opt/extensions/lambda-adapter
COPY --from=builder /build/issuer /
ENV PORT=8080
ENV READINESS_CHECK_PATH="/metrics"
Expand Down
54 changes: 23 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
![My First CA](docs/my-first-ca.jpg)

Bifrost is a minimal Certificate Authority that issues X.509 certificates meant for
mTLS client authentication. Bifrost CA does not do authenticate or check authorisation
before issuing certificates.
mTLS client authentication. Bifrost CA does not authenticate certificate signing
requests before issuance. You must authorise or control access to Bifrost CA as needed.

Bifrost CA issues certificates signed by a configured key pair and a TLS X.509 certificate.
Bifrost CA issues certificates signed by a private key and TLS X.509 certificate.
A TLS reverse proxy can use the same certificate to authenticate clients and secure
access to web applications.
Bifrost identifies clients uniquely by mapping an ECDSA public key to a UUID deterministically.
Expand All @@ -18,7 +18,7 @@ Client identity namespaces allow Bifrost to be natively multi-tenant.

Bifrost binaries are available on the [releases](https://github.com/RealImage/bifrost/releases)
page.
Container images are on <ghcr.io>.
Container images are on ghcr.io.

[bifrost-bouncer](ghcr.io/realimage/bifrost-bouncer):

Expand All @@ -45,6 +45,11 @@ In pseudo-code,

## Components

## [`bf`](cmd/bf)

`bf` is an interactive tool that generates Bifrost CA material.
It uses [Charm Cloud] to securely store your key material securely in the cloud.

### [`bfid`](cmd/bfid)

`bfid` prints the Bifrost UUID of a certificate, public key, or private key.
Expand Down Expand Up @@ -84,26 +89,19 @@ env BACKEND_URL=http://127.0.0.1:5000 ./bouncer

[OpenAPI schema](docs/issuer/openapi.yml)

`issuer` signs certificates with a configured private key and self-signed certificate.
`issuer` signs certificates with the configured certificate and its private key.
Clients must send certificate requests signed by an ECDSA P256 private key
using the ECDSA SHA256 signature algorithm.

`issuer` can read the private key and root certificate in PEM form from a variety
of sources. It looks for `crt.pem` and `key.pem` in the same directory by default.
`issuer` can read its signing certificate and private key in PEM form from a variety
of sources.
If unconfigured, it looks for `crt.pem` and `key.pem` in the current working directory.

The `BF_NS` environment variable sets the Bifrost Identifier Namespace to use.
If unset, it defaults to `bifrost.Namespace`.

`issuer` exposes prometheus format metrics at the `/metrics` path.

#### Run locally

Run `issuer` with a certificate from AWS S3 and a private key from a local file:

```bash
env CRT_URI=s3://bifrost-trust-store/crt.pem KEY_URI=./key.pem ./issuer
```

## Build

### Go toolchain
Expand All @@ -129,12 +127,7 @@ issuer:
podman build -t ghcr.io/realimage/bifrost-issuer --target=issuer .
```

## Run CA

`issuer` is the server and `curl` + `bfid` are the client.

First create the CA material.
Then pass the certificate and private key as environment variables to the binary.
## Run Issuer CA

1. Create ECDSA P256 Private Key in PEM format:

Expand All @@ -148,20 +141,19 @@ Then pass the certificate and private key as environment variables to the binary

`./issuer`

4. Generate a client key, a CSR, and get it signed by `issuer`:
4. Generate a new client key and CSR, and get it signed by `issuer`:

`openssl ecparam -out clientkey.pem -name prime256v1 -genkey -noout`

5. Create a Certificate Signing Request using the new private key:

```bash
# ecdsa private key
openssl ecparam -out clientkey.pem -name prime256v1 -genkey -noout
`openssl req -new -key clientkey.pem -sha256 -subj "/CN=$(./bfid clientkey.pem)" -out csr.pem`

# certificate request with CommonName set to UUID of public key using `bfid`
openssl req -new -key clientkey.pem -sha256 -subj "/CN=$(./bfid clientkey.pem)" -out csr.pem
6. Fetch signed certificate from the CA:

# fetch certificate
curl -X POST -H "Content-Type: text/plain" --data-binary "@csr.pem" localhost:8888/issue >clientcrt.pem
```
`curl -X POST -H "Content-Type: text/plain" --data-binary "@csr.pem" localhost:8888/issue >clientcrt.pem`

5. Admire your shiny new client certificate:
7. Admire your shiny new client certificate (optional):

`openssl x509 -in clientcrt.pem -noout -text`

Expand Down
17 changes: 16 additions & 1 deletion bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,20 @@ func ParseCertificate(der []byte) (uuid.UUID, *x509.Certificate, error) {
ErrInvalidPublicKey,
)
}
return UUID(Namespace, pubkey), cert, nil
if cert.SignatureAlgorithm != SignatureAlgorithm {
return uuid.UUID{}, nil, fmt.Errorf(
"unsupported signature algorithm: %s, %w",
cert.SignatureAlgorithm,
ErrUnsupportedAlgorithm,
)
}
id := UUID(Namespace, pubkey)
cnid, err := uuid.Parse(cert.Subject.CommonName)
if err != nil {
return uuid.UUID{}, nil, fmt.Errorf("invalid common name: %s", cert.Subject.CommonName)
}
if cnid != id {
return uuid.UUID{}, nil, ErrWrongNamespace
}
return id, cert, nil
}
61 changes: 32 additions & 29 deletions cmd/bfid/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,27 @@ import (
"fmt"
"io"
"os"
"time"

"github.com/RealImage/bifrost"
"github.com/google/uuid"
)

const (
usageHeader = `bfid prints the UUID identifier of a bifrost identity file
usageHeader = `bfid prints the UUID of a Bifrost public key, private key, or certificate.
Usage: bfid -ns=<uuid> <file.pem>
Usage:
bfid [-v] [-ns=UUID] FILE
bfid [-v] [-ns=UUID] < FILE
`
usageTrailer = ` env BF_NS takes precedence over this flag.
0 or empty string means no namespace.
usageTrailer = `
The environment variable BF_NS takes precedence over the -ns flag.
`
)

var namespace uuid.UUID
var (
namespace uuid.UUID
verbose bool
)

func init() {
flag.Usage = func() {
Expand All @@ -39,6 +43,7 @@ func init() {
var ns string
flag.StringVar(&ns, "ns", bifrost.Namespace.String(),
"Bifrost Identity Namespace")
flag.BoolVar(&verbose, "v", false, "Verbose output")
flag.Parse()
// BF_NS env var overrides the flag.
if n, ok := os.LookupEnv("BF_NS"); ok {
Expand All @@ -53,37 +58,30 @@ func init() {
}

func main() {
var filedata []byte
args := flag.Args()
if len(args) > 1 {
fmt.Fprint(os.Stderr, "too many arguments")
os.Exit(1)
}
// Read the input file or stdin.
var data []byte
var err error
switch len(flag.Args()) {
case 0:
done := make(chan struct{})
go func() {
filedata, err = io.ReadAll(os.Stdin)
close(done)
}()
select {
case <-done:
case <-time.After(time.Second * 2):
err = fmt.Errorf("timed out waiting for stdin")
}
case 1:
filedata, err = os.ReadFile(flag.Arg(0))
default:
err = fmt.Errorf("too many arguments")
if len(args) == 1 {
data, err = os.ReadFile(args[0])
} else {
data, err = io.ReadAll(os.Stdin)
}
if err != nil {
fmt.Fprintf(os.Stderr,
"error: %s, expects an identity file\n\ntry: bfid <file> or echo file | bfid\n", err)
fmt.Fprintf(os.Stderr, "error reading input: %s\n", err)
os.Exit(1)
}

block, _ := pem.Decode(filedata)
block, _ := pem.Decode(data)
if block == nil {
fmt.Fprint(os.Stderr, "no pem data found in input file\n")
os.Exit(1)
}

// Parse the key or certificate.
var pubkey *ecdsa.PublicKey
var unknownBlock bool
switch block.Type {
Expand Down Expand Up @@ -124,6 +122,11 @@ func main() {
fmt.Fprintf(os.Stderr, "error parsing key: %s\n", err)
os.Exit(1)
}

fmt.Println(bifrost.UUID(namespace, pubkey))
// Print the UUID.
id := bifrost.UUID(namespace, pubkey)
if verbose {
fmt.Printf("namespace:\t%s\nuuid:\t\t%s\n", namespace, id)
} else {
fmt.Println(id)
}
}
8 changes: 7 additions & 1 deletion cmd/bouncer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/RealImage/bifrost"
"github.com/RealImage/bifrost/internal/cafiles"
"github.com/RealImage/bifrost/internal/config"
"github.com/RealImage/bifrost/internal/stats"
"github.com/RealImage/bifrost/pkg/club"
"github.com/kelseyhightower/envconfig"
"golang.org/x/exp/slog"
Expand Down Expand Up @@ -72,8 +73,13 @@ func main() {
r.SetXForwarded()
},
}

mux := http.NewServeMux()
mux.Handle("/proxy", club.Bouncer(reverseProxy))
mux.HandleFunc("/metrics", stats.MetricsHandler)

server := http.Server{
Handler: club.Bouncer(reverseProxy),
Handler: mux,
Addr: spec.Address,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{*bifrost.X509ToTLSCertificate(crt, key)},
Expand Down
2 changes: 1 addition & 1 deletion csr.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func RequestCertificate(
return cert, nil
}

// Convert an x509.Certificate to a tls.Certificate
// X509ToTLSCertificate puts an x509.Certificate inside a tls.Certificate.
func X509ToTLSCertificate(crt *x509.Certificate, key *ecdsa.PrivateKey) *tls.Certificate {
return &tls.Certificate{
Certificate: [][]byte{
Expand Down
2 changes: 1 addition & 1 deletion pkg/club/bouncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type validity struct {
func Bouncer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
panic("request must have tls client certificate")
panic("bouncer operates on TLS connections with client certificates only")
}
peerCert := r.TLS.PeerCertificates[0]
var requestCtx RequestContext
Expand Down

0 comments on commit 08d34ac

Please sign in to comment.