diff --git a/cmd/ntpresponder/main.go b/cmd/ntpresponder/main.go index e98e6a1d..e9d3cf80 100644 --- a/cmd/ntpresponder/main.go +++ b/cmd/ntpresponder/main.go @@ -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" @@ -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") @@ -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() @@ -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() { diff --git a/ntp/ntske/keystore.go b/ntp/ntske/keystore.go index 275ce18c..6bf3b590 100644 --- a/ntp/ntske/keystore.go +++ b/ntp/ntske/keystore.go @@ -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 @@ -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 @@ -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. diff --git a/ntp/ntske/keystore_test.go b/ntp/ntske/keystore_test.go index d689e41e..00fab022 100644 --- a/ntp/ntske/keystore_test.go +++ b/ntp/ntske/keystore_test.go @@ -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) +}