|
| 1 | +use aztec::{ |
| 2 | + protocol::{ |
| 3 | + address::{AztecAddress, PartialAddress}, |
| 4 | + constants::DOM_SEP__INTERACTIVE_HANDSHAKE_SIGNATURE, |
| 5 | + hash::poseidon2_hash_with_separator, |
| 6 | + public_keys::{hash_public_key, PublicKeys}, |
| 7 | + traits::{Deserialize, ToField}, |
| 8 | + }, |
| 9 | + utils::point::point_from_x_coord_and_sign, |
| 10 | +}; |
| 11 | +use std::embedded_curve_ops::EmbeddedCurveScalar; |
| 12 | + |
| 13 | +/// The recipient's signed authorization for an interactive handshake. |
| 14 | +/// |
| 15 | +/// Returned by the interactive-handshake |
| 16 | +/// [`resolve_custom_request`](aztec::oracle::resolve_custom_request::resolve_custom_request) oracle call. It is how |
| 17 | +/// the sender asserts that the recipient received the ephemeral key correctly and will be able to receive messages |
| 18 | +/// properly, and it carries everything needed to verify that in-circuit. |
| 19 | +#[derive(Deserialize)] |
| 20 | +pub struct RecipientSignature { |
| 21 | + /// The recipient's master public keys. With `partial_address` they derive the recipient's address, which is how |
| 22 | + /// [`verify`][Self::verify] binds them to `recipient`. |
| 23 | + pub public_keys: PublicKeys, |
| 24 | + /// The recipient's partial address, combined with `public_keys` to reproduce the recipient's address. |
| 25 | + pub partial_address: PartialAddress, |
| 26 | + /// The x-coordinate of the recipient's master message-signing public key. `PublicKeys` exposes this key only as |
| 27 | + /// its `mspk_m_hash`, so the point travels separately and is reconstructed on-curve, then tied back to that hash. |
| 28 | + pub mspk_x: Field, |
| 29 | + /// Whether that key's y-coordinate is positive, used to pick the correct y when reconstructing the point from |
| 30 | + /// `mspk_x`. |
| 31 | + pub mspk_y_is_positive: bool, |
| 32 | + /// The schnorr signature over the handshake message: its response scalar `s` and challenge `e`. |
| 33 | + pub signature: (EmbeddedCurveScalar, EmbeddedCurveScalar), |
| 34 | +} |
| 35 | + |
| 36 | +impl RecipientSignature { |
| 37 | + /// Verifies that `recipient` authorized a handshake using ephemeral key `eph_pk.x` on this chain. |
| 38 | + /// |
| 39 | + /// # Panics |
| 40 | + /// If the returned public keys do not derive `recipient`'s address, `mspk_x` is not on the curve, the signing |
| 41 | + /// key does not match the address-committed `mspk_m_hash`, or the signature does not verify. Note that an |
| 42 | + /// off-curve `ivpk_m` fails inside the embedded-curve addition in [`AztecAddress::compute`] rather than with |
| 43 | + /// this method's error messages. |
| 44 | + pub fn verify( |
| 45 | + self, |
| 46 | + recipient: AztecAddress, |
| 47 | + chain_id: Field, |
| 48 | + version: Field, |
| 49 | + registry: AztecAddress, |
| 50 | + eph_pk_x: Field, |
| 51 | + ) { |
| 52 | + // Bind the returned keys to the recipient's address. |
| 53 | + assert_eq( |
| 54 | + recipient, |
| 55 | + AztecAddress::compute(self.public_keys, self.partial_address), |
| 56 | + "handshake public keys do not match recipient", |
| 57 | + ); |
| 58 | + |
| 59 | + // Reconstruct the signing key on-curve and tie it to the authenticated hash. |
| 60 | + let mspk = point_from_x_coord_and_sign(self.mspk_x, self.mspk_y_is_positive).expect( |
| 61 | + f"mspk_x is not a valid curve x-coordinate", |
| 62 | + ); |
| 63 | + assert( |
| 64 | + hash_public_key(mspk) == self.public_keys.mspk_m_hash, |
| 65 | + "handshake signature key does not match recipient", |
| 66 | + ); |
| 67 | + |
| 68 | + // At this point `mspk` is proven to be the recipient's message-signing key, so a valid signature over the |
| 69 | + // recomputed message can only come from the recipient. The message commits to the ephemeral key and chain |
| 70 | + // context, never to the sender, so the recipient authorizes without learning who initiated it. |
| 71 | + let message = poseidon2_hash_with_separator( |
| 72 | + [chain_id, version, registry.to_field(), eph_pk_x], |
| 73 | + DOM_SEP__INTERACTIVE_HANDSHAKE_SIGNATURE, |
| 74 | + ); |
| 75 | + assert(schnorr::verify_signature(mspk, self.signature, message), "invalid recipient handshake signature"); |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +mod test { |
| 80 | + use super::RecipientSignature; |
| 81 | + use aztec::protocol::{ |
| 82 | + address::{AztecAddress, PartialAddress}, |
| 83 | + point::EmbeddedCurvePoint, |
| 84 | + public_keys::{IvpkM, PublicKeys}, |
| 85 | + traits::{FromField, ToField}, |
| 86 | + }; |
| 87 | + use std::embedded_curve_ops::EmbeddedCurveScalar; |
| 88 | + |
| 89 | + #[test] |
| 90 | + unconstrained fn interactive_signature_verifies_valid_fixture() { |
| 91 | + let (response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture(); |
| 92 | + response.verify(recipient, chain_id, version, registry, eph_pk_x); |
| 93 | + } |
| 94 | + |
| 95 | + #[test(should_fail_with = "handshake public keys do not match recipient")] |
| 96 | + unconstrained fn interactive_signature_rejects_wrong_recipient() { |
| 97 | + let (response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture(); |
| 98 | + let wrong_recipient = AztecAddress::from_field(recipient.to_field() + 1); |
| 99 | + response.verify(wrong_recipient, chain_id, version, registry, eph_pk_x); |
| 100 | + } |
| 101 | + |
| 102 | + #[test(should_fail_with = "handshake signature key does not match recipient")] |
| 103 | + unconstrained fn interactive_signature_rejects_wrong_signing_key() { |
| 104 | + let (mut response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture(); |
| 105 | + // x = 1 is the generator's x-coordinate: a valid curve point, but not the recipient's signing key. |
| 106 | + response.mspk_x = 1; |
| 107 | + response.verify(recipient, chain_id, version, registry, eph_pk_x); |
| 108 | + } |
| 109 | + |
| 110 | + #[test(should_fail_with = "mspk_x is not a valid curve x-coordinate")] |
| 111 | + unconstrained fn interactive_signature_rejects_off_curve_signing_key() { |
| 112 | + let (mut response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture(); |
| 113 | + // x = 3 is a non-residue for the curve, so no point has this x-coordinate. |
| 114 | + response.mspk_x = 3; |
| 115 | + response.verify(recipient, chain_id, version, registry, eph_pk_x); |
| 116 | + } |
| 117 | + |
| 118 | + #[test(should_fail_with = "invalid recipient handshake signature")] |
| 119 | + unconstrained fn interactive_signature_rejects_tampered_signature() { |
| 120 | + let (mut response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture(); |
| 121 | + response.signature.0.lo += 1; |
| 122 | + response.verify(recipient, chain_id, version, registry, eph_pk_x); |
| 123 | + } |
| 124 | + |
| 125 | + #[test(should_fail_with = "invalid recipient handshake signature")] |
| 126 | + unconstrained fn interactive_signature_rejects_wrong_eph_pk() { |
| 127 | + let (response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture(); |
| 128 | + response.verify(recipient, chain_id, version, registry, eph_pk_x + 1); |
| 129 | + } |
| 130 | + |
| 131 | + #[test(should_fail_with = "invalid recipient handshake signature")] |
| 132 | + unconstrained fn interactive_signature_rejects_wrong_registry() { |
| 133 | + let (response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture(); |
| 134 | + let wrong_registry = AztecAddress::from_field(registry.to_field() + 1); |
| 135 | + response.verify(recipient, chain_id, version, wrong_registry, eph_pk_x); |
| 136 | + } |
| 137 | + |
| 138 | + #[test(should_fail_with = "invalid recipient handshake signature")] |
| 139 | + unconstrained fn interactive_signature_rejects_wrong_chain_context() { |
| 140 | + let (response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture(); |
| 141 | + response.verify(recipient, chain_id + 1, version, registry, eph_pk_x); |
| 142 | + } |
| 143 | + |
| 144 | + // A real interactive-handshake response fixture generated offline: recipient keys, partial address, |
| 145 | + // message-signing key, and a genuine schnorr signature over `[chain_id, version, registry, eph_pk_x]`. |
| 146 | + unconstrained fn interactive_handshake_fixture() -> (RecipientSignature, AztecAddress, Field, Field, AztecAddress, Field) { |
| 147 | + let public_keys = PublicKeys { |
| 148 | + npk_m_hash: 0x1b6146272d5dcb12676fa83f74475545fee7e36b4cb93e0e1ae4b2098f442de2, |
| 149 | + ivpk_m: IvpkM { |
| 150 | + inner: EmbeddedCurvePoint { |
| 151 | + x: 0x03e89026e063059e6069e82a81a3e16d28f94a900ed08dadbd8407bf472d16c4, |
| 152 | + y: 0x0c8a8b9ab39ea7563eb22a85905408a061e09bde12ff788a84e772b3e6af2130, |
| 153 | + }, |
| 154 | + }, |
| 155 | + ovpk_m_hash: 0x279e44ade5739fdbdb91ea20489a20183fd6f38ddefafff8748bcfd8f72ad195, |
| 156 | + tpk_m_hash: 0x124b66b8152df6debefebd7561f43ad0179f908b3b7b573963efb1685c80b0fc, |
| 157 | + mspk_m_hash: 0x0e5480b4d52b9630b06e8d0e2ca8932457099274d522bb8ea6a60c1d7da871bf, |
| 158 | + fbpk_m_hash: 0x26dbd06da054ead46dcf2232ac07be3ab5839d8287ac0f4ed69f62eb7ffaa2d0, |
| 159 | + }; |
| 160 | + let response = RecipientSignature { |
| 161 | + public_keys, |
| 162 | + partial_address: PartialAddress::from_field(0x0fedcba987654321), |
| 163 | + mspk_x: 0x22d507026547e1bd19b507a234a689833ab1604c00ff093574c44aea7176cfd5, |
| 164 | + mspk_y_is_positive: true, |
| 165 | + signature: ( |
| 166 | + EmbeddedCurveScalar { lo: 0xd88c1b23cafce3c85432b3393c8f1975, hi: 0x04f3972857d4b92ef44c2ea1d15e6abb }, |
| 167 | + EmbeddedCurveScalar { lo: 0x01b6f5567970c74d567270c239f235ab, hi: 0x0db0cf5e2cfba9c07df49a357b6dcb2c }, |
| 168 | + ), |
| 169 | + }; |
| 170 | + let recipient = AztecAddress::from_field( |
| 171 | + 0x1b94108a8172e103629879215c11f224522aa1bd0e4d50505c041c500f82d15b, |
| 172 | + ); |
| 173 | + let chain_id = 0x7a69; |
| 174 | + let version = 1; |
| 175 | + let registry = AztecAddress::from_field(0x0abcabcabcabcabc); |
| 176 | + let eph_pk_x = 0x2468ace2468ace; |
| 177 | + (response, recipient, chain_id, version, registry, eph_pk_x) |
| 178 | + } |
| 179 | +} |
0 commit comments