The below is a request for comments. I checked it for error “lightly” and am going to check more. Your input is also welcome.
So, I studied vetKeys. My proposal for vetKeys (I replaced vetKeys by plain tECDSA, by update callback to the canister) in our proxy:
First, rewrite my code in Rust to easily access the needed cryptography. (I know Rust and some Rust Web frameworks well and can easily translate Python code to Rust.) I propose to use my Python code, not @zensh’s code, as the base, because I have better idea of using a hash rather than a specific header with randomness, that complicates things a little on the side of IC canister.
Second, make usage of vetKeys optional (using Bearer
token as an alternative), because apparently vetKeys will eat much cycles.
For the proxy’s config there will be one or more principals that outcalls from are allowed.
We will consider two canisters: The “keeper” canister (that does the cryptography and to which the proxy trusts) and the calling canister that does an actual HTTPS outcalls.
The workflow:
- The keeper canister generates two nonces: long-time-nonce and short-time-nonce.
- The calling canister sends the HTTP outcall with headers
Canister: <keeper canister principal>
and Canister-Key: signature (with sign_with_ecdsa) of (keeper principal, long-time-nonce, short-time-nonce)>
and with header Nonce:
set by the calling canister to contain the two nonces.
- The proxy returns access denied if short-time-nonce is repeated compared to a previous call.
- The proxy returns access denied if keeper canister isn’t in the list of allowed callers.
- The proxy verifies the signed principal of calling canister by using
ecdsa_public_key
.
- The proxy returns access denied if the signature does not match.
- The proxy returns a signature of the nonce by a private key stored in the proxy back with the proxied content in
Signature:
header.
- We trap in the transformation function, if Signature header does not match nonce of keeper principal (using public key openly stored in the user’s code).
The above scheme is still insecure in that a hacked hardware running canisters (ours and possibly another one) can receive and steal the SK and use it to send either a modified request from our canister (with fake Canister-Key:
) or even from an another arbitrary canister (with fake Canister-Key:
and possibly fake Canister:
), that will be considered by our proxy as a genuine request. This way, for example, an amount of OpenAI tokens can be stolen.
But if Nonce:
is a unique value, then misbehavior of the canister hardware will be revealed by a trap in the transformation function by its inability to serve replies with correct nonces (while a majority other nodes served it correctly), and it can be knocked out of the system. Short-time-nonce is used to prevent the hardware to make the correct request soon after a fake one (by a stolen SK). Long-time nonce is used to prevent the hardware to store nonce data for a long time and fetch stored data to hack the system by repeating the short-time nonce.
Note that the proxy does not need to store the full history of nonces, but just for about a few minutes, because outcalls needs to be answered quickly.
P.S. Will a canister be knocked out from the system, if it repeatedly returns results of HTTPS outcalls different than the consensus? (This is at one side desirable, because it knocks out hacked canisters; and at other side, not desirable, because a site owner of the outcall can intentionally harm a canister with the given IP, giving it incorrect results.) This security vulnerability (if it exists), can be mostly solved by DFINITY allocating a big enough pool of IPv6 addresses and allowing canisters to use random addresses from it. Having this, a particular canister could still be targeted by its response times “profile”, but that becomes difficult for an attacker.