Skip to content

Commit

Permalink
crypto/tls: add support for Certificate Transparency
Browse files Browse the repository at this point in the history
This change adds support for serving and receiving Signed Certificate
Timestamps as described in RFC 6962.

The server is now capable of serving SCTs listed in the Certificate
structure. The client now asks for SCTs and, if any are received,
they are exposed in the ConnectionState structure.

Fixes #10201

Change-Id: Ib3adae98cb4f173bc85cec04d2bdd3aa0fec70bb
Reviewed-on: https://go-review.googlesource.com/8988
Reviewed-by: Adam Langley <agl@golang.org>
Run-TryBot: Adam Langley <agl@golang.org>
Reviewed-by: Jonathan Rudenberg <jonathan@titanous.com>
  • Loading branch information
titanous authored and agl committed Apr 26, 2015
1 parent 06b2973 commit cf04082
Show file tree
Hide file tree
Showing 31 changed files with 1,106 additions and 779 deletions.
23 changes: 14 additions & 9 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const (
extensionSupportedPoints uint16 = 11
extensionSignatureAlgorithms uint16 = 13
extensionALPN uint16 = 16
extensionSCT uint16 = 18 // https://tools.ietf.org/html/rfc6962#section-6
extensionSessionTicket uint16 = 35
extensionNextProtoNeg uint16 = 13172 // not IANA assigned
extensionRenegotiationInfo uint16 = 0xff01
Expand Down Expand Up @@ -157,15 +158,16 @@ var supportedClientCertSignatureAlgorithms = []signatureAndHash{

// ConnectionState records basic TLS details about the connection.
type ConnectionState struct {
Version uint16 // TLS version used by the connection (e.g. VersionTLS12)
HandshakeComplete bool // TLS handshake is complete
DidResume bool // connection resumes a previous TLS connection
CipherSuite uint16 // cipher suite in use (TLS_RSA_WITH_RC4_128_SHA, ...)
NegotiatedProtocol string // negotiated next protocol (from Config.NextProtos)
NegotiatedProtocolIsMutual bool // negotiated protocol was advertised by server
ServerName string // server name requested by client, if any (server side only)
PeerCertificates []*x509.Certificate // certificate chain presented by remote peer
VerifiedChains [][]*x509.Certificate // verified chains built from PeerCertificates
Version uint16 // TLS version used by the connection (e.g. VersionTLS12)
HandshakeComplete bool // TLS handshake is complete
DidResume bool // connection resumes a previous TLS connection
CipherSuite uint16 // cipher suite in use (TLS_RSA_WITH_RC4_128_SHA, ...)
NegotiatedProtocol string // negotiated next protocol (from Config.NextProtos)
NegotiatedProtocolIsMutual bool // negotiated protocol was advertised by server
ServerName string // server name requested by client, if any (server side only)
PeerCertificates []*x509.Certificate // certificate chain presented by remote peer
VerifiedChains [][]*x509.Certificate // verified chains built from PeerCertificates
SignedCertificateTimestamps [][]byte // SCTs from the server, if any

// TLSUnique contains the "tls-unique" channel binding value (see RFC
// 5929, section 3). For resumed sessions this value will be nil
Expand Down Expand Up @@ -497,6 +499,9 @@ type Certificate struct {
// OCSPStaple contains an optional OCSP response which will be served
// to clients that request it.
OCSPStaple []byte
// SignedCertificateTimestamps contains an optional list of Signed
// Certificate Timestamps which will be served to clients that request it.
SignedCertificateTimestamps [][]byte
// Leaf is the parsed form of the leaf certificate, which may be
// initialized using x509.ParseCertificate to reduce per-handshake
// processing for TLS clients doing client authentication. If nil, the
Expand Down
4 changes: 3 additions & 1 deletion conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ type Conn struct {
handshakeComplete bool
didResume bool // whether this connection was a session resumption
cipherSuite uint16
ocspResponse []byte // stapled OCSP response
ocspResponse []byte // stapled OCSP response
scts [][]byte // signed certificate timestamps from server
peerCertificates []*x509.Certificate
// verifiedChains contains the certificate chains that we built, as
// opposed to the ones presented by the server.
Expand Down Expand Up @@ -993,6 +994,7 @@ func (c *Conn) ConnectionState() ConnectionState {
state.PeerCertificates = c.peerCertificates
state.VerifiedChains = c.verifiedChains
state.ServerName = c.serverName
state.SignedCertificateTimestamps = c.scts
if !c.didResume {
state.TLSUnique = c.firstFinished[:]
}
Expand Down
2 changes: 2 additions & 0 deletions handshake_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func (c *Conn) clientHandshake() error {
compressionMethods: []uint8{compressionNone},
random: make([]byte, 32),
ocspStapling: true,
scts: true,
serverName: c.config.ServerName,
supportedCurves: c.config.curvePreferences(),
supportedPoints: []uint8{pointFormatUncompressed},
Expand Down Expand Up @@ -522,6 +523,7 @@ func (hs *clientHandshakeState) processServerHello() (bool, error) {
c.clientProtocol = hs.serverHello.alpnProtocol
c.clientProtocolFallback = false
}
c.scts = hs.serverHello.scts

if hs.serverResumedSession() {
// Restore masterSecret and peerCerts from previous state
Expand Down
61 changes: 59 additions & 2 deletions handshake_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"encoding/pem"
"fmt"
"io"
Expand Down Expand Up @@ -49,6 +51,10 @@ type clientTest struct {
// key, if not nil, contains either a *rsa.PrivateKey or
// *ecdsa.PrivateKey which is the private key for the reference server.
key interface{}
// extensions, if not nil, contains a list of extension data to be returned
// from the ServerHello. The data should be in standard TLS format with
// a 2-byte uint16 type, 2-byte data length, followed by the extension data.
extensions [][]byte
// validate, if not nil, is a function that will be called with the
// ConnectionState of the resulting connection. It returns a non-nil
// error if the ConnectionState is unacceptable.
Expand Down Expand Up @@ -111,6 +117,19 @@ func (test *clientTest) connFromCommand() (conn *recordingConn, child *exec.Cmd,
const serverPort = 24323
command = append(command, "-accept", strconv.Itoa(serverPort))

if len(test.extensions) > 0 {
var serverInfo bytes.Buffer
for _, ext := range test.extensions {
pem.Encode(&serverInfo, &pem.Block{
Type: fmt.Sprintf("SERVERINFO FOR EXTENSION %d", binary.BigEndian.Uint16(ext)),
Bytes: ext,
})
}
serverInfoPath := tempFile(serverInfo.String())
defer os.Remove(serverInfoPath)
command = append(command, "-serverinfo", serverInfoPath)
}

cmd := exec.Command(command[0], command[1:]...)
stdin = blockingSource(make(chan bool))
cmd.Stdin = stdin
Expand Down Expand Up @@ -193,7 +212,7 @@ func (test *clientTest) run(t *testing.T, write bool) {
}
if test.validate != nil {
if err := test.validate(client.ConnectionState()); err != nil {
t.Logf("validate callback returned error: %s", err)
t.Errorf("validate callback returned error: %s", err)
}
}
client.Close()
Expand Down Expand Up @@ -394,7 +413,7 @@ func TestClientResumption(t *testing.T) {
}

testResumeState := func(test string, didResume bool) {
hs, err := testHandshake(clientConfig, serverConfig)
_, hs, err := testHandshake(clientConfig, serverConfig)
if err != nil {
t.Fatalf("%s: handshake failed: %s", test, err)
}
Expand Down Expand Up @@ -507,3 +526,41 @@ func TestHandshakeClientALPNNoMatch(t *testing.T) {
}
runClientTestTLS12(t, test)
}

// sctsBase64 contains data from `openssl s_client -serverinfo 18 -connect ritter.vg:443`
const sctsBase64 = "ABIBaQFnAHUApLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BAAAAFHl5nuFgAABAMARjBEAiAcS4JdlW5nW9sElUv2zvQyPoZ6ejKrGGB03gjaBZFMLwIgc1Qbbn+hsH0RvObzhS+XZhr3iuQQJY8S9G85D9KeGPAAdgBo9pj4H2SCvjqM7rkoHUz8cVFdZ5PURNEKZ6y7T0/7xAAAAUeX4bVwAAAEAwBHMEUCIDIhFDgG2HIuADBkGuLobU5a4dlCHoJLliWJ1SYT05z6AiEAjxIoZFFPRNWMGGIjskOTMwXzQ1Wh2e7NxXE1kd1J0QsAdgDuS723dc5guuFCaR+r4Z5mow9+X7By2IMAxHuJeqj9ywAAAUhcZIqHAAAEAwBHMEUCICmJ1rBT09LpkbzxtUC+Hi7nXLR0J+2PmwLp+sJMuqK+AiEAr0NkUnEVKVhAkccIFpYDqHOlZaBsuEhWWrYpg2RtKp0="

func TestHandshakClientSCTs(t *testing.T) {
config := *testConfig

scts, err := base64.StdEncoding.DecodeString(sctsBase64)
if err != nil {
t.Fatal(err)
}

test := &clientTest{
name: "SCT",
// Note that this needs OpenSSL 1.0.2 because that is the first
// version that supports the -serverinfo flag.
command: []string{"openssl", "s_server"},
config: &config,
extensions: [][]byte{scts},
validate: func(state ConnectionState) error {
expectedSCTs := [][]byte{
scts[8:125],
scts[127:245],
scts[247:],
}
if n := len(state.SignedCertificateTimestamps); n != len(expectedSCTs) {
return fmt.Errorf("Got %d scts, wanted %d", n, len(expectedSCTs))
}
for i, expected := range expectedSCTs {
if sct := state.SignedCertificateTimestamps[i]; !bytes.Equal(sct, expected) {
return fmt.Errorf("SCT #%d contained %x, expected %x", i, sct, expected)
}
}
return nil
},
}
runClientTestTLS12(t, test)
}
82 changes: 82 additions & 0 deletions handshake_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type clientHelloMsg struct {
nextProtoNeg bool
serverName string
ocspStapling bool
scts bool
supportedCurves []CurveID
supportedPoints []uint8
ticketSupported bool
Expand All @@ -40,6 +41,7 @@ func (m *clientHelloMsg) equal(i interface{}) bool {
m.nextProtoNeg == m1.nextProtoNeg &&
m.serverName == m1.serverName &&
m.ocspStapling == m1.ocspStapling &&
m.scts == m1.scts &&
eqCurveIDs(m.supportedCurves, m1.supportedCurves) &&
bytes.Equal(m.supportedPoints, m1.supportedPoints) &&
m.ticketSupported == m1.ticketSupported &&
Expand Down Expand Up @@ -99,6 +101,9 @@ func (m *clientHelloMsg) marshal() []byte {
}
numExtensions++
}
if m.scts {
numExtensions++
}
if numExtensions > 0 {
extensionsLength += 4 * numExtensions
length += 2 + extensionsLength
Expand Down Expand Up @@ -271,6 +276,13 @@ func (m *clientHelloMsg) marshal() []byte {
lengths[0] = byte(stringsLength >> 8)
lengths[1] = byte(stringsLength)
}
if m.scts {
// https://tools.ietf.org/html/rfc6962#section-3.3.1
z[0] = byte(extensionSCT >> 8)
z[1] = byte(extensionSCT)
// zero uint16 for the zero-length extension_data
z = z[4:]
}

m.raw = x

Expand Down Expand Up @@ -326,6 +338,7 @@ func (m *clientHelloMsg) unmarshal(data []byte) bool {
m.sessionTicket = nil
m.signatureAndHashes = nil
m.alpnProtocols = nil
m.scts = false

if len(data) == 0 {
// ClientHello is optionally followed by extension data
Expand Down Expand Up @@ -453,6 +466,11 @@ func (m *clientHelloMsg) unmarshal(data []byte) bool {
m.alpnProtocols = append(m.alpnProtocols, string(d[:stringLen]))
d = d[stringLen:]
}
case extensionSCT:
m.scts = true
if length != 0 {
return false
}
}
data = data[length:]
}
Expand All @@ -470,6 +488,7 @@ type serverHelloMsg struct {
nextProtoNeg bool
nextProtos []string
ocspStapling bool
scts [][]byte
ticketSupported bool
secureRenegotiation bool
alpnProtocol string
Expand All @@ -481,6 +500,15 @@ func (m *serverHelloMsg) equal(i interface{}) bool {
return false
}

if len(m.scts) != len(m1.scts) {
return false
}
for i, sct := range m.scts {
if !bytes.Equal(sct, m1.scts[i]) {
return false
}
}

return bytes.Equal(m.raw, m1.raw) &&
m.vers == m1.vers &&
bytes.Equal(m.random, m1.random) &&
Expand Down Expand Up @@ -530,6 +558,14 @@ func (m *serverHelloMsg) marshal() []byte {
extensionsLength += 2 + 1 + alpnLen
numExtensions++
}
sctLen := 0
if len(m.scts) > 0 {
for _, sct := range m.scts {
sctLen += len(sct) + 2
}
extensionsLength += 2 + sctLen
numExtensions++
}

if numExtensions > 0 {
extensionsLength += 4 * numExtensions
Expand Down Expand Up @@ -605,6 +641,23 @@ func (m *serverHelloMsg) marshal() []byte {
copy(z[7:], []byte(m.alpnProtocol))
z = z[7+alpnLen:]
}
if sctLen > 0 {
z[0] = byte(extensionSCT >> 8)
z[1] = byte(extensionSCT)
l := sctLen + 2
z[2] = byte(l >> 8)
z[3] = byte(l)
z[4] = byte(sctLen >> 8)
z[5] = byte(sctLen)

z = z[6:]
for _, sct := range m.scts {
z[0] = byte(len(sct) >> 8)
z[1] = byte(len(sct))
copy(z[2:], sct)
z = z[len(sct)+2:]
}
}

m.raw = x

Expand Down Expand Up @@ -634,6 +687,7 @@ func (m *serverHelloMsg) unmarshal(data []byte) bool {
m.nextProtoNeg = false
m.nextProtos = nil
m.ocspStapling = false
m.scts = nil
m.ticketSupported = false
m.alpnProtocol = ""

Expand Down Expand Up @@ -706,6 +760,34 @@ func (m *serverHelloMsg) unmarshal(data []byte) bool {
}
d = d[1:]
m.alpnProtocol = string(d)
case extensionSCT:
d := data[:length]

if len(d) < 2 {
return false
}
l := int(d[0])<<8 | int(d[1])
d = d[2:]
if len(d) != l {
return false
}
if l == 0 {
continue
}

m.scts = make([][]byte, 0, 3)
for len(d) != 0 {
if len(d) < 2 {
return false
}
sctLen := int(d[0])<<8 | int(d[1])
d = d[2:]
if len(d) < sctLen {
return false
}
m.scts = append(m.scts, d[:sctLen])
d = d[sctLen:]
}
}
data = data[length:]
}
Expand Down
11 changes: 11 additions & 0 deletions handshake_messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ func (*clientHelloMsg) Generate(rand *rand.Rand, size int) reflect.Value {
for i := range m.alpnProtocols {
m.alpnProtocols[i] = randomString(rand.Intn(20)+1, rand)
}
if rand.Intn(10) > 5 {
m.scts = true
}

return reflect.ValueOf(m)
}
Expand Down Expand Up @@ -172,6 +175,14 @@ func (*serverHelloMsg) Generate(rand *rand.Rand, size int) reflect.Value {
}
m.alpnProtocol = randomString(rand.Intn(32)+1, rand)

if rand.Intn(10) > 5 {
numSCTs := rand.Intn(4)
m.scts = make([][]byte, numSCTs)
for i := range m.scts {
m.scts[i] = randomBytes(rand.Intn(500), rand)
}
}

return reflect.ValueOf(m)
}

Expand Down
3 changes: 3 additions & 0 deletions handshake_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ Curves:
return false, err
}
}
if hs.clientHello.scts {
hs.hello.scts = hs.cert.SignedCertificateTimestamps
}

if priv, ok := hs.cert.PrivateKey.(crypto.Signer); ok {
switch priv.Public().(type) {
Expand Down
Loading

0 comments on commit cf04082

Please sign in to comment.