internal/server/seal/seal_test.go
Ref: Size: 3.8 KiB History
package seal
import (
"bytes"
"encoding/base64"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testKEK is a fixed key-encryption key; kek2 is a different one of the same
// size, for the wrong-key case.
var (
testKEK = bytes.Repeat([]byte{0x2b}, KEKSize)
kek2 = bytes.Repeat([]byte{0x7f}, KEKSize)
)
// samplePEM stands in for the key material this package protects — a private
// key PEM, whether it came from a file or a database column.
const samplePEM = "-----BEGIN OPENSSH PRIVATE KEY-----\nc29tZS1rZXktbWF0ZXJpYWw=\n-----END OPENSSH PRIVATE KEY-----\n"
// TestSealOpenRoundTrip: what was sealed comes back byte for byte.
func TestSealOpenRoundTrip(t *testing.T) {
blob, err := Seal(testKEK, samplePEM)
require.NoError(t, err)
got, err := Open(testKEK, blob)
require.NoError(t, err)
assert.Equal(t, samplePEM, got)
}
// TestSealHidesThePlaintext is the property the whole feature exists for: what
// gets stored carries no trace of the key.
func TestSealHidesThePlaintext(t *testing.T) {
blob, err := Seal(testKEK, samplePEM)
require.NoError(t, err)
assert.NotContains(t, blob, "PRIVATE KEY")
assert.NotContains(t, blob, samplePEM)
assert.True(t, IsSealed(blob), "the stored form is versioned, got %q", blob[:min(8, len(blob))])
}
// TestIsSealedTellsTheFormsApart: a file written before sealing holds a PEM,
// and the load path decides what to do by looking at exactly this.
func TestIsSealedTellsTheFormsApart(t *testing.T) {
assert.False(t, IsSealed(samplePEM))
assert.False(t, IsSealed(""))
assert.False(t, IsSealed("v2:abc"))
blob, err := Seal(testKEK, samplePEM)
require.NoError(t, err)
assert.True(t, IsSealed(blob))
}
// TestSealUsesAFreshNonce: sealing the same key twice must not produce the same
// bytes, or a reader could tell two holders share a key.
func TestSealUsesAFreshNonce(t *testing.T) {
first, err := Seal(testKEK, "the-same-key")
require.NoError(t, err)
second, err := Seal(testKEK, "the-same-key")
require.NoError(t, err)
assert.NotEqual(t, first, second)
}
// TestOpenFailsClosed walks every way a sealed value can be wrong. All of them
// are the same outcome — nothing — and none of the errors quote the material.
func TestOpenFailsClosed(t *testing.T) {
blob, err := Seal(testKEK, samplePEM)
require.NoError(t, err)
for _, tc := range []struct {
name, blob string
kek []byte
}{
{"wrong kek", blob, kek2},
{"tampered ciphertext", flipLastByte(t, blob), testKEK},
{"unknown prefix", "v2:" + strings.TrimPrefix(blob, Version), testKEK},
{"no prefix at all", strings.TrimPrefix(blob, Version), testKEK},
{"plaintext pem", samplePEM, testKEK},
{"not base64", Version + "!!!not-base64!!!", testKEK},
{"truncated", Version + base64.StdEncoding.EncodeToString([]byte("short")), testKEK},
{"kek of the wrong length", blob, testKEK[:16]},
{"no kek", blob, nil},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := Open(tc.kek, tc.blob)
require.Error(t, err)
assert.Empty(t, got)
assert.NotContains(t, err.Error(), "PRIVATE KEY", "errors never echo key material")
assert.NotContains(t, err.Error(), tc.blob, "errors never echo the sealed value")
})
}
}
// TestSealRefusesAWrongSizedKEK: a short or absent KEK is a configuration
// mistake, and sealing under one would be worse than refusing.
func TestSealRefusesAWrongSizedKEK(t *testing.T) {
for _, kek := range [][]byte{nil, {}, testKEK[:31], append(bytes.Clone(testKEK), 0x00)} {
_, err := Seal(kek, samplePEM)
assert.Error(t, err)
}
}
// flipLastByte changes one byte of a sealed value's ciphertext.
func flipLastByte(t *testing.T, blob string) string {
t.Helper()
raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(blob, Version))
require.NoError(t, err)
raw[len(raw)-1] ^= 0xff
return Version + base64.StdEncoding.EncodeToString(raw)
}