-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for handling billing process for the service offered by the operator #18
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Operator key can't be used inside enclave. Think, how will it be configured? Using startup script in docker image. And that startup script is included as part of calculating PCRs, implies static contents of enclaves are public, implies operator key is public, which is obviously not possible. Need to think of another way. Also comment if my understanding is wrong. cc: @roshanr95
We'll need the operator's private key in some way to be able to send signed billing transactions using their wallet. One way can be to ask them to store their private key in their system and allow the docker image or the backend to fetch it from there. Not sure if this will be very helpful and can increase the complexity. @roshanr95 Let us discuss this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, except the usage of operator wallet key inside of enclave. So, finalise upon that.
1ec904d
to
838b64f
Compare
Sure, @roshanr95 is looking into it. |
@Ayush-Yadav Let's pull it outside I guess. There's no easy way to feed the enclave with the operator key and whatever we end up doing is going to be useless once we pull it outside, might as well do it now itself. |
Also, |
196716e
to
5b1cc2d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rest looks good to me.
b9da522
to
7ab6664
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the design idea behind separating these two calls?
There are some additional checks that the user/client need to run on the bill data to ensure that claiming the service fees is actually worth it, given there will be some contract method call cost and balance transfer costs. The client will first fetch the current bill from the enclave, run those checks externally on them and will prepare a list of tx hashes that are worth claiming. They will then hit the 'export' endpoint with those tx hashes and get the final data and the signature required to claim those costs. The client will be then responsible for keeping the data and interacting with the billing contract to claim the bills. |
src/billing_handler.rs
Outdated
let signature = hex::encode(rs.to_bytes().append(27 + v.to_byte()).as_slice()); | ||
let signed_data = hex::encode(signed_data.as_slice()); | ||
|
||
HttpResponse::Ok().json(json!({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it safe to assume that nonce is supposed to be unique? If yes, then can we save this data with given nonce as key. So that if in a case, client loses the bill, it can query bill again by providing the nonce. And maybe delete the cache after some timeout.
To avoid the case, where client got disconnected from enclave.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes nonce is supposed to be unique based on which the contract will catch duplication of bill data. We can store this data in cache for some time but I think once the client receives the data, they are supposed to claim the bill first thing and even if not that they are atleast supposed to store the bill data until they do. I don't know if we should populate the enclave with more data and affect their performance or capacity just to handle this. @roshanr95 Please comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the contract will deduplicate receipts based on the nonce
Storing all, or even some, of them is probably overkill, can maybe just store the very last one that the client can then requery (/billing/latest
). Assuming proper persistence (and API usage) is the responsibility of the client, the only real loss vector that remains is network issues while the response is being transmitted, just the last one should be fine to handle this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can also protect against duplicate requests by checking the nonce and seeing if it has changed since the last one 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this required inside the enclave? Our major aim is to not allow duplicate requests to the billing contract because that's where the actual transactions happen. If client is sending a duplicate nonce to the enclave then it's their fault because then the enclave will think some balances have been settled when in fact the contract will reject it ,leading to lost bill info. If we were to check nonce inside the enclave then we'll have to check inside the list of all received yet and not just the last one which is probably an overkill.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. Nonce check should also be enforced. Also storing the last bill is sufficient to handle the network issue.
src/billing_handler.rs
Outdated
let mut hash = [0u8; 32]; | ||
hasher.finalize(&mut hash); | ||
let signature = sign_data(bill_claim_data.as_slice(), &appstate.signer).await; | ||
if signature.is_err() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If signature is failing here, then it will fail again when billing/latest
will be called. There is no change in parameters or other context data. Signature failing here seems fatal error to me. Double check where it can fail and how developers has suggested to handle those errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked into the error conditions and possibilities while signing prehash messages and saw if we're using a valid secret scalar key for signing (that I suppose we must be using) and a valid hashing algorithm that gives out a 32-byte hash (which is the case for our Keccak::v256()
hasher) then it is very rare that an error would occur. Have added the error checks just in case something weird happens (because of outdated crate version or something).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw If we look more closely, there is an aggressive exit happening when serving the requests as well after failing to sign the requests/response data. So even after getting a successful response from the worker serving the app, we're throwing an error response to the user of the app. Is this an acceptable behaviour? @roshanr95 @vg-27
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, if signing is throwing exception then it will be there for every sign call. That means this application is not usable at all in that case, in production.
Then I think, if you just return exception here, that would be sufficient. Any exception due to implementation change, would be hit during testing phase of release.
You can remove the special handling here, it looks like you are trying to recover from error, but it's not. Kinda misleading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just handling it the way it was done when the serverless requests are getting processed by the enclave. I think that is more concerning since it is part of the code already in production. So shouldn't we add an exception here as well:-
pub async fn get_workerd_response(
port: u16,
req: HttpRequest,
body: actix_web::web::Bytes,
signer: &k256::ecdsa::SigningKey,
host_header: &str,
) -> Result<HttpResponse, anyhow::Error> {
let mut hasher = Keccak::v256();
hasher.update(b"|oyster-serverless-hasher|");
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs();
hasher.update(b"|timestamp|");
hasher.update(×tamp.to_be_bytes());
hasher.update(b"|request|");
hasher.update(b"|method|");
hasher.update(req.method().to_string().as_bytes());
hasher.update(b"|pathandquery|");
hasher.update(
req.uri()
.path_and_query()
.map(|x| x.as_str())
.unwrap_or("")
.as_bytes(),
);
hasher.update(b"|host|");
hasher.update(host_header.as_bytes());
hasher.update(b"|body|");
hasher.update(&body);
let port_str = port.to_string();
let req_url = "http://127.0.0.1:".to_string() + &port_str + "/";
let client = reqwest::Client::new();
let response = req
.headers()
.into_iter()
.fold(
client.request(req.method().clone(), req_url),
|req, header| req.header(header.0.clone(), header.1.clone()),
)
.body(body)
.send()
.await?;
hasher.update(b"|response|");
let mut actix_resp = response.headers().into_iter().fold(
HttpResponse::build(response.status()),
|mut resp, header| {
resp.append_header((header.0.clone(), header.1.clone()));
resp
},
);
let response_body = response.bytes().await?;
hasher.update(b"|body|");
hasher.update(&response_body);
let mut hash = [0u8; 32];
hasher.finalize(&mut hash);
let (rs, v) = signer.sign_prehash_recoverable(&hash)?;`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here in this case ?
is used to pass on exception. No explicit handling required. Even for your case, I was suggesting just respond back with InternalError. And that would be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If signature is failing here, then it will fail again when
billing/latest
will be called. There is no change in parameters or other context data. Signature failing here seems fatal error to me. Double check where it can fail and how developers has suggested to handle those errors.
Depends on why it can fail and if it's consistent or not. For example, pretty sure it generates a random number inside, which can probably fail inconsistently.
Storing the last response is mainly just a safeguard I guess, if we don't even want to spend time really figuring out the failure conditions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw If we look more closely, there is an aggressive exit happening when serving the requests as well after failing to sign the requests/response data. So even after getting a successful response from the worker serving the app, we're throwing an error response to the user of the app. Is this an acceptable behaviour? @roshanr95 @vg-27
Yes, responses are insecure without a signature. No point in returning a response if not signed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, pretty sure it generates a random number inside, which can probably fail inconsistently.
Yeah, that's right. Plain ECDSA signature uses random number. But here implementation seems to be using RFC6979, where k
is computed deterministically.
Anyways, if there is possibility of inconsistent error. Let's keep the backup of billing data.
2a763f5
to
d317e86
Compare
src/billing_handler.rs
Outdated
.sign_prehash_recoverable(&hash) | ||
.context("Failed to sign billing data"); | ||
if sign.is_err() { | ||
return HttpResponse::InternalServerError().body(format!("{}", sign.unwrap_err())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@roshanr95 how to avoid aggressive exit here? by adding a retry mechanism or something?
src/billing_handler.rs
Outdated
let signature = hex::encode(rs.to_bytes().append(27 + v.to_byte()).as_slice()); | ||
let signed_data = hex::encode(signed_data.as_slice()); | ||
|
||
HttpResponse::Ok().json(json!({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes nonce is supposed to be unique based on which the contract will catch duplication of bill data. We can store this data in cache for some time but I think once the client receives the data, they are supposed to claim the bill first thing and even if not that they are atleast supposed to store the bill data until they do. I don't know if we should populate the enclave with more data and affect their performance or capacity just to handle this. @roshanr95 Please comment.
src/billing_handler.rs
Outdated
let signature = hex::encode(rs.to_bytes().append(27 + v.to_byte()).as_slice()); | ||
let signed_data = hex::encode(signed_data.as_slice()); | ||
|
||
HttpResponse::Ok().json(json!({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this required inside the enclave? Our major aim is to not allow duplicate requests to the billing contract because that's where the actual transactions happen. If client is sending a duplicate nonce to the enclave then it's their fault because then the enclave will think some balances have been settled when in fact the contract will reject it ,leading to lost bill info. If we were to check nonce inside the enclave then we'll have to check inside the list of all received yet and not just the last one which is probably an overkill.
src/billing_handler.rs
Outdated
let mut hash = [0u8; 32]; | ||
hasher.finalize(&mut hash); | ||
let signature = sign_data(bill_claim_data.as_slice(), &appstate.signer).await; | ||
if signature.is_err() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked into the error conditions and possibilities while signing prehash messages and saw if we're using a valid secret scalar key for signing (that I suppose we must be using) and a valid hashing algorithm that gives out a 32-byte hash (which is the case for our Keccak::v256()
hasher) then it is very rare that an error would occur. Have added the error checks just in case something weird happens (because of outdated crate version or something).
src/billing_handler.rs
Outdated
let mut hash = [0u8; 32]; | ||
hasher.finalize(&mut hash); | ||
let signature = sign_data(bill_claim_data.as_slice(), &appstate.signer).await; | ||
if signature.is_err() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw If we look more closely, there is an aggressive exit happening when serving the requests as well after failing to sign the requests/response data. So even after getting a successful response from the worker serving the app, we're throwing an error response to the user of the app. Is this an acceptable behaviour? @roshanr95 @vg-27
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3 comments are still unresolved.
src/billing_handler.rs
Outdated
let mut hash = [0u8; 32]; | ||
hasher.finalize(&mut hash); | ||
let signature = sign_data(bill_claim_data.as_slice(), &appstate.signer).await; | ||
if signature.is_err() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, if signing is throwing exception then it will be there for every sign call. That means this application is not usable at all in that case, in production.
Then I think, if you just return exception here, that would be sufficient. Any exception due to implementation change, would be hit during testing phase of release.
You can remove the special handling here, it looks like you are trying to recover from error, but it's not. Kinda misleading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Let's get approval from Roshan too.
Add folllowing functionalities to the serverless backend :-