File
Blob: src/workerd/server/channel-token.capnp
| 1 | # Copyright (c) 2025 Cloudflare, Inc. |
| 2 | # Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | # https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | @0xc086f616deb649e5; |
| 6 | |
| 7 | using Cxx = import "/capnp/c++.capnp"; |
| 8 | $Cxx.namespace("workerd::server"); |
| 9 | $Cxx.allowCancellation; |
| 10 | |
| 11 | using Frankenvalue = import "/workerd/io/frankenvalue.capnp".Frankenvalue; |
| 12 | |
| 13 | struct ChannelToken { |
| 14 | # Internal structure of a channel token in workerd, as returned by |
| 15 | # {SubrequestChannel,ActorClassChannel}::getToken(). |
| 16 | # |
| 17 | # For RPC tokens, this structure is encoded as a "packed" capnp message and then AES-GCM |
| 18 | # encrypted using a secret service key and random IV to form the final token. The full token |
| 19 | # contains, in order: |
| 20 | # * 4-byte magic number: 0x821dad26 (little-endian encoded) |
| 21 | # * 12-byte IV |
| 22 | # * 16-byte key ID (prefix of SHA-256 hash of secret key, not encrypted) |
| 23 | # * ciphertext |
| 24 | # * 16-byte MAC (covering ciphertext and the first 32 bytes as AAD) |
| 25 | # |
| 26 | # The encryption (particularly the MAC) is important in order to ensure that someone speaking to |
| 27 | # workerd over RPC cannot trivially invoke an arbitrary service with arbitrary props by simply |
| 28 | # presenting a channel token. |
| 29 | # |
| 30 | # As of this writing, for RPC tokens, the secret key is generated randomly at process startup. |
| 31 | # This means that RPC tokens are only usable within the same workerd process that created them, |
| 32 | # which also has the side effect of meaning there is no backwards-compatibilty concern. The |
| 33 | # format is likely to change in the future once we figure out more how it should actually be used. |
| 34 | # |
| 35 | # The 16-byte key ID is included to enable routing. Hypothetically, if workerd processes were to |
| 36 | # register their key IDs in some lookup service, then based on the key ID you could find an |
| 37 | # appropriate workerd instance to connect to to use this token. As of this writing, this is still |
| 38 | # speculative. |
| 39 | # |
| 40 | # For storage tokens -- which are only permitted today with the experimental |
| 41 | # allow_irrevocable_stub_storage compat flag -- the format is: |
| 42 | # * 4-byte magic number: 0x9082806d (little-endian encoded) |
| 43 | # * plaintext ("packed" ChannelToken) |
| 44 | # |
| 45 | # There is no encryption in this case. This format is experimental and will be replaced in the |
| 46 | # future with a different kind of token that refers into some sort of token grant table. |
| 47 | |
| 48 | const rpcTokenMagic :UInt32 = 0x821dad26; |
| 49 | const storageTokenMagic :UInt32 = 0x9082806d; |
| 50 | |
| 51 | type @0 :Type; |
| 52 | # What type of channel does this point to? This is encoded as a safety measure. In normal |
| 53 | # operation the envelope containing the token always knows what type it is meant to be, but we |
| 54 | # want to prevent any possible shenanigans from someone taking a channel token of one type and |
| 55 | # trying to stuff it in an envelope for a different type. |
| 56 | |
| 57 | enum Type { |
| 58 | subrequest @0; # token for IoChannelFactory::SubrequestChannel |
| 59 | actorClass @1; # token for IoChannelFactory::ActorClassChannel |
| 60 | } |
| 61 | |
| 62 | name @1 :Text; |
| 63 | # Name of the service in the workerd config's services list. |
| 64 | |
| 65 | entrypoint @2 :Text; |
| 66 | # Name of the entrypoint the channel points at. For subrequest channels this must be a |
| 67 | # WorkerEntrypoint derivative (or plain object implementing `ExportedHandlers`). For actor class |
| 68 | # channels this must be a `DurableObject` implementation. |
| 69 | |
| 70 | props @3 :Frankenvalue; |
| 71 | |
| 72 | struct FrankenvalueCapTable { |
| 73 | # CapTable representation for `ChannelToken.props`. |
| 74 | |
| 75 | caps @0 :List(Cap); |
| 76 | |
| 77 | struct Cap { |
| 78 | union { |
| 79 | unknown @0 :Void; |
| 80 | # Dummy default value, never appears in practice. |
| 81 | |
| 82 | subrequestChannel @1 :Data; |
| 83 | actorClassChannel @2 :Data; |
| 84 | # Nested capabilities are represented using fully encoded channel tokens themselves (rather |
| 85 | # than ChannelToken capnp structs) for a couple reasons: |
| 86 | # 1. This simplifies the abstractions needed when encoding a channel token -- just call |
| 87 | # getToken() on any nested channels. |
| 88 | # 2. Channel tokens may be tied to the particular workerd instance that they came from, and |
| 89 | # cannot be decoded on other instances, but I suspect we will eventually want to support |
| 90 | # props containing capabilities pointing to other workerd instances. |
| 91 | } |
| 92 | } |
| 93 | } |
| 94 | } |