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() } }