You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
120 lines
5.0 KiB
120 lines
5.0 KiB
package applications
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"encoding/json"
|
|
"git.openprivacy.ca/cwtch.im/tapir"
|
|
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
|
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
|
|
"git.openprivacy.ca/openprivacy/log"
|
|
"golang.org/x/crypto/ed25519"
|
|
)
|
|
|
|
// AuthMessage is exchanged between peers to obtain the Auth Capability
|
|
type AuthMessage struct {
|
|
LongTermPublicKey ed25519.PublicKey
|
|
EphemeralPublicKey ed25519.PublicKey
|
|
}
|
|
|
|
// AuthCapability defines the Authentication Capability granted by AuthApp
|
|
const AuthCapability = tapir.Capability("AuthenticationCapability")
|
|
|
|
// AuthApp is the concrete Application type that handles Authentication
|
|
type AuthApp struct {
|
|
TranscriptApp
|
|
}
|
|
|
|
// NewInstance creates a new instance of the AuthApp
|
|
func (ea AuthApp) NewInstance() tapir.Application {
|
|
return new(AuthApp)
|
|
}
|
|
|
|
// Init runs the entire AuthApp protocol, at the end of the protocol either the connection is granted AUTH capability
|
|
// or the connection is closed.
|
|
func (ea *AuthApp) Init(connection tapir.Connection) {
|
|
ea.TranscriptApp.Init(connection)
|
|
longTermPubKey := ed25519.PublicKey(connection.ID().PublicKeyBytes())
|
|
ephemeralIdentity, _ := primitives.InitializeEphemeralIdentity()
|
|
authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralIdentity.PublicKey()}
|
|
serialized, _ := json.Marshal(authMessage)
|
|
connection.Send(serialized)
|
|
message := connection.Expect()
|
|
|
|
var remoteAuthMessage AuthMessage
|
|
err := json.Unmarshal(message, &remoteAuthMessage)
|
|
if err != nil {
|
|
connection.Close()
|
|
return
|
|
}
|
|
|
|
// If we are an outbound connection we can perform an additional check to ensure that the server sent us back the correct long term
|
|
// public key
|
|
if connection.IsOutbound() && torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) != connection.Hostname() {
|
|
log.Errorf("The remote server (%v) has attempted to authenticate with a different public key %v", connection.Hostname(), torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey))
|
|
connection.Close()
|
|
return
|
|
}
|
|
|
|
// Perform the triple-diffie-hellman exchange.
|
|
key, err := primitives.Perform3DH(connection.ID(), &ephemeralIdentity, remoteAuthMessage.LongTermPublicKey, remoteAuthMessage.EphemeralPublicKey, connection.IsOutbound())
|
|
if err != nil {
|
|
log.Errorf("Failed Auth Challenge %v", err)
|
|
connection.Close()
|
|
return
|
|
}
|
|
connection.SetEncryptionKey(key)
|
|
|
|
// We just successfully unmarshaled both of these, so we can safely ignore the err return from these functions.
|
|
challengeRemote, _ := json.Marshal(remoteAuthMessage)
|
|
challengeLocal, _ := json.Marshal(authMessage)
|
|
|
|
// Define canonical labels so both sides of the connection can generate the same key
|
|
var outboundAuthMessage []byte
|
|
var outboundHostname string
|
|
var inboundAuthMessage []byte
|
|
var inboundHostname string
|
|
|
|
if connection.IsOutbound() {
|
|
outboundHostname = connection.ID().Hostname()
|
|
inboundHostname = torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
|
|
outboundAuthMessage = challengeLocal
|
|
inboundAuthMessage = challengeRemote
|
|
} else {
|
|
outboundHostname = torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
|
|
inboundHostname = connection.ID().Hostname()
|
|
outboundAuthMessage = challengeRemote
|
|
inboundAuthMessage = challengeLocal
|
|
}
|
|
|
|
// Derive a challenge from the transcript of the public parameters of this authentication protocol
|
|
transcript := ea.Transcript()
|
|
transcript.NewProtocol("auth-app")
|
|
transcript.AddToTranscript("outbound-hostname", []byte(outboundHostname))
|
|
transcript.AddToTranscript("inbound-hostname", []byte(inboundHostname))
|
|
transcript.AddToTranscript("outbound-challenge", outboundAuthMessage)
|
|
transcript.AddToTranscript("inbound-challenge", inboundAuthMessage)
|
|
challengeBytes := transcript.CommitToTranscript("3dh-auth-challenge")
|
|
|
|
// If debug is turned on we will dump the transcript to log.
|
|
// There is nothing sensitive in this transcript
|
|
log.Debugf("Transcript: %s", transcript.OutputTranscriptToAudit())
|
|
|
|
// Since we have set the encryption key on the connection the connection will encrypt any messages we send with that key
|
|
// To test that the remote peer has done the same we calculate a challenge hash based on the transcript so far and send it to them
|
|
// along with our hostname
|
|
// We expect the remote to do the same, and compare the two.
|
|
// If successful we extend our auth capability to the connection and reassert the hostname.
|
|
// We note that the only successful scenario here requires that the remote peer have successfully derived the same
|
|
// encryption key and the same transcript challenge.
|
|
connection.Send(append(challengeBytes, []byte(connection.ID().Hostname())...))
|
|
remoteChallenge := connection.Expect()
|
|
assertedHostname := torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
|
|
if subtle.ConstantTimeCompare(append(challengeBytes, []byte(assertedHostname)...), remoteChallenge) == 1 {
|
|
connection.SetHostname(assertedHostname)
|
|
connection.SetCapability(AuthCapability)
|
|
} else {
|
|
log.Debugf("Failed Decrypt Challenge: [%x] [%x]\n", remoteChallenge, challengeBytes)
|
|
connection.Close()
|
|
}
|
|
}
|