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,760 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'ffi'
|
|
5
|
+
require 'pwn/plugins/yubikey/transport' if File.file?(File.join(__dir__, '../../../../../lib/pwn/plugins/yubikey/transport.rb'))
|
|
6
|
+
|
|
7
|
+
RSpec.shared_context 'native YubiKey wire fixtures' do
|
|
8
|
+
let(:transport) { PWN::Plugins::YubiKey::Transport }
|
|
9
|
+
|
|
10
|
+
def packet(hex)
|
|
11
|
+
[hex].pack('H*').ljust(64, "\0")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def fake_io(packets: [], features: [])
|
|
15
|
+
Class.new do
|
|
16
|
+
attr_reader :writes, :feature_writes, :closed
|
|
17
|
+
|
|
18
|
+
def initialize(packets, features)
|
|
19
|
+
@packets = packets
|
|
20
|
+
@features = features
|
|
21
|
+
@writes = []
|
|
22
|
+
@feature_writes = []
|
|
23
|
+
@closed = 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def read_packet(_milliseconds)
|
|
27
|
+
@packets.shift || ''.b
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def write_packet(bytes)
|
|
31
|
+
@writes << bytes
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def read_feature
|
|
35
|
+
@features.shift || ("\0" * 8)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def write_feature(bytes)
|
|
39
|
+
@feature_writes << bytes
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def close
|
|
43
|
+
@closed += 1
|
|
44
|
+
end
|
|
45
|
+
end.new(packets, features)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def fake_hid_library
|
|
49
|
+
lib = Module.new
|
|
50
|
+
allow(lib).to receive(:ffi_lib)
|
|
51
|
+
allow(lib).to receive(:attach_function)
|
|
52
|
+
lib
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
RSpec.describe 'PWN::Plugins::YubiKey::Transport native boundary' do
|
|
57
|
+
include_context 'native YubiKey wire fixtures'
|
|
58
|
+
|
|
59
|
+
it 'negotiates CTAPHID INIT with a nonce and reassembles an independent continuation fixture' do
|
|
60
|
+
allow(SecureRandom).to receive(:random_bytes).with(8).and_return('abcdefgh')
|
|
61
|
+
# FIDO CTAPHID: broadcast INIT, 17-byte reply, CID 01020304, v2, fw 5.7.1, CBOR.
|
|
62
|
+
io = fake_io(packets: [packet('ffffffff8600116162636465666768010203040205070104'),
|
|
63
|
+
packet("0102030490003c#{'a5' * 57}"),
|
|
64
|
+
packet('0102030400a5a5a5')])
|
|
65
|
+
conn = transport::Connection.new(descriptor: { protocol: :ctap }, io: io, timeout: 1)
|
|
66
|
+
expect(conn.exchange(protocol: :ctap, command: 0x10, data: "\x01".b)).to eq("\xa5".b * 60)
|
|
67
|
+
expect(io.writes).to eq([packet('ffffffff8600086162636465666768'), packet('0102030490000101')])
|
|
68
|
+
expect(conn.descriptor).to include(protocol: :ctap, version: [5, 7, 1], capabilities: 4)
|
|
69
|
+
conn.close
|
|
70
|
+
conn.close
|
|
71
|
+
expect(io.closed).to eq(1)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'frames OTP serial requests with a fixed CRC fixture and strips the checked response CRC' do
|
|
75
|
+
status = ['0005070109000000'].pack('H*')
|
|
76
|
+
io = fake_io(features: [status, status, status, ['0001020329a70040'].pack('H*'), ['0000000000000040'].pack('H*')])
|
|
77
|
+
conn = transport::Connection.new(descriptor: { protocol: :otp }, io: io, timeout: 1)
|
|
78
|
+
expect(conn.exchange(protocol: :otp, command: 0x10, data: ''.b, response_length: 4)).to eq(['00010203'].pack('H*'))
|
|
79
|
+
# CRC16 of 64 zero bytes is 5b6b (uncomplemented). Only first/last chunks are sent.
|
|
80
|
+
expect(io.feature_writes).to eq(%w[0000000000000080 00106b5b00000089 00000000000000ff].map { |h| [h].pack('H*') })
|
|
81
|
+
conn.close
|
|
82
|
+
expect(io.closed).to eq(1)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'enumerates HID interfaces via FFI, preserves wide serial text, and frees all native resources' do
|
|
86
|
+
native = transport::NativeLibrary.new(:hid, library: fake_hid_library, platform: :linux)
|
|
87
|
+
lib = native.library
|
|
88
|
+
text = FFI::MemoryPointer.new(:uint32, 7)
|
|
89
|
+
text.write_array_of_uint32(' 0007 '.codepoints + [0])
|
|
90
|
+
path = FFI::MemoryPointer.from_string('/fake/hid')
|
|
91
|
+
# Independent C hid_device_info layout (HIDAPI 0.14 ABI prefix).
|
|
92
|
+
struct = Class.new(FFI::Struct) do
|
|
93
|
+
layout :path, :pointer, :vendor_id, :ushort, :product_id, :ushort,
|
|
94
|
+
:serial_number, :pointer, :release_number, :ushort, :manufacturer_string, :pointer,
|
|
95
|
+
:product_string, :pointer, :usage_page, :ushort, :usage, :ushort,
|
|
96
|
+
:interface_number, :int, :next, :pointer
|
|
97
|
+
end.new
|
|
98
|
+
struct[:path] = path
|
|
99
|
+
struct[:vendor_id] = 0x1050
|
|
100
|
+
struct[:product_id] = 0x0407
|
|
101
|
+
struct[:serial_number] = text
|
|
102
|
+
struct[:usage_page] = 0xf1d0
|
|
103
|
+
struct[:usage] = 1
|
|
104
|
+
struct[:next] = FFI::Pointer::NULL
|
|
105
|
+
expect(lib).to receive(:hid_init).ordered.and_return(0)
|
|
106
|
+
expect(lib).to receive(:hid_enumerate).with(0x1050, 0).ordered.and_return(struct.pointer)
|
|
107
|
+
expect(lib).to receive(:hid_free_enumeration).with(struct.pointer).ordered
|
|
108
|
+
expect(lib).to receive(:hid_exit).ordered.and_return(0)
|
|
109
|
+
expect(native.devices).to include(include(protocol: :ctap, device_path: '/fake/hid', serial: ' 0007 '))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'uses HIDAPI report-id prefixes and exact transfer lengths at the C boundary' do
|
|
113
|
+
native = transport::NativeLibrary.new(:hid, library: fake_hid_library, platform: :linux)
|
|
114
|
+
lib = native.library
|
|
115
|
+
ptr = FFI::MemoryPointer.new(:char)
|
|
116
|
+
expect(lib).to receive(:hid_init).and_return(0)
|
|
117
|
+
expect(lib).to receive(:hid_open_path).with('/fake/hid').and_return(ptr)
|
|
118
|
+
expect(lib).to receive(:hid_write) do |handle, buffer, length|
|
|
119
|
+
expect(handle).to eq(ptr)
|
|
120
|
+
expect(length).to eq(65)
|
|
121
|
+
expect(buffer.read_bytes(length)).to eq("\0#{packet('01020304810000')}")
|
|
122
|
+
65
|
|
123
|
+
end
|
|
124
|
+
expect(lib).to receive(:hid_get_feature_report) do |handle, buffer, length|
|
|
125
|
+
expect(handle).to eq(ptr)
|
|
126
|
+
expect(length).to eq(9)
|
|
127
|
+
expect(buffer.get_uint8(0)).to eq(0)
|
|
128
|
+
buffer.put_bytes(0, ['000005070109000000'].pack('H*'))
|
|
129
|
+
9
|
|
130
|
+
end
|
|
131
|
+
expect(lib).to receive(:hid_close).with(ptr).ordered
|
|
132
|
+
expect(lib).to receive(:hid_exit).ordered.and_return(0)
|
|
133
|
+
io = native.open({ device_path: '/fake/hid', protocol: :ctap })
|
|
134
|
+
io.write_packet(packet('01020304810000'))
|
|
135
|
+
expect(io.read_feature).to eq(['0005070109000000'].pack('H*'))
|
|
136
|
+
io.close
|
|
137
|
+
io.close
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'performs PC/SC exclusive connect and a raw APDU exchange through the C ABI' do
|
|
141
|
+
lib = fake_hid_library
|
|
142
|
+
pci = FFI::MemoryPointer.new(:ulong, 2)
|
|
143
|
+
pci.write_array_of_ulong([2, FFI.type_size(:ulong) * 2])
|
|
144
|
+
allow(lib).to receive(:ffi_libraries).and_return([double(find_symbol: pci)])
|
|
145
|
+
native = transport::NativeLibrary.new(:pcsc, library: lib, platform: :linux)
|
|
146
|
+
expect(lib).to receive(:SCardEstablishContext).with(2, nil, nil, anything) do |_, _, _, ptr|
|
|
147
|
+
ptr.write_long(123)
|
|
148
|
+
0
|
|
149
|
+
end
|
|
150
|
+
expect(lib).to receive(:SCardConnect) do |context, reader, share, preferred, card, protocol| # rubocop:disable Metrics/ParameterLists -- Mirrors the native C signature.
|
|
151
|
+
expect([context, reader.read_string, share, preferred]).to eq([123, 'Yubico YubiKey synthetic', 1, 3])
|
|
152
|
+
card.write_long(456)
|
|
153
|
+
protocol.write_ulong(2)
|
|
154
|
+
0
|
|
155
|
+
end
|
|
156
|
+
expect(lib).to receive(:SCardGetAttrib) do |card, attribute, atr, length|
|
|
157
|
+
expect([card, attribute]).to eq([456, 0x00090303])
|
|
158
|
+
atr.put_bytes(0, ['3bf81300008131fe15597562696b657934d4'].pack('H*'))
|
|
159
|
+
length.write_ulong(17)
|
|
160
|
+
0
|
|
161
|
+
end
|
|
162
|
+
expect(lib).to receive(:SCardTransmit) do |card, send_pci, data, size, receive_pci, output, length| # rubocop:disable Metrics/ParameterLists -- Mirrors the native C signature.
|
|
163
|
+
expect([card, send_pci, data.read_bytes(size), receive_pci]).to eq([456, pci, ['00a4040000'].pack('H*'), nil])
|
|
164
|
+
output.put_bytes(0, ['0102039000'].pack('H*'))
|
|
165
|
+
length.write_ulong(5)
|
|
166
|
+
0
|
|
167
|
+
end
|
|
168
|
+
expect(lib).to receive(:SCardDisconnect).with(456, 0).ordered.and_return(0)
|
|
169
|
+
expect(lib).to receive(:SCardReleaseContext).with(123).ordered.and_return(0)
|
|
170
|
+
io = native.open({ reader: 'Yubico YubiKey synthetic', protocol: :apdu })
|
|
171
|
+
conn = transport::Connection.new(descriptor: { protocol: :apdu }, io: io, timeout: 1)
|
|
172
|
+
expect(conn.descriptor[:transport]).to eq(:usb)
|
|
173
|
+
expect(conn.exchange(protocol: :apdu, data: ['00a4040000'].pack('H*'))).to eq(['0102039000'].pack('H*'))
|
|
174
|
+
conn.close
|
|
175
|
+
conn.close
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it 'uses platform-specific PC/SC DWORD and handle widths without establishing contexts' do
|
|
179
|
+
{ linux: %i[ulong long long], macos: %i[uint32 int32 int32], windows: %i[uint32 uintptr_t int32] }.each do |platform, (dword, handle, result)|
|
|
180
|
+
lib = fake_hid_library
|
|
181
|
+
allow(lib).to receive(:ffi_convention)
|
|
182
|
+
allow(lib).to receive(:ffi_libraries).and_return([double(find_symbol: FFI::Pointer.new(1))])
|
|
183
|
+
expect(lib).to receive(:attach_function).with(:SCardEstablishContext, [dword, :pointer, :pointer, :pointer], result, blocking: true)
|
|
184
|
+
expect(lib).to receive(:attach_function).with(:SCardTransmit, [handle, :pointer, :pointer, dword, :pointer, :pointer, :pointer], result, blocking: true)
|
|
185
|
+
expect(lib).not_to receive(:SCardEstablishContext)
|
|
186
|
+
transport::NativeLibrary.new(:pcsc, library: lib, platform: platform)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'bounds a blocked C call without freeing its handle early and closes it after the call returns' do
|
|
191
|
+
entered = Queue.new
|
|
192
|
+
release = Queue.new
|
|
193
|
+
io = fake_io
|
|
194
|
+
allow(io).to receive(:transmit) do |_data|
|
|
195
|
+
entered << true
|
|
196
|
+
release.pop
|
|
197
|
+
"\x90\x00".b
|
|
198
|
+
end
|
|
199
|
+
conn = transport::Connection.new(descriptor: { protocol: :apdu }, io: io, timeout: 0.04)
|
|
200
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
201
|
+
timer = Thread.new do
|
|
202
|
+
entered.pop
|
|
203
|
+
sleep(0.2)
|
|
204
|
+
release << true
|
|
205
|
+
end
|
|
206
|
+
expect { conn.exchange(protocol: :apdu, data: "\x00\xa4\x04\x00".b) }.to raise_error(PWN::Plugins::YubiKey::TimeoutError)
|
|
207
|
+
expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - start).to be < 0.18
|
|
208
|
+
expect(io.closed).to eq(0)
|
|
209
|
+
expect { conn.exchange(protocol: :apdu, data: "\x00\xa4\x04\x00".b) }.to raise_error(PWN::Plugins::YubiKey::Error, /closed/)
|
|
210
|
+
timer.join
|
|
211
|
+
conn.close
|
|
212
|
+
expect(io.closed).to eq(1)
|
|
213
|
+
ensure
|
|
214
|
+
release << true if release
|
|
215
|
+
timer&.join(1)
|
|
216
|
+
conn&.close
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it 'cancels CTAPHID on touch-wait interruption and closes exactly once' do
|
|
220
|
+
allow(SecureRandom).to receive(:random_bytes).and_return('abcdefgh')
|
|
221
|
+
io = fake_io(packets: [packet('ffffffff8600116162636465666768010203040205070104'), packet('01020304bb000102')])
|
|
222
|
+
conn = transport::Connection.new(descriptor: { protocol: :ctap }, io: io, timeout: 1)
|
|
223
|
+
expect { conn.exchange(protocol: :ctap, command: 0x10, data: ''.b, on_keepalive: ->(_status) { raise Interrupt }) }.to raise_error(Interrupt)
|
|
224
|
+
conn.close
|
|
225
|
+
expect(io.writes.last).to eq(packet('01020304910000'))
|
|
226
|
+
expect(io.closed).to eq(1)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it 'selects exact serial strings and never chooses the first of multiple keys' do
|
|
230
|
+
records = [{ backend: :hid, protocol: :otp, serial: '0007', device_path: '/fake/one' },
|
|
231
|
+
{ backend: :hid, protocol: :otp, serial: '7', device_path: '/fake/two' }]
|
|
232
|
+
native = double(devices: records)
|
|
233
|
+
allow(transport).to receive(:native).with(kind: :hid).and_return(native)
|
|
234
|
+
allow(native).to receive(:open) { raise 'must not open an ambiguous selection' }
|
|
235
|
+
expect { transport.open(application: :otp) }.to raise_error(PWN::Plugins::YubiKey::Error, /multiple/i)
|
|
236
|
+
expect { transport.open(application: :otp, serial: 7) }.to raise_error(ArgumentError, /serial/)
|
|
237
|
+
io = fake_io
|
|
238
|
+
allow(native).to receive(:open).with(records.first).and_return(io)
|
|
239
|
+
conn = transport.open(application: :otp, serial: '0007')
|
|
240
|
+
expect(conn.descriptor[:serial]).to eq('0007')
|
|
241
|
+
conn.close
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'selects serial-less Security Keys by path and reader-selected OTP over PC/SC' do
|
|
245
|
+
hid = double(devices: [{ backend: :hid, protocol: :ctap, serial: nil, device_path: '/fake/security-key' }])
|
|
246
|
+
pcsc = double(devices: [{ backend: :pcsc, protocol: :apdu, serial: nil, reader: 'NFC test reader' }])
|
|
247
|
+
allow(transport).to receive(:native).with(kind: :hid).and_return(hid)
|
|
248
|
+
allow(transport).to receive(:native).with(kind: :pcsc).and_return(pcsc)
|
|
249
|
+
allow(SecureRandom).to receive(:random_bytes).with(8).and_return('abcdefgh')
|
|
250
|
+
allow(hid).to receive(:open).and_return(fake_io(packets: [packet('ffffffff8600116162636465666768010203040205070104')]))
|
|
251
|
+
allow(pcsc).to receive(:open).and_return(fake_io)
|
|
252
|
+
conn = transport.open(application: :fido, device_path: '/fake/security-key')
|
|
253
|
+
expect(conn.descriptor[:protocol]).to eq(:ctap)
|
|
254
|
+
conn.close
|
|
255
|
+
conn = transport.open(application: :otp, reader: 'NFC test reader')
|
|
256
|
+
expect(conn.descriptor[:protocol]).to eq(:apdu)
|
|
257
|
+
conn.close
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it 'enumerates PC/SC reader strings without connecting and releases its context' do
|
|
261
|
+
lib = fake_hid_library
|
|
262
|
+
allow(lib).to receive(:ffi_libraries).and_return([double(find_symbol: FFI::Pointer.new(1))])
|
|
263
|
+
native = transport::NativeLibrary.new(:pcsc, library: lib, platform: :linux)
|
|
264
|
+
names = "Yubico YubiKey test\0NFC test\0\0".b
|
|
265
|
+
expect(lib).to receive(:SCardEstablishContext) { |_, _, _, ptr|
|
|
266
|
+
ptr.write_long(123)
|
|
267
|
+
0
|
|
268
|
+
}
|
|
269
|
+
expect(lib).to(receive(:SCardListReaders).with(123, nil, nil, anything).ordered do |_, _, _, len|
|
|
270
|
+
len.write_ulong(names.bytesize)
|
|
271
|
+
0
|
|
272
|
+
end)
|
|
273
|
+
expect(lib).to(receive(:SCardListReaders).with(123, nil, kind_of(FFI::MemoryPointer), anything).ordered do |_, _, ptr, len|
|
|
274
|
+
ptr.put_bytes(0, names)
|
|
275
|
+
len.write_ulong(names.bytesize)
|
|
276
|
+
0
|
|
277
|
+
end)
|
|
278
|
+
expect(lib).to receive(:SCardReleaseContext).with(123).ordered.and_return(0)
|
|
279
|
+
expect(lib).not_to receive(:SCardConnect)
|
|
280
|
+
expect(native.devices.map { |entry| entry[:reader] }).to eq(['Yubico YubiKey test', 'NFC test'])
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it 'resolves a PC/SC serial using read-only OTP APDUs and rejects alternate spellings' do
|
|
284
|
+
entry = { backend: :pcsc, protocol: :apdu, reader: 'Yubico YubiKey synthetic', serial: nil }
|
|
285
|
+
binding = double(devices: [entry])
|
|
286
|
+
allow(transport).to receive(:native).with(kind: :pcsc).and_return(binding)
|
|
287
|
+
io = fake_io
|
|
288
|
+
allow(binding).to receive(:open).and_return(io)
|
|
289
|
+
expect(io).to receive(:transmit).with(['00a4040007a0000005272001'].pack('H*')).and_return(['0507010900009000'].pack('H*'))
|
|
290
|
+
expect(io).to receive(:transmit).with(['0001100000'].pack('H*')).and_return(['000000079000'].pack('H*'))
|
|
291
|
+
conn = transport.open(application: :piv, reader: entry[:reader], serial: '7')
|
|
292
|
+
expect(conn.descriptor[:serial]).to eq('7')
|
|
293
|
+
conn.close
|
|
294
|
+
expect(io.closed).to eq(1)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
it 'does not accept an unobservable PC/SC serial or coerce leading zero selectors' do
|
|
298
|
+
entry = { backend: :pcsc, protocol: :apdu, reader: 'Yubico YubiKey synthetic', serial: nil }
|
|
299
|
+
binding = double(devices: [entry])
|
|
300
|
+
allow(transport).to receive(:native).with(kind: :pcsc).and_return(binding)
|
|
301
|
+
io = fake_io
|
|
302
|
+
allow(binding).to receive(:open).and_return(io)
|
|
303
|
+
allow(io).to receive(:transmit).and_return(['9000'].pack('H*'), ['000000079000'].pack('H*'))
|
|
304
|
+
expect { transport.open(application: :piv, reader: entry[:reader], serial: '0007') }.to raise_error(PWN::Plugins::YubiKey::Error, /serial|matching/i)
|
|
305
|
+
expect(io.closed).to eq(1)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it 'finds a PC/SC serial through management when the OTP applet is disabled' do
|
|
309
|
+
require 'pwn/plugins/yubikey/protocol'
|
|
310
|
+
entry = { backend: :pcsc, protocol: :apdu, reader: 'NFC test reader', serial: nil }
|
|
311
|
+
io = fake_io
|
|
312
|
+
binding = double(devices: [entry], open: io)
|
|
313
|
+
allow(transport).to receive(:native).with(kind: :pcsc).and_return(binding)
|
|
314
|
+
expect(io).to receive(:transmit).with(['00a4040007a0000005272001'].pack('H*')).ordered.and_return(['6a82'].pack('H*'))
|
|
315
|
+
expect(io).to receive(:transmit).with(['00a4040008a000000527471117'].pack('H*')).ordered.and_return("5.7.1\x90\0".b)
|
|
316
|
+
expect(io).to receive(:transmit).with(['001d000000'].pack('H*')).ordered.and_return(['060204000000079000'].pack('H*'))
|
|
317
|
+
conn = transport.open(application: :piv, reader: entry[:reader], serial: '7')
|
|
318
|
+
expect(conn.descriptor[:serial]).to eq('7')
|
|
319
|
+
conn.close
|
|
320
|
+
expect(io.closed).to eq(1)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it 'binds HIDAPI lazily using a fake FFI shared-library boundary' do
|
|
324
|
+
ffi_library = Module.new
|
|
325
|
+
allow(ffi_library).to receive(:extend)
|
|
326
|
+
expect(ffi_library).to receive(:ffi_lib).with(kind_of(Array))
|
|
327
|
+
expect(ffi_library).to receive(:attach_function).with(:hid_init, [], :int, blocking: true)
|
|
328
|
+
allow(ffi_library).to receive(:attach_function)
|
|
329
|
+
expect(ffi_library).not_to receive(:hid_init)
|
|
330
|
+
expect(ffi_library).not_to receive(:hid_enumerate)
|
|
331
|
+
bound = transport::NativeLibrary.new(:hid, library: ffi_library, platform: :linux)
|
|
332
|
+
expect(bound.library).to eq(ffi_library)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it 'probes native library symbols without initializing or discovering hardware' do
|
|
336
|
+
library = double('native shared library')
|
|
337
|
+
expect(library).not_to receive(:hid_init)
|
|
338
|
+
expect(library).not_to receive(:hid_enumerate)
|
|
339
|
+
expect(library).not_to receive(:SCardEstablishContext)
|
|
340
|
+
allow(transport).to receive(:native).with(kind: :hid).and_return(library)
|
|
341
|
+
expect(transport.available?(backend: :hid)).to be(true)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
RSpec.describe 'PWN::Plugins::YubiKey::Transport dependency and lifetime safety' do
|
|
346
|
+
include_context 'native YubiKey wire fixtures'
|
|
347
|
+
|
|
348
|
+
it 'documents its public selectors, protocol contract and driver timeout limit' do
|
|
349
|
+
expect { transport.help }.to output(/available\?.*devices.*open.*driver/m).to_stdout
|
|
350
|
+
expect(transport.authors).to include('0day Inc.')
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
it 'probes only PC/SC for an APDU-only application and never initializes hardware' do
|
|
354
|
+
expect(transport).to receive(:native).with(kind: :pcsc).and_return(double('loaded PC/SC symbols'))
|
|
355
|
+
expect(transport).not_to receive(:native).with(kind: :hid)
|
|
356
|
+
expect(transport.available?(application: :piv)).to be(true)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
it 'rejects explicit false backend and application selectors before any native lookup' do
|
|
360
|
+
expect(transport).not_to receive(:native)
|
|
361
|
+
expect { transport.available?(backend: false) }.to raise_error(ArgumentError)
|
|
362
|
+
expect { transport.available?(application: false) }.to raise_error(ArgumentError)
|
|
363
|
+
expect { transport.devices(backend: false) }.to raise_error(ArgumentError)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
it 'reports absent shared libraries without touching an initialization entry point' do
|
|
367
|
+
lib = fake_hid_library
|
|
368
|
+
allow(lib).to receive(:ffi_lib).and_raise(LoadError, 'synthetic absence')
|
|
369
|
+
expect(lib).not_to receive(:hid_init)
|
|
370
|
+
expect { transport::NativeLibrary.new(:hid, library: lib) }.to raise_error(PWN::Plugins::YubiKey::DependencyError, /HIDAPI/)
|
|
371
|
+
allow(transport).to receive(:native).and_raise(PWN::Plugins::YubiKey::DependencyError, 'missing native library')
|
|
372
|
+
expect(transport.available?).to be(false)
|
|
373
|
+
expect { transport.devices }.to raise_error(PWN::Plugins::YubiKey::DependencyError)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
it 'balances HIDAPI initialization when a selected path cannot be opened' do
|
|
377
|
+
native = transport::NativeLibrary.new(:hid, library: fake_hid_library)
|
|
378
|
+
expect(native.library).to receive(:hid_init).and_return(0)
|
|
379
|
+
expect(native.library).to receive(:hid_open_path).and_return(FFI::Pointer::NULL)
|
|
380
|
+
expect(native.library).not_to receive(:hid_close)
|
|
381
|
+
expect(native.library).to receive(:hid_exit).and_return(0)
|
|
382
|
+
expect { native.open({ device_path: '/fake/unavailable' }) }.to raise_error(PWN::Plugins::YubiKey::CommandError, /permissions/)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
it 'does not exit HIDAPI while another handle still owns a reference' do
|
|
386
|
+
native = transport::NativeLibrary.new(:hid, library: fake_hid_library)
|
|
387
|
+
lib = native.library
|
|
388
|
+
one = FFI::MemoryPointer.new(:char)
|
|
389
|
+
two = FFI::MemoryPointer.new(:char)
|
|
390
|
+
expect(lib).to receive(:hid_init).once.and_return(0)
|
|
391
|
+
expect(lib).to receive(:hid_open_path).with('/fake/1').and_return(one)
|
|
392
|
+
expect(lib).to receive(:hid_open_path).with('/fake/2').and_return(two)
|
|
393
|
+
expect(lib).to receive(:hid_close).with(one).ordered
|
|
394
|
+
expect(lib).to receive(:hid_close).with(two).ordered
|
|
395
|
+
expect(lib).to receive(:hid_exit).ordered.and_return(0)
|
|
396
|
+
first = native.open({ device_path: '/fake/1' })
|
|
397
|
+
second = native.open({ device_path: '/fake/2' })
|
|
398
|
+
first.close
|
|
399
|
+
second.close
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
it 'decodes Windows UTF-16 surrogate pairs and Unix wchar strings without stripping serials' do
|
|
403
|
+
{ windows: :uint16, linux: :uint32, macos: :uint32 }.each do |platform, type|
|
|
404
|
+
native = transport::NativeLibrary.new(:hid, library: fake_hid_library, platform: platform)
|
|
405
|
+
value = " 00\u{1f511} "
|
|
406
|
+
units = type == :uint16 ? value.encode(Encoding::UTF_16LE).unpack('v*') : value.codepoints
|
|
407
|
+
memory = FFI::MemoryPointer.new(type, units.length + 1)
|
|
408
|
+
memory.public_send(:"write_array_of_#{type}", units + [0])
|
|
409
|
+
expect(transport::HIDInventory.new(native).send(:wide_string, memory)).to eq(value)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
it 'releases a PC/SC context after an exclusive connect failure without disconnecting an invalid handle' do
|
|
414
|
+
lib = fake_hid_library
|
|
415
|
+
allow(lib).to receive(:ffi_libraries).and_return([double(find_symbol: FFI::Pointer.new(1))])
|
|
416
|
+
native = transport::NativeLibrary.new(:pcsc, library: lib, platform: :linux)
|
|
417
|
+
expect(lib).to receive(:SCardEstablishContext) { |_, _, _, ptr|
|
|
418
|
+
ptr.write_long(123)
|
|
419
|
+
0
|
|
420
|
+
}
|
|
421
|
+
expect(lib).to receive(:SCardConnect).and_return(0x8010000b)
|
|
422
|
+
expect(lib).not_to receive(:SCardDisconnect)
|
|
423
|
+
expect(lib).to receive(:SCardReleaseContext).with(123).and_return(0)
|
|
424
|
+
expect { native.open({ reader: 'Yubico YubiKey fixture' }) }.to raise_error(PWN::Plugins::YubiKey::CommandError, /exclusive/)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
it 'rejects unknown applications, conflicting selectors and empty discovery without choosing another reader' do
|
|
428
|
+
binding = double(devices: [])
|
|
429
|
+
allow(transport).to receive(:native).and_return(binding)
|
|
430
|
+
expect(binding).not_to receive(:open)
|
|
431
|
+
expect { transport.open(application: :unknown) }.to raise_error(ArgumentError)
|
|
432
|
+
expect { transport.open(application: :piv, reader: 'one', device_path: 'two') }.to raise_error(ArgumentError)
|
|
433
|
+
expect { transport.open(application: :fido) }.to raise_error(PWN::Plugins::YubiKey::Error, /No matching/)
|
|
434
|
+
allow(binding).to receive(:devices).and_return([{ backend: :pcsc, protocol: :apdu, reader: 'Other card', serial: nil }])
|
|
435
|
+
expect { transport.open(application: :piv) }.to raise_error(PWN::Plugins::YubiKey::Error, /No matching/)
|
|
436
|
+
expect { transport.open(application: :piv, reader: 'missing') }.to raise_error(PWN::Plugins::YubiKey::Error, /No matching/)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
it 'attempts every handle cleanup when ambiguous serial probes encounter a close failure' do
|
|
440
|
+
entries = [1, 2].map { |number| { backend: :pcsc, protocol: :apdu, reader: "Yubico YubiKey fixture #{number}", serial: nil } }
|
|
441
|
+
first = fake_io
|
|
442
|
+
second = fake_io
|
|
443
|
+
[first, second].each do |io|
|
|
444
|
+
allow(io).to receive(:transmit).and_return(['9000'].pack('H*'), ['000000079000'].pack('H*'))
|
|
445
|
+
end
|
|
446
|
+
allow(first).to receive(:close).and_wrap_original do |method|
|
|
447
|
+
method.call
|
|
448
|
+
raise IOError, 'synthetic close failure'
|
|
449
|
+
end
|
|
450
|
+
binding = double(devices: entries)
|
|
451
|
+
allow(binding).to receive(:open).and_return(first, second)
|
|
452
|
+
allow(transport).to receive(:native).with(kind: :pcsc).and_return(binding)
|
|
453
|
+
expect { transport.open(application: :piv, serial: '7') }.to raise_error(PWN::Plugins::YubiKey::Error, /Multiple/)
|
|
454
|
+
expect([first.closed, second.closed]).to eq([1, 1])
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it 'uses bounded HIDAPI reads and zero report IDs for OTP writes at the native boundary' do
|
|
458
|
+
native = transport::NativeLibrary.new(:hid, library: fake_hid_library)
|
|
459
|
+
lib = native.library
|
|
460
|
+
handle = FFI::MemoryPointer.new(:char)
|
|
461
|
+
expect(lib).to receive(:hid_init).and_return(0)
|
|
462
|
+
expect(lib).to receive(:hid_open_path).and_return(handle)
|
|
463
|
+
expect(lib).to receive(:hid_read_timeout).with(handle, anything, 64, 17) do |_, buffer, _, _|
|
|
464
|
+
buffer.put_bytes(0, packet('0102030490000100'))
|
|
465
|
+
64
|
|
466
|
+
end
|
|
467
|
+
expect(lib).to receive(:hid_send_feature_report).with(handle, anything, 9) do |_, buffer, length|
|
|
468
|
+
expect(buffer.read_bytes(length)).to eq(['0000000000000000ff'].pack('H*'))
|
|
469
|
+
9
|
|
470
|
+
end
|
|
471
|
+
expect(lib).to receive(:hid_close).with(handle)
|
|
472
|
+
expect(lib).to receive(:hid_exit).and_return(0)
|
|
473
|
+
io = native.open({ device_path: '/fake/hid' })
|
|
474
|
+
expect(io.read_packet(17)).to eq(packet('0102030490000100'))
|
|
475
|
+
io.write_feature(['00000000000000ff'].pack('H*'))
|
|
476
|
+
io.close
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
it 'rejects partial HID writes and truncated input reports without retrying the C call' do
|
|
480
|
+
native = transport::NativeLibrary.new(:hid, library: fake_hid_library)
|
|
481
|
+
lib = native.library
|
|
482
|
+
handle = FFI::MemoryPointer.new(:char)
|
|
483
|
+
allow(lib).to receive(:hid_init).and_return(0)
|
|
484
|
+
allow(lib).to receive(:hid_open_path).and_return(handle)
|
|
485
|
+
expect(lib).to receive(:hid_write).once.and_return(64)
|
|
486
|
+
expect(lib).to receive(:hid_read_timeout).once.and_return(63)
|
|
487
|
+
expect(lib).to receive(:hid_get_feature_report).once.and_return(8)
|
|
488
|
+
expect(lib).to receive(:hid_close).with(handle)
|
|
489
|
+
expect(lib).to receive(:hid_exit).and_return(0)
|
|
490
|
+
io = native.open({ device_path: '/fake/hid' })
|
|
491
|
+
expect { io.write_packet(packet('0102030490000100')) }.to raise_error(PWN::Plugins::YubiKey::CommandError, /incomplete/)
|
|
492
|
+
expect { io.read_packet(20) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /length/)
|
|
493
|
+
expect { io.read_feature }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /length/)
|
|
494
|
+
io.close
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
it 'closes an acquired native handle that arrives after its opening deadline' do
|
|
498
|
+
release = Queue.new
|
|
499
|
+
cleaned = Queue.new
|
|
500
|
+
io = fake_io
|
|
501
|
+
allow(io).to(receive(:close).and_wrap_original do |method|
|
|
502
|
+
method.call
|
|
503
|
+
cleaned << true
|
|
504
|
+
end)
|
|
505
|
+
entry = { backend: :hid, protocol: :otp, serial: '7', device_path: '/fake/late' }
|
|
506
|
+
binding = double(devices: [entry])
|
|
507
|
+
allow(binding).to receive(:open) {
|
|
508
|
+
release.pop
|
|
509
|
+
io
|
|
510
|
+
}
|
|
511
|
+
allow(transport).to receive(:native).with(kind: :hid).and_return(binding)
|
|
512
|
+
expect { transport.open(application: :otp, serial: '7', timeout: 0.03) }.to raise_error(PWN::Plugins::YubiKey::TimeoutError)
|
|
513
|
+
expect(io.closed).to eq(0)
|
|
514
|
+
release << true
|
|
515
|
+
expect(cleaned.pop(timeout: 1)).to be(true)
|
|
516
|
+
expect(io.closed).to eq(1)
|
|
517
|
+
ensure
|
|
518
|
+
release << true if release
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
it 'keeps the public discovery waiter bounded while the native enumerator still owns its resources' do
|
|
522
|
+
release = Queue.new
|
|
523
|
+
cleaned = Queue.new
|
|
524
|
+
binding = double
|
|
525
|
+
allow(binding).to receive(:devices) do
|
|
526
|
+
release.pop
|
|
527
|
+
[]
|
|
528
|
+
ensure
|
|
529
|
+
cleaned << true
|
|
530
|
+
end
|
|
531
|
+
allow(transport).to receive(:native).with(kind: :hid).and_return(binding)
|
|
532
|
+
expect { transport.devices(backend: :hid, timeout: 0.03) }.to raise_error(PWN::Plugins::YubiKey::TimeoutError)
|
|
533
|
+
expect(cleaned.empty?).to be(true)
|
|
534
|
+
release << true
|
|
535
|
+
expect(cleaned.pop(timeout: 1)).to be(true)
|
|
536
|
+
ensure
|
|
537
|
+
release << true if release
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
it 'requires exact Windows wide reader names and uint32 reader-list lengths' do
|
|
541
|
+
lib = fake_hid_library
|
|
542
|
+
allow(lib).to receive(:ffi_convention)
|
|
543
|
+
allow(lib).to receive(:ffi_libraries).and_return([double(find_symbol: FFI::Pointer.new(1))])
|
|
544
|
+
expect(lib).to receive(:attach_function).with(:SCardListReaders, :SCardListReadersW, %i[uintptr_t pointer pointer pointer], :int32, blocking: true)
|
|
545
|
+
native = transport::NativeLibrary.new(:pcsc, library: lib, platform: :windows)
|
|
546
|
+
names = "NFC \u{1f511}\0\0".encode(Encoding::UTF_16LE).b
|
|
547
|
+
expect(lib).to receive(:SCardEstablishContext) do |_, _, _, pointer|
|
|
548
|
+
pointer.write(:uintptr_t, 123)
|
|
549
|
+
0
|
|
550
|
+
end
|
|
551
|
+
expect(lib).to receive(:SCardListReaders).with(123, nil, nil, anything) do |_, _, _, length|
|
|
552
|
+
length.write_uint32(names.bytesize / 2)
|
|
553
|
+
0
|
|
554
|
+
end
|
|
555
|
+
expect(lib).to receive(:SCardListReaders).with(123, nil, kind_of(FFI::MemoryPointer), anything) do |_, _, buffer, length|
|
|
556
|
+
buffer.put_bytes(0, names)
|
|
557
|
+
length.write_uint32(names.bytesize / 2)
|
|
558
|
+
0
|
|
559
|
+
end
|
|
560
|
+
expect(lib).to receive(:SCardReleaseContext).with(123).and_return(0)
|
|
561
|
+
expect(native.devices.first[:reader]).to eq("NFC \u{1f511}")
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
it 'rejects simultaneous exchanges instead of interleaving packets and closes after the active call' do
|
|
565
|
+
entered = Queue.new
|
|
566
|
+
release = Queue.new
|
|
567
|
+
io = fake_io
|
|
568
|
+
allow(io).to receive(:transmit) { |_data|
|
|
569
|
+
entered << true
|
|
570
|
+
release.pop
|
|
571
|
+
"\x90\0".b
|
|
572
|
+
}
|
|
573
|
+
conn = transport::Connection.new(descriptor: { protocol: :apdu }, io: io, timeout: 1)
|
|
574
|
+
first = Thread.new { conn.exchange(protocol: :apdu, data: 'abcd') }
|
|
575
|
+
entered.pop
|
|
576
|
+
expect { conn.exchange(protocol: :apdu, data: 'abcd') }.to raise_error(PWN::Plugins::YubiKey::CommandError, /active/)
|
|
577
|
+
release << true
|
|
578
|
+
expect(first.value).to eq("\x90\0".b)
|
|
579
|
+
conn.close
|
|
580
|
+
expect(io.closed).to eq(1)
|
|
581
|
+
ensure
|
|
582
|
+
release << true if release
|
|
583
|
+
first&.join(1)
|
|
584
|
+
conn&.close
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
RSpec.describe 'PWN::Plugins::YubiKey::Transport framing failures and limits' do
|
|
589
|
+
include_context 'native YubiKey wire fixtures'
|
|
590
|
+
|
|
591
|
+
let(:init_reply) { packet('ffffffff8600116162636465666768010203040205070104') }
|
|
592
|
+
let(:status) { ['0005070109000000'].pack('H*') }
|
|
593
|
+
|
|
594
|
+
before { allow(SecureRandom).to receive(:random_bytes).with(8).and_return('abcdefgh') }
|
|
595
|
+
|
|
596
|
+
it 'rejects reserved CTAPHID control commands before any device I/O' do
|
|
597
|
+
[0x06, 0x11, 0x3b, 0x3f].each do |command|
|
|
598
|
+
io = fake_io
|
|
599
|
+
conn = transport::Connection.new(descriptor: { protocol: :ctap }, io: io, timeout: 0.02)
|
|
600
|
+
expect { conn.exchange(protocol: :ctap, command: command, data: ''.b) }.to raise_error(ArgumentError, /reserved/)
|
|
601
|
+
expect(io.writes).to be_empty
|
|
602
|
+
conn.close
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
it 'rejects a status-only OTP reply when a checked data response was required' do
|
|
607
|
+
io = fake_io(features: [status, status, status, ['000507010a010000'].pack('H*')])
|
|
608
|
+
conn = transport::Connection.new(descriptor: { protocol: :otp }, io: io)
|
|
609
|
+
expect { conn.exchange(protocol: :otp, command: 0x10, data: ''.b, response_length: 4) }.to raise_error(PWN::Plugins::YubiKey::ResponseError)
|
|
610
|
+
expect(io.closed).to eq(1)
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
it 'exposes CTAPHID INIT capabilities before returning an opened FIDO connection' do
|
|
614
|
+
entry = { backend: :hid, protocol: :ctap, device_path: '/fake/security-key', serial: nil }
|
|
615
|
+
io = fake_io(packets: [init_reply])
|
|
616
|
+
binding = double(devices: [entry], open: io)
|
|
617
|
+
allow(transport).to receive(:native).with(kind: :hid).and_return(binding)
|
|
618
|
+
conn = transport.open(application: :fido, device_path: entry[:device_path], timeout: 1)
|
|
619
|
+
expect(conn.descriptor).to include(version: [5, 7, 1], capabilities: 4)
|
|
620
|
+
expect(io.writes).to eq([packet('ffffffff8600086162636465666768')])
|
|
621
|
+
conn.close
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
{
|
|
625
|
+
nonce: 'ffffffff8600116162636465666769010203040205070104',
|
|
626
|
+
channel: 'ffffffff8600116162636465666768ffffffff0205070104',
|
|
627
|
+
version: 'ffffffff8600116162636465666768010203040105070104',
|
|
628
|
+
length: 'ffffffff86001061626364656667680102030402050701'
|
|
629
|
+
}.each do |label, hex|
|
|
630
|
+
it "rejects a CTAPHID INIT with invalid #{label} and closes the handle" do
|
|
631
|
+
io = fake_io(packets: [packet(hex)])
|
|
632
|
+
conn = transport::Connection.new(descriptor: { protocol: :ctap }, io: io, timeout: 0.5)
|
|
633
|
+
expect { conn.exchange(protocol: :ctap, command: 1, data: ''.b) }.to raise_error(PWN::Plugins::YubiKey::ResponseError)
|
|
634
|
+
expect(io.closed).to eq(1)
|
|
635
|
+
expect(io.writes.length).to eq(1)
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
it 'ignores unrelated channels and reports keepalive while preserving the binary CTAP status byte' do
|
|
640
|
+
io = fake_io(packets: [init_reply, packet('0506070890000101'), packet('01020304bb000102'), packet('0102030490000300a100')])
|
|
641
|
+
conn = transport::Connection.new(descriptor: { protocol: :ctap }, io: io)
|
|
642
|
+
statuses = []
|
|
643
|
+
expect(conn.exchange(protocol: :ctap, command: 0x10, data: ''.b, on_keepalive: ->(value) { statuses << value })).to eq(['00a100'].pack('H*'))
|
|
644
|
+
expect(statuses).to eq([2])
|
|
645
|
+
conn.close
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
{
|
|
649
|
+
sequence: ["0102030490003c#{'00' * 57}", '0102030401000000'],
|
|
650
|
+
oversized: ['01020304901dba'],
|
|
651
|
+
keepalive: ['01020304bb000103'],
|
|
652
|
+
command: ['01020304810000']
|
|
653
|
+
}.each do |label, response|
|
|
654
|
+
it "rejects malformed CTAPHID #{label} without replaying the request" do
|
|
655
|
+
io = fake_io(packets: [init_reply] + response.map { |hex| packet(hex) })
|
|
656
|
+
conn = transport::Connection.new(descriptor: { protocol: :ctap }, io: io)
|
|
657
|
+
expect { conn.exchange(protocol: :ctap, command: 0x10, data: ''.b) }.to raise_error(PWN::Plugins::YubiKey::ResponseError)
|
|
658
|
+
expect(io.writes.map { |wire| wire.getbyte(4) }).to eq([0x86, 0x90, 0x91])
|
|
659
|
+
expect(io.closed).to eq(1)
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
it 'raises CTAPHID busy errors without an implicit retry of an uncertain command' do
|
|
664
|
+
io = fake_io(packets: [init_reply, packet('01020304bf000106')])
|
|
665
|
+
conn = transport::Connection.new(descriptor: { protocol: :ctap }, io: io)
|
|
666
|
+
expect { conn.exchange(protocol: :ctap, command: 0x10, data: ''.b) }.to raise_error(PWN::Plugins::YubiKey::CommandError, /0x06/)
|
|
667
|
+
expect(io.writes.count { |wire| wire.getbyte(4) == 0x90 }).to eq(1)
|
|
668
|
+
expect(io.closed).to eq(1)
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
it 'frames the full 7609-byte CTAPHID limit without wrapping continuation numbers' do
|
|
672
|
+
io = fake_io(packets: [init_reply, packet('01020304810000')])
|
|
673
|
+
conn = transport::Connection.new(descriptor: { protocol: :ctap }, io: io)
|
|
674
|
+
expect(conn.exchange(protocol: :ctap, command: 1, data: 'Z' * 7609)).to eq(''.b)
|
|
675
|
+
expect(io.writes.length).to eq(130)
|
|
676
|
+
expect(io.writes[1]).to eq(packet("01020304811db9#{'5a' * 57}"))
|
|
677
|
+
expect(io.writes.last).to eq(packet("010203047f#{'5a' * 59}"))
|
|
678
|
+
conn.close
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
it 'waits for OTP touch then validates a fixed data CRC' do
|
|
682
|
+
io = fake_io(features: [status, status, status, ['0000000000000020'].pack('H*'), ['0001020329a70040'].pack('H*'), ['0000000000000040'].pack('H*')])
|
|
683
|
+
conn = transport::Connection.new(descriptor: { protocol: :otp }, io: io)
|
|
684
|
+
statuses = []
|
|
685
|
+
expect(conn.exchange(protocol: :otp, command: 0x10, data: ''.b, response_length: 4, on_keepalive: ->(value) { statuses << value })).to eq(['00010203'].pack('H*'))
|
|
686
|
+
expect(statuses).to eq([2])
|
|
687
|
+
conn.close
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
it 'aborts the OTP touch state when the cancellation predicate becomes true' do
|
|
691
|
+
io = fake_io(features: [status, status, status, ['0000000000000020'].pack('H*')])
|
|
692
|
+
conn = transport::Connection.new(descriptor: { protocol: :otp }, io: io)
|
|
693
|
+
stopped = false
|
|
694
|
+
expect do
|
|
695
|
+
conn.exchange(protocol: :otp, command: 0x10, data: ''.b, response_length: 4,
|
|
696
|
+
on_keepalive: ->(_value) { stopped = true }, cancelled: -> { stopped })
|
|
697
|
+
end.to raise_error(transport::CancelledError)
|
|
698
|
+
expect(io.feature_writes.last).to eq(['00000000000000ff'].pack('H*'))
|
|
699
|
+
expect(io.closed).to eq(1)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
{
|
|
703
|
+
crc: %w[0001020328a70040 0000000000000040],
|
|
704
|
+
sequence: %w[0001020329a70041],
|
|
705
|
+
truncated: %w[0001020329a70040 0005070109000000]
|
|
706
|
+
}.each do |label, response|
|
|
707
|
+
it "rejects an OTP #{label} fault and resets the device state" do
|
|
708
|
+
io = fake_io(features: [status, status, status] + response.map { |hex| [hex].pack('H*') })
|
|
709
|
+
conn = transport::Connection.new(descriptor: { protocol: :otp }, io: io)
|
|
710
|
+
expect { conn.exchange(protocol: :otp, command: 0x10, data: ''.b, response_length: 4) }.to raise_error(PWN::Plugins::YubiKey::ResponseError)
|
|
711
|
+
expect(io.feature_writes.last).to eq(['00000000000000ff'].pack('H*'))
|
|
712
|
+
expect(io.closed).to eq(1)
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
it 'returns programming status only after the sequence increments or the last slot is deleted' do
|
|
717
|
+
%w[000507010a010000 0005070100000000].each do |updated|
|
|
718
|
+
io = fake_io(features: [status, status, status, [updated].pack('H*')])
|
|
719
|
+
conn = transport::Connection.new(descriptor: { protocol: :otp }, io: io)
|
|
720
|
+
expect(conn.exchange(protocol: :otp, command: 1, data: ''.b)).to eq([updated].pack('H*').byteslice(1, 6))
|
|
721
|
+
conn.close
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
it 'does not accept a zero programming sequence while a slot still reports programmed' do
|
|
726
|
+
previous = ['00050701ff010000'].pack('H*')
|
|
727
|
+
io = fake_io(features: [previous, previous, previous, ['0005070100010000'].pack('H*')])
|
|
728
|
+
conn = transport::Connection.new(descriptor: { protocol: :otp }, io: io)
|
|
729
|
+
expect { conn.exchange(protocol: :otp, command: 1, data: ''.b) }.to raise_error(PWN::Plugins::YubiKey::CommandError)
|
|
730
|
+
expect(io.closed).to eq(1)
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
it 'bounds the OTP busy writer without sending the application payload' do
|
|
734
|
+
io = fake_io(features: [status])
|
|
735
|
+
allow(io).to receive(:read_feature).and_return(status, ['0005070109000080'].pack('H*'))
|
|
736
|
+
conn = transport::Connection.new(descriptor: { protocol: :otp }, io: io, timeout: 0.03)
|
|
737
|
+
expect { conn.exchange(protocol: :otp, command: 1, data: ''.b) }.to raise_error(PWN::Plugins::YubiKey::TimeoutError)
|
|
738
|
+
expect(io.feature_writes).to eq([['00000000000000ff'].pack('H*')])
|
|
739
|
+
expect(io.closed).to eq(1)
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
it 'rejects invalid protocol, sizes, callbacks and nonfinite deadlines before I/O' do
|
|
743
|
+
%i[ctap otp apdu].each do |protocol|
|
|
744
|
+
limit = { ctap: 7609, otp: 64, apdu: 65_544 }.fetch(protocol)
|
|
745
|
+
io = fake_io
|
|
746
|
+
conn = transport::Connection.new(descriptor: { protocol: protocol }, io: io)
|
|
747
|
+
expect { conn.exchange(protocol: protocol, command: 1, data: 'x' * (limit + 1)) }.to raise_error(ArgumentError)
|
|
748
|
+
expect { conn.exchange(protocol: :other, command: 1, data: 'data') }.to raise_error(ArgumentError)
|
|
749
|
+
[true, false].each do |callback|
|
|
750
|
+
expect { conn.exchange(protocol: protocol, command: 1, data: 'data', on_keepalive: callback, timeout: 0.02) }.to raise_error(ArgumentError)
|
|
751
|
+
expect { conn.exchange(protocol: protocol, command: 1, data: 'data', cancelled: callback) }.to raise_error(ArgumentError)
|
|
752
|
+
end
|
|
753
|
+
[0, -1, Float::NAN, Float::INFINITY, '1'].each do |timeout|
|
|
754
|
+
expect { conn.exchange(protocol: protocol, command: 1, data: 'data', timeout: timeout) }.to raise_error(ArgumentError)
|
|
755
|
+
end
|
|
756
|
+
expect(io.writes + io.feature_writes).to be_empty
|
|
757
|
+
conn.close
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
end
|