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,341 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PWN
4
+ module Plugins
5
+ module YubiKey
6
+ # Native binary codecs shared by YubiKey application protocols.
7
+ module Protocol
8
+ MAX_BYTES = 1_048_576
9
+
10
+ public_class_method def self.require_confirm(opts = {})
11
+ raise ConfirmationRequired, 'This device operation requires confirm: true' unless opts[:confirm] == true
12
+
13
+ :confirmed
14
+ end
15
+
16
+ public_class_method def self.secret(opts = {})
17
+ name = opts[:name]
18
+ raise ArgumentError, 'A nonsecret field name Symbol is required' unless name.is_a?(Symbol)
19
+
20
+ value = opts[:value]
21
+ provider = opts[:secret_provider]
22
+ value = provider.call(name) if value.nil? && provider.respond_to?(:call)
23
+ raise ArgumentError, "An explicit #{name} String is required" unless value.is_a?(String) && !value.empty? && value.bytesize <= MAX_BYTES
24
+
25
+ value.b
26
+ end
27
+
28
+ public_class_method def self.version_at_least?(opts = {})
29
+ version = opts[:version]
30
+ minimum = opts[:minimum]
31
+ raise ArgumentError, 'Firmware versions must be three integer bytes' unless [version, minimum].all? { |tuple| tuple.is_a?(Array) && tuple.length == 3 && tuple.all? { |part| part.is_a?(Integer) && part.between?(0, 255) } }
32
+
33
+ (version <=> minimum) >= 0
34
+ end
35
+
36
+ public_class_method def self.parse_args(opts = {})
37
+ args = opts[:args]
38
+ raise ArgumentError, 'args must be a bounded Array of exact text arguments' unless args.is_a?(Array) && args.length <= 256 && args.all? { |item| item.is_a?(String) && item.valid_encoding? && !item.b.match?(/[\x00-\x1f\x7f]/) } && args.sum(&:bytesize) <= 65_536
39
+
40
+ flags = opts.fetch(:flags, [])
41
+ values = opts.fetch(:values, [])
42
+ remaining = args.dup
43
+ options = {}
44
+ positionals = []
45
+ literal = false
46
+ until remaining.empty?
47
+ item = remaining.shift
48
+ if literal || !item.start_with?('-') || item == '-'
49
+ positionals << item
50
+ elsif item == '--'
51
+ literal = true
52
+ elsif flags.include?(item)
53
+ options[item.delete_prefix('--').tr('-', '_').to_sym] = true
54
+ elsif values.include?(item)
55
+ raise ArgumentError, 'Missing option value' if remaining.empty? || remaining.first.start_with?('--')
56
+
57
+ key = item.delete_prefix('--').tr('-', '_').to_sym
58
+ value = remaining.shift
59
+ options[key] = options.key?(key) ? [*options[key], value] : value
60
+ else
61
+ raise ArgumentError, 'Unsupported native option; credentials belong in memory, not command arguments'
62
+ end
63
+ end
64
+ { positionals: positionals, options: options }
65
+ end
66
+
67
+ # Carries only the status word, never credential-bearing command data.
68
+ class APDUError < ResponseError
69
+ attr_reader :status
70
+
71
+ def initialize(opts = {})
72
+ @status = opts.fetch(:status)
73
+ super(format('YubiKey APDU failed (status %04x)', @status))
74
+ end
75
+ end
76
+
77
+ public_class_method def self.select(opts = {})
78
+ aid = opts[:aid]
79
+ raise ArgumentError, 'Application AID must contain 1 to 32 bytes' unless aid.is_a?(String) && aid.bytesize.between?(1, 32)
80
+
81
+ apdu(opts.except(:aid).merge(ins: 0xa4, p1: 4, data: aid))
82
+ end
83
+
84
+ public_class_method def self.apdu(opts = {})
85
+ connection = opts[:connection]
86
+ raise ArgumentError, 'A native connection with exchange is required' unless connection.respond_to?(:exchange)
87
+
88
+ timeout = opts.fetch(:timeout, 30)
89
+ maximum = opts.fetch(:max_response, MAX_BYTES)
90
+ raise ArgumentError, 'APDU timeout must be finite positive seconds' unless (timeout.is_a?(Integer) || timeout.is_a?(Float)) && timeout.finite? && timeout.positive?
91
+ raise ArgumentError, 'APDU response limit must be a positive bounded integer' unless maximum.is_a?(Integer) && maximum.between?(1, MAX_BYTES)
92
+
93
+ command_data = opts.fetch(:data, ''.b)
94
+ descriptor = connection.respond_to?(:descriptor) ? connection.descriptor : nil
95
+ forced = descriptor.is_a?(Hash) && descriptor[:extended_apdu] == true
96
+ raise ArgumentError, 'Secure channels require extended APDU framing' if forced && opts[:extended] == false
97
+
98
+ extended = forced ? true : opts.fetch(:extended, false)
99
+ raise ArgumentError, 'APDU data must be a String bounded to 65535 bytes' unless command_data.is_a?(String) && command_data.bytesize <= 65_535
100
+ raise ArgumentError, 'extended must be a Boolean' unless [true, false].include?(extended)
101
+
102
+ continuation = { cla: opts.fetch(:cla, 0), ins: opts.fetch(:get_response_ins, 0xc0), le: opts[:get_response_le] }
103
+ command_frame(continuation)
104
+ frames = []
105
+ offset = 0
106
+ unless extended
107
+ while command_data.bytesize - offset > 255
108
+ frames << opts.merge(cla: opts.fetch(:cla, 0) | 0x10, data: command_data.byteslice(offset, 255), le: nil, extended: false)
109
+ offset += 255
110
+ end
111
+ end
112
+ frames << opts.merge(data: command_data.byteslice(offset..), extended: extended)
113
+ frames = frames.map { |command| { command: command, frame: command_frame(command) } }
114
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
115
+ context = { connection: connection, deadline: deadline, maximum: maximum, continuation: continuation }
116
+ frames[0...-1].each do |frame|
117
+ response = exchange_frame(context.merge(frame))
118
+ raise ResponseError, 'Unexpected data in command-chaining acknowledgement' unless response.empty?
119
+ end
120
+ exchange_frame(context.merge(frames.last))
121
+ end
122
+
123
+ private_class_method def self.exchange_frame(opts = {})
124
+ connection = opts[:connection]
125
+ frame = opts[:frame]
126
+ command = opts[:command]
127
+ maximum = opts[:maximum]
128
+ data = ''.b
129
+ corrected = false
130
+ 256.times do
131
+ remaining = opts[:deadline] - Process.clock_gettime(Process::CLOCK_MONOTONIC)
132
+ raise TimeoutError, 'YubiKey APDU timed out; command state may be uncertain' unless remaining.positive?
133
+
134
+ response = connection.exchange(protocol: :apdu, data: frame, timeout: remaining)
135
+ raise ResponseError, 'Invalid or oversized APDU response' unless response.is_a?(String) && response.bytesize.between?(2, maximum + 2)
136
+
137
+ status = response.byteslice(-2, 2).unpack1('n')
138
+ if (status & 0xff00) == 0x6c00 && !corrected
139
+ length = status & 0xff
140
+ command = command.merge(le: length.zero? ? 256 : length)
141
+ frame = command_frame(command)
142
+ corrected = true
143
+ next
144
+ end
145
+ raise APDUError.new(status: status) unless status == 0x9000 || (status & 0xff00) == 0x6100
146
+
147
+ data << response.b.byteslice(0, response.bytesize - 2)
148
+ raise ResponseError, 'APDU response limit exceeded' if data.bytesize > maximum
149
+ return data if status == 0x9000
150
+
151
+ continuation = opts[:continuation]
152
+ command = continuation.merge(cla: continuation[:cla] & ~0x10, le: continuation[:le].nil? ? status & 0xff : continuation[:le])
153
+ frame = command_frame(command)
154
+ end
155
+ raise ResponseError, 'Too many APDU response fragments'
156
+ end
157
+
158
+ private_class_method def self.command_frame(opts = {})
159
+ header = [opts.fetch(:cla, 0), opts[:ins], opts.fetch(:p1, 0), opts.fetch(:p2, 0)]
160
+ raise ArgumentError, 'APDU header fields must be integer bytes' unless header.all? { |byte| byte.is_a?(Integer) && byte.between?(0, 255) }
161
+
162
+ data = opts.fetch(:data, ''.b)
163
+ le = opts[:le]
164
+ extended = opts.fetch(:extended, false)
165
+ maximum = extended ? 65_535 : 255
166
+ raise ArgumentError, 'APDU data exceeds frame capacity' unless data.is_a?(String) && data.bytesize <= maximum
167
+ raise ArgumentError, 'APDU Le is outside the selected frame capacity' unless le.nil? || (le.is_a?(Integer) && le.between?(0, maximum + 1))
168
+
169
+ frame = header.pack('C*')
170
+ if extended
171
+ frame << "\x00".b << [data.bytesize].pack('n') << data.b unless data.empty?
172
+ unless le.nil?
173
+ frame << "\x00".b if data.empty?
174
+ frame << [le & 0xffff].pack('n')
175
+ end
176
+ else
177
+ frame << [data.bytesize].pack('C') << data.b unless data.empty?
178
+ frame << [le & 0xff].pack('C') unless le.nil?
179
+ frame << "\x00".b if data.empty? && le.nil?
180
+ end
181
+ frame
182
+ end
183
+
184
+ public_class_method def self.tlv(opts = {})
185
+ tag = opts[:tag]
186
+ value = opts[:value]
187
+ raise ArgumentError, 'TLV tag must be a positive integer' unless tag.is_a?(Integer) && tag.positive? && tag <= 0xffffffffff
188
+ raise ArgumentError, 'TLV value must be a bounded String' unless value.is_a?(String) && value.bytesize <= MAX_BYTES
189
+
190
+ hex = tag.to_s(16)
191
+ hex = "0#{hex}" if hex.length.odd?
192
+ tag_bytes = [hex].pack('H*')
193
+ parsed, finish = read_tag(data: tag_bytes, offset: 0)
194
+ raise ArgumentError, 'Invalid BER tag' unless parsed == tag && finish == tag_bytes.bytesize
195
+
196
+ length = value.bytesize
197
+ if length < 128
198
+ length_bytes = [length].pack('C')
199
+ else
200
+ hex = length.to_s(16)
201
+ hex = "0#{hex}" if hex.length.odd?
202
+ encoded = [hex].pack('H*')
203
+ length_bytes = [0x80 | encoded.bytesize].pack('C') + encoded
204
+ end
205
+ raise ArgumentError, 'TLV frame exceeds one MiB' if tag_bytes.bytesize + length_bytes.bytesize + value.bytesize > MAX_BYTES
206
+
207
+ tag_bytes + length_bytes + value.b
208
+ rescue ResponseError
209
+ raise ArgumentError, 'Invalid BER tag', cause: nil
210
+ end
211
+
212
+ public_class_method def self.untlv(opts = {})
213
+ data = opts[:data]
214
+ raise ArgumentError, 'TLV data must be a bounded String' unless data.is_a?(String) && data.bytesize <= MAX_BYTES
215
+
216
+ data = data.b
217
+ rows = []
218
+ offset = 0
219
+ while offset < data.bytesize
220
+ tag, offset = read_tag(data: data, offset: offset)
221
+ length = data.getbyte(offset)
222
+ raise ResponseError, 'Truncated TLV length' unless length
223
+
224
+ offset += 1
225
+ if length >= 128
226
+ count = length & 0x7f
227
+ raise ResponseError, 'Invalid or truncated TLV length' unless count.between?(1, 4) && offset + count <= data.bytesize
228
+
229
+ length = data.byteslice(offset, count).bytes.reduce(0) { |total, byte| (total << 8) | byte }
230
+ offset += count
231
+ end
232
+ raise ResponseError, 'Truncated or oversized TLV value' if length > MAX_BYTES || offset + length > data.bytesize
233
+
234
+ rows << { tag: tag, value: data.byteslice(offset, length) }
235
+ offset += length
236
+ end
237
+ rows
238
+ end
239
+
240
+ public_class_method def self.tlv_map(opts = {})
241
+ untlv(data: opts[:data]).each_with_object({}) do |row, result|
242
+ raise ResponseError, 'Duplicate TLV tag in unique-field response' if result.key?(row[:tag])
243
+
244
+ result[row[:tag]] = row[:value]
245
+ end
246
+ end
247
+
248
+ private_class_method def self.read_tag(opts = {})
249
+ data = opts[:data]
250
+ offset = opts[:offset]
251
+ first = data.getbyte(offset)
252
+ raise ResponseError, 'Invalid or truncated BER tag' unless first && first.positive?
253
+
254
+ tag = first
255
+ offset += 1
256
+ if first.allbits?(0x1f)
257
+ count = 1
258
+ loop do
259
+ byte = data.getbyte(offset)
260
+ raise ResponseError, 'Invalid or truncated BER tag' unless byte && count < 5
261
+
262
+ tag = (tag << 8) | byte
263
+ offset += 1
264
+ count += 1
265
+ break if byte.nobits?(0x80)
266
+ end
267
+ end
268
+ [tag, offset]
269
+ end
270
+
271
+ public_class_method def self.authors
272
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
273
+ end
274
+
275
+ public_class_method def self.help
276
+ puts <<~HELP
277
+ USAGE:
278
+ # Require explicit caller consent before device mutations or authentication.
279
+ #{self}.require_confirm(
280
+ confirm: 'required - literal true authorizing the selected operation'
281
+ )
282
+ # Obtain a secret in memory without implicit prompting or fallback.
283
+ #{self}.secret(
284
+ name: 'required - nonsecret field-name Symbol identifying the credential',
285
+ value: 'optional - explicit nonempty raw secret String',
286
+ secret_provider: 'optional - caller callback receiving the field Symbol when value is nil'
287
+ )
288
+ # Compare explicit firmware tuples without repairing invalid inputs.
289
+ #{self}.version_at_least?(
290
+ version: 'required - observed firmware Array of three integer bytes',
291
+ minimum: 'required - minimum firmware Array of three integer bytes'
292
+ )
293
+ # Parse declared native operation options without normalizing argument values.
294
+ #{self}.parse_args(
295
+ args: 'required - bounded Array of exact argument Strings',
296
+ flags: 'optional - supported Boolean long options, defaults to empty',
297
+ values: 'optional - supported value-taking long options, defaults to empty'
298
+ )
299
+ # Select a smartcard application by its binary AID.
300
+ #{self}.select(
301
+ connection: 'required - native exchange-capable connection',
302
+ aid: 'required - raw application identifier, 1 to 32 bytes',
303
+ timeout: 'optional - finite positive total seconds, defaults to 30'
304
+ )
305
+ # Exchange a smartcard command and assemble bounded response fragments.
306
+ #{self}.apdu(
307
+ connection: 'required - native exchange-capable connection',
308
+ ins: 'required - integer instruction byte',
309
+ cla: 'optional - integer class byte, defaults to zero',
310
+ p1: 'optional - first integer parameter byte, defaults to zero',
311
+ p2: 'optional - second integer parameter byte, defaults to zero',
312
+ data: 'optional - raw command bytes, defaults to empty',
313
+ le: 'optional - response-length integer; nil omits Le',
314
+ extended: 'optional - true selects extended APDU framing; false chains short frames',
315
+ get_response_ins: 'optional - continuation instruction byte, defaults to ISO GET RESPONSE (192)',
316
+ get_response_le: 'optional - fixed short continuation Le; nil uses the card status length',
317
+ timeout: 'optional - finite positive total seconds, defaults to 30',
318
+ max_response: 'optional - bounded maximum response bytes, defaults to one MiB'
319
+ )
320
+ # Encode one BER-TLV record without interpreting its binary value.
321
+ #{self}.tlv(
322
+ tag: 'required - positive BER tag integer',
323
+ value: 'required - raw String; the complete encoded frame is bounded to one MiB'
324
+ )
325
+ # Decode ordered BER-TLV records while retaining repeated tags.
326
+ #{self}.untlv(
327
+ data: 'required - raw BER-TLV String bounded to one MiB'
328
+ )
329
+ # Decode a unique-field response and reject duplicate TLV tags.
330
+ #{self}.tlv_map(
331
+ data: 'required - raw BER-TLV String with unique tags'
332
+ )
333
+ # Return module author information.
334
+ #{self}.authors
335
+ HELP
336
+ nil
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end