| 1 | package keyless |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "crypto/ecdh" |
| 6 | "crypto/hkdf" |
| 7 | "crypto/sha256" |
| 8 | "crypto/tls" |
| 9 | "encoding/binary" |
| 10 | "errors" |
| 11 | "fmt" |
| 12 | "strings" |
| 13 | |
| 14 | "github.com/gosuda/portal-tunnel/v2/utils" |
| 15 | ) |
| 16 | |
| 17 | const ( |
| 18 | echConfigVersion = 0xfe0d |
| 19 | echKEMX25519 = 0x0020 |
| 20 | echKDFHKDFSHA256 = 0x0001 |
| 21 | echAEADAES128GCM = 0x0001 |
| 22 | echMaximumNameLength = 255 |
| 23 | echMaxConfigListLength = 4096 |
| 24 | echX25519PrivateLength = 32 |
| 25 | echHKDFInfoPrefix = "portal relay ech v1:" |
| 26 | ) |
| 27 | |
| 28 | // MinTLSVersion returns the minimum TLS version required when ECH is enabled. |
| 29 | func MinTLSVersion(echEnabled bool) uint16 { |
| 30 | if echEnabled { |
| 31 | return tls.VersionTLS13 |
| 32 | } |
| 33 | return tls.VersionTLS12 |
| 34 | } |
| 35 | |
| 36 | func EncryptedClientHelloMaterials(seed, publicName string) ([]tls.EncryptedClientHelloKey, []byte, error) { |
| 37 | publicName = utils.NormalizeHostname(publicName) |
| 38 | if publicName == "" { |
| 39 | return nil, nil, errors.New("ech public name is required") |
| 40 | } |
| 41 | seed = strings.TrimSpace(seed) |
| 42 | if seed == "" { |
| 43 | return nil, nil, errors.New("ech seed is required") |
| 44 | } |
| 45 | |
| 46 | if len(publicName) > echMaximumNameLength { |
| 47 | return nil, nil, errors.New("ech public name is too long") |
| 48 | } |
| 49 | |
| 50 | privateKey, err := hkdf.Key(sha256.New, []byte(seed), nil, echHKDFInfoPrefix+publicName, echX25519PrivateLength) |
| 51 | if err != nil { |
| 52 | return nil, nil, fmt.Errorf("derive ech private key: %w", err) |
| 53 | } |
| 54 | key, err := ecdh.X25519().NewPrivateKey(privateKey) |
| 55 | if err != nil { |
| 56 | return nil, nil, fmt.Errorf("parse ech private key: %w", err) |
| 57 | } |
| 58 | publicKey := key.PublicKey().Bytes() |
| 59 | configID := sha256.Sum256(bytes.Join([][]byte{ |
| 60 | []byte("portal relay ech config id v1"), |
| 61 | []byte(publicName), |
| 62 | publicKey, |
| 63 | }, []byte{0}))[0] |
| 64 | |
| 65 | writeUint16 := func(buf *bytes.Buffer, value uint16) { |
| 66 | var out [2]byte |
| 67 | binary.BigEndian.PutUint16(out[:], value) |
| 68 | buf.Write(out[:]) |
| 69 | } |
| 70 | writeUint16LengthPrefixed := func(buf *bytes.Buffer, data []byte) { |
| 71 | writeUint16(buf, uint16(len(data))) |
| 72 | buf.Write(data) |
| 73 | } |
| 74 | |
| 75 | var body bytes.Buffer |
| 76 | body.WriteByte(configID) |
| 77 | writeUint16(&body, echKEMX25519) |
| 78 | writeUint16LengthPrefixed(&body, publicKey) |
| 79 | |
| 80 | var cipherSuites bytes.Buffer |
| 81 | writeUint16(&cipherSuites, echKDFHKDFSHA256) |
| 82 | writeUint16(&cipherSuites, echAEADAES128GCM) |
| 83 | writeUint16LengthPrefixed(&body, cipherSuites.Bytes()) |
| 84 | |
| 85 | body.WriteByte(echMaximumNameLength) |
| 86 | body.WriteByte(byte(len(publicName))) |
| 87 | body.WriteString(publicName) |
| 88 | writeUint16(&body, 0) |
| 89 | |
| 90 | var out bytes.Buffer |
| 91 | writeUint16(&out, echConfigVersion) |
| 92 | writeUint16LengthPrefixed(&out, body.Bytes()) |
| 93 | |
| 94 | keys := []tls.EncryptedClientHelloKey{{ |
| 95 | Config: out.Bytes(), |
| 96 | PrivateKey: privateKey, |
| 97 | SendAsRetry: true, |
| 98 | }} |
| 99 | |
| 100 | var configList bytes.Buffer |
| 101 | var configListLength [2]byte |
| 102 | binary.BigEndian.PutUint16(configListLength[:], uint16(len(keys[0].Config))) |
| 103 | configList.Write(configListLength[:]) |
| 104 | configList.Write(keys[0].Config) |
| 105 | |
| 106 | return keys, configList.Bytes(), nil |
| 107 | } |
| 108 | |
| 109 | func NormalizeEncryptedClientHelloConfigList(raw []byte) ([]byte, error) { |
| 110 | if len(raw) == 0 { |
| 111 | return nil, errors.New("ech config list is required") |
| 112 | } |
| 113 | if len(raw) > echMaxConfigListLength { |
| 114 | return nil, errors.New("ech config list is too large") |
| 115 | } |
| 116 | if len(raw) < 2 { |
| 117 | return nil, errors.New("ech config list is invalid") |
| 118 | } |
| 119 | listLength := int(binary.BigEndian.Uint16(raw[:2])) |
| 120 | if listLength != len(raw)-2 { |
| 121 | return nil, errors.New("ech config list length prefix is invalid") |
| 122 | } |
| 123 | return bytes.Clone(raw), nil |
| 124 | } |