mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-25 22:09:08 +01:00
implement certificate posture check
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user