atls: make client cfg reusable

This commit is contained in:
Thomas Tendyck 2022-06-02 17:22:11 +02:00 committed by Thomas Tendyck
parent 989c128fa6
commit e9916a7d3a
3 changed files with 91 additions and 46 deletions

View File

@ -27,16 +27,16 @@ sequenceDiagram
Note over Client: Verify Attestation Note over Client: Verify Attestation
Client->>Server: ClientKeyExchange Client->>Server: ClientKeyExchange
Client->>Server: ChangeCipherSpec, Finished Client->>Server: ChangeCipherSpec, Finished
Server->>Client: Server->>Client: #
``` ```
## Server side verification ## Server side verification
1. The client sends a ClientHello message 1. The client sends a ClientHello message
2. The server sends back a certificate with a random nonce. The nonce is embedded using x509 certificate extensions with the OID `1.3.9900.0.1`. 2. The server sends back a certificate and a random nonce. The nonce is encoded as the Distinguished Name of an acceptable CA.
3. The client does not verify the servers certificate, but uses the embedded nonce to generate an attestation based on its CC capabilities. 3. The client does not verify the servers certificate, but uses the nonce to generate an attestation based on its CC capabilities.
* The attestation is embedded in the client certificate using x509 certificate extensions with an OID to identify the CC attestation type. * The attestation is embedded in the client certificate using x509 certificate extensions with an OID to identify the CC attestation type.
4. The server verifies the client's attestation statement. 4. The server verifies the client's attestation statement.
@ -48,7 +48,7 @@ sequenceDiagram
participant Client participant Client
participant Server participant Server
Client->>Server: ClientHello Client->>Server: ClientHello
Server->>Client: ServerCertificate(nonce), ServerHelloDone Server->>Client: ServerCertificate, AcceptableCAs(nonce), ServerHelloDone
Client->>Server: ClientKeyExchange, ClientCertificate(AttestationStatement) Client->>Server: ClientKeyExchange, ClientCertificate(AttestationStatement)
Client->>Server: ChangeCipherSpec, Finished Client->>Server: ChangeCipherSpec, Finished
Note over Server: Verify Attestation Note over Server: Verify Attestation
@ -61,11 +61,11 @@ sequenceDiagram
2. The server generates an attestation statement using the clients nonce and its CC capabilities. 2. The server generates an attestation statement using the clients nonce and its CC capabilities.
* The attestation is embedded in the server certificate using x509 certificate extensions with an OID to identify the attestation type. * The attestation is embedded in the server certificate using x509 certificate extensions with an OID to identify the attestation type.
* A nonce is embedded using x509 certificate extensions with the OID `1.3.9900.0.1`. * A nonce is encoded as the Distinguished Name of an acceptable CA.
3. The client verifies the attestation statement. 3. The client verifies the attestation statement.
4. The client uses the nonce embedded in the server's certificate to generate an attestation based on its CC capabilities. 4. The client uses the nonce to generate an attestation based on its CC capabilities.
* The attestation is embedded in the client certificate using x509 certificate extensions with an OID to identify the CC attestation type. * The attestation is embedded in the client certificate using x509 certificate extensions with an OID to identify the CC attestation type.
5. The server verifies the client's attestation statement. 5. The server verifies the client's attestation statement.
@ -77,7 +77,7 @@ sequenceDiagram
participant Client participant Client
participant Server participant Server
Client->>Server: ClientHello(nonce) Client->>Server: ClientHello(nonce)
Server->>Client: ServerCertificate(AttestationStatement, nonce), ServerHelloDone Server->>Client: ServerCertificate(AttestationStatement), AcceptableCAs(nonce), ServerHelloDone
Note over Client: Verify Attestation Note over Client: Verify Attestation
Client->>Server: ClientKeyExchange, ClientCertificate(AttestationStatement) Client->>Server: ClientKeyExchange, ClientCertificate(AttestationStatement)
Client->>Server: ChangeCipherSpec, Finished Client->>Server: ChangeCipherSpec, Finished

View File

@ -9,8 +9,11 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/asn1"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt"
"math/big"
"time" "time"
"github.com/edgelesssys/constellation/coordinator/config" "github.com/edgelesssys/constellation/coordinator/config"
@ -33,6 +36,11 @@ func CreateAttestationServerTLSConfig(issuer Issuer, validators []Validator) (*t
} }
// CreateAttestationClientTLSConfig creates a tls.Config object that verifies a certificate with an embedded attestation document. // CreateAttestationClientTLSConfig creates a tls.Config object that verifies a certificate with an embedded attestation document.
//
// ATTENTION: The tls.Config ensures freshness of the server's attestation only for the first connection it is used for.
// If freshness is required, you must create a new tls.Config for each connection or ensure freshness on the protocol level.
// If freshness is not required, you can reuse this tls.Config.
//
// If no validators are set, the server's attestation document will not be verified. // If no validators are set, the server's attestation document will not be verified.
// If issuer is nil, the client will be unable to perform mutual aTLS. // If issuer is nil, the client will be unable to perform mutual aTLS.
func CreateAttestationClientTLSConfig(issuer Issuer, validators []Validator) (*tls.Config, error) { func CreateAttestationClientTLSConfig(issuer Issuer, validators []Validator) (*tls.Config, error) {
@ -67,7 +75,7 @@ type Validator interface {
// getATLSConfigForClientFunc returns a config setup function that is called once for every client connecting to the server. // getATLSConfigForClientFunc returns a config setup function that is called once for every client connecting to the server.
// This allows for different server configuration for every client. // This allows for different server configuration for every client.
// In aTLS this is used to generate unique nonces for every client and embed them in the server's certificate. // In aTLS this is used to generate unique nonces for every client.
func getATLSConfigForClientFunc(issuer Issuer, validators []Validator) (func(*tls.ClientHelloInfo) (*tls.Config, error), error) { func getATLSConfigForClientFunc(issuer Issuer, validators []Validator) (func(*tls.ClientHelloInfo) (*tls.Config, error), error) {
// generate key for the server // generate key for the server
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
@ -90,25 +98,29 @@ func getATLSConfigForClientFunc(issuer Issuer, validators []Validator) (func(*tl
serverNonce: serverNonce, serverNonce: serverNonce,
} }
clientAuth := tls.NoClientCert cfg := &tls.Config{
// enable mutual aTLS if any validators are set
if len(validators) > 0 {
clientAuth = tls.RequireAnyClientCert // validity of certificate will be checked by our custom verify function
}
return &tls.Config{
ClientAuth: clientAuth,
VerifyPeerCertificate: serverConn.verify, VerifyPeerCertificate: serverConn.verify,
GetCertificate: serverConn.getCertificate, GetCertificate: serverConn.getCertificate,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
}, nil }
// enable mutual aTLS if any validators are set
if len(validators) > 0 {
cfg.ClientAuth = tls.RequireAnyClientCert // validity of certificate will be checked by our custom verify function
// ugly hack: abuse acceptable client CAs as a channel to transmit the nonce
if cfg.ClientCAs, err = encodeNonceToCertPool(serverNonce, priv); err != nil {
return nil, fmt.Errorf("encode nonce: %w", err)
}
}
return cfg, nil
}, nil }, nil
} }
// getCertificate creates a client or server certificate for aTLS connections. // getCertificate creates a client or server certificate for aTLS connections.
// The certificate uses certificate extensions to embed an attestation document generated using remoteNonce. // The certificate uses certificate extensions to embed an attestation document generated using nonce.
// If localNonce is set, it is also embedded as a certificate extension. func getCertificate(issuer Issuer, priv, pub any, nonce []byte) (*tls.Certificate, error) {
func getCertificate(issuer Issuer, priv, pub any, remoteNonce, localNonce []byte) (*tls.Certificate, error) {
serialNumber, err := util.GenerateCertificateSerialNumber() serialNumber, err := util.GenerateCertificateSerialNumber()
if err != nil { if err != nil {
return nil, err return nil, err
@ -124,7 +136,7 @@ func getCertificate(issuer Issuer, priv, pub any, remoteNonce, localNonce []byte
} }
// create attestation document using the nonce send by the remote party // create attestation document using the nonce send by the remote party
attDoc, err := issuer.Issue(hash, remoteNonce) attDoc, err := issuer.Issue(hash, nonce)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -132,12 +144,7 @@ func getCertificate(issuer Issuer, priv, pub any, remoteNonce, localNonce []byte
extensions = append(extensions, pkix.Extension{Id: issuer.OID(), Value: attDoc}) extensions = append(extensions, pkix.Extension{Id: issuer.OID(), Value: attDoc})
} }
// embed locally generated nonce in certificate // create certificate that includes the attestation document as extension
if len(localNonce) > 0 {
extensions = append(extensions, pkix.Extension{Id: oid.ATLSNonce, Value: localNonce})
}
// create certificate that includes the attestation document and the server nonce as extension
now := time.Now() now := time.Now()
template := &x509.Certificate{ template := &x509.Certificate{
SerialNumber: serialNumber, SerialNumber: serialNumber,
@ -208,12 +215,59 @@ func hashPublicKey(pub any) ([]byte, error) {
return result[:], nil return result[:], nil
} }
// encodeNonceToCertPool returns a cert pool that contains a certificate whose CN is the base64-encoded nonce.
func encodeNonceToCertPool(nonce []byte, privKey *ecdsa.PrivateKey) (*x509.CertPool, error) {
template := &x509.Certificate{
SerialNumber: &big.Int{},
Subject: pkix.Name{CommonName: base64.StdEncoding.EncodeToString(nonce)},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &privKey.PublicKey, privKey)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, err
}
pool := x509.NewCertPool()
pool.AddCert(cert)
return pool, nil
}
// decodeNonceFromAcceptableCAs interprets the CN of acceptableCAs[0] as base64-encoded nonce and returns the decoded nonce.
// acceptableCAs should have been received by a client where the server used encodeNonceToCertPool to transmit the nonce.
func decodeNonceFromAcceptableCAs(acceptableCAs [][]byte) ([]byte, error) {
if len(acceptableCAs) != 1 {
return nil, errors.New("unexpected acceptableCAs length")
}
var rdnSeq pkix.RDNSequence
if _, err := asn1.Unmarshal(acceptableCAs[0], &rdnSeq); err != nil {
return nil, err
}
// https://github.com/golang/go/blob/19309779ac5e2f5a2fd3cbb34421dafb2855ac21/src/crypto/x509/pkix/pkix.go#L188
oidCommonName := asn1.ObjectIdentifier{2, 5, 4, 3}
for _, rdnSet := range rdnSeq {
for _, rdn := range rdnSet {
if rdn.Type.Equal(oidCommonName) {
nonce, ok := rdn.Value.(string)
if !ok {
return nil, errors.New("unexpected RDN type")
}
return base64.StdEncoding.DecodeString(nonce)
}
}
}
return nil, errors.New("CN not found")
}
// clientConnection holds state for client to server connections. // clientConnection holds state for client to server connections.
type clientConnection struct { type clientConnection struct {
issuer Issuer issuer Issuer
validators []Validator validators []Validator
clientNonce []byte clientNonce []byte
serverNonce []byte
} }
// verify the validity of an aTLS server certificate. // verify the validity of an aTLS server certificate.
@ -223,13 +277,6 @@ func (c *clientConnection) verify(rawCerts [][]byte, verifiedChains [][]*x509.Ce
return err return err
} }
// get nonce send by server from cert extensions and save to connection state
for _, ex := range cert.Extensions {
if ex.Id.Equal(oid.ATLSNonce) {
c.serverNonce = ex.Value
}
}
// don't perform verification of attestation document if no validators are set // don't perform verification of attestation document if no validators are set
if len(c.validators) == 0 { if len(c.validators) == 0 {
return nil return nil
@ -239,18 +286,20 @@ func (c *clientConnection) verify(rawCerts [][]byte, verifiedChains [][]*x509.Ce
} }
// getCertificate generates a client certificate for mutual aTLS connections. // getCertificate generates a client certificate for mutual aTLS connections.
func (c *clientConnection) getCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) { func (c *clientConnection) getCertificate(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
// generate and hash key // generate and hash key
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// create aTLS certificate using the server's nonce as read by clientConnection.verify // ugly hack: abuse acceptable client CAs as a channel to receive the nonce
// we do not pass a nonce because serverNonce, err := decodeNonceFromAcceptableCAs(cri.AcceptableCAs)
// 1. we already received a certificate from the server if err != nil {
// 2. we transmitted the client nonce as our server name in our client-hello message return nil, fmt.Errorf("decode nonce: %w", err)
return getCertificate(c.issuer, priv, &priv.PublicKey, c.serverNonce, nil) }
return getCertificate(c.issuer, priv, &priv.PublicKey, serverNonce)
} }
// serverConnection holds state for server to client connections. // serverConnection holds state for server to client connections.
@ -282,6 +331,5 @@ func (c *serverConnection) getCertificate(chi *tls.ClientHelloInfo) (*tls.Certif
} }
// create aTLS certificate using the nonce as extracted from the client-hello message // create aTLS certificate using the nonce as extracted from the client-hello message
// we also embed the nonce generated for this connection in case of mutual aTLS return getCertificate(c.issuer, c.privKey, &c.privKey.PublicKey, clientNonce)
return getCertificate(c.issuer, c.privKey, &c.privKey.PublicKey, clientNonce, c.serverNonce)
} }

View File

@ -4,9 +4,6 @@ import (
"encoding/asn1" "encoding/asn1"
) )
// ATLSNonce is the ASN.1 object identifier used to transmit a nonce from server to client.
var ATLSNonce = asn1.ObjectIdentifier{1, 3, 9900, 0, 1}
// Getter returns an ASN.1 Object Identifier. // Getter returns an ASN.1 Object Identifier.
type Getter interface { type Getter interface {
OID() asn1.ObjectIdentifier OID() asn1.ObjectIdentifier