diff --git a/CHANGELOG.md b/CHANGELOG.md index 36c96e21..322efcbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,18 @@ ### Not released yet +BREAKING-CHANGES: + +* container/seal: introduce a naming convention for identity and container keys. [#89](https://github.com/elastic/harp/pull/89) + CHANGES: +* container/seal: FIPS compatible container sealing process (ECDH+AES256-CTR+HMAC-SHA384 / ECDSA P-384 / HMAC-SHA512). [#89](https://github.com/elastic/harp/pull/89) * crypto/paseto: move PASETO v4 primitives to `sdk/security/paseto/v4`. [#87](https://github.com/elastic/harp/pull/87) DIST: +* go: Build with Golang 1.17.4. * nix/shell: Expose `shell.nix` to get a consistent development environment. [#87](https://github.com/elastic/harp/pull/87) ## 0.2.2 diff --git a/FEATURES.md b/FEATURES.md index 9b425f48..a80f0f42 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,43 +1,45 @@ # Features overview -* [Glossary](#glossary) -* [Bundle management](#bundle-management) - * [Features](#features) - * [Pipelines](#pipelines) -* [Template Engine](#template-engine) - * [Render a template](#render-a-template) - * [Set external values](#set-external-values) - * [Load values from file](#load-values-from-file) - * [Value object debugger](#value-object-debugger) - * [Load from different filetypes](#load-from-different-filetypes) -* [Secret Bundle](#secret-bundle) - * [Create a bundle from template](#create-a-bundle-from-template) - * [Create a bundle from a JSON map](#create-a-bundle-from-a-json-map) - * [Read a secret value](#read-a-secret-value) - * [Patch a bundle](#patch-a-bundle) - * [Calculate a bundle difference](#calculate-a-bundle-difference) - * [Dump a secret bundle](#dump-a-secret-bundle) - * [Encrypt secret values](#encrypt-secret-values) - * [Decrypt secret values](#decrypt-secret-values) - * [Linter / Structure checker](#linter--structure-checker) - * [Check that all packages are CSO compliant](#check-that-all-packages-are-cso-compliant) - * [Validate a secret structure](#validate-a-secret-structure) - * [Generate a ruleset from a bundle](#generate-a-ruleset-from-a-bundle) -* [Secret Container](#secret-container) - * [Seal a secret container](#seal-a-secret-container) - * [Create an identity](#create-an-identity) - * [Use a passphrase as private key protection](#use-a-passphrase-as-private-key-protection) - * [Ephemeral Container Key](#ephemeral-container-key) - * [Deterministic Container Key](#deterministic-container-key) - * [Recover a container key from indentity](#recover-a-container-key-from-indentity) - * [Unseal a secret container](#unseal-a-secret-container) -* [Vault specific commands](#vault-specific-commands) - * [Export a complete secret backend from Vault](#export-a-complete-secret-backend-from-vault) - * [Import a bundle in a target secret backend in Vault](#import-a-bundle-in-a-target-secret-backend-in-vault) - * [Share simple secret between 2 users](#share-simple-secret-between-2-users) - * [Share a container](#share-a-container) - * [Prepare a secret bundle for an ephemeral worker](#prepare-a-secret-bundle-for-an-ephemeral-worker) - * [Use Vault in\-transit key to encrypt a container identity](#use-vault-in-transit-key-to-encrypt-a-container-identity) +- [Features overview](#features-overview) + - [Glossary](#glossary) + - [Bundle management](#bundle-management) + - [Features](#features) + - [Pipelines](#pipelines) + - [Template Engine](#template-engine) + - [Render a template](#render-a-template) + - [Set external values](#set-external-values) + - [Load values from file](#load-values-from-file) + - [Value object debugger](#value-object-debugger) + - [Load from different filetypes](#load-from-different-filetypes) + - [Secret Bundle](#secret-bundle) + - [Create a bundle from template](#create-a-bundle-from-template) + - [Create a bundle from a JSON map](#create-a-bundle-from-a-json-map) + - [Read a secret value](#read-a-secret-value) + - [Example](#example) + - [Patch a bundle](#patch-a-bundle) + - [Calculate a bundle difference](#calculate-a-bundle-difference) + - [Dump a secret bundle](#dump-a-secret-bundle) + - [Encrypt secret values](#encrypt-secret-values) + - [Decrypt secret values](#decrypt-secret-values) + - [Linter / Structure checker](#linter--structure-checker) + - [Check that all packages are CSO compliant](#check-that-all-packages-are-cso-compliant) + - [Validate a secret structure](#validate-a-secret-structure) + - [Generate a ruleset from a bundle](#generate-a-ruleset-from-a-bundle) + - [Secret Container](#secret-container) + - [Seal a secret container](#seal-a-secret-container) + - [Create an identity](#create-an-identity) + - [Use a passphrase as private key protection](#use-a-passphrase-as-private-key-protection) + - [Ephemeral Container Key](#ephemeral-container-key) + - [Deterministic Container Key](#deterministic-container-key) + - [Recover a container key from identity](#recover-a-container-key-from-identity) + - [Unseal a secret container](#unseal-a-secret-container) + - [Vault specific commands](#vault-specific-commands) + - [Export a complete secret backend from Vault](#export-a-complete-secret-backend-from-vault) + - [Import a bundle in a target secret backend in Vault](#import-a-bundle-in-a-target-secret-backend-in-vault) + - [Share simple secret between 2 users](#share-simple-secret-between-2-users) + - [Share a container](#share-a-container) + - [Prepare a secret bundle for an ephemeral worker](#prepare-a-secret-bundle-for-an-ephemeral-worker) + - [Use Vault in-transit key to encrypt a container identity](#use-vault-in-transit-key-to-encrypt-a-container-identity) ## Glossary @@ -79,33 +81,33 @@ and reproductible. ### Pipelines `harp` allows you to handle secret using deterministic pipelines expressed -using a serie of atomic cli operations. +using series of atomic cli operations. ![Pipelines](docs/harp/img/SM-HARP.png) > The main objective is to reach as soon as possible the harp native > container to be used by the harp core cli. > If you need to pull or push secret from / to external secret storage engine, -> just use the SDK du generate a harp plugin to pull secret and store +> just use the SDK to generate a harp plugin to pull secret and store > them as a harp container. ## Template Engine The provided template engine is used to describe and implement the value generation algorithms. You can use it for secret data generation but also for -various other usecases. [Sample usecases](./samples/onboarding/1-template-engine/9-usecases.md) +various other use cases. [Sample use cases](./docs/onboarding/1-template-engine/9-usecases.md) -As input you can take almost anything which is a string stream. +As an input you can take almost anything which is a string stream. > For more information about the template engine, please read the dedicated -> section - [Template Engine](./samples/onboarding/1-template-engine/1-introduction.md) +> section - [Template Engine](./docs/onboarding/1-template-engine/1-introduction.md) ### Render a template `harp` exposes data generation function used to generate the data according to the specification described by the user. -> Generate an EC P-256 curve keypar, and output the public key using JWK encoding +> Generate an EC P-256 curve keypair, and output the public key using JWK encoding ```sh echo '{{ $key := cryptoPair "ec:p256" }}{{ $key.Public | toJwk }}' | harp template @@ -223,7 +225,7 @@ $ harp values ## Secret Bundle -The `SecretBundle` [object](./samples/onboarding/3-secret-bundle/2-bundle.md) is +The `SecretBundle` [object](./docs/onboarding/3-secret-bundle/2-bundle.md) is used to represent the secret tree mapped using a K/V store. ### Create a bundle from template @@ -889,7 +891,7 @@ Container key : .... * The `dckd-target` flag defines an arbitry string acting as a salt for Key Derivation Function. -### Recover a container key from indentity +### Recover a container key from identity When the container key is lost, you can use attached one of identity private keys to unseal the container. @@ -901,7 +903,7 @@ $ harp container recover --identity recovery.json --passphrase $(cat passphrase. Container key : mPjzX1A5PcGtZ0nacxkhjl0pZE8XYw84KYF5NO6jhVA ``` -Fo Vault recovery : +For Vault recovery : ```sh harp container recover --vault-transit-key harp --identity recovery.json diff --git a/README.md b/README.md index 4edac338..f3ce19cc 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ pipeline execution. ## What can I do? -> New to harp, let's start with [onboarding tutorial](samples/onboarding/README.md) ! +> New to harp, let's start with [onboarding tutorial](docs/onboarding/README.md) ! > TL;DR - [Features overview](FEATURES.md) Harp provides : @@ -208,7 +208,7 @@ $ export HARP_REPOSITORY=$(pwd)/harp ```sh $ go version -go version go1.17.3 linux/amd64 +go version go1.17.4 linux/amd64 ``` > Simple go version manager - diff --git a/api/gen/go/cso/v1/validator_api_grpc.pb.go b/api/gen/go/cso/v1/validator_api_grpc.pb.go index e537f11c..67971d08 100644 --- a/api/gen/go/cso/v1/validator_api_grpc.pb.go +++ b/api/gen/go/cso/v1/validator_api_grpc.pb.go @@ -58,21 +58,19 @@ func (c *validatorAPIClient) Validate(ctx context.Context, in *ValidateRequest, } // ValidatorAPIServer is the server API for ValidatorAPI service. -// All implementations must embed UnimplementedValidatorAPIServer +// All implementations should embed UnimplementedValidatorAPIServer // for forward compatibility type ValidatorAPIServer interface { // Validate given path according to CSO sepcification. Validate(context.Context, *ValidateRequest) (*ValidateResponse, error) - mustEmbedUnimplementedValidatorAPIServer() } -// UnimplementedValidatorAPIServer must be embedded to have forward compatible implementations. +// UnimplementedValidatorAPIServer should be embedded to have forward compatible implementations. type UnimplementedValidatorAPIServer struct{} func (UnimplementedValidatorAPIServer) Validate(context.Context, *ValidateRequest) (*ValidateResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Validate not implemented") } -func (UnimplementedValidatorAPIServer) mustEmbedUnimplementedValidatorAPIServer() {} // UnsafeValidatorAPIServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ValidatorAPIServer will diff --git a/api/gen/go/harp/bundle/v1/bundle_api_grpc.pb.go b/api/gen/go/harp/bundle/v1/bundle_api_grpc.pb.go index a8c67494..59cc3916 100644 --- a/api/gen/go/harp/bundle/v1/bundle_api_grpc.pb.go +++ b/api/gen/go/harp/bundle/v1/bundle_api_grpc.pb.go @@ -58,21 +58,19 @@ func (c *bundleAPIClient) GetSecret(ctx context.Context, in *GetSecretRequest, o } // BundleAPIServer is the server API for BundleAPI service. -// All implementations must embed UnimplementedBundleAPIServer +// All implementations should embed UnimplementedBundleAPIServer // for forward compatibility type BundleAPIServer interface { // GetSecret returns the matching RAW secret value according to requested path. GetSecret(context.Context, *GetSecretRequest) (*GetSecretResponse, error) - mustEmbedUnimplementedBundleAPIServer() } -// UnimplementedBundleAPIServer must be embedded to have forward compatible implementations. +// UnimplementedBundleAPIServer should be embedded to have forward compatible implementations. type UnimplementedBundleAPIServer struct{} func (UnimplementedBundleAPIServer) GetSecret(context.Context, *GetSecretRequest) (*GetSecretResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetSecret not implemented") } -func (UnimplementedBundleAPIServer) mustEmbedUnimplementedBundleAPIServer() {} // UnsafeBundleAPIServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to BundleAPIServer will diff --git a/api/gen/go/harp/container/v1/container.pb.go b/api/gen/go/harp/container/v1/container.pb.go index 62a381ca..9ed46ee4 100644 --- a/api/gen/go/harp/container/v1/container.pb.go +++ b/api/gen/go/harp/container/v1/container.pb.go @@ -56,6 +56,8 @@ type Header struct { ContainerBox []byte `protobuf:"bytes,4,opt,name=container_box,json=containerBox,proto3" json:"container_box,omitempty"` // Recipient list for identity bound secret container. Recipients []*Recipient `protobuf:"bytes,6,rep,name=recipients,proto3" json:"recipients,omitempty"` + // Seal strategy + SealVersion uint32 `protobuf:"varint,7,opt,name=seal_version,json=sealVersion,proto3" json:"seal_version,omitempty"` } func (x *Header) Reset() { @@ -125,6 +127,13 @@ func (x *Header) GetRecipients() []*Recipient { return nil } +func (x *Header) GetSealVersion() uint32 { + if x != nil { + return x.SealVersion + } + return 0 +} + // Recipient describes container recipient informations. type Recipient struct { state protoimpl.MessageState @@ -247,7 +256,7 @@ var file_harp_container_v1_container_proto_rawDesc = []byte{ 0x0a, 0x21, 0x68, 0x61, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x68, 0x61, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x22, 0xed, 0x01, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, + 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x22, 0x90, 0x02, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x21, 0x0a, 0x0c, @@ -262,28 +271,30 @@ var file_harp_container_v1_container_proto_rawDesc = []byte{ 0x70, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x68, 0x61, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x0a, 0x72, 0x65, 0x63, 0x69, - 0x70, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x3d, 0x0a, 0x09, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, - 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x52, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x61, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x07, - 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x72, 0x61, 0x77, 0x42, 0xb1, 0x01, 0x0a, 0x2d, 0x63, 0x6f, - 0x6d, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x65, 0x6c, 0x61, 0x73, 0x74, 0x69, 0x63, - 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x73, 0x65, 0x63, 0x2e, 0x68, 0x61, 0x72, 0x70, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x42, 0x0e, 0x43, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x40, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x6c, 0x61, 0x73, 0x74, 0x69, - 0x63, 0x2f, 0x68, 0x61, 0x72, 0x70, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, - 0x6f, 0x2f, 0x68, 0x61, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x76, 0x31, 0xa2, - 0x02, 0x03, 0x53, 0x43, 0x58, 0xaa, 0x02, 0x11, 0x68, 0x61, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x11, 0x68, 0x61, 0x72, 0x70, - 0x5c, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x70, 0x69, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x61, 0x6c, 0x5f, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x73, 0x65, + 0x61, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3d, 0x0a, 0x09, 0x52, 0x65, 0x63, + 0x69, 0x70, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x52, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x68, 0x61, 0x72, 0x70, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x61, + 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x72, 0x61, 0x77, 0x42, 0xb1, 0x01, 0x0a, + 0x2d, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x65, 0x6c, 0x61, 0x73, + 0x74, 0x69, 0x63, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x73, 0x65, 0x63, 0x2e, 0x68, 0x61, 0x72, + 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x42, 0x0e, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x6c, 0x61, + 0x73, 0x74, 0x69, 0x63, 0x2f, 0x68, 0x61, 0x72, 0x70, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, + 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x68, 0x61, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x76, 0x31, 0xa2, 0x02, 0x03, 0x53, 0x43, 0x58, 0xaa, 0x02, 0x11, 0x68, 0x61, 0x72, 0x70, 0x2e, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x11, 0x68, + 0x61, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5c, 0x56, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/api/proto/harp/container/v1/container.proto b/api/proto/harp/container/v1/container.proto index 1f1347e7..5173e2a2 100644 --- a/api/proto/harp/container/v1/container.proto +++ b/api/proto/harp/container/v1/container.proto @@ -41,6 +41,8 @@ message Header { bytes container_box = 4; // Recipient list for identity bound secret container. repeated Recipient recipients = 6; + // Seal strategy + uint32 seal_version = 7; } // Recipient describes container recipient informations. diff --git a/build/fips/fips.go b/build/fips/fips.go new file mode 100644 index 00000000..219f900e --- /dev/null +++ b/build/fips/fips.go @@ -0,0 +1,24 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build fips + +package fips + +func Enabled() bool { + return true +} diff --git a/build/fips/non_fips.go b/build/fips/non_fips.go new file mode 100644 index 00000000..15a784de --- /dev/null +++ b/build/fips/non_fips.go @@ -0,0 +1,27 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build !fips + +package fips + +import "os" + +func Enabled() bool { + // Get from env. + return os.Getenv("HARP_FIPS_MODE") != "" +} diff --git a/build/mage/golang/build.go b/build/mage/golang/build.go index 64de8689..fc6aa37c 100644 --- a/build/mage/golang/build.go +++ b/build/mage/golang/build.go @@ -83,6 +83,7 @@ func GOARM(value string) BuildOption { // ----------------------------------------------------------------------------- // Build the given binary using the given package. +//nolint:funlen // to refactor func Build(name, packageName, version string, opts ...BuildOption) func() error { const ( defaultCgoEnabled = false @@ -116,6 +117,14 @@ func Build(name, packageName, version string, opts ...BuildOption) func() error // Compilation flags compilationFlags := []string{} + // Check if fips is enabled + buildTags := "-tags=!fips" + if os.Getenv("HARP_BUILD_FIPS_MODE") == "1" { + artifactName = fmt.Sprintf("%s-fips", artifactName) + compilationFlags = append(compilationFlags, "fips") + buildTags = "-tags=fips" + } + // Check if CGO is enabled if defaultOpts.cgoEnabled { artifactName = fmt.Sprintf("%s-cgo", artifactName) @@ -178,6 +187,6 @@ func Build(name, packageName, version string, opts ...BuildOption) func() error filename = fmt.Sprintf("%s.exe", filename) } - return sh.RunWith(env, "go", "build", buildMode, "-trimpath", "-mod=readonly", "-ldflags", ldflagsValue, "-o", filename, packageName) + return sh.RunWith(env, "go", "build", buildMode, buildTags, "-trimpath", "-mod=readonly", "-ldflags", ldflagsValue, "-o", filename, packageName) } } diff --git a/build/mage/golang/init.go b/build/mage/golang/init.go index ff69bf70..91cb5e89 100644 --- a/build/mage/golang/init.go +++ b/build/mage/golang/init.go @@ -27,8 +27,8 @@ import ( // Keep only last 2 versions var goVersions = []string{ - "~1.17.3", - "~1.16.10", + "~1.17.4", + "~1.16.11", } func init() { diff --git a/build/version/version.go b/build/version/version.go index 9f3abfc9..699ae0de 100644 --- a/build/version/version.go +++ b/build/version/version.go @@ -24,6 +24,8 @@ import ( "runtime/debug" "github.com/dchest/uniuri" + + "github.com/elastic/harp/build/fips" ) // Build information. Populated at build-time. @@ -71,6 +73,10 @@ type Info struct { // Full returns full composed version string func (i *Info) String() string { + if fips.Enabled() { + return fmt.Sprintf("%s [%s:%s] (Go: %s, FIPS Mode, Flags: %s, Date: %s)", i.Version, i.GitBranch, i.GitCommit, i.GoVersion, i.BuildTags, BuildDate) + } + return fmt.Sprintf("%s [%s:%s] (Go: %s, Flags: %s, Date: %s)", i.Version, i.GitBranch, i.GitCommit, i.GoVersion, i.BuildTags, BuildDate) } diff --git a/cmd/harp/internal/cmd/container_identity.go b/cmd/harp/internal/cmd/container_identity.go index 6624dc1d..1b6ca996 100644 --- a/cmd/harp/internal/cmd/container_identity.go +++ b/cmd/harp/internal/cmd/container_identity.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/cobra" "go.uber.org/zap" + "github.com/elastic/harp/build/fips" "github.com/elastic/harp/pkg/sdk/cmdutil" "github.com/elastic/harp/pkg/sdk/log" "github.com/elastic/harp/pkg/sdk/value" @@ -38,6 +39,7 @@ type containerIdentityParams struct { passPhrase string vaultTransitPath string vaultTransitKey string + version uint } var containerIdentityCmd = func() *cobra.Command { @@ -78,6 +80,7 @@ var containerIdentityCmd = func() *cobra.Command { OutputWriter: cmdutil.FileWriter(params.outputPath), Description: params.description, Transformer: transformer, + Version: container.IdentityVersion(params.version + 1), } // Run the task @@ -87,6 +90,12 @@ var containerIdentityCmd = func() *cobra.Command { }, } + // Select default identity version. + identityVersion := uint(container.ModernIdentity) + if fips.Enabled() { + identityVersion = uint(container.NISTIdentity) + } + // Flags cmd.Flags().StringVar(¶ms.outputPath, "out", "", "Identity information output ('-' for stdout or filename)") cmd.Flags().StringVar(¶ms.key, "key", "", "Transformer key") @@ -95,6 +104,6 @@ var containerIdentityCmd = func() *cobra.Command { cmd.Flags().StringVar(¶ms.vaultTransitKey, "vault-transit-key", "", "Use Vault transit encryption to protect identity private key") cmd.Flags().StringVar(¶ms.description, "description", "", "Identity description") log.CheckErr("unable to mark 'description' flag as required.", cmd.MarkFlagRequired("description")) - + cmd.Flags().UintVar(¶ms.version, "version", identityVersion-1, "Select identity version (0:legacy, 1:modern, 2:nist)") return cmd } diff --git a/cmd/harp/internal/cmd/container_seal.go b/cmd/harp/internal/cmd/container_seal.go index 89140bc6..4204e068 100644 --- a/cmd/harp/internal/cmd/container_seal.go +++ b/cmd/harp/internal/cmd/container_seal.go @@ -18,15 +18,15 @@ package cmd import ( - "encoding/base64" - - "github.com/awnumar/memguard" "github.com/spf13/cobra" "go.uber.org/zap" + "github.com/elastic/harp/build/fips" "github.com/elastic/harp/pkg/container/identity" + "github.com/elastic/harp/pkg/container/identity/key" "github.com/elastic/harp/pkg/sdk/cmdutil" "github.com/elastic/harp/pkg/sdk/log" + "github.com/elastic/harp/pkg/sdk/types" "github.com/elastic/harp/pkg/tasks/container" ) @@ -41,6 +41,7 @@ type containerSealParams struct { target string noContainerIdentity bool jsonOutput bool + sealVersion uint } var containerSealCmd = func() *cobra.Command { @@ -54,71 +55,57 @@ var containerSealCmd = func() *cobra.Command { ctx, cancel := cmdutil.Context(cmd.Context(), "harp-container-seal", conf.Debug.Enable, conf.Instrumentation.Logs.Level) defer cancel() + var sealingPublicKeys types.StringArray + // Load idenity from files - if len(params.identityFilePaths) > 0 { - for _, f := range params.identityFilePaths { - if f == "" { - // Ignore empty - continue - } - - // Open for reading - r, err := cmdutil.Reader(f) - if err != nil { - log.For(ctx).Fatal("unable to read identity file", zap.Error(err), zap.String("identity", f)) - } - - // Decode identity - id, err := identity.FromReader(r) - if err != nil { - log.For(ctx).Fatal("unable to decode identity from file", zap.Error(err), zap.String("identity", f)) - } - - // Append to identity list - params.identities = append(params.identities, id.Public) + for _, f := range params.identityFilePaths { + if f == "" { + // Ignore empty + continue } - } - - // Convert identities to sealing keys - peerPublicKeys, err := identity.SealingKeys(params.identities...) - if err != nil { - log.For(ctx).Fatal("unable to transform identity to a sealing key", zap.Error(err)) - return - } - // Prepare task - t := &container.SealTask{ - ContainerReader: cmdutil.FileReader(params.inputPath), - SealedContainerWriter: cmdutil.FileWriter(params.outputPath), - OutputWriter: cmdutil.StdoutWriter(), - JSONOutput: params.jsonOutput, - PeerPublicKeys: peerPublicKeys, - DisableContainerIdentity: params.noContainerIdentity, - } + // Open for reading + r, err := cmdutil.Reader(f) + if err != nil { + log.For(ctx).Fatal("unable to read identity file", zap.Error(err), zap.String("identity", f)) + } - // Check container sealing master key usage - if params.masterKey != "" { - // Process target - if params.target == "" { - log.For(ctx).Fatal("target flag (string) is mandatory for key derivation") + // Decode identity + id, err := identity.FromReader(r) + if err != nil { + log.For(ctx).Fatal("unable to decode identity from file", zap.Error(err), zap.String("identity", f)) } - // Assign target parameter - t.DCKDTarget = params.target + // Append to identity list + params.identities = append(params.identities, id.Public) + } - // Decode master key - masterKeyRaw, err := base64.RawURLEncoding.DecodeString(params.masterKey) + // Process identities + for _, ipk := range params.identities { + // Convert to sealing public key + identityPublicKey, err := key.FromString(ipk) if err != nil { - log.For(ctx).Fatal("unable to decode master key", zap.Error(err)) + log.For(ctx).Fatal("unbale to parse identity public key", zap.Error(err), zap.String("ipk", ipk)) } - // Check appropriate lengh - if len(masterKeyRaw) != 32 { - log.For(ctx).Fatal("invalid master key length, it should be 32 bytes after decoding") + // Try to convert the key + sealingPublicKey := identityPublicKey.SealingKey() + if sealingPublicKey == "" { + log.For(ctx).Fatal("unbale to convert identity public key to a sealing key", zap.Error(err), zap.String("ipk", ipk)) } - // Assign as seed - t.DCKDMasterKey = memguard.NewBufferFromBytes(masterKeyRaw) + // Add to sealing keys + sealingPublicKeys.AddIfNotContains(sealingPublicKey) + } + + // Prepare task + t := &container.SealTask{ + ContainerReader: cmdutil.FileReader(params.inputPath), + SealedContainerWriter: cmdutil.FileWriter(params.outputPath), + OutputWriter: cmdutil.StdoutWriter(), + JSONOutput: params.jsonOutput, + PeerPublicKeys: sealingPublicKeys, + SealVersion: params.sealVersion, } // Run the task @@ -128,6 +115,11 @@ var containerSealCmd = func() *cobra.Command { }, } + sealVersion := uint(1) + if fips.Enabled() { + sealVersion = uint(2) + } + // Parameters cmd.Flags().StringVar(¶ms.inputPath, "in", "", "Unsealed container input ('-' for stdin or filename)") cmd.Flags().StringVar(¶ms.outputPath, "out", "", "Sealed container output ('-' for stdout or filename)") @@ -138,6 +130,7 @@ var containerSealCmd = func() *cobra.Command { cmd.Flags().BoolVar(¶ms.noContainerIdentity, "no-container-identity", false, "Disable container identity") cmd.Flags().StringVar(¶ms.masterKey, "dckd-master-key", "", "Master key used for deterministic container key derivation") cmd.Flags().StringVar(¶ms.target, "dckd-target", "", "Target parameter for deterministic container key derivation") + cmd.Flags().UintVar(¶ms.sealVersion, "seal-version", sealVersion, "Select the sealing strategy version (1:modern, 2:fips-compliant)") return cmd } diff --git a/cmd/harp/internal/cmd/keygen.go b/cmd/harp/internal/cmd/keygen.go index 276f8125..6b00e87f 100644 --- a/cmd/harp/internal/cmd/keygen.go +++ b/cmd/harp/internal/cmd/keygen.go @@ -19,6 +19,8 @@ package cmd import ( "github.com/spf13/cobra" + + "github.com/elastic/harp/build/fips" ) // ----------------------------------------------------------------------------- @@ -32,14 +34,16 @@ var keygenCmd = func() *cobra.Command { // Subcommands cmd.AddCommand(keygenFernetCmd()) - cmd.AddCommand(keygenSecretBoxCmd()) cmd.AddCommand(keygenAESCmd()) cmd.AddCommand(keygenMasterKeyCmd()) - cmd.AddCommand(keygenChaChaCmd()) - cmd.AddCommand(keygenXChaChaCmd()) - cmd.AddCommand(keygenAESPMACSIVCmd()) - cmd.AddCommand(keygenAESSIVCmd()) - cmd.AddCommand(keygenPasetoCmd()) + if !fips.Enabled() { + cmd.AddCommand(keygenSecretBoxCmd()) + cmd.AddCommand(keygenChaChaCmd()) + cmd.AddCommand(keygenXChaChaCmd()) + cmd.AddCommand(keygenAESPMACSIVCmd()) + cmd.AddCommand(keygenAESSIVCmd()) + cmd.AddCommand(keygenPasetoCmd()) + } return cmd } diff --git a/cmd/harp/internal/cmd/keygen_paseto.go b/cmd/harp/internal/cmd/keygen_paseto.go index 9116dea6..b157d8d9 100644 --- a/cmd/harp/internal/cmd/keygen_paseto.go +++ b/cmd/harp/internal/cmd/keygen_paseto.go @@ -31,7 +31,6 @@ import ( // ----------------------------------------------------------------------------- var keygenPasetoCmd = func() *cobra.Command { - cmd := &cobra.Command{ Use: "paseto", Short: "Generate and print an v4.local paseto key", diff --git a/cmd/harp/loader_fips.go b/cmd/harp/loader_fips.go new file mode 100644 index 00000000..707d4e5b --- /dev/null +++ b/cmd/harp/loader_fips.go @@ -0,0 +1,28 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build fips + +package main + +import ( + // Register encryption transformers + _ "github.com/elastic/harp/pkg/sdk/value/encryption/aead" + _ "github.com/elastic/harp/pkg/sdk/value/encryption/fernet" + _ "github.com/elastic/harp/pkg/sdk/value/encryption/jwe" + _ "github.com/elastic/harp/pkg/vault" +) diff --git a/cmd/harp/loader_nonfips.go b/cmd/harp/loader_nonfips.go new file mode 100644 index 00000000..f4d5d8be --- /dev/null +++ b/cmd/harp/loader_nonfips.go @@ -0,0 +1,31 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build !fips + +package main + +import ( + + // Register encryption transformers + _ "github.com/elastic/harp/pkg/sdk/value/encryption/aead" + _ "github.com/elastic/harp/pkg/sdk/value/encryption/fernet" + _ "github.com/elastic/harp/pkg/sdk/value/encryption/jwe" + _ "github.com/elastic/harp/pkg/sdk/value/encryption/paseto" + _ "github.com/elastic/harp/pkg/sdk/value/encryption/secretbox" + _ "github.com/elastic/harp/pkg/vault" +) diff --git a/cmd/harp/main.go b/cmd/harp/main.go index be94849f..1b981aba 100644 --- a/cmd/harp/main.go +++ b/cmd/harp/main.go @@ -23,14 +23,6 @@ import ( "github.com/elastic/harp/cmd/harp/internal/cmd" "github.com/elastic/harp/pkg/sdk/log" - - // Register encryption transformers - _ "github.com/elastic/harp/pkg/sdk/value/encryption/aead" - _ "github.com/elastic/harp/pkg/sdk/value/encryption/fernet" - _ "github.com/elastic/harp/pkg/sdk/value/encryption/jwe" - _ "github.com/elastic/harp/pkg/sdk/value/encryption/paseto" - _ "github.com/elastic/harp/pkg/sdk/value/encryption/secretbox" - _ "github.com/elastic/harp/pkg/vault" ) func init() { diff --git a/docs/cmd/harp_container_identity.md b/docs/cmd/harp_container_identity.md index 83ad0f81..833a0edc 100644 --- a/docs/cmd/harp_container_identity.md +++ b/docs/cmd/harp_container_identity.md @@ -11,10 +11,12 @@ harp container identity [flags] ``` --description string Identity description -h, --help help for identity + --key string Transformer key --out string Identity information output ('-' for stdout or filename) --passphrase string Identity private key passphrase --vault-transit-key string Use Vault transit encryption to protect identity private key --vault-transit-path string Vault transit backend mount path (default "transit") + --version uint Select identity version (0:legacy, 1:modern, 2:nist) (default 1) ``` ### SEE ALSO diff --git a/docs/cmd/harp_container_recover.md b/docs/cmd/harp_container_recover.md index ef313bb4..7a7eb96c 100644 --- a/docs/cmd/harp_container_recover.md +++ b/docs/cmd/harp_container_recover.md @@ -12,6 +12,7 @@ harp container recover [flags] -h, --help help for recover --identity string Identity input ('-' for stdout or filename) --json Display container key as json + --key string Transformer key --passphrase string Identity private key passphrase --vault-transit-key string Use Vault transit encryption to protect identity private key --vault-transit-path string Vault transit backend mount path (default "transit") diff --git a/docs/cmd/harp_container_seal.md b/docs/cmd/harp_container_seal.md index 1fae5684..acbb0a38 100644 --- a/docs/cmd/harp_container_seal.md +++ b/docs/cmd/harp_container_seal.md @@ -18,6 +18,7 @@ harp container seal [flags] --json Display seal info as json --no-container-identity Disable container identity --out string Sealed container output ('-' for stdout or filename) + --seal-version uint Select the sealing strategy version (1:modern, 2:fips-compliant) (default 1) ``` ### SEE ALSO diff --git a/docs/cso/HowTo.md b/docs/cso/HowTo.md index 54625404..469061d3 100644 --- a/docs/cso/HowTo.md +++ b/docs/cso/HowTo.md @@ -64,7 +64,7 @@ The `platform` directory is used for storing secrets relating to the software ru ### `product` - product keys and registration information -The `product` directory is used for product-related secrets. The basic structure is `/product/[name]/[version]/[component]/[secret]`. +The `product` directory is used for product-related secrets. The basic structure is `/product/[name]/[version]/[component]/[secret]`. - `/product/macosx/v11.0.2/antivirus/enterprise/registration-key` - `/product/elastic/v7.10.1/xpack/license/serialnumber` @@ -97,7 +97,7 @@ Now that we have some idea how to structure the secrets, we need to store them. `Harp` uses specification documents called `spec` files to determine what to store where. These can be formatted as either YAML or JSON. The format allows `harp` to validate both the location (based on the directory paths above) and the format of a secret. If needed, the `spec` file will instruct `harp` on how to generate a secret based on specific criteria if one does not exist. It is possible, with the right syntax, to populate a completely blank `secret store` with all the values needed for a new service deployment automatically. -The spec file is documented in full in the [https://github.com/elastic/harp/tree/main/samples/onboarding/1-template-engine](Harp Onboarding Docs). There is a LOT in there, and we cannot possible cover all the cases in this document. +The spec file is documented in full in the [https://github.com/elastic/harp/tree/main/docs/onboarding/1-template-engine](Harp Onboarding Docs). There is a LOT in there, and we cannot possible cover all the cases in this document. ### A Sample set of secrets @@ -168,7 +168,7 @@ spec: "private_key": "{{ .Values.secrets.infra.local.global.ssh_private_key }}" } - provider: aws - description: aws keys + description: aws keys account: "elastic-cloud.com" regions: - name: us-east-1 @@ -187,7 +187,7 @@ spec: ### OK, Now what? -Great question! Now it is time to actually store those secrets in a locked bundle. +Great question! Now it is time to actually store those secrets in a locked bundle. ### Working with Bundles @@ -215,7 +215,7 @@ Be aware that the command line below is NOT secure. You should probably use a `s harp bundle encrypt --in example.bundle --out example.encrypted.bundle --key [some base64 string] ``` -In order to decerypt the bundle, you can use the same key with the `decrypt` command. +In order to decerypt the bundle, you can use the same key with the `decrypt` command. ```bash harp bundle decrypt --in example.encrypted.bundle --out example.bundle --key [some base64 string] @@ -227,7 +227,7 @@ Since the `--in` and `--out` commands can also read and write to the console wit harp from bundle-template --in example.spec.yaml --values example.values.yaml --out - | harp bundle encrypt --in - --out example.encrypted.bundle --key [some base64 string] ``` -This creates a new bundle and pipes the data into the encrypt command, resulting in an encrypted bundle. +This creates a new bundle and pipes the data into the encrypt command, resulting in an encrypted bundle. If we want to be SUPERFANCY, we can add some `jq` parsing to the `decrypt` command and pull a single value out. diff --git a/samples/onboarding/1-template-engine/1-introduction.md b/docs/onboarding/1-template-engine/1-introduction.md similarity index 100% rename from samples/onboarding/1-template-engine/1-introduction.md rename to docs/onboarding/1-template-engine/1-introduction.md diff --git a/samples/onboarding/1-template-engine/2-functions.md b/docs/onboarding/1-template-engine/2-functions.md similarity index 100% rename from samples/onboarding/1-template-engine/2-functions.md rename to docs/onboarding/1-template-engine/2-functions.md diff --git a/samples/onboarding/1-template-engine/3-variables.md b/docs/onboarding/1-template-engine/3-variables.md similarity index 100% rename from samples/onboarding/1-template-engine/3-variables.md rename to docs/onboarding/1-template-engine/3-variables.md diff --git a/samples/onboarding/1-template-engine/4-values.md b/docs/onboarding/1-template-engine/4-values.md similarity index 100% rename from samples/onboarding/1-template-engine/4-values.md rename to docs/onboarding/1-template-engine/4-values.md diff --git a/samples/onboarding/1-template-engine/5-files.md b/docs/onboarding/1-template-engine/5-files.md similarity index 100% rename from samples/onboarding/1-template-engine/5-files.md rename to docs/onboarding/1-template-engine/5-files.md diff --git a/samples/onboarding/1-template-engine/6-lists-and-maps.md b/docs/onboarding/1-template-engine/6-lists-and-maps.md similarity index 100% rename from samples/onboarding/1-template-engine/6-lists-and-maps.md rename to docs/onboarding/1-template-engine/6-lists-and-maps.md diff --git a/samples/onboarding/1-template-engine/7-alternative-delimiters.md b/docs/onboarding/1-template-engine/7-alternative-delimiters.md similarity index 100% rename from samples/onboarding/1-template-engine/7-alternative-delimiters.md rename to docs/onboarding/1-template-engine/7-alternative-delimiters.md diff --git a/samples/onboarding/1-template-engine/8-whitespace-controls.md b/docs/onboarding/1-template-engine/8-whitespace-controls.md similarity index 100% rename from samples/onboarding/1-template-engine/8-whitespace-controls.md rename to docs/onboarding/1-template-engine/8-whitespace-controls.md diff --git a/samples/onboarding/1-template-engine/9-usecases.md b/docs/onboarding/1-template-engine/9-usecases.md similarity index 96% rename from samples/onboarding/1-template-engine/9-usecases.md rename to docs/onboarding/1-template-engine/9-usecases.md index 17210540..5b057248 100644 --- a/samples/onboarding/1-template-engine/9-usecases.md +++ b/docs/onboarding/1-template-engine/9-usecases.md @@ -32,7 +32,7 @@ harp template \ --in privaas.env.sh.tmpl \ --out privaas.env.sh \ --secrets-from privaas.container \ - --secrets-from vault # Fallback to vault if secret value not found. + --secrets-from vault # Fallback to vault if secret value is not found. ``` ## Generate AWS profile settings based on TF variable diff --git a/samples/onboarding/2-secret-container/1-introduction.md b/docs/onboarding/2-secret-container/1-introduction.md similarity index 100% rename from samples/onboarding/2-secret-container/1-introduction.md rename to docs/onboarding/2-secret-container/1-introduction.md diff --git a/samples/onboarding/2-secret-container/2-specifications.md b/docs/onboarding/2-secret-container/2-specifications.md similarity index 98% rename from samples/onboarding/2-secret-container/2-specifications.md rename to docs/onboarding/2-secret-container/2-specifications.md index 77a8e118..f6c702d0 100644 --- a/samples/onboarding/2-secret-container/2-specifications.md +++ b/docs/onboarding/2-secret-container/2-specifications.md @@ -35,6 +35,8 @@ message Header { bytes container_box = 4; // Recipient list for identity bound secret container. repeated Recipient recipients = 6; + // Seal strategy + uint32 seal_version = 7; } ``` @@ -46,6 +48,7 @@ message Header { * The `container_box` is the signature public key encrypted with the payload key. * The `recipients` is a NaCL `box` that contains the x25519 private used for encryption protected using the passphrase during `sealing` process. +* The `seal_version` indicates the algorithm used to seal the container. Recipient definition : diff --git a/samples/onboarding/2-secret-container/3-seal.md b/docs/onboarding/2-secret-container/3-seal.md similarity index 94% rename from samples/onboarding/2-secret-container/3-seal.md rename to docs/onboarding/2-secret-container/3-seal.md index 98da8d2b..35b2649e 100644 --- a/samples/onboarding/2-secret-container/3-seal.md +++ b/docs/onboarding/2-secret-container/3-seal.md @@ -2,7 +2,11 @@ > [Container sealing algorithm is inspired from Keybase saltpack specification](https://saltpack.org/) -## Sealing Process +This sealing process is based on Public Key Encryption scheme. + +## Version 1 - Modern (Ed25519 / X25519 / CHACHA20+Poly1305 / Blake2b) + +### Sealing Process This algorithm implements multi recipient authenticated encryption with `sign-then-encrypt` pattern. @@ -54,7 +58,7 @@ This algorithm implements multi recipient authenticated encryption with * Seal the result with `payload_key` and the first 24 bytes of `header_hash` as nonce * Set `Raw` to encryption result -## Unsealing Process +### Unsealing Process * Requirements * A set of `identity_private_key` @@ -103,6 +107,10 @@ This algorithm implements multi recipient authenticated encryption with * Save signature as `content_signature` * Unmarshal `payload` as `&containerv1.Container{}` +## Version 2 - Modern NIST (ECDSA P-384 / EC P-384 / AES-CTR+HMAC-SHA512 / HKDF-HMAC-SHA512) + +TODO + --- * [Previous topic](2-specifications.md) diff --git a/samples/onboarding/3-secret-bundle/1-introduction.md b/docs/onboarding/3-secret-bundle/1-introduction.md similarity index 100% rename from samples/onboarding/3-secret-bundle/1-introduction.md rename to docs/onboarding/3-secret-bundle/1-introduction.md diff --git a/samples/onboarding/3-secret-bundle/2-bundle.md b/docs/onboarding/3-secret-bundle/2-bundle.md similarity index 100% rename from samples/onboarding/3-secret-bundle/2-bundle.md rename to docs/onboarding/3-secret-bundle/2-bundle.md diff --git a/samples/onboarding/3-secret-bundle/3-template.md b/docs/onboarding/3-secret-bundle/3-template.md similarity index 100% rename from samples/onboarding/3-secret-bundle/3-template.md rename to docs/onboarding/3-secret-bundle/3-template.md diff --git a/samples/onboarding/3-secret-bundle/4-patch.md b/docs/onboarding/3-secret-bundle/4-patch.md similarity index 100% rename from samples/onboarding/3-secret-bundle/4-patch.md rename to docs/onboarding/3-secret-bundle/4-patch.md diff --git a/samples/onboarding/README.md b/docs/onboarding/README.md similarity index 100% rename from samples/onboarding/README.md rename to docs/onboarding/README.md diff --git a/go.mod b/go.mod index 87c9b157..e10321f9 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,6 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/cloudflare/tableflip v1.2.2 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be - github.com/davecgh/go-spew v1.1.1 github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 github.com/fatih/color v1.13.0 github.com/fatih/structs v1.1.0 @@ -63,7 +62,7 @@ require ( go.etcd.io/etcd/client/v3 v3.5.1 go.step.sm/crypto v0.13.0 go.uber.org/zap v1.19.1 - golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa + golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 @@ -94,6 +93,7 @@ require ( github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v20.10.8+incompatible // indirect github.com/docker/docker v20.10.7+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect @@ -152,7 +152,7 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/net v0.0.0-20210913180222-943fd674d43e // indirect + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect diff --git a/go.sum b/go.sum index 587c3836..11500b5a 100644 --- a/go.sum +++ b/go.sum @@ -698,6 +698,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -779,6 +781,8 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8= golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/pkg/container/codec.go b/pkg/container/codec.go index 24ff7a0c..744b3dac 100644 --- a/pkg/container/codec.go +++ b/pkg/container/codec.go @@ -18,31 +18,24 @@ package container import ( - "bytes" - "crypto/ed25519" - "crypto/rand" "encoding/binary" "fmt" "io" "github.com/awnumar/memguard" - "golang.org/x/crypto/blake2b" - "golang.org/x/crypto/nacl/box" - "golang.org/x/crypto/nacl/secretbox" "google.golang.org/protobuf/proto" containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" - "github.com/elastic/harp/pkg/sdk/security/crypto/extra25519" + "github.com/elastic/harp/pkg/container/seal" + v1 "github.com/elastic/harp/pkg/container/seal/v1" + v2 "github.com/elastic/harp/pkg/container/seal/v2" "github.com/elastic/harp/pkg/sdk/types" ) const ( + containerSealedContentType = "application/vnd.harp.v1.SealedContainer" containerMagic = uint32(0x53CB3701) containerVersion = uint16(0x0002) - containerSealedContentType = "application/vnd.harp.v1.SealedContainer" - publicKeySize = 32 - privateKeySize = 32 - encryptionKeySize = 32 ) // Load a reader to extract as a container. @@ -132,116 +125,7 @@ func Dump(w io.Writer, c *containerv1.Container) error { return nil } -// Seal a secret container -//nolint:funlen // To refactor -func Seal(container *containerv1.Container, peersPublicKey ...*[32]byte) (*containerv1.Container, error) { - // Check parameters - if types.IsNil(container) { - return nil, fmt.Errorf("unable to process nil container") - } - if types.IsNil(container.Headers) { - return nil, fmt.Errorf("unable to process nil container headers") - } - if len(peersPublicKey) == 0 { - return nil, fmt.Errorf("unable to process empty public keys") - } - for _, pub := range peersPublicKey { - if pub == nil { - // Skip nil keys - continue - } - - k := *pub - if extra25519.IsEdLowOrder(k[:]) { - return nil, fmt.Errorf("unable to process with low order public key") - } - } - - // Serialize protobuf payload - content, err := proto.Marshal(container) - if err != nil { - return container, fmt.Errorf("unable to encode container content: %w", err) - } - - // Generate payload encryption key - var payloadKey [32]byte - if _, err = io.ReadFull(rand.Reader, payloadKey[:]); err != nil { - return nil, fmt.Errorf("unable to generate payload key for encryption") - } - - // Generate ephemeral signing key - sigPub, sigPriv, err := ed25519.GenerateKey(nil) - if err != nil { - return nil, fmt.Errorf("unable to generate signing keypair") - } - - // Encrypt public signature key - var pubSigNonce [24]byte - copy(pubSigNonce[:], "harp_container_psigk_box") - encryptedPubSig := secretbox.Seal(nil, sigPub, &pubSigNonce, &payloadKey) - memguard.WipeBytes(pubSigNonce[:]) - - // Generate ephemeral encryption key - encPub, encPriv, err := box.GenerateKey(rand.Reader) - if err != nil { - return nil, fmt.Errorf("unable to generate ephemeral encryption keypair") - } - - // Prepare sealed container - containerHeaders := &containerv1.Header{ - ContentType: containerSealedContentType, - EncryptionPublicKey: encPub[:], - ContainerBox: encryptedPubSig, - Recipients: []*containerv1.Recipient{}, - } - - // Process recipients - for _, peerPublicKey := range peersPublicKey { - // Ignore nil key - if peerPublicKey == nil { - continue - } - - // Pack recipient using its public key - r, errPack := packRecipient(&payloadKey, encPriv, peerPublicKey) - if errPack != nil { - return nil, fmt.Errorf("unable to pack container recipient (%X): %w", *peerPublicKey, err) - } - - // Append to container - containerHeaders.Recipients = append(containerHeaders.Recipients, r) - } - - // Compute header hash - headerHash, err := computeHeaderHash(containerHeaders) - if err != nil { - return nil, fmt.Errorf("unable to compute header hash: %w", err) - } - - // Prepare protected content - protected := bytes.Buffer{} - protected.Write([]byte("harp encrypted signature")) - protected.WriteByte(0x00) - protected.Write(headerHash) - contentHash := blake2b.Sum512(content) - protected.Write(contentHash[:]) - - // Sign th protected content - containerSig := ed25519.Sign(sigPriv, protected.Bytes()) - - // Prepare encryption nonce form sigHash - var sigNonce [24]byte - copy(sigNonce[:], headerHash[:24]) - - // No error - return &containerv1.Container{ - Headers: containerHeaders, - Raw: secretbox.Seal(nil, append(containerSig, content...), &sigNonce, &payloadKey), - }, nil -} - // Unseal a sealed container with the given identity -//nolint:funlen,gocyclo // To refactor func Unseal(container *containerv1.Container, identity *memguard.LockedBuffer) (*containerv1.Container, error) { // Check parameters if types.IsNil(container) { @@ -259,89 +143,15 @@ func Unseal(container *containerv1.Container, identity *memguard.LockedBuffer) ( return nil, fmt.Errorf("unable to unseal container") } - // Check ephemeral container public encryption key - if len(container.Headers.EncryptionPublicKey) != publicKeySize { - return nil, fmt.Errorf("invalid container public size") + // Build appropriate unseal strategy processor. + var ss seal.Strategy + switch container.Headers.SealVersion { + case 2: + ss = v2.New() + default: + ss = v1.New() } - var publicKey [publicKeySize]byte - copy(publicKey[:], container.Headers.EncryptionPublicKey[:publicKeySize]) - - // Check identity private encryption key - privRaw := identity.Bytes() - if len(privRaw) != privateKeySize { - return nil, fmt.Errorf("invalid identity private key length") - } - var pk [privateKeySize]byte - copy(pk[:], privRaw[:privateKeySize]) - - // Precompute identifier - derivedKey := deriveSharedKeyFromRecipient(&publicKey, &pk) - - // Try recipients - payloadKey, err := tryRecipientKeys(&derivedKey, container.Headers.Recipients) - if err != nil { - return nil, fmt.Errorf("unable to unseal container: error occurred during recipient key tests: %w", err) - } - - // Check private key - if len(payloadKey) != encryptionKeySize { - return nil, fmt.Errorf("unable to unseal container: invalid encryption key size") - } - var encryptionKey [encryptionKeySize]byte - copy(encryptionKey[:], payloadKey[:encryptionKeySize]) - - // Prepare sig nonce - var pubSigNonce [24]byte - copy(pubSigNonce[:], "harp_container_psigk_box") - // Decrypt signing public key - containerSignKeyRaw, ok := secretbox.Open(nil, container.Headers.ContainerBox, &pubSigNonce, &encryptionKey) - if !ok { - return nil, fmt.Errorf("invalid container key") - } - if len(containerSignKeyRaw) != ed25519.PublicKeySize { - return nil, fmt.Errorf("unable to unseal container: invalid signature key size") - } - - // Compute headers hash - headerHash, err := computeHeaderHash(container.Headers) - if err != nil { - return nil, fmt.Errorf("unable to compute header hash: %w", err) - } - - // Extract payload nonce - var payloadNonce [24]byte - copy(payloadNonce[:], headerHash[:24]) - - // Decrypt payload - payloadRaw, ok := secretbox.Open(nil, container.Raw, &payloadNonce, &encryptionKey) - if !ok || len(payloadRaw) < ed25519.SignatureSize { - return nil, fmt.Errorf("invalid ciphered content") - } - - // Prepare protected content - protected := bytes.Buffer{} - protected.Write([]byte("harp encrypted signature")) - protected.WriteByte(0x00) - protected.Write(headerHash) - contentHash := blake2b.Sum512(payloadRaw[ed25519.SignatureSize:]) - protected.Write(contentHash[:]) - - // Extract signature / content - detachedSig := payloadRaw[:ed25519.SignatureSize] - content := payloadRaw[ed25519.SignatureSize:] - - // Validate signature - if !ed25519.Verify(containerSignKeyRaw, protected.Bytes(), detachedSig) { - return nil, fmt.Errorf("invalid container signature") - } - - // Unmarshal inner container - out := &containerv1.Container{} - if err := proto.Unmarshal(content, out); err != nil { - return nil, fmt.Errorf("unable to unpack inner content: %w", err) - } - - // No error - return out, nil + // Delegate to strategy + return ss.Unseal(container, identity) } diff --git a/pkg/container/codec_test.go b/pkg/container/codec_test.go index 8308dddc..f7d03118 100644 --- a/pkg/container/codec_test.go +++ b/pkg/container/codec_test.go @@ -24,12 +24,9 @@ import ( "io/ioutil" "testing" - "github.com/awnumar/memguard" - "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" fuzz "github.com/google/gofuzz" - "golang.org/x/crypto/nacl/box" containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" ) @@ -191,112 +188,6 @@ func TestLoad(t *testing.T) { } } -func TestSeal(t *testing.T) { - publicKey1, _, _ := box.GenerateKey(bytes.NewReader([]byte("deterministic-generation-for-tests-0002"))) - publicKey2, _, _ := box.GenerateKey(bytes.NewReader([]byte("deterministic-generation-for-tests-0003"))) - - type args struct { - container *containerv1.Container - peersPublicKey []*[32]byte - } - tests := []struct { - name string - args args - want *containerv1.Container - wantErr bool - }{ - { - name: "nil", - wantErr: true, - }, - { - name: "empty container", - args: args{ - container: &containerv1.Container{}, - }, - wantErr: true, - }, - { - name: "empty container headers", - args: args{ - container: &containerv1.Container{ - Headers: &containerv1.Header{}, - }, - }, - wantErr: true, - }, - { - name: "empty container with public keys", - args: args{ - container: &containerv1.Container{ - Headers: &containerv1.Header{}, - }, - peersPublicKey: []*[32]byte{ - publicKey1, - publicKey2, - }, - }, - wantErr: false, - }, - { - name: "valid container with public keys", - args: args{ - container: &containerv1.Container{ - Headers: &containerv1.Header{}, - Raw: []byte{0x01, 0x02, 0x03, 0x04}, - }, - peersPublicKey: []*[32]byte{ - publicKey1, - publicKey2, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := Seal(tt.args.container, tt.args.peersPublicKey...) - if (err != nil) != tt.wantErr { - t.Errorf("Seal() error = %v, wantErr %v", err, tt.wantErr) - return - } - }) - } -} - -// ----------------------------------------------------------------------------- - -func Test_Seal_Unseal(t *testing.T) { - publicKey1, privateKey1, err := box.GenerateKey(bytes.NewReader([]byte("deterministic-generation-for-tests-0002"))) - if err != nil { - t.Fatalf("%v", err) - } - - input := &containerv1.Container{ - Headers: &containerv1.Header{ - ContentEncoding: "gzip", - ContentType: "application/vnd.harp.v1.Bundle", - }, - Raw: []byte{0x00, 0x00}, - } - - sealed, err := Seal(input, nil, publicKey1) - if err != nil { - t.Fatalf("unable to seal container: %v", err) - } - - spew.Dump(sealed) - - unsealed, err := Unseal(sealed, memguard.NewBufferFromBytes(privateKey1[:])) - if err != nil { - t.Fatalf("unable to unseal container: %v", err) - } - - if diff := cmp.Diff(unsealed, input, ignoreOpts...); diff != "" { - t.Errorf("Seal/Unseal()\n-got/+want\ndiff %s", diff) - } -} - // ----------------------------------------------------------------------------- func Test_Load_Fuzz(t *testing.T) { @@ -332,49 +223,3 @@ func Test_Dump_Fuzz(t *testing.T) { Dump(ioutil.Discard, &input) } } - -func Test_Seal_Fuzz(t *testing.T) { - // Making sure the function never panics - for i := 0; i < 500; i++ { - f := fuzz.New() - - // Prepare arguments - var ( - publicKey [32]byte - ) - input := containerv1.Container{ - Headers: &containerv1.Header{}, - Raw: []byte{0x00, 0x00}, - } - - f.Fuzz(&input.Headers) - f.Fuzz(&input.Raw) - f.Fuzz(&publicKey) - - // Execute - Seal(&input, &publicKey) - } -} - -func Test_UnSeal_Fuzz(t *testing.T) { - // Memguard buffer is excluded from fuzz for random race condition error - // investigation will be done in a separated thread. - identity := memguard.NewBufferRandom(32) - - // Making sure the function never panics - for i := 0; i < 500; i++ { - f := fuzz.New() - - // Prepare arguments - input := containerv1.Container{ - Headers: &containerv1.Header{}, - Raw: []byte{0x00, 0x00}, - } - - f.Fuzz(&input.Headers) - f.Fuzz(&input.Raw) - - // Execute - Unseal(&input, identity) - } -} diff --git a/pkg/container/identity/api.go b/pkg/container/identity/api.go index 49f238d0..3d1dbf4a 100644 --- a/pkg/container/identity/api.go +++ b/pkg/container/identity/api.go @@ -22,11 +22,11 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "time" - "github.com/elastic/harp/pkg/sdk/security" - "github.com/elastic/harp/pkg/sdk/security/crypto/bech32" + "github.com/elastic/harp/pkg/container/identity/key" "github.com/elastic/harp/pkg/sdk/types" "github.com/elastic/harp/pkg/sdk/value" ) @@ -39,6 +39,7 @@ type Identity struct { Description string `json:"@description"` Public string `json:"public"` Private *PrivateKey `json:"private"` + Signature string `json:"signature"` } // HasPrivateKey returns true if identity as a wrapped private. @@ -47,7 +48,7 @@ func (i *Identity) HasPrivateKey() bool { } // Decrypt private key with given transformer. -func (i *Identity) Decrypt(ctx context.Context, t value.Transformer) (*JSONWebKey, error) { +func (i *Identity) Decrypt(ctx context.Context, t value.Transformer) (*key.JSONWebKey, error) { // Check arguments if types.IsNil(t) { return nil, fmt.Errorf("can't process with nil transformer") @@ -69,30 +70,49 @@ func (i *Identity) Decrypt(ctx context.Context, t value.Transformer) (*JSONWebKe } // Decode key - var key JSONWebKey - if err = json.NewDecoder(bytes.NewReader(clearText)).Decode(&key); err != nil { + var pk key.JSONWebKey + if err = json.NewDecoder(bytes.NewReader(clearText)).Decode(&pk); err != nil { return nil, fmt.Errorf("unable to decode payload as JSON: %w", err) } - // Build public key - _, pubKey, err := bech32.Decode(i.Public) + // Return result + return &pk, nil +} + +// Verify the identity signature using its own public key. +func (i *Identity) Verify() error { + // Clear the signature + id := &Identity{} + *id = *i + + // Clean protected + id.Signature = "" + id.Private = nil + + // Prepare protected + protected, err := json.Marshal(id) if err != nil { - return nil, fmt.Errorf("invalid public key encoding: %w", err) + return fmt.Errorf("unable to serialize identity for signature: %w", err) } - // Decode base64 public key - pubKeyRaw, err := base64.RawURLEncoding.DecodeString(key.X) + // Decode the signature + sig, err := base64.RawURLEncoding.DecodeString(i.Signature) if err != nil { - return nil, fmt.Errorf("invalid public key, the decoded public is corrupted") + return fmt.Errorf("unable to decode the signature: %w", err) } - // Check validity - if !security.SecureCompare(pubKey, pubKeyRaw) { - return nil, fmt.Errorf("invalid identity, key mismatch detected") + // Decode public key + pubKey, err := key.FromString(id.Public) + if err != nil { + return fmt.Errorf("unable to decode public key: %w", err) } - // Return result - return &key, nil + // Validate signature + if pubKey.Verify(protected, sig) { + return nil + } + + return errors.New("unable to validate identity signature") } // PrivateKey wraps encoded private and related informations. @@ -100,11 +120,3 @@ type PrivateKey struct { Encoding string `json:"encoding,omitempty"` Content string `json:"content"` } - -// JSONWebKey holds internal container key attributes. -type JSONWebKey struct { - Kty string `json:"kty"` - Crv string `json:"crv"` - X string `json:"x"` - D string `json:"d"` -} diff --git a/pkg/container/identity/codec.go b/pkg/container/identity/codec.go index b8ace565..68b5bc59 100644 --- a/pkg/container/identity/codec.go +++ b/pkg/container/identity/codec.go @@ -18,20 +18,15 @@ package identity import ( - "crypto/ed25519" - "encoding/base64" "encoding/json" - "errors" "fmt" "io" "time" validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/go-ozzo/ozzo-validation/v4/is" - "github.com/gosimple/slug" - "github.com/elastic/harp/pkg/sdk/security/crypto/bech32" - "github.com/elastic/harp/pkg/sdk/security/crypto/extra25519" + "github.com/elastic/harp/pkg/container/identity/key" "github.com/elastic/harp/pkg/sdk/types" ) @@ -42,25 +37,19 @@ const ( // ----------------------------------------------------------------------------- +type PrivateKeyGeneratorFunc func(io.Reader) (*key.JSONWebKey, string, error) + // New identity from description. -func New(random io.Reader, description string) (*Identity, []byte, error) { +func New(random io.Reader, description string, generator PrivateKeyGeneratorFunc) (*Identity, []byte, error) { // Check arguments if err := validation.Validate(description, validation.Required, is.ASCII); err != nil { return nil, nil, fmt.Errorf("unable to create identity with invalid description: %w", err) } - // Generate ed25519 keys as identity - pub, priv, err := ed25519.GenerateKey(random) + // Delegate to generator + jwk, encodedPub, err := generator(random) if err != nil { - return nil, nil, fmt.Errorf("unable to generate identity keypair: %w", err) - } - - // Wrap as JWK - jwk := JSONWebKey{ - Kty: "OKP", - Crv: "Ed25519", - X: base64.RawURLEncoding.EncodeToString(pub[:]), - D: base64.RawURLEncoding.EncodeToString(priv[:]), + return nil, nil, fmt.Errorf("unable to generate identity private key: %w", err) } // Encode JWK as json @@ -69,20 +58,32 @@ func New(random io.Reader, description string) (*Identity, []byte, error) { return nil, nil, fmt.Errorf("unable to serialize identity keypair: %w", err) } - // Encode public key using Bech32 with description as hrp - encoded, err := bech32.Encode(slug.Make(description), pub[:]) - if err != nil { - return nil, nil, fmt.Errorf("unable to encode public key: %w", err) - } - - // Return unsealed identity - return &Identity{ + // Prepae identity object + id := &Identity{ APIVersion: apiVersion, Kind: kind, Timestamp: time.Now().UTC(), Description: description, - Public: encoded, - }, payload, nil + Public: encodedPub, + } + + // Encode to json for signature + protected, err := json.Marshal(id) + if err != nil { + return nil, nil, fmt.Errorf("unable to serialize identity for signature: %w", err) + } + + // Sign the protected data + sig, err := jwk.Sign(protected) + if err != nil { + return nil, nil, fmt.Errorf("unable to sign protected data: %w", err) + } + + // Auto-assign the signature + id.Signature = sig + + // Return unsealed identity + return id, payload, nil } // FromReader extract identity instance from reader. @@ -98,87 +99,16 @@ func FromReader(r io.Reader) (*Identity, error) { return nil, fmt.Errorf("unable to decode input JSON: %w", err) } - // Check public key encoding - _, _, err := bech32.Decode(input.Public) - if err != nil { - return nil, fmt.Errorf("invalid public key encoding") - } - // Check component if input.Private == nil { return nil, fmt.Errorf("invalid identity: missing private component") } - // Return no error - return &input, nil -} - -// RecoveryKey returns the x25519 private encryption key from the private -// identity key. -func RecoveryKey(key *JSONWebKey) (*[32]byte, error) { - // Check arguments - if key == nil { - return nil, errors.New("unable to get container key from a nil identity") - } - - // Decode ed25519 private key - privKeyRaw, err := base64.RawURLEncoding.DecodeString(key.D) - if err != nil { - return nil, errors.New("invalid identity, private key is invalid") - } - - var recoveryPrivateKey [32]byte - - switch key.Crv { - case "X25519": // Legacy keys - copy(recoveryPrivateKey[:], privKeyRaw) - case "Ed25519": - // Convert Ed25519 private key to x25519 key. - extra25519.PrivateKeyToCurve25519(&recoveryPrivateKey, privKeyRaw) - default: - return nil, fmt.Errorf("unhandled private key format '%s'", key.Crv) - } - - // No error - return &recoveryPrivateKey, nil -} - -// SealingPublicKeys convert ed25519 public key to x25519 public container key. -func SealingKeys(publicKeys ...string) ([]*[32]byte, error) { - // If using sealing seed - peerPublicKeys := []*[32]byte{} - - // Given identities - if len(publicKeys) == 0 { - return nil, fmt.Errorf("at least one public key must be provided") - } - - // Filter identities - var filteredIdentities types.StringArray - - // Process identities - for _, id := range publicKeys { - // Check if identity is already added - if !filteredIdentities.AddIfNotContains(id) { - continue - } - - // Check encoding - _, publicKeyRaw, errDecode := bech32.Decode(id) - if errDecode != nil { - return nil, fmt.Errorf("invalid '%s' as public identity: %w", id, errDecode) - } - - // Convert ed25519 public to x25519 key - var publicKey [32]byte - if !extra25519.PublicKeyToCurve25519(&publicKey, publicKeyRaw) { - return nil, fmt.Errorf("unable to convert identity '%s' to container sealing key", id) - } - - // Append to identity - peerPublicKeys = append(peerPublicKeys, &publicKey) + // Validate self signature + if errVerify := input.Verify(); errVerify != nil { + return nil, fmt.Errorf("unable to verify identity: %w", errVerify) } - // No error - return peerPublicKeys, nil + // Return no error + return &input, nil } diff --git a/pkg/container/identity/codec_test.go b/pkg/container/identity/codec_test.go index 6f4b5550..95d5e924 100644 --- a/pkg/container/identity/codec_test.go +++ b/pkg/container/identity/codec_test.go @@ -21,45 +21,80 @@ import ( "crypto/rand" "testing" + "github.com/elastic/harp/pkg/container/identity/key" "github.com/stretchr/testify/assert" ) var ( - securityIdentity = []byte(`{"@apiVersion": "harp.elastic.co/v1", "@kind": "ContainerIdentity", "@timestamp": "2021-11-15T11:58:13.662568Z", "@description": "security", "public": "security1r6t9kagaafun6zvkx4ysm2kh9xswca6x79dlu4lvmg6hynywx7nsvpgple", "private": { "encoding": "jwe", "content": "eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjUwMDAwMSwicDJzIjoiTlVVNVlWZDZTRVpJVldoMFNFSnNaUSJ9.pqZ9kim7OzW6lVLmPf4wXRYx8IvHmZi7ChzxmWqtGHo2zHeyp3Bhqw.x76wqFYsB-E-E0ov.1Adrme-LS8tC05n1D3FLUSiDGCMcf30lRjWDCB2CSh-3x4K2fZ2gibsvtp7aO4IjxkESnrUV6vCCAtXDa2I4f-aYAYzl1CkgSw-1JulQmVjl4l3NTcI189icJT0HxJ7-F0SGtpmTU1bGoGR9z_ERVErom3I6bSAl2OV4WcDVTfmyXBoJqM-hXYtIeIpLC0B4sxi3CFPhFQlEHF65AYwC2QgZb2qoP-tLnJG1FA.g-hH5zr7ksKhWS2aXAWP0Q"}}`) - publicOnly = []byte(`{"@apiVersion": "harp.elastic.co/v1", "@kind": "ContainerIdentity", "@timestamp": "2021-11-15T11:58:13.662568Z", "@description": "security", "public": "security1r6t9kagaafun6zvkx4ysm2kh9xswca6x79dlu4lvmg6hynywx7nsvpgple"}`) + v1SecurityIdentity = []byte(`{"@apiVersion":"harp.elastic.co/v1","@kind":"ContainerIdentity","@timestamp":"2021-12-01T22:15:11.144249Z","@description":"security","public":"v1.ipk.7u8B1VFrHyMeWyt8Jzj1Nj2BgVB7z-umD8R-OOnJahE","private":{"content":"ZXlKaGJHY2lPaUpRUWtWVE1pMUlVelV4TWl0Qk1qVTJTMWNpTENKbGJtTWlPaUpCTWpVMlIwTk5JaXdpY0RKaklqbzFNREF3TURFc0luQXljeUk2SW1obU1UVnpUVmRwTmtaMmNUSlVZM0p5TVhReVZtY2lmUS5ZOGtfVXR2dWtmcVRxTE5fQ2l5ajdTejU2dThOYV9uMG1FTG5jMHFCQ1d0MkVqX2VHRk80RmcuN0p2ekhGYkZrRXdXWGxOeC5ycVJLSno1ZWFGajRqSl9wOVAzUVBuUUs3dXhkWUhBOUNIZFUxTEswWkQ3Q2dickJzUDFRRFRTRU1lX3lqbTZVQ1dpNzFUVmxfX3JISVdSR3VDVVpWSE1KMXNtRnR5c2UzdHBURkdnZFRCaVQxTmw4dWlNZ2JiUEN1cHJ4Uy0wUjRGU2dobXFLU0s3TGhRcUxFWFVaNFF0SVliMDd6Y19vMnRZNlVnU3NMaFBlSUFPM1M2WlBwQXFYU3lfSjR3NzEzdFhEU1ZTX2ZuOFJ5MlF2NTJmOHg0cXBiN0Q3NGlTTndOb052Zy5rcHVzTTVoZ21RT3JhS1luNGxTVjZ3"},"signature":"Kq1OJlAOexIvt9TXETYeYGotqqCz8PiqFEYuSbHmJPVBqtYpI2Q_zNE0fO5hs-JdTqG3p6oLiITHK9cYyx2hBw"}`) + publicOnly = []byte(`{"@apiVersion":"harp.elastic.co/v1","@kind":"ContainerIdentity","@timestamp":"2021-12-01T20:56:30.832199Z","@description":"security","public":"v1.ipk.PRdbQ8qbrDsfTLA-aeQIdUF0VwnauvWqQF-CXNFp9oM"}`) + v2SecurityIdentity = []byte(`{"@apiVersion":"harp.elastic.co/v1","@kind":"ContainerIdentity","@timestamp":"2021-12-01T22:15:07.586373Z","@description":"security","public":"v2.ipk.AkLr_HHMO5Loy2bK42mvCADrJ7s2PSYCRTnqDWJV8PCK2EXmu-GTV8HmNJwmA8IJ8Q","private":{"content":"ZXlKaGJHY2lPaUpRUWtWVE1pMUlVelV4TWl0Qk1qVTJTMWNpTENKbGJtTWlPaUpCTWpVMlIwTk5JaXdpY0RKaklqbzFNREF3TURFc0luQXljeUk2SWpCMFozUk5OMm80TmxacFNrWXpjemhYWDBsb05VRWlmUS5QXzVVMTdSR3JSRVg4UHoyNGpRQkQzdGROWGU3ci1UMVh2bnBnT21aMkwweGZXQXNfT2dWcVEuYUpuekZCQTNBWllXSld2TC53SVAtZlRERjN5R0NaRGtldThOM3A1NUZPRF9ZX3QzSV9ubHN2MDVqcWNLdlJLczFfWjVfM2Zhc2Z0cU0tMlRoN25VdDZIaXZWLVB1ckVIQ2hBRENHaF9SZTBySVVwZkV4OHBCcUk4V3BIYTdSYktUTUN3RmNpSDMzeTQxZ1duT1lpN1R1TmJBamhNMjZMdDZZMFN0ekcyRi1FUm9jSWotWklwMDJwcGZjdUpKOU91S1BDOThKTl9ZV3EzcVA2TW55Ym1WTnFFZ1hwdWFVZm9GcTN3ZWlSX2paVkNsRzU5cTBGdWplVHN0UnRzU2xuZFlndTVBTl9LanFWRmluNDBXNGcxZWRMdWZDM1U0UGZhZVMzUlQxSS0wRkVnN0ZGMnE0QVdINy01aF9IQWg4WFR4eXBCTjR3THE1TTd6ZExRLlkzTTlBeDc1bGNYbmNNaGNxV3dOMXc"},"signature":"dpbnMGAPvFbHSjEXs1GMyO8Kmw9cZqTOKI5wAA1ApcO1RXtFGS_GyC1zAtuFDhhVmTWdFzS4HdVg0LEhxBivbqsr_cft_9CR-7uVUPpkb2Hz2d4BkL3yzDo9bkLfllaM"}`) ) func TestCodec_New(t *testing.T) { t.Run("invalid description", func(t *testing.T) { - id, pub, err := New(rand.Reader, "é") + id, pub, err := New(rand.Reader, "é", key.Ed25519) assert.Error(t, err) assert.Nil(t, pub) assert.Nil(t, id) }) - t.Run("large description", func(t *testing.T) { - id, pub, err := New(rand.Reader, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + t.Run("ed25519 - invalid random source", func(t *testing.T) { + id, pub, err := New(bytes.NewBuffer(nil), "test", key.Ed25519) assert.Error(t, err) assert.Nil(t, pub) assert.Nil(t, id) }) - t.Run("invalid random source", func(t *testing.T) { - id, pub, err := New(bytes.NewBuffer(nil), "test") + t.Run("p384 - invalid random source", func(t *testing.T) { + id, pub, err := New(bytes.NewBuffer(nil), "test", key.P384) assert.Error(t, err) assert.Nil(t, pub) assert.Nil(t, id) }) - t.Run("valid", func(t *testing.T) { - id, pub, err := New(bytes.NewBuffer([]byte("deterministic-random-source-for-test-0001")), "security") + t.Run("legacy - invalid random source", func(t *testing.T) { + id, pub, err := New(bytes.NewBuffer(nil), "test", key.Legacy) + assert.Error(t, err) + assert.Nil(t, pub) + assert.Nil(t, id) + }) + + t.Run("valid - ed25519", func(t *testing.T) { + id, pub, err := New(bytes.NewBuffer([]byte("deterministic-random-source-for-test-0001")), "security", key.Ed25519) assert.NoError(t, err) assert.NotNil(t, pub) assert.NotNil(t, id) assert.Equal(t, "harp.elastic.co/v1", id.APIVersion) assert.Equal(t, "security", id.Description) assert.Equal(t, "ContainerIdentity", id.Kind) - assert.Equal(t, "security1mqtkctl32wy695wryccfgrdw4hr8cn9smk9vduc9yy5l3dfwr69swl0vee", id.Public) + assert.Equal(t, "v1.ipk.2BdsL_FTiaLRwyYwlA2urcZ8TLDdisbzBSEp-LUuHos", id.Public) + assert.Nil(t, id.Private) + assert.False(t, id.HasPrivateKey()) + }) + + t.Run("valid - p-384", func(t *testing.T) { + id, pub, err := New(bytes.NewBuffer([]byte("deterministic-random-source-for-test-0001-1ioQiLEbVCm1Y7NfWCf6oNWoV6p5E4spJgRXKQHdV44XcNvqywMnIYYcL8qZ4Wk")), "security", key.P384) + assert.NoError(t, err) + assert.NotNil(t, pub) + assert.NotNil(t, id) + assert.Equal(t, "harp.elastic.co/v1", id.APIVersion) + assert.Equal(t, "security", id.Description) + assert.Equal(t, "ContainerIdentity", id.Kind) + assert.Equal(t, "v2.ipk.A0X20rlE8Pqp-YoMG8SNOop918AyfoSF_R9Z7MF5vP5nUoc_ZSRWauQR6cL4DqgrRA", id.Public) + assert.Nil(t, id.Private) + assert.False(t, id.HasPrivateKey()) + }) + + t.Run("valid - legacy", func(t *testing.T) { + id, pub, err := New(bytes.NewBuffer([]byte("deterministic-random-source-for-test-0001")), "security", key.Legacy) + assert.NoError(t, err) + assert.NotNil(t, pub) + assert.NotNil(t, id) + assert.Equal(t, "harp.elastic.co/v1", id.APIVersion) + assert.Equal(t, "security", id.Description) + assert.Equal(t, "ContainerIdentity", id.Kind) + assert.Equal(t, "ZxTKWxgrG341_FxatkkfAxedMtfz1zJzAm6FUmitxHM", id.Public) assert.Nil(t, id.Private) assert.False(t, id.HasPrivateKey()) }) @@ -90,52 +125,15 @@ func TestCodec_FromReader(t *testing.T) { assert.Nil(t, id) }) - t.Run("valid", func(t *testing.T) { - id, err := FromReader(bytes.NewReader(securityIdentity)) + t.Run("valid - v1", func(t *testing.T) { + id, err := FromReader(bytes.NewReader(v1SecurityIdentity)) assert.NoError(t, err) assert.NotNil(t, id) }) -} - -func TestPublicKeysFromIdentities(t *testing.T) { - t.Run("empty", func(t *testing.T) { - identities := []string{} - publicKeys, err := SealingKeys(identities...) - assert.Error(t, err) - assert.Nil(t, publicKeys) - }) - - t.Run("invalid bech32 encoding", func(t *testing.T) { - identities := []string{ - "recovery1hytnx4qpta252s5s7wypzq7rp3puks38vd8p4x9nhysvfylyjl/q^^wvr6", - } - publicKeys, err := SealingKeys(identities...) - assert.Error(t, err) - assert.Nil(t, publicKeys) - }) - t.Run("valid", func(t *testing.T) { - identities := []string{ - "recovery1hytnx4qpta252s5s7wypzq7rp3puks38vd8p4x9nhysvfylyjlwscswvr6", - "security1eervzac2v26wxehktnf6lq0vpegrqcpg4uv4uxdr5vpzzmtdpflqttlghh", - } - publicKeys, err := SealingKeys(identities...) + t.Run("valid - v2", func(t *testing.T) { + id, err := FromReader(bytes.NewReader(v2SecurityIdentity)) assert.NoError(t, err) - assert.NotEmpty(t, publicKeys) - assert.Equal(t, [32]byte{0xc1, 0x48, 0x9f, 0x5a, 0x41, 0xc7, 0x32, 0xa1, 0x3, 0xc1, 0x65, 0x9e, 0xeb, 0xc1, 0x95, 0x47, 0xd6, 0xea, 0x53, 0x7f, 0xf5, 0x48, 0x2d, 0x61, 0xa0, 0x60, 0x81, 0xe2, 0xe9, 0x37, 0x8, 0x2}, *publicKeys[0]) - assert.Equal(t, [32]byte{0x74, 0x70, 0x5d, 0xdc, 0x92, 0xa0, 0x95, 0x8b, 0xa6, 0x45, 0xfd, 0x52, 0xe0, 0x10, 0x69, 0x71, 0x9f, 0x92, 0x5d, 0xdf, 0x7d, 0x86, 0x6b, 0xf7, 0x20, 0x80, 0xfa, 0xd4, 0x5c, 0x59, 0x70, 0x70}, *publicKeys[1]) - }) - - t.Run("valid - dedup", func(t *testing.T) { - identities := []string{ - "recovery1hytnx4qpta252s5s7wypzq7rp3puks38vd8p4x9nhysvfylyjlwscswvr6", - "recovery1hytnx4qpta252s5s7wypzq7rp3puks38vd8p4x9nhysvfylyjlwscswvr6", - "security1eervzac2v26wxehktnf6lq0vpegrqcpg4uv4uxdr5vpzzmtdpflqttlghh", - } - publicKeys, err := SealingKeys(identities...) - assert.NoError(t, err) - assert.NotEmpty(t, publicKeys) - assert.Equal(t, [32]byte{0xc1, 0x48, 0x9f, 0x5a, 0x41, 0xc7, 0x32, 0xa1, 0x3, 0xc1, 0x65, 0x9e, 0xeb, 0xc1, 0x95, 0x47, 0xd6, 0xea, 0x53, 0x7f, 0xf5, 0x48, 0x2d, 0x61, 0xa0, 0x60, 0x81, 0xe2, 0xe9, 0x37, 0x8, 0x2}, *publicKeys[0]) - assert.Equal(t, [32]byte{0x74, 0x70, 0x5d, 0xdc, 0x92, 0xa0, 0x95, 0x8b, 0xa6, 0x45, 0xfd, 0x52, 0xe0, 0x10, 0x69, 0x71, 0x9f, 0x92, 0x5d, 0xdf, 0x7d, 0x86, 0x6b, 0xf7, 0x20, 0x80, 0xfa, 0xd4, 0x5c, 0x59, 0x70, 0x70}, *publicKeys[1]) + assert.NotNil(t, id) }) } diff --git a/pkg/container/identity/key/generator.go b/pkg/container/identity/key/generator.go new file mode 100644 index 00000000..4a83e54c --- /dev/null +++ b/pkg/container/identity/key/generator.go @@ -0,0 +1,81 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package key + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "encoding/base64" + "fmt" + "io" + + "golang.org/x/crypto/nacl/box" +) + +func Legacy(random io.Reader) (*JSONWebKey, string, error) { + // Generate X25519 keys as identity + pub, priv, err := box.GenerateKey(random) + if err != nil { + return nil, "", fmt.Errorf("unable to generate identity keypair: %w", err) + } + + // Wrap as JWK + return &JSONWebKey{ + Kty: "OKP", + Crv: "X25519", + X: base64.RawURLEncoding.EncodeToString(pub[:]), + D: base64.RawURLEncoding.EncodeToString(priv[:]), + }, base64.RawURLEncoding.EncodeToString(pub[:]), err +} + +func Ed25519(random io.Reader) (*JSONWebKey, string, error) { + // Generate ed25519 keys as identity + pub, priv, err := ed25519.GenerateKey(random) + if err != nil { + return nil, "", fmt.Errorf("unable to generate identity keypair: %w", err) + } + + // Wrap as JWK + return &JSONWebKey{ + Kty: "OKP", + Crv: "Ed25519", + X: base64.RawURLEncoding.EncodeToString(pub[:]), + D: base64.RawURLEncoding.EncodeToString(priv[:]), + }, fmt.Sprintf("v1.ipk.%s", base64.RawURLEncoding.EncodeToString(pub[:])), err +} + +func P384(random io.Reader) (*JSONWebKey, string, error) { + // Generate ecdsa P-384 keys as identity + priv, err := ecdsa.GenerateKey(elliptic.P384(), random) + if err != nil { + return nil, "", fmt.Errorf("unable to generate identity keypair: %w", err) + } + + // Marshall as compressed point + pub := elliptic.MarshalCompressed(priv.Curve, priv.PublicKey.X, priv.PublicKey.Y) + + // Wrap as JWK + return &JSONWebKey{ + Kty: "EC", + Crv: "P-384", + X: base64.RawURLEncoding.EncodeToString(priv.PublicKey.X.Bytes()), + Y: base64.RawURLEncoding.EncodeToString(priv.PublicKey.Y.Bytes()), + D: base64.RawURLEncoding.EncodeToString(priv.D.Bytes()), + }, fmt.Sprintf("v2.ipk.%s", base64.RawURLEncoding.EncodeToString(pub)), err +} diff --git a/pkg/container/identity/key/json.go b/pkg/container/identity/key/json.go new file mode 100644 index 00000000..c1766df9 --- /dev/null +++ b/pkg/container/identity/key/json.go @@ -0,0 +1,108 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package key + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/sha512" + "encoding/base64" + "errors" + "fmt" + "math/big" + + "github.com/elastic/harp/pkg/sdk/security/crypto/extra25519" +) + +// JSONWebKey holds internal container key attributes. +type JSONWebKey struct { + Kty string `json:"kty"` + Crv string `json:"crv"` + X string `json:"x,omitempty"` + Y string `json:"y,omitempty"` + D string `json:"d,omitempty"` +} + +func (k *JSONWebKey) Sign(message []byte) (string, error) { + var sig []byte + + // Decode private key + d, err := base64.RawURLEncoding.DecodeString(k.D) + if err != nil { + return "", fmt.Errorf("unable to decode private key: %w", err) + } + + switch k.Crv { + case "Ed25519": + if len(d) != ed25519.PrivateKeySize { + return "", errors.New("invalid private key size") + } + + // Sign the message + sig = ed25519.Sign(ed25519.PrivateKey(d), message) + case "P-384": + if len(d) != 48 { + return "", errors.New("invalid private key size") + } + + // Rebuild the private key + var sk ecdsa.PrivateKey + sk.Curve = elliptic.P384() + sk.D = new(big.Int).SetBytes(d) + + digest := sha512.Sum384(message) + r, s, err := ecdsa.Sign(rand.Reader, &sk, digest[:]) + if err != nil { + return "", fmt.Errorf("unable to sign the identity: %w", err) + } + + // Assemble the signature + sig = append(r.Bytes(), s.Bytes()...) + } + + // Encode the signature + return base64.RawURLEncoding.EncodeToString(sig), nil +} + +// RecoveryKey returns the private encryption key from the private identity key. +func (k *JSONWebKey) RecoveryKey() (string, error) { + // Decode private key + privKeyRaw, err := base64.RawURLEncoding.DecodeString(k.D) + if err != nil { + return "", errors.New("invalid identity, private key is invalid") + } + + switch k.Crv { + case "X25519": // Legacy keys + return base64.RawURLEncoding.EncodeToString(privKeyRaw), nil + case "Ed25519": + // Convert Ed25519 private key to x25519 key. + var sk [32]byte + extra25519.PrivateKeyToCurve25519(&sk, privKeyRaw) + return fmt.Sprintf("v1.ck.%s", base64.RawURLEncoding.EncodeToString(sk[:])), nil + case "P-384": + // FIPS compliant sealing process use ECDSA P-384 key. + return fmt.Sprintf("v2.ck.%s", base64.RawURLEncoding.EncodeToString(privKeyRaw)), nil + default: + } + + // Unhandled key + return "", fmt.Errorf("unhandled private key format '%s'", k.Crv) +} diff --git a/pkg/container/identity/key/json_test.go b/pkg/container/identity/key/json_test.go new file mode 100644 index 00000000..09634ad8 --- /dev/null +++ b/pkg/container/identity/key/json_test.go @@ -0,0 +1,82 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package key + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + legacyPrivateKey = &JSONWebKey{ + Kty: "OKP", + Crv: "X25519", + X: "ZxTKWxgrG341_FxatkkfAxedMtfz1zJzAm6FUmitxHM", + D: "ZGV0ZXJtaW5pc3RpYy1yYW5kb20tc291cmNlLWZvci0", + } + v1PrivateKey = &JSONWebKey{ + Kty: "OKP", + Crv: "Ed25519", + X: "2BdsL_FTiaLRwyYwlA2urcZ8TLDdisbzBSEp-LUuHos", + D: "ZGV0ZXJtaW5pc3RpYy1yYW5kb20tc291cmNlLWZvci3YF2wv8VOJotHDJjCUDa6txnxMsN2KxvMFISn4tS4eiw", + } + v2PrivateKey = &JSONWebKey{ + Kty: "EC", + Crv: "P-384", + X: "RfbSuUTw-qn5igwbxI06in3XwDJ-hIX9H1nswXm8_mdShz9lJFZq5BHpwvgOqCtE", + Y: "ag16lWruEPkhWChmZnO52ne1iyLGAEVNbyx38NPMOqNZzV7yP9ugrzCa7pCz8eBr", + D: "aXN0aWMtcmFuZG9tLXNvdYiXCnZ-xg0Te8QN3AId4n-bdBdDfhXJjz1OngEo78g8", + } +) + +func TestJSONWebKey_RecoveryKey(t *testing.T) { + t.Run("D has invalid encoding", func(t *testing.T) { + id, err := (&JSONWebKey{ + D: "é", + }).RecoveryKey() + assert.Error(t, err) + assert.Empty(t, id) + }) + + t.Run("unhandled private key", func(t *testing.T) { + id, err := (&JSONWebKey{ + Crv: "P-256", + }).RecoveryKey() + assert.Error(t, err) + assert.Empty(t, id) + }) + + t.Run("valid - legacy", func(t *testing.T) { + id, err := legacyPrivateKey.RecoveryKey() + assert.NoError(t, err) + assert.Equal(t, "ZGV0ZXJtaW5pc3RpYy1yYW5kb20tc291cmNlLWZvci0", id) + }) + + t.Run("valid - v1", func(t *testing.T) { + id, err := v1PrivateKey.RecoveryKey() + assert.NoError(t, err) + assert.Equal(t, "v1.ck.6Of3g6qt-NPBzXSMNl4jPIZbrZIIwonT2pn7GCc4i3o", id) + }) + + t.Run("valid - v2", func(t *testing.T) { + id, err := v2PrivateKey.RecoveryKey() + assert.NoError(t, err) + assert.Equal(t, "v2.ck.aXN0aWMtcmFuZG9tLXNvdYiXCnZ-xg0Te8QN3AId4n-bdBdDfhXJjz1OngEo78g8", id) + }) +} diff --git a/pkg/container/identity/key/key.go b/pkg/container/identity/key/key.go new file mode 100644 index 00000000..b906349b --- /dev/null +++ b/pkg/container/identity/key/key.go @@ -0,0 +1,157 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package key + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/sha512" + "encoding/base64" + "errors" + "fmt" + "math/big" + "strings" + + "github.com/elastic/harp/pkg/sdk/security/crypto/extra25519" +) + +const ( + V1IdentityPublicKeyPrefix = "v1.ipk." + V2IdentityPublicKeyPrefix = "v2.ipk." +) + +// ----------------------------------------------------------------------------- + +type Key struct { + version uint32 + key interface{} + identity bool + public bool +} + +func FromString(input string) (*Key, error) { + switch { + // Ed25519 public key + case strings.HasPrefix(input, V1IdentityPublicKeyPrefix): + // Decode public key + pk, err := base64.RawURLEncoding.DecodeString(input[7:]) + if err != nil { + return nil, fmt.Errorf("unable to decode public key: %w", err) + } + if len(pk) != ed25519.PublicKeySize { + return nil, errors.New("invalid public key size") + } + + // Return wrapped key + return &Key{ + version: 1, + key: ed25519.PublicKey(pk), + identity: true, + public: true, + }, nil + + // EC P-384 public key + case strings.HasPrefix(input, V2IdentityPublicKeyPrefix): + // Decode public key + pkRaw, err := base64.RawURLEncoding.DecodeString(input[7:]) + if err != nil { + return nil, fmt.Errorf("unable to decode public key: %w", err) + } + x, y := elliptic.UnmarshalCompressed(elliptic.P384(), pkRaw) + if x == nil || y == nil { + return nil, errors.New("unable to unmarshal the public key") + } + + // Rebuild the public key + var pk ecdsa.PublicKey + pk.Curve = elliptic.P384() + pk.X = x + pk.Y = y + + // Return wrapped key + return &Key{ + version: 2, + key: &pk, + identity: true, + public: true, + }, nil + + // Unrecognized + default: + } + + // Default to error + return nil, fmt.Errorf("unrecognized key '%s'", input) +} + +// ----------------------------------------------------------------------------- + +func (k *Key) Verify(message, signature []byte) bool { + switch keyRaw := k.key.(type) { + case *ecdsa.PublicKey: + // Unpack signature + r := new(big.Int).SetBytes(signature[:48]) + s := new(big.Int).SetBytes(signature[48:]) + + // Compute digest + digest := sha512.Sum384(message) + + // Verify signature + return ecdsa.Verify(keyRaw, digest[:], r, s) + case ed25519.PublicKey: + // Verify the signature + return ed25519.Verify(keyRaw, message, signature) + default: + } + + return false +} + +func (k *Key) String() string { + var payload []byte + switch keyRaw := k.key.(type) { + case *ecdsa.PublicKey: + payload = elliptic.MarshalCompressed(keyRaw.Curve, keyRaw.X, keyRaw.Y) + case ed25519.PublicKey: + payload = keyRaw + default: + return "" + } + + return fmt.Sprintf("v%d.ipk.%s", k.version, base64.RawURLEncoding.EncodeToString(payload)) +} + +func (k *Key) SealingKey() string { + var payload []byte + switch keyRaw := k.key.(type) { + case *ecdsa.PublicKey: + payload = elliptic.MarshalCompressed(keyRaw.Curve, keyRaw.X, keyRaw.Y) + case ed25519.PublicKey: + // Convert Ed25519 to X25519 + var pkRaw [32]byte + if !extra25519.PublicKeyToCurve25519(&pkRaw, keyRaw) { + return "" + } + payload = pkRaw[:] + default: + return "" + } + + return fmt.Sprintf("v%d.sk.%s", k.version, base64.RawURLEncoding.EncodeToString(payload)) +} diff --git a/pkg/container/key.go b/pkg/container/key.go deleted file mode 100644 index bd23ba94..00000000 --- a/pkg/container/key.go +++ /dev/null @@ -1,101 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. -package container - -import ( - "bytes" - "crypto/rand" - "fmt" - "io" - - "github.com/awnumar/memguard" - "golang.org/x/crypto/argon2" - "golang.org/x/crypto/blake2b" - "golang.org/x/crypto/nacl/box" -) - -// GenerateOptions represents container key generation options. -type generateOptions struct { - dckdMasterKey *memguard.LockedBuffer - dckdTarget string - randomSource io.Reader -} - -// GenerateOption represents functional pattern builder for optional parameters. -type GenerateOption func(o *generateOptions) - -// WithDeterministicKey enables deterministic container key generation. -func WithDeterministicKey(masterKey *memguard.LockedBuffer, target string) GenerateOption { - return func(o *generateOptions) { - o.dckdMasterKey = masterKey - o.dckdTarget = target - } -} - -// WithRandom provides the random source for key generation. -func WithRandom(random io.Reader) GenerateOption { - return func(o *generateOptions) { - o.randomSource = random - } -} - -// ----------------------------------------------------------------------------- - -// CenerateKey create an X25519 key pair used as container identifier. -func GenerateKey(fopts ...GenerateOption) (publicKey, privateKey *[32]byte, err error) { - // Prepare defaults - opts := &generateOptions{ - dckdMasterKey: nil, - dckdTarget: "", - randomSource: rand.Reader, - } - - // Apply optional parameters - for _, f := range fopts { - f(opts) - } - - // Master key derivation - if opts.dckdMasterKey != nil { - // Argon2ID(masterKey, Blake2B-512(Target), 1, 64Mb, 4, 64) - // Don't clean bytes, already done by memguard. - masterKey := opts.dckdMasterKey.Bytes() - if len(masterKey) < 32 { - return nil, nil, fmt.Errorf("the master key must be 32 bytes long at least") - } - - // Generate deterministic salt - salt := blake2b.Sum512([]byte(opts.dckdTarget)) - defer memguard.WipeBytes(salt[:]) - - // Derive deterministic container key using Argon2id - dk := argon2.IDKey(masterKey[:32], salt[:], 1, 64*1024, 4, 64) - defer memguard.WipeBytes(dk) - - // Assign to seed - opts.randomSource = bytes.NewBuffer(dk) - } - - // Generate x25519 container key pair - pub, priv, errGen := box.GenerateKey(opts.randomSource) - if errGen != nil { - return nil, nil, fmt.Errorf("unable to generate container key: %w", errGen) - } - - // No error - return pub, priv, nil -} diff --git a/pkg/container/key_test.go b/pkg/container/key_test.go deleted file mode 100644 index d64c37a1..00000000 --- a/pkg/container/key_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. -package container - -import ( - "bytes" - "testing" - - "github.com/awnumar/memguard" - "github.com/stretchr/testify/assert" -) - -func TestGenerateKey(t *testing.T) { - t.Run("deterministic", func(t *testing.T) { - pub, pk, err := GenerateKey( - WithDeterministicKey(memguard.NewBufferFromBytes([]byte("deterministic-seed-for-test-00001")), "Release 64"), - ) - assert.NoError(t, err) - assert.NotNil(t, pk) - assert.Equal(t, [32]byte{0x8d, 0xa5, 0x56, 0x9c, 0x1d, 0xc3, 0xf7, 0x83, 0x3, 0xff, 0x54, 0x1b, 0x2a, 0x3f, 0xcb, 0x3a, 0xbb, 0x88, 0x9f, 0x91, 0x9, 0x93, 0xdf, 0xfc, 0xc7, 0x25, 0x8b, 0xeb, 0xfe, 0x27, 0x95, 0x2b}, *pk) - assert.NotNil(t, pub) - assert.Equal(t, [32]byte{0x77, 0xd8, 0x26, 0xf1, 0x0, 0xf3, 0x1d, 0x5c, 0xab, 0x5, 0x15, 0x3e, 0x19, 0xa0, 0xc4, 0xa, 0x17, 0xcb, 0x5c, 0x20, 0x2f, 0xa7, 0x19, 0x26, 0xc6, 0x63, 0x23, 0x75, 0x98, 0x2d, 0xc7, 0x4d}, *pub) - }) - - t.Run("deterministic - same key with different target", func(t *testing.T) { - pub, pk, err := GenerateKey( - WithDeterministicKey(memguard.NewBufferFromBytes([]byte("deterministic-seed-for-test-00001")), "Release 65"), - ) - assert.NoError(t, err) - assert.NotNil(t, pk) - assert.Equal(t, [32]byte{0x42, 0x59, 0x8a, 0xbb, 0xe3, 0xdc, 0xf5, 0x3b, 0xc4, 0x8, 0xd3, 0x2d, 0x11, 0x61, 0x66, 0xc0, 0x68, 0xb7, 0xce, 0xbd, 0xf1, 0x18, 0x59, 0x30, 0x26, 0x20, 0x56, 0xb9, 0x4b, 0x25, 0xfb, 0xc0}, *pk) - assert.NotNil(t, pub) - assert.Equal(t, [32]byte{0x58, 0x6a, 0xd, 0x2c, 0x80, 0x56, 0xb3, 0xe, 0x44, 0xcf, 0x4e, 0xec, 0x5a, 0x21, 0x91, 0xd0, 0xb0, 0xa, 0xc9, 0x5e, 0xf2, 0x8, 0x5c, 0x34, 0x42, 0xa9, 0x12, 0x3b, 0xb, 0xc7, 0xd0, 0x43}, *pub) - }) - - t.Run("master key too short", func(t *testing.T) { - pub, pk, err := GenerateKey( - WithDeterministicKey(memguard.NewBufferFromBytes([]byte("determini")), "Release 64"), - ) - assert.Error(t, err) - assert.Nil(t, pk) - assert.Nil(t, pub) - }) - - t.Run("default with given random source", func(t *testing.T) { - pub, pk, err := GenerateKey(WithRandom(bytes.NewReader([]byte("deterministic-seed-for-test-00001")))) - assert.NoError(t, err) - assert.NotNil(t, pk) - assert.Equal(t, [32]byte{0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x69, 0x73, 0x74, 0x69, 0x63, 0x2d, 0x73, 0x65, 0x65, 0x64, 0x2d, 0x66, 0x6f, 0x72, 0x2d, 0x74, 0x65, 0x73, 0x74, 0x2d, 0x30, 0x30, 0x30, 0x30}, *pk) - assert.NotNil(t, pub) - assert.Equal(t, [32]byte{0xb1, 0x8a, 0x7d, 0xd2, 0x0, 0xb6, 0xf7, 0x22, 0x9f, 0x51, 0x4b, 0x6b, 0xe7, 0x4a, 0x4c, 0x47, 0x81, 0x5a, 0x7f, 0xb7, 0x37, 0x77, 0x8f, 0x98, 0x5f, 0x8c, 0x59, 0xb1, 0xbc, 0x0, 0xb3, 0x5d}, *pub) - - }) - - t.Run("default", func(t *testing.T) { - pub, pk, err := GenerateKey() - assert.NoError(t, err) - assert.NotNil(t, pk) - assert.NotNil(t, pub) - }) -} diff --git a/pkg/container/seal/api.go b/pkg/container/seal/api.go new file mode 100644 index 00000000..b4b144d4 --- /dev/null +++ b/pkg/container/seal/api.go @@ -0,0 +1,61 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package seal + +import ( + "io" + + "github.com/awnumar/memguard" + + containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" +) + +// Streategy describes the sealing/unsealing contract. +type Strategy interface { + // CenerateKey create an key pair used as container identifier. + GenerateKey(...GenerateOption) (publicKey, privateKey string, err error) + // Seal the given container using the implemented algorithm. + Seal(io.Reader, *containerv1.Container, ...string) (*containerv1.Container, error) + // Unseal the given container using the given identity. + Unseal(*containerv1.Container, *memguard.LockedBuffer) (*containerv1.Container, error) +} + +// GenerateOptions represents container key generation options. +type GenerateOptions struct { + DCKDMasterKey *memguard.LockedBuffer + DCKDTarget string + RandomSource io.Reader +} + +// GenerateOption represents functional pattern builder for optional parameters. +type GenerateOption func(o *GenerateOptions) + +// WithDeterministicKey enables deterministic container key generation. +func WithDeterministicKey(masterKey *memguard.LockedBuffer, target string) GenerateOption { + return func(o *GenerateOptions) { + o.DCKDMasterKey = masterKey + o.DCKDTarget = target + } +} + +// WithRandom provides the random source for key generation. +func WithRandom(random io.Reader) GenerateOption { + return func(o *GenerateOptions) { + o.RandomSource = random + } +} diff --git a/pkg/container/seal/v1/api.go b/pkg/container/seal/v1/api.go new file mode 100644 index 00000000..6f8e66bb --- /dev/null +++ b/pkg/container/seal/v1/api.go @@ -0,0 +1,50 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package v1 + +import ( + "crypto/ed25519" + + "github.com/elastic/harp/pkg/container/seal" +) + +const ( + SealVersion = 1 +) + +const ( + containerSealedContentType = "application/vnd.harp.v1.SealedContainer" + publicKeySize = 32 + privateKeySize = 32 + encryptionKeySize = 32 + keyIdentifierSize = 32 + nonceSize = 24 + signatureSize = ed25519.SignatureSize + messageLimit = 64 * 1024 * 1024 + + staticSignatureNonce = "harp_container_psigk_box" + signatureDomainSeparation = "harp encrypted signature" +) + +// ----------------------------------------------------------------------------- + +func New() seal.Strategy { + return &adapter{} +} + +type adapter struct{} diff --git a/pkg/container/helpers.go b/pkg/container/seal/v1/helpers.go similarity index 72% rename from pkg/container/helpers.go rename to pkg/container/seal/v1/helpers.go index 6852fce0..7a7aa791 100644 --- a/pkg/container/helpers.go +++ b/pkg/container/seal/v1/helpers.go @@ -15,10 +15,10 @@ // specific language governing permissions and limitations // under the License. -package container +package v1 import ( - "crypto/rand" + "bytes" "errors" "fmt" "io" @@ -33,7 +33,57 @@ import ( "github.com/elastic/harp/pkg/sdk/security" ) -func packRecipient(payloadKey, ephPrivKey, peerPublicKey *[32]byte) (*containerv1.Recipient, error) { +func deriveSharedKeyFromRecipient(publicKey, privateKey *[privateKeySize]byte) *[encryptionKeySize]byte { + // Prepare nonce + var nonce [nonceSize]byte + copy(nonce[:], "harp_derived_id_sboxkey0") + + // Prepare payload + zero := make([]byte, 32) + memguard.WipeBytes(zero) + + // Use box as a key agreement function + var sharedKey [encryptionKeySize]byte + derivedKey := box.Seal(nil, zero, &nonce, publicKey, privateKey) + copy(sharedKey[:], derivedKey[len(derivedKey)-encryptionKeySize:]) + + // No error + return &sharedKey +} + +func computeHeaderHash(headers *containerv1.Header) ([]byte, error) { + // Check arguments + if headers == nil { + return nil, errors.New("unable process with nil headers") + } + + // Prepare signature + header, err := proto.Marshal(headers) + if err != nil { + return nil, fmt.Errorf("unable to marshal container headers") + } + + // Hash serialized proto + hash := blake2b.Sum512(header) + + // No error + return hash[:], nil +} + +func computeProtectedHash(headerHash, content []byte) []byte { + // Prepare protected content + protected := bytes.Buffer{} + protected.Write([]byte(signatureDomainSeparation)) + protected.WriteByte(0x00) + protected.Write(headerHash) + contentHash := blake2b.Sum512(content) + protected.Write(contentHash[:]) + + // No error + return protected.Bytes() +} + +func packRecipient(rand io.Reader, payloadKey, ephPrivKey, peerPublicKey *[publicKeySize]byte) (*containerv1.Recipient, error) { // Check arguments if payloadKey == nil { return nil, fmt.Errorf("unable to proceed with nil payload key") @@ -49,65 +99,28 @@ func packRecipient(payloadKey, ephPrivKey, peerPublicKey *[32]byte) (*containerv recipientKey := deriveSharedKeyFromRecipient(peerPublicKey, ephPrivKey) // Calculate identifier - identifier, err := keyIdentifierFromDerivedKey(&recipientKey) + identifier, err := keyIdentifierFromDerivedKey(recipientKey) if err != nil { return nil, fmt.Errorf("unable to derive key identifier: %w", err) } // Generate recipient nonce - var recipientNonce [24]byte - if _, err := io.ReadFull(rand.Reader, recipientNonce[:]); err != nil { + var recipientNonce [nonceSize]byte + if _, err := io.ReadFull(rand, recipientNonce[:]); err != nil { return nil, fmt.Errorf("unable to generate recipient nonce for encryption") } // Pack recipient recipient := &containerv1.Recipient{ Identifier: identifier, - Key: secretbox.Seal(recipientNonce[:], payloadKey[:], &recipientNonce, &recipientKey), + Key: secretbox.Seal(recipientNonce[:], payloadKey[:], &recipientNonce, recipientKey), } // Return recipient return recipient, nil } -func computeHeaderHash(headers *containerv1.Header) ([]byte, error) { - // Check arguments - if headers == nil { - return nil, errors.New("unable process with nil headers") - } - - // Prepare signature - header, err := proto.Marshal(headers) - if err != nil { - return nil, fmt.Errorf("unable to marshal container headers") - } - - // Hash serialized proto - hash := blake2b.Sum512(header) - - // No error - return hash[:], nil -} - -func deriveSharedKeyFromRecipient(publicKey, privateKey *[32]byte) [32]byte { - // Prepare nonce - var nonce [24]byte - copy(nonce[:], "harp_derived_id_sboxkey0") - - // Prepare payload - zero := make([]byte, 32) - memguard.WipeBytes(zero) - - // Use box as a key agreement function - var sharedKey [32]byte - derivedKey := box.Seal(nil, zero, &nonce, publicKey, privateKey) - copy(sharedKey[:], derivedKey[len(derivedKey)-32:]) - - // No error - return sharedKey -} - -func keyIdentifierFromDerivedKey(derivedKey *[32]byte) ([]byte, error) { +func keyIdentifierFromDerivedKey(derivedKey *[encryptionKeySize]byte) ([]byte, error) { // Hash the derived key h, err := blake2b.New512([]byte("harp signcryption box key identifier")) if err != nil { @@ -118,10 +131,10 @@ func keyIdentifierFromDerivedKey(derivedKey *[32]byte) ([]byte, error) { } // Return 32 bytes trucanted hash. - return h.Sum(nil)[0:32], nil + return h.Sum(nil)[0:keyIdentifierSize], nil } -func tryRecipientKeys(derivedKey *[32]byte, recipients []*containerv1.Recipient) ([]byte, error) { +func tryRecipientKeys(derivedKey *[encryptionKeySize]byte, recipients []*containerv1.Recipient) ([]byte, error) { // Calculate recipient identifier identifier, err := keyIdentifierFromDerivedKey(derivedKey) if err != nil { @@ -135,11 +148,11 @@ func tryRecipientKeys(derivedKey *[32]byte, recipients []*containerv1.Recipient) continue } - var nonce [24]byte - copy(nonce[:], r.Key[:24]) + var nonce [nonceSize]byte + copy(nonce[:], r.Key[:nonceSize]) // Try to decrypt the secretbox with the derived key. - payloadKey, isValid := secretbox.Open(nil, r.Key[24:], &nonce, derivedKey) + payloadKey, isValid := secretbox.Open(nil, r.Key[nonceSize:], &nonce, derivedKey) if !isValid { return nil, fmt.Errorf("invalid recipient encryption key") } diff --git a/pkg/container/seal/v1/helpers_test.go b/pkg/container/seal/v1/helpers_test.go new file mode 100644 index 00000000..b15ae402 --- /dev/null +++ b/pkg/container/seal/v1/helpers_test.go @@ -0,0 +1,170 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package v1 + +import ( + "bytes" + "testing" + + containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/nacl/box" +) + +func Test_deriveSharedKeyFromRecipient(t *testing.T) { + pk1, sk1, err := box.GenerateKey(bytes.NewReader([]byte("00001-deterministic-buffer-for-tests-26FBE7DED9E992BC36C06C988C1AC8A1E672B4B5959EF60672A983EFA7C8EE0F"))) + assert.NoError(t, err) + assert.NotNil(t, pk1) + assert.NotNil(t, sk1) + + pk2, sk2, err := box.GenerateKey(bytes.NewReader([]byte("00002-deterministic-buffer-for-tests-37ACB0DD3A3CE5A0960CCE0F6A0D7E663DFFD221FBE8EEB03B20D3AD91BCDD55"))) + assert.NoError(t, err) + assert.NotNil(t, pk2) + assert.NotNil(t, sk2) + + // ECDH(pk2, sk1) + dk1 := deriveSharedKeyFromRecipient(pk2, sk1) + assert.Equal(t, &[32]byte{ + 0xd1, 0xc0, 0x22, 0x38, 0x31, 0x6d, 0x7d, 0x6b, + 0x57, 0x88, 0x72, 0x3d, 0xc2, 0x82, 0xd7, 0xe2, + 0xfa, 0x6, 0x43, 0xb0, 0x98, 0x6f, 0x8, 0xe6, + 0x28, 0xb0, 0x86, 0x73, 0x15, 0x2e, 0x6e, 0x5, + }, dk1) + + // ECDH(pk1, sk2) + dk2 := deriveSharedKeyFromRecipient(pk1, sk2) + assert.NoError(t, err) + + // ECDH(pk1, sk2) == ECDH(pk2, sk1) + assert.Equal(t, dk1, dk2) +} + +func Test_keyIdentifierFromDerivedKey(t *testing.T) { + dk := &[32]byte{ + 0xd1, 0xc0, 0x22, 0x38, 0x31, 0x6d, 0x7d, 0x6b, + 0x57, 0x88, 0x72, 0x3d, 0xc2, 0x82, 0xd7, 0xe2, + 0xfa, 0x6, 0x43, 0xb0, 0x98, 0x6f, 0x8, 0xe6, + 0x28, 0xb0, 0x86, 0x73, 0x15, 0x2e, 0x6e, 0x5, + } + + id, err := keyIdentifierFromDerivedKey(dk) + assert.NoError(t, err) + assert.Equal(t, []byte{ + 0xdd, 0x3e, 0x93, 0x8a, 0x57, 0x74, 0xbd, 0xf5, + 0xed, 0xe, 0x9a, 0xae, 0x86, 0xd5, 0xd6, 0xf7, + 0xfc, 0xbb, 0x3d, 0xe0, 0x54, 0xa8, 0x18, 0x38, + 0x5e, 0xea, 0xa6, 0x46, 0xa9, 0xb0, 0xc8, 0x57, + }, id) +} + +func Test_packRecipient(t *testing.T) { + payloadKey := &[32]byte{} + + pk1, sk1, err := box.GenerateKey(bytes.NewReader([]byte("00001-deterministic-buffer-for-tests-26FBE7DED9E992BC36C06C988C1AC8A1E672B4B5959EF60672A983EFA7C8EE0F"))) + assert.NoError(t, err) + assert.NotNil(t, pk1) + assert.NotNil(t, sk1) + + pk2, sk2, err := box.GenerateKey(bytes.NewReader([]byte("00002-deterministic-buffer-for-tests-37ACB0DD3A3CE5A0960CCE0F6A0D7E663DFFD221FBE8EEB03B20D3AD91BCDD55"))) + assert.NoError(t, err) + assert.NotNil(t, pk2) + assert.NotNil(t, sk2) + + recipient, err := packRecipient(bytes.NewReader([]byte("00003-deterministic-buffer-for-tests")), payloadKey, sk1, pk2) + assert.NoError(t, err) + assert.NotNil(t, recipient) + assert.Equal(t, []byte{ + 0xdd, 0x3e, 0x93, 0x8a, 0x57, 0x74, 0xbd, 0xf5, + 0xed, 0xe, 0x9a, 0xae, 0x86, 0xd5, 0xd6, 0xf7, + 0xfc, 0xbb, 0x3d, 0xe0, 0x54, 0xa8, 0x18, 0x38, + 0x5e, 0xea, 0xa6, 0x46, 0xa9, 0xb0, 0xc8, 0x57, + }, recipient.Identifier) + assert.Equal(t, []byte{ + 0x30, 0x30, 0x30, 0x30, 0x33, 0x2d, 0x64, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x69, 0x73, + 0x74, 0x69, 0x63, 0x2d, 0x62, 0x75, 0x66, 0x66, + 0xc7, 0xc3, 0x15, 0x7a, 0x5b, 0x1b, 0x47, 0x31, + 0x5b, 0xb7, 0xb4, 0x4d, 0xe5, 0x92, 0x32, 0xd6, + 0xbf, 0xe7, 0x52, 0x9a, 0xff, 0x84, 0xb7, 0xaf, + 0x5f, 0x7, 0x47, 0xca, 0x57, 0x8f, 0x4e, 0x3c, + 0x28, 0x3a, 0x66, 0xa0, 0x4b, 0xec, 0x12, 0x23, + 0x7e, 0xa4, 0x37, 0x47, 0xf5, 0x53, 0x2a, 0xbb, + }, recipient.Key) +} + +func Test_tryRecipientKeys(t *testing.T) { + payloadKey := &[32]byte{} + + pk1, sk1, err := box.GenerateKey(bytes.NewReader([]byte("00001-deterministic-buffer-for-tests-26FBE7DED9E992BC36C06C988C1AC8A1E672B4B5959EF60672A983EFA7C8EE0F"))) + assert.NoError(t, err) + assert.NotNil(t, pk1) + assert.NotNil(t, sk1) + + pk2, sk2, err := box.GenerateKey(bytes.NewReader([]byte("00002-deterministic-buffer-for-tests-37ACB0DD3A3CE5A0960CCE0F6A0D7E663DFFD221FBE8EEB03B20D3AD91BCDD55"))) + assert.NoError(t, err) + assert.NotNil(t, pk2) + assert.NotNil(t, sk2) + + recipient, err := packRecipient(bytes.NewReader([]byte("00003-deterministic-buffer-for-tests")), payloadKey, sk1, pk2) + assert.NoError(t, err) + assert.NotNil(t, recipient) + assert.Equal(t, []byte{ + 0xdd, 0x3e, 0x93, 0x8a, 0x57, 0x74, 0xbd, 0xf5, + 0xed, 0xe, 0x9a, 0xae, 0x86, 0xd5, 0xd6, 0xf7, + 0xfc, 0xbb, 0x3d, 0xe0, 0x54, 0xa8, 0x18, 0x38, + 0x5e, 0xea, 0xa6, 0x46, 0xa9, 0xb0, 0xc8, 0x57, + }, recipient.Identifier) + assert.Equal(t, []byte{ + 0x30, 0x30, 0x30, 0x30, 0x33, 0x2d, 0x64, 0x65, + 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x69, 0x73, + 0x74, 0x69, 0x63, 0x2d, 0x62, 0x75, 0x66, 0x66, + 0xc7, 0xc3, 0x15, 0x7a, 0x5b, 0x1b, 0x47, 0x31, + 0x5b, 0xb7, 0xb4, 0x4d, 0xe5, 0x92, 0x32, 0xd6, + 0xbf, 0xe7, 0x52, 0x9a, 0xff, 0x84, 0xb7, 0xaf, + 0x5f, 0x7, 0x47, 0xca, 0x57, 0x8f, 0x4e, 0x3c, + 0x28, 0x3a, 0x66, 0xa0, 0x4b, 0xec, 0x12, 0x23, + 0x7e, 0xa4, 0x37, 0x47, 0xf5, 0x53, 0x2a, 0xbb, + }, recipient.Key) + + // ------------------------------------------------------------------------- + // ECDH(pk2, sk1) + dk := deriveSharedKeyFromRecipient(pk2, sk1) + assert.Equal(t, &[32]byte{ + 0xd1, 0xc0, 0x22, 0x38, 0x31, 0x6d, 0x7d, 0x6b, + 0x57, 0x88, 0x72, 0x3d, 0xc2, 0x82, 0xd7, 0xe2, + 0xfa, 0x6, 0x43, 0xb0, 0x98, 0x6f, 0x8, 0xe6, + 0x28, 0xb0, 0x86, 0x73, 0x15, 0x2e, 0x6e, 0x5, + }, dk) + + expectedID := []byte{ + 0xdd, 0x3e, 0x93, 0x8a, 0x57, 0x74, 0xbd, 0xf5, + 0xed, 0xe, 0x9a, 0xae, 0x86, 0xd5, 0xd6, 0xf7, + 0xfc, 0xbb, 0x3d, 0xe0, 0x54, 0xa8, 0x18, 0x38, + 0x5e, 0xea, 0xa6, 0x46, 0xa9, 0xb0, 0xc8, 0x57, + } + id, err := keyIdentifierFromDerivedKey(dk) + assert.NoError(t, err) + assert.Equal(t, expectedID, id) + assert.Equal(t, expectedID, recipient.Identifier) + + decodedPayloadKey, err := tryRecipientKeys(dk, []*containerv1.Recipient{ + recipient, + }) + assert.NoError(t, err) + assert.Equal(t, payloadKey[:], decodedPayloadKey) +} diff --git a/pkg/container/seal/v1/key.go b/pkg/container/seal/v1/key.go new file mode 100644 index 00000000..721ccb7c --- /dev/null +++ b/pkg/container/seal/v1/key.go @@ -0,0 +1,134 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package v1 + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "fmt" + "strings" + + "github.com/awnumar/memguard" + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/nacl/box" + + "github.com/elastic/harp/pkg/container/seal" + "github.com/elastic/harp/pkg/sdk/security/crypto/extra25519" +) + +const ( + PublicKeyPrefix = "v1.sk." + PrivateKeyPrefix = "v1.ck." +) + +// ----------------------------------------------------------------------------- + +// CenerateKey create an X25519 key pair used as container identifier. +func (a *adapter) GenerateKey(fopts ...seal.GenerateOption) (publicKey, privateKey string, err error) { + // Prepare defaults + opts := &seal.GenerateOptions{ + DCKDMasterKey: nil, + DCKDTarget: "", + RandomSource: rand.Reader, + } + + // Apply optional parameters + for _, f := range fopts { + f(opts) + } + + // Master key derivation + if opts.DCKDMasterKey != nil { + // Argon2ID(masterKey, Blake2B-512('harp deterministic salt v1', Target), 1, 64Mb, 4, 64) + // Don't clean bytes, already done by memguard. + masterKey := opts.DCKDMasterKey.Bytes() + if len(masterKey) < 32 { + return "", "", fmt.Errorf("the master key must be 32 bytes long at least") + } + + // Generate deterministic salt + h, err := blake2b.New512([]byte("harp deterministic salt v1")) + if err != nil { + return "", "", fmt.Errorf("unable to initialize salt derivation: %w", err) + } + h.Write([]byte(opts.DCKDTarget)) + salt := h.Sum(nil) + defer memguard.WipeBytes(salt) + + // Derive deterministic container key using Argon2id + dk := argon2.IDKey(masterKey[:32], salt, 1, 64*1024, 4, 64) + defer memguard.WipeBytes(dk) + + // Assign to seed + opts.RandomSource = bytes.NewBuffer(dk) + } + + // Generate x25519 container key pair + pub, priv, errGen := box.GenerateKey(opts.RandomSource) + if errGen != nil { + return "", "", fmt.Errorf("unable to generate container key: %w", errGen) + } + + // Encode keys + encodedPub := append([]byte(PublicKeyPrefix), base64.RawURLEncoding.EncodeToString(pub[:])...) + encodedPriv := append([]byte(PrivateKeyPrefix), base64.RawURLEncoding.EncodeToString(priv[:])...) + + // No error + return string(encodedPub), string(encodedPriv), nil +} + +// PublicKeys return the appropriate key format used by the sealing strategy. +func (a *adapter) publicKeys(keys ...string) ([]*[32]byte, error) { + // v1.pk.[data] + res := []*[publicKeySize]byte{} + + for _, key := range keys { + // Check key prefix + if !strings.HasPrefix(key, PublicKeyPrefix) { + return nil, fmt.Errorf("unsuppored public key '%s' for v1 seal algorithm", key) + } + + // Remove prefix if exists + key = strings.TrimPrefix(key, PublicKeyPrefix) + + // Decode key + keyRaw, err := base64.RawURLEncoding.DecodeString(key) + if err != nil { + return nil, fmt.Errorf("unable to decode public key '%s': %w", key, err) + } + + // Public key sanity checks + if len(keyRaw) != publicKeySize { + return nil, fmt.Errorf("invalid public key length for key '%s'", key) + } + if extra25519.IsEdLowOrder(keyRaw) { + return nil, fmt.Errorf("low order public key usage is forbidden for key '%s, try to generate a new one to fix the issue", key) + } + + // Copy the public key + var pk [publicKeySize]byte + copy(pk[:], keyRaw[:publicKeySize]) + + // Append it to sealing keys + res = append(res, &pk) + } + + // No error + return res, nil +} diff --git a/pkg/container/seal/v1/key_test.go b/pkg/container/seal/v1/key_test.go new file mode 100644 index 00000000..2279b0e1 --- /dev/null +++ b/pkg/container/seal/v1/key_test.go @@ -0,0 +1,78 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package v1 + +import ( + "bytes" + "testing" + + "github.com/awnumar/memguard" + "github.com/elastic/harp/pkg/container/seal" + "github.com/stretchr/testify/assert" +) + +func TestGenerateKey(t *testing.T) { + adapter := New() + + t.Run("deterministic", func(t *testing.T) { + pub, pk, err := adapter.GenerateKey( + seal.WithDeterministicKey(memguard.NewBufferFromBytes([]byte("deterministic-seed-for-test-00001")), "Release 64"), + ) + assert.NoError(t, err) + assert.NotNil(t, pk) + assert.Equal(t, "v1.ck.8B_H8o7_ygAD27fFbqhgq97hLeJb5Nh4v3xy0C9JYPg", pk) + assert.NotNil(t, pub) + assert.Equal(t, "v1.sk.qKXPnUP6-2Bb_4nYnmxOXyCdN4IV3AR5HooB33N3g2E", pub) + }) + + t.Run("deterministic - same key with different target", func(t *testing.T) { + pub, pk, err := adapter.GenerateKey( + seal.WithDeterministicKey(memguard.NewBufferFromBytes([]byte("deterministic-seed-for-test-00001")), "Release 65"), + ) + assert.NoError(t, err) + assert.NotNil(t, pk) + assert.Equal(t, "v1.ck.RIdVmnxg69ZKXkd7YknoIfvsnyfOTi792AhwlAIcaJ8", pk) + assert.NotNil(t, pub) + assert.Equal(t, "v1.sk.SLP3GYe7UT-ADwuS2Ak-UEFCKR3ddvMawbwlgUSDG3k", pub) + }) + + t.Run("master key too short", func(t *testing.T) { + pub, pk, err := adapter.GenerateKey( + seal.WithDeterministicKey(memguard.NewBufferFromBytes([]byte("determini")), "Release 64"), + ) + assert.Error(t, err) + assert.Empty(t, pk) + assert.Empty(t, pub) + }) + + t.Run("default with given random source", func(t *testing.T) { + pub, pk, err := adapter.GenerateKey(seal.WithRandom(bytes.NewReader([]byte("deterministic-seed-for-test-00001")))) + assert.NoError(t, err) + assert.NotNil(t, pk) + assert.Equal(t, "v1.ck.ZGV0ZXJtaW5pc3RpYy1zZWVkLWZvci10ZXN0LTAwMDA", pk) + assert.NotNil(t, pub) + assert.Equal(t, "v1.sk.sYp90gC29yKfUUtr50pMR4Faf7c3d4-YX4xZsbwAs10", pub) + + }) + + t.Run("default", func(t *testing.T) { + pub, pk, err := adapter.GenerateKey() + assert.NoError(t, err) + assert.NotEmpty(t, pk) + assert.NotEmpty(t, pub) + }) +} diff --git a/pkg/container/seal/v1/seal.go b/pkg/container/seal/v1/seal.go new file mode 100644 index 00000000..5c8c88e1 --- /dev/null +++ b/pkg/container/seal/v1/seal.go @@ -0,0 +1,146 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package v1 + +import ( + "crypto/ed25519" + "errors" + "fmt" + "io" + + "github.com/awnumar/memguard" + "golang.org/x/crypto/nacl/box" + "golang.org/x/crypto/nacl/secretbox" + "google.golang.org/protobuf/proto" + + containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" + "github.com/elastic/harp/pkg/sdk/security/crypto/extra25519" + "github.com/elastic/harp/pkg/sdk/types" +) + +// Seal a secret container +//nolint:funlen,gocyclo // To refactor +func (a *adapter) Seal(rand io.Reader, container *containerv1.Container, encodedPeerPublicKeys ...string) (*containerv1.Container, error) { + // Check parameters + if types.IsNil(container) { + return nil, fmt.Errorf("unable to process nil container") + } + if types.IsNil(container.Headers) { + return nil, fmt.Errorf("unable to process nil container headers") + } + if len(encodedPeerPublicKeys) == 0 { + return nil, fmt.Errorf("unable to process empty public keys") + } + + // Convert public keys + peerPublicKeys, err := a.publicKeys(encodedPeerPublicKeys...) + if err != nil { + return nil, fmt.Errorf("unable to convert peer public keys: %w", err) + } + + // Serialize protobuf payload + content, err := proto.Marshal(container) + if err != nil { + return nil, fmt.Errorf("unable to encode container content: %w", err) + } + + // Check cleartext message size. + if len(content) > messageLimit { + return nil, errors.New("unable to seal the container, container is too large") + } + + // Generate payload encryption key + var payloadKey [encryptionKeySize]byte + if _, err = io.ReadFull(rand, payloadKey[:]); err != nil { + return nil, fmt.Errorf("unable to generate payload key for encryption") + } + + // Generate ephemeral signing key + sigPub, sigPriv, err := ed25519.GenerateKey(rand) + if err != nil { + return nil, fmt.Errorf("unable to generate signing keypair") + } + + // Encrypt public signature key + var pubSigNonce [nonceSize]byte + copy(pubSigNonce[:], staticSignatureNonce) + encryptedPubSig := secretbox.Seal(nil, sigPub, &pubSigNonce, &payloadKey) + memguard.WipeBytes(pubSigNonce[:]) + + // Generate ephemeral encryption key + encPub, encPriv, err := box.GenerateKey(rand) + if err != nil { + return nil, fmt.Errorf("unable to generate ephemeral encryption keypair") + } + + // Prepare sealed container + containerHeaders := &containerv1.Header{ + ContentType: containerSealedContentType, + EncryptionPublicKey: encPub[:], + ContainerBox: encryptedPubSig, + Recipients: []*containerv1.Recipient{}, + SealVersion: SealVersion, + } + + // Process recipients + for _, peerPublicKey := range peerPublicKeys { + if types.IsNil(peerPublicKey) { + // Ignore nil key + continue + } + if extra25519.IsEdLowOrder(peerPublicKey[:]) { + return nil, fmt.Errorf("unable to process with low order public key") + } + + // Pack recipient using its public key + r, errPack := packRecipient(rand, &payloadKey, encPriv, peerPublicKey) + if errPack != nil { + return nil, fmt.Errorf("unable to pack container recipient (%X): %w", *peerPublicKey, err) + } + + // Append to container + containerHeaders.Recipients = append(containerHeaders.Recipients, r) + } + + // Sanity check + if len(containerHeaders.Recipients) == 0 { + return nil, errors.New("unable to seal a container without recipients") + } + + // Compute header hash + headerHash, err := computeHeaderHash(containerHeaders) + if err != nil { + return nil, fmt.Errorf("unable to compute header hash: %w", err) + } + + // Prepare protected content + protectedHash := computeProtectedHash(headerHash, content) + + // Sign th protected content + containerSig := ed25519.Sign(sigPriv, protectedHash) + + // Prepare encryption nonce form sigHash + var sigNonce [nonceSize]byte + copy(sigNonce[:], headerHash[:nonceSize]) + + // No error + return &containerv1.Container{ + Headers: containerHeaders, + Raw: secretbox.Seal(nil, append(containerSig, content...), &sigNonce, &payloadKey), + }, nil +} diff --git a/pkg/container/seal/v1/seal_test.go b/pkg/container/seal/v1/seal_test.go new file mode 100644 index 00000000..3350c099 --- /dev/null +++ b/pkg/container/seal/v1/seal_test.go @@ -0,0 +1,227 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package v1 + +import ( + "crypto/rand" + "testing" + + "github.com/awnumar/memguard" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/assert" + + containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" +) + +var ( + opt = cmp.FilterPath( + func(p cmp.Path) bool { + // Remove ignoring of the fields below once go-cmp is able to ignore generated fields. + // See https://github.com/google/go-cmp/issues/153 + ignoreXXXCache := + p.String() == "XXX_sizecache" || + p.String() == "Headers.XXX_sizecache" + return ignoreXXXCache + }, cmp.Ignore()) + + ignoreOpts = []cmp.Option{ + cmpopts.IgnoreUnexported(containerv1.Container{}), + cmpopts.IgnoreUnexported(containerv1.Header{}), + opt, + } +) + +// ----------------------------------------------------------------------------- + +func TestSeal(t *testing.T) { + + type args struct { + container *containerv1.Container + peersPublicKey []string + } + tests := []struct { + name string + args args + want *containerv1.Container + wantErr bool + }{ + { + name: "nil", + wantErr: true, + }, + { + name: "empty container", + args: args{ + container: &containerv1.Container{}, + }, + wantErr: true, + }, + { + name: "empty container headers", + args: args{ + container: &containerv1.Container{ + Headers: &containerv1.Header{}, + }, + }, + wantErr: true, + }, + { + name: "empty container with public keys", + args: args{ + container: &containerv1.Container{ + Headers: &containerv1.Header{}, + }, + peersPublicKey: []string{ + "v1.sk.qKXPnUP6-2Bb_4nYnmxOXyCdN4IV3AR5HooB33N3g2E", + "v1.sk.sYp90gC29yKfUUtr50pMR4Faf7c3d4-YX4xZsbwAs10", + }, + }, + wantErr: false, + }, + { + name: "valid container with public keys", + args: args{ + container: &containerv1.Container{ + Headers: &containerv1.Header{}, + Raw: memguard.NewBufferRandom(1024).Bytes(), + }, + peersPublicKey: []string{ + "v1.sk.qKXPnUP6-2Bb_4nYnmxOXyCdN4IV3AR5HooB33N3g2E", + "v1.sk.sYp90gC29yKfUUtr50pMR4Faf7c3d4-YX4xZsbwAs10", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adapter := New() + _, err := adapter.Seal(rand.Reader, tt.args.container, tt.args.peersPublicKey...) + if (err != nil) != tt.wantErr { + t.Errorf("Seal() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +// ----------------------------------------------------------------------------- + +func Test_Seal_Unseal(t *testing.T) { + adapter := New() + + publicKey1, privateKey1, err := adapter.GenerateKey() + assert.NoError(t, err) + + input := &containerv1.Container{ + Headers: &containerv1.Header{ + ContentEncoding: "gzip", + ContentType: "application/vnd.harp.v1.Bundle", + }, + Raw: memguard.NewBufferRandom(1024).Bytes(), + } + + sealed, err := adapter.Seal(rand.Reader, input, publicKey1) + if err != nil { + t.Fatalf("unable to seal container: %v", err) + } + + unsealed, err := adapter.Unseal(sealed, memguard.NewBufferFromBytes([]byte(privateKey1))) + if err != nil { + t.Fatalf("unable to unseal container: %v", err) + } + + if diff := cmp.Diff(unsealed, input, ignoreOpts...); diff != "" { + t.Errorf("Seal/Unseal()\n-got/+want\ndiff %s", diff) + } +} + +func Test_Seal_Fuzz(t *testing.T) { + adapter := New() + + // Making sure the function never panics + for i := 0; i < 500; i++ { + f := fuzz.New() + + // Prepare arguments + var publicKey string + input := containerv1.Container{ + Headers: &containerv1.Header{}, + Raw: []byte{0x00, 0x00}, + } + + f.Fuzz(&input.Headers) + f.Fuzz(&input.Raw) + f.Fuzz(&publicKey) + + // Execute + adapter.Seal(rand.Reader, &input, publicKey) + } +} + +func Test_UnSeal_Fuzz(t *testing.T) { + // Memguard buffer is excluded from fuzz for random race condition error + // investigation will be done in a separated thread. + identity := memguard.NewBufferRandom(32) + + adapter := New() + + // Making sure the function never panics + for i := 0; i < 500; i++ { + f := fuzz.New() + + // Prepare arguments + input := containerv1.Container{ + Headers: &containerv1.Header{}, + Raw: []byte{0x00, 0x00}, + } + + f.Fuzz(&input.Headers) + f.Fuzz(&input.Raw) + + // Execute + adapter.Unseal(&input, identity) + } +} + +// ----------------------------------------------------------------------------- +func benchmarkSeal(container *containerv1.Container, peersPublicKeys []string, b *testing.B) { + adapter := New() + for n := 0; n < b.N; n++ { + _, err := adapter.Seal(rand.Reader, container, peersPublicKeys...) + if err != nil { + b.Fatal(err) + } + } +} + +func Benchmark_Seal(b *testing.B) { + publicKey, _, err := New().GenerateKey() + assert.NoError(b, err) + + input := &containerv1.Container{ + Headers: &containerv1.Header{ + ContentEncoding: "gzip", + ContentType: "application/vnd.harp.v1.Bundle", + }, + Raw: make([]byte, 1024), + } + + benchmarkSeal(input, []string{publicKey}, b) +} diff --git a/pkg/container/seal/v1/unseal.go b/pkg/container/seal/v1/unseal.go new file mode 100644 index 00000000..060033c2 --- /dev/null +++ b/pkg/container/seal/v1/unseal.go @@ -0,0 +1,136 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package v1 + +import ( + "crypto/ed25519" + "encoding/base64" + "fmt" + "strings" + + "github.com/awnumar/memguard" + "golang.org/x/crypto/nacl/secretbox" + "google.golang.org/protobuf/proto" + + containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" + "github.com/elastic/harp/pkg/sdk/types" +) + +// Unseal a sealed container with the given identity +//nolint:gocyclo,funlen // To refactor +func (a *adapter) Unseal(container *containerv1.Container, identity *memguard.LockedBuffer) (*containerv1.Container, error) { + // Check parameters + if types.IsNil(container) { + return nil, fmt.Errorf("unable to process nil container") + } + if types.IsNil(container.Headers) { + return nil, fmt.Errorf("unable to process nil container headers") + } + if identity == nil { + return nil, fmt.Errorf("unable to process without container key") + } + + // Check headers + if container.Headers.ContentType != containerSealedContentType { + return nil, fmt.Errorf("unable to unseal container") + } + + // Check ephemeral container public encryption key + if len(container.Headers.EncryptionPublicKey) != publicKeySize { + return nil, fmt.Errorf("invalid container public size") + } + var publicKey [publicKeySize]byte + copy(publicKey[:], container.Headers.EncryptionPublicKey[:publicKeySize]) + + // Decode private key + privRaw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(identity.String(), PrivateKeyPrefix)) + if err != nil { + return nil, fmt.Errorf("unable to decode private key: %w", err) + } + if len(privRaw) != privateKeySize { + return nil, fmt.Errorf("invalid identity private key length") + } + var pk [privateKeySize]byte + copy(pk[:], privRaw[:privateKeySize]) + + // Precompute identifier + derivedKey := deriveSharedKeyFromRecipient(&publicKey, &pk) + + // Try recipients + payloadKey, err := tryRecipientKeys(derivedKey, container.Headers.Recipients) + if err != nil { + return nil, fmt.Errorf("unable to unseal container: error occurred during recipient key tests: %w", err) + } + + // Check private key + if len(payloadKey) != encryptionKeySize { + return nil, fmt.Errorf("unable to unseal container: invalid encryption key size") + } + var encryptionKey [encryptionKeySize]byte + copy(encryptionKey[:], payloadKey[:encryptionKeySize]) + + // Prepare sig nonce + var pubSigNonce [nonceSize]byte + copy(pubSigNonce[:], "harp_container_psigk_box") + + // Decrypt signing public key + containerSignKeyRaw, ok := secretbox.Open(nil, container.Headers.ContainerBox, &pubSigNonce, &encryptionKey) + if !ok { + return nil, fmt.Errorf("invalid container key") + } + if len(containerSignKeyRaw) != ed25519.PublicKeySize { + return nil, fmt.Errorf("unable to unseal container: invalid signature key size") + } + + // Compute headers hash + headerHash, err := computeHeaderHash(container.Headers) + if err != nil { + return nil, fmt.Errorf("unable to compute header hash: %w", err) + } + + // Extract payload nonce + var payloadNonce [nonceSize]byte + copy(payloadNonce[:], headerHash[:nonceSize]) + + // Decrypt payload + payloadRaw, ok := secretbox.Open(nil, container.Raw, &payloadNonce, &encryptionKey) + if !ok || len(payloadRaw) < signatureSize { + return nil, fmt.Errorf("invalid ciphered content") + } + + // Extract signature / content + detachedSig := payloadRaw[:signatureSize] + content := payloadRaw[signatureSize:] + + // Prepare protected content + protectedHash := computeProtectedHash(headerHash, content) + + // Validate signature + if !ed25519.Verify(containerSignKeyRaw, protectedHash, detachedSig) { + return nil, fmt.Errorf("invalid container signature") + } + + // Unmarshal inner container + out := &containerv1.Container{} + if err := proto.Unmarshal(content, out); err != nil { + return nil, fmt.Errorf("unable to unpack inner content: %w", err) + } + + // No error + return out, nil +} diff --git a/pkg/container/seal/v2/api.go b/pkg/container/seal/v2/api.go new file mode 100644 index 00000000..e5ba1f9d --- /dev/null +++ b/pkg/container/seal/v2/api.go @@ -0,0 +1,53 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package v2 + +import ( + "crypto/elliptic" + + "github.com/elastic/harp/pkg/container/seal" +) + +const ( + SealVersion = 2 +) + +const ( + containerSealedContentType = "application/vnd.harp.v1.SealedContainer" + seedSize = 32 + publicKeySize = 49 + privateKeySize = 48 + encryptionKeySize = 32 + nonceSize = 16 + macSize = 48 + signatureSize = 96 + messageLimit = 64 * 1024 * 1024 +) + +var ( + encryptionCurve = elliptic.P384() + signatureCurve = elliptic.P384() +) + +// ----------------------------------------------------------------------------- + +func New() seal.Strategy { + return &adapter{} +} + +type adapter struct{} diff --git a/pkg/container/seal/v2/helpers.go b/pkg/container/seal/v2/helpers.go new file mode 100644 index 00000000..207f95b9 --- /dev/null +++ b/pkg/container/seal/v2/helpers.go @@ -0,0 +1,418 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package v2 + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/hmac" + cryptorand "crypto/rand" + "crypto/sha512" + "encoding/binary" + "errors" + "fmt" + "io" + + "github.com/awnumar/memguard" + "golang.org/x/crypto/hkdf" + "google.golang.org/protobuf/proto" + + containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" + "github.com/elastic/harp/pkg/sdk/security" +) + +func tryRecipientKeys(derivedKey *[32]byte, recipients []*containerv1.Recipient) (*[32]byte, error) { + // Calculate recipient identifier + identifier, err := keyIdentifierFromDerivedKey(derivedKey) + if err != nil { + return nil, fmt.Errorf("unable to generate identifier: %w", err) + } + + // Find matching recipient + for _, r := range recipients { + // Check recipient identifiers + if !security.SecureCompare(identifier, r.Identifier) { + continue + } + + // Try to decrypt the secretbox with the derived key. + clearText, err := decrypt(r.Key, derivedKey) + if err != nil { + return nil, fmt.Errorf("invalid recipient encryption key") + } + + var payloadKey [32]byte + copy(payloadKey[:], clearText) + + // Encryption key found, return no error. + return &payloadKey, nil + } + + // No recipient found in list. + return nil, fmt.Errorf("no recipient found") +} + +func prepareSignature(rand io.Reader, encryptionKey *[32]byte) (*ecdsa.PrivateKey, []byte, error) { + // Generate ephemeral signing key + sigPriv, err := ecdsa.GenerateKey(signatureCurve, cryptorand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("unable to generate signing keypair") + } + + // Compress public key point + sigPub := elliptic.MarshalCompressed(sigPriv.Curve, sigPriv.PublicKey.X, sigPriv.PublicKey.Y) + + // Encrypt public signing key + encryptedPubSig, err := encrypt(rand, sigPub, encryptionKey) + if err != nil { + return nil, nil, fmt.Errorf("unable to encrypt public signing key: %w", err) + } + + // Cleanup + memguard.WipeBytes(sigPub) + + // No error + return sigPriv, encryptedPubSig, nil +} + +func signContainer(sigPriv *ecdsa.PrivateKey, headers *containerv1.Header, container *containerv1.Container) (content, containerSig []byte, err error) { + // Serialize protobuf payload + content, err = proto.Marshal(container) + if err != nil { + return nil, nil, fmt.Errorf("unable to encode container content: %w", err) + } + + // Compute header hash + headerHash, err := computeHeaderHash(headers) + if err != nil { + return nil, nil, fmt.Errorf("unable to compute header hash: %w", err) + } + + // Compute protected content hash + protectedHash := computeProtectedHash(headerHash, content) + + // Sign the protected content + r, s, err := ecdsa.Sign(cryptorand.Reader, sigPriv, protectedHash) + if err != nil { + return nil, nil, fmt.Errorf("unable to sign protected content: %w", err) + } + + // Container signature + containerSig = append(r.Bytes(), s.Bytes()...) + + // No error + return content, containerSig, nil +} + +func generatedEncryptionKey(rand io.Reader) (*[32]byte, error) { + // Generate payload encryption key + var payloadKey [encryptionKeySize]byte + if _, err := io.ReadFull(rand, payloadKey[:]); err != nil { + return nil, fmt.Errorf("unable to generate payload key for encryption") + } + + // No error + return &payloadKey, nil +} + +func encrypt(rand io.Reader, plaintext []byte, key *[32]byte) ([]byte, error) { + // Check cleartext message size. + if len(plaintext) > messageLimit { + return nil, errors.New("value too large") + } + + // Generate random nonce + var seed [seedSize]byte + if _, err := io.ReadFull(rand, seed[:]); err != nil { + return nil, fmt.Errorf("unable to generate random encryption nonce: %w", err) + } + + // Derive keys from seed and secret key + ek, n2, ak, err := kdf(key, seed[:]) + if err != nil { + return nil, fmt.Errorf("unable to derive keys from seed: %w", err) + } + + // Prepare an AES-256-CTR stream cipher + block, err := aes.NewCipher(ek) + if err != nil { + return nil, fmt.Errorf("unable to prepare block cipher: %w", err) + } + ciph := cipher.NewCTR(block, n2) + + // Encrypt the payload + c := make([]byte, len(plaintext)) + ciph.XORKeyStream(c, plaintext) + + // Compute MAC + t, err := mac(ak, seed[:], c) + if err != nil { + return nil, fmt.Errorf("paseto: unable to compute MAC: %w", err) + } + + // Serialize final payload + // n || c || t + body := append([]byte{}, seed[:]...) + body = append(body, c...) + body = append(body, t...) + + // No error + return body, nil +} + +func decrypt(ciphertext []byte, key *[32]byte) ([]byte, error) { + // Check arguments + if len(ciphertext) < nonceSize { + return nil, errors.New("ciphered text too short") + } + + // Extract components + n := ciphertext[:seedSize] + t := ciphertext[len(ciphertext)-macSize:] + c := ciphertext[seedSize : len(ciphertext)-macSize] + + // Derive keys from seed and secret key + ek, n2, ak, err := kdf(key, n) + if err != nil { + return nil, fmt.Errorf("unable to derive keys from seed: %w", err) + } + + // Compute MAC + t2, err := mac(ak, n, c) + if err != nil { + return nil, fmt.Errorf("unable to compute MAC: %w", err) + } + + // Time-constant compare MAC + if !security.SecureCompare(t, t2) { + return nil, errors.New("invalid pre-authentication header") + } + + // Prepare an AES-256-CTR stream cipher + block, err := aes.NewCipher(ek) + if err != nil { + return nil, fmt.Errorf("unable to prepare block cipher: %w", err) + } + ciph := cipher.NewCTR(block, n2) + + // Decrypt the payload + m := make([]byte, len(c)) + ciph.XORKeyStream(m, c) + + // No error + return m, nil +} + +func computeHeaderHash(headers *containerv1.Header) ([]byte, error) { + // Check arguments + if headers == nil { + return nil, errors.New("unable process with nil headers") + } + + // Prepare signature + header, err := proto.Marshal(headers) + if err != nil { + return nil, fmt.Errorf("unable to marshal container headers") + } + + // Hash serialized proto + hash := sha512.Sum512(header) + + // No error + return hash[:], nil +} + +func computeProtectedHash(headerHash, content []byte) []byte { + // Prepare protected content + protected := bytes.Buffer{} + protected.Write([]byte("harp fips encrypted signature")) + protected.WriteByte(0x00) + protected.Write(headerHash) + contentHash := sha512.Sum512(content) + protected.Write(contentHash[:]) + + // No error + return protected.Bytes() +} + +func packRecipient(rand io.Reader, payloadKey *[32]byte, ephPrivKey *ecdsa.PrivateKey, peerPublicKey *ecdsa.PublicKey) (*containerv1.Recipient, error) { + // Check arguments + if payloadKey == nil { + return nil, fmt.Errorf("unable to proceed with nil payload key") + } + if ephPrivKey == nil { + return nil, fmt.Errorf("unable to proceed with nil private key") + } + if peerPublicKey == nil { + return nil, fmt.Errorf("unable to proceed with nil public key") + } + + // Create identifier + recipientKey, err := deriveSharedKeyFromRecipient(peerPublicKey, ephPrivKey) + if err != nil { + return nil, fmt.Errorf("unable to execute key agreement: %w", err) + } + + // Calculate identifier + identifier, err := keyIdentifierFromDerivedKey(recipientKey) + if err != nil { + return nil, fmt.Errorf("unable to derive key identifier: %w", err) + } + + // Encrypt the payload key + encryptedKey, err := encrypt(rand, payloadKey[:], recipientKey) + if err != nil { + return nil, fmt.Errorf("unable to encrypt payload key for recipient: %w", err) + } + + // Pack recipient + recipient := &containerv1.Recipient{ + Identifier: identifier, + Key: encryptedKey, + } + + // Return recipient + return recipient, nil +} + +func deriveSharedKeyFromRecipient(publicKey *ecdsa.PublicKey, privateKey *ecdsa.PrivateKey) (*[32]byte, error) { + // Compute Z - ECDH(localPrivate, remotePublic) + Z, _ := privateKey.Curve.ScalarMult(publicKey.X, publicKey.Y, privateKey.D.Bytes()) + + // Prepare info: ( AlgorithmID || PartyInfo || KeyLength ) + fixedInfo := []byte{} + fixedInfo = append(fixedInfo, lengthPrefixedArray([]byte("A256CTR"))...) + fixedInfo = append(fixedInfo, uint32ToBytes(encryptionKeySize)...) + + // HKDF-HMAC-SHA512 + kdf := hkdf.New(sha512.New, Z.Bytes(), nil, fixedInfo) + + var sharedSecret [encryptionKeySize]byte + if _, err := io.ReadFull(kdf, sharedSecret[:]); err != nil { + return nil, fmt.Errorf("unable to derive shared secret: %w", err) + } + + // No error + return &sharedSecret, nil +} + +func keyIdentifierFromDerivedKey(derivedKey *[32]byte) ([]byte, error) { + // HMAC-SHA512 + h := hmac.New(sha512.New, []byte("harp signcryption box key identifier")) + if _, err := h.Write(derivedKey[:]); err != nil { + return nil, fmt.Errorf("unable to generate recipient identifier") + } + + // Return 32 bytes truncated hash. + return h.Sum(nil)[0:encryptionKeySize], nil +} + +func lengthPrefixedArray(value []byte) []byte { + if len(value) == 0 { + return []byte{} + } + result := make([]byte, 4) + binary.BigEndian.PutUint32(result, uint32(len(value))) + + return append(result, value...) +} + +func uint32ToBytes(value uint32) []byte { + result := make([]byte, 4) + binary.BigEndian.PutUint32(result, value) + + return result +} + +func kdf(key *[32]byte, n []byte) (ek, n2, ak []byte, err error) { + // Check arguments + if key == nil { + return nil, nil, nil, errors.New("unable to derive keys from a nil seed") + } + + // Prepare HKDF-HMAC-SHA384 + encKDF := hkdf.New(sha512.New384, key[:], nil, append([]byte("harp-encryption-key-v2"), n...)) + + // Derive encryption key + tmp := make([]byte, encryptionKeySize+nonceSize) + if _, err := io.ReadFull(encKDF, tmp); err != nil { + return nil, nil, nil, fmt.Errorf("unable to generate encryption key from seed: %w", err) + } + + // Split encryption key (Ek) and nonce (n2) + ek = tmp[:encryptionKeySize] + n2 = tmp[encryptionKeySize:] + + // Derive authentication key + authKDF := hkdf.New(sha512.New384, key[:], nil, append([]byte("harp-auth-key-for-aead"), n...)) + + // Derive authentication key + ak = make([]byte, nonceSize) + if _, err := io.ReadFull(authKDF, ak); err != nil { + return nil, nil, nil, fmt.Errorf("unable to generate authentication key from seed: %w", err) + } + + // No error + return ek, n2, ak, nil +} + +func mac(ak, n, c []byte) ([]byte, error) { + // Compute pre-authenticated content + preAuth, err := pae([]byte("harp-authentication-tag-v2"), n, c) + if err != nil { + return nil, err + } + + // Compute MAC + mac := hmac.New(sha512.New384, ak) + + // Hash pre-authentication content + mac.Write(preAuth) + + // No error + return mac.Sum(nil), nil +} + +func pae(pieces ...[]byte) ([]byte, error) { + output := &bytes.Buffer{} + + // Encode piece count + count := len(pieces) + if err := binary.Write(output, binary.LittleEndian, uint64(count)); err != nil { + return nil, err + } + + // For each element + for i := range pieces { + // Encode size + if err := binary.Write(output, binary.LittleEndian, uint64(len(pieces[i]))); err != nil { + return nil, err + } + + // Encode data + if _, err := output.Write(pieces[i]); err != nil { + return nil, err + } + } + + // No error + return output.Bytes(), nil +} diff --git a/pkg/container/seal/v2/helpers_test.go b/pkg/container/seal/v2/helpers_test.go new file mode 100644 index 00000000..73ac80b0 --- /dev/null +++ b/pkg/container/seal/v2/helpers_test.go @@ -0,0 +1,186 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package v2 + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "reflect" + "testing" + + containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" + "github.com/stretchr/testify/assert" +) + +func Test_deriveSharedKeyFromRecipient(t *testing.T) { + key1, err := ecdsa.GenerateKey(elliptic.P384(), bytes.NewReader([]byte("00001-deterministic-buffer-for-tests-26FBE7DED9E992BC36C06C988C1AC8A1E672B4B5959EF60672A983EFA7C8EE0F"))) + assert.NoError(t, err) + assert.NotNil(t, key1) + + key2, err := ecdsa.GenerateKey(elliptic.P384(), bytes.NewReader([]byte("00002-deterministic-buffer-for-tests-37ACB0DD3A3CE5A0960CCE0F6A0D7E663DFFD221FBE8EEB03B20D3AD91BCDD55"))) + assert.NoError(t, err) + assert.NotNil(t, key2) + + dk1, err := deriveSharedKeyFromRecipient(&key1.PublicKey, key2) + assert.NoError(t, err) + assert.Equal(t, &[32]byte{ + 0xfa, 0x88, 0x52, 0x30, 0x55, 0xe8, 0xd6, 0x8a, + 0xa8, 0x11, 0xa9, 0xf7, 0x92, 0x79, 0x2a, 0xe6, + 0x10, 0x12, 0xbd, 0x9d, 0xee, 0x98, 0x54, 0x9e, + 0x50, 0x25, 0xb3, 0xaa, 0x79, 0x77, 0xce, 0xd3, + }, dk1) + + dk2, err := deriveSharedKeyFromRecipient(&key2.PublicKey, key1) + assert.NoError(t, err) + assert.Equal(t, dk1, dk2) +} + +func Test_keyIdentifierFromDerivedKey(t *testing.T) { + dk := &[32]byte{ + 0x9f, 0x6c, 0xb8, 0x33, 0xf4, 0x7a, 0x4, 0xb2, + 0xba, 0x65, 0x30, 0xf2, 0xc, 0x7c, 0xb1, 0x30, + 0x22, 0xa0, 0x6a, 0x15, 0x57, 0x73, 0xc1, 0xa9, + 0xc7, 0x21, 0x48, 0xdd, 0x3c, 0xc8, 0x36, 0xc7, + } + + id, err := keyIdentifierFromDerivedKey(dk) + assert.NoError(t, err) + assert.Equal(t, []byte{ + 0xe0, 0xe9, 0xd5, 0xc5, 0x7a, 0x9e, 0x1c, 0x3, + 0x9d, 0x4b, 0xc0, 0x21, 0x6e, 0x72, 0x1a, 0xda, + 0xac, 0xd0, 0x57, 0xb8, 0x21, 0x16, 0x48, 0xac, + 0x46, 0x7c, 0x64, 0xf9, 0x4d, 0xe5, 0x86, 0x23, + }, id) +} + +func Test_packRecipient(t *testing.T) { + payloadKey := &[32]byte{} + + key1, err := ecdsa.GenerateKey(elliptic.P384(), bytes.NewReader([]byte("00001-deterministic-buffer-for-tests-26FBE7DED9E992BC36C06C988C1AC8A1E672B4B5959EF60672A983EFA7C8EE0F"))) + assert.NoError(t, err) + assert.NotNil(t, key1) + + key2, err := ecdsa.GenerateKey(elliptic.P384(), bytes.NewReader([]byte("00002-deterministic-buffer-for-tests-37ACB0DD3A3CE5A0960CCE0F6A0D7E663DFFD221FBE8EEB03B20D3AD91BCDD55"))) + assert.NoError(t, err) + assert.NotNil(t, key2) + + recipient, err := packRecipient(rand.Reader, payloadKey, key1, &key2.PublicKey) + assert.NoError(t, err) + assert.NotNil(t, recipient) + assert.Equal(t, []byte{ + 0xaa, 0xc5, 0x2b, 0x2e, 0xdf, 0x44, 0x9e, 0x87, + 0xc3, 0xc9, 0x9a, 0x98, 0xb1, 0xad, 0x7a, 0xcd, + 0x70, 0xe9, 0xa1, 0x56, 0xf6, 0xd5, 0x87, 0xb8, + 0x25, 0x94, 0x18, 0x3f, 0xf7, 0x8e, 0xdc, 0x46, + }, recipient.Identifier) + assert.Equal(t, seedSize+encryptionKeySize+macSize, len(recipient.Key)) +} + +func Test_tryRecipientKeys(t *testing.T) { + payloadKey := &[32]byte{} + + key1, err := ecdsa.GenerateKey(elliptic.P384(), bytes.NewReader([]byte("00001-deterministic-buffer-for-tests-26FBE7DED9E992BC36C06C988C1AC8A1E672B4B5959EF60672A983EFA7C8EE0F"))) + assert.NoError(t, err) + assert.NotNil(t, key1) + + key2, err := ecdsa.GenerateKey(elliptic.P384(), bytes.NewReader([]byte("00002-deterministic-buffer-for-tests-37ACB0DD3A3CE5A0960CCE0F6A0D7E663DFFD221FBE8EEB03B20D3AD91BCDD55"))) + assert.NoError(t, err) + assert.NotNil(t, key2) + + recipient, err := packRecipient(rand.Reader, payloadKey, key1, &key2.PublicKey) + assert.NoError(t, err) + assert.NotNil(t, recipient) + assert.Equal(t, []byte{ + 0xaa, 0xc5, 0x2b, 0x2e, 0xdf, 0x44, 0x9e, 0x87, + 0xc3, 0xc9, 0x9a, 0x98, 0xb1, 0xad, 0x7a, 0xcd, + 0x70, 0xe9, 0xa1, 0x56, 0xf6, 0xd5, 0x87, 0xb8, + 0x25, 0x94, 0x18, 0x3f, 0xf7, 0x8e, 0xdc, 0x46, + }, recipient.Identifier) + assert.Equal(t, seedSize+encryptionKeySize+macSize, len(recipient.Key)) + + // ------------------------------------------------------------------------- + dk, err := deriveSharedKeyFromRecipient(&key1.PublicKey, key2) + assert.NoError(t, err) + assert.Equal(t, &[32]byte{0xfa, 0x88, 0x52, 0x30, 0x55, 0xe8, 0xd6, 0x8a, 0xa8, 0x11, 0xa9, 0xf7, 0x92, 0x79, 0x2a, 0xe6, 0x10, 0x12, 0xbd, 0x9d, 0xee, 0x98, 0x54, 0x9e, 0x50, 0x25, 0xb3, 0xaa, 0x79, 0x77, 0xce, 0xd3}, dk) + + expectedID := []byte{ + 0xaa, 0xc5, 0x2b, 0x2e, 0xdf, 0x44, 0x9e, 0x87, + 0xc3, 0xc9, 0x9a, 0x98, 0xb1, 0xad, 0x7a, 0xcd, + 0x70, 0xe9, 0xa1, 0x56, 0xf6, 0xd5, 0x87, 0xb8, + 0x25, 0x94, 0x18, 0x3f, 0xf7, 0x8e, 0xdc, 0x46, + } + id, err := keyIdentifierFromDerivedKey(dk) + assert.NoError(t, err) + assert.Equal(t, expectedID, id) + assert.Equal(t, expectedID, recipient.Identifier) + + decodedPayloadKey, err := tryRecipientKeys(dk, []*containerv1.Recipient{ + recipient, + }) + assert.NoError(t, err) + assert.Equal(t, payloadKey, decodedPayloadKey) +} + +func TestPreAuthenticationEncoding(t *testing.T) { + type args struct { + pieces [][]byte + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + { + name: "empty", + args: args{ + pieces: nil, + }, + wantErr: false, + want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + }, + { + name: "one", + args: args{ + pieces: [][]byte{ + []byte("test"), + }, + }, + wantErr: false, + want: []byte{ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Count + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Length + 't', 'e', 's', 't', + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := pae(tt.args.pieces...) + if (err != nil) != tt.wantErr { + t.Errorf("pae() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("pae() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/container/seal/v2/key.go b/pkg/container/seal/v2/key.go new file mode 100644 index 00000000..e9fb508b --- /dev/null +++ b/pkg/container/seal/v2/key.go @@ -0,0 +1,136 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package v2 + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/hmac" + "crypto/rand" + "crypto/sha512" + "encoding/base64" + "fmt" + "strings" + + "github.com/awnumar/memguard" + "golang.org/x/crypto/pbkdf2" + + "github.com/elastic/harp/pkg/container/seal" +) + +const ( + PublicKeyPrefix = "v2.sk." + PrivateKeyPrefix = "v2.ck." +) + +// CenerateKey create an ECDSA P-384 key pair used as container identifier. +func (a *adapter) GenerateKey(fopts ...seal.GenerateOption) (publicKey, privateKey string, err error) { + // Prepare defaults + opts := &seal.GenerateOptions{ + DCKDMasterKey: nil, + DCKDTarget: "", + RandomSource: rand.Reader, + } + + // Apply optional parameters + for _, f := range fopts { + f(opts) + } + + // Master key derivation + if opts.DCKDMasterKey != nil { + // PBKDF2-SHA512(masterKey, HMAC-SHA-512('harp deterministic salt v2', Target), 250000, 64) + // Don't clean bytes, already done by memguard. + masterKey := opts.DCKDMasterKey.Bytes() + if len(masterKey) < 32 { + return "", "", fmt.Errorf("the master key must be 32 bytes long at least") + } + + // Generate deterministic salt + h := hmac.New(sha512.New, []byte("harp deterministic salt v2")) + h.Write([]byte(opts.DCKDTarget)) + salt := h.Sum(nil) + defer memguard.WipeBytes(salt) + + // Derive deterministic container key using PBKDF2-SHA512 + dk := pbkdf2.Key(masterKey[:32], salt, 250000, 64, sha512.New) + defer memguard.WipeBytes(dk) + + // Assign to seed + opts.RandomSource = bytes.NewBuffer(dk) + } + + // Generate ECDSA P-384 container key pair + priv, errGen := ecdsa.GenerateKey(elliptic.P384(), opts.RandomSource) + if errGen != nil { + return "", "", fmt.Errorf("unable to generate container key: %w", errGen) + } + + // Encode keys + encodedPub := append([]byte(PublicKeyPrefix), base64.RawURLEncoding.EncodeToString(elliptic.MarshalCompressed(priv.Curve, priv.PublicKey.X, priv.PublicKey.Y))...) + encodedPriv := append([]byte(PrivateKeyPrefix), base64.RawURLEncoding.EncodeToString(priv.D.Bytes())...) + + // No error + return string(encodedPub), string(encodedPriv), nil +} + +// PublicKeys return the appropriate key format used by the sealing strategy. +func (a *adapter) publicKeys(keys ...string) ([]*ecdsa.PublicKey, error) { + // v2.pk.[data] + res := []*ecdsa.PublicKey{} + + for _, key := range keys { + // Check key prefix + if !strings.HasPrefix(key, PublicKeyPrefix) { + return nil, fmt.Errorf("unsuppored public key '%s' for v2 seal algorithm", key) + } + + // Remove prefix if exists + key = strings.TrimPrefix(key, PublicKeyPrefix) + + // Decode key + keyRaw, err := base64.RawURLEncoding.DecodeString(key) + if err != nil { + return nil, fmt.Errorf("unable to decode public key '%s': %w", key, err) + } + + // Public key sanity checks + if len(keyRaw) != publicKeySize { + return nil, fmt.Errorf("invalid public key length for key '%s'", key) + } + + // Decode the compressed point + x, y := elliptic.UnmarshalCompressed(elliptic.P384(), keyRaw) + if x == nil { + return nil, fmt.Errorf("invalid public key '%s'", key) + } + + // Reassemble the public key + pub := ecdsa.PublicKey{ + Curve: elliptic.P384(), + X: x, + Y: y, + } + + // Append it to sealing keys + res = append(res, &pub) + } + + // No error + return res, nil +} diff --git a/pkg/container/seal/v2/key_test.go b/pkg/container/seal/v2/key_test.go new file mode 100644 index 00000000..0cea67ac --- /dev/null +++ b/pkg/container/seal/v2/key_test.go @@ -0,0 +1,78 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package v2 + +import ( + "bytes" + "testing" + + "github.com/awnumar/memguard" + "github.com/elastic/harp/pkg/container/seal" + "github.com/stretchr/testify/assert" +) + +func TestGenerateKey(t *testing.T) { + adapter := New() + + t.Run("deterministic", func(t *testing.T) { + pub, pk, err := adapter.GenerateKey( + seal.WithDeterministicKey(memguard.NewBufferFromBytes([]byte("deterministic-seed-for-test-00001")), "Release 64"), + ) + assert.NoError(t, err) + assert.NotNil(t, pk) + assert.Equal(t, "v2.ck.QwUEpYFxXpwFGrHQbHXGH0k4w_g9iDw38d67f9YHZwhvmEyE0R3McDMYr260lNck", pk) + assert.NotNil(t, pub) + assert.Equal(t, "v2.sk.AuSjVpMZben6n9fXiaDj8bMjSvhcZ9n7c82VOt7v9_UBzZJaMLamkQUFAVp_9frpAg", pub) + }) + + t.Run("deterministic - same key with different target", func(t *testing.T) { + pub, pk, err := adapter.GenerateKey( + seal.WithDeterministicKey(memguard.NewBufferFromBytes([]byte("deterministic-seed-for-test-00001")), "Release 65"), + ) + assert.NoError(t, err) + assert.NotNil(t, pk) + assert.Equal(t, "v2.ck.2pWmwDtEjYAsLMR-7es_p3IvyYNrc3qSo5KbqrYmbCq5COcquwpr3SDnOmJrrbDp", pk) + assert.NotNil(t, pub) + assert.Equal(t, "v2.sk.AwzwXF1XaZVry-pppsQ1ovSIMLtix-Nhq8NkBDEp46ulrHuY2onMg2_VusdD5D2YXg", pub) + }) + + t.Run("master key too short", func(t *testing.T) { + pub, pk, err := adapter.GenerateKey( + seal.WithDeterministicKey(memguard.NewBufferFromBytes([]byte("too-short-masterkey")), "Release 64"), + ) + assert.Error(t, err) + assert.Empty(t, pk) + assert.Empty(t, pub) + }) + + t.Run("default with given random source", func(t *testing.T) { + pub, pk, err := adapter.GenerateKey(seal.WithRandom(bytes.NewReader([]byte("UlLYMVJzTrAv0KYbl2KqCo9fnsyPLu9YNAO5iUsABeYMmkKe2TnSp8JLD9zThZk")))) + assert.NoError(t, err) + assert.NotNil(t, pk) + assert.Equal(t, "v2.ck.VHJBdjBLWWJsMktxQ285ZoFXc5G4HY_0qSMZAibGlchUmqt915byglIOGeel-5X5", pk) + assert.NotNil(t, pub) + assert.Equal(t, "v2.sk.A0V1xCxGNtVAE9EVhaKi-pIADhd1in8xV_FI5Y0oHSHLAkew9gDAqiALSd6VgvBCbQ", pub) + + }) + + t.Run("default", func(t *testing.T) { + pub, pk, err := adapter.GenerateKey() + assert.NoError(t, err) + assert.NotEmpty(t, pk) + assert.NotEmpty(t, pub) + }) +} diff --git a/pkg/container/seal/v2/seal.go b/pkg/container/seal/v2/seal.go new file mode 100644 index 00000000..28b74a0c --- /dev/null +++ b/pkg/container/seal/v2/seal.go @@ -0,0 +1,116 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package v2 + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "errors" + "fmt" + "io" + + containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" + "github.com/elastic/harp/pkg/sdk/types" +) + +// Seal a secret container +func (a *adapter) Seal(rand io.Reader, container *containerv1.Container, encodedPeersPublicKey ...string) (*containerv1.Container, error) { + // Check parameters + if types.IsNil(container) { + return nil, fmt.Errorf("unable to process nil container") + } + if types.IsNil(container.Headers) { + return nil, fmt.Errorf("unable to process nil container headers") + } + if len(encodedPeersPublicKey) == 0 { + return nil, fmt.Errorf("unable to process empty public keys") + } + + // Convert public keys + peersPublicKey, err := a.publicKeys(encodedPeersPublicKey...) + if err != nil { + return nil, fmt.Errorf("unable to convert peer public keys: %w", err) + } + + // Generate encryption key + payloadKey, err := generatedEncryptionKey(rand) + if err != nil { + return nil, fmt.Errorf("unable to generate encryption key: %w", err) + } + + // Prepare signature identity + sigPriv, encryptedPubSig, err := prepareSignature(rand, payloadKey) + if err != nil { + return nil, fmt.Errorf("unable to prepare signature materials: %w", err) + } + + // Generate ephemeral encryption key + encPriv, err := ecdsa.GenerateKey(encryptionCurve, rand) + if err != nil { + return nil, fmt.Errorf("unable to generate ephemeral encryption keypair") + } + + // Prepare sealed container + containerHeaders := &containerv1.Header{ + ContentType: containerSealedContentType, + EncryptionPublicKey: elliptic.MarshalCompressed(encPriv.Curve, encPriv.PublicKey.X, encPriv.PublicKey.Y), + ContainerBox: encryptedPubSig, + Recipients: []*containerv1.Recipient{}, + SealVersion: SealVersion, + } + + // Process recipients + for _, peerPublicKey := range peersPublicKey { + // Ignore nil key + if peerPublicKey == nil { + continue + } + + // Pack recipient using its public key + r, errPack := packRecipient(rand, payloadKey, encPriv, peerPublicKey) + if errPack != nil { + return nil, fmt.Errorf("unable to pack container recipient (%X): %w", *peerPublicKey, err) + } + + // Append to container + containerHeaders.Recipients = append(containerHeaders.Recipients, r) + } + + // Sanity check + if len(containerHeaders.Recipients) == 0 { + return nil, errors.New("unable to seal a container without recipients") + } + + // Sign given container + content, containerSig, err := signContainer(sigPriv, containerHeaders, container) + if err != nil { + return nil, fmt.Errorf("unable to sign container data: %w", err) + } + + // Encrypt payload + encryptedPayload, err := encrypt(rand, append(containerSig, content...), payloadKey) + if err != nil { + return nil, fmt.Errorf("unable to encrypt container data: %w", err) + } + + // No error + return &containerv1.Container{ + Headers: containerHeaders, + Raw: encryptedPayload, + }, nil +} diff --git a/pkg/container/seal/v2/seal_test.go b/pkg/container/seal/v2/seal_test.go new file mode 100644 index 00000000..329d38ca --- /dev/null +++ b/pkg/container/seal/v2/seal_test.go @@ -0,0 +1,191 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package v2 + +import ( + "crypto/rand" + "testing" + + "github.com/awnumar/memguard" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/assert" + + containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" +) + +var ( + opt = cmp.FilterPath( + func(p cmp.Path) bool { + // Remove ignoring of the fields below once go-cmp is able to ignore generated fields. + // See https://github.com/google/go-cmp/issues/153 + ignoreXXXCache := + p.String() == "XXX_sizecache" || + p.String() == "Headers.XXX_sizecache" + return ignoreXXXCache + }, cmp.Ignore()) + + ignoreOpts = []cmp.Option{ + cmpopts.IgnoreUnexported(containerv1.Container{}), + cmpopts.IgnoreUnexported(containerv1.Header{}), + opt, + } +) + +// ----------------------------------------------------------------------------- + +func TestSeal(t *testing.T) { + type args struct { + container *containerv1.Container + peersPublicKey []string + } + tests := []struct { + name string + args args + want *containerv1.Container + wantErr bool + }{ + { + name: "nil", + wantErr: true, + }, + { + name: "empty container", + args: args{ + container: &containerv1.Container{}, + }, + wantErr: true, + }, + { + name: "empty container headers", + args: args{ + container: &containerv1.Container{ + Headers: &containerv1.Header{}, + }, + }, + wantErr: true, + }, + // --------------------------------------------------------------------- + { + name: "valid", + args: args{ + container: &containerv1.Container{ + Headers: &containerv1.Header{}, + Raw: []byte{0x01, 0x02, 0x03, 0x04}, + }, + peersPublicKey: []string{ + "v2.sk.AuSjVpMZben6n9fXiaDj8bMjSvhcZ9n7c82VOt7v9_UBzZJaMLamkQUFAVp_9frpAg", + "v2.sk.A0V1xCxGNtVAE9EVhaKi-pIADhd1in8xV_FI5Y0oHSHLAkew9gDAqiALSd6VgvBCbQ", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adapter := New() + _, err := adapter.Seal(rand.Reader, tt.args.container, tt.args.peersPublicKey...) + if (err != nil) != tt.wantErr { + t.Errorf("Seal() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +// ----------------------------------------------------------------------------- + +func Test_Seal_Unseal(t *testing.T) { + adapter := New() + + pubKey, privKey, err := adapter.GenerateKey() + assert.NoError(t, err) + + input := &containerv1.Container{ + Headers: &containerv1.Header{ + ContentEncoding: "gzip", + ContentType: "application/vnd.harp.v1.Bundle", + }, + Raw: []byte{0x00, 0x00}, + } + + sealed, err := adapter.Seal(rand.Reader, input, pubKey) + if err != nil { + t.Fatalf("unable to seal container: %v", err) + } + + unsealed, err := adapter.Unseal(sealed, memguard.NewBufferFromBytes([]byte(privKey))) + if err != nil { + t.Fatalf("unable to unseal container: %v", err) + } + + if diff := cmp.Diff(unsealed, input, ignoreOpts...); diff != "" { + t.Errorf("Seal/Unseal()\n-got/+want\ndiff %s", diff) + } +} + +func Test_Seal_Fuzz(t *testing.T) { + adapter := New() + + // Making sure the function never panics + for i := 0; i < 500; i++ { + f := fuzz.New() + + // Prepare arguments + var ( + publicKey string + ) + input := containerv1.Container{ + Headers: &containerv1.Header{}, + Raw: []byte{0x00, 0x00}, + } + + f.Fuzz(&input.Headers) + f.Fuzz(&input.Raw) + f.Fuzz(&publicKey) + + // Execute + adapter.Seal(rand.Reader, &input, publicKey) + } +} + +// ----------------------------------------------------------------------------- +func benchmarkSeal(container *containerv1.Container, peersPublicKeys []string, b *testing.B) { + adapter := New() + for n := 0; n < b.N; n++ { + _, err := adapter.Seal(rand.Reader, container, peersPublicKeys...) + if err != nil { + b.Fatal(err) + } + } +} + +func Benchmark_Seal(b *testing.B) { + publicKey, _, err := New().GenerateKey() + assert.NoError(b, err) + + input := &containerv1.Container{ + Headers: &containerv1.Header{ + ContentEncoding: "gzip", + ContentType: "application/vnd.harp.v1.Bundle", + }, + Raw: make([]byte, 1024), + } + + benchmarkSeal(input, []string{publicKey}, b) +} diff --git a/pkg/container/seal/v2/unseal.go b/pkg/container/seal/v2/unseal.go new file mode 100644 index 00000000..5a09caf7 --- /dev/null +++ b/pkg/container/seal/v2/unseal.go @@ -0,0 +1,148 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package v2 + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha512" + "encoding/base64" + "errors" + "fmt" + "math/big" + "strings" + + "github.com/awnumar/memguard" + "google.golang.org/protobuf/proto" + + containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1" + "github.com/elastic/harp/pkg/sdk/types" +) + +// Unseal a sealed container with the given identity +//nolint:funlen,gocyclo // To refactor +func (a *adapter) Unseal(container *containerv1.Container, identity *memguard.LockedBuffer) (*containerv1.Container, error) { + // Check parameters + if types.IsNil(container) { + return nil, fmt.Errorf("unable to process nil container") + } + if types.IsNil(container.Headers) { + return nil, fmt.Errorf("unable to process nil container headers") + } + if identity == nil { + return nil, fmt.Errorf("unable to process without container key") + } + + // Check headers + if container.Headers.ContentType != containerSealedContentType { + return nil, fmt.Errorf("unable to unseal container") + } + + // Check ephemeral container public encryption key + if len(container.Headers.EncryptionPublicKey) != publicKeySize { + return nil, fmt.Errorf("invalid container public size") + } + + // Decode public key + var publicKey ecdsa.PublicKey + publicKey.Curve = elliptic.P384() + publicKey.X, publicKey.Y = elliptic.UnmarshalCompressed(elliptic.P384(), container.Headers.EncryptionPublicKey) + if publicKey.X == nil { + return nil, errors.New("invalid container signing public key") + } + + // Decode private key + privRaw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(identity.String(), PrivateKeyPrefix)) + if err != nil { + return nil, fmt.Errorf("unable to decode private key: %w", err) + } + if len(privRaw) != privateKeySize { + return nil, fmt.Errorf("invalid identity private key length") + } + var pk ecdsa.PrivateKey + pk.PublicKey.Curve = elliptic.P384() + pk.D = big.NewInt(0).SetBytes(privRaw) + + // Precompute identifier + derivedKey, err := deriveSharedKeyFromRecipient(&publicKey, &pk) + if err != nil { + return nil, fmt.Errorf("unable to execute key agreement: %w", err) + } + + // Try recipients + payloadKey, err := tryRecipientKeys(derivedKey, container.Headers.Recipients) + if err != nil { + return nil, fmt.Errorf("unable to unseal container: error occurred during recipient key tests: %w", err) + } + + // Check private key + if len(payloadKey) != encryptionKeySize { + return nil, fmt.Errorf("unable to unseal container: invalid encryption key size") + } + var encryptionKey [encryptionKeySize]byte + copy(encryptionKey[:], payloadKey[:encryptionKeySize]) + + // Decrypt signing public key + containerSignKeyRaw, err := decrypt(container.Headers.ContainerBox, payloadKey) + if err != nil { + return nil, fmt.Errorf("invalid container key") + } + if len(containerSignKeyRaw) != publicKeySize { + return nil, fmt.Errorf("unable to unseal container: invalid signature key size") + } + + // Compute headers hash + headerHash, err := computeHeaderHash(container.Headers) + if err != nil { + return nil, fmt.Errorf("unable to compute header hash: %w", err) + } + + // Decrypt payload + payloadRaw, err := decrypt(container.Raw, &encryptionKey) + if err != nil || len(payloadRaw) < signatureSize { + return nil, fmt.Errorf("invalid ciphered content") + } + + // Prepare protected content + protectedHash := computeProtectedHash(headerHash, payloadRaw) + + // Extract signature / content + detachedSig := payloadRaw[:signatureSize] + content := payloadRaw[signatureSize:] + + // Compute SHA-384 checksum + digest := sha512.Sum384(protectedHash) + + var ( + r = big.NewInt(0).SetBytes(detachedSig[:48]) + s = big.NewInt(0).SetBytes(detachedSig[48:]) + ) + // Validate signature + if ecdsa.Verify(&publicKey, digest[:], r, s) { + return nil, fmt.Errorf("unable to verify protected content: %w", err) + } + + // Unmarshal inner container + out := &containerv1.Container{} + if err := proto.Unmarshal(content, out); err != nil { + return nil, fmt.Errorf("unable to unpack inner content: %w", err) + } + + // No error + return out, nil +} diff --git a/pkg/sdk/security/crypto/asymmetric.go b/pkg/sdk/security/crypto/asymmetric.go index da3fc525..e8e42d71 100644 --- a/pkg/sdk/security/crypto/asymmetric.go +++ b/pkg/sdk/security/crypto/asymmetric.go @@ -25,7 +25,10 @@ import ( "crypto/rsa" "fmt" + "github.com/pkg/errors" "golang.org/x/crypto/nacl/box" + + "github.com/elastic/harp/build/fips" ) // Keypair generates crypto keys according to given key type. @@ -48,6 +51,7 @@ func Keypair(keyType string) (interface{}, error) { // ----------------------------------------------------------------------------- +//nolint:gocyclo // To refactor func generateKeyPair(keyType string) (publicKey, privateKey interface{}, err error) { switch keyType { case "rsa", "rsa:normal", "rsa:2048": @@ -86,12 +90,18 @@ func generateKeyPair(keyType string) (publicKey, privateKey interface{}, err err pub := key.Public() return pub, key, nil case "ssh", "ed25519": + if fips.Enabled() { + return nil, nil, errors.New("ed25519 key processing is disabled in FIPS Mode") + } pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, nil, fmt.Errorf("unable to generate ed25519 key: %w", err) } return pub, priv, nil case "naclbox", "x25519": + if fips.Enabled() { + return nil, nil, errors.New("x25519 key processing is disabled in FIPS Mode") + } pub, priv, err := box.GenerateKey(rand.Reader) if err != nil { return nil, nil, fmt.Errorf("unable to generate naclbox key: %w", err) diff --git a/pkg/sdk/security/crypto/encoder.go b/pkg/sdk/security/crypto/encoder.go index 7b211202..87e27b2c 100644 --- a/pkg/sdk/security/crypto/encoder.go +++ b/pkg/sdk/security/crypto/encoder.go @@ -29,6 +29,7 @@ import ( "encoding/pem" "fmt" + "github.com/pkg/errors" "go.step.sm/crypto/pemutil" // Import Blake2b @@ -36,6 +37,7 @@ import ( "golang.org/x/crypto/ssh" jose "gopkg.in/square/go-jose.v2" + "github.com/elastic/harp/build/fips" "github.com/elastic/harp/pkg/sdk/security/crypto/bech32" "github.com/elastic/harp/pkg/sdk/types" ) @@ -50,8 +52,16 @@ func ToJWK(key interface{}) (string, error) { // Wrap key keyWrapper := jose.JSONWebKey{Key: key, KeyID: ""} + // Don't process Ed25519 keys + if fips.Enabled() { + switch key.(type) { + case ed25519.PrivateKey, ed25519.PublicKey: + return "", errors.New("ed25519 key processing is disabled in FIPS Mode") + } + } + // Generate thumbprint - thumb, err := keyWrapper.Thumbprint(crypto.BLAKE2b_256) + thumb, err := keyWrapper.Thumbprint(crypto.SHA512_256) if err != nil { return "", err } @@ -78,6 +88,14 @@ func FromJWK(jwk string) (interface{}, error) { return nil, fmt.Errorf("unable to decode JWK: %w", err) } + // Don't process Ed25519 keys + if fips.Enabled() { + switch k.Key.(type) { + case ed25519.PrivateKey, ed25519.PublicKey: + return "", errors.New("ed25519 key processing is disabled in FIPS Mode") + } + } + if k.IsPublic() { // No error return struct { @@ -104,6 +122,14 @@ func ToPEM(key interface{}) (string, error) { return "", fmt.Errorf("unable to encode nil key") } + // Don't process Ed25519 keys + if fips.Enabled() { + switch key.(type) { + case ed25519.PrivateKey, ed25519.PublicKey: + return "", errors.New("ed25519 key processing is disabled in FIPS Mode") + } + } + // Delegate to smallstep library pemBlock, err := pemutil.Serialize(key, pemutil.WithPKCS8(true)) if err != nil { @@ -132,6 +158,9 @@ func KeyToBytes(key interface{}) ([]byte, error) { return nil, err } case ed25519.PrivateKey: + if fips.Enabled() { + return nil, errors.New("ed25519 private key processing is disabled in FIPS Mode") + } out = []byte(k) // Public keys ------------------------------------------------------------ case *rsa.PublicKey: @@ -142,6 +171,9 @@ func KeyToBytes(key interface{}) ([]byte, error) { case *ecdsa.PublicKey: out = elliptic.MarshalCompressed(k.Curve, k.X, k.Y) case ed25519.PublicKey: + if fips.Enabled() { + return nil, errors.New("ed25519 private key processing is disabled in FIPS Mode") + } out = []byte(k) default: return nil, fmt.Errorf("given key type is not supported") @@ -188,6 +220,9 @@ func ToSSH(key interface{}) (string, error) { switch k := key.(type) { // Public keys ------------------------------------------------------------ case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + if _, ok := k.(ed25519.PublicKey); ok && fips.Enabled() { + return "", errors.New("ed25519 public key processing is disabled in FIPS Mode") + } pubKey, err := ssh.NewPublicKey(k) if err != nil { return "", fmt.Errorf("unable to convert key as ssh public key: %w", err) @@ -195,6 +230,9 @@ func ToSSH(key interface{}) (string, error) { result = ssh.MarshalAuthorizedKey(pubKey) // Private keys -------------------------------------------------------- default: + if _, ok := k.(ed25519.PrivateKey); ok && fips.Enabled() { + return "", errors.New("ed25519 private key processing is disabled in FIPS Mode") + } pemBlock, err := pemutil.Serialize(key, pemutil.WithOpenSSH(true)) if err != nil { return "", fmt.Errorf("unable to encode SSH key: %w", err) @@ -272,6 +310,9 @@ func ToJWS(payload, privkey interface{}) (string, error) { alg = jose.ES512 } case ed25519.PrivateKey: + if fips.Enabled() { + return "", errors.New("signature with Ed25519 key is disabled in FIPS Mode") + } alg = jose.EdDSA default: return "", fmt.Errorf("this private key type is not supported '%T'", privkey) diff --git a/pkg/sdk/security/crypto/symmetric.go b/pkg/sdk/security/crypto/symmetric.go index 802ef28c..ee88f223 100644 --- a/pkg/sdk/security/crypto/symmetric.go +++ b/pkg/sdk/security/crypto/symmetric.go @@ -23,6 +23,9 @@ import ( "github.com/awnumar/memguard" "github.com/fernet/fernet-go" + "github.com/pkg/errors" + + "github.com/elastic/harp/build/fips" ) // ----------------------------------------------------------------------------- @@ -40,12 +43,21 @@ func Key(keyType string) (string, error) { key := memguard.NewBufferRandom(32).Bytes() return base64.StdEncoding.EncodeToString(key), nil case "aes:siv": + if fips.Enabled() { + return "", errors.New("aes:siv key generation is disabled in FIPS Mode") + } key := memguard.NewBufferRandom(64).Bytes() return base64.StdEncoding.EncodeToString(key), nil case "secretbox": + if fips.Enabled() { + return "", errors.New("secretbox key generation is disabled in FIPS Mode") + } key := memguard.NewBufferRandom(32).Bytes() return base64.StdEncoding.EncodeToString(key), nil case "chacha20": + if fips.Enabled() { + return "", errors.New("chacha20 key generation is disabled in FIPS Mode") + } key := memguard.NewBufferRandom(32).Bytes() return base64.StdEncoding.EncodeToString(key), nil case "fernet": diff --git a/pkg/sdk/tlsconfig/config_client_ciphers.go b/pkg/sdk/tlsconfig/config_client_ciphers.go index 870b842d..6b3d3d28 100644 --- a/pkg/sdk/tlsconfig/config_client_ciphers.go +++ b/pkg/sdk/tlsconfig/config_client_ciphers.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build !fips + // Package tlsconfig provides primitives to retrieve secure-enough TLS configurations for both clients and servers. // package tlsconfig diff --git a/pkg/sdk/tlsconfig/config_client_ciphers_fips.go b/pkg/sdk/tlsconfig/config_client_ciphers_fips.go new file mode 100644 index 00000000..a1728a2e --- /dev/null +++ b/pkg/sdk/tlsconfig/config_client_ciphers_fips.go @@ -0,0 +1,34 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build fips + +// Package tlsconfig provides primitives to retrieve secure-enough TLS configurations for both clients and servers. +// +package tlsconfig + +import ( + "crypto/tls" +) + +// Client TLS cipher suites (dropping CBC ciphers for client preferred suite set) +var clientCipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, +} diff --git a/pkg/sdk/value/encryption/aead/builders.go b/pkg/sdk/value/encryption/aead/builders.go index ef6168d8..0a677b3c 100644 --- a/pkg/sdk/value/encryption/aead/builders.go +++ b/pkg/sdk/value/encryption/aead/builders.go @@ -27,6 +27,7 @@ import ( miscreant "github.com/miscreant/miscreant.go" "golang.org/x/crypto/chacha20poly1305" + "github.com/elastic/harp/build/fips" "github.com/elastic/harp/pkg/sdk/value" "github.com/elastic/harp/pkg/sdk/value/encryption" ) @@ -41,10 +42,13 @@ var ( func init() { encryption.Register(aesgcmPrefix, AESGCM) - encryption.Register(aespmacsivPrefix, AESPMACSIV) - encryption.Register(aessivPrefix, AESSIV) - encryption.Register(chachaPrefix, Chacha20Poly1305) - encryption.Register(xchachaPrefix, XChacha20Poly1305) + + if !fips.Enabled() { + encryption.Register(aespmacsivPrefix, AESPMACSIV) + encryption.Register(aessivPrefix, AESSIV) + encryption.Register(chachaPrefix, Chacha20Poly1305) + encryption.Register(xchachaPrefix, XChacha20Poly1305) + } } // AESGCM returns an AES-GCM value transformer instance. diff --git a/pkg/sdk/value/encryption/jwe/builders.go b/pkg/sdk/value/encryption/jwe/builders.go index 467e9cfc..90f16bc1 100644 --- a/pkg/sdk/value/encryption/jwe/builders.go +++ b/pkg/sdk/value/encryption/jwe/builders.go @@ -25,6 +25,7 @@ import ( "gopkg.in/square/go-jose.v2" + "github.com/elastic/harp/build/fips" "github.com/elastic/harp/pkg/sdk/value" "github.com/elastic/harp/pkg/sdk/value/encryption" ) @@ -42,7 +43,9 @@ var ( ) func init() { - encryption.Register("jwe", FromKey) + if !fips.Enabled() { + encryption.Register("jwe", FromKey) + } } // FromKey returns an encryption transformer instance according to the given key format. diff --git a/pkg/sdk/value/encryption/paseto/transformer.go b/pkg/sdk/value/encryption/paseto/transformer.go index 24bb7b85..f9d7b20e 100644 --- a/pkg/sdk/value/encryption/paseto/transformer.go +++ b/pkg/sdk/value/encryption/paseto/transformer.go @@ -24,13 +24,16 @@ import ( "fmt" "strings" + "github.com/elastic/harp/build/fips" pasetov4 "github.com/elastic/harp/pkg/sdk/security/crypto/paseto/v4" "github.com/elastic/harp/pkg/sdk/value" "github.com/elastic/harp/pkg/sdk/value/encryption" ) func init() { - encryption.Register("paseto", Transformer) + if !fips.Enabled() { + encryption.Register("paseto", Transformer) + } } func Transformer(key string) (value.Transformer, error) { diff --git a/pkg/sdk/value/encryption/secretbox/transformer.go b/pkg/sdk/value/encryption/secretbox/transformer.go index 0f75a986..d53e68b1 100644 --- a/pkg/sdk/value/encryption/secretbox/transformer.go +++ b/pkg/sdk/value/encryption/secretbox/transformer.go @@ -23,12 +23,15 @@ import ( "fmt" "strings" + "github.com/elastic/harp/build/fips" "github.com/elastic/harp/pkg/sdk/value" "github.com/elastic/harp/pkg/sdk/value/encryption" ) func init() { - encryption.Register("secretbox", Transformer) + if !fips.Enabled() { + encryption.Register("secretbox", Transformer) + } } // Transformer returns a Nacl SecretBox encryption value transformer diff --git a/pkg/tasks/container/identity.go b/pkg/tasks/container/identity.go index 2927e105..3b658416 100644 --- a/pkg/tasks/container/identity.go +++ b/pkg/tasks/container/identity.go @@ -25,17 +25,28 @@ import ( "errors" "fmt" + "github.com/elastic/harp/build/fips" "github.com/elastic/harp/pkg/container/identity" + "github.com/elastic/harp/pkg/container/identity/key" "github.com/elastic/harp/pkg/sdk/types" "github.com/elastic/harp/pkg/sdk/value" "github.com/elastic/harp/pkg/tasks" ) +type IdentityVersion uint + +const ( + LegacyIdentity IdentityVersion = 1 + ModernIdentity IdentityVersion = 2 + NISTIdentity IdentityVersion = 3 +) + // IdentityTask implements secret container identity creation task. type IdentityTask struct { OutputWriter tasks.WriterProvider Description string Transformer value.Transformer + Version IdentityVersion } // Run the task. @@ -51,8 +62,26 @@ func (t *IdentityTask) Run(ctx context.Context) error { return fmt.Errorf("description must not be blank") } + // Select appropriate strategy. + var generator identity.PrivateKeyGeneratorFunc + + if fips.Enabled() { + generator = key.P384 + } else { + switch t.Version { + case LegacyIdentity: + generator = key.Legacy + case ModernIdentity: + generator = key.Ed25519 + case NISTIdentity: + generator = key.P384 + default: + return fmt.Errorf("invalid or unsupported identity version '%d'", t.Version) + } + } + // Create identity - id, payload, err := identity.New(rand.Reader, t.Description) + id, payload, err := identity.New(rand.Reader, t.Description, generator) if err != nil { return fmt.Errorf("unable to create a new identity: %w", err) } diff --git a/pkg/tasks/container/identity_test.go b/pkg/tasks/container/identity_test.go index 2c7a1d39..c04d648a 100644 --- a/pkg/tasks/container/identity_test.go +++ b/pkg/tasks/container/identity_test.go @@ -35,6 +35,7 @@ func TestIdentityTask_Run(t *testing.T) { OutputWriter tasks.WriterProvider Description string Transformer value.Transformer + Version IdentityVersion } type args struct { ctx context.Context @@ -104,13 +105,43 @@ func TestIdentityTask_Run(t *testing.T) { }, wantErr: true, }, + { + name: "version unspecified", + fields: fields{ + OutputWriter: cmdutil.DiscardWriter(), + Description: "test", + Transformer: identity.Transformer(), + }, + wantErr: true, + }, // --------------------------------------------------------------------- { - name: "valid", + name: "valid - v1", + fields: fields{ + OutputWriter: cmdutil.DiscardWriter(), + Description: "test", + Transformer: identity.Transformer(), + Version: LegacyIdentity, + }, + wantErr: false, + }, + { + name: "valid - v2", + fields: fields{ + OutputWriter: cmdutil.DiscardWriter(), + Description: "test", + Transformer: identity.Transformer(), + Version: ModernIdentity, + }, + wantErr: false, + }, + { + name: "valid - v3", fields: fields{ OutputWriter: cmdutil.DiscardWriter(), Description: "test", Transformer: identity.Transformer(), + Version: NISTIdentity, }, wantErr: false, }, @@ -121,6 +152,7 @@ func TestIdentityTask_Run(t *testing.T) { OutputWriter: tt.fields.OutputWriter, Description: tt.fields.Description, Transformer: tt.fields.Transformer, + Version: tt.fields.Version, } if err := tr.Run(tt.args.ctx); (err != nil) != tt.wantErr { t.Errorf("IdentityTask.Run() error = %v, wantErr %v", err, tt.wantErr) diff --git a/pkg/tasks/container/recover.go b/pkg/tasks/container/recover.go index c9c0b393..170b20c4 100644 --- a/pkg/tasks/container/recover.go +++ b/pkg/tasks/container/recover.go @@ -19,7 +19,6 @@ package container import ( "context" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -64,13 +63,13 @@ func (t *RecoverTask) Run(ctx context.Context) error { } // Try to decrypt the private key - key, err := input.Decrypt(ctx, t.Transformer) + privateKey, err := input.Decrypt(ctx, t.Transformer) if err != nil { return fmt.Errorf("unable to decrypt private key: %w", err) } - // Retrieve recoevery key - recoveryPrivateKey, err := identity.RecoveryKey(key) + // Retrieve recovery key + recoveryPrivateKey, err := privateKey.RecoveryKey() if err != nil { return fmt.Errorf("unable to retrieve recovery key from identity: %w", err) } @@ -84,13 +83,13 @@ func (t *RecoverTask) Run(ctx context.Context) error { // Display as json if t.JSONOutput { if errJSON := json.NewEncoder(outputWriter).Encode(map[string]interface{}{ - "container_key": base64.RawURLEncoding.EncodeToString(recoveryPrivateKey[:]), + "container_key": recoveryPrivateKey, }); errJSON != nil { return fmt.Errorf("unable to display as json: %w", errJSON) } } else { // Display container key - if _, err := fmt.Fprintf(outputWriter, "Container key : %s\n", base64.RawURLEncoding.EncodeToString(recoveryPrivateKey[:])); err != nil { + if _, err := fmt.Fprintf(outputWriter, "Container key : %s\n", recoveryPrivateKey); err != nil { return fmt.Errorf("unable to display result: %w", err) } } diff --git a/pkg/tasks/container/recover_test.go b/pkg/tasks/container/recover_test.go index 285f466b..5de25232 100644 --- a/pkg/tasks/container/recover_test.go +++ b/pkg/tasks/container/recover_test.go @@ -64,7 +64,7 @@ func TestRecoverTask_Run(t *testing.T) { { name: "nil outputWriter", fields: fields{ - JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.json"), + JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.v1.json"), OutputWriter: nil, }, wantErr: true, @@ -72,7 +72,7 @@ func TestRecoverTask_Run(t *testing.T) { { name: "nil transformer", fields: fields{ - JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.json"), + JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.v1.json"), OutputWriter: cmdutil.DiscardWriter(), Transformer: nil, }, @@ -99,7 +99,7 @@ func TestRecoverTask_Run(t *testing.T) { { name: "transformer error", fields: fields{ - JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.json"), + JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.v1.json"), OutputWriter: cmdutil.DiscardWriter(), Transformer: mock.Transformer(errors.New("test")), }, @@ -108,7 +108,7 @@ func TestRecoverTask_Run(t *testing.T) { { name: "outputWriter error", fields: fields{ - JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.json"), + JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.v1.json"), OutputWriter: func(ctx context.Context) (io.Writer, error) { return nil, errors.New("test") }, @@ -119,7 +119,7 @@ func TestRecoverTask_Run(t *testing.T) { { name: "outputWriter closed", fields: fields{ - JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.json"), + JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.v1.json"), Transformer: encryption.Must(encryption.FromKey("jwe:pbes2-hs512-a256kw:test")), OutputWriter: func(ctx context.Context) (io.Writer, error) { return cmdutil.NewClosedWriter(), nil @@ -130,7 +130,7 @@ func TestRecoverTask_Run(t *testing.T) { { name: "outputWriter closed - json", fields: fields{ - JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.json"), + JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.v1.json"), Transformer: encryption.Must(encryption.FromKey("jwe:pbes2-hs512-a256kw:test")), OutputWriter: func(ctx context.Context) (io.Writer, error) { return cmdutil.NewClosedWriter(), nil @@ -141,18 +141,37 @@ func TestRecoverTask_Run(t *testing.T) { }, // --------------------------------------------------------------------- { - name: "valid", + name: "valid - v1", fields: fields{ - JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.json"), + JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.v1.json"), OutputWriter: cmdutil.DiscardWriter(), Transformer: encryption.Must(encryption.FromKey("jwe:pbes2-hs512-a256kw:test")), }, wantErr: false, }, { - name: "valid - json output", + name: "valid - v1 - json output", fields: fields{ - JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.json"), + JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.v2.json"), + OutputWriter: cmdutil.DiscardWriter(), + Transformer: encryption.Must(encryption.FromKey("jwe:pbes2-hs512-a256kw:test")), + JSONOutput: true, + }, + wantErr: false, + }, + { + name: "valid - v2", + fields: fields{ + JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.v2.json"), + OutputWriter: cmdutil.DiscardWriter(), + Transformer: encryption.Must(encryption.FromKey("jwe:pbes2-hs512-a256kw:test")), + }, + wantErr: false, + }, + { + name: "valid - v2 - json output", + fields: fields{ + JSONReader: cmdutil.FileReader("../../../test/fixtures/identity/security.v2.json"), OutputWriter: cmdutil.DiscardWriter(), Transformer: encryption.Must(encryption.FromKey("jwe:pbes2-hs512-a256kw:test")), JSONOutput: true, diff --git a/pkg/tasks/container/seal.go b/pkg/tasks/container/seal.go index e0fad1c6..d1af3119 100644 --- a/pkg/tasks/container/seal.go +++ b/pkg/tasks/container/seal.go @@ -19,6 +19,7 @@ package container import ( "context" + "crypto/rand" "encoding/base64" "encoding/json" "errors" @@ -27,6 +28,9 @@ import ( "github.com/awnumar/memguard" "github.com/elastic/harp/pkg/container" + "github.com/elastic/harp/pkg/container/seal" + sealv1 "github.com/elastic/harp/pkg/container/seal/v1" + sealv2 "github.com/elastic/harp/pkg/container/seal/v2" "github.com/elastic/harp/pkg/sdk/types" "github.com/elastic/harp/pkg/tasks" ) @@ -36,15 +40,16 @@ type SealTask struct { ContainerReader tasks.ReaderProvider SealedContainerWriter tasks.WriterProvider OutputWriter tasks.WriterProvider - PeerPublicKeys []*[32]byte - DCKDMasterKey *memguard.LockedBuffer + PeerPublicKeys []string + DCKDMasterKey string DCKDTarget string JSONOutput bool DisableContainerIdentity bool + SealVersion uint } // Run the task. -//nolint:gocyclo // to refactor +//nolint:funlen,gocyclo // to refactor func (t *SealTask) Run(ctx context.Context) error { // Check arguments if types.IsNil(t.ContainerReader) { @@ -72,29 +77,53 @@ func (t *SealTask) Run(ctx context.Context) error { return fmt.Errorf("unable to read input container: %w", err) } + // Initialize seal strategy + var ss seal.Strategy + switch t.SealVersion { + case 1: + ss = sealv1.New() + case 2: + ss = sealv2.New() + default: + ss = sealv1.New() + } + var containerKey string if !t.DisableContainerIdentity { - opts := []container.GenerateOption{} - // Enable deterministic generation - if t.DCKDMasterKey != nil { - opts = append(opts, container.WithDeterministicKey(t.DCKDMasterKey, t.DCKDTarget)) + opts := []seal.GenerateOption{} + + // Check container sealing master key usage + if t.DCKDMasterKey != "" { + // Process target + if t.DCKDTarget == "" { + return errors.New("target flag (string) is mandatory for key derivation") + } + + // Decode master key + masterKeyRaw, errDecode := base64.RawURLEncoding.DecodeString(t.DCKDMasterKey) + if errDecode != nil { + return fmt.Errorf("unable to decode master key: %w", errDecode) + } + + // Enable deterministic container key generation + opts = append(opts, seal.WithDeterministicKey(memguard.NewBufferFromBytes(masterKeyRaw), t.DCKDTarget)) } // Generate container key - containerPublicKey, containerPrivateKey, errContainerGen := container.GenerateKey(opts...) - if errContainerGen != nil { - return fmt.Errorf("unable to generate container key: %w", errContainerGen) + containerPublicKey, containerSecretKey, errGenerate := ss.GenerateKey(opts...) + if errGenerate != nil { + return fmt.Errorf("unable to generate container key: %w", errGenerate) } - // Append to identity + // Append to identities t.PeerPublicKeys = append(t.PeerPublicKeys, containerPublicKey) - // Serialize container key - containerKey = base64.RawURLEncoding.EncodeToString(containerPrivateKey[:]) + // Assign container key + containerKey = containerSecretKey } // Seal the container - sealedContainer, err := container.Seal(in, t.PeerPublicKeys...) + sealedContainer, err := ss.Seal(rand.Reader, in, t.PeerPublicKeys...) if err != nil { return fmt.Errorf("unable to seal container: %w", err) } @@ -110,13 +139,13 @@ func (t *SealTask) Run(ctx context.Context) error { return fmt.Errorf("unable to write sealed container: %w", err) } - // Get output writer - outputWriter, err := t.OutputWriter(ctx) - if err != nil { - return fmt.Errorf("unable to retrieve output writer: %w", err) - } - if !t.DisableContainerIdentity { + // Get output writer + outputWriter, err := t.OutputWriter(ctx) + if err != nil { + return fmt.Errorf("unable to retrieve output writer: %w", err) + } + // Display as json if t.JSONOutput { if err := json.NewEncoder(outputWriter).Encode(map[string]interface{}{ diff --git a/pkg/tasks/container/seal_test.go b/pkg/tasks/container/seal_test.go index a73670ac..e46099c8 100644 --- a/pkg/tasks/container/seal_test.go +++ b/pkg/tasks/container/seal_test.go @@ -23,19 +23,20 @@ import ( "io" "testing" - "github.com/awnumar/memguard" "github.com/elastic/harp/pkg/sdk/cmdutil" "github.com/elastic/harp/pkg/tasks" fuzz "github.com/google/gofuzz" ) -func TestSealTask_Run(t *testing.T) { +func TestSealTask_Run_V1(t *testing.T) { + pub := "v1.sk.qKXPnUP6-2Bb_4nYnmxOXyCdN4IV3AR5HooB33N3g2E" + type fields struct { ContainerReader tasks.ReaderProvider SealedContainerWriter tasks.WriterProvider OutputWriter tasks.WriterProvider - PeerPublicKeys []*[32]byte - DCKDMasterKey *memguard.LockedBuffer + PeerPublicKeys []string + DCKDMasterKey string DCKDTarget string JSONOutput bool DisableContainerIdentity bool @@ -83,17 +84,7 @@ func TestSealTask_Run(t *testing.T) { ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), SealedContainerWriter: cmdutil.DiscardWriter(), OutputWriter: cmdutil.DiscardWriter(), - PeerPublicKeys: []*[32]byte{}, - }, - wantErr: true, - }, - { - name: "no public keys", - fields: fields{ - ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), - SealedContainerWriter: cmdutil.DiscardWriter(), - OutputWriter: cmdutil.DiscardWriter(), - PeerPublicKeys: []*[32]byte{}, + PeerPublicKeys: []string{}, }, wantErr: true, }, @@ -103,12 +94,7 @@ func TestSealTask_Run(t *testing.T) { ContainerReader: cmdutil.FileReader("non-existent.bundle"), SealedContainerWriter: cmdutil.DiscardWriter(), OutputWriter: cmdutil.DiscardWriter(), - PeerPublicKeys: []*[32]byte{ - { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - }, + PeerPublicKeys: []string{pub}, }, wantErr: true, }, @@ -118,27 +104,7 @@ func TestSealTask_Run(t *testing.T) { ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.json"), SealedContainerWriter: cmdutil.DiscardWriter(), OutputWriter: cmdutil.DiscardWriter(), - PeerPublicKeys: []*[32]byte{ - { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - }, - }, - wantErr: true, - }, - { - name: "low-order public keys", - fields: fields{ - ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), - SealedContainerWriter: cmdutil.DiscardWriter(), - OutputWriter: cmdutil.DiscardWriter(), - PeerPublicKeys: []*[32]byte{ - { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - }, + PeerPublicKeys: []string{pub}, }, wantErr: true, }, @@ -149,13 +115,8 @@ func TestSealTask_Run(t *testing.T) { SealedContainerWriter: func(ctx context.Context) (io.Writer, error) { return nil, errors.New("test") }, - OutputWriter: cmdutil.DiscardWriter(), - PeerPublicKeys: []*[32]byte{ - { - 0x97, 0x75, 0x9e, 0x17, 0x35, 0x8a, 0x5b, 0xae, 0x6b, 0x5a, 0xfc, 0xde, 0x97, 0x40, 0x84, 0x7f, - 0xad, 0x59, 0xe6, 0x0a, 0x25, 0x81, 0xbe, 0xcd, 0xc6, 0xa0, 0x37, 0x0e, 0x0b, 0x66, 0x1d, 0x49, - }, - }, + OutputWriter: cmdutil.DiscardWriter(), + PeerPublicKeys: []string{pub}, }, wantErr: true, }, @@ -166,130 +127,154 @@ func TestSealTask_Run(t *testing.T) { SealedContainerWriter: func(ctx context.Context) (io.Writer, error) { return cmdutil.NewClosedWriter(), nil }, - OutputWriter: cmdutil.DiscardWriter(), - PeerPublicKeys: []*[32]byte{ - { - 0x97, 0x75, 0x9e, 0x17, 0x35, 0x8a, 0x5b, 0xae, 0x6b, 0x5a, 0xfc, 0xde, 0x97, 0x40, 0x84, 0x7f, - 0xad, 0x59, 0xe6, 0x0a, 0x25, 0x81, 0xbe, 0xcd, 0xc6, 0xa0, 0x37, 0x0e, 0x0b, 0x66, 0x1d, 0x49, - }, - }, + OutputWriter: cmdutil.DiscardWriter(), + PeerPublicKeys: []string{pub}, }, wantErr: true, }, + // --------------------------------------------------------------------- { - name: "outputWriter error", + name: "valid", fields: fields{ ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), SealedContainerWriter: cmdutil.DiscardWriter(), - OutputWriter: func(ctx context.Context) (io.Writer, error) { - return nil, errors.New("test") - }, - PeerPublicKeys: []*[32]byte{ - { - 0x97, 0x75, 0x9e, 0x17, 0x35, 0x8a, 0x5b, 0xae, 0x6b, 0x5a, 0xfc, 0xde, 0x97, 0x40, 0x84, 0x7f, - 0xad, 0x59, 0xe6, 0x0a, 0x25, 0x81, 0xbe, 0xcd, 0xc6, 0xa0, 0x37, 0x0e, 0x0b, 0x66, 0x1d, 0x49, - }, - }, + OutputWriter: cmdutil.DiscardWriter(), + PeerPublicKeys: []string{pub}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &SealTask{ + ContainerReader: tt.fields.ContainerReader, + SealedContainerWriter: tt.fields.SealedContainerWriter, + OutputWriter: tt.fields.OutputWriter, + PeerPublicKeys: tt.fields.PeerPublicKeys, + DCKDMasterKey: tt.fields.DCKDMasterKey, + DCKDTarget: tt.fields.DCKDTarget, + JSONOutput: tt.fields.JSONOutput, + DisableContainerIdentity: tt.fields.DisableContainerIdentity, + } + if err := tr.Run(tt.args.ctx); (err != nil) != tt.wantErr { + t.Errorf("SealTask.Run() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSealTask_Run_V2(t *testing.T) { + pk := "v2.sk.A0V1xCxGNtVAE9EVhaKi-pIADhd1in8xV_FI5Y0oHSHLAkew9gDAqiALSd6VgvBCbQ" + + type fields struct { + ContainerReader tasks.ReaderProvider + SealedContainerWriter tasks.WriterProvider + OutputWriter tasks.WriterProvider + PeerPublicKeys []string + DCKDMasterKey string + DCKDTarget string + JSONOutput bool + DisableContainerIdentity bool + } + type args struct { + ctx context.Context + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "nil", + wantErr: true, + }, + { + name: "nil containerReader", + fields: fields{ + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), }, wantErr: true, }, { - name: "outputWriter closed", + name: "nil sealedContainerWriter", fields: fields{ ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), - SealedContainerWriter: cmdutil.DiscardWriter(), - OutputWriter: func(ctx context.Context) (io.Writer, error) { - return cmdutil.NewClosedWriter(), nil - }, - PeerPublicKeys: []*[32]byte{ - { - 0x97, 0x75, 0x9e, 0x17, 0x35, 0x8a, 0x5b, 0xae, 0x6b, 0x5a, 0xfc, 0xde, 0x97, 0x40, 0x84, 0x7f, - 0xad, 0x59, 0xe6, 0x0a, 0x25, 0x81, 0xbe, 0xcd, 0xc6, 0xa0, 0x37, 0x0e, 0x0b, 0x66, 0x1d, 0x49, - }, - }, + SealedContainerWriter: nil, }, wantErr: true, }, { - name: "outputWriter closed - json", + name: "nil outputWriter", fields: fields{ ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), SealedContainerWriter: cmdutil.DiscardWriter(), - OutputWriter: func(ctx context.Context) (io.Writer, error) { - return cmdutil.NewClosedWriter(), nil - }, - PeerPublicKeys: []*[32]byte{ - { - 0x97, 0x75, 0x9e, 0x17, 0x35, 0x8a, 0x5b, 0xae, 0x6b, 0x5a, 0xfc, 0xde, 0x97, 0x40, 0x84, 0x7f, - 0xad, 0x59, 0xe6, 0x0a, 0x25, 0x81, 0xbe, 0xcd, 0xc6, 0xa0, 0x37, 0x0e, 0x0b, 0x66, 0x1d, 0x49, - }, - }, - JSONOutput: true, + OutputWriter: nil, }, wantErr: true, }, - // --------------------------------------------------------------------- { - name: "valid", + name: "no public keys", fields: fields{ ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), SealedContainerWriter: cmdutil.DiscardWriter(), OutputWriter: cmdutil.DiscardWriter(), - PeerPublicKeys: []*[32]byte{ - { - 0x97, 0x75, 0x9e, 0x17, 0x35, 0x8a, 0x5b, 0xae, 0x6b, 0x5a, 0xfc, 0xde, 0x97, 0x40, 0x84, 0x7f, - 0xad, 0x59, 0xe6, 0x0a, 0x25, 0x81, 0xbe, 0xcd, 0xc6, 0xa0, 0x37, 0x0e, 0x0b, 0x66, 0x1d, 0x49, - }, - }, + PeerPublicKeys: []string{}, }, - wantErr: false, + wantErr: true, }, { - name: "valid - no container identity", + name: "containerReader error", fields: fields{ - ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), + ContainerReader: cmdutil.FileReader("non-existent.bundle"), SealedContainerWriter: cmdutil.DiscardWriter(), OutputWriter: cmdutil.DiscardWriter(), - PeerPublicKeys: []*[32]byte{ - { - 0x97, 0x75, 0x9e, 0x17, 0x35, 0x8a, 0x5b, 0xae, 0x6b, 0x5a, 0xfc, 0xde, 0x97, 0x40, 0x84, 0x7f, - 0xad, 0x59, 0xe6, 0x0a, 0x25, 0x81, 0xbe, 0xcd, 0xc6, 0xa0, 0x37, 0x0e, 0x0b, 0x66, 0x1d, 0x49, - }, - }, - DisableContainerIdentity: true, + PeerPublicKeys: []string{pk}, }, - wantErr: false, + wantErr: true, }, { - name: "valid - json output", + name: "containerReader not a bundle", fields: fields{ - ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.json"), SealedContainerWriter: cmdutil.DiscardWriter(), OutputWriter: cmdutil.DiscardWriter(), - PeerPublicKeys: []*[32]byte{ - { - 0x97, 0x75, 0x9e, 0x17, 0x35, 0x8a, 0x5b, 0xae, 0x6b, 0x5a, 0xfc, 0xde, 0x97, 0x40, 0x84, 0x7f, - 0xad, 0x59, 0xe6, 0x0a, 0x25, 0x81, 0xbe, 0xcd, 0xc6, 0xa0, 0x37, 0x0e, 0x0b, 0x66, 0x1d, 0x49, - }, + PeerPublicKeys: []string{pk}, + }, + wantErr: true, + }, + { + name: "sealedContainerWriter error", + fields: fields{ + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), + SealedContainerWriter: func(ctx context.Context) (io.Writer, error) { + return nil, errors.New("test") }, - JSONOutput: true, + OutputWriter: cmdutil.DiscardWriter(), + PeerPublicKeys: []string{pk}, }, - wantErr: false, + wantErr: true, + }, + { + name: "sealedContainerWriter closed", + fields: fields{ + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), + SealedContainerWriter: func(ctx context.Context) (io.Writer, error) { + return cmdutil.NewClosedWriter(), nil + }, + OutputWriter: cmdutil.DiscardWriter(), + PeerPublicKeys: []string{pk}, + }, + wantErr: true, }, + // --------------------------------------------------------------------- { - name: "valid - dckd", + name: "valid", fields: fields{ ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), SealedContainerWriter: cmdutil.DiscardWriter(), OutputWriter: cmdutil.DiscardWriter(), - DCKDMasterKey: memguard.NewBuffer(32), - DCKDTarget: "test", - PeerPublicKeys: []*[32]byte{ - { - 0x97, 0x75, 0x9e, 0x17, 0x35, 0x8a, 0x5b, 0xae, 0x6b, 0x5a, 0xfc, 0xde, 0x97, 0x40, 0x84, 0x7f, - 0xad, 0x59, 0xe6, 0x0a, 0x25, 0x81, 0xbe, 0xcd, 0xc6, 0xa0, 0x37, 0x0e, 0x0b, 0x66, 0x1d, 0x49, - }, - }, + PeerPublicKeys: []string{pk}, }, wantErr: false, }, @@ -305,6 +290,7 @@ func TestSealTask_Run(t *testing.T) { DCKDTarget: tt.fields.DCKDTarget, JSONOutput: tt.fields.JSONOutput, DisableContainerIdentity: tt.fields.DisableContainerIdentity, + SealVersion: 2, } if err := tr.Run(tt.args.ctx); (err != nil) != tt.wantErr { t.Errorf("SealTask.Run() error = %v, wantErr %v", err, tt.wantErr) @@ -318,7 +304,7 @@ func TestSealTask_Fuzz(t *testing.T) { ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.bundle"), SealedContainerWriter: cmdutil.DiscardWriter(), OutputWriter: cmdutil.DiscardWriter(), - PeerPublicKeys: []*[32]byte{}, + PeerPublicKeys: []string{}, DisableContainerIdentity: true, } diff --git a/pkg/tasks/container/unseal.go b/pkg/tasks/container/unseal.go index e1731f7a..393a6662 100644 --- a/pkg/tasks/container/unseal.go +++ b/pkg/tasks/container/unseal.go @@ -19,7 +19,6 @@ package container import ( "context" - "encoding/base64" "errors" "fmt" @@ -62,15 +61,8 @@ func (t *UnsealTask) Run(ctx context.Context) error { return fmt.Errorf("unable to read input container: %w", err) } - // Decode container key - privateKeyRaw, err := base64.RawURLEncoding.DecodeString(t.ContainerKey.String()) - if err != nil { - return fmt.Errorf("unable to decode container key: %w", err) - } - defer memguard.WipeBytes(privateKeyRaw) - // Unseal the bundle - out, err := container.Unseal(in, memguard.NewBufferFromBytes(privateKeyRaw)) + out, err := container.Unseal(in, t.ContainerKey) if err != nil { return fmt.Errorf("unable to unseal bundle content: %w", err) } diff --git a/pkg/tasks/container/unseal_test.go b/pkg/tasks/container/unseal_test.go index f613d222..aef53068 100644 --- a/pkg/tasks/container/unseal_test.go +++ b/pkg/tasks/container/unseal_test.go @@ -101,41 +101,77 @@ func TestUnsealTask_Run(t *testing.T) { { name: "outputWriter error", fields: fields{ - ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.sealed"), + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.v1.sealed"), OutputWriter: func(ctx context.Context) (io.Writer, error) { return nil, errors.New("test") }, - ContainerKey: memguard.NewBufferFromBytes([]byte("MiVGh4KOmdzZbej17BZGChkCPZ9uK9uBWdPNU0GlBNg")), + ContainerKey: memguard.NewBufferFromBytes([]byte("v1.ck.MiVGh4KOmdzZbej17BZGChkCPZ9uK9uBWdPNU0GlBNg")), }, wantErr: true, }, { name: "outputWriter closed", fields: fields{ - ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.sealed"), + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.v1.sealed"), OutputWriter: func(ctx context.Context) (io.Writer, error) { return cmdutil.NewClosedWriter(), nil }, - ContainerKey: memguard.NewBufferFromBytes([]byte("MiVGh4KOmdzZbej17BZGChkCPZ9uK9uBWdPNU0GlBNg")), + ContainerKey: memguard.NewBufferFromBytes([]byte("v1.ck.MiVGh4KOmdzZbej17BZGChkCPZ9uK9uBWdPNU0GlBNg")), + }, + wantErr: true, + }, + { + name: "v2 without prefix", + fields: fields{ + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.v2.sealed"), + OutputWriter: cmdutil.DiscardWriter(), + ContainerKey: memguard.NewBufferFromBytes([]byte("v2.ck.dAYx4CeTMRGKfpFHA7Q926qMz8imo1VJIToMw9uvH7HfPJTRpLUSMUS07JAdV-1R")), }, wantErr: true, }, // --------------------------------------------------------------------- { - name: "valid", + name: "valid - v1", + fields: fields{ + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.v1.sealed"), + OutputWriter: cmdutil.DiscardWriter(), + ContainerKey: memguard.NewBufferFromBytes([]byte("v1.ck.MiVGh4KOmdzZbej17BZGChkCPZ9uK9uBWdPNU0GlBNg")), + }, + wantErr: false, + }, + { + name: "valid - v1 - with identity recovery key", + fields: fields{ + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.v1.sealed"), + OutputWriter: cmdutil.DiscardWriter(), + ContainerKey: memguard.NewBufferFromBytes([]byte("v1.ck.IO6bCjACnqsCP0ahT--CVBhryzhe-ZFroVzn5Dx3D0U")), + }, + wantErr: false, + }, + { + name: "valid - v1 - with identity recovery key with prefix", + fields: fields{ + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.v1.sealed"), + OutputWriter: cmdutil.DiscardWriter(), + ContainerKey: memguard.NewBufferFromBytes([]byte("v1.ck.IO6bCjACnqsCP0ahT--CVBhryzhe-ZFroVzn5Dx3D0U")), + }, + wantErr: false, + }, + { + name: "valid - v2", fields: fields{ - ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.sealed"), + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.v2.sealed"), OutputWriter: cmdutil.DiscardWriter(), - ContainerKey: memguard.NewBufferFromBytes([]byte("MiVGh4KOmdzZbej17BZGChkCPZ9uK9uBWdPNU0GlBNg")), + ContainerKey: memguard.NewBufferFromBytes([]byte("v2.ck.CLMEUoY-EgvMGKCcKeByPdJjQDod6fqTnqvxtD_Z0_SX4PMITu_emttDL91z_61D")), }, wantErr: false, }, { - name: "valid - with identity recovery key", + name: "valid - v2 - with identity recovery key", fields: fields{ - ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.sealed"), + ContainerReader: cmdutil.FileReader("../../../test/fixtures/bundles/complete.v2.sealed"), OutputWriter: cmdutil.DiscardWriter(), - ContainerKey: memguard.NewBufferFromBytes([]byte("IO6bCjACnqsCP0ahT--CVBhryzhe-ZFroVzn5Dx3D0U")), + ContainerKey: memguard.NewBufferFromBytes([]byte("v2.ck.8DwD8D-TUB9w-NzXBXySz4PkAIrWUc09TOJKdJ495MJ-AJ2lvDlj1Pnw1rSUAwVg")), }, wantErr: false, }, diff --git a/samples/customer-bundle/policy.rego b/samples/customer-bundle/policy.rego new file mode 100644 index 00000000..a46baf9f --- /dev/null +++ b/samples/customer-bundle/policy.rego @@ -0,0 +1,36 @@ +package main + +# ------------------------------------------------------------------------------ + +is_template { + input.kind == "BundleTemplate" +} + +# ------------------------------------------------------------------------------ + +deny[msg] { + not is_template + msg = "HRP-BT-SCHEMA-00001 - The given BundleTemplate source doesn't declare a 'BundleTemplate' resource." +} + +deny[msg] { + is_template + not input.meta + msg = "HRP-BT-SCHEMA-00002 - Missing 'meta' section." +} + +# ------------------------------------------------------------------------------ + +violation[msg] { + not input.meta + msg = "HRP-BT-CORE-00001 - The 'meta.name' must be defined." +} + +violation[msg] { + secrets := input.spec.namespaces.infrastructure[_].regions[_].services[_].secrets[_] + not secrets.description + msg = "HRP-BT-INFRA-00001 - Infrastructure secrets must have a description." +} + +# ------------------------------------------------------------------------------ + diff --git a/shell.nix b/shell.nix index 41703792..69a42e19 100644 --- a/shell.nix +++ b/shell.nix @@ -18,6 +18,7 @@ mkShell { protobuf golangci-lint upx + go-task ]; shellHook = '' diff --git a/test/fixtures/bundles/complete.sealed b/test/fixtures/bundles/complete.v1.sealed similarity index 100% rename from test/fixtures/bundles/complete.sealed rename to test/fixtures/bundles/complete.v1.sealed diff --git a/test/fixtures/bundles/complete.v2.sealed b/test/fixtures/bundles/complete.v2.sealed new file mode 100644 index 00000000..d018ad91 Binary files /dev/null and b/test/fixtures/bundles/complete.v2.sealed differ diff --git a/test/fixtures/identity/security.json b/test/fixtures/identity/security.json deleted file mode 100644 index 02ddc2cf..00000000 --- a/test/fixtures/identity/security.json +++ /dev/null @@ -1 +0,0 @@ -{"@apiVersion":"harp.elastic.co/v1","@kind":"ContainerIdentity","@timestamp":"2021-11-17T12:56:46.136924Z","@description":"security","public":"security1fdgkzxp808vhvv6v58vpgqma85ass87jf855f0ttpujqstx7aadsxkjzr9","private":{"encoding":"jwe","content":"ZXlKaGJHY2lPaUpRUWtWVE1pMUlVelV4TWl0Qk1qVTJTMWNpTENKbGJtTWlPaUpCTWpVMlIwTk5JaXdpY0RKaklqbzFNREF3TURFc0luQXljeUk2SWpSbWRqVXphRlp1ZDFWdVdsQk5WVE4zU1RsUFlYY2lmUS5qWWYxUzZhUEZGcUYtWGFJd2NzNFpjSnoyblZULS1jZThkTkloS0ljX0FiTi13Qk1hcUdYUUEub3QtVUlpSG9mUkhzN3pVOS5WMElUTktTYk00enNGZEdSelZKSGVSRmRxODd1Y2x2cU1lWVFEa25sVlRjd3JSS2lBLXZTdWNQOGE0X3hvZUFoSjJSQzRBZWE2SkxMN3dLNTR1dDFWN2o5UHQ0dV9pdkNTNWlISHVPX0FPeTF4VVJvRXk4OXNXeVhCOERMVFVwVklHMjktQmpVQ1BoNFZiVGVsZGZYX2xJUkMyTk41YS1xSkJZNjRsdEpaa2RPWXJyRWdKNjQxMXR3RWVpODlBbWt1UUE4RjcwVEJpd2dGMUJRVmZ5TnVWYTZIWjhWazN1ekdrSm1hZy50aU9IVExkUXZ1cG4xeGRTTVgxZ21B"}} diff --git a/test/fixtures/identity/security.v1.json b/test/fixtures/identity/security.v1.json new file mode 100644 index 00000000..3cd16f6f --- /dev/null +++ b/test/fixtures/identity/security.v1.json @@ -0,0 +1,11 @@ +{ + "@apiVersion": "harp.elastic.co/v1", + "@kind": "ContainerIdentity", + "@timestamp": "2021-12-01T22:15:11.144249Z", + "@description": "security", + "public": "v1.ipk.7u8B1VFrHyMeWyt8Jzj1Nj2BgVB7z-umD8R-OOnJahE", + "private": { + "content": "ZXlKaGJHY2lPaUpRUWtWVE1pMUlVelV4TWl0Qk1qVTJTMWNpTENKbGJtTWlPaUpCTWpVMlIwTk5JaXdpY0RKaklqbzFNREF3TURFc0luQXljeUk2SW1obU1UVnpUVmRwTmtaMmNUSlVZM0p5TVhReVZtY2lmUS5ZOGtfVXR2dWtmcVRxTE5fQ2l5ajdTejU2dThOYV9uMG1FTG5jMHFCQ1d0MkVqX2VHRk80RmcuN0p2ekhGYkZrRXdXWGxOeC5ycVJLSno1ZWFGajRqSl9wOVAzUVBuUUs3dXhkWUhBOUNIZFUxTEswWkQ3Q2dickJzUDFRRFRTRU1lX3lqbTZVQ1dpNzFUVmxfX3JISVdSR3VDVVpWSE1KMXNtRnR5c2UzdHBURkdnZFRCaVQxTmw4dWlNZ2JiUEN1cHJ4Uy0wUjRGU2dobXFLU0s3TGhRcUxFWFVaNFF0SVliMDd6Y19vMnRZNlVnU3NMaFBlSUFPM1M2WlBwQXFYU3lfSjR3NzEzdFhEU1ZTX2ZuOFJ5MlF2NTJmOHg0cXBiN0Q3NGlTTndOb052Zy5rcHVzTTVoZ21RT3JhS1luNGxTVjZ3" + }, + "signature": "Kq1OJlAOexIvt9TXETYeYGotqqCz8PiqFEYuSbHmJPVBqtYpI2Q_zNE0fO5hs-JdTqG3p6oLiITHK9cYyx2hBw" +} diff --git a/test/fixtures/identity/security.v2.json b/test/fixtures/identity/security.v2.json new file mode 100644 index 00000000..e09a9559 --- /dev/null +++ b/test/fixtures/identity/security.v2.json @@ -0,0 +1,11 @@ +{ + "@apiVersion": "harp.elastic.co/v1", + "@kind": "ContainerIdentity", + "@timestamp": "2021-12-01T22:15:07.586373Z", + "@description": "security", + "public": "v2.ipk.AkLr_HHMO5Loy2bK42mvCADrJ7s2PSYCRTnqDWJV8PCK2EXmu-GTV8HmNJwmA8IJ8Q", + "private": { + "content": "ZXlKaGJHY2lPaUpRUWtWVE1pMUlVelV4TWl0Qk1qVTJTMWNpTENKbGJtTWlPaUpCTWpVMlIwTk5JaXdpY0RKaklqbzFNREF3TURFc0luQXljeUk2SWpCMFozUk5OMm80TmxacFNrWXpjemhYWDBsb05VRWlmUS5QXzVVMTdSR3JSRVg4UHoyNGpRQkQzdGROWGU3ci1UMVh2bnBnT21aMkwweGZXQXNfT2dWcVEuYUpuekZCQTNBWllXSld2TC53SVAtZlRERjN5R0NaRGtldThOM3A1NUZPRF9ZX3QzSV9ubHN2MDVqcWNLdlJLczFfWjVfM2Zhc2Z0cU0tMlRoN25VdDZIaXZWLVB1ckVIQ2hBRENHaF9SZTBySVVwZkV4OHBCcUk4V3BIYTdSYktUTUN3RmNpSDMzeTQxZ1duT1lpN1R1TmJBamhNMjZMdDZZMFN0ekcyRi1FUm9jSWotWklwMDJwcGZjdUpKOU91S1BDOThKTl9ZV3EzcVA2TW55Ym1WTnFFZ1hwdWFVZm9GcTN3ZWlSX2paVkNsRzU5cTBGdWplVHN0UnRzU2xuZFlndTVBTl9LanFWRmluNDBXNGcxZWRMdWZDM1U0UGZhZVMzUlQxSS0wRkVnN0ZGMnE0QVdINy01aF9IQWg4WFR4eXBCTjR3THE1TTd6ZExRLlkzTTlBeDc1bGNYbmNNaGNxV3dOMXc" + }, + "signature": "dpbnMGAPvFbHSjEXs1GMyO8Kmw9cZqTOKI5wAA1ApcO1RXtFGS_GyC1zAtuFDhhVmTWdFzS4HdVg0LEhxBivbqsr_cft_9CR-7uVUPpkb2Hz2d4BkL3yzDo9bkLfllaM" +}