Conversation
Code Review: feat(sdk): add verification support of foreign transaction signaturesIssues Found1.
The contract's In practice, for the The Suggested fix — Validate that the raw let s = Option::<k256::Scalar>::from(k256::Scalar::from_repr(signature.s.scalar.into()))
.ok_or(VerificationError::InvalidSignature)?;
let r = reduce_scalar(r_bytes.into()); // reduction on r is fine (standard ECDSA)Severity: Medium. In this specific context (SDK for verifying MPC-produced signatures), the practical exploit surface is low — the MPC nodes produce canonical signatures. However, as a public SDK verification function, it should enforce canonical inputs. Per CONTRIBUTING.md: "Security concerns trump everything else." 2. Missing test for non-canonical The Everything else looks solid:
|
netrome
left a comment
There was a problem hiding this comment.
Nice stuff, just some nits
| pub fn verify_signature( | ||
| self, | ||
| response: &VerifyForeignTransactionResponse, | ||
| // TODO(#2232): don't use interface API types for public keys |
There was a problem hiding this comment.
Check the match statement below. A PublicKey can also be a bls public key, which means a possible error case due to invalid input from the caller.
There was a problem hiding this comment.
Now we've had quite some time pressure, so it's good enough. But Ideally I wonder if we can remove all the public re-exports for the contract interface types and have all types that are part of the SDK interface defined here instead.
Otherwise any change we do in the interface crate will also have to adhere to semver compatiblity to not break users of the SDK crate.
There was a problem hiding this comment.
Otherwise any change we do in the interface crate will also have to adhere to semver compatiblity to not break users of the SDK crate.
wasnt that one of the purposes of the interface crate (it should avoid as much as possible breaking changes)? Or we are treating it now as an internal interface crate, and we will need a more stable external one?
There was a problem hiding this comment.
Yeah I've always pictured the contract-interface to be an externally facing crate and re-exporting types from there should not be an issue.
This also becomes an interesting design question. Would it be wrong from the perspective of this crate if we got a BLS signature? Should we be aware at this point that that is not a supported signing scheme? What happens if we were to add BLS signatures for foreign tx verification down the line?
Perhaps we don't need to bikeshed this too much but when thinking about the interface and separation of concerns this also becomes a consideration imo.
There was a problem hiding this comment.
From my pov the interface crate has to be stable in terms of how types are serialized/deserialized. Any other change which are breaking semver, such as renaming, moving definitions are all currently fair game and happens frequently.
The latter changes are fine because it's all in the monorepo, but for outside dependants those change can break semver
There was a problem hiding this comment.
Hmm yeah I pictured that we'd be cautious about these types of changes in both the contract-interface crate and the SDK, and actually adhere to semver. Although we could start to version the interface/SDK separately from the rest of the MPC repo for this reason.
Until we publish a crate though everything should be considered unstable.
gilcu3
left a comment
There was a problem hiding this comment.
I believe the current implementation of check_ec_signature might accept "invalid" signatures. I think we should once and for all unify these crypto type conversions and implementations in a single place/crate, so that we don't need to implement them each time we use them, which is quite error prone. These seems seem fit for our contract-interface crate IMO, maybe behind appropriate feature gates.
If for the moment this is temporarily implemented here, then we should strictly follow the existing implementation in the contract. Let me know if that was the actual aim here.
| all(feature = "abi", not(target_arch = "wasm32")), | ||
| derive(schemars::JsonSchema) | ||
| )] | ||
| pub struct VerifyForeignTransactionResponse { |
There was a problem hiding this comment.
A little bit off topic, but is it possible to make this (and maybe other) structures borsh serializable?
For example, right now I'm doing this (I'm still writing PoC, so maybe I'm missing something out and it can be done easier):
#[near(serializers=[borsh])]
#[derive(Debug, Clone)]
pub struct MpcVerifyProofArgs {
pub proof_kind: ProofKind,
pub sign_payload: Vec<u8>,
pub mpc_response_json: String,
}
// ...
#[allow(clippy::needless_pass_by_value)]
#[handle_result]
#[result_serializer(borsh)]
pub fn verify_proof(
&self,
#[serializer(borsh)] input: Vec<u8>,
) -> Result<ProverResult, String> {
let args = MpcVerifyProofArgs::try_from_slice(&input).near_expect(ProverError::ParseArgs);
let sign_payload = ForeignTxSignPayload::try_from_slice(&args.sign_payload)
.near_expect(ProverError::ParseArgs);
let mpc_response: VerifyForeignTransactionResponse =
serde_json::from_str(&args.mpc_response_json).near_expect(ProverError::ParseArgs);
// ...
}If this structure supported borsh, I could've store and parse it directly as:
#[near(serializers=[borsh])]
#[derive(Debug, Clone)]
pub struct MpcVerifyProofArgs {
pub proof_kind: ProofKind,
pub sign_payload: Vec<u8>,
pub mpc_response: VerifyForeignTransactionResponse,
}There was a problem hiding this comment.
Yes, we can add this. But just be aware that we haven't published this crate yet for external usage (it's being discussed), so there is no guarantee of maintaining semver compatiblity.
netrome
left a comment
There was a problem hiding this comment.
I think we need to clean up the error type now when we don't do signature verification
closes #2130