pwn 0.5.783 → 0.5.785

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +8 -8
  4. data/README.md +37 -0
  5. data/Rakefile +19 -1
  6. data/bin/pwn +2 -1
  7. data/bin/pwn_setup +1 -1
  8. data/bin/pwn_web_cache_deception +35 -7
  9. data/build_gem.sh +1 -1
  10. data/documentation/AI-Integration.md +7 -5
  11. data/documentation/Agent-Tool-Registry.md +2 -2
  12. data/documentation/Backup-Retention.md +82 -0
  13. data/documentation/Configuration.md +10 -6
  14. data/documentation/Contributing.md +1 -1
  15. data/documentation/Cron.md +64 -35
  16. data/documentation/Diagrams.md +2 -2
  17. data/documentation/General-PWN-Usage.md +4 -3
  18. data/documentation/Home.md +6 -5
  19. data/documentation/How-PWN-Works.md +7 -6
  20. data/documentation/Installation.md +254 -10
  21. data/documentation/Mission-Cancellation.md +134 -0
  22. data/documentation/Operability.md +17 -0
  23. data/documentation/Persistence.md +8 -6
  24. data/documentation/Plugin-Degradation.md +12 -0
  25. data/documentation/Plugins.md +3 -2
  26. data/documentation/Reinforcement-Learning.md +5 -6
  27. data/documentation/Runtime-Source-Protection.md +162 -0
  28. data/documentation/Transparent-Browser.md +11 -0
  29. data/documentation/Troubleshooting.md +3 -2
  30. data/documentation/What-is-PWN.md +2 -2
  31. data/documentation/YubiKey-Vaults.md +183 -0
  32. data/documentation/YubiKey.md +339 -0
  33. data/documentation/diagrams/cron-scheduling.svg +130 -128
  34. data/documentation/diagrams/dot/cron-scheduling.dot +5 -5
  35. data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
  36. data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
  37. data/documentation/diagrams/persistence-filesystem.svg +172 -142
  38. data/documentation/diagrams/plugin-ecosystem.svg +64 -64
  39. data/documentation/pwn-ai-Agent.md +10 -4
  40. data/etc/default_cron/session_cleanup.rb +1245 -22
  41. data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
  42. data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
  43. data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
  44. data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
  45. data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
  46. data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
  47. data/etc/default_skills/pwn/backups/SKILL.md +54 -0
  48. data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
  49. data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
  50. data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
  51. data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
  52. data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
  53. data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
  54. data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
  55. data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
  56. data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
  57. data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
  58. data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
  59. data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
  60. data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
  61. data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
  62. data/lib/pwn/ai/agent/dispatch.rb +26 -15
  63. data/lib/pwn/ai/agent/extrospection.rb +6 -7
  64. data/lib/pwn/ai/agent/learning.rb +62 -25
  65. data/lib/pwn/ai/agent/loop.rb +17 -1
  66. data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
  67. data/lib/pwn/ai/agent/policy.rb +5 -0
  68. data/lib/pwn/ai/agent/policy_evaluation.rb +8 -8
  69. data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
  70. data/lib/pwn/ai/agent/request_runtime.rb +122 -18
  71. data/lib/pwn/ai/agent/reward.rb +5 -3
  72. data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
  73. data/lib/pwn/ai/agent/skill_review.rb +9 -5
  74. data/lib/pwn/ai/agent/swarm.rb +35 -16
  75. data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
  76. data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
  77. data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
  78. data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
  79. data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
  80. data/lib/pwn/ai/agent.rb +1 -0
  81. data/lib/pwn/ai/anthropic.rb +8 -33
  82. data/lib/pwn/ai/cli.rb +34 -5
  83. data/lib/pwn/ai/gemini.rb +1 -0
  84. data/lib/pwn/ai/grok.rb +39 -28
  85. data/lib/pwn/ai/ollama.rb +4 -2
  86. data/lib/pwn/ai/open_ai.rb +11 -32
  87. data/lib/pwn/ai/open_web_ui.rb +62 -4
  88. data/lib/pwn/ai/router.rb +53 -3
  89. data/lib/pwn/ai/server_tools.rb +231 -0
  90. data/lib/pwn/ai.rb +1 -0
  91. data/lib/pwn/backups.rb +347 -0
  92. data/lib/pwn/config.rb +70 -39
  93. data/lib/pwn/cron.rb +120 -49
  94. data/lib/pwn/migrate.rb +150 -68
  95. data/lib/pwn/module_skills.rb +7 -0
  96. data/lib/pwn/plugins/ai_sandbox.rb +538 -1
  97. data/lib/pwn/plugins/authentication_helper.rb +74 -4
  98. data/lib/pwn/plugins/basic_auth.rb +2 -1
  99. data/lib/pwn/plugins/binary_parser.rb +5 -4
  100. data/lib/pwn/plugins/ghidra.rb +4 -3
  101. data/lib/pwn/plugins/google_workspace.rb +4 -21
  102. data/lib/pwn/plugins/jobs.rb +1 -1
  103. data/lib/pwn/plugins/log.rb +21 -3
  104. data/lib/pwn/plugins/preflight_checker.rb +34 -9
  105. data/lib/pwn/plugins/recon.rb +4 -1
  106. data/lib/pwn/plugins/repl/ai.rb +305 -27
  107. data/lib/pwn/plugins/repl/ai_console.rb +273 -89
  108. data/lib/pwn/plugins/repl/ai_swarm.rb +181 -62
  109. data/lib/pwn/plugins/repl/mesh.rb +3 -16
  110. data/lib/pwn/plugins/repl/vault.rb +4 -15
  111. data/lib/pwn/plugins/repl.rb +1 -216
  112. data/lib/pwn/plugins/sandbox/driver.rb +4 -0
  113. data/lib/pwn/plugins/transparent_browser.rb +215 -0
  114. data/lib/pwn/plugins/vault.rb +412 -135
  115. data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
  116. data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
  117. data/lib/pwn/plugins/yubikey/management.rb +686 -0
  118. data/lib/pwn/plugins/yubikey/oath.rb +766 -0
  119. data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
  120. data/lib/pwn/plugins/yubikey/otp.rb +897 -0
  121. data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
  122. data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
  123. data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
  124. data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
  125. data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
  126. data/lib/pwn/plugins/yubikey.rb +844 -0
  127. data/lib/pwn/plugins.rb +1 -0
  128. data/lib/pwn/redaction.rb +69 -12
  129. data/lib/pwn/session_trace.rb +4 -0
  130. data/lib/pwn/sessions.rb +58 -16
  131. data/lib/pwn/setup.rb +123 -41
  132. data/lib/pwn/version.rb +1 -1
  133. data/lib/pwn/www/app_cobalt_io.rb +16 -5
  134. data/lib/pwn/www/bug_crowd.rb +16 -5
  135. data/lib/pwn/www/coinbase_pro.rb +16 -5
  136. data/lib/pwn/www/github.rb +30 -14
  137. data/lib/pwn/www/paypal.rb +15 -4
  138. data/lib/pwn/www/synack.rb +16 -5
  139. data/lib/pwn/www/twitter.rb +17 -6
  140. data/lib/pwn.rb +1 -0
  141. data/scripts/build_gate.rb +58 -0
  142. data/spec/documentation/installation_md_spec.rb +1 -1
  143. data/spec/fixtures/yubikey/apdu_frames.json +162 -0
  144. data/spec/integration/authentication_helper_oath_spec.rb +117 -0
  145. data/spec/integration/build_gate_spec.rb +73 -0
  146. data/spec/integration/controller_spill_spec.rb +48 -0
  147. data/spec/integration/harness_process_spec.rb +97 -0
  148. data/spec/integration/mission_cancel_spec.rb +79 -0
  149. data/spec/integration/mission_curses_pty_spec.rb +44 -0
  150. data/spec/integration/persistence_roundtrip_spec.rb +1 -7
  151. data/spec/integration/readme_security_spec.rb +47 -0
  152. data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
  153. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
  154. data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
  155. data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
  156. data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
  157. data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
  158. data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
  159. data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
  160. data/spec/lib/pwn/ai/agent/policy_spec.rb +27 -0
  161. data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
  162. data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
  163. data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
  164. data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
  165. data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
  166. data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
  167. data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
  168. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
  169. data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
  170. data/spec/lib/pwn/ai/cli_spec.rb +6 -9
  171. data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
  172. data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
  173. data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
  174. data/spec/lib/pwn/ai/router_spec.rb +31 -3
  175. data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
  176. data/spec/lib/pwn/backups_spec.rb +114 -0
  177. data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
  178. data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
  179. data/spec/lib/pwn/cron_spec.rb +113 -6
  180. data/spec/lib/pwn/migrate_spec.rb +108 -5
  181. data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
  182. data/spec/lib/pwn/module_skills_spec.rb +8 -0
  183. data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
  184. data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
  185. data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
  186. data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
  187. data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
  188. data/spec/lib/pwn/plugins/log_spec.rb +7 -0
  189. data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
  190. data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
  191. data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
  192. data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
  193. data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +125 -78
  194. data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
  195. data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +30 -16
  196. data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
  197. data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
  198. data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
  199. data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
  200. data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
  201. data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
  202. data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
  203. data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
  204. data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
  205. data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
  206. data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
  207. data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
  208. data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
  209. data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
  210. data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
  211. data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
  212. data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
  213. data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
  214. data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
  215. data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
  216. data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
  217. data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
  218. data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
  219. data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
  220. data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
  221. data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
  222. data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
  223. data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
  224. data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
  225. data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
  226. data/spec/lib/pwn/redaction_spec.rb +13 -10
  227. data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
  228. data/spec/lib/pwn/session_cleanup_concurrency_spec.rb +201 -0
  229. data/spec/lib/pwn/session_cleanup_default_spec.rb +167 -2
  230. data/spec/lib/pwn/session_cleanup_legacy_spec.rb +101 -0
  231. data/spec/lib/pwn/session_cleanup_payload_spec.rb +335 -2
  232. data/spec/lib/pwn/session_cleanup_recovery_spec.rb +403 -0
  233. data/spec/lib/pwn/sessions_spec.rb +112 -0
  234. data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
  235. data/spec/lib/pwn/setup_spec.rb +3 -1
  236. data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
  237. data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
  238. data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
  239. data/spec/spec_helper.rb +15 -0
  240. data/spec/support/gate_failure_formatter.rb +14 -0
  241. data/spec/support/harness/cli_faults.rb +13 -9
  242. data/spec/support/harness/controller_spill.rb +26 -0
  243. data/spec/support/harness/controller_state.rb +54 -0
  244. data/spec/support/harness/evaluate.rb +12 -1
  245. data/spec/support/harness/experiments.rb +1 -1
  246. data/spec/support/harness/fake_provider.rb +22 -6
  247. data/spec/support/harness/fault_preload.rb +3 -1
  248. data/spec/support/harness/mission_console.rb +54 -0
  249. data/spec/support/harness/mission_curses_pty.rb +41 -0
  250. data/spec/support/harness/mission_dispatch.rb +30 -0
  251. data/spec/support/harness/mission_fixture_preload.rb +24 -0
  252. data/spec/support/harness/mission_job_lifetime.rb +25 -0
  253. data/spec/support/harness/mission_legacy.rb +25 -0
  254. data/spec/support/harness/mission_normal_exit.rb +34 -0
  255. data/spec/support/harness/mission_swarm.rb +23 -0
  256. data/spec/support/harness/mission_unsupported.rb +29 -0
  257. data/spec/support/harness/package_acceptance.rb +33 -5
  258. data/spec/support/harness/session_acceptance.rb +4 -4
  259. data/spec/support/harness/swarm_acceptance.rb +1 -1
  260. data/spec/support/harness/swarm_control.rb +44 -0
  261. data/spec/support/mission_namespace.rb +14 -0
  262. data/spec/support/mission_pipe_queue.rb +33 -0
  263. data/third_party/pwn_rdoc.jsonl +599 -15
  264. metadata +140 -17
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'pwn/plugins/yubikey/security_domain'
5
+ require_relative 'security_domain/support'
6
+
7
+ class SDCard
8
+ attr_reader :commands
9
+
10
+ def initialize(replies)
11
+ @commands = []
12
+ @replies = replies
13
+ end
14
+
15
+ def descriptor
16
+ { protocol: :apdu }
17
+ end
18
+
19
+ def exchange(opts)
20
+ @commands << opts.fetch(:data)
21
+ raise 'unexpected APDU' if @replies.empty?
22
+
23
+ @replies.shift
24
+ end
25
+
26
+ def close
27
+ :closed
28
+ end
29
+ end
30
+
31
+ RSpec.describe PWN::Plugins::YubiKey::SecurityDomain do
32
+ def hex(string)
33
+ [string].pack('H*')
34
+ end
35
+
36
+ def authenticated_card
37
+ card = Scp03Card.new
38
+ key = (0...16).to_a.pack('C*')
39
+ connection = described_class.secure_connection(connection: card, scp: :scp03, kvn: 1, key_enc: key, key_mac: key, key_dek: key, confirm: true)
40
+ [card, connection, key]
41
+ end
42
+
43
+ it 'imports SCP03 keys in memory with DEK wrapping and verifies all key check values' do
44
+ card, connection, dek = authenticated_card
45
+ keys = %w[A B C].map { |char| char.b * 16 }
46
+ crypto = ReferenceCrypto.new
47
+ card.handler = lambda do |header, data|
48
+ expect(header).to eq(hex('84d80281'))
49
+ expect(data.getbyte(0)).to eq(7)
50
+ offset = 1
51
+ checks = keys.map do |key|
52
+ expect(data.byteslice(offset, 2)).to eq(hex('8810'))
53
+ expect(crypto.aes(dek, data.byteslice(offset + 2, 16), "\0".b * 16, decrypt: true)).to eq(key)
54
+ check = crypto.aes(key, "\1".b * 16, "\0".b * 16).byteslice(0, 3)
55
+ expect(data.byteslice(offset + 18, 4)).to eq("\3".b + check)
56
+ offset += 22
57
+ check
58
+ end
59
+ expect(data.bytesize).to eq(offset)
60
+ ["\7".b + checks.join, hex('9000')]
61
+ end
62
+ expect(described_class.execute(args: ['keys', 'import', '1', '7', '--replace-kvn', '2'], connection: connection, key_enc: keys[0], key_mac: keys[1], key_dek: keys[2], confirm: true)).to include(imported: true, kid: 1, kvn: 7)
63
+ expect(card.commands.none? { |command| keys.any? { |key| command.include?(key) } }).to be(true)
64
+ end
65
+
66
+ it 'generates an SCP11 key with explicit curve and replacement KVN and returns only its public PEM' do
67
+ card, connection = authenticated_card
68
+ generated = OpenSSL::PKey::EC.generate('prime256v1')
69
+ card.handler = lambda do |header, data|
70
+ expect(header).to eq(hex('84f10213'))
71
+ expect(data).to eq(hex('07f00100'))
72
+ [hex('b041') + generated.public_key.to_octet_string(:uncompressed), hex('9000')]
73
+ end
74
+ pem = described_class.execute(args: %w[keys generate 0x13 7 --replace-kvn 2], connection: connection, confirm: true)
75
+ expect(OpenSSL::PKey.read(pem).public_to_der).to eq(generated.public_to_der)
76
+ expect(OpenSSL::PKey.read(pem).private?).to be(false)
77
+ end
78
+
79
+ it 'imports a private SCP11 EC key as a DEK-wrapped scalar and a public CA key as an EC point' do
80
+ [0x13, 0x10].each do |kid|
81
+ card, connection, dek = authenticated_card
82
+ key = OpenSSL::PKey::EC.generate('prime256v1')
83
+ card.handler = lambda do |header, data|
84
+ expect(header).to eq([0x84, 0xd8, 0, kid].pack('C*'))
85
+ expect(data.getbyte(0)).to eq(7)
86
+ if kid == 0x13
87
+ expect(data.byteslice(1, 2)).to eq(hex('b120'))
88
+ scalar = ReferenceCrypto.new.aes(dek, data.byteslice(3, 32), "\0".b * 16, decrypt: true)
89
+ expect(OpenSSL::BN.new(scalar, 2)).to eq(key.private_key)
90
+ else
91
+ expect(data.byteslice(1, 67)).to eq(hex('b041') + key.public_key.to_octet_string(:uncompressed))
92
+ end
93
+ expect(data.byteslice(-4, 4)).to eq(hex('f0010000'))
94
+ [hex('07'), hex('9000')]
95
+ end
96
+ material = kid == 0x13 ? key : OpenSSL::PKey.read(key.public_to_der)
97
+ expect(described_class.execute(args: ['keys', 'import', kid.to_s, '7'], connection: connection, key: material, confirm: true)).to include(imported: true)
98
+ end
99
+ end
100
+
101
+ it 'deletes an SCP03 keyset by version without treating the key identifier as a wildcard' do
102
+ card, connection = authenticated_card
103
+ card.handler = lambda do |header, data|
104
+ expect(header).to eq(hex('84e40000'))
105
+ expect(data).to eq(hex('d20107'))
106
+ [''.b, hex('9000')]
107
+ end
108
+ expect(described_class.execute(args: %w[keys delete 1 7], connection: connection, confirm: true)).to include(deleted: true)
109
+ end
110
+
111
+ it 'encodes allowlisted certificate serials as nonnegative ASN1 integers' do
112
+ card, connection = authenticated_card
113
+ card.handler = lambda do |header, data|
114
+ expect(header).to eq(hex('04e29000'))
115
+ expect(data).to eq(hex('a60483021007700a9301009302008093017f'))
116
+ [''.b, hex('9000')]
117
+ end
118
+ expect(described_class.execute(args: %w[keys set-allowlist 0x10 7 0 128 127], connection: connection, confirm: true)).to include(stored: true)
119
+ end
120
+
121
+ it 'resets only with explicit consent and bounds each destructive authentication loop' do
122
+ card = SDCard.new(%w[9000 c00401018810c00402018810c00403018810c0041301b0209000 6a80 6983 6982 c00401ff88109000].map { |data| hex(data) })
123
+ expect(described_class.execute(args: ['reset'], connection: card, confirm: true)).to include(reset: true)
124
+ expect(card.commands[2].byteslice(0, 4)).to eq(hex('80500000'))
125
+ expect(card.commands[4].byteslice(0, 4)).to eq(hex('80880113'))
126
+ expect(card.commands.length).to eq(6)
127
+ end
128
+
129
+ def test_certificate
130
+ key = OpenSSL::PKey::EC.generate('prime256v1')
131
+ cert = OpenSSL::X509::Certificate.new
132
+ cert.version = 2
133
+ cert.serial = 9
134
+ cert.subject = cert.issuer = OpenSSL::X509::Name.parse('/CN=Synthetic SD')
135
+ cert.not_before = Time.utc(2020)
136
+ cert.not_after = Time.utc(2090)
137
+ cert.public_key = key
138
+ factory = OpenSSL::X509::ExtensionFactory.new
139
+ cert.extensions = [factory.create_extension('keyUsage', 'keyAgreement', true)]
140
+ cert.sign(key, OpenSSL::Digest.new('SHA256'))
141
+ cert
142
+ end
143
+
144
+ it 'exports stored certificate chains without exporting private key material' do
145
+ certificate = test_certificate
146
+ card = SDCard.new([hex('9000'), certificate.to_der + hex('9000')])
147
+ expect(described_class.execute(args: %w[keys export 0x13 7], connection: card)).to eq(certificate.to_pem.b)
148
+ expect(card.commands.last.byteslice(0, 4)).to eq(hex('00cabf21'))
149
+ expect(card.commands.last.byteslice(-6, 6)).to eq(hex('a60483021307'))
150
+ end
151
+
152
+ it 'stores a certificate bundle and CA issuer with native binary STORE DATA templates' do
153
+ card, connection = authenticated_card
154
+ certificate = test_certificate
155
+ payloads = []
156
+ card.handler = lambda do |header, data|
157
+ expect(header).to eq(hex('04e29000'))
158
+ payloads << data
159
+ [''.b, hex('9000')]
160
+ end
161
+ expect(described_class.execute(args: %w[keys set-certificate 0x13 7], connection: connection, certificates: [certificate], confirm: true)).to include(stored: true)
162
+ expect(payloads[0].byteslice(0, 6)).to eq(hex('a60483021307'))
163
+ body = PWN::Plugins::YubiKey::Protocol.untlv(data: payloads[0]).last
164
+ expect(body).to eq(tag: 0xbf21, value: certificate.to_der)
165
+ described_class.execute(args: %w[keys set-ca-issuer 0x13 7], connection: connection, ski: 'abc'.b, confirm: true)
166
+ expect(payloads[1]).to eq(hex('a60c800101420361626383021307'))
167
+ end
168
+
169
+ it 'rejects unsupported, malformed and unconfirmed operations before any I/O' do
170
+ cases = [
171
+ { args: %w[keys generate 1 7], confirm: true },
172
+ { args: %w[keys import 1 7 argv-secret], confirm: true },
173
+ { args: %w[keys import 1 256], confirm: true },
174
+ { args: %w[keys delete 1 0], confirm: true },
175
+ { args: %w[keys delete 0 0], confirm: true },
176
+ { args: %w[keys set-allowlist 0x10 7 -1], confirm: true },
177
+ { args: ['reset'], confirm: 'true' },
178
+ { args: %w[keys delete 0x13 7 --force] },
179
+ { args: ['invented'] }
180
+ ]
181
+ cases.each do |options|
182
+ card = SDCard.new([])
183
+ expect { described_class.execute(options.merge(connection: card)) }.to raise_error(StandardError)
184
+ expect(card.commands).to be_empty
185
+ end
186
+ expect(described_class.execute(args: ['keys'])).to eq(operations: described_class.operations)
187
+ end
188
+
189
+ it 'does not trust forged authentication descriptors or allow SCP11b administration' do
190
+ fake = SDCard.new([])
191
+ allow(fake).to receive(:descriptor).and_return(protocol: :apdu, authenticated: true, secure_channel: :scp03)
192
+ expect { described_class.execute(args: %w[keys delete 0x13 7], connection: fake, confirm: true) }.to raise_error(described_class::SCP::ChannelError)
193
+ card = Scp11Card.new
194
+ channel = described_class.secure_connection(connection: card, scp: :scp11b, kvn: 1, card_public_key: card.public_key, confirm: true)
195
+ count = card.commands.length
196
+ expect { described_class.execute(args: %w[keys generate 0x13 7], connection: channel, confirm: true) }.to raise_error(described_class::SCP::ChannelError)
197
+ expect { described_class.execute(args: %w[reset], connection: channel, confirm: true) }.to raise_error(ArgumentError)
198
+ expect(card.commands.length).to eq(count)
199
+ end
200
+
201
+ it 'refuses bad key check values without retrying an uncertain PUT KEY' do
202
+ card, connection = authenticated_card
203
+ card.handler = ->(_header, _data) { [hex('07'), hex('9000')] }
204
+ keys = 'x'.b * 16
205
+ expect { described_class.execute(args: %w[keys import 1 7], key_enc: keys, key_mac: keys, key_dek: keys, connection: connection, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /check value/)
206
+ expect(card.commands.count { |command| command.getbyte(1) == 0xd8 }).to eq(1)
207
+ end
208
+
209
+ it 'rejects malformed or duplicate key metadata and missing certificate exports' do
210
+ %w[c003010188 c00401018810c00401018810].each do |wire|
211
+ card = SDCard.new([hex('9000'), hex(wire) + hex('9000')])
212
+ expect { described_class.execute(args: ['info'], connection: card) }.to raise_error(PWN::Plugins::YubiKey::ResponseError)
213
+ end
214
+ card = SDCard.new([hex('9000'), hex('9000')])
215
+ expect { described_class.execute(args: %w[keys export 0x13 7], connection: card) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /No certificate/)
216
+ end
217
+
218
+ it 'bounds reset retries and rejects unexpected statuses without automatically retrying writes' do
219
+ initial = [hex('9000'), hex('c0041301b0209000')]
220
+ card = SDCard.new(initial + Array.new(65) { hex('6a80') })
221
+ expect { described_class.execute(args: ['reset'], connection: card, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /bound/)
222
+ expect(card.commands.count { |command| command.getbyte(1) == 0x88 }).to eq(65)
223
+ card = SDCard.new(initial + [hex('6f00')])
224
+ expect { described_class.execute(args: ['reset'], connection: card, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::Protocol::APDUError)
225
+ expect(card.commands.count { |command| command.getbyte(1) == 0x88 }).to eq(1)
226
+ end
227
+
228
+ it 'imports a CA certificate in memory and stores its explicit issuer identifier' do
229
+ card, connection = authenticated_card
230
+ key = OpenSSL::PKey::EC.generate('prime256v1')
231
+ cert = test_certificate
232
+ cert.public_key = key
233
+ factory = OpenSSL::X509::ExtensionFactory.new
234
+ cert.extensions = [factory.create_extension('basicConstraints', 'CA:TRUE', true), factory.create_extension('keyUsage', 'keyCertSign', true)]
235
+ cert.sign(key, OpenSSL::Digest.new('SHA256'))
236
+ calls = []
237
+ card.handler = lambda do |header, data|
238
+ calls << [header, data]
239
+ [header.getbyte(1) == 0xd8 ? hex('07') : ''.b, hex('9000')]
240
+ end
241
+ expect(described_class.execute(args: %w[keys import 0x10 7], connection: connection, certificate: cert, ski: 'abc', confirm: true)).to include(imported: true)
242
+ expect(calls[0][1].byteslice(1, 67)).to eq(hex('b041') + key.public_key.to_octet_string(:uncompressed))
243
+ expect(calls[1][1]).to eq(hex('a60c800100420361626383021007'))
244
+ end
245
+
246
+ it 'validates an imported private key certificate before PUT KEY and stores the matching bundle' do
247
+ card, connection = authenticated_card
248
+ key = OpenSSL::PKey::EC.generate('prime256v1')
249
+ cert = test_certificate
250
+ initial_count = card.commands.length
251
+ expect { described_class.execute(args: %w[keys import 0x13 7], connection: connection, key: key, certificates: [cert], confirm: true) }.to raise_error(ArgumentError, /match/)
252
+ expect(card.commands.length).to eq(initial_count)
253
+ cert.public_key = key
254
+ cert.sign(key, OpenSSL::Digest.new('SHA256'))
255
+ calls = []
256
+ card.handler = lambda do |header, data|
257
+ calls << [header, data]
258
+ [header.getbyte(1) == 0xd8 ? hex('07') : ''.b, hex('9000')]
259
+ end
260
+ expect(described_class.execute(args: %w[keys import 0x13 7], connection: connection, key: key, certificates: [cert], confirm: true)).to include(imported: true)
261
+ expect(calls.map { |header, _data| header.getbyte(1) }).to eq([0xd8, 0xe2])
262
+ expect(calls.last.last).to include(cert.to_der)
263
+ end
264
+
265
+ it 'encodes each documented EC curve identifier for native key generation' do
266
+ { 'prime256v1' => 0, 'secp384r1' => 1, 'secp521r1' => 2, 'brainpoolP256r1' => 3, 'brainpoolP384r1' => 5, 'brainpoolP512r1' => 7 }.each do |curve, identifier|
267
+ card, connection = authenticated_card
268
+ public_key = OpenSSL::PKey::EC.generate(curve)
269
+ card.handler = lambda do |_header, data|
270
+ expect(data).to eq([7, 0xf0, 1, identifier].pack('C*'))
271
+ encoded = public_key.public_key.to_octet_string(:uncompressed)
272
+ size = encoded.bytesize < 128 ? [encoded.bytesize].pack('C') : [0x81, encoded.bytesize].pack('CC')
273
+ ["\xb0".b + size + encoded, hex('9000')]
274
+ end
275
+ result = described_class.execute(args: %w[keys generate 0x13 7], curve: curve, connection: connection, confirm: true)
276
+ expect(OpenSSL::PKey.read(result).public_to_der).to eq(public_key.public_to_der)
277
+ end
278
+ end
279
+
280
+ it 'validates the complete reset inventory before blocking keys and checks post-reset state' do
281
+ card = SDCard.new([hex('9000'), hex('c00401018810c004fe01b0209000')])
282
+ expect { described_class.execute(args: ['reset'], connection: card, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /Unknown/)
283
+ expect(card.commands.length).to eq(2)
284
+ card = SDCard.new([hex('9000'), hex('c004010188109000'), hex('6983'), hex('c004010188109000')])
285
+ expect { described_class.execute(args: ['reset'], connection: card, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /reset state/)
286
+ end
287
+
288
+ it 'lists native key types, CA identifiers and card recognition data' do
289
+ card = SDCard.new(%w[9000 c006010188100100c0041301b0209000 73036162639000 4203616263830213019000 6a88].map { |data| hex(data) })
290
+ info = described_class.execute(args: ['info'], connection: card)
291
+ expect(info[:keys]).to eq([{ kid: 1, kvn: 1, types: { 0x88 => 16, 1 => 0 } }, { kid: 0x13, kvn: 1, types: { 0xb0 => 32 } }])
292
+ expect(info[:ca_identifiers]).to eq([{ kid: 0x13, kvn: 1, ski: 'abc'.b, kind: :kloc }])
293
+ expect(info[:card_recognition_data]).to eq('abc'.b)
294
+ expect(card.commands.map { |command| command.byteslice(0, 4).unpack1('H*') }).to eq(%w[00a40400 00ca00e0 00ca0066 00caff33 00caff34])
295
+ end
296
+ end