mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-19 11:59:09 +02:00
137 lines
4.3 KiB
Go
137 lines
4.3 KiB
Go
//go:build unix
|
|
|
|
package certproof
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/shared/management/certposture/certtest"
|
|
)
|
|
|
|
// devicePair writes a certificate and its key as separate files into dir.
|
|
func devicePair(t *testing.T, dir string) {
|
|
t.Helper()
|
|
ca := certtest.NewCA(t, "corp-root")
|
|
key := certtest.ECDSAKey(t)
|
|
writeFile(t, dir, "device.crt", certtest.CertPEM(ca.Issue(t, key, "device")))
|
|
writeFile(t, dir, "device.key", certtest.KeyPEM(t, key))
|
|
}
|
|
|
|
func candidates(t *testing.T, dir string) ([]Candidate, error) {
|
|
t.Helper()
|
|
return newFileStore(dir).Candidates(context.Background())
|
|
}
|
|
|
|
func TestFileStore_AcceptsAPrivatePair(t *testing.T) {
|
|
dir := storeDir(t)
|
|
devicePair(t, dir)
|
|
|
|
got, err := candidates(t, dir)
|
|
require.NoError(t, err)
|
|
assert.Len(t, got, 1, "a certificate and key only their owner can write are used")
|
|
}
|
|
|
|
func TestFileStore_RefusesASymlinkedKey(t *testing.T) {
|
|
dir := storeDir(t)
|
|
devicePair(t, dir)
|
|
|
|
// The real key moves elsewhere and the directory only links to it, as a user able
|
|
// to write the directory would do to point the daemon at a key it should not use.
|
|
elsewhere := filepath.Join(storeDir(t), "other.key")
|
|
require.NoError(t, os.Rename(filepath.Join(dir, "device.key"), elsewhere))
|
|
require.NoError(t, os.Symlink(elsewhere, filepath.Join(dir, "device.key")))
|
|
|
|
got, err := candidates(t, dir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got, "a key reached through a symlink is never used")
|
|
}
|
|
|
|
func TestFileStore_RefusesASymlinkedCertificateFile(t *testing.T) {
|
|
dir := storeDir(t)
|
|
other := storeDir(t)
|
|
devicePair(t, other)
|
|
require.NoError(t, os.Symlink(filepath.Join(other, "device.crt"), filepath.Join(dir, "device.crt")))
|
|
require.NoError(t, os.Symlink(filepath.Join(other, "device.key"), filepath.Join(dir, "device.key")))
|
|
|
|
got, err := candidates(t, dir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got, "a certificate file that is a symlink is skipped")
|
|
}
|
|
|
|
func TestFileStore_RefusesFilesOthersCanWrite(t *testing.T) {
|
|
for name, mode := range map[string]os.FileMode{"world-writable": 0o602, "group-writable": 0o620} {
|
|
t.Run(name, func(t *testing.T) {
|
|
dir := storeDir(t)
|
|
devicePair(t, dir)
|
|
require.NoError(t, os.Chmod(filepath.Join(dir, "device.key"), mode))
|
|
|
|
got, err := candidates(t, dir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got, "a key %s may have been replaced by someone else, even when the group is root's", name)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFileStore_RefusesADirectoryOthersCanWrite(t *testing.T) {
|
|
dir := storeDir(t)
|
|
devicePair(t, dir)
|
|
require.NoError(t, os.Chmod(dir, 0o777))
|
|
|
|
_, err := candidates(t, dir)
|
|
assert.ErrorContains(t, err, "refusing certificate store", "files in a directory anyone can write are not trusted")
|
|
}
|
|
|
|
func TestFileStore_SkipsAFIFOWithoutBlocking(t *testing.T) {
|
|
dir := storeDir(t)
|
|
require.NoError(t, syscall.Mkfifo(filepath.Join(dir, "trap.pem"), 0o600))
|
|
devicePair(t, dir)
|
|
|
|
got, err := candidates(t, dir)
|
|
require.NoError(t, err)
|
|
assert.Len(t, got, 1, "a FIFO is skipped and the real pair still loads")
|
|
}
|
|
|
|
func TestReadStoreFile_RefusesOversizedFiles(t *testing.T) {
|
|
dir := storeDir(t)
|
|
path := filepath.Join(dir, "huge.pem")
|
|
require.NoError(t, os.WriteFile(path, make([]byte, maxStoreFileSize+1), 0o600))
|
|
|
|
_, err := readStoreFile(path)
|
|
assert.ErrorContains(t, err, "limit")
|
|
}
|
|
|
|
func TestFileStore_AcceptsASymlinkedDirectoryItsOwnerControls(t *testing.T) {
|
|
target := storeDir(t)
|
|
devicePair(t, target)
|
|
link := filepath.Join(storeDir(t), "certs")
|
|
require.NoError(t, os.Symlink(target, link))
|
|
|
|
// The link belongs to this process's user, as one root creates belongs to root.
|
|
got, err := candidates(t, link)
|
|
require.NoError(t, err)
|
|
assert.Len(t, got, 1, "a certificate directory behind a symlink the owner controls is read")
|
|
}
|
|
|
|
func TestFileStore_ChecksTheDirectoryBeforeListingIt(t *testing.T) {
|
|
dir := storeDir(t)
|
|
devicePair(t, dir)
|
|
require.NoError(t, os.Chmod(dir, 0o777))
|
|
|
|
paths, err := certFiles(dir)
|
|
assert.ErrorContains(t, err, "refusing certificate store")
|
|
assert.Nil(t, paths, "nothing in a directory anyone can write to is listed")
|
|
}
|
|
|
|
func TestFileStore_MissingDirectoryIsEmpty(t *testing.T) {
|
|
paths, err := certFiles(filepath.Join(storeDir(t), "missing"))
|
|
assert.NoError(t, err, "a store directory that does not exist yet is not an error")
|
|
assert.Empty(t, paths)
|
|
}
|