Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit ffb0c38

Browse files
chore(go): Add various keyrings examples in Go (#1948)
1 parent bf5a106 commit ffb0c38

File tree

8 files changed

+1197
-18
lines changed

8 files changed

+1197
-18
lines changed
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package keyring
5+
6+
import (
7+
"context"
8+
"encoding/base64"
9+
"fmt"
10+
"os"
11+
"path/filepath"
12+
"reflect"
13+
"strings"
14+
15+
mpl "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographymaterialproviderssmithygenerated"
16+
mpltypes "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographymaterialproviderssmithygeneratedtypes"
17+
dbesdkdynamodbencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes"
18+
dbesdkstructuredencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkstructuredencryptionsmithygeneratedtypes"
19+
"github.com/aws/aws-database-encryption-sdk-dynamodb/dbesdkmiddleware"
20+
"github.com/aws/aws-database-encryption-sdk-dynamodb/examples/utils"
21+
22+
"github.com/aws/aws-sdk-go-v2/aws"
23+
"github.com/aws/aws-sdk-go-v2/config"
24+
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
25+
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
26+
"github.com/aws/aws-sdk-go-v2/service/kms"
27+
kmstypes "github.com/aws/aws-sdk-go-v2/service/kms/types"
28+
)
29+
30+
/*
31+
This example sets up DynamoDb Encryption for the AWS SDK client
32+
using the KMS RSA Keyring. This keyring uses a KMS RSA key pair to
33+
encrypt and decrypt records. The client uses the downloaded public key
34+
to encrypt items it adds to the table.
35+
The keyring uses the private key to decrypt existing table items it retrieves,
36+
by calling KMS' decrypt API.
37+
38+
Running this example requires access to the DDB Table whose name
39+
is provided in CLI arguments.
40+
This table must be configured with the following
41+
primary key configuration:
42+
- Partition key is named "partition_key" with type (S)
43+
- Sort key is named "sort_key" with type (S)
44+
This example also requires access to a KMS RSA key.
45+
Our tests provide a KMS RSA ARN that anyone can use, but you
46+
can also provide your own KMS RSA key.
47+
To use your own KMS RSA key, you must have either:
48+
- Its public key downloaded in a UTF-8 encoded PEM file
49+
- kms:GetPublicKey permissions on that key
50+
If you do not have the public key downloaded, running this example
51+
through its main method will download the public key for you
52+
by calling kms:GetPublicKey.
53+
You must also have kms:Decrypt permissions on the KMS RSA key.
54+
*/
55+
56+
func KmsRsaKeyringExample(ddbTableName, rsaKeyArn, rsaPublicKeyFilename string) {
57+
// You may provide your own RSA public key at rsaPublicKeyFilename.
58+
// This must be the public key for the RSA key represented at rsaKeyArn.
59+
// If this file is not present, this will write a UTF-8 encoded PEM file for you.
60+
if !utils.FileExists(rsaPublicKeyFilename) {
61+
writePublicKeyPemForRsaKey(rsaKeyArn, rsaPublicKeyFilename)
62+
}
63+
64+
// 1. Load UTF-8 encoded public key PEM file.
65+
// You may have an RSA public key file already defined.
66+
// If not, the main method in this class will call
67+
// the KMS RSA key, retrieve its public key, and store it
68+
// in a PEM file for example use.
69+
publicKeyUtf8EncodedBytes, err := os.ReadFile(rsaPublicKeyFilename)
70+
utils.HandleError(err)
71+
72+
// 2. Create a KMS RSA keyring.
73+
// This keyring takes in:
74+
// - kmsClient
75+
// - kmsKeyId: Must be an ARN representing a KMS RSA key
76+
// - publicKey: A ByteBuffer of a UTF-8 encoded PEM file representing the public
77+
// key for the key passed into kmsKeyId
78+
// - encryptionAlgorithm: Must be either RSAES_OAEP_SHA_256 or RSAES_OAEP_SHA_1
79+
cfg, err := config.LoadDefaultConfig(context.TODO())
80+
utils.HandleError(err)
81+
kmsClient := kms.NewFromConfig(cfg, func(o *kms.Options) {
82+
o.Region = "us-west-2"
83+
})
84+
matProv, err := mpl.NewClient(mpltypes.MaterialProvidersConfig{})
85+
utils.HandleError(err)
86+
87+
createAwsKmsRsaKeyringInput := mpltypes.CreateAwsKmsRsaKeyringInput{
88+
KmsClient: kmsClient,
89+
KmsKeyId: rsaKeyArn,
90+
PublicKey: publicKeyUtf8EncodedBytes,
91+
EncryptionAlgorithm: kmstypes.EncryptionAlgorithmSpecRsaesOaepSha256,
92+
}
93+
awsKmsRsaKeyring, err := matProv.CreateAwsKmsRsaKeyring(context.Background(), createAwsKmsRsaKeyringInput)
94+
utils.HandleError(err)
95+
96+
// 3. Configure which attributes are encrypted and/or signed when writing new items.
97+
// For each attribute that may exist on the items we plan to write to our DynamoDbTable,
98+
// we must explicitly configure how they should be treated during item encryption:
99+
// - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
100+
// - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
101+
// - DO_NOTHING: The attribute is not encrypted and not included in the signature
102+
attributeActions := map[string]dbesdkstructuredencryptiontypes.CryptoAction{
103+
"partition_key": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our partition attribute must be SIGN_ONLY
104+
"sort_key": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, // Our sort attribute must be SIGN_ONLY
105+
"sensitive_data": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign,
106+
}
107+
108+
// 4. Configure which attributes we expect to be included in the signature
109+
// when reading items. There are two options for configuring this:
110+
//
111+
// - (Recommended) Configure `allowedUnsignedAttributesPrefix`:
112+
// When defining your DynamoDb schema and deciding on attribute names,
113+
// choose a distinguishing prefix (such as ":") for all attributes that
114+
// you do not want to include in the signature.
115+
// This has two main benefits:
116+
// - It is easier to reason about the security and authenticity of data within your item
117+
// when all unauthenticated data is easily distinguishable by their attribute name.
118+
// - If you need to add new unauthenticated attributes in the future,
119+
// you can easily make the corresponding update to your `attributeActions`
120+
// and immediately start writing to that new attribute, without
121+
// any other configuration update needed.
122+
// Once you configure this field, it is not safe to update it.
123+
//
124+
// - Configure `allowedUnsignedAttributes`: You may also explicitly list
125+
// a set of attributes that should be considered unauthenticated when encountered
126+
// on read. Be careful if you use this configuration. Do not remove an attribute
127+
// name from this configuration, even if you are no longer writing with that attribute,
128+
// as old items may still include this attribute, and our configuration needs to know
129+
// to continue to exclude this attribute from the signature scope.
130+
// If you add new attribute names to this field, you must first deploy the update to this
131+
// field to all readers in your host fleet before deploying the update to start writing
132+
// with that new attribute.
133+
//
134+
// For this example, we currently authenticate all attributes. To make it easier to
135+
// add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
136+
unsignAttrPrefix := ":"
137+
138+
// 5. Create the DynamoDb Encryption configuration for the table we will be writing to.
139+
// Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite
140+
// that does not use asymmetric signing.
141+
partitionKey := "partition_key"
142+
sortKeyName := "sort_key"
143+
// Specify algorithmSuite without asymmetric signing here
144+
// The only supported algorithmSuite without asymmetric signing is
145+
// ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384.
146+
algorithmSuiteID := mpltypes.DBEAlgorithmSuiteIdAlgAes256GcmHkdfSha512CommitKeySymsigHmacSha384
147+
148+
tableConfig := dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{
149+
LogicalTableName: ddbTableName,
150+
PartitionKeyName: partitionKey,
151+
SortKeyName: &sortKeyName,
152+
AttributeActionsOnEncrypt: attributeActions,
153+
Keyring: awsKmsRsaKeyring,
154+
AllowedUnsignedAttributePrefix: &unsignAttrPrefix,
155+
AlgorithmSuiteId: &algorithmSuiteID,
156+
}
157+
158+
tableConfigsMap := make(map[string]dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig)
159+
tableConfigsMap[ddbTableName] = tableConfig
160+
listOfTableConfigs := dbesdkdynamodbencryptiontypes.DynamoDbTablesEncryptionConfig{
161+
TableEncryptionConfigs: tableConfigsMap,
162+
}
163+
164+
// 6. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor
165+
dbEsdkMiddleware, err := dbesdkmiddleware.NewDBEsdkMiddleware(listOfTableConfigs)
166+
utils.HandleError(err)
167+
ddb := dynamodb.NewFromConfig(cfg, dbEsdkMiddleware.CreateMiddleware())
168+
169+
// 7. Put an item into our table using the above client.
170+
// Before the item gets sent to DynamoDb, it will be encrypted
171+
// client-side, according to our configuration.
172+
item := map[string]types.AttributeValue{
173+
"partition_key": &types.AttributeValueMemberS{Value: "awsKmsRsaKeyringItem"},
174+
"sort_key": &types.AttributeValueMemberN{Value: "0"},
175+
"sensitive_data": &types.AttributeValueMemberS{Value: "encrypt and sign me!"},
176+
}
177+
178+
putInput := &dynamodb.PutItemInput{
179+
TableName: aws.String(ddbTableName),
180+
Item: item,
181+
}
182+
183+
_, err = ddb.PutItem(context.TODO(), putInput)
184+
utils.HandleError(err)
185+
186+
// 8. Get the item back from our table using the client.
187+
// The client will decrypt the item client-side using the RSA keyring
188+
// and return the original item.
189+
keyToGet := map[string]types.AttributeValue{
190+
"partition_key": &types.AttributeValueMemberS{Value: "awsKmsRsaKeyringItem"},
191+
"sort_key": &types.AttributeValueMemberN{Value: "0"},
192+
}
193+
194+
getInput := &dynamodb.GetItemInput{
195+
Key: keyToGet,
196+
TableName: aws.String(ddbTableName),
197+
// In this example we configure a strongly consistent read
198+
// because we perform a read immediately after a write (for demonstrative purposes).
199+
// By default, reads are only eventually consistent.
200+
// Read our docs to determine which read consistency to use for your application:
201+
// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html
202+
ConsistentRead: aws.Bool(true),
203+
}
204+
205+
getResponse, err := ddb.GetItem(context.TODO(), getInput)
206+
utils.HandleError(err)
207+
208+
returnedItem := getResponse.Item
209+
if !reflect.DeepEqual(item, returnedItem) {
210+
panic("Decrypted item does not match original item")
211+
}
212+
213+
fmt.Println("AWS KMS RSA Keyring Example successful.")
214+
}
215+
216+
func writePublicKeyPemForRsaKey(rsaKeyArn, rsaPublicKeyFilename string) {
217+
// This code will call KMS to get the public key for the KMS RSA key.
218+
// You must have kms:GetPublicKey permissions on the key for this to succeed.
219+
// The public key will be written to the file rsaPublicKeyFilename.
220+
cfg, err := config.LoadDefaultConfig(context.TODO())
221+
utils.HandleError(err)
222+
223+
getterForPublicKey := kms.NewFromConfig(cfg, func(o *kms.Options) {
224+
o.Region = "us-west-2"
225+
})
226+
227+
response, err := getterForPublicKey.GetPublicKey(context.TODO(), &kms.GetPublicKeyInput{
228+
KeyId: aws.String(rsaKeyArn),
229+
})
230+
utils.HandleError(err)
231+
232+
publicKeyByteArray := response.PublicKey
233+
234+
// Create PEM formatted public key
235+
pemContent := fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n",
236+
encodeToPemBase64(publicKeyByteArray))
237+
238+
// Ensure directory exists
239+
dir := filepath.Dir(rsaPublicKeyFilename)
240+
if err := os.MkdirAll(dir, 0755); err != nil {
241+
utils.HandleError(err)
242+
}
243+
244+
// Write to file
245+
err = os.WriteFile(rsaPublicKeyFilename, []byte(pemContent), 0644)
246+
utils.HandleError(err)
247+
}
248+
249+
// encodeToPemBase64 encodes bytes to base64 with proper line breaks for PEM format
250+
func encodeToPemBase64(data []byte) string {
251+
const lineLength = 64
252+
encoded := base64.StdEncoding.EncodeToString(data)
253+
254+
// Add line breaks every 64 characters
255+
var result strings.Builder
256+
for i := 0; i < len(encoded); i += lineLength {
257+
end := i + lineLength
258+
if end > len(encoded) {
259+
end = len(encoded)
260+
}
261+
result.WriteString(encoded[i:end])
262+
if end < len(encoded) {
263+
result.WriteString("\n")
264+
}
265+
}
266+
267+
return result.String()
268+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package keyring
5+
6+
import (
7+
"fmt"
8+
9+
dbesdkdynamodbencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes"
10+
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
11+
)
12+
13+
// ExampleBranchKeyIdSupplier is used in the 'HierarchicalKeyringExample'.
14+
// In that example, we have a table where we distinguish multiple tenants
15+
// by a tenant ID that is stored in our partition attribute.
16+
// The expectation is that this does not produce a confused deputy
17+
// because the tenants are separated by partition.
18+
// In order to create a Hierarchical Keyring that is capable of encrypting or
19+
// decrypting data for either tenant, we implement this interface
20+
// to map the correct branch key ID to the correct tenant ID.
21+
type ExampleBranchKeyIdSupplier struct {
22+
branchKeyIdForTenant1 string
23+
branchKeyIdForTenant2 string
24+
}
25+
26+
// NewExampleBranchKeyIdSupplier creates a new instance of ExampleBranchKeyIdSupplier
27+
func NewExampleBranchKeyIdSupplier(tenant1Id, tenant2Id string) *ExampleBranchKeyIdSupplier {
28+
return &ExampleBranchKeyIdSupplier{
29+
branchKeyIdForTenant1: tenant1Id,
30+
branchKeyIdForTenant2: tenant2Id,
31+
}
32+
}
33+
34+
// GetBranchKeyIdFromDdbKey implements the DynamoDbKeyBranchKeyIdSupplier interface
35+
func (s *ExampleBranchKeyIdSupplier) GetBranchKeyIdFromDdbKey(
36+
input dbesdkdynamodbencryptiontypes.GetBranchKeyIdFromDdbKeyInput,
37+
) (*dbesdkdynamodbencryptiontypes.GetBranchKeyIdFromDdbKeyOutput, error) {
38+
if input.DdbKey == nil {
39+
return nil, fmt.Errorf("DdbKey cannot be nil")
40+
}
41+
42+
key := input.DdbKey
43+
44+
partitionKeyAttr, exists := key["partition_key"]
45+
if !exists {
46+
return nil, fmt.Errorf("item invalid, does not contain expected partition key attribute")
47+
}
48+
49+
// Extract the string value from the AttributeValue
50+
var tenantKeyId string
51+
switch attr := partitionKeyAttr.(type) {
52+
case *types.AttributeValueMemberS:
53+
tenantKeyId = attr.Value
54+
default:
55+
return nil, fmt.Errorf("partition_key must be a string attribute")
56+
}
57+
58+
var branchKeyId string
59+
switch tenantKeyId {
60+
case "tenant1Id":
61+
branchKeyId = s.branchKeyIdForTenant1
62+
case "tenant2Id":
63+
branchKeyId = s.branchKeyIdForTenant2
64+
default:
65+
return nil, fmt.Errorf("item does not contain valid tenant ID")
66+
}
67+
68+
return &dbesdkdynamodbencryptiontypes.GetBranchKeyIdFromDdbKeyOutput{
69+
BranchKeyId: branchKeyId,
70+
}, nil
71+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /