Skip to content

Commit 1660405

Browse files
authored
Client: Implement certificate pinning (#359)
1 parent 2975b0f commit 1660405

File tree

3 files changed

+82
-8
lines changed

3 files changed

+82
-8
lines changed

elasticsearch.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ type Config struct {
6666
Username string // Username for HTTP Basic Authentication.
6767
Password string // Password for HTTP Basic Authentication.
6868

69-
CloudID string // Endpoint for the Elastic Service (https://elastic.co/cloud).
70-
APIKey string // Base64-encoded token for authorization; if set, overrides username/password and service token.
71-
ServiceToken string // Service token for authorization; if set, overrides username/password.
69+
CloudID string // Endpoint for the Elastic Service (https://elastic.co/cloud).
70+
APIKey string // Base64-encoded token for authorization; if set, overrides username/password and service token.
71+
ServiceToken string // Service token for authorization; if set, overrides username/password.
72+
CertificateFingerprint string // SHA256 hex fingerprint given by Elasticsearch on first launch.
7273

7374
Header http.Header // Global HTTP request header.
7475

@@ -189,11 +190,12 @@ func NewClient(cfg Config) (*Client, error) {
189190
}
190191

191192
tp, err := estransport.New(estransport.Config{
192-
URLs: urls,
193-
Username: cfg.Username,
194-
Password: cfg.Password,
195-
APIKey: cfg.APIKey,
196-
ServiceToken: cfg.ServiceToken,
193+
URLs: urls,
194+
Username: cfg.Username,
195+
Password: cfg.Password,
196+
APIKey: cfg.APIKey,
197+
ServiceToken: cfg.ServiceToken,
198+
CertificateFingerprint: cfg.CertificateFingerprint,
197199

198200
Header: cfg.Header,
199201
CACert: cfg.CACert,

elasticsearch_internal_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
package elasticsearch
2121

2222
import (
23+
"bytes"
24+
"crypto/x509"
2325
"encoding/base64"
2426
"errors"
2527
"io/ioutil"
@@ -774,3 +776,41 @@ func TestCompatibilityHeader(t *testing.T) {
774776
client.Search.WithBody(strings.NewReader("{}")),
775777
)
776778
}
779+
780+
781+
782+
func TestFingerprint(t *testing.T) {
783+
body := []byte(`{"body": true"}"`)
784+
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
785+
w.Header().Set("X-Elastic-Product", "Elasticsearch")
786+
w.Write(body)
787+
}))
788+
defer server.Close()
789+
790+
config := Config{
791+
Addresses: []string{server.URL},
792+
DisableRetry: true,
793+
}
794+
795+
// Without certificate and authority, client should fail on TLS
796+
client, _ := NewClient(config)
797+
res, err := client.Info()
798+
if _, ok := err.(x509.UnknownAuthorityError); !ok {
799+
t.Fatalf("Uknown error, expected UnknownAuthorityError, got: %s", err)
800+
}
801+
802+
// We add the fingerprint corresponding ton testcert.LocalhostCert
803+
//
804+
config.CertificateFingerprint = "448F628A8A65AA18560E53A80C53ACB38C51B427DF0334082349141147DC9BF6"
805+
client, _ = NewClient(config)
806+
res, err = client.Info()
807+
if err != nil {
808+
t.Fatal(err)
809+
}
810+
defer res.Body.Close()
811+
812+
data, _ := ioutil.ReadAll(res.Body)
813+
if bytes.Compare(data, body) != 0 {
814+
t.Fatalf("unexpected payload returned: expected: %s, got: %s", body, data)
815+
}
816+
}

estransport/estransport.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ package estransport
2020
import (
2121
"bytes"
2222
"compress/gzip"
23+
"crypto/sha256"
24+
"crypto/tls"
2325
"crypto/x509"
26+
"encoding/hex"
2427
"errors"
2528
"fmt"
2629
"io"
@@ -106,6 +109,8 @@ type Config struct {
106109
Selector Selector
107110

108111
ConnectionPoolFunc func([]*Connection, Selector) ConnectionPool
112+
113+
CertificateFingerprint string
109114
}
110115

111116
// Client represents the HTTP client.
@@ -118,6 +123,7 @@ type Client struct {
118123
password string
119124
apikey string
120125
servicetoken string
126+
fingerprint string
121127
header http.Header
122128

123129
retryOnStatus []int
@@ -150,6 +156,32 @@ func New(cfg Config) (*Client, error) {
150156
cfg.Transport = http.DefaultTransport
151157
}
152158

159+
if transport, ok := cfg.Transport.(*http.Transport); ok {
160+
if cfg.CertificateFingerprint != "" {
161+
transport.DialTLS = func(network, addr string) (net.Conn, error) {
162+
fingerprint, _ := hex.DecodeString(cfg.CertificateFingerprint)
163+
164+
c, err := tls.Dial(network, addr, &tls.Config{InsecureSkipVerify: true})
165+
if err != nil {
166+
return nil, err
167+
}
168+
169+
// Retrieve the connection state from the remote server.
170+
cState := c.ConnectionState()
171+
for _, cert := range cState.PeerCertificates {
172+
// Compute digest for each certificate.
173+
digest := sha256.Sum256(cert.Raw)
174+
175+
// Provided fingerprint should match at least one certificate from remote before we continue.
176+
if bytes.Compare(digest[0:], fingerprint) == 0 {
177+
return c, nil
178+
}
179+
}
180+
return nil, fmt.Errorf("fingerprint mismatch, provided: %s", cfg.CertificateFingerprint)
181+
}
182+
}
183+
}
184+
153185
if cfg.CACert != nil {
154186
httpTransport, ok := cfg.Transport.(*http.Transport)
155187
if !ok {

0 commit comments

Comments
 (0)