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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +8 -8
- data/README.md +37 -0
- data/Rakefile +19 -1
- data/bin/pwn +2 -1
- data/bin/pwn_setup +1 -1
- data/bin/pwn_web_cache_deception +35 -7
- data/build_gem.sh +1 -1
- data/documentation/AI-Integration.md +7 -5
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Backup-Retention.md +82 -0
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +64 -35
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +7 -6
- data/documentation/Installation.md +254 -10
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +17 -0
- data/documentation/Persistence.md +8 -6
- data/documentation/Plugin-Degradation.md +12 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Reinforcement-Learning.md +5 -6
- data/documentation/Runtime-Source-Protection.md +162 -0
- data/documentation/Transparent-Browser.md +11 -0
- data/documentation/Troubleshooting.md +3 -2
- data/documentation/What-is-PWN.md +2 -2
- data/documentation/YubiKey-Vaults.md +183 -0
- data/documentation/YubiKey.md +339 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -128
- data/documentation/diagrams/dot/cron-scheduling.dot +5 -5
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +1245 -22
- data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
- data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
- data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
- data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
- data/etc/default_skills/pwn/backups/SKILL.md +54 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
- data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
- data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
- data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
- data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
- data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
- data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
- data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
- data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
- data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
- data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
- data/lib/pwn/ai/agent/dispatch.rb +26 -15
- data/lib/pwn/ai/agent/extrospection.rb +6 -7
- data/lib/pwn/ai/agent/learning.rb +62 -25
- data/lib/pwn/ai/agent/loop.rb +17 -1
- data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
- data/lib/pwn/ai/agent/policy.rb +5 -0
- data/lib/pwn/ai/agent/policy_evaluation.rb +8 -8
- data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
- data/lib/pwn/ai/agent/request_runtime.rb +122 -18
- data/lib/pwn/ai/agent/reward.rb +5 -3
- data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
- data/lib/pwn/ai/agent/skill_review.rb +9 -5
- data/lib/pwn/ai/agent/swarm.rb +35 -16
- data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
- data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
- data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
- data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/ai/anthropic.rb +8 -33
- data/lib/pwn/ai/cli.rb +34 -5
- data/lib/pwn/ai/gemini.rb +1 -0
- data/lib/pwn/ai/grok.rb +39 -28
- data/lib/pwn/ai/ollama.rb +4 -2
- data/lib/pwn/ai/open_ai.rb +11 -32
- data/lib/pwn/ai/open_web_ui.rb +62 -4
- data/lib/pwn/ai/router.rb +53 -3
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +347 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/cron.rb +120 -49
- data/lib/pwn/migrate.rb +150 -68
- data/lib/pwn/module_skills.rb +7 -0
- data/lib/pwn/plugins/ai_sandbox.rb +538 -1
- data/lib/pwn/plugins/authentication_helper.rb +74 -4
- data/lib/pwn/plugins/basic_auth.rb +2 -1
- data/lib/pwn/plugins/binary_parser.rb +5 -4
- data/lib/pwn/plugins/ghidra.rb +4 -3
- data/lib/pwn/plugins/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +21 -3
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +305 -27
- data/lib/pwn/plugins/repl/ai_console.rb +273 -89
- data/lib/pwn/plugins/repl/ai_swarm.rb +181 -62
- data/lib/pwn/plugins/repl/mesh.rb +3 -16
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +1 -216
- data/lib/pwn/plugins/sandbox/driver.rb +4 -0
- data/lib/pwn/plugins/transparent_browser.rb +215 -0
- data/lib/pwn/plugins/vault.rb +412 -135
- data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
- data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
- data/lib/pwn/plugins/yubikey/management.rb +686 -0
- data/lib/pwn/plugins/yubikey/oath.rb +766 -0
- data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
- data/lib/pwn/plugins/yubikey/otp.rb +897 -0
- data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
- data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
- data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
- data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
- data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
- data/lib/pwn/plugins/yubikey.rb +844 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/redaction.rb +69 -12
- data/lib/pwn/session_trace.rb +4 -0
- data/lib/pwn/sessions.rb +58 -16
- data/lib/pwn/setup.rb +123 -41
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn/www/app_cobalt_io.rb +16 -5
- data/lib/pwn/www/bug_crowd.rb +16 -5
- data/lib/pwn/www/coinbase_pro.rb +16 -5
- data/lib/pwn/www/github.rb +30 -14
- data/lib/pwn/www/paypal.rb +15 -4
- data/lib/pwn/www/synack.rb +16 -5
- data/lib/pwn/www/twitter.rb +17 -6
- data/lib/pwn.rb +1 -0
- data/scripts/build_gate.rb +58 -0
- data/spec/documentation/installation_md_spec.rb +1 -1
- data/spec/fixtures/yubikey/apdu_frames.json +162 -0
- data/spec/integration/authentication_helper_oath_spec.rb +117 -0
- data/spec/integration/build_gate_spec.rb +73 -0
- data/spec/integration/controller_spill_spec.rb +48 -0
- data/spec/integration/harness_process_spec.rb +97 -0
- data/spec/integration/mission_cancel_spec.rb +79 -0
- data/spec/integration/mission_curses_pty_spec.rb +44 -0
- data/spec/integration/persistence_roundtrip_spec.rb +1 -7
- data/spec/integration/readme_security_spec.rb +47 -0
- data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
- data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
- data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
- data/spec/lib/pwn/ai/agent/policy_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
- data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
- data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
- data/spec/lib/pwn/ai/cli_spec.rb +6 -9
- data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
- data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
- data/spec/lib/pwn/ai/router_spec.rb +31 -3
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +114 -0
- data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
- data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
- data/spec/lib/pwn/cron_spec.rb +113 -6
- data/spec/lib/pwn/migrate_spec.rb +108 -5
- data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/module_skills_spec.rb +8 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
- data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
- data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
- data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
- data/spec/lib/pwn/plugins/log_spec.rb +7 -0
- data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
- data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
- data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +125 -78
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +30 -16
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
- data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
- data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
- data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
- data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
- data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
- data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
- data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
- data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
- data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
- data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
- data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
- data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
- data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
- data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
- data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
- data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
- data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
- data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
- data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
- data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
- data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
- data/spec/lib/pwn/redaction_spec.rb +13 -10
- data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_concurrency_spec.rb +201 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +167 -2
- data/spec/lib/pwn/session_cleanup_legacy_spec.rb +101 -0
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +335 -2
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +403 -0
- data/spec/lib/pwn/sessions_spec.rb +112 -0
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- data/spec/lib/pwn/setup_spec.rb +3 -1
- data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/gate_failure_formatter.rb +14 -0
- data/spec/support/harness/cli_faults.rb +13 -9
- data/spec/support/harness/controller_spill.rb +26 -0
- data/spec/support/harness/controller_state.rb +54 -0
- data/spec/support/harness/evaluate.rb +12 -1
- data/spec/support/harness/experiments.rb +1 -1
- data/spec/support/harness/fake_provider.rb +22 -6
- data/spec/support/harness/fault_preload.rb +3 -1
- data/spec/support/harness/mission_console.rb +54 -0
- data/spec/support/harness/mission_curses_pty.rb +41 -0
- data/spec/support/harness/mission_dispatch.rb +30 -0
- data/spec/support/harness/mission_fixture_preload.rb +24 -0
- data/spec/support/harness/mission_job_lifetime.rb +25 -0
- data/spec/support/harness/mission_legacy.rb +25 -0
- data/spec/support/harness/mission_normal_exit.rb +34 -0
- data/spec/support/harness/mission_swarm.rb +23 -0
- data/spec/support/harness/mission_unsupported.rb +29 -0
- data/spec/support/harness/package_acceptance.rb +33 -5
- data/spec/support/harness/session_acceptance.rb +4 -4
- data/spec/support/harness/swarm_acceptance.rb +1 -1
- data/spec/support/harness/swarm_control.rb +44 -0
- data/spec/support/mission_namespace.rb +14 -0
- data/spec/support/mission_pipe_queue.rb +33 -0
- data/third_party/pwn_rdoc.jsonl +599 -15
- 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
|