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
Client->>Server: ClientKeyExchange
Client->>Server: ChangeCipherSpec, Finished
Server->>Client:
Server->>Client: #
```
## Server side verification
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.
4. The server verifies the client's attestation statement.
@ -48,7 +48,7 @@ sequenceDiagram
participant Client
participant Server
Client->>Server: ClientHello
Server->>Client: ServerCertificate(nonce), ServerHelloDone
Server->>Client: ServerCertificate, AcceptableCAs(nonce), ServerHelloDone
Client->>Server: ClientKeyExchange, ClientCertificate(AttestationStatement)
Client->>Server: ChangeCipherSpec, Finished
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.
* 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.
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.
5. The server verifies the client's attestation statement.
@ -77,7 +77,7 @@ sequenceDiagram
participant Client
participant Server
Client->>Server: ClientHello(nonce)
Server->>Client: ServerCertificate(AttestationStatement, nonce), ServerHelloDone
Server->>Client: ServerCertificate(AttestationStatement), AcceptableCAs(nonce), ServerHelloDone
Note over Client: Verify Attestation
Client->>Server: ClientKeyExchange, ClientCertificate(AttestationStatement)
Client->>Server: ChangeCipherSpec, Finished

View File

@ -9,8 +9,11 @@ import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"errors"
"fmt"
"math/big"
"time"
"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.
//
// 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 issuer is nil, the client will be unable to perform mutual aTLS.
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.
// 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) {
// generate key for the server
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
@ -90,25 +98,29 @@ func getATLSConfigForClientFunc(issuer Issuer, validators []Validator) (func(*tl
serverNonce: serverNonce,
}
clientAuth := tls.NoClientCert
// 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,
cfg := &tls.Config{
VerifyPeerCertificate: serverConn.verify,
GetCertificate: serverConn.getCertificate,
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
}
// getCertificate creates a client or server certificate for aTLS connections.
// The certificate uses certificate extensions to embed an attestation document generated using remoteNonce.
// If localNonce is set, it is also embedded as a certificate extension.
func getCertificate(issuer Issuer, priv, pub any, remoteNonce, localNonce []byte) (*tls.Certificate, error) {
// The certificate uses certificate extensions to embed an attestation document generated using nonce.
func getCertificate(issuer Issuer, priv, pub any, nonce []byte) (*tls.Certificate, error) {
serialNumber, err := util.GenerateCertificateSerialNumber()
if err != nil {
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
attDoc, err := issuer.Issue(hash, remoteNonce)
attDoc, err := issuer.Issue(hash, nonce)
if err != nil {
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})
}
// embed locally generated nonce in certificate
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
// create certificate that includes the attestation document as extension
now := time.Now()
template := &x509.Certificate{
SerialNumber: serialNumber,
@ -208,12 +215,59 @@ func hashPublicKey(pub any) ([]byte, error) {
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.
type clientConnection struct {
issuer Issuer
validators []Validator
clientNonce []byte
serverNonce []byte
}
// verify the validity of an aTLS server certificate.
@ -223,13 +277,6 @@ func (c *clientConnection) verify(rawCerts [][]byte, verifiedChains [][]*x509.Ce
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
if len(c.validators) == 0 {
return nil
@ -239,18 +286,20 @@ func (c *clientConnection) verify(rawCerts [][]byte, verifiedChains [][]*x509.Ce
}
// 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
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
// create aTLS certificate using the server's nonce as read by clientConnection.verify
// we do not pass a nonce because
// 1. we already received a certificate from the server
// 2. we transmitted the client nonce as our server name in our client-hello message
return getCertificate(c.issuer, priv, &priv.PublicKey, c.serverNonce, nil)
// ugly hack: abuse acceptable client CAs as a channel to receive the nonce
serverNonce, err := decodeNonceFromAcceptableCAs(cri.AcceptableCAs)
if err != nil {
return nil, fmt.Errorf("decode nonce: %w", err)
}
return getCertificate(c.issuer, priv, &priv.PublicKey, serverNonce)
}
// 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
// we also embed the nonce generated for this connection in case of mutual aTLS
return getCertificate(c.issuer, c.privKey, &c.privKey.PublicKey, clientNonce, c.serverNonce)
return getCertificate(c.issuer, c.privKey, &c.privKey.PublicKey, clientNonce)
}

View File

@ -4,9 +4,6 @@ import (
"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.
type Getter interface {
OID() asn1.ObjectIdentifier