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,810 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'pwn/plugins/yubikey'
|
|
6
|
+
require 'pwn/plugins/yubikey/openpgp'
|
|
7
|
+
|
|
8
|
+
# Synthetic OpenPGP Card 3.4 APDUs. No encoder constants are borrowed from production.
|
|
9
|
+
# Every exchange is consumed in order; unexpected auth/write/retry fails immediately.
|
|
10
|
+
class OpenPGPWireConnection
|
|
11
|
+
attr_reader :commands, :descriptor
|
|
12
|
+
|
|
13
|
+
def initialize(script, descriptor = {})
|
|
14
|
+
@script = script.dup
|
|
15
|
+
@descriptor = { protocol: :apdu }.merge(descriptor)
|
|
16
|
+
@commands = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def exchange(opts = {})
|
|
20
|
+
raise 'wrong protocol' unless opts[:protocol] == :apdu
|
|
21
|
+
|
|
22
|
+
command = opts.fetch(:data).unpack1('H*')
|
|
23
|
+
@commands << command
|
|
24
|
+
expected, reply = @script.shift
|
|
25
|
+
raise "Unexpected C-APDU header #{command[0, 8]}" unless expected
|
|
26
|
+
raise "Wrong C-APDU header #{command[0, 8]} (expected #{expected[0, 8]})" unless command == expected
|
|
27
|
+
raise reply if reply.is_a?(Exception)
|
|
28
|
+
|
|
29
|
+
[reply].pack('H*')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def complete?
|
|
33
|
+
@script.empty?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
RSpec.shared_context 'OpenPGP native wire' do
|
|
38
|
+
let(:api) { PWN::Plugins::YubiKey::OpenPGP }
|
|
39
|
+
|
|
40
|
+
def hx(bytes)
|
|
41
|
+
bytes.unpack1('H*')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def tagged(tag, bytes)
|
|
45
|
+
length = bytes.bytesize
|
|
46
|
+
encoded_length = if length < 128
|
|
47
|
+
[length].pack('C')
|
|
48
|
+
elsif length < 256
|
|
49
|
+
[0x81, length].pack('CC')
|
|
50
|
+
else
|
|
51
|
+
[0x82, length].pack('Cn')
|
|
52
|
+
end
|
|
53
|
+
[tag].pack('H*') + encoded_length + bytes
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def app_hex(overrides = {}, card_version = '0304')
|
|
57
|
+
fields = {
|
|
58
|
+
'c0' => '74000020080000fe0000', 'c1' => '010800001100',
|
|
59
|
+
'c2' => '010800001100', 'c3' => '010800001100',
|
|
60
|
+
'c4' => '007f7f7f030003', 'c5' => '00' * 60,
|
|
61
|
+
'c6' => '00' * 60, 'cd' => '00' * 12,
|
|
62
|
+
'de' => '010102020301', 'd6' => '0020', 'd7' => '0120', 'd8' => '0320'
|
|
63
|
+
}.merge(overrides).compact
|
|
64
|
+
discretion = fields.map { |tag, value| tagged(tag, [value].pack('H*')) }.join
|
|
65
|
+
outer = ["4f10d27600012401#{card_version}00061234567800005f5203000000"].pack('H*') + tagged('73', discretion)
|
|
66
|
+
hx(tagged('6e', outer))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def command_chunks(header, bytes)
|
|
70
|
+
chunks = bytes.bytes.each_slice(255).map { |chunk| chunk.pack('C*') }
|
|
71
|
+
return [header] if chunks.empty?
|
|
72
|
+
|
|
73
|
+
chunks.map.with_index do |chunk, index|
|
|
74
|
+
cla = header[0, 2].to_i(16) | (index == chunks.length - 1 ? 0 : 0x10)
|
|
75
|
+
format('%<cla>02x%<header>s%<length>02x%<payload>s', cla: cla, header: header[2..], length: chunk.bytesize, payload: hx(chunk))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def fixture_certificate
|
|
80
|
+
key = OpenSSL::PKey::EC.generate('prime256v1')
|
|
81
|
+
cert = OpenSSL::X509::Certificate.new
|
|
82
|
+
cert.version = 2
|
|
83
|
+
cert.serial = 1
|
|
84
|
+
cert.subject = cert.issuer = OpenSSL::X509::Name.parse('/CN=OpenPGP offline fixture')
|
|
85
|
+
cert.public_key = key
|
|
86
|
+
cert.not_before = Time.at(0)
|
|
87
|
+
cert.not_after = Time.at(1_000_000)
|
|
88
|
+
cert.sign(key, OpenSSL::Digest.new('SHA256'))
|
|
89
|
+
cert
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def prelude(version = '050701', overrides = {})
|
|
93
|
+
[
|
|
94
|
+
%w[00a4040006d27600012401 9000],
|
|
95
|
+
['00f1000000', "#{version}9000"],
|
|
96
|
+
['00ca006e00', "#{app_hex(overrides)}9000"]
|
|
97
|
+
]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
RSpec.describe PWN::Plugins::YubiKey do
|
|
102
|
+
include_context 'OpenPGP native wire'
|
|
103
|
+
|
|
104
|
+
it 'reads version, capability, PIN and key metadata from actual SELECT and GET DATA APDUs' do
|
|
105
|
+
connection = OpenPGPWireConnection.new(prelude + [%w[00ca007a00 7a05930300000a9000]])
|
|
106
|
+
result = api.execute(args: ['info'], connection: connection)
|
|
107
|
+
expect(result).to include(application: :openpgp, firmware: [5, 7, 1], card_version: [3, 4], signature_counter: 10)
|
|
108
|
+
expect(result[:pin_status]).to include(user: 3, reset: 0, admin: 3, signature_policy: :always)
|
|
109
|
+
expect(result[:keys][:sig]).to include(origin: :generated, algorithm: :rsa, bits: 2048, touch: :off)
|
|
110
|
+
expect(result[:keys][:dec]).to include(origin: :imported, touch: :on)
|
|
111
|
+
expect(result[:keys][:aut]).to include(origin: :generated, touch: :cached)
|
|
112
|
+
expect(connection).to be_complete
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'changes a user PIN with CHANGE REFERENCE DATA and reads fresh retry status' do
|
|
116
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
117
|
+
%w[002400810c313131313131323232323232 9000], %w[00ca00c400 007f7f7f0300039000]
|
|
118
|
+
])
|
|
119
|
+
result = api.execute(args: %w[access change-pin], connection: connection, confirm: true, pin: '111111', new_pin: '222222')
|
|
120
|
+
expect(result).to include(changed: :pin, pin_status: include(user: 3))
|
|
121
|
+
expect(connection).to be_complete
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'does not authenticate or query a provider without literal consent' do
|
|
125
|
+
connection = double('unused connection')
|
|
126
|
+
provider = double('secret provider')
|
|
127
|
+
expect(connection).not_to receive(:exchange)
|
|
128
|
+
expect(provider).not_to receive(:call)
|
|
129
|
+
expect { api.execute(args: %w[access change-pin], connection: connection, confirm: 'true', secret_provider: provider) }.to raise_error(described_class::ConfirmationRequired)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'does not verify an old credential when the replacement has invalid length' do
|
|
133
|
+
connection = OpenPGPWireConnection.new(prelude)
|
|
134
|
+
expect { api.execute(args: %w[access change-pin], connection: connection, confirm: true, pin: '111111', new_pin: 'x') }.to raise_error(ArgumentError, /length/)
|
|
135
|
+
expect(connection.commands.none? { |command| %w[0020 0024].include?(command[0, 4]) }).to be true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'signs RSA DigestInfo through PSO and returns a host-verifiable signature' do
|
|
139
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
140
|
+
digest_info = '3031300d060960864801650304020105000420ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
|
|
141
|
+
signature = key.sign('SHA256', 'abc')
|
|
142
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
143
|
+
%w[0020008106313131313131 9000], ["002a9e9a33#{digest_info}", "#{hx(signature)}9000"]
|
|
144
|
+
])
|
|
145
|
+
result = api.sign(connection: connection, confirm: true, pin: '111111', message: 'abc', hash_algorithm: :sha256)
|
|
146
|
+
expect(result).to eq(signature)
|
|
147
|
+
expect(key.public_key.verify('SHA256', result, 'abc')).to be true
|
|
148
|
+
expect(connection).to be_complete
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'changes an admin PIN without first verifying it or replaying CHANGE' do
|
|
152
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
153
|
+
%w[002400831038383838383838383939393939393939 9000], %w[00ca00c400 007f7f7f0300039000]
|
|
154
|
+
])
|
|
155
|
+
result = api.execute(args: %w[access change-admin-pin], connection: connection, confirm: true, admin_pin: '88888888', new_admin_pin: '99999999')
|
|
156
|
+
expect(result[:changed]).to eq(:admin_pin)
|
|
157
|
+
expect(connection).to be_complete
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'sets retries after explicit admin authentication and verifies counters by reading C4' do
|
|
161
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
162
|
+
%w[00200083083838383838383838 9000], %w[00f2000003050607 9000], %w[00ca00c400 007f7f7f0506079000]
|
|
163
|
+
])
|
|
164
|
+
result = api.execute(args: %w[access set-retries 5 6 7], connection: connection, confirm: true, admin_pin: '88888888')
|
|
165
|
+
expect(result).to include(verified: true, pins_reset: false)
|
|
166
|
+
expect(connection).to be_complete
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'rejects invalid retry counts before admin authentication or connection I/O' do
|
|
170
|
+
connection = OpenPGPWireConnection.new([])
|
|
171
|
+
expect { api.execute(args: %w[access set-retries 0 2 3], connection: connection, confirm: true) }.to raise_error(ArgumentError, /retries/)
|
|
172
|
+
expect(connection.commands).to be_empty
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'sets a reset code using PUT DATA only after validating both secrets' do
|
|
176
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
177
|
+
%w[00200083083838383838383838 9000], %w[00da00d3083737373737373737 9000], %w[00ca00c400 007f7f7f0303039000]
|
|
178
|
+
])
|
|
179
|
+
result = api.execute(args: %w[access change-reset-code], connection: connection, confirm: true, admin_pin: '88888888', reset_code: '77777777')
|
|
180
|
+
expect(result[:pin_status][:reset]).to eq(3)
|
|
181
|
+
expect(connection).to be_complete
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'unblocks using the reset code with RESET RETRY COUNTER P1 zero and no VERIFY' do
|
|
185
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
186
|
+
%w[002c00810e3737373737373737323232323232 9000], %w[00ca00c400 007f7f7f0303039000]
|
|
187
|
+
])
|
|
188
|
+
result = api.execute(args: %w[access unblock-pin], connection: connection, confirm: true, reset_code: '77777777', new_pin: '222222')
|
|
189
|
+
expect(result).to include(unblocked: true)
|
|
190
|
+
expect(connection).to be_complete
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'unblocks using admin authorization with RESET RETRY COUNTER P1 two' do
|
|
194
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
195
|
+
%w[00200083083838383838383838 9000], %w[002c028106323232323232 9000], %w[00ca00c400 007f7f7f0300039000]
|
|
196
|
+
])
|
|
197
|
+
result = api.execute(args: %w[access unblock-pin], connection: connection, confirm: true, admin_pin: '88888888', new_pin: '222222')
|
|
198
|
+
expect(result).to include(unblocked: true)
|
|
199
|
+
expect(connection).to be_complete
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it 'sets signature PIN policy and rejects a mismatching readback without replay' do
|
|
203
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
204
|
+
%w[00200083083838383838383838 9000], %w[00da00c40101 9000], %w[00ca00c400 007f7f7f0300039000]
|
|
205
|
+
])
|
|
206
|
+
expect { api.execute(args: %w[access set-signature-policy once], connection: connection, confirm: true, admin_pin: '88888888') }.to raise_error(described_class::ResponseError, /readback/)
|
|
207
|
+
expect(connection).to be_complete
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'applies card KDF S2K bytes for PIN changes using the card salt and byte count' do
|
|
211
|
+
salt = 'saltsalt'
|
|
212
|
+
old_hash = OpenSSL::Digest::SHA256.digest(("#{salt}111111" * 2) + "#{salt}111111"[0, 4])
|
|
213
|
+
new_hash = OpenSSL::Digest::SHA256.digest(("#{salt}222222" * 2) + "#{salt}222222"[0, 4])
|
|
214
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c0' => '75000020080000fe0000') + [
|
|
215
|
+
%w[00ca00f900 810103820108830400000020840873616c7473616c749000],
|
|
216
|
+
["0024008140#{hx(old_hash + new_hash)}", '9000'], %w[00ca00c400 007f7f7f0300039000]
|
|
217
|
+
])
|
|
218
|
+
expect(api.execute(args: %w[access change-pin], connection: connection, confirm: true, pin: '111111', new_pin: '222222')[:changed]).to eq(:pin)
|
|
219
|
+
expect(connection).to be_complete
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it 'sets touch UIF including its button byte and verifies it by fresh GET DATA' do
|
|
223
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
224
|
+
%w[00ca00d600 00209000], %w[00200083083838383838383838 9000],
|
|
225
|
+
%w[00da00d6020420 9000], %w[00ca00d600 04209000]
|
|
226
|
+
])
|
|
227
|
+
result = api.execute(args: %w[keys set-touch sig cached-fixed], connection: connection, confirm: true, admin_pin: '88888888')
|
|
228
|
+
expect(result).to include(touch: :'cached-fixed', irreversible: true, verified: true)
|
|
229
|
+
expect(connection).to be_complete
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'refuses changing an already fixed touch policy before admin verification' do
|
|
233
|
+
connection = OpenPGPWireConnection.new(prelude + [%w[00ca00d600 02209000]])
|
|
234
|
+
expect { api.execute(args: %w[keys set-touch sig off], connection: connection, confirm: true, admin_pin: '88888888') }.to raise_error(api::UnsupportedError, /fixed/i)
|
|
235
|
+
expect(connection).to be_complete
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'reads key metadata from application data without authentication' do
|
|
239
|
+
connection = OpenPGPWireConnection.new(prelude)
|
|
240
|
+
expect(api.execute(args: %w[keys info dec], connection: connection)).to include(slot: :dec, origin: :imported)
|
|
241
|
+
expect(connection).to be_complete
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'exports a real DER certificate after selecting the signature slot' do
|
|
245
|
+
certificate = fixture_certificate
|
|
246
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
247
|
+
%w[00a502040660045c027f21 9000], ['00ca7f2100', "#{hx(certificate.to_der)}9000"]
|
|
248
|
+
])
|
|
249
|
+
result = api.execute(args: %w[certificates export sig - --format DER], connection: connection)
|
|
250
|
+
expect(result).to eq(certificate.to_der)
|
|
251
|
+
expect(OpenSSL::X509::Certificate.new(result).verify(certificate.public_key)).to be true
|
|
252
|
+
expect(connection).to be_complete
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it 'imports a certificate through command chaining and verifies the exact stored DER' do
|
|
256
|
+
certificate = fixture_certificate
|
|
257
|
+
script = prelude + [%w[00200083083838383838383838 9000], %w[00a501040660045c027f21 9000]]
|
|
258
|
+
script += command_chunks('00da7f21', certificate.to_der).map { |command| [command, '9000'] }
|
|
259
|
+
script += [%w[00a501040660045c027f21 9000], ['00ca7f2100', "#{hx(certificate.to_der)}9000"]]
|
|
260
|
+
connection = OpenPGPWireConnection.new(script)
|
|
261
|
+
result = api.execute(args: %w[certificates import dec -], connection: connection, certificate: certificate.to_pem, confirm: true, admin_pin: '88888888')
|
|
262
|
+
expect(result).to include(imported: :certificate, verified: true)
|
|
263
|
+
expect(connection.commands.any? { |command| command.start_with?('10da7f21ff') }).to be true
|
|
264
|
+
expect(connection).to be_complete
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it 'deletes certificates using an empty PUT DATA and reads the empty slot back' do
|
|
268
|
+
connection = OpenPGPWireConnection.new(prelude('050403') + [
|
|
269
|
+
%w[00200083083838383838383838 9000], %w[00a50004070660045c027f21 9000], %w[00da7f2100 9000],
|
|
270
|
+
%w[00a50004070660045c027f21 9000], %w[00ca7f2100 9000]
|
|
271
|
+
])
|
|
272
|
+
result = api.execute(args: %w[certificates delete aut], connection: connection, confirm: true, admin_pin: '88888888')
|
|
273
|
+
expect(result).to include(deleted: :certificate, verified: true)
|
|
274
|
+
expect(connection).to be_complete
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it 'attests a generated key with proprietary CLA 80 and returns the created certificate' do
|
|
278
|
+
certificate = fixture_certificate
|
|
279
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
280
|
+
%w[0020008106313131313131 9000], %w[80fb010000 9000],
|
|
281
|
+
%w[00a502040660045c027f21 9000], ['00ca7f2100', "#{hx(certificate.to_der)}9000"]
|
|
282
|
+
])
|
|
283
|
+
result = api.execute(args: %w[keys attest sig -], connection: connection, confirm: true, pin: '111111')
|
|
284
|
+
expect(result).to eq(certificate.to_pem)
|
|
285
|
+
expect(connection).to be_complete
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
it 'resets by deliberately exhausting only observed retry counters before TERMINATE and ACTIVATE' do
|
|
289
|
+
script = prelude('050701', 'c4' => '007f7f7f010001') + [
|
|
290
|
+
%w[00200081080000000000000000 6982], %w[00ca00c400 007f7f7f0000019000],
|
|
291
|
+
%w[00200083080000000000000000 6983], %w[00ca00c400 007f7f7f0000009000],
|
|
292
|
+
%w[00e6000000 9000], %w[0044000000 9000], %w[00a4040006d27600012401 9000],
|
|
293
|
+
['00ca006e00', "#{app_hex('de' => '010002000300')}9000"]
|
|
294
|
+
]
|
|
295
|
+
connection = OpenPGPWireConnection.new(script)
|
|
296
|
+
result = api.execute(args: ['reset', '--force'], connection: connection, confirm: true)
|
|
297
|
+
expect(result).to include(reset: true, key_slots_verified_empty: true, attestation_preserved: true)
|
|
298
|
+
expect(connection).to be_complete
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it 'aborts a reset if the invalid sentinel unexpectedly verifies rather than trying guessed PINs' do
|
|
302
|
+
connection = OpenPGPWireConnection.new(prelude + [%w[00200081080000000000000000 9000]])
|
|
303
|
+
expect { api.execute(args: ['reset'], connection: connection, confirm: true) }.to raise_error(described_class::ResponseError, /unexpectedly verified/)
|
|
304
|
+
expect(connection).to be_complete
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it 'never treats --force as consent for reset or irreversible touch policies' do
|
|
308
|
+
connection = OpenPGPWireConnection.new([])
|
|
309
|
+
expect { api.execute(args: %w[reset --force], connection: connection) }.to raise_error(described_class::ConfirmationRequired)
|
|
310
|
+
expect { api.execute(args: %w[keys set-touch att fixed --force], connection: connection) }.to raise_error(described_class::ConfirmationRequired)
|
|
311
|
+
expect(connection.commands).to be_empty
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it 'returns local metadata for every OpenPGP group without a connection' do
|
|
315
|
+
[[], ['access'], ['keys'], ['certificates']].each do |args|
|
|
316
|
+
expect(api.execute(args: args)).to include(application: :openpgp, group: args.join(' '))
|
|
317
|
+
end
|
|
318
|
+
expect(api.operations).to include('info', 'access change-pin', 'keys attest', 'certificates import')
|
|
319
|
+
expect(api.operations).not_to include('access', 'keys', 'certificates')
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it 'rejects unknown commands and credential argv without touching a connection' do
|
|
323
|
+
connection = double('unopened connection')
|
|
324
|
+
expect(connection).not_to receive(:exchange)
|
|
325
|
+
expect { api.execute(args: %w[keys explode], connection: connection) }.to raise_error(ArgumentError)
|
|
326
|
+
expect { api.execute(args: ['access', 'change-pin', '--pin', 'synthetic-pin'], connection: connection) }.to raise_error(ArgumentError)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
RSpec.describe PWN::Plugins::YubiKey, 'native cryptographic operations' do
|
|
331
|
+
include_context 'OpenPGP native wire'
|
|
332
|
+
|
|
333
|
+
it 'converts ECDSA r||s card signatures to canonical DER verified by OpenSSL' do
|
|
334
|
+
key = OpenSSL::PKey::EC.generate('prime256v1')
|
|
335
|
+
digest = OpenSSL::Digest.digest('SHA256', 'abc')
|
|
336
|
+
der = key.dsa_sign_asn1(digest)
|
|
337
|
+
parts = OpenSSL::ASN1.decode(der).value.map { |number| number.value.to_s(2).rjust(32, "\0") }
|
|
338
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c1' => '132a8648ce3d030107') + [
|
|
339
|
+
%w[0020008106313131313131 9000], ["002a9e9a20#{hx(digest)}", "#{hx(parts.join)}9000"]
|
|
340
|
+
])
|
|
341
|
+
result = api.sign(connection: connection, confirm: true, pin: '111111', message: 'abc')
|
|
342
|
+
expect(result).to eq(der)
|
|
343
|
+
expect(key.dsa_verify_asn1(digest, result)).to be true
|
|
344
|
+
expect(connection).to be_complete
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it 'performs Ed25519 INTERNAL AUTHENTICATE on the raw message with extended PW1' do
|
|
348
|
+
seed = ['9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60'].pack('H*')
|
|
349
|
+
key = OpenSSL::PKey.read(['302e020100300506032b657004220420'].pack('H*') + seed)
|
|
350
|
+
signature = key.sign(nil, 'abc')
|
|
351
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c3' => '162b06010401da470f01') + [
|
|
352
|
+
%w[0020008206313131313131 9000], ['0088000003616263', "#{hx(signature)}9000"]
|
|
353
|
+
])
|
|
354
|
+
result = api.authenticate(connection: connection, confirm: true, pin: '111111', message: 'abc')
|
|
355
|
+
expect(key.verify(nil, result, 'abc')).to be true
|
|
356
|
+
expect(connection).to be_complete
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
it 'deciphers an RSA PKCS1 encrypted block with padding indicator zero and command chaining' do
|
|
360
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
361
|
+
ciphertext = key.public_encrypt('offline message')
|
|
362
|
+
script = prelude + [%w[0020008206313131313131 9000]]
|
|
363
|
+
blocks = command_chunks('002a8086', "\0#{ciphertext}")
|
|
364
|
+
script += blocks.map.with_index { |command, index| [command, index == blocks.size - 1 ? "#{hx(key.private_decrypt(ciphertext))}9000" : '9000'] }
|
|
365
|
+
connection = OpenPGPWireConnection.new(script)
|
|
366
|
+
expect(api.decipher(connection: connection, confirm: true, pin: '111111', value: ciphertext)).to eq('offline message')
|
|
367
|
+
expect(connection.commands).to include(blocks[0])
|
|
368
|
+
expect(connection).to be_complete
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
it 'derives an X25519 secret using the OpenPGP A6/7F49/86 peer template' do
|
|
372
|
+
local = OpenSSL::PKey.generate_key('X25519')
|
|
373
|
+
peer = OpenSSL::PKey.generate_key('X25519')
|
|
374
|
+
point = OpenSSL::ASN1.decode(peer.public_to_der).value[1].value
|
|
375
|
+
secret = local.derive(peer)
|
|
376
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c2' => '122b060104019755010501') + [
|
|
377
|
+
%w[0020008206313131313131 9000], ["002a808627a6257f49228620#{hx(point)}", "#{hx(secret)}9000"]
|
|
378
|
+
])
|
|
379
|
+
expect(api.decipher(connection: connection, confirm: true, pin: '111111', value: peer)).to eq(peer.derive(local))
|
|
380
|
+
expect(connection).to be_complete
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
it 'rejects truncated ECDSA card signatures rather than returning invalid DER' do
|
|
384
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c1' => '132a8648ce3d030107') + [
|
|
385
|
+
%w[0020008106313131313131 9000], ["002a9e9a20#{hx(OpenSSL::Digest.digest('SHA256', 'abc'))}", "#{'01' * 63}9000"]
|
|
386
|
+
])
|
|
387
|
+
expect { api.sign(connection: connection, confirm: true, pin: '111111', message: 'abc') }.to raise_error(described_class::ResponseError, /signature/)
|
|
388
|
+
expect(connection).to be_complete
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
it 'rejects incompatible ECDH points before a PIN attempt' do
|
|
392
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c2' => '122a8648ce3d030107'))
|
|
393
|
+
peer = OpenSSL::PKey::EC.generate('secp384r1')
|
|
394
|
+
expect { api.decipher(connection: connection, confirm: true, pin: '111111', value: peer) }.to raise_error(ArgumentError, /curve/)
|
|
395
|
+
expect(connection).to be_complete
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
it 'rejects invalid prehashed lengths before a PIN attempt' do
|
|
399
|
+
connection = OpenPGPWireConnection.new(prelude)
|
|
400
|
+
expect { api.sign(connection: connection, confirm: true, pin: '111111', message: 'abc', prehashed: true) }.to raise_error(ArgumentError, /digest length/)
|
|
401
|
+
expect(connection).to be_complete
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
RSpec.describe PWN::Plugins::YubiKey, 'key provisioning' do
|
|
406
|
+
include_context 'OpenPGP native wire'
|
|
407
|
+
|
|
408
|
+
def public_wire(key)
|
|
409
|
+
if key.is_a?(OpenSSL::PKey::RSA)
|
|
410
|
+
inner = tagged('81', key.n.to_s(2)) + tagged('82', key.e.to_s(2))
|
|
411
|
+
else
|
|
412
|
+
raw = key.is_a?(OpenSSL::PKey::EC) ? key.public_key.to_octet_string(:uncompressed) : OpenSSL::ASN1.decode(key.public_to_der).value[1].value
|
|
413
|
+
inner = tagged('86', raw)
|
|
414
|
+
end
|
|
415
|
+
hx(tagged('7f49', inner))
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it 'generates an RSA key in-card and verifies its exported public key without private host generation' do
|
|
419
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
420
|
+
wire = public_wire(key)
|
|
421
|
+
connection = OpenPGPWireConnection.new(prelude('040305') + [
|
|
422
|
+
%w[00200083083838383838383838 9000], ['0047800002b600', "#{wire}9000"], ['0047810002b600', "#{wire}9000"]
|
|
423
|
+
])
|
|
424
|
+
result = api.generate_key(connection: connection, confirm: true, slot: :sig, algorithm: :rsa2048, admin_pin: '88888888')
|
|
425
|
+
expect(result).to be_a(OpenSSL::PKey::RSA)
|
|
426
|
+
expect(result.public_to_der).to eq(key.public_to_der)
|
|
427
|
+
expect(result).not_to be_private
|
|
428
|
+
expect(connection).to be_complete
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
it 'uses advertised repeated FA algorithm entries before changing attributes for EC generation' do
|
|
432
|
+
key = OpenSSL::PKey::EC.generate('prime256v1')
|
|
433
|
+
wire = public_wire(key)
|
|
434
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
435
|
+
%w[00ca00fa00 fa13c106010800001100c109132a8648ce3d0301079000],
|
|
436
|
+
%w[00200083083838383838383838 9000], %w[00da00c109132a8648ce3d030107 9000],
|
|
437
|
+
%w[00ca00c100 132a8648ce3d0301079000], ['0047800002b600', "#{wire}9000"], ['0047810002b600', "#{wire}9000"]
|
|
438
|
+
])
|
|
439
|
+
result = api.generate_key(connection: connection, confirm: true, slot: :sig, algorithm: :secp256r1, admin_pin: '88888888')
|
|
440
|
+
expect(result.public_to_der).to eq(key.public_to_der)
|
|
441
|
+
expect(connection).to be_complete
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
it 'rejects token-unsupported curves and vulnerable RSA generation before admin verification' do
|
|
445
|
+
connection = OpenPGPWireConnection.new(prelude + [%w[00ca00fa00 fa08c1060108000011009000]])
|
|
446
|
+
expect { api.generate_key(connection: connection, confirm: true, slot: :sig, algorithm: :secp384r1, admin_pin: '88888888') }.to raise_error(api::UnsupportedError)
|
|
447
|
+
expect(connection).to be_complete
|
|
448
|
+
vulnerable = OpenPGPWireConnection.new(prelude('040304'))
|
|
449
|
+
expect { api.generate_key(connection: vulnerable, confirm: true, slot: :sig, algorithm: :rsa2048, admin_pin: '88888888') }.to raise_error(api::UnsupportedError, /RSA/)
|
|
450
|
+
expect(vulnerable).to be_complete
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
it 'imports a real PKCS8 Ed25519 fixture as its seed rather than the DER wrapper' do
|
|
454
|
+
seed = ['9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60'].pack('H*')
|
|
455
|
+
key_der = ['302e020100300506032b657004220420'].pack('H*') + seed
|
|
456
|
+
key = OpenSSL::PKey.read(key_der)
|
|
457
|
+
template = tagged('4d', ['b6007f480292205f4820'].pack('H*') + seed)
|
|
458
|
+
script = prelude('050701', 'c1' => '162b06010401da470f01') + [
|
|
459
|
+
%w[00ca00fa00 fa0cc10a162b06010401da470f019000], %w[00200083083838383838383838 9000]
|
|
460
|
+
]
|
|
461
|
+
script += command_chunks('00db3fff', template).map { |command| [command, '9000'] }
|
|
462
|
+
script << ['0047810002b600', "#{public_wire(key)}9000"]
|
|
463
|
+
connection = OpenPGPWireConnection.new(script)
|
|
464
|
+
result = api.import_key(connection: connection, confirm: true, slot: :sig, key: key_der, admin_pin: '88888888')
|
|
465
|
+
expect(result).to include(imported: :key, slot: :sig, verified: true)
|
|
466
|
+
expect(result[:public_key].public_to_der).to eq(key.public_to_der)
|
|
467
|
+
expect(connection).to be_complete
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
it 'reverses only the X25519 raw scalar bytes for OpenPGP private key import' do
|
|
471
|
+
seed = ['77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a'].pack('H*')
|
|
472
|
+
key = OpenSSL::PKey.read(['302e020100300506032b656e04220420'].pack('H*') + seed)
|
|
473
|
+
template = tagged('4d', ['b8007f480292205f4820'].pack('H*') + seed.reverse)
|
|
474
|
+
script = prelude('050701', 'c2' => '122b060104019755010501') + [
|
|
475
|
+
%w[00ca00fa00 fa0dc20b122b0601040197550105019000], %w[00200083083838383838383838 9000]
|
|
476
|
+
]
|
|
477
|
+
script += command_chunks('00db3fff', template).map { |command| [command, '9000'] }
|
|
478
|
+
script << ['0047810002b800', "#{public_wire(key)}9000"]
|
|
479
|
+
connection = OpenPGPWireConnection.new(script)
|
|
480
|
+
expect(api.import_key(connection: connection, confirm: true, slot: :dec, key: key, admin_pin: '88888888')[:verified]).to be true
|
|
481
|
+
expect(connection).to be_complete
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
{
|
|
485
|
+
secp256r1: ['prime256v1', '132a8648ce3d030107', :sig],
|
|
486
|
+
secp256k1: ['secp256k1', '132b8104000a', :sig],
|
|
487
|
+
secp384r1: ['secp384r1', '132b81040022', :sig],
|
|
488
|
+
secp521r1: ['secp521r1', '132b81040023', :sig],
|
|
489
|
+
brainpoolp256r1: ['brainpoolP256r1', '132b2403030208010107', :sig],
|
|
490
|
+
brainpoolp384r1: ['brainpoolP384r1', '132b240303020801010b', :sig],
|
|
491
|
+
brainpoolp512r1: ['brainpoolP512r1', '122b240303020801010d', :dec],
|
|
492
|
+
ed25519: ['ED25519', '162b06010401da470f01', :sig],
|
|
493
|
+
x25519: ['X25519', '122b060104019755010501', :dec]
|
|
494
|
+
}.each do |algorithm, (curve, encoded, slot)|
|
|
495
|
+
it "generates advertised #{algorithm} keys and reconstructs the real OpenSSL public key" do
|
|
496
|
+
key = %w[ED25519 X25519].include?(curve) ? OpenSSL::PKey.generate_key(curve) : OpenSSL::PKey::EC.generate(curve)
|
|
497
|
+
tag = slot == :sig ? 'c1' : 'c2'
|
|
498
|
+
crt = slot == :sig ? 'b600' : 'b800'
|
|
499
|
+
connection = OpenPGPWireConnection.new(prelude('050701', tag => encoded) + [
|
|
500
|
+
['00ca00fa00', "#{hx(tagged('fa', tagged(tag, [encoded].pack('H*'))))}9000"],
|
|
501
|
+
%w[00200083083838383838383838 9000], ["0047800002#{crt}", "#{public_wire(key)}9000"], ["0047810002#{crt}", "#{public_wire(key)}9000"]
|
|
502
|
+
])
|
|
503
|
+
result = api.generate_key(connection: connection, confirm: true, slot: slot, algorithm: algorithm, admin_pin: '88888888')
|
|
504
|
+
expect(result.public_to_der).to eq(key.public_to_der)
|
|
505
|
+
expect(connection).to be_complete
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
[3072, 4096].each do |bits|
|
|
510
|
+
it "generates RSA#{bits} with the exact advertised modulus attributes" do
|
|
511
|
+
key = OpenSSL::PKey::RSA.generate(bits)
|
|
512
|
+
encoded = %w[010c00001100 011000001100][bits == 3072 ? 0 : 1]
|
|
513
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c3' => encoded) + [
|
|
514
|
+
['00ca00fa00', "#{hx(tagged('fa', tagged('c3', [encoded].pack('H*'))))}9000"],
|
|
515
|
+
%w[00200083083838383838383838 9000], ['0047800002a400', "#{public_wire(key)}9000"], ['0047810002a400', "#{public_wire(key)}9000"]
|
|
516
|
+
])
|
|
517
|
+
expect(api.generate_key(connection: connection, confirm: true, slot: :aut, algorithm: "rsa#{bits}", admin_pin: '88888888').n).to eq(key.n)
|
|
518
|
+
expect(connection).to be_complete
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
it 'imports a P521 scalar with 66-byte width rather than truncating the leading bit' do
|
|
523
|
+
key = OpenSSL::PKey::EC.generate('secp521r1')
|
|
524
|
+
scalar = key.private_key.to_s(2).rjust(66, "\0")
|
|
525
|
+
template = tagged('4d', ['b6007f480292425f4842'].pack('H*') + scalar)
|
|
526
|
+
script = prelude('050701', 'c1' => '132b81040023') + [
|
|
527
|
+
%w[00ca00fa00 fa08c106132b810400239000], %w[00200083083838383838383838 9000]
|
|
528
|
+
]
|
|
529
|
+
script += command_chunks('00db3fff', template).map { |command| [command, '9000'] }
|
|
530
|
+
script << ['0047810002b600', "#{public_wire(key)}9000"]
|
|
531
|
+
connection = OpenPGPWireConnection.new(script)
|
|
532
|
+
expect(api.import_key(connection: connection, slot: :sig, key: key.private_to_der, confirm: true, admin_pin: '88888888')[:verified]).to be true
|
|
533
|
+
expect(connection).to be_complete
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
it 'imports legacy NEO RSA CRT factors including iqmp, dp, dq and modulus' do
|
|
537
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
538
|
+
values = [key.e.to_s(2), *[key.p, key.q, key.iqmp, key.dmp1, key.dmq1].map { |num| num.to_s(2).rjust(128, "\0") }, key.n.to_s(2)].join
|
|
539
|
+
template = tagged('4d', ['b600'].pack('H*') + tagged('7f48', ['910392818093818094818095818096818097820100'].pack('H*')) + tagged('5f48', values))
|
|
540
|
+
script = prelude('010007', 'c1' => '010800001103') + [%w[00200083083838383838383838 9000]]
|
|
541
|
+
script += command_chunks('00db3fff', template).map { |command| [command, '9000'] }
|
|
542
|
+
script << ['0047810002b600', "#{public_wire(key)}9000"]
|
|
543
|
+
connection = OpenPGPWireConnection.new(script)
|
|
544
|
+
expect(api.import_key(connection: connection, confirm: true, slot: :sig, key: key, admin_pin: '88888888')[:verified]).to be true
|
|
545
|
+
expect(connection).to be_complete
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
it 'imports NEO RSA CRT when algorithm attributes are fixed rather than writable' do
|
|
549
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
550
|
+
values = [key.e.to_s(2), *[key.p, key.q, key.iqmp, key.dmp1, key.dmq1].map { |num| num.to_s(2).rjust(128, "\0") }, key.n.to_s(2)].join
|
|
551
|
+
template = tagged('4d', ['b600'].pack('H*') + tagged('7f48', ['910392818093818094818095818096818097820100'].pack('H*')) + tagged('5f48', values))
|
|
552
|
+
script = prelude('010007', 'c0' => '70000020080000fe0000') + [%w[00200083083838383838383838 9000]]
|
|
553
|
+
script += command_chunks('00db3fff', template).map { |command| [command, '9000'] }
|
|
554
|
+
script << ['0047810002b600', "#{public_wire(key)}9000"]
|
|
555
|
+
connection = OpenPGPWireConnection.new(script)
|
|
556
|
+
expect(api.import_key(connection: connection, confirm: true, slot: :sig, key: key, admin_pin: '88888888')[:verified]).to be true
|
|
557
|
+
expect(connection).to be_complete
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
it 'exports the public key as a typed read-only API without any authentication' do
|
|
561
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
562
|
+
connection = OpenPGPWireConnection.new(prelude + [['0047810002b600', "#{public_wire(key)}9000"]])
|
|
563
|
+
expect(api.public_key(connection: connection, slot: :sig).public_to_der).to eq(key.public_to_der)
|
|
564
|
+
expect(connection).to be_complete
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
it 'decrypts an actual encrypted PKCS8 key using only the required in-memory provider fields' do
|
|
568
|
+
key = OpenSSL::PKey.generate_key('ED25519')
|
|
569
|
+
pem = key.private_to_pem(OpenSSL::Cipher.new('aes-256-cbc'), 'offline fixture passphrase')
|
|
570
|
+
seed = OpenSSL::ASN1.decode(OpenSSL::ASN1.decode(key.private_to_der).value[2].value).value
|
|
571
|
+
template = tagged('4d', ['b6007f480292205f4820'].pack('H*') + seed)
|
|
572
|
+
script = prelude('050701', 'c1' => '162b06010401da470f01') + [
|
|
573
|
+
%w[00ca00fa00 fa0cc10a162b06010401da470f019000], %w[00200083083838383838383838 9000]
|
|
574
|
+
]
|
|
575
|
+
script += command_chunks('00db3fff', template).map { |command| [command, '9000'] }
|
|
576
|
+
script << ['0047810002b600', "#{public_wire(key)}9000"]
|
|
577
|
+
connection = OpenPGPWireConnection.new(script)
|
|
578
|
+
names = []
|
|
579
|
+
provider = lambda do |name|
|
|
580
|
+
names << name
|
|
581
|
+
{ key_password: 'offline fixture passphrase', admin_pin: '88888888' }.fetch(name)
|
|
582
|
+
end
|
|
583
|
+
expect(api.import_key(connection: connection, confirm: true, slot: :sig, key: pem, secret_provider: provider)[:verified]).to be true
|
|
584
|
+
expect(names).to eq(%i[key_password admin_pin])
|
|
585
|
+
expect(connection).to be_complete
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
it 'imports RSA private key factors with independent 7F48 length headers and chained PUT DATA ODD' do
|
|
589
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
590
|
+
components = key.e.to_s(2) + key.p.to_s(2).rjust(128, "\0") + key.q.to_s(2).rjust(128, "\0")
|
|
591
|
+
template = tagged('4d', ['b6038401817f48089103928180938180'].pack('H*') + tagged('5f48', components))
|
|
592
|
+
script = prelude('050701', 'da' => '010800001100') + [
|
|
593
|
+
%w[00ca00fa00 fa08da060108000011009000], %w[00200083083838383838383838 9000]
|
|
594
|
+
]
|
|
595
|
+
script += command_chunks('00db3fff', template).map { |command| [command, '9000'] }
|
|
596
|
+
script << ['0047810005b603840181', "#{public_wire(key)}9000"]
|
|
597
|
+
connection = OpenPGPWireConnection.new(script)
|
|
598
|
+
result = api.execute(args: %w[keys import att -], connection: connection, key: key.private_to_pem, confirm: true, admin_pin: '88888888')
|
|
599
|
+
expect(result).to include(imported: :key, verified: true)
|
|
600
|
+
expect(connection.commands.any? { |command| command.start_with?('10db3fffff') }).to be true
|
|
601
|
+
expect(connection).to be_complete
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
RSpec.describe PWN::Plugins::YubiKey, 'safety and firmware compatibility' do
|
|
606
|
+
include_context 'OpenPGP native wire'
|
|
607
|
+
|
|
608
|
+
it 'never replays an incorrect PIN or an uncertain signing command' do
|
|
609
|
+
connection = OpenPGPWireConnection.new(prelude + [%w[0020008106313131313131 63c2]])
|
|
610
|
+
expect { api.sign(connection: connection, confirm: true, pin: '111111', message: 'abc') }.to raise_error(described_class::Protocol::APDUError) { |error| expect(error.status).to eq(0x63C2) }
|
|
611
|
+
expect(connection).to be_complete
|
|
612
|
+
timeout = OpenPGPWireConnection.new(prelude + [
|
|
613
|
+
%w[0020008106313131313131 9000],
|
|
614
|
+
["002a9e9a333031300d060960864801650304020105000420#{hx(OpenSSL::Digest.digest('SHA256', 'abc'))}", described_class::TimeoutError.new('uncertain')]
|
|
615
|
+
])
|
|
616
|
+
expect { api.sign(connection: timeout, confirm: true, pin: '111111', message: 'abc') }.to raise_error(described_class::TimeoutError)
|
|
617
|
+
expect(timeout).to be_complete
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
it 'obtains only required credentials from the in-memory provider without logging them' do
|
|
621
|
+
names = []
|
|
622
|
+
provider = lambda { |name|
|
|
623
|
+
names << name
|
|
624
|
+
'88888888'
|
|
625
|
+
}
|
|
626
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
627
|
+
%w[00200083083838383838383838 9000], %w[00da00c40101 9000], %w[00ca00c400 017f7f7f0300039000]
|
|
628
|
+
])
|
|
629
|
+
result = api.execute(args: %w[access set-signature-policy once], connection: connection, confirm: true, secret_provider: provider)
|
|
630
|
+
expect(result[:verified]).to be true
|
|
631
|
+
expect(result.inspect).not_to include('88888888')
|
|
632
|
+
expect(names).to eq([:admin_pin])
|
|
633
|
+
expect(connection).to be_complete
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
it 'rejects absent credentials without guessing factory PINs' do
|
|
637
|
+
connection = OpenPGPWireConnection.new(prelude)
|
|
638
|
+
expect { api.sign(connection: connection, confirm: true, message: 'abc') }.to raise_error(ArgumentError, /pin/)
|
|
639
|
+
expect(connection).to be_complete
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
it 'rejects oversized Ed25519 messages before authenticating' do
|
|
643
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c1' => '162b06010401da470f01'))
|
|
644
|
+
expect { api.sign(connection: connection, confirm: true, pin: '111111', message: 'x' * 65_536) }.to raise_error(ArgumentError, /length/)
|
|
645
|
+
expect(connection).to be_complete
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
it 'does not automatically activate a terminated application during an info call' do
|
|
649
|
+
connection = OpenPGPWireConnection.new([%w[00a4040006d27600012401 6285]])
|
|
650
|
+
expect { api.execute(args: ['info'], connection: connection) }.to raise_error(described_class::Protocol::APDUError)
|
|
651
|
+
expect(connection).to be_complete
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
it 'activates a terminated application only as an explicitly confirmed reset and verifies it' do
|
|
655
|
+
connection = OpenPGPWireConnection.new([
|
|
656
|
+
%w[00a4040006d27600012401 6285], %w[0044000000 9000],
|
|
657
|
+
%w[00a4040006d27600012401 9000], ['00ca006e00', "#{app_hex('de' => '010002000300')}9000"]
|
|
658
|
+
])
|
|
659
|
+
expect(api.execute(args: ['reset'], connection: connection, confirm: true)[:key_slots_verified_empty]).to be true
|
|
660
|
+
expect(connection).to be_complete
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
it 'recovers a corrupted application only on reset with bounded deliberate blocking' do
|
|
664
|
+
connection = OpenPGPWireConnection.new([
|
|
665
|
+
%w[00a4040006d27600012401 6581], %w[00200081080000000000000000 6982], %w[00200081080000000000000000 6983],
|
|
666
|
+
%w[00200083080000000000000000 6983], %w[00e6000000 9000], %w[0044000000 9000],
|
|
667
|
+
%w[00a4040006d27600012401 9000], ['00ca006e00', "#{app_hex('de' => '010002000300')}9000"]
|
|
668
|
+
])
|
|
669
|
+
expect(api.execute(args: ['reset'], connection: connection, confirm: true)[:reset]).to be true
|
|
670
|
+
expect(connection).to be_complete
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
it 'exposes PIN verification and reset-verification with their explicit reference and version gate' do
|
|
674
|
+
connection = OpenPGPWireConnection.new(prelude + [%w[0020008206313131313131 9000]])
|
|
675
|
+
expect(api.verify_pin(connection: connection, confirm: true, pin: '111111', mode: :extended)).to include(verified: true, mode: :extended)
|
|
676
|
+
expect(connection).to be_complete
|
|
677
|
+
unverify = OpenPGPWireConnection.new(prelude + [%w[0020ff8200 9000]])
|
|
678
|
+
expect(api.unverify_pin(connection: unverify, confirm: true, mode: :extended)).to include(cleared: :extended)
|
|
679
|
+
expect(unverify).to be_complete
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
it 'exposes algorithm information without assuming absent capabilities' do
|
|
683
|
+
connection = OpenPGPWireConnection.new(prelude('040400'))
|
|
684
|
+
result = api.algorithms(connection: connection)
|
|
685
|
+
expect(result.keys).to eq(%i[sig dec aut])
|
|
686
|
+
expect(result[:sig].map { |entry| entry[:bits] }).to eq([2048])
|
|
687
|
+
expect(connection).to be_complete
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
it 'handles the documented pre-5.6.1 FA length and X25519 advertising errata narrowly' do
|
|
691
|
+
# FA declares two bytes too many; DEC contains the historical invalid EdDSA/X25519 entry.
|
|
692
|
+
connection = OpenPGPWireConnection.new(prelude('050403') + [%w[00ca00fa00 fa0fc20b162b0601040197550105019000]])
|
|
693
|
+
result = api.algorithms(connection: connection)
|
|
694
|
+
expect(result[:dec]).to include(include(algorithm: :ecdh, curve: :x25519))
|
|
695
|
+
expect(result[:dec]).not_to include(include(algorithm: :eddsa))
|
|
696
|
+
expect(connection).to be_complete
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
it 'rejects unrelated malformed FA lengths rather than inventing capabilities' do
|
|
700
|
+
connection = OpenPGPWireConnection.new(prelude + [%w[00ca00fa00 fa20c1060108000011009000]])
|
|
701
|
+
expect { api.algorithms(connection: connection) }.to raise_error(described_class::ResponseError)
|
|
702
|
+
expect(connection).to be_complete
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
it 'reports whether the actual card KDF object enables PIN hashing' do
|
|
706
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c0' => '75000020080000fe0000') + [
|
|
707
|
+
%w[00ca007a00 7a0593030000009000], %w[00ca00f900 810103820108830400000020840873616c7473616c749000]
|
|
708
|
+
])
|
|
709
|
+
expect(api.execute(args: ['info'], connection: connection)).to include(kdf_enabled: true)
|
|
710
|
+
expect(connection).to be_complete
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
it 'reads legacy UIF separately when it is absent from application data' do
|
|
714
|
+
raw = app_hex({ 'd6' => nil }, '0201')
|
|
715
|
+
connection = OpenPGPWireConnection.new([%w[00a4040006d27600012401 9000], %w[00f1000000 0403059000], ['00ca006e00', "#{raw}9000"], %w[00ca00d600 01209000]])
|
|
716
|
+
expect(api.execute(args: %w[keys info sig], connection: connection)[:touch]).to eq(:on)
|
|
717
|
+
expect(connection).to be_complete
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
it 'rejects misleading EC signing attributes before authentication' do
|
|
721
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c1' => '132b060104019755010501'))
|
|
722
|
+
expect { api.sign(connection: connection, confirm: true, message: 'abc', pin: '111111') }.to raise_error(api::UnsupportedError, /curve/)
|
|
723
|
+
expect(connection).to be_complete
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
it 'rejects empty slots before consuming a PIN attempt for crypto' do
|
|
727
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'de' => '010002000300'))
|
|
728
|
+
expect { api.sign(connection: connection, confirm: true, message: 'abc', pin: '111111') }.to raise_error(api::UnsupportedError, /empty/)
|
|
729
|
+
expect(connection).to be_complete
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
it 'rejects repeated format options and invalid certificates before authentication' do
|
|
733
|
+
connection = OpenPGPWireConnection.new([])
|
|
734
|
+
expect { api.execute(args: %w[keys attest sig - --format DER --format PEM], connection: connection, confirm: true) }.to raise_error(ArgumentError)
|
|
735
|
+
expect { api.execute(args: %w[certificates import sig -], certificate: 'invalid certificate', connection: connection, confirm: true) }.to raise_error(ArgumentError)
|
|
736
|
+
expect(connection.commands).to be_empty
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
it 'rejects oversized certificates and unsupported old certificate slots before admin verification' do
|
|
740
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c0' => '74000020001000fe0000'))
|
|
741
|
+
expect { api.execute(args: %w[certificates import sig -], certificate: fixture_certificate.to_der, connection: connection, confirm: true, admin_pin: '88888888') }.to raise_error(api::UnsupportedError, /certificate limit/)
|
|
742
|
+
expect(connection).to be_complete
|
|
743
|
+
old = OpenPGPWireConnection.new(prelude('040305'))
|
|
744
|
+
expect { api.execute(args: %w[certificates delete sig], connection: old, confirm: true, admin_pin: '88888888') }.to raise_error(api::UnsupportedError, /firmware/)
|
|
745
|
+
expect(old).to be_complete
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
it 'exports a certificate to an explicit exclusive output file through GET RESPONSE chaining' do
|
|
749
|
+
cert = fixture_certificate
|
|
750
|
+
der = cert.to_der
|
|
751
|
+
connection = OpenPGPWireConnection.new(prelude + [
|
|
752
|
+
%w[00a500040660045c027f21 9000], ['00ca7f2100', "#{hx(der[0, 128])}61ff"], ['00c00000ff', "#{hx(der[128..])}9000"]
|
|
753
|
+
])
|
|
754
|
+
Dir.mktmpdir('openpgp-cert-') do |dir|
|
|
755
|
+
output = File.join(dir, 'certificate.der')
|
|
756
|
+
result = api.execute(args: ['certificates', 'export', 'aut', output, '--format', 'DER'], connection: connection)
|
|
757
|
+
expect(File.binread(output)).to eq(result)
|
|
758
|
+
expect(result).to eq(der)
|
|
759
|
+
expect(File.stat(output).mode & 0o777).to eq(0o600)
|
|
760
|
+
end
|
|
761
|
+
expect(connection).to be_complete
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
it 'deciphers through the decrypt alias without changing its binary result' do
|
|
765
|
+
connection = OpenPGPWireConnection.new(prelude + [%w[0020008206313131313131 9000]] + command_chunks('002a8086', "\0".b + ('x' * 256)).map.with_index { |command, index| [command, index.zero? ? '9000' : '0001ff9000'] })
|
|
766
|
+
expect(api.decrypt(connection: connection, confirm: true, pin: '111111', value: 'x' * 256)).to eq("\0\x01\xFF".b)
|
|
767
|
+
expect(connection).to be_complete
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
it 'uses the admin-specific S2K salt and SHA512 when advertised by the card' do
|
|
771
|
+
digest = OpenSSL::Digest.digest('SHA512', 'adminslt88888888' * 4)
|
|
772
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c0' => '75000020080000fe0000') + [
|
|
773
|
+
%w[00ca00f900 81010382010a83040000004084087573657273616c74860861646d696e736c749000],
|
|
774
|
+
["0020008340#{hx(digest)}", '9000']
|
|
775
|
+
])
|
|
776
|
+
expect(api.verify_pin(connection: connection, confirm: true, admin_pin: '88888888', mode: :admin)[:verified]).to be true
|
|
777
|
+
expect(connection).to be_complete
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
it 'rejects an unsupported KDF instead of sending plaintext PINs' do
|
|
781
|
+
connection = OpenPGPWireConnection.new(prelude('050701', 'c0' => '75000020080000fe0000') + [%w[00ca00f900 81017f9000]])
|
|
782
|
+
expect { api.verify_pin(connection: connection, confirm: true, pin: '111111') }.to raise_error(api::UnsupportedError, /KDF/)
|
|
783
|
+
expect(connection).to be_complete
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
it 'disables unsupported touch or retry mutations before admin authentication' do
|
|
787
|
+
touch = OpenPGPWireConnection.new(prelude('010007'))
|
|
788
|
+
expect { api.execute(args: %w[keys set-touch sig on], connection: touch, confirm: true, admin_pin: '88888888') }.to raise_error(api::UnsupportedError)
|
|
789
|
+
expect(touch).to be_complete
|
|
790
|
+
retries = OpenPGPWireConnection.new(prelude('040300'))
|
|
791
|
+
expect { api.execute(args: %w[access set-retries 3 3 3], connection: retries, confirm: true, admin_pin: '88888888') }.to raise_error(api::UnsupportedError)
|
|
792
|
+
expect(retries).to be_complete
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
it 'documents every public method and preserves exact leaf coverage separately from groups' do
|
|
796
|
+
output = StringIO.new
|
|
797
|
+
original = $stdout
|
|
798
|
+
begin
|
|
799
|
+
$stdout = output
|
|
800
|
+
api.help
|
|
801
|
+
ensure
|
|
802
|
+
$stdout = original
|
|
803
|
+
end
|
|
804
|
+
%i[execute operations generate_key import_key sign authenticate decipher decrypt algorithms public_key verify_pin unverify_pin authors].each do |method|
|
|
805
|
+
expect(output.string).to include("#{api}.#{method}")
|
|
806
|
+
end
|
|
807
|
+
expect(api.operations.sort).to eq(['info', 'reset', 'access set-retries', 'access change-pin', 'access change-reset-code', 'access change-admin-pin', 'access unblock-pin', 'access set-signature-policy', 'keys info', 'keys set-touch', 'keys import', 'keys attest', 'certificates export', 'certificates delete', 'certificates import'].sort)
|
|
808
|
+
expect(output.string).to include('no factory credentials', 'ECDH returns a raw shared secret')
|
|
809
|
+
end
|
|
810
|
+
end
|