Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions cmd/ntpresponder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"os/signal"
"runtime"

"github.com/facebook/time/ntp/ntske"
"github.com/facebook/time/ntp/responder/announce"
"github.com/facebook/time/ntp/responder/checker"
"github.com/facebook/time/ntp/responder/server"
Expand All @@ -41,8 +42,10 @@ func main() {
s := server.Server{}

var (
debugger bool
logLevel string
debugger bool
logLevel string
enableNTS bool // master switch for the NTS path
ntsKeystoreKeys int // size of the cookie master-key ring
)

flag.StringVar(&logLevel, "loglevel", "info", "Set a log level. Can be: debug, info, warning, error")
Expand All @@ -58,6 +61,8 @@ func main() {
flag.DurationVar(&s.Config.ExtraOffset, "extraoffset", 0, "Extra offset to return to clients")
flag.BoolVar(&s.Config.ManageLoopback, "manage-loopback", true, "Add/remove IPs. If false, these must be managed elsewhere")
flag.TextVar(&s.Config.TimestampType, "timestamptype", timestamp.SWRX, fmt.Sprintf("Timestamp type. Can be: %s, %s", timestamp.HWRX, timestamp.SWRX))
flag.BoolVar(&enableNTS, "enable-nts", false, "Enable NTS (Network Time Security) authenticated NTP")
flag.IntVar(&ntsKeystoreKeys, "nts-keystore-keys", 2, "Number of NTS cookie master keys kept in the rotating ring")

flag.Parse()
s.Config.IPs.SetDefault()
Expand All @@ -79,6 +84,21 @@ func main() {
log.Fatalf("Config is invalid: %v", err)
}

if enableNTS {
if ntsKeystoreKeys < 1 {
log.Fatalf("nts-keystore-keys must be >= 1, got %d", ntsKeystoreKeys)
}
ks, err := ntske.NewInMemoryKeystore(ntske.InMemoryKeystoreOptions{
MaxKeys: uint32(ntsKeystoreKeys), // #nosec G115 -- ntsKeystoreKeys guarded >= 1
InitialKey: ntske.SharedTestMasterKey,
})
if err != nil {
log.Fatalf("Failed to set up NTS keystore: %v", err)
}
s.Config.Keystore = ks
log.Info("NTS enabled: NTP requests carrying extension fields will be authenticated")
}

if debugger {
log.Warningf("Staring profiler on %s", pprofHTTP)
go func() {
Expand Down
44 changes: 44 additions & 0 deletions ntp/ntske/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ const (
// deterministic AEAD whose 16-octet synthetic IV doubles as the auth tag.
masterAEADID = protocol.AEADAESSIVCMAC512
)

// SharedTestMasterKey is a fixed 64-octet cookie master key used ONLY to bridge
// the gap until a Keychain-backed keystore lets every responder in the fleet
// share sealing keys. Seeding both the NTP responder and the standalone NTS-KE
// server with it (via InMemoryKeystoreOptions.InitialKey) lets cookies sealed
// by one open on the other.
//
// NOT for production: anyone holding these bytes can forge cookies. Delete this
// once the fleet-wide (Keychain) keystore lands.
var SharedTestMasterKey = []byte{
0xad, 0x17, 0x97, 0x8b, 0x12, 0x2b, 0x0c, 0xc1, 0x66, 0x81, 0x04, 0x55,
0xb4, 0xbb, 0x9a, 0xca, 0x0e, 0x85, 0x9b, 0xf0, 0x2d, 0x19, 0xe1, 0xdd,
0xb8, 0x1a, 0x85, 0xdb, 0x41, 0xd7, 0x48, 0x5f, 0xe4, 0x4a, 0x27, 0x06,
0xaa, 0x2d, 0x1b, 0x01, 0x7f, 0xab, 0x86, 0xaa, 0xf9, 0xe9, 0x01, 0xee,
0x8b, 0x3c, 0x2d, 0x58, 0x90, 0xdd, 0xa4, 0xc3, 0x46, 0x63, 0xb8, 0xa4,
0x2b, 0x55, 0xe5, 0x0c,
}

const (
cookieKeyIDLen = 4 // big-endian master-key identifier
cookieNonceLen = 16 // random per-cookie nonce, mixed into the SIV as AD
Expand Down Expand Up @@ -131,6 +149,10 @@ var _ Keystore = (*InMemoryKeystore)(nil)
type InMemoryKeystoreOptions struct {
MaxKeys uint32
Rand io.Reader
// InitialKey, if set, seeds the ring with this exact master key instead of
// generating a random one, letting separate processes share a sealing key.
// Must be masterKeyLen octets.
InitialKey []byte
}

// NewInMemoryKeystore returns a keystore seeded with one freshly generated
Expand All @@ -148,12 +170,34 @@ func NewInMemoryKeystore(opts InMemoryKeystoreOptions) (*InMemoryKeystore, error
if opts.Rand != nil {
ks.rand = opts.Rand
}
if len(opts.InitialKey) > 0 {
if err := ks.seedKey(opts.InitialKey); err != nil {
return nil, err
}
return ks, nil
}
if err := ks.Rotate(); err != nil {
return nil, err
}
return ks, nil
}

// seedKey installs key as the initial sealing key (Key ID 1). Unlike Rotate it
// takes a caller-supplied key rather than generating a random one.
func (ks *InMemoryKeystore) seedKey(key []byte) error {
if len(key) != masterKeyLen {
return fmt.Errorf("ntske: initial key must be %d octets, got %d", masterKeyLen, len(key))
}
ks.mu.Lock()
defer ks.mu.Unlock()
ks.nextID++
id := ks.nextID
ks.ring[id] = bytes.Clone(key)
ks.order = append(ks.order, id)
ks.current = id
return nil
}

// Rotate generates a new master key, makes it the sealing key, and ages out the
// oldest key if the ring is over capacity. Cookies sealed with a still-present
// key remain openable.
Expand Down
27 changes: 27 additions & 0 deletions ntp/ntske/keystore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,30 @@ func TestConcurrentSealOpen(t *testing.T) {
require.NoError(t, err)
}
}

// TestInitialKeyEnablesCrossKeystoreOpen verifies the shared-key bridge: two
// independent keystores seeded with the same InitialKey can open each other's
// cookies, which is what lets the standalone NTS-KE server and the NTP
// responder interoperate.
func TestInitialKeyEnablesCrossKeystoreOpen(t *testing.T) {
sealer, err := NewInMemoryKeystore(InMemoryKeystoreOptions{InitialKey: SharedTestMasterKey})
require.NoError(t, err)
opener, err := NewInMemoryKeystore(InMemoryKeystoreOptions{InitialKey: SharedTestMasterKey})
require.NoError(t, err)

c2s, s2c := sessionKeys(64)
cookie, err := sealer.SealCookie(protocol.AEADAESSIVCMAC512, c2s, s2c)
require.NoError(t, err)

_, gotC2S, gotS2C, err := opener.OpenCookie(cookie)
require.NoError(t, err)
require.Equal(t, c2s, gotC2S)
require.Equal(t, s2c, gotS2C)
}

// TestInitialKeyWrongLength checks that a master key of the wrong size is
// rejected rather than silently producing a broken keystore.
func TestInitialKeyWrongLength(t *testing.T) {
_, err := NewInMemoryKeystore(InMemoryKeystoreOptions{InitialKey: make([]byte, masterKeyLen-1)})
require.Error(t, err)
}
Loading