diff --git a/chia/_tests/clvm/test_member_puzzles.py b/chia/_tests/clvm/test_member_puzzles.py index 6e0d4bbbf6cf..2f7fd37cb5e6 100644 --- a/chia/_tests/clvm/test_member_puzzles.py +++ b/chia/_tests/clvm/test_member_puzzles.py @@ -36,6 +36,7 @@ BLSMember, FixedPuzzleMember, PasskeyMember, + PasskeyPuzzleAssertMember, SECPK1Member, SECPK1PuzzleAssertMember, SECPR1Member, @@ -298,6 +299,82 @@ async def test_passkey_member(cost_logger: CostLogger) -> None: await sim.rewind(block_height) +@pytest.mark.anyio +async def test_passkey_puzzle_assert_member(cost_logger: CostLogger) -> None: + async with sim_and_client() as (sim, client): + delegated_puzzle = Program.to(1) + delegated_puzzle_hash = delegated_puzzle.get_tree_hash() + + # setup keys + seed = 0x1A62C9636D1C9DB2E7D564D0C11603BF456AAD25AA7B12BDFD762B4E38E7EDC6 + secp_sk = ec.derive_private_key(seed, ec.SECP256R1(), default_backend()) + secp_pk = secp_sk.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) + + passkey_member = PasskeyPuzzleAssertMember(secp_pk, sim.defaults.GENESIS_CHALLENGE) + + passkey_puzzle = PuzzleWithRestrictions(0, [], passkey_member) + + # Farm and find coin + await sim.farm_block(passkey_puzzle.puzzle_hash()) + coin = ( + await client.get_coin_records_by_puzzle_hashes([passkey_puzzle.puzzle_hash()], include_spent_coins=False) + )[0].coin + block_height = sim.block_height + + # Create an announcements to be asserted in the delegated puzzle + announcement = CreateCoinAnnouncement(msg=b"foo", coin_id=coin.name()) + + # Get signature for AGG_SIG_ME + authenticator_data = b"foo" + client_data = {"challenge": passkey_member.create_message(delegated_puzzle_hash, coin.puzzle_hash)} + client_data_hash = std_hash(PasskeyMember.format_client_data_as_str(client_data).encode("utf8")) + signature_message = authenticator_data + client_data_hash + der_sig = secp_sk.sign( + signature_message, + # The type stubs are weird here, `deterministic_signing` is assuredly an argument + ec.ECDSA(hashes.SHA256(), deterministic_signing=True), # type: ignore[call-arg] + ) + r, s = decode_dss_signature(der_sig) + sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") + sb = WalletSpendBundle( + [ + make_spend( + coin, + passkey_puzzle.puzzle_reveal(), + passkey_puzzle.solve( + [], + [], + passkey_member.solve( + authenticator_data, + client_data, + sig, + coin.puzzle_hash, + ), + DelegatedPuzzleAndSolution( + delegated_puzzle, + Program.to( + [ + announcement.to_program(), + announcement.corresponding_assertion().to_program(), + ] + ), + ), + ), + ) + ], + G2Element(), + ) + result = await client.push_tx( + cost_logger.add_cost( + "Passkey w/ puzzle assert spendbundle", + sb, + ) + ) + assert result == (MempoolInclusionStatus.SUCCESS, None) + await sim.farm_block() + await sim.rewind(block_height) + + @pytest.mark.anyio async def test_secp256r1_member(cost_logger: CostLogger) -> None: async with sim_and_client() as (sim, client): diff --git a/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py b/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py index b509af46c23c..9fa3a6a58176 100644 --- a/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py +++ b/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py @@ -24,6 +24,9 @@ PASSKEY_MEMBER_MOD = load_clvm_maybe_recompile( "passkey_member.clsp", package_or_requirement="chia.wallet.puzzles.custody.member_puzzles" ) +PASSKEY_PUZZLE_ASSERT_MEMBER_MOD = load_clvm_maybe_recompile( + "passkey_member_puzzle_assert.clsp", package_or_requirement="chia.wallet.puzzles.custody.member_puzzles" +) SECPR1_MEMBER_MOD = load_clvm_maybe_recompile( "secp256r1_member.clsp", package_or_requirement="chia.wallet.puzzles.custody.member_puzzles" @@ -78,8 +81,8 @@ def puzzle(self, nonce: int) -> Program: def puzzle_hash(self, nonce: int) -> bytes32: return self.puzzle(nonce).get_tree_hash() - def create_message(self, delegated_puzzle_hash: bytes32, coin_id: bytes32) -> str: - message = base64.urlsafe_b64encode(std_hash(delegated_puzzle_hash + coin_id + self.genesis_challenge)) + def create_message(self, delegated_puzzle_hash: bytes32, asserted_info: bytes32) -> str: + message = base64.urlsafe_b64encode(std_hash(delegated_puzzle_hash + asserted_info + self.genesis_challenge)) return message.decode("utf-8").rstrip("=") @staticmethod @@ -87,10 +90,16 @@ def format_client_data_as_str(client_data: dict[str, Any]) -> str: return json.dumps(client_data, separators=(",", ":")) def solve( - self, authenticator_data: bytes, client_data: dict[str, Any], signature: bytes, coin_id: bytes32 + self, authenticator_data: bytes, client_data: dict[str, Any], signature: bytes, asserted_info: bytes32 ) -> Program: json_str = PasskeyMember.format_client_data_as_str(client_data) - return Program.to([authenticator_data, json_str, json_str.find('"challenge":'), signature, coin_id]) + return Program.to([authenticator_data, json_str, json_str.find('"challenge":'), signature, asserted_info]) + + +@dataclass(frozen=True) +class PasskeyPuzzleAssertMember(PasskeyMember): + def puzzle(self, nonce: int) -> Program: + return PASSKEY_PUZZLE_ASSERT_MEMBER_MOD.curry(self.genesis_challenge, self.secp_pk) @dataclass(frozen=True) diff --git a/chia/wallet/puzzles/custody/member_puzzles/passkey_member_puzzle_assert.clsp b/chia/wallet/puzzles/custody/member_puzzles/passkey_member_puzzle_assert.clsp new file mode 100644 index 000000000000..d9653c08bc26 --- /dev/null +++ b/chia/wallet/puzzles/custody/member_puzzles/passkey_member_puzzle_assert.clsp @@ -0,0 +1,91 @@ +; member puzzle with SECP256-R1 signature provided by a passkey (ie Yubikey) + +(mod (GENESIS_CHALLENGE SECP_PK + Delegated_Puzzle_Hash + ; The WebAuthn authenticator data. + ; See https://www.w3.org/TR/webauthn-2/#dom-authenticatorassertionresponse-authenticatordata. + authenticator_data + ; The WebAuthn client data JSON. + ; See https://www.w3.org/TR/webauthn-2/#dom-authenticatorresponse-clientdatajson. + client_data_json + ; The index at which "challenge":"..." occurs in `clientDataJSON`. + challenge_index + ; the signature returned by the authenticator + signature + ; my puzzle hash + puzzle_hash + ) + + (include *standard-cl-23*) + (include condition_codes.clib) + (include sha256tree.clib) + + (defconstant b64-charset "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") + + (defun map-triples (fun n blob) + (if (any (= n (strlen blob)) (> n (strlen blob))) + () + (c (a fun (list (substr blob n (+ n 3)))) (map-triples fun (+ n 3) blob)) + ) + ) + + (defun flat-map (fun lst) + (if lst + (if (f lst) + (c (a fun (list (f (f lst)))) (flat-map fun (c (r (f lst)) (r lst)))) + (flat-map fun (r lst)) + ) + () + ) + ) + + (defun lookup-b64 (byte) + (substr b64-charset byte (+ byte 1)) + ) + + (defun-inline trim-padding (plen output) + (substr output 0 (- (strlen output) plen)) + ) + + (defun-inline convert (int) + (if (> int -1) int (+ int 256)) + ) + + (defun b64-encode-blob (blob) + (assign + pad_mod (r (divmod (strlen blob) 3)) + padding (if pad_mod (- 3 pad_mod) 0) + bytes (concat blob (substr 0x000000 0 padding)) + sextets + (map-triples (lambda ((& padding) bytes) + (assign + (fb_upper . fb_lower) (divmod (convert (substr bytes 0 1)) 4) + (sb_upper . sb_lower) (divmod (convert (substr bytes 1 2)) 16) + (tb_upper . tb_lower) (divmod (convert (substr bytes 2 3)) 64) + (list + fb_upper + (logior (ash fb_lower 4) sb_upper) + (logior (ash sb_lower 2) tb_upper) + tb_lower + ) + ) + ) + 0 + bytes + ) + (trim-padding padding (a (c (list 14) (flat-map lookup-b64 sextets)) ())) + ) + ) + + (assign + message (b64-encode-blob (sha256 Delegated_Puzzle_Hash puzzle_hash GENESIS_CHALLENGE)) + challenge (concat '"challenge":"' message '"') + (if (= (substr client_data_json challenge_index (+ challenge_index (strlen challenge))) challenge) + (c + (list ASSERT_MY_PUZZLEHASH puzzle_hash) + (secp256r1_verify SECP_PK (sha256 authenticator_data (sha256 client_data_json)) signature) + ) + (x) + ) + ) +) diff --git a/chia/wallet/puzzles/custody/member_puzzles/passkey_member_puzzle_assert.clsp.hex b/chia/wallet/puzzles/custody/member_puzzles/passkey_member_puzzle_assert.clsp.hex new file mode 100644 index 000000000000..518c13625ed2 --- /dev/null +++ b/chia/wallet/puzzles/custody/member_puzzles/passkey_member_puzzle_assert.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff02ff3effff04ff02ffff04ff03ffff04ffff02ff2cffff04ff02ffff04ffff0bff17ff8202ffff0580ff80808080ff8080808080ffff04ffff01ffffffff02ffff03ffff21ffff09ff0bffff0dff178080ffff15ff0bffff0dff17808080ffff01ff0180ffff01ff04ffff02ff05ffff04ffff0cff17ff0bffff10ff0bffff01038080ff808080ffff02ff10ffff04ff02ffff04ff05ffff04ffff10ff0bffff010380ffff04ff17ff8080808080808080ff0180ff02ffff03ff0bffff01ff02ffff03ff13ffff01ff04ffff02ff05ffff04ff23ff808080ffff02ff18ffff04ff02ffff04ff05ffff04ffff04ff33ff1b80ff808080808080ffff01ff02ff18ffff04ff02ffff04ff05ffff04ff1bff808080808080ff0180ffff01ff018080ff0180ffff0cffff01c0404142434445464748494a4b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f707172737475767778797a303132333435363738392d5fff05ffff10ff05ffff01018080ffff02ff3cffff04ff02ffff04ff03ffff04ffff06ffff14ffff0dff0580ffff01038080ff8080808080ff02ff12ffff04ff02ffff04ff03ffff04ffff02ffff03ff0bffff01ff11ffff0103ff0b80ffff01ff018080ff0180ff8080808080ffffff02ff2affff04ff02ffff04ff03ffff04ffff0eff11ffff0cffff0183000000ff80ff0b8080ff8080808080ffff02ff2effff04ff02ffff04ff03ffff04ffff02ff10ffff04ff02ffff04ffff04ffff0102ffff04ffff04ffff0101ffff04ffff0102ffff04ffff04ffff0101ff1680ffff04ffff04ffff0104ffff04ffff04ffff0101ff0280ffff04ffff0101ff80808080ff8080808080ffff04ffff04ffff0104ffff04ffff04ffff0101ffff04ff15ff808080ffff04ffff0101ff80808080ff80808080ffff04ff80ffff04ff0bff808080808080ff8080808080ff04ff4fffff04ffff19ffff16ff6fffff010480ff2780ffff04ffff19ffff16ff37ffff010280ff1380ffff04ff1bff8080808080ffff02ff3affff04ff02ffff04ffff04ffff04ff09ff8080ffff04ff0bff808080ffff04ffff14ffff02ffff03ffff15ffff0cff0bffff0102ffff010380ffff0181ff80ffff01ff0cff0bffff0102ffff010380ffff01ff10ffff0cff0bffff0102ffff010380ffff018201008080ff0180ffff014080ffff04ffff14ffff02ffff03ffff15ffff0cff0bffff0101ffff010280ffff0181ff80ffff01ff0cff0bffff0101ffff010280ffff01ff10ffff0cff0bffff0101ffff010280ffff018201008080ff0180ffff011080ffff04ffff14ffff02ffff03ffff15ffff0cff0bff80ffff010180ffff0181ff80ffff01ff0cff0bff80ffff010180ffff01ff10ffff0cff0bff80ffff010180ffff018201008080ff0180ffff010480ff80808080808080ffff0cffff02ffff04ffff04ffff010eff8080ffff02ff18ffff04ff02ffff04ffff04ffff0102ffff04ffff04ffff0101ff1480ffff04ffff04ffff0104ffff04ffff04ffff0101ff0280ffff04ffff0101ff80808080ff80808080ffff04ff0bff808080808080ff8080ff80ffff11ffff0dffff02ffff04ffff04ffff010eff8080ffff02ff18ffff04ff02ffff04ffff04ffff0102ffff04ffff04ffff0101ff1480ffff04ffff04ffff0104ffff04ffff04ffff0101ff0280ffff04ffff0101ff80808080ff80808080ffff04ff0bff808080808080ff808080ff298080ff02ffff03ffff09ffff0cff8200bdff82017dffff10ff82017dffff0dffff0effff018d226368616c6c656e6765223a22ff0bffff012280808080ffff0effff018d226368616c6c656e6765223a22ff0bffff01228080ffff01ff04ffff04ffff0148ffff04ff8205fdff808080ffff841c3a8f00ff15ffff0bff5dffff0bff8200bd8080ff8202fd8080ffff01ff088080ff0180ff018080 diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 760f71b50cbb..bb54eac48271 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -64,6 +64,7 @@ "p2_singleton_or_delayed_puzhash": "adb656e0211e2ab4f42069a4c5efc80dc907e7062be08bf1628c8e5b6d94d25b", "p2_singleton_via_delegated_puzzle": "9590eaa169e45b655a31d3c06bbd355a3e2b2e3e410d3829748ce08ab249c39e", "passkey_member": "2877c080c18a408111ec86b108da56dd667f968ce38f87623ca084934127059c", + "passkey_member_puzzle_assert": "0bf2d91cf0442a139b2cb9f9c2a68a3ac3b497093fc66595b3c4c398ae94d206", "pool_member_innerpuz": "a8490702e333ddd831a3ac9c22d0fa26d2bfeaf2d33608deb22f0e0123eb0494", "pool_waitingroom_innerpuz": "a317541a765bf8375e1c6e7c13503d0d2cbf56cacad5182befe947e78e2c0307", "restrictions": "a28d59d39f964a93159c986b1914694f6f2f1c9901178f91e8b0ba4045980eef",