crypto: simplify Context encoding (#11812)

This commit adds a `MarshalText` implementation
to the `crypto.Context` type.
The `MarshalText` implementation replaces the
`WriteTo` and `AppendTo` implementation.

It is slightly slower than the `AppendTo` implementation
```
goos: darwin
goarch: arm64
pkg: github.com/minio/minio/cmd/crypto
BenchmarkContext_AppendTo/0-elems-8         	381475698	         2.892 ns/op	       0 B/op	       0 allocs/op
BenchmarkContext_AppendTo/1-elems-8         	17945088	        67.54 ns/op	       0 B/op	       0 allocs/op
BenchmarkContext_AppendTo/3-elems-8         	 5431770	       221.2 ns/op	      72 B/op	       2 allocs/op
BenchmarkContext_AppendTo/4-elems-8         	 3430684	       346.7 ns/op	      88 B/op	       2 allocs/op
```
vs.
```
BenchmarkContext/0-elems-8         	135819834	         8.658 ns/op	       2 B/op	       1 allocs/op
BenchmarkContext/1-elems-8         	13326243	        89.20 ns/op	     128 B/op	       1 allocs/op
BenchmarkContext/3-elems-8         	 4935301	       243.1 ns/op	     200 B/op	       3 allocs/op
BenchmarkContext/4-elems-8         	 2792142	       428.2 ns/op	     504 B/op	       4 allocs/op
goos: darwin
```

However, the `AppendTo` benchmark used a pre-allocated buffer. While
this improves its performance it does not match the actual usage of
`crypto.Context` which is passed to a `KMS` and always encoded into
a newly allocated buffer.

Therefore, this change seems acceptable since it should not impact the
actual performance but reduces the overall code for Context marshaling.
This commit is contained in:
Andreas Auernhammer 2021-03-20 10:48:48 +01:00 committed by GitHub
parent 9a6487319a
commit 7b3719c17b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 57 additions and 132 deletions

View File

@ -81,8 +81,6 @@ var (
errInvalidInternalIV = Errorf("The internal encryption IV is malformed")
errInvalidInternalSealAlgorithm = Errorf("The internal seal algorithm is invalid and not supported")
errMissingUpdatedKey = Errorf("The key update returned no error but also no sealed key")
)
var (

View File

@ -181,7 +181,10 @@ func (kes *kesService) CreateKey(keyID string) error { return kes.client.CreateK
// named key referenced by keyID. It also binds the generated key
// cryptographically to the provided context.
func (kes *kesService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
context := ctx.AppendTo(make([]byte, 0, 128))
context, err := ctx.MarshalText()
if err != nil {
return key, nil, err
}
var plainKey []byte
plainKey, sealedKey, err = kes.client.GenerateDataKey(keyID, context)
@ -203,7 +206,10 @@ func (kes *kesService) GenerateKey(keyID string, ctx Context) (key [32]byte, sea
// The context must be same context as the one provided while
// generating the plaintext key / sealedKey.
func (kes *kesService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
context := ctx.AppendTo(make([]byte, 0, 128))
context, err := ctx.MarshalText()
if err != nil {
return key, err
}
var plainKey []byte
plainKey, err = kes.client.DecryptDataKey(keyID, sealedKey, context)

View File

@ -21,7 +21,6 @@ import (
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io"
"sort"
@ -33,74 +32,29 @@ import (
// associated with a certain object.
type Context map[string]string
// WriteTo writes the context in a canonical from to w.
// It returns the number of bytes and the first error
// encounter during writing to w, if any.
//
// WriteTo sorts the context keys and writes the sorted
// key-value pairs as canonical JSON object to w.
// Sort order is based on the un-escaped keys.
//
// Note that neither keys nor values are escaped for JSON.
func (c Context) WriteTo(w io.Writer) (n int64, err error) {
sortedKeys := make(sort.StringSlice, 0, len(c))
for k := range c {
sortedKeys = append(sortedKeys, k)
}
sort.Sort(sortedKeys)
// MarshalText returns a canonical text representation of
// the Context.
escape := func(s string) string {
buf := bytes.NewBuffer(make([]byte, 0, len(s)))
EscapeStringJSON(buf, s)
return buf.String()
}
nn, err := io.WriteString(w, "{")
if err != nil {
return n + int64(nn), err
}
n += int64(nn)
for i, k := range sortedKeys {
s := fmt.Sprintf("\"%s\":\"%s\",", escape(k), escape(c[k]))
if i == len(sortedKeys)-1 {
s = s[:len(s)-1] // remove last ','
}
nn, err = io.WriteString(w, s)
if err != nil {
return n + int64(nn), err
}
n += int64(nn)
}
nn, err = io.WriteString(w, "}")
return n + int64(nn), err
}
// AppendTo appends the context in a canonical from to dst.
//
// AppendTo sorts the context keys and writes the sorted
// key-value pairs as canonical JSON object to w.
// Sort order is based on the un-escaped keys.
//
// Note that neither keys nor values are escaped for JSON.
func (c Context) AppendTo(dst []byte) (output []byte) {
// MarshalText sorts the context keys and writes the sorted
// key-value pairs as canonical JSON object. The sort order
// is based on the un-escaped keys.
func (c Context) MarshalText() ([]byte, error) {
if len(c) == 0 {
return append(dst, '{', '}')
return []byte{'{', '}'}, nil
}
// out should not escape.
out := bytes.NewBuffer(dst)
// No need to copy+sort
// Pre-allocate a buffer - 128 bytes is an arbitrary
// heuristic value that seems like a good starting size.
var b = bytes.NewBuffer(make([]byte, 0, 128))
if len(c) == 1 {
for k, v := range c {
out.WriteString(`{"`)
EscapeStringJSON(out, k)
out.WriteString(`":"`)
EscapeStringJSON(out, v)
out.WriteString(`"}`)
b.WriteString(`{"`)
EscapeStringJSON(b, k)
b.WriteString(`":"`)
EscapeStringJSON(b, v)
b.WriteString(`"}`)
}
return out.Bytes()
return b.Bytes(), nil
}
sortedKeys := make([]string, 0, len(c))
@ -109,19 +63,19 @@ func (c Context) AppendTo(dst []byte) (output []byte) {
}
sort.Strings(sortedKeys)
out.WriteByte('{')
b.WriteByte('{')
for i, k := range sortedKeys {
out.WriteByte('"')
EscapeStringJSON(out, k)
out.WriteString(`":"`)
EscapeStringJSON(out, c[k])
out.WriteByte('"')
b.WriteByte('"')
EscapeStringJSON(b, k)
b.WriteString(`":"`)
EscapeStringJSON(b, c[k])
b.WriteByte('"')
if i < len(sortedKeys)-1 {
out.WriteByte(',')
b.WriteByte(',')
}
}
out.WriteByte('}')
return out.Bytes()
b.WriteByte('}')
return b.Bytes(), nil
}
// KMS represents an active and authenticted connection
@ -225,9 +179,11 @@ func (kms *masterKeyKMS) deriveKey(keyID string, context Context) (key [32]byte)
if context == nil {
context = Context{}
}
ctxBytes, _ := context.MarshalText()
mac := hmac.New(sha256.New, kms.masterKey[:])
mac.Write([]byte(keyID))
mac.Write(context.AppendTo(make([]byte, 0, 128)))
mac.Write(ctxBytes)
mac.Sum(key[:0])
return key
}

View File

@ -18,7 +18,6 @@ import (
"bytes"
"fmt"
"path"
"strings"
"testing"
)
@ -61,7 +60,7 @@ func TestMasterKeyKMS(t *testing.T) {
}
}
var contextWriteToTests = []struct {
var contextMarshalTextTests = []struct {
Context Context
ExpectedJSON string
}{
@ -76,43 +75,29 @@ var contextWriteToTests = []struct {
6: {Context: Context{"a": "<>&"}, ExpectedJSON: `{"a":"\u003c\u003e\u0026"}`},
}
func TestContextWriteTo(t *testing.T) {
for i, test := range contextWriteToTests {
var jsonContext strings.Builder
if _, err := test.Context.WriteTo(&jsonContext); err != nil {
t.Errorf("Test %d: Failed to encode context: %v", i, err)
continue
func TestContextMarshalText(t *testing.T) {
for i, test := range contextMarshalTextTests {
text, err := test.Context.MarshalText()
if err != nil {
t.Fatalf("Test %d: Failed to encode context: %v", i, err)
}
if s := jsonContext.String(); s != test.ExpectedJSON {
t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, s, test.ExpectedJSON)
if string(text) != test.ExpectedJSON {
t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, string(text), test.ExpectedJSON)
}
}
}
func TestContextAppendTo(t *testing.T) {
for i, test := range contextWriteToTests {
dst := make([]byte, 0, 1024)
dst = test.Context.AppendTo(dst)
if s := string(dst); s != test.ExpectedJSON {
t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, s, test.ExpectedJSON)
}
// Append one more
dst = test.Context.AppendTo(dst)
if s := string(dst); s != test.ExpectedJSON+test.ExpectedJSON {
t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, s, test.ExpectedJSON+test.ExpectedJSON)
}
}
}
func BenchmarkContext_AppendTo(b *testing.B) {
func BenchmarkContext(b *testing.B) {
tests := []Context{{}, {"bucket": "warp-benchmark-bucket"}, {"0": "1", "-": "2", ".": "#"}, {"34trg": "dfioutr89", "ikjfdghkjf": "jkedfhgfjkhg", "sdfhsdjkh": "if88889", "asddsirfh804": "kjfdshgdfuhgfg78-45604586#$%<>&"}}
for _, test := range tests {
b.Run(fmt.Sprintf("%d-elems", len(test)), func(b *testing.B) {
dst := make([]byte, 0, 1024)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
dst = test.AppendTo(dst[:0])
_, err := test.MarshalText()
if err != nil {
b.Fatal(err)
}
}
})
}

View File

@ -223,7 +223,10 @@ func (v *vaultService) CreateKey(keyID string) error {
// named key referenced by keyID. It also binds the generated key
// cryptographically to the provided context.
func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
context := ctx.AppendTo(make([]byte, 0, 128))
context, err := ctx.MarshalText()
if err != nil {
return key, sealedKey, err
}
payload := map[string]interface{}{
"context": base64.StdEncoding.EncodeToString(context),
@ -258,7 +261,10 @@ func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sea
// The context must be same context as the one provided while
// generating the plaintext key / sealedKey.
func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
context := ctx.AppendTo(make([]byte, 0, 128))
context, err := ctx.MarshalText()
if err != nil {
return key, err
}
payload := map[string]interface{}{
"ciphertext": string(sealedKey),
@ -282,29 +288,3 @@ func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (k
copy(key[:], plainKey)
return key, nil
}
// UpdateKey re-wraps the sealedKey if the master key referenced by the keyID
// has been changed by the KMS operator - i.e. the master key has been rotated.
// If the master key hasn't changed since the sealedKey has been created / updated
// it may return the same sealedKey as rotatedKey.
//
// The context must be same context as the one provided while
// generating the plaintext key / sealedKey.
func (v *vaultService) UpdateKey(keyID string, sealedKey []byte, ctx Context) (rotatedKey []byte, err error) {
context := ctx.AppendTo(make([]byte, 0, 128))
payload := map[string]interface{}{
"ciphertext": string(sealedKey),
"context": base64.StdEncoding.EncodeToString(context),
}
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/rewrap/%s", keyID), payload)
if err != nil {
return nil, Errorf("crypto: client error %w", err)
}
ciphertext, ok := s.Data["ciphertext"]
if !ok {
return nil, errMissingUpdatedKey
}
rotatedKey = []byte(ciphertext.(string))
return rotatedKey, nil
}