mirror of https://github.com/tailscale/tailscale/
types/key,wgengine/magicsock,control/controlclient,ipn: add debug disco key rotation
Adds the ability to rotate discovery keys on running clients, needed for testing upcoming disco key distribution changes. Introduces key.DiscoKey, an atomic container for a disco private key, public key, and the public key's ShortString, replacing the prior separate atomic fields. magicsock.Conn has a new RotateDiscoKey method, and access to this is provided via localapi and a CLI debug command. Note that this implementation is primarily for testing as it stands, and regular use should likely introduce an additional mechanism that allows the old key to be used for some time, to provide a seamless key rotation rather than one that invalidates all sessions. Updates tailscale/corp#34037 Signed-off-by: James Tucker <james@tailscale.com>pull/17730/merge
parent
da508c504d
commit
c09c95ef67
@ -0,0 +1,58 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package magicsock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
type discoKeyPair struct {
|
||||||
|
private key.DiscoPrivate
|
||||||
|
public key.DiscoPublic
|
||||||
|
short string // public.ShortString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// discoAtomic is an atomic container for a disco private key, public key, and
|
||||||
|
// the public key's ShortString. The private and public keys are always kept
|
||||||
|
// synchronized.
|
||||||
|
//
|
||||||
|
// The zero value is not ready for use. Use [Set] to provide a usable value.
|
||||||
|
type discoAtomic struct {
|
||||||
|
pair atomic.Pointer[discoKeyPair]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pair returns the private and public keys together atomically.
|
||||||
|
// Code that needs both the private and public keys synchronized should
|
||||||
|
// use Pair instead of calling Private and Public separately.
|
||||||
|
func (dk *discoAtomic) Pair() (key.DiscoPrivate, key.DiscoPublic) {
|
||||||
|
p := dk.pair.Load()
|
||||||
|
return p.private, p.public
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private returns the private key.
|
||||||
|
func (dk *discoAtomic) Private() key.DiscoPrivate {
|
||||||
|
return dk.pair.Load().private
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public returns the public key.
|
||||||
|
func (dk *discoAtomic) Public() key.DiscoPublic {
|
||||||
|
return dk.pair.Load().public
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short returns the short string of the public key (see [DiscoPublic.ShortString]).
|
||||||
|
func (dk *discoAtomic) Short() string {
|
||||||
|
return dk.pair.Load().short
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set updates the private key (and the cached public key and short string).
|
||||||
|
func (dk *discoAtomic) Set(private key.DiscoPrivate) {
|
||||||
|
public := private.Public()
|
||||||
|
dk.pair.Store(&discoKeyPair{
|
||||||
|
private: private,
|
||||||
|
public: public,
|
||||||
|
short: public.ShortString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package magicsock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDiscoAtomic(t *testing.T) {
|
||||||
|
var dk discoAtomic
|
||||||
|
dk.Set(key.NewDisco())
|
||||||
|
|
||||||
|
private := dk.Private()
|
||||||
|
public := dk.Public()
|
||||||
|
short := dk.Short()
|
||||||
|
|
||||||
|
if private.IsZero() {
|
||||||
|
t.Fatal("DiscoKey private key should not be zero")
|
||||||
|
}
|
||||||
|
if public.IsZero() {
|
||||||
|
t.Fatal("DiscoKey public key should not be zero")
|
||||||
|
}
|
||||||
|
if short == "" {
|
||||||
|
t.Fatal("DiscoKey short string should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if public != private.Public() {
|
||||||
|
t.Fatal("DiscoKey public key doesn't match private key")
|
||||||
|
}
|
||||||
|
if short != public.ShortString() {
|
||||||
|
t.Fatal("DiscoKey short string doesn't match public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPrivate, gotPublic := dk.Pair()
|
||||||
|
if !gotPrivate.Equal(private) {
|
||||||
|
t.Fatal("Pair() returned different private key")
|
||||||
|
}
|
||||||
|
if gotPublic != public {
|
||||||
|
t.Fatal("Pair() returned different public key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoAtomicSet(t *testing.T) {
|
||||||
|
var dk discoAtomic
|
||||||
|
dk.Set(key.NewDisco())
|
||||||
|
oldPrivate := dk.Private()
|
||||||
|
oldPublic := dk.Public()
|
||||||
|
|
||||||
|
newPrivate := key.NewDisco()
|
||||||
|
dk.Set(newPrivate)
|
||||||
|
|
||||||
|
currentPrivate := dk.Private()
|
||||||
|
currentPublic := dk.Public()
|
||||||
|
|
||||||
|
if currentPrivate.Equal(oldPrivate) {
|
||||||
|
t.Fatal("DiscoKey private key should have changed after Set")
|
||||||
|
}
|
||||||
|
if currentPublic == oldPublic {
|
||||||
|
t.Fatal("DiscoKey public key should have changed after Set")
|
||||||
|
}
|
||||||
|
if !currentPrivate.Equal(newPrivate) {
|
||||||
|
t.Fatal("DiscoKey private key doesn't match the set key")
|
||||||
|
}
|
||||||
|
if currentPublic != newPrivate.Public() {
|
||||||
|
t.Fatal("DiscoKey public key doesn't match derived from set private key")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue