|
| 1 | +# ZF FROST Server (frostd) |
| 2 | + |
| 3 | +One challenge for using FROST is allowing participants to communicate securely |
| 4 | +with one another. Devices are usually behind firewalls and NATs, which make |
| 5 | +direct connections hard. |
| 6 | + |
| 7 | +To mitigate this issue and to make it easier to use FROST, the ZF FROST Server |
| 8 | +(frostd) was created. It is a JSON-HTTP server with a small API to allow |
| 9 | +participants to create signing sessions and to communicate with one another. |
| 10 | + |
| 11 | +It works like this: |
| 12 | + |
| 13 | +- Clients (coordinator or participants) authenticate to the server using a key |
| 14 | + pair, which will likely be the same key pair they use to end-to-end encrypt |
| 15 | + messages. |
| 16 | +- The Coordinator creates a session, specifying the public keys of the |
| 17 | + participants. |
| 18 | +- Participants list sessions they're participating in, and choose the proceed |
| 19 | + with the signing session. |
| 20 | +- Coordinator and Participants run the FROST protocol, end-to-end encrypting |
| 21 | + messages and sending them to the server. |
| 22 | +- The Coordinator closes the session. |
| 23 | + |
| 24 | +Note that the server doesn't really care about the particular key pair being |
| 25 | +used; it is only used to enforce who can send messages to who. |
| 26 | + |
| 27 | +## Compiling, Running and Deploying |
| 28 | + |
| 29 | +You will need to have [Rust and |
| 30 | +Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) |
| 31 | +installed. Run: |
| 32 | + |
| 33 | +``` |
| 34 | +cargo install --git https://github.com/ZcashFoundation/frost-zcash-demo.git --locked frostd |
| 35 | +``` |
| 36 | + |
| 37 | +The `frostd` binary will be installed [per `cargo` |
| 38 | +config](https://doc.rust-lang.org/cargo/commands/cargo-install.html#description) |
| 39 | +and it will likely be in your `$PATH`, so you can run by simply running |
| 40 | +`frostd`. |
| 41 | + |
| 42 | +To deploy the FROST Server, **you need TLS/HTTPS certificates**. We strongly |
| 43 | +recommend using a reverse proxy such as `nginx` to handle TLS and to also add |
| 44 | +denial of service protections. In that case, use the `--no-tls-very-insecure` |
| 45 | +flag in `frostd` and make `nginx` connect to it (see example config below). |
| 46 | + |
| 47 | +If you want to expose `frostd` directly, use the `--tls-cert` and |
| 48 | +`--tls-key` to specify the paths of the PEM-encoded certificate and key. You can |
| 49 | +use [Let's Encrypt](https://letsencrypt.org/) to get a free certificate. |
| 50 | + |
| 51 | + |
| 52 | +### Local Testing |
| 53 | + |
| 54 | +For local testing, you can use the [`mkcert` |
| 55 | +tool](https://github.com/FiloSottile/mkcert). Install it and run: |
| 56 | + |
| 57 | +``` |
| 58 | +mkcert -install |
| 59 | +mkcert localhost 127.0.0.1 ::1 |
| 60 | +``` |
| 61 | + |
| 62 | +Then start the server with: |
| 63 | + |
| 64 | +``` |
| 65 | +frostd --tls-cert localhost+2.pem --tls-key localhost+2-key.pem |
| 66 | +``` |
| 67 | + |
| 68 | + |
| 69 | +### Sample nginx Config |
| 70 | + |
| 71 | +This is a sample nginx config file tested in a Ubuntu deployment (i.e. it |
| 72 | +assumes it's in a `http` block and it's included by `/etc/nginx/nginx.conf`); |
| 73 | +copy it to `/etc/nginx/sites-enabled/frostd` and run `sudo service nginx |
| 74 | +restart`. |
| 75 | + |
| 76 | +The config assumes the certificates were copied to `/etc/ssl`. |
| 77 | + |
| 78 | + |
| 79 | +``` |
| 80 | +limit_req_zone $binary_remote_addr zone=challenge:10m rate=30r/m; |
| 81 | +limit_req_zone $binary_remote_addr zone=create:10m rate=10r/m; |
| 82 | +limit_req_zone $binary_remote_addr zone=other:10m rate=240r/m; |
| 83 | +limit_conn_zone $binary_remote_addr zone=addr:10m; |
| 84 | +
|
| 85 | +server { |
| 86 | + listen 443 ssl; |
| 87 | + listen [::]:443 ssl; |
| 88 | + ssl_certificate /etc/ssl/localhost+2.pem; |
| 89 | + ssl_certificate_key /etc/ssl/localhost+2-key.pem; |
| 90 | + ssl_protocols TLSv1.3; |
| 91 | + ssl_ecdh_curve X25519:prime256v1:secp384r1; |
| 92 | + ssl_prefer_server_ciphers off; |
| 93 | +
|
| 94 | + server_name localhost; |
| 95 | +
|
| 96 | + client_body_timeout 5s; |
| 97 | + client_header_timeout 5s; |
| 98 | +
|
| 99 | + location / { |
| 100 | + proxy_pass http://127.0.0.1:2744; |
| 101 | + limit_req zone=other burst=5; |
| 102 | + limit_conn addr 10; |
| 103 | + } |
| 104 | + location /challenge { |
| 105 | + proxy_pass http://127.0.0.1:2744/challenge; |
| 106 | + limit_req zone=challenge burst=3; |
| 107 | + limit_conn addr 10; |
| 108 | + } |
| 109 | + location /create_new_session { |
| 110 | + proxy_pass http://127.0.0.1:2744/create_new_session; |
| 111 | + limit_req zone=create burst=3; |
| 112 | + limit_conn addr 10; |
| 113 | + } |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +## API |
| 118 | + |
| 119 | +The API uses JSON/HTTP. All requests should have `Content-Type: |
| 120 | +application/json`. Errors are returned with status code 500 and the content |
| 121 | +body will have a JSON such as: |
| 122 | + |
| 123 | +``` |
| 124 | +{ code: 1, msg: "error message" } |
| 125 | +``` |
| 126 | + |
| 127 | +The |
| 128 | +[codes](https://github.com/ZcashFoundation/frost-zcash-demo/blob/548a8a7329c6eed8180464662f430d12cd71dfcc/frostd/src/lib.rs#L95-L98) |
| 129 | +are: |
| 130 | + |
| 131 | +``` |
| 132 | +pub const INVALID_ARGUMENT: usize = 1; |
| 133 | +pub const UNAUTHORIZED: usize = 2; |
| 134 | +pub const SESSION_NOT_FOUND: usize = 3; |
| 135 | +pub const NOT_COORDINATOR: usize = 4; |
| 136 | +``` |
| 137 | + |
| 138 | + |
| 139 | +### Usage flow |
| 140 | + |
| 141 | +For the Coordinator: |
| 142 | + |
| 143 | +- Log in with `/challenge` and `/login` |
| 144 | +- Create a new signing session with `/create_new_session` |
| 145 | +- Wait for round 1 messages by repeatedly polling `/receive` each 2 seconds or longer |
| 146 | +- Send round 2 messages by using `/send` |
| 147 | +- Wait for round 2 message by repeatedly polling `/receive` each 2 seconds or longer |
| 148 | +- Close the session with `/close_session` |
| 149 | + |
| 150 | +For Participants: |
| 151 | + |
| 152 | +- Log in with `/challenge` and `/login` |
| 153 | +- Wait for signing sessions with `/list_sessions`, either by the user's request or by repeatedly |
| 154 | + polling each 10 seconds or longer |
| 155 | +- Get the session information with `/get_session_info` |
| 156 | +- Show the user the session information (who the participants are) to select which |
| 157 | + session (if more than one) |
| 158 | +- Send round 1 message by using `/send` |
| 159 | +- Wait for round 2 message by repeatedly polling `/receive` each 2 seconds or longer |
| 160 | +- Send round 2 message by using `/send` |
| 161 | + |
| 162 | +```admonish info |
| 163 | +**Polling** is not optimal. The server will support a better mechanism in the |
| 164 | +future. |
| 165 | +``` |
| 166 | + |
| 167 | +```admonish info |
| 168 | +Selecting sessions is tricky. Ideally, the user should select what session |
| 169 | +to proceed by checking the message being signed; however, that is usually |
| 170 | +sent in Round 2. There are multiple ways to handle this: |
| 171 | +
|
| 172 | +- Simply show the users who are participants, hoping that is enough to |
| 173 | + disambiguate (we assume that concurrent signing sessions won't be that common) |
| 174 | +- Quietly proceed with all sessions, and only prompt the user after the message |
| 175 | + is received. (It's harmless to do round 1 of FROST even if the user might |
| 176 | + not have agreed to sign the message yet.) |
| 177 | +- Change the application so that the message is sent to the participants first |
| 178 | + (the server does not really care how the protocol is run). |
| 179 | +``` |
| 180 | + |
| 181 | +```admonish critical |
| 182 | +Always gather consent from the user by showing them the message before |
| 183 | +signing it. |
| 184 | +``` |
| 185 | + |
| 186 | +### `/challenge` |
| 187 | + |
| 188 | +Input: empty |
| 189 | + |
| 190 | +Sample output: |
| 191 | + |
| 192 | +``` |
| 193 | +{"challenge":"2c5cdb6d-a7db-470e-9e6f-2a7062532825"} |
| 194 | +``` |
| 195 | + |
| 196 | +Returns a challenge that the client will need to sign in order to authenticate. |
| 197 | + |
| 198 | +### `/login` |
| 199 | + |
| 200 | +To call `/login`, you will need to sign the challenge with XEdDSA, see |
| 201 | +[example](https://github.com/ZcashFoundation/frost-zcash-demo/blob/548a8a7329c6eed8180464662f430d12cd71dfcc/frostd/tests/integration_tests.rs#L443-L476). |
| 202 | +Sign the challenge UUID, converted to bytes. |
| 203 | + |
| 204 | + |
| 205 | +Input sample: |
| 206 | + |
| 207 | +``` |
| 208 | +{ |
| 209 | + "challenge":"b771757e-085a-4a88-ab8f-28bd4ba67f3a", |
| 210 | + "pubkey":"f5bf1b8194e20ebdd28e662b1efcf1c5cd2aaade5d5dd83cf89b246b5492726b", |
| 211 | + "signature":"bba398d0963ab88e28134ad41c127eeee816a219838db01dd7bcd9d7fcd975f082330c134e6f7238580ba8434652aa116891495452d9048f5615e07f4ad6b204" |
| 212 | +} |
| 213 | +``` |
| 214 | + |
| 215 | +Output sample: |
| 216 | + |
| 217 | +``` |
| 218 | +{"access_token":"061a18ba-2c3c-4685-a79e-2c0c93000af5"} |
| 219 | +``` |
| 220 | + |
| 221 | +The returned access token must be included as a bearer token in an |
| 222 | +`Authorization` header; e.g. `Authorization: Bearer |
| 223 | +061a18ba-2c3c-4685-a79e-2c0c93000af5`. |
| 224 | + |
| 225 | +Access tokens are currently valid for 1 hour. It's recommended to login at the |
| 226 | +beginning of each FROST session; log in again if it needs to take longer. |
| 227 | + |
| 228 | +### `/logout` |
| 229 | + |
| 230 | +Input: empty (it will logout the authenticated user) |
| 231 | + |
| 232 | +Output: empty |
| 233 | + |
| 234 | +Logs out, invalidating the access token. Note that access tokens expire after |
| 235 | +1 hour anyway. |
| 236 | + |
| 237 | +### `/create_new_session` |
| 238 | + |
| 239 | +Input sample: |
| 240 | + |
| 241 | +``` |
| 242 | +{ |
| 243 | + "pubkeys": [ |
| 244 | + "3c9f4a3b2ae28c8e11fbc90b693a9712c181275fb4b554a140c68dc13cdd9b4c", |
| 245 | + "edbd661dec0a9d0468b4a166a4afa80560d769f6bcb152fb8f4224059329a518" |
| 246 | + ], |
| 247 | + message_count: 1, |
| 248 | +} |
| 249 | +``` |
| 250 | + |
| 251 | +Output sample: |
| 252 | + |
| 253 | +``` |
| 254 | +{"session_id": "2c5cdb6d-a7db-470e-9e6f-2a7062532825"} |
| 255 | +``` |
| 256 | + |
| 257 | +Creates a new session. The requesting user will be the Coordinator, and the |
| 258 | +users with the hex-encoded public keys given in `pubkeys` will be the |
| 259 | +participants (which might or might not include the Coordinator itself). |
| 260 | + |
| 261 | +The `message_count` parameter allows signing more than one message in the same |
| 262 | +signing session, which will save roundtrips. This does not impacts the server |
| 263 | +itself and is used to signal the participants (via `/get_session_info`). |
| 264 | + |
| 265 | +### `/list_sessions` |
| 266 | + |
| 267 | +Input: empty (it will list for the authenticated user) |
| 268 | + |
| 269 | +Output sample: |
| 270 | + |
| 271 | +``` |
| 272 | +{"session_ids": ["2c5cdb6d-a7db-470e-9e6f-2a7062532825"]} |
| 273 | +``` |
| 274 | + |
| 275 | +List the sessions IDs of the session a participant is in. |
| 276 | + |
| 277 | +### `/get_session_info` |
| 278 | + |
| 279 | +Input sample: |
| 280 | + |
| 281 | +```{"session_id": "2c5cdb6d-a7db-470e-9e6f-2a7062532825"}``` |
| 282 | + |
| 283 | +Output sample: |
| 284 | + |
| 285 | +``` |
| 286 | +{ |
| 287 | + "message_count": 1, |
| 288 | + "pubkeys": [ |
| 289 | + "3c9f4a3b2ae28c8e11fbc90b693a9712c181275fb4b554a140c68dc13cdd9b4c", |
| 290 | + "edbd661dec0a9d0468b4a166a4afa80560d769f6bcb152fb8f4224059329a518" |
| 291 | + ], |
| 292 | + "coordinator_pubkey": "3c9f4a3b2ae28c8e11fbc90b693a9712c181275fb4b554a140c68dc13cdd9b4c", |
| 293 | +} |
| 294 | +``` |
| 295 | + |
| 296 | +Returns information about the given session. |
| 297 | + |
| 298 | +### `/send` |
| 299 | + |
| 300 | +Input sample: |
| 301 | + |
| 302 | +``` |
| 303 | +{ |
| 304 | + "session_id": "2c5cdb6d-a7db-470e-9e6f-2a7062532825", |
| 305 | + "recipients": ["3c9f4a3b2ae28c8e11fbc90b693a9712c181275fb4b554a140c68dc13cdd9b4c"], |
| 306 | + "msg": "000102", |
| 307 | +} |
| 308 | +``` |
| 309 | + |
| 310 | +Output: empty |
| 311 | + |
| 312 | +Sends a (hex-encoded) message to one or more participants. To send to the |
| 313 | +Coordinator, pass an empty list in `recipients` (**do not** use the |
| 314 | +Coordinator's public key, because that might be ambiguous if they're also a |
| 315 | +Participant). |
| 316 | + |
| 317 | +```admonish critical |
| 318 | +Messages **MUST** be end-to-end encrypted between recipients. The server can't |
| 319 | +enforce this and if you fail to encrypt them then the server could read |
| 320 | +all the messages. |
| 321 | +``` |
| 322 | + |
| 323 | +### `/receive` |
| 324 | + |
| 325 | +Input sample: |
| 326 | + |
| 327 | +``` |
| 328 | +{ |
| 329 | + "session_id": "2c5cdb6d-a7db-470e-9e6f-2a7062532825", |
| 330 | + "as_coordinator": false, |
| 331 | +} |
| 332 | +``` |
| 333 | + |
| 334 | +Output sample: |
| 335 | + |
| 336 | +``` |
| 337 | +{ |
| 338 | + "msgs":[ |
| 339 | + { |
| 340 | + "sender": "3c9f4a3b2ae28c8e11fbc90b693a9712c181275fb4b554a140c68dc13cdd9b4c", |
| 341 | + "msg": "000102", |
| 342 | + } |
| 343 | + ] |
| 344 | +} |
| 345 | +``` |
| 346 | + |
| 347 | +Receives messages sent to the requesting user. Note that if a user is both a |
| 348 | +Coordinator and a Participant, it is not possible to distinguish if a message |
| 349 | +received from them was sent as Coordinator or as a Participant. This does not |
| 350 | +matter in FROST since this ambiguity never arises (Participants always receive |
| 351 | +messages from the Coordinator, and vice-versa, except during DKG where there is |
| 352 | +no Coordinator anyway). |
| 353 | + |
| 354 | +### `/close_session` |
| 355 | + |
| 356 | +Input sample: |
| 357 | + |
| 358 | +```{"session_id": "2c5cdb6d-a7db-470e-9e6f-2a7062532825"}``` |
| 359 | + |
| 360 | +Output: empty |
| 361 | + |
| 362 | +Closes the given session. Only the Coordinator who created the session can close |
| 363 | +it. Sessions also expire by default after 24 hours. |
0 commit comments