mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-11 21:27:31 +00:00
all: implement lock revoke-keys command
The revoke-keys command allows nodes with tailnet lock keys to collaborate to erase the use of a compromised key, and remove trust in it. Signed-off-by: Tom DNetto <tom@tailscale.com> Updates ENG-1848
This commit is contained in:
@@ -845,6 +845,93 @@ func (b *LocalBackend) NetworkLockAffectedSigs(keyID tkatype.KeyID) ([]tkatype.M
|
||||
return resp.Signatures, nil
|
||||
}
|
||||
|
||||
// NetworkLockGenerateRecoveryAUM generates an AUM which retroactively removes trust in the
|
||||
// specified keys. This AUM is signed by the current node and returned.
|
||||
//
|
||||
// If forkFrom is specified, it is used as the parent AUM to fork from. If the zero value,
|
||||
// the parent AUM is determined automatically.
|
||||
func (b *LocalBackend) NetworkLockGenerateRecoveryAUM(removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) (*tka.AUM, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
return nil, errNetworkLockNotActive
|
||||
}
|
||||
var nlPriv key.NLPrivate
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() {
|
||||
nlPriv = p.Persist().NetworkLockKey()
|
||||
}
|
||||
if nlPriv.IsZero() {
|
||||
return nil, errMissingNetmap
|
||||
}
|
||||
|
||||
aum, err := b.tka.authority.MakeRetroactiveRevocation(b.tka.storage, removeKeys, nlPriv.KeyID(), forkFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sign it ourselves.
|
||||
aum.Signatures, err = nlPriv.SignAUM(aum.SigHash())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signing failed: %w", err)
|
||||
}
|
||||
|
||||
return aum, nil
|
||||
}
|
||||
|
||||
// NetworkLockCosignRecoveryAUM co-signs the provided recovery AUM and returns
|
||||
// the updated structure.
|
||||
//
|
||||
// The recovery AUM provided should be the output from a previous call to
|
||||
// NetworkLockGenerateRecoveryAUM or NetworkLockCosignRecoveryAUM.
|
||||
func (b *LocalBackend) NetworkLockCosignRecoveryAUM(aum *tka.AUM) (*tka.AUM, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
return nil, errNetworkLockNotActive
|
||||
}
|
||||
var nlPriv key.NLPrivate
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() {
|
||||
nlPriv = p.Persist().NetworkLockKey()
|
||||
}
|
||||
if nlPriv.IsZero() {
|
||||
return nil, errMissingNetmap
|
||||
}
|
||||
for _, sig := range aum.Signatures {
|
||||
if bytes.Equal(sig.KeyID, nlPriv.KeyID()) {
|
||||
return nil, errors.New("this node has already signed this recovery AUM")
|
||||
}
|
||||
}
|
||||
|
||||
// Sign it ourselves.
|
||||
sigs, err := nlPriv.SignAUM(aum.SigHash())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signing failed: %w", err)
|
||||
}
|
||||
aum.Signatures = append(aum.Signatures, sigs...)
|
||||
|
||||
return aum, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) NetworkLockSubmitRecoveryAUM(aum *tka.AUM) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
return errNetworkLockNotActive
|
||||
}
|
||||
var ourNodeKey key.NodePublic
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
|
||||
ourNodeKey = p.Persist().PublicNodeKey()
|
||||
}
|
||||
if ourNodeKey.IsZero() {
|
||||
return errors.New("no node-key: is tailscale logged in?")
|
||||
}
|
||||
|
||||
b.mu.Unlock()
|
||||
_, err := b.tkaDoSyncSend(ourNodeKey, aum.Hash(), []tka.AUM{*aum}, false)
|
||||
b.mu.Lock()
|
||||
return err
|
||||
}
|
||||
|
||||
var tkaSuffixEncoder = base64.RawStdEncoding
|
||||
|
||||
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
|
||||
|
@@ -994,3 +994,129 @@ func TestTKAAffectedSigs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
|
||||
nodePriv := key.NewNode()
|
||||
nlPriv := key.NewNLPrivate()
|
||||
cosignPriv := key.NewNLPrivate()
|
||||
compromisedPriv := 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)
|
||||
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||||
cosignKey := tka.Key{Kind: tka.Key25519, Public: cosignPriv.Public().Verifier(), Votes: 2}
|
||||
compromisedKey := tka.Key{Kind: tka.Key25519, Public: compromisedPriv.Public().Verifier(), Votes: 1}
|
||||
|
||||
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{key, compromisedKey, cosignKey},
|
||||
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
|
||||
}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
|
||||
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
switch r.URL.Path {
|
||||
case "/machine/tka/sync/send":
|
||||
body := new(tailcfg.TKASyncSendRequest)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("got sync send:\n%+v", body)
|
||||
|
||||
var remoteHead tka.AUMHash
|
||||
if err := remoteHead.UnmarshalText([]byte(body.Head)); err != nil {
|
||||
t.Fatalf("head unmarshal: %v", err)
|
||||
}
|
||||
toApply := make([]tka.AUM, len(body.MissingAUMs))
|
||||
for i, a := range body.MissingAUMs {
|
||||
if err := toApply[i].Unserialize(a); err != nil {
|
||||
t.Fatalf("decoding missingAUM[%d]: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the recovery AUM to an authority to make sure it works.
|
||||
if err := authority.Inform(chonk, toApply); err != nil {
|
||||
t.Errorf("recovery AUM could not be applied: %v", err)
|
||||
}
|
||||
// Make sure the key we removed isn't trusted.
|
||||
if authority.KeyTrusted(compromisedPriv.KeyID()) {
|
||||
t.Error("compromised key was not removed from tka")
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
if err := json.NewEncoder(w).Encode(tailcfg.TKASubmitSignatureResponse{}); 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(),
|
||||
}
|
||||
|
||||
aum, err := b.NetworkLockGenerateRecoveryAUM([]tkatype.KeyID{compromisedPriv.KeyID()}, tka.AUMHash{})
|
||||
if err != nil {
|
||||
t.Fatalf("NetworkLockGenerateRecoveryAUM() failed: %v", err)
|
||||
}
|
||||
|
||||
// Cosign using the cosigning key.
|
||||
{
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
NetworkLockKey: cosignPriv,
|
||||
},
|
||||
}).View()))
|
||||
b := LocalBackend{
|
||||
varRoot: temp,
|
||||
logf: t.Logf,
|
||||
tka: &tkaState{
|
||||
authority: authority,
|
||||
storage: chonk,
|
||||
},
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
}
|
||||
if aum, err = b.NetworkLockCosignRecoveryAUM(aum); err != nil {
|
||||
t.Fatalf("NetworkLockCosignRecoveryAUM() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, submit the recovery AUM. Validation is done
|
||||
// in the fake control handler.
|
||||
if err := b.NetworkLockSubmitRecoveryAUM(aum); err != nil {
|
||||
t.Errorf("NetworkLockSubmitRecoveryAUM() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user