openssl / project

Tracking of project related issues
2 stars 1 forks source link

Encrypt validation token #928

Open andrewkdinh opened 19 hours ago

andrewkdinh commented 19 hours ago

As a successor for https://github.com/openssl/project/issues/912, we need to encrypt the token so that it cannot be tampered with by malicious actors and is completely opaque to the client. We need to decide if the key should be stored to/loaded from a file, or maybe ephemeral by being generated when the server starts up. We can take notes from quic-go: https://github.com/quic-go/quic-go/blob/master/internal/handshake/token_protector.go

Sashan commented 18 hours ago

I will just put quic summary of what quic-go token protector is doing.

The key to protect can be either randomly generated or provided by application. Comment in transport.go reads as follows:

 63         // The TokenGeneratorKey is used to encrypt session resumption tokens.
 64         // If no key is configured, a random key will be generated.
 65         // If multiple servers are authoritative for the same domain, they should use the same key,
 66         // see section 8.1.3 of RFC 9000 for details.
 67         TokenGeneratorKey *TokenGeneratorKey

the key is created in transport.init() function:

233                 if t.TokenGeneratorKey == nil {
234                         var key TokenGeneratorKey
235                         if _, err := rand.Read(key[:]); err != nil {
236                                 t.initErr = err
237                                 return
238                         }
239                         t.TokenGeneratorKey = &key
240                 } 

The TokenGeneratorKey type is defined in internal/handshake/token_protector.go as 32 byte array (256bits)

The token protector uses AEAD with AES-256. To see how things are marshaled to packet take a look at NewRetryToken in internal/handshake/token_generator.go

 52 // NewRetryToken generates a new token for a Retry for a given source address
 53 func (g *TokenGenerator) NewRetryToken(
 54         raddr net.Addr,
 55         origDestConnID protocol.ConnectionID,
 56         retrySrcConnID protocol.ConnectionID,
 57 ) ([]byte, error) {
 58         data, err := asn1.Marshal(token{
 59                 IsRetryToken:             true,
 60                 RemoteAddr:               encodeRemoteAddr(raddr),
 61                 OriginalDestConnectionID: origDestConnID.Bytes(),
 62                 RetrySrcConnectionID:     retrySrcConnID.Bytes(),
 63                 Timestamp:                time.Now().UnixNano(),
 64         })
 65         if err != nil {
 66                 return nil, err
 67         }
 68         return g.tokenProtector.NewToken(data)
 69 }

it creates token (blob), encodes it as asn1 and passes it to protector at line 68.

  37 // NewToken encodes data into a new token.
 38 func (s *tokenProtectorImpl) NewToken(data []byte) ([]byte, error) {
 39         var nonce [tokenNonceSize]byte
 40         if _, err := rand.Read(nonce[:]); err != nil {
 41                 return nil, err
 42         }
 43         aead, aeadNonce, err := s.createAEAD(nonce[:])
 44         if err != nil {
 45                 return nil, err
 46         }
 47         return append(nonce[:], aead.Seal(nil, aeadNonce, data, nil)...), nil
 48 }

at line 47 we append token protected by aead to nonce. So this is something what appears in packet: [ 32 bytes nonce | token sealed by aead ]

The verification is reverse process.

t8m commented 15 hours ago

For MVP the key should be just ephemeral, generated at server start up.