mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-10 02:53:12 +00:00
303 lines
9.3 KiB
Go
303 lines
9.3 KiB
Go
|
// Copyright 2020 Cloudflare, Inc. All rights reserved. Use of this source code
|
||
|
// is governed by a BSD-style license that can be found in the LICENSE file.
|
||
|
|
||
|
package tls
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
|
||
|
"github.com/cloudflare/circl/hpke"
|
||
|
"github.com/cloudflare/circl/kem"
|
||
|
"golang.org/x/crypto/cryptobyte"
|
||
|
)
|
||
|
|
||
|
// ECHProvider specifies the interface of an ECH service provider that decrypts
|
||
|
// the ECH payload on behalf of the client-facing server. It also defines the
|
||
|
// set of acceptable ECH configurations.
|
||
|
type ECHProvider interface {
|
||
|
// GetDecryptionContext attempts to construct the HPKE context used by the
|
||
|
// client-facing server for decryption. (See draft-irtf-cfrg-hpke-07,
|
||
|
// Section 5.2.)
|
||
|
//
|
||
|
// handle encodes the parameters of the client's "encrypted_client_hello"
|
||
|
// extension that are needed to construct the context. Since
|
||
|
// draft-ietf-tls-esni-10 these are the ECH cipher suite, the identity of
|
||
|
// the ECH configuration, and the encapsulated key.
|
||
|
//
|
||
|
// version is the version of ECH indicated by the client.
|
||
|
//
|
||
|
// res.Status == ECHProviderStatusSuccess indicates the call was successful
|
||
|
// and the caller may proceed. res.Context is set.
|
||
|
//
|
||
|
// res.Status == ECHProviderStatusReject indicates the caller must reject
|
||
|
// ECH. res.RetryConfigs may be set.
|
||
|
//
|
||
|
// res.Status == ECHProviderStatusAbort indicates the caller should abort
|
||
|
// the handshake. Note that, in some cases, it's appropriate to reject
|
||
|
// rather than abort. In particular, aborting with "illegal_parameter" might
|
||
|
// "stick out". res.Alert and res.Error are set.
|
||
|
GetDecryptionContext(handle []byte, version uint16) (res ECHProviderResult)
|
||
|
}
|
||
|
|
||
|
// ECHProviderStatus is the status of the ECH provider's response.
|
||
|
type ECHProviderStatus uint
|
||
|
|
||
|
const (
|
||
|
ECHProviderSuccess ECHProviderStatus = 0
|
||
|
ECHProviderReject = 1
|
||
|
ECHProviderAbort = 2
|
||
|
|
||
|
errHPKEInvalidPublicKey = "hpke: invalid KEM public key"
|
||
|
)
|
||
|
|
||
|
// ECHProviderResult represents the result of invoking the ECH provider.
|
||
|
type ECHProviderResult struct {
|
||
|
Status ECHProviderStatus
|
||
|
|
||
|
// Alert is the TLS alert sent by the caller when aborting the handshake.
|
||
|
Alert uint8
|
||
|
|
||
|
// Error is the error propagated by the caller when aborting the handshake.
|
||
|
Error error
|
||
|
|
||
|
// RetryConfigs is the sequence of ECH configs to offer to the client for
|
||
|
// retrying the handshake. This may be set in case of success or rejection.
|
||
|
RetryConfigs []byte
|
||
|
|
||
|
// Context is the server's HPKE context. This is set if ECH is not rejected
|
||
|
// by the provider and no error was reported. The data has the following
|
||
|
// format (in TLS syntax):
|
||
|
//
|
||
|
// enum { sealer(0), opener(1) } HpkeRole;
|
||
|
//
|
||
|
// struct {
|
||
|
// HpkeRole role;
|
||
|
// HpkeKemId kem_id; // as defined in draft-irtf-cfrg-hpke-07
|
||
|
// HpkeKdfId kdf_id; // as defined in draft-irtf-cfrg-hpke-07
|
||
|
// HpkeAeadId aead_id; // as defined in draft-irtf-cfrg-hpke-07
|
||
|
// opaque exporter_secret<0..255>;
|
||
|
// opaque key<0..255>;
|
||
|
// opaque base_nonce<0..255>;
|
||
|
// opaque seq<0..255>;
|
||
|
// } HpkeContext;
|
||
|
Context []byte
|
||
|
}
|
||
|
|
||
|
// EXP_ECHKeySet implements the ECHProvider interface for a sequence of ECH keys.
|
||
|
//
|
||
|
// NOTE: This API is EXPERIMENTAL and subject to change.
|
||
|
type EXP_ECHKeySet struct {
|
||
|
// The serialized ECHConfigs, in order of the server's preference.
|
||
|
configs []byte
|
||
|
|
||
|
// Maps a configuration identifier to its secret key.
|
||
|
sk map[uint8]EXP_ECHKey
|
||
|
}
|
||
|
|
||
|
// EXP_NewECHKeySet constructs an EXP_ECHKeySet.
|
||
|
func EXP_NewECHKeySet(keys []EXP_ECHKey) (*EXP_ECHKeySet, error) {
|
||
|
if len(keys) > 255 {
|
||
|
return nil, fmt.Errorf("tls: ech provider: unable to support more than 255 ECH configurations at once")
|
||
|
}
|
||
|
|
||
|
keySet := new(EXP_ECHKeySet)
|
||
|
keySet.sk = make(map[uint8]EXP_ECHKey)
|
||
|
configs := make([]byte, 0)
|
||
|
for _, key := range keys {
|
||
|
if _, ok := keySet.sk[key.config.configId]; ok {
|
||
|
return nil, fmt.Errorf("tls: ech provider: ECH config conflict for configId %d", key.config.configId)
|
||
|
}
|
||
|
|
||
|
keySet.sk[key.config.configId] = key
|
||
|
configs = append(configs, key.config.raw...)
|
||
|
}
|
||
|
|
||
|
var b cryptobyte.Builder
|
||
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||
|
b.AddBytes(configs)
|
||
|
})
|
||
|
keySet.configs = b.BytesOrPanic()
|
||
|
|
||
|
return keySet, nil
|
||
|
}
|
||
|
|
||
|
// GetDecryptionContext is required by the ECHProvider interface.
|
||
|
func (keySet *EXP_ECHKeySet) GetDecryptionContext(rawHandle []byte, version uint16) (res ECHProviderResult) {
|
||
|
// Propagate retry configurations regardless of the result. The caller sends
|
||
|
// these to the clients only if it rejects.
|
||
|
res.RetryConfigs = keySet.configs
|
||
|
|
||
|
// Ensure we know how to proceed, i.e., the caller has indicated a supported
|
||
|
// version of ECH. Currently only draft-ietf-tls-esni-13 is supported.
|
||
|
if version != extensionECH {
|
||
|
res.Status = ECHProviderAbort
|
||
|
res.Alert = uint8(alertInternalError)
|
||
|
res.Error = errors.New("version not supported")
|
||
|
return // Abort
|
||
|
}
|
||
|
|
||
|
// Parse the handle.
|
||
|
s := cryptobyte.String(rawHandle)
|
||
|
handle := new(echContextHandle)
|
||
|
if !echReadContextHandle(&s, handle) || !s.Empty() {
|
||
|
// This is the result of a client-side error. However, aborting with
|
||
|
// "illegal_parameter" would stick out, so we reject instead.
|
||
|
res.Status = ECHProviderReject
|
||
|
res.RetryConfigs = keySet.configs
|
||
|
return // Reject
|
||
|
}
|
||
|
handle.raw = rawHandle
|
||
|
|
||
|
// Look up the secret key for the configuration indicated by the client.
|
||
|
key, ok := keySet.sk[handle.configId]
|
||
|
if !ok {
|
||
|
res.Status = ECHProviderReject
|
||
|
res.RetryConfigs = keySet.configs
|
||
|
return // Reject
|
||
|
}
|
||
|
|
||
|
// Ensure that support for the selected ciphersuite is indicated by the
|
||
|
// configuration.
|
||
|
suite := handle.suite
|
||
|
if !key.config.isPeerCipherSuiteSupported(suite) {
|
||
|
// This is the result of a client-side error. However, aborting with
|
||
|
// "illegal_parameter" would stick out, so we reject instead.
|
||
|
res.Status = ECHProviderReject
|
||
|
res.RetryConfigs = keySet.configs
|
||
|
return // Reject
|
||
|
}
|
||
|
|
||
|
// Ensure the version indicated by the client matches the version supported
|
||
|
// by the configuration.
|
||
|
if version != key.config.version {
|
||
|
// This is the result of a client-side error. However, aborting with
|
||
|
// "illegal_parameter" would stick out, so we reject instead.
|
||
|
res.Status = ECHProviderReject
|
||
|
res.RetryConfigs = keySet.configs
|
||
|
return // Reject
|
||
|
}
|
||
|
|
||
|
// Compute the decryption context.
|
||
|
opener, err := key.setupOpener(handle.enc, suite)
|
||
|
if err != nil {
|
||
|
if err.Error() == errHPKEInvalidPublicKey {
|
||
|
// This occurs if the KEM algorithm used to generate handle.enc is
|
||
|
// not the same as the KEM algorithm of the key. One way this can
|
||
|
// happen is if the client sent a GREASE ECH extension with a
|
||
|
// config_id that happens to match a known config, but which uses a
|
||
|
// different KEM algorithm.
|
||
|
res.Status = ECHProviderReject
|
||
|
res.RetryConfigs = keySet.configs
|
||
|
return // Reject
|
||
|
}
|
||
|
|
||
|
res.Status = ECHProviderAbort
|
||
|
res.Alert = uint8(alertInternalError)
|
||
|
res.Error = err
|
||
|
return // Abort
|
||
|
}
|
||
|
|
||
|
// Serialize the decryption context.
|
||
|
res.Context, err = opener.MarshalBinary()
|
||
|
if err != nil {
|
||
|
res.Status = ECHProviderAbort
|
||
|
res.Alert = uint8(alertInternalError)
|
||
|
res.Error = err
|
||
|
return // Abort
|
||
|
}
|
||
|
|
||
|
res.Status = ECHProviderSuccess
|
||
|
return // Success
|
||
|
}
|
||
|
|
||
|
// EXP_ECHKey represents an ECH key and its corresponding configuration. The
|
||
|
// encoding of an ECH Key has the format defined below (in TLS syntax). Note
|
||
|
// that the ECH standard does not specify this format.
|
||
|
//
|
||
|
// struct {
|
||
|
// opaque sk<0..2^16-1>;
|
||
|
// ECHConfig config<0..2^16>; // draft-ietf-tls-esni-13
|
||
|
// } ECHKey;
|
||
|
type EXP_ECHKey struct {
|
||
|
sk kem.PrivateKey
|
||
|
config ECHConfig
|
||
|
}
|
||
|
|
||
|
// EXP_UnmarshalECHKeys parses a sequence of ECH keys.
|
||
|
func EXP_UnmarshalECHKeys(raw []byte) ([]EXP_ECHKey, error) {
|
||
|
var (
|
||
|
err error
|
||
|
key EXP_ECHKey
|
||
|
sk, config, contents cryptobyte.String
|
||
|
)
|
||
|
s := cryptobyte.String(raw)
|
||
|
keys := make([]EXP_ECHKey, 0)
|
||
|
KeysLoop:
|
||
|
for !s.Empty() {
|
||
|
if !s.ReadUint16LengthPrefixed(&sk) ||
|
||
|
!s.ReadUint16LengthPrefixed(&config) {
|
||
|
return nil, errors.New("error parsing key")
|
||
|
}
|
||
|
|
||
|
key.config.raw = config
|
||
|
if !config.ReadUint16(&key.config.version) ||
|
||
|
!config.ReadUint16LengthPrefixed(&contents) ||
|
||
|
!config.Empty() {
|
||
|
return nil, errors.New("error parsing config")
|
||
|
}
|
||
|
|
||
|
if key.config.version != extensionECH {
|
||
|
continue KeysLoop
|
||
|
}
|
||
|
if !readConfigContents(&contents, &key.config) {
|
||
|
return nil, errors.New("error parsing config contents")
|
||
|
}
|
||
|
|
||
|
for _, suite := range key.config.suites {
|
||
|
if !hpke.KDF(suite.kdfId).IsValid() ||
|
||
|
!hpke.AEAD(suite.aeadId).IsValid() {
|
||
|
continue KeysLoop
|
||
|
}
|
||
|
}
|
||
|
|
||
|
kem := hpke.KEM(key.config.kemId)
|
||
|
if !kem.IsValid() {
|
||
|
continue KeysLoop
|
||
|
}
|
||
|
key.config.pk, err = kem.Scheme().UnmarshalBinaryPublicKey(key.config.rawPublicKey)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("error parsing public key: %s", err)
|
||
|
}
|
||
|
key.sk, err = kem.Scheme().UnmarshalBinaryPrivateKey(sk)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("error parsing secret key: %s", err)
|
||
|
}
|
||
|
|
||
|
keys = append(keys, key)
|
||
|
}
|
||
|
return keys, nil
|
||
|
}
|
||
|
|
||
|
// setupOpener computes the HPKE context used by the server in the ECH
|
||
|
// extension.i
|
||
|
func (key *EXP_ECHKey) setupOpener(enc []byte, suite hpkeSymmetricCipherSuite) (hpke.Opener, error) {
|
||
|
if key.config.raw == nil {
|
||
|
panic("raw config not set")
|
||
|
}
|
||
|
hpkeSuite, err := hpkeAssembleSuite(
|
||
|
key.config.kemId,
|
||
|
suite.kdfId,
|
||
|
suite.aeadId,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
info := append(append([]byte(echHpkeInfoSetup), 0), key.config.raw...)
|
||
|
receiver, err := hpkeSuite.NewReceiver(key.sk, info)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return receiver.Setup(enc)
|
||
|
}
|