mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
164 lines
5.3 KiB
Go
164 lines
5.3 KiB
Go
package certposture
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/x509"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/shared/management/certposture/certtest"
|
|
)
|
|
|
|
var (
|
|
secret = []byte("test-secret")
|
|
peerKey = []byte("peer-public-key-aaaaaaaaaaaaaaaa")
|
|
otherKey = []byte("peer-public-key-bbbbbbbbbbbbbbbb")
|
|
now = time.Now().Truncate(Window)
|
|
)
|
|
|
|
func TestChallenger_NonceIsStableWithinWindowAndPerPeer(t *testing.T) {
|
|
c := NewChallenger(secret)
|
|
|
|
assert.Equal(t, c.Nonce(peerKey, now), c.Nonce(peerKey, now.Add(time.Minute)))
|
|
assert.NotEqual(t, c.Nonce(peerKey, now), c.Nonce(peerKey, now.Add(Window)))
|
|
assert.NotEqual(t, c.Nonce(peerKey, now), c.Nonce(otherKey, now))
|
|
assert.NotEqual(t, c.Nonce(peerKey, now), NewChallenger([]byte("other")).Nonce(peerKey, now))
|
|
}
|
|
|
|
func TestChallenger_VerifyNonce(t *testing.T) {
|
|
c := NewChallenger(secret)
|
|
nonce := c.Nonce(peerKey, now)
|
|
|
|
tests := []struct {
|
|
name string
|
|
nonce []byte
|
|
peerKey []byte
|
|
at time.Time
|
|
wantErr error
|
|
}{
|
|
{"current window", nonce, peerKey, now, nil},
|
|
{"previous window still accepted", nonce, peerKey, now.Add(Window), nil},
|
|
{"two windows later expired", nonce, peerKey, now.Add(2 * Window), ErrNonceExpired},
|
|
{"issued in the future rejected", c.Nonce(peerKey, now.Add(Window)), peerKey, now, ErrNonceExpired},
|
|
{"other peer", nonce, otherKey, now, ErrNonceMismatch},
|
|
{"tampered mac", tamper(nonce, len(nonce)-1), peerKey, now, ErrNonceMismatch},
|
|
{"malformed", nonce[:10], peerKey, now, ErrNonceMalformed},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := c.verifyNonce(tt.nonce, tt.peerKey, tt.at)
|
|
assert.ErrorIs(t, err, tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSignVerify_RoundTripPerKeyType(t *testing.T) {
|
|
ca := certtest.NewCA(t, "root")
|
|
keys := map[string]crypto.Signer{
|
|
SigAlgECDSASHA256: certtest.ECDSAKey(t),
|
|
SigAlgRSAPSSSHA256: certtest.RSAKey(t),
|
|
SigAlgEd25519: certtest.Ed25519Key(t),
|
|
}
|
|
for wantAlg, key := range keys {
|
|
t.Run(wantAlg, func(t *testing.T) {
|
|
c := NewChallenger(secret)
|
|
proof := signedProof(t, c, ca, key)
|
|
assert.Equal(t, wantAlg, proof.SigAlg)
|
|
|
|
chain, err := c.Verify(proof, peerKey, now)
|
|
require.NoError(t, err)
|
|
require.Len(t, chain, 1)
|
|
assert.NoError(t, VerifyChain(chain, mustPool(t, ca.PEM), now))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerify_Rejections(t *testing.T) {
|
|
ca := certtest.NewCA(t, "root")
|
|
c := NewChallenger(secret)
|
|
good := signedProof(t, c, ca, certtest.ECDSAKey(t))
|
|
|
|
tests := []struct {
|
|
name string
|
|
mutate func(p Proof) Proof
|
|
peerKey []byte
|
|
wantErr error
|
|
}{
|
|
{"replayed for other peer", identity, otherKey, ErrNonceMismatch},
|
|
{"signature tampered", func(p Proof) Proof { p.Signature = tamper(p.Signature, 5); return p }, peerKey, ErrSignatureInvalid},
|
|
{"nonce swapped after signing", func(p Proof) Proof { p.Nonce = c.Nonce(peerKey, now.Add(-Window)); return p }, peerKey, ErrSignatureInvalid},
|
|
{"foreign leaf presented", func(p Proof) Proof {
|
|
p.Chain = [][]byte{ca.Issue(t, certtest.ECDSAKey(t), "other").Raw}
|
|
return p
|
|
}, peerKey, ErrSignatureInvalid},
|
|
{"alg mismatch", func(p Proof) Proof { p.SigAlg = SigAlgEd25519; return p }, peerKey, ErrSigAlgMismatch},
|
|
{"empty chain", func(p Proof) Proof { p.Chain = nil; return p }, peerKey, ErrEmptyChain},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := c.Verify(tt.mutate(good), tt.peerKey, now)
|
|
assert.ErrorIs(t, err, tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerify_SecretMismatchAcrossChallengers(t *testing.T) {
|
|
ca := certtest.NewCA(t, "root")
|
|
proof := signedProof(t, NewChallenger(secret), ca, certtest.ECDSAKey(t))
|
|
|
|
_, err := NewChallenger([]byte("other-instance-secret")).Verify(proof, peerKey, now)
|
|
assert.ErrorIs(t, err, ErrNonceMismatch)
|
|
}
|
|
|
|
func TestChainMatchesCAs(t *testing.T) {
|
|
ca := certtest.NewCA(t, "root")
|
|
otherCA := certtest.NewCA(t, "other-root")
|
|
leaf := ca.Issue(t, certtest.ECDSAKey(t), "device")
|
|
chainPEM := EncodeChainPEM([]*x509.Certificate{leaf})
|
|
|
|
assert.True(t, ChainMatchesCAs(chainPEM, []string{ca.PEM}, now))
|
|
assert.True(t, ChainMatchesCAs(chainPEM, []string{otherCA.PEM, ca.PEM}, now))
|
|
assert.False(t, ChainMatchesCAs(chainPEM, []string{otherCA.PEM}, now))
|
|
assert.False(t, ChainMatchesCAs(chainPEM, []string{ca.PEM}, now.Add(30*24*time.Hour)))
|
|
assert.False(t, ChainMatchesCAs(chainPEM, []string{"not a pem"}, now))
|
|
assert.False(t, ChainMatchesCAs("not a pem", []string{ca.PEM}, now))
|
|
}
|
|
|
|
func TestChainPEM_RoundTrip(t *testing.T) {
|
|
ca := certtest.NewCA(t, "root")
|
|
leaf := ca.Issue(t, certtest.ECDSAKey(t), "device")
|
|
|
|
chain, err := ParseChainPEM(EncodeChainPEM([]*x509.Certificate{leaf, ca.Cert}))
|
|
require.NoError(t, err)
|
|
require.Len(t, chain, 2)
|
|
assert.Equal(t, leaf.Raw, chain[0].Raw)
|
|
assert.Equal(t, ca.Cert.Raw, chain[1].Raw)
|
|
}
|
|
|
|
func signedProof(t *testing.T, c *Challenger, ca *certtest.CA, key crypto.Signer) Proof {
|
|
t.Helper()
|
|
leaf := ca.Issue(t, key, "device")
|
|
nonce := c.Nonce(peerKey, now)
|
|
sigAlg, sig, err := Sign(key, nonce, peerKey)
|
|
require.NoError(t, err)
|
|
return Proof{Nonce: nonce, Chain: [][]byte{leaf.Raw}, SigAlg: sigAlg, Signature: sig}
|
|
}
|
|
|
|
func mustPool(t *testing.T, pems ...string) *x509.CertPool {
|
|
t.Helper()
|
|
pool, err := ParseCAs(pems)
|
|
require.NoError(t, err)
|
|
return pool
|
|
}
|
|
|
|
func identity(p Proof) Proof { return p }
|
|
|
|
func tamper(b []byte, i int) []byte {
|
|
out := append([]byte(nil), b...)
|
|
out[i] ^= 0xff
|
|
return out
|
|
}
|