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,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
# Independent wire fixtures: Yubico OATH protocol SELECT/LIST/CALCULATE,
|
|
6
|
+
# RFC 6238 SHA1 time 59 truncation (0x41397eea => 94287082).
|
|
7
|
+
describe 'PWN::Plugins::YubiKey::OATH native protocol' do
|
|
8
|
+
let(:oath) { PWN::Plugins::YubiKey::OATH }
|
|
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 expected && (expected.is_a?(Regexp) ? expected.match?(actual) : actual == expected)
|
|
29
|
+
|
|
30
|
+
response.respond_to?(:call) ? response.call(opts[:data]) : [response].pack('H*')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def complete?
|
|
34
|
+
@steps.empty?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
let(:select) { %w[00a4040007a0000005272101 7903050800710831323334353637389000] }
|
|
39
|
+
|
|
40
|
+
before do
|
|
41
|
+
require 'pwn/plugins/yubikey/protocol'
|
|
42
|
+
require 'pwn/plugins/yubikey/oath'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'calculates only the unique exact account with RFC 6238 leading digits' do
|
|
46
|
+
wire = wire_class.new([
|
|
47
|
+
select,
|
|
48
|
+
%w[00a1000000 7205216163637472062161636374329000],
|
|
49
|
+
%w[00a200011071046163637474080000000000000001 76050841397eea9000]
|
|
50
|
+
])
|
|
51
|
+
expect(oath.code(connection: wire, account: 'acct', timestamp: 59, confirm: true)).to eq('94287082')
|
|
52
|
+
expect(wire.complete?).to be(true)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'performs mutual authentication exactly once before listing a locked application' do
|
|
56
|
+
allow(SecureRandom).to receive(:random_bytes).with(8).and_return('ABCDEFGH')
|
|
57
|
+
wire = wire_class.new([
|
|
58
|
+
[select[0], '790305080071083132333435363738740861626364656667689000'],
|
|
59
|
+
%w[00a30000207514db45e133f47e3e7a338c965f23bb02012cc4330a74084142434445464748 751496aa98f40d9ee3dc4b3c243ebd3dc8e53fcf4ef59000],
|
|
60
|
+
%w[00a1000000 720521616363749000]
|
|
61
|
+
])
|
|
62
|
+
expect(oath.execute(connection: wire, args: %w[accounts list], password: 'synthetic oath', confirm: true)).to include(include(id: 'acct', period: 30))
|
|
63
|
+
expect(wire.complete?).to be(true)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'requires explicit consent before any code operation or secret-provider invocation' do
|
|
67
|
+
wire = wire_class.new([])
|
|
68
|
+
provider = ->(_name) { raise 'must not ask for a secret' }
|
|
69
|
+
expect { oath.code(connection: wire, account: 'acct', secret_provider: provider) }.to raise_error(PWN::Plugins::YubiKey::ConfirmationRequired)
|
|
70
|
+
expect(wire.seen).to be_empty
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'returns leading zeroes for HOTP and uses the empty challenge' do
|
|
74
|
+
wire = wire_class.new([select, %w[00a1000000 720511616363749000], %w[00a20001087104616363747400 760506000000019000]])
|
|
75
|
+
expect(oath.code(connection: wire, account: 'acct', confirm: true)).to eq('000001')
|
|
76
|
+
expect(wire.complete?).to be(true)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'does not calculate a substring or Unicode-normalized account identity' do
|
|
80
|
+
['acc', "a\u0301", ' acct'].each do |account|
|
|
81
|
+
wire = wire_class.new([select, %w[00a1000000 720521616363749000]])
|
|
82
|
+
expect { oath.code(connection: wire, account: account, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /exact/)
|
|
83
|
+
expect(wire.complete?).to be(true)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'does not retry or delete on a device memory error' do
|
|
88
|
+
wire = wire_class.new([select, %w[00a1000000 6581]])
|
|
89
|
+
expect { oath.code(connection: wire, account: 'acct', confirm: true) }.to raise_error(PWN::Plugins::YubiKey::Error)
|
|
90
|
+
expect(wire.complete?).to be(true)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def apdu(ins, payload = ''.b, param1: 0, param2: 0)
|
|
94
|
+
([0, ins, param1, param2] + (payload.empty? ? [] : [payload.bytesize])).pack('C*').unpack1('H*') + payload.unpack1('H*')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def field(tag, value)
|
|
98
|
+
[tag, value.bytesize].pack('CC') + value.b
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'imports a raw secret with explicit overwrite consent and reads back the exact ID' do
|
|
102
|
+
payload = field(0x71, 'acct') + field(0x73, "\x21\x0612345678901234567890")
|
|
103
|
+
wire = wire_class.new([select, %w[00a1000000 9000], [apdu(1, payload), '9000'], %w[00a1000000 720521616363749000]])
|
|
104
|
+
result = oath.execute(connection: wire, args: %w[accounts add acct], secret: '12345678901234567890', confirm: true)
|
|
105
|
+
expect(result).to include(id: 'acct', verified: true)
|
|
106
|
+
expect(wire.complete?).to be(true)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'validates URI metadata without repairing conflicting issuers or malformed secrets' do
|
|
110
|
+
parsed = oath.parse_uri(uri: 'otpauth://totp/Example%3Aalice?secret=GEZDGNBVGY3TQOJQ&issuer=Example&algorithm=SHA256&digits=8&period=60')
|
|
111
|
+
expect(parsed).to include(name: 'alice', issuer: 'Example', secret: '1234567890', algorithm: :sha256, digits: 8, period: 60)
|
|
112
|
+
expect { oath.parse_uri(uri: 'otpauth://totp/A:x?secret=MY&issuer=B') }.to raise_error(ArgumentError)
|
|
113
|
+
expect { oath.parse_uri(uri: 'otpauth://totp/x?secret=MZ') }.to raise_error(ArgumentError)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'refuses invalid public import arguments before authentication or prompting' do
|
|
117
|
+
wire = wire_class.new([])
|
|
118
|
+
expect { oath.execute(connection: wire, args: %w[accounts add acct --digits 9], secret_provider: ->(_key) { raise 'prompted' }, confirm: true) }.to raise_error(ArgumentError)
|
|
119
|
+
expect(wire.seen).to be_empty
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'renames exact IDs without deleting either account and verifies the new name' do
|
|
123
|
+
payload = field(0x71, 'acct') + field(0x71, 'new')
|
|
124
|
+
wire = wire_class.new([select, %w[00a1000000 720521616363749000], [apdu(5, payload), '9000'], %w[00a1000000 7204216e65779000]])
|
|
125
|
+
expect(oath.execute(connection: wire, args: %w[accounts rename acct new], confirm: true)).to include(id: 'new', verified: true)
|
|
126
|
+
expect(wire.complete?).to be(true)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'deletes only the exact ID and verifies absence' do
|
|
130
|
+
wire = wire_class.new([select, %w[00a1000000 720521616363749000], %w[0002000006710461636374 9000], %w[00a1000000 9000]])
|
|
131
|
+
expect(oath.execute(connection: wire, args: %w[accounts delete acct], confirm: true)).to include(deleted: 'acct', verified: true)
|
|
132
|
+
expect(wire.complete?).to be(true)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'sets an access key with PBKDF2/HMAC and reads back password protection' do
|
|
136
|
+
allow(SecureRandom).to receive(:random_bytes).with(8).and_return('ABCDEFGH')
|
|
137
|
+
payload = field(0x73, ['21ed78608d6e1b6954df83a2044711b48f'].pack('H*')) + field(0x74, 'ABCDEFGH') + field(0x75, ['96aa98f40d9ee3dc4b3c243ebd3dc8e53fcf4ef5'].pack('H*'))
|
|
138
|
+
locked_select = [select[0], '790305080071083132333435363738740861626364656667689000']
|
|
139
|
+
wire = wire_class.new([select, [apdu(3, payload), '9000'], locked_select])
|
|
140
|
+
expect(oath.execute(connection: wire, args: %w[access change], new_password: 'synthetic oath', confirm: true)).to include(locked: true, verified: true)
|
|
141
|
+
expect(wire.complete?).to be(true)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'clears access protection only after mutual authentication' do
|
|
145
|
+
allow(SecureRandom).to receive(:random_bytes).with(8).and_return('ABCDEFGH')
|
|
146
|
+
locked_select = [select[0], '790305080071083132333435363738740861626364656667689000']
|
|
147
|
+
wire = wire_class.new([locked_select, %w[00a30000207514db45e133f47e3e7a338c965f23bb02012cc4330a74084142434445464748 751496aa98f40d9ee3dc4b3c243ebd3dc8e53fcf4ef59000], %w[00030000027300 9000], select])
|
|
148
|
+
expect(oath.execute(connection: wire, args: %w[access change --clear], password: 'synthetic oath', confirm: true)).to include(locked: false, verified: true)
|
|
149
|
+
expect(wire.complete?).to be(true)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'requires an explicit encrypted-cache handoff and verifies remember and forget effects' do
|
|
153
|
+
cache = Class.new do
|
|
154
|
+
def encrypted? = true
|
|
155
|
+
def store(device_id:, access_key:) = @record = [device_id, access_key]
|
|
156
|
+
def fetch(device_id:) = @record && @record[0] == device_id ? @record[1] : nil
|
|
157
|
+
|
|
158
|
+
def delete(device_id:)
|
|
159
|
+
@record = nil if @record && @record[0] == device_id
|
|
160
|
+
end
|
|
161
|
+
end.new
|
|
162
|
+
allow(SecureRandom).to receive(:random_bytes).with(8).and_return('ABCDEFGH')
|
|
163
|
+
locked_select = [select[0], '790305080071083132333435363738740861626364656667689000']
|
|
164
|
+
wire = wire_class.new([locked_select, %w[00a30000207514db45e133f47e3e7a338c965f23bb02012cc4330a74084142434445464748 751496aa98f40d9ee3dc4b3c243ebd3dc8e53fcf4ef59000]])
|
|
165
|
+
result = oath.execute(connection: wire, args: %w[access remember], password: 'synthetic oath', encrypted_cache: cache, confirm: true)
|
|
166
|
+
expect(result).to include(cache_handoff: true, verified: true)
|
|
167
|
+
wire = wire_class.new([select])
|
|
168
|
+
expect(oath.execute(connection: wire, args: %w[access forget], encrypted_cache: cache, confirm: true)).to include(forgotten: true, verified: true)
|
|
169
|
+
expect(wire.complete?).to be(true)
|
|
170
|
+
wire = wire_class.new([])
|
|
171
|
+
expect { oath.execute(connection: wire, args: %w[access remember], password: 'synthetic oath', confirm: true) }.to raise_error(ArgumentError, /encrypted_cache/)
|
|
172
|
+
expect(wire.seen).to be_empty
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'resets with DEAD and verifies new salt plus empty inventory' do
|
|
176
|
+
wire = wire_class.new([select, %w[0004dead00 9000], [select[0], '7903050800710838373635343332319000'], %w[00a1000000 9000]])
|
|
177
|
+
expect(oath.execute(connection: wire, args: ['reset'], confirm: true)).to include(reset: true, verified: true)
|
|
178
|
+
expect(wire.complete?).to be(true)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'calculates all TOTP codes but leaves HOTP and touch records untriggered' do
|
|
182
|
+
response = field(0x71, 'acct') + field(0x76, ['0600000001'].pack('H*')) + field(0x71, 'hotp') + field(0x77, "\x06") + field(0x71, 'touch') + field(0x7c, "\x06")
|
|
183
|
+
wire = wire_class.new([select, [apdu(0xa4, field(0x74, "#{"\x00" * 7}\x01"), param2: 1), "#{response.unpack1('H*')}9000"]])
|
|
184
|
+
values = oath.calculate_all(connection: wire, timestamp: 59, confirm: true)
|
|
185
|
+
expect(values.map { |record| record[:code]&.fetch(:value) }).to eq(['000001', nil, nil])
|
|
186
|
+
expect(values.map { |record| record[:oath_type] }).to eq(%i[totp hotp totp])
|
|
187
|
+
expect(wire.complete?).to be(true)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'exposes raw HMAC calculation without treating it as a numeric code' do
|
|
191
|
+
payload = field(0x71, 'acct') + field(0x74, 'challenge')
|
|
192
|
+
wire = wire_class.new([select, %w[00a1000000 720521616363749000], [apdu(0xa2, payload), "751506#{'ab' * 20}9000"]])
|
|
193
|
+
expect(oath.calculate(connection: wire, account: 'acct', challenge: 'challenge', confirm: true)).to eq(['ab' * 20].pack('H*'))
|
|
194
|
+
expect(wire.complete?).to be(true)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'imports an in-memory PSKC container without skipping invalid records' do
|
|
198
|
+
xml = '<KeyContainer xmlns="urn:ietf:params:xml:ns:keyprov:pskc" Version="1.0"><KeyPackage><Key Id="acct" Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:totp"><AlgorithmParameters><ResponseFormat Length="6" Encoding="DECIMAL"/></AlgorithmParameters><Data><Secret><PlainValue>MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=</PlainValue></Secret></Data></Key></KeyPackage></KeyContainer>'
|
|
199
|
+
payload = field(0x71, 'acct') + field(0x73, "\x21\x0612345678901234567890")
|
|
200
|
+
wire = wire_class.new([select, %w[00a1000000 9000], [apdu(1, payload), '9000'], %w[00a1000000 720521616363749000]])
|
|
201
|
+
expect(oath.execute(connection: wire, args: %w[accounts import], pskc: xml, confirm: true)).to include(include(id: 'acct', verified: true))
|
|
202
|
+
expect(wire.complete?).to be(true)
|
|
203
|
+
wire = wire_class.new([])
|
|
204
|
+
expect { oath.execute(connection: wire, args: %w[accounts import], pskc: xml.sub('pskc:totp', 'pskc:unknown'), confirm: true) }.to raise_error(ArgumentError)
|
|
205
|
+
expect(wire.seen).to be_empty
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'unwraps a PSKC AES key-wrap secret using the RFC 3394 published vector' do
|
|
209
|
+
xml = '<KeyContainer xmlns="urn:ietf:params:xml:ns:keyprov:pskc" xmlns:x="http://www.w3.org/2001/04/xmlenc#" Version="1.0"><KeyPackage><Key Id="acct" Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:hotp"><Data><Secret><EncryptedValue><x:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#kw-aes128"/><x:CipherData><x:CipherValue>H6aLCoEStEeu80vY+1p7gp0+hiNx0s/l</x:CipherValue></x:CipherData></EncryptedValue></Secret><Counter><PlainValue>0</PlainValue></Counter></Data></Key></KeyPackage></KeyContainer>'
|
|
210
|
+
secret = ['00112233445566778899aabbccddeeff'].pack('H*')
|
|
211
|
+
payload = field(0x71, 'acct') + field(0x73, "\x11\x06".b + secret)
|
|
212
|
+
wire = wire_class.new([select, %w[00a1000000 9000], [apdu(1, payload), '9000'], %w[00a1000000 720511616363749000]])
|
|
213
|
+
key = ['000102030405060708090a0b0c0d0e0f'].pack('H*')
|
|
214
|
+
expect(oath.execute(connection: wire, args: %w[accounts import], pskc: xml, pskc_key: key, confirm: true)).to include(include(id: 'acct', verified: true))
|
|
215
|
+
expect(wire.complete?).to be(true)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'rejects malformed XML and an invalid later import record before any APDU' do
|
|
219
|
+
wire = wire_class.new([])
|
|
220
|
+
expect { oath.execute(connection: wire, args: %w[accounts import], pskc: '<!DOCTYPE a [<!ENTITY x SYSTEM "file:///none">]><a/>', confirm: true) }.to raise_error(ArgumentError)
|
|
221
|
+
expect { oath.execute(connection: wire, args: %w[accounts import], credentials: [{ name: 'one', secret: 'x' * 20 }, { name: 'two', secret: 'x' }], confirm: true) }.to raise_error(ArgumentError)
|
|
222
|
+
expect(wire.seen).to be_empty
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it 'imports authenticated AES-CBC PSKC with PBKDF2 and rejects a bad MAC before I/O' do
|
|
226
|
+
# Independently encrypted with Python cryptography AES-CBC / hashlib PBKDF2.
|
|
227
|
+
xml = <<~XML
|
|
228
|
+
<KeyContainer xmlns="urn:ietf:params:xml:ns:keyprov:pskc" xmlns:x="http://www.w3.org/2001/04/xmlenc#" xmlns:k="http://www.w3.org/2009/xmlenc11#" Version="1.0">
|
|
229
|
+
<EncryptionKey><k:DerivedKey><k:KeyDerivationMethod Algorithm="http://www.w3.org/2009/xmlenc11#pbkdf2"><k:PBKDF2-params><k:Salt><k:Specified>MDEyMzQ1Njc4OWFiY2RlZg==</k:Specified></k:Salt><k:IterationCount>1000</k:IterationCount><k:KeyLength>16</k:KeyLength><k:PRF Algorithm="http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"/></k:PBKDF2-params></k:KeyDerivationMethod></k:DerivedKey></EncryptionKey>
|
|
230
|
+
<MACMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"><MACKey><x:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/><x:CipherData><x:CipherValue>SkpKSkpKSkpKSkpKSkpKSmxrtkfn/Z3m3XbUoFWKDNZhAjYTOVdo4oaO5plAkray</x:CipherValue></x:CipherData></MACKey></MACMethod>
|
|
231
|
+
<KeyPackage><Key Id="acct" Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:totp"><Data><Secret><EncryptedValue><x:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/><x:CipherData><x:CipherValue>SUlJSUlJSUlJSUlJSUlJSdb4gWWgYtJgOxUEkeaFZQt1DfO02SIT0Ze547y+hMFy</x:CipherValue></x:CipherData></EncryptedValue><ValueMAC>RO0CeBbbOXmD5PARLx3KS5LiCdVI9dgZgoUMW7FjX3A=</ValueMAC></Secret></Data></Key></KeyPackage>
|
|
232
|
+
</KeyContainer>
|
|
233
|
+
XML
|
|
234
|
+
payload = field(0x71, 'acct') + field(0x73, "\x21\x0612345678901234567890")
|
|
235
|
+
wire = wire_class.new([select, %w[00a1000000 9000], [apdu(1, payload), '9000'], %w[00a1000000 720521616363749000]])
|
|
236
|
+
expect(oath.execute(connection: wire, args: %w[accounts import], pskc: xml, pskc_password: 'synthetic PSKC', confirm: true)).to include(include(verified: true))
|
|
237
|
+
expect(wire.complete?).to be(true)
|
|
238
|
+
wire = wire_class.new([])
|
|
239
|
+
expect { oath.execute(connection: wire, args: %w[accounts import], pskc: xml.sub('RO0Ce', 'AO0Ce'), pskc_password: 'synthetic PSKC', confirm: true) }.to raise_error(ArgumentError, /integrity/)
|
|
240
|
+
expect(wire.seen).to be_empty
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'uses OATH SEND REMAINING with zero Le across fragmented LIST responses' do
|
|
244
|
+
wire = wire_class.new([select, %w[00a1000000 72056104], %w[00a5000000 21616363749000]])
|
|
245
|
+
expect(oath.execute(connection: wire, args: %w[accounts list])).to include(include(id: 'acct'))
|
|
246
|
+
expect(wire.complete?).to be(true)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it 'matches independent RFC 6238 SHA1, SHA256 and SHA512 truncation vectors' do
|
|
250
|
+
[[1, 59, '41397eea', '94287082'], [1, 1_111_111_109, '3610f84c', '07081804'], [2, 59, '2c78e04e', '46119246'], [3, 59, '1d3f6530', '90693936']].each do |algorithm, time, truncated, value|
|
|
251
|
+
inventory = "#{field(0x72, "#{[0x20 | algorithm].pack('C')}acct").unpack1('H*')}9000"
|
|
252
|
+
payload = field(0x71, 'acct') + field(0x74, [time / 30].pack('Q>'))
|
|
253
|
+
wire = wire_class.new([select, ['00a1000000', inventory], [apdu(0xa2, payload, param2: 1), "760508#{truncated}9000"]])
|
|
254
|
+
expect(oath.code(connection: wire, account: 'acct', timestamp: time, confirm: true)).to eq(value)
|
|
255
|
+
expect(wire.complete?).to be(true)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it 'uses the nonstandard period encoded in the exact credential ID' do
|
|
260
|
+
payload = field(0x71, '60/acct') + field(0x74, [1].pack('Q>'))
|
|
261
|
+
wire = wire_class.new([select, ['00a1000000', "#{field(0x72, "\x2260/acct").unpack1('H*')}9000"], [apdu(0xa2, payload, param2: 1), '760506000000019000']])
|
|
262
|
+
expect(oath.code(connection: wire, account: '60/acct', timestamp: 119, confirm: true)).to eq('000001')
|
|
263
|
+
expect(wire.complete?).to be(true)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it 'imports HOTP touch/counter attributes using the property pair rather than a TLV' do
|
|
267
|
+
secret = 'x' * 20
|
|
268
|
+
payload = "#{field(0x71, 'acct')}#{field(0x73, "\x12\x07#{secret}")}\x78\x02#{field(0x7a, [7].pack('N'))}"
|
|
269
|
+
wire = wire_class.new([select, %w[00a1000000 9000], [apdu(1, payload), '9000'], %w[00a1000000 720512616363749000]])
|
|
270
|
+
expect(oath.execute(connection: wire, args: %w[accounts add acct --oath-type HOTP --algorithm SHA256 --digits 7 --counter 7 --touch], secret: secret, confirm: true)).to include(oath_type: :hotp, algorithm: :sha256)
|
|
271
|
+
expect(wire.complete?).to be(true)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it 'refuses overwrite, unsupported touch and unsupported rename without mutation' do
|
|
275
|
+
wire = wire_class.new([select, %w[00a1000000 720521616363749000]])
|
|
276
|
+
expect { oath.execute(connection: wire, args: %w[accounts add acct], secret: 'x' * 20, confirm: true) }.to raise_error(ArgumentError, /overwrite/)
|
|
277
|
+
old = [select[0], '7903040205710831323334353637389000']
|
|
278
|
+
wire = wire_class.new([old])
|
|
279
|
+
expect { oath.execute(connection: wire, args: %w[accounts add acct --touch], secret: 'x' * 20, confirm: true) }.to raise_error(PWN::Plugins::YubiKey::Error, /unsupported/)
|
|
280
|
+
wire = wire_class.new([old])
|
|
281
|
+
expect { oath.execute(connection: wire, args: %w[accounts rename acct new], confirm: true) }.to raise_error(PWN::Plugins::YubiKey::Error, /unsupported/)
|
|
282
|
+
expect(wire.complete?).to be(true)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it 'fails mutual authentication without invoking list, fallback credentials or repair' do
|
|
286
|
+
allow(SecureRandom).to receive(:random_bytes).with(8).and_return('ABCDEFGH')
|
|
287
|
+
wire = wire_class.new([[select[0], '790305080071083132333435363738740861626364656667689000'], ['00a30000207514db45e133f47e3e7a338c965f23bb02012cc4330a74084142434445464748', "7514#{'00' * 20}9000"]])
|
|
288
|
+
expect { oath.code(connection: wire, account: 'acct', password: 'synthetic oath', confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /authentication/)
|
|
289
|
+
expect(wire.complete?).to be(true)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
it 'rejects duplicate inventory identities and malformed code responses' do
|
|
293
|
+
wire = wire_class.new([select, %w[00a1000000 72052161636374720521616363749000]])
|
|
294
|
+
expect { oath.code(connection: wire, account: 'acct', confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /Duplicate/)
|
|
295
|
+
["\x06\x01", "\x09\x00\x00\x00\x01"].each do |truncated|
|
|
296
|
+
wire = wire_class.new([select, %w[00a1000000 720511616363749000], ['00a20001087104616363747400', "#{field(0x76, truncated).unpack1('H*')}9000"]])
|
|
297
|
+
expect { oath.code(connection: wire, account: 'acct', confirm: true) }.to raise_error(PWN::Plugins::YubiKey::ResponseError, /Malformed/)
|
|
298
|
+
expect(wire.complete?).to be(true)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
it 'preserves URI touch policy and rejects nonboolean controls before I/O' do
|
|
303
|
+
payload = "#{field(0x71, 'acct')}#{field(0x73, "\x21\x06#{'1234567890'.ljust(14, "\x00")}")}\x78\x02"
|
|
304
|
+
wire = wire_class.new([select, %w[00a1000000 9000], [apdu(1, payload), '9000'], %w[00a1000000 720521616363749000]])
|
|
305
|
+
expect(oath.execute(connection: wire, args: %w[accounts uri --touch], uri: 'otpauth://totp/acct?secret=GEZDGNBVGY3TQOJQ', confirm: true)).to include(verified: true)
|
|
306
|
+
expect(wire.complete?).to be(true)
|
|
307
|
+
wire = wire_class.new([])
|
|
308
|
+
expect { oath.execute(connection: wire, args: %w[accounts add acct], generate: 'false', confirm: true) }.to raise_error(ArgumentError, /boolean/)
|
|
309
|
+
expect(wire.seen).to be_empty
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it 'supports all-account command routing without incrementing HOTP counters' do
|
|
313
|
+
response = field(0x71, 'hotp') + field(0x77, "\x06")
|
|
314
|
+
wire = wire_class.new([select, [apdu(0xa4, field(0x74, [1].pack('Q>')), param2: 1), "#{response.unpack1('H*')}9000"]])
|
|
315
|
+
expect(oath.execute(connection: wire, args: %w[accounts code], timestamp: 59, confirm: true)).to include(include(oath_type: :hotp, code: nil))
|
|
316
|
+
expect(wire.complete?).to be(true)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
it 'does not treat nonboolean direct cache controls as consent to a cache handoff' do
|
|
320
|
+
wire = wire_class.new([])
|
|
321
|
+
expect { oath.code(connection: wire, account: 'acct', confirm: true, remember: 'false') }.to raise_error(ArgumentError, /boolean/)
|
|
322
|
+
expect(wire.seen).to be_empty
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
it 'reads OATH version and protection state without trying an access password' do
|
|
326
|
+
wire = wire_class.new([select])
|
|
327
|
+
expect(oath.execute(connection: wire, args: ['info'])).to include(version: [5, 8, 0], locked: false)
|
|
328
|
+
expect(wire.complete?).to be(true)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
it 'propagates a calculation timeout without retry or hidden mutation' do
|
|
332
|
+
failure = ->(_packet) { raise PWN::Plugins::YubiKey::TimeoutError, 'synthetic cancelled I/O' }
|
|
333
|
+
wire = wire_class.new([select, %w[00a1000000 720511616363749000], ['00a20001087104616363747400', failure]])
|
|
334
|
+
expect { oath.code(connection: wire, account: 'acct', confirm: true) }.to raise_error(PWN::Plugins::YubiKey::TimeoutError)
|
|
335
|
+
expect(wire.complete?).to be(true)
|
|
336
|
+
expect(wire.seen.length).to eq(3)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it 'documents every public API and secret option in actual help output' do
|
|
340
|
+
expect { oath.help }.to output(/\.execute\(.*secret_base32:.*encrypted_cache:.*\.operations.*\.parse_uri.*\.code\(.*account:.*\.calculate\(.*challenge:.*\.calculate_all\(.*\.authors/m).to_stdout
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it 'keeps help local and does not claim groups as implemented leaves' do
|
|
344
|
+
wire = wire_class.new([])
|
|
345
|
+
expect(oath.execute(connection: wire, args: ['accounts'])).to include(application: :oath)
|
|
346
|
+
expect(oath.operations).not_to include('accounts', 'access')
|
|
347
|
+
expect(wire.seen).to be_empty
|
|
348
|
+
end
|
|
349
|
+
end
|