-
Notifications
You must be signed in to change notification settings - Fork 272
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #691 from rod-hynes/server-fixes
Server fixes
- Loading branch information
Showing
19 changed files
with
539 additions
and
213 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ import ( | |
|
||
// [Psiphon] | ||
|
||
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common" | ||
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng" | ||
) | ||
|
||
|
@@ -467,6 +468,12 @@ const ( | |
kexStrictServer = "[email protected]" | ||
) | ||
|
||
// [Psiphon] | ||
// For testing only. Enables testing support for legacy clients, which have | ||
// only the legacy algorithm lists and no weak-MAC or new-server-algos logic. | ||
// Not safe for concurrent access. | ||
var testLegacyClient = false | ||
|
||
// sendKexInit sends a key change message. | ||
func (t *handshakeTransport) sendKexInit() error { | ||
t.mu.Lock() | ||
|
@@ -550,8 +557,8 @@ func (t *handshakeTransport) sendKexInit() error { | |
// its KEX using the specified seed; deterministically adjust own | ||
// randomized KEX to ensure negotiation succeeds. | ||
// | ||
// When NoEncryptThenMACHash is specified, do not use Encrypt-then-MAC has | ||
// algorithms. | ||
// When NoEncryptThenMACHash is specified, do not use Encrypt-then-MAC | ||
// hash algorithms. | ||
// | ||
// Limitations: | ||
// | ||
|
@@ -632,6 +639,59 @@ func (t *handshakeTransport) sendKexInit() error { | |
return list | ||
} | ||
|
||
avoid := func(PRNG *prng.PRNG, list, avoidList, addList []string) []string { | ||
|
||
// Avoid negotiating items in avoidList, by moving a non-avoid | ||
// item to the front of the list; either by swapping with a | ||
// later, non-avoid item, or inserting a new item. | ||
|
||
if len(list) < 1 { | ||
return list | ||
} | ||
if !common.Contains(avoidList, list[0]) { | ||
// The first item isn't on the avoid list. | ||
return list | ||
} | ||
for i := 1; i < len(list); i++ { | ||
if !common.Contains(avoidList, list[i]) { | ||
// Swap with a later, existing non-avoid item. | ||
list[0], list[i] = list[i], list[0] | ||
return list | ||
} | ||
} | ||
for _, item := range permute(PRNG, addList) { | ||
if !common.Contains(avoidList, item) { | ||
// Insert a randomly selected non-avoid item. | ||
return append([]string{item}, list...) | ||
} | ||
} | ||
// Can't avoid. | ||
return list | ||
} | ||
|
||
addSome := func(PRNG *prng.PRNG, list, addList []string) []string { | ||
newList := list | ||
for _, item := range addList { | ||
if PRNG.FlipCoin() { | ||
index := PRNG.Range(0, len(newList)) | ||
newList = append( | ||
newList[:index], | ||
append([]string{item}, newList[index:]...)...) | ||
} | ||
} | ||
return newList | ||
} | ||
|
||
toFront := func(list []string, item string) []string { | ||
for index, existingItem := range list { | ||
if existingItem == item { | ||
list[0], list[index] = list[index], list[0] | ||
return list | ||
} | ||
} | ||
return append([]string{item}, list...) | ||
} | ||
|
||
firstKexAlgo := func(kexAlgos []string) (string, bool) { | ||
for _, kexAlgo := range kexAlgos { | ||
switch kexAlgo { | ||
|
@@ -662,10 +722,9 @@ func (t *handshakeTransport) sendKexInit() error { | |
// server's algorithms; (b) random truncation by the server doesn't | ||
// select only new algorithms unknown to existing clients. | ||
// | ||
// TODO: add a versioning mechanism, such as a SSHv2 capability, to | ||
// allow for servers with new algorithm lists, where older clients | ||
// won't try to connect to these servers, and new clients know to use | ||
// non-legacy lists in the PeerKEXPRNGSeed mechanism. | ||
// New algorithms are then randomly inserted only after the legacy | ||
// lists are processed in legacy PRNG state order. | ||
|
||
legacyServerKexAlgos := []string{ | ||
kexAlgoCurve25519SHA256LibSSH, | ||
kexAlgoECDH256, kexAlgoECDH384, kexAlgoECDH521, | ||
|
@@ -681,105 +740,175 @@ func (t *handshakeTransport) sendKexInit() error { | |
"hmac-sha2-256", "hmac-sha1", "hmac-sha1-96", | ||
} | ||
legacyServerNoEncryptThenMACs := []string{ | ||
"hmac-sha2-256", "hmac-sha1", "hmac-sha1-96"} | ||
|
||
isServer := len(t.hostKeys) > 0 | ||
"hmac-sha2-256", "hmac-sha1", "hmac-sha1-96", | ||
} | ||
if t.config.NoEncryptThenMACHash { | ||
legacyServerMACs = legacyServerNoEncryptThenMACs | ||
} | ||
|
||
PRNG := prng.NewPRNGWithSeed(t.config.KEXPRNGSeed) | ||
|
||
startingKexAlgos := msg.KexAlgos | ||
startingCiphers := msg.CiphersClientServer | ||
startingMACs := msg.MACsClientServer | ||
|
||
if isServer { | ||
// testLegacyClient: legacy clients are older clients which start with | ||
// the same algorithm lists as legacyServer and have neither the | ||
// newServer-algorithm nor the weak-MAC KEX prediction logic. | ||
|
||
if isServer || testLegacyClient { | ||
startingKexAlgos = legacyServerKexAlgos | ||
startingCiphers = legacyServerCiphers | ||
startingMACs = legacyServerMACs | ||
if t.config.NoEncryptThenMACHash { | ||
startingMACs = legacyServerNoEncryptThenMACs | ||
} | ||
} | ||
|
||
msg.KexAlgos = selectKexAlgos(PRNG, startingKexAlgos) | ||
kexAlgos := selectKexAlgos(PRNG, startingKexAlgos) | ||
|
||
ciphers := truncate(PRNG, permute(PRNG, startingCiphers)) | ||
msg.CiphersClientServer = ciphers | ||
msg.CiphersServerClient = ciphers | ||
|
||
MACs := truncate(PRNG, permute(PRNG, startingMACs)) | ||
msg.MACsClientServer = MACs | ||
msg.MACsServerClient = MACs | ||
|
||
var hostKeyAlgos []string | ||
if isServer { | ||
msg.ServerHostKeyAlgos = permute(PRNG, msg.ServerHostKeyAlgos) | ||
hostKeyAlgos = permute(PRNG, msg.ServerHostKeyAlgos) | ||
} else { | ||
// Must offer KeyAlgoRSA to Psiphon server. | ||
msg.ServerHostKeyAlgos = retain( | ||
hostKeyAlgos = retain( | ||
PRNG, | ||
truncate(PRNG, permute(PRNG, msg.ServerHostKeyAlgos)), | ||
KeyAlgoRSA) | ||
} | ||
|
||
if !isServer && t.config.PeerKEXPRNGSeed != nil { | ||
// To ensure compatibility with server KEX prediction in legacy | ||
// clients, all preceeding PRNG operations must be performed in the | ||
// given order, and all before the following operations. | ||
|
||
// Generate the peer KEX and make adjustments if negotiation would | ||
// fail. This assumes that PeerKEXPRNGSeed remains static (in | ||
// Psiphon, the peer is the server and PeerKEXPRNGSeed is derived | ||
// from the server entry); and that the PRNG is invoked in the | ||
// exact same order on the peer (i.e., the code block immediately | ||
// above is what the peer runs); and that the peer sets | ||
// NoEncryptThenMACHash in the same cases. | ||
// Avoid negotiating weak MAC algorithms. Servers will ensure that no | ||
// weakMACs are the highest priority item. Clients will make | ||
// adjustments after predicting the server KEX. | ||
|
||
PeerPRNG := prng.NewPRNGWithSeed(t.config.PeerKEXPRNGSeed) | ||
weakMACs := []string{"hmac-sha1-96"} | ||
|
||
if isServer { | ||
MACs = avoid(PRNG, MACs, weakMACs, startingMACs) | ||
} | ||
|
||
// Randomly insert new algorithms. For servers, the preceeding legacy | ||
// operations will ensure selection of at least one legacy algorithm | ||
// of each type, ensuring compatibility with legacy clients. | ||
|
||
newServerKexAlgos := []string{ | ||
kexAlgoCurve25519SHA256, kexAlgoDH16SHA512, | ||
"[email protected]", | ||
} | ||
newServerCiphers := []string{ | ||
gcm256CipherID, | ||
} | ||
newServerMACs := []string{ | ||
"[email protected]", "hmac-sha2-512", | ||
} | ||
newServerNoEncryptThenMACs := []string{ | ||
"hmac-sha2-512", | ||
} | ||
if t.config.NoEncryptThenMACHash { | ||
newServerMACs = newServerNoEncryptThenMACs | ||
} | ||
|
||
if isServer { | ||
kexAlgos = addSome(PRNG, kexAlgos, newServerKexAlgos) | ||
ciphers = addSome(PRNG, ciphers, newServerCiphers) | ||
MACs = addSome(PRNG, MACs, newServerMACs) | ||
} | ||
|
||
msg.KexAlgos = kexAlgos | ||
msg.CiphersClientServer = ciphers | ||
msg.CiphersServerClient = ciphers | ||
msg.MACsClientServer = MACs | ||
msg.MACsServerClient = MACs | ||
msg.ServerHostKeyAlgos = hostKeyAlgos | ||
|
||
if !isServer && t.config.PeerKEXPRNGSeed != nil { | ||
|
||
// Generate the server KEX and make adjustments if negotiation | ||
// would fail. This assumes that PeerKEXPRNGSeed remains static | ||
// (in Psiphon, the peer is the server and PeerKEXPRNGSeed is | ||
// derived from the server entry); and that the PRNG is invoked | ||
// in the exact same order on the server (i.e., the code block | ||
// immediately above is what the peer runs); and that the server | ||
// sets NoEncryptThenMACHash in the same cases. | ||
// | ||
// Note that only the client sends "ext-info-c" | ||
// and "[email protected]" and only the server | ||
// sends "[email protected]", so these will never | ||
// match and do not need to be filtered out before findCommon. | ||
// | ||
// The following assumes that the server always starts with the | ||
// default preferredKexAlgos along with | ||
// "[email protected]" appended before randomizing. | ||
|
||
serverKexAlgos := append( | ||
append([]string(nil), preferredKexAlgos...), | ||
"[email protected]") | ||
serverCiphers := preferredCiphers | ||
serverMACS := supportedMACs | ||
serverNoEncryptThenMACs := noEncryptThenMACs | ||
|
||
// Switch to using the legacy algorithms that the server currently | ||
// downgrades to (see comment above). | ||
// | ||
// TODO: for servers without legacy backwards compatibility | ||
// concerns, skip the following lines. | ||
serverKexAlgos = legacyServerKexAlgos | ||
serverCiphers = legacyServerCiphers | ||
serverMACS = legacyServerMACs | ||
serverNoEncryptThenMACs = legacyServerNoEncryptThenMACs | ||
|
||
serverKexAlgos = selectKexAlgos(PeerPRNG, serverKexAlgos) | ||
PeerPRNG := prng.NewPRNGWithSeed(t.config.PeerKEXPRNGSeed) | ||
|
||
startingKexAlgos := legacyServerKexAlgos | ||
startingCiphers := legacyServerCiphers | ||
startingMACs := legacyServerMACs | ||
if t.config.NoEncryptThenMACHash { | ||
startingMACs = legacyServerNoEncryptThenMACs | ||
} | ||
|
||
// The server populates msg.ServerHostKeyAlgos based on the host | ||
// key type, which, for Psiphon servers, is "ssh-rsa", so | ||
// algorithmsForKeyFormat("ssh-rsa") predicts the server | ||
// msg.ServerHostKeyAlgos value. | ||
startingHostKeyAlgos := algorithmsForKeyFormat("ssh-rsa") | ||
|
||
serverKexAlgos := selectKexAlgos(PeerPRNG, startingKexAlgos) | ||
serverCiphers := truncate(PeerPRNG, permute(PeerPRNG, startingCiphers)) | ||
serverMACs := truncate(PeerPRNG, permute(PeerPRNG, startingMACs)) | ||
|
||
if !testLegacyClient { | ||
|
||
// This value is not used, but the identical PRNG operation must be | ||
// performed in order to predict the PeerPRNG state. | ||
_ = permute(PeerPRNG, startingHostKeyAlgos) | ||
|
||
serverMACs = avoid(PeerPRNG, serverMACs, weakMACs, startingMACs) | ||
|
||
serverKexAlgos = addSome(PeerPRNG, serverKexAlgos, newServerKexAlgos) | ||
serverCiphers = addSome(PeerPRNG, serverCiphers, newServerCiphers) | ||
serverMACs = addSome(PeerPRNG, serverMACs, newServerMACs) | ||
} | ||
|
||
// Adjust to ensure compatibility with the server KEX. | ||
|
||
if _, err := findCommon("", msg.KexAlgos, serverKexAlgos); err != nil { | ||
if kexAlgo, ok := firstKexAlgo(serverKexAlgos); ok { | ||
msg.KexAlgos = retain(PRNG, msg.KexAlgos, kexAlgo) | ||
kexAlgos = retain(PRNG, msg.KexAlgos, kexAlgo) | ||
} | ||
} | ||
|
||
serverCiphers = truncate(PeerPRNG, permute(PeerPRNG, serverCiphers)) | ||
if _, err := findCommon("", ciphers, serverCiphers); err != nil { | ||
ciphers = retain(PRNG, ciphers, serverCiphers[0]) | ||
msg.CiphersClientServer = ciphers | ||
msg.CiphersServerClient = ciphers | ||
} | ||
|
||
if t.config.NoEncryptThenMACHash { | ||
serverMACS = serverNoEncryptThenMACs | ||
if _, err := findCommon("", MACs, serverMACs); err != nil { | ||
MACs = retain(PRNG, MACs, serverMACs[0]) | ||
} | ||
|
||
serverMACS = truncate(PeerPRNG, permute(PeerPRNG, serverMACS)) | ||
if _, err := findCommon("", MACs, serverMACS); err != nil { | ||
MACs = retain(PRNG, MACs, serverMACS[0]) | ||
msg.MACsClientServer = MACs | ||
msg.MACsServerClient = MACs | ||
// Avoid negotiating weak MAC algorithms. | ||
// | ||
// Legacy clients, without this logic, may still select only weak | ||
// MACs or predict only weak MACs for the server KEX. | ||
|
||
commonMAC, _ := findCommon("", MACs, serverMACs) | ||
if common.Contains(weakMACs, commonMAC) { | ||
// serverMACs[0] is not in weakMACs. | ||
MACs = toFront(MACs, serverMACs[0]) | ||
} | ||
|
||
msg.KexAlgos = kexAlgos | ||
msg.CiphersClientServer = ciphers | ||
msg.CiphersServerClient = ciphers | ||
msg.MACsClientServer = MACs | ||
msg.MACsServerClient = MACs | ||
} | ||
|
||
// Offer "[email protected]", which is offered by OpenSSH. Compression | ||
|
Oops, something went wrong.