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,482 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require_relative '../protocol'
|
|
6
|
+
|
|
7
|
+
module PWN
|
|
8
|
+
module Plugins
|
|
9
|
+
module YubiKey
|
|
10
|
+
module SecurityDomain
|
|
11
|
+
# GlobalPlatform SCP03 (S8/AES128) and SCP11 session primitives.
|
|
12
|
+
# Protocol reference: GP Card Specification 2.3 Amendments D/F;
|
|
13
|
+
# interoperability reference: Yubico yubikit 5.9.1 core/smartcard/scp.py.
|
|
14
|
+
# Independently implemented Ruby; no executable or Python backend.
|
|
15
|
+
module SCP
|
|
16
|
+
class ChannelError < YubiKey::ResponseError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Native AES-CMAC, EC encoding and offline X509 verification.
|
|
20
|
+
class Crypto
|
|
21
|
+
def ec_key(value, private_key: false)
|
|
22
|
+
key = value.is_a?(OpenSSL::PKey::EC) ? value : OpenSSL::PKey.read(value, '')
|
|
23
|
+
raise ArgumentError, 'An EC key of the required kind is needed' unless key.is_a?(OpenSSL::PKey::EC) && (!private_key || key.private?) && key.check_key
|
|
24
|
+
|
|
25
|
+
key
|
|
26
|
+
rescue OpenSSL::OpenSSLError, TypeError
|
|
27
|
+
raise ArgumentError, 'Invalid EC key encoding', cause: nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def public_point(point, curve)
|
|
31
|
+
group = OpenSSL::PKey::EC::Group.new(curve)
|
|
32
|
+
raise ChannelError, 'Invalid uncompressed EC point' unless point.is_a?(String) && point.getbyte(0) == 4 && point.bytesize == 1 + (2 * ((group.degree + 7) / 8))
|
|
33
|
+
|
|
34
|
+
algorithm = OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::ObjectId.new('id-ecPublicKey'), OpenSSL::ASN1::ObjectId.new(curve)])
|
|
35
|
+
ec_key(OpenSSL::ASN1::Sequence.new([algorithm, OpenSSL::ASN1::BitString.new(point)]).to_der)
|
|
36
|
+
rescue OpenSSL::OpenSSLError, ArgumentError
|
|
37
|
+
raise ChannelError, 'Invalid peer EC point', cause: nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def certificates(values)
|
|
41
|
+
raise ArgumentError, 'Certificates must be a nonempty bounded Array' unless values.is_a?(Array) && values.length.between?(1, 32)
|
|
42
|
+
|
|
43
|
+
values.map do |value|
|
|
44
|
+
cert = OpenSSL::X509::Certificate.new(value.is_a?(OpenSSL::X509::Certificate) ? value.to_der : value)
|
|
45
|
+
raise ArgumentError, 'Certificate is too large' if cert.to_der.bytesize > 65_000
|
|
46
|
+
|
|
47
|
+
cert
|
|
48
|
+
end
|
|
49
|
+
rescue OpenSSL::OpenSSLError, TypeError
|
|
50
|
+
raise ArgumentError, 'Invalid X509 certificate encoding', cause: nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_chain(chain, roots = nil)
|
|
54
|
+
now = Time.now
|
|
55
|
+
raise ChannelError, 'Certificate validity check failed' if chain.any? { |cert| now < cert.not_before || now > cert.not_after }
|
|
56
|
+
|
|
57
|
+
chain.each_cons(2) do |parent, child|
|
|
58
|
+
ca = parent.extensions.find { |ext| ext.oid == 'basicConstraints' }
|
|
59
|
+
usage = parent.extensions.find { |ext| ext.oid == 'keyUsage' }
|
|
60
|
+
valid = child.issuer == parent.subject && child.verify(parent.public_key) && ca&.value&.include?('CA:TRUE') && (!usage || usage.value.include?('Certificate Sign'))
|
|
61
|
+
raise ChannelError, 'Certificate chain verification failed' unless valid
|
|
62
|
+
end
|
|
63
|
+
if roots
|
|
64
|
+
store = OpenSSL::X509::Store.new
|
|
65
|
+
roots.each { |root| store.add_cert(root) }
|
|
66
|
+
raise ChannelError, 'Untrusted card certificate chain' unless store.verify(chain.last, chain[0...-1])
|
|
67
|
+
end
|
|
68
|
+
usage = chain.last.extensions.find { |ext| ext.oid == 'keyUsage' }
|
|
69
|
+
raise ChannelError, 'SCP11 leaf certificate requires keyAgreement usage' unless usage&.value&.include?('Key Agreement')
|
|
70
|
+
|
|
71
|
+
ec_key(chain.last.public_key)
|
|
72
|
+
rescue OpenSSL::OpenSSLError
|
|
73
|
+
raise ChannelError, 'Certificate verification failed', cause: nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def derive(key, type, context, bits = 128)
|
|
77
|
+
cmac(key, ("\0".b * 11) + [type, 0, bits, 1].pack('CCnC') + context).byteslice(0, bits / 8)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def equal?(left, right)
|
|
81
|
+
left.bytesize == right.bytesize && OpenSSL.fixed_length_secure_compare(left, right)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def cipher_iv(key, counter, response: false)
|
|
85
|
+
aes(key, [response ? 0x80 : 0].pack('C') + [counter.to_s(16).rjust(30, '0')].pack('H*'))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def pad(data)
|
|
89
|
+
(data + "\x80".b).ljust(((data.bytesize / 16) + 1) * 16, "\0".b)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def unpad(data)
|
|
93
|
+
value = data.sub(/\x00*\z/n, ''.b)
|
|
94
|
+
raise ChannelError, 'Invalid secure response padding' unless value.end_with?("\x80".b) && data.bytesize - value.bytesize < 16
|
|
95
|
+
|
|
96
|
+
value.byteslice(0, value.bytesize - 1)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def aes(key, data, initialization_vector: nil, decrypt: false)
|
|
100
|
+
cipher = OpenSSL::Cipher.new("aes-#{key.bytesize * 8}-#{initialization_vector ? 'cbc' : 'ecb'}")
|
|
101
|
+
decrypt ? cipher.decrypt : cipher.encrypt
|
|
102
|
+
cipher.padding = 0
|
|
103
|
+
cipher.key = key
|
|
104
|
+
cipher.iv = initialization_vector if initialization_vector
|
|
105
|
+
(data.empty? ? ''.b : cipher.update(data)) + cipher.final
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def xor(left, right)
|
|
109
|
+
left.bytes.zip(right.bytes).map { |a, b| a ^ b }.pack('C*')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def double(block)
|
|
113
|
+
value = block.unpack1('H*').to_i(16)
|
|
114
|
+
value = ((value << 1) & ((1 << 128) - 1)) ^ (value[127] == 1 ? 0x87 : 0)
|
|
115
|
+
[value.to_s(16).rjust(32, '0')].pack('H*')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def cmac(key, data)
|
|
119
|
+
first = double(aes(key, "\0".b * 16))
|
|
120
|
+
complete = !data.empty? && (data.bytesize % 16).zero?
|
|
121
|
+
count = [1, (data.bytesize + 15) / 16].max
|
|
122
|
+
last = data.byteslice((count - 1) * 16, 16) || ''.b
|
|
123
|
+
last = complete ? xor(last, first) : xor((last + "\x80".b).ljust(16, "\0".b), double(first))
|
|
124
|
+
chain = "\0".b * 16
|
|
125
|
+
(count - 1).times { |index| chain = aes(key, xor(chain, data.byteslice(index * 16, 16))) }
|
|
126
|
+
aes(key, xor(chain, last))
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The connection owns the MAC chain, encryption counter and lifecycle.
|
|
131
|
+
class Connection # rubocop:disable Metrics/ClassLength
|
|
132
|
+
attr_reader :scp
|
|
133
|
+
|
|
134
|
+
def initialize(opts)
|
|
135
|
+
raise YubiKey::ConfirmationRequired, 'Secure-channel authentication requires confirm: true' unless opts[:confirm] == true
|
|
136
|
+
|
|
137
|
+
@connection = opts.fetch(:connection)
|
|
138
|
+
raise ArgumentError, 'Expected a plain APDU connection' unless @connection.descriptor[:protocol] == :apdu && !@connection.is_a?(Connection)
|
|
139
|
+
|
|
140
|
+
@scp = opts.fetch(:scp)
|
|
141
|
+
raise ArgumentError, 'Unsupported secure-channel protocol' unless %i[scp03 scp11a scp11b scp11c].include?(@scp)
|
|
142
|
+
|
|
143
|
+
expected_kid = { scp03: 1, scp11a: 0x11, scp11b: 0x13, scp11c: 0x15 }.fetch(@scp)
|
|
144
|
+
raise ArgumentError, 'KID does not match selected SCP variant' if opts.key?(:kid) && byte(opts[:kid]) != expected_kid
|
|
145
|
+
|
|
146
|
+
@kvn = byte(opts.fetch(:kvn))
|
|
147
|
+
@timeout = timeout(opts.fetch(:timeout, 30))
|
|
148
|
+
@crypto = Crypto.new
|
|
149
|
+
@mutex = Mutex.new
|
|
150
|
+
@enc_counter = 1
|
|
151
|
+
@chain = "\0".b * 16
|
|
152
|
+
if @scp == :scp03
|
|
153
|
+
keys = %i[key_enc key_mac].map { |name| aes_key(opts.fetch(name)) }
|
|
154
|
+
@dek = aes_key(opts[:key_dek]) if opts[:key_dek]
|
|
155
|
+
else
|
|
156
|
+
prepare_scp11(opts)
|
|
157
|
+
end
|
|
158
|
+
@auth_deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
|
|
159
|
+
begin
|
|
160
|
+
plain([0, 0xa4, 4, 0], ['a000000151000000'].pack('H*'))
|
|
161
|
+
@scp == :scp03 ? initialize_scp03(keys) : initialize_scp11
|
|
162
|
+
@ready = true
|
|
163
|
+
ensure
|
|
164
|
+
@oce_key = nil
|
|
165
|
+
invalidate unless @ready
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def inspect
|
|
170
|
+
"#<#{self.class} #{@scp} redacted>"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def descriptor
|
|
174
|
+
@connection.descriptor.merge(secure_channel: @scp, extended_apdu: true, authenticated: @ready == true && !@closed && %i[scp03 scp11a].include?(@scp))
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def authenticated?
|
|
178
|
+
@ready == true && !@closed && %i[scp03 scp11a].include?(@scp)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def close
|
|
182
|
+
@mutex.synchronize { invalidate }
|
|
183
|
+
:closed
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def exchange(opts = {})
|
|
187
|
+
raise ArgumentError, 'Secure channel accepts only APDUs' unless opts[:protocol] == :apdu
|
|
188
|
+
|
|
189
|
+
header, data, le, extended = decode(opts[:data])
|
|
190
|
+
seconds = timeout(opts.fetch(:timeout, @timeout))
|
|
191
|
+
@mutex.synchronize do
|
|
192
|
+
raise ChannelError, 'Secure channel is closed' if @closed || !@ready
|
|
193
|
+
|
|
194
|
+
complete = false
|
|
195
|
+
begin
|
|
196
|
+
raise ChannelError, 'Secure-channel counter exhausted' if @enc_counter > 0xffffffff
|
|
197
|
+
|
|
198
|
+
counter = @enc_counter
|
|
199
|
+
encrypted = @crypto.aes(@enc, @crypto.pad(data), initialization_vector: @crypto.cipher_iv(@enc, counter))
|
|
200
|
+
@enc_counter += 1
|
|
201
|
+
header[0] |= 4
|
|
202
|
+
frame = encode(header, encrypted + ("\0".b * 8), nil, extended: extended)
|
|
203
|
+
@chain = @crypto.cmac(@mac, @chain + frame.byteslice(0, frame.bytesize - 8))
|
|
204
|
+
command = encode(header, encrypted + @chain.byteslice(0, 8), le, extended: extended)
|
|
205
|
+
response = transmit(command, Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds)
|
|
206
|
+
value = verify_response(response, counter)
|
|
207
|
+
complete = true
|
|
208
|
+
value
|
|
209
|
+
ensure
|
|
210
|
+
invalidate unless complete
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Key wrapping never exports the session DEK. The caller must already
|
|
216
|
+
# hold SD administrative authentication and explicit mutation consent.
|
|
217
|
+
def wrap_key(key)
|
|
218
|
+
@mutex.synchronize do
|
|
219
|
+
raise ChannelError, 'Authenticated channel with a DEK required' unless authenticated? && @dek
|
|
220
|
+
raise ArgumentError, 'Key material must be whole AES blocks' unless key.is_a?(String) && !key.empty? && (key.bytesize % 16).zero?
|
|
221
|
+
|
|
222
|
+
@crypto.aes(@dek, key.b, initialization_vector: "\0".b * 16)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private
|
|
227
|
+
|
|
228
|
+
def byte(value)
|
|
229
|
+
raise ArgumentError, 'KID/KVN must be integers in 0..255' unless value.is_a?(Integer) && value.between?(0, 255)
|
|
230
|
+
|
|
231
|
+
value
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def timeout(value)
|
|
235
|
+
raise ArgumentError, 'Timeout must be finite and positive' unless value.is_a?(Numeric) && value.finite? && value.positive?
|
|
236
|
+
|
|
237
|
+
value
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def aes_key(value)
|
|
241
|
+
raise ArgumentError, 'SCP03 requires explicit 16-byte AES keys' unless value.is_a?(String) && value.bytesize == 16
|
|
242
|
+
|
|
243
|
+
value.b.dup
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def encode(header, data, expected_length = nil, extended: false)
|
|
247
|
+
raise ArgumentError, 'APDU too large for secure messaging' if data.bytesize > 65_535
|
|
248
|
+
|
|
249
|
+
extended ||= data.bytesize > 255 || (expected_length && expected_length > 256)
|
|
250
|
+
result = header.pack('C*')
|
|
251
|
+
if extended
|
|
252
|
+
result += [0, data.bytesize].pack('Cn') unless data.empty?
|
|
253
|
+
result += data
|
|
254
|
+
result += data.empty? ? [0, expected_length & 0xffff].pack('Cn') : [expected_length & 0xffff].pack('n') if expected_length
|
|
255
|
+
else
|
|
256
|
+
result += [data.bytesize].pack('C') + data unless data.empty?
|
|
257
|
+
result += [expected_length & 0xff].pack('C') if expected_length
|
|
258
|
+
end
|
|
259
|
+
result
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def decode(command)
|
|
263
|
+
raise ArgumentError, 'Malformed APDU' unless command.is_a?(String) && command.bytesize.between?(4, 65_544)
|
|
264
|
+
|
|
265
|
+
bytes = command.b
|
|
266
|
+
header = bytes.byteslice(0, 4).bytes
|
|
267
|
+
raise ArgumentError, 'Pre-wrapped secure APDUs are not accepted' unless header[0].nobits?(4)
|
|
268
|
+
return [header, ''.b, nil, false] if bytes.bytesize == 4
|
|
269
|
+
return [header, ''.b, bytes.getbyte(4).zero? ? 256 : bytes.getbyte(4), false] if bytes.bytesize == 5
|
|
270
|
+
|
|
271
|
+
extended = bytes.getbyte(4).zero?
|
|
272
|
+
offset = extended ? 7 : 5
|
|
273
|
+
raise ArgumentError, 'Truncated extended APDU' if bytes.bytesize < offset
|
|
274
|
+
|
|
275
|
+
length = extended ? bytes.byteslice(5, 2).unpack1('n') : bytes.getbyte(4)
|
|
276
|
+
return [header, ''.b, length.zero? ? 65_536 : length, true] if extended && bytes.bytesize == 7
|
|
277
|
+
|
|
278
|
+
remaining = bytes.bytesize - offset - length
|
|
279
|
+
raise ArgumentError, 'Malformed APDU length' unless length.positive? && [0, extended ? 2 : 1].include?(remaining)
|
|
280
|
+
|
|
281
|
+
le = if remaining.zero?
|
|
282
|
+
nil
|
|
283
|
+
else
|
|
284
|
+
bytes.byteslice(-remaining, remaining).unpack1(extended ? 'n' : 'C')
|
|
285
|
+
end
|
|
286
|
+
le = extended ? 65_536 : 256 if le&.zero?
|
|
287
|
+
[header, bytes.byteslice(offset, length), le, extended]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# GET RESPONSE retrieves fragments of the existing protected reply;
|
|
291
|
+
# it does not start another secure command or consume its counter.
|
|
292
|
+
def transmit(command, deadline)
|
|
293
|
+
result = ''.b
|
|
294
|
+
64.times do
|
|
295
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
296
|
+
raise YubiKey::TimeoutError, 'Secure-channel exchange timed out' unless remaining.positive?
|
|
297
|
+
|
|
298
|
+
response = @connection.exchange(protocol: :apdu, data: command, timeout: remaining)
|
|
299
|
+
raise ChannelError, 'Malformed or oversized APDU response' unless response.is_a?(String) && response.bytesize.between?(2, 1_048_586)
|
|
300
|
+
|
|
301
|
+
result += response.byteslice(0, response.bytesize - 2)
|
|
302
|
+
raise ChannelError, 'Secure response size limit exceeded' if result.bytesize > 1_048_584
|
|
303
|
+
return result + response.byteslice(-2, 2) unless response.getbyte(-2) == 0x61
|
|
304
|
+
|
|
305
|
+
command = "\0\xc0\0\0".b
|
|
306
|
+
end
|
|
307
|
+
raise ChannelError, 'Secure response fragmentation limit exceeded'
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def plain(header, data)
|
|
311
|
+
response = transmit(encode(header, data), @auth_deadline)
|
|
312
|
+
raise ChannelError, 'Truncated authentication response' unless response.is_a?(String) && response.bytesize >= 2
|
|
313
|
+
raise ChannelError, 'Secure-channel authentication rejected' unless response.byteslice(-2, 2) == "\x90\0".b
|
|
314
|
+
|
|
315
|
+
response.byteslice(0, response.bytesize - 2)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def initialize_scp03(keys)
|
|
319
|
+
challenge = SecureRandom.random_bytes(8)
|
|
320
|
+
response = plain([0x80, 0x50, @kvn, 0], challenge)
|
|
321
|
+
raise ChannelError, 'Invalid SCP03 INITIALIZE UPDATE' unless [29, 32].include?(response.bytesize) && response.getbyte(11) == 3 && response.getbyte(12).nobits?(1)
|
|
322
|
+
raise ChannelError, 'Unexpected SCP03 key version' unless @kvn.zero? || @kvn == response.getbyte(10)
|
|
323
|
+
|
|
324
|
+
context = challenge + response.byteslice(13, 8)
|
|
325
|
+
@enc = @crypto.derive(keys[0], 4, context)
|
|
326
|
+
@mac = @crypto.derive(keys[1], 6, context)
|
|
327
|
+
@rmac = @crypto.derive(keys[1], 7, context)
|
|
328
|
+
expected = @crypto.derive(@mac, 0, context, 64)
|
|
329
|
+
raise ChannelError, 'SCP03 card cryptogram verification failed' unless @crypto.equal?(expected, response.byteslice(21, 8))
|
|
330
|
+
|
|
331
|
+
host = @crypto.derive(@mac, 1, context, 64)
|
|
332
|
+
frame = encode([0x84, 0x82, 0x33, 0], host + ("\0".b * 8))
|
|
333
|
+
@chain = @crypto.cmac(@mac, @chain + frame.byteslice(0, frame.bytesize - 8))
|
|
334
|
+
reply = plain([0x84, 0x82, 0x33, 0], host + @chain.byteslice(0, 8))
|
|
335
|
+
# EXTERNAL AUTHENTICATE may acknowledge with bare 9000. All
|
|
336
|
+
# subsequent responses MUST authenticate their status and payload.
|
|
337
|
+
verify_response(reply + "\x90\0".b, 0) unless reply.empty?
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def prepare_scp11(opts)
|
|
341
|
+
@kid = { scp11a: 0x11, scp11b: 0x13, scp11c: 0x15 }.fetch(@scp)
|
|
342
|
+
@card_key = @crypto.ec_key(opts[:card_public_key]) if opts[:card_public_key]
|
|
343
|
+
@roots = @crypto.certificates(opts[:ca_certificates]) if opts[:ca_certificates]
|
|
344
|
+
raise ArgumentError, 'SCP11 needs a pinned card_public_key or explicit ca_certificates' unless @card_key || @roots
|
|
345
|
+
|
|
346
|
+
@card_certs = @crypto.certificates(opts[:card_certificates]) if opts[:card_certificates]
|
|
347
|
+
if @scp == :scp11b
|
|
348
|
+
raise ArgumentError, 'SCP11b does not use OCE credentials' if opts[:oce_private_key] || opts[:oce_certificates]
|
|
349
|
+
else
|
|
350
|
+
@oce_key = @crypto.ec_key(opts.fetch(:oce_private_key), private_key: true)
|
|
351
|
+
@oce_certs = @crypto.certificates(opts.fetch(:oce_certificates))
|
|
352
|
+
public_key = @crypto.validate_chain(@oce_certs)
|
|
353
|
+
raise ArgumentError, 'OCE private key does not match leaf certificate' unless @crypto.equal?(@oce_key.public_to_der, public_key.public_to_der)
|
|
354
|
+
|
|
355
|
+
@oce_kid = byte(opts.fetch(:oce_kid, 0))
|
|
356
|
+
@oce_kvn = byte(opts.fetch(:oce_kvn, 0))
|
|
357
|
+
raise ArgumentError, 'Invalid OCE CA KID' unless @oce_kid.zero? || @oce_kid == 0x10 || @oce_kid.between?(0x20, 0x2f)
|
|
358
|
+
end
|
|
359
|
+
validate_card_certificates if @card_certs
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def validate_card_certificates
|
|
363
|
+
key = @crypto.validate_chain(@card_certs, @roots)
|
|
364
|
+
raise ChannelError, 'Card public-key pin mismatch' if @card_key && !@crypto.equal?(@card_key.public_to_der, key.public_to_der)
|
|
365
|
+
|
|
366
|
+
@card_key = key
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def initialize_scp11
|
|
370
|
+
if @roots && !@card_certs
|
|
371
|
+
reference = Protocol.tlv(tag: 0xa6, value: Protocol.tlv(tag: 0x83, value: [@kid, @kvn].pack('CC')))
|
|
372
|
+
raw = plain([0, 0xca, 0xbf, 0x21], reference)
|
|
373
|
+
@card_certs = @crypto.certificates(Protocol.untlv(data: raw).map do |row|
|
|
374
|
+
raise ChannelError, 'Invalid certificate bundle tag' unless row[:tag] == 0x30
|
|
375
|
+
|
|
376
|
+
Protocol.tlv(tag: row[:tag], value: row[:value])
|
|
377
|
+
end)
|
|
378
|
+
validate_card_certificates
|
|
379
|
+
end
|
|
380
|
+
raise ArgumentError, 'OCE and card EC curves must match' if @oce_key && @oce_key.group.curve_name != @card_key.group.curve_name
|
|
381
|
+
|
|
382
|
+
@oce_certs&.each_with_index do |cert, index|
|
|
383
|
+
more = index < @oce_certs.length - 1 ? 0x80 : 0
|
|
384
|
+
plain([0x80, 0x2a, @oce_kvn, @oce_kid | more], cert.to_der)
|
|
385
|
+
end
|
|
386
|
+
ephemeral = OpenSSL::PKey::EC.generate(@card_key.group.curve_name)
|
|
387
|
+
parameters = { scp11a: 1, scp11b: 0, scp11c: 3 }.fetch(@scp)
|
|
388
|
+
template = Protocol.tlv(tag: 0x90, value: [0x11, parameters].pack('CC')) + Protocol.tlv(tag: 0x95, value: "\x3c".b) + Protocol.tlv(tag: 0x80, value: "\x88".b) + Protocol.tlv(tag: 0x81, value: "\x10".b)
|
|
389
|
+
data = Protocol.tlv(tag: 0xa6, value: template) + Protocol.tlv(tag: 0x5f49, value: ephemeral.public_key.to_octet_string(:uncompressed))
|
|
390
|
+
ins = @scp == :scp11b ? 0x88 : 0x82
|
|
391
|
+
raw_response = plain([0x80, ins, @kvn, @kid], data)
|
|
392
|
+
response = Protocol.untlv(data: raw_response)
|
|
393
|
+
raise ChannelError, 'Invalid SCP11 key agreement response' unless response.map { |row| row[:tag] } == [0x5f49, 0x86] && response[1][:value].bytesize == 16
|
|
394
|
+
|
|
395
|
+
canonical = response.map { |row| Protocol.tlv(tag: row[:tag], value: row[:value]) }.join.b
|
|
396
|
+
raise ChannelError, 'Noncanonical SCP11 receipt transcript' unless @crypto.equal?(raw_response, canonical)
|
|
397
|
+
|
|
398
|
+
peer = @crypto.public_point(response[0][:value], @card_key.group.curve_name)
|
|
399
|
+
secret = ephemeral.derive(peer) + (@oce_key || ephemeral).derive(@card_key)
|
|
400
|
+
# ANSI X9.63 KDF: SHA256(Z || counter32be || sharedInfo).
|
|
401
|
+
material = (1..3).map { |counter| OpenSSL::Digest::SHA256.digest(secret + [counter].pack('N') + "\x3c\x88\x10".b) }.join.byteslice(0, 80)
|
|
402
|
+
receipt_key, @enc, @mac, @rmac, @dek = 5.times.map { |index| material.byteslice(index * 16, 16) }
|
|
403
|
+
expected = @crypto.cmac(receipt_key, data + Protocol.tlv(tag: 0x5f49, value: response[0][:value]))
|
|
404
|
+
raise ChannelError, 'SCP11 receipt verification failed' unless @crypto.equal?(expected, response[1][:value])
|
|
405
|
+
|
|
406
|
+
@chain = expected
|
|
407
|
+
rescue OpenSSL::OpenSSLError
|
|
408
|
+
raise ChannelError, 'SCP11 key agreement failed', cause: nil
|
|
409
|
+
ensure
|
|
410
|
+
secret&.replace("\0".b * secret.bytesize)
|
|
411
|
+
material&.replace("\0".b * material.bytesize)
|
|
412
|
+
receipt_key&.replace("\0".b * receipt_key.bytesize)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def verify_response(response, counter)
|
|
416
|
+
raise ChannelError, 'Missing or oversized secure response MAC' unless response.is_a?(String) && response.bytesize.between?(10, 1_048_586)
|
|
417
|
+
|
|
418
|
+
ciphertext = response.byteslice(0, response.bytesize - 10)
|
|
419
|
+
received = response.byteslice(-10, 8)
|
|
420
|
+
status = response.byteslice(-2, 2)
|
|
421
|
+
expected = @crypto.cmac(@rmac, @chain + ciphertext + status).byteslice(0, 8)
|
|
422
|
+
raise ChannelError, 'Secure response MAC verification failed' unless @crypto.equal?(expected, received)
|
|
423
|
+
raise ChannelError, 'Truncated secure ciphertext' unless (ciphertext.bytesize % 16).zero?
|
|
424
|
+
|
|
425
|
+
data = ciphertext.empty? ? ''.b : @crypto.unpad(@crypto.aes(@enc, ciphertext, initialization_vector: @crypto.cipher_iv(@enc, counter, response: true), decrypt: true))
|
|
426
|
+
data + status
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def invalidate
|
|
430
|
+
return if @closed
|
|
431
|
+
|
|
432
|
+
@closed = true
|
|
433
|
+
@ready = false
|
|
434
|
+
# Best effort only: Ruby/OpenSSL/GC may retain copies. No locked
|
|
435
|
+
# memory or guaranteed zeroization is claimed.
|
|
436
|
+
[@enc, @mac, @rmac, @dek, @chain].compact.each { |value| value.replace("\0".b * value.bytesize) unless value.frozen? }
|
|
437
|
+
@connection.close
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Authenticate a native APDU connection with explicit in-memory keys.
|
|
442
|
+
public_class_method def self.open(opts = {})
|
|
443
|
+
raise ArgumentError, 'connection is required' unless opts[:connection]
|
|
444
|
+
|
|
445
|
+
Connection.new(opts)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Return module attribution without device access.
|
|
449
|
+
public_class_method def self.authors
|
|
450
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Describe the private implementation module without accessing a device.
|
|
454
|
+
public_class_method def self.help
|
|
455
|
+
puts <<~HELP
|
|
456
|
+
Authenticate a native APDU connection; prefer the parent secure_connection API.
|
|
457
|
+
#{self}.open(
|
|
458
|
+
connection: 'required - plain native APDU transport owned by the resulting wrapper',
|
|
459
|
+
scp: 'required - Symbol scp03, scp11a, scp11b or scp11c',
|
|
460
|
+
kvn: 'required - exact integer key version 0..255',
|
|
461
|
+
confirm: 'required - literal true permits authentication attempts',
|
|
462
|
+
key_enc: 'optional - required 16-byte binary K-ENC for SCP03',
|
|
463
|
+
key_mac: 'optional - required 16-byte binary K-MAC for SCP03',
|
|
464
|
+
key_dek: 'optional - 16-byte binary K-DEK for SCP03 key import',
|
|
465
|
+
card_public_key: 'optional - SCP11 pinned EC public key object/PEM/DER or supply CA anchors',
|
|
466
|
+
ca_certificates: 'optional - trusted X509 object/PEM/DER Array for SCP11',
|
|
467
|
+
card_certificates: 'optional - leaf-last X509 card bundle; otherwise fetched for CA validation',
|
|
468
|
+
oce_private_key: 'optional - required EC private key object/PEM/DER for SCP11a/c',
|
|
469
|
+
oce_certificates: 'optional - required leaf-last X509 OCE chain for SCP11a/c',
|
|
470
|
+
oce_kid: 'optional - OCE CA KID; 0, 16 or 32..47',
|
|
471
|
+
oce_kvn: 'optional - OCE CA version 0..255; default zero',
|
|
472
|
+
timeout: 'optional - finite positive handshake timeout in seconds; default 30'
|
|
473
|
+
)
|
|
474
|
+
Return module attribution without device access.
|
|
475
|
+
#{self}.authors
|
|
476
|
+
HELP
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|