judu/ex-egopublic Fork
⑂ aeae9bb
Commits
⬇ Clone ▾
git clone https://git.rickub.com/judu/ex-ego.git
git clone ssh://git@rickub.com/judu/ex-ego.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(webauthn): socle — vérifieur, clé de scellement, quota d'échecsUnverified

WEBAUTHN_ORIGIN explicite (absente = feature éteinte), état de cérémonie
scellé HMAC dans un cookie éphémère (motif csrf.rs + expiration + étanchéité
reg/auth), Quota 10 échecs/h/IP.
Julien Durillon committed 2026-08-26T14:01:17+02:00 Browse files
aeae9bb parent: 7f1719b
modified Cargo.lock +357 -17
@@ -147,6 +147,45 @@ dependencies = [
147147 "winnow",
148148 ]
149149
150+[[package]]
151+name = "asn1-rs"
152+version = "0.6.2"
153+source = "registry+https://github.com/rust-lang/crates.io-index"
154+checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
155+dependencies = [
156+ "asn1-rs-derive",
157+ "asn1-rs-impl",
158+ "displaydoc",
159+ "nom",
160+ "num-traits",
161+ "rusticata-macros",
162+ "thiserror 1.0.69",
163+ "time",
164+]
165+
166+[[package]]
167+name = "asn1-rs-derive"
168+version = "0.5.1"
169+source = "registry+https://github.com/rust-lang/crates.io-index"
170+checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
171+dependencies = [
172+ "proc-macro2",
173+ "quote",
174+ "syn 2.0.119",
175+ "synstructure",
176+]
177+
178+[[package]]
179+name = "asn1-rs-impl"
180+version = "0.2.0"
181+source = "registry+https://github.com/rust-lang/crates.io-index"
182+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
183+dependencies = [
184+ "proc-macro2",
185+ "quote",
186+ "syn 2.0.119",
187+]
188+
150189 [[package]]
151190 name = "atoi"
152191 version = "2.0.0"
@@ -254,6 +293,12 @@ version = "0.13.1"
254293 source = "registry+https://github.com/rust-lang/crates.io-index"
255294 checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
256295
296+[[package]]
297+name = "base64"
298+version = "0.21.7"
299+source = "registry+https://github.com/rust-lang/crates.io-index"
300+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
301+
257302 [[package]]
258303 name = "base64"
259304 version = "0.22.1"
@@ -266,6 +311,17 @@ version = "1.8.3"
266311 source = "registry+https://github.com/rust-lang/crates.io-index"
267312 checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
268313
314+[[package]]
315+name = "base64urlsafedata"
316+version = "0.5.5"
317+source = "registry+https://github.com/rust-lang/crates.io-index"
318+checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c"
319+dependencies = [
320+ "base64 0.21.7",
321+ "pastey",
322+ "serde",
323+]
324+
269325 [[package]]
270326 name = "basic-toml"
271327 version = "0.1.10"
@@ -294,8 +350,8 @@ dependencies = [
294350 "pkcs8 0.9.0",
295351 "prost",
296352 "prost-types",
297- "rand",
298- "rand_core",
353+ "rand 0.8.7",
354+ "rand_core 0.6.4",
299355 "regex",
300356 "serde_json",
301357 "sha2 0.9.9",
@@ -533,6 +589,12 @@ version = "0.8.22"
533589 source = "registry+https://github.com/rust-lang/crates.io-index"
534590 checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
535591
592+[[package]]
593+name = "crunchy"
594+version = "0.2.4"
595+source = "registry+https://github.com/rust-lang/crates.io-index"
596+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
597+
536598 [[package]]
537599 name = "crypto-bigint"
538600 version = "0.5.5"
@@ -540,7 +602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
540602 checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
541603 dependencies = [
542604 "generic-array",
543- "rand_core",
605+ "rand_core 0.6.4",
544606 "subtle",
545607 "zeroize",
546608 ]
@@ -582,6 +644,12 @@ dependencies = [
582644 "syn 2.0.119",
583645 ]
584646
647+[[package]]
648+name = "data-encoding"
649+version = "2.11.1"
650+source = "registry+https://github.com/rust-lang/crates.io-index"
651+checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
652+
585653 [[package]]
586654 name = "der"
587655 version = "0.6.1"
@@ -602,6 +670,20 @@ dependencies = [
602670 "zeroize",
603671 ]
604672
673+[[package]]
674+name = "der-parser"
675+version = "9.0.0"
676+source = "registry+https://github.com/rust-lang/crates.io-index"
677+checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
678+dependencies = [
679+ "asn1-rs",
680+ "displaydoc",
681+ "nom",
682+ "num-bigint",
683+ "num-traits",
684+ "rusticata-macros",
685+]
686+
605687 [[package]]
606688 name = "deranged"
607689 version = "0.5.8"
@@ -679,7 +761,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
679761 dependencies = [
680762 "curve25519-dalek",
681763 "ed25519",
682- "rand_core",
764+ "rand_core 0.6.4",
683765 "serde",
684766 "sha2 0.10.9",
685767 "subtle",
@@ -709,7 +791,7 @@ dependencies = [
709791 "group",
710792 "pem-rfc7468",
711793 "pkcs8 0.10.2",
712- "rand_core",
794+ "rand_core 0.6.4",
713795 "sec1",
714796 "serdect",
715797 "subtle",
@@ -762,6 +844,7 @@ dependencies = [
762844 "askama",
763845 "axum",
764846 "axum-extra",
847+ "base64 0.22.1",
765848 "biscuit-auth",
766849 "chrono",
767850 "chrono-tz",
@@ -770,6 +853,7 @@ dependencies = [
770853 "hmac",
771854 "rpassword",
772855 "serde",
856+ "serde_json",
773857 "sha2 0.10.9",
774858 "sqlx",
775859 "subtle",
@@ -779,6 +863,7 @@ dependencies = [
779863 "tracing",
780864 "tracing-subscriber",
781865 "uuid",
866+ "webauthn-rs",
782867 ]
783868
784869 [[package]]
@@ -787,7 +872,7 @@ version = "0.13.1"
787872 source = "registry+https://github.com/rust-lang/crates.io-index"
788873 checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
789874 dependencies = [
790- "rand_core",
875+ "rand_core 0.6.4",
791876 "subtle",
792877 ]
793878
@@ -820,6 +905,21 @@ version = "0.1.5"
820905 source = "registry+https://github.com/rust-lang/crates.io-index"
821906 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
822907
908+[[package]]
909+name = "foreign-types"
910+version = "0.3.2"
911+source = "registry+https://github.com/rust-lang/crates.io-index"
912+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
913+dependencies = [
914+ "foreign-types-shared",
915+]
916+
917+[[package]]
918+name = "foreign-types-shared"
919+version = "0.1.1"
920+source = "registry+https://github.com/rust-lang/crates.io-index"
921+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
922+
823923 [[package]]
824924 name = "form_urlencoded"
825925 version = "1.2.2"
@@ -922,6 +1022,18 @@ dependencies = [
9221022 "wasi",
9231023 ]
9241024
1025+[[package]]
1026+name = "getrandom"
1027+version = "0.3.4"
1028+source = "registry+https://github.com/rust-lang/crates.io-index"
1029+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
1030+dependencies = [
1031+ "cfg-if",
1032+ "libc",
1033+ "r-efi 5.3.0",
1034+ "wasip2",
1035+]
1036+
9251037 [[package]]
9261038 name = "getrandom"
9271039 version = "0.4.3"
@@ -930,7 +1042,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
9301042 dependencies = [
9311043 "cfg-if",
9321044 "libc",
933- "r-efi",
1045+ "r-efi 6.0.0",
9341046 ]
9351047
9361048 [[package]]
@@ -946,10 +1058,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
9461058 checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
9471059 dependencies = [
9481060 "ff",
949- "rand_core",
1061+ "rand_core 0.6.4",
9501062 "subtle",
9511063 ]
9521064
1065+[[package]]
1066+name = "half"
1067+version = "2.7.1"
1068+source = "registry+https://github.com/rust-lang/crates.io-index"
1069+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
1070+dependencies = [
1071+ "cfg-if",
1072+ "crunchy",
1073+ "zerocopy",
1074+]
1075+
9531076 [[package]]
9541077 name = "hashbrown"
9551078 version = "0.15.5"
@@ -1401,6 +1524,16 @@ dependencies = [
14011524 "windows-sys 0.61.2",
14021525 ]
14031526
1527+[[package]]
1528+name = "num-bigint"
1529+version = "0.4.8"
1530+source = "registry+https://github.com/rust-lang/crates.io-index"
1531+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
1532+dependencies = [
1533+ "num-integer",
1534+ "num-traits",
1535+]
1536+
14041537 [[package]]
14051538 name = "num-bigint-dig"
14061539 version = "0.8.6"
@@ -1412,7 +1545,7 @@ dependencies = [
14121545 "num-integer",
14131546 "num-iter",
14141547 "num-traits",
1415- "rand",
1548+ "rand 0.8.7",
14161549 "smallvec",
14171550 "zeroize",
14181551 ]
@@ -1452,6 +1585,15 @@ dependencies = [
14521585 "libm",
14531586 ]
14541587
1588+[[package]]
1589+name = "oid-registry"
1590+version = "0.7.1"
1591+source = "registry+https://github.com/rust-lang/crates.io-index"
1592+checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
1593+dependencies = [
1594+ "asn1-rs",
1595+]
1596+
14551597 [[package]]
14561598 name = "once_cell"
14571599 version = "1.21.4"
@@ -1470,6 +1612,43 @@ version = "0.3.1"
14701612 source = "registry+https://github.com/rust-lang/crates.io-index"
14711613 checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
14721614
1615+[[package]]
1616+name = "openssl"
1617+version = "0.10.81"
1618+source = "registry+https://github.com/rust-lang/crates.io-index"
1619+checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
1620+dependencies = [
1621+ "bitflags",
1622+ "cfg-if",
1623+ "foreign-types",
1624+ "libc",
1625+ "openssl-macros",
1626+ "openssl-sys",
1627+]
1628+
1629+[[package]]
1630+name = "openssl-macros"
1631+version = "0.1.1"
1632+source = "registry+https://github.com/rust-lang/crates.io-index"
1633+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
1634+dependencies = [
1635+ "proc-macro2",
1636+ "quote",
1637+ "syn 2.0.119",
1638+]
1639+
1640+[[package]]
1641+name = "openssl-sys"
1642+version = "0.9.117"
1643+source = "registry+https://github.com/rust-lang/crates.io-index"
1644+checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
1645+dependencies = [
1646+ "cc",
1647+ "libc",
1648+ "pkg-config",
1649+ "vcpkg",
1650+]
1651+
14731652 [[package]]
14741653 name = "p256"
14751654 version = "0.13.2"
@@ -1518,10 +1697,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
15181697 checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
15191698 dependencies = [
15201699 "base64ct",
1521- "rand_core",
1700+ "rand_core 0.6.4",
15221701 "subtle",
15231702 ]
15241703
1704+[[package]]
1705+name = "pastey"
1706+version = "0.1.1"
1707+source = "registry+https://github.com/rust-lang/crates.io-index"
1708+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
1709+
15251710 [[package]]
15261711 name = "pem-rfc7468"
15271712 version = "0.7.0"
@@ -1710,6 +1895,12 @@ dependencies = [
17101895 "proc-macro2",
17111896 ]
17121897
1898+[[package]]
1899+name = "r-efi"
1900+version = "5.3.0"
1901+source = "registry+https://github.com/rust-lang/crates.io-index"
1902+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
1903+
17131904 [[package]]
17141905 name = "r-efi"
17151906 version = "6.0.0"
@@ -1723,8 +1914,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
17231914 checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
17241915 dependencies = [
17251916 "libc",
1726- "rand_chacha",
1727- "rand_core",
1917+ "rand_chacha 0.3.1",
1918+ "rand_core 0.6.4",
1919+]
1920+
1921+[[package]]
1922+name = "rand"
1923+version = "0.9.5"
1924+source = "registry+https://github.com/rust-lang/crates.io-index"
1925+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
1926+dependencies = [
1927+ "rand_chacha 0.9.0",
1928+ "rand_core 0.9.5",
17281929 ]
17291930
17301931 [[package]]
@@ -1734,7 +1935,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
17341935 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
17351936 dependencies = [
17361937 "ppv-lite86",
1737- "rand_core",
1938+ "rand_core 0.6.4",
1939+]
1940+
1941+[[package]]
1942+name = "rand_chacha"
1943+version = "0.9.0"
1944+source = "registry+https://github.com/rust-lang/crates.io-index"
1945+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
1946+dependencies = [
1947+ "ppv-lite86",
1948+ "rand_core 0.9.5",
17381949 ]
17391950
17401951 [[package]]
@@ -1746,6 +1957,15 @@ dependencies = [
17461957 "getrandom 0.2.17",
17471958 ]
17481959
1960+[[package]]
1961+name = "rand_core"
1962+version = "0.9.5"
1963+source = "registry+https://github.com/rust-lang/crates.io-index"
1964+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
1965+dependencies = [
1966+ "getrandom 0.3.4",
1967+]
1968+
17491969 [[package]]
17501970 name = "redox_syscall"
17511971 version = "0.5.18"
@@ -1841,7 +2061,7 @@ dependencies = [
18412061 "num-traits",
18422062 "pkcs1",
18432063 "pkcs8 0.10.2",
1844- "rand_core",
2064+ "rand_core 0.6.4",
18452065 "signature",
18462066 "spki 0.7.3",
18472067 "subtle",
@@ -1873,6 +2093,15 @@ dependencies = [
18732093 "semver",
18742094 ]
18752095
2096+[[package]]
2097+name = "rusticata-macros"
2098+version = "4.1.0"
2099+source = "registry+https://github.com/rust-lang/crates.io-index"
2100+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
2101+dependencies = [
2102+ "nom",
2103+]
2104+
18762105 [[package]]
18772106 name = "rustls"
18782107 version = "0.23.43"
@@ -1956,6 +2185,16 @@ dependencies = [
19562185 "serde_derive",
19572186 ]
19582187
2188+[[package]]
2189+name = "serde_cbor_2"
2190+version = "0.13.0"
2191+source = "registry+https://github.com/rust-lang/crates.io-index"
2192+checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c"
2193+dependencies = [
2194+ "half",
2195+ "serde",
2196+]
2197+
19592198 [[package]]
19602199 name = "serde_core"
19612200 version = "1.0.229"
@@ -2089,7 +2328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
20892328 checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
20902329 dependencies = [
20912330 "digest 0.10.7",
2092- "rand_core",
2331+ "rand_core 0.6.4",
20932332 ]
20942333
20952334 [[package]]
@@ -2270,7 +2509,7 @@ dependencies = [
22702509 "memchr",
22712510 "once_cell",
22722511 "percent-encoding",
2273- "rand",
2512+ "rand 0.8.7",
22742513 "rsa",
22752514 "serde",
22762515 "sha1",
@@ -2310,7 +2549,7 @@ dependencies = [
23102549 "md-5",
23112550 "memchr",
23122551 "once_cell",
2313- "rand",
2552+ "rand 0.8.7",
23142553 "serde",
23152554 "serde_json",
23162555 "sha2 0.10.9",
@@ -2709,6 +2948,7 @@ dependencies = [
27092948 "idna",
27102949 "percent-encoding",
27112950 "serde",
2951+ "serde_derive",
27122952 ]
27132953
27142954 [[package]]
@@ -2759,6 +2999,15 @@ version = "0.11.1+wasi-snapshot-preview1"
27592999 source = "registry+https://github.com/rust-lang/crates.io-index"
27603000 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
27613001
3002+[[package]]
3003+name = "wasip2"
3004+version = "1.0.4+wasi-0.2.12"
3005+source = "registry+https://github.com/rust-lang/crates.io-index"
3006+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
3007+dependencies = [
3008+ "wit-bindgen",
3009+]
3010+
27623011 [[package]]
27633012 name = "wasite"
27643013 version = "0.1.0"
@@ -2810,6 +3059,74 @@ dependencies = [
28103059 "unicode-ident",
28113060 ]
28123061
3062+[[package]]
3063+name = "webauthn-attestation-ca"
3064+version = "0.5.5"
3065+source = "registry+https://github.com/rust-lang/crates.io-index"
3066+checksum = "6475c0bbd1a3f04afaa3e98880408c5be61680c5e6bd3c6f8c250990d5d3e18e"
3067+dependencies = [
3068+ "base64urlsafedata",
3069+ "openssl",
3070+ "openssl-sys",
3071+ "serde",
3072+ "tracing",
3073+ "uuid",
3074+]
3075+
3076+[[package]]
3077+name = "webauthn-rs"
3078+version = "0.5.5"
3079+source = "registry+https://github.com/rust-lang/crates.io-index"
3080+checksum = "6c548915e0e92ee946bbf2aecf01ea21bef53d974b0793cc6732ba81a03fc422"
3081+dependencies = [
3082+ "base64urlsafedata",
3083+ "serde",
3084+ "tracing",
3085+ "url",
3086+ "uuid",
3087+ "webauthn-rs-core",
3088+]
3089+
3090+[[package]]
3091+name = "webauthn-rs-core"
3092+version = "0.5.5"
3093+source = "registry+https://github.com/rust-lang/crates.io-index"
3094+checksum = "296d2d501feb715d80b8e186fb88bab1073bca17f460303a1013d17b673bea6a"
3095+dependencies = [
3096+ "base64 0.21.7",
3097+ "base64urlsafedata",
3098+ "der-parser",
3099+ "hex",
3100+ "nom",
3101+ "openssl",
3102+ "openssl-sys",
3103+ "rand 0.9.5",
3104+ "rand_chacha 0.9.0",
3105+ "serde",
3106+ "serde_cbor_2",
3107+ "serde_json",
3108+ "thiserror 1.0.69",
3109+ "tracing",
3110+ "url",
3111+ "uuid",
3112+ "webauthn-attestation-ca",
3113+ "webauthn-rs-proto",
3114+ "x509-parser",
3115+]
3116+
3117+[[package]]
3118+name = "webauthn-rs-proto"
3119+version = "0.5.5"
3120+source = "registry+https://github.com/rust-lang/crates.io-index"
3121+checksum = "c37393beac9c1ed1ca6dbb30b1e01783fb316ab3a45d90ecd48c99052dd7ef1e"
3122+dependencies = [
3123+ "base64 0.21.7",
3124+ "base64urlsafedata",
3125+ "serde",
3126+ "serde_json",
3127+ "url",
3128+]
3129+
28133130 [[package]]
28143131 name = "webpki-roots"
28153132 version = "0.26.11"
@@ -3063,12 +3380,35 @@ dependencies = [
30633380 "memchr",
30643381 ]
30653382
3383+[[package]]
3384+name = "wit-bindgen"
3385+version = "0.57.1"
3386+source = "registry+https://github.com/rust-lang/crates.io-index"
3387+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
3388+
30663389 [[package]]
30673390 name = "writeable"
30683391 version = "0.6.3"
30693392 source = "registry+https://github.com/rust-lang/crates.io-index"
30703393 checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
30713394
3395+[[package]]
3396+name = "x509-parser"
3397+version = "0.16.0"
3398+source = "registry+https://github.com/rust-lang/crates.io-index"
3399+checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
3400+dependencies = [
3401+ "asn1-rs",
3402+ "data-encoding",
3403+ "der-parser",
3404+ "lazy_static",
3405+ "nom",
3406+ "oid-registry",
3407+ "rusticata-macros",
3408+ "thiserror 1.0.69",
3409+ "time",
3410+]
3411+
30723412 [[package]]
30733413 name = "yoke"
30743414 version = "0.8.3"
@@ -147,6 +147,45 @@ dependencies = [
147 "winnow",147 "winnow",
148 ]148 ]
149 149
150+[[package]]
151+name = "asn1-rs"
152+version = "0.6.2"
153+source = "registry+https://github.com/rust-lang/crates.io-index"
154+checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
155+dependencies = [
156+ "asn1-rs-derive",
157+ "asn1-rs-impl",
158+ "displaydoc",
159+ "nom",
160+ "num-traits",
161+ "rusticata-macros",
162+ "thiserror 1.0.69",
163+ "time",
164+]
165+
166+[[package]]
167+name = "asn1-rs-derive"
168+version = "0.5.1"
169+source = "registry+https://github.com/rust-lang/crates.io-index"
170+checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
171+dependencies = [
172+ "proc-macro2",
173+ "quote",
174+ "syn 2.0.119",
175+ "synstructure",
176+]
177+
178+[[package]]
179+name = "asn1-rs-impl"
180+version = "0.2.0"
181+source = "registry+https://github.com/rust-lang/crates.io-index"
182+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
183+dependencies = [
184+ "proc-macro2",
185+ "quote",
186+ "syn 2.0.119",
187+]
188+
150 [[package]]189 [[package]]
151 name = "atoi"190 name = "atoi"
152 version = "2.0.0"191 version = "2.0.0"
@@ -254,6 +293,12 @@ version = "0.13.1"
254 source = "registry+https://github.com/rust-lang/crates.io-index"293 source = "registry+https://github.com/rust-lang/crates.io-index"
255 checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"294 checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
256 295
296+[[package]]
297+name = "base64"
298+version = "0.21.7"
299+source = "registry+https://github.com/rust-lang/crates.io-index"
300+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
301+
257 [[package]]302 [[package]]
258 name = "base64"303 name = "base64"
259 version = "0.22.1"304 version = "0.22.1"
@@ -266,6 +311,17 @@ version = "1.8.3"
266 source = "registry+https://github.com/rust-lang/crates.io-index"311 source = "registry+https://github.com/rust-lang/crates.io-index"
267 checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"312 checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
268 313
314+[[package]]
315+name = "base64urlsafedata"
316+version = "0.5.5"
317+source = "registry+https://github.com/rust-lang/crates.io-index"
318+checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c"
319+dependencies = [
320+ "base64 0.21.7",
321+ "pastey",
322+ "serde",
323+]
324+
269 [[package]]325 [[package]]
270 name = "basic-toml"326 name = "basic-toml"
271 version = "0.1.10"327 version = "0.1.10"
@@ -294,8 +350,8 @@ dependencies = [
294 "pkcs8 0.9.0",350 "pkcs8 0.9.0",
295 "prost",351 "prost",
296 "prost-types",352 "prost-types",
297- "rand",353+ "rand 0.8.7",
298- "rand_core",354+ "rand_core 0.6.4",
299 "regex",355 "regex",
300 "serde_json",356 "serde_json",
301 "sha2 0.9.9",357 "sha2 0.9.9",
@@ -533,6 +589,12 @@ version = "0.8.22"
533 source = "registry+https://github.com/rust-lang/crates.io-index"589 source = "registry+https://github.com/rust-lang/crates.io-index"
534 checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"590 checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
535 591
592+[[package]]
593+name = "crunchy"
594+version = "0.2.4"
595+source = "registry+https://github.com/rust-lang/crates.io-index"
596+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
597+
536 [[package]]598 [[package]]
537 name = "crypto-bigint"599 name = "crypto-bigint"
538 version = "0.5.5"600 version = "0.5.5"
@@ -540,7 +602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
540 checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"602 checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
541 dependencies = [603 dependencies = [
542 "generic-array",604 "generic-array",
543- "rand_core",605+ "rand_core 0.6.4",
544 "subtle",606 "subtle",
545 "zeroize",607 "zeroize",
546 ]608 ]
@@ -582,6 +644,12 @@ dependencies = [
582 "syn 2.0.119",644 "syn 2.0.119",
583 ]645 ]
584 646
647+[[package]]
648+name = "data-encoding"
649+version = "2.11.1"
650+source = "registry+https://github.com/rust-lang/crates.io-index"
651+checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
652+
585 [[package]]653 [[package]]
586 name = "der"654 name = "der"
587 version = "0.6.1"655 version = "0.6.1"
@@ -602,6 +670,20 @@ dependencies = [
602 "zeroize",670 "zeroize",
603 ]671 ]
604 672
673+[[package]]
674+name = "der-parser"
675+version = "9.0.0"
676+source = "registry+https://github.com/rust-lang/crates.io-index"
677+checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
678+dependencies = [
679+ "asn1-rs",
680+ "displaydoc",
681+ "nom",
682+ "num-bigint",
683+ "num-traits",
684+ "rusticata-macros",
685+]
686+
605 [[package]]687 [[package]]
606 name = "deranged"688 name = "deranged"
607 version = "0.5.8"689 version = "0.5.8"
@@ -679,7 +761,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
679 dependencies = [761 dependencies = [
680 "curve25519-dalek",762 "curve25519-dalek",
681 "ed25519",763 "ed25519",
682- "rand_core",764+ "rand_core 0.6.4",
683 "serde",765 "serde",
684 "sha2 0.10.9",766 "sha2 0.10.9",
685 "subtle",767 "subtle",
@@ -709,7 +791,7 @@ dependencies = [
709 "group",791 "group",
710 "pem-rfc7468",792 "pem-rfc7468",
711 "pkcs8 0.10.2",793 "pkcs8 0.10.2",
712- "rand_core",794+ "rand_core 0.6.4",
713 "sec1",795 "sec1",
714 "serdect",796 "serdect",
715 "subtle",797 "subtle",
@@ -762,6 +844,7 @@ dependencies = [
762 "askama",844 "askama",
763 "axum",845 "axum",
764 "axum-extra",846 "axum-extra",
847+ "base64 0.22.1",
765 "biscuit-auth",848 "biscuit-auth",
766 "chrono",849 "chrono",
767 "chrono-tz",850 "chrono-tz",
@@ -770,6 +853,7 @@ dependencies = [
770 "hmac",853 "hmac",
771 "rpassword",854 "rpassword",
772 "serde",855 "serde",
856+ "serde_json",
773 "sha2 0.10.9",857 "sha2 0.10.9",
774 "sqlx",858 "sqlx",
775 "subtle",859 "subtle",
@@ -779,6 +863,7 @@ dependencies = [
779 "tracing",863 "tracing",
780 "tracing-subscriber",864 "tracing-subscriber",
781 "uuid",865 "uuid",
866+ "webauthn-rs",
782 ]867 ]
783 868
784 [[package]]869 [[package]]
@@ -787,7 +872,7 @@ version = "0.13.1"
787 source = "registry+https://github.com/rust-lang/crates.io-index"872 source = "registry+https://github.com/rust-lang/crates.io-index"
788 checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"873 checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
789 dependencies = [874 dependencies = [
790- "rand_core",875+ "rand_core 0.6.4",
791 "subtle",876 "subtle",
792 ]877 ]
793 878
@@ -820,6 +905,21 @@ version = "0.1.5"
820 source = "registry+https://github.com/rust-lang/crates.io-index"905 source = "registry+https://github.com/rust-lang/crates.io-index"
821 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"906 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
822 907
908+[[package]]
909+name = "foreign-types"
910+version = "0.3.2"
911+source = "registry+https://github.com/rust-lang/crates.io-index"
912+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
913+dependencies = [
914+ "foreign-types-shared",
915+]
916+
917+[[package]]
918+name = "foreign-types-shared"
919+version = "0.1.1"
920+source = "registry+https://github.com/rust-lang/crates.io-index"
921+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
922+
823 [[package]]923 [[package]]
824 name = "form_urlencoded"924 name = "form_urlencoded"
825 version = "1.2.2"925 version = "1.2.2"
@@ -922,6 +1022,18 @@ dependencies = [
922 "wasi",1022 "wasi",
923 ]1023 ]
924 1024
1025+[[package]]
1026+name = "getrandom"
1027+version = "0.3.4"
1028+source = "registry+https://github.com/rust-lang/crates.io-index"
1029+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
1030+dependencies = [
1031+ "cfg-if",
1032+ "libc",
1033+ "r-efi 5.3.0",
1034+ "wasip2",
1035+]
1036+
925 [[package]]1037 [[package]]
926 name = "getrandom"1038 name = "getrandom"
927 version = "0.4.3"1039 version = "0.4.3"
@@ -930,7 +1042,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
930 dependencies = [1042 dependencies = [
931 "cfg-if",1043 "cfg-if",
932 "libc",1044 "libc",
933- "r-efi",1045+ "r-efi 6.0.0",
934 ]1046 ]
935 1047
936 [[package]]1048 [[package]]
@@ -946,10 +1058,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
946 checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"1058 checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
947 dependencies = [1059 dependencies = [
948 "ff",1060 "ff",
949- "rand_core",1061+ "rand_core 0.6.4",
950 "subtle",1062 "subtle",
951 ]1063 ]
952 1064
1065+[[package]]
1066+name = "half"
1067+version = "2.7.1"
1068+source = "registry+https://github.com/rust-lang/crates.io-index"
1069+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
1070+dependencies = [
1071+ "cfg-if",
1072+ "crunchy",
1073+ "zerocopy",
1074+]
1075+
953 [[package]]1076 [[package]]
954 name = "hashbrown"1077 name = "hashbrown"
955 version = "0.15.5"1078 version = "0.15.5"
@@ -1401,6 +1524,16 @@ dependencies = [
1401 "windows-sys 0.61.2",1524 "windows-sys 0.61.2",
1402 ]1525 ]
1403 1526
1527+[[package]]
1528+name = "num-bigint"
1529+version = "0.4.8"
1530+source = "registry+https://github.com/rust-lang/crates.io-index"
1531+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
1532+dependencies = [
1533+ "num-integer",
1534+ "num-traits",
1535+]
1536+
1404 [[package]]1537 [[package]]
1405 name = "num-bigint-dig"1538 name = "num-bigint-dig"
1406 version = "0.8.6"1539 version = "0.8.6"
@@ -1412,7 +1545,7 @@ dependencies = [
1412 "num-integer",1545 "num-integer",
1413 "num-iter",1546 "num-iter",
1414 "num-traits",1547 "num-traits",
1415- "rand",1548+ "rand 0.8.7",
1416 "smallvec",1549 "smallvec",
1417 "zeroize",1550 "zeroize",
1418 ]1551 ]
@@ -1452,6 +1585,15 @@ dependencies = [
1452 "libm",1585 "libm",
1453 ]1586 ]
1454 1587
1588+[[package]]
1589+name = "oid-registry"
1590+version = "0.7.1"
1591+source = "registry+https://github.com/rust-lang/crates.io-index"
1592+checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
1593+dependencies = [
1594+ "asn1-rs",
1595+]
1596+
1455 [[package]]1597 [[package]]
1456 name = "once_cell"1598 name = "once_cell"
1457 version = "1.21.4"1599 version = "1.21.4"
@@ -1470,6 +1612,43 @@ version = "0.3.1"
1470 source = "registry+https://github.com/rust-lang/crates.io-index"1612 source = "registry+https://github.com/rust-lang/crates.io-index"
1471 checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"1613 checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
1472 1614
1615+[[package]]
1616+name = "openssl"
1617+version = "0.10.81"
1618+source = "registry+https://github.com/rust-lang/crates.io-index"
1619+checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
1620+dependencies = [
1621+ "bitflags",
1622+ "cfg-if",
1623+ "foreign-types",
1624+ "libc",
1625+ "openssl-macros",
1626+ "openssl-sys",
1627+]
1628+
1629+[[package]]
1630+name = "openssl-macros"
1631+version = "0.1.1"
1632+source = "registry+https://github.com/rust-lang/crates.io-index"
1633+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
1634+dependencies = [
1635+ "proc-macro2",
1636+ "quote",
1637+ "syn 2.0.119",
1638+]
1639+
1640+[[package]]
1641+name = "openssl-sys"
1642+version = "0.9.117"
1643+source = "registry+https://github.com/rust-lang/crates.io-index"
1644+checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
1645+dependencies = [
1646+ "cc",
1647+ "libc",
1648+ "pkg-config",
1649+ "vcpkg",
1650+]
1651+
1473 [[package]]1652 [[package]]
1474 name = "p256"1653 name = "p256"
1475 version = "0.13.2"1654 version = "0.13.2"
@@ -1518,10 +1697,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1518 checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"1697 checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
1519 dependencies = [1698 dependencies = [
1520 "base64ct",1699 "base64ct",
1521- "rand_core",1700+ "rand_core 0.6.4",
1522 "subtle",1701 "subtle",
1523 ]1702 ]
1524 1703
1704+[[package]]
1705+name = "pastey"
1706+version = "0.1.1"
1707+source = "registry+https://github.com/rust-lang/crates.io-index"
1708+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
1709+
1525 [[package]]1710 [[package]]
1526 name = "pem-rfc7468"1711 name = "pem-rfc7468"
1527 version = "0.7.0"1712 version = "0.7.0"
@@ -1710,6 +1895,12 @@ dependencies = [
1710 "proc-macro2",1895 "proc-macro2",
1711 ]1896 ]
1712 1897
1898+[[package]]
1899+name = "r-efi"
1900+version = "5.3.0"
1901+source = "registry+https://github.com/rust-lang/crates.io-index"
1902+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
1903+
1713 [[package]]1904 [[package]]
1714 name = "r-efi"1905 name = "r-efi"
1715 version = "6.0.0"1906 version = "6.0.0"
@@ -1723,8 +1914,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1723 checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"1914 checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
1724 dependencies = [1915 dependencies = [
1725 "libc",1916 "libc",
1726- "rand_chacha",1917+ "rand_chacha 0.3.1",
1727- "rand_core",1918+ "rand_core 0.6.4",
1919+]
1920+
1921+[[package]]
1922+name = "rand"
1923+version = "0.9.5"
1924+source = "registry+https://github.com/rust-lang/crates.io-index"
1925+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
1926+dependencies = [
1927+ "rand_chacha 0.9.0",
1928+ "rand_core 0.9.5",
1728 ]1929 ]
1729 1930
1730 [[package]]1931 [[package]]
@@ -1734,7 +1935,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1734 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"1935 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
1735 dependencies = [1936 dependencies = [
1736 "ppv-lite86",1937 "ppv-lite86",
1737- "rand_core",1938+ "rand_core 0.6.4",
1939+]
1940+
1941+[[package]]
1942+name = "rand_chacha"
1943+version = "0.9.0"
1944+source = "registry+https://github.com/rust-lang/crates.io-index"
1945+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
1946+dependencies = [
1947+ "ppv-lite86",
1948+ "rand_core 0.9.5",
1738 ]1949 ]
1739 1950
1740 [[package]]1951 [[package]]
@@ -1746,6 +1957,15 @@ dependencies = [
1746 "getrandom 0.2.17",1957 "getrandom 0.2.17",
1747 ]1958 ]
1748 1959
1960+[[package]]
1961+name = "rand_core"
1962+version = "0.9.5"
1963+source = "registry+https://github.com/rust-lang/crates.io-index"
1964+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
1965+dependencies = [
1966+ "getrandom 0.3.4",
1967+]
1968+
1749 [[package]]1969 [[package]]
1750 name = "redox_syscall"1970 name = "redox_syscall"
1751 version = "0.5.18"1971 version = "0.5.18"
@@ -1841,7 +2061,7 @@ dependencies = [
1841 "num-traits",2061 "num-traits",
1842 "pkcs1",2062 "pkcs1",
1843 "pkcs8 0.10.2",2063 "pkcs8 0.10.2",
1844- "rand_core",2064+ "rand_core 0.6.4",
1845 "signature",2065 "signature",
1846 "spki 0.7.3",2066 "spki 0.7.3",
1847 "subtle",2067 "subtle",
@@ -1873,6 +2093,15 @@ dependencies = [
1873 "semver",2093 "semver",
1874 ]2094 ]
1875 2095
2096+[[package]]
2097+name = "rusticata-macros"
2098+version = "4.1.0"
2099+source = "registry+https://github.com/rust-lang/crates.io-index"
2100+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
2101+dependencies = [
2102+ "nom",
2103+]
2104+
1876 [[package]]2105 [[package]]
1877 name = "rustls"2106 name = "rustls"
1878 version = "0.23.43"2107 version = "0.23.43"
@@ -1956,6 +2185,16 @@ dependencies = [
1956 "serde_derive",2185 "serde_derive",
1957 ]2186 ]
1958 2187
2188+[[package]]
2189+name = "serde_cbor_2"
2190+version = "0.13.0"
2191+source = "registry+https://github.com/rust-lang/crates.io-index"
2192+checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c"
2193+dependencies = [
2194+ "half",
2195+ "serde",
2196+]
2197+
1959 [[package]]2198 [[package]]
1960 name = "serde_core"2199 name = "serde_core"
1961 version = "1.0.229"2200 version = "1.0.229"
@@ -2089,7 +2328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2089 checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"2328 checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
2090 dependencies = [2329 dependencies = [
2091 "digest 0.10.7",2330 "digest 0.10.7",
2092- "rand_core",2331+ "rand_core 0.6.4",
2093 ]2332 ]
2094 2333
2095 [[package]]2334 [[package]]
@@ -2270,7 +2509,7 @@ dependencies = [
2270 "memchr",2509 "memchr",
2271 "once_cell",2510 "once_cell",
2272 "percent-encoding",2511 "percent-encoding",
2273- "rand",2512+ "rand 0.8.7",
2274 "rsa",2513 "rsa",
2275 "serde",2514 "serde",
2276 "sha1",2515 "sha1",
@@ -2310,7 +2549,7 @@ dependencies = [
2310 "md-5",2549 "md-5",
2311 "memchr",2550 "memchr",
2312 "once_cell",2551 "once_cell",
2313- "rand",2552+ "rand 0.8.7",
2314 "serde",2553 "serde",
2315 "serde_json",2554 "serde_json",
2316 "sha2 0.10.9",2555 "sha2 0.10.9",
@@ -2709,6 +2948,7 @@ dependencies = [
2709 "idna",2948 "idna",
2710 "percent-encoding",2949 "percent-encoding",
2711 "serde",2950 "serde",
2951+ "serde_derive",
2712 ]2952 ]
2713 2953
2714 [[package]]2954 [[package]]
@@ -2759,6 +2999,15 @@ version = "0.11.1+wasi-snapshot-preview1"
2759 source = "registry+https://github.com/rust-lang/crates.io-index"2999 source = "registry+https://github.com/rust-lang/crates.io-index"
2760 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"3000 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
2761 3001
3002+[[package]]
3003+name = "wasip2"
3004+version = "1.0.4+wasi-0.2.12"
3005+source = "registry+https://github.com/rust-lang/crates.io-index"
3006+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
3007+dependencies = [
3008+ "wit-bindgen",
3009+]
3010+
2762 [[package]]3011 [[package]]
2763 name = "wasite"3012 name = "wasite"
2764 version = "0.1.0"3013 version = "0.1.0"
@@ -2810,6 +3059,74 @@ dependencies = [
2810 "unicode-ident",3059 "unicode-ident",
2811 ]3060 ]
2812 3061
3062+[[package]]
3063+name = "webauthn-attestation-ca"
3064+version = "0.5.5"
3065+source = "registry+https://github.com/rust-lang/crates.io-index"
3066+checksum = "6475c0bbd1a3f04afaa3e98880408c5be61680c5e6bd3c6f8c250990d5d3e18e"
3067+dependencies = [
3068+ "base64urlsafedata",
3069+ "openssl",
3070+ "openssl-sys",
3071+ "serde",
3072+ "tracing",
3073+ "uuid",
3074+]
3075+
3076+[[package]]
3077+name = "webauthn-rs"
3078+version = "0.5.5"
3079+source = "registry+https://github.com/rust-lang/crates.io-index"
3080+checksum = "6c548915e0e92ee946bbf2aecf01ea21bef53d974b0793cc6732ba81a03fc422"
3081+dependencies = [
3082+ "base64urlsafedata",
3083+ "serde",
3084+ "tracing",
3085+ "url",
3086+ "uuid",
3087+ "webauthn-rs-core",
3088+]
3089+
3090+[[package]]
3091+name = "webauthn-rs-core"
3092+version = "0.5.5"
3093+source = "registry+https://github.com/rust-lang/crates.io-index"
3094+checksum = "296d2d501feb715d80b8e186fb88bab1073bca17f460303a1013d17b673bea6a"
3095+dependencies = [
3096+ "base64 0.21.7",
3097+ "base64urlsafedata",
3098+ "der-parser",
3099+ "hex",
3100+ "nom",
3101+ "openssl",
3102+ "openssl-sys",
3103+ "rand 0.9.5",
3104+ "rand_chacha 0.9.0",
3105+ "serde",
3106+ "serde_cbor_2",
3107+ "serde_json",
3108+ "thiserror 1.0.69",
3109+ "tracing",
3110+ "url",
3111+ "uuid",
3112+ "webauthn-attestation-ca",
3113+ "webauthn-rs-proto",
3114+ "x509-parser",
3115+]
3116+
3117+[[package]]
3118+name = "webauthn-rs-proto"
3119+version = "0.5.5"
3120+source = "registry+https://github.com/rust-lang/crates.io-index"
3121+checksum = "c37393beac9c1ed1ca6dbb30b1e01783fb316ab3a45d90ecd48c99052dd7ef1e"
3122+dependencies = [
3123+ "base64 0.21.7",
3124+ "base64urlsafedata",
3125+ "serde",
3126+ "serde_json",
3127+ "url",
3128+]
3129+
2813 [[package]]3130 [[package]]
2814 name = "webpki-roots"3131 name = "webpki-roots"
2815 version = "0.26.11"3132 version = "0.26.11"
@@ -3063,12 +3380,35 @@ dependencies = [
3063 "memchr",3380 "memchr",
3064 ]3381 ]
3065 3382
3383+[[package]]
3384+name = "wit-bindgen"
3385+version = "0.57.1"
3386+source = "registry+https://github.com/rust-lang/crates.io-index"
3387+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
3388+
3066 [[package]]3389 [[package]]
3067 name = "writeable"3390 name = "writeable"
3068 version = "0.6.3"3391 version = "0.6.3"
3069 source = "registry+https://github.com/rust-lang/crates.io-index"3392 source = "registry+https://github.com/rust-lang/crates.io-index"
3070 checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"3393 checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
3071 3394
3395+[[package]]
3396+name = "x509-parser"
3397+version = "0.16.0"
3398+source = "registry+https://github.com/rust-lang/crates.io-index"
3399+checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
3400+dependencies = [
3401+ "asn1-rs",
3402+ "data-encoding",
3403+ "der-parser",
3404+ "lazy_static",
3405+ "nom",
3406+ "oid-registry",
3407+ "rusticata-macros",
3408+ "thiserror 1.0.69",
3409+ "time",
3410+]
3411+
3072 [[package]]3412 [[package]]
3073 name = "yoke"3413 name = "yoke"
3074 version = "0.8.3"3414 version = "0.8.3"
modified Cargo.toml +7 -1
@@ -9,8 +9,12 @@ axum = "0.8"
99 axum-extra = { version = "0.12", features = ["cookie"] }
1010 tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] }
1111 askama = "0.16"
12-sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls-ring", "postgres", "chrono", "migrate", "uuid"] }
12+sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls-ring", "postgres", "chrono", "migrate", "uuid", "json"] }
1313 uuid = { version = "1", features = ["v7", "serde"] }
14+# conditional-ui : login usernameless (discoverable credentials) ;
15+# danger-allow-state-serialisation : l'état de cérémonie part dans un cookie
16+# signé éphémère (l'app est stateless), pas dans une table.
17+webauthn-rs = { version = "0.5", features = ["conditional-ui", "danger-allow-state-serialisation"] }
1418 # NOTE : tire biscuit-quote → proc-macro-error2 (non maintenue, future-incompat
1519 # E0365) via la feature par défaut datalog-macro. On n'utilise pas ces macros,
1620 # mais biscuit-auth 6.0.0 ne compile pas en default-features = false (bug de
@@ -29,6 +33,8 @@ subtle = "2"
2933 time = "0.3"
3034 rpassword = "7"
3135 serde = { version = "1", features = ["derive"] }
36+serde_json = "1"
37+base64 = "0.22"
3238 tracing = "0.1"
3339 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
3440
@@ -9,8 +9,12 @@ axum = "0.8"
9 axum-extra = { version = "0.12", features = ["cookie"] }9 axum-extra = { version = "0.12", features = ["cookie"] }
10 tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] }10 tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] }
11 askama = "0.16"11 askama = "0.16"
12-sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls-ring", "postgres", "chrono", "migrate", "uuid"] }12+sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls-ring", "postgres", "chrono", "migrate", "uuid", "json"] }
13 uuid = { version = "1", features = ["v7", "serde"] }13 uuid = { version = "1", features = ["v7", "serde"] }
14+# conditional-ui : login usernameless (discoverable credentials) ;
15+# danger-allow-state-serialisation : l'état de cérémonie part dans un cookie
16+# signé éphémère (l'app est stateless), pas dans une table.
17+webauthn-rs = { version = "0.5", features = ["conditional-ui", "danger-allow-state-serialisation"] }
14 # NOTE : tire biscuit-quote → proc-macro-error2 (non maintenue, future-incompat18 # NOTE : tire biscuit-quote → proc-macro-error2 (non maintenue, future-incompat
15 # E0365) via la feature par défaut datalog-macro. On n'utilise pas ces macros,19 # E0365) via la feature par défaut datalog-macro. On n'utilise pas ces macros,
16 # mais biscuit-auth 6.0.0 ne compile pas en default-features = false (bug de20 # mais biscuit-auth 6.0.0 ne compile pas en default-features = false (bug de
@@ -29,6 +33,8 @@ subtle = "2"
29 time = "0.3"33 time = "0.3"
30 rpassword = "7"34 rpassword = "7"
31 serde = { version = "1", features = ["derive"] }35 serde = { version = "1", features = ["derive"] }
36+serde_json = "1"
37+base64 = "0.22"
32 tracing = "0.1"38 tracing = "0.1"
33 tracing-subscriber = { version = "0.3", features = ["env-filter"] }39 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
34 40
modified src/auth/mod.rs +1 -0
@@ -4,6 +4,7 @@ pub mod invite;
44 pub mod password;
55 pub mod revocation;
66 pub mod token;
7+pub mod webauthn;
78
89 pub const SESSION_COOKIE: &str = "tt_session";
910 /// ~6 mois.
@@ -4,6 +4,7 @@ pub mod invite;
4 pub mod password;4 pub mod password;
5 pub mod revocation;5 pub mod revocation;
6 pub mod token;6 pub mod token;
7+pub mod webauthn;
7 8
8 pub const SESSION_COOKIE: &str = "tt_session";9 pub const SESSION_COOKIE: &str = "tt_session";
9 /// ~6 mois.10 /// ~6 mois.
added src/auth/webauthn.rs +228 -0
new file mode 100644
@@ -0,0 +1,228 @@
1+//! Connexion par clé d'accès (WebAuthn/passkey) : construction du vérifieur
2+//! et scellement de l'état de cérémonie dans un cookie éphémère.
3+//!
4+//! Une cérémonie WebAuthn tient en deux requêtes : le serveur émet un
5+//! challenge, le navigateur le fait signer par l'authenticator, le serveur
6+//! vérifie. L'état entre les deux (`PasskeyRegistration` /
7+//! `DiscoverableAuthentication`) part dans un cookie **signé** plutôt qu'en
8+//! base : l'app est stateless côté sessions (biscuit + CSRF HMAC) et un
9+//! déploiement ne doit pas casser une cérémonie en cours. Le contenu n'est pas
10+//! secret — c'est exactement ce qui vient d'être envoyé au client — seule
11+//! l'intégrité compte : même recette que `csrf.rs` (HMAC-SHA256 sur une clé
12+//! dérivée de la clé privée biscuit), plus une expiration et une étanchéité
13+//! entre les deux types de cérémonie (le préfixe de domaine entre dans le MAC,
14+//! comme les faits distincts d'`invite.rs`).
15+//!
16+//! Taille : l'état ≈ challenge + politique + ~100 o par clé exclue à
17+//! l'enregistrement — très loin des ~4 Ko que tolère un cookie.
18+
19+use std::time::{SystemTime, UNIX_EPOCH};
20+
21+use anyhow::{Context, Result, bail};
22+use base64::Engine;
23+use base64::engine::general_purpose::URL_SAFE_NO_PAD;
24+use hmac::{Hmac, Mac};
25+use serde::Serialize;
26+use serde::de::DeserializeOwned;
27+use sha2::Sha256;
28+use subtle::ConstantTimeEq;
29+use uuid::Uuid;
30+use webauthn_rs::prelude::{DiscoverableAuthentication, PasskeyRegistration, Url, Webauthn};
31+use webauthn_rs::WebauthnBuilder;
32+
33+/// Cookie portant l'état d'un enregistrement de clé (utilisateur connecté).
34+pub const REG_COOKIE: &str = "tt_webauthn_reg";
35+/// Cookie portant l'état d'une connexion par clé (anonyme).
36+pub const AUTH_COOKIE: &str = "tt_webauthn_auth";
37+/// Une cérémonie a 5 minutes pour aboutir — large pour taper un PIN,
38+/// court pour un cookie qui traîne.
39+pub const CEREMONY_SECONDS: i64 = 300;
40+
41+/// rp_id WebAuthn = l'hôte de l'origine, sans schéma ni port.
42+pub fn rp_id_of(origin: &str) -> Result<String> {
43+ let url = Url::parse(origin).with_context(|| format!("WEBAUTHN_ORIGIN invalide : {origin}"))?;
44+ let host = url
45+ .host_str()
46+ .with_context(|| format!("WEBAUTHN_ORIGIN sans hôte : {origin}"))?;
47+ Ok(host.to_string())
48+}
49+
50+/// Construit le vérifieur pour une origine. Une origine invalide est une
51+/// erreur franche : mieux vaut refuser de démarrer que servir un rp_id faux
52+/// (les credentials enregistrés seraient liés au mauvais domaine).
53+pub fn build(origin: &str) -> Result<Webauthn> {
54+ let rp_id = rp_id_of(origin)?;
55+ let url = Url::parse(origin).expect("déjà validée par rp_id_of");
56+ let webauthn = WebauthnBuilder::new(&rp_id, &url)
57+ .context("configuration WebAuthn refusée")?
58+ .rp_name("Ex Ego")
59+ .build()
60+ .context("construction WebAuthn impossible")?;
61+ Ok(webauthn)
62+}
63+
64+/// État d'un enregistrement : lié au compte (la session peut changer entre
65+/// début et fin) et porteur du libellé choisi — signé avec le reste, rien à
66+/// rejouer ni à falsifier au finish.
67+#[derive(serde::Serialize, serde::Deserialize)]
68+pub struct RegCeremony {
69+ pub user_id: Uuid,
70+ pub label: String,
71+ pub state: PasskeyRegistration,
72+}
73+
74+pub fn seal_reg(key: &[u8; 32], ceremony: &RegCeremony) -> Result<String> {
75+ seal_at(key, b"reg", ceremony, expiry_now())
76+}
77+
78+pub fn unseal_reg(key: &[u8; 32], cookie: &str) -> Result<RegCeremony> {
79+ unseal_at(key, b"reg", cookie, unix_now())
80+}
81+
82+pub fn seal_auth(key: &[u8; 32], state: &DiscoverableAuthentication) -> Result<String> {
83+ seal_at(key, b"auth", state, expiry_now())
84+}
85+
86+pub fn unseal_auth(key: &[u8; 32], cookie: &str) -> Result<DiscoverableAuthentication> {
87+ unseal_at(key, b"auth", cookie, unix_now())
88+}
89+
90+// ─── mécanique ───
91+
92+#[derive(serde::Serialize, serde::Deserialize)]
93+struct Sealed<T> {
94+ exp: u64,
95+ body: T,
96+}
97+
98+fn unix_now() -> u64 {
99+ SystemTime::now()
100+ .duration_since(UNIX_EPOCH)
101+ .expect("horloge avant 1970")
102+ .as_secs()
103+}
104+
105+fn expiry_now() -> u64 {
106+ unix_now() + CEREMONY_SECONDS as u64
107+}
108+
109+/// `base64url(json({exp, body})) + "." + hex(HMAC(key, purpose ‖ "." ‖ gauche))`.
110+/// Le purpose n'est pas transmis : il entre dans le MAC, donc un cookie scellé
111+/// pour un enregistrement est illisible comme connexion (et inversement).
112+fn seal_at<T: Serialize>(key: &[u8; 32], purpose: &[u8], body: &T, exp: u64) -> Result<String> {
113+ let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&Sealed { exp, body })?);
114+ Ok(format!("{payload}.{}", tag_for(key, purpose, &payload)))
115+}
116+
117+fn unseal_at<T: DeserializeOwned>(
118+ key: &[u8; 32],
119+ purpose: &[u8],
120+ cookie: &str,
121+ now: u64,
122+) -> Result<T> {
123+ let Some((payload, tag)) = cookie.rsplit_once('.') else {
124+ bail!("cookie de cérémonie malformé");
125+ };
126+ let expected = tag_for(key, purpose, payload);
127+ if !bool::from(expected.as_bytes().ct_eq(tag.as_bytes())) {
128+ bail!("cookie de cérémonie invalide");
129+ }
130+ let sealed: Sealed<T> = serde_json::from_slice(
131+ &URL_SAFE_NO_PAD
132+ .decode(payload)
133+ .context("cookie de cérémonie illisible")?,
134+ )?;
135+ if now > sealed.exp {
136+ bail!("cérémonie expirée");
137+ }
138+ Ok(sealed.body)
139+}
140+
141+fn tag_for(key: &[u8; 32], purpose: &[u8], payload: &str) -> String {
142+ let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepte toute taille");
143+ mac.update(purpose);
144+ mac.update(b".");
145+ mac.update(payload.as_bytes());
146+ hex::encode(mac.finalize().into_bytes())
147+}
148+
149+#[cfg(test)]
150+mod tests {
151+ use super::*;
152+
153+ const KEY: [u8; 32] = [7u8; 32];
154+
155+ fn testbed() -> Webauthn {
156+ build("https://example.com").expect("origine de test valide")
157+ }
158+
159+ fn reg_ceremony() -> RegCeremony {
160+ let (_, state) = testbed()
161+ .start_passkey_registration(Uuid::now_v7(), "alex", "alex", None)
162+ .expect("challenge d'enregistrement");
163+ RegCeremony {
164+ user_id: Uuid::now_v7(),
165+ label: "Yubikey du bureau".into(),
166+ state,
167+ }
168+ }
169+
170+ #[test]
171+ fn rp_id_is_the_bare_host() {
172+ assert_eq!(rp_id_of("https://ex-ego.durillon.family").unwrap(), "ex-ego.durillon.family");
173+ assert_eq!(rp_id_of("http://localhost:8090").unwrap(), "localhost");
174+ assert!(rp_id_of("pas une url").is_err());
175+ assert!(rp_id_of("mailto:x@y.z").is_err(), "URL sans hôte refusée");
176+ }
177+
178+ #[test]
179+ fn reg_roundtrip() {
180+ let c = reg_ceremony();
181+ let cookie = seal_reg(&KEY, &c).unwrap();
182+ let back = unseal_reg(&KEY, &cookie).unwrap();
183+ assert_eq!(back.user_id, c.user_id);
184+ assert_eq!(back.label, c.label);
185+ }
186+
187+ #[test]
188+ fn auth_roundtrip() {
189+ let (_, state) = testbed()
190+ .start_discoverable_authentication()
191+ .expect("challenge de connexion");
192+ let cookie = seal_auth(&KEY, &state).unwrap();
193+ assert!(unseal_auth(&KEY, &cookie).is_ok());
194+ }
195+
196+ /// Un cookie d'enregistrement ne vaut rien comme connexion, et
197+ /// réciproquement — même garantie que les faits distincts d'invite.rs.
198+ #[test]
199+ fn purposes_are_sealed_off() {
200+ let cookie = seal_reg(&KEY, &reg_ceremony()).unwrap();
201+ assert!(unseal_auth(&KEY, &cookie).is_err());
202+
203+ let (_, state) = testbed().start_discoverable_authentication().unwrap();
204+ let cookie = seal_auth(&KEY, &state).unwrap();
205+ assert!(unseal_reg(&KEY, &cookie).is_err());
206+ }
207+
208+ #[test]
209+ fn expiry_tampering_and_wrong_key_are_refused() {
210+ let c = reg_ceremony();
211+
212+ // Expirée : scellée avec une échéance déjà passée.
213+ let stale = seal_at(&KEY, b"reg", &c, unix_now() - 1).unwrap();
214+ assert!(unseal_at::<RegCeremony>(&KEY, b"reg", &stale, unix_now()).is_err());
215+
216+ let cookie = seal_reg(&KEY, &c).unwrap();
217+ // Charge trafiquée : le MAC ne colle plus.
218+ let mut forged = format!("A{}", &cookie[1..]);
219+ if forged == cookie {
220+ forged = format!("B{}", &cookie[1..]);
221+ }
222+ assert!(unseal_reg(&KEY, &forged).is_err());
223+ // Autre clé serveur.
224+ assert!(unseal_reg(&[8u8; 32], &cookie).is_err());
225+ // Pas de séparateur.
226+ assert!(unseal_reg(&KEY, "nimportequoi").is_err());
227+ }
228+}
new file mode 100644
@@ -0,0 +1,228 @@
1+//! Connexion par clé d'accès (WebAuthn/passkey) : construction du vérifieur
2+//! et scellement de l'état de cérémonie dans un cookie éphémère.
3+//!
4+//! Une cérémonie WebAuthn tient en deux requêtes : le serveur émet un
5+//! challenge, le navigateur le fait signer par l'authenticator, le serveur
6+//! vérifie. L'état entre les deux (`PasskeyRegistration` /
7+//! `DiscoverableAuthentication`) part dans un cookie **signé** plutôt qu'en
8+//! base : l'app est stateless côté sessions (biscuit + CSRF HMAC) et un
9+//! déploiement ne doit pas casser une cérémonie en cours. Le contenu n'est pas
10+//! secret — c'est exactement ce qui vient d'être envoyé au client — seule
11+//! l'intégrité compte : même recette que `csrf.rs` (HMAC-SHA256 sur une clé
12+//! dérivée de la clé privée biscuit), plus une expiration et une étanchéité
13+//! entre les deux types de cérémonie (le préfixe de domaine entre dans le MAC,
14+//! comme les faits distincts d'`invite.rs`).
15+//!
16+//! Taille : l'état ≈ challenge + politique + ~100 o par clé exclue à
17+//! l'enregistrement — très loin des ~4 Ko que tolère un cookie.
18+
19+use std::time::{SystemTime, UNIX_EPOCH};
20+
21+use anyhow::{Context, Result, bail};
22+use base64::Engine;
23+use base64::engine::general_purpose::URL_SAFE_NO_PAD;
24+use hmac::{Hmac, Mac};
25+use serde::Serialize;
26+use serde::de::DeserializeOwned;
27+use sha2::Sha256;
28+use subtle::ConstantTimeEq;
29+use uuid::Uuid;
30+use webauthn_rs::prelude::{DiscoverableAuthentication, PasskeyRegistration, Url, Webauthn};
31+use webauthn_rs::WebauthnBuilder;
32+
33+/// Cookie portant l'état d'un enregistrement de clé (utilisateur connecté).
34+pub const REG_COOKIE: &str = "tt_webauthn_reg";
35+/// Cookie portant l'état d'une connexion par clé (anonyme).
36+pub const AUTH_COOKIE: &str = "tt_webauthn_auth";
37+/// Une cérémonie a 5 minutes pour aboutir — large pour taper un PIN,
38+/// court pour un cookie qui traîne.
39+pub const CEREMONY_SECONDS: i64 = 300;
40+
41+/// rp_id WebAuthn = l'hôte de l'origine, sans schéma ni port.
42+pub fn rp_id_of(origin: &str) -> Result<String> {
43+ let url = Url::parse(origin).with_context(|| format!("WEBAUTHN_ORIGIN invalide : {origin}"))?;
44+ let host = url
45+ .host_str()
46+ .with_context(|| format!("WEBAUTHN_ORIGIN sans hôte : {origin}"))?;
47+ Ok(host.to_string())
48+}
49+
50+/// Construit le vérifieur pour une origine. Une origine invalide est une
51+/// erreur franche : mieux vaut refuser de démarrer que servir un rp_id faux
52+/// (les credentials enregistrés seraient liés au mauvais domaine).
53+pub fn build(origin: &str) -> Result<Webauthn> {
54+ let rp_id = rp_id_of(origin)?;
55+ let url = Url::parse(origin).expect("déjà validée par rp_id_of");
56+ let webauthn = WebauthnBuilder::new(&rp_id, &url)
57+ .context("configuration WebAuthn refusée")?
58+ .rp_name("Ex Ego")
59+ .build()
60+ .context("construction WebAuthn impossible")?;
61+ Ok(webauthn)
62+}
63+
64+/// État d'un enregistrement : lié au compte (la session peut changer entre
65+/// début et fin) et porteur du libellé choisi — signé avec le reste, rien à
66+/// rejouer ni à falsifier au finish.
67+#[derive(serde::Serialize, serde::Deserialize)]
68+pub struct RegCeremony {
69+ pub user_id: Uuid,
70+ pub label: String,
71+ pub state: PasskeyRegistration,
72+}
73+
74+pub fn seal_reg(key: &[u8; 32], ceremony: &RegCeremony) -> Result<String> {
75+ seal_at(key, b"reg", ceremony, expiry_now())
76+}
77+
78+pub fn unseal_reg(key: &[u8; 32], cookie: &str) -> Result<RegCeremony> {
79+ unseal_at(key, b"reg", cookie, unix_now())
80+}
81+
82+pub fn seal_auth(key: &[u8; 32], state: &DiscoverableAuthentication) -> Result<String> {
83+ seal_at(key, b"auth", state, expiry_now())
84+}
85+
86+pub fn unseal_auth(key: &[u8; 32], cookie: &str) -> Result<DiscoverableAuthentication> {
87+ unseal_at(key, b"auth", cookie, unix_now())
88+}
89+
90+// ─── mécanique ───
91+
92+#[derive(serde::Serialize, serde::Deserialize)]
93+struct Sealed<T> {
94+ exp: u64,
95+ body: T,
96+}
97+
98+fn unix_now() -> u64 {
99+ SystemTime::now()
100+ .duration_since(UNIX_EPOCH)
101+ .expect("horloge avant 1970")
102+ .as_secs()
103+}
104+
105+fn expiry_now() -> u64 {
106+ unix_now() + CEREMONY_SECONDS as u64
107+}
108+
109+/// `base64url(json({exp, body})) + "." + hex(HMAC(key, purpose ‖ "." ‖ gauche))`.
110+/// Le purpose n'est pas transmis : il entre dans le MAC, donc un cookie scellé
111+/// pour un enregistrement est illisible comme connexion (et inversement).
112+fn seal_at<T: Serialize>(key: &[u8; 32], purpose: &[u8], body: &T, exp: u64) -> Result<String> {
113+ let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&Sealed { exp, body })?);
114+ Ok(format!("{payload}.{}", tag_for(key, purpose, &payload)))
115+}
116+
117+fn unseal_at<T: DeserializeOwned>(
118+ key: &[u8; 32],
119+ purpose: &[u8],
120+ cookie: &str,
121+ now: u64,
122+) -> Result<T> {
123+ let Some((payload, tag)) = cookie.rsplit_once('.') else {
124+ bail!("cookie de cérémonie malformé");
125+ };
126+ let expected = tag_for(key, purpose, payload);
127+ if !bool::from(expected.as_bytes().ct_eq(tag.as_bytes())) {
128+ bail!("cookie de cérémonie invalide");
129+ }
130+ let sealed: Sealed<T> = serde_json::from_slice(
131+ &URL_SAFE_NO_PAD
132+ .decode(payload)
133+ .context("cookie de cérémonie illisible")?,
134+ )?;
135+ if now > sealed.exp {
136+ bail!("cérémonie expirée");
137+ }
138+ Ok(sealed.body)
139+}
140+
141+fn tag_for(key: &[u8; 32], purpose: &[u8], payload: &str) -> String {
142+ let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepte toute taille");
143+ mac.update(purpose);
144+ mac.update(b".");
145+ mac.update(payload.as_bytes());
146+ hex::encode(mac.finalize().into_bytes())
147+}
148+
149+#[cfg(test)]
150+mod tests {
151+ use super::*;
152+
153+ const KEY: [u8; 32] = [7u8; 32];
154+
155+ fn testbed() -> Webauthn {
156+ build("https://example.com").expect("origine de test valide")
157+ }
158+
159+ fn reg_ceremony() -> RegCeremony {
160+ let (_, state) = testbed()
161+ .start_passkey_registration(Uuid::now_v7(), "alex", "alex", None)
162+ .expect("challenge d'enregistrement");
163+ RegCeremony {
164+ user_id: Uuid::now_v7(),
165+ label: "Yubikey du bureau".into(),
166+ state,
167+ }
168+ }
169+
170+ #[test]
171+ fn rp_id_is_the_bare_host() {
172+ assert_eq!(rp_id_of("https://ex-ego.durillon.family").unwrap(), "ex-ego.durillon.family");
173+ assert_eq!(rp_id_of("http://localhost:8090").unwrap(), "localhost");
174+ assert!(rp_id_of("pas une url").is_err());
175+ assert!(rp_id_of("mailto:x@y.z").is_err(), "URL sans hôte refusée");
176+ }
177+
178+ #[test]
179+ fn reg_roundtrip() {
180+ let c = reg_ceremony();
181+ let cookie = seal_reg(&KEY, &c).unwrap();
182+ let back = unseal_reg(&KEY, &cookie).unwrap();
183+ assert_eq!(back.user_id, c.user_id);
184+ assert_eq!(back.label, c.label);
185+ }
186+
187+ #[test]
188+ fn auth_roundtrip() {
189+ let (_, state) = testbed()
190+ .start_discoverable_authentication()
191+ .expect("challenge de connexion");
192+ let cookie = seal_auth(&KEY, &state).unwrap();
193+ assert!(unseal_auth(&KEY, &cookie).is_ok());
194+ }
195+
196+ /// Un cookie d'enregistrement ne vaut rien comme connexion, et
197+ /// réciproquement — même garantie que les faits distincts d'invite.rs.
198+ #[test]
199+ fn purposes_are_sealed_off() {
200+ let cookie = seal_reg(&KEY, &reg_ceremony()).unwrap();
201+ assert!(unseal_auth(&KEY, &cookie).is_err());
202+
203+ let (_, state) = testbed().start_discoverable_authentication().unwrap();
204+ let cookie = seal_auth(&KEY, &state).unwrap();
205+ assert!(unseal_reg(&KEY, &cookie).is_err());
206+ }
207+
208+ #[test]
209+ fn expiry_tampering_and_wrong_key_are_refused() {
210+ let c = reg_ceremony();
211+
212+ // Expirée : scellée avec une échéance déjà passée.
213+ let stale = seal_at(&KEY, b"reg", &c, unix_now() - 1).unwrap();
214+ assert!(unseal_at::<RegCeremony>(&KEY, b"reg", &stale, unix_now()).is_err());
215+
216+ let cookie = seal_reg(&KEY, &c).unwrap();
217+ // Charge trafiquée : le MAC ne colle plus.
218+ let mut forged = format!("A{}", &cookie[1..]);
219+ if forged == cookie {
220+ forged = format!("B{}", &cookie[1..]);
221+ }
222+ assert!(unseal_reg(&KEY, &forged).is_err());
223+ // Autre clé serveur.
224+ assert!(unseal_reg(&[8u8; 32], &cookie).is_err());
225+ // Pas de séparateur.
226+ assert!(unseal_reg(&KEY, "nimportequoi").is_err());
227+ }
228+}
modified src/config.rs +14 -0
@@ -46,6 +46,20 @@ pub fn signup_key() -> Option<String> {
4646 .filter(|k| !k.is_empty())
4747 }
4848
49+/// Origine WebAuthn explicite (`WEBAUTHN_ORIGIN`, ex.
50+/// `https://ex-ego.durillon.family` ; `http://localhost:8090` en local).
51+/// `None` = variable absente = **connexion par clé d'accès éteinte partout**
52+/// (bouton masqué, endpoints en 404), sur le modèle de `SIGNUP_KEY`.
53+///
54+/// Volontairement PAS `public_origin()` : le rp_id lie cryptographiquement les
55+/// credentials au domaine, il ne doit jamais dériver d'en-têtes forgeables.
56+pub fn webauthn_origin() -> Option<String> {
57+ std::env::var("WEBAUTHN_ORIGIN")
58+ .ok()
59+ .map(|o| o.trim().trim_end_matches('/').to_string())
60+ .filter(|o| !o.is_empty())
61+}
62+
4963 /// Commit déployé — `COMMIT_ID`, posée par Clever Cloud dans l'environnement
5064 /// de l'app. Tronqué à 8 caractères ; « dev » en local (variable absente).
5165 pub fn commit_id() -> String {
@@ -46,6 +46,20 @@ pub fn signup_key() -> Option<String> {
46 .filter(|k| !k.is_empty())46 .filter(|k| !k.is_empty())
47 }47 }
48 48
49+/// Origine WebAuthn explicite (`WEBAUTHN_ORIGIN`, ex.
50+/// `https://ex-ego.durillon.family` ; `http://localhost:8090` en local).
51+/// `None` = variable absente = **connexion par clé d'accès éteinte partout**
52+/// (bouton masqué, endpoints en 404), sur le modèle de `SIGNUP_KEY`.
53+///
54+/// Volontairement PAS `public_origin()` : le rp_id lie cryptographiquement les
55+/// credentials au domaine, il ne doit jamais dériver d'en-têtes forgeables.
56+pub fn webauthn_origin() -> Option<String> {
57+ std::env::var("WEBAUTHN_ORIGIN")
58+ .ok()
59+ .map(|o| o.trim().trim_end_matches('/').to_string())
60+ .filter(|o| !o.is_empty())
61+}
62+
49 /// Commit déployé — `COMMIT_ID`, posée par Clever Cloud dans l'environnement63 /// Commit déployé — `COMMIT_ID`, posée par Clever Cloud dans l'environnement
50 /// de l'app. Tronqué à 8 caractères ; « dev » en local (variable absente).64 /// de l'app. Tronqué à 8 caractères ; « dev » en local (variable absente).
51 pub fn commit_id() -> String {65 pub fn commit_id() -> String {
modified src/ratelimit.rs +5 -0
@@ -52,6 +52,11 @@ impl Quota {
5252 Self::new(10, DAY)
5353 }
5454
55+ /// 10 connexions par clé d'accès échouées par heure.
56+ pub fn webauthn_fails() -> Self {
57+ Self::new(10, HOUR)
58+ }
59+
5560 /// L'IP a-t-elle encore du quota ? N'enregistre rien.
5661 pub fn allows(&self, ip: &str) -> bool {
5762 self.allows_at(ip, Instant::now())
@@ -52,6 +52,11 @@ impl Quota {
52 Self::new(10, DAY)52 Self::new(10, DAY)
53 }53 }
54 54
55+ /// 10 connexions par clé d'accès échouées par heure.
56+ pub fn webauthn_fails() -> Self {
57+ Self::new(10, HOUR)
58+ }
59+
55 /// L'IP a-t-elle encore du quota ? N'enregistre rien.60 /// L'IP a-t-elle encore du quota ? N'enregistre rien.
56 pub fn allows(&self, ip: &str) -> bool {61 pub fn allows(&self, ip: &str) -> bool {
57 self.allows_at(ip, Instant::now())62 self.allows_at(ip, Instant::now())
modified src/state.rs +25 -0
@@ -5,6 +5,8 @@ use biscuit_auth::{KeyPair, PrivateKey, PublicKey, builder::Algorithm};
55 use sha2::{Digest, Sha256};
66 use sqlx::PgPool;
77
8+use webauthn_rs::prelude::Webauthn;
9+
810 use crate::auth::revocation::RevocationCache;
911 use crate::ratelimit::Quota;
1012
@@ -19,6 +21,13 @@ pub struct AppState {
1921 pub signups: Quota,
2022 /// Clés d'inscription fausses par IP (10/jour) — frein à la force brute.
2123 pub bad_keys: Quota,
24+ /// Vérifieur WebAuthn — `None` = `WEBAUTHN_ORIGIN` absente = connexion
25+ /// par clé d'accès éteinte (motif `SIGNUP_KEY`).
26+ pub webauthn: Option<Arc<Webauthn>>,
27+ /// Clé de scellement des cookies de cérémonie WebAuthn (dérivée comme `csrf_key`).
28+ pub webauthn_key: [u8; 32],
29+ /// Connexions par clé échouées par IP (10/h) — frein au rejeu de credentials.
30+ pub webauthn_fails: Quota,
2231 }
2332
2433 impl AppState {
@@ -33,6 +42,19 @@ impl AppState {
3342 hasher.update(private.to_bytes().as_slice());
3443 let csrf_key: [u8; 32] = hasher.finalize().into();
3544
45+ // Même recette, autre domaine : la clé des cookies de cérémonie WebAuthn.
46+ let mut hasher = Sha256::new();
47+ hasher.update(b"webauthn");
48+ hasher.update(private.to_bytes().as_slice());
49+ let webauthn_key: [u8; 32] = hasher.finalize().into();
50+
51+ // Origine absente = feature éteinte ; origine invalide = refus de
52+ // démarrer (une misconfiguration doit se voir, pas s'ignorer).
53+ let webauthn = match crate::config::webauthn_origin() {
54+ Some(origin) => Some(Arc::new(crate::auth::webauthn::build(&origin)?)),
55+ None => None,
56+ };
57+
3658 Ok(Self {
3759 pool,
3860 root: Arc::new(root),
@@ -41,6 +63,9 @@ impl AppState {
4163 revocations: RevocationCache::default(),
4264 signups: Quota::signups(),
4365 bad_keys: Quota::bad_keys(),
66+ webauthn,
67+ webauthn_key,
68+ webauthn_fails: Quota::webauthn_fails(),
4469 })
4570 }
4671 }
@@ -5,6 +5,8 @@ use biscuit_auth::{KeyPair, PrivateKey, PublicKey, builder::Algorithm};
5 use sha2::{Digest, Sha256};5 use sha2::{Digest, Sha256};
6 use sqlx::PgPool;6 use sqlx::PgPool;
7 7
8+use webauthn_rs::prelude::Webauthn;
9+
8 use crate::auth::revocation::RevocationCache;10 use crate::auth::revocation::RevocationCache;
9 use crate::ratelimit::Quota;11 use crate::ratelimit::Quota;
10 12
@@ -19,6 +21,13 @@ pub struct AppState {
19 pub signups: Quota,21 pub signups: Quota,
20 /// Clés d'inscription fausses par IP (10/jour) — frein à la force brute.22 /// Clés d'inscription fausses par IP (10/jour) — frein à la force brute.
21 pub bad_keys: Quota,23 pub bad_keys: Quota,
24+ /// Vérifieur WebAuthn — `None` = `WEBAUTHN_ORIGIN` absente = connexion
25+ /// par clé d'accès éteinte (motif `SIGNUP_KEY`).
26+ pub webauthn: Option<Arc<Webauthn>>,
27+ /// Clé de scellement des cookies de cérémonie WebAuthn (dérivée comme `csrf_key`).
28+ pub webauthn_key: [u8; 32],
29+ /// Connexions par clé échouées par IP (10/h) — frein au rejeu de credentials.
30+ pub webauthn_fails: Quota,
22 }31 }
23 32
24 impl AppState {33 impl AppState {
@@ -33,6 +42,19 @@ impl AppState {
33 hasher.update(private.to_bytes().as_slice());42 hasher.update(private.to_bytes().as_slice());
34 let csrf_key: [u8; 32] = hasher.finalize().into();43 let csrf_key: [u8; 32] = hasher.finalize().into();
35 44
45+ // Même recette, autre domaine : la clé des cookies de cérémonie WebAuthn.
46+ let mut hasher = Sha256::new();
47+ hasher.update(b"webauthn");
48+ hasher.update(private.to_bytes().as_slice());
49+ let webauthn_key: [u8; 32] = hasher.finalize().into();
50+
51+ // Origine absente = feature éteinte ; origine invalide = refus de
52+ // démarrer (une misconfiguration doit se voir, pas s'ignorer).
53+ let webauthn = match crate::config::webauthn_origin() {
54+ Some(origin) => Some(Arc::new(crate::auth::webauthn::build(&origin)?)),
55+ None => None,
56+ };
57+
36 Ok(Self {58 Ok(Self {
37 pool,59 pool,
38 root: Arc::new(root),60 root: Arc::new(root),
@@ -41,6 +63,9 @@ impl AppState {
41 revocations: RevocationCache::default(),63 revocations: RevocationCache::default(),
42 signups: Quota::signups(),64 signups: Quota::signups(),
43 bad_keys: Quota::bad_keys(),65 bad_keys: Quota::bad_keys(),
66+ webauthn,
67+ webauthn_key,
68+ webauthn_fails: Quota::webauthn_fails(),
44 })69 })
45 }70 }
46 }71 }