Connecting as a client¶
The client signs in with a username and a password. It needs no CA certificates: a server that completes the handshake has proved that it holds the verifier of that username and password.
Running the handshake¶
Client wraps a connection you dialed. Config.Username is required. In this code from Example,
l is the listener of the server on Getting started:
raw, err := net.DialTimeout("tcp", l.Addr().String(), 10*time.Second)
if err != nil {
log.Fatal(err)
}
conn := srptls.Client(raw, &srptls.Config{
Username: "device-1",
Password: "secret",
ServerName: "localhost",
})
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
err = conn.HandshakeContext(ctx)
cancel()
if err != nil {
log.Fatal(err)
}
greeting, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
log.Fatal(err)
}
state := conn.ConnectionState()
fmt.Print(greeting)
fmt.Println(srptls.CipherSuiteName(state.CipherSuite))
fmt.Println("extended master secret:", state.ExtendedMasterSecret)
fmt.Println("tls-unique bytes:", len(state.TLSUnique))
conn.Close()
The handshake runs when you call Handshake or HandshakeContext or on the first Read or Write.
Close the connection when the handshake fails.
| Field | Meaning |
|---|---|
Username |
Required, 1 to 255 bytes. Sent in the clear in the ClientHello |
Password |
The password the server's verifier was made from |
ServerName |
Sent as SNI. A trailing dot is removed and an IP address is not sent |
MinGroupBits |
The smallest group accepted. 2048 when zero |
CipherSuites |
The cipher suites to offer, in order. See Cipher suites |
Rand |
The source of the client random, the SRP exponent and the CBC IVs. crypto/rand when nil |
KeyLogWriter |
NSS key log output for debugging |
The username and the password are hashed as given. RFC 5054 asks for SASLprep, but the library does not apply it, as OpenSSL does not. Two passwords that look the same but differ in Unicode normalization give different verifiers.
Configuration errors¶
The handshake fails before anything is sent when:
- the
Configis nil, - the username is empty or longer than 255 bytes,
MinGroupBitsis negative or above 8192,CipherSuiteslists none of the package's cipher suites.
What the client checks¶
The client fails the handshake and sends an alert when the server:
| Server behavior | Alert the client sends |
|---|---|
| Selects a version other than TLS 1.2 | protocol_version |
| Selects a cipher suite the client did not offer or a compression method other than null | illegal_parameter |
Sends a non-empty renegotiation_info |
handshake_failure |
| Answers an extension the client did not send | unsupported_extension |
Sends a group that is smaller than MinGroupBits or not in RFC 5054, Appendix A |
insufficient_security |
Sends B that is zero modulo N, not below N or gives u = 0 |
illegal_parameter |
| Sends a Finished message that does not decrypt | bad_record_mac |
| Sends a Finished message that decrypts but does not verify | handshake_failure |
The last two mean that the server does not hold the verifier of this username and password or that the records were altered on the way. The client's error then wraps none of the sentinel errors below, because those stand for alerts the server sent.
When the server refuses the client, the handshake error wraps a sentinel error:
| Server alert | Client error wraps | Meaning |
|---|---|---|
bad_record_mac |
ErrWrongPassword |
Wrong password or an unknown username the server hides with a simulated verifier |
unknown_psk_identity |
ErrUnknownUsername |
The server says the username does not exist |
access_denied |
ErrAccessDenied |
The server refused the client, for example over a rate limit |
RFC 5054, Section 3.3 warns that an attacker on
the path can insert an unknown_psk_identity alert, so do not draw conclusions from it alone.
Errors and alerts lists every error form.
Groups¶
The client accepts only the seven groups of RFC 5054 and, by default, only those of 2048 bits and
more. Set MinGroupBits to 1024 or 1536 to sign in to a server that still uses a smaller group:
config := &srptls.Config{
Username: "device-1",
Password: password,
MinGroupBits: 1024,
}
MinGroupBits is compared with the prime size, so a value between two group sizes accepts the
larger groups only.
With net/http¶
Run the handshake in http.Transport.DialTLSContext. net/http fills Response.TLS only for
crypto/tls connections, so read the connection state from the Conn instead:
config := &srptls.Config{
Username: "device-1",
Password: "secret",
ServerName: "est.example",
}
transport := &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
raw, err := new(net.Dialer).DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
conn := srptls.Client(raw, config)
if err := conn.HandshakeContext(ctx); err != nil {
raw.Close()
if errors.Is(err, srptls.ErrWrongPassword) {
return nil, fmt.Errorf("wrong username or password: %w", err)
}
return nil, err
}
log.Printf("tls-unique %x", conn.ConnectionState().TLSUnique)
return conn, nil
},
}
client := &http.Client{Transport: transport, Timeout: 30 * time.Second}
resp, err := client.Get("https://est.example:8443/.well-known/est/cacerts")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
This code is Example_httpClient in example_test.go. The transport dials a new connection with a new
handshake whenever it has no idle connection to reuse.
Renegotiation¶
The client never renegotiates. A HelloRequest from the server after the handshake is answered with a
no_renegotiation warning, the Read that received it returns an error and the connection can no
longer be written to.