Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support file://, s3://, and ARNs for loading files #4

Merged
merged 2 commits into from
Dec 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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