Add keyring package for kernel-keyring-backed secret sealing #3
@@ -13,6 +13,7 @@ imports it inherits them.
|
||||
| --- | --- |
|
||||
| `git.unkin.net/unkin/golib/pg` | Postgres: DSN from the environment, pgxpool construction, read/write splitting across a primary and a replica, and the estate's migration runner. |
|
||||
| `git.unkin.net/unkin/golib/pg/pgtest` | A throwaway Postgres container for a consumer's own `_test.go` files. Test-only. |
|
||||
| `git.unkin.net/unkin/golib/keyring` | Encrypts secrets at rest with a key held in the Linux kernel user keyring, scoped to the current boot and uid. |
|
||||
| `git.unkin.net/unkin/golib/colour` | jq-style JSON and yq-style YAML highlighting for CLI output, preserving the input byte for byte, plus the TTY/`NO_COLOR` check. |
|
||||
|
||||
### pg
|
||||
@@ -185,6 +186,25 @@ func TestSomething(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
### keyring
|
||||
|
||||
```go
|
||||
kr, err := keyring.Open("golib:vault-tools") // get-or-create the key
|
||||
sealed, err := kr.Seal(token)
|
||||
token, err := kr.Unseal(sealed)
|
||||
if errors.Is(err, keyring.ErrKeyUnavailable) {
|
||||
// rebooted or Forget()-ed: log in again
|
||||
}
|
||||
```
|
||||
|
||||
The key is 32 random bytes stored as a `user` key in the uid's user keyring
|
||||
(`@u`), so every login shell, terminal and systemd user service of that uid
|
||||
shares it until reboot. Its permissions grant the uid read access as well as the
|
||||
possessor, because processes whose session keyring does not link `@u` do not
|
||||
possess it. Data is AES-256-GCM in a versioned envelope; a missing, revoked or
|
||||
different key returns `ErrKeyUnavailable`, damaged data returns `ErrCorrupt`.
|
||||
`Forget` revokes the key. Non-Linux builds return `errors.ErrUnsupported`.
|
||||
|
||||
## Consuming
|
||||
|
||||
golib is versioned with semver tags and consumed like any Go module. Pin a tag:
|
||||
|
||||
@@ -6,6 +6,7 @@ require (
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
github.com/testcontainers/testcontainers-go v0.44.0
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/term v0.45.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
@@ -63,6 +64,5 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// Package keyring encrypts small secrets at rest with a random key held in the
|
||||
// Linux kernel user keyring, so the key lives only for the current boot and is
|
||||
// readable only by the current uid. A missing key (after a reboot or Forget)
|
||||
// surfaces as ErrKeyUnavailable, which callers treat as "log in again".
|
||||
package keyring
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrKeyUnavailable means the key is missing, revoked or not the one the
|
||||
// data was sealed with.
|
||||
ErrKeyUnavailable = errors.New("keyring: key unavailable")
|
||||
// ErrCorrupt means the envelope is malformed or fails authentication.
|
||||
ErrCorrupt = errors.New("keyring: sealed data corrupt")
|
||||
)
|
||||
|
||||
const (
|
||||
keySize = 32
|
||||
version1 = 1
|
||||
fpSize = 8
|
||||
nonceSize = 12
|
||||
headerLen = 1 + fpSize + nonceSize
|
||||
)
|
||||
|
||||
func fingerprint(key []byte) []byte {
|
||||
sum := sha256.Sum256(key)
|
||||
return sum[:fpSize]
|
||||
}
|
||||
|
||||
// seal returns version || fingerprint || nonce || AES-256-GCM(plaintext), with
|
||||
// version and fingerprint authenticated as additional data.
|
||||
func seal(key, plaintext []byte) ([]byte, error) {
|
||||
gcm, err := newGCM(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]byte, headerLen, headerLen+len(plaintext)+gcm.Overhead())
|
||||
out[0] = version1
|
||||
copy(out[1:], fingerprint(key))
|
||||
if _, err := rand.Read(out[1+fpSize : headerLen]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gcm.Seal(out, out[1+fpSize:headerLen], plaintext, out[:1+fpSize]), nil
|
||||
}
|
||||
|
||||
func unseal(key, sealed []byte) ([]byte, error) {
|
||||
if len(sealed) < headerLen || sealed[0] != version1 {
|
||||
return nil, ErrCorrupt
|
||||
}
|
||||
if subtle.ConstantTimeCompare(sealed[1:1+fpSize], fingerprint(key)) != 1 {
|
||||
return nil, ErrKeyUnavailable
|
||||
}
|
||||
gcm, err := newGCM(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pt, err := gcm.Open(nil, sealed[1+fpSize:headerLen], sealed[headerLen:], sealed[:1+fpSize])
|
||||
if err != nil {
|
||||
return nil, ErrCorrupt
|
||||
}
|
||||
return pt, nil
|
||||
}
|
||||
|
||||
func newGCM(key []byte) (cipher.AEAD, error) {
|
||||
if len(key) != keySize {
|
||||
return nil, ErrKeyUnavailable
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cipher.NewGCM(block)
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package keyring
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Possessor: all. User (same uid): view, read, write, search, setattr. Granting
|
||||
// the uid matters because the user keyring is only possessed by processes whose
|
||||
// session keyring links it, which a systemd user service or a `keyctl session`
|
||||
// shell does not.
|
||||
const keyPerm = 0x3f000000 | 0x002f0000
|
||||
|
||||
const createAttempts = 5
|
||||
|
||||
// Keyring seals and unseals data with the kernel-held key named by its
|
||||
// description.
|
||||
type Keyring struct {
|
||||
desc string
|
||||
}
|
||||
|
||||
// Open returns the keyring for desc, creating its key in the user keyring
|
||||
// (KEY_SPEC_USER_KEYRING, shared by every session of this uid until reboot) if
|
||||
// absent.
|
||||
func Open(desc string) (*Keyring, error) {
|
||||
if desc == "" {
|
||||
return nil, errors.New("keyring: empty description")
|
||||
}
|
||||
k := &Keyring{desc: desc}
|
||||
if _, err := k.getOrCreate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// Seal encrypts plaintext, recreating the key if it has been dropped.
|
||||
func (k *Keyring) Seal(plaintext []byte) ([]byte, error) {
|
||||
key, err := k.getOrCreate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return seal(key, plaintext)
|
||||
}
|
||||
|
||||
// Unseal decrypts data from Seal. It returns ErrKeyUnavailable when the key is
|
||||
// gone or differs from the sealing key, and ErrCorrupt when data is damaged.
|
||||
func (k *Keyring) Unseal(sealed []byte) ([]byte, error) {
|
||||
_, key, err := k.lookup()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return unseal(key, sealed)
|
||||
}
|
||||
|
||||
// Forget revokes the key and unlinks it from the user keyring, so everything
|
||||
// sealed with it becomes unreadable. Forgetting an absent key is not an error.
|
||||
func (k *Keyring) Forget() error {
|
||||
id, err := unix.KeyctlSearch(unix.KEY_SPEC_USER_KEYRING, "user", k.desc, 0)
|
||||
if err != nil {
|
||||
if missing(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("keyring: search %q: %w", k.desc, err)
|
||||
}
|
||||
if _, err := unix.KeyctlInt(unix.KEYCTL_REVOKE, id, 0, 0, 0); err != nil {
|
||||
return fmt.Errorf("keyring: revoke %q: %w", k.desc, err)
|
||||
}
|
||||
unlink(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookup returns the key and its id; the id is non-zero whenever the search
|
||||
// found a key, even one that turned out unusable.
|
||||
func (k *Keyring) lookup() (int, []byte, error) {
|
||||
id, err := unix.KeyctlSearch(unix.KEY_SPEC_USER_KEYRING, "user", k.desc, 0)
|
||||
if err != nil {
|
||||
if missing(err) {
|
||||
return 0, nil, fmt.Errorf("%w: %w", ErrKeyUnavailable, err)
|
||||
}
|
||||
return 0, nil, fmt.Errorf("keyring: search %q: %w", k.desc, err)
|
||||
}
|
||||
buf := make([]byte, 2*keySize)
|
||||
n, err := unix.KeyctlBuffer(unix.KEYCTL_READ, id, buf, 0)
|
||||
if err != nil {
|
||||
if missing(err) {
|
||||
return id, nil, fmt.Errorf("%w: %v", ErrKeyUnavailable, err)
|
||||
}
|
||||
return id, nil, fmt.Errorf("keyring: read %q: %w", k.desc, err)
|
||||
}
|
||||
if n != keySize {
|
||||
return id, nil, fmt.Errorf("%w: key %q is %d bytes", ErrKeyUnavailable, k.desc, n)
|
||||
}
|
||||
return id, buf[:n], nil
|
||||
}
|
||||
|
||||
// getOrCreate converges concurrent first users on one key: create links
|
||||
// exclusively, so a loser gets EEXIST and re-reads the winner's key.
|
||||
func (k *Keyring) getOrCreate() ([]byte, error) {
|
||||
for range createAttempts {
|
||||
id, key, err := k.lookup()
|
||||
if !errors.Is(err, ErrKeyUnavailable) {
|
||||
return key, err
|
||||
}
|
||||
// An unusable key of our description blocks the exclusive link.
|
||||
if id != 0 {
|
||||
unlink(id)
|
||||
} else if errors.Is(err, unix.EKEYREVOKED) || errors.Is(err, unix.EKEYEXPIRED) {
|
||||
purgeDead()
|
||||
}
|
||||
key, err = k.create()
|
||||
if !errors.Is(err, unix.EEXIST) {
|
||||
return key, err
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("keyring: %q: key creation kept racing", k.desc)
|
||||
}
|
||||
|
||||
// create adds a fresh key to this thread's keyring, which is always possessed,
|
||||
// so setperm succeeds, then moves it into the user keyring with MOVE_EXCL,
|
||||
// failing with EEXIST if a key of the same description is already there.
|
||||
func (k *Keyring) create() ([]byte, error) {
|
||||
key := make([]byte, keySize)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
id, err := unix.AddKey("user", k.desc, key, unix.KEY_SPEC_THREAD_KEYRING)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("keyring: add %q: %w", k.desc, err)
|
||||
}
|
||||
defer func() { _, _ = unix.KeyctlInt(unix.KEYCTL_UNLINK, id, unix.KEY_SPEC_THREAD_KEYRING, 0, 0) }()
|
||||
if err := unix.KeyctlSetperm(id, keyPerm); err != nil {
|
||||
return nil, fmt.Errorf("keyring: setperm %q: %w", k.desc, err)
|
||||
}
|
||||
if _, err := unix.KeyctlInt(unix.KEYCTL_MOVE, id, unix.KEY_SPEC_THREAD_KEYRING, unix.KEY_SPEC_USER_KEYRING, unix.KEYCTL_MOVE_EXCL); err != nil {
|
||||
if errors.Is(err, unix.EEXIST) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("keyring: move %q: %w", k.desc, err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// purgeDead unlinks every revoked or expired key in the user keyring: a dead key
|
||||
// cannot be described, so ours cannot be singled out. Call it only after the
|
||||
// search hit a dead key of our description.
|
||||
func purgeDead() {
|
||||
buf := make([]byte, 256)
|
||||
for {
|
||||
n, err := unix.KeyctlBuffer(unix.KEYCTL_READ, unix.KEY_SPEC_USER_KEYRING, buf, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n <= len(buf) {
|
||||
buf = buf[:n]
|
||||
break
|
||||
}
|
||||
buf = make([]byte, n)
|
||||
}
|
||||
for ; len(buf) >= 4; buf = buf[4:] {
|
||||
id := int(int32(binary.NativeEndian.Uint32(buf)))
|
||||
if _, err := unix.KeyctlString(unix.KEYCTL_DESCRIBE, id); missing(err) {
|
||||
unlink(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unlink(id int) {
|
||||
_, _ = unix.KeyctlInt(unix.KEYCTL_UNLINK, id, unix.KEY_SPEC_USER_KEYRING, 0, 0)
|
||||
}
|
||||
|
||||
func missing(err error) bool {
|
||||
return errors.Is(err, unix.ENOKEY) || errors.Is(err, unix.EKEYREVOKED) || errors.Is(err, unix.EKEYEXPIRED)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package keyring
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// open opens a test-unique keyring, skipping outside CI where keyctl is
|
||||
// unimplemented. Permission errors always fail: they are bugs, not absence.
|
||||
func open(t *testing.T) *Keyring {
|
||||
t.Helper()
|
||||
k, err := Open(fmt.Sprintf("golib-test:%s:%d", t.Name(), os.Getpid()))
|
||||
if errors.Is(err, unix.ENOSYS) && os.Getenv("CI") == "" {
|
||||
t.Skipf("kernel keyring unavailable: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = k.Forget() })
|
||||
return k
|
||||
}
|
||||
|
||||
func TestKernelRoundTrip(t *testing.T) {
|
||||
k := open(t)
|
||||
sealed, err := k.Seal([]byte("hvs.token"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
k2, err := Open(k.desc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := k2.Unseal(sealed)
|
||||
if err != nil || string(got) != "hvs.token" {
|
||||
t.Fatalf("Unseal = %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKernelKeyPermissions(t *testing.T) {
|
||||
k := open(t)
|
||||
id, err := unix.KeyctlSearch(unix.KEY_SPEC_USER_KEYRING, "user", k.desc, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
desc, err := unix.KeyctlString(unix.KEYCTL_DESCRIBE, id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f := strings.Split(desc, ";"); len(f) < 4 || f[3] != "3f2f0000" {
|
||||
t.Fatalf("describe = %q, want perm 3f2f0000", desc)
|
||||
}
|
||||
}
|
||||
|
||||
// A process whose session keyring does not link the user keyring (a systemd
|
||||
// user service, a fresh `keyctl session`) must still read the key.
|
||||
func TestKernelUnsealWithoutPossession(t *testing.T) {
|
||||
k := open(t)
|
||||
sealed, err := k.Seal([]byte("secret"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
done := make(chan error)
|
||||
go func() {
|
||||
runtime.LockOSThread() // never unlocked: the thread exits with its new session
|
||||
if _, err := unix.KeyctlInt(unix.KEYCTL_JOIN_SESSION_KEYRING, 0, 0, 0, 0); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
got, err := k.Unseal(sealed)
|
||||
if err == nil && !bytes.Equal(got, []byte("secret")) {
|
||||
err = fmt.Errorf("got %q", got)
|
||||
}
|
||||
done <- err
|
||||
}()
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKernelForget(t *testing.T) {
|
||||
k := open(t)
|
||||
sealed, err := k.Seal([]byte("secret"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := k.Forget(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := k.Unseal(sealed); !errors.Is(err, ErrKeyUnavailable) {
|
||||
t.Fatalf("Unseal after Forget: err = %v, want ErrKeyUnavailable", err)
|
||||
}
|
||||
if err := k.Forget(); err != nil {
|
||||
t.Fatalf("second Forget: %v", err)
|
||||
}
|
||||
resealed, err := k.Seal([]byte("again"))
|
||||
if err != nil {
|
||||
t.Fatalf("Seal after Forget: %v", err)
|
||||
}
|
||||
if _, err := k.Unseal(sealed); !errors.Is(err, ErrKeyUnavailable) {
|
||||
t.Fatalf("old data under new key: err = %v, want ErrKeyUnavailable", err)
|
||||
}
|
||||
if got, err := k.Unseal(resealed); err != nil || string(got) != "again" {
|
||||
t.Fatalf("Unseal = %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKernelWrongSizedKeyIsReplaced(t *testing.T) {
|
||||
k := open(t)
|
||||
sealed, err := k.Seal([]byte("secret"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := unix.AddKey("user", k.desc, []byte("short"), unix.KEY_SPEC_USER_KEYRING); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := k.Unseal(sealed); !errors.Is(err, ErrKeyUnavailable) {
|
||||
t.Fatalf("err = %v, want ErrKeyUnavailable", err)
|
||||
}
|
||||
if _, err := Open(k.desc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := k.lookup(); err != nil {
|
||||
t.Fatalf("key not replaced: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Racing first users must all end up sealing under the one key that stays linked.
|
||||
func TestKernelConcurrentFirstSeal(t *testing.T) {
|
||||
open(t) // skips where keyctl is unavailable
|
||||
const rounds, workers = 20, 8
|
||||
for r := range rounds {
|
||||
desc := fmt.Sprintf("golib-test:%s:%d:%d", t.Name(), os.Getpid(), r)
|
||||
sealed := make([][]byte, workers)
|
||||
errs := make([]error, workers)
|
||||
var wg sync.WaitGroup
|
||||
for w := range workers {
|
||||
wg.Go(func() {
|
||||
k, err := Open(desc)
|
||||
if err == nil {
|
||||
sealed[w], err = k.Seal([]byte("secret"))
|
||||
}
|
||||
errs[w] = err
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
k := &Keyring{desc: desc}
|
||||
t.Cleanup(func() { _ = k.Forget() })
|
||||
for w := range workers {
|
||||
if errs[w] != nil {
|
||||
t.Fatalf("round %d worker %d: %v", r, w, errs[w])
|
||||
}
|
||||
if got, err := k.Unseal(sealed[w]); err != nil || string(got) != "secret" {
|
||||
t.Fatalf("round %d worker %d: Unseal = %q, %v", r, w, got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A key revoked behind our back still blocks an exclusive link until unlinked.
|
||||
func TestKernelExternallyRevokedKeyIsReplaced(t *testing.T) {
|
||||
k := open(t)
|
||||
id, _, err := k.lookup()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := unix.KeyctlInt(unix.KEYCTL_REVOKE, id, 0, 0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sealed, err := k.Seal([]byte("secret"))
|
||||
if err != nil {
|
||||
t.Fatalf("Seal after external revoke: %v", err)
|
||||
}
|
||||
if got, err := k.Unseal(sealed); err != nil || string(got) != "secret" {
|
||||
t.Fatalf("Unseal = %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Another application's dead key in the user keyring is not ours to unlink.
|
||||
func TestKernelForeignDeadKeySurvivesOpen(t *testing.T) {
|
||||
open(t) // skips where keyctl is unavailable
|
||||
foreign := &Keyring{desc: fmt.Sprintf("golib-test-foreign:%s:%d", t.Name(), os.Getpid())}
|
||||
if _, err := foreign.create(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id, _, err := foreign.lookup()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { unlink(id) })
|
||||
if _, err := unix.KeyctlInt(unix.KEYCTL_REVOKE, id, 0, 0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
k, err := Open(fmt.Sprintf("golib-test:%s:%d:fresh", t.Name(), os.Getpid()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = k.Forget() })
|
||||
// Unlinking an id that is no longer linked fails with ENOENT.
|
||||
if _, err := unix.KeyctlInt(unix.KEYCTL_UNLINK, id, unix.KEY_SPEC_USER_KEYRING, 0, 0); err != nil {
|
||||
t.Fatalf("foreign dead key was unlinked: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenEmptyDescription(t *testing.T) {
|
||||
if _, err := Open(""); err == nil {
|
||||
t.Fatal("Open(\"\") succeeded")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build !linux
|
||||
|
||||
package keyring
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var errUnsupported = fmt.Errorf("keyring: kernel keyring requires linux: %w", errors.ErrUnsupported)
|
||||
|
||||
// Keyring is a stub: the kernel key retention service is Linux-only.
|
||||
type Keyring struct{}
|
||||
|
||||
// Open always fails: the kernel key retention service is Linux-only.
|
||||
func Open(string) (*Keyring, error) { return nil, errUnsupported }
|
||||
|
||||
// Seal always fails on this platform.
|
||||
func (*Keyring) Seal([]byte) ([]byte, error) { return nil, errUnsupported }
|
||||
|
||||
// Unseal always fails on this platform.
|
||||
func (*Keyring) Unseal([]byte) ([]byte, error) { return nil, errUnsupported }
|
||||
|
||||
// Forget always fails on this platform.
|
||||
func (*Keyring) Forget() error { return errUnsupported }
|
||||
@@ -0,0 +1,90 @@
|
||||
package keyring
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testKey(b byte) []byte { return bytes.Repeat([]byte{b}, keySize) }
|
||||
|
||||
func TestSealUnsealRoundTrip(t *testing.T) {
|
||||
for _, pt := range [][]byte{nil, []byte("hvs.token"), bytes.Repeat([]byte("x"), 4096)} {
|
||||
sealed, err := seal(testKey(1), pt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := unseal(testKey(1), sealed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, pt) {
|
||||
t.Fatalf("got %q, want %q", got, pt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSealUsesFreshNonce(t *testing.T) {
|
||||
a, _ := seal(testKey(1), []byte("same"))
|
||||
b, _ := seal(testKey(1), []byte("same"))
|
||||
if bytes.Equal(a, b) {
|
||||
t.Fatal("two seals of the same plaintext are identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsealWrongKey(t *testing.T) {
|
||||
sealed, _ := seal(testKey(1), []byte("secret"))
|
||||
if _, err := unseal(testKey(2), sealed); !errors.Is(err, ErrKeyUnavailable) {
|
||||
t.Fatalf("err = %v, want ErrKeyUnavailable", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsealTampered(t *testing.T) {
|
||||
sealed, _ := seal(testKey(1), []byte("secret"))
|
||||
cases := map[string]func([]byte) []byte{
|
||||
"ciphertext": func(b []byte) []byte { b[len(b)-1] ^= 1; return b },
|
||||
"nonce": func(b []byte) []byte { b[1+fpSize] ^= 1; return b },
|
||||
"version": func(b []byte) []byte { b[0] = 2; return b },
|
||||
"truncated": func(b []byte) []byte { return b[:headerLen-1] },
|
||||
"empty": func([]byte) []byte { return nil },
|
||||
}
|
||||
for name, mutate := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
b := mutate(bytes.Clone(sealed))
|
||||
if _, err := unseal(testKey(1), b); !errors.Is(err, ErrCorrupt) {
|
||||
t.Fatalf("err = %v, want ErrCorrupt", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadKeyLength(t *testing.T) {
|
||||
if _, err := seal([]byte("short"), []byte("x")); !errors.Is(err, ErrKeyUnavailable) {
|
||||
t.Fatalf("seal err = %v, want ErrKeyUnavailable", err)
|
||||
}
|
||||
short := []byte("short")
|
||||
sealed, _ := seal(testKey(1), []byte("x"))
|
||||
copy(sealed[1:], fingerprint(short))
|
||||
if _, err := unseal(short, sealed); !errors.Is(err, ErrKeyUnavailable) {
|
||||
t.Fatalf("unseal err = %v, want ErrKeyUnavailable", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The header must be bound as additional data: a valid GCM ciphertext under a
|
||||
// genuine header but without that binding is rejected.
|
||||
func TestUnsealRequiresHeaderAAD(t *testing.T) {
|
||||
key := testKey(1)
|
||||
gcm, err := newGCM(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
env := append([]byte{version1}, fingerprint(key)...)
|
||||
nonce := make([]byte, nonceSize)
|
||||
_, _ = rand.Read(nonce)
|
||||
env = append(env, nonce...)
|
||||
env = gcm.Seal(env, nonce, []byte("secret"), nil)
|
||||
if _, err := unseal(key, env); !errors.Is(err, ErrCorrupt) {
|
||||
t.Fatalf("err = %v, want ErrCorrupt", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user