a73x

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)
}