pwn 0.5.784 → 0.5.787

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 (254) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +6 -6
  4. data/README.md +37 -0
  5. data/bin/pwn +2 -1
  6. data/bin/pwn_web_cache_deception +35 -7
  7. data/build_gem.sh +1 -1
  8. data/documentation/AI-Integration.md +7 -5
  9. data/documentation/Agent-Tool-Registry.md +2 -2
  10. data/documentation/Backup-Retention.md +69 -15
  11. data/documentation/Configuration.md +10 -6
  12. data/documentation/Contributing.md +1 -1
  13. data/documentation/Cron.md +2 -2
  14. data/documentation/Diagrams.md +2 -2
  15. data/documentation/General-PWN-Usage.md +4 -3
  16. data/documentation/Harness-Evaluation.md +1 -1
  17. data/documentation/Home.md +6 -5
  18. data/documentation/How-PWN-Works.md +6 -5
  19. data/documentation/Installation.md +12 -9
  20. data/documentation/Mission-Cancellation.md +134 -0
  21. data/documentation/Operability.md +18 -0
  22. data/documentation/Persistence.md +7 -5
  23. data/documentation/Plugin-Degradation.md +14 -0
  24. data/documentation/Plugins.md +3 -2
  25. data/documentation/Policy-Benchmark.md +32 -33
  26. data/documentation/Runtime-Source-Protection.md +162 -0
  27. data/documentation/Transparent-Browser.md +11 -0
  28. data/documentation/Troubleshooting.md +3 -2
  29. data/documentation/What-is-PWN.md +2 -2
  30. data/documentation/YubiKey-Vaults.md +185 -0
  31. data/documentation/YubiKey.md +218 -0
  32. data/documentation/diagrams/cron-scheduling.svg +130 -130
  33. data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
  34. data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
  35. data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
  36. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
  37. data/documentation/diagrams/dot/reinforcement-learning.dot +2 -2
  38. data/documentation/diagrams/persistence-filesystem.svg +172 -142
  39. data/documentation/diagrams/plugin-ecosystem.svg +64 -64
  40. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +1 -1
  41. data/documentation/diagrams/reinforcement-learning.svg +89 -89
  42. data/documentation/pwn-ai-Agent.md +10 -4
  43. data/etc/default_cron/session_cleanup.rb +43 -27
  44. data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
  45. data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
  46. data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
  47. data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
  48. data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
  49. data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
  50. data/etc/default_skills/pwn/backups/SKILL.md +54 -0
  51. data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
  52. data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
  53. data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
  54. data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
  55. data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
  56. data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
  57. data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
  58. data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
  59. data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
  60. data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
  61. data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
  62. data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
  63. data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
  64. data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
  65. data/lib/pwn/ai/agent/dispatch.rb +26 -15
  66. data/lib/pwn/ai/agent/learning.rb +14 -18
  67. data/lib/pwn/ai/agent/loop.rb +17 -1
  68. data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
  69. data/lib/pwn/ai/agent/policy_evaluation.rb +334 -36
  70. data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
  71. data/lib/pwn/ai/agent/request_runtime.rb +122 -18
  72. data/lib/pwn/ai/agent/reward.rb +5 -3
  73. data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
  74. data/lib/pwn/ai/agent/skill_review.rb +9 -5
  75. data/lib/pwn/ai/agent/swarm.rb +4 -0
  76. data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
  77. data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
  78. data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
  79. data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
  80. data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
  81. data/lib/pwn/ai/agent.rb +1 -0
  82. data/lib/pwn/ai/anthropic.rb +8 -33
  83. data/lib/pwn/ai/cli.rb +34 -5
  84. data/lib/pwn/ai/gemini.rb +1 -0
  85. data/lib/pwn/ai/grok.rb +39 -28
  86. data/lib/pwn/ai/ollama.rb +4 -2
  87. data/lib/pwn/ai/open_ai.rb +11 -32
  88. data/lib/pwn/ai/open_web_ui.rb +62 -4
  89. data/lib/pwn/ai/router.rb +8 -1
  90. data/lib/pwn/ai/server_tools.rb +231 -0
  91. data/lib/pwn/ai.rb +1 -0
  92. data/lib/pwn/backups.rb +349 -0
  93. data/lib/pwn/config.rb +70 -39
  94. data/lib/pwn/migrate.rb +85 -280
  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/google_workspace.rb +4 -21
  100. data/lib/pwn/plugins/jobs.rb +1 -1
  101. data/lib/pwn/plugins/log.rb +3 -1
  102. data/lib/pwn/plugins/preflight_checker.rb +34 -9
  103. data/lib/pwn/plugins/recon.rb +4 -1
  104. data/lib/pwn/plugins/repl/ai.rb +293 -26
  105. data/lib/pwn/plugins/repl/ai_console.rb +268 -88
  106. data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
  107. data/lib/pwn/plugins/repl/mesh.rb +14 -19
  108. data/lib/pwn/plugins/repl/vault.rb +4 -15
  109. data/lib/pwn/plugins/repl.rb +9 -216
  110. data/lib/pwn/plugins/sandbox/driver.rb +4 -0
  111. data/lib/pwn/plugins/transparent_browser.rb +215 -0
  112. data/lib/pwn/plugins/vault.rb +412 -135
  113. data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
  114. data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
  115. data/lib/pwn/plugins/yubikey/management.rb +686 -0
  116. data/lib/pwn/plugins/yubikey/oath.rb +766 -0
  117. data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
  118. data/lib/pwn/plugins/yubikey/otp.rb +897 -0
  119. data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
  120. data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
  121. data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
  122. data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
  123. data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
  124. data/lib/pwn/plugins/yubikey.rb +844 -0
  125. data/lib/pwn/plugins.rb +1 -0
  126. data/lib/pwn/redaction.rb +69 -12
  127. data/lib/pwn/session_trace.rb +4 -0
  128. data/lib/pwn/setup.rb +114 -24
  129. data/lib/pwn/version.rb +1 -1
  130. data/lib/pwn/www/app_cobalt_io.rb +16 -5
  131. data/lib/pwn/www/bug_crowd.rb +16 -5
  132. data/lib/pwn/www/coinbase_pro.rb +16 -5
  133. data/lib/pwn/www/github.rb +30 -14
  134. data/lib/pwn/www/paypal.rb +15 -4
  135. data/lib/pwn/www/synack.rb +16 -5
  136. data/lib/pwn/www/twitter.rb +17 -6
  137. data/lib/pwn.rb +1 -0
  138. data/spec/documentation/installation_md_spec.rb +1 -1
  139. data/spec/fixtures/yubikey/apdu_frames.json +162 -0
  140. data/spec/integration/authentication_helper_oath_spec.rb +117 -0
  141. data/spec/integration/controller_spill_spec.rb +48 -0
  142. data/spec/integration/harness_process_spec.rb +97 -0
  143. data/spec/integration/mission_cancel_spec.rb +79 -0
  144. data/spec/integration/mission_curses_pty_spec.rb +44 -0
  145. data/spec/integration/readme_security_spec.rb +47 -0
  146. data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
  147. data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
  148. data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
  149. data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
  150. data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
  151. data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
  152. data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
  153. data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
  154. data/spec/lib/pwn/ai/agent/policy_evaluation_spec.rb +10 -23
  155. data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
  156. data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
  157. data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
  158. data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
  159. data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
  160. data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
  161. data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
  162. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
  163. data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
  164. data/spec/lib/pwn/ai/cli_spec.rb +8 -16
  165. data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
  166. data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
  167. data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
  168. data/spec/lib/pwn/ai/router_spec.rb +1 -1
  169. data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
  170. data/spec/lib/pwn/backups_spec.rb +129 -0
  171. data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
  172. data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
  173. data/spec/lib/pwn/migrate_spec.rb +29 -81
  174. data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
  175. data/spec/lib/pwn/module_skills_spec.rb +8 -0
  176. data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
  177. data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
  178. data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
  179. data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
  180. data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
  181. data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
  182. data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
  183. data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
  184. data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
  185. data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +110 -75
  186. data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
  187. data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -16
  188. data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
  189. data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
  190. data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
  191. data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
  192. data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
  193. data/spec/lib/pwn/plugins/repl_mesh_layout_spec.rb +2 -0
  194. data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
  195. data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
  196. data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
  197. data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
  198. data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
  199. data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
  200. data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
  201. data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
  202. data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
  203. data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
  204. data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
  205. data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
  206. data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
  207. data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
  208. data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
  209. data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
  210. data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
  211. data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
  212. data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
  213. data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
  214. data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
  215. data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
  216. data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
  217. data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
  218. data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
  219. data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
  220. data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
  221. data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
  222. data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
  223. data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
  224. data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
  225. data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
  226. data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
  227. data/spec/spec_helper.rb +15 -0
  228. data/spec/support/harness/cli_faults.rb +13 -9
  229. data/spec/support/harness/controller_spill.rb +26 -0
  230. data/spec/support/harness/controller_state.rb +54 -0
  231. data/spec/support/harness/evaluate.rb +12 -1
  232. data/spec/support/harness/experiments.rb +1 -1
  233. data/spec/support/harness/fake_provider.rb +22 -6
  234. data/spec/support/harness/fault_preload.rb +3 -1
  235. data/spec/support/harness/mission_console.rb +54 -0
  236. data/spec/support/harness/mission_curses_pty.rb +41 -0
  237. data/spec/support/harness/mission_dispatch.rb +30 -0
  238. data/spec/support/harness/mission_fixture_preload.rb +24 -0
  239. data/spec/support/harness/mission_job_lifetime.rb +25 -0
  240. data/spec/support/harness/mission_legacy.rb +25 -0
  241. data/spec/support/harness/mission_normal_exit.rb +34 -0
  242. data/spec/support/harness/mission_swarm.rb +23 -0
  243. data/spec/support/harness/mission_unsupported.rb +29 -0
  244. data/spec/support/harness/package_acceptance.rb +33 -5
  245. data/spec/support/harness/session_acceptance.rb +4 -4
  246. data/spec/support/harness/swarm_acceptance.rb +1 -1
  247. data/spec/support/harness/swarm_control.rb +44 -0
  248. data/spec/support/mission_namespace.rb +14 -0
  249. data/spec/support/mission_pipe_queue.rb +33 -0
  250. data/third_party/pwn_rdoc.jsonl +605 -19
  251. metadata +129 -16
  252. data/scripts/benchmark_policy.rb +0 -376
  253. data/scripts/build_gate.rb +0 -58
  254. data/spec/integration/build_gate_spec.rb +0 -73
@@ -0,0 +1,464 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require_relative 'protocol'
5
+ require_relative 'security_domain/scp'
6
+
7
+ module PWN
8
+ module Plugins
9
+ module YubiKey
10
+ # Native Security Domain administration. Reference: Yubico yubikit 5.9.1
11
+ # securitydomain.py; GlobalPlatform Card Specification 2.3, Amendments D/F.
12
+ module SecurityDomain
13
+ AID = ['a000000151000000'].pack('H*').freeze
14
+ OPERATIONS = ['info', 'reset', 'keys generate', 'keys import', 'keys export', 'keys delete', 'keys set-allowlist', 'keys set-certificate', 'keys set-ca-issuer'].freeze
15
+ SCP11_KIDS = [0x11, 0x13, 0x15].freeze
16
+ CA_KIDS = [0x10, *(0x20..0x2f)].freeze
17
+ CURVES = { 'prime256v1' => 0, 'secp384r1' => 1, 'secp521r1' => 2, 'brainpoolP256r1' => 3, 'brainpoolP384r1' => 5, 'brainpoolP512r1' => 7 }.freeze
18
+
19
+ # Parse nonsecret argv before selecting or authenticating a device.
20
+ class Request # rubocop:disable Metrics/ClassLength
21
+ def initialize(opts)
22
+ @opts = opts
23
+ @crypto = SCP::Crypto.new
24
+ parsed = Protocol.parse_args(args: opts.fetch(:args, []), flags: ['--force'], values: ['--replace-kvn'])
25
+ words = parsed[:positionals]
26
+ @operation = words.shift
27
+ @operation += " #{words.shift}" if @operation == 'keys'
28
+ raise ArgumentError, 'Unknown Security Domain operation' unless OPERATIONS.include?(@operation)
29
+
30
+ options = parsed[:options]
31
+ @params = {}
32
+ if %w[info reset].include?(@operation)
33
+ raise ArgumentError, 'Operation takes no positional arguments' unless words.empty?
34
+ raise ArgumentError, 'Unsupported option for operation' unless (options.keys - (@operation == 'reset' ? [:force] : [])).empty?
35
+ else
36
+ raise ArgumentError, 'Expected KID and KVN' if words.length < 2
37
+
38
+ @params = { kid: integer(words.shift), kvn: integer(words.shift) }
39
+ @params[:replace_kvn] = integer(options.fetch(:replace_kvn, opts.fetch(:replace_kvn, 0)))
40
+ allowed = if ['keys generate', 'keys import'].include?(@operation)
41
+ [:replace_kvn]
42
+ else
43
+ (@operation == 'keys delete' ? [:force] : [])
44
+ end
45
+ raise ArgumentError, 'Unsupported option for operation' unless (options.keys - allowed).empty?
46
+ raise ArgumentError, 'Replacement KVN must be in 0..127' unless @params[:replace_kvn].between?(0, 127)
47
+
48
+ # Binary export/generation returns data; '-' is an optional stdout
49
+ # compatibility marker, never an instruction to write a file.
50
+ words.pop if ['keys generate', 'keys export'].include?(@operation) && words == ['-']
51
+ if @operation == 'keys set-allowlist'
52
+ values = opts.fetch(:serials, words)
53
+ raise ArgumentError, 'Serials must be a bounded Array' unless values.is_a?(Array) && values.length <= 256
54
+
55
+ @params[:serials] = values.map { |value| integer(value, maximum: (1 << 160) - 1) }
56
+ words = []
57
+ end
58
+ raise ArgumentError, 'Unexpected positional argument; supply keys/certificates in memory' unless words.empty?
59
+
60
+ validate_reference
61
+ end
62
+ validate_consent
63
+ prepare_material
64
+ end
65
+
66
+ def execute
67
+ session = Session.new(@opts)
68
+ case @operation
69
+ when 'info' then session.info
70
+ when 'reset' then session.reset
71
+ when 'keys generate' then session.generate(@params)
72
+ when 'keys import'
73
+ result = session.import_key(@params)
74
+ session.store_certificates(@params) if @params[:certificates]
75
+ session.store_ca(@params) if @params[:ski]
76
+ result
77
+ when 'keys export' then session.export(@params)
78
+ when 'keys delete' then session.delete(@params)
79
+ when 'keys set-allowlist' then session.allowlist(@params)
80
+ when 'keys set-certificate' then session.store_certificates(@params)
81
+ when 'keys set-ca-issuer' then session.store_ca(@params)
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def validate_reference
88
+ kid = @params[:kid]
89
+ kvn = @params[:kvn]
90
+ case @operation
91
+ when 'keys delete'
92
+ raise ArgumentError, 'Deleting all keys requires reset, not wildcard deletion' if kid.zero? && kvn.zero?
93
+ raise ArgumentError, 'Invalid key identifier' unless [0, 1, 2, 3, *SCP11_KIDS, *CA_KIDS].include?(kid)
94
+ raise ArgumentError, 'SCP03 keys must be deleted by a nonzero KVN' if [1, 2, 3].include?(kid) && kvn.zero?
95
+ when 'keys import'
96
+ raise ArgumentError, 'Invalid import KID' unless [1, *SCP11_KIDS, *CA_KIDS].include?(kid)
97
+ raise ArgumentError, 'Import KVN must be in 1..127' unless kvn.between?(1, 127)
98
+ when 'keys generate'
99
+ raise ArgumentError, 'Generation requires SCP11 KID and KVN in 1..127' unless SCP11_KIDS.include?(kid) && kvn.between?(1, 127)
100
+ when 'keys set-allowlist'
101
+ raise ArgumentError, 'Allowlist requires an OCE CA key reference' unless CA_KIDS.include?(kid) && kvn.positive?
102
+ else
103
+ raise ArgumentError, 'Invalid SCP11/CA key reference' unless [*SCP11_KIDS, *CA_KIDS].include?(kid) && kvn.positive?
104
+ end
105
+ end
106
+
107
+ def validate_consent
108
+ return if ['info', 'keys export'].include?(@operation)
109
+
110
+ raise ConfirmationRequired, 'Security Domain mutation requires confirm: true' unless @opts[:confirm] == true
111
+
112
+ connection = @opts[:connection]
113
+ if @operation == 'reset'
114
+ raise ArgumentError, 'Reset requires a plain connection without an active secure channel' if connection.is_a?(SCP::Connection) || connection&.descriptor&.dig(:secure_channel)
115
+ else
116
+ raise SCP::ChannelError, 'SCP03 or SCP11a administration required' unless connection.is_a?(SCP::Connection) && connection.authenticated?
117
+ end
118
+ end
119
+
120
+ def prepare_material
121
+ case @operation
122
+ when 'keys generate'
123
+ @params[:curve] = @opts.fetch(:curve, 'prime256v1')
124
+ raise ArgumentError, 'Unsupported EC curve' unless CURVES.key?(@params[:curve])
125
+ when 'keys import'
126
+ prepare_import
127
+ when 'keys set-certificate'
128
+ @params[:certificates] = @crypto.certificates(@opts.fetch(:certificates))
129
+ @crypto.validate_chain(@params[:certificates])
130
+ raise ArgumentError, 'Certificate bundle too large' if @params[:certificates].sum { |cert| cert.to_der.bytesize } > 60_000
131
+ when 'keys set-ca-issuer'
132
+ @params[:ski] = @opts[:ski]
133
+ raise ArgumentError, 'SKI must be a nonempty binary String of at most 64 bytes' unless @params[:ski].is_a?(String) && @params[:ski].bytesize.between?(1, 64)
134
+ end
135
+ end
136
+
137
+ def prepare_import
138
+ if @params[:kid] == 1
139
+ @params[:keys] = %i[key_enc key_mac key_dek].map do |name|
140
+ value = @opts[name]
141
+ value = @opts[:secret_provider].call(name) if value.nil? && @opts[:secret_provider].respond_to?(:call)
142
+ raise ArgumentError, 'Import requires three explicit 16-byte AES keys' unless value.is_a?(String) && value.bytesize == 16
143
+
144
+ value.b.dup
145
+ end
146
+ return
147
+ end
148
+ private_key = SCP11_KIDS.include?(@params[:kid])
149
+ value = @opts[:key]
150
+ if @opts[:certificate]
151
+ raise ArgumentError, 'CA certificate input cannot be combined with a key or private-key KID' if value || private_key
152
+
153
+ cert = @crypto.certificates([@opts[:certificate]]).first
154
+ constraints = cert.extensions.find { |ext| ext.oid == 'basicConstraints' }
155
+ usage = cert.extensions.find { |ext| ext.oid == 'keyUsage' }
156
+ raise ArgumentError, 'CA certificate requires CA:true and keyCertSign usage' unless constraints&.value&.include?('CA:TRUE') && usage&.value&.include?('Certificate Sign')
157
+ raise ArgumentError, 'CA certificate validity check failed' unless Time.now.between?(cert.not_before, cert.not_after)
158
+
159
+ value = cert.public_key
160
+ ski = cert.extensions.find { |ext| ext.oid == 'subjectKeyIdentifier' }
161
+ @params[:ski] = [ski.value.delete(':')].pack('H*') if ski
162
+ end
163
+ value = @opts[:secret_provider].call(:key) if value.nil? && @opts[:secret_provider].respond_to?(:call)
164
+ @params[:key] = @crypto.ec_key(value, private_key: private_key)
165
+ raise ArgumentError, 'CA imports must contain a public key, not private material' if !private_key && @params[:key].private?
166
+
167
+ @params[:curve] = @params[:key].group.curve_name
168
+ raise ArgumentError, 'Unsupported EC curve' unless CURVES.key?(@params[:curve])
169
+ raise ArgumentError, 'Non-block-aligned EC private scalars are unsupported by the YubiKey PUT KEY format' if private_key && (((@params[:key].group.degree + 7) / 8) % 16).nonzero?
170
+
171
+ if @opts[:certificates]
172
+ raise ArgumentError, 'Certificate bundles accompany SCP11 private keys only' unless private_key
173
+
174
+ @params[:certificates] = @crypto.certificates(@opts[:certificates])
175
+ leaf = @crypto.validate_chain(@params[:certificates])
176
+ raise ArgumentError, 'Imported key does not match certificate' unless @crypto.equal?(leaf.public_to_der, @params[:key].public_to_der)
177
+ raise ArgumentError, 'Certificate bundle too large' if @params[:certificates].sum { |cert| cert.to_der.bytesize } > 60_000
178
+ end
179
+ @params[:ski] = @opts[:ski] if @opts.key?(:ski)
180
+ return unless @params[:ski] && (!@params[:ski].is_a?(String) || !@params[:ski].bytesize.between?(1, 64))
181
+
182
+ raise ArgumentError, 'SKI must be a nonempty binary String of at most 64 bytes'
183
+ end
184
+
185
+ def integer(value, maximum: 255)
186
+ return value if value.is_a?(Integer) && value.between?(0, maximum)
187
+ raise ArgumentError, 'Expected exact unsigned decimal or 0x-hex integer' unless value.is_a?(String) && value.match?(/\A(?:0x[0-9a-fA-F]+|[0-9]+)\z/)
188
+
189
+ number = Integer(value, value.start_with?('0x') ? 16 : 10)
190
+ raise ArgumentError, 'Integer outside permitted range' unless number.between?(0, maximum)
191
+
192
+ number
193
+ end
194
+ end
195
+
196
+ # Native administrative wire commands, after Request validation.
197
+ class Session # rubocop:disable Metrics/ClassLength
198
+ def initialize(opts)
199
+ @connection = opts.fetch(:connection)
200
+ @timeout = opts.fetch(:timeout, 30)
201
+ raise ArgumentError, 'An APDU connection is required' unless @connection.descriptor[:protocol] == :apdu
202
+ raise ArgumentError, 'Timeout must be finite and positive' unless @timeout.is_a?(Numeric) && @timeout.finite? && @timeout.positive?
203
+
204
+ Protocol.select(connection: @connection, aid: AID, timeout: @timeout) unless @connection.is_a?(SCP::Connection)
205
+ end
206
+
207
+ def info
208
+ keys = key_information
209
+ recognition = Protocol.untlv(data: get(0x66))
210
+ raise ResponseError, 'Invalid card recognition data' unless recognition.length == 1 && recognition.first[:tag] == 0x73
211
+
212
+ { keys: keys, card_recognition_data: recognition.first[:value], ca_identifiers: ca_identifiers }
213
+ end
214
+
215
+ def key_information
216
+ keys = Protocol.untlv(data: get(0xe0)).map do |row|
217
+ value = row[:value]
218
+ raise ResponseError, 'Invalid key-information template' unless row[:tag] == 0xc0 && value.bytesize >= 4 && value.bytesize.even?
219
+
220
+ pairs = value.byteslice(2..).bytes.each_slice(2).to_a
221
+ raise ResponseError, 'Duplicate key type in key-information template' unless pairs.map(&:first).uniq.length == pairs.length
222
+
223
+ { kid: value.getbyte(0), kvn: value.getbyte(1), types: pairs.to_h }
224
+ end
225
+ raise ResponseError, 'Duplicate key reference' unless keys.map { |key| [key[:kid], key[:kvn]] }.uniq.length == keys.length
226
+
227
+ keys
228
+ end
229
+
230
+ def ca_identifiers
231
+ [[0xff33, :kloc], [0xff34, :klcc]].flat_map do |tag, kind|
232
+ begin
233
+ rows = Protocol.untlv(data: get(tag))
234
+ rescue Protocol::APDUError => e
235
+ raise unless e.status == 0x6a88
236
+
237
+ next []
238
+ end
239
+ raise ResponseError, 'Invalid CA identifier sequence' unless rows.length.even?
240
+
241
+ rows.each_slice(2).map do |ski, ref|
242
+ raise ResponseError, 'Invalid CA identifier record' unless ski[:tag] == 0x42 && ref[:tag] == 0x83 && ref[:value].bytesize == 2 && !ski[:value].empty?
243
+
244
+ { kid: ref[:value].getbyte(0), kvn: ref[:value].getbyte(1), ski: ski[:value], kind: kind }
245
+ end
246
+ end
247
+ end
248
+
249
+ def import_key(params)
250
+ crypto = SCP::Crypto.new
251
+ data = [params[:kvn]].pack('C')
252
+ expected = data.dup
253
+ p2 = params[:kid]
254
+ if params[:keys]
255
+ p2 |= 0x80
256
+ params[:keys].each do |key|
257
+ check = crypto.aes(key, "\1".b * 16, initialization_vector: "\0".b * 16).byteslice(0, 3)
258
+ data += Protocol.tlv(tag: 0x88, value: @connection.wrap_key(key)) + "\3".b + check
259
+ expected += check
260
+ end
261
+ else
262
+ key = params[:key]
263
+ if key.private?
264
+ size = (key.group.degree + 7) / 8
265
+ scalar = key.private_key.to_s(2).rjust(size, "\0".b)
266
+ data += Protocol.tlv(tag: 0xb1, value: @connection.wrap_key(scalar))
267
+ else
268
+ data += Protocol.tlv(tag: 0xb0, value: key.public_key.to_octet_string(:uncompressed))
269
+ end
270
+ data += Protocol.tlv(tag: 0xf0, value: [CURVES.fetch(params[:curve])].pack('C')) + "\0".b
271
+ end
272
+ response = send_apdu(cla: 0x80, ins: 0xd8, p1: params[:replace_kvn], p2: p2, data: data)
273
+ raise ResponseError, 'Imported key check value mismatch; do not retry an uncertain write' unless crypto.equal?(expected, response)
274
+
275
+ { imported: true, kid: params[:kid], kvn: params[:kvn] }
276
+ end
277
+
278
+ def generate(params)
279
+ data = [params[:kvn]].pack('C') + Protocol.tlv(tag: 0xf0, value: [CURVES.fetch(params[:curve])].pack('C'))
280
+ response = Protocol.untlv(data: send_apdu(cla: 0x80, ins: 0xf1, p1: params[:replace_kvn], p2: params[:kid], data: data))
281
+ raise ResponseError, 'Invalid generated public key response' unless response.length == 1 && response.first[:tag] == 0xb0
282
+
283
+ SCP::Crypto.new.public_point(response.first[:value], params[:curve]).public_to_pem.b
284
+ end
285
+
286
+ def export(params)
287
+ rows = Protocol.untlv(data: get(0xbf21, reference(params)))
288
+ raise ResponseError, 'No certificate chain stored for key' if rows.empty?
289
+
290
+ rows.reverse.map do |row|
291
+ raise ResponseError, 'Invalid certificate bundle' unless row[:tag] == 0x30
292
+
293
+ OpenSSL::X509::Certificate.new(Protocol.tlv(tag: 0x30, value: row[:value])).to_pem.b
294
+ end.join.b
295
+ rescue OpenSSL::OpenSSLError
296
+ raise ResponseError, 'Invalid stored certificate encoding', cause: nil
297
+ end
298
+
299
+ def delete(params)
300
+ kid = [1, 2, 3].include?(params[:kid]) ? 0 : params[:kid]
301
+ data = kid.zero? ? ''.b : Protocol.tlv(tag: 0xd0, value: [kid].pack('C'))
302
+ data += Protocol.tlv(tag: 0xd2, value: [params[:kvn]].pack('C')) unless params[:kvn].zero?
303
+ send_apdu(cla: 0x80, ins: 0xe4, p1: 0, p2: 0, data: data)
304
+ { deleted: true, kid: params[:kid], kvn: params[:kvn] }
305
+ end
306
+
307
+ def allowlist(params)
308
+ serials = params[:serials].map do |serial|
309
+ hex = serial.to_s(16)
310
+ hex = "0#{hex}" if hex.length.odd?
311
+ bytes = [hex].pack('H*')
312
+ bytes = "\0".b + bytes if bytes.getbyte(0).anybits?(0x80)
313
+ Protocol.tlv(tag: 0x93, value: bytes)
314
+ end.join.b
315
+ store(reference(params) + Protocol.tlv(tag: 0x70, value: serials))
316
+ end
317
+
318
+ def store_certificates(params)
319
+ store(reference(params) + Protocol.tlv(tag: 0xbf21, value: params[:certificates].map(&:to_der).join.b))
320
+ end
321
+
322
+ def store_ca(params)
323
+ role = SCP11_KIDS.include?(params[:kid]) ? 1 : 0
324
+ store(Protocol.tlv(tag: 0xa6, value: Protocol.tlv(tag: 0x80, value: [role].pack('C')) + Protocol.tlv(tag: 0x42, value: params[:ski]) + Protocol.tlv(tag: 0x83, value: [params[:kid], params[:kvn]].pack('CC'))))
325
+ end
326
+
327
+ def reset
328
+ keys = key_information
329
+ raise ResponseError, 'Unknown key type; refusing incomplete reset' unless keys.all? { |key| [1, 2, 3, *SCP11_KIDS, *CA_KIDS].include?(key[:kid]) }
330
+
331
+ keys.each do |key|
332
+ kid = key[:kid]
333
+ next if [2, 3].include?(kid)
334
+
335
+ kvn = key[:kvn]
336
+ ins = case kid
337
+ when 1
338
+ kid = kvn = 0
339
+ 0x50
340
+ when 0x11, 0x15 then 0x82
341
+ when 0x13 then 0x88
342
+ else
343
+ raise ResponseError, 'Unknown key type; refusing incomplete reset' unless CA_KIDS.include?(kid)
344
+
345
+ 0x2a
346
+ end
347
+ blocked = false
348
+ 65.times do
349
+ send_apdu(cla: 0x80, ins: ins, p1: kvn, p2: kid, data: "\0".b * 8)
350
+ rescue Protocol::APDUError => e
351
+ if [0x6983, 0x6982].include?(e.status)
352
+ blocked = true
353
+ break
354
+ end
355
+ raise unless e.status == 0x6a80
356
+ end
357
+ raise ResponseError, 'Reset retry bound reached; state uncertain, do not retry automatically' unless blocked
358
+ end
359
+ after = key_information
360
+ default_present = after.any? { |key| key[:kid] == 1 && key[:kvn] == 0xff }
361
+ default_only = after.all? { |key| key[:kid] == 0x13 || ([1, 2, 3].include?(key[:kid]) && key[:kvn] == 0xff) }
362
+ raise ResponseError, 'Security Domain reset state could not be verified' unless default_present && default_only
363
+
364
+ { reset: true, keys: after }
365
+ end
366
+
367
+ private
368
+
369
+ def reference(params)
370
+ Protocol.tlv(tag: 0xa6, value: Protocol.tlv(tag: 0x83, value: [params[:kid], params[:kvn]].pack('CC')))
371
+ end
372
+
373
+ def store(data)
374
+ send_apdu(ins: 0xe2, p1: 0x90, p2: 0, data: data)
375
+ { stored: true }
376
+ end
377
+
378
+ def get(tag, data = ''.b)
379
+ send_apdu(ins: 0xca, p1: tag >> 8, p2: tag & 0xff, data: data)
380
+ end
381
+
382
+ def send_apdu(opts)
383
+ Protocol.apdu(opts.merge(connection: @connection, timeout: @timeout, extended: true))
384
+ end
385
+ end
386
+
387
+ private_constant :Request, :Session
388
+
389
+ # Execute a native Security Domain management leaf operation.
390
+ public_class_method def self.execute(opts = {})
391
+ args = opts[:args] || []
392
+ raise ArgumentError, 'args must be an Array of Strings' unless args.is_a?(Array) && args.all?(String)
393
+ return { operations: operations } if args.empty? || args == ['keys']
394
+
395
+ Request.new(opts).execute
396
+ end
397
+
398
+ # Authenticate an APDU transport with explicit, memory-only keys.
399
+ public_class_method def self.secure_connection(opts = {})
400
+ raise ArgumentError, 'connection is required' unless opts[:connection]
401
+
402
+ SCP.open(opts)
403
+ end
404
+
405
+ # Enumerate implemented leaf operations without accessing a device.
406
+ public_class_method def self.operations
407
+ OPERATIONS.dup
408
+ end
409
+
410
+ # Return module attribution without device access.
411
+ public_class_method def self.authors
412
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
413
+ end
414
+
415
+ # Print callable API documentation without device access.
416
+ public_class_method def self.help
417
+ puts <<~HELP
418
+ Execute a native Security Domain management operation.
419
+ #{self}.execute(
420
+ args: 'optional - leaf command Array; empty returns local metadata',
421
+ connection: 'required - native APDU handle; SCP03/SCP11a wrapper for mutations except reset',
422
+ timeout: 'optional - finite positive per-command seconds, default 30',
423
+ confirm: 'optional - literal true required for mutations, including reset',
424
+ key_enc: 'optional - required 16-byte new K-ENC for SCP03 import',
425
+ key_mac: 'optional - required 16-byte new K-MAC for SCP03 import',
426
+ key_dek: 'optional - required 16-byte new K-DEK for SCP03 import',
427
+ key: 'optional - in-memory EC private key for SCP11 or public key for CA import; object/PEM/DER',
428
+ certificate: 'optional - in-memory CA X509 object/PEM/DER instead of key for CA import',
429
+ certificates: 'optional - leaf-last X509 bundle for set-certificate or private-key import',
430
+ ski: 'optional - raw 1..64 byte issuer identifier for set-ca-issuer or import',
431
+ curve: 'optional - OpenSSL named curve for generation; default prime256v1',
432
+ replace_kvn: 'optional - integer 0..127 replacing an existing KVN; default 0 creates',
433
+ serials: 'optional - Array of certificate serial integers for set-allowlist, instead of argv serials',
434
+ secret_provider: 'optional - callable accepting a missing import field Symbol; invoked only after consent'
435
+ )
436
+ Authenticate a native APDU transport using explicit in-memory keys.
437
+ #{self}.secure_connection(
438
+ connection: 'required - plain native APDU transport; owned by returned wrapper',
439
+ scp: 'required - Symbol scp03, scp11a, scp11b or scp11c',
440
+ kvn: 'required - exact integer card key version 0..255; zero explicitly selects any version',
441
+ kid: 'optional - exact integer KID; must match selected protocol',
442
+ confirm: 'required - literal true permits authentication attempts',
443
+ timeout: 'optional - finite positive handshake seconds, default 30',
444
+ key_enc: 'optional - required 16-byte K-ENC binary String for SCP03',
445
+ key_mac: 'optional - required 16-byte K-MAC binary String for SCP03',
446
+ key_dek: 'optional - 16-byte SCP03 K-DEK, required to import secret keys',
447
+ card_public_key: 'optional - SCP11 pinned EC public key object/PEM/DER; required unless CA anchors supplied',
448
+ ca_certificates: 'optional - explicit trusted CA X509 object/PEM/DER Array for SCP11 card verification',
449
+ card_certificates: 'optional - leaf-last card X509 bundle; otherwise fetched when CA anchors provided',
450
+ oce_private_key: 'optional - required EC private key object/PEM/DER for SCP11a/c',
451
+ oce_certificates: 'optional - required leaf-last OCE X509 bundle for SCP11a/c',
452
+ oce_kid: 'optional - OCE CA KID integer 0, 16 or 32..47; default zero selects a matching CA',
453
+ oce_kvn: 'optional - OCE CA key version integer 0..255; default zero selects any'
454
+ )
455
+ Enumerate implemented native leaf operations without device access.
456
+ #{self}.operations
457
+ Return module author information.
458
+ #{self}.authors
459
+ HELP
460
+ end
461
+ end
462
+ end
463
+ end
464
+ end