diff --git a/applications/auth.go b/applications/auth.go index 8460afc..5763232 100644 --- a/applications/auth.go +++ b/applications/auth.go @@ -8,8 +8,6 @@ import ( "git.openprivacy.ca/openprivacy/libricochet-go/log" "git.openprivacy.ca/openprivacy/libricochet-go/utils" "golang.org/x/crypto/ed25519" - "golang.org/x/crypto/sha3" - "time" ) // AuthMessage is exchanged between peers to obtain the Auth Capability @@ -59,36 +57,45 @@ func (ea AuthApp) Init(connection tapir.Connection) { key := primitives.Perform3DH(connection.ID(), &ephemeralIdentity, remoteAuthMessage.LongTermPublicKey, remoteAuthMessage.EphemeralPublicKey, connection.IsOutbound()) connection.SetEncryptionKey(key) - // Wait to Sync (we need to ensure that both the Local and Remote server have turned encryption on - // otherwise our next Send will fail. - time.Sleep(time.Second) + // 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) - // TODO: Replace this with proper transcript primitive - challengeRemote, err := json.Marshal(remoteAuthMessage) - if err != nil { - connection.Close() - return - } - challengeLocal, err := json.Marshal(authMessage) - if err != nil { - connection.Close() - return - } - challenge := sha3.New512() + // Define canonical labels so both sides of the + var outboundAuthMessage []byte + var outboundHostname string + var inboundAuthMessage []byte + var inboundHostname string if connection.IsOutbound() { - challenge.Write(challengeLocal) - challenge.Write(challengeRemote) + outboundHostname = connection.ID().Hostname() + inboundHostname = utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) + outboundAuthMessage = challengeLocal + inboundAuthMessage = challengeRemote } else { - challenge.Write(challengeRemote) - challenge.Write(challengeLocal) + outboundHostname = utils.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 + var transcript *primitives.Transcript + transcript = primitives.NewTranscript("tapir-auth-" + outboundHostname + "-" + 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 // 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. - challengeBytes := challenge.Sum([]byte{}) + // 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(challengeBytes) remoteChallenge := connection.Expect() if subtle.ConstantTimeCompare(challengeBytes, remoteChallenge) == 1 { diff --git a/primitives/transcript.go b/primitives/transcript.go new file mode 100644 index 0000000..ebb4bed --- /dev/null +++ b/primitives/transcript.go @@ -0,0 +1,51 @@ +package primitives + +import ( + "fmt" + "golang.org/x/crypto/sha3" + "hash" +) + +// Transcript implements a transcript of a public coin argument. +// +// We have the following goals: +// - Provide a consisted transcript API for our zero knowledge protocols +// - Allow sequential proofs over a common transcript (ensuring a single proof cannot be extracted standalone) +// - produce an auditable human-readable transcript. +// +// The design of this API was inspired by Merlin: https://docs.rs/crate/merlin/ +// +// At some point we might want to extend this to be compatible with Merlin transcripts, built on STROBE +type Transcript struct { + hash hash.Hash + transcript string +} + +// NewTranscript creates a new Transcript with the given Label, the label should be unique to the application +func NewTranscript(label string) *Transcript { + transcript := new(Transcript) + transcript.hash = sha3.New256() + transcript.AddToTranscript("protocol", []byte(label)) + return transcript +} + +// AddToTranscript appends a value to the transcript with the given label +// This binds the given data to the label. +func (t *Transcript) AddToTranscript(label string, b []byte) { + op := fmt.Sprintf("%s (%d) %x;", label, len(b), b) + t.transcript = fmt.Sprintf("%v\n%v", t.transcript, op) + t.hash.Write([]byte(op)) +} + +// OutputTranscriptToAudit outputs a human-readable copy of the transcript so far. +func (t Transcript) OutputTranscriptToAudit() string { + return t.transcript +} + +// CommitToTranscript generates a challenge based on the current transcript, it also commits the challenge to the transcript. +func (t *Transcript) CommitToTranscript(label string) []byte { + t.AddToTranscript("commit", []byte(label)) + b := t.hash.Sum([]byte{}) + t.AddToTranscript(label, b) + return b +}