Compare commits

...

6 Commits

Author SHA1 Message Date
nervuri 6bbec77a83 frontend: add special case for RENEGOTIATION_INFO_SCSV cipher suite
TLS_EMPTY_RENEGOTIATION_INFO_SCSV is not a real cipher suite, it is a
signal indicating support for secure renegotiation.  This commit removes
its broken ciphersuite.info link and its misapplied "(not recommended)"
string from the HTML and gemtext frontends.
2023-09-30 15:43:06 +00:00
nervuri 690bb167a4 more text tweaks 2023-09-30 15:30:45 +00:00
nervuri 763458fbf1 refactor isGREASE8 and isGREASE16 into isGREASE 2023-09-26 10:07:32 +00:00
nervuri b1e820afcf decode delegated_credential extension 2023-09-26 08:22:34 +00:00
nervuri 3b852c0669 decode record_size_limit extension 2023-09-26 08:16:04 +00:00
nervuri 1c333f8733 another small text tweak 2023-09-26 08:12:27 +00:00
11 changed files with 86 additions and 62 deletions

2
DOC.md
View File

@ -40,7 +40,7 @@ The outline of the `v1` output is:
"ocsp_stapling_support": false,
"sct_support": false,
"ja3": "...",
"ja3_md5": "..."
"ja3_md5": "...",
"nja3v1": "...",
"nja3v1_sha256_128": "..."
}

View File

@ -93,4 +93,4 @@ On a final note, string-based fingerprinting is fundamentally limited compared t
## Implementation
The first implementation is written in Go and can be found [here](https://tildegit.org/nervuri/client-hello-mirror/src/branch/master/clienthello/fingerprint.go#L69). This code is part of the TLS Client Hello Mirror, a live instance of which is running at [tlsprivacy.nervuri.net](https://tlsprivacy.nervuri.net/), which will (among other things) generate the NJA3 fingerprint of any HTTPS or [Gemini](https://geminiprotocol.net/) client you connect to it.
The first implementation is written in Go and can be found [here](https://tildegit.org/nervuri/client-hello-mirror/src/branch/master/clienthello/fingerprint.go#L69). This code is part of TLS Client Hello Mirror, a live instance of which is running at [tlsprivacy.nervuri.net](https://tlsprivacy.nervuri.net/), which will (among other things) generate the NJA3 fingerprint of any HTTPS or [Gemini](https://geminiprotocol.net/) client you connect to it.

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: BSD-3-Clause
This test:
* reflects the complete [Client Hello](https://tls13.xargs.org/#client-hello) message in multiple forms, preserving the order in which TLS parameters and extensions are sent;
* reflects the complete [Client Hello](https://tls13.xargs.org/#client-hello) message in multiple formats, preserving the order in which TLS parameters and extensions are sent;
* can be used to check for TLS privacy pitfalls ([session resumption](https://svs.informatik.uni-hamburg.de/publications/2018/2018-12-06-Sy-ACSAC-Tracking_Users_across_the_Web_via_TLS_Session_Resumption.pdf), [TLS fingerprinting](https://tlsfingerprint.io/), [system time exposure](https://datatracker.ietf.org/doc/html/draft-mathewson-no-gmtunixtime));
* supports both HTTP and [Gemini](https://geminiprotocol.net/) on the same port;
* is [free as in freedom](https://www.gnu.org/philosophy/free-sw.en.html) and trivial to self-host.

View File

@ -36,7 +36,7 @@ type CipherSuiteInfo = struct {
// TLS signaling cipher suite values
const (
scsvRenegotiation uint16 = 0x00ff
SCSVRenegotiation uint16 = 0x00ff
)
func parseCipherSuitesCSV() map[uint16]CipherSuiteInfo {

View File

@ -18,33 +18,31 @@ import (
"golang.org/x/crypto/cryptobyte"
)
// Check if this is a 16-bit GREASE value (RFC 8701).
func isGREASE16(value uint16) bool {
// Check if this is a GREASE value (RFC 8701).
func isGREASE[N uint16 | uint8](val N) bool {
// Values for: cipher suites, ALPN,
// extensions, named groups, signature algorithms and TLS versions:
greaseValues := [16]uint16{
greaseValues16 := [16]uint16{
0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a,
0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa,
}
// Run the check
for _, greaseValue := range greaseValues {
if value == greaseValue {
return true
}
}
return false
}
// Check if this is an 8-bit GREASE value (PskKeyExchangeModes) (RFC 8701).
func isGREASE8(value uint8) bool {
// Values for PskKeyExchangeModes:
greaseValues := [8]uint8{
greaseValues8 := [8]uint8{
0x0B, 0x2A, 0x49, 0x68, 0x87, 0xA6, 0xC5, 0xE4,
}
// Run the check
for _, greaseValue := range greaseValues {
if value == greaseValue {
return true
switch any(val).(type) {
case uint16:
for _, greaseValue := range greaseValues16 {
if any(val).(uint16) == greaseValue {
return true
}
}
case uint8:
for _, greaseValue := range greaseValues8 {
if any(val).(uint8) == greaseValue {
return true
}
}
}
return false
@ -127,7 +125,7 @@ func (m *ClientHelloMsg) Unmarshal(data []byte) bool {
if !cipherSuites.ReadUint16(&suite) {
return false
}
if suite == scsvRenegotiation {
if suite == SCSVRenegotiation {
m.Highlights.SecureRenegotiationSupport = true
}
m.CipherSuites = append(m.CipherSuites, suite)
@ -274,6 +272,22 @@ func (m *ClientHelloMsg) Unmarshal(data []byte) bool {
extension.Data.SupportedSignatureAlgorithms = append(
extension.Data.SupportedSignatureAlgorithms, sigAndAlg)
}
case extensionDelegatedCredential:
// Delegated Credential
// RFC 9345
extension.Data.SupportedSignatureAlgorithms = []SignatureScheme{}
var sigAndAlgs cryptobyte.String
if !extData.ReadUint16LengthPrefixed(&sigAndAlgs) || sigAndAlgs.Empty() {
return false
}
for !sigAndAlgs.Empty() {
var sigAndAlg uint16
if !sigAndAlgs.ReadUint16(&sigAndAlg) {
return false
}
extension.Data.SupportedSignatureAlgorithms = append(
extension.Data.SupportedSignatureAlgorithms, sigAndAlg)
}
case extensionRenegotiationInfo:
// Renegotiation Indication
// RFC 5746, Section 3.2
@ -415,6 +429,12 @@ func (m *ClientHelloMsg) Unmarshal(data []byte) bool {
extension.Data.CertificateCompressionAlgos = append(
extension.Data.CertificateCompressionAlgos, algo)
}
case extensionRecordSizeLimit:
// Record Size Limit
// RFC 8449
if !extData.ReadUint16(&extension.Data.RecordSizeLimit) {
return false
}
// Draft extensions (not IANA assigned)
case extensionApplicationSettings:
// Application-Layer Protocol Settings

View File

@ -39,6 +39,8 @@ const (
extensionExtendedMasterSecret uint16 = 23
extensionTokenBinding uint16 = 24
extensionCompressCertificate uint16 = 27
extensionRecordSizeLimit uint16 = 28
extensionDelegatedCredential uint16 = 34
extensionSessionTicket uint16 = 35
extensionPreSharedKey uint16 = 41
extensionEarlyData uint16 = 42
@ -86,6 +88,7 @@ type ExtensionData struct {
PSKModes []PSKMode `json:"psk_modes,omitempty"`
PSKIdentities []PSKIdentity `json:"psk_identities,omitempty"`
PSKBinders []byteSlice `json:"psk_binders,omitempty"`
RecordSizeLimit uint16 `json:"record_size_limit,omitempty"`
Length uint16 `json:"length,omitempty"` // padding
}

View File

@ -12,28 +12,21 @@ import (
"strings"
)
func toString[N uint8 | uint16](val N) string {
func toString[N uint16 | uint8](val N) string {
return strconv.FormatUint(uint64(val), 10)
}
func deGREASE16(val uint16, greaseReplacement string) string {
if isGREASE16(val) {
if greaseReplacement == "" {
return ""
func deGREASE[N uint16 | uint8](val N, replace bool) string {
if isGREASE(val) {
if replace {
switch any(val).(type) {
case uint16:
return "2570-" // generic 16-bit GREASE code 0x0A0A (2570)
default: // uint8
return "11-" // generic 8-bit GREASE code 0x0B (11)
}
} else {
return greaseReplacement + "-"
}
} else {
return toString(val) + "-"
}
}
func deGREASE8(val uint8, greaseReplacement string) string {
if isGREASE8(val) {
if greaseReplacement == "" {
return ""
} else {
return greaseReplacement + "-"
}
} else {
return toString(val) + "-"
@ -44,14 +37,14 @@ func (m *ClientHelloMsg) ja3() {
var codeGroups [5]string
codeGroups[0] = toString(m.TLSVersion.(uint16))
for _, cs := range m.CipherSuites {
codeGroups[1] += deGREASE16(cs.(uint16), "")
codeGroups[1] += deGREASE(cs.(uint16), false)
}
for _, e := range m.Extensions {
codeGroups[2] += deGREASE16(e.Code, "")
codeGroups[2] += deGREASE(e.Code, false)
switch e.Code {
case extensionSupportedGroups:
for _, g := range e.Data.SupportedGroups {
codeGroups[3] += deGREASE16(g.(uint16), "")
codeGroups[3] += deGREASE(g.(uint16), false)
}
case extensionSupportedPointFormats:
for _, pf := range e.Data.SupportedPointFormats {
@ -69,15 +62,12 @@ func (m *ClientHelloMsg) ja3() {
func (m *ClientHelloMsg) nja3() {
const genericGREASECode16 = uint16(0x0a0a) // 2570
const genericGREASECode8 = uint8(0x0B) // 11
var genericGreaseString16 = toString(genericGREASECode16)
var genericGreaseString8 = toString(genericGREASECode8)
var codeGroups [10]string
var extCodes []uint16
codeGroups[0] = toString(m.RecordHeaderTLSVersion.(uint16))
codeGroups[1] = toString(m.TLSVersion.(uint16))
for _, cs := range m.CipherSuites {
codeGroups[2] += deGREASE16(cs.(uint16), genericGreaseString16)
codeGroups[2] += deGREASE(cs.(uint16), true)
}
for _, e := range m.Extensions {
// Ignore conditional extensions.
@ -92,14 +82,14 @@ func (m *ClientHelloMsg) nja3() {
e.Code == extensionChannelIDOld {
continue
}
if isGREASE16(e.Code) {
if isGREASE(e.Code) {
e.Code = genericGREASECode16
}
extCodes = append(extCodes, e.Code)
switch e.Code {
case extensionSupportedGroups:
for _, g := range e.Data.SupportedGroups {
codeGroups[4] += deGREASE16(g.(uint16), genericGreaseString16)
codeGroups[4] += deGREASE(g.(uint16), true)
}
case extensionSupportedPointFormats:
for _, pf := range e.Data.SupportedPointFormats {
@ -107,15 +97,15 @@ func (m *ClientHelloMsg) nja3() {
}
case extensionSupportedVersions:
for _, v := range e.Data.SupportedVersions {
codeGroups[6] += deGREASE16(v.(uint16), genericGreaseString16)
codeGroups[6] += deGREASE(v.(uint16), true)
}
case extensionSignatureAlgorithms:
for _, sa := range e.Data.SupportedSignatureAlgorithms {
codeGroups[7] += deGREASE16(sa.(uint16), genericGreaseString16)
codeGroups[7] += deGREASE(sa.(uint16), true)
}
case extensionPSKModes:
for _, mode := range e.Data.PSKModes {
codeGroups[8] += deGREASE8(mode.(uint8), genericGreaseString8)
codeGroups[8] += deGREASE(mode.(uint8), true)
}
case extensionCompressCertificate:
for _, algo := range e.Data.CertificateCompressionAlgos {
@ -127,7 +117,7 @@ func (m *ClientHelloMsg) nja3() {
sort.Slice(extCodes, func(i, j int) bool { return extCodes[i] < extCodes[j] })
// Add sorted extension codes to NJA3 string.
for _, code := range extCodes {
codeGroups[3] += deGREASE16(code, genericGreaseString16)
codeGroups[3] += deGREASE(code, true)
}
for i, cg := range codeGroups {
codeGroups[i] = strings.TrimSuffix(cg, "-")

View File

@ -125,7 +125,7 @@ func GetSignatureSchemeInfo(sigSchemeCode uint16, mustName bool) SignatureScheme
Code: sigSchemeCode,
HexCode: fmt.Sprintf("%04X", sigSchemeCode),
}
if isGREASE16(sigSchemeCode) {
if isGREASE(sigSchemeCode) {
// As of September 2023, the IANA signature scheme registry
// doesn't include GREASE values, so we need to check here.
info.Name = "GREASE"
@ -166,8 +166,10 @@ func GetSignatureSchemeInfo(sigSchemeCode uint16, mustName bool) SignatureScheme
func (m *ClientHelloMsg) AddSignatureSchemeInfo() {
for i, ext := range m.Extensions {
if ext.Code == extensionSignatureAlgorithms ||
ext.Code == extensionSignatureAlgorithmsCert {
switch ext.Code {
case extensionSignatureAlgorithms,
extensionSignatureAlgorithmsCert,
extensionDelegatedCredential:
for j, sigAlg := range ext.Data.SupportedSignatureAlgorithms {
m.Extensions[i].Data.SupportedSignatureAlgorithms[j] =
GetSignatureSchemeInfo(sigAlg.(uint16), false)

View File

@ -69,8 +69,11 @@ func getCipherSuiteHTML(cs clienthello.CipherSuite) string {
s = "<span class=\"dim\">0x" + csInfo.HexCode + " (GREASE)</span>"
} else {
visibleName := strings.Join(strings.Split(csInfo.Name, "_"), "_<wbr/>")
if csInfo.Recommended {
if csInfo.HexCode[:2] == "13" {
if csInfo.Code == clienthello.SCSVRenegotiation {
// TLS_EMPTY_RENEGOTIATION_INFO_SCSV
s = visibleName
} else if csInfo.Recommended {
if csInfo.HexCode[:2] == "13" { // TLS 1.3 cipher suites
s = "<a href=\"https://ciphersuite.info/cs/" + csInfo.Name +
"/\" class=\"good\" target=\"_blank\">" + visibleName + "</a>"
} else {
@ -224,6 +227,9 @@ func getCipherSuiteGemtext(cs clienthello.CipherSuite, link bool) string {
csInfo := clienthello.GetCipherSuiteInfo(cs.(uint16), true)
if csInfo.Name == "GREASE" {
s = "0x" + csInfo.HexCode + " (GREASE)"
} else if csInfo.Code == clienthello.SCSVRenegotiation {
// TLS_EMPTY_RENEGOTIATION_INFO_SCSV
s = csInfo.Name
} else {
if link {
s = "=> https://ciphersuite.info/cs/" + csInfo.Name + "/ "

View File

@ -1,6 +1,6 @@
# TLS Client Hello Mirror
This service reflects your browser's TLS Client Hello message in multiple forms. It can be used directly or in CI tests to check for TLS privacy pitfalls (session resumption, fingerprinting, system time exposure) and security shortcommings (deprecated TLS versions, weak cipher suites, missing features, etc).
This service presents your browser's TLS Client Hello message in multiple formats. It can be used directly or in CI tests to check for TLS privacy pitfalls (session resumption, fingerprinting, system time exposure) and security shortcomings (deprecated TLS versions, weak cipher suites, missing features, etc).
=> https://tildegit.org/nervuri/client-hello-mirror#tls-client-hello-mirror Details here
@ -50,7 +50,9 @@ This service reflects your browser's TLS Client Hello message in multiple forms.
* NJA3v1: {{.NJA3v1}}
* NJA3v1 SHA256/128: {{.NJA3v1Hash}}
Parameters in the Client Hello message differ between clients, enabling servers and on-path observers to detect what browser you are likely using (down to its version, or a range of versions) by deriving its fingerprint from said parameters. JA3 is a simple and popular type of TLS fingerprint. NJA3 is a similar style of fingerprint which aims to improve the robustness and accuracy of JA3.
Parameters in the Client Hello message differ between clients, enabling servers and on-path observers to detect what browser you are likely using (down to its version, or a range of versions) by deriving its fingerprint from said parameters. Worse, if you change any TLS-related settings, your TLS fingerprint becomes specific to a much smaller group of users, possibly even to you alone.
JA3 is a simple and popular type of TLS fingerprint. NJA3 is a similar style of fingerprint which aims to improve the robustness and accuracy of JA3.
=> https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/ TLS Fingerprinting with JA3 and JA3S
=> https://tildegit.org/nervuri/client-hello-mirror/src/branch/master/NJA3.md NJA3 documentation

View File

@ -20,7 +20,7 @@ SPDX-License-Identifier: BSD-3-Clause
<main>
<h1>TLS Client Hello Mirror</h1>
<p>This service reflects your browser's TLS <a href="https://tls13.xargs.org/#client-hello" target="_blank">Client Hello</a> message in multiple forms. It can be used directly or in CI tests to check for TLS privacy pitfalls (<a href="https://svs.informatik.uni-hamburg.de/publications/2018/2018-12-06-Sy-ACSAC-Tracking_Users_across_the_Web_via_TLS_Session_Resumption.pdf" target="_blank">session resumption</a>, <a href="https://tlsfingerprint.io/" target="_blank">fingerprinting</a>, <a href="https://datatracker.ietf.org/doc/html/draft-mathewson-no-gmtunixtime" target="_blank">system time exposure</a>) and security shortcommings (deprecated TLS versions, weak cipher suites, missing features, etc). <a href="https://tildegit.org/nervuri/client-hello-mirror#tls-client-hello-mirror" target="_blank">Details here</a>.</p>
<p>This service presents your browser's TLS <a href="https://tls13.xargs.org/#client-hello" target="_blank">Client Hello</a> message in multiple formats. It can be used directly or in CI tests to check for TLS privacy pitfalls (<a href="https://svs.informatik.uni-hamburg.de/publications/2018/2018-12-06-Sy-ACSAC-Tracking_Users_across_the_Web_via_TLS_Session_Resumption.pdf" target="_blank">session resumption</a>, <a href="https://tlsfingerprint.io/" target="_blank">fingerprinting</a>, <a href="https://datatracker.ietf.org/doc/html/draft-mathewson-no-gmtunixtime" target="_blank">system time exposure</a>) and security shortcomings (deprecated TLS versions, weak cipher suites, missing features, etc). <a href="https://tildegit.org/nervuri/client-hello-mirror#tls-client-hello-mirror" target="_blank">Details here</a>.</p>
<h3>API endpoints</h3>
<ul>
@ -105,7 +105,8 @@ SPDX-License-Identifier: BSD-3-Clause
<li><b>NJA3v1:</b> <code>{{.NJA3v1}}</code></li>
<li><b>NJA3v1 SHA256/128:</b> <code class="hash">{{.NJA3v1Hash}}</code></li>
</ul>
<p>Parameters in the Client Hello message differ between clients, enabling servers and on-path observers to detect what browser you are likely using (down to its version, or a range of versions) by deriving its fingerprint from said parameters. JA3 is a simple and popular type of TLS fingerprint. NJA3 is a similar style of fingerprint which aims to improve the robustness and accuracy of JA3.</p>
<p>Parameters in the Client Hello message differ between clients, enabling servers and on-path observers to detect what browser you are likely using (down to its version, or a range of versions) by deriving its fingerprint from said parameters. Worse, if you change any TLS-related settings, your TLS fingerprint becomes specific to a much smaller group of users, possibly even to you alone.</p>
<p>JA3 is a simple and popular type of TLS fingerprint. NJA3 is a similar style of fingerprint which aims to improve the robustness and accuracy of JA3.</p>
<ul>
<li><a href="https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/" target="_blank">TLS Fingerprinting with JA3 and JA3S</a></li>
<li><a href="https://tildegit.org/nervuri/client-hello-mirror/src/branch/master/NJA3.md" target="_blank">NJA3 documentation</a></li>