This is a proposal for handing accounts and quotas in Tahoe. Nothing is final yet.. we are still evaluating the options. = Accounts = The basic Tahoe account is defined by a DSA key pair. The holder of the private key has the ability to consume storage in conjunction with a specific account number. The Account Server has a long-term keypair. Valid accounts are marked as such by the Account Server's signature on a "membership card", which binds a specific pubkey to an account number and declares that this pair is a valid account. Each Storage Server which participages in the AS's domain will have the AS's pubkey in its list of valid AS keys, and will thus accept membership cards that were signed by that AS. If the SS accepts multiple ASs, then it will give each a distinct number, and leases will be labled with an (AS#,Account#) pair. If there is only one AS, then leases will be labeled with just the Account#. Each client node is given the FURL of their personal Account object. The Account will accept a DSA public key and return a signed membership card that authorizes the corresponding private key to consume storage on behalf of the account. The client will create its own DSA keypair the first time it connects to the Account, and will then use the resulting membership card for all subsequent storage operations. == Storage Server Goals == The Storage Server cares about two things: 1: maintaining an accurate refcount on each bucket, so it can delete the bucket when the refcount goes to zero 2: being able to answer questions about aggregate usage per account The SS conceptually maintains a big matrix of lease information: one column per account, one row per storage index. The cells contain a boolean (has-lease or no-lease). If the grid uses per-lease timers, then each has-lease cell also contains a lease timer. This matrix may be stored in a variety of ways: entries in each share file, or items in a SQL database, according to the desired tradeoff between complexity, robustness, read speed, and write speed. Each client (by virtue of their knowledge of an authorized private key) gets to manipulate their column of this matrix in any way they like: add lease, renew lease, delete lease. (TODO: for reconcilliation purposes, the should also be able to enumerate leases). == Storage Operations == Side-effect-causing storage operations come in three forms: 1: allocate bucket / add lease to existing bucket arguments: storage_index=, storage_server=, ueb_hash=, account= 2: renew lease arguments: storage_index=, storage_server=, account= 3: cancel lease arguments: storage_index=, storage_server=, account= (where lease renewal is only relevant for grids which use per-lease timers). Clients do add-lease when they upload a file, and cancel-lease when they remove their last reference to it. Storage Servers publish a "public storage port" through the introducer, which does not actually enable storage operations, but is instead used in a rights-amplification pattern to grant authorized parties access to a "personal storage server facet". This personal facet is the one that implements allocate_bucket. All clients get access to the same public storage port, which means that we can improve the introduction mechanism later (to use a gossip-based protocol) without affecting the authority-granting protocols. The public storage port accepts signed messages asking for storage authority. It responds by creating a personal facet and making it available to the requester. The account number is curried into the facet, so that all lease-creating operations will record this account number into the lease. By restricting the nature of the personal facets that a client can access, we restrict them to using their designated account number. ======================================== There are two kinds of signed messages: use (other names: connection, FURLification, activation, reification, grounding, specific-making, ?), and delegation. The FURLification message results in a FURL that points to an object which can actually accept RIStorageServer methods. The delegation message results in a new signed message. The furlification message looks like: (pubkey, signed(serialized({limitations}, beneficiary_furl))) The delegation message looks like: (pubkey, signed(serialized({limitations}, delegate_pubkey))) The limitations dict indicates what the resulting connection or delegation can be used for. All limitations for the cert chain are applied, and the result must be restricted to their overall minimum. The following limitation keys are defined: 'account': a number. All resulting leases must be tagged with this account number. A chain with multiple distinct 'account' limitations is an error (the result will not permit leases) 'SI': a storage index (binary string). Leases may only be created for this specific storage index, no other. 'serverid': a peerid (binary string). Leases may only be created on the storage server identified by this serverid. 'UEB_hash': (binary string): Leases may only be created for shares which contain a matching UEB_hash. Note: this limitation is a nuisance to implement correctly: it requires that the storage server parse the share and verify all hashes. 'before': a timestamp (seconds since epoch). All leases must be made before this time. In addition, all liverefs and FURLs must expire and cease working at this time. 'server_size': a number, measuring share size (in bytes). A storage server which sees this message should keep track of how much storage space has been consumed using this liveref/FURL, and throw an exception when receiving a lease request that would bring this total above 'server_size'. Note: this limitation is a nuisance to implement (it works best if 'before' is used and provides a short lifetime). Actually, let's merge the two, and put the type in the limitations dict. 'furl_to' and 'delegate_key' are mutually exclusive. 'furl_to': (string): Used only on furlification messages. This requests the recipient to create an object which implements the given access, then send a FURL which references this object to an RIFURLReceiver.furl() call at the given 'furl_to' FURL: facet = create_storage_facet(limitations) facet_furl = tub.registerReference(facet) d = tub.getReference(limitations['furl_to']) d.addCallback(lambda rref: rref.furl(facet_furl)) The facet_furl should be persistent, so to reduce storage space, facet_furl should contain an HMAC'ed list of all limitations, and create_storage_facet() should be deferred until the client actually tries to use the furl. This leads to 150-200 byte base32 swissnums. 'delegate_key': (binary string, a DSA pubkey). Used only on delegation messages. This requests all observers to accept messages signed by the given public key and to apply the associated limitations. I also want to keep the message size small, so I'm going to define a custom netstring-based encoding format for it (JSON expands binary data by about 3.5x). Each dict entry will be encoded as netstring(key)+netstring(value). The container is responsible for providing the size of this serialized structure. The actual message will then look like: def make_message(privkey, limitations): message_to_sign = "".join([ netstring(k) + netstring(v) for k,v in limitations ]) signature = privkey.sign(message_to_sign) pubkey = privkey.get_public_key() msg = netstring(message_to_sign) + netstring(signature) + netstring(pubkey) return msg The deserialization code MUST throw an exception if the same limitations key appears twice, to ensure that everybody interprets the dict the same way. These messages are passed over foolscap connections as a single string. They are also saved to disk in this format. Code should only store them in a deserialized form if the signature has been verified, the cert chain verified, and the limitations accumulated. The membership card is just the following: membership_card = make_message(account_server_privkey, {'account': account_number, 'before': time.time() + 1*MONTH, 'delegate_key': client_pubkey}) This card is provided on demand by the given user's Account facet, for whatever pubkey they submit. When a client learns about a new storage server, they create a new receiver object (and stash the peerid in it), and submit the following message to the RIStorageServerWelcome.get_personal_facet() method: mymsg = make_message(client_privkey, {'furl_to': receiver_furl}) send(membership_card, mymsg) (note that the receiver_furl will probably not have a routeable address, but this won't matter because the client is already attached, so foolscap can use the existing connection.) The server will validate the cert chain (see below) and wind up with a complete list of limitations that are to be applied to the facet it will provide to the caller. This list must combine limitations from the entire chain: in particular it must enforce the account= limitation from the membership card. The server will then serialize this limitation dict into a string, compute a fixed-size HMAC code using a server-private secret, then base32 encode the (hmac+limitstring) value (and prepend a "0-" version indicator). The resulting string is used as the swissnum portion of the FURL that is sent to the furl_to target. Later, when the client tries to dereference this FURL, a Tub.registerNameLookupHandler hook will notice the attempt, claim the "0-" namespace, base32decode the string, check the HMAC, decode the limitation dict, then create and return an RIStorageServer facet with these limitations. The client should cache the (peerid, FURL) mapping in persistent storage. Later, when it learns about this storage server again, it will use the cached FURL instead of signing another message. If the getReference or the storage operation fails with StorageAuthorityExpiredError, the cache entry should be removed and the client should sign a new message to obtain a new one. (security note: an evil storage server can take 'mymsg' and present it to someone else, but other servers will only send the resulting authority to the client's receiver_furl, so the evil server cannot benefit from this. The receiver object has the serverid curried into it, so the evil server can only affect the client's mapping for this one serverid, not anything else, so the server cannot hurt the client in any way other than denying service to itself. It might be a good idea to include serverid= in the message, but it isn't clear that it really helps anything). When the client wants to use a Helper, it needs to delegate some amount of storage authority to the helper. The first phase has the client send the storage index to the helper, so it can query servers and decide whether the file needs to be uploaded or not. If it decides yes, the Helper creates a new Uploader object and a receiver object, and sends the Uploader liveref and the receiver FURL to the client. The client then creates a message for the helper to use: helper_msg = make_message(client_privkey, {'furl_to': helper_rx_furl, 'SI': storage_index, 'before': time.time() + 1*DAY, #? 'server_size': filesize/k+overhead, }) The client then sends (membership_card, helper_msg) to the helper. The Helper sends (membership_card, helper_msg) to each storage server that it needs to use for the upload. This gives the Helper access to a limited facet on each storage server. This facet gives the helper the authority to upload data for a specific storage index, for a limited time, using leases that are tagged by the user's account number. The helper cannot use the client's storage authority for any other file. The size limit prevents the helper from storing some other (larger) file of its own using this authority. The time restriction allows the storage servers to expire their 'server_size' table entry quickly, and prevents the helper from hanging on to the storage authority indefinitely. The Helper only gets one furl_to target, which must be used for multiple SS peerids. The helper's receiver must parse the FURL that gets returned to determine which server is which. [problems: an evil server could deliver a bogus FURL which points to a different server. The Helper might reject the real server's good FURL as a duplicate. This allows an evil server to block access to a good server. Queries could be sent sequentially, which would partially mitigate this problem (an evil server could send multiple requests). Better: if the cert-chain send message could include a nonce, which is supposed to be returned with the FURL, then the helper could use this to correlate sends and receives.] === repair caps === There are three basic approaches to provide a Repairer with the storage authority that it needs. The first is to give the Repairer complete authority: allow it to place leases for whatever account number it wishes. This is simple and requires the least overhead, but of course it give the Repairer the ability to abuse everyone's quota. The second is to give the Repairer no user authority: instead, give the repairer its own account, and build it to keep track of which leases it is holding on behalf of one of its customers. This repairer will slowly accumulate quota space over time, as it creates new shares to replace ones that have decayed. Eventually, when the client comes back online, the client should establish its own leases on these new shares and allow the repairer to cancel its temporary ones. The third approach is in between the other two: give the repairer some limited authority over the customer's account, but not enough to let it consume the user's whole quota. To create the storage-authority portion of a (one-month) repair-cap, the client creates a new DSA keypair (repair_privkey, repair_pubkey), and then creates a signed message and bundles it into the repaircap: repair_msg = make_message(client_privkey, {'delegate_key': repair_pubkey, 'SI': storage_index, 'UEB_hash': file_ueb_hash}) repair_cap = (verify_cap, repair_privkey, (membership_card, repair_msg)) This gives the holder of the repair cap a time-limited authority to upload shares for the given storage index which contain the given data. This prohibits the repair-cap from being used to upload or repair any other file. When the repairer needs to upload a new share, it will use the delegated key to create its own signed message: upload_msg = make_message(repair_privkey, {'furl_to': repairer_rx_furl}) send(membership_card, repair_msg, upload_msg) The biggest problem with the low-authority approaches is the expiration time of the membership card, which limits the duration for which the repair-cap authority is valid. It would be nice if repair-caps could last a long time, years perhaps, so that clients can be offline for a similar period of time. However to retain a reasonable revocation interval for users, the membership card's before= timeout needs to be closer to a month. [it might be reasonable to use some sort of rights-amplification: the repairer has a special cert which allows it to remove the before= value from a chain]. === chain verification === The server will create a chain that starts with the AS's certificate: an unsigned message which derives its authority from being manually placed in the SS's configdir. The only limitation in the AS certificate will be on some kind of meta-account, in case we want to use multiple account servers and allow their account numbers to live in distinct number spaces (think sub-accounts or business partners to buy storage in bulk and resell it to users). The rest of the chain comes directly from what the client sent. The server walks the chain, keeping an accumulated limitations dictionary along the way. At each step it knows the pubkey that was delegated by the previous step. == client config == Clients are configured with an Account FURL that points to a private facet on the Account Server. The client generates a private key at startup. It sends the pubkey to the AS facet, which will return a signed delegate_key message (the "membership card") that grants the client's privkey any storage authority it wishes (as long as the account number is set to a specific value). The client stores this membership card in private/membership.cert . RIStorageServer messages will accept an optional account= argument. If left unspecified, the value is taken from the limitations that were curried into the SS facet. In all cases, the value used must meet those limitations. The value must not be None: Helpers/Repairers or other super-powered storage clients are obligated to specify an account number. == server config == Storage servers are configured with an unsigned root authority message. This is like the output of make_message(account_server_privkey, {}) but has empty 'signature' and 'pubkey' strings. This root goes into NODEDIR/storage_authority_root.cert . It is prepended to all chains that arrive. [if/when we accept multiple authorities, storage_authority_root.cert will turn into a storage_authority_root/ directory with *.cert files, and each arriving chain will cause a search through these root certs for a matching pubkey. The empty limitations will be replaced by {domain=X}, which is used as a sort of meta-account.. the details depend upon whether we express account numbers as an int (with various ranges) or as a tuple] The root authority message is published by the Account Server through its web interface, and also into a local file: NODEDIR/storage_authority_root.cert . The admin of the storage server is responsible for copying this file into place, thus enabling clients to use storage services. ---------------------------------------- -- Text beyond this point is out-of-date, and exists purely for background -- Each storage server offers a "public storage port", which only accepts signed messages. The Introducer mechanism exists to give clients a reference to a set of these public storage ports. All clients get access to the same ports. If clients did all their work themselves, these public storage ports would be enough, and no further code would be necessary (all storage requests would we signed the same way). Fundamentally, each storage request must be signed by the account's private key, giving the SS an authenticated Account Number to go with the request. This is used to index the correct cell in the lease matrix. The holder of the account privkey is allowed to manipulate their column of the matrix in any way they like: add leases, renew leases, delete leases. (TODO: for reconcilliation purposes, they should also be able to enumerate leases). The storage request is sent in the form of a signed request message, accompanied by the membership card. For example: req = SIGN("allocate SI=123 SSID=abc", accountprivkey) , membership_card -> RemoteBucketWriter reference Upon receipt of this request, the storage server will return a reference to a RemoteBucketWriter object, which the client can use to fill and close the bucket. The SS must perform two DSA signature verifications before accepting this request. The first is to validate the membership card: the Account Server's pubkey is used to verify the membership card's signature, from which an account pubkey and account# is extracted. The second is to validate the request: the account pubkey is used to verify the request signature. If both are valid, the full request (with account# and storage index) is delivered to the internal StorageServer object. Note that the signed request message includes the Storage Server's node ID, to prevent this storage server from taking the signed message and echoing to other storage servers. Each SS will ignore any request that is not addressed to the right SSID. Also note that the SI= and SSID= fields may contain wildcards, if the signing client so chooses. == Caching Signature Verification == We add some complexity to this simple model to achieve two goals: to enable fine-grained delegation of storage capabilities (specifically for renewers and repairers), and to reduce the number of public-key crypto operations that must be performed. The first enhancement is to allow the SS to cache the results of the verification step. To do this, the client creates a signed message which asks the SS to return a FURL of an object which can be used to execute further operations *without* a DSA signature. The FURL is expected to contain a MAC'ed string that contains the account# and the argument restrictions, effectively currying a subset of arguments into the RemoteReference. Clients which do all their operations themselves would use this to obtain a private storage port for each public storage port, stashing the FURLs in a local table, and then later storage operations would be done to those FURLs instead of creating signed requests. For example: req = SIGN("FURL(allocate SI=* SSID=abc)", accountprivkey), membership_card -> FURL Tub.getReference(FURL).allocate(SI=123) -> RemoteBucketWriter reference == Renewers and Repairers A brief digression is in order, to motivate the other enhancement. The "manifest" is a list of caps, one for each node that is reachable from the user's root directory/directories. The client is expected to generate the manifest on a periodic basis (perhaps once a day), and to keep track of which files/dirnodes have been added and removed. Items which have been removed must be explicitly dereferenced to reclaim their storage space. For grids which use per-file lease timers, the manifest is used to drive the Renewer: a process which renews the lease timers on a periodic basis (perhaps once a week). The manifest can also be used to drive a Checker, which in turn feeds work into the Repairer. The manifest should contain the minimum necessary authority to do its job, which generally means it contains the "verify cap" for each node. For immutable files, the verify cap contains the storage index and the UEB hash: enough information to retrieve and validate the ciphertext but not enough to decrypt it. For mutable files, the verify cap contains the storage index and the pubkey hash, which also serves to retrieve and validate ciphertext but not decrypt it. If the client does its own Renewing and Repairing, then a verifycap-based manifest is sufficient. However, if the user wants to be able to turn their computer off for a few months and still keep their files around, they need to delegate this job off to some other willing node. In a commercial network, there will be centralized (and perhaps trusted) Renewer/Repairer nodes, but in a friendnet these may not be available, and the user will depend upon one of their friends being willing to run this service for them while they are away. In either of these cases, the verifycaps are not enough: the Renewer will need additional authority to renew the client's leases, and the Repairer will need the authority to create new shares (in the client's name) when necessary. A trusted central service could be given all-account superpowers, allowing it to exercise storage authority on behalf of all users as it pleases. If this is the case, the verifycaps are sufficient. But if we desire to grant less authority to the Renewer/Repairer, then we need a mechanism to attenuate this authority. The usual objcap approach is to create a proxy: an intermediate object which itself is given full authority, but which is unwilling to exercise more than a portion of that authority in response to incoming requests. The not-fully-trusted service is then only given access to the proxy, not the final authority. For example: class Proxy(RemoteReference): def __init__(self, original, storage_index): self.original = original self.storage_index = storage_index def remote_renew_leases(self): return self.original.renew_leases(self.storage_index) renewer.grant(Proxy(target, "abcd")) But this approach interposes the proxy in the calling chain, requiring the machine which hosts the proxy to be available and on-line at all times, which runs opposite to our use case (turning the client off for a month). == Creating Attenuated Authorities == The other enhancement is to use more public-key operations to allow the delegation of reduced authority to external helper services. Specifically, we want to give then Renewer the ability to renew leases for a specific file, rather than giving it lease-renewal power for all files. Likewise, the Repairer should have the ability to create new shares, but only for the file that is being repaired, not for unrelated files. If we do not mind giving the storage servers the ability to replay their inbound message to other storage servers, then the client can simply generate a signed message with a wildcard SSID= argument and leave it in the care of the Renewer or Repairer. For example, the Renewer would get: SIGN("renew-lease SI=123 SSID=*", accountprivkey), membership_card Then, when the Renewer needed to renew a lease, it would deliver this signed request message to the storage server. The SS would verify the signatures just as if the message came from the original client, find them good, and perform the desired operation. With this approach, the manifest that is delivered to the remote Renewer process needs to include a signed lease-renewal request for each file: we use the term "renew-cap" for this combined (verifycap + signed lease-renewal request) message. Likewise the "repair-cap" would be the verifycap plus a signed allocate-bucket message. A renew-cap manifest would be enough for a remote Renewer to do its job, a repair-cap manifest would provide a remote Repairer with enough authority, and a cancel-cap manifest would be used for a remote Canceller (used, e.g., to make sure that file has been dereferenced even if the client does not stick around long enough to track down and inform all of the storage servers involved). The only concern is that the SS could also take this exact same renew-lease message and deliver it to other storage servers. This wouldn't cause a concern for mere lease renewal, but the allocate-share message might be a bit less comfortable (you might not want to grant the first storage server the ability to claim space in your name on all other storage servers). Ideally we'd like to send a different message to each storage server, each narrowed in scope to a single SSID, since then none of these messages would be useful on any other SS. If the client knew the identities of all the storage servers in the system ahead of time, it might create a whole slew of signed messages, but a) this is a lot of signatures, only a fraction of which will ever actually be used, and b) new servers might be introduced after the manifest is created, particularly if we're talking about repair-caps instead of renewal-caps. The Renewer can't generate these one-per-SSID messages from the SSID=* message, because it doesn't have a privkey to make the correct signatures. So without some other mechanism, we're stuck with these relatively coarse authorities. If we want to limit this sort of authority, then we need to introduce a new method. The client begins by generating a new DSA keypair. Then it signs a message that declares the new pubkey to be valid for a specific subset of storage operations (such as "renew-lease SI=123 SSID=*"). Then it delivers the new privkey, the declaration message, and the membership card to the Renewer. The renewer uses the new privkey to sign its own one-per-SSID request message for each server, then sends the (signed request, declaration, membership card) triple to the server. The server needs to perform three verification checks per message: first the membership card, then the declaration message, then the actual request message. == Other Enhancements == If a given authority is likely to be used multiple times, the same give-me-a-FURL trick can be used to cut down on the number of public key operations that must be performed. This is trickier with the per-SI messages. When storing the manifest, things like the membership card should be amortized across a set of common entries. An isolated renew-cap needs to contain the verifycap, the signed renewal request, and the membership card. But a manifest with a thousand entries should only include one copy of the membership card. It might be sensible to define a signed renewal request that grants authority for a set of storage indicies, so that the signature can be shared among several entries (to save space and perhaps processing time). The request could include a Bloom filter of authorized SI values: when the request is actually sent to the server, the renewer would add a list of actual SI values to renew, and the server would accept all that are contained in the filter. == Revocation == The lifetime of the storage authority included in the manifest's renew-caps or repair-caps will determine the lifetime of those caps. In particular, if we implement account revocation by using time-limited membership cards (requiring the client to get a new card once a month), then the repair-caps won't work for more than a month, which kind of defeats the purpose. A related issue is the FURL-shortcut: the MAC'ed message needs to include a validity period of some sort, and if the client tries to use a old FURL they should get an error message that will prompt them to try and acquire a newer one. ------------------------------ The client can produce a repair-cap manifest for a specific Repairer's pubkey, so it can produce a signed message that includes the pubkey (instead of needing to generate a new privkey just for this purpose). The result is not a capability, since it can only be used by the holder of the corresponding privkey. So the generic form of the storage operation message is the request (which has all the argument values filled in), followed by a chain of authorizations. The first authorization must be signed by the Account Server's key. Each authorization must be signed by the key mentioned in the previous one. Each one adds a new limitation on the power of the following ones. The actual request is bounded by all the limitations of the chain. The membership card is an authorization that simply limits the account number that can be used: "op=* SI=* SSID=* account=4 signed-by=CLIENT-PUBKEY". So a repair manifest created for a Repairer with pubkey ABCD could consist of a list of verifycaps plus a single authorization (using a Bloom filter to identify the SIs that were allowed): SIGN("allocate SI=[bloom] SSID=* signed-by=ABCD") If/when the Repairer needed to allocate a share, it would use its own privkey to sign an additional message and send the whole list to the SS: request=allocate SI=1234 SSID=EEFS account=4 shnum=2 SIGN("allocate SI=1234 SSID=EEFS", ABCD) SIGN("allocate SI=[bloom] SSID=* signed-by=ABCD", clientkey) membership: SIGN("op=* SI=* SSID=* account=4 signed-by=clientkey", ASkey) [implicit]: ASkey ---------------------------------------- Things would be a lot simpler if the Repairer (actually the Re-Leaser) had everybody's account authority. One simplifying approach: the Repairer/Re-Leaser has its own account, and the shares it creates are leased under that account number. The R/R keeps track of which leases it has created for whom. When the client eventually comes back online, it is told to perform a re-leasing run, and after that occurs the R/R can cancel its own temporary leases. This would effectively transfer storage quota from the original client to the R/R over time (as shares are regenerated by the R/R while the client remains offline). If the R/R is centrally managed, the quota mechanism can sum the R/R's numbers with the SS's numbers when determining how much storage is consumed by any given account. Not quite as clean as storing the exact information in the SS's lease tables directly, but: * the R/R no longer needs any special account authority (it merely needs an accurate account number, which can be supplied by giving the client a specific facet that is bound to that account number) * the verify-cap manifest is sufficient to perform repair * no extra DSA keys are necessary * account authority could be implemented with either DSA keys or personal SS facets: i.e. we don't need the delegability aspects of DSA keys for use by the repair mechanism (we might still want them to simplify introduction). I *think* this would eliminate all that complexity of chained authorization messages.