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

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...

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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