Skip to content

It would be nice if the "-S" option without an argument automatically fetched C2S and COOKIE, or maybe add a new argument for that #12

@oittaa

Description

@oittaa

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

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions