Skip to content

Commit

Permalink
Support file://, s3://, and ARNs for loading files (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
ananthb authored Dec 18, 2022
1 parent 42132f9 commit 40b0ac6
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 10 deletions.
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Bifrost

[![CI](https://github.com/RealImage/WireApp/actions/workflows/ci.yaml/badge.svg)](https://github.com/RealImage/WireApp/actions/workflows/ci.yaml)
[![CI](https://github.com/RealImage/bifrost/actions/workflows/ci.yaml/badge.svg)](https://github.com/RealImage/bifrost/actions/workflows/ci.yaml)

Bifrost is a tiny mTLS authentication toolkit.
The CA [`issuer`](#issuer) issues signed certificates.
Expand Down Expand Up @@ -128,28 +128,30 @@ API Gateway mTLS expects an `s3://` uri that points to a PEM certificate bundle.
Client certificates must be signed with at least one of the certificates from the bundle.
This allows API Gateway and `issuer` to share the same certificate PEM bundle.

##### Key Rotation
##### Zero Downtime Key Rotation

Assume that an s3 bucket, `bifrost-trust-store` exists, with versioning turned on.

s3://bifrost-trust-store:

- crt.pem
- key.pem

crt.pem contains one or more PEM encoded root certificates.
key.pem contains exactly one PEM encoded private key that corresponds to the first certificate in crt.pem.

The corresponding private key for `crt.pem` is stored as in AWS Secrets Manager and identified
here as `key.pem`.
`key.pem` contains exactly one PEM encoded private key that pairs with the first certificate in `crt.pem`.

To replace the current signing certificate and key:

1. Create the new ECDSA key-pair and self-signed certificate.
2. Create a new revision of `s3://bifrost-trust-store/crt.pem` containing the new certificate as the first in the file.
3. Create a new revision of `s3://bifrost-trust-store/key.pem` replacing its contents entirely with that of the new key.
2. Create a new revision of `s3://bifrost-trust-store/crt.pem` adding the new certificate as the first in the file, with older certificates immediately below it. Each cerificate should be separated by a newline.
3. Create a new revision of `key.pem` in Secrets Manager containing the newly generated key in PEM encoded ASN.1 DER form.

API Gateway will pick up the updated client trust bundle in crt.pem.
This allows it to trust certificates issued with the new certificate as well as any older certificates.
This allows it to trust certificates issued with the new certificate in addition to all of the previous certificates that may exist.
Bifrost issuer only uses the first certificate from crt.pem along with key.pem, so it will start issuing
certificates with the new root.
certificates with the new root certificate once its configuration has been updated.

### Build

Expand Down
24 changes: 22 additions & 2 deletions internal/cafiles/cafiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/secretsmanager"
Expand All @@ -22,6 +23,8 @@ import (
const getTImeout = time.Minute

// GetCertificate retrieves a PEM encoded certificate from uri.
// uri can be one of a relative or absolute file path, file://... uri, s3://... uri,
// or an AWS S3 or AWS Secrets Manager ARN.
func GetCertificate(ctx context.Context, uri string) (*x509.Certificate, error) {
ctx, cancel := context.WithTimeout(ctx, getTImeout)
defer cancel()
Expand All @@ -40,6 +43,8 @@ func GetCertificate(ctx context.Context, uri string) (*x509.Certificate, error)
}

// GetPrivateKey retrieves a PEM encoded private key from uri.
// uri can be one of a relative or absolute file path, file://... uri, s3://... uri,
// or an AWS S3 or AWS Secrets Manager ARN.
func GetPrivateKey(ctx context.Context, uri string) (*ecdsa.PrivateKey, error) {
ctx, cancel := context.WithTimeout(ctx, getTImeout)
defer cancel()
Expand All @@ -60,14 +65,29 @@ func GetPrivateKey(ctx context.Context, uri string) (*ecdsa.PrivateKey, error) {
func getPemFile(ctx context.Context, uri string) ([]byte, error) {
url, err := url.Parse(uri)
if err != nil {
return nil, err
return nil, fmt.Errorf("error parsing file uri %w", err)
}
var pemData []byte
switch s := url.Scheme; s {
case "s3":
pemData, err = getS3Key(ctx, url.Host, url.Path[1:])
case "arn":
pemData, err = getSecret(ctx, uri)
// s3 and secretsmanager arns are supported
parsedArn, err := arn.Parse(uri)
if err != nil {
return nil, fmt.Errorf("error parsing arn %w", err)
}
switch svc := parsedArn.Service; svc {
case "s3":
pemData, err = getS3Key(ctx, url.Host, url.Path[1:])
case "secretsmanager":
pemData, err = getSecret(ctx, uri)
default:
return nil, fmt.Errorf("cannot load pem file from %s", svc)
}
if err != nil {
return nil, fmt.Errorf("error fetching pem file: %w", err)
}
case "", "file":
pemData, err = os.ReadFile(url.Path)
default:
Expand Down

0 comments on commit 40b0ac6

Please sign in to comment.