-
Notifications
You must be signed in to change notification settings - Fork 13
Open
Description
Thanks for all the work you've done. This is a great tool to test servers.
It's just a bit difficult to use with NTS servers. So I basically vibe coded a little tool in Go to get those values. The issue is that I haven't really written C in 20 years so I didn't feel comfortable making any pull requests here, but I will attach the tool below if someone else needs it. I also searched the documentation of Chrony if it could extract those values some way, but I didn't see anything there either.
package main
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"os"
)
const (
ntskeProtocol = "ntske/1"
ntsKePort = 4460
ntpProtocolID = uint16(0)
algoAeadAesSivCmac256 = uint16(15)
keyExporterLabel = "EXPORTER-network-time-security"
keyLength = 32
)
// Record types for the NTS-KE protocol.
type recordType uint16
const (
recEOM recordType = 0
recProtocols recordType = 1
recError recordType = 2
recWarning recordType = 3
recAlgorithms recordType = 4
recCookie recordType = 5
recServer recordType = 6
recPort recordType = 7
recCritical recordType = 0x8000
)
var (
ErrNtsKeFailed = errors.New("NTS key exchange failure")
)
// Main program entry point.
func main() {
if len(os.Args) < 2 {
log.Fatalf("Usage: go run . <server_hostname>")
}
server := os.Args[1]
c2sKey, firstCookie, err := performNTSKE(server)
if err != nil {
log.Fatalf("[!] %v", err)
}
fmt.Printf("\n✅ Success! Copy and paste the following command:\n\n")
fmt.Printf("ntpperf -S %x,%x\n", c2sKey, firstCookie)
}
func performNTSKE(server string) (c2sKey, firstCookie []byte, err error) {
addr := fmt.Sprintf("%s:%d", server, ntsKePort)
fmt.Fprintf(os.Stderr, "[*] Attempting NTS Key Establishment with %s...\n", addr)
// 1. Establish a TLS 1.3 connection.
tlsConfig := &tls.Config{
ServerName: server,
NextProtos: []string{ntskeProtocol},
}
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil {
return nil, nil, fmt.Errorf("TLS dial failed: %w", err)
}
defer conn.Close()
// 2. Verify the negotiated protocol is correct.
state := conn.ConnectionState()
if state.NegotiatedProtocol != ntskeProtocol {
return nil, nil, fmt.Errorf("%w: server did not negotiate protocol '%s'", ErrNtsKeFailed, ntskeProtocol)
}
fmt.Fprintf(os.Stderr, "[+] TLS Handshake successful. Negotiated Protocol: %s\n", state.NegotiatedProtocol)
// 3. Write the NTS-KE request records.
var xmitBuf bytes.Buffer
protoBytes := make([]byte, 2)
binary.BigEndian.PutUint16(protoBytes, ntpProtocolID)
writeRec(&xmitBuf, recProtocols|recCritical, protoBytes)
algoBytes := make([]byte, 2)
binary.BigEndian.PutUint16(algoBytes, algoAeadAesSivCmac256)
writeRec(&xmitBuf, recAlgorithms|recCritical, algoBytes)
writeRec(&xmitBuf, recEOM|recCritical, nil)
_, err = conn.Write(xmitBuf.Bytes())
if err != nil {
return nil, nil, fmt.Errorf("writing request failed: %w", err)
}
fmt.Fprintf(os.Stderr, "[+] Sent NTS request to server.\n")
// 4. Read and parse the server's response records.
var cookies [][]byte
reader := bufio.NewReader(conn)
loop:
for {
header := make([]byte, 4)
if _, err := io.ReadFull(reader, header); err != nil {
return nil, nil, fmt.Errorf("reading record header failed: %w", err)
}
rtype := recordType(binary.BigEndian.Uint16(header[0:2]))
critical := (rtype & recCritical) != 0
rtype &= ^recCritical
rlen := int(binary.BigEndian.Uint16(header[2:4]))
rbody := make([]byte, rlen)
if _, err := io.ReadFull(reader, rbody); err != nil {
return nil, nil, fmt.Errorf("reading record body failed: %w", err)
}
switch rtype {
case recEOM:
if !critical {
return nil, nil, fmt.Errorf("%w: EOM record not critical", ErrNtsKeFailed)
}
break loop
case recCookie:
cookies = append(cookies, rbody)
case recError:
return nil, nil, fmt.Errorf("%w: received error from server: 0x%x", ErrNtsKeFailed, rbody)
case recWarning:
fmt.Fprintf(os.Stderr, "[!] Received warning from server: 0x%x\n", rbody)
case recProtocols, recAlgorithms, recServer, recPort:
// We don't need to process these for this specific goal.
continue
default:
if critical {
return nil, nil, fmt.Errorf("%w: unhandled critical record type %d", ErrNtsKeFailed, rtype)
}
}
}
fmt.Fprintf(os.Stderr, "[+] Received %d cookies from server.\n", len(cookies))
if len(cookies) == 0 {
return nil, nil, fmt.Errorf("%w: server did not provide any cookies", ErrNtsKeFailed)
}
// 5. Extract the C2S key using the TLS exporter.
fmt.Fprintf(os.Stderr, "[+] Extracting keys from TLS session...\n")
context := make([]byte, 5)
binary.BigEndian.PutUint16(context[0:2], ntpProtocolID)
binary.BigEndian.PutUint16(context[2:4], algoAeadAesSivCmac256)
context[4] = 0 // c2s indicator
c2s, err := state.ExportKeyingMaterial(keyExporterLabel, context, keyLength)
if err != nil {
return nil, nil, fmt.Errorf("ExportKeyingMaterial failed: %w", err)
}
return c2s, cookies[0], nil
}
// writeRec is a helper to write a single NTS-KE record.
func writeRec(w io.Writer, rtype recordType, body []byte) {
binary.Write(w, binary.BigEndian, rtype)
binary.Write(w, binary.BigEndian, uint16(len(body)))
if len(body) > 0 {
w.Write(body)
}
}go run get_nts_keys.go time.cloudflare.com
[*] Attempting NTS Key Establishment with time.cloudflare.com:4460...
[+] TLS Handshake successful. Negotiated Protocol: ntske/1
[+] Sent NTS request to server.
[+] Received 8 cookies from server.
[+] Extracting keys from TLS session...
✅ Success! Copy and paste the following command:
ntpperf -S 5fb9ff00515...Metadata
Metadata
Assignees
Labels
No labels