pwn 0.5.784 → 0.5.787
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +6 -6
- data/README.md +37 -0
- data/bin/pwn +2 -1
- data/bin/pwn_web_cache_deception +35 -7
- data/build_gem.sh +1 -1
- data/documentation/AI-Integration.md +7 -5
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Backup-Retention.md +69 -15
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +2 -2
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Harness-Evaluation.md +1 -1
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +6 -5
- data/documentation/Installation.md +12 -9
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +18 -0
- data/documentation/Persistence.md +7 -5
- data/documentation/Plugin-Degradation.md +14 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Policy-Benchmark.md +32 -33
- data/documentation/Runtime-Source-Protection.md +162 -0
- data/documentation/Transparent-Browser.md +11 -0
- data/documentation/Troubleshooting.md +3 -2
- data/documentation/What-is-PWN.md +2 -2
- data/documentation/YubiKey-Vaults.md +185 -0
- data/documentation/YubiKey.md +218 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -130
- data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
- data/documentation/diagrams/dot/reinforcement-learning.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +1 -1
- data/documentation/diagrams/reinforcement-learning.svg +89 -89
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +43 -27
- data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
- data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
- data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
- data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
- data/etc/default_skills/pwn/backups/SKILL.md +54 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
- data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
- data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
- data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
- data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
- data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
- data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
- data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
- data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
- data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
- data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
- data/lib/pwn/ai/agent/dispatch.rb +26 -15
- data/lib/pwn/ai/agent/learning.rb +14 -18
- data/lib/pwn/ai/agent/loop.rb +17 -1
- data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
- data/lib/pwn/ai/agent/policy_evaluation.rb +334 -36
- data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
- data/lib/pwn/ai/agent/request_runtime.rb +122 -18
- data/lib/pwn/ai/agent/reward.rb +5 -3
- data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
- data/lib/pwn/ai/agent/skill_review.rb +9 -5
- data/lib/pwn/ai/agent/swarm.rb +4 -0
- data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
- data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
- data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
- data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/ai/anthropic.rb +8 -33
- data/lib/pwn/ai/cli.rb +34 -5
- data/lib/pwn/ai/gemini.rb +1 -0
- data/lib/pwn/ai/grok.rb +39 -28
- data/lib/pwn/ai/ollama.rb +4 -2
- data/lib/pwn/ai/open_ai.rb +11 -32
- data/lib/pwn/ai/open_web_ui.rb +62 -4
- data/lib/pwn/ai/router.rb +8 -1
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +349 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/migrate.rb +85 -280
- data/lib/pwn/module_skills.rb +7 -0
- data/lib/pwn/plugins/ai_sandbox.rb +538 -1
- data/lib/pwn/plugins/authentication_helper.rb +74 -4
- data/lib/pwn/plugins/basic_auth.rb +2 -1
- data/lib/pwn/plugins/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +3 -1
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +293 -26
- data/lib/pwn/plugins/repl/ai_console.rb +268 -88
- data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
- data/lib/pwn/plugins/repl/mesh.rb +14 -19
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +9 -216
- data/lib/pwn/plugins/sandbox/driver.rb +4 -0
- data/lib/pwn/plugins/transparent_browser.rb +215 -0
- data/lib/pwn/plugins/vault.rb +412 -135
- data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
- data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
- data/lib/pwn/plugins/yubikey/management.rb +686 -0
- data/lib/pwn/plugins/yubikey/oath.rb +766 -0
- data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
- data/lib/pwn/plugins/yubikey/otp.rb +897 -0
- data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
- data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
- data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
- data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
- data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
- data/lib/pwn/plugins/yubikey.rb +844 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/redaction.rb +69 -12
- data/lib/pwn/session_trace.rb +4 -0
- data/lib/pwn/setup.rb +114 -24
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn/www/app_cobalt_io.rb +16 -5
- data/lib/pwn/www/bug_crowd.rb +16 -5
- data/lib/pwn/www/coinbase_pro.rb +16 -5
- data/lib/pwn/www/github.rb +30 -14
- data/lib/pwn/www/paypal.rb +15 -4
- data/lib/pwn/www/synack.rb +16 -5
- data/lib/pwn/www/twitter.rb +17 -6
- data/lib/pwn.rb +1 -0
- data/spec/documentation/installation_md_spec.rb +1 -1
- data/spec/fixtures/yubikey/apdu_frames.json +162 -0
- data/spec/integration/authentication_helper_oath_spec.rb +117 -0
- data/spec/integration/controller_spill_spec.rb +48 -0
- data/spec/integration/harness_process_spec.rb +97 -0
- data/spec/integration/mission_cancel_spec.rb +79 -0
- data/spec/integration/mission_curses_pty_spec.rb +44 -0
- data/spec/integration/readme_security_spec.rb +47 -0
- data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
- data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
- data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
- data/spec/lib/pwn/ai/agent/policy_evaluation_spec.rb +10 -23
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
- data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
- data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
- data/spec/lib/pwn/ai/cli_spec.rb +8 -16
- data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
- data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
- data/spec/lib/pwn/ai/router_spec.rb +1 -1
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +129 -0
- data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
- data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
- data/spec/lib/pwn/migrate_spec.rb +29 -81
- data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/module_skills_spec.rb +8 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
- data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
- data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
- data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
- data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
- data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
- data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +110 -75
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -16
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
- data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
- data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
- data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
- data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
- data/spec/lib/pwn/plugins/repl_mesh_layout_spec.rb +2 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
- data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
- data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
- data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
- data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
- data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
- data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
- data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
- data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
- data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
- data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
- data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
- data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
- data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
- data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
- data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
- data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
- data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
- data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/harness/cli_faults.rb +13 -9
- data/spec/support/harness/controller_spill.rb +26 -0
- data/spec/support/harness/controller_state.rb +54 -0
- data/spec/support/harness/evaluate.rb +12 -1
- data/spec/support/harness/experiments.rb +1 -1
- data/spec/support/harness/fake_provider.rb +22 -6
- data/spec/support/harness/fault_preload.rb +3 -1
- data/spec/support/harness/mission_console.rb +54 -0
- data/spec/support/harness/mission_curses_pty.rb +41 -0
- data/spec/support/harness/mission_dispatch.rb +30 -0
- data/spec/support/harness/mission_fixture_preload.rb +24 -0
- data/spec/support/harness/mission_job_lifetime.rb +25 -0
- data/spec/support/harness/mission_legacy.rb +25 -0
- data/spec/support/harness/mission_normal_exit.rb +34 -0
- data/spec/support/harness/mission_swarm.rb +23 -0
- data/spec/support/harness/mission_unsupported.rb +29 -0
- data/spec/support/harness/package_acceptance.rb +33 -5
- data/spec/support/harness/session_acceptance.rb +4 -4
- data/spec/support/harness/swarm_acceptance.rb +1 -1
- data/spec/support/harness/swarm_control.rb +44 -0
- data/spec/support/mission_namespace.rb +14 -0
- data/spec/support/mission_pipe_queue.rb +33 -0
- data/third_party/pwn_rdoc.jsonl +605 -19
- metadata +129 -16
- data/scripts/benchmark_policy.rb +0 -376
- data/scripts/build_gate.rb +0 -58
- data/spec/integration/build_gate_spec.rb +0 -73
|
@@ -0,0 +1,1315 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
require 'zlib'
|
|
7
|
+
|
|
8
|
+
module PWN
|
|
9
|
+
module Plugins
|
|
10
|
+
module YubiKey
|
|
11
|
+
# Native PIV application. Wire reference: NIST SP 800-73 and Yubico
|
|
12
|
+
# yubikey-manager 5.9.1, yubikit/piv.py and ykman/{piv,_cli/piv}.py.
|
|
13
|
+
# Those sources are specifications only; no executable backend is used.
|
|
14
|
+
module PIV
|
|
15
|
+
AID = ['a000000308000010000100'].pack('H*').freeze
|
|
16
|
+
|
|
17
|
+
MANAGEMENT = { 'TDES' => [3, 24, 'DES-EDE3-ECB'], 'AES128' => [8, 16, 'AES-128-ECB'], 'AES192' => [10, 24, 'AES-192-ECB'], 'AES256' => [12, 32, 'AES-256-ECB'] }.freeze
|
|
18
|
+
GRAMMAR = {
|
|
19
|
+
'info' => [0, [], []], 'reset' => [0, %w[force], []],
|
|
20
|
+
'access change-pin' => [0, [], []], 'access change-puk' => [0, [], []],
|
|
21
|
+
'access unblock-pin' => [0, [], []], 'access set-retries' => [2, %w[force], []],
|
|
22
|
+
'access change-management-key' => [0, %w[touch protect generate force], %w[algorithm]],
|
|
23
|
+
'objects import' => [2, [], []], 'objects export' => [2, [], []], 'objects generate' => [1, [], []],
|
|
24
|
+
'keys generate' => [2, [], %w[algorithm format pin-policy touch-policy]],
|
|
25
|
+
'keys import' => [2, [], %w[pin-policy touch-policy]], 'keys info' => [1, [], []],
|
|
26
|
+
'keys export' => [2, %w[verify], %w[format]], 'keys attest' => [2, [], %w[format]],
|
|
27
|
+
'keys move' => [2, [], []], 'keys delete' => [1, [], []],
|
|
28
|
+
'certificates import' => [2, %w[verify compress update-chuid no-update-chuid], []],
|
|
29
|
+
'certificates export' => [2, [], %w[format]],
|
|
30
|
+
'certificates generate' => [1..2, %w[update-chuid no-update-chuid], %w[subject valid-days hash-algorithm]],
|
|
31
|
+
'certificates request' => [3, [], %w[subject hash-algorithm]],
|
|
32
|
+
'certificates delete' => [1, %w[update-chuid no-update-chuid], []]
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
ALGORITHMS = { 'RSA1024' => [6, 128], 'RSA2048' => [7, 256], 'RSA3072' => [5, 384], 'RSA4096' => [0x16, 512],
|
|
36
|
+
'ECCP256' => [0x11, 32], 'ECCP384' => [0x14, 48], 'ED25519' => [0xe0, 32], 'X25519' => [0xe1, 32] }.freeze
|
|
37
|
+
PIN_POLICIES = { 'DEFAULT' => 0, 'NEVER' => 1, 'ONCE' => 2, 'ALWAYS' => 3, 'MATCH_ONCE' => 4, 'MATCH_ALWAYS' => 5 }.freeze
|
|
38
|
+
TOUCH_POLICIES = { 'DEFAULT' => 0, 'NEVER' => 1, 'ALWAYS' => 2, 'CACHED' => 3 }.freeze
|
|
39
|
+
SLOTS = { 'AUTHENTICATION' => 0x9a, 'SIGNATURE' => 0x9c, 'KEY_MANAGEMENT' => 0x9d, 'CARD_AUTH' => 0x9e, 'ATTESTATION' => 0xf9 }.merge((1..20).to_h { |n| ["RETIRED#{n}", 0x81 + n] }).freeze
|
|
40
|
+
OBJECTS = { 'CCC' => 0x5fc107, 'CAPABILITY' => 0x5fc107, 'CHUID' => 0x5fc102, 'DISCOVERY' => 0x7e,
|
|
41
|
+
'AUTHENTICATION' => 0x5fc105, 'SIGNATURE' => 0x5fc10a, 'KEY_MANAGEMENT' => 0x5fc10b, 'CARD_AUTH' => 0x5fc101,
|
|
42
|
+
'FINGERPRINTS' => 0x5fc103, 'SECURITY' => 0x5fc106, 'FACIAL' => 0x5fc108, 'PRINTED' => 0x5fc109,
|
|
43
|
+
'KEY_HISTORY' => 0x5fc10c, 'IRIS' => 0x5fc121, 'ATTESTATION' => 0x5fff01, 'PIVMAN' => 0x5fff00 }.merge((1..20).to_h { |n| ["RETIRED#{n}", 0x5fc10c + n] }).freeze
|
|
44
|
+
|
|
45
|
+
# Firmware does not support the requested operation.
|
|
46
|
+
class UnsupportedError < Error; end
|
|
47
|
+
|
|
48
|
+
# A failed reference verification; the secret is never included.
|
|
49
|
+
class PinError < Error
|
|
50
|
+
attr_reader :attempts_remaining
|
|
51
|
+
|
|
52
|
+
def initialize(attempts_remaining)
|
|
53
|
+
@attempts_remaining = attempts_remaining
|
|
54
|
+
super("PIV verification failed; #{attempts_remaining} attempts remaining")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Execute a native PIV leaf with secrets passed only in memory.
|
|
59
|
+
# @param opts [Hash] args and connection plus operation-specific options
|
|
60
|
+
# @return [Hash, String] structured outcome or explicit binary export
|
|
61
|
+
public_class_method def self.execute(opts = {})
|
|
62
|
+
Session.new(opts).execute(opts[:args] || [])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Authenticate both card and host with a fresh random challenge.
|
|
66
|
+
# @param opts [Hash] connection, management_key and literal confirmation
|
|
67
|
+
# @return [Symbol] :authenticated
|
|
68
|
+
public_class_method def self.authenticate(opts = {})
|
|
69
|
+
Session.new(opts).authenticate(opts[:management_key])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Verify the caller's PIN without guessing or retrying credentials.
|
|
73
|
+
# @param opts [Hash] native connection, pin, confirmation and timeout
|
|
74
|
+
# @return [Symbol] :verified
|
|
75
|
+
public_class_method def self.verify_pin(opts = {})
|
|
76
|
+
Session.new(opts).verify_pin(opts[:pin])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Sign a message with a non-exportable device private key.
|
|
80
|
+
# @param opts [Hash] slot, algorithm, message, PIN and padding options
|
|
81
|
+
# @return [String] raw signature
|
|
82
|
+
public_class_method def self.sign(opts = {})
|
|
83
|
+
Session.new(opts).sign(opts[:slot], opts[:message])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Decrypt an RSA ciphertext on the device and unpad using OpenSSL.
|
|
87
|
+
# @param opts [Hash] slot, ciphertext, PIN and OAEP or PKCS1 options
|
|
88
|
+
# @return [String] binary plaintext
|
|
89
|
+
public_class_method def self.decrypt(opts = {})
|
|
90
|
+
Session.new(opts).decrypt(opts[:slot], opts[:ciphertext])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Derive an ECDH or X25519 shared secret using the device key.
|
|
94
|
+
# @param opts [Hash] slot, peer_public_key and authentication options
|
|
95
|
+
# @return [String] binary shared secret
|
|
96
|
+
public_class_method def self.calculate_secret(opts = {})
|
|
97
|
+
Session.new(opts).calculate_secret(opts[:slot], opts[:peer_public_key])
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Verify a biometric and optionally obtain a session-local temporary PIN.
|
|
101
|
+
# @param opts [Hash] connection, confirmation and temporary PIN request
|
|
102
|
+
# @return [Symbol, String] verified status or temporary PIN bytes
|
|
103
|
+
public_class_method def self.verify_uv(opts = {})
|
|
104
|
+
Session.new(opts).verify_uv(request_pin: opts[:request_temporary_pin])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Verify a session-local temporary PIN previously issued by the card.
|
|
108
|
+
# @param opts [Hash] connection, confirmation and temporary_pin bytes
|
|
109
|
+
# @return [Symbol] :verified
|
|
110
|
+
public_class_method def self.verify_temporary_pin(opts = {})
|
|
111
|
+
Session.new(opts).verify_temporary_pin(opts[:temporary_pin])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# List native leaf names without selecting a device.
|
|
115
|
+
# @param opts [Hash] optional operation prefix
|
|
116
|
+
# @return [Array<String>] implemented operations
|
|
117
|
+
public_class_method def self.operations(opts = {})
|
|
118
|
+
prefix = opts[:prefix].to_s
|
|
119
|
+
GRAMMAR.keys.select { |name| name.start_with?(prefix) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Show native PIV usage and secret-handling boundaries.
|
|
123
|
+
# @return [nil]
|
|
124
|
+
public_class_method def self.help
|
|
125
|
+
puts <<~HELP
|
|
126
|
+
USAGE:
|
|
127
|
+
# Execute a native PIV leaf; group paths return local help.
|
|
128
|
+
#{self}.execute(
|
|
129
|
+
args: 'required - family-relative Array of nonsecret arguments',
|
|
130
|
+
connection: 'required - native APDU connection selected by the parent transport',
|
|
131
|
+
confirm: 'optional - literal true required for authentication, writes and private-key use',
|
|
132
|
+
pin: 'optional - current PIN in memory, never an argument token',
|
|
133
|
+
puk: 'optional - current PUK in memory for change or unblock operations',
|
|
134
|
+
new_pin: 'optional - replacement PIN in memory for change or unblock',
|
|
135
|
+
new_puk: 'optional - replacement PUK in memory for change-puk',
|
|
136
|
+
management_key: 'optional - raw binary management key supplied only in memory',
|
|
137
|
+
new_management_key: 'optional - raw binary replacement management key',
|
|
138
|
+
password: 'optional - in-memory PEM or PKCS12 import password',
|
|
139
|
+
secret_provider: 'optional - callable receiving a missing secret field Symbol',
|
|
140
|
+
secret_sink: 'optional - callable receiving field and newly generated unprotected management key',
|
|
141
|
+
input: 'optional - binary input for a dash filename; no implicit terminal reads',
|
|
142
|
+
use_protected_key: 'optional - literal true selects PIN-protected PIVMAN authentication',
|
|
143
|
+
block_retries: 'optional - literal true explicitly authorizes PIN and PUK blocking before reset',
|
|
144
|
+
public_key_source: 'optional - metadata, attestation or certificate; no error-triggered fallback',
|
|
145
|
+
fips_restrictions: 'optional - Boolean for newer FIPS device capability restrictions',
|
|
146
|
+
not_before: 'optional - Time for generated certificate start, default current UTC time',
|
|
147
|
+
not_after: 'optional - Time for generated certificate expiry, default valid-days',
|
|
148
|
+
certificate_serial: 'optional - positive integer certificate serial under 160 bits',
|
|
149
|
+
extensions: 'optional - Array of OpenSSL X509 Extension objects',
|
|
150
|
+
timeout: 'optional - finite positive APDU timeout seconds, default 30'
|
|
151
|
+
)
|
|
152
|
+
# Verify the supplied PIN exactly once without guessing credentials.
|
|
153
|
+
#{self}.verify_pin(connection: 'required - APDU connection', pin: 'required - PIN in memory', confirm: 'required - literal true')
|
|
154
|
+
# Mutually authenticate with the configured 3DES or AES management key.
|
|
155
|
+
#{self}.authenticate(connection: 'required - APDU connection', management_key: 'required - raw binary secret', confirm: 'required - literal true')
|
|
156
|
+
# Create a device-backed RSA, ECDSA or Ed25519 signature.
|
|
157
|
+
#{self}.sign(
|
|
158
|
+
connection: 'required - APDU connection', slot: 'required - PIV slot hex String or Integer',
|
|
159
|
+
message: 'required - binary message String', algorithm: 'optional - algorithm name, otherwise slot metadata',
|
|
160
|
+
hash_algorithm: 'optional - SHA1, SHA256, SHA384 or SHA512; default SHA256',
|
|
161
|
+
padding: 'optional - pkcs1 or pss for RSA; default pkcs1',
|
|
162
|
+
prehashed: 'optional - Boolean to use a digest instead of hashing the input',
|
|
163
|
+
salt_length: 'optional - PSS salt length in bytes; default digest size',
|
|
164
|
+
mgf1_hash: 'optional - PSS or OAEP MGF1 hash; default hash_algorithm',
|
|
165
|
+
pin: 'optional - current PIN; the card enforces its policy if omitted', confirm: 'required - literal true'
|
|
166
|
+
)
|
|
167
|
+
# Decrypt a device-backed RSA ciphertext with native OpenSSL padding.
|
|
168
|
+
#{self}.decrypt(
|
|
169
|
+
connection: 'required - APDU connection', slot: 'required - PIV key slot',
|
|
170
|
+
ciphertext: 'required - binary RSA ciphertext', padding: 'optional - oaep, pkcs1 or none; default oaep',
|
|
171
|
+
label: 'optional - binary OAEP label', pin: 'optional - current PIN', confirm: 'required - literal true'
|
|
172
|
+
)
|
|
173
|
+
# Derive a device-backed ECDH or X25519 shared secret.
|
|
174
|
+
#{self}.calculate_secret(
|
|
175
|
+
connection: 'required - APDU connection', slot: 'required - PIV key slot',
|
|
176
|
+
peer_public_key: 'required - PEM, DER or OpenSSL peer public key', pin: 'optional - current PIN', confirm: 'required - literal true'
|
|
177
|
+
)
|
|
178
|
+
# Verify an enrolled biometric and optionally request temporary PIN bytes.
|
|
179
|
+
#{self}.verify_uv(connection: 'required - APDU connection', confirm: 'required - literal true', request_temporary_pin: 'optional - Boolean to request a session-local PIN')
|
|
180
|
+
# Verify a card-issued temporary PIN during its session lifetime.
|
|
181
|
+
#{self}.verify_temporary_pin(connection: 'required - APDU connection', confirm: 'required - literal true', temporary_pin: 'required - sixteen raw bytes in memory')
|
|
182
|
+
# List the implemented PIV leaf catalog without device operations.
|
|
183
|
+
#{self}.operations(prefix: 'optional - exact operation prefix filter')
|
|
184
|
+
# Identify the module authors.
|
|
185
|
+
#{self}.authors
|
|
186
|
+
HELP
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Identify the module authors.
|
|
190
|
+
# @return [String] authors
|
|
191
|
+
public_class_method def self.authors
|
|
192
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# A selected connection owns authentication state for its lifetime.
|
|
196
|
+
class Session # rubocop:disable Metrics/ClassLength
|
|
197
|
+
def initialize(opts)
|
|
198
|
+
raise ArgumentError, 'PIV options must be a Hash' unless opts.is_a?(Hash)
|
|
199
|
+
|
|
200
|
+
@opts = opts
|
|
201
|
+
validate_options
|
|
202
|
+
@connection = opts[:connection]
|
|
203
|
+
@selected = false
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def execute(args)
|
|
207
|
+
raise ArgumentError, 'args must be an Array of Strings' unless args.is_a?(Array) && args.all?(String)
|
|
208
|
+
|
|
209
|
+
return { application: :piv, operations: GRAMMAR.keys } if args.empty? || %w[access keys certificates objects].include?(args.join(' ')) || args == ['--help']
|
|
210
|
+
|
|
211
|
+
operation = GRAMMAR.keys.sort_by(&:length).reverse.find { |name| args.first(name.split.length) == name.split }
|
|
212
|
+
raise ArgumentError, 'Unknown PIV operation' unless operation
|
|
213
|
+
|
|
214
|
+
count, flags, values = GRAMMAR.fetch(operation)
|
|
215
|
+
parsed = Protocol.parse_args(args: args.drop(operation.split.length), flags: flags.map { |flag| "--#{flag}" }, values: values.map { |value| "--#{value}" })
|
|
216
|
+
positions = parsed.fetch(:positionals)
|
|
217
|
+
raise ArgumentError, 'Incorrect number of PIV arguments' unless count.is_a?(Range) ? count.cover?(positions.length) : positions.length == count
|
|
218
|
+
|
|
219
|
+
@opts = @opts.merge(parsed.fetch(:options))
|
|
220
|
+
validate_options
|
|
221
|
+
output_index = { 'keys generate' => 1, 'keys export' => 1, 'keys attest' => 1, 'objects export' => 1,
|
|
222
|
+
'certificates export' => 1, 'certificates request' => 2 }[operation]
|
|
223
|
+
if output_index && positions[output_index] != '-'
|
|
224
|
+
confirm!
|
|
225
|
+
path = positions.fetch(output_index)
|
|
226
|
+
raise Errno::EEXIST, 'PIV output already exists' if File.exist?(path) || File.symlink?(path)
|
|
227
|
+
raise Errno::ENOENT, 'PIV output parent does not exist' unless File.directory?(File.dirname(path))
|
|
228
|
+
end
|
|
229
|
+
case operation
|
|
230
|
+
when 'info' then info
|
|
231
|
+
when 'reset' then reset
|
|
232
|
+
when 'access change-pin' then change_reference(0x24, 0x80, :pin, :new_pin)
|
|
233
|
+
when 'access change-puk' then change_reference(0x24, 0x81, :puk, :new_puk)
|
|
234
|
+
when 'access unblock-pin' then change_reference(0x2c, 0x80, :puk, :new_pin)
|
|
235
|
+
when 'access set-retries' then set_retries(*positions)
|
|
236
|
+
when 'access change-management-key' then change_management_key
|
|
237
|
+
when 'objects import' then import_object(*positions)
|
|
238
|
+
when 'objects export'
|
|
239
|
+
id = parse_object_id(positions[0])
|
|
240
|
+
verify_pin if @opts[:pin]
|
|
241
|
+
output(read_object(id), positions[1])
|
|
242
|
+
when 'objects generate' then generate_object(positions[0])
|
|
243
|
+
when 'keys generate' then output(encode(generate_key(positions[0])), positions[1])
|
|
244
|
+
when 'keys import' then import_key(positions[0], input(positions[1]))
|
|
245
|
+
when 'keys info' then metadata(slot(positions[0]))
|
|
246
|
+
when 'keys export' then output(encode(export_key(positions[0])), positions[1])
|
|
247
|
+
when 'keys attest' then output(encode(attest(positions[0])), positions[1])
|
|
248
|
+
when 'keys move' then move_key(*positions)
|
|
249
|
+
when 'keys delete' then move_key(positions[0], nil)
|
|
250
|
+
when 'certificates import' then import_certificate(positions[0], input(positions[1]))
|
|
251
|
+
when 'certificates export' then output(encode(read_certificate(positions[0])), positions[1])
|
|
252
|
+
when 'certificates generate' then generate_certificate(positions[0], positions[1] ? input(positions[1]) : nil)
|
|
253
|
+
when 'certificates request' then output(encode(certificate_request(positions[0], input(positions[1]))), positions[2])
|
|
254
|
+
when 'certificates delete' then delete_certificate(positions[0])
|
|
255
|
+
else raise ArgumentError, 'Unknown PIV operation'
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def verify_pin(pin = nil)
|
|
260
|
+
confirm!
|
|
261
|
+
reference_apdu(0x20, p2: 0x80, data: pin_bytes(pin || secret(:pin)))
|
|
262
|
+
:verified
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def verify_uv(request_pin: false)
|
|
266
|
+
raise ArgumentError, 'request_temporary_pin must be a Boolean' unless [true, false, nil].include?(request_pin)
|
|
267
|
+
|
|
268
|
+
confirm!
|
|
269
|
+
biometric_support
|
|
270
|
+
response = reference_apdu(0x20, p2: 0x96, data: tlv(request_pin ? 2 : 3))
|
|
271
|
+
raise ResponseError, 'Invalid temporary PIN length' if request_pin && response.bytesize != 16
|
|
272
|
+
|
|
273
|
+
request_pin ? response : :verified
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def verify_temporary_pin(value)
|
|
277
|
+
confirm!
|
|
278
|
+
value ||= secret(:temporary_pin)
|
|
279
|
+
raise ArgumentError, 'temporary_pin must contain exactly 16 bytes' unless value.is_a?(String) && value.bytesize == 16
|
|
280
|
+
|
|
281
|
+
biometric_support
|
|
282
|
+
reference_apdu(0x20, p2: 0x96, data: tlv(1, value))
|
|
283
|
+
:verified
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def authenticate(key = nil)
|
|
287
|
+
confirm!
|
|
288
|
+
key ||= secret(:management_key)
|
|
289
|
+
algorithm = management_algorithm
|
|
290
|
+
id, size, cipher_name = MANAGEMENT.fetch(algorithm)
|
|
291
|
+
require_version([5, 4, 0]) unless id == 3
|
|
292
|
+
raise ArgumentError, 'Invalid management key length' unless key.is_a?(String) && key.bytesize == size
|
|
293
|
+
|
|
294
|
+
witness = unpack(0x80, unpack(0x7c, apdu(0x87, p1: id, p2: 0x9b, data: tlv(0x7c, tlv(0x80)))))
|
|
295
|
+
challenge_size = id == 3 ? 8 : 16
|
|
296
|
+
raise ResponseError, 'Malformed management authentication witness' unless witness.bytesize == challenge_size
|
|
297
|
+
|
|
298
|
+
cipher = OpenSSL::Cipher.new(cipher_name)
|
|
299
|
+
cipher.decrypt
|
|
300
|
+
cipher.padding = 0
|
|
301
|
+
cipher.key = key
|
|
302
|
+
plain = cipher.update(witness) + cipher.final
|
|
303
|
+
challenge = SecureRandom.random_bytes(challenge_size)
|
|
304
|
+
response = unpack(0x82, unpack(0x7c, apdu(0x87, p1: id, p2: 0x9b, data: tlv(0x7c, tlv(0x80, plain) + tlv(0x81, challenge)))))
|
|
305
|
+
cipher.encrypt
|
|
306
|
+
cipher.padding = 0
|
|
307
|
+
cipher.key = key
|
|
308
|
+
expected = cipher.update(challenge) + cipher.final
|
|
309
|
+
raise ResponseError, 'Management authentication response does not verify' unless response.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(response, expected)
|
|
310
|
+
|
|
311
|
+
:authenticated
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def change_reference(ins, reference, current_name, new_name)
|
|
315
|
+
confirm!
|
|
316
|
+
current_value = secret(current_name)
|
|
317
|
+
replacement_value = secret(new_name)
|
|
318
|
+
current = pin_bytes(current_value)
|
|
319
|
+
replacement = pin_bytes(replacement_value)
|
|
320
|
+
data = pivman if current_name == :pin
|
|
321
|
+
reference_apdu(ins, p2: reference, data: current + replacement)
|
|
322
|
+
if data&.key?(0x82)
|
|
323
|
+
old_key = OpenSSL::PKCS5.pbkdf2_hmac(current_value, data.fetch(0x82), 10_000, 24, 'SHA1')
|
|
324
|
+
authenticate(old_key)
|
|
325
|
+
verify_pin(replacement_value)
|
|
326
|
+
salt = SecureRandom.random_bytes(16)
|
|
327
|
+
new_key = OpenSSL::PKCS5.pbkdf2_hmac(replacement_value, salt, 10_000, 24, 'SHA1')
|
|
328
|
+
set_management_key('TDES', new_key)
|
|
329
|
+
authenticate(new_key)
|
|
330
|
+
data[0x82] = salt
|
|
331
|
+
write_object(0x5fff00, container(0x80, data))
|
|
332
|
+
end
|
|
333
|
+
{ status: :changed, reference: reference }
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def set_retries(pin_retries, puk_retries)
|
|
337
|
+
pin_count = integer(pin_retries, 1..255)
|
|
338
|
+
puk_count = integer(puk_retries, 0..255)
|
|
339
|
+
confirm!
|
|
340
|
+
pin = secret(:pin)
|
|
341
|
+
pin_bytes(pin)
|
|
342
|
+
if version_at_least?([5, 3, 0])
|
|
343
|
+
begin
|
|
344
|
+
metadata(0x96)
|
|
345
|
+
raise UnsupportedError, 'PIN/PUK retries cannot be changed on biometric PIV'
|
|
346
|
+
rescue Error => e
|
|
347
|
+
raise unless [0x6a88, 0x6d00].include?(status_word(e))
|
|
348
|
+
end
|
|
349
|
+
raise UnsupportedError, 'FIPS retry limits must be set before changing PIN/PUK' if @opts[:fips_restrictions] && !(metadata(0x80).fetch(:default) && metadata(0x81).fetch(:default))
|
|
350
|
+
end
|
|
351
|
+
data = pivman
|
|
352
|
+
admin_authenticate
|
|
353
|
+
verify_pin(pin)
|
|
354
|
+
apdu(0xfa, p1: pin_count, p2: puk_count)
|
|
355
|
+
raise ResponseError, 'PIN/PUK retry-limit readback mismatch' if version_at_least?([5, 3, 0]) && !(metadata(0x80).fetch(:total_attempts) == pin_count && metadata(0x81).fetch(:total_attempts) == puk_count)
|
|
356
|
+
|
|
357
|
+
if data.fetch(0x81, "\x00".b).getbyte(0).anybits?(1)
|
|
358
|
+
data[0x81] = [data.fetch(0x81).getbyte(0) & ~1].pack('C')
|
|
359
|
+
write_object(0x5fff00, container(0x80, data))
|
|
360
|
+
end
|
|
361
|
+
{ status: :changed, pin_retries: pin_count, puk_retries: puk_count, references_reset: true }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def import_object(identifier, source)
|
|
365
|
+
id = parse_object_id(identifier)
|
|
366
|
+
bytes = input(source)
|
|
367
|
+
confirm!
|
|
368
|
+
if id == 0x5fc109
|
|
369
|
+
data = pivman
|
|
370
|
+
raise ArgumentError, 'Cannot overwrite protected management-key object' if data.key?(0x82) || data.fetch(0x81, "\x00".b).getbyte(0).anybits?(2)
|
|
371
|
+
end
|
|
372
|
+
admin_authenticate
|
|
373
|
+
write_object(id, bytes)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def generate_object(identifier)
|
|
377
|
+
id = parse_object_id(identifier)
|
|
378
|
+
raise ArgumentError, 'Only CHUID and CCC can be generated' unless [0x5fc102, 0x5fc107].include?(id)
|
|
379
|
+
|
|
380
|
+
confirm!
|
|
381
|
+
data = id == 0x5fc102 ? chuid : ccc
|
|
382
|
+
admin_authenticate
|
|
383
|
+
write_object(id, data)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def read_object(identifier)
|
|
387
|
+
id = parse_object_id(identifier)
|
|
388
|
+
unpack(id == 0x7e ? 0x7e : 0x53, apdu(0xcb, p1: 0x3f, p2: 0xff, data: tlv(0x5c, number_bytes(id))))
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def write_object(identifier, bytes)
|
|
392
|
+
confirm!
|
|
393
|
+
id = parse_object_id(identifier)
|
|
394
|
+
raise ArgumentError, 'Object data must be a String' unless bytes.is_a?(String)
|
|
395
|
+
|
|
396
|
+
apdu(0xdb, p1: 0x3f, p2: 0xff, data: tlv(0x5c, number_bytes(id)) + tlv(0x53, bytes))
|
|
397
|
+
observed = optional_object(id)
|
|
398
|
+
raise ResponseError, 'PIV object write readback mismatch; do not retry automatically' unless observed == bytes || (bytes.empty? && observed.nil?)
|
|
399
|
+
|
|
400
|
+
{ status: :written, object_id: id, bytes: bytes.bytesize }
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def change_management_key
|
|
404
|
+
confirm!
|
|
405
|
+
name = @opts[:algorithm] ? enum_name(@opts[:algorithm], MANAGEMENT) : management_algorithm
|
|
406
|
+
id, size = MANAGEMENT.fetch(name)
|
|
407
|
+
require_version([5, 4, 0]) unless id == 3
|
|
408
|
+
require_version([4, 0, 0]) if @opts[:touch]
|
|
409
|
+
raise UnsupportedError, 'TDES management keys are not allowed by FIPS restrictions' if @opts[:fips_restrictions] && id == 3
|
|
410
|
+
raise ArgumentError, 'generate conflicts with new_management_key' if @opts[:generate] && @opts[:new_management_key]
|
|
411
|
+
|
|
412
|
+
generated = @opts[:generate] || (@opts[:protect] && !@opts[:new_management_key])
|
|
413
|
+
raise ArgumentError, 'Generated unprotected management key requires secret_sink to retain it securely' if generated && !@opts[:protect] && !@opts[:secret_sink].respond_to?(:call)
|
|
414
|
+
|
|
415
|
+
replacement = generated ? SecureRandom.random_bytes(size) : secret(:new_management_key)
|
|
416
|
+
raise ArgumentError, 'Invalid new management key length' unless replacement.bytesize == size
|
|
417
|
+
|
|
418
|
+
data = pivman
|
|
419
|
+
stored = data.fetch(0x81, "\x00".b).getbyte(0).anybits?(2)
|
|
420
|
+
pin = secret(:pin) if @opts[:protect] || stored
|
|
421
|
+
pin_bytes(pin) if pin
|
|
422
|
+
admin_authenticate
|
|
423
|
+
verify_pin(pin) if pin
|
|
424
|
+
protected_fields = protected_pivman if @opts[:protect] || stored
|
|
425
|
+
@opts[:secret_sink].call(:new_management_key, replacement.dup) if generated && !@opts[:protect]
|
|
426
|
+
set_management_key(name, replacement, touch: @opts[:touch] == true)
|
|
427
|
+
authenticate(replacement)
|
|
428
|
+
verify_pin(pin) if pin
|
|
429
|
+
data.delete(0x82)
|
|
430
|
+
flags = data.fetch(0x81, "\x00".b).getbyte(0)
|
|
431
|
+
flags = @opts[:protect] ? flags | 2 : flags & ~2
|
|
432
|
+
data[0x81] = [flags].pack('C')
|
|
433
|
+
if protected_fields
|
|
434
|
+
@opts[:protect] ? protected_fields[0x89] = replacement : protected_fields.delete(0x89)
|
|
435
|
+
write_object(0x5fc109, container(0x88, protected_fields))
|
|
436
|
+
end
|
|
437
|
+
write_object(0x5fff00, container(0x80, data))
|
|
438
|
+
{ status: :changed, algorithm: name, protected: @opts[:protect] == true }
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def sign(identifier, message, algorithm: nil)
|
|
442
|
+
selected = slot(identifier)
|
|
443
|
+
raise ArgumentError, 'message must be a String' unless message.is_a?(String)
|
|
444
|
+
|
|
445
|
+
confirm!
|
|
446
|
+
algorithm ||= @opts[:algorithm] || metadata(selected).fetch(:algorithm)
|
|
447
|
+
algorithm = enum_name(algorithm, ALGORITHMS)
|
|
448
|
+
raise ArgumentError, 'X25519 is a key-agreement algorithm, not a signature algorithm' if algorithm == 'X25519'
|
|
449
|
+
|
|
450
|
+
policies(algorithm, generate: false)
|
|
451
|
+
padded = signature_input(algorithm, message.b)
|
|
452
|
+
crypto_pin
|
|
453
|
+
private_operation(selected, algorithm, padded)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def decrypt(identifier, ciphertext)
|
|
457
|
+
selected = slot(identifier)
|
|
458
|
+
raise ArgumentError, 'ciphertext must be a String' unless ciphertext.is_a?(String)
|
|
459
|
+
|
|
460
|
+
algorithm = "RSA#{ciphertext.bytesize * 8}"
|
|
461
|
+
raise ArgumentError, 'Invalid RSA ciphertext size' unless ALGORITHMS.key?(algorithm)
|
|
462
|
+
|
|
463
|
+
padding = @opts.fetch(:padding, :oaep).to_sym
|
|
464
|
+
raise ArgumentError, 'RSA decryption padding must be oaep, pkcs1 or none' unless %i[oaep pkcs1 none].include?(padding)
|
|
465
|
+
|
|
466
|
+
digest = digest_name
|
|
467
|
+
confirm!
|
|
468
|
+
policies(algorithm, generate: false)
|
|
469
|
+
crypto_pin
|
|
470
|
+
padded = private_operation(selected, algorithm, ciphertext.b)
|
|
471
|
+
raise ResponseError, 'Invalid RSA result size' unless padded.bytesize == ciphertext.bytesize
|
|
472
|
+
return padded if padding == :none
|
|
473
|
+
|
|
474
|
+
# The host only handles padding: an unrelated ephemeral key sends the
|
|
475
|
+
# raw device result through OpenSSL's hardened padding implementation.
|
|
476
|
+
dummy = OpenSSL::PKey::RSA.generate(ciphertext.bytesize * 8)
|
|
477
|
+
wrapped = dummy.encrypt(padded, 'rsa_padding_mode' => 'none')
|
|
478
|
+
options = { 'rsa_padding_mode' => padding == :oaep ? 'oaep' : 'pkcs1' }
|
|
479
|
+
options['rsa_pkcs1_implicit_rejection'] = '0' if padding == :pkcs1 && OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30200000
|
|
480
|
+
if padding == :oaep
|
|
481
|
+
options['rsa_oaep_md'] = digest
|
|
482
|
+
options['rsa_mgf1_md'] = @opts.fetch(:mgf1_hash, digest)
|
|
483
|
+
options['rsa_oaep_label'] = @opts[:label].b.unpack1('H*') if @opts[:label]
|
|
484
|
+
end
|
|
485
|
+
dummy.decrypt(wrapped, options)
|
|
486
|
+
rescue OpenSSL::OpenSSLError
|
|
487
|
+
raise ResponseError, 'RSA decryption failed', cause: nil
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def calculate_secret(identifier, peer)
|
|
491
|
+
selected = slot(identifier)
|
|
492
|
+
public_part = parse_key(peer)
|
|
493
|
+
algorithm = key_algorithm(public_part)
|
|
494
|
+
raise ArgumentError, 'Key agreement requires P-256, P-384 or X25519' unless %w[ECCP256 ECCP384 X25519].include?(algorithm)
|
|
495
|
+
|
|
496
|
+
confirm!
|
|
497
|
+
policies(algorithm, generate: false)
|
|
498
|
+
bytes = algorithm == 'X25519' ? public_part.raw_public_key : public_part.public_key.to_octet_string(:uncompressed)
|
|
499
|
+
crypto_pin
|
|
500
|
+
result = private_operation(selected, algorithm, bytes, exponentiation: true)
|
|
501
|
+
raise ResponseError, 'Invalid key agreement result' unless result.bytesize == ALGORITHMS.fetch(algorithm)[1] && result.bytes.any?(&:positive?)
|
|
502
|
+
|
|
503
|
+
result
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def generate_key(identifier)
|
|
507
|
+
selected = slot(identifier)
|
|
508
|
+
algorithm = enum_name(@opts.fetch(:algorithm, 'RSA2048'), ALGORITHMS)
|
|
509
|
+
policy = policies(algorithm, generate: true)
|
|
510
|
+
confirm!
|
|
511
|
+
admin_authenticate
|
|
512
|
+
response = apdu(0x47, p2: selected, data: tlv(0xac, tlv(0x80, [ALGORITHMS.fetch(algorithm)[0]].pack('C')) + policy))
|
|
513
|
+
public_key(algorithm, unpack(0x7f49, response))
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def import_key(identifier, bytes)
|
|
517
|
+
selected = slot(identifier)
|
|
518
|
+
confirm!
|
|
519
|
+
key = parse_key(bytes, private_key: true)
|
|
520
|
+
algorithm = key_algorithm(key)
|
|
521
|
+
policy = policies(algorithm, generate: false)
|
|
522
|
+
id, length = ALGORITHMS.fetch(algorithm)
|
|
523
|
+
data = if key.is_a?(OpenSSL::PKey::RSA)
|
|
524
|
+
raise ArgumentError, 'RSA public exponent must be 65537' unless key.e.to_i == 65_537
|
|
525
|
+
|
|
526
|
+
[key.p, key.q, key.dmp1, key.dmq1, key.iqmp].each_with_index.map { |value, index| tlv(index + 1, number_bytes(value.to_i, length / 2)) }.join.b
|
|
527
|
+
elsif key.is_a?(OpenSSL::PKey::EC)
|
|
528
|
+
tlv(6, number_bytes(key.private_key.to_i, length))
|
|
529
|
+
else
|
|
530
|
+
tlv(algorithm == 'ED25519' ? 7 : 8, key.raw_private_key)
|
|
531
|
+
end
|
|
532
|
+
admin_authenticate
|
|
533
|
+
apdu(0xfe, p1: id, p2: selected, data: data + policy)
|
|
534
|
+
if version_at_least?([5, 3, 0])
|
|
535
|
+
observed = metadata(selected).fetch(:public_key)
|
|
536
|
+
raise ResponseError, 'Imported public key readback mismatch' unless observed.public_to_der == key.public_to_der
|
|
537
|
+
end
|
|
538
|
+
{ status: :imported, slot: selected, algorithm: algorithm }
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def export_key(identifier)
|
|
542
|
+
selected = slot(identifier)
|
|
543
|
+
source = @opts.fetch(:public_key_source, :metadata).to_sym
|
|
544
|
+
key = case source
|
|
545
|
+
when :metadata then metadata(selected).fetch(:public_key)
|
|
546
|
+
when :attestation then attest(selected).public_key
|
|
547
|
+
when :certificate then read_certificate(selected).public_key
|
|
548
|
+
else raise ArgumentError, 'public_key_source must be metadata, attestation or certificate; private keys cannot be exported'
|
|
549
|
+
end
|
|
550
|
+
verify_key(selected, key) if @opts[:verify]
|
|
551
|
+
key
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def attest(identifier)
|
|
555
|
+
selected = slot(identifier)
|
|
556
|
+
require_version([4, 3, 0])
|
|
557
|
+
OpenSSL::X509::Certificate.new(apdu(0xf9, p1: selected))
|
|
558
|
+
rescue OpenSSL::X509::CertificateError
|
|
559
|
+
raise ResponseError, 'Malformed PIV attestation certificate', cause: nil
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def move_key(source, destination)
|
|
563
|
+
from = slot(source)
|
|
564
|
+
to = destination.nil? ? 0xff : slot(destination)
|
|
565
|
+
raise ArgumentError, 'Source and destination slots must differ' if from == to
|
|
566
|
+
|
|
567
|
+
confirm!
|
|
568
|
+
require_version([5, 7, 0])
|
|
569
|
+
original = metadata(from).fetch(:public_key)
|
|
570
|
+
admin_authenticate
|
|
571
|
+
apdu(0xf6, p1: to, p2: from)
|
|
572
|
+
begin
|
|
573
|
+
metadata(from)
|
|
574
|
+
raise ResponseError, 'PIV source key still present after move/delete'
|
|
575
|
+
rescue Error => e
|
|
576
|
+
raise unless status_word(e) == 0x6a88
|
|
577
|
+
end
|
|
578
|
+
raise ResponseError, 'PIV moved key readback mismatch' if destination && metadata(to).fetch(:public_key).public_to_der != original.public_to_der
|
|
579
|
+
|
|
580
|
+
{ status: destination ? :moved : :deleted, source: from, destination: destination ? to : nil }
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def reset
|
|
584
|
+
confirm!
|
|
585
|
+
begin
|
|
586
|
+
bio = metadata(0x96)
|
|
587
|
+
raise UnsupportedError, 'Cannot reset PIV with enrolled biometrics; a full-device reset is required' if bio[:configured]
|
|
588
|
+
rescue UnsupportedError
|
|
589
|
+
raise if bio
|
|
590
|
+
rescue Error => e
|
|
591
|
+
raise unless [0x6a88, 0x6d00].include?(status_word(e))
|
|
592
|
+
end
|
|
593
|
+
if @opts[:block_retries] == true
|
|
594
|
+
block_reference(0x20, "\xff".b * 8)
|
|
595
|
+
block_reference(0x2c, "\xff".b * 16)
|
|
596
|
+
end
|
|
597
|
+
apdu(0xfb)
|
|
598
|
+
@management_algorithm = nil
|
|
599
|
+
raise ResponseError, 'PIV reset metadata is not default' if version_at_least?([5, 3, 0]) && !(metadata(0x80).fetch(:default) && metadata(0x9b).fetch(:default))
|
|
600
|
+
|
|
601
|
+
{ status: :reset, blocked_retries_authorized: @opts[:block_retries] == true }
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def info
|
|
605
|
+
result = { version: version, slots: {} }
|
|
606
|
+
result[:serial] = apdu(0xf8).unpack1('H*').to_i(16) if version_at_least?([5, 0, 3])
|
|
607
|
+
if version_at_least?([5, 3, 0])
|
|
608
|
+
result[:pin] = metadata(0x80)
|
|
609
|
+
result[:management_key] = metadata(0x9b)
|
|
610
|
+
begin
|
|
611
|
+
result[:biometrics] = metadata(0x96)
|
|
612
|
+
rescue Error => e
|
|
613
|
+
raise unless [0x6a88, 0x6d00].include?(status_word(e))
|
|
614
|
+
|
|
615
|
+
result[:puk] = metadata(0x81)
|
|
616
|
+
end
|
|
617
|
+
else
|
|
618
|
+
begin
|
|
619
|
+
reference_apdu(0x20, p2: 0x80)
|
|
620
|
+
result[:pin] = { verified: true, attempts_remaining: nil }
|
|
621
|
+
rescue PinError => e
|
|
622
|
+
result[:pin] = { attempts_remaining: e.attempts_remaining }
|
|
623
|
+
end
|
|
624
|
+
result[:management_key] = { algorithm: 'TDES' }
|
|
625
|
+
end
|
|
626
|
+
data = pivman
|
|
627
|
+
result[:management_key][:protected] = data.key?(0x82) || data.fetch(0x81, "\x00".b).getbyte(0).anybits?(2)
|
|
628
|
+
result[:chuid] = optional_object(0x5fc102)
|
|
629
|
+
result[:ccc] = optional_object(0x5fc107)
|
|
630
|
+
SLOTS.values.reject { |selected| selected == 0xf9 }.each do |selected|
|
|
631
|
+
entry = {}
|
|
632
|
+
if version_at_least?([5, 3, 0])
|
|
633
|
+
begin
|
|
634
|
+
entry[:key] = metadata(selected)
|
|
635
|
+
rescue Error => e
|
|
636
|
+
raise unless status_word(e) == 0x6a88
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
begin
|
|
640
|
+
certificate = read_certificate(selected)
|
|
641
|
+
entry[:certificate] = { subject: certificate.subject.to_s, issuer: certificate.issuer.to_s, serial: certificate.serial.to_i,
|
|
642
|
+
not_before: certificate.not_before, not_after: certificate.not_after, sha256: OpenSSL::Digest.hexdigest('SHA256', certificate.to_der) }
|
|
643
|
+
rescue Error => e
|
|
644
|
+
raise unless status_word(e) == 0x6a82
|
|
645
|
+
end
|
|
646
|
+
result[:slots][format('%02x', selected)] = entry unless entry.empty?
|
|
647
|
+
end
|
|
648
|
+
result
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def read_certificate(identifier)
|
|
652
|
+
fields = Protocol.tlv_map(data: read_object(certificate_object(slot(identifier))))
|
|
653
|
+
bytes = field(fields, 0x70)
|
|
654
|
+
compression = fields.fetch(0x71, "\x00".b)
|
|
655
|
+
raise ResponseError, 'Unsupported certificate compression' unless ["\x00".b, "\x01".b].include?(compression)
|
|
656
|
+
|
|
657
|
+
bytes = decompress(bytes) if compression == "\x01".b
|
|
658
|
+
OpenSSL::X509::Certificate.new(bytes)
|
|
659
|
+
rescue OpenSSL::X509::CertificateError
|
|
660
|
+
raise ResponseError, 'Malformed PIV certificate', cause: nil
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def import_certificate(identifier, bytes)
|
|
664
|
+
selected = slot(identifier)
|
|
665
|
+
confirm!
|
|
666
|
+
certificate = parse_certificate(bytes)
|
|
667
|
+
admin_authenticate
|
|
668
|
+
verify_key(selected, certificate.public_key) if @opts[:verify]
|
|
669
|
+
store_certificate(selected, certificate)
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def delete_certificate(identifier)
|
|
673
|
+
selected = slot(identifier)
|
|
674
|
+
confirm!
|
|
675
|
+
admin_authenticate
|
|
676
|
+
result = write_object(certificate_object(selected), ''.b)
|
|
677
|
+
result[:chuid] = update_chuid unless @opts[:no_update_chuid] || @opts[:update_chuid] == false
|
|
678
|
+
result
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def generate_certificate(identifier, bytes)
|
|
682
|
+
selected = slot(identifier)
|
|
683
|
+
confirm!
|
|
684
|
+
key = bytes ? parse_key(bytes) : export_key(selected)
|
|
685
|
+
algorithm = key_algorithm(key)
|
|
686
|
+
raise ArgumentError, 'X25519 cannot self-sign a certificate' if algorithm == 'X25519'
|
|
687
|
+
|
|
688
|
+
subject = certificate_subject
|
|
689
|
+
days = integer(@opts.fetch(:valid_days, 365), 1..36_500)
|
|
690
|
+
not_before = @opts.fetch(:not_before, Time.now.utc)
|
|
691
|
+
not_after = @opts.fetch(:not_after, not_before + (days * 86_400))
|
|
692
|
+
raise ArgumentError, 'Invalid certificate validity times' unless not_before.is_a?(Time) && not_after.is_a?(Time) && not_after > not_before
|
|
693
|
+
|
|
694
|
+
asn1 = OpenSSL::ASN1
|
|
695
|
+
signature_algorithm = signature_identifier(algorithm)
|
|
696
|
+
serial = @opts.fetch(:certificate_serial, SecureRandom.random_number((2**159) - 1) + 1)
|
|
697
|
+
raise ArgumentError, 'Certificate serial must be a positive integer under 160 bits' unless serial.is_a?(Integer) && serial.positive? && serial.bit_length <= 159
|
|
698
|
+
|
|
699
|
+
fields = [asn1::ASN1Data.new([asn1::Integer.new(2)], 0, :CONTEXT_SPECIFIC), asn1::Integer.new(serial), signature_algorithm,
|
|
700
|
+
asn1.decode(subject.to_der), asn1::Sequence.new([asn1_time(not_before), asn1_time(not_after)]),
|
|
701
|
+
asn1.decode(subject.to_der), asn1.decode(key.public_to_der)]
|
|
702
|
+
extensions = @opts.fetch(:extensions, [])
|
|
703
|
+
raise ArgumentError, 'extensions must be an Array of X509 extensions' unless extensions.is_a?(Array) && extensions.all?(OpenSSL::X509::Extension)
|
|
704
|
+
|
|
705
|
+
fields << asn1::ASN1Data.new([asn1::Sequence.new(extensions.map { |ext| asn1.decode(ext.to_der) })], 3, :CONTEXT_SPECIFIC) unless extensions.empty?
|
|
706
|
+
tbs = asn1::Sequence.new(fields)
|
|
707
|
+
admin_authenticate
|
|
708
|
+
signature = sign(selected, tbs.to_der, algorithm: algorithm)
|
|
709
|
+
certificate = OpenSSL::X509::Certificate.new(asn1::Sequence.new([tbs, signature_algorithm, asn1::BitString.new(signature)]).to_der)
|
|
710
|
+
raise ResponseError, 'Device-signed certificate does not verify against supplied public key' unless certificate.verify(key)
|
|
711
|
+
|
|
712
|
+
store_certificate(selected, certificate).merge(certificate: encode(certificate))
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def certificate_request(identifier, bytes)
|
|
716
|
+
selected = slot(identifier)
|
|
717
|
+
confirm!
|
|
718
|
+
key = parse_key(bytes)
|
|
719
|
+
algorithm = key_algorithm(key)
|
|
720
|
+
raise ArgumentError, 'X25519 cannot sign a CSR' if algorithm == 'X25519'
|
|
721
|
+
|
|
722
|
+
asn1 = OpenSSL::ASN1
|
|
723
|
+
info = asn1::Sequence.new([asn1::Integer.new(0), asn1.decode(certificate_subject.to_der), asn1.decode(key.public_to_der), asn1::ASN1Data.new([], 0, :CONTEXT_SPECIFIC)])
|
|
724
|
+
signature_algorithm = signature_identifier(algorithm)
|
|
725
|
+
signature = sign(selected, info.to_der, algorithm: algorithm)
|
|
726
|
+
csr = OpenSSL::X509::Request.new(asn1::Sequence.new([info, signature_algorithm, asn1::BitString.new(signature)]).to_der)
|
|
727
|
+
raise ResponseError, 'Device-signed CSR does not verify against supplied public key' unless csr.verify(key)
|
|
728
|
+
|
|
729
|
+
csr
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def version
|
|
733
|
+
@version ||= begin
|
|
734
|
+
response = apdu(0xfd)
|
|
735
|
+
raise ResponseError, 'Invalid PIV version response' unless response.bytesize == 3
|
|
736
|
+
|
|
737
|
+
response.bytes
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def metadata(reference)
|
|
742
|
+
require_version([5, 3, 0])
|
|
743
|
+
fields = Protocol.tlv_map(data: apdu(0xf7, p2: reference))
|
|
744
|
+
case reference
|
|
745
|
+
when 0x80, 0x81
|
|
746
|
+
attempts = field(fields, 6, 2).bytes
|
|
747
|
+
{ default: field(fields, 5, 1) != "\x00".b, total_attempts: attempts[0], attempts_remaining: attempts[1] }
|
|
748
|
+
when 0x9b
|
|
749
|
+
id = fields.fetch(1, "\x03".b).getbyte(0)
|
|
750
|
+
name = MANAGEMENT.keys.find { |key| MANAGEMENT[key][0] == id }
|
|
751
|
+
raise ResponseError, 'Unknown management algorithm' unless name
|
|
752
|
+
|
|
753
|
+
{ algorithm: name, default: field(fields, 5, 1) != "\x00".b, touch_policy: field(fields, 2, 2).getbyte(1) }
|
|
754
|
+
when 0x96
|
|
755
|
+
{ configured: field(fields, 7, 1).getbyte(0) == 1, attempts_remaining: field(fields, 6, 1).getbyte(0), temporary_pin: fields.fetch(8, "\x00".b).getbyte(0) == 1 }
|
|
756
|
+
else
|
|
757
|
+
id = field(fields, 1, 1).getbyte(0)
|
|
758
|
+
algorithm = ALGORITHMS.keys.find { |key| ALGORITHMS[key][0] == id }
|
|
759
|
+
raise ResponseError, 'Unknown PIV key algorithm' unless algorithm
|
|
760
|
+
|
|
761
|
+
policy = field(fields, 2, 2).bytes
|
|
762
|
+
{ algorithm: algorithm, pin_policy: PIN_POLICIES.key(policy[0]), touch_policy: TOUCH_POLICIES.key(policy[1]),
|
|
763
|
+
generated: field(fields, 3, 1).getbyte(0) == 1, public_key: public_key(algorithm, field(fields, 4)) }
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
private
|
|
768
|
+
|
|
769
|
+
def biometric_support
|
|
770
|
+
metadata(0x96)
|
|
771
|
+
rescue Error => e
|
|
772
|
+
raise UnsupportedError, 'Biometric verification is not supported by this PIV device', cause: nil if [0x6a88, 0x6d00].include?(status_word(e))
|
|
773
|
+
|
|
774
|
+
raise
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
def validate_options
|
|
778
|
+
flags = %i[protect generate touch use_protected_key verify compress update_chuid no_update_chuid block_retries prehashed fips_restrictions]
|
|
779
|
+
flags.each do |flag|
|
|
780
|
+
raise ArgumentError, "#{flag} must be a Boolean" if @opts.key?(flag) && ![true, false].include?(@opts[flag])
|
|
781
|
+
end
|
|
782
|
+
raise ArgumentError, 'Conflicting CHUID update options' if @opts[:update_chuid] && @opts[:no_update_chuid]
|
|
783
|
+
raise ArgumentError, 'format must be PEM or DER' if @opts[:format] && !%w[PEM DER].include?(@opts[:format].to_s.upcase)
|
|
784
|
+
raise ArgumentError, 'label must be binary String' if @opts[:label] && !@opts[:label].is_a?(String)
|
|
785
|
+
raise ArgumentError, 'mgf1_hash must be SHA1, SHA256, SHA384 or SHA512' if @opts[:mgf1_hash] && !%w[SHA1 SHA256 SHA384 SHA512].include?(@opts[:mgf1_hash].to_s.upcase)
|
|
786
|
+
|
|
787
|
+
digest_name if @opts[:hash_algorithm]
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def block_reference(ins, data)
|
|
791
|
+
# Only reachable with BOTH confirm:true and block_retries:true. Empty
|
|
792
|
+
# padded references are deliberately invalid, never factory guesses.
|
|
793
|
+
previous = 256
|
|
794
|
+
256.times do
|
|
795
|
+
reference_apdu(ins, p2: 0x80, data: data)
|
|
796
|
+
raise ResponseError, 'Unexpected successful blocking attempt; reset stopped'
|
|
797
|
+
rescue PinError => e
|
|
798
|
+
return :blocked if e.attempts_remaining.zero?
|
|
799
|
+
# Firmware caps SW retries at 15; allow that plateau only.
|
|
800
|
+
raise ResponseError, 'PIN/PUK retry counter did not decrease; reset stopped' if e.attempts_remaining > previous || (e.attempts_remaining == previous && previous != 15)
|
|
801
|
+
|
|
802
|
+
previous = e.attempts_remaining
|
|
803
|
+
end
|
|
804
|
+
raise ResponseError, 'PIV blocking limit exceeded; reset stopped'
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def certificate_object(selected)
|
|
808
|
+
return 0x5fc10d + selected - 0x82 if (0x82..0x95).cover?(selected)
|
|
809
|
+
|
|
810
|
+
{ 0x9a => 0x5fc105, 0x9c => 0x5fc10a, 0x9d => 0x5fc10b, 0x9e => 0x5fc101, 0xf9 => 0x5fff01 }.fetch(selected)
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def store_certificate(selected, certificate)
|
|
814
|
+
bytes = certificate.to_der
|
|
815
|
+
if @opts[:compress]
|
|
816
|
+
buffer = StringIO.new(''.b)
|
|
817
|
+
gzip = Zlib::GzipWriter.new(buffer)
|
|
818
|
+
gzip.write(bytes)
|
|
819
|
+
gzip.close
|
|
820
|
+
bytes = buffer.string
|
|
821
|
+
end
|
|
822
|
+
result = write_object(certificate_object(selected), tlv(0x70, bytes) + tlv(0x71, @opts[:compress] ? "\x01".b : "\x00".b) + tlv(0xfe))
|
|
823
|
+
result[:chuid] = update_chuid unless @opts[:no_update_chuid] || @opts[:update_chuid] == false
|
|
824
|
+
result
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def update_chuid
|
|
828
|
+
original = optional_object(0x5fc102)
|
|
829
|
+
if original && !original.empty?
|
|
830
|
+
begin
|
|
831
|
+
fields = Protocol.tlv_map(data: original)
|
|
832
|
+
return :preserved unless fields.key?(0x30) && fields.key?(0x35) && fields[0x34]&.bytesize == 16 && fields[0x3e] == ''.b
|
|
833
|
+
|
|
834
|
+
fields[0x34] = [SecureRandom.uuid.delete('-')].pack('H*')
|
|
835
|
+
replacement = fields.map { |tag, data| tlv(tag, data) }.join.b
|
|
836
|
+
rescue ResponseError
|
|
837
|
+
return :preserved
|
|
838
|
+
end
|
|
839
|
+
else
|
|
840
|
+
replacement = chuid
|
|
841
|
+
end
|
|
842
|
+
write_object(0x5fc102, replacement)
|
|
843
|
+
:updated
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def decompress(bytes)
|
|
847
|
+
limit = 65_536
|
|
848
|
+
if bytes.start_with?("\x1f\x8b".b)
|
|
849
|
+
reader = Zlib::GzipReader.new(StringIO.new(bytes))
|
|
850
|
+
begin
|
|
851
|
+
result = reader.read(limit + 1)
|
|
852
|
+
ensure
|
|
853
|
+
reader.close
|
|
854
|
+
end
|
|
855
|
+
elsif bytes.start_with?("\x01\x00".b) && bytes.bytesize >= 4
|
|
856
|
+
expected = bytes.byteslice(2, 2).unpack1('v')
|
|
857
|
+
inflater = Zlib::Inflate.new
|
|
858
|
+
begin
|
|
859
|
+
result = ''.b
|
|
860
|
+
inflater.inflate(bytes.byteslice(4..)) do |chunk|
|
|
861
|
+
result << chunk
|
|
862
|
+
raise ResponseError, 'Decompressed certificate exceeds limit' if result.bytesize > limit
|
|
863
|
+
end
|
|
864
|
+
raise ResponseError, 'Compressed certificate length mismatch' unless result.bytesize == expected && inflater.finished?
|
|
865
|
+
ensure
|
|
866
|
+
inflater.close
|
|
867
|
+
end
|
|
868
|
+
else
|
|
869
|
+
raise ResponseError, 'Unknown certificate compression format'
|
|
870
|
+
end
|
|
871
|
+
raise ResponseError, 'Decompressed certificate exceeds limit' if result.bytesize > limit
|
|
872
|
+
|
|
873
|
+
result
|
|
874
|
+
rescue Zlib::Error, EOFError
|
|
875
|
+
raise ResponseError, 'Invalid compressed certificate', cause: nil
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def parse_certificate(bytes)
|
|
879
|
+
return bytes if bytes.is_a?(OpenSSL::X509::Certificate)
|
|
880
|
+
|
|
881
|
+
blocks = bytes.scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m)
|
|
882
|
+
certificates = if blocks.empty?
|
|
883
|
+
begin
|
|
884
|
+
[OpenSSL::X509::Certificate.new(bytes)]
|
|
885
|
+
rescue OpenSSL::X509::CertificateError
|
|
886
|
+
archive = OpenSSL::PKCS12.new(bytes, @opts[:password] || '')
|
|
887
|
+
[archive.certificate, *archive.ca_certs].compact
|
|
888
|
+
end
|
|
889
|
+
else
|
|
890
|
+
blocks.map { |block| OpenSSL::X509::Certificate.new(block) }
|
|
891
|
+
end
|
|
892
|
+
leaves = certificates.reject { |candidate| certificates.any? { |other| other != candidate && other.issuer.to_der == candidate.subject.to_der } }
|
|
893
|
+
raise ArgumentError, 'Certificate input must contain one unambiguous leaf' unless leaves.size == 1
|
|
894
|
+
|
|
895
|
+
leaves.first
|
|
896
|
+
rescue OpenSSL::OpenSSLError
|
|
897
|
+
raise ArgumentError, 'Invalid certificate or PKCS12 password', cause: nil
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def certificate_subject
|
|
901
|
+
value = @opts[:subject]
|
|
902
|
+
return value if value.is_a?(OpenSSL::X509::Name)
|
|
903
|
+
raise ArgumentError, 'subject is required' unless value.is_a?(String) && !value.empty?
|
|
904
|
+
|
|
905
|
+
value = "CN=#{value}" unless value.include?('=')
|
|
906
|
+
OpenSSL::X509::Name.parse_rfc2253(value)
|
|
907
|
+
rescue OpenSSL::X509::NameError
|
|
908
|
+
raise ArgumentError, 'Invalid RFC4514 certificate subject', cause: nil
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
def signature_identifier(algorithm)
|
|
912
|
+
raise ArgumentError, 'Certificates and CSRs use PKCS1 signature padding' if @opts[:padding] && @opts[:padding].to_sym != :pkcs1
|
|
913
|
+
raise ArgumentError, 'Certificates and CSRs cannot sign prehashed inputs' if @opts[:prehashed]
|
|
914
|
+
|
|
915
|
+
asn1 = OpenSSL::ASN1
|
|
916
|
+
digest = digest_name
|
|
917
|
+
oid = if algorithm == 'ED25519'
|
|
918
|
+
'ED25519'
|
|
919
|
+
elsif algorithm.start_with?('RSA')
|
|
920
|
+
"#{digest.downcase}WithRSAEncryption"
|
|
921
|
+
else
|
|
922
|
+
"ecdsa-with-#{digest}"
|
|
923
|
+
end
|
|
924
|
+
values = [asn1::ObjectId.new(oid)]
|
|
925
|
+
values << asn1::Null.new(nil) if algorithm.start_with?('RSA')
|
|
926
|
+
asn1::Sequence.new(values)
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
def asn1_time(time)
|
|
930
|
+
(1950..2049).cover?(time.year) ? OpenSSL::ASN1::UTCTime.new(time.utc) : OpenSSL::ASN1::GeneralizedTime.new(time.utc)
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
def crypto_pin
|
|
934
|
+
verify_pin if @opts[:pin] || @opts[:secret_provider]
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
def private_operation(selected, algorithm, bytes, exponentiation: false)
|
|
938
|
+
confirm!
|
|
939
|
+
unpack(0x82, unpack(0x7c, apdu(0x87, p1: ALGORITHMS.fetch(algorithm)[0], p2: selected, data: tlv(0x7c, tlv(0x82) + tlv(exponentiation ? 0x85 : 0x81, bytes)))))
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
def digest_name
|
|
943
|
+
name = @opts.fetch(:hash_algorithm, 'SHA256').to_s.upcase
|
|
944
|
+
raise ArgumentError, 'hash_algorithm must be SHA1, SHA256, SHA384 or SHA512' unless %w[SHA1 SHA256 SHA384 SHA512].include?(name)
|
|
945
|
+
|
|
946
|
+
name
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
def signature_input(algorithm, message)
|
|
950
|
+
return message if algorithm == 'ED25519'
|
|
951
|
+
|
|
952
|
+
digest = digest_name
|
|
953
|
+
hashed = @opts[:prehashed] ? message : OpenSSL::Digest.digest(digest, message)
|
|
954
|
+
raise ArgumentError, 'Prehashed message has the wrong digest size' unless hashed.bytesize == OpenSSL::Digest.new(digest).digest_length
|
|
955
|
+
|
|
956
|
+
size = ALGORITHMS.fetch(algorithm)[1]
|
|
957
|
+
return hashed.byteslice(0, size).rjust(size, "\x00".b) if algorithm.start_with?('ECC')
|
|
958
|
+
|
|
959
|
+
padding = @opts.fetch(:padding, :pkcs1).to_sym
|
|
960
|
+
return pss_encode(hashed, size, digest) if padding == :pss
|
|
961
|
+
raise ArgumentError, 'RSA signature padding must be pkcs1 or pss' unless padding == :pkcs1
|
|
962
|
+
|
|
963
|
+
asn1 = OpenSSL::ASN1
|
|
964
|
+
info = asn1::Sequence.new([asn1::Sequence.new([asn1::ObjectId.new(digest), asn1::Null.new(nil)]), asn1::OctetString.new(hashed)]).to_der
|
|
965
|
+
raise ArgumentError, 'Digest too large for RSA signature' if size < info.bytesize + 11
|
|
966
|
+
|
|
967
|
+
"\x00\x01".b + ("\xff".b * (size - info.bytesize - 3)) + "\x00".b + info
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
def pss_encode(hashed, size, digest)
|
|
971
|
+
salt_length = integer(@opts.fetch(:salt_length, hashed.bytesize), 0..(size - hashed.bytesize - 2))
|
|
972
|
+
salt = SecureRandom.random_bytes(salt_length)
|
|
973
|
+
h = OpenSSL::Digest.digest(digest, ("\x00".b * 8) + hashed + salt)
|
|
974
|
+
data = ("\x00".b * (size - hashed.bytesize - salt_length - 2)) + "\x01".b + salt
|
|
975
|
+
masked = xor_bytes(data, mgf1(h, data.bytesize, @opts.fetch(:mgf1_hash, digest)))
|
|
976
|
+
masked.setbyte(0, masked.getbyte(0) & 0x7f)
|
|
977
|
+
masked + h + "\xbc".b
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
def mgf1(seed, length, digest)
|
|
981
|
+
output = ''.b
|
|
982
|
+
counter = 0
|
|
983
|
+
while output.bytesize < length
|
|
984
|
+
output << OpenSSL::Digest.digest(digest, seed + [counter].pack('N'))
|
|
985
|
+
counter += 1
|
|
986
|
+
end
|
|
987
|
+
output.byteslice(0, length)
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
def xor_bytes(left, right)
|
|
991
|
+
left.bytes.zip(right.bytes).map { |a, b| a ^ b }.pack('C*')
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
def verify_key(selected, key)
|
|
995
|
+
confirm!
|
|
996
|
+
algorithm = key_algorithm(key)
|
|
997
|
+
if algorithm == 'X25519'
|
|
998
|
+
peer = OpenSSL::PKey.generate_key('X25519')
|
|
999
|
+
shared = calculate_secret(selected, OpenSSL::PKey.read(peer.public_to_der))
|
|
1000
|
+
expected = peer.derive(key)
|
|
1001
|
+
valid = OpenSSL.fixed_length_secure_compare(shared, expected)
|
|
1002
|
+
else
|
|
1003
|
+
challenge = SecureRandom.random_bytes(32)
|
|
1004
|
+
signature = sign(selected, challenge, algorithm: algorithm)
|
|
1005
|
+
valid = key.verify(algorithm == 'ED25519' ? nil : digest_name, signature, challenge)
|
|
1006
|
+
end
|
|
1007
|
+
raise ResponseError, 'Public key does not match the device private key' unless valid
|
|
1008
|
+
|
|
1009
|
+
:verified
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
def slot(value)
|
|
1013
|
+
return value if value.is_a?(Integer) && SLOTS.value?(value)
|
|
1014
|
+
|
|
1015
|
+
name = value.to_s.upcase.tr('-', '_')
|
|
1016
|
+
return SLOTS.fetch(name) if SLOTS.key?(name)
|
|
1017
|
+
raise ArgumentError, 'Invalid PIV key slot' unless value.is_a?(String) && value.match?(/\A(?:0x)?[0-9a-fA-F]{2}\z/) && SLOTS.value?(value.to_i(16))
|
|
1018
|
+
|
|
1019
|
+
value.to_i(16)
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
def policies(algorithm, generate:)
|
|
1023
|
+
pin = PIN_POLICIES.fetch(enum_name(@opts.fetch(:pin_policy, 'DEFAULT'), PIN_POLICIES))
|
|
1024
|
+
touch = TOUCH_POLICIES.fetch(enum_name(@opts.fetch(:touch_policy, 'DEFAULT'), TOUCH_POLICIES))
|
|
1025
|
+
require_version([4, 0, 0]) if algorithm == 'ECCP384' || pin.positive? || touch.positive?
|
|
1026
|
+
require_version([4, 3, 0]) if touch == 3
|
|
1027
|
+
require_version([5, 7, 0]) if %w[RSA3072 RSA4096 ED25519 X25519].include?(algorithm)
|
|
1028
|
+
raise UnsupportedError, 'RSA generation is disabled for ROCA-vulnerable firmware' if generate && algorithm.start_with?('RSA') && version_at_least?([4, 2, 0]) && !version_at_least?([4, 3, 5])
|
|
1029
|
+
|
|
1030
|
+
raise UnsupportedError, 'Algorithm or PIN policy forbidden by FIPS restrictions' if (@opts[:fips_restrictions] || (version_at_least?([4, 4, 0]) && !version_at_least?([4, 5, 0]))) && (%w[RSA1024 X25519].include?(algorithm) || pin == 1)
|
|
1031
|
+
|
|
1032
|
+
if pin >= 4
|
|
1033
|
+
begin
|
|
1034
|
+
metadata(0x96)
|
|
1035
|
+
rescue Error => e
|
|
1036
|
+
raise UnsupportedError, 'MATCH policies require biometric PIV firmware', cause: nil if [0x6a88, 0x6d00].include?(status_word(e))
|
|
1037
|
+
|
|
1038
|
+
raise
|
|
1039
|
+
end
|
|
1040
|
+
end
|
|
1041
|
+
(pin.positive? ? tlv(0xaa, [pin].pack('C')) : ''.b) + (touch.positive? ? tlv(0xab, [touch].pack('C')) : ''.b)
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
def public_key(algorithm, encoded)
|
|
1045
|
+
fields = Protocol.tlv_map(data: encoded)
|
|
1046
|
+
asn1 = OpenSSL::ASN1
|
|
1047
|
+
if algorithm.start_with?('RSA')
|
|
1048
|
+
n = OpenSSL::BN.new(field(fields, 0x81), 2)
|
|
1049
|
+
e = OpenSSL::BN.new(field(fields, 0x82), 2)
|
|
1050
|
+
rsa = asn1::Sequence.new([asn1::Integer.new(n), asn1::Integer.new(e)]).to_der
|
|
1051
|
+
der = asn1::Sequence.new([asn1::Sequence.new([asn1::ObjectId.new('rsaEncryption'), asn1::Null.new(nil)]), asn1::BitString.new(rsa)]).to_der
|
|
1052
|
+
else
|
|
1053
|
+
point = field(fields, 0x86)
|
|
1054
|
+
oids = case algorithm
|
|
1055
|
+
when 'ECCP256' then %w[id-ecPublicKey prime256v1]
|
|
1056
|
+
when 'ECCP384' then %w[id-ecPublicKey secp384r1]
|
|
1057
|
+
else [algorithm]
|
|
1058
|
+
end
|
|
1059
|
+
der = asn1::Sequence.new([asn1::Sequence.new(oids.map { |oid| asn1::ObjectId.new(oid) }), asn1::BitString.new(point)]).to_der
|
|
1060
|
+
end
|
|
1061
|
+
key = OpenSSL::PKey.read(der)
|
|
1062
|
+
raise ResponseError, 'Public key algorithm mismatch' unless key_algorithm(key) == algorithm
|
|
1063
|
+
|
|
1064
|
+
key
|
|
1065
|
+
rescue OpenSSL::PKey::PKeyError, OpenSSL::ASN1::ASN1Error
|
|
1066
|
+
raise ResponseError, 'Malformed PIV public key', cause: nil
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
def key_algorithm(key)
|
|
1070
|
+
name = if key.is_a?(OpenSSL::PKey::RSA)
|
|
1071
|
+
"RSA#{key.n.num_bits}"
|
|
1072
|
+
elsif key.is_a?(OpenSSL::PKey::EC)
|
|
1073
|
+
{ 'prime256v1' => 'ECCP256', 'secp384r1' => 'ECCP384' }.fetch(key.group.curve_name, '')
|
|
1074
|
+
else
|
|
1075
|
+
key.oid
|
|
1076
|
+
end
|
|
1077
|
+
enum_name(name, ALGORITHMS)
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
def parse_key(bytes, private_key: false)
|
|
1081
|
+
return bytes if bytes.is_a?(OpenSSL::PKey::PKey) && !private_key
|
|
1082
|
+
|
|
1083
|
+
password = @opts[:password]
|
|
1084
|
+
password ||= secret(:password) if bytes.is_a?(String) && bytes.include?('ENCRYPTED') && @opts[:secret_provider]
|
|
1085
|
+
key = if bytes.is_a?(OpenSSL::PKey::PKey)
|
|
1086
|
+
bytes
|
|
1087
|
+
else
|
|
1088
|
+
begin
|
|
1089
|
+
OpenSSL::PKey.read(bytes, password || '')
|
|
1090
|
+
rescue OpenSSL::PKey::PKeyError
|
|
1091
|
+
raise unless private_key
|
|
1092
|
+
|
|
1093
|
+
OpenSSL::PKCS12.new(bytes, password || '').key || (raise ArgumentError, 'PKCS12 contains no private key')
|
|
1094
|
+
end
|
|
1095
|
+
end
|
|
1096
|
+
key.private_to_der if private_key
|
|
1097
|
+
key_algorithm(key)
|
|
1098
|
+
key
|
|
1099
|
+
rescue OpenSSL::OpenSSLError
|
|
1100
|
+
raise ArgumentError, 'Invalid key data or key password; private import requires a private key', cause: nil
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
def encode(value)
|
|
1104
|
+
encoding = @opts.fetch(:format, 'PEM').to_s.upcase
|
|
1105
|
+
raise ArgumentError, 'format must be PEM or DER' unless %w[PEM DER].include?(encoding)
|
|
1106
|
+
|
|
1107
|
+
if value.is_a?(OpenSSL::PKey::PKey)
|
|
1108
|
+
encoding == 'PEM' ? value.public_to_pem : value.public_to_der
|
|
1109
|
+
else
|
|
1110
|
+
encoding == 'PEM' ? value.to_pem : value.to_der
|
|
1111
|
+
end
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
def input(path)
|
|
1115
|
+
data = path == '-' ? @opts[:input] : File.binread(path)
|
|
1116
|
+
raise ArgumentError, 'Binary input is required in opts[:input] for -' unless data.is_a?(String)
|
|
1117
|
+
|
|
1118
|
+
data.b
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
def output(bytes, path)
|
|
1122
|
+
return bytes if path.nil? || path == '-'
|
|
1123
|
+
|
|
1124
|
+
confirm!
|
|
1125
|
+
# Never clobber a caller's existing file on an uncertain operation.
|
|
1126
|
+
File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file|
|
|
1127
|
+
file.binmode
|
|
1128
|
+
file.write(bytes)
|
|
1129
|
+
file.flush
|
|
1130
|
+
file.fsync
|
|
1131
|
+
end
|
|
1132
|
+
raise IOError, 'PIV export readback mismatch' unless File.binread(path) == bytes
|
|
1133
|
+
|
|
1134
|
+
bytes
|
|
1135
|
+
end
|
|
1136
|
+
|
|
1137
|
+
def enum_name(value, table)
|
|
1138
|
+
name = value.to_s.upcase.tr('-', '_')
|
|
1139
|
+
raise ArgumentError, 'Invalid PIV algorithm or policy' unless table.key?(name)
|
|
1140
|
+
|
|
1141
|
+
name
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
def parse_object_id(value)
|
|
1145
|
+
return value if value.is_a?(Integer) && (1..0xffffff).cover?(value)
|
|
1146
|
+
|
|
1147
|
+
name = value.to_s.upcase.tr('-', '_')
|
|
1148
|
+
return OBJECTS.fetch(name) if OBJECTS.key?(name)
|
|
1149
|
+
raise ArgumentError, 'Invalid PIV object identifier' unless value.is_a?(String) && value.match?(/\A(?:0x)?[0-9a-fA-F]{1,6}\z/)
|
|
1150
|
+
|
|
1151
|
+
number = value.to_i(16)
|
|
1152
|
+
raise ArgumentError, 'Invalid PIV object identifier' if number.zero?
|
|
1153
|
+
|
|
1154
|
+
number
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
def number_bytes(number, length = nil)
|
|
1158
|
+
bytes = [number.to_s(16).rjust(2, '0').then { |hex| hex.length.odd? ? "0#{hex}" : hex }].pack('H*')
|
|
1159
|
+
length ? bytes.rjust(length, "\x00".b) : bytes
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
def optional_object(id)
|
|
1163
|
+
read_object(id)
|
|
1164
|
+
rescue Error => e
|
|
1165
|
+
raise unless status_word(e) == 0x6a82
|
|
1166
|
+
|
|
1167
|
+
nil
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
def pivman
|
|
1171
|
+
bytes = optional_object(0x5fff00)
|
|
1172
|
+
bytes.nil? || bytes.empty? ? {} : Protocol.tlv_map(data: unpack(0x80, bytes))
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
def protected_pivman
|
|
1176
|
+
bytes = optional_object(0x5fc109)
|
|
1177
|
+
bytes.nil? || bytes.empty? ? {} : Protocol.tlv_map(data: unpack(0x88, bytes))
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
def container(tag, fields)
|
|
1181
|
+
tlv(tag, fields.map { |key, value| tlv(key, value) }.join.b)
|
|
1182
|
+
end
|
|
1183
|
+
|
|
1184
|
+
def admin_authenticate
|
|
1185
|
+
return authenticate unless @opts[:use_protected_key] == true
|
|
1186
|
+
|
|
1187
|
+
confirm!
|
|
1188
|
+
pin = secret(:pin)
|
|
1189
|
+
pin_bytes(pin)
|
|
1190
|
+
data = pivman
|
|
1191
|
+
verify_pin(pin)
|
|
1192
|
+
key = if data.key?(0x82)
|
|
1193
|
+
OpenSSL::PKCS5.pbkdf2_hmac(pin, data.fetch(0x82), 10_000, 24, 'SHA1')
|
|
1194
|
+
elsif data.fetch(0x81, "\x00".b).getbyte(0).anybits?(2)
|
|
1195
|
+
protected_pivman.fetch(0x89) { raise ResponseError, 'Protected management key is absent' }
|
|
1196
|
+
else
|
|
1197
|
+
raise ArgumentError, 'No protected management key is configured'
|
|
1198
|
+
end
|
|
1199
|
+
authenticate(key)
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
def set_management_key(name, key, touch: false)
|
|
1203
|
+
apdu(0xff, p1: 0xff, p2: touch ? 0xfe : 0xff, data: [MANAGEMENT.fetch(name)[0]].pack('C') + tlv(0x9b, key))
|
|
1204
|
+
@management_algorithm = name
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1207
|
+
def chuid
|
|
1208
|
+
# Non-federal issuer FASC-N from the SP 800-73 CHUID layout.
|
|
1209
|
+
digits = %w[9999 9999 999999 0 1 0000000000 3 0000 1]
|
|
1210
|
+
pieces = digits.map do |value|
|
|
1211
|
+
value.chars.map do |char|
|
|
1212
|
+
bits = char.to_i.to_s(2).rjust(4, '0').reverse
|
|
1213
|
+
bits + ((bits.count('1') + 1) % 2).to_s
|
|
1214
|
+
end.join
|
|
1215
|
+
end
|
|
1216
|
+
bits = "11010#{pieces.first(5).join('10110')}10110#{pieces.drop(5).join}11111"
|
|
1217
|
+
lrc = bits.scan(/.{5}/).map { |part| part.to_i(2) }.reduce(0, :^)
|
|
1218
|
+
fascn = [bits + lrc.to_s(2).rjust(5, '0')].pack('B*')
|
|
1219
|
+
guid = [SecureRandom.uuid.delete('-')].pack('H*')
|
|
1220
|
+
tlv(0x30, fascn) + tlv(0x34, guid) + tlv(0x35, '20300101') + tlv(0x3e) + tlv(0xfe)
|
|
1221
|
+
end
|
|
1222
|
+
|
|
1223
|
+
def ccc
|
|
1224
|
+
tlv(0xf0, ['a000000116ff02'].pack('H*') + SecureRandom.random_bytes(14)) + tlv(0xf1, "\x21".b) + tlv(0xf2, "\x21".b) + tlv(0xf3) + tlv(0xf4, "\x00".b) + tlv(0xf5, "\x10".b) + [0xf6, 0xf7, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe].map { |tag| tlv(tag) }.join.b
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1227
|
+
def field(fields, tag, size = nil)
|
|
1228
|
+
value = fields[tag]
|
|
1229
|
+
raise ResponseError, 'Missing or malformed PIV response field' unless value.is_a?(String) && (size.nil? || value.bytesize == size)
|
|
1230
|
+
|
|
1231
|
+
value
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
def pin_bytes(value)
|
|
1235
|
+
raise ArgumentError, 'PIN/PUK must contain 6 to 8 bytes' unless value.is_a?(String) && (6..8).cover?(value.bytesize)
|
|
1236
|
+
|
|
1237
|
+
value.b.ljust(8, "\xff".b)
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
def integer(value, range)
|
|
1241
|
+
raise ArgumentError, 'Invalid integer argument' unless value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A[0-9]+\z/))
|
|
1242
|
+
|
|
1243
|
+
number = value.to_i
|
|
1244
|
+
raise ArgumentError, 'Integer argument out of range' unless range.cover?(number)
|
|
1245
|
+
|
|
1246
|
+
number
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
def management_algorithm
|
|
1250
|
+
@management_algorithm ||= version_at_least?([5, 3, 0]) ? metadata(0x9b).fetch(:algorithm) : 'TDES'
|
|
1251
|
+
end
|
|
1252
|
+
|
|
1253
|
+
def version_at_least?(minimum)
|
|
1254
|
+
Protocol.version_at_least?(version: version, minimum: minimum)
|
|
1255
|
+
end
|
|
1256
|
+
|
|
1257
|
+
def require_version(minimum)
|
|
1258
|
+
raise UnsupportedError, "PIV operation requires firmware #{minimum.join('.')} or later" unless version_at_least?(minimum)
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
def tlv(tag, value = ''.b)
|
|
1262
|
+
Protocol.tlv(tag: tag, value: value)
|
|
1263
|
+
end
|
|
1264
|
+
|
|
1265
|
+
def unpack(tag, value)
|
|
1266
|
+
fields = Protocol.untlv(data: value)
|
|
1267
|
+
raise ResponseError, 'Malformed PIV response container' unless fields.length == 1 && fields.first[:tag] == tag
|
|
1268
|
+
|
|
1269
|
+
fields.first.fetch(:value)
|
|
1270
|
+
end
|
|
1271
|
+
|
|
1272
|
+
def status_word(error)
|
|
1273
|
+
return error.status if error.respond_to?(:status)
|
|
1274
|
+
return error.sw if error.respond_to?(:sw)
|
|
1275
|
+
return error.status_word if error.respond_to?(:status_word)
|
|
1276
|
+
|
|
1277
|
+
nil
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1280
|
+
def reference_apdu(ins, **)
|
|
1281
|
+
apdu(ins, **)
|
|
1282
|
+
rescue Error => e
|
|
1283
|
+
sw = status_word(e)
|
|
1284
|
+
retries = if sw == 0x6983
|
|
1285
|
+
0
|
|
1286
|
+
elsif sw && sw & 0xfff0 == 0x63c0
|
|
1287
|
+
sw & 15
|
|
1288
|
+
elsif sw && sw & 0xff00 == 0x6300
|
|
1289
|
+
sw & 255
|
|
1290
|
+
end
|
|
1291
|
+
raise PinError.new(retries), cause: nil unless retries.nil?
|
|
1292
|
+
|
|
1293
|
+
raise
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1296
|
+
def confirm!
|
|
1297
|
+
Protocol.require_confirm(@opts)
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
def secret(name)
|
|
1301
|
+
Protocol.secret(value: @opts[name], secret_provider: @opts[:secret_provider], name: name)
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
def apdu(ins, p1: 0, p2: 0, data: ''.b) # rubocop:disable Naming/MethodParameterName
|
|
1305
|
+
unless @selected
|
|
1306
|
+
Protocol.select(connection: @connection, aid: AID, timeout: @opts.fetch(:timeout, 30))
|
|
1307
|
+
@selected = true
|
|
1308
|
+
end
|
|
1309
|
+
Protocol.apdu(connection: @connection, ins: ins, p1: p1, p2: p2, data: data, timeout: @opts.fetch(:timeout, 30))
|
|
1310
|
+
end
|
|
1311
|
+
end
|
|
1312
|
+
end
|
|
1313
|
+
end
|
|
1314
|
+
end
|
|
1315
|
+
end
|