diff --git a/README.md b/README.md index bfd0cad..9331aed 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/internal/cafiles/cafiles.go b/internal/cafiles/cafiles.go index 2bb9754..9587aac 100644 --- a/internal/cafiles/cafiles.go +++ b/internal/cafiles/cafiles.go @@ -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" @@ -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() @@ -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() @@ -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: