WebAuthn.t.sol 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. // SPDX-License-Identifier: MIT
  2. pragma solidity ^0.8.20;
  3. import {Test} from "forge-std/Test.sol";
  4. import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol";
  5. import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
  6. import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";
  7. import {WebAuthn} from "@openzeppelin/contracts/utils/cryptography/WebAuthn.sol";
  8. contract WebAuthnTest is Test {
  9. /// forge-config: default.fuzz.runs = 512
  10. function testVerify(bytes memory challenge, uint256 seed) public view {
  11. assertTrue(
  12. _runVerify(
  13. seed,
  14. challenge,
  15. _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP),
  16. _encodeClientDataJSON(challenge),
  17. false
  18. )
  19. );
  20. }
  21. /// forge-config: default.fuzz.runs = 512
  22. function testVerifyInvalidType(bytes memory challenge, uint256 seed) public view {
  23. assertFalse(
  24. _runVerify(
  25. seed,
  26. challenge,
  27. _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP | WebAuthn.AUTH_DATA_FLAGS_UV),
  28. // solhint-disable-next-line quotes
  29. string.concat('{"type":"webauthn.create","challenge":"', Base64.encodeURL(challenge), '"}'),
  30. false
  31. )
  32. );
  33. }
  34. /// forge-config: default.fuzz.runs = 512
  35. function testVerifyInvalidChallenge(bytes memory challenge, uint256 seed) public view {
  36. assertFalse(
  37. _runVerify(
  38. seed,
  39. challenge,
  40. _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP | WebAuthn.AUTH_DATA_FLAGS_UV),
  41. _encodeClientDataJSON(bytes("invalid_challenge")),
  42. false
  43. )
  44. );
  45. }
  46. /// forge-config: default.fuzz.runs = 512
  47. function testVerifyFlagsUP(bytes memory challenge, uint256 seed) public view {
  48. // UP = false: FAIL
  49. assertFalse(
  50. _runVerify(
  51. seed,
  52. challenge,
  53. _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UV),
  54. _encodeClientDataJSON(challenge),
  55. false
  56. )
  57. );
  58. }
  59. /// forge-config: default.fuzz.runs = 512
  60. function testVerifyFlagsUV(bytes memory challenge, uint256 seed) public view {
  61. // UV = false, requireUV = false: SUCCESS
  62. assertTrue(
  63. _runVerify(
  64. seed,
  65. challenge,
  66. _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP),
  67. _encodeClientDataJSON(challenge),
  68. false
  69. )
  70. );
  71. // UV = false, requireUV = true: FAIL
  72. assertFalse(
  73. _runVerify(
  74. seed,
  75. challenge,
  76. _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP),
  77. _encodeClientDataJSON(challenge),
  78. true
  79. )
  80. );
  81. // UV = true, requireUV = true: SUCCESS
  82. assertTrue(
  83. _runVerify(
  84. seed,
  85. challenge,
  86. _encodeAuthenticatorData(WebAuthn.AUTH_DATA_FLAGS_UP | WebAuthn.AUTH_DATA_FLAGS_UV),
  87. _encodeClientDataJSON(challenge),
  88. true
  89. )
  90. );
  91. }
  92. /// forge-config: default.fuzz.runs = 512
  93. function testVerifyFlagsBEBS(bytes memory challenge, uint256 seed) public view {
  94. // BS = true, BE = false: FAIL
  95. assertFalse(
  96. _runVerify(
  97. seed,
  98. challenge,
  99. _encodeAuthenticatorData(
  100. WebAuthn.AUTH_DATA_FLAGS_UP | WebAuthn.AUTH_DATA_FLAGS_UV | WebAuthn.AUTH_DATA_FLAGS_BS
  101. ),
  102. _encodeClientDataJSON(challenge),
  103. false
  104. )
  105. );
  106. // BS = false, BE = true: SUCCESS
  107. assertTrue(
  108. _runVerify(
  109. seed,
  110. challenge,
  111. _encodeAuthenticatorData(
  112. WebAuthn.AUTH_DATA_FLAGS_UP | WebAuthn.AUTH_DATA_FLAGS_UV | WebAuthn.AUTH_DATA_FLAGS_BE
  113. ),
  114. _encodeClientDataJSON(challenge),
  115. false
  116. )
  117. );
  118. // BS = true, BE = true: SUCCESS
  119. assertTrue(
  120. _runVerify(
  121. seed,
  122. challenge,
  123. _encodeAuthenticatorData(
  124. WebAuthn.AUTH_DATA_FLAGS_UP |
  125. WebAuthn.AUTH_DATA_FLAGS_UV |
  126. WebAuthn.AUTH_DATA_FLAGS_BE |
  127. WebAuthn.AUTH_DATA_FLAGS_BS
  128. ),
  129. _encodeClientDataJSON(challenge),
  130. false
  131. )
  132. );
  133. }
  134. function _runVerify(
  135. uint256 seed,
  136. bytes memory challenge,
  137. bytes memory authenticatorData,
  138. string memory clientDataJSON,
  139. bool requireUV
  140. ) private view returns (bool) {
  141. // Generate private key and get public key
  142. uint256 privateKey = bound(seed, 1, P256.N - 1);
  143. (uint256 x, uint256 y) = vm.publicKeyP256(privateKey);
  144. // Sign the message
  145. bytes32 messageHash = sha256(abi.encodePacked(authenticatorData, sha256(bytes(clientDataJSON))));
  146. (bytes32 r, bytes32 s) = vm.signP256(privateKey, messageHash);
  147. // Verify the signature
  148. return
  149. WebAuthn.verify(
  150. challenge,
  151. WebAuthn.WebAuthnAuth({
  152. authenticatorData: authenticatorData,
  153. clientDataJSON: clientDataJSON,
  154. challengeIndex: 23, // Position of challenge in clientDataJSON
  155. typeIndex: 1, // Position of type in clientDataJSON
  156. r: r,
  157. s: bytes32(Math.min(uint256(s), P256.N - uint256(s)))
  158. }),
  159. bytes32(x),
  160. bytes32(y),
  161. requireUV
  162. );
  163. }
  164. function testTryDecodeAuthValid(
  165. bytes32 r,
  166. bytes32 s,
  167. uint256 challengeIndex,
  168. uint256 typeIndex,
  169. bytes memory authenticatorData,
  170. string memory clientDataJSON
  171. ) public view {
  172. (bool success, WebAuthn.WebAuthnAuth memory auth) = this.tryDecodeAuth(
  173. abi.encode(r, s, challengeIndex, typeIndex, authenticatorData, clientDataJSON)
  174. );
  175. assertTrue(success);
  176. assertEq(auth.r, r);
  177. assertEq(auth.s, s);
  178. assertEq(auth.challengeIndex, challengeIndex);
  179. assertEq(auth.typeIndex, typeIndex);
  180. assertEq(auth.authenticatorData, authenticatorData);
  181. assertEq(auth.clientDataJSON, clientDataJSON);
  182. }
  183. function testTryDecodeAuthInvalid() public view {
  184. bytes32 r = keccak256("r");
  185. bytes32 s = keccak256("s");
  186. uint256 challengeIndex = 17;
  187. uint256 typeIndex = 1;
  188. // too short
  189. assertFalse(this.tryDecodeAuthDrop(abi.encodePacked(r, s, challengeIndex, typeIndex)));
  190. // offset out of bound
  191. assertFalse(
  192. this.tryDecodeAuthDrop(abi.encodePacked(r, s, challengeIndex, typeIndex, uint256(0xc0), uint256(0)))
  193. );
  194. assertFalse(
  195. this.tryDecodeAuthDrop(abi.encodePacked(r, s, challengeIndex, typeIndex, uint256(0), uint256(0xc0)))
  196. );
  197. // minimal valid (bytes and string both length 0, at the same position)
  198. assertTrue(
  199. this.tryDecodeAuthDrop(
  200. abi.encodePacked(r, s, challengeIndex, typeIndex, uint256(0xc0), uint256(0xc0), uint256(0))
  201. )
  202. );
  203. // length out of bound
  204. assertTrue(
  205. this.tryDecodeAuthDrop(
  206. abi.encodePacked(
  207. r,
  208. s,
  209. challengeIndex,
  210. typeIndex,
  211. uint256(0xc0),
  212. uint256(0xe0),
  213. uint256(0x20),
  214. uint256(0)
  215. )
  216. )
  217. );
  218. assertFalse(
  219. this.tryDecodeAuthDrop(
  220. abi.encodePacked(
  221. r,
  222. s,
  223. challengeIndex,
  224. typeIndex,
  225. uint256(0xc0),
  226. uint256(0xe0),
  227. uint256(0x21),
  228. uint256(0)
  229. )
  230. )
  231. );
  232. assertTrue(
  233. this.tryDecodeAuthDrop(
  234. abi.encodePacked(
  235. r,
  236. s,
  237. challengeIndex,
  238. typeIndex,
  239. uint256(0xc0),
  240. uint256(0xe0),
  241. uint256(0),
  242. uint256(0x00)
  243. )
  244. )
  245. );
  246. assertFalse(
  247. this.tryDecodeAuthDrop(
  248. abi.encodePacked(
  249. r,
  250. s,
  251. challengeIndex,
  252. typeIndex,
  253. uint256(0xc0),
  254. uint256(0xe0),
  255. uint256(0),
  256. uint256(0x01)
  257. )
  258. )
  259. );
  260. }
  261. function tryDecodeAuth(
  262. bytes calldata encoded
  263. ) public pure returns (bool success, WebAuthn.WebAuthnAuth calldata auth) {
  264. (success, auth) = WebAuthn.tryDecodeAuth(encoded);
  265. }
  266. function tryDecodeAuthDrop(bytes calldata encoded) public pure returns (bool success) {
  267. (success, ) = WebAuthn.tryDecodeAuth(encoded);
  268. }
  269. function _encodeAuthenticatorData(bytes1 flags) private pure returns (bytes memory) {
  270. return abi.encodePacked(bytes32(0), flags, bytes4(0));
  271. }
  272. function _encodeClientDataJSON(bytes memory challenge) private pure returns (string memory) {
  273. // solhint-disable-next-line quotes
  274. return string.concat('{"type":"webauthn.get","challenge":"', Base64.encodeURL(challenge), '"}');
  275. }
  276. }