Tapir provides a framework for building Anonymous / metadata resistant Services
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

  1. package applications
  2. import (
  3. "crypto/subtle"
  4. "cwtch.im/tapir"
  5. "cwtch.im/tapir/primitives"
  6. "encoding/json"
  7. torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
  8. "git.openprivacy.ca/openprivacy/log"
  9. "golang.org/x/crypto/ed25519"
  10. )
  11. // AuthMessage is exchanged between peers to obtain the Auth Capability
  12. type AuthMessage struct {
  13. LongTermPublicKey ed25519.PublicKey
  14. EphemeralPublicKey ed25519.PublicKey
  15. }
  16. // AuthCapability defines the Authentication Capability granted by AuthApp
  17. const AuthCapability = tapir.Capability("AuthenticationCapability")
  18. // AuthApp is the concrete Application type that handles Authentication
  19. type AuthApp struct {
  20. TranscriptApp
  21. }
  22. // NewInstance creates a new instance of the AuthApp
  23. func (ea AuthApp) NewInstance() tapir.Application {
  24. return new(AuthApp)
  25. }
  26. // Init runs the entire AuthApp protocol, at the end of the protocol either the connection is granted AUTH capability
  27. // or the connection is closed.
  28. func (ea *AuthApp) Init(connection tapir.Connection) {
  29. ea.TranscriptApp.Init(connection)
  30. longTermPubKey := ed25519.PublicKey(connection.ID().PublicKeyBytes())
  31. ephemeralIdentity, _ := primitives.InitializeEphemeralIdentity()
  32. authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralIdentity.PublicKey()}
  33. serialized, _ := json.Marshal(authMessage)
  34. connection.Send(serialized)
  35. message := connection.Expect()
  36. var remoteAuthMessage AuthMessage
  37. err := json.Unmarshal(message, &remoteAuthMessage)
  38. if err != nil {
  39. connection.Close()
  40. return
  41. }
  42. // If we are an outbound connection we can perform an additional check to ensure that the server sent us back the correct long term
  43. // public key
  44. if connection.IsOutbound() && torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) != connection.Hostname() {
  45. log.Errorf("The remote server (%v) has attempted to authenticate with a different public key %v", connection.Hostname(), torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey))
  46. connection.Close()
  47. return
  48. }
  49. // Perform the triple-diffie-hellman exchange.
  50. key, err := primitives.Perform3DH(connection.ID(), &ephemeralIdentity, remoteAuthMessage.LongTermPublicKey, remoteAuthMessage.EphemeralPublicKey, connection.IsOutbound())
  51. if err != nil {
  52. log.Errorf("Failed Auth Challenge %v", err)
  53. connection.Close()
  54. return
  55. }
  56. connection.SetEncryptionKey(key)
  57. // We just successfully unmarshaled both of these, so we can safely ignore the err return from these functions.
  58. challengeRemote, _ := json.Marshal(remoteAuthMessage)
  59. challengeLocal, _ := json.Marshal(authMessage)
  60. // Define canonical labels so both sides of the connection can generate the same key
  61. var outboundAuthMessage []byte
  62. var outboundHostname string
  63. var inboundAuthMessage []byte
  64. var inboundHostname string
  65. if connection.IsOutbound() {
  66. outboundHostname = connection.ID().Hostname()
  67. inboundHostname = torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
  68. outboundAuthMessage = challengeLocal
  69. inboundAuthMessage = challengeRemote
  70. } else {
  71. outboundHostname = torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
  72. inboundHostname = connection.ID().Hostname()
  73. outboundAuthMessage = challengeRemote
  74. inboundAuthMessage = challengeLocal
  75. }
  76. // Derive a challenge from the transcript of the public parameters of this authentication protocol
  77. transcript := ea.Transcript()
  78. transcript.NewProtocol("auth-app")
  79. transcript.AddToTranscript("outbound-hostname", []byte(outboundHostname))
  80. transcript.AddToTranscript("inbound-hostname", []byte(inboundHostname))
  81. transcript.AddToTranscript("outbound-challenge", outboundAuthMessage)
  82. transcript.AddToTranscript("inbound-challenge", inboundAuthMessage)
  83. challengeBytes := transcript.CommitToTranscript("3dh-auth-challenge")
  84. // If debug is turned on we will dump the transcript to log.
  85. // There is nothing sensitive in this transcript
  86. log.Debugf("Transcript: %s", transcript.OutputTranscriptToAudit())
  87. // Since we have set the encryption key on the connection the connection will encrypt any messages we send with that key
  88. // 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
  89. // along with our hostname
  90. // We expect the remote to do the same, and compare the two.
  91. // If successful we extend our auth capability to the connection and reassert the hostname.
  92. // We note that the only successful scenario here requires that the remote peer have successfully derived the same
  93. // encryption key and the same transcript challenge.
  94. connection.Send(append(challengeBytes, []byte(connection.ID().Hostname())...))
  95. remoteChallenge := connection.Expect()
  96. assertedHostname := torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
  97. if subtle.ConstantTimeCompare(append(challengeBytes, []byte(assertedHostname)...), remoteChallenge) == 1 {
  98. connection.SetHostname(assertedHostname)
  99. connection.SetCapability(AuthCapability)
  100. } else {
  101. log.Errorf("Failed Decrypt Challenge: [%x] [%x]\n", remoteChallenge, challengeBytes)
  102. connection.Close()
  103. }
  104. }