ipn,cmd/tailscale: implement resigning nodes on tka key removal

Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
Tom DNetto
2023-03-01 12:47:29 -08:00
committed by Tom
parent 3f8e8b04fd
commit e2d652ec4d
5 changed files with 332 additions and 4 deletions
+133
View File
@@ -7,6 +7,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
@@ -877,3 +878,135 @@ func TestTKAForceDisable(t *testing.T) {
t.Fatal("tka was re-initalized")
}
}
func TestTKAAffectedSigs(t *testing.T) {
nodePriv := key.NewNode()
// toSign := key.NewNode()
nlPriv := key.NewNLPrivate()
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
must.Do(pm.SetPrefs((&ipn.Prefs{
Persist: &persist.Persist{
PrivateNodeKey: nodePriv,
NetworkLockKey: nlPriv,
},
}).View()))
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
tkaKey := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
temp := t.TempDir()
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
os.Mkdir(tkaPath, 0755)
chonk, err := tka.ChonkDir(tkaPath)
if err != nil {
t.Fatal(err)
}
authority, _, err := tka.Create(chonk, tka.State{
Keys: []tka.Key{tkaKey},
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
}, nlPriv)
if err != nil {
t.Fatalf("tka.Create() failed: %v", err)
}
untrustedKey := key.NewNLPrivate()
tcs := []struct {
name string
makeSig func() *tka.NodeKeySignature
wantErr string
}{
{
"no error",
func() *tka.NodeKeySignature {
sig, _ := signNodeKey(tailcfg.TKASignInfo{NodePublic: nodePriv.Public()}, nlPriv)
return sig
},
"",
},
{
"signature for different keyID",
func() *tka.NodeKeySignature {
sig, _ := signNodeKey(tailcfg.TKASignInfo{NodePublic: nodePriv.Public()}, untrustedKey)
return sig
},
fmt.Sprintf("got signature with keyID %X from request for %X", untrustedKey.KeyID(), nlPriv.KeyID()),
},
{
"invalid signature",
func() *tka.NodeKeySignature {
sig, _ := signNodeKey(tailcfg.TKASignInfo{NodePublic: nodePriv.Public()}, nlPriv)
copy(sig.Signature, []byte{1, 2, 3, 4, 5, 6}) // overwrite with trash to invalid signature
return sig
},
"signature 0 is not valid: invalid signature",
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
s := tc.makeSig()
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
switch r.URL.Path {
case "/machine/tka/affected-sigs":
body := new(tailcfg.TKASignaturesUsingKeyRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
if body.Version != tailcfg.CurrentCapabilityVersion {
t.Errorf("sign CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
}
if body.NodeKey != nodePriv.Public() {
t.Errorf("nodeKey = %v, want %v", body.NodeKey, nodePriv.Public())
}
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(tailcfg.TKASignaturesUsingKeyResponse{
Signatures: []tkatype.MarshaledSignature{s.Serialize()},
}); err != nil {
t.Fatal(err)
}
default:
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
w.WriteHeader(404)
}
}))
defer ts.Close()
cc := fakeControlClient(t, client)
b := LocalBackend{
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
tka: &tkaState{
authority: authority,
storage: chonk,
},
pm: pm,
store: pm.Store(),
}
sigs, err := b.NetworkLockAffectedSigs(nlPriv.KeyID())
switch {
case tc.wantErr == "" && err != nil:
t.Errorf("NetworkLockAffectedSigs() failed: %v", err)
case tc.wantErr != "" && err == nil:
t.Errorf("NetworkLockAffectedSigs().err = nil, want %q", tc.wantErr)
case tc.wantErr != "" && err.Error() != tc.wantErr:
t.Errorf("NetworkLockAffectedSigs().err = %q, want %q", err.Error(), tc.wantErr)
}
if tc.wantErr == "" {
if len(sigs) != 1 {
t.Fatalf("len(sigs) = %d, want 1", len(sigs))
}
if !bytes.Equal(s.Serialize(), sigs[0]) {
t.Errorf("unexpected signature: got %v, want %v", sigs[0], s.Serialize())
}
}
})
}
}