mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-20 00:56:47 +00:00
Compare commits
86 Commits
v3.21.2
...
dbef40cfc5
| Author | SHA1 | Date | |
|---|---|---|---|
| dbef40cfc5 | |||
| 8b891fb3e7 | |||
| 94125056ab | |||
| 675b37a2fa | |||
| 9d4415d8cc | |||
| 4557f54e2f | |||
| 05623a9e49 | |||
| 42605c1923 | |||
| 9f4801b738 | |||
| 4e6236611a | |||
| 0800aeea50 | |||
| b230f2ece6 | |||
| d44c488ed5 | |||
| 8237129670 | |||
| 8e634995c5 | |||
| 10a685a123 | |||
| 896f50c754 | |||
| 60633fc09c | |||
| 9c5b5c2ac3 | |||
| 4f4a2c3fd8 | |||
| 120a7b3626 | |||
| 7cf3b6fb7b | |||
| 03c9455b0d | |||
| 61ca604ace | |||
| a8caec560e | |||
| df78e29234 | |||
| 6105f32c75 | |||
| da76784290 | |||
| 43cbedafb8 | |||
| 0d33cc5000 | |||
| ed5adb18fb | |||
| 85a91c5572 | |||
| 56d4bfbb71 | |||
| 48a75b0dd7 | |||
| b84663dd7a | |||
| cd8db6fd1c | |||
| a5e0f85a58 | |||
| 6cbe51138a | |||
| 82607efe1c | |||
| 961dc9435f | |||
| b574ccb6ea | |||
| 2569e83e51 | |||
| f34a7ff0ed | |||
| da069a0155 | |||
| 384fa4eb4b | |||
| 0c6e4ffa35 | |||
| 4951244400 | |||
| d65d6ee2e5 | |||
| 097d6f86d3 | |||
| 9894cf9744 | |||
| f84067de3e | |||
| f885bfbcf4 | |||
| f3aac09ecb | |||
| 38d692ebfb | |||
| 1acc7eb7db | |||
| 248fbf5e33 | |||
| 8b12a454ea | |||
| 310fcffc7b | |||
| 318ad16378 | |||
| 8be4246f7e | |||
| e580f89106 | |||
| 01043e033e | |||
| 94b44b383a | |||
| a3b8fabb26 | |||
| 275b30e518 | |||
| bf244e5c86 | |||
| cf9651bb94 | |||
| ba65ffdbc7 | |||
| 4b95ef4d82 | |||
| 951c7c27fb | |||
| e7423a9519 | |||
| d3582fa981 | |||
| 80c852a5b2 | |||
| 51498e3e37 | |||
| b7ef6e1486 | |||
| 0d03f84711 | |||
| 949666724d | |||
| bbe19bf960 | |||
| bfe25e3a46 | |||
| 236c958703 | |||
| e6b312b437 | |||
| 384154c767 | |||
| 45d2e9ea63 | |||
| 86e8a566c7 | |||
| a80fd92018 | |||
| 71063ac5ee |
@ -3,12 +3,6 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
## Kanmon Bridge 3.21.2
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
* BRIDGE-406: Fixed faulty certificate chain validation logic. Made certificate pin checks exclusive to leaf certificates.
|
|
||||||
|
|
||||||
|
|
||||||
## Kanmon Bridge 3.21.1
|
## Kanmon Bridge 3.21.1
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.21.2+git
|
BRIDGE_APP_VERSION?=3.21.1+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
|
|||||||
@ -22,8 +22,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,11 +29,6 @@ type TLSDialer interface {
|
|||||||
DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error)
|
DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SecureTLSDialer interface {
|
|
||||||
DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error)
|
|
||||||
ShouldSkipCertificateChainVerification(address string) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetBasicTransportTimeouts(t *http.Transport) {
|
func SetBasicTransportTimeouts(t *http.Transport) {
|
||||||
t.MaxIdleConns = 100
|
t.MaxIdleConns = 100
|
||||||
t.MaxIdleConnsPerHost = 100
|
t.MaxIdleConnsPerHost = 100
|
||||||
@ -78,35 +71,6 @@ func NewBasicTLSDialer(hostURL string) *BasicTLSDialer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractDomain(hostname string) string {
|
|
||||||
parts := strings.Split(hostname, ".")
|
|
||||||
if len(parts) >= 2 {
|
|
||||||
return strings.Join(parts[len(parts)-2:], ".")
|
|
||||||
}
|
|
||||||
return hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShouldSkipCertificateChainVerification determines whether certificate chain validation should be skipped.
|
|
||||||
// It compares the domain of the requested address with the configured host URL domain.
|
|
||||||
// Returns true if the domains don't match (skip verification), false if they do (perform verification).
|
|
||||||
//
|
|
||||||
// NOTE: This assumes single-part TLDs (.com, .me) and won't handle multi-part TLDs correctly.
|
|
||||||
func (d *BasicTLSDialer) ShouldSkipCertificateChainVerification(address string) bool {
|
|
||||||
parsedURL, err := url.Parse(d.hostURL)
|
|
||||||
if err != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
addressHost, _, err := net.SplitHostPort(address)
|
|
||||||
if err != nil {
|
|
||||||
addressHost = address
|
|
||||||
}
|
|
||||||
|
|
||||||
hostDomain := extractDomain(parsedURL.Host)
|
|
||||||
addressDomain := extractDomain(addressHost)
|
|
||||||
return addressDomain != hostDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialTLSContext returns a connection to the given address using the given network.
|
// DialTLSContext returns a connection to the given address using the given network.
|
||||||
func (d *BasicTLSDialer) DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
func (d *BasicTLSDialer) DialTLSContext(ctx context.Context, network, address string) (conn net.Conn, err error) {
|
||||||
return (&tls.Dialer{
|
return (&tls.Dialer{
|
||||||
@ -114,7 +78,7 @@ func (d *BasicTLSDialer) DialTLSContext(ctx context.Context, network, address st
|
|||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
},
|
},
|
||||||
Config: &tls.Config{
|
Config: &tls.Config{
|
||||||
InsecureSkipVerify: d.ShouldSkipCertificateChainVerification(address), //nolint:gosec
|
InsecureSkipVerify: address != d.hostURL, //nolint:gosec
|
||||||
},
|
},
|
||||||
}).DialContext(ctx, network, address)
|
}).DialContext(ctx, network, address)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,134 +0,0 @@
|
|||||||
// Copyright (c) 2025 Proton AG
|
|
||||||
//
|
|
||||||
// This file is part of Proton Mail Bridge.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBasicTLSDialer_ShouldSkipCertificateChainVerification(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
hostURL string
|
|
||||||
address string
|
|
||||||
expected bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
hostURL: "https://mail-api.proton.me",
|
|
||||||
address: "mail-api.proton.me:443",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://proton.me",
|
|
||||||
address: "proton.me",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://api.proton.me",
|
|
||||||
address: "mail.proton.me:443",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://proton.me",
|
|
||||||
address: "mail-api.proton.me:443",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://mail-api.proton.me",
|
|
||||||
address: "proton.me:443",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://mail.google.com",
|
|
||||||
address: "mail-api.proton.me:443",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://mail-api.protonmail.com",
|
|
||||||
address: "mail-api.proton.me:443",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://proton.me",
|
|
||||||
address: "google.com:443",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://proton.me",
|
|
||||||
address: "proton.com:443",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://proton.me",
|
|
||||||
address: "example.me:443",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://proton.me",
|
|
||||||
address: "mail.example.com:443",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://proton.me",
|
|
||||||
address: "proton.me",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://proton.me:8080",
|
|
||||||
address: "proton.me:443",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://proton.me/api/v1",
|
|
||||||
address: "proton.me:443",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://proton.black",
|
|
||||||
address: "mail-api.pascal.proton.black",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://mail-api.pascal.proton.black",
|
|
||||||
address: "mail-api.pascal.proton.black",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://mail-api.pascal.proton.black",
|
|
||||||
address: "proton.black:332",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://mail-api.pascal.proton.black",
|
|
||||||
address: "proton.me",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostURL: "https://mail-api.pascal.proton.black",
|
|
||||||
address: "proton.me:332",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
dialer := NewBasicTLSDialer(tt.hostURL)
|
|
||||||
result := dialer.ShouldSkipCertificateChainVerification(tt.address)
|
|
||||||
require.Equal(t, tt.expected, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -50,12 +50,12 @@ var TrustedAPIPins = []string{ //nolint:gochecknoglobals
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TLSReportURI is the address where TLS reports should be sent.
|
// TLSReportURI is the address where TLS reports should be sent.
|
||||||
const TLSReportURI = "https://reports.proton.me/reports/tls"
|
const TLSReportURI = "https://reports.protonmail.ch/reports/tls"
|
||||||
|
|
||||||
// PinningTLSDialer wraps a TLSDialer to check fingerprints after connecting and
|
// PinningTLSDialer wraps a TLSDialer to check fingerprints after connecting and
|
||||||
// to report errors if the fingerprint check fails.
|
// to report errors if the fingerprint check fails.
|
||||||
type PinningTLSDialer struct {
|
type PinningTLSDialer struct {
|
||||||
dialer SecureTLSDialer
|
dialer TLSDialer
|
||||||
pinChecker PinChecker
|
pinChecker PinChecker
|
||||||
reporter Reporter
|
reporter Reporter
|
||||||
tlsIssueCh chan struct{}
|
tlsIssueCh chan struct{}
|
||||||
@ -68,13 +68,13 @@ type Reporter interface {
|
|||||||
|
|
||||||
// PinChecker is used to check TLS keys of connections.
|
// PinChecker is used to check TLS keys of connections.
|
||||||
type PinChecker interface {
|
type PinChecker interface {
|
||||||
CheckCertificate(conn net.Conn, certificateChainVerificationSkipped bool) error
|
CheckCertificate(conn net.Conn) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPinningTLSDialer constructs a new dialer which only returns TCP connections to servers
|
// NewPinningTLSDialer constructs a new dialer which only returns TCP connections to servers
|
||||||
// which present known certificates.
|
// which present known certificates.
|
||||||
// It checks pins using the given pinChecker and reports issues using the given reporter.
|
// It checks pins using the given pinChecker and reports issues using the given reporter.
|
||||||
func NewPinningTLSDialer(dialer SecureTLSDialer, reporter Reporter, pinChecker PinChecker) *PinningTLSDialer {
|
func NewPinningTLSDialer(dialer TLSDialer, reporter Reporter, pinChecker PinChecker) *PinningTLSDialer {
|
||||||
return &PinningTLSDialer{
|
return &PinningTLSDialer{
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
pinChecker: pinChecker,
|
pinChecker: pinChecker,
|
||||||
@ -85,7 +85,6 @@ func NewPinningTLSDialer(dialer SecureTLSDialer, reporter Reporter, pinChecker P
|
|||||||
|
|
||||||
// DialTLSContext dials the given network/address, returning an error if the certificates don't match the trusted pins.
|
// DialTLSContext dials the given network/address, returning an error if the certificates don't match the trusted pins.
|
||||||
func (p *PinningTLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) {
|
func (p *PinningTLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
shouldSkipCertificateChainVerification := p.dialer.ShouldSkipCertificateChainVerification(address)
|
|
||||||
conn, err := p.dialer.DialTLSContext(ctx, network, address)
|
conn, err := p.dialer.DialTLSContext(ctx, network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -96,7 +95,7 @@ func (p *PinningTLSDialer) DialTLSContext(ctx context.Context, network, address
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.pinChecker.CheckCertificate(conn, shouldSkipCertificateChainVerification); err != nil {
|
if err := p.pinChecker.CheckCertificate(conn); err != nil {
|
||||||
if tlsConn, ok := conn.(*tls.Conn); ok && p.reporter != nil {
|
if tlsConn, ok := conn.(*tls.Conn); ok && p.reporter != nil {
|
||||||
p.reporter.ReportCertIssue(TLSReportURI, host, port, tlsConn.ConnectionState())
|
p.reporter.ReportCertIssue(TLSReportURI, host, port, tlsConn.ConnectionState())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,15 +41,3 @@ func NewTLSPinChecker(trustedPins []string) *TLSPinChecker {
|
|||||||
func certFingerprint(cert *x509.Certificate) string {
|
func certFingerprint(cert *x509.Certificate) string {
|
||||||
return fmt.Sprintf(`pin-sha256=%q`, algo.HashBase64SHA256(string(cert.RawSubjectPublicKeyInfo)))
|
return fmt.Sprintf(`pin-sha256=%q`, algo.HashBase64SHA256(string(cert.RawSubjectPublicKeyInfo)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TLSPinChecker) isCertFoundInKnownPins(cert *x509.Certificate) bool {
|
|
||||||
fingerprint := certFingerprint(cert)
|
|
||||||
|
|
||||||
for _, pin := range p.trustedPins {
|
|
||||||
if pin == fingerprint {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@ -25,8 +25,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckCertificate verifies that the connection presents a known pinned leaf TLS certificate.
|
// CheckCertificate returns whether the connection presents a known TLS certificate.
|
||||||
func (p *TLSPinChecker) CheckCertificate(conn net.Conn, certificateChainVerificationSkipped bool) error {
|
func (p *TLSPinChecker) CheckCertificate(conn net.Conn) error {
|
||||||
tlsConn, ok := conn.(*tls.Conn)
|
tlsConn, ok := conn.(*tls.Conn)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("connection is not a TLS connection")
|
return errors.New("connection is not a TLS connection")
|
||||||
@ -34,31 +34,14 @@ func (p *TLSPinChecker) CheckCertificate(conn net.Conn, certificateChainVerifica
|
|||||||
|
|
||||||
connState := tlsConn.ConnectionState()
|
connState := tlsConn.ConnectionState()
|
||||||
|
|
||||||
// When certificate chain verification is enabled (e.g., for known API hosts), we expect the TLS handshake to produce verified chains.
|
for _, peerCert := range connState.PeerCertificates {
|
||||||
// We then validate that the leaf certificate of at least one verified chain matches a known pinned public key.
|
fingerprint := certFingerprint(peerCert)
|
||||||
if !certificateChainVerificationSkipped {
|
|
||||||
if len(connState.VerifiedChains) == 0 {
|
|
||||||
return errors.New("no verified certificate chains")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, chain := range connState.VerifiedChains {
|
for _, pin := range p.trustedPins {
|
||||||
// Check if the leaf certificate is one of the trusted pins.
|
if pin == fingerprint {
|
||||||
if p.isCertFoundInKnownPins(chain[0]) {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ErrTLSMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
// When certificate chain verification is skipped (e.g., for DoH proxies using self-signed certs),
|
|
||||||
// we only validate the leaf certificate against known pinned public keys.
|
|
||||||
if len(connState.PeerCertificates) == 0 {
|
|
||||||
return errors.New("no peer certificates available")
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.isCertFoundInKnownPins(connState.PeerCertificates[0]) {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ErrTLSMismatch
|
return ErrTLSMismatch
|
||||||
|
|||||||
@ -23,6 +23,6 @@ import "net"
|
|||||||
|
|
||||||
// CheckCertificate returns whether the connection presents a known TLS certificate.
|
// CheckCertificate returns whether the connection presents a known TLS certificate.
|
||||||
// The QA implementation always returns nil.
|
// The QA implementation always returns nil.
|
||||||
func (p *TLSPinChecker) CheckCertificate(conn net.Conn, _ bool) error {
|
func (p *TLSPinChecker) CheckCertificate(conn net.Conn) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,7 +64,8 @@ func TestTLSPinInvalid(t *testing.T) {
|
|||||||
checkTLSIssueHandler(t, 1, called)
|
checkTLSIssueHandler(t, 1, called)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTLSPinNoMatch(t *testing.T) {
|
// Disabled for now we'll need to patch this up.
|
||||||
|
func _TestTLSPinNoMatch(t *testing.T) { //nolint:unused
|
||||||
skipIfProxyIsSet(t)
|
skipIfProxyIsSet(t)
|
||||||
|
|
||||||
called, _, reporter, checker, cm := createClientWithPinningDialer(getRootURL())
|
called, _, reporter, checker, cm := createClientWithPinningDialer(getRootURL())
|
||||||
|
|||||||
Reference in New Issue
Block a user