pwn 0.5.784 → 0.5.787
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +6 -6
- data/README.md +37 -0
- data/bin/pwn +2 -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 +69 -15
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +2 -2
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Harness-Evaluation.md +1 -1
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +6 -5
- data/documentation/Installation.md +12 -9
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +18 -0
- data/documentation/Persistence.md +7 -5
- data/documentation/Plugin-Degradation.md +14 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Policy-Benchmark.md +32 -33
- 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 +185 -0
- data/documentation/YubiKey.md +218 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -130
- data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
- data/documentation/diagrams/dot/reinforcement-learning.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +1 -1
- data/documentation/diagrams/reinforcement-learning.svg +89 -89
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +43 -27
- 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/learning.rb +14 -18
- 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_evaluation.rb +334 -36
- 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 +4 -0
- 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 +8 -1
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +349 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/migrate.rb +85 -280
- 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/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +3 -1
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +293 -26
- data/lib/pwn/plugins/repl/ai_console.rb +268 -88
- data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
- data/lib/pwn/plugins/repl/mesh.rb +14 -19
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +9 -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/setup.rb +114 -24
- 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/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/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/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_evaluation_spec.rb +10 -23
- 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 +8 -16
- 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 +1 -1
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +129 -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/migrate_spec.rb +29 -81
- 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_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 +110 -75
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -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_mesh_layout_spec.rb +2 -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_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- 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/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 +605 -19
- metadata +129 -16
- data/scripts/benchmark_policy.rb +0 -376
- data/scripts/build_gate.rb +0 -58
- data/spec/integration/build_gate_spec.rb +0 -73
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
|
|
6
|
+
# YubiHSM Auth v5.9.1 protocol fixtures. Keys are synthetic, never hardware captures.
|
|
7
|
+
describe 'PWN::Plugins::YubiKey::HSMAuth native protocol' do
|
|
8
|
+
let(:hsmauth) { PWN::Plugins::YubiKey::HSMAuth }
|
|
9
|
+
let(:wire_class) do
|
|
10
|
+
Class.new do
|
|
11
|
+
attr_reader :seen
|
|
12
|
+
|
|
13
|
+
def initialize(steps)
|
|
14
|
+
@steps = steps.dup
|
|
15
|
+
@seen = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def descriptor
|
|
19
|
+
{ protocol: :apdu }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def exchange(opts = {})
|
|
23
|
+
raise 'wrong transport' unless opts[:protocol] == :apdu
|
|
24
|
+
|
|
25
|
+
actual = opts[:data].unpack1('H*')
|
|
26
|
+
@seen << actual
|
|
27
|
+
expected, response = @steps.shift
|
|
28
|
+
raise "unexpected APDU #{actual}" unless actual == expected
|
|
29
|
+
|
|
30
|
+
[response].pack('H*')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def complete?
|
|
34
|
+
@steps.empty?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
let(:select) { %w[00a4040008a000000527210701 79030508009000] }
|
|
39
|
+
let(:management) { 'm' * 16 }
|
|
40
|
+
let(:password) { 'synthetic' }
|
|
41
|
+
let(:entry) { '7207260061636374089000' }
|
|
42
|
+
|
|
43
|
+
def field(tag, value)
|
|
44
|
+
(value.bytesize < 128 ? [tag, value.bytesize] : [tag, 0x81, value.bytesize]).pack('C*') + value.b
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def apdu(ins, payload = ''.b, param1: 0, param2: 0)
|
|
48
|
+
([0, ins, param1, param2] + (payload.empty? ? [] : [payload.bytesize])).pack('C*').unpack1('H*') + payload.unpack1('H*')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
before do
|
|
52
|
+
require 'pwn/plugins/yubikey/protocol'
|
|
53
|
+
require 'pwn/plugins/yubikey/hsmauth'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'lists exact labels, algorithms, touch policies and retry counters' do
|
|
57
|
+
wire = wire_class.new([select, ['0005000000', entry]])
|
|
58
|
+
expect(hsmauth.execute(connection: wire, args: %w[credentials list])).to eq([{ label: 'acct', algorithm: :symmetric, touch: false, retries: 8 }])
|
|
59
|
+
expect(wire.complete?).to be(true)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'imports symmetric keys through the actual raw APDU boundary and verifies metadata' do
|
|
63
|
+
payload = field(0x7b, management) + field(0x71, 'acct') + field(0x74, "\x26") + field(0x75, 'e' * 16) + field(0x76, 'a' * 16) + field(0x73, password.ljust(16, "\x00")) + field(0x7a, "\x00")
|
|
64
|
+
wire = wire_class.new([select, %w[0005000000 9000], [apdu(1, payload), '9000'], ['0005000000', entry]])
|
|
65
|
+
result = hsmauth.execute(connection: wire, args: %w[credentials symmetric acct], key_enc: 'e' * 16, key_mac: 'a' * 16, credential_password: password, management_key: management, confirm: true)
|
|
66
|
+
expect(result).to include(label: 'acct', verified: true)
|
|
67
|
+
expect(wire.complete?).to be(true)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'derives AES keys using the independently calculated PBKDF2 vector' do
|
|
71
|
+
keys = ['6b267091bd381008dacf8eeddd0e3cd7a9cbea9664f5fc538491ce2945fb4991'].pack('H*')
|
|
72
|
+
payload = field(0x7b, management) + field(0x71, 'acct') + field(0x74, "\x26") + field(0x75, keys[0, 16]) + field(0x76, keys[16, 16]) + field(0x73, password.ljust(16, "\x00")) + field(0x7a, "\x00")
|
|
73
|
+
wire = wire_class.new([select, %w[0005000000 9000], [apdu(1, payload), '9000'], ['0005000000', entry]])
|
|
74
|
+
expect(hsmauth.execute(connection: wire, args: %w[credentials derive acct], derivation_password: 'synthetic derive', credential_password: password, management_key: management, confirm: true)).to include(verified: true)
|
|
75
|
+
expect(wire.complete?).to be(true)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'calculates and strictly splits three session keys without exposing them to a process' do
|
|
79
|
+
context = ('H' * 8) + ('C' * 8)
|
|
80
|
+
payload = field(0x71, 'acct') + field(0x77, context) + field(0x78, 'R' * 8) + field(0x73, password.ljust(16, "\x00"))
|
|
81
|
+
wire = wire_class.new([select, [apdu(3, payload), "#{(('E' * 16) + ('M' * 16) + ('R' * 16)).unpack1('H*')}9000"]])
|
|
82
|
+
expect(hsmauth.calculate(connection: wire, label: 'acct', algorithm: :symmetric, context: context, card_crypto: 'R' * 8, credential_password: password, confirm: true)).to eq(key_senc: 'E' * 16, key_smac: 'M' * 16, key_srmac: 'R' * 16)
|
|
83
|
+
expect(wire.complete?).to be(true)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'never assumes the factory management key or invokes a provider without consent' do
|
|
87
|
+
wire = wire_class.new([])
|
|
88
|
+
expect { hsmauth.execute(connection: wire, args: %w[credentials delete acct], secret_provider: ->(_name) { raise 'prompted' }) }.to raise_error(PWN::Plugins::YubiKey::ConfirmationRequired)
|
|
89
|
+
expect(wire.seen).to be_empty
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
let(:p256_point) { ['046b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c2964fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5'].pack('H*') }
|
|
93
|
+
|
|
94
|
+
it 'generates an asymmetric key on device using the empty private-key TLV' do
|
|
95
|
+
payload = field(0x7b, management) + field(0x71, 'acct') + field(0x74, "\x27") + field(0x7d, '') + field(0x73, password.ljust(16, "\x00")) + field(0x7a, "\x00")
|
|
96
|
+
wire = wire_class.new([select, %w[0005000000 9000], [apdu(1, payload), '9000'], ['0005000000', entry.sub('2600', '2700')]])
|
|
97
|
+
expect(hsmauth.execute(connection: wire, args: %w[credentials generate acct], management_key: management, credential_password: password, confirm: true)).to include(algorithm: :asymmetric, verified: true)
|
|
98
|
+
expect(wire.complete?).to be(true)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'imports a P-256 private scalar without treating a filename as secret input' do
|
|
102
|
+
scalar = "#{"\x00" * 31}\x01"
|
|
103
|
+
payload = field(0x7b, management) + field(0x71, 'acct') + field(0x74, "\x27") + field(0x7d, scalar) + field(0x73, password.ljust(16, "\x00")) + field(0x7a, "\x00")
|
|
104
|
+
wire = wire_class.new([select, %w[0005000000 9000], [apdu(1, payload), '9000'], ['0005000000', entry.sub('2600', '2700')]])
|
|
105
|
+
expect(hsmauth.execute(connection: wire, args: %w[credentials import acct], private_key: scalar, management_key: management, credential_password: password, confirm: true)).to include(verified: true)
|
|
106
|
+
expect(wire.complete?).to be(true)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'exports validated P-256 public keys as raw points, PEM and DER' do
|
|
110
|
+
%w[RAW PEM DER].each do |format|
|
|
111
|
+
wire = wire_class.new([select, [apdu(10, field(0x71, 'acct')), "#{p256_point.unpack1('H*')}9000"]])
|
|
112
|
+
result = hsmauth.execute(connection: wire, args: ['credentials', 'export', 'acct', '--format', format])
|
|
113
|
+
expect(format == 'RAW' ? result : OpenSSL::PKey.read(result).public_key.to_octet_string(:uncompressed)).to eq(p256_point)
|
|
114
|
+
expect(wire.complete?).to be(true)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'changes the credential password using the old credential password without retry' do
|
|
119
|
+
payload = field(0x71, 'acct') + field(0x73, password.ljust(16, "\x00")) + field(0x73, 'new'.ljust(16, "\x00"))
|
|
120
|
+
wire = wire_class.new([select, [apdu(11, payload), '9000']])
|
|
121
|
+
expect(hsmauth.execute(connection: wire, args: %w[credentials change-password acct], credential_password: password, new_credential_password: 'new', confirm: true)).to include(status: :acknowledged, verification: :not_readable)
|
|
122
|
+
expect(wire.complete?).to be(true)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'changes the credential password with the explicit management authorization mode' do
|
|
126
|
+
payload = field(0x71, 'acct') + field(0x7b, management) + field(0x73, 'new'.ljust(16, "\x00"))
|
|
127
|
+
wire = wire_class.new([select, [apdu(11, payload, param1: 1), '9000']])
|
|
128
|
+
expect(hsmauth.execute(connection: wire, args: %w[credentials change-password acct], management_key: management, new_credential_password: 'new', confirm: true)).to include(status: :acknowledged)
|
|
129
|
+
expect(wire.complete?).to be(true)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'changes management keys and padded passwords without assuming a default' do
|
|
133
|
+
payload = field(0x7b, management) + field(0x7b, 'n' * 16)
|
|
134
|
+
wire = wire_class.new([select, [apdu(8, payload), '9000']])
|
|
135
|
+
expect(hsmauth.execute(connection: wire, args: %w[access change-management-key], management_key: management, new_management_key: 'n' * 16, confirm: true)).to include(status: :acknowledged)
|
|
136
|
+
expect(wire.complete?).to be(true)
|
|
137
|
+
payload = field(0x7b, 'old'.ljust(16, "\x00")) + field(0x7b, 'new'.ljust(16, "\x00"))
|
|
138
|
+
wire = wire_class.new([select, [apdu(8, payload), '9000']])
|
|
139
|
+
expect(hsmauth.execute(connection: wire, args: %w[access change-management-password], management_password: 'old', new_management_password: 'new', confirm: true)).to include(status: :acknowledged)
|
|
140
|
+
expect(wire.complete?).to be(true)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'resets with DEAD confirmation bytes and verifies empty inventory' do
|
|
144
|
+
wire = wire_class.new([select, %w[0006dead00 9000], select, %w[0005000000 9000], %w[0009000000 089000]])
|
|
145
|
+
expect(hsmauth.execute(connection: wire, args: ['reset'], confirm: true)).to include(verified: true)
|
|
146
|
+
expect(wire.complete?).to be(true)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'reads application version and management retry status without credentials' do
|
|
150
|
+
wire = wire_class.new([select, %w[0009000000 059000]])
|
|
151
|
+
expect(hsmauth.execute(connection: wire, args: ['info'])).to include(version: [5, 8, 0], management_key_retries: 5)
|
|
152
|
+
expect(wire.complete?).to be(true)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'retains a selected session between asymmetric challenge and session-key calculation' do
|
|
156
|
+
context = p256_point + p256_point
|
|
157
|
+
challenge_payload = field(0x71, 'acct') + field(0x73, password.ljust(16, "\x00"))
|
|
158
|
+
payload = field(0x71, 'acct') + field(0x77, context) + field(0x7c, p256_point) + field(0x78, 'R' * 16) + field(0x73, password.ljust(16, "\x00"))
|
|
159
|
+
wire = wire_class.new([select, [apdu(4, challenge_payload), "#{p256_point.unpack1('H*')}9000"], [apdu(3, payload), "#{'01' * 48}9000"]])
|
|
160
|
+
session = hsmauth.open_session(connection: wire)
|
|
161
|
+
expect(hsmauth.challenge(session: session, label: 'acct', algorithm: :asymmetric, credential_password: password, confirm: true)).to eq(p256_point)
|
|
162
|
+
expect(hsmauth.calculate(session: session, label: 'acct', algorithm: :asymmetric, context: context, public_key: p256_point, card_crypto: 'R' * 16, credential_password: password, confirm: true)).to include(key_senc: "\x01" * 16)
|
|
163
|
+
expect(wire.complete?).to be(true)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'deletes an exact label with management authorization and verifies absence' do
|
|
167
|
+
payload = field(0x7b, management) + field(0x71, 'acct')
|
|
168
|
+
wire = wire_class.new([select, ['0005000000', entry], [apdu(2, payload), '9000'], %w[0005000000 9000]])
|
|
169
|
+
expect(hsmauth.execute(connection: wire, args: %w[credentials delete acct], management_key: management, confirm: true)).to include(deleted: 'acct', verified: true)
|
|
170
|
+
expect(wire.complete?).to be(true)
|
|
171
|
+
wire = wire_class.new([select, ['0005000000', entry]])
|
|
172
|
+
expect { hsmauth.execute(connection: wire, args: %w[credentials delete acc], management_key: management, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /exact/)
|
|
173
|
+
expect(wire.complete?).to be(true)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'reports wrong credentials once without trying a default or another password' do
|
|
177
|
+
payload = field(0x71, 'acct') + field(0x77, 'c' * 16) + field(0x73, password.ljust(16, "\x00"))
|
|
178
|
+
wire = wire_class.new([select, [apdu(3, payload), '63c7']])
|
|
179
|
+
expect { hsmauth.calculate(connection: wire, label: 'acct', context: 'c' * 16, credential_password: password, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::Protocol::APDUError) { |error| expect(error.status).to eq(0x63c7) }
|
|
180
|
+
expect(wire.complete?).to be(true)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'rejects malformed session-key output instead of returning partial keys' do
|
|
184
|
+
payload = field(0x71, 'acct') + field(0x77, 'c' * 16) + field(0x73, password.ljust(16, "\x00"))
|
|
185
|
+
wire = wire_class.new([select, [apdu(3, payload), "#{'00' * 47}9000"]])
|
|
186
|
+
expect { hsmauth.calculate(connection: wire, label: 'acct', context: 'c' * 16, credential_password: password, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /exactly/)
|
|
187
|
+
expect(wire.complete?).to be(true)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'gates asymmetric and password-changing commands on the selected version' do
|
|
191
|
+
old = [select[0], '79030504039000']
|
|
192
|
+
wire = wire_class.new([old])
|
|
193
|
+
expect { hsmauth.execute(connection: wire, args: %w[credentials generate acct], management_key: management, credential_password: password, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::Error, /unsupported/)
|
|
194
|
+
wire = wire_class.new([old])
|
|
195
|
+
expect { hsmauth.execute(connection: wire, args: %w[credentials change-password acct], credential_password: password, new_credential_password: 'new', confirm: true) }.to raise_error(PWN::Plugins::YubiKey::Error, /unsupported/)
|
|
196
|
+
expect(wire.complete?).to be(true)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'refuses missing management credentials and malformed private keys without I/O' do
|
|
200
|
+
wire = wire_class.new([])
|
|
201
|
+
expect { hsmauth.execute(connection: wire, args: %w[credentials delete acct], confirm: true) }.to raise_error(ArgumentError, /management_key/)
|
|
202
|
+
expect { hsmauth.execute(connection: wire, args: %w[credentials import acct], private_key: "\x00" * 32, management_key: management, credential_password: password, confirm: true) }.to raise_error(ArgumentError, /scalar/)
|
|
203
|
+
expect(wire.seen).to be_empty
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'gets a symmetric challenge without guessing a credential password' do
|
|
207
|
+
wire = wire_class.new([select, [apdu(4, field(0x71, 'acct')), '01020304050607089000']])
|
|
208
|
+
expect(hsmauth.challenge(connection: wire, label: 'acct', confirm: true)).to eq(['0102030405060708'].pack('H*'))
|
|
209
|
+
expect(wire.complete?).to be(true)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'documents every public API and secret option in actual help output' do
|
|
213
|
+
expect { hsmauth.help }.to output(/\.execute\(.*new_credential_password:.*management_key:.*\.operations.*\.open_session\(.*\.challenge\(.*\.calculate\(.*public_key:.*\.authors/m).to_stdout
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it 'keeps groups local and rejects secret argv even with confirmation' do
|
|
217
|
+
wire = wire_class.new([])
|
|
218
|
+
expect(hsmauth.execute(connection: wire, args: ['credentials'])).to include(application: :hsmauth)
|
|
219
|
+
expect { hsmauth.execute(connection: wire, args: %w[credentials generate acct --management-key secret], confirm: true) }.to raise_error(ArgumentError)
|
|
220
|
+
expect(wire.seen).to be_empty
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'does not silently change an explicit credential authentication mode' do
|
|
224
|
+
wire = wire_class.new([])
|
|
225
|
+
expect { hsmauth.execute(connection: wire, args: %w[credentials change-password acct], auth_method: :credential, management_key: management, new_credential_password: 'new', confirm: true) }.to raise_error(ArgumentError, /conflict/)
|
|
226
|
+
expect { hsmauth.execute(connection: wire, args: %w[credentials symmetric acct], generate: 'false', confirm: true) }.to raise_error(ArgumentError, /boolean/)
|
|
227
|
+
expect(wire.seen).to be_empty
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it 'validates all public calculation arguments before credentials or I/O' do
|
|
231
|
+
wire = wire_class.new([])
|
|
232
|
+
expect { hsmauth.calculate(connection: wire, label: 'acct', algorithm: :symmetric, context: 'short', credential_password: password, confirm: true) }.to raise_error(ArgumentError)
|
|
233
|
+
expect(wire.seen).to be_empty
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'pwn/plugins/yubikey/protocol'
|
|
5
|
+
require 'pwn/plugins/yubikey/management'
|
|
6
|
+
|
|
7
|
+
RSpec.describe 'PWN::Plugins::YubiKey::Management native protocol' do
|
|
8
|
+
subject(:management) { PWN::Plugins::YubiKey::Management }
|
|
9
|
+
|
|
10
|
+
let(:wire) do
|
|
11
|
+
Class.new do
|
|
12
|
+
attr_reader :calls, :descriptor
|
|
13
|
+
|
|
14
|
+
def initialize(responses, descriptor = { protocol: :otp })
|
|
15
|
+
@responses = responses
|
|
16
|
+
@descriptor = descriptor
|
|
17
|
+
@calls = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def exchange(opts = {})
|
|
21
|
+
@calls << opts
|
|
22
|
+
raise 'Unexpected management request' if @responses.empty?
|
|
23
|
+
|
|
24
|
+
response = @responses.shift
|
|
25
|
+
raise response if response.is_a?(Exception)
|
|
26
|
+
|
|
27
|
+
response
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
let(:status) { [5, 7, 1, 9, 3, 0].pack('C*') }
|
|
33
|
+
# Literal protocol fields, independent of the application encoder.
|
|
34
|
+
let(:tlvs) { ['0102023f020400bc614e0302023f04018105030507010602000007010f0801400a01000d02023f0e02023f'].pack('H*') }
|
|
35
|
+
let(:encoded) { [tlvs.bytesize].pack('C') + tlvs }
|
|
36
|
+
|
|
37
|
+
it 'reads exact native device capabilities without inventing application support' do
|
|
38
|
+
peer = wire.new([status, encoded])
|
|
39
|
+
info = management.info(connection: peer)
|
|
40
|
+
expect(info).to include(version: [5, 7, 1], serial: '12345678', is_locked: false, is_fips: true, form_factor: 1)
|
|
41
|
+
expect(info[:supported_capabilities]).to eq(usb: 0x23f, nfc: 0x23f)
|
|
42
|
+
expect(info[:enabled_capabilities]).to eq(usb: 0x23f, nfc: 0x23f)
|
|
43
|
+
expect(peer.calls.last).to include(protocol: :otp, command: 0x13, data: "\0".b)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'reads length-prefixed information over management smartcard and CTAP vendor commands' do
|
|
47
|
+
peer = wire.new(["YubiKey 5.7.1\x90\0".b, encoded + "\x90\0".b], protocol: :apdu)
|
|
48
|
+
expect(management.info(connection: peer)[:serial]).to eq('12345678')
|
|
49
|
+
expect(peer.calls.first[:data]).to eq(['00a4040008a000000527471117'].pack('H*'))
|
|
50
|
+
expect(peer.calls.last[:data]).to eq(['001d000000'].pack('H*'))
|
|
51
|
+
peer = wire.new([encoded], protocol: :ctap, version: [5, 7, 1])
|
|
52
|
+
expect(management.info(connection: peer)[:version]).to eq([5, 7, 1])
|
|
53
|
+
expect(peer.calls.first).to include(command: 0x42, data: "\0".b)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'changes only requested USB applications and verifies new device fields' do
|
|
57
|
+
changed = encoded.sub(['0302023f'].pack('H*'), ['0302022f'].pack('H*'))
|
|
58
|
+
peer = wire.new([status, encoded, ''.b, changed])
|
|
59
|
+
result = management.configure_usb(connection: peer, disable: ['PIV'], confirm: true)
|
|
60
|
+
expect(peer.calls[2]).to include(command: 0x15, data: ['040302022f'].pack('H*'))
|
|
61
|
+
expect(result).to include(verified: true, verification: :device_information)
|
|
62
|
+
expect(result[:info][:enabled_capabilities][:usb]).to eq(0x22f)
|
|
63
|
+
expect(peer.calls.last[:command]).to eq(0x13)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'requires explicit consent and refuses unsupported capabilities without a write' do
|
|
67
|
+
peer = wire.new([])
|
|
68
|
+
expect { management.configure_usb(connection: peer, disable: ['PIV'], confirm: 'true') }.to raise_error(PWN::Plugins::YubiKey::ConfirmationRequired)
|
|
69
|
+
expect(peer.calls).to be_empty
|
|
70
|
+
peer = wire.new([status, encoded])
|
|
71
|
+
expect { management.configure_usb(connection: peer, enable: ['HSMAUTH'], confirm: true) }.to raise_error(PWN::Plugins::YubiKey::CommandError, /supported/i)
|
|
72
|
+
expect(peer.calls.none? { |call| call[:command] == 0x15 }).to be(true)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'treats lost ACK and mismatching readback as uncertain without replay' do
|
|
76
|
+
peer = wire.new([status, encoded, ''.b, encoded])
|
|
77
|
+
expect { management.configure_usb(connection: peer, disable: ['PIV'], confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /uncertain/i)
|
|
78
|
+
expect(peer.calls.count { |call| call[:command] == 0x15 }).to eq(1)
|
|
79
|
+
peer = wire.new([status, encoded, PWN::Plugins::YubiKey::TimeoutError.new('synthetic timeout')])
|
|
80
|
+
expect { management.configure_usb(connection: peer, disable: ['PIV'], confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /uncertain/i)
|
|
81
|
+
expect(peer.calls.count { |call| call[:command] == 0x15 }).to eq(1)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'routes all config leaves and treats group/list paths as read-only' do
|
|
85
|
+
expect(management.operations).to contain_exactly('usb', 'nfc', 'mode', 'set-lock-code', 'reset')
|
|
86
|
+
expect(management.execute(args: [])).to include(application: :management)
|
|
87
|
+
peer = wire.new([status, encoded])
|
|
88
|
+
expect(management.execute(args: ['usb', '--list'], connection: peer)[:enabled]).to include('OTP', 'PIV', 'FIDO2')
|
|
89
|
+
expect(peer.calls.length).to eq(2)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'disables NFC applications while retaining USB settings and validates readback' do
|
|
93
|
+
changed = encoded.sub(['0e02023f'].pack('H*'), ['0e020000'].pack('H*'))
|
|
94
|
+
peer = wire.new([status, encoded, ''.b, changed])
|
|
95
|
+
result = management.execute(args: ['nfc', '--disable-all'], connection: peer, confirm: true)
|
|
96
|
+
expect(peer.calls[2]).to include(command: 0x15, data: ['040e020000'].pack('H*'))
|
|
97
|
+
expect(result[:info][:enabled_capabilities]).to eq(usb: 0x23f, nfc: 0)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'sets a configuration lock with secret bytes only in the native request and reads the flag back' do
|
|
101
|
+
locked = encoded.sub(['0a0100'].pack('H*'), ['0a0101'].pack('H*'))
|
|
102
|
+
peer = wire.new([status, encoded, ''.b, locked])
|
|
103
|
+
result = management.execute(args: ['set-lock-code'], connection: peer, confirm: true, new_lock_code: 'L' * 16)
|
|
104
|
+
expect(peer.calls[2][:data]).to eq([18, 10, 16].pack('C*') + ('L' * 16))
|
|
105
|
+
expect(result[:info][:is_locked]).to be(true)
|
|
106
|
+
expect(result[:lock_secret_readback]).to be(false)
|
|
107
|
+
expect(result.to_s).not_to include('L' * 16)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'supplies existing lock through an explicit secret provider and clears protection once' do
|
|
111
|
+
locked = encoded.sub(['0a0100'].pack('H*'), ['0a0101'].pack('H*'))
|
|
112
|
+
provider = instance_double(Proc)
|
|
113
|
+
expect(provider).to receive(:call).with(:lock_code).once.and_return('L' * 16)
|
|
114
|
+
peer = wire.new([status, locked, ''.b, encoded])
|
|
115
|
+
result = management.execute(args: ['set-lock-code', '--clear'], connection: peer, confirm: true, secret_provider: provider)
|
|
116
|
+
expect(result[:info][:is_locked]).to be(false)
|
|
117
|
+
expect(peer.calls[2][:data]).to eq([36, 11, 16].pack('C*') + ('L' * 16) + [10, 16].pack('C*') + ("\0" * 16))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'sets legacy mode using little-endian fields and verifies a fresh USB product descriptor' do
|
|
121
|
+
old_status = [4, 3, 1, 9, 3, 0].pack('C*')
|
|
122
|
+
old_info = encoded.sub(['0503050701'].pack('H*'), ['0503040301'].pack('H*'))
|
|
123
|
+
peer = wire.new([old_status, old_info, ''.b], protocol: :otp, product_id: 0x0407)
|
|
124
|
+
fresh = wire.new([old_status, old_info], protocol: :otp, product_id: 0x0403)
|
|
125
|
+
result = management.execute(args: ['mode', 'OTP+FIDO', '--chalresp-timeout', '15'], connection: peer, confirm: true, reconnect: ->(_identity) { fresh })
|
|
126
|
+
expect(peer.calls.last).to include(command: 0x11, data: [4, 15, 0, 0].pack('C*'))
|
|
127
|
+
expect(result).to include(verified: true, verification: :usb_descriptor, usb_interfaces: 3)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'translates modern mode to capabilities and verifies reboot on a fresh handle' do
|
|
131
|
+
changed = encoded.sub(['0302023f'].pack('H*'), ['03020203'].pack('H*'))
|
|
132
|
+
peer = wire.new([status, encoded, ''.b])
|
|
133
|
+
fresh = wire.new([status, changed])
|
|
134
|
+
result = management.execute(args: ['mode', 'OTP+FIDO'], connection: peer, confirm: true, reconnect: ->(_identity) { fresh })
|
|
135
|
+
expect(peer.calls.last).to include(command: 0x15, data: ['060c0003020203'].pack('H*'))
|
|
136
|
+
expect(result[:reconnected]).to be(true)
|
|
137
|
+
expect(result[:info][:enabled_capabilities][:usb]).to eq(0x203)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'factory-resets only a reported Bio multi-protocol device and verifies reset-state metadata' do
|
|
141
|
+
bio = tlvs.sub(['040181'].pack('H*'), ['040106'].pack('H*')) + ['18021010'].pack('H*')
|
|
142
|
+
cleared = bio.sub(['18021010'].pack('H*'), ['18020000'].pack('H*'))
|
|
143
|
+
before = [bio.bytesize].pack('C') + bio
|
|
144
|
+
after = [cleared.bytesize].pack('C') + cleared
|
|
145
|
+
peer = wire.new(["5.7.1\x90\0".b, before + "\x90\0".b, "\x90\0".b, after + "\x90\0".b], protocol: :apdu)
|
|
146
|
+
result = management.execute(args: ['reset'], connection: peer, confirm: true)
|
|
147
|
+
expect(peer.calls[2][:data]).to eq(['001f000000'].pack('H*'))
|
|
148
|
+
expect(result).to include(verified: true, verification: :reset_state_metadata, data_erasure_verified: false)
|
|
149
|
+
peer = wire.new(["5.7.1\x90\0".b, encoded + "\x90\0".b], protocol: :apdu)
|
|
150
|
+
expect { management.reset(connection: peer, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::CommandError, /supported/)
|
|
151
|
+
expect(peer.calls.length).to eq(2)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'restricts NFC only on 5.7 or later and verifies the actual restriction field' do
|
|
155
|
+
restricted = tlvs + ['170101'].pack('H*')
|
|
156
|
+
peer = wire.new([status, encoded, ''.b, [restricted.bytesize].pack('C') + restricted])
|
|
157
|
+
expect(management.execute(args: ['nfc', '--restrict'], connection: peer, confirm: true)[:info][:nfc_restricted]).to be(true)
|
|
158
|
+
expect(peer.calls[2][:data]).to eq(['03170101'].pack('H*'))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'supports legacy NEO mode switching without fabricating capability metadata' do
|
|
162
|
+
old_status = [3, 4, 1, 9, 3, 0].pack('C*')
|
|
163
|
+
peer = wire.new([old_status, ''.b], protocol: :otp, product_id: 0x0116, serial: '12345678')
|
|
164
|
+
fresh = wire.new([old_status], protocol: :otp, product_id: 0x0114, serial: '12345678')
|
|
165
|
+
result = management.set_mode(connection: peer, mode: 'OTP+FIDO', confirm: true, reconnect: ->(_identity) { fresh })
|
|
166
|
+
expect(result[:info][:supported_capabilities]).to be_empty
|
|
167
|
+
expect(result[:usb_interfaces]).to eq(3)
|
|
168
|
+
expect(peer.calls.last).to include(command: 0x11, data: [4, 0, 0, 0].pack('C*'))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it 'applies the YubiKey 4.2.4 capability-reporting correction but never trusts its enabled tag' do
|
|
172
|
+
old_status = [4, 2, 4, 9, 3, 0].pack('C*')
|
|
173
|
+
old_info = encoded.sub(['0503050701'].pack('H*'), ['0503040204'].pack('H*')).sub(['0102023f'].pack('H*'), ['01020000'].pack('H*'))
|
|
174
|
+
peer = wire.new([old_status, old_info], protocol: :otp, product_id: 0x0407)
|
|
175
|
+
result = management.info(connection: peer)
|
|
176
|
+
expect(result[:supported_capabilities][:usb]).to eq(0x3f)
|
|
177
|
+
expect(result[:enabled_capabilities]).not_to have_key(:usb)
|
|
178
|
+
expect(result[:usb_interfaces]).to eq(7)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'collects paginated metadata and retains exact additional fields' do
|
|
182
|
+
first = tlvs + ['100101'].pack('H*')
|
|
183
|
+
second = ['1305414243313216010114011115010120030102032103040506'].pack('H*')
|
|
184
|
+
peer = wire.new([status, [first.bytesize].pack('C') + first, [second.bytesize].pack('C') + second])
|
|
185
|
+
result = management.info(connection: peer)
|
|
186
|
+
expect(peer.calls.last[:data]).to eq("\1".b)
|
|
187
|
+
expect(result).to include(part_number: 'ABC12', pin_complexity: true, fps_version: [1, 2, 3], stm_version: [4, 5, 6], fips_capable: 0x300, fips_approved: 0x200)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'requires an explicit reconnection verifier before any reboot write' do
|
|
191
|
+
peer = wire.new([status, encoded])
|
|
192
|
+
expect { management.reboot(connection: peer, confirm: true) }.to raise_error(ArgumentError, /reconnect/i)
|
|
193
|
+
expect(peer.calls.count { |call| call[:command] == 0x15 }).to eq(0)
|
|
194
|
+
peer = wire.new([status, encoded, ''.b])
|
|
195
|
+
fresh = wire.new([status, encoded])
|
|
196
|
+
result = management.reboot(connection: peer, confirm: true, reconnect: ->(_identity) { fresh })
|
|
197
|
+
expect(peer.calls.last[:data]).to eq(['020c00'].pack('H*'))
|
|
198
|
+
expect(result[:reconnected]).to be(true)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'does not silently ignore configuration options or guess absent lock metadata' do
|
|
202
|
+
peer = wire.new([])
|
|
203
|
+
expect { management.configure_usb(connection: peer, confirm: true) }.to raise_error(ArgumentError)
|
|
204
|
+
expect { management.execute(args: ['usb', '--list', '--disable', 'PIV'], connection: peer, confirm: true) }.to raise_error(ArgumentError)
|
|
205
|
+
expect(peer.calls).to be_empty
|
|
206
|
+
missing = tlvs.sub(['0a0100'].pack('H*'), '')
|
|
207
|
+
peer = wire.new([status, [missing.bytesize].pack('C') + missing])
|
|
208
|
+
expect { management.configure_usb(connection: peer, disable: ['PIV'], confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /lock/)
|
|
209
|
+
expect(peer.calls.length).to eq(2)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'uses the NEO OTP applet for smartcard mode commands after management selection' do
|
|
213
|
+
old_status = [3, 4, 1, 9, 3, 0].pack('C*')
|
|
214
|
+
peer = wire.new(["3.4.1\x90\0".b, "\x90\0".b, old_status + "\x90\0".b, "\x90\0".b], protocol: :apdu, serial: '12345678', product_id: 0x0116)
|
|
215
|
+
fresh = wire.new([old_status], protocol: :otp, serial: '12345678', product_id: 0x0114)
|
|
216
|
+
result = management.set_mode(connection: peer, mode: 4, confirm: true, reconnect: ->(_identity) { fresh })
|
|
217
|
+
expect(peer.calls[1][:data]).to eq(['a4040008'].pack('H*'))
|
|
218
|
+
expect(peer.calls[2][:data]).to eq(['00a4040007a0000005272001'].pack('H*'))
|
|
219
|
+
expect(peer.calls[3][:data]).to eq(['000111000404000000'].pack('H*'))
|
|
220
|
+
expect(result[:verified]).to be(true)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'parses qualified development firmware metadata instead of guessing a production version' do
|
|
224
|
+
qualifier = ['190b0103050800020101030107'].pack('H*')
|
|
225
|
+
reported = tlvs.sub(['0503050701'].pack('H*'), ['0503000001'].pack('H*')) + qualifier
|
|
226
|
+
peer = wire.new([[0, 0, 1, 0, 0, 0].pack('C*'), [reported.bytesize].pack('C') + reported])
|
|
227
|
+
result = management.info(connection: peer)
|
|
228
|
+
expect(result[:version]).to eq([5, 8, 0])
|
|
229
|
+
expect(result[:reported_version]).to eq([0, 0, 1])
|
|
230
|
+
expect(result[:version_qualifier]).to eq(version: [5, 8, 0], type: :beta, iteration: 7)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'falls back only for an absent NFC management applet on a legacy NEO' do
|
|
234
|
+
old_status = [3, 4, 1, 9, 3, 0].pack('C*')
|
|
235
|
+
peer = wire.new(["\x6a\x82".b, old_status + "\x90\0".b], protocol: :apdu, transport: :nfc)
|
|
236
|
+
expect(management.info(connection: peer)[:version]).to eq([3, 4, 1])
|
|
237
|
+
expect(peer.calls.last[:data]).to eq(['00a4040007a0000005272001'].pack('H*'))
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'rejects ambiguous boolean controls instead of interpreting strings as consented flags' do
|
|
241
|
+
peer = wire.new([])
|
|
242
|
+
expect { management.configure_usb(connection: peer, enable_all: 'false', confirm: true) }.to raise_error(ArgumentError)
|
|
243
|
+
expect { management.set_lock_code(connection: peer, clear: 'false', confirm: true) }.to raise_error(ArgumentError)
|
|
244
|
+
expect(peer.calls).to be_empty
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'writes capability settings through actual APDU and CTAP vendor request codecs' do
|
|
248
|
+
changed = encoded.sub(['0302023f'].pack('H*'), ['0302022f'].pack('H*'))
|
|
249
|
+
peer = wire.new(["5.7.1\x90\0".b, encoded + "\x90\0".b, "\x90\0".b, changed + "\x90\0".b], protocol: :apdu)
|
|
250
|
+
expect(management.configure_usb(connection: peer, disable: ['PIV'], confirm: true)[:verified]).to be(true)
|
|
251
|
+
expect(peer.calls[2][:data]).to eq(['001c000005040302022f'].pack('H*'))
|
|
252
|
+
peer = wire.new([encoded, ''.b, changed], protocol: :ctap, version: [5, 7, 1])
|
|
253
|
+
expect(management.configure_usb(connection: peer, disable: ['PIV'], confirm: true)[:verified]).to be(true)
|
|
254
|
+
expect(peer.calls.map { |call| call[:command] }).to eq([0x42, 0x43, 0x42])
|
|
255
|
+
expect(peer.calls[1][:data]).to eq(['040302022f'].pack('H*'))
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'does not verify a different serial-less selector after reboot' do
|
|
259
|
+
no_serial = tlvs.sub(['020400bc614e'].pack('H*'), '')
|
|
260
|
+
data = [no_serial.bytesize].pack('C') + no_serial
|
|
261
|
+
peer = wire.new([status, data, ''.b], protocol: :otp, device_path: '/synthetic/key-a')
|
|
262
|
+
fresh = wire.new([status, data], protocol: :otp, device_path: '/synthetic/key-b')
|
|
263
|
+
expect { management.reboot(connection: peer, confirm: true, reconnect: ->(_identity) { fresh }) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /uncertain/)
|
|
264
|
+
expect(peer.calls.count { |call| call[:command] == 0x15 }).to eq(1)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it 'rejects bad length and duplicate metadata rather than silently overwriting it' do
|
|
268
|
+
peer = wire.new([status, "#{encoded}\0"])
|
|
269
|
+
expect { management.info(connection: peer) }.to raise_error(PWN::Plugins::YubiKey::ResponseError)
|
|
270
|
+
duplicate = tlvs + ['0503050701'].pack('H*')
|
|
271
|
+
peer = wire.new([status, [duplicate.bytesize].pack('C') + duplicate])
|
|
272
|
+
expect { management.info(connection: peer) }.to raise_error(PWN::Plugins::YubiKey::ResponseError)
|
|
273
|
+
duplicate = tlvs + ['100101100100'].pack('H*')
|
|
274
|
+
peer = wire.new([status, [duplicate.bytesize].pack('C') + duplicate])
|
|
275
|
+
expect { management.info(connection: peer) }.to raise_error(PWN::Plugins::YubiKey::ResponseError)
|
|
276
|
+
end
|
|
277
|
+
end
|