Compare commits

...

127 Commits

Author SHA1 Message Date
Sarah Jamie Lewis 4129031391 Error on Non Auth
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is pending Details
2022-10-24 13:10:48 -07:00
Sarah Jamie Lewis 139a35c219 Expose token.T in json + fix issue with new auth protocol 2022-10-24 13:10:48 -07:00
Sarah Jamie Lewis 4e4e3b4422 Formatting + go 1.17 ioutil deprecation 2022-10-24 13:10:48 -07:00
Dan Ballard 015307d907 Merge pull request 'Remove bignum ed25519->curve25519 implementation / replace with filippo.io/edwards25519' (#54) from crypto_improvement into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #54
2022-09-13 07:00:51 +00:00
Sarah Jamie Lewis 13effd5457 Merge branch 'master' into crypto_improvement
continuous-integration/drone/pr Build is passing Details
2022-09-13 02:15:40 +00:00
Sarah Jamie Lewis 9012720973 Remove bignum ed25519->curve25519 implementation / replace with filippo.io/edwards25519
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2022-09-12 19:13:48 -07:00
Sarah Jamie Lewis af2f509711 Merge pull request 'update drone format' (#53) from drone_update into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #53
2022-09-06 15:48:49 +00:00
Dan Ballard eceddc676d update drone format
continuous-integration/drone/pr Build is passing Details
2022-09-05 23:28:54 -07:00
Dan Ballard bf7a0a4b39 Merge pull request 'Upgrade Dependencies + Ristretto API' (#52) from update-deps into master
continuous-integration/drone/tag Build was killed Details
continuous-integration/drone/push Build is failing Details
Reviewed-on: #52
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-08-29 03:42:45 +00:00
Sarah Jamie Lewis cefe182b80 Ignore 1 linter error
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-08-28 20:29:29 -07:00
Sarah Jamie Lewis 2da2c98b91 Merge branch 'master' into update-deps
continuous-integration/drone/pr Build is failing Details
continuous-integration/drone/push Build is passing Details
2022-08-28 20:26:31 -07:00
Sarah Jamie Lewis 7ee88b7ba8 Merge branch 'master' into update-deps
continuous-integration/drone/push Build was killed Details
continuous-integration/drone/pr Build was killed Details
2022-08-28 20:25:39 -07:00
Sarah Jamie Lewis 2da293f118 Upgrade Dependencies
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build was killed Details
2022-08-28 20:22:28 -07:00
Sarah Jamie Lewis ae04cf7983 Merge pull request 'connectivity version bump' (#51) from cbump into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #51
2022-08-08 20:08:01 +00:00
Dan Ballard 0b5a1a4345 connectivity version bump
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-08-08 12:41:28 -07:00
Dan Ballard f610638146 Merge pull request 'Add Garbage Collection for Old Connections' (#50) from gc into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #50
2022-04-21 21:37:36 +00:00
Sarah Jamie Lewis bacc9a47c4 Add Garbage Collection for Old Connections
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-04-21 13:07:42 -07:00
Dan Ballard d21010257d Merge pull request 'Move decrypt error to debug' (#49) from decrypt_error into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #49
2022-04-20 23:50:53 +00:00
Sarah Jamie Lewis 29084d0d60 Move decrypt error to debug
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is passing Details
2022-04-20 16:26:04 -07:00
Dan Ballard 10980c7bee Merge pull request 'Reduce new allocations, propagate errors in PRNG.Next()' (#48) from perf into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #48
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2022-04-20 19:11:35 +00:00
Sarah Jamie Lewis 89d12f812f Upgrade Connectivity
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-04-20 11:15:11 -07:00
Sarah Jamie Lewis 3b4fee2e72 Reduce new allocations, propagate errors in PRNG.Next()
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-04-19 15:16:51 -07:00
Dan Ballard 4cce393e12 Merge pull request 'Update Connectivity' (#47) from upgrade_conn1.8.2 into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #47
2022-04-18 22:14:21 +00:00
Sarah Jamie Lewis 26141185e0 Merge branch 'master' into upgrade_conn1.8.2
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-04-18 22:05:43 +00:00
Sarah Jamie Lewis f6e1d496c1 Update Connectivity
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2022-04-18 15:04:35 -07:00
erinn 29628df452 Merge pull request 'Expose errors in Send API.' (#46) from send_fixes into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #46
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-01-24 21:58:59 +00:00
Sarah Jamie Lewis 09c0e97336 Merge branch 'master' into send_fixes
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-01-24 20:30:18 +00:00
Sarah Jamie Lewis e87f578c01 Expose errors in Send API.
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
2022-01-24 12:28:55 -08:00
erinn 51710f2991 Merge pull request 'Upgrade Connectivity, upgrade quality.sh and drone.yml to use staticcheck' (#45) from upgrade_conn into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #45
2022-01-12 20:34:31 +00:00
Sarah Jamie Lewis eac1c48044 Merge branch 'master' into upgrade_conn
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-01-12 20:24:01 +00:00
Sarah Jamie Lewis 100554ebc4 Upgrade Connectivity, upgrade quality.sh and drone.yml to use staticcheck
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2022-01-12 12:22:47 -08:00
erinn 8655bedf08 Merge pull request 'Upgrade Connectivity' (#44) from upgrades into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #44
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-09-28 21:14:33 +00:00
Sarah Jamie Lewis d80a94b970 Upgrade Connectivity
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-09-28 14:12:57 -07:00
erinn aae071a121 Merge pull request 'Refactor WaitForCapabilityOrClose' (#43) from wait into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #43
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-09-27 22:09:35 +00:00
Sarah Jamie Lewis b32cad2c27 Don't Log Conn
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-09-27 14:57:06 -07:00
Sarah Jamie Lewis 12156065c3 Upgrade Logging
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is failing Details
2021-09-27 14:50:42 -07:00
Sarah Jamie Lewis f1fe281ab4 Fixup debug messages 2021-09-27 14:43:54 -07:00
Sarah Jamie Lewis 2ff23bc89c Merge branch 'master' into wait
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is failing Details
2021-09-27 21:39:26 +00:00
Sarah Jamie Lewis 81810ed531 Refactor WaitForCapabilityOrClose
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is failing Details
2021-09-27 14:38:01 -07:00
erinn 459e00b423 Merge pull request 'Correctly resolve multiple inbound connections to complete-closure' (#42) from networking into master
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/tag Build is pending Details
Reviewed-on: #42
Reviewed-by: erinn <erinn@openprivacy.ca>
2021-09-21 23:18:52 +00:00
Sarah Jamie Lewis f1e3f2ca54 Correctly resolve multiple inbound connections to complete-closure
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Connect() now prevents Open if there are *any connections* under the assumption
that one of them will be resolved valid. (Before connect allowed Open on any
error, this was a bug that would occasionally be tripped by multiple Inbounds)
2021-09-21 15:49:32 -07:00
Dan Ballard 137461e2cd Merge pull request 'Set the App Before Initializing The App Over the Connection...' (#41) from chainbug into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #41
2021-09-10 18:21:59 +00:00
Sarah Jamie Lewis 1750d634f2 Merge branch 'master' into chainbug
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-09-10 18:16:28 +00:00
Sarah Jamie Lewis 759ab44a47 Set the App Before Initializing The App Over the Connection...
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-09-10 11:15:14 -07:00
Dan Ballard 0845156bea Merge pull request 'Actively Deduplicate Connections on WaitForCapabilityOrClose' (#40) from dedupe into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is failing Details
Reviewed-on: #40
2021-09-08 20:36:38 +00:00
Sarah Jamie Lewis 364521e4c1 Merge branch 'master' into dedupe
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is passing Details
2021-09-08 18:57:56 +00:00
Sarah Jamie Lewis c19b1011ee Actively Deduplicate Connections on WaitForCapabilityOrClose
continuous-integration/drone/push Build is pending Details
continuous-integration/drone/pr Build is pending Details
A very rare bug happens when 2 contacts peer with each other at the same
time. This results in duplicate higher level constructs like PeerApp
which can make tracking state-related bugs difficult, especially
in integration tests.

This commit fixes an existing bug in WaitForCapabilityOrClose
which hid the existence of a duplicate connections from clients
(and replaces it with active deduping)
2021-09-08 11:53:39 -07:00
Sarah Jamie Lewis fd31e9c31a Merge pull request 'connecivity version bump' (#39) from conVbump into master
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #39
2021-06-25 09:13:43 -07:00
Dan Ballard d4b9c378eb connecivity version bump
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-06-25 09:12:22 -07:00
Dan Ballard 7f00d05a04 Merge pull request 'Staticcheck and Connectivtiy Upgrade' (#38) from thread_safety into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #38
2021-06-09 10:43:09 -07:00
Sarah Jamie Lewis 6e7fcad7a6 Staticcheck and Connectivtiy Upgrade
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-06-09 10:36:34 -07:00
Dan Ballard e7da782cfe Merge pull request 'Fix NPE when ACN is in Error State on Listen' (#37) from bug_fix into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #37
2021-06-02 11:01:52 -07:00
Sarah Jamie Lewis 88ddcc8fa2 Fix NPE when ACN is in Error State on Listen
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-06-02 10:22:39 -07:00
erinn 7f8b475fd7 Merge pull request 'Check solution length in validate challenge' (#36) from bugfix into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #36
2021-05-13 13:01:23 -07:00
Sarah Jamie Lewis 770f36afad Check solution length in validate challenge
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
Found by Fuzz Bot.
2021-05-13 12:38:47 -07:00
erinn 7444d1e0cf Merge pull request 'Don't store server key in token database, make TokenServer responsible for database closure.' (#35) from bugfix into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #35
2021-05-05 12:44:31 -07:00
Sarah Jamie Lewis c47e478e01 Don't store server key in token database, make TokenServer responsible for database closure.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-05-05 12:03:49 -07:00
Sarah Jamie Lewis 98ff3244c0 Merge pull request 'Upgrade connectivity' (#34) from bugfix into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #34
2021-05-03 14:04:42 -07:00
Sarah Jamie Lewis 38351e486f Upgrade connectivity
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-05-03 14:03:52 -07:00
erinn 0cc499b42c Merge pull request 'Upgrade Connectivity' (#33) from bugfix into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #33
2021-04-13 15:10:19 -07:00
Sarah Jamie Lewis e16d2e30f0 Merge branch 'master' into bugfix
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-04-13 15:09:13 -07:00
Sarah Jamie Lewis edba40cfee Upgrade Connectivity
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-04-13 15:08:16 -07:00
Dan Ballard 05d679aa5a Merge pull request 'Upgrade Connectivity' (#32) from bugfix into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #32
2021-04-13 13:54:05 -07:00
Sarah Jamie Lewis 1233b1e85a Upgrade Connectivity
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-04-13 13:44:48 -07:00
Dan Ballard cc760fddcb Merge pull request 'Merge missing bugfixes...' (#31) from bugfix into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #31
2021-04-09 14:44:58 -07:00
Sarah Jamie Lewis 453a93d579 Merge branch 'hmac_size_fix' into bugfix
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-04-09 14:33:38 -07:00
Sarah Jamie Lewis db89cf80f3 Merge branch 'hmac_size_fix' 2021-04-09 14:18:56 -07:00
Dan Ballard 8f6408433b Merge pull request 'Fix minor "datarace" caused by unecessary assignment after close' (#30) from bugfix into master
Reviewed-on: #30
2021-04-09 14:17:45 -07:00
Dan Ballard 48395506a5 Merge pull request 'Fix minor "datarace" caused by unecessary assignment after close' (#30) from bugfix into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #30
2021-04-09 12:43:00 -07:00
Sarah Jamie Lewis 2310dec631 Fix minor "datarace" caused by unecessary assignment after close
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-04-09 12:29:07 -07:00
Dan Ballard 64317efb0c Merge pull request 'Remove custom url forwarding for tapir module' (#29) from bugfix into master
continuous-integration/drone/tag Build is passing Details
continuous-integration/drone/push Build is passing Details
Reviewed-on: #29
2021-04-08 18:17:27 -07:00
Sarah Jamie Lewis b68d838295 Remove custom url forwarding for tapir module
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details
2021-04-08 18:09:24 -07:00
Sarah Jamie Lewis 905dc52b7a Update Connectivity and Logging 2021-04-08 18:06:08 -07:00
Sarah Jamie Lewis b7a99d6e85 Updating drone for new module name
continuous-integration/drone/push Build is failing Details
2021-04-08 18:05:38 -07:00
Sarah Jamie Lewis 9fba459adc Check if Listener exists before trying to close it 2020-10-29 15:45:07 -07:00
Sarah Jamie Lewis c0b675b011 Use hmac.Write instead of hmac.Sum when verifying tokens
the build was successful Details
2020-07-20 17:41:50 -07:00
Sarah Jamie Lewis f26a98895a Add Metrics (Fix: #21)
the build was successful Details
2020-07-14 16:42:20 -07:00
Sarah Jamie Lewis c15ac5767c Add Metrics
continuous-integration/drone/push Build is failing Details
2020-07-14 16:39:08 -07:00
Dan Ballard 28b2f8212f Merge pull request 'Add Broadcast' (#23) from broadcast into master
the build was successful Details
Reviewed-on: #23
2020-07-14 15:05:53 -07:00
Sarah Jamie Lewis 7eb492d77b Merge branch 'broadcast' of git.openprivacy.ca:cwtch.im/tapir into broadcast
the build was successful Details
2020-07-14 14:59:28 -07:00
Sarah Jamie Lewis 8eeea02997 Update Signature 2020-07-14 14:59:08 -07:00
Sarah Jamie Lewis 4b6a451bbe Merge branch 'master' into broadcast
the build failed Details
2020-07-14 14:29:00 -07:00
Sarah Jamie Lewis 66d6b0b51e Add Broacast
the build was successful Details
2020-07-14 14:23:27 -07:00
Dan Ballard 55da8ce31e Merge pull request 'Change Errorf to Debugf log message in the case of a connection close.' (#19) from upgrade-connectivity into master
the build was successful Details
Reviewed-on: #19
2020-07-07 14:58:10 -07:00
Sarah Jamie Lewis b6eddf79b1 Merge branch 'master' of git.openprivacy.ca:cwtch.im/tapir into upgrade-connectivity
the build was successful Details
2020-07-07 11:39:20 -07:00
Sarah Jamie Lewis 3508656d90 Change Errorf on Connection Close to Debugf 2020-07-07 11:38:33 -07:00
Dan Ballard 960c61abf5 Merge pull request 'Precautionary Panic' (#18) from upgrade-connectivity into master
the build was successful Details
2020-07-02 15:26:15 -07:00
Sarah Jamie Lewis 0150af69f0 Precautionary Panic
the build was successful Details
2020-07-02 14:05:02 -07:00
Dan Ballard 1de3a9a83a Merge pull request 'Update connectivity to 1.2.0' (#17) from upgrade-connectivity into master
the build was successful Details
2020-06-29 15:05:25 -07:00
Sarah Jamie Lewis 3a2a4f24e3 Update connectivity to 1.2.0
the build was successful Details
2020-06-29 14:56:45 -07:00
Sarah Jamie Lewis 5a4351acdc Add 'LICENSE'
the build was successful Details
2020-06-17 12:38:13 -07:00
Sarah Jamie Lewis 5e507e15fa Merge pull request 'Replace extra25519' (#16) from bugfix into master
the build was successful Details
2020-03-27 20:18:13 -07:00
Sarah Jamie Lewis 7ef301c8b5 Replace extra25519
the build was successful Details
2020-03-27 17:40:21 -07:00
Dan Ballard 8af47de107 Merge pull request 'Fixing a Bug where Multiple Active Connections are being waited on' (#14) from bugfix into master 2020-03-19 17:23:30 -07:00
Sarah Jamie Lewis 0422d36f7b Fixing a Bug where Multiple Active Connections are being waited on 2020-03-19 17:08:19 -07:00
Sarah Jamie Lewis 69a8668d53 Merge branch 'race2' of dan/tapir into master
the build was successful Details
2020-02-11 12:18:04 -08:00
Dan Ballard 525cacffed fix race ondition in base onion service
the build was successful Details
2020-02-11 15:12:23 -05:00
Sarah Jamie Lewis 91a6e17d19 Merge branch 'op' of dan/tapir into master
the build was successful Details
2020-02-07 15:18:09 -08:00
Dan Ballard 021c15ebf8 migrate to standalone op log and conectivity packages
the build was successful Details
2020-02-07 18:11:19 -05:00
Dan Ballard a3262d3478 Merge branch 'master' of https://git.openprivacy.ca/cwtch.im/tapir
the build was successful Details
2019-12-17 14:46:03 -08:00
Dan Ballard 65697b1efb drone no dup builds 2019-12-17 14:45:42 -08:00
Dan Ballard 883a5424c6 Merge branch 'upgrade-ristretto' of cwtch.im/tapir into master
the build was successful Details
2019-12-17 14:37:39 -08:00
Sarah Jamie Lewis b2975e7224 Updating ristretto dependency
the build failed Details
2019-12-17 14:28:34 -08:00
erinn 89ba421cd9 Merge branch 'transcript' of cwtch.im/tapir into master
the build was successful Details
2019-12-02 18:20:59 -08:00
Sarah Jamie Lewis c5e3837a5d Amend sign with token constraint api to use []byte instead of token
the build was successful Details
2019-12-02 13:15:46 -08:00
Sarah Jamie Lewis bcaeb969e4 Merge branch 'transcript' of cwtch.im/tapir into master
the build was successful Details
2019-11-29 18:01:05 -08:00
Sarah Jamie Lewis dcf0635f5f Fixing Versions
the build was successful Details
2019-11-29 13:34:44 -08:00
Sarah Jamie Lewis a0e58b8736 Update per Dans' Comments
the build was successful Details
2019-11-27 15:16:29 -08:00
Sarah Jamie Lewis a19204caf4 Updates given Erinns Comments
the build was successful Details
2019-11-27 13:27:58 -08:00
Sarah Jamie Lewis ff7a32722d Move to ristretto255 lib 2019-11-27 13:27:58 -08:00
Sarah Jamie Lewis 345d11f506 First cut of Token Board 2019-11-27 13:27:58 -08:00
Sarah Jamie Lewis 5b64c2d708 Auditable Store 2019-11-27 13:27:58 -08:00
Dan Ballard ac6e44b09a Merge branch 'transcript' of cwtch.im/tapir into master 2019-08-26 12:43:32 -07:00
Sarah Jamie Lewis ce17c81d7a Adding explicit transcript to auth protocol 2019-08-26 12:25:05 -07:00
Dan Ballard f38efc6bf2 drone to use go modules 2019-08-13 12:23:12 -07:00
Dan Ballard 3a6cd3bd79 Merge branch 'identity' of cwtch.im/tapir into master 2019-08-13 10:54:42 -07:00
Sarah Jamie Lewis 5af71324b2 Updated Logo 2019-08-12 15:50:47 -07:00
Sarah Jamie Lewis 9c91a4e000 Force Failure if remote attempts to authenticate with a different
identity.
2019-08-12 15:31:23 -07:00
Dan Ballard 5e4c386847 Merge branch 'identity' of cwtch.im/tapir into master 2019-08-12 12:15:55 -07:00
Sarah Jamie Lewis f2958b21ca Adding Tapir Logo 2019-08-12 12:14:46 -07:00
Dan Ballard 6515a4e160 Merge branch 'identity' of cwtch.im/tapir into master 2019-08-08 12:43:36 -07:00
Sarah Jamie Lewis 875690aff2 Updating Initialize Identity Interface 2019-08-08 12:07:13 -07:00
Sarah Jamie Lewis 164e91fa17 Exposing App() from Connection Interface 2019-08-08 11:28:55 -07:00
Sarah Jamie Lewis 42d3cb196a Adding Integration Test
- Converted cmd/main to integ test
- Factored in Identity from libricochet-go
- Breaking up Auth run into smaller contained functions
2019-08-08 11:11:31 -07:00
Sarah Jamie Lewis f664afb021 Removing integ test spec 2019-08-07 13:16:23 -07:00
Sarah Jamie Lewis f571fad47a Updating README 2019-08-07 13:15:21 -07:00
Sarah Jamie Lewis 63d6c1a1aa Moving towards a release candidate. 2019-08-07 13:08:02 -07:00
46 changed files with 2958 additions and 541 deletions

73
.drone.yml Normal file
View File

@ -0,0 +1,73 @@
---
kind: pipeline
type: docker
name: linux-test
steps:
- name: fetch
image: golang:1.17.5
volumes:
- name: deps
path: /go
commands:
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
- chmod a+x tor
- export GO111MODULE=on
- go mod download
- go install honnef.co/go/tools/cmd/staticcheck@latest
- name: quality
image: golang:1.17.5
volumes:
- name: deps
path: /go
commands:
- staticcheck ./...
- name: units-tests
image: golang:1.17.5
volumes:
- name: deps
path: /go
commands:
- export PATH=`pwd`:$PATH
- sh testing/tests.sh
- name: integ-test
image: golang:1.17.5
volumes:
- name: deps
path: /go
commands:
- export PATH=`pwd`:$PATH
- go test -race -v git.openprivacy.ca/cwtch.im/tapir/testing
- name: notify-email
image: drillster/drone-email
host: build.openprivacy.ca
port: 25
skip_verify: true
from: drone@openprivacy.ca
when:
status: [ failure ]
- name: notify-gogs
image: openpriv/drone-gogs
pull: if-not-exists
when:
event: pull_request
status: [ success, changed, failure ]
environment:
GOGS_ACCOUNT_TOKEN:
from_secret: gogs_account_token
settings:
gogs_url: https://git.openprivacy.ca
volumes:
# gopath where bin and pkg lives to persist across steps
- name: deps
temp: {}
trigger:
repo: cwtch.im/tapir
branch: master
event:
- push
- pull_request
- tag

7
.gitignore vendored
View File

@ -1,3 +1,10 @@
vendor/
.idea
/tor/
coverage.out
/testing/tor/
/applications/tor/
*.db
/applications/tokenboard/tor/
fuzzing/
*.cover.out

8
LICENSE Normal file
View File

@ -0,0 +1,8 @@
MIT License
Copyright (c) 2019 Open Privacy Research Society
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,7 +1,14 @@
# TAPir: Tiny Anonymous Peer
![](tapir.png)
Tapir is a small library for building p2p applications over anonymous communication systems (right now Tapir only supports Tor v3 Onion Services).
Tapir has been designed as a replacement to the Ricochet protocol.
## Features
* New Authentication Protocol based on v3 Onion Services
* Bidirectional Application Channels.
**Work In Progress**

View File

@ -1,7 +1,24 @@
package tapir
import (
"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
)
// Capability defines a status granted to a connection, from an application. That allows the connection to access
// other Application or functions within an Application.
type Capability string
// Application defines the interface for all Tapir Applications
type Application interface {
NewInstance() Application
Init(connection *Connection)
Init(connection Connection)
Transcript() *core.Transcript
PropagateTranscript(transcript *core.Transcript)
}
// InteractiveApplication defines the interface for interactive Tapir applications (apps that expect the user to send
// and receive messages from)
type InteractiveApplication interface {
Application
Listen()
}

View File

@ -0,0 +1,62 @@
package applications
import (
"git.openprivacy.ca/cwtch.im/tapir"
)
// ApplicationChain is a meta-app that can be used to build complex applications from other applications
type ApplicationChain struct {
TranscriptApp
apps []tapir.Application
endapp tapir.InteractiveApplication
capabilities []tapir.Capability
}
// ChainApplication adds a new application to the chain. Returns a pointer to app so this call
// can itself be chained.
func (appchain *ApplicationChain) ChainApplication(app tapir.Application, capability tapir.Capability) *ApplicationChain {
appchain.apps = append(appchain.apps, app.NewInstance())
appchain.capabilities = append(appchain.capabilities, capability)
return appchain
}
// ChainInteractiveApplication adds an interactive application to the chain. There can only be 1 interactive application.
func (appchain *ApplicationChain) ChainInteractiveApplication(app tapir.InteractiveApplication) *ApplicationChain {
appchain.endapp = app
return appchain
}
// NewInstance should always return a new instantiation of the application.
func (appchain *ApplicationChain) NewInstance() tapir.Application {
applicationChain := new(ApplicationChain)
for _, app := range appchain.apps {
applicationChain.apps = append(applicationChain.apps, app.NewInstance())
}
applicationChain.capabilities = appchain.capabilities
return applicationChain
}
// Init is run when the connection is first started.
func (appchain *ApplicationChain) Init(connection tapir.Connection) {
appchain.TranscriptApp.Init(connection)
for i, app := range appchain.apps {
// propagate the transcript to the app
app.PropagateTranscript(appchain.transcript)
// apply the app to the connection
connection.SetApp(app)
// initialize the application given the connection
app.Init(connection)
// if we hit our guard then carry on, otherwise close...
if !connection.HasCapability(appchain.capabilities[i]) {
connection.Close()
return
}
}
}
// Listen calls listen on the Interactive application
func (appchain *ApplicationChain) Listen() {
if appchain.endapp != nil {
appchain.endapp.Listen()
}
}

View File

@ -1,16 +1,13 @@
package applications
import (
"crypto/rand"
"crypto/subtle"
"cwtch.im/tapir"
"encoding/json"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"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"
"golang.org/x/crypto/sha3"
"time"
)
// AuthMessage is exchanged between peers to obtain the Auth Capability
@ -20,10 +17,11 @@ type AuthMessage struct {
}
// AuthCapability defines the Authentication Capability granted by AuthApp
const AuthCapability = "AUTH"
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
@ -33,13 +31,11 @@ func (ea AuthApp) NewInstance() tapir.Application {
// 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) {
longTermPubKey := ed25519.PublicKey(connection.ID.PublicKeyBytes())
epk, esk, _ := ed25519.GenerateKey(rand.Reader)
ephemeralPublicKey := ed25519.PublicKey(epk)
ephemeralPrivateKey := ed25519.PrivateKey(esk)
ephemeralIdentity := identity.InitializeV3("", &ephemeralPrivateKey, &ephemeralPublicKey)
authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralPublicKey}
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()
@ -51,52 +47,73 @@ func (ea AuthApp) Init(connection *tapir.Connection) {
return
}
// 3DH Handshake
l2e := connection.ID.EDH(remoteAuthMessage.EphemeralPublicKey)
e2l := ephemeralIdentity.EDH(remoteAuthMessage.LongTermPublicKey)
e2e := ephemeralIdentity.EDH(remoteAuthMessage.EphemeralPublicKey)
// We need to define an order for the result concatenation so that both sides derive the same key.
var result [96]byte
if connection.Outbound {
copy(result[0:32], l2e)
copy(result[32:64], e2l)
copy(result[64:96], e2e)
} else {
copy(result[0:32], e2l)
copy(result[32:64], l2e)
copy(result[64:96], e2e)
// 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
}
connection.SetEncryptionKey(sha3.Sum256(result[:]))
// Wait to Sync
time.Sleep(time.Second)
// TODO: Replace this with proper transcript
challengeRemote, err := json.Marshal(remoteAuthMessage)
challengeLocal, err := json.Marshal(authMessage)
challenge := sha3.New512()
if connection.Outbound {
challenge.Write(challengeLocal)
challenge.Write(challengeRemote)
} else {
challenge.Write(challengeRemote)
challenge.Write(challengeLocal)
// 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.
challengeBytes := challenge.Sum([]byte{})
connection.Send(challengeBytes)
// 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()
if subtle.ConstantTimeCompare(challengeBytes, remoteChallenge) == 1 {
connection.SetHostname(utils.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey))
assertedHostname := torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
if subtle.ConstantTimeCompare(append(challengeBytes, []byte(assertedHostname)...), remoteChallenge) == 1 {
connection.SetHostname(assertedHostname)
connection.SetCapability(AuthCapability)
} else {
log.Errorf("Failed Decrypt Challenge: [%x] [%x]\n", remoteChallenge, challengeBytes)
log.Debugf("Failed Decrypt Challenge: [%x] [%x]\n", remoteChallenge, challengeBytes)
connection.Close()
}
}

94
applications/auth_test.go Normal file
View File

@ -0,0 +1,94 @@
package applications
import (
"crypto/rand"
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"golang.org/x/crypto/ed25519"
"testing"
)
type MockConnection struct {
id primitives.Identity
outbound bool
}
func (mc *MockConnection) Init(outbound bool) {
mc.id, _ = primitives.InitializeEphemeralIdentity()
mc.outbound = outbound
}
func (mc MockConnection) Hostname() string {
return mc.id.Hostname()
}
func (mc MockConnection) IsOutbound() bool {
return mc.outbound
}
func (mc MockConnection) ID() *primitives.Identity {
return &mc.id
}
func (mc MockConnection) Expect() []byte {
longTermPubKey := ed25519.PublicKey(mc.id.PublicKeyBytes())
epk, _, _ := ed25519.GenerateKey(rand.Reader)
ephemeralPublicKey := ed25519.PublicKey(epk)
//ephemeralPrivateKey := ed25519.PrivateKey(esk)
//ephemeralIdentity := identity.InitializeV3("", &ephemeralPrivateKey, &ephemeralPublicKey)
authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralPublicKey}
serialized, _ := json.Marshal(authMessage)
return serialized
}
func (MockConnection) SetHostname(hostname string) {
panic("implement me")
}
func (MockConnection) HasCapability(name tapir.Capability) bool {
panic("implement me")
}
func (MockConnection) SetCapability(name tapir.Capability) {
panic("implement me")
}
func (MockConnection) SetEncryptionKey(key [32]byte) {
// no op
}
func (MockConnection) Send(message []byte) error {
// no op
return nil
}
func (MockConnection) Close() {
// no op
}
func (MockConnection) App() tapir.Application {
// no op
return nil
}
func (MockConnection) SetApp(tapir.Application) {
// no op
}
func (MockConnection) IsClosed() bool {
panic("implement me")
}
func (MockConnection) Broadcast(message []byte, capability tapir.Capability) error {
panic("implement me")
}
func TestAuthApp_Failed(t *testing.T) {
var authApp AuthApp
ai := authApp.NewInstance()
mc := new(MockConnection)
mc.Init(true)
ai.Init(mc)
}

View File

@ -0,0 +1,119 @@
package applications
import (
"crypto/sha256"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/log"
ristretto "github.com/gtank/ristretto255"
)
// ProofOfWorkApplication forces the incoming connection to do proof of work before granting a capability
type ProofOfWorkApplication struct {
TranscriptApp
}
// transcript constants
const (
PoWApp = "pow-app"
PoWSeed = "pow-seed"
PoWChallenge = "pow-challenge"
PoWPRNG = "pow-prng"
PoWSolution = "pow-solution"
)
// SuccessfulProofOfWorkCapability is given when a successfully PoW Challenge has been Completed
const SuccessfulProofOfWorkCapability = tapir.Capability("SuccessfulProofOfWorkCapability")
// NewInstance should always return a new instantiation of the application.
func (powapp *ProofOfWorkApplication) NewInstance() tapir.Application {
return new(ProofOfWorkApplication)
}
// Init is run when the connection is first started.
func (powapp *ProofOfWorkApplication) Init(connection tapir.Connection) {
powapp.Transcript().NewProtocol(PoWApp)
if connection.IsOutbound() {
powapp.Transcript().AddToTranscript(PoWSeed, connection.Expect())
solution := powapp.solveChallenge(powapp.Transcript().CommitToTranscript(PoWChallenge), powapp.transcript.CommitToPRNG(PoWPRNG))
powapp.transcript.AddToTranscript(PoWSolution, solution)
connection.Send(solution)
connection.SetCapability(SuccessfulProofOfWorkCapability) // We can self grant.because the server will close the connection on failure
return
}
// We may be the first application, in which case we need to randomize the transcript challenge
// We use the random hostname of the inbound server (if we've authenticated them then the challenge will
// already be sufficiently randomized, so this doesn't hurt)
// It does sadly mean an additional round trip.
powapp.Transcript().AddToTranscript(PoWSeed, []byte(connection.Hostname()))
connection.Send([]byte(connection.Hostname()))
solution := connection.Expect()
challenge := powapp.Transcript().CommitToTranscript(PoWChallenge)
// soft-commitment to the prng, doesn't force the client to use it (but we could technically check that it did, not necessary for the security of this App)
powapp.transcript.CommitToPRNG(PoWPRNG)
powapp.transcript.AddToTranscript(PoWSolution, solution)
if powapp.validateChallenge(challenge, solution) {
connection.SetCapability(SuccessfulProofOfWorkCapability)
return
}
}
// SolveChallenge takes in a challenge and a message and returns a solution
// The solution is a 24 byte nonce which when hashed with the challenge and the message
// produces a sha256 hash with Difficulty leading 0s
func (powapp *ProofOfWorkApplication) solveChallenge(challenge []byte, prng core.PRNG) []byte {
solved := false
var sum [32]byte
solution := []byte{}
solve := make([]byte, len(challenge)+32)
// reuse our allocation
buf := make([]byte, 64)
next := new(ristretto.Scalar)
encodedSolution := make([]byte, 0, 32)
for !solved {
err := prng.Next(buf, next)
if err != nil {
// this will cause the challenge to fail...
log.Errorf("error completing challenge: %v", err)
return nil
}
//lint:ignore SA1019 API this is "deprecated", but without it it will cause an allocation on every single check
solution = next.Encode(encodedSolution)
copy(solve[0:], solution[:])
copy(solve[len(solution):], challenge[:])
sum = sha256.Sum256(solve)
solved = true
for i := 0; i < 2; i++ {
if sum[i] != 0x00 {
solved = false
}
}
// reuse this allocated memory next time...
encodedSolution = encodedSolution[:0]
}
log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum)
return solution[:]
}
// ValidateChallenge returns true if the message and spamguard pass the challenge
func (powapp *ProofOfWorkApplication) validateChallenge(challenge []byte, solution []byte) bool {
if len(solution) != 32 {
return false
}
solve := make([]byte, len(challenge)+32)
copy(solve[0:], solution[0:32])
copy(solve[32:], challenge[:])
sum := sha256.Sum256(solve)
for i := 0; i < 2; i++ {
if sum[i] != 0x00 {
return false
}
}
log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum)
return true
}

67
applications/token_app.go Normal file
View File

@ -0,0 +1,67 @@
package applications
import (
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/log"
)
// TokenApplication provides Tokens for PoW
type TokenApplication struct {
TranscriptApp
TokenService *privacypass.TokenServer
Tokens []*privacypass.Token
}
// HasTokensCapability is granted once the client has obtained signed tokens
const HasTokensCapability = tapir.Capability("HasTokensCapability")
const numTokens = 10
// NewInstance should always return a new instantiation of the application.
func (tokenapp *TokenApplication) NewInstance() tapir.Application {
app := new(TokenApplication)
app.TokenService = tokenapp.TokenService
return app
}
// Init is run when the connection is first started.
func (tokenapp *TokenApplication) Init(connection tapir.Connection) {
tokenapp.Transcript().NewProtocol("token-app")
log.Debugf(tokenapp.Transcript().OutputTranscriptToAudit())
if connection.IsOutbound() {
tokens, blinded := privacypass.GenerateBlindedTokenBatch(numTokens)
data, _ := json.Marshal(blinded)
connection.Send(data)
var signedBatch privacypass.SignedBatchWithProof
err := json.Unmarshal(connection.Expect(), &signedBatch)
if err == nil {
verified := privacypass.UnblindSignedTokenBatch(tokens, blinded, signedBatch.SignedTokens, tokenapp.TokenService.Y, signedBatch.Proof, tokenapp.Transcript())
if verified {
log.Debugf("Successfully obtained signed tokens")
tokenapp.Tokens = tokens
connection.SetCapability(HasTokensCapability)
return
}
// This will close the connection by default and no tokens will be available.
// This usecase can be checked by the existing WaitForCapabilityOrClose() function using the HasTokensCapability
// If the connection closes without the HasTokensCapability then the error can be handled by whatever client needs it
log.Debugf("Failed to verify signed token batch")
}
return
}
// We are the server
var blinded []privacypass.BlindedToken
err := json.Unmarshal(connection.Expect(), &blinded)
if err == nil {
batchProof, err := tokenapp.TokenService.SignBlindedTokenBatch(blinded, tokenapp.Transcript())
if err != nil {
return
}
log.Debugf(tokenapp.Transcript().OutputTranscriptToAudit())
data, _ := json.Marshal(batchProof)
connection.Send(data)
return
}
}

View File

@ -0,0 +1,111 @@
package tokenboard
import (
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/primitives/auditable"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/log"
)
// NewTokenBoardClient generates a new Client for Token Board
func NewTokenBoardClient(store *auditable.Store, handler AppHandler, paymentHandler privacypass.TokenPaymentHandler) tapir.Application {
tba := new(Client)
tba.AuditableStore = store
tba.handler = handler
tba.paymentHandler = paymentHandler
return tba
}
// Client defines a client for the TokenBoard server
type Client struct {
applications.AuthApp
connection tapir.Connection
AuditableStore *auditable.Store
paymentHandler privacypass.TokenPaymentHandler
handler AppHandler
}
// NewInstance Client a new TokenBoardApp
func (ta *Client) NewInstance() tapir.Application {
tba := new(Client)
tba.AuditableStore = ta.AuditableStore
tba.handler = ta.handler
tba.paymentHandler = ta.paymentHandler
return tba
}
// Init initializes the cryptographic TokenBoardApp
func (ta *Client) Init(connection tapir.Connection) {
ta.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
ta.connection = connection
go ta.Listen()
return
}
connection.Close()
}
// Listen processes the messages for this application
func (ta *Client) Listen() {
for {
log.Debugf("Client waiting...")
data := ta.connection.Expect()
if len(data) == 0 {
log.Debugf("Server closed the connection...")
return // connection is closed
}
var message Message
json.Unmarshal(data, &message)
switch message.MessageType {
case postResultMessage:
log.Debugf("Post result: %x", message.PostResult.Proof)
case replayResultMessage:
var state auditable.State
log.Debugf("Replaying %v Messages...", message.ReplayResult.NumMessages)
lastCommit := ta.AuditableStore.LatestCommit
for i := 0; i < message.ReplayResult.NumMessages; i++ {
message := ta.connection.Expect()
state.Messages = append(state.Messages, message)
}
data := ta.connection.Expect()
var signedProof auditable.SignedProof
json.Unmarshal(data, &signedProof)
state.SignedProof = signedProof
err := ta.AuditableStore.AppendState(state)
if err == nil {
log.Debugf("Successfully updated Auditable Store %v", ta.AuditableStore.LatestCommit)
ta.handler.HandleNewMessages(lastCommit)
} else {
log.Debugf("Error updating Auditable Store %v", err)
}
}
}
}
// Replay posts a Replay Message to the server.
func (ta *Client) Replay() {
log.Debugf("Sending replay request for %v", ta.AuditableStore.LatestCommit)
data, _ := json.Marshal(Message{MessageType: replayRequestMessage, ReplayRequest: replayRequest{LastCommit: ta.AuditableStore.LatestCommit}})
ta.connection.Send(data)
}
// PurchaseTokens purchases the given number of tokens from the server (using the provided payment handler)
func (ta *Client) PurchaseTokens() {
ta.paymentHandler.MakePayment()
}
// Post sends a Post Request to the server
func (ta *Client) Post(message auditable.Message) bool {
token, err := ta.paymentHandler.NextToken(message, ta.connection.Hostname())
if err == nil {
data, _ := json.Marshal(Message{MessageType: postRequestMessage, PostRequest: postRequest{Token: token, Message: message}})
ta.connection.Send(data)
return true
}
log.Debugf("No Valid Tokens: %v", err)
return false
}

View File

@ -0,0 +1,52 @@
package tokenboard
import (
"git.openprivacy.ca/cwtch.im/tapir/primitives/auditable"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
)
// AppHandler allows clients to react to specific events.
type AppHandler interface {
HandleNewMessages(previousLastCommit []byte)
}
// MessageType defines the enum for TokenBoard messages
type messageType int
const (
replayRequestMessage messageType = iota
replayResultMessage
postRequestMessage
postResultMessage
)
// Message encapsulates the application protocol
type Message struct {
MessageType messageType
PostRequest postRequest `json:",omitempty"`
PostResult postResult `json:",omitempty"`
ReplayRequest replayRequest `json:",omitempty"`
ReplayResult replayResult `json:",omitempty"`
}
// ReplayRequest requests a reply from the given Commit
type replayRequest struct {
LastCommit []byte
}
// PostRequest requests to post the message to the board with the given token
type postRequest struct {
Token privacypass.SpentToken
Message auditable.Message
}
// PostResult returns the success of a given post attempt
type postResult struct {
Success bool
Proof auditable.SignedProof
}
// ReplayResult is sent by the server before a stream of replayed messages
type replayResult struct {
NumMessages int
}

View File

@ -0,0 +1,92 @@
package tokenboard
// NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed.
import (
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/primitives/auditable"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/log"
)
// NewTokenBoardServer generates new Server for Token Board
func NewTokenBoardServer(tokenService *privacypass.TokenServer, store *auditable.Store) tapir.Application {
tba := new(Server)
tba.TokenService = tokenService
tba.AuditableStore = store
return tba
}
// Server defines the token board server
type Server struct {
applications.AuthApp
connection tapir.Connection
TokenService *privacypass.TokenServer
AuditableStore *auditable.Store
}
// NewInstance creates a new TokenBoardApp
func (ta *Server) NewInstance() tapir.Application {
tba := new(Server)
tba.TokenService = ta.TokenService
tba.AuditableStore = ta.AuditableStore
return tba
}
// Init initializes the cryptographic TokenBoardApp
func (ta *Server) Init(connection tapir.Connection) {
ta.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
ta.connection = connection
go ta.Listen()
return
}
connection.Close()
}
// Listen processes the messages for this application
func (ta *Server) Listen() {
for {
data := ta.connection.Expect()
if len(data) == 0 {
return // connection is closed
}
var message Message
json.Unmarshal(data, &message)
switch message.MessageType {
case postRequestMessage:
postrequest := message.PostRequest
log.Debugf("Received a Post Message Request: %x %x", postrequest.Token, postrequest.Message)
ta.postMessageRequest(postrequest.Token, postrequest.Message)
case replayRequestMessage:
log.Debugf("Received Replay Request %v", message.ReplayRequest)
state := ta.AuditableStore.GetStateAfter(message.ReplayRequest.LastCommit)
response, _ := json.Marshal(Message{MessageType: replayResultMessage, ReplayResult: replayResult{len(state.Messages)}})
log.Debugf("Sending Replay Response %v", replayResult{len(state.Messages)})
ta.connection.Send(response)
for _, message := range state.Messages {
ta.connection.Send(message)
}
data, _ := json.Marshal(state.SignedProof)
ta.connection.Send(data)
}
}
}
func (ta *Server) postMessageRequest(token privacypass.SpentToken, message auditable.Message) {
if err := ta.TokenService.SpendToken(token, append(message, ta.connection.ID().Hostname()...)); err == nil {
log.Debugf("Token is valid")
signedproof := ta.AuditableStore.Add(message)
data, _ := json.Marshal(Message{MessageType: postResultMessage, PostResult: postResult{true, signedproof}})
ta.connection.Send(data)
} else {
log.Debugf("Attempt to spend an invalid token: %v", err)
data, _ := json.Marshal(Message{MessageType: postResultMessage, PostResult: postResult{false, auditable.SignedProof{}}})
ta.connection.Send(data)
}
}

View File

@ -0,0 +1,162 @@
package tokenboard
import (
"errors"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives/auditable"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/connectivity"
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"os"
"runtime"
"sync"
"testing"
"time"
)
type Handler struct {
Store *auditable.Store
}
func (h Handler) HandleNewMessages(previousLastCommit []byte) {
log.Debugf("Handling Messages After %x", previousLastCommit)
messages := h.Store.GetMessagesAfter(previousLastCommit)
for _, message := range messages {
log.Debugf("Message %s", message)
}
}
type FreePaymentHandler struct {
tokens []*privacypass.Token
TokenService *privacypass.TokenServer
ACN connectivity.ACN
ServerHostname string
}
func (fph *FreePaymentHandler) MakePayment() {
id, sk := primitives.InitializeEphemeralIdentity()
client := new(tor.BaseOnionService)
client.Init(fph.ACN, sk, &id)
tokenApplication := new(applications.TokenApplication)
tokenApplication.TokenService = fph.TokenService
powTokenApp := new(applications.ApplicationChain).
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
ChainApplication(tokenApplication, applications.HasTokensCapability)
client.Connect(fph.ServerHostname, powTokenApp)
conn, err := client.WaitForCapabilityOrClose(fph.ServerHostname, applications.HasTokensCapability)
if err == nil {
powtapp, _ := conn.App().(*applications.TokenApplication)
fph.tokens = append(fph.tokens, powtapp.Tokens...)
log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit())
conn.Close()
return
}
log.Debugf("Error making payment: %v", err)
}
func (fph *FreePaymentHandler) NextToken(data []byte, hostname string) (privacypass.SpentToken, error) {
if len(fph.tokens) == 0 {
return privacypass.SpentToken{}, errors.New("No more tokens")
}
token := fph.tokens[0]
fph.tokens = fph.tokens[1:]
return token.SpendToken(append(data, hostname...)), nil
}
func TestTokenBoardApp(t *testing.T) {
// numRoutinesStart := runtime.NumGoroutine()
log.SetLevel(log.LevelDebug)
log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine())
os.MkdirAll("./tor/", 0700)
builder := new(torProvider.TorrcBuilder)
builder.WithSocksPort(9059).WithControlPort(9060).WithHashedPassword("tapir-integration-test").Build("./tor/torrc")
torDataDir := ""
var err error
if torDataDir, err = os.MkdirTemp("./tor/", "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
// Connect to Tor
acn, err := torProvider.NewTorACNWithAuth("./", "", torDataDir, 9060, torProvider.HashedPasswordAuthenticator{Password: "tapir-integration-test"})
if err != nil {
t.Fatalf("could not launch ACN %v", err)
}
acn.WaitTillBootstrapped()
// Generate Server Key
sid, sk := primitives.InitializeEphemeralIdentity()
tokenService := privacypass.NewTokenServer()
serverAuditableStore := new(auditable.Store)
serverAuditableStore.Init(sid)
clientAuditableStore := new(auditable.Store)
// Only initialize with public parameters
sidpubk := sid.PublicKey()
publicsid := primitives.InitializeIdentity("server", nil, &sidpubk)
clientAuditableStore.Init(publicsid)
// Init the Server running the Simple App.
service := new(tor.BaseOnionService)
service.Init(acn, sk, &sid)
// Goroutine Management
sg := new(sync.WaitGroup)
sg.Add(1)
go func() {
service.Listen(NewTokenBoardServer(tokenService, serverAuditableStore))
sg.Done()
}()
// Init the Server running the PoW Token App.
powTokenService := new(tor.BaseOnionService)
spowid, spowk := primitives.InitializeEphemeralIdentity()
powTokenService.Init(acn, spowk, &spowid)
sg.Add(1)
go func() {
tokenApplication := new(applications.TokenApplication)
tokenApplication.TokenService = tokenService
powTokenApp := new(applications.ApplicationChain).
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
ChainApplication(tokenApplication, applications.HasTokensCapability)
powTokenService.Listen(powTokenApp)
sg.Done()
}()
time.Sleep(time.Second * 60) // wait for server to initialize
id, sk := primitives.InitializeEphemeralIdentity()
client := new(tor.BaseOnionService)
client.Init(acn, sk, &id)
client.Connect(sid.Hostname(), NewTokenBoardClient(clientAuditableStore, Handler{Store: clientAuditableStore}, &FreePaymentHandler{ACN: acn, TokenService: tokenService, ServerHostname: spowid.Hostname()}))
client.WaitForCapabilityOrClose(sid.Hostname(), applications.AuthCapability)
conn, _ := client.GetConnection(sid.Hostname())
tba, _ := conn.App().(*Client)
tba.PurchaseTokens()
tba.Post([]byte("HELLO 1"))
tba.Post([]byte("HELLO 2"))
tba.Post([]byte("HELLO 3"))
tba.Post([]byte("HELLO 4"))
tba.Post([]byte("HELLO 5"))
tba.Replay()
time.Sleep(time.Second * 10) // We have to wait for the async replay request!
tba.Post([]byte("HELLO 6"))
tba.Post([]byte("HELLO 7"))
tba.Post([]byte("HELLO 8"))
tba.Post([]byte("HELLO 9"))
tba.Post([]byte("HELLO 10"))
tba.Replay()
time.Sleep(time.Second * 10) // We have to wait for the async replay request!
if tba.Post([]byte("HELLO 11")) {
t.Errorf("Post should have failed.")
}
time.Sleep(time.Second * 10)
acn.Close()
sg.Wait()
}

View File

@ -0,0 +1,50 @@
package applications
import (
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/log"
)
// TranscriptApp defines a Tapir Meta-App which provides a global cryptographic transcript
type TranscriptApp struct {
transcript *core.Transcript
}
// NewInstance creates a new TranscriptApp
func (TranscriptApp) NewInstance() tapir.Application {
ta := new(TranscriptApp)
return ta
}
// Init initializes the cryptographic transcript
func (ta *TranscriptApp) Init(connection tapir.Connection) {
if ta.transcript != nil {
ta.panic()
}
ta.transcript = core.NewTranscript("tapir-transcript")
}
// Transcript returns a pointer to the cryptographic transcript
func (ta *TranscriptApp) Transcript() *core.Transcript {
return ta.transcript
}
// PropagateTranscript overrides the default transcript and propagates a transcript from a previous session
func (ta *TranscriptApp) PropagateTranscript(transcript *core.Transcript) {
if ta.transcript != nil {
ta.panic()
}
ta.transcript = transcript
}
func (ta *TranscriptApp) panic() {
// Note: if this is ever happens it is a critical application bug
// This will prevent a misuse of application chains that cause an earlier
// transcript to be overwritten. Since we expect the security of many higher level applications
// to be reliant on the randomness provided by the transcript we want to be actively hostile to any potential
// misuse.
log.Errorf("apps should not attempt to intitalize or overwrite a transcript once one has been initialized - this is a CRITICAL bug and so we have safely crashed")
// We could silently fail to do anything here, but that is likely more dangerous in the long run...
panic("apps should not attempt to intitalize or overwrite a transcript a transcript once one has been initialized - this is a CRITICAL bug and so we have safely crashed")
}

View File

@ -0,0 +1,22 @@
package applications
import "testing"
func TestTranscriptApp(t *testing.T) {
ta := new(TranscriptApp)
ta.Init(MockConnection{})
ta.Transcript().NewProtocol("test")
ta.transcript.CommitToTranscript("test-commit")
t.Logf(ta.Transcript().OutputTranscriptToAudit())
// Now we test panic'ing....
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic - it definitely should have")
}
}()
// Attempt to reinitialized the transcript, apps should *never* do this and we want to be hostile to that
// behaviour
ta.Init(MockConnection{})
}

View File

@ -1,105 +0,0 @@
package main
import (
"crypto/rand"
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"golang.org/x/crypto/ed25519"
"os"
"time"
)
// SimpleApp is a trivial implementation of a basic p2p application
type SimpleApp struct {
applications.AuthApp
}
// NewInstance should always return a new instantiation of the application.
func (ea SimpleApp) NewInstance() tapir.Application {
return new(SimpleApp)
}
// Init is run when the connection is first started.
func (ea SimpleApp) Init(connection *tapir.Connection) {
// First run the Authentication App
ea.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
// The code for out simple application (We just send and receive "Hello"
connection.Send([]byte("Hello"))
message := connection.Expect()
log.Infof("Received: %q", message)
}
}
// CheckConnection is a simple test that GetConnection is working.
func CheckConnection(service tapir.Service, hostname string) {
for {
_, err := service.GetConnection(hostname)
if err == nil {
log.Infof("Authed!")
return
} else {
log.Errorf("Error %v", err)
}
time.Sleep(time.Second)
}
}
func main() {
log.SetLevel(log.LevelDebug)
// Connect to Tor
var acn connectivity.ACN
acn, _ = connectivity.StartTor("./", "")
acn.WaitTillBootstrapped()
// Generate Server Keys
pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
sk := ed25519.PrivateKey(privateKey)
pk := ed25519.PublicKey(pubkey)
id := identity.InitializeV3("server", &sk, &pk)
// Init a Client to Connect to the Server
client, clienthostname := genclient(acn)
go connectclient(client, pubkey)
// Init the Server running the Simple App.
var service tapir.Service
service = new(tor.BaseOnionService)
service.Init(acn, sk, id)
go CheckConnection(service, clienthostname)
service.Listen(SimpleApp{})
}
func genclient(acn connectivity.ACN) (tapir.Service, string) {
pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
sk := ed25519.PrivateKey(privateKey)
pk := ed25519.PublicKey(pubkey)
id := identity.InitializeV3("client", &sk, &pk)
var client tapir.Service
client = new(tor.BaseOnionService)
client.Init(acn, sk, id)
return client, utils.GetTorV3Hostname(pk)
}
// Client will Connect and launch it's own Echo App goroutine.
func connectclient(client tapir.Service, key ed25519.PublicKey) {
client.Connect(utils.GetTorV3Hostname(key), SimpleApp{})
// Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo
// we will wait a little while then exit.
time.Sleep(time.Second * 5)
conn, _ := client.GetConnection(utils.GetTorV3Hostname(key))
log.Debugf("Client has Auth: %v", conn.HasCapability(applications.AuthCapability))
os.Exit(0)
}

21
go.mod
View File

@ -1,6 +1,21 @@
module cwtch.im/tapir
module git.openprivacy.ca/cwtch.im/tapir
go 1.17
require (
git.openprivacy.ca/openprivacy/libricochet-go v1.0.4
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
filippo.io/edwards25519 v1.0.0
git.openprivacy.ca/openprivacy/connectivity v1.8.6
git.openprivacy.ca/openprivacy/log v1.0.3
github.com/gtank/merlin v0.1.1
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d
)
require (
git.openprivacy.ca/openprivacy/bine v0.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
)

66
go.sum
View File

@ -1,28 +1,56 @@
git.openprivacy.ca/openprivacy/libricochet-go v1.0.4 h1:GWLMJ5jBSIC/gFXzdbbeVz7fIAn2FTgW8+wBci6/3Ek=
git.openprivacy.ca/openprivacy/libricochet-go v1.0.4/go.mod h1:yMSG1gBaP4f1U+RMZXN85d29D39OK5s8aTpyVRoH5FY=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
github.com/cretz/bine v0.1.0 h1:1/fvhLE+fk0bPzjdO5Ci+0ComYxEMuB1JhM4X5skT3g=
github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c=
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
git.openprivacy.ca/openprivacy/connectivity v1.8.6 h1:g74PyDGvpMZ3+K0dXy3mlTJh+e0rcwNk0XF8owzkmOA=
git.openprivacy.ca/openprivacy/connectivity v1.8.6/go.mod h1:Hn1gpOx/bRZp5wvCtPQVJPXrfeUH0EGiG/Aoa0vjGLg=
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c h1:gkfmnY4Rlt3VINCo4uKdpvngiibQyoENVj5Q88sxXhE=
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c/go.mod h1:tDPFhGdt3hJWqtKwx57i9baiB1Cj0yAg22VOPUqm5vY=
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b h1:QrHweqAtyJ9EwCaGHBu1fghwxIPiopAHV06JlXrMHjk=
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b/go.mod h1:xxLb2ip6sSUts3g1irPVHyk/DGslwQsNOo9I7smJfNU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,13 +1,14 @@
package tor
import (
"context"
"crypto/rand"
"cwtch.im/tapir"
"encoding/base64"
"errors"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/ed25519"
"sync"
"time"
@ -15,64 +16,214 @@ import (
// BaseOnionService is a concrete implementation of the service interface over Tor onion services.
type BaseOnionService struct {
connections sync.Map
acn connectivity.ACN
id identity.Identity
privateKey ed25519.PrivateKey
ls connectivity.ListenService
connections sync.Map
acn connectivity.ACN
id *primitives.Identity
privateKey ed25519.PrivateKey
ls connectivity.ListenService
lock sync.Mutex
port int
shutdownChannel chan bool
}
// Metrics provides a report of useful information about the status of the service e.g. the number of active
// connections
func (s *BaseOnionService) Metrics() tapir.ServiceMetrics {
s.lock.Lock()
defer s.lock.Unlock()
count := 0
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if !connection.IsClosed() {
count++
}
return true
})
return tapir.ServiceMetrics{
ConnectionCount: count,
}
}
// Init initializes a BaseOnionService with a given private key and identity
// The private key is needed to initialize the Onion listen socket, ideally we could just pass an Identity in here.
func (s *BaseOnionService) Init(acn connectivity.ACN, sk ed25519.PrivateKey, id identity.Identity) {
func (s *BaseOnionService) Init(acn connectivity.ACN, sk ed25519.PrivateKey, id *primitives.Identity) {
// run add onion
// get listen context
s.acn = acn
s.id = id
s.privateKey = sk
s.port = 9878
// blocking so we can wait on shutdown for closing of this goroutine
s.shutdownChannel = make(chan bool)
go func() {
for s.waitOrTimeout() {
s.GarbageCollect()
}
log.Debugf("closing down garbage collection goroutine")
}()
}
func (s *BaseOnionService) waitOrTimeout() bool {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
select {
case <-s.shutdownChannel:
return false
case <-ctx.Done():
return true
}
}
// SetPort configures the port that the service uses.
func (s *BaseOnionService) SetPort(port int) {
s.port = port
}
// WaitForCapabilityOrClose blocks until the connection has the given capability or the underlying connection is closed
// (through error or user action)
func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name string) (*tapir.Connection, error) {
conn, err := s.GetConnection(cid)
if err == nil {
for {
func (s *BaseOnionService) WaitForCapabilityOrClose(cid string, name tapir.Capability) (tapir.Connection, error) {
attempts := 0
for {
if attempts > 4 {
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if connection.Hostname() == cid {
if !connection.IsClosed() {
connection.Close()
s.connections.Delete(key)
}
}
return true
})
log.Debugf("WaitForCapabilityOrClose attempts exceeded for %v, all connections closed", cid)
return nil, errors.New("failed to acquire capability after multiple attempts, forcibly closing all connections with the peer")
}
if attempts > 0 {
// Allow connections to be torn down / closed before checking again
// There is no point in busy looping...
time.Sleep(time.Second * time.Duration(attempts))
}
// Increment Attempts
attempts++
log.Debugf("Lookup up a connection %v...", cid)
// Lookup the connection...
conn, err := s.GetConnection(cid)
// If there are no active connections then return an error...
if conn == nil {
log.Debugf("no active connection found for %v", cid)
return nil, err
}
if err == nil {
// if we have only one connection and it has the desired capability then return the connection with
// no error...
if conn.HasCapability(name) {
return conn, nil
}
if conn.Closed {
return nil, errors.New("connection is closed")
log.Debugf("Found 1 connections for %v, but it lacks the desired capability %v", cid, name)
continue
}
// If We have 2 connections for the same hostname...
if err != nil {
log.Debugf("found duplicate connections for %v <-> %v %v", s.id.Hostname(), cid, err)
inboundCount := 0
// By convention the lowest lexicographical hostname purges all their outbounds to the higher
// hostname
// Which should only leave a single connection remaining (as we dedupe on connect too)
// This does allow people to attempt to guarantee being the outbound connection but due to the bidirectional
// authentication this should never result in an advantage in the protocol.
// Close all outbound connections to connection
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if connection.Hostname() == cid {
if !connection.IsClosed() && connection.IsOutbound() && s.id.Hostname() < cid {
connection.Close()
s.connections.Delete(key)
}
if !connection.IsClosed() && !connection.IsOutbound() {
inboundCount++
}
}
return true
})
// If we have more than 1 inbound count then forcibly close all connections...
// This shouldn't happen honestly, but if it does then it can cause an infinite check here
if inboundCount > 1 {
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if connection.Hostname() == cid {
if !connection.IsClosed() {
connection.Close()
s.connections.Delete(key)
}
}
return true
})
return nil, errors.New("multiple inbound connections found and closed; the only resolution to this is to close them all and try connecting again")
}
time.Sleep(time.Millisecond * 200)
}
}
return nil, err
}
// GetConnection returns a connection for a given hostname.
func (s *BaseOnionService) GetConnection(hostname string) (*tapir.Connection, error) {
var conn *tapir.Connection
func (s *BaseOnionService) GetConnection(hostname string) (tapir.Connection, error) {
conn := make([]tapir.Connection, 0)
s.connections.Range(func(key, value interface{}) bool {
connection := value.(*tapir.Connection)
if connection.Hostname == hostname {
if !connection.Closed {
conn = connection
return false
connection := value.(tapir.Connection)
if connection.Hostname() == hostname {
if !connection.IsClosed() {
conn = append(conn, connection)
} else {
// Delete this Closed Connection
s.connections.Delete(key)
}
}
return true
})
if conn == nil {
if len(conn) == 0 {
return nil, errors.New("no connection found")
}
return conn, nil
if len(conn) > 1 {
// If there are multiple connections we return the first one but also notify the user (this should be a
// temporary edgecase (see Connect))
return conn[0], errors.New("multiple connections found")
}
return conn[0], nil
}
// GarbageCollect iterates through the connection pool and cleans up any connections that are closed
// that haven't been removed from the map.
func (s *BaseOnionService) GarbageCollect() {
log.Debugf("running garbage collection...")
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if connection.IsClosed() {
// Delete this Closed Connection
s.connections.Delete(key)
}
return true
})
}
// Connect initializes a new outbound connection to the given peer, using the defined Application
func (s *BaseOnionService) Connect(hostname string, app tapir.Application) (bool, error) {
_, err := s.GetConnection(hostname)
if err == nil {
currconn, _ := s.GetConnection(hostname)
// We already have a connection
if currconn != nil {
// Note: This check is not 100% reliable. And we may end up with two connections between peers
// This can happen when a client connects to a server as the server is connecting to the client
// Because at the start of the connection the server cannot derive the true hostname of the client until it
@ -80,6 +231,7 @@ func (s *BaseOnionService) Connect(hostname string, app tapir.Application) (bool
// We mitigate this by performing multiple checks when Connect'ing
return true, errors.New("already connected to " + hostname)
}
// connects to a remote server
// spins off to a connection struct
log.Debugf("Connecting to %v", hostname)
@ -90,14 +242,14 @@ func (s *BaseOnionService) Connect(hostname string, app tapir.Application) (bool
// Second check. If we didn't catch a double connection attempt before the Open we *should* catch it now because
// the auth protocol is quick and Open over onion connections can take some time.
// Again this isn't 100% reliable.
_, err := s.GetConnection(hostname)
if err == nil {
tconn, _ := s.GetConnection(hostname)
if tconn != nil {
conn.Close()
return true, errors.New("already connected to " + hostname)
}
log.Debugf("Connected to %v [%v]", hostname, connectionID)
s.connections.Store(connectionID, tapir.NewConnection(s.id, hostname, true, conn, app.NewInstance()))
s.connections.Store(connectionID, tapir.NewConnection(s, s.id, hostname, true, conn, app.NewInstance()))
return true, nil
}
log.Debugf("Error connecting to %v %v", hostname, err)
@ -116,16 +268,19 @@ func (s *BaseOnionService) getNewConnectionID() string {
func (s *BaseOnionService) Listen(app tapir.Application) error {
// accepts a new connection
// spins off to a connection struct
ls, err := s.acn.Listen(s.privateKey, 9878)
s.lock.Lock()
ls, err := s.acn.Listen(s.privateKey, s.port)
s.ls = ls
log.Debugf("Starting a service on %v ", ls.AddressFull())
s.lock.Unlock()
if err == nil {
log.Debugf("Starting a service on %v ", s.ls.AddressFull())
for {
conn, err := s.ls.Accept()
if err == nil {
tempHostname := s.getNewConnectionID()
log.Debugf("Accepted connection from %v", tempHostname)
s.connections.Store(tempHostname, tapir.NewConnection(s.id, tempHostname, false, conn, app.NewInstance()))
s.connections.Store(tempHostname, tapir.NewConnection(s, s.id, tempHostname, false, conn, app.NewInstance()))
} else {
log.Debugf("Error accepting connection %v", err)
return err
@ -136,11 +291,35 @@ func (s *BaseOnionService) Listen(app tapir.Application) error {
return err
}
// Shutdown closes the service and ensures that any connections are closed.
func (s *BaseOnionService) Shutdown() {
s.ls.Close()
s.lock.Lock()
defer s.lock.Unlock()
if s.ls != nil {
s.ls.Close()
}
// close all existing connections manually
s.connections.Range(func(key, value interface{}) bool {
connection := value.(*tapir.Connection)
connection := value.(tapir.Connection)
connection.Close()
return true
})
// wait for the return of our garbage collection goroutine
s.shutdownChannel <- true
}
// Broadcast sends a message to all connections who possess the given capability
func (s *BaseOnionService) Broadcast(message []byte, capability tapir.Capability) error {
s.lock.Lock()
defer s.lock.Unlock()
s.connections.Range(func(key, value interface{}) bool {
connection := value.(tapir.Connection)
if connection.HasCapability(capability) {
connection.Send(message)
}
return true
})
return nil
}

View File

@ -1,192 +0,0 @@
package main
import (
"bytes"
"compress/gzip"
"crypto/rand"
"crypto/sha512"
"cwtch.im/tapir"
"cwtch.im/tapir/applications"
"cwtch.im/tapir/networks/tor"
"cwtch.im/tapir/primitives"
"encoding/hex"
"encoding/json"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"git.openprivacy.ca/openprivacy/libricochet-go/utils"
"golang.org/x/crypto/ed25519"
"io/ioutil"
"os"
"time"
)
// This example implements a basic notification application which allows peers to notify each other of new messages without downloading
// the entire contents of the server.
// NOTE: Very Incomplete Prototype.
// Notification contains a Topic string and a Message.
type Notification struct {
Topic string // A hex encoded string of the hash of the topic string
Message string
}
// NotificationClient allows publishing and reading from the notifications server
type NotificationClient struct {
applications.AuthApp
connection *tapir.Connection
}
// NewInstance should always return a new instantiation of the application.
func (nc NotificationClient) NewInstance() tapir.Application {
app := new(NotificationClient)
return app
}
// Init is run when the connection is first started.
func (nc *NotificationClient) Init(connection *tapir.Connection) {
// First run the Authentication App
nc.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
nc.connection = connection
}
}
// Publish transforms the given topic string into a hashed ID, and sends the ID along with the message
// NOTE: Server learns the hash of the topic (and therefore can correlate repeated use of the same topic)
func (nc NotificationClient) Publish(topic string, message string) {
log.Debugf("Sending Publish Request")
hashedTopic := sha512.Sum512([]byte(topic))
data, _ := json.Marshal(notificationRequest{RequestType: "Publish", RequestData: map[string]string{"Topic": hex.EncodeToString(hashedTopic[:])}})
nc.connection.Send([]byte(data))
}
// Check returns true if the server might have notifications related to the topic.
// This check reveals nothing about the topic to the server.
func (nc NotificationClient) Check(topic string) bool {
log.Debugf("Sending Filter Request")
// Get an updated bloom filter
data, _ := json.Marshal(notificationRequest{RequestType: "BloomFilter", RequestData: map[string]string{}})
nc.connection.Send(data)
response := nc.connection.Expect()
var bf []primitives.BloomFilter
r, _ := gzip.NewReader(bytes.NewReader(response))
bfb, _ := ioutil.ReadAll(r)
json.Unmarshal(bfb, &bf)
// Check the topic handle in the bloom filter
hashedTopic := sha512.Sum512([]byte(topic))
return bf[time.Now().Hour()].Check(hashedTopic[:])
}
type notificationRequest struct {
RequestType string
RequestData map[string]string
}
// NotificationsServer implements the metadata resistant notifications server
type NotificationsServer struct {
applications.AuthApp
Filter []*primitives.BloomFilter
timeProvider primitives.TimeProvider
}
const DefaultNumberOfBuckets = 24 // 1 per hour of the day
// NewInstance should always return a new instantiation of the application.
func (ns NotificationsServer) NewInstance() tapir.Application {
app := new(NotificationsServer)
app.timeProvider = new(primitives.OSTimeProvider)
app.Filter = make([]*primitives.BloomFilter, DefaultNumberOfBuckets)
for i := range app.Filter {
app.Filter[i] = new(primitives.BloomFilter)
app.Filter[i].Init(1024)
}
return app
}
// Configure overrides the default parameters for the Notification Server
func (ns NotificationsServer) Configure(timeProvider primitives.TimeProvider) {
ns.timeProvider = timeProvider
}
// Init initializes the application.
func (ns NotificationsServer) Init(connection *tapir.Connection) {
// First run the Authentication App
ns.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
for {
request := connection.Expect()
var nr notificationRequest
json.Unmarshal(request, &nr)
log.Debugf("Received Request %v", nr)
switch nr.RequestType {
case "Publish":
log.Debugf("Received Publish Request")
topic := nr.RequestData["Topic"]
// message := nr.RequestData["Message"]
topicID, err := hex.DecodeString(topic)
if err == nil {
currentBucket := ns.timeProvider.GetCurrentTime().Hour()
ns.Filter[currentBucket].Insert(topicID)
}
case "BloomFilter":
log.Debugf("Received Filter Request")
response, _ := json.Marshal(ns.Filter)
var b bytes.Buffer
w := gzip.NewWriter(&b)
w.Write(response)
w.Close()
connection.Send(b.Bytes())
}
}
}
}
func main() {
log.SetLevel(log.LevelDebug)
// Connect to Tor
var acn connectivity.ACN
acn, _ = connectivity.StartTor("./", "")
acn.WaitTillBootstrapped()
// Generate Server Keys
pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
sk := ed25519.PrivateKey(privateKey)
pk := ed25519.PublicKey(pubkey)
id := identity.InitializeV3("server", &sk, &pk)
// Init a Client to Connect to the Server
go client(acn, pubkey)
rm
}
// Client will Connect and launch it's own Echo App goroutine.
func client(acn connectivity.ACN, key ed25519.PublicKey) {
pubkey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
sk := ed25519.PrivateKey(privateKey)
pk := ed25519.PublicKey(pubkey)
id := identity.InitializeV3("client", &sk, &pk)
var client tapir.Service
client = new(tor.BaseOnionService)
client.Init(acn, sk, id)
cid, _ := client.Connect(utils.GetTorV3Hostname(key), new(NotificationClient))
conn, err := client.WaitForCapabilityOrClose(cid, applications.AuthCapability)
if err == nil {
log.Debugf("Client has Auth: %v", conn.HasCapability(applications.AuthCapability))
nc := conn.App.(*NotificationClient)
// Basic Demonstration of Notification
log.Infof("Publishing to #astronomy: %v", nc.Check("#astronomy"))
nc.Publish("#astronomy", "New #Astronomy Post!")
log.Infof("Checking #astronomy: %v", nc.Check("#astronomy"))
}
os.Exit(0)
}

View File

@ -0,0 +1,77 @@
package persistence
import (
"encoding/json"
"git.openprivacy.ca/openprivacy/log"
bolt "go.etcd.io/bbolt"
)
// BoltPersistence creates a persistence services backed by an on-disk bolt database
type BoltPersistence struct {
db *bolt.DB
}
// Open opens a database
func (bp *BoltPersistence) Open(handle string) error {
db, err := bolt.Open(handle, 0600, nil)
bp.db = db
log.Debugf("Loaded the Database")
return err
}
// Setup initializes the given buckets if they do not exist in the database
func (bp *BoltPersistence) Setup(buckets []string) error {
return bp.db.Update(func(tx *bolt.Tx) error {
for _, bucket := range buckets {
tx.CreateBucketIfNotExists([]byte(bucket))
}
return nil
})
}
// Close closes the databases
func (bp *BoltPersistence) Close() {
bp.db.Close()
}
// Persist stores a record in the database
func (bp *BoltPersistence) Persist(bucket string, name string, value interface{}) error {
valueBytes, _ := json.Marshal(value)
return bp.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(bucket))
b.Put([]byte(name), valueBytes)
return nil
})
}
// Check returns true if the record exists in the given bucket.
func (bp *BoltPersistence) Check(bucket string, name string) (bool, error) {
log.Debugf("Checking database: %v %v", bucket, name)
var val []byte
err := bp.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(bucket))
val = b.Get([]byte(name))
return nil
})
if err != nil {
return false, err
} else if val != nil {
return true, nil
}
return false, nil
}
// Load reads a value from a given bucket.
func (bp *BoltPersistence) Load(bucket string, name string, value interface{}) error {
var val []byte
err := bp.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(bucket))
val = b.Get([]byte(name))
return nil
})
if err != nil {
return err
}
return json.Unmarshal(val, &value)
}

View File

@ -0,0 +1,27 @@
package persistence
import (
"os"
"testing"
)
func TestBoltPersistence_Open(t *testing.T) {
os.Remove("test.dbgi")
db := new(BoltPersistence)
db.Open("test.dbgi")
db.Setup([]string{"tokens"})
// 2020.02: Fails in WSL1 because of a mmap issue.
// https://github.com/microsoft/WSL/issues/4873
// Scheduled to be fixed in the 20h1 Win10 release
db.Persist("tokens", "random_value", true)
var exists bool
db.Load("tokens", "random_value", &exists)
if exists {
t.Logf("Successfully stored: %v", exists)
} else {
t.Fatalf("Failure to store record in DB!")
}
db.Close()
}

View File

@ -0,0 +1,11 @@
package persistence
// Service provides a consistent interface for interacting with on-disk, in-memory or server-backed storage
type Service interface {
Open(handle string) error
Setup(buckets []string) error
Persist(bucket string, name string, value interface{}) error
Check(bucket string, name string) (bool, error)
Load(bucket string, name string, value interface{}) error
Close()
}

BIN
persistence/test.dbgi Normal file

Binary file not shown.

39
primitives/3DH.go Normal file
View File

@ -0,0 +1,39 @@
package primitives
import (
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/sha3"
)
// Perform3DH encapsulates a triple-diffie-hellman key exchange.
// In this exchange Alice and Bob both hold longterm identity keypairs
// Both Alice and Bob generate an additional ephemeral key pair:
// Three Diffie Hellman exchanges are then performed:
// Alice Long Term <-> Bob Ephemeral
// Alice Ephemeral <-> Bob Long Term
// Alice Ephemeral <-> Bob Ephemeral
//
// Through this, a unique session key is derived. The exchange is offline-deniable (in the context of Tapir and Onion Service)
func Perform3DH(longtermIdentity *Identity, ephemeralIdentity *Identity, remoteLongTermPublicKey ed25519.PublicKey, remoteEphemeralPublicKey ed25519.PublicKey, outbound bool) ([32]byte, error) {
// 3DH Handshake
l2e, err1 := longtermIdentity.EDH(remoteEphemeralPublicKey)
e2l, err2 := ephemeralIdentity.EDH(remoteLongTermPublicKey)
e2e, err3 := ephemeralIdentity.EDH(remoteEphemeralPublicKey)
if err1 != nil || err2 != nil || err3 != nil {
return [32]byte{}, err1
}
// We need to define an order for the result concatenation so that both sides derive the same key.
var result [96]byte
if outbound {
copy(result[0:32], l2e)
copy(result[32:64], e2l)
copy(result[64:96], e2e)
} else {
copy(result[0:32], e2l)
copy(result[32:64], l2e)
copy(result[64:96], e2e)
}
return sha3.Sum256(result[:]), nil
}

View File

@ -0,0 +1,186 @@
package auditable
// WARNING NOTE: This is a sketch implementation, Not suitable for production use. The real auditable store is still being designed.
import (
"encoding/base64"
"errors"
"git.openprivacy.ca/cwtch.im/tapir/persistence"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/ed25519"
"sync"
)
// SignedProof encapsulates a signed proof
type SignedProof []byte
// Message encapsulates a message for more readable code.
type Message []byte
// State defines an array of messages.
type State struct {
SignedProof SignedProof
Messages []Message
}
const (
auditableDataStoreProtocol = "auditable-data-store"
newMessage = "new-message"
commit = "commit"
collapse = "collapse"
)
// Store defines a cryptographically secure & auditable transcript of messages sent from multiple
// unrelated clients to a server.
type Store struct {
state State
identity primitives.Identity
transcript *core.Transcript
LatestCommit []byte
commits map[string]int
mutex sync.Mutex
db persistence.Service
}
// Init initializes an auditable store
func (as *Store) Init(identity primitives.Identity) {
as.identity = identity
as.transcript = core.NewTranscript(auditableDataStoreProtocol)
as.commits = make(map[string]int)
}
const messageBucket = "auditable-messages"
// LoadFromStorage initializes an auditable store from a DB
func (as *Store) LoadFromStorage(db persistence.Service) {
db.Setup([]string{messageBucket})
var messages []Message
db.Load(messageBucket, "messages", &messages)
log.Debugf("Loaded from Database: %v", len(messages))
for _, message := range messages {
as.add(message)
}
log.Debugf("Loaded %v Messages from the Database", len(messages))
as.db = db
}
// Add adds a message to the auditable store
func (as *Store) Add(message Message) SignedProof {
sp := as.add(message)
if as.db != nil {
as.db.Persist(messageBucket, "messages", as.state.Messages)
}
return sp
}
// Add adds a message to the auditable store
func (as *Store) add(message Message) SignedProof {
as.mutex.Lock()
defer as.mutex.Unlock()
as.transcript.AddToTranscript(newMessage, message)
as.LatestCommit = as.transcript.CommitToTranscript(commit)
as.state.Messages = append(as.state.Messages, message)
as.state.SignedProof = as.identity.Sign(as.LatestCommit)
as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = len(as.state.Messages) - 1
return as.state.SignedProof
}
// GetState returns the current auditable state
func (as *Store) GetState() State {
as.mutex.Lock()
defer as.mutex.Unlock()
return as.state
}
// GetStateAfter returns the current auditable state after a given commitment
func (as *Store) GetStateAfter(commitment []byte) State {
if commitment == nil {
return as.GetState()
}
var state State
state.Messages = as.GetMessagesAfter(commitment)
state.SignedProof = as.identity.Sign(as.LatestCommit)
return state
}
// GetMessagesAfter provides access to messages after the given commit.
func (as *Store) GetMessagesAfter(latestCommit []byte) []Message {
as.mutex.Lock()
defer as.mutex.Unlock()
index, ok := as.commits[base64.StdEncoding.EncodeToString(latestCommit)]
if !ok && len(latestCommit) == 32 {
return []Message{}
} else if len(latestCommit) == 0 {
index = -1
}
return as.state.Messages[index+1:]
}
// AppendState merges a given state onto our state, first verifying that the two transcripts align
func (as *Store) AppendState(state State) error {
next := len(as.state.Messages)
for i, m := range state.Messages {
as.state.Messages = append(as.state.Messages, m)
// We reconstruct the transcript
as.transcript.AddToTranscript(newMessage, m)
as.LatestCommit = as.transcript.CommitToTranscript(commit)
log.Debugf("Adding message %d commit: %x", next+i, as.LatestCommit)
as.commits[base64.StdEncoding.EncodeToString(as.LatestCommit)] = next + i
}
// verify that our state matches the servers signed state
// this is *not* a security check, as a rogue server can simply sign any state
// however committing to a state allows us to build fraud proofs for malicious servers later on.
if !ed25519.Verify(as.identity.PublicKey(), as.LatestCommit, state.SignedProof) {
return errors.New("state is not consistent, the server is malicious")
}
return nil
}
// MergeState merges a given state onto our state, first verifying that the two transcripts align
func (as *Store) MergeState(state State) error {
return as.AppendState(State{Messages: state.Messages[len(as.state.Messages):], SignedProof: state.SignedProof})
}
// VerifyFraudProof - the main idea behind this is as follows:
//
// Every update requires the server to sign, and thus commit to, a transcript
// Clients reconstruct the transcript via MergeState, as such clients can keep track of every commit.
// if a client can present a signed transcript commit from the server that other clients do not have, it is proof
// that either 1) they are out of sync with the server or 2) the server is presenting different transcripts to different people
//
// If, after syncing, the FraudProof still validates, then the server must be malicious.
// the information revealed by publicizing a fraud proof is minimal it only reveals the inconsistent transcript commit
// and not the cause (which could be reordered messages, dropped messages, additional messages or any combination)
func (as *Store) VerifyFraudProof(fraudCommit []byte, signedFraudProof SignedProof, key ed25519.PublicKey) (bool, error) {
if !ed25519.Verify(key, fraudCommit, signedFraudProof) {
// This could happen due to misuse of this function (trying to verify a proof with the wrong public key)
// This could happen if the server lies to us and submits a fake state proof, however we cannot use this to
// prove that the server is acting maliciously
return false, errors.New("signed proof has not been signed by the given public key")
}
_, exists := as.commits[base64.StdEncoding.EncodeToString(fraudCommit)]
if !exists {
// We have a message signed by the server which verifies that a message was inserted into the state at a given index
// However this directly contradicts our version of the state.
// There is still a possibility that we are out of sync with the server and that new messages have since been added
// We assume that the caller has first Merged the most recent state.
return true, nil
}
return false, nil
}
// Collapse constructs a verifiable proof stating that the server has collapsed the previous history into the current
// root = H(onion)
// L = H(Sign(LatestCommit))
func (as *Store) Collapse() {
as.LatestCommit = as.identity.Sign(as.transcript.CommitToTranscript(collapse))
}

View File

@ -0,0 +1,70 @@
package auditable
import (
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/persistence"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/log"
"os"
"testing"
)
func BenchmarkAuditableStore(b *testing.B) {
log.SetLevel(log.LevelDebug)
os.Remove("benchmark-auditablestore.db")
as := new(Store)
serverID, _ := primitives.InitializeEphemeralIdentity()
as.Init(serverID)
db := new(persistence.BoltPersistence)
db.Open("benchmark-auditablestore.db")
as.LoadFromStorage(db)
for i := 0; i < b.N; i++ {
data := fmt.Sprintf("Message %v", i)
as.Add(Message(data))
}
db.Close()
db.Open("benchmark-auditablestore.db")
vs := new(Store)
vs.Init(serverID)
vs.LoadFromStorage(db)
db.Close()
os.Remove("benchmark-auditablestore.db")
}
func TestAuditableStore(t *testing.T) {
as := new(Store)
vs := new(Store)
serverID, _ := primitives.InitializeEphemeralIdentity()
as.Init(serverID)
vs.Init(serverID) // This doesn't do anything
as.Add([]byte("Hello World"))
state := as.GetState()
if vs.MergeState(state) != nil {
t.Fatalf("Fraud Proof Failed on Honest Proof")
}
fraudProof := as.Add([]byte("Hello World 2"))
// If you comment these out it simulates a lying server.
state = as.GetState()
if vs.MergeState(state) != nil {
t.Fatalf("Fraud Proof Failed on Honest Proof")
}
fraud, err := vs.VerifyFraudProof(as.LatestCommit, fraudProof, serverID.PublicKey())
if err != nil {
t.Fatalf("Error validated fraud proof: %v", err)
}
if fraud {
t.Fatalf("Technically a fraud, but the client hasn't updated yet")
}
}

View File

@ -1,62 +0,0 @@
package primitives
import (
"crypto/sha256"
"sync"
)
// BloomFilter implements a bloom filter
type BloomFilter struct {
B []bool
lock sync.Mutex
}
// Init constructs a bloom filter of size m
func (bf *BloomFilter) Init(m int16) {
bf.B = make([]bool, m)
}
// Hash transforms a message to a set of bit flips
// Supports up to m == 65535
func (bf BloomFilter) Hash(msg []byte) []int {
hash := sha256.Sum256(msg)
pos1a := (int(hash[0]) + int(hash[1]) + int(hash[2]) + int(hash[3])) % 0xFF
pos1b := (int(hash[4]) + int(hash[5]) + int(hash[6]) + int(hash[7])) % 0xFF
pos1 := ((pos1a << 8) + pos1b) & (0xFFFF % len(bf.B))
pos2a := (int(hash[8]) + int(hash[9]) + int(hash[10]) + int(hash[11])) % 0xFF
pos2b := (int(hash[12]) + int(hash[13]) + int(hash[14]) + int(hash[15])) % 0xFF
pos2 := ((pos2a << 8) + pos2b) & (0xFFFF % len(bf.B))
pos3a := (int(hash[16]) + int(hash[17]) + int(hash[18]) + int(hash[19])) % 0xFF
pos3b := (int(hash[20]) + int(hash[21]) + int(hash[22]) + int(hash[23])) % 0xFF
pos3 := ((pos3a << 8) + pos3b) & (0xFFFF % len(bf.B))
pos4a := (int(hash[24]) + int(hash[25]) + int(hash[26]) + int(hash[27])) % 0xFF
pos4b := (int(hash[28]) + int(hash[29]) + int(hash[30]) + int(hash[31])) % 0xFF
pos4 := ((pos4a << 8) + pos4b) & (0xFFFF % len(bf.B))
return []int{pos1, pos2, pos3, pos4}
}
// Insert updates the BloomFilter (suitable for concurrent use)
func (bf *BloomFilter) Insert(msg []byte) {
pos := bf.Hash(msg)
bf.lock.Lock()
defer bf.lock.Unlock()
bf.B[pos[0]] = true
bf.B[pos[1]] = true
bf.B[pos[2]] = true
bf.B[pos[3]] = true
}
// Check returns true if the messages might be in the BloomFilter
// (No false positives, possible false negatives due to the probabilistic nature of the filter)
func (bf BloomFilter) Check(msg []byte) bool {
pos := bf.Hash(msg)
if bf.B[pos[0]] && bf.B[pos[1]] && bf.B[pos[2]] && bf.B[pos[3]] {
return true
}
return false
}

View File

@ -0,0 +1,110 @@
package core
import (
"fmt"
"git.openprivacy.ca/openprivacy/log"
"github.com/gtank/merlin"
ristretto "github.com/gtank/ristretto255"
"golang.org/x/crypto/sha3"
"io"
)
// Transcript provides a consistent transcript primitive for our protocols
//
// We have the following goals:
// - Allow sequential proofs over a common transcript (ensuring a single proof cannot be extracted standalone)
// - be able to produce a human-readable transcript for auditing.
//
// The design of this API was inspired by Merlin: https://docs.rs/crate/merlin/
type Transcript struct {
merlinTranscript *merlin.Transcript
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.merlinTranscript = merlin.NewTranscript(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.merlinTranscript.AppendMessage([]byte(label), b)
}
// AddElementToTranscript appends a value to the transcript with the given label
// This binds the given data to the label.
func (t *Transcript) AddElementToTranscript(label string, element *ristretto.Element) {
t.AddToTranscript(label, element.Bytes())
}
// OutputTranscriptToAudit outputs a human-readable copy of the transcript so far.
func (t Transcript) OutputTranscriptToAudit() string {
return t.transcript
}
// NewProtocol provides explicit protocol separation in a transcript (more readable audit scripts and even more explicit
// binding of committed values to a given context)
func (t *Transcript) NewProtocol(label string) {
op := fmt.Sprintf("---- new-protcol: %s ----", label)
t.transcript = fmt.Sprintf("%v\n%v", t.transcript, op)
t.merlinTranscript.AppendMessage([]byte("protocol"), []byte(label))
}
// CommitToTranscript generates a challenge based on the current transcript, it also commits the challenge to the transcript.
func (t *Transcript) CommitToTranscript(label string) []byte {
b := t.merlinTranscript.ExtractBytes([]byte(label), 64)
t.transcript = fmt.Sprintf("%v\nextract %v: %v", t.transcript, label, b)
return b
}
// PRNG defines a psuedorandom number generator
type PRNG struct {
prng io.Reader
}
// Next returns the next "random" scalar from the PRNG
func (prng *PRNG) Next(buf []byte, next *ristretto.Scalar) error {
n, err := io.ReadFull(prng.prng, buf)
if n != 64 || err != nil {
log.Errorf("could not read prng: %v %v", n, err)
return fmt.Errorf("error fetching complete output from prng: %v", err)
}
next.SetUniformBytes(buf)
return nil
}
// CommitToPRNG commits the label to the transcript and derives a PRNG from the transcript.
func (t *Transcript) CommitToPRNG(label string) PRNG {
b := t.merlinTranscript.ExtractBytes([]byte(label), 64)
prng := sha3.NewShake256()
prng.Write(b)
return PRNG{prng: prng}
}
// CommitToGenerator derives a verifiably random generator from the transcript
func (t *Transcript) CommitToGenerator(label string) *ristretto.Element {
c := t.CommitToTranscript(label)
result, _ := new(ristretto.Element).SetUniformBytes(c)
return result
}
// CommitToGenerators derives a set of verifiably random generators from the transcript
func (t *Transcript) CommitToGenerators(label string, n int) (generators []*ristretto.Element) {
for i := 0; i < n; i++ {
generators = append(generators, t.CommitToGenerator(fmt.Sprintf("%v-%d", label, i)))
}
return generators
}
// CommitToTranscriptScalar is a convenience method for CommitToTranscript which returns a ristretto Scalar
func (t *Transcript) CommitToTranscriptScalar(label string) *ristretto.Scalar {
c := t.CommitToTranscript(label)
s := new(ristretto.Scalar)
s.SetUniformBytes(c[:])
return s
}

View File

@ -0,0 +1,30 @@
package core
import (
"testing"
)
func TestNewTranscript(t *testing.T) {
// Some very basic integrity checking
transcript := NewTranscript("label")
transcript.AddToTranscript("action", []byte("test data"))
firstAudit := transcript.OutputTranscriptToAudit()
secondAudit := transcript.OutputTranscriptToAudit()
if firstAudit != secondAudit {
t.Fatalf("Multiple Audit Calls should not impact underlying Transcript")
}
t.Logf("%v", transcript.OutputTranscriptToAudit())
t.Logf("%v", transcript.CommitToTranscript("first commit"))
t.Logf("%v", transcript.OutputTranscriptToAudit())
t.Logf("%v", transcript.CommitToTranscript("second commit"))
t.Logf("%v", transcript.OutputTranscriptToAudit())
transcript.AddToTranscript("action", []byte("test data"))
t.Logf("%v", transcript.CommitToTranscript("third commit"))
t.Logf("%v", transcript.OutputTranscriptToAudit())
}

59
primitives/identity.go Normal file
View File

@ -0,0 +1,59 @@
package primitives
import (
"crypto/rand"
"git.openprivacy.ca/cwtch.im/tapir/utils"
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
"golang.org/x/crypto/ed25519"
)
// Identity is an encapsulation of Name, PrivateKey and other features
// that make up a Tapir client.
// The purpose of Identity is to prevent other classes directly accessing private key
// and to ensure the integrity of security-critical functions.
type Identity struct {
Name string
edpk *ed25519.PrivateKey
edpubk *ed25519.PublicKey
}
// InitializeIdentity is a courtesy function for initializing a V3 Identity in-code.
func InitializeIdentity(name string, pk *ed25519.PrivateKey, pubk *ed25519.PublicKey) Identity {
return Identity{name, pk, pubk}
}
// InitializeEphemeralIdentity generates a new ephemeral identity, the private key of this identity is provided in the response.
func InitializeEphemeralIdentity() (Identity, ed25519.PrivateKey) {
epk, esk, _ := ed25519.GenerateKey(rand.Reader)
ephemeralPublicKey := ed25519.PublicKey(epk)
ephemeralPrivateKey := ed25519.PrivateKey(esk)
ephemeralIdentity := InitializeIdentity("", &ephemeralPrivateKey, &ephemeralPublicKey)
return ephemeralIdentity, ephemeralPrivateKey
}
// PublicKeyBytes returns the public key associated with this Identity in serializable-friendly
// format.
func (i *Identity) PublicKeyBytes() []byte {
return *i.edpubk
}
// PublicKey returns the public key associated with this Identity
func (i *Identity) PublicKey() ed25519.PublicKey {
return *i.edpubk
}
// EDH performs a diffie-hellman operation on this identities private key with the given public key.
func (i *Identity) EDH(key ed25519.PublicKey) ([]byte, error) {
secret, err := utils.EDH(*i.edpk, key)
return secret[:], err
}
// Hostname provides the onion address associated with this Identity.
func (i *Identity) Hostname() string {
return torProvider.GetTorV3Hostname(*i.edpubk)
}
// Sign produces a signature for a given message attributable to the given identity
func (i *Identity) Sign(input []byte) []byte {
return ed25519.Sign(*i.edpk, input)
}

View File

@ -0,0 +1,37 @@
package primitives
import (
"crypto/subtle"
"testing"
)
func TestIdentity_EDH(t *testing.T) {
id1, _ := InitializeEphemeralIdentity()
id2, _ := InitializeEphemeralIdentity()
k1, err1 := id1.EDH(id2.PublicKey())
k2, err2 := id2.EDH(id1.PublicKey())
if err1 == nil && err2 == nil && subtle.ConstantTimeCompare(k1, k2) == 1 {
t.Logf("k1: %x\nk2: %x\n", k1, k2)
} else {
t.Fatalf("The derived keys should be identical")
}
}
func BenchmarkEDH(b *testing.B) {
id1, _ := InitializeEphemeralIdentity()
id2, _ := InitializeEphemeralIdentity()
for i := 0; i < b.N; i++ {
k1, err1 := id1.EDH(id2.PublicKey())
k2, err2 := id2.EDH(id1.PublicKey())
if err1 == nil && err2 == nil && subtle.ConstantTimeCompare(k1, k2) == 1 {
//b.Logf("k1: %x\nk2: %x\n", k1, k2)
} else {
b.Fatalf("The derived keys should be identical")
}
}
}

View File

@ -0,0 +1,17 @@
package privacypass
// Transcript Constants
const (
BatchProofProtocol = "privacy-pass-batch-proof"
BatchProofX = "X-batch"
BatchProofY = "Y-batch"
BatchProofPVector = "P-vector"
BatchProofQVector = "Q-vector"
DLEQX = "X"
DLEQY = "Y"
DLEQP = "P"
DLEQQ = "Q"
DLEQA = "A"
DLEQB = "B"
)

View File

@ -0,0 +1,78 @@
package privacypass
import (
"crypto/rand"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
ristretto "github.com/gtank/ristretto255"
)
// DLEQProof encapsulates a Chaum-Pedersen DLEQ Proof
// gut In Ernest F. Brickell, editor,CRYPTO92,volume 740 ofLNCS, pages 89105. Springer, Heidelberg,August 1993
type DLEQProof struct {
C *ristretto.Scalar
S *ristretto.Scalar
}
// DiscreteLogEquivalenceProof constructs a valid DLEQProof for the given parameters and transcript
// Given Y = kX & Q = kP
// Peggy: t := choose randomly from Zq
//
// A := tX
// B := tP
// c := H(transcript(X,Y,P,Q,A,B))
// s := (t + ck) mod q
//
// Sends c,s to Vicky
func DiscreteLogEquivalenceProof(k *ristretto.Scalar, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) DLEQProof {
private := make([]byte, 64)
rand.Read(private)
t, err := new(ristretto.Scalar).SetUniformBytes(private)
if err != nil {
return DLEQProof{ristretto.NewScalar(), ristretto.NewScalar()}
}
A := new(ristretto.Element).ScalarMult(t, X)
B := new(ristretto.Element).ScalarMult(t, P)
transcript.AddToTranscript(DLEQX, X.Bytes())
transcript.AddToTranscript(DLEQY, Y.Bytes())
transcript.AddToTranscript(DLEQP, P.Bytes())
transcript.AddToTranscript(DLEQQ, Q.Bytes())
transcript.AddToTranscript(DLEQA, A.Bytes())
transcript.AddToTranscript(DLEQB, B.Bytes())
c := transcript.CommitToTranscriptScalar("c")
s := new(ristretto.Scalar).Subtract(t, new(ristretto.Scalar).Multiply(c, k))
return DLEQProof{c, s}
}
// VerifyDiscreteLogEquivalenceProof verifies the DLEQ for the given parameters and transcript
// Given Y = kX & Q = kP and Proof = (c,s)
// Vicky: X' := sX
//
// Y' := cY
// P' := sP
// Q' := cQ
// A' = X'+Y' == sX + cY ?= sG + ckG == (s+ck)X == tX == A
// B' = P'+Q' == sP + cQ ?= sP + ckP == (s+ck)P == tP == B
// c' := H(transcript(X,Y,P,Q,A',B'))
//
// Tests c ?= c
func VerifyDiscreteLogEquivalenceProof(dleq DLEQProof, X *ristretto.Element, Y *ristretto.Element, P *ristretto.Element, Q *ristretto.Element, transcript *core.Transcript) bool {
Xs := new(ristretto.Element).ScalarMult(dleq.S, X)
Yc := new(ristretto.Element).ScalarMult(dleq.C, Y)
Ps := new(ristretto.Element).ScalarMult(dleq.S, P)
Qc := new(ristretto.Element).ScalarMult(dleq.C, Q)
A := new(ristretto.Element).Add(Xs, Yc)
B := new(ristretto.Element).Add(Ps, Qc)
transcript.AddToTranscript(DLEQX, X.Bytes())
transcript.AddToTranscript(DLEQY, Y.Bytes())
transcript.AddToTranscript(DLEQP, P.Bytes())
transcript.AddToTranscript(DLEQQ, Q.Bytes())
transcript.AddToTranscript(DLEQA, A.Bytes())
transcript.AddToTranscript(DLEQB, B.Bytes())
return transcript.CommitToTranscriptScalar("c").Equal(dleq.C) == 1
}

View File

@ -0,0 +1,138 @@
package privacypass
import (
"crypto/hmac"
"crypto/rand"
"encoding/json"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/log"
ristretto "github.com/gtank/ristretto255"
"golang.org/x/crypto/sha3"
)
// Token is an implementation of PrivacyPass
// Davidson A, Goldberg I, Sullivan N, Tankersley G, Valsorda F. Privacy pass: Bypassing internet challenges anonymously. Proceedings on Privacy Enhancing Technologies. 2018 Jun 1;2018(3):164-80.
type Token struct {
t []byte
r *ristretto.Scalar
W *ristretto.Element
}
// GetT returns the underlying bytes for token for use in constraint proofs.
func (t Token) GetT() []byte {
return t.t
}
// BlindedToken encapsulates a Blinded Token
type BlindedToken struct {
P *ristretto.Element
}
// SignedToken encapsulates a Signed (Blinded) Token
type SignedToken struct {
Q *ristretto.Element
}
// SpentToken encapsulates the parameters needed to spend a Token
type SpentToken struct {
T []byte
MAC []byte
}
// TokenPaymentHandler defines an interface with external payment processors
type TokenPaymentHandler interface {
MakePayment()
// Next Token
NextToken(data []byte, hostname string) (SpentToken, error)
}
// GenBlindedToken initializes the Token
// GenToken() & Blind()
func (t *Token) GenBlindedToken() BlindedToken {
t.t = make([]byte, 32)
rand.Read(t.t)
t.r = new(ristretto.Scalar)
b := make([]byte, 64)
rand.Read(b)
t.r.SetUniformBytes(b)
Ht := sha3.Sum512(t.t)
T, _ := new(ristretto.Element).SetUniformBytes(Ht[:])
P := new(ristretto.Element).ScalarMult(t.r, T)
return BlindedToken{P}
}
// unblindSignedToken unblinds a token that has been signed by a server
func (t *Token) unblindSignedToken(token SignedToken) {
t.W = new(ristretto.Element).ScalarMult(new(ristretto.Scalar).Invert(t.r), token.Q)
}
// SpendToken binds the token with data and then redeems the token
func (t *Token) SpendToken(data []byte) SpentToken {
key := sha3.Sum256(append(t.t, t.W.Bytes()...))
mac := hmac.New(sha3.New512, key[:])
mac.Write(data)
return SpentToken{t.t, mac.Sum(nil)}
}
// GenerateBlindedTokenBatch generates a batch of blinded tokens (and their unblinded equivalents)
func GenerateBlindedTokenBatch(num int) (tokens []*Token, blindedTokens []BlindedToken) {
for i := 0; i < num; i++ {
tokens = append(tokens, new(Token))
blindedTokens = append(blindedTokens, tokens[i].GenBlindedToken())
}
return
}
// verifyBatchProof verifies a given batch proof (see also UnblindSignedTokenBatch)
func verifyBatchProof(dleq DLEQProof, Y *ristretto.Element, blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) bool {
transcript.NewProtocol(BatchProofProtocol)
transcript.AddToTranscript(BatchProofX, ristretto.NewGeneratorElement().Bytes())
transcript.AddToTranscript(BatchProofY, Y.Bytes())
transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens)))
transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens)))
prng := transcript.CommitToPRNG("w")
M := ristretto.NewIdentityElement()
Z := ristretto.NewIdentityElement()
buf := make([]byte, 64)
c := new(ristretto.Scalar)
for i := range blindedTokens {
err := prng.Next(buf, c)
if err != nil {
log.Errorf("error verifying batch proof: %v", err)
return false
}
M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M)
Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z)
}
return VerifyDiscreteLogEquivalenceProof(dleq, ristretto.NewGeneratorElement(), Y, M, Z, transcript)
}
// UnblindSignedTokenBatch taking in a set of tokens, their blinded & signed counterparts, a server public key (Y), a DLEQ proof and a transcript
// verifies that the signing procedure has taken place correctly and unblinds the tokens.
func UnblindSignedTokenBatch(tokens []*Token, blindedTokens []BlindedToken, signedTokens []SignedToken, Y *ristretto.Element, proof DLEQProof, transcript *core.Transcript) bool {
verified := verifyBatchProof(proof, Y, blindedTokens, signedTokens, transcript)
if !verified {
log.Debugf("Failed to unblind tokens: %v", transcript.OutputTranscriptToAudit())
return false
}
for i, t := range tokens {
t.unblindSignedToken(signedTokens[i])
}
return true
}
// MarshalJSON - in order to store tokens in a serialized form we need to expose the private, unexported value
// `t`. Note that `r` is not needed to spend the token, and as such we effectively destroy it when we serialize.
// Ideally, go would let us do this with an annotation, alas.
func (t Token) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
T []byte `json:"t"`
W *ristretto.Element
}{
T: t.t,
W: t.W,
})
}

View File

@ -0,0 +1,114 @@
package privacypass
import (
"crypto/sha512"
"git.openprivacy.ca/cwtch.im/tapir/persistence"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/log"
"github.com/gtank/ristretto255"
"golang.org/x/crypto/sha3"
"testing"
)
func TestToken_SpendToken(t *testing.T) {
server := NewTokenServer()
token := new(Token)
blindedToken := token.GenBlindedToken()
signedToken := server.SignBlindedToken(blindedToken)
token.unblindSignedToken(signedToken)
spentToken := token.SpendToken([]byte("Hello"))
if server.SpendToken(spentToken, []byte("Hello World")) == nil {
t.Errorf("Token Should be InValid")
}
if err := server.SpendToken(spentToken, []byte("Hello")); err != nil {
t.Errorf("Token Should be Valid: %v", err)
}
if err := server.SpendToken(spentToken, []byte("Hello")); err == nil {
t.Errorf("Token Should be Spent")
}
}
func TestToken_ConstrainToToken(t *testing.T) {
server := NewTokenServer()
token := new(Token)
blindedToken := token.GenBlindedToken()
signedToken := server.SignBlindedToken(blindedToken)
token.unblindSignedToken(signedToken)
spentToken := token.SpendToken([]byte("Hello"))
if server.SpendToken(spentToken, []byte("Hello World")) == nil {
t.Errorf("Token Should be InValid")
}
token2 := new(Token)
blindedToken2 := token2.GenBlindedToken()
Ht := sha3.Sum512(token.t)
T, _ := new(ristretto255.Element).SetUniformBytes(Ht[:])
// Constraint forces T = kW to be part of the batch proof
// And because the batch proof must prove that *all* inputs share the same key and also checks the servers public key
// We get a consistency check for almost free.
signedTokens, err := server.SignBlindedTokenBatchWithConstraint([]BlindedToken{blindedToken2}, token.t, core.NewTranscript(""))
if err != nil {
t.Fatalf("error signing tokens with constraints")
}
transcript := core.NewTranscript("")
// NOTE: For this to work token.t and token.W need to be obtain by the client from known source e.g. a public message board.
t.Logf("Result of constaint proof %v", UnblindSignedTokenBatch([]*Token{token2}, []BlindedToken{blindedToken2, {P: T}}, append(signedTokens.SignedTokens, SignedToken{token.W}), server.Y, signedTokens.Proof, transcript))
t.Log(transcript.OutputTranscriptToAudit())
}
func TestGenerateBlindedTokenBatch(t *testing.T) {
log.SetLevel(log.LevelDebug)
db := new(persistence.BoltPersistence)
db.Open("tokens.db")
fakeRand := sha512.Sum512([]byte{})
k, _ := ristretto255.NewScalar().SetUniformBytes(fakeRand[:])
server := NewTokenServerFromStore(k, db)
defer server.Close()
clientTranscript := core.NewTranscript("privacyPass")
serverTranscript := core.NewTranscript("privacyPass")
tokens, blindedTokens := GenerateBlindedTokenBatch(10)
batchProof, err := server.SignBlindedTokenBatch(blindedTokens, serverTranscript)
if err != nil {
t.Fatalf("error constructing signed/blinded token batch: %v", err)
}
verified := UnblindSignedTokenBatch(tokens, blindedTokens, batchProof.SignedTokens, server.Y, batchProof.Proof, clientTranscript)
if !verified {
t.Errorf("Something went wrong, the proof did not pass")
}
// Attempt to Spend All the tokens
for _, token := range tokens {
spentToken := token.SpendToken([]byte("Hello"))
if err := server.SpendToken(spentToken, []byte("Hello")); err != nil {
t.Errorf("Token Should be Valid: %v", err)
}
}
t.Logf("Client Transcript,: %s", clientTranscript.OutputTranscriptToAudit())
t.Logf("Server Transcript,: %s", serverTranscript.OutputTranscriptToAudit())
wrongTranscript := core.NewTranscript("wrongTranscript")
verified = UnblindSignedTokenBatch(tokens, blindedTokens, batchProof.SignedTokens, server.Y, batchProof.Proof, wrongTranscript)
if verified {
t.Errorf("Something went wrong, the proof passed with wrong transcript: %s", wrongTranscript.OutputTranscriptToAudit())
}
}

View File

@ -0,0 +1,171 @@
package privacypass
import (
"crypto/hmac"
"crypto/rand"
"encoding/hex"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/persistence"
"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
"git.openprivacy.ca/openprivacy/log"
ristretto "github.com/gtank/ristretto255"
"golang.org/x/crypto/sha3"
"sync"
)
// TokenServer implements a token server.
type TokenServer struct {
k *ristretto.Scalar
Y *ristretto.Element
seen map[string]bool
persistanceService persistence.Service
mutex sync.Mutex
}
// SignedBatchWithProof encapsulates a signed batch of blinded tokens with a batch proof for verification
type SignedBatchWithProof struct {
SignedTokens []SignedToken `json:"st"`
Proof DLEQProof `json:"dp"`
}
const tokenBucket = "tokens"
// NewTokenServer generates a new TokenServer (used mostly for testing with ephemeral instances)
func NewTokenServer() *TokenServer {
k := new(ristretto.Scalar)
b := make([]byte, 64)
_, err := rand.Read(b)
if err != nil {
// unable to generate secure random numbers
panic("unable to generate secure random numbers")
}
k.SetUniformBytes(b)
return &TokenServer{k, new(ristretto.Element).ScalarBaseMult(k), make(map[string]bool), nil, sync.Mutex{}}
}
// NewTokenServerFromStore generates a new TokenServer backed by a persistence service.
func NewTokenServerFromStore(k *ristretto.Scalar, persistenceService persistence.Service) *TokenServer {
tokenServer := NewTokenServer()
persistenceService.Setup([]string{tokenBucket})
// recalculate public key from k
tokenServer.k = k
tokenServer.Y = new(ristretto.Element).ScalarBaseMult(tokenServer.k)
tokenServer.persistanceService = persistenceService
return tokenServer
}
// Close ensures that the database is properly closed...
func (ts *TokenServer) Close() {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.persistanceService.Close()
}
// SignBlindedToken calculates kP for the given BlindedToken P
func (ts *TokenServer) SignBlindedToken(bt BlindedToken) SignedToken {
Q := new(ristretto.Element).ScalarMult(ts.k, bt.P)
return SignedToken{Q}
}
// SignBlindedTokenBatch signs a batch of blinded tokens under a given transcript
func (ts *TokenServer) SignBlindedTokenBatch(blindedTokens []BlindedToken, transcript *core.Transcript) (*SignedBatchWithProof, error) {
var signedTokens []SignedToken
for _, bt := range blindedTokens {
signedTokens = append(signedTokens, ts.SignBlindedToken(bt))
}
proof, err := ts.constructBatchProof(blindedTokens, signedTokens, transcript)
if err != nil {
return nil, err
}
signedProof := SignedBatchWithProof{signedTokens, *proof}
return &signedProof, nil
}
// SignBlindedTokenBatchWithConstraint signs a batch of blinded tokens under a given transcript given a constraint that the tokens must be signed
// by the same public key as an existing token
func (ts *TokenServer) SignBlindedTokenBatchWithConstraint(blindedTokens []BlindedToken, constraintToken []byte, transcript *core.Transcript) (*SignedBatchWithProof, error) {
var signedTokens []SignedToken
for _, bt := range blindedTokens {
signedTokens = append(signedTokens, ts.SignBlindedToken(bt))
}
Ht := sha3.Sum512(constraintToken)
T, err := new(ristretto.Element).SetUniformBytes(Ht[:])
if err != nil {
return nil, err
}
// W == kT
W := new(ristretto.Element).ScalarMult(ts.k, T)
blindedTokens = append(blindedTokens, BlindedToken{P: T})
proof, err := ts.constructBatchProof(blindedTokens, append(signedTokens, SignedToken{Q: W}), transcript)
if err != nil {
return nil, err
}
signedProof := SignedBatchWithProof{signedTokens, *proof}
return &signedProof, nil
}
// constructBatchProof construct a batch proof that all the signed tokens have been signed correctly
func (ts *TokenServer) constructBatchProof(blindedTokens []BlindedToken, signedTokens []SignedToken, transcript *core.Transcript) (*DLEQProof, error) {
transcript.NewProtocol(BatchProofProtocol)
transcript.AddToTranscript(BatchProofX, ristretto.NewGeneratorElement().Bytes())
transcript.AddToTranscript(BatchProofY, ts.Y.Bytes())
transcript.AddToTranscript(BatchProofPVector, []byte(fmt.Sprintf("%v", blindedTokens)))
transcript.AddToTranscript(BatchProofQVector, []byte(fmt.Sprintf("%v", signedTokens)))
prng := transcript.CommitToPRNG("w")
M := ristretto.NewIdentityElement()
Z := ristretto.NewIdentityElement()
buf := make([]byte, 64)
c := new(ristretto.Scalar)
for i := range blindedTokens {
err := prng.Next(buf, c)
if err != nil {
log.Errorf("error constructing batch proof: %v", err)
return nil, err
}
M = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, blindedTokens[i].P), M)
Z = new(ristretto.Element).Add(new(ristretto.Element).ScalarMult(c, signedTokens[i].Q), Z)
}
proof := DiscreteLogEquivalenceProof(ts.k, ristretto.NewGeneratorElement(), ts.Y, M, Z, transcript)
return &proof, nil
}
// SpendToken returns true a SpentToken is valid and has never been spent before, false otherwise.
func (ts *TokenServer) SpendToken(token SpentToken, data []byte) error {
ts.mutex.Lock()
defer ts.mutex.Unlock() // We only want 1 client at a time redeeming tokens to prevent double-spends
if ts.persistanceService == nil {
if _, spent := ts.seen[hex.EncodeToString(token.T)]; spent {
return fmt.Errorf("token: %v has already been spent", token)
}
} else {
spent, err := ts.persistanceService.Check(tokenBucket, hex.EncodeToString(token.T))
if err != nil || spent {
return fmt.Errorf("token: %v has already been spent", token)
}
}
Ht := sha3.Sum512(token.T)
T, err := new(ristretto.Element).SetUniformBytes(Ht[:])
if err != nil {
return err
}
W := new(ristretto.Element).ScalarMult(ts.k, T)
key := sha3.Sum256(append(token.T, W.Bytes()...))
mac := hmac.New(sha3.New512, key[:])
mac.Write(data)
computedMAC := mac.Sum(nil)
result := hmac.Equal(token.MAC, computedMAC)
if result {
if ts.persistanceService == nil {
ts.seen[hex.EncodeToString(token.T)] = true
} else {
ts.persistanceService.Persist(tokenBucket, hex.EncodeToString(token.T), true)
}
return nil
}
return fmt.Errorf("token: %v is invalid and/or has not been signed by this service", token)
}

View File

@ -1,20 +0,0 @@
package primitives
import (
"time"
)
// TimeProvider is an interface used by services to timestamp events. Why not just have them use time.Now()? We want
// to be able to write tests that simulate behavior over several hours, and thus having an interface to abstract away
// time details for the services is very useful.
type TimeProvider interface {
GetCurrentTime() time.Time
}
// OSTimeProvider provides a wrapper around time provider which simply provides the time as given by the operating system.
type OSTimeProvider struct {
}
func (ostp OSTimeProvider) GetCurrentTime() time.Time {
return time.Now()
}

View File

@ -3,134 +3,231 @@ package tapir
import (
"crypto/rand"
"encoding/binary"
"git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
"git.openprivacy.ca/openprivacy/libricochet-go/identity"
"git.openprivacy.ca/openprivacy/libricochet-go/log"
"errors"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/nacl/secretbox"
"io"
"net"
"sync"
)
// ServiceMetrics outlines higher level information about the service e.g. counts of connections
type ServiceMetrics struct {
ConnectionCount int
}
// Service defines the interface for a Tapir Service
type Service interface {
Init(acn connectivity.ACN, privateKey ed25519.PrivateKey, identity identity.Identity)
Init(acn connectivity.ACN, privateKey ed25519.PrivateKey, identity *primitives.Identity)
Connect(hostname string, application Application) (bool, error)
Listen(application Application) error
GetConnection(connectionID string) (*Connection, error)
WaitForCapabilityOrClose(connectionID string, capability string) (*Connection, error)
GetConnection(connectionID string) (Connection, error)
Metrics() ServiceMetrics
Broadcast(message []byte, capability Capability) error
WaitForCapabilityOrClose(connectionID string, capability Capability) (Connection, error)
Shutdown()
}
// Connection Interface
type Connection interface {
Hostname() string
IsOutbound() bool
ID() *primitives.Identity
Expect() []byte
SetHostname(hostname string)
HasCapability(name Capability) bool
SetCapability(name Capability)
SetEncryptionKey(key [32]byte)
Send(message []byte) error
Close()
App() Application
SetApp(application Application)
IsClosed() bool
Broadcast(message []byte, capability Capability) error
}
// Connection defines a Tapir Connection
type Connection struct {
Hostname string
conn net.Conn
type connection struct {
hostname string
conn io.ReadWriteCloser
capabilities sync.Map
encrypted bool
key [32]byte
App Application
ID identity.Identity
Outbound bool
Closed bool
app Application
identity *primitives.Identity
outbound bool
closed bool
MaxLength int
lock sync.Mutex
service Service
expectBuffer []byte
}
// NewConnection creates a new Connection
func NewConnection(id identity.Identity, hostname string, outbound bool, conn net.Conn, app Application) *Connection {
connection := new(Connection)
connection.Hostname = hostname
func NewConnection(service Service, id *primitives.Identity, hostname string, outbound bool, conn io.ReadWriteCloser, app Application) Connection {
connection := new(connection)
connection.hostname = hostname
connection.conn = conn
connection.App = app
connection.ID = id
connection.Outbound = outbound
connection.MaxLength = 1024
go connection.App.Init(connection)
connection.app = app
connection.identity = id
connection.outbound = outbound
connection.MaxLength = 8192
connection.service = service
connection.expectBuffer = make([]byte, 8192)
go connection.app.Init(connection)
return connection
}
// ID returns an identity.Identity encapsulation (for the purposes of cryptographic protocols)
func (c *connection) ID() *primitives.Identity {
return c.identity
}
// App returns the overarching application using this Connection.
func (c *connection) App() Application {
c.lock.Lock()
defer c.lock.Unlock()
return c.app
}
// App returns the overarching application using this Connection.
func (c *connection) SetApp(application Application) {
c.lock.Lock()
defer c.lock.Unlock()
c.app = application
}
// Hostname returns the hostname of the connection (if the connection has not been authorized it will return the
// temporary hostname identifier)
func (c *connection) Hostname() string {
c.lock.Lock()
defer c.lock.Unlock()
return c.hostname
}
// IsOutbound returns true if this caller was the originator of the connection (i.e. the connection was started
// by calling Connect() rather than Accept()
func (c *connection) IsOutbound() bool {
return c.outbound
}
// IsClosed returns true if the connection is closed (connections cannot be reopened)
func (c *connection) IsClosed() bool {
c.lock.Lock()
defer c.lock.Unlock()
return c.closed
}
// SetHostname sets the hostname on the connection
func (c *Connection) SetHostname(hostname string) {
log.Debugf("[%v -- %v] Asserting Remote Hostname: %v", c.ID.Hostname(), c.Hostname, hostname)
c.Hostname = hostname
func (c *connection) SetHostname(hostname string) {
c.lock.Lock()
defer c.lock.Unlock()
log.Debugf("[%v -- %v] Asserting Remote Hostname: %v", c.identity.Hostname(), c.hostname, hostname)
c.hostname = hostname
}
// SetCapability sets a capability on the connection
func (c *Connection) SetCapability(name string) {
log.Debugf("[%v -- %v] Setting Capability %v", c.ID.Hostname(), c.Hostname, name)
func (c *connection) SetCapability(name Capability) {
log.Debugf("[%v -- %v] Setting Capability %v", c.identity.Hostname(), c.hostname, name)
c.capabilities.Store(name, true)
}
// HasCapability checks if the connection has a given capability
func (c *Connection) HasCapability(name string) bool {
func (c *connection) HasCapability(name Capability) bool {
_, ok := c.capabilities.Load(name)
return ok
}
// Close forcibly closes the connection
func (c *Connection) Close() {
func (c *connection) Close() {
c.lock.Lock()
defer c.lock.Unlock()
c.closeInner()
}
func (c *connection) closeInner() {
c.closed = true
c.conn.Close()
}
// Expect blocks and reads a single Tapir packet , from the connection.
func (c *Connection) Expect() []byte {
buffer := make([]byte, c.MaxLength)
n, err := io.ReadFull(c.conn, buffer)
func (c *connection) Expect() []byte {
// Multiple goroutines may invoke methods on a Conn simultaneously.
// As such we don't need to mutex around closed.
n, err := io.ReadFull(c.conn, c.expectBuffer)
if n != c.MaxLength || err != nil {
log.Errorf("[%v -> %v] Wire Error Reading, Read %d bytes, Error: %v", c.Hostname, c.ID.Hostname(), n, err)
c.conn.Close()
c.Closed = true
log.Debugf("[%v -> %v] Wire Error Reading, Read %d bytes, Error: %v", c.hostname, c.identity.Hostname(), n, err)
c.Close() // use the full close function which acquires a lock for the connection state...
return []byte{}
}
c.lock.Lock()
defer c.lock.Unlock()
if c.encrypted {
var decryptNonce [24]byte
copy(decryptNonce[:], buffer[:24])
decrypted, ok := secretbox.Open(nil, buffer[24:], &decryptNonce, &c.key)
copy(decryptNonce[:], c.expectBuffer[:24])
decrypted, ok := secretbox.Open(nil, c.expectBuffer[24:], &decryptNonce, &c.key)
if ok {
copy(buffer, decrypted)
copy(c.expectBuffer, decrypted)
} else {
log.Errorf("[%v -> %v] Error Decrypting Message On Wire", c.Hostname, c.ID.Hostname())
c.conn.Close()
c.Closed = true
log.Errorf("[%v -> %v] Error Decrypting Message On Wire", c.hostname, c.identity.Hostname())
c.closeInner()
return []byte{}
}
}
len, _ := binary.Uvarint(buffer[0:2])
length, _ := binary.Uvarint(c.expectBuffer[0:2])
if length+2 >= uint64(c.MaxLength) {
return []byte{}
}
//cplog.Debugf("[%v -> %v] Wire Receive: (%d) %x", c.hostname, c.ID.Hostname(), len, buffer)
return buffer[2 : len+2]
return c.expectBuffer[2 : length+2]
}
// SetEncryptionKey turns on application-level encryption on the connection using the given key.
func (c *Connection) SetEncryptionKey(key [32]byte) {
func (c *connection) SetEncryptionKey(key [32]byte) {
c.lock.Lock()
defer c.lock.Unlock()
c.key = key
c.encrypted = true
}
// Send writes a given message to a Tapir packet (of 1024 bytes in length).
func (c *Connection) Send(message []byte) {
func (c *connection) Send(message []byte) error {
// We can only encode messages up to maxLength
if len(message) >= c.MaxLength {
log.Errorf("attempting to send a message that is too big")
return errors.New("message too long")
}
buffer := make([]byte, c.MaxLength)
binary.PutUvarint(buffer[0:2], uint64(len(message)))
copy(buffer[2:], message)
c.lock.Lock()
defer c.lock.Unlock()
if c.encrypted {
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
// TODO: Surface is Error
c.conn.Close()
c.Closed = true
log.Errorf("Could not read sufficient randomness %v. Closing connection", err)
c.closeInner()
return errors.New("could not read random")
}
// MaxLength - 40 = MaxLength - 24 nonce bytes and 16 auth tag.
encrypted := secretbox.Seal(nonce[:], buffer[0:c.MaxLength-40], &nonce, &c.key)
copy(buffer, encrypted[0:c.MaxLength])
}
log.Debugf("[%v -> %v] Wire Send %x", c.ID.Hostname(), c.Hostname, buffer)
log.Debugf("[%v -> %v] Wire Send %x", c.identity.Hostname(), c.hostname, buffer)
_, err := c.conn.Write(buffer)
if err != nil {
c.conn.Close()
c.Closed = true
c.closeInner()
}
return err
}
// Broadcast sends a message to all active service connections with a given capability
func (c *connection) Broadcast(message []byte, capability Capability) error {
return c.service.Broadcast(message, capability)
}

BIN
tapir.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

24
testing/quality.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/sh
echo "Checking code quality (you want to see no output here)"
echo ""
echo "Vetting:"
go list ./... | xargs go vet
echo ""
echo "Linting:"
staticcheck ./...
echo "Time to format"
gofmt -l -s -w .
# ineffassign (https://github.com/gordonklaus/ineffassign)
echo "Checking for ineffectual assignment of errors (unchecked errors...)"
ineffassign .
# misspell (https://github.com/client9/misspell/cmd/misspell)
echo "Checking for misspelled words..."
misspell . | grep -v "testing/" | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea"

View File

@ -0,0 +1,158 @@
package testing
import (
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity"
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/ed25519"
"os"
"runtime"
"runtime/pprof"
"sync"
"testing"
"time"
)
// SimpleApp is a trivial implementation of a basic p2p application
type SimpleApp struct {
applications.AuthApp
}
// NewInstance should always return a new instantiation of the application.
func (ea *SimpleApp) NewInstance() tapir.Application {
return new(SimpleApp)
}
// Init is run when the connection is first started.
func (ea *SimpleApp) Init(connection tapir.Connection) {
// First run the Authentication App
ea.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
// The code for out simple application (We just send and receive "Hello"
connection.Send([]byte("Hello"))
message := connection.Expect()
log.Infof("Received: %q", message)
}
}
var AuthSuccess = false
// CheckConnection is a simple test that GetConnection is working.
func CheckConnection(service tapir.Service, hostname string, group *sync.WaitGroup) {
for {
_, err := service.GetConnection(hostname)
if err == nil {
log.Infof("Authed!")
group.Done()
return
}
log.Infof("Waiting for Authentication...%v", err)
time.Sleep(time.Second * 5)
}
}
func TestTapir(t *testing.T) {
numRoutinesStart := runtime.NumGoroutine()
log.SetLevel(log.LevelDebug)
log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine())
// Connect to Tor
os.MkdirAll("./tor/", 0700)
builder := new(torProvider.TorrcBuilder)
builder.WithSocksPort(9059).WithControlPort(9060).WithHashedPassword("tapir-integration-test").Build("./tor/torrc")
torDataDir := ""
var err error
if torDataDir, err = os.MkdirTemp("./tor/", "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
// Connect to Tor
acn, err := torProvider.NewTorACNWithAuth("./", "", torDataDir, 9060, torProvider.HashedPasswordAuthenticator{Password: "tapir-integration-test"})
if err != nil {
t.Fatalf("could not launch ACN %v", err)
}
acn.WaitTillBootstrapped()
// Generate Server Keys
id, sk := primitives.InitializeEphemeralIdentity()
// Init the Server running the Simple App.
service := new(tor.BaseOnionService)
service.Init(acn, sk, &id)
// Goroutine Management
sg := new(sync.WaitGroup)
sg.Add(1)
go func() {
service.Listen(new(SimpleApp))
sg.Done()
}()
// Wait for server to come online
time.Sleep(time.Second * 30)
wg := new(sync.WaitGroup)
wg.Add(2)
// Init a Client to Connect to the Server
client, clienthostname := genclient(acn)
go connectclient(t, client, id.PublicKey(), wg)
CheckConnection(service, clienthostname, wg)
wg.Wait()
// Wait for Garbage Collection...
time.Sleep(time.Second * 60)
log.Infof("Closing ACN...")
client.Shutdown()
service.Shutdown()
acn.Close()
sg.Wait()
time.Sleep(time.Second * 5)
log.Infof("Number of goroutines open at close: %d", runtime.NumGoroutine())
if numRoutinesStart != runtime.NumGoroutine() {
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
t.Errorf("Potential goroutine leak: Num Start:%v NumEnd: %v", numRoutinesStart, runtime.NumGoroutine())
}
if !AuthSuccess {
t.Fatalf("Integration Test FAILED, client did not auth with server")
}
}
func genclient(acn connectivity.ACN) (tapir.Service, string) {
id, sk := primitives.InitializeEphemeralIdentity()
client := new(tor.BaseOnionService)
client.Init(acn, sk, &id)
return client, id.Hostname()
}
// Client will Connect and launch it's own Echo App goroutine.
func connectclient(t *testing.T, client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup) {
client.Connect(torProvider.GetTorV3Hostname(key), new(SimpleApp))
// Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo
// we will wait a little while then exit.
time.Sleep(time.Second * 5)
conn, _ := client.GetConnection(torProvider.GetTorV3Hostname(key))
log.Debugf("Client has Auth: %v", conn.HasCapability(applications.AuthCapability))
if conn.HasCapability(applications.AuthCapability) == false {
t.Errorf("tapir auth failed")
}
// attempt to send a message that is too long
var long [8195]byte
err := conn.Send(long[:])
if err == nil {
t.Errorf("should have errored on message being too long...")
}
AuthSuccess = true
group.Done()
}

View File

@ -0,0 +1,100 @@
package testing
import (
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/ed25519"
"os"
"runtime"
"sync"
"testing"
"time"
)
func TestTapirMaliciousRemote(t *testing.T) {
numRoutinesStart := runtime.NumGoroutine()
log.SetLevel(log.LevelDebug)
log.Infof("Number of goroutines open at start: %d", runtime.NumGoroutine())
// Connect to Tor
os.MkdirAll("./tor/", 0700)
builder := new(torProvider.TorrcBuilder)
builder.WithHashedPassword("tapir-integration-test").Build("./tor/torrc")
// Connect to Tor
torDataDir := ""
var err error
if torDataDir, err = os.MkdirTemp("./tor/", "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
// Connect to Tor
acn, err := torProvider.NewTorACNWithAuth("./", "", torDataDir, 9051, torProvider.HashedPasswordAuthenticator{Password: "tapir-integration-test"})
if err != nil {
t.Fatalf("could not launch ACN %v", err)
}
acn.WaitTillBootstrapped()
// Generate Server Keys, not we generate two sets
id, _ := primitives.InitializeEphemeralIdentity()
id2, sk2 := primitives.InitializeEphemeralIdentity()
// Init the Server running the Simple App.
service := new(tor.BaseOnionService)
// Initialize an onion service with one identity, but the auth app with another, this should
// trigger a failure in authentication protocol
service.Init(acn, sk2, &id)
// Goroutine Management
sg := new(sync.WaitGroup)
sg.Add(1)
go func() {
service.Listen(new(applications.AuthApp))
sg.Done()
}()
// Wait for server to come online
time.Sleep(time.Second * 30)
wg := new(sync.WaitGroup)
wg.Add(1)
// Init a Client to Connect to the Server
log.Infof("initializing the client....")
client, _ := genclient(acn)
go connectclientandfail(client, id2.PublicKey(), wg, t)
wg.Wait()
// Wait for Server to Sync
time.Sleep(time.Second * 2)
log.Infof("closing ACN...")
client.Shutdown()
service.Shutdown()
acn.Close()
sg.Wait()
time.Sleep(time.Second * 5) // wait for goroutines to finish...
log.Infof("Number of goroutines open at close: %d", runtime.NumGoroutine())
if numRoutinesStart != runtime.NumGoroutine() {
t.Errorf("Potential goroutine leak: Num Start:%v NumEnd: %v", numRoutinesStart, runtime.NumGoroutine())
}
}
// Client will Connect and launch it's own Echo App goroutine.
func connectclientandfail(client tapir.Service, key ed25519.PublicKey, group *sync.WaitGroup, t *testing.T) {
client.Connect(torProvider.GetTorV3Hostname(key), new(applications.AuthApp))
// Once connected, it shouldn't take long to authenticate and run the application. So for the purposes of this demo
// we will wait a little while then exit.
time.Sleep(time.Second * 5)
log.Infof("Checking connection status...")
conn, err := client.GetConnection(torProvider.GetTorV3Hostname(key))
if err == nil {
group.Done()
t.Errorf("Connection should have failed! %v %v", conn, err)
}
log.Infof("Successfully failed to authenticate...")
group.Done()
}

21
testing/tests.sh Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
set -e
pwd
go test -race ${1} -coverprofile=applications.cover.out -v ./applications
go test -race ${1} -coverprofile=applications.tokenboard.cover.out -v ./applications/tokenboard
# persistence is broken in WSL
if grep -q -v Microsoft /proc/version; then
go test -race ${1} -coverprofile=persistence.cover.out -v ./persistence
fi
go test -race ${1} -coverprofile=primitives.cover.out -v ./primitives
go test -race ${1} -coverprofile=primitives.auditable.cover.out -v ./primitives/auditable
go test -race ${1} -coverprofile=primitives.core.cover.out -v ./primitives/core
# persistence is broken in WSL
if grep -q -v Microsoft /proc/version; then
go test -race ${1} -coverprofile=primitives.privacypass.cover.out -v ./primitives/privacypass
go test -bench "BenchmarkAuditableStore" -benchtime 1000x primitives/auditable/*.go
fi
echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \
awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out
rm -rf *.cover.out

50
utils/crypto.go Normal file
View File

@ -0,0 +1,50 @@
package utils
import (
"crypto/sha512"
"filippo.io/edwards25519"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/ed25519"
)
// EDH implements diffie hellman using curve25519 keys derived from ed25519 keys
func EDH(privateKey ed25519.PrivateKey, remotePublicKey ed25519.PublicKey) ([]byte, error) {
var privKeyBytes [64]byte
var remotePubKeyBytes [32]byte
copy(privKeyBytes[:], privateKey[:])
copy(remotePubKeyBytes[:], remotePublicKey[:])
var curve25519priv [32]byte
PrivateKeyToCurve25519(&curve25519priv, &privKeyBytes)
remoteCurve25519pub, err := ed25519PublicKeyToCurve25519New(remotePublicKey)
if err != nil {
return []byte{}, err
}
secret, err := curve25519.X25519(curve25519priv[:], remoteCurve25519pub[:])
return secret, err
}
// reproduced from https://github.com/FiloSottile/age/blob/main/agessh/agessh.go#L190
func ed25519PublicKeyToCurve25519New(pk ed25519.PublicKey) ([]byte, error) {
// See https://blog.filippo.io/using-ed25519-keys-for-encryption and
// https://pkg.go.dev/filippo.io/edwards25519#Point.BytesMontgomery.
p, err := new(edwards25519.Point).SetBytes(pk)
if err != nil {
return nil, err
}
return p.BytesMontgomery(), nil
}
// PrivateKeyToCurve25519 converts an ed25519 private key into a corresponding
// curve25519 private key
func PrivateKeyToCurve25519(curve25519Private *[32]byte, privateKey *[64]byte) {
h := sha512.New()
h.Write(privateKey[:32])
digest := h.Sum(nil)
digest[0] &= 248
digest[31] &= 127
digest[31] |= 64
copy(curve25519Private[:], digest)
}