Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore(examples): Add examples for Go with CI #733

Merged
merged 13 commits into from
Jan 9, 2025
9 changes: 8 additions & 1 deletion .github/workflows/library_go_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,11 @@ jobs:
working-directory: ${{ matrix.library }}
shell: bash
run: |
make test_go
make test_go

- name: Test Examples for Go
if: matrix.library == 'AwsEncryptionSDK'
working-directory: ${{ matrix.library }}/runtimes/go/examples
shell: bash
run: |
go run main.go
15 changes: 15 additions & 0 deletions AwsEncryptionSDK/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,18 @@ TYPES_FILE_WITHOUT_EXTERN_STRING="module AwsCryptographyEncryptionSdkTypes"
INDEX_FILE_PATH=dafny/AwsEncryptionSdk/src/Index.dfy
INDEX_FILE_WITH_EXTERN_STRING="module {:extern \"software.amazon.cryptography.encryptionsdk.internaldafny\" } ESDK refines AbstractAwsCryptographyEncryptionSdkService {"
INDEX_FILE_WITHOUT_EXTERN_STRING="module ESDK refines AbstractAwsCryptographyEncryptionSdkService {"

# Target to restore all directories in a list
# TODO: remove this once we don't copy all of the directories into implementation and test. This is done by make file target _mv_polymorph_go in smithy-dafny.
RESTORE_DIRS := examples
_polymorph_go: restore_directories
restore_directories:
@for dir in $(RESTORE_DIRS); do \
if [ -d "runtimes/go/ImplementationFromDafny-go/$$dir" ]; then \
cp -Rf runtimes/go/ImplementationFromDafny-go/$$dir runtimes/go/; \
rm -rf runtimes/go/ImplementationFromDafny-go/$$dir; \
rm -rf runtimes/go/TestsFromDafny-go/$$dir; \
else \
echo "Directory $$dir not found"; \
fi \
done
90 changes: 90 additions & 0 deletions AwsEncryptionSDK/runtimes/go/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# AWS Encryption SDK for Go Examples

This section features examples that show you
how to use the AWS Encryption SDK.
We demonstrate how to use the encryption and decryption APIs
and how to set up some common configuration patterns.

## APIs

The AWS Encryption SDK provides two high-level APIs:
one-step APIs that process the entire operation in memory
and streaming APIs.

You can find examples that demonstrate these APIs
in the [`examples/`](./) directory.

* [How to encrypt and decrypt](./keyring/awskmskeyring/awskmskeyring.go)
* [How to change the algorithm suite](./misc/setencryptionalgorithmsuite.go)
* [How to set the commitment policy](./misc/commitmentpolicy.go)
* [How to limit the number of encrypted data keys (EDKs)](./misc/limitencrypteddatakeysexample.go)

## Configuration

To use the encryption and decryption APIs,
you need to describe how you want the library to protect your data keys.
You can do this by configuring
[keyrings](#keyrings) or [cryptographic materials managers](#cryptographic-materials-managers).
These examples will show you how to use the configuration tools that we include for you
and how to create some of your own.
We start with AWS KMS examples, then show how to use other wrapping keys.

* Using AWS Key Management Service (AWS KMS)
* [How to use one AWS KMS key](./keyring/awskmskeyring/awskmskeyring.go)
* [How to use multiple AWS KMS keys in different regions](./keyring/awskmsmrkmultikeyring/awskmsmrkmultikeyring.go)
* [How to decrypt when you don't know the AWS KMS key](./keyring/awskmsdiscoverykeyring/awskmsdiscoverykeyring.go)
* [How to limit decryption to a single region](./keyring/awskmsmrkdiscoverykeyring/awskmsmrkdiscoverykeyring.go)
* [How to decrypt with a preferred region but failover to others](./keyring/awskmsmrkdiscoverykeyring/awskmsmrkdiscoverykeyring.go)
* [How to reproduce the behavior of an AWS KMS master key provider](./keyring/awskmsmultikeyring/awskmsmultikeyring.go)
* Using raw wrapping keys
* [How to use a raw AES wrapping key](./keyring/rawaeskeyring/rawaeskeyring.go)
* [How to use a raw RSA wrapping key](./keyring/rawrsakeyring/rawrasakeyring.go)
* Combining wrapping keys
* [How to combine AWS KMS with an offline escrow key](./keyring/multikeyring/multikeyring.go)
* How to restrict algorithm suites
* [with a custom cryptographic materials manager](./cryptographicmaterialsmanager/restrictalgorithmsuite/signingsuiteonlycmm.go)

### Keyrings

Keyrings are the most common way for you to configure the AWS Encryption SDK.
They determine how the AWS Encryption SDK protects your data.
You can find these examples in [`examples/keyring`](./keyring).

### Cryptographic Materials Managers

Keyrings define how your data keys are protected,
but there is more going on here than just protecting data keys.

Cryptographic materials managers give you higher-level controls
over how the AWS Encryption SDK protects your data.
This can include things like
enforcing the use of certain algorithm suites or encryption context settings,
reusing data keys across messages,
or changing how you interact with keyrings.
You can find these examples in
[`examples/cryptographic_materials_manager`](./cryptographicmaterialsmanager).

### Client Supplier

The AWS Encryption SDK creates AWS KMS clients when interacting with AWS KMS.
In case the default AWS KMS client configuration doesn't suit your needs,
you can configure clients by defining a custom Client Supplier.
For example, your Client Supplier could tune
the retry and timeout settings on the client, or use different credentials
based on which region is being called. In our
[regional_role_client_supplier](./clientsupplier/regionalroleclientsupplier.go)
example, we show how you can build a custom Client Supplier which
creates clients by assuming different IAM roles for different regions.

# Writing Examples

If you want to contribute a new example, that's awesome!
To make sure that your example runs in our CI,
please make sure that it meets the following requirements:

1. The example MUST be a distinct subdirectory or file in the [`examples/`](./) directory.
1. The example MAY be nested arbitrarily deeply.
1. Each example file MUST contain exactly one example.
1. Each example filename MUST be descriptive.
1. Each example file MUST contain validation checks to check for expected returned values and MUST panic is the returned value is no expected.
1. Each example MUST also be called inside the `main` function of [main.go](./main.go).
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

/*
This example sets up an MRK multi-keyring and an MRK discovery
multi-keyring using a custom client supplier.
A custom client supplier grants users access to more granular
configuration aspects of their authentication details and KMS
client. In this example, we create a simple custom client supplier
that authenticates with a different IAM role based on the
region of the KMS key.

This example creates a MRK multi-keyring configured with a custom
client supplier using a single MRK and encrypts the example_data with it.
Then, it creates a MRK discovery multi-keyring to decrypt the ciphertext.
*/

package clientsupplier

import (
"context"
"fmt"

mpl "github.com/aws/aws-cryptographic-material-providers-library/mpl/awscryptographymaterialproviderssmithygenerated"
mpltypes "github.com/aws/aws-cryptographic-material-providers-library/mpl/awscryptographymaterialproviderssmithygeneratedtypes"
client "github.com/aws/aws-encryption-sdk/awscryptographyencryptionsdksmithygenerated"
esdktypes "github.com/aws/aws-encryption-sdk/awscryptographyencryptionsdksmithygeneratedtypes"
)

func ClientSupplierExample(exampleText, mrkKeyIdEncrypt, awsAccountId string, awsRegions []string) {
// Step 1: Instantiate the encryption SDK client.
// This builds the default client with the RequireEncryptRequireDecrypt commitment policy,
// which enforces that this client only encrypts using committing algorithm suites and enforces
// that this client will only decrypt encrypted messages that were created with a committing
// algorithm suite.
encryptionClient, err := client.NewClient(esdktypes.AwsEncryptionSdkConfig{})
if err != nil {
panic(err)
}
// Step 2: Create your encryption context.
// Remember that your encryption context is NOT SECRET.
// For more information, see
// https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context
encryptionContext := map[string]string{
"encryption": "context",
"is not": "secret",
"but adds": "useful metadata",
"that can help you": "be confident that",
"the data you are handling": "is what you think it is",
}
// Step 3: Initialize the mpl client
matProv, err := mpl.NewClient(mpltypes.MaterialProvidersConfig{})
if err != nil {
panic(err)
}
// Step 4: Create keyrings
// First Keyring: Create the multi-keyring using our custom client supplier
// defined in the RegionalRoleClientSupplier class in this directory.
// Note: RegionalRoleClientSupplier will internally use the key_arn's region
// to retrieve the correct IAM role.
awsKmsMrkKeyringMultiInput := mpltypes.CreateAwsKmsMrkMultiKeyringInput{
ClientSupplier: &RegionalRoleClientSupplier{},
Generator: &mrkKeyIdEncrypt,
}
awsKmsMrkMultiKeyring, err := matProv.CreateAwsKmsMrkMultiKeyring(context.Background(), awsKmsMrkKeyringMultiInput)
if err != nil {
panic(err)
}
// Second Keyring: Create a MRK discovery multi-keyring with a custom client supplier.
// A discovery MRK multi-keyring will be composed of
// multiple discovery MRK keyrings, one for each region.
// Each component keyring has its own KMS client in a particular region.
// When we provide a client supplier to the multi-keyring, all component
// keyrings will use that client supplier configuration.
// In our tests, we make `mrk_key_id_encrypt` an MRK with a replica, and
// provide only the replica region in our discovery filter.
discoveryFilter := mpltypes.DiscoveryFilter{
AccountIds: []string{awsAccountId},
Partition: "aws",
}
awsKmsMrkDiscoveryMultiKeyringInput := mpltypes.CreateAwsKmsMrkDiscoveryMultiKeyringInput{
ClientSupplier: &RegionalRoleClientSupplier{},
Regions: awsRegions,
DiscoveryFilter: &discoveryFilter,
}
awsKmsMrkDiscoveryMultiKeyring, err := matProv.CreateAwsKmsMrkDiscoveryMultiKeyring(context.Background(), awsKmsMrkDiscoveryMultiKeyringInput)
// Step 5a: Encrypt
res, err := encryptionClient.Encrypt(context.Background(), esdktypes.EncryptInput{
Plaintext: []byte(exampleText),
EncryptionContext: encryptionContext,
Keyring: awsKmsMrkMultiKeyring,
})
if err != nil {
panic(err)
}
// Validate Ciphertext and Plaintext before encryption are NOT the same
if string(res.Ciphertext) == exampleText {
panic("Ciphertext and Plaintext before encryption are the same")
}
// Step 5b: Decrypt
// Decrypt your encrypted data using the discovery multi keyring.
// On Decrypt, the header of the encrypted message (ciphertext) will be parsed.
// The header contains the Encrypted Data Keys (EDKs), which, if the EDK
// was encrypted by a KMS Keyring, includes the KMS Key ARN.
// For each member of the Multi Keyring, every EDK will try to be decrypted until a decryption
// is successful.
// Since every member of the Multi Keyring is a Discovery Keyring:
// Each Keyring will filter the EDKs by the Discovery Filter and the Keyring's region.
// For each filtered EDK, the keyring will attempt decryption with the keyring's client.
// All of this is done serially, until a success occurs or all keyrings have failed
// all (filtered) EDKs. KMS MRK Discovery Keyrings will attempt to decrypt
// Multi Region Keys (MRKs) and regular KMS Keys.
decryptOutput, err := encryptionClient.Decrypt(context.Background(), esdktypes.DecryptInput{
EncryptionContext: encryptionContext,
Keyring: awsKmsMrkDiscoveryMultiKeyring,
Ciphertext: res.Ciphertext,
})
if err != nil {
panic(err)
}
// Validate Plaintext after decryption and Plaintext before encryption ARE the same
if string(decryptOutput.Plaintext) != exampleText {
panic("Plaintext after decryption and Plaintext before encryption are NOT the same")
}
// If you do not specify the encryption context on Decrypt, it's recommended to check if the resulting encryption context matches.
// The encryption context was specified on decrypt; we are validating the encryption context for demonstration only.
// Before your application uses plaintext data, verify that the encryption context that
// you used to encrypt the message is included in the encryption context that was used to
// decrypt the message. The AWS Encryption SDK can add pairs, so don't require an exact match.
if err = validateEncryptionContext(encryptionContext, decryptOutput.EncryptionContext); err != nil {
panic(err)
}
// Test the Missing Region Exception
// (This is for demonstration; you do not need to do this in your code.)

// Create a MRK discovery multi-keyring with a custom client supplier and a fake region.
awsKmsMrkDiscoveryMultiKeyringInputMissingRegion := mpltypes.CreateAwsKmsMrkDiscoveryMultiKeyringInput{
ClientSupplier: &RegionalRoleClientSupplier{},
Regions: []string{"fake-region"},
DiscoveryFilter: &discoveryFilter,
}
_, err = matProv.CreateAwsKmsMrkDiscoveryMultiKeyring(context.Background(), awsKmsMrkDiscoveryMultiKeyringInputMissingRegion)
// Swallow the AwsCryptographicMaterialProvidersException but you may choose how to handle the exception
switch err.(type) {
case mpltypes.AwsCryptographicMaterialProvidersException:
// You may choose how to handle the exception in this switch case.
default:
panic("Decryption using discovery keyring with missing region MUST raise AwsCryptographicMaterialProvidersException")
}
fmt.Println("Client Supplier Example completed successfully")
}

// This function only does subset matching because AWS Encryption SDK can add pairs, so don't require an exact match.
func validateEncryptionContext(expected, actual map[string]string) error {
for expectedKey, expectedValue := range expected {
actualValue, exists := actual[expectedKey]
if !exists || actualValue != expectedValue {
return fmt.Errorf("encryption context mismatch: expected key '%s' with value '%s'",
expectedKey, expectedValue)
}
}
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package clientsupplier

import (
"context"

mpltypes "github.com/aws/aws-cryptographic-material-providers-library/mpl/awscryptographymaterialproviderssmithygeneratedtypes"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/aws/aws-sdk-go-v2/service/sts"
)

/*
Example class demonstrating an implementation of a custom client supplier.
This particular implementation will create KMS clients with different IAM roles,
depending on the region passed.
*/

// RegionalRoleClientSupplier provides implementation for mpltypes.IClientSupplier
type RegionalRoleClientSupplier struct {
}

func (this *RegionalRoleClientSupplier) GetClient(input mpltypes.GetClientInput) (kms.Client, error) {
region := input.Region
// Check if the region is supported
regionIamRoleMap := RegionIamRoleMap()
var defaultVal kms.Client
// Check if region is supported
if _, exists := regionIamRoleMap[region]; !exists {
return defaultVal, mpltypes.AwsCryptographicMaterialProvidersException{
Message: "Region is not supported by this client supplier",
}
}
// Get the IAM role ARN associated with the region
arn := regionIamRoleMap[region]
ctx := context.TODO()
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion(region),
)
if err != nil {
return defaultVal, err
}
stsClient := sts.NewFromConfig(cfg)
// Create the AssumeRoleProvider
provider := stscreds.NewAssumeRoleProvider(stsClient, arn, func(o *stscreds.AssumeRoleOptions) {
o.RoleSessionName = "Go-ESDK-Client-Supplier-Example-Session"
})
// Load AWS SDK configuration with the AssumeRoleProvider
sdkConfig, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region), config.WithCredentialsProvider(provider))
if err != nil {
return defaultVal, mpltypes.AwsCryptographicMaterialProvidersException{Message: "failed to load AWS SDK config"}
}
// Create the KMS client
kmsClient := kms.NewFromConfig(sdkConfig)
// Return the KMS client wrapped in a custom type
return *kmsClient, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package clientsupplier

/*
File containing config for the RegionalRoleClientSupplier.
In your own code, this might be hardcoded, or reference
an external source, e.g. environment variables or AWS AppConfig.
*/

const (
usEast1IamRole = "arn:aws:iam::370957321024:role/GitHub-CI-Public-ESDK-Dafny-Role-only-us-east-1-KMS-keys"
euWest1IamRole = "arn:aws:iam::370957321024:role/GitHub-CI-Public-ESDK-Dafny-Role-only-eu-west-1-KMS-keys"
)

func RegionIamRoleMap() map[string]string {
return map[string]string{
"us-east-1": usEast1IamRole,
"eu-west-1": euWest1IamRole,
}
}
Loading
Loading