Compare commits

..

329 Commits

Author SHA1 Message Date
8a50024285 feat: v1.13.21 2025-10-18 14:29:51 +08:00
07e1671867 feat: v1.13.20, fix incomp with future compiler 2025-09-27 16:56:05 +08:00
4537b6ece9 feat: v1.13.19 2025-09-27 16:25:03 +08:00
f96144f450 feat: update dependencies 2025-09-27 12:12:57 +08:00
1d49b7c1c1 feat: external_ecdh for ML-KEM 2025-09-27 12:11:03 +08:00
10c38cda8a feat: external_public_key supports ML-KEM 2025-09-27 12:02:26 +08:00
9f544e3cb7 feat: ml-kem generate 2025-09-27 11:32:42 +08:00
3d29fe6a6d feat: add generate_ml_kem_768, pending implement ML-KEM later 2025-09-27 00:49:37 +08:00
1d23dba248 feat: updates 2025-07-24 22:43:47 +08:00
6f556cc2d6 feat: v1.13.18, external_sign supports --message-type 2025-07-19 13:58:51 +08:00
33a6661c3f feat: v1.13.17, claim support mutiple times 2025-07-14 22:19:10 +08:00
421f2e2ffe feat: update dependencies 2025-06-30 00:16:02 +08:00
3647515321 feat: v1.13.16, update dependencies 2025-06-30 00:05:01 +08:00
6bd4d0ba57 feat: 1.13.15, make clippy happy 2025-06-29 23:23:19 +08:00
d272904357 feat: v1.13.14 2025-06-29 00:11:10 +08:00
0bc671be7b feat: v1.13.13 2025-06-09 22:42:12 +08:00
a698a852fd feat: update ssh agent 2025-05-27 00:04:26 +08:00
febdf659cd feat: updates ssh agent 2025-05-26 00:16:39 +08:00
62110ed7fb feat: update ssh agent 2025-05-26 00:07:40 +08:00
149650bf15 feat: updates ssh agent 2025-05-26 00:06:11 +08:00
5b3e0bc8cb feat: update ssh agent 2025-05-25 23:17:46 +08:00
f870c07387 feat: ssh agent, not works yet 2025-05-25 22:39:02 +08:00
ecf034376d feat: update ssh agent 2025-05-25 20:56:20 +08:00
488db38387 feat: add ssh-agent-gpg 2025-05-25 20:23:15 +08:00
f6b9671872 feat: add ssh-agent-gpg 2025-05-25 20:22:57 +08:00
87e51cc7e4 feat: 1.13.12, support external command 2025-05-24 10:31:13 +08:00
bb8d804505 feat: openwebstandard.org/rfc1 2025-05-23 07:04:21 +08:00
d104d4405e feat: update readme 2025-05-22 23:57:08 +08:00
f5a15ca0ae feat: update readme 2025-05-22 23:56:54 +08:00
28c2f096f7 feat: update readme 2025-05-22 23:56:22 +08:00
9926dbf09d feat: update readme 2025-05-22 23:55:43 +08:00
b23f4a3a69 feat: v1.13.10 2025-05-17 22:08:46 +08:00
d42bfd4bcc feat: v1.13.9 2025-05-17 22:03:27 +08:00
f74820903a feat: 1.13.8 2025-05-17 21:55:58 +08:00
21b5cc8221 feat: v1.13.7, add subcommand yubikey 2025-05-15 22:56:33 +08:00
7fa6aa1146 feat: v1.13.6 2025-05-14 00:30:19 +08:00
b4beaa3a75 feat: v1.13.5 2025-05-14 00:08:56 +08:00
fb026c9f21 feat: v1.13.4 2025-05-12 23:38:45 +08:00
4431bff9e6 feat: update zeroize 2025-05-10 00:17:32 +08:00
58f665823d feat: v1.13.3 2025-05-09 23:47:10 +08:00
fdf02bc976 feat: v1.13.2 2025-05-09 23:33:11 +08:00
8e4cf5cec8 feat: se revover 2025-05-08 22:40:17 +08:00
0b9ec436ba feat: v1.13.1 2025-05-08 22:29:19 +08:00
a1ae0ff4dc feat: v1.13.0 2025-05-07 23:54:20 +08:00
06d2da4ddf feat: updates 2025-05-07 22:16:45 +08:00
0513dd2398 feat: update hmac decrypt 2025-05-06 23:52:07 +08:00
d6ecdb5ed4 feat: v1.12.10 2025-05-06 23:47:18 +08:00
81f7a6d77e feat: updates hmac enc 2025-05-06 23:19:40 +08:00
63fabc6054 feat: v1.12.9 2025-05-06 22:35:03 +08:00
57c3ec57df feat: pbe encryption 2025-05-05 23:35:41 +08:00
c0ea3b773d feat: use nonce, salt 16 bytes 2025-05-05 23:28:58 +08:00
67568f8f15 feat: v1.12.7 2025-05-05 23:22:16 +08:00
9435b287c8 feat: key type rs3072, rsa4096 2025-05-05 13:00:45 +08:00
96927e0dab feat: rsa3072,4096 2025-05-05 12:25:38 +08:00
8894a2156a feat: v1.12.6 2025-05-05 11:22:28 +08:00
e52e42d48c feat: v1.12.5 2025-05-02 13:13:32 +08:00
a3541e7b68 feat: v1.12.4 2025-05-02 12:43:26 +08:00
d7f52530df feat: update ecdsautil 2025-05-02 00:01:11 +08:00
3dae02e090 feat: v1.12.3 2025-05-01 23:38:36 +08:00
86489c5d29 feat: fix YubiKey typo 2025-05-01 22:41:09 +08:00
1773186dbf feat: update parse_ecdsa_private_key 2025-05-01 21:55:14 +08:00
cec27e0f88 feat: v1.12.2 2025-05-01 21:46:04 +08:00
9a749b63eb feat: v1.12.1 2025-05-01 10:30:24 +08:00
fcb10f5efa feat: update external_sign 2025-05-01 00:37:12 +08:00
5329108380 feat: update external_sign 2025-05-01 00:35:04 +08:00
b8f0be2023 feat: external_sign secure enclave only supports ES256 2025-05-01 00:29:01 +08:00
0ac9300262 feat: fix external_sign rsa1024 2025-05-01 00:27:27 +08:00
c270c2e369 feat: v1.12.0 2025-05-01 00:22:42 +08:00
3af863762f feat: update crates 2025-04-30 01:23:16 +08:00
21676451fd feat: v1.11.17 2025-04-30 01:19:21 +08:00
4dca8e0146 feat: updates 2025-03-31 00:40:35 +08:00
dc56f2df77 feat: updates 2025-03-31 00:32:29 +08:00
4dac890200 feat: udpates 2025-03-31 00:26:03 +08:00
e7b20abd6d feat: v1.11.16 2025-03-31 00:22:20 +08:00
492c434f62 feat: v1.11.15, add convert-jwk-to-pem 2025-03-30 23:13:30 +08:00
fe30f538ba feat: v1.11.14, add convert-pem-to-jwk 2025-03-29 17:09:13 +08:00
e6409174b6 feat: v1.11.13 2025-03-29 16:38:26 +08:00
bb02c7c823 feat: bse64 2025-03-29 16:31:08 +08:00
e9388eb164 feat: udptes 2025-03-29 16:16:38 +08:00
a6bff6d31c feat: updates 2025-03-29 00:04:25 +08:00
6d3298549e feat: updates 2025-03-28 23:28:20 +08:00
417e3f6a49 feat: v1.11.12, optimize no-pin 2025-03-28 23:25:53 +08:00
3a40d7f0ad feat: updates 2025-03-28 07:35:53 +08:00
1be5754ed1 feat: 1.11.10, piv slot support f9 2025-03-27 23:58:55 +08:00
7ec3a705cf feat: v1.11.9 2025-03-27 07:29:32 +08:00
e2fa3bba9f feat: update justfile 2025-03-26 23:41:59 +08:00
070161c056 feat: v1.11.8, add keypair-keychain-import/export 2025-03-26 23:37:21 +08:00
3fb43403aa feat: udptes 2025-03-26 23:10:09 +08:00
c2b3a779c8 feat: renames 2025-03-26 23:05:48 +08:00
755d61fa86 feat: v1.11.7, support keychain store 2025-03-26 22:51:52 +08:00
af20f4c4a0 feat: v1.11.6, rename file-sign, file-verify 2025-03-26 07:40:54 +08:00
1582f76cae feat: 1.11.5 2025-03-26 07:35:38 +08:00
3848b65ff1 feat: v1.11.4, support hmac encrypt in generate-keypair 2025-03-26 07:18:47 +08:00
d4fce3f4fc feat: v1.11.3 2025-03-24 23:28:24 +08:00
8aeb47c66f feat: v1.11.3 2025-03-24 23:25:31 +08:00
d04038ccd9 feat: v1.11.3 2025-03-24 23:21:47 +08:00
aee2f8d5d3 feat: v1.11.3 2025-03-24 23:12:42 +08:00
25e80661bb feat: update Cargo.toml 2025-03-24 07:35:36 +08:00
c5e35fd941 feat: v1.11.2 2025-03-24 07:31:29 +08:00
468701b3ff feat: v1.11.1 2025-03-24 01:14:42 +08:00
4a48e932d4 Merge pull request 'feat: v1.11.0, remove swiftc dependency' (#10) from use-swift-se-tool into master
Reviewed-on: #10
2025-03-24 00:38:11 +08:00
8b6056db34 feat: v1.11.0, remove swiftc dependency 2025-03-24 00:05:30 +08:00
ea0b091414 feat: 1.10.21, add hmac-encrypt, hmac-decrypt 2025-03-23 18:23:36 +08:00
31e710d779 feat: v1.10.20, add sign-jwt-soft 2025-03-22 23:14:34 +08:00
0894366331 feat: v1.10.19, add generate-keypair, supports p256 and p384 2025-03-22 21:16:20 +08:00
7d2cf85f89 feat: v1.10.18, add parse-ecdsa-signature 2025-03-22 10:04:39 +08:00
04247bb846 feat: fix sign jwt 2025-03-22 09:33:07 +08:00
2f3bf2eec7 feat: update crates 2025-02-08 23:08:07 +08:00
c48493f892 feat: v1.10.16 2025-01-12 10:12:14 +08:00
d27f0d5f83 feat: v1.10.15, add debuging 2025-01-11 22:31:50 +08:00
73b5cc1f5f check piv pin policy 2024-12-29 12:22:24 +08:00
5a1942e150 feat: v1.10.14, add --no-pin for many subcommands 2024-12-29 11:54:25 +08:00
ef1f637c83 feat: v1.10.13, ec-sign support --no-pin 2024-12-29 01:53:38 +08:00
a19a7822c4 feat: update readme 2024-12-16 23:16:22 +08:00
3653d2f5f9 Update README.md 2024-12-16 23:14:46 +08:00
4824e2329d feat: updates 2024-12-16 22:47:54 +08:00
61804ab029 feat: v1.10.12, se-recover, se-ecdh support public key point 2024-12-16 22:42:34 +08:00
cd6e34ffd8 feat: v1.10.11, ec-verify 2024-12-15 20:52:49 +08:00
4406cf6d2e feat: updates 2024-12-15 14:14:06 +08:00
d686bbe767 feat: updates 2024-12-15 13:42:09 +08:00
a12fdfd27f feat: updates 2024-12-15 13:27:27 +08:00
776c7eb245 feat: updates 2024-12-15 13:25:51 +08:00
883e41b2cf feat: updates 2024-12-15 13:19:23 +08:00
df6e7eee55 feat: v1.10.10, se-ecdh and se-ecsign 2024-12-15 12:55:44 +08:00
25b0efd272 feat: update se-generate 2024-12-15 01:10:48 +08:00
0cda981409 feat: revert readme 2024-12-15 01:00:00 +08:00
bf9f228967 feat: v1.10.9, add se supprot 2024-12-15 00:56:51 +08:00
0fec0c25e7 feat: update dependencies 2024-11-09 23:45:53 +08:00
81940853f9 feat: update dependencies 2024-09-14 23:46:20 +08:00
a5ea22f622 feat: v1.10.8, pgp-age-address support --json 2024-09-14 23:44:59 +08:00
8031fca7e7 feat: v1.10.7, add --json for piv-summary 2024-09-14 23:19:39 +08:00
0cfe26e238 feat: v1.10.6, updates feature with-sequoia-openpgp 2024-09-12 01:01:01 +08:00
81149f6aa2 feat: v1.10.5, improve list json outputs 2024-09-12 00:54:56 +08:00
582ea8a9bb feat: update readme 2024-09-07 19:36:43 +08:00
a61782e3fe feat: v0.10.4, add feature with-sequoia-openpgp 2024-09-07 19:32:26 +08:00
8ec07eb01a feat: v1.10.3, add ssh-parse 2024-09-07 09:00:56 +08:00
135631df90 feat: v1.10.2, improve ssh-piv-cert 2024-09-07 08:34:50 +08:00
6f2d4d2369 feat: update readme 2024-09-07 01:36:29 +08:00
50e40522e9 feat: udpate justfile 2024-09-07 01:33:57 +08:00
7be3e51da0 feat: v1.10.1, update dependencies, ssh-piv-cert support multiple principals 2024-09-07 01:29:25 +08:00
4fdca28679 feat: update readme 2024-09-07 01:02:06 +08:00
059728137b feat: update readme 2024-09-07 01:01:16 +08:00
61854b7abf feat: v1.10.0, SSH CA works 2024-09-07 00:43:30 +08:00
cbf127a297 feat: v1.9.13, support ssh-piv-cert, but not works 2024-09-06 23:06:21 +08:00
1d2a00a0c8 feat: update comments 2024-08-22 22:59:04 +08:00
ddc3ff98a2 feat: v1.9.12, add ssh-pub-key support 2024-08-22 22:53:49 +08:00
87d62eae4a feat: update dependencies 2024-07-28 21:11:22 +08:00
f47a4fc90a feat: v1.9.11, piv meta outputs ssh public key 2024-07-04 22:20:55 +08:00
7287193e49 feat: update dependencies 2024-07-04 22:01:24 +08:00
61f5d8c909 feat: 1.9.9, ssh-piv-sign works now 2024-07-04 21:58:10 +08:00
9fa90827aa feat: add ssh-piv-sign, but it not works right 2024-07-04 00:10:45 +08:00
fed67019aa feat: add ssh-piv-sign, but it not works right 2024-07-03 23:58:34 +08:00
8a4d030d82 feat: add ssh-parse-sign 2024-07-03 00:58:30 +08:00
14585af7c6 feat: 1.9.7, add pinentry support 2024-06-30 23:34:53 +08:00
6e32dc341c feat: update dependencies 2024-06-16 10:47:14 +08:00
7d5af78078 feat: v1.9.6 2024-06-16 10:20:23 +08:00
d4b9b852c1 feat: v1.9.5 2024-06-16 01:05:15 +08:00
32ab2d3d6d feat: v1.9.4 2024-06-16 00:07:50 +08:00
320664bfa0 feat: piv decrypt 2024-06-10 19:51:22 +08:00
9caee95711 feat: v1.9.3, pivutil::get_slot_id support 9b(Management) 2024-06-10 19:35:15 +08:00
8ba43f8c6b feat: ecdh support p384 2024-06-09 16:52:53 +08:00
639f87576c feat: card cli 2024-06-08 23:11:40 +08:00
9404d8e5d7 feat: filename can be none 2024-04-21 23:38:34 +08:00
0dd5aa59ab feat: v1.9.1, optimize sign/verify file 2024-04-21 23:24:46 +08:00
266cf01930 feat: add verify-file 2024-04-21 16:08:49 +08:00
b3d8c791c4 feat: fix clippy & update dependencies 2024-04-21 11:15:14 +08:00
68473e2f01 feat: claim types 2024-04-15 00:40:38 +08:00
e13503116a feat: v1.8.5, claims support jti, validity 2024-04-14 09:43:11 +08:00
44a0e3512e feat: add LICENSE.simple-sig 2023-11-26 16:05:25 +08:00
4c5f5b4e13 feat: update filename 2023-11-26 16:02:11 +08:00
bb10a24693 feat: change to schema 2023-11-26 15:41:20 +08:00
99d478def5 feat: updates 2023-11-26 15:35:42 +08:00
4eaf083e07 feat: updates 2023-11-26 15:29:26 +08:00
dd2e804ee6 feat: sign-file works 2023-11-26 12:26:47 +08:00
3b3ce7b623 feat: v1.8.4, add sign-file 2023-11-26 11:41:04 +08:00
8d163a5ec2 feat: optimize code 2023-11-20 22:40:18 +08:00
e9afbe528e feat: optimize code 2023-11-20 22:38:46 +08:00
5faf29020c feat: update Cargo.toml 2023-11-19 19:31:36 +08:00
ed140470ae feat: 1.8.3, optimize code 2023-11-19 19:30:19 +08:00
030ccd6cfb feat: v1.8.2, piv verify works for rsa 2023-11-19 18:02:40 +08:00
753e1b0c38 feat: update dependencies 2023-11-19 17:38:38 +08:00
ebc21e47c4 feat: v1.8.1, piv-sign supports any slot 2023-11-19 17:37:46 +08:00
4b442ff71a feat: fix clippy 2023-11-18 13:39:01 +08:00
cda80af731 v1.8.0, sign-jwt works 2023-11-18 13:27:08 +08:00
6fbf0943aa feat: works2 2023-11-17 23:30:34 +08:00
116abd276b feat: works 2023-11-17 22:57:10 +08:00
b17ab578d5 feat: update dependencies 2023-11-17 00:13:38 +08:00
f8bf21f549 feat: v1.7.11, pending add sign jwt 2023-11-17 00:11:50 +08:00
d5fd1ad57a feat: update dependencies 2023-11-04 23:23:19 +08:00
1fc9b0d1ba feat: add argsutil 2023-11-04 22:09:16 +08:00
7cfe51fa96 feat: ignore one clippy warn 2023-11-03 21:30:57 +08:00
2f0042e209 feat: v1.7.9, add card-cli piv-verify 2023-11-03 21:23:29 +08:00
66fdde9b72 feat: optimize code 2023-11-02 01:10:07 +08:00
c43c7011e7 feat: v1.7.8 2023-11-01 07:36:36 +08:00
4f015be358 feat: v1.7.8 2023-11-01 00:32:33 +08:00
aadb4b0530 feat: fix open-card-sign --algo 2023-10-14 00:18:54 +08:00
b5e65cc54d feat: update dependencies 2023-10-14 00:08:11 +08:00
09bb894242 feat: fix clippy 2023-10-14 00:05:11 +08:00
69529b1813 feat: v1.7.7, add pk_point_hex_compressed 2023-10-10 23:56:08 +08:00
7b79f6aa18 feat: update readme 2023-10-06 18:46:56 +08:00
362a4ca81b feat: pgp-age-address 2023-10-06 11:07:47 +08:00
a544daae67 feat: update readme, add age-address 2023-10-06 10:51:56 +08:00
4b7743a68b feat: v1.7.6, add subcommand age-address 2023-10-06 10:46:45 +08:00
78824f279b feat: update pgp 2023-10-05 23:15:42 +08:00
81c20a6872 feat: update pgpcarddecrypt 2023-10-05 23:08:27 +08:00
d90c2ebfee feat: update pgpcardlist 2023-10-05 22:01:42 +08:00
1d0fe631a2 feat: piv-summary 2023-10-05 21:56:48 +08:00
506f34fe79 feat: update piv-summary 2023-10-05 21:44:38 +08:00
c71b6bef89 feat: piv summary 2023-10-05 21:40:10 +08:00
1a5173bba9 feat: update get_slot_id 2023-10-05 21:16:03 +08:00
d6ffc0fbd0 feat: udpate dependencies 2023-10-05 20:26:11 +08:00
fd2f3213fe feat: v1.7.5, supports sign algo 2023-10-05 20:20:32 +08:00
1c08f1c264 feat: v1.7.4 2023-10-05 17:24:57 +08:00
08581ecfa7 v0.7.3 2023-10-05 16:27:45 +08:00
c21a4082dc feat: update dependencies, fix u2f compile failure 2023-10-01 22:05:45 +08:00
1018d408b4 feat: v1.7.1 2023-10-01 15:05:49 +08:00
0679f73dae feat: v1.7.2 2023-10-01 14:58:05 +08:00
746fdbb53d feat: table style 2023-08-26 14:21:19 +08:00
8e2dce9b39 feat: v1.7.1 2023-08-26 14:16:09 +08:00
4b6352759b feat: v1.7.0 2023-08-26 13:46:37 +08:00
e6bb1cf292 feat: update clap 2023-08-26 12:27:17 +08:00
ffd0e6233f feat: update x509-parser version 2023-08-26 12:25:04 +08:00
1698e2d9f8 feat: update base64 version 2023-08-26 12:02:45 +08:00
2ada122834 feat: udpate dependencies 2023-08-26 11:41:46 +08:00
7f5a5a7d3c feat: update yubikey to v0.8 2023-08-20 15:24:28 +08:00
9fb0da7d33 feat: v1.5.8, opt piv-meta 2023-05-17 00:53:17 +08:00
0ad8c12c38 feat: v1.5.7, update piv 2023-05-14 23:26:13 +08:00
0af2940cd0 feat: rename pivsign 2023-05-14 22:29:23 +08:00
6c600ae4b0 feat: update dependencies 2023-05-13 22:31:46 +08:00
709f83ce77 feat: v1.5.6, piv-meta supports 9a,9c,9d,9e 2023-05-13 00:45:45 +08:00
d3c666de4c feat: update dependencies 2023-05-13 00:16:23 +08:00
330759fcb9 feat: update readme 2023-04-22 17:08:22 +08:00
cdc12e38f8 feat: update dependencies 2023-04-22 15:23:37 +08:00
19a2d58bdb feat: v1.5.4, list 2023-04-22 12:31:17 +08:00
8177586645 feat: update readme 2023-04-22 11:21:53 +08:00
689cda9446 feat: v1.5.3, add -x, -i for piv-ecsign 2023-04-22 10:45:04 +08:00
a5bb172bf5 feat: update readme 2023-04-22 10:39:32 +08:00
599cba9bf4 feat: update readme 2023-04-22 10:35:36 +08:00
e08ea3995c feat: update readme 2023-04-22 10:34:35 +08:00
aeb8279bfa feat: update readme 2023-04-22 10:33:24 +08:00
39d159d682 feat: update readme 2023-04-22 10:31:34 +08:00
b275d1b58b feat: update readme 2023-04-22 10:30:31 +08:00
a2e545e89a feat: v1.5.2, update crates, improve piv-ecsign 2023-04-21 00:01:53 +08:00
e08282870e feat: v1.5.1, add piv public key outputs 2023-04-19 23:36:53 +08:00
e42a73b2c9 feat: v1.5.0, update version 2023-04-19 00:18:54 +08:00
b39edfa00b feat: v1.5.0, add piv-ecsign subcommand 2023-04-19 00:01:12 +08:00
7886a0755f feat: update readme 2023-03-31 00:55:12 +08:00
f2c3dbdcd5 feat: update readme 2023-03-25 21:15:30 +08:00
269eaa9304 feat: add examples hmac_sha1.rs 2023-03-25 20:31:52 +08:00
a28f4ddced feat: update readme 2023-03-25 19:15:59 +08:00
426bc46a09 feat: update piv meta 2023-03-25 18:52:21 +08:00
7348e6d51f feat: v1.4.4, add piv-meta 2023-03-16 00:03:19 +08:00
90d4723f86 feat: update piv-ecdh 2023-03-15 08:46:41 +08:00
118a797d7c feat: update piv-ecdh 2023-03-15 00:29:59 +08:00
9955341c81 feat: v1.4.3, add piv-ecdh subcommand 2023-03-15 00:25:16 +08:00
d9bb0ce106 feat: v1.4.2 2023-03-14 23:19:53 +08:00
8a19502400 feat: v1.4.1 2023-03-14 23:09:55 +08:00
60847e8edf feat: update version 0.4.0 2023-03-14 22:51:47 +08:00
1d3745f44c feat: update list subcommand 2023-03-14 22:48:05 +08:00
6437b955d7 feat: update crates 2023-03-14 08:58:05 +08:00
19c84448fb feat: update crates 2023-02-10 00:42:41 +08:00
733cd7eb67 feat: update yubikey version 2023-02-10 00:37:58 +08:00
3c883f976e feat: update cates version 2022-10-04 00:09:36 +08:00
7902ec5655 feat: update cates version 2022-10-03 23:55:59 +08:00
623f8fb585 feat: v1.3.4 return error codes 2022-08-05 23:22:17 +08:00
5aadb3f082 feat: v1.3.3, remove dependency lazy_static 2022-05-01 13:56:56 +08:00
57df2177cc feat: v1.3.3, remove dependency lazy_static 2022-05-01 13:53:06 +08:00
07ff1d8187 chore: update generate_ssh_string 2022-04-30 19:40:50 +08:00
a95fdb5d09 feat: v1.3.2, piv 2022-04-30 19:28:36 +08:00
5a09200966 chore: update cargo lock 2022-04-28 23:58:33 +08:00
30b14346e3 chore: add sekey to readme 2022-04-28 23:42:37 +08:00
7b847ea569 feat: v1.3.1, add --pgp-auth 2022-04-28 23:03:45 +08:00
74482ae918 feat: v1.3.0, ssh agent works 2022-04-28 00:24:39 +08:00
033942c2b6 feat: RequestIdentities works 2022-04-27 00:06:01 +08:00
003ed28b37 feat: add --pgp --piv check 2022-04-26 00:48:55 +08:00
d7ae185264 feat: add ssh agnet(not yet work) 2022-04-26 00:44:34 +08:00
bae59b745e feat: update example ssh agent 2022-04-25 00:35:52 +08:00
9b35e28819 feat: update example ssh agent 2022-04-25 00:34:48 +08:00
962750c670 feat: add example ssh agent 2022-04-24 01:01:06 +08:00
d597a1722c feat: add example ssh agent 2022-04-24 01:00:58 +08:00
6be02db2a9 feat: v1.2.1, pgp-card-make print-*-keys 2022-04-14 23:26:49 +08:00
b2e83b1c71 feat: v1.2.1, pgp-card-make print-*-keys 2022-04-14 23:26:22 +08:00
ef100c2921 feat: v1.2.1, pgp-card-make print-*-keys 2022-04-14 23:24:38 +08:00
d9a5b5c831 feat: v1.2.0, add piv-decrypt 2022-04-14 21:27:43 +08:00
5ad16984f2 chore: new line 2022-04-13 00:40:56 +08:00
041777d78c chore: rm unused code 2022-04-13 00:29:56 +08:00
9f7d9773f1 chore: digest_bytes 2022-04-13 00:27:32 +08:00
98ef85f9e6 chore: rm comments 2022-04-13 00:22:53 +08:00
295ceef6df chore: rm comments 2022-04-13 00:22:04 +08:00
7c486dfa82 feat: v1.1.17, decrypt hex 2022-04-13 00:19:45 +08:00
9595c6cc6d chore: rm comments 2022-04-13 00:03:07 +08:00
9dafa55afa feat: decrypt print raw data 2022-04-12 23:37:48 +08:00
4a90258b6b chore: re-org u2f code 2022-04-12 23:06:59 +08:00
20754c119e feat: v1.1.16 2022-04-12 22:45:28 +08:00
2f0d4d6e8a feat: v1.1.15, supports u2f-sign --json 2022-04-12 00:29:10 +08:00
3cf62cb687 chore: make clippy happy 2022-04-10 22:31:35 +08:00
be2e014638 feat: v1.1.14 2022-04-10 22:17:42 +08:00
088910fb8d feat: v1.1.13, u2f-register 2022-04-10 21:35:50 +08:00
af8a9c3047 feat: v1.1.12, piv sign works 2022-04-10 18:17:10 +08:00
51ba3d8500 feat: v1.1.11, piv sign works, add piv generate 2022-04-09 19:57:32 +08:00
27dca7af84 feat: v1.1.9, add rsa-verify sub command 2022-04-09 17:54:13 +08:00
b4ea907fed chore: reorg 2022-04-09 16:19:45 +08:00
77771397a6 feat: add parse_padding, padding_to_string 2022-04-09 11:31:19 +08:00
cf1e307a06 feat: v1.1.8, add rsa-decrypt sub command 2022-04-09 09:48:38 +08:00
9e6a1f3ab5 feat: v1.1.7, add rsa-encrypt sub command 2022-04-09 09:34:24 +08:00
d97ac5398f chore: update versions 2022-04-09 00:30:42 +08:00
c7560e24f3 feat: v1.1.6, pgp card make 2022-04-09 00:26:52 +08:00
ebce55e76c feat: pgp card make 2022-04-06 00:34:25 +08:00
e841ba3331 feat: pgp card make 2022-04-06 00:33:40 +08:00
02a9e72a8a feat: pgp card make 2022-04-05 23:08:55 +08:00
ce0351a721 feat: add rsa 2022-04-05 20:42:05 +08:00
2a34c9f905 feat: pgp-card-list 2022-04-04 16:28:00 +08:00
a67e8dfe4b feat: update u2f 2022-04-04 16:09:10 +08:00
8d988f5427 feat: to_pem -> bytes_to_pem 2022-04-04 16:05:55 +08:00
d90223a8ca feat: byte_to_pem 2022-04-04 15:59:35 +08:00
c9ab9820a8 feat: v1.1.5, piv display public key pem 2022-04-04 15:42:55 +08:00
359db9eaf8 feat: pkiutil 2022-04-03 21:46:27 +08:00
779f76f3ec feat: v1.1.4 add pkiutil 2022-04-03 21:37:55 +08:00
6f4f3f74f1 chroe: readme 2022-04-03 21:16:38 +08:00
d980b2b3fb feat: v1.1.3, pgp-card-list add public key sha256 2022-04-03 21:11:42 +08:00
fc28ed40e3 feat: add example - rsa_encrypt 2022-04-03 18:52:42 +08:00
83d25d6d97 feat: pgp card list add algo info 2022-04-03 17:48:55 +08:00
5c093cd6c8 chore: update versions 2022-04-03 17:25:51 +08:00
1c3b9b3e29 chore: update versions 2022-04-03 17:19:10 +08:00
c8dc5703ab feat: v1.1.2 make clippy happy 2022-04-03 17:03:31 +08:00
97a29546bd feat: v1.1.1, supports file in for pgp-card-sign 2022-04-03 16:36:28 +08:00
8668479b4e feat: pgp-card-list 2022-04-03 13:12:39 +08:00
9e89152836 Merge pull request 'v1.1.0, update openpgp-card versions' (#3) from update-open-pgp-version into master
Reviewed-on: #3
2022-04-03 12:58:59 +08:00
e34d02e622 v1.1.0, update openpgp-card versions 2022-04-03 12:57:05 +08:00
99 changed files with 13757 additions and 1668 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,5 @@
__external_gitignore/
__*.pem
test.txt test.txt
enc.txt enc.txt
enc_key.pem enc_key.pem
@@ -24,6 +26,7 @@ target/
# Icon must end with two \r # Icon must end with two \r
Icon Icon
# Thumbnails # Thumbnails
._* ._*

4761
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,76 @@
[package] [package]
name = "card-cli" name = "card-cli"
version = "1.0.2" version = "1.13.21"
authors = ["Hatter Jiang <jht5945@gmail.com>"] authors = ["Hatter Jiang <jht5945@gmail.com>"]
edition = "2018" edition = "2018"
description = "FIDO(U2F, WebAuthn), YubiKey, OpenPGP command line tool"
license = "MIT OR Apache-2.0"
repository = "https://git.hatter.ink/hatter/card-cli"
[features]
default = ["with-sequoia-openpgp"]
with-sequoia-openpgp = ["sequoia-openpgp", "openpgp-card-sequoia"]
[dependencies] [dependencies]
authenticator = "0.3" authenticator = "0.3"
clap = "2.33" clap = "2.0"
rust_util = { version = "0.6", features = ["use_clap"] } rust_util = { version = "0.6", features = ["use_clap"] }
sha1 = "0.6.0" digest = "0.10"
sha2 = "0.9" sha1 = "0.10"
sha2 = "0.10"
rand = "0.8" rand = "0.8"
base64 = "0.13" base64 = "0.22"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
hex = "0.4" hex = "0.4"
u2f = "0.2" openpgp-card = "0.3"
openpgp-card = "0.0.1" openpgp-card-pcsc = "0.3"
sequoia-openpgp = "1.3.0" openpgp-card-sequoia = { version = "0.2", optional = true }
chrono = "0.4.19" sequoia-openpgp = { version = "2.0", optional = true }
simpledateformat = "0.1.2" chrono = "0.4"
ring = "0.16.20" simpledateformat = "0.1"
openssl = "0.10.35" ring = "0.17"
pem = "0.8.3" openssl = "0.10"
digest = "0.9.0" pem = "3.0"
yubikey = "0.5" yubikey = { version = "0.8", features = ["untested"] }
yubico_manager = "0.9" yubico_manager = "0.9"
x509 = "0.2" x509 = "0.2"
x509-parser = "0.9" x509-parser = { version = "0.17", features = ["verify"] }
ssh-agent = { version = "0.2", features = ["agent"] }
p256 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
p384 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
p521 = { version = "0.13", features = ["pem", "ecdh", "ecdsa", "jwk"] }
spki = { version = "0.7", features = ["pem"] }
tabled = "0.20"
env_logger = "0.11"
bech32 = "0.9"
ecdsa = { version = "0.16", features = ["verifying", "spki", "pem", "der"] }
jwt = "0.16"
reqwest = { version = "0.12", features = ["blocking"] }
pinentry = "0.6"
rpassword = "7.3"
secrecy = "0.10"
der-parser = "10.0"
sshcerts-hatter-fork = "0.14.1"
regex = "1.11"
aes-gcm-stream = "0.2"
swift-secure-enclave-tool-rs = "1.0"
u2f-hatter-fork = "0.2"
security-framework = { version = "3.0", features = ["OSX_10_15"] }
rsa = "0.9"
which = "8.0"
percent-encoding = "2.3.1"
external-command-rs = "0.1.1"
ssh-agent-lib = { version = "0.5.1" }
ssh-key = { version = "0.6", features = ["ecdsa", "alloc"] }
tokio = "1.45.1"
ssh-encoding = { version = "0.2.0", features = ["alloc"] }
zeroize = "1.8"
ml-kem = { version = "0.2.1", features = ["zeroize"] }
zeroizing-alloc = "0.1.0"
#lazy_static = "1.4.0"
#ctap-hid-fido2 = "2.1.3" #ctap-hid-fido2 = "2.1.3"
#[patch.crates-io]
#ml-kem = { path = "externals/ml-kem" }

17
LICENSE.simple-sig Normal file
View File

@@ -0,0 +1,17 @@
{
"schema": "https://openwebstandard.org/simple-sign-file/v1",
"filename": "LICENSE",
"digest": "sha256-8f25018489d6fe0dec34a352314c38dc146247b7de65735790f4398a92afa84b",
"timestamp": 1700985908804,
"signatures": [
{
"algorithm": "SHA256withECDSA",
"signature": "MGQCMGEHQZZ8idpUsV8Fl0gVfXzeKc+1n+EuRmxVhpmNr4FcrrhD/LJsDtRPVw3WgB2dEgIwR/GxVSR0fsE/WoRD+zf8FFLrkKGM6jPvOUiNIuuVQvvHC4C/BfbHdp+tUvJgdR2G",
"certificates": [
"-----BEGIN CERTIFICATE-----\nMIIB+DCCAX6gAwIBAgIVALe/Gyof7wdOqA5Hw+BfxLKsKctUMAoGCCqGSM49BAMC\nMCQxIjAgBgNVBAMMGUhhdHRlciBFQyBJbnRlcm1lZGlhdGUgQ0EwHhcNMjMxMDMw\nMDAwMDAwWhcNMzMxMDMwMDAwMDAwWjAcMRowGAYDVQQDDBFIYXR0ZXIgU2lnbmlu\nZyBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABNA3bQZm7Fz93A7wjR4TZnfZ/yZD\nJDA/bMOyU0R1Xj2nyp164jWut7Y7k+wEUQObOqb6mtml3YK24kDSc75+vTBAzSsz\nJWVpS4XgYGZ1u41L7Ns7un56uZocnuP2liFcSqN4MHYwDgYDVR0PAQH/BAQDAgWg\nMAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwMwHQYDVR0OBBYE\nFP9cz42+U6fP5YZXpJLM/TschPmkMB8GA1UdIwQYMBaAFKWHFKtlvWFHtpitgmmc\nMK8CJAY8MAoGCCqGSM49BAMCA2gAMGUCMQCjs/EbpNpOa6LoKRqEu6AdKaKA4mlN\n2xIVU6cIViwv4Lj0K/nmPHnAnPOu4yiLr1UCMFKcIfdZBn5mQ9DoT6Rbefy4SH6P\ndrQlvOTIBRQh9kiQoA2clTG1d8DFc0PpRF9pXA==\n-----END CERTIFICATE-----",
"-----BEGIN CERTIFICATE-----\nMIIB5DCCAWugAwIBAgIUPQMQohzxKUPB5kNVqucFbULevIMwCgYIKoZIzj0EAwIw\nHDEaMBgGA1UEAwwRSGF0dGVyIEVDIFJvb3QgQ0EwHhcNMjMxMDI5MDAwMDAwWhcN\nMzMxMDI5MDAwMDAwWjAkMSIwIAYDVQQDDBlIYXR0ZXIgRUMgSW50ZXJtZWRpYXRl\nIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEImblRzI8dv8ea7y8kR2X0ZM56BF3\ntjjzjIJ7zmXaMO3DU9JbCdXZJoogLytTuKA5hmSPD0aXbnzQ89mZ7KWVA2qI2cjH\nwN5u+KtQM2oPvhH0nhMVFifcM7IeP6quihqko2YwZDAOBgNVHQ8BAf8EBAMCAQYw\nEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUpYcUq2W9YUe2mK2CaZwwrwIk\nBjwwHwYDVR0jBBgwFoAUeYIe16r9vuTceUDXG0CAbI9Pp+owCgYIKoZIzj0EAwID\nZwAwZAIwd9dqszZM7lKcf+LtDc0VkbNlBZVIS0jjZfUn6nUXOizfjNM3UzLcMKVO\nTQP1pb2XAjAeISWnbTaxxQPCG/6mzfMw9CfqPS6ECuHfrXyfAw45AI7CpUArDhZW\nZKV6vlnkzHc=\n-----END CERTIFICATE-----",
"-----BEGIN CERTIFICATE-----\nMIIB3DCCAWKgAwIBAgIUBmlMvQ8s4PNWa2dFxhZH6gpVEpUwCgYIKoZIzj0EAwIw\nHDEaMBgGA1UEAwwRSGF0dGVyIEVDIFJvb3QgQ0EwIBcNMjMxMDI5MDAwMDAwWhgP\nMjA2MzEwMjkwMDAwMDBaMBwxGjAYBgNVBAMMEUhhdHRlciBFQyBSb290IENBMHYw\nEAYHKoZIzj0CAQYFK4EEACIDYgAE3hLba+pjLyUPUiXO6DcSM0326f4yuziZiKNU\nrBKfgJ7GZ6Yydlh2Ke33vyhoBcvTQlHP4ocWGwm0RdJ0Wz+99tkxegv8VskEqIEo\nCU/U78w6DbcWvzQAAKfXUfGjjNpBo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T\nAQH/BAUwAwEB/zAdBgNVHQ4EFgQUeYIe16r9vuTceUDXG0CAbI9Pp+owHwYDVR0j\nBBgwFoAUeYIe16r9vuTceUDXG0CAbI9Pp+owCgYIKoZIzj0EAwIDaAAwZQIxANym\nCiIqwtBXwcvn887Z9dnrdWXDEpJanID2nvwqa57ACIhTTu3d/UzFdOM6GWDR8AIw\nbC9qIy+izBeFPfbggsz6U9nF5++LbtRHBFQ2InWoI4GZd074SGPcYRalMV3AUZ5m\n-----END CERTIFICATE-----"
]
}
]
}

249
README.md
View File

@@ -1,17 +1,22 @@
# card-cli # card-cli
> A cli using https://github.com/mozilla/authenticator-rs > FIDO(U2F, WebAuthn), YubiKey, OpenPGP command line tool
Install: Install:
``` ```shell
cargo install --git https://git.hatter.ink/hatter/card-cli.git cargo install --git https://git.hatter.ink/hatter/card-cli.git
``` ```
Compile without features:
```shell
cargo build --release --no-default-features
```
# PGP # PGP
## decrypt text ## encrypt & decrypt
sample public key sample encrypt public key
``` ```
-----BEGIN PUBLIC KEY----- -----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApUM8M+QRMUw0dIvXISFx MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApUM8M+QRMUw0dIvXISFx
@@ -30,51 +35,231 @@ hW53WfImja+b5kwwyqUikyMCAwEAAQ==
``` ```
encrypt encrypt
``` ```shell
$ openssl rsautl -encrypt -pubin -inkey enc_key.pem -in test.txt -out enc.txt -pkcs $ openssl rsautl -encrypt -pubin -inkey enc_key.pem -in test.txt -out enc.txt -pkcs
OR
$ openssl pkeyutl -encrypt -inkey enc_key.pem -pubin -in a.txt -out enc.txt
``` ```
decrypt decrypt
``` ```shell
$ cargo r -- pgp-card-decrypt -c $(cat enc.txt | xxd -ps -c 11111) $ card-cli pgp-card-decrypt -c $(cat enc.txt | xxd -ps -c 11111)
OR
$ card-cli piv-decrypt -s r3 -c "$(cat enc.txt | base64)"
``` ```
## sign ## sign & verify
sign sign
``` ```shell
$ cargo r -- pgp-card-sign -2 $(shasum -a 256 test.txt | awk '{print $1}') $ card-cli pgp-card-sign -2 $(shasum -a 256 test.txt | awk '{print $1}')
OR
$ card-cli pgp-card-sign --in test.txt --use-sha256
``` ```
verify verify
``` ```shell
$ openssl dgst -sha256 -verify sign_key.pem -signature sig test.txt $ openssl dgst -sha256 -verify sign_key.pem -signature sig test.txt
Verified OK Verified OK
``` ```
# sample public keys
Awesome webauthn: ```
* https://github.com/herrjemand/awesome-webauthn [INFO ] Authentication fingerprint: EB0A43A10BFC6E58323F7650BA42AE533FDCE10E
[INFO ] Authentication public key sha256: ac97c7f9f500f3fbab635536096311c62698f8c22abd9e9687de7893932bc15b
[INFO ] Authentication public key: -----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA8Kg5fg47YilT/xFOZ7xK
17T47cfwzS6L/4IRtTjcygvmOVSdOISihQxVfsygpxhThRQ3pjqhFGqH9LUIpry/
a8hWfPMZYolYywBvdx5S6UGDUeRf2zLcRYrQo+Fs9oxdhxPE05HhWl9L5ORn4HWz
RZSkNfh7PDKPJRUaJV85uB6Fyvt0GGY14pmINZ7NRLLi2ubYBlp3CLSh7XdleVE8
/Q6gya501INhXUksuwHXdPYtcXF3l+VIdMc6YJTxivFLtujqiEAfEwauuv+1GzsN
ZDOg6JfSc+1d7iZMixU4RrKtzM57ZwGX0bAK3MQdP6iT20DOYq/BDJTXJuhQBWgE
6pIDiTJF4q/If0ZLxU+kxstAEg0fuD+wOg/+4W1BSn5D3hSdvVOxgj3hWtPudAVp
QucP8LKnq5B0oy4LdGqXXAQYJ2Q+ln0N9By2T8N/P37HOsR7yJLl8cM2FptCoo4x
ViGzmIbir8EyZ6VQmoi8fqOP4x9nH5XeNA2JCVLEc0o6n5PJ4IitYYCb0NGOPTHV
FEz2qzxkQDJxS5oC7GddWQB/pa4Jq0EL9dEabB2oPyvYBAmmE0HzZWLl3T1kR1dJ
fAXuqgShFcZLXa1SFUpLzlJi3jARuxoaUeHnKP3xeAd8o5WPBwzXM7LL47nTueNa
uFZKwHs/e9x4EszQ/qFo2uECAwEAAQ==
-----END PUBLIC KEY-----
[INFO ] Encryption fingerprint: E48EC98FE6CAE85AAFD5A68AC37A909EAF1BFB00
[INFO ] Encryption public key sha256: de5a99c239a82adf039982cb6319abcb95f44cfc76a5027ae6f7819cfc5fde7c
[INFO ] Encryption public key: -----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApUM8M+QRMUw0dIvXISFx
43j4h9CK38Y9HD6kPcc3Z0dCGPiFy7Ze0OQebPWHyUZ2YmqsdyzFuOQuV9P2pxxj
/WLIgRqZV8Jk8tWhtAjOOvm0MTc2rg+EJHfa+zhX4eFEMsj4DvQBMJDXiKnpXTM/
j7oMKpIUQHqfXBwsEJHLmHZTLeEBEYKcZXTAmuu3WdxK5jvEc02Xt2hZ1fBs0M9e
/2EMe3t69aH4/rabiBjF2h9Jde15wrJMxXaCCWJqYhbBS0CJ3BdjkAqOIpcqPXva
xiJN1pNpK8ejA9Q4Nmx7pxnvfv+hCPkWXZS3r/BWZ9lFZc8uErQEbB4gLgko8jOl
fQF7cYqtZEs69qY8nnIUBsqZYfAp+bQd2xCFSbEZAl+OrtGzfVjD9YFMPy02+xRg
v2N3KT3KHHvuU7WxrvffrshP2fwDuG2MBlmcq1suAKxA0cYPSyajceEqw/3ogSp7
7SYx41rT8EWLmTvU0CHzCsuf/O7sDWZRfxatAzWhBBhnKCPqzizpOQOqm8XhCt74
FfnabPpHM9XUjoQIPrTssyS3eWqynzJiAqez6v2LK2fhL7IkcLtvt5p59Y+KY4I6
YQ09iUh7lKJHRhkgTomUurJHieVHMWFGIHofEC+nU6pGIUh0P7Nr0Gz45GJTwWGd
hW53WfImja+b5kwwyqUikyMCAwEAAQ==
-----END PUBLIC KEY-----
[INFO ] Signature fingerprint: 6FAFC0E0170985AA71545483C794B1646A886CD6
[INFO ] Signature public key sha256: d65831b0316a03828eeb31fe6a51e6eec59e7092eb6d3477404ad2f5fa08e903
[INFO ] Signature public key: -----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7kVYTHxFjZD9kT+w97B
GiHfqlyoulJ10cRqaWwX3/mZKfoeGJkBDglFnLfgtHhXivPqRSn73sCX6M0HCzSq
9M/drkms/H8cecM08SoZdZTM0TVr/c8w0ZA7Ipoder9K/9LdGpIgoc3qa8hdY2nH
TwGYJ53aQv32neOcg3p/vzqdzgmwbk4JLjcIMhOuTUj4xM8OMnkxRpyy9+Ghi22X
oZXDxu8meI2Pc8jM+zpRYb0wd06dd231m03CK80LAwSvIn7dGFAr+xTF5XKopXHY
vuT+9SshszbP4+pSqbEHZhJOX1/os+Uo8KKfysifJBKfKCVvVWho8QCWoXgiNuOJ
3cYoThfWwUpIS1S51el/fPPSk3K295jlZAON9yEszdzKHGVGOrtJ7e9XSxKIXqhG
us2XA14eMvhQdaOgd/bscXIYe4YzqvaqvVRiDUP8bzA+4w0ctB0w9HRFGK5lajTn
/QQvkKP9JQXm6Tb2GB+wjuU3wPXhKRWscEzbHVwMq2WiaYH5vWVhHI6lbqXcWkvZ
i2gZXQPyrAKzUau1Z2lBN2xi2cv5+9JJth5pHebuLOWbuf1WV4nR1fdSNdG7GGmj
G951w/1bTqIlzN4Vl6kdore4u45U4kO4Xf7Hq8b8k8ys107ENpgO7lB9KLoMMFKS
vjG+EPEF3g8ywKaS8mZQX+sCAwEAAQ==
-----END PUBLIC KEY-----
```
Hard U2F projects: # piv-ecdh
* https://github.com/google/OpenSK
* https://github.com/solokeys/solo
* https://github.com/conorpp/u2f-zero
* https://github.com/makerdiary/nrf52-u2f
Soft U2F projects: ```shell
* https://github.com/github/SoftU2F $ card-cli piv-ecdh --public-256 --public-key-point-hex 04dd3eebd906c9cf00b08ec29f7ed61804d1cc1d1352d9257b628191e08fc3717c4fae3298cd5c4829cec8bf3a946e7db60b7857e1287f6a0bae6b3f2342f007d0 --json
* https://github.com/SoftU2F/SoftU2F-Win {
* https://github.com/danstiner/rust-u2f "epk_point_hex": "04bbb6a458e81d2c646587118abfb029ff715db366f92a1d0468887f9947f176c11961eccebd5b9cbbb8b67e33fa8d3f0010a4aaf5010d0f419f1f99b4c2d7aa56",
"pk_point_hex": "04dd3eebd906c9cf00b08ec29f7ed61804d1cc1d1352d9257b628191e08fc3717c4fae3298cd5c4829cec8bf3a946e7db60b7857e1287f6a0bae6b3f2342f007d0",
"shared_secret_hex": "58069f1b2ce85c4f2232070567bef99f71b45f69ab321c4c782e599813b56f25"
}
Related webauthn Rust projects: $ card-cli piv-ecdh --private --slot 82 --epk 04bbb6a458e81d2c646587118abfb029ff715db366f92a1d0468887f9947f176c11961eccebd5b9cbbb8b67e33fa8d3f0010a4aaf5010d0f419f1f99b4c2d7aa56 --json
* https://github.com/mozilla/authenticator-rs/ [WARN ] Get slot: 82 meta data failed
* https://github.com/gebogebogebo/ctap-hid-fido2 {
* https://github.com/kanidm/webauthn-rs "shared_secret_hex": "58069f1b2ce85c4f2232070567bef99f71b45f69ab321c4c782e599813b56f25"
* https://github.com/kanidm/webauthn-authenticator-rs }
* https://github.com/shimunn/ctap ```
OpenPGP projects: # piv-ecsign
* https://github.com/solokeys/piv-authenticator
* https://gitlab.com/hkos/openpgp-card ```shell
* https://gitlab.com/sequoia-pgp/sequoia $ card-cli piv-ecsign -s 82 --hash-hex 8f25018489d6fe0dec34a352314c38dc146247b7de65735790f4398a92afa84b --json
{
"hash_hex": "8f25018489d6fe0dec34a352314c38dc146247b7de65735790f4398a92afa84b",
"signed_data_base64": "MEUCICdes5Y0Id7KBNL23ZsTXXXGAzmsWYyDa6szQwjCxhCJAiEAhJotD2dPK/fWNjNrwkrPd0F20MpGgIY3WiKDR7YgJbk=",
"signed_data_hex": "30450220275eb3963421deca04d2f6dd9b135d75c60339ac598c836bab334308c2c61089022100849a2d0f674f2bf7d636336bc24acf774176d0ca468086375a228347b62025b9",
"slot": "82"
}
```
# se - secure enclave
```shell
% card-cli se-recover --key key://macbook:se/p256:key_agreement:BIICNTGCAjEwgfUMAnJrMYHuMAsMA2JpZAQEHZA4qzBIDANwdWIEQQTPKTHhfq7TY6k9HTEDZAj/YZTXVLYJ9qdeiqoKyRlFtmPB2Y7WkCoWiwUbLYPWH6Xrcq+BuHGvqLUm/0dcgOenMAgMA3JrbwIBADAHDAJrdAIBBDAuDAJ3awQoasVSuHpKW4nKiJyUGkSmLy1gZzF6m7jKA/b2R/qJkAjYWrxnLiP5njAHDAJiYwIBCTAHDAJrdgIBAjAXDANraWQEEIHUu/8JxEJ6uY2da1axAVgwJwwDcmttBCBVMoa+gs08uUAN1wzjPK4WxxKjkzHwlgZ6fLUZXAAZSDCCATUMAmVkMYIBLTCCASkMA2FjbDGCASAwVwwDb2NrMVAwTgwEY2JpbzFGMBkMBXBiaW9jBBDCvCHdqryeEqlb/kVKG1tZMCkMBXBiaW9oBCCoodocQw4rE75LKlztGnyw4QzDMMXvbLSe2mJgYutOJjAJDARvZGVsAQEBMFgMBG9zZ24xUDBODARjYmlvMUYwGQwFcGJpb2MEEMK8Id2qvJ4SqVv+RUobW1kwKQwFcGJpb2gEIKih2hxDDisTvksqXO0afLDhDMMwxe9stJ7aYmBi604mMAcMAm9hAQEBMFcMA29rZDFQME4MBGNiaW8xRjAZDAVwYmlvYwQQwrwh3aq8nhKpW/5FShtbWTApDAVwYmlvaAQgqKHaHEMOKxO+Sypc7Rp8sOEMwzDF72y0ntpiYGLrTiY= --json
{
"key": "key://macbook:se/p256:key_agreement:BIICNTGCAjEwgfUMAnJrMYHuMAsMA2JpZAQEHZA4qzBIDANwdWIEQQTPKTHhfq7TY6k9HTEDZAj/YZTXVLYJ9qdeiqoKyRlFtmPB2Y7WkCoWiwUbLYPWH6Xrcq+BuHGvqLUm/0dcgOenMAgMA3JrbwIBADAHDAJrdAIBBDAuDAJ3awQoasVSuHpKW4nKiJyUGkSmLy1gZzF6m7jKA/b2R/qJkAjYWrxnLiP5njAHDAJiYwIBCTAHDAJrdgIBAjAXDANraWQEEIHUu/8JxEJ6uY2da1axAVgwJwwDcmttBCBVMoa+gs08uUAN1wzjPK4WxxKjkzHwlgZ6fLUZXAAZSDCCATUMAmVkMYIBLTCCASkMA2FjbDGCASAwVwwDb2NrMVAwTgwEY2JpbzFGMBkMBXBiaW9jBBDCvCHdqryeEqlb/kVKG1tZMCkMBXBiaW9oBCCoodocQw4rE75LKlztGnyw4QzDMMXvbLSe2mJgYutOJjAJDARvZGVsAQEBMFgMBG9zZ24xUDBODARjYmlvMUYwGQwFcGJpb2MEEMK8Id2qvJ4SqVv+RUobW1kwKQwFcGJpb2gEIKih2hxDDisTvksqXO0afLDhDMMwxe9stJ7aYmBi604mMAcMAm9hAQEBMFcMA29rZDFQME4MBGNiaW8xRjAZDAVwYmlvYwQQwrwh3aq8nhKpW/5FShtbWTApDAVwYmlvaAQgqKHaHEMOKxO+Sypc7Rp8sOEMwzDF72y0ntpiYGLrTiY=",
"public_key_pem": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzykx4X6u02OpPR0xA2QI/2GU11S2CfanXoqqCskZRbZjwdmO1pAqFosFGy2D1h+l63Kvgbhxr6i1Jv9HXIDnpw==",
"public_key_point": "04cf2931e17eaed363a93d1d31036408ff6194d754b609f6a75e8aaa0ac91945b663c1d98ed6902a168b051b2d83d61fa5eb72af81b871afa8b526ff475c80e7a7"
}
% card-cli piv-ecdh --public-256 --public-key-point-hex 04cf2931e17eaed363a93d1d31036408ff6194d754b609f6a75e8aaa0ac91945b663c1d98ed6902a168b051b2d83d61fa5eb72af81b871afa8b526ff475c80e7a7 --json
{
"epk_point_hex": "041237ac2455b730057be2d58e9aa471cb8e1e9a9c094d72acbc641ccdf5f5e3999405019990e83e45d556528f3d44ec3489e6e68489e234cb4a97e2c99eb139e3",
"pk_point_hex": "04cf2931e17eaed363a93d1d31036408ff6194d754b609f6a75e8aaa0ac91945b663c1d98ed6902a168b051b2d83d61fa5eb72af81b871afa8b526ff475c80e7a7",
"shared_secret_hex": "e928a426d75cd5baa7eccb9dad704dbd8cd2047f9df54b0560ae461c5af10f25"
}
% card-cli se-ecdh --key key://macbook:se/p256:key_agreement:BIICNTGCAjEwgfUMAnJrMYHuMAsMA2JpZAQEHZA4qzBIDANwdWIEQQTPKTHhfq7TY6k9HTEDZAj/YZTXVLYJ9qdeiqoKyRlFtmPB2Y7WkCoWiwUbLYPWH6Xrcq+BuHGvqLUm/0dcgOenMAgMA3JrbwIBADAHDAJrdAIBBDAuDAJ3awQoasVSuHpKW4nKiJyUGkSmLy1gZzF6m7jKA/b2R/qJkAjYWrxnLiP5njAHDAJiYwIBCTAHDAJrdgIBAjAXDANraWQEEIHUu/8JxEJ6uY2da1axAVgwJwwDcmttBCBVMoa+gs08uUAN1wzjPK4WxxKjkzHwlgZ6fLUZXAAZSDCCATUMAmVkMYIBLTCCASkMA2FjbDGCASAwVwwDb2NrMVAwTgwEY2JpbzFGMBkMBXBiaW9jBBDCvCHdqryeEqlb/kVKG1tZMCkMBXBiaW9oBCCoodocQw4rE75LKlztGnyw4QzDMMXvbLSe2mJgYutOJjAJDARvZGVsAQEBMFgMBG9zZ24xUDBODARjYmlvMUYwGQwFcGJpb2MEEMK8Id2qvJ4SqVv+RUobW1kwKQwFcGJpb2gEIKih2hxDDisTvksqXO0afLDhDMMwxe9stJ7aYmBi604mMAcMAm9hAQEBMFcMA29rZDFQME4MBGNiaW8xRjAZDAVwYmlvYwQQwrwh3aq8nhKpW/5FShtbWTApDAVwYmlvaAQgqKHaHEMOKxO+Sypc7Rp8sOEMwzDF72y0ntpiYGLrTiY= --epk 041237ac2455b730057be2d58e9aa471cb8e1e9a9c094d72acbc641ccdf5f5e3999405019990e83e45d556528f3d44ec3489e6e68489e234cb4a97e2c99eb139e3 --json
{
"shared_secret_hex": "e928a426d75cd5baa7eccb9dad704dbd8cd2047f9df54b0560ae461c5af10f25"
}
```
# import private key to PIV card &amp; generate certificate
```shell
$ ykman piv keys import --pin-policy NEVER --touch-policy CACHED 82 private_key.pem
```
| Parameter | Description |
| ---- |------------------------------------------------------------------|
| --pin-policy | \[ DEFAULT \| NEVER \| ONCE \| ALWAYS \] PIN policy for slot |
| --touch-policy | \[ DEFAULT \| NEVER \| ALWAYS \| CACHED \] touch policy for slot |
```shell
$ ykman piv certificates generate 82 public_key.pem -s 'O=age-plugin-yubikey,OU=0.3.3,CN=hatter-yk'
```
# age
## pgp-age-address
```shell
$ card-cli pgp-age-address
[INFO ] Found 1 card(s)
[OK ] Found card #0: Ok(ApplicationIdentifier { application: 1, version: 772, manufacturer: 6, serial: 370378374 })
[OK ] Age address: age10l464vxcpnkjguctvylnmp5jg4swhncn4quda0qxta3ud8pycc0qeaj2te
```
# sign-jwt
Sign a JWT:
```shell
card-cli sign-jwt -s r3 \
-C iss:****** \
-C sub:****** \
-C aud:client_gard****** \
-K KEY=ID \
--jti \
--validity 10m --json
```
# SSH CA
## Generate SSH root CA
```shell
card-cli ssh-pub-key --ca -s r15
```
Outputs:
```
cert-authority,principals="root" ecdsa-sha2-nistp384 AAAAE2VjZHNh****** Yubikey-PIV-R15
```
> `principals` can be multiple items, split by `,`, e.g. `root,hatterink`
## Generate SSH user CA
```shell
ssh-keygen -f id_user
card-cli ssh-piv-cert --pub id_user.pub -s r15
```
Show SSH CA cert details:
```shell
ssh-keygen -L -f id_user-cert.pub
```
SSH to server:
```shell
ssh -i id_user root@example.com
```
<br>
> `external_*` subcommands follow &lt;&lt;Cryptography external command specification&gt;&gt;<br>
> Specification: https://openwebstandard.org/rfc1
<br><br>
Downloads:
* https://developers.yubico.com/yubikey-manager/
<br>
Related projects:
* https://crates.io/crates/openpgp-card-tools
* https://github.com/sekey/sekey
* https://github.com/str4d/age-plugin-yubikey

31
examples/hmac_sha1.rs Normal file
View File

@@ -0,0 +1,31 @@
use rust_util::{opt_result, XResult};
use yubico_manager::hmacmode::HmacKey;
use yubico_manager::sec::hmac_sha1;
fn main() -> XResult<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.len() != 2 && args.len() != 3 {
println!("cargo r --example hmac_sha1 <hmac_key_hex;20bytes> <challenge_hex> [variable;bool]");
return Ok(());
}
let hmac_key_bytes = opt_result!(hex::decode(&args[0]), "Decode hmac key hex failed: {}");
let challenge_bytes = opt_result!(hex::decode(&args[1]), "Decode challenge hex failed: {}");
let variable = if args.len() == 3 { "true" == &args[2] } else { false };
let hmac_key = HmacKey::from_slice(&hmac_key_bytes);
let mut challenge = [0; 64];
if variable && challenge_bytes.last() == Some(&0) {
challenge = [0xff; 64];
}
(&mut challenge[..challenge_bytes.len()]).copy_from_slice(&challenge_bytes);
let hmac_sha_result = hmac_sha1(&hmac_key, &challenge);
println!("Variable: {}", variable);
println!("Hmac challenge: {}", hex::encode(&challenge));
println!("Hmac_Sha1: {}", hex::encode(&hmac_sha_result));
Ok(())
}

103
examples/pgp_key.rs Normal file
View File

@@ -0,0 +1,103 @@
use openpgp::crypto::Password;
use openpgp::Packet;
use openpgp::packet::Key;
use openpgp::packet::key::{PrimaryRole, SecretKeyMaterial, SecretParts};
use openpgp::parse::{PacketParser, PacketParserResult, Parse};
use openssl::bn::{BigNum, BigNumContext};
use rust_util::XResult;
use sequoia_openpgp as openpgp;
use sequoia_openpgp::packet::key::SubordinateRole;
fn main() -> XResult<()> {
let pass = std::env::var("PASS").expect("PASS is not set");
let password = Password::from(pass.as_str());
let mut ppr = PacketParser::from_file("/Users/hatterjiang/key.asc")?;
while let PacketParserResult::Some(pp) = ppr {
// println!("{:?}", pp);
if let Packet::SecretKey(sk) = &pp.packet {
print_key(sk, &password);
} else if let Packet::SecretSubkey(ssk) = &pp.packet {
print_key2(ssk, &password);
}
// Start parsing the next packet, recursing.
ppr = pp.recurse()?.1;
}
Ok(())
}
fn print_key(key: &Key<SecretParts, PrimaryRole>, password: &Password) {
let is_encrypted = key.has_secret() && !key.has_unencrypted_secret();
println!("\n\nFound key, has secret: {}, encrypted: {}, key id: {}, fingerprint: {}", key.has_secret(), is_encrypted, key.keyid(), key.fingerprint());
println!(" - create time: {:?}", key.creation_time());
println!(" - public key : {:?}", key.mpis());
let a = key.clone().decrypt_secret(&password).unwrap();
println!("----------- {:?}", a);
if let Key::V4(key4) = a {
let secret = key4.secret();
if let SecretKeyMaterial::Unencrypted(unencrypted) = secret {
unencrypted.map(|f| {
// println!("<><> {:?}", f);
if let openpgp::crypto::mpi::SecretKeyMaterial::RSA { d, p, q, u } = f {
println!(">>>> {:?}", d);
println!(">>>> {:?}", p);
println!(">>>> {:?}", q);
println!(">>>> {:?}", u);
}
});
}
}
}
fn print_key2(key: &Key<SecretParts, SubordinateRole>, password: &Password) {
let is_encrypted = key.has_secret() && !key.has_unencrypted_secret();
println!("\n\nFound key, has secret: {}, encrypted: {}, key id: {}, fingerprint: {}", key.has_secret(), is_encrypted, key.keyid(), key.fingerprint());
println!(" - create time: {:?}", key.creation_time());
println!(" - public key : {:?}", key.mpis());
let decrypted_key = key.clone().decrypt_secret(&password).unwrap();
if let Key::V4(key4) = decrypted_key {
let secret = key4.secret();
if let SecretKeyMaterial::Unencrypted(unencrypted) = secret {
unencrypted.map(|f| {
// println!("<><> {:?}", f);
if let openpgp::crypto::mpi::SecretKeyMaterial::RSA { d, p, q, u } = f {
println!(">>>> {:?}", d);
println!(">>>> {:?}", p);
println!(">>>> {:?}", q);
println!(">>>> {:?}", u);
let p = BigNum::from_slice(p.value()).unwrap();
let q = BigNum::from_slice(q.value()).unwrap();
let e = BigNum::from_u32(65537).unwrap();
let mut n = BigNum::new().unwrap();
n.checked_mul(&p, &q, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> n: {}", hex::encode(&n.to_vec()).to_uppercase());
let mut p_m1 = BigNum::from_slice(p.to_vec().as_slice()).unwrap();
p_m1.sub_word(1).unwrap();
let mut q_m1 = BigNum::from_slice(q.to_vec().as_slice()).unwrap();
q_m1.sub_word(1).unwrap();
let mut m = BigNum::new().unwrap();
m.checked_mul(&p_m1, &q_m1, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> m: {}", hex::encode(&m.to_vec()).to_uppercase());
let mut d = BigNum::new().unwrap();
d.mod_inverse(&e, &m, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> d: {}", hex::encode(&d.to_vec()).to_uppercase());
let mut dp = BigNum::new().unwrap();
dp.nnmod(&d, &p_m1, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> dp: {}", hex::encode(&dp.to_vec()).to_uppercase());
let mut dq = BigNum::new().unwrap();
dq.nnmod(&d, &q_m1, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> dq: {}", hex::encode(&dq.to_vec()).to_uppercase());
let mut qinv = BigNum::new().unwrap();
qinv.mod_inverse(&q, &p, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> qinv: {}", hex::encode(&qinv.to_vec()).to_uppercase());
}
});
}
}
}

26
examples/rsa.rs Normal file
View File

@@ -0,0 +1,26 @@
use base64::Engine;
use rand::rngs::OsRng;
use rsa::pkcs1::LineEnding;
use rsa::pkcs8::DecodePrivateKey;
use rsa::pkcs8::EncodePrivateKey;
use rsa::traits::PublicKeyParts;
use rsa::RsaPrivateKey;
use spki::EncodePublicKey;
fn main() {
let key = RsaPrivateKey::new(&mut OsRng, 1024).unwrap();
let pem = key.to_pkcs8_pem(LineEnding::LF).unwrap();
println!("{}", pem.as_str());
let key2 = RsaPrivateKey::from_pkcs8_pem(pem.as_ref()).unwrap();
let pub_key = key2.to_public_key();
let public_key_pem = pub_key.to_public_key_pem(LineEnding::LF).unwrap();
println!("{}", public_key_pem);
let n = pub_key.n();
let e = pub_key.e();
let url_safe = base64::engine::general_purpose::URL_SAFE_NO_PAD;
println!("n: {}", url_safe.encode(&n.to_bytes_be()));
println!("e: {}", url_safe.encode(&e.to_bytes_be()));
}

88
examples/rsa_crt.rs Normal file
View File

@@ -0,0 +1,88 @@
use openssl::bn::{BigNum, BigNumContext};
// modulus:
// 00:af:49:c0:89:17:c1:83:78:17:f9:a1:c3:02:54:
// b6:c1:2b:d2:34:94:31:c8:76:fe:59:8a:e9:dc:9e:
// b2:ca:75:99:8b:58:2e:1a:81:a9:14:5f:43:9b:9c:
// 42:37:56:d5:56:6d:0f:65:a3:77:62:06:de:59:0b:
// 1c:71:3b:22:8a:e2:06:bd:96:76:f3:6f:fb:fe:c0:
// 9c:03:aa:14:e3:1e:3d:10:9a:9f:8d:85:21:08:da:
// 68:82:05:f1:31:d0:98:b3:2d:02:a7:0c:c1:f1:2d:
// 3b:63:37:a4:0a:cb:4e:3b:4e:70:40:fb:6c:94:bb:
// 23:48:fb:90:09:ab:e8:05:4f
// publicExponent: 5 (0x5)
// privateExponent:
// 23:0e:c0:1b:6b:26:b3:e4:d1:98:53:8d:66:dd:be:
// 26:a2:5d:3d:b7:3d:28:17:cc:78:4e:fb:92:86:23:
// c2:17:85:1b:de:6f:9e:e6:bb:6a:79:73:eb:ec:0d:
// 3e:44:91:11:49:03:14:53:e4:ad:34:92:de:9b:d2:
// 7d:0b:d3:b5:0b:3c:ed:f9:db:96:28:cb:08:34:16:
// d9:68:20:52:54:bd:47:b3:2d:35:4c:ad:bf:05:45:
// 2e:0a:b7:30:34:58:c8:2b:35:86:c2:84:c1:0f:06:
// 5b:95:72:4f:d8:5e:38:88:4e:43:5c:ad:57:fa:d0:
// 56:f6:12:6b:4f:76:26:c5
// prime1:
// 00:ea:5f:f0:9b:37:7f:ca:92:fb:88:98:58:84:39:
// 76:e4:6f:d6:ab:b2:3c:9f:26:54:16:16:53:68:24:
// 56:74:ea:f6:41:8b:20:0a:90:7e:6b:7d:da:8f:19:
// a2:42:e0:73:cf:de:3c:02:d8:92:c7:36:be:16:2a:
// 56:12:0a:35:5b
// prime2:
// 00:bf:76:27:19:f5:84:d9:71:da:33:91:6c:1d:39:
// d0:92:1a:d3:6f:0b:46:66:95:f9:a9:f8:48:68:38:
// ea:55:f1:d4:13:6b:e5:35:99:ad:76:9d:be:bd:4e:
// d9:4e:96:ac:d5:0a:b4:29:31:4a:0d:da:d8:17:09:
// 9b:0c:8f:0e:1d
// exponent1:
// 00:8c:9f:f6:c3:87:b3:13:24:fd:51:f5:01:e8:ef:
// 47:55:dc:b4:00:9e:24:5f:7d:65:a6:da:32:0b:49:
// 00:ac:8c:fa:27:53:79:9f:f0:4b:da:18:4f:ef:75:
// c7:c1:b9:df:16:52:24:01:b5:24:dd:ed:a5:40:7f:
// cd:3e:06:20:03
// exponent2:
// 72:e0:7d:dc:60:1c:82:77:82:eb:bd:a7:44:bc:49:
// f1:43:4b:a9:06:c3:d7:26:c8:ff:94:f8:3e:88:8c:
// 99:f7:7f:3e:da:56:53:5c:34:e0:c5:0c:0b:2f:4f:
// 2f:27:34:7f:d3:38:e5:83:f9:3b:83:4e:74:38:f6:
// a1:22:a2:11
// coefficient:
// 00:c2:ee:8a:96:0e:f3:5f:d5:31:86:c7:1f:9a:9c:
// b4:27:5b:04:82:e2:56:e4:2d:cb:bd:5f:62:ec:e9:
// d5:8e:5a:87:47:ec:bf:94:b3:96:d6:15:c7:51:3a:
// c0:a1:5b:63:33:ad:32:60:46:51:6c:7a:e4:38:7e:
// d4:9e:42:d5:35
fn main() {
let p = BigNum::from_hex_str("00ea5ff09b377fca92fb889858843976e46fd6abb23c9f265416165368245674eaf6418b200a907e6b7dda8f19a242e073cfde3c02d892c736be162a56120a355b").unwrap();
let q = BigNum::from_hex_str("00bf762719f584d971da33916c1d39d0921ad36f0b466695f9a9f8486838ea55f1d4136be53599ad769dbebd4ed94e96acd50ab429314a0ddad817099b0c8f0e1d").unwrap();
let e = BigNum::from_u32(5).unwrap();
let mut n = BigNum::new().unwrap();
n.checked_mul(&p, &q, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> n: {}", hex::encode(&n.to_vec()).to_uppercase());
let mut p_m1 = BigNum::from_slice(p.to_vec().as_slice()).unwrap();
p_m1.sub_word(1).unwrap();
let mut q_m1 = BigNum::from_slice(q.to_vec().as_slice()).unwrap();
q_m1.sub_word(1).unwrap();
let mut m = BigNum::new().unwrap();
m.checked_mul(&p_m1, &q_m1, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> m: {}", hex::encode(&m.to_vec()).to_uppercase());
let mut d = BigNum::new().unwrap();
d.mod_inverse(&e, &m, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> d: {}", hex::encode(&d.to_vec()).to_uppercase());
let mut dp = BigNum::new().unwrap();
dp.nnmod(&d, &p_m1, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> dp: {}", hex::encode(&dp.to_vec()).to_uppercase());
let mut dq = BigNum::new().unwrap();
dq.nnmod(&d, &q_m1, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> dq: {}", hex::encode(&dq.to_vec()).to_uppercase());
let mut qinv = BigNum::new().unwrap();
qinv.mod_inverse(&q, &p, &mut BigNumContext::new().unwrap()).unwrap();
println!(">>>> qinv: {}", hex::encode(&qinv.to_vec()).to_uppercase());
let rsa_crt = crate::rsautil::RsaCrt::from(p, q, e);
}

37
examples/rsa_encrypt.rs Normal file
View File

@@ -0,0 +1,37 @@
use openssl::encrypt::Encrypter;
use openssl::pkey::PKey;
use openssl::rsa::{Padding, Rsa};
use rust_util::information;
fn main() {
let data = b"hello, world!";
let rsa = Rsa::public_key_from_pem(
b"-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApUM8M+QRMUw0dIvXISFx
43j4h9CK38Y9HD6kPcc3Z0dCGPiFy7Ze0OQebPWHyUZ2YmqsdyzFuOQuV9P2pxxj
/WLIgRqZV8Jk8tWhtAjOOvm0MTc2rg+EJHfa+zhX4eFEMsj4DvQBMJDXiKnpXTM/
j7oMKpIUQHqfXBwsEJHLmHZTLeEBEYKcZXTAmuu3WdxK5jvEc02Xt2hZ1fBs0M9e
/2EMe3t69aH4/rabiBjF2h9Jde15wrJMxXaCCWJqYhbBS0CJ3BdjkAqOIpcqPXva
xiJN1pNpK8ejA9Q4Nmx7pxnvfv+hCPkWXZS3r/BWZ9lFZc8uErQEbB4gLgko8jOl
fQF7cYqtZEs69qY8nnIUBsqZYfAp+bQd2xCFSbEZAl+OrtGzfVjD9YFMPy02+xRg
v2N3KT3KHHvuU7WxrvffrshP2fwDuG2MBlmcq1suAKxA0cYPSyajceEqw/3ogSp7
7SYx41rT8EWLmTvU0CHzCsuf/O7sDWZRfxatAzWhBBhnKCPqzizpOQOqm8XhCt74
FfnabPpHM9XUjoQIPrTssyS3eWqynzJiAqez6v2LK2fhL7IkcLtvt5p59Y+KY4I6
YQ09iUh7lKJHRhkgTomUurJHieVHMWFGIHofEC+nU6pGIUh0P7Nr0Gz45GJTwWGd
hW53WfImja+b5kwwyqUikyMCAwEAAQ==
-----END PUBLIC KEY-----");
let pub_key = PKey::from_rsa(rsa.unwrap()).unwrap();
// Encrypt the data with RSA PKCS1
let mut encrypter = Encrypter::new(&pub_key).unwrap();
encrypter.set_rsa_padding(Padding::PKCS1).unwrap();
// Create an output buffer
let buffer_len = encrypter.encrypt_len(data).unwrap();
let mut encrypted = vec![0; buffer_len];
// Encrypt and truncate the buffer
let encrypted_len = encrypter.encrypt(data, &mut encrypted).unwrap();
encrypted.truncate(encrypted_len);
information!("Clear text: {}", String::from_utf8_lossy(data));
information!("Encrypted message base64: {}", base64::encode(&encrypted));
}

225
examples/ssh_agent.rs Normal file
View File

@@ -0,0 +1,225 @@
use std::error::Error;
use std::fs::remove_file;
use std::sync::RwLock;
use openssl::bn::BigNum;
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::pkey::Private;
use openssl::rsa::Rsa;
use openssl::sign::Signer;
use rust_util::{debugging, information};
use ssh_agent::agent::Agent;
use ssh_agent::proto::{from_bytes, RsaPublicKey, to_bytes};
use ssh_agent::proto::message::{self, Message, SignRequest};
use ssh_agent::proto::private_key::{PrivateKey, RsaPrivateKey};
use ssh_agent::proto::public_key::PublicKey;
use ssh_agent::proto::signature::{self, Signature};
#[derive(Clone, PartialEq, Debug)]
struct Identity {
pubkey: PublicKey,
privkey: PrivateKey,
comment: String,
}
struct KeyStorage {
identities: RwLock<Vec<Identity>>,
}
impl KeyStorage {
fn new() -> Self {
let rsa = Rsa::generate(2048).unwrap();
let pubkey = PublicKey::Rsa(RsaPublicKey {
e: with_sign(rsa.e().to_vec()),
n: with_sign(rsa.n().to_vec()),
});
let privkey = PrivateKey::Rsa(RsaPrivateKey {
e: with_sign(rsa.e().to_vec()),
n: with_sign(rsa.n().to_vec()),
d: with_sign(rsa.d().to_vec()),
iqmp: with_sign(rsa.iqmp().unwrap().to_vec()),
p: with_sign(rsa.p().unwrap().to_vec()),
q: with_sign(rsa.q().unwrap().to_vec()),
});
let ident = Identity {
pubkey,
privkey,
comment: "testkey".to_string(),
};
let mut ssh_key = vec![];
let ssh_rsa_bytes = "ssh-rsa".as_bytes();
ssh_key.extend_from_slice(&(ssh_rsa_bytes.len() as u32).to_be_bytes()[..]);
ssh_key.extend_from_slice(ssh_rsa_bytes);
let e = with_sign(rsa.e().to_vec());
ssh_key.extend_from_slice(&(e.len() as u32).to_be_bytes()[..]);
ssh_key.extend_from_slice(&e);
let n = with_sign(rsa.n().to_vec());
ssh_key.extend_from_slice(&(n.len() as u32).to_be_bytes()[..]);
ssh_key.extend_from_slice(&n);
debugging!("{:?}", ssh_key);
information!("ssh-rsa {} {}", base64::encode(&ssh_key), ident.comment);
Self {
identities: RwLock::new(vec![ident])
}
}
fn identity_index_from_pubkey(
identities: &Vec<Identity>,
pubkey: &PublicKey,
) -> Option<usize> {
for (index, identity) in identities.iter().enumerate() {
if &identity.pubkey == pubkey {
return Some(index);
}
}
return None;
}
fn identity_from_pubkey(&self, pubkey: &PublicKey) -> Option<Identity> {
let identities = self.identities.read().unwrap();
let index = Self::identity_index_from_pubkey(&identities, pubkey)?;
Some(identities[index].clone())
}
fn identity_add(&self, identity: Identity) {
let mut identities = self.identities.write().unwrap();
if Self::identity_index_from_pubkey(&identities, &identity.pubkey) == None {
identities.push(identity);
}
}
fn identity_remove(&self, pubkey: &PublicKey) -> Result<(), Box<dyn Error>> {
let mut identities = self.identities.write().unwrap();
if let Some(index) = Self::identity_index_from_pubkey(&identities, &pubkey) {
identities.remove(index);
Ok(())
} else {
Err(From::from("Failed to remove identity: identity not found"))
}
}
fn sign(&self, sign_request: &SignRequest) -> Result<Signature, Box<dyn Error>> {
let pubkey: PublicKey = from_bytes(&sign_request.pubkey_blob)?;
if let Some(identity) = self.identity_from_pubkey(&pubkey) {
match identity.privkey {
PrivateKey::Rsa(ref key) => {
let algorithm;
let digest;
if sign_request.flags & signature::RSA_SHA2_512 != 0 {
algorithm = "rsa-sha2-512";
digest = MessageDigest::sha512();
} else if sign_request.flags & signature::RSA_SHA2_256 != 0 {
algorithm = "rsa-sha2-256";
digest = MessageDigest::sha256();
} else {
algorithm = "ssh-rsa";
digest = MessageDigest::sha1();
}
let keypair = PKey::from_rsa(rsa_openssl_from_ssh(key)?)?;
let mut signer = Signer::new(digest, &keypair)?;
signer.update(&sign_request.data)?;
Ok(Signature {
algorithm: algorithm.to_string(),
blob: signer.sign_to_vec()?,
})
}
_ => Err(From::from("Signature for key type not implemented"))
}
} else {
Err(From::from("Failed to create signature: identity not found"))
}
}
fn handle_message(&self, request: Message) -> Result<Message, Box<dyn Error>> {
information!("Request: {:?}", request);
let response = match request {
Message::RequestIdentities => {
let mut identities = vec![];
for identity in self.identities.read().unwrap().iter() {
identities.push(message::Identity {
pubkey_blob: to_bytes(&identity.pubkey)?,
comment: identity.comment.clone(),
})
}
identities.iter().for_each(|i| {
information!("ssh-rsa {} {}", base64::encode(&i.pubkey_blob), &i.comment);
// information!(">> {}", String::from_utf8_lossy(&i.pubkey_blob));
});
Ok(Message::IdentitiesAnswer(identities))
}
Message::RemoveIdentity(identity) => {
let pubkey: PublicKey = from_bytes(&identity.pubkey_blob)?;
self.identity_remove(&pubkey)?;
Ok(Message::Success)
}
Message::AddIdentity(identity) => {
self.identity_add(Identity {
pubkey: PublicKey::from(&identity.privkey),
privkey: identity.privkey,
comment: identity.comment,
});
Ok(Message::Success)
}
Message::SignRequest(request) => {
let signature = to_bytes(&self.sign(&request)?)?;
Ok(Message::SignResponse(signature))
}
_ => Err(From::from(format!("Unknown message: {:?}", request)))
};
information!("Response {:?}", response);
return response;
}
}
impl Agent for KeyStorage {
type Error = ();
fn handle(&self, message: Message) -> Result<Message, ()> {
self.handle_message(message).or_else(|error| {
println!("Error handling message - {:?}", error);
Ok(Message::Failure)
})
}
}
fn rsa_openssl_from_ssh(ssh_rsa: &RsaPrivateKey) -> Result<Rsa<Private>, Box<dyn Error>> {
let n = BigNum::from_slice(&ssh_rsa.n)?;
let e = BigNum::from_slice(&ssh_rsa.e)?;
let d = BigNum::from_slice(&ssh_rsa.d)?;
let qi = BigNum::from_slice(&ssh_rsa.iqmp)?;
let p = BigNum::from_slice(&ssh_rsa.p)?;
let q = BigNum::from_slice(&ssh_rsa.q)?;
let dp = &d % &(&p - &BigNum::from_u32(1)?);
let dq = &d % &(&q - &BigNum::from_u32(1)?);
Ok(Rsa::from_private_components(n, e, d, p, q, dp, dq, qi)?)
}
// SSH_AUTH_SOCK=connect.sock ssh root@example.com
// SSH_AUTH_SOCK=connect.sock ssh-add -l
fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let agent = KeyStorage::new();
let socket = "connect.sock";
let _ = remove_file(socket);
information!("Start unix socket: {}", socket);
agent.run_unix(socket)?;
Ok(())
}
pub fn with_sign(mut vec: Vec<u8>) -> Vec<u8> {
if vec.len() > 0 && vec[0] >= 128 {
vec.insert(0, 0x00);
}
vec
}

35
justfile Normal file
View File

@@ -0,0 +1,35 @@
_:
@just --list
# publish
publish:
cargo publish --registry crates-io
# install card-cli
install:
cargo install --path .
# build without default features
build-simple:
cargo build --no-default-features
# install without default features
install-simple:
cargo install --no-default-features --path .
# run --help
help:
cargo r -- --help
# run pgp-card-list
pgp-list:
cargo r -- pgp-card-list
# run example: rsa_encrypt
example-rsa-encrypt:
cargo r --example rsa_encrypt
# run example: ssh_agent
example-ssh-agent:
cargo r --example ssh_agent

47
src/argsutil.rs Normal file
View File

@@ -0,0 +1,47 @@
use std::fs;
use std::fs::File;
use std::io::Read;
use clap::ArgMatches;
use rust_util::XResult;
use crate::digestutil::DigestAlgorithm;
pub fn get_sha256_digest_or_hash(sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> {
get_sha256_digest_or_hash_with_file_opt(sub_arg_matches, &None)
}
pub fn get_digest_or_hash(sub_arg_matches: &ArgMatches, digest: DigestAlgorithm) -> XResult<Vec<u8>> {
get_digest_or_hash_with_file_opt(sub_arg_matches, &None, digest)
}
pub fn get_sha256_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, file_opt: &Option<String>) -> XResult<Vec<u8>> {
get_digest_or_hash_with_file_opt(sub_arg_matches, file_opt, DigestAlgorithm::Sha256)
}
pub fn get_digest_or_hash_with_file_opt(sub_arg_matches: &ArgMatches, file_opt: &Option<String>, digest: DigestAlgorithm) -> XResult<Vec<u8>> {
let file_opt = file_opt.as_ref().map(String::as_str);
if let Some(file) = sub_arg_matches.value_of("file").or(file_opt) {
let metadata = opt_result!(fs::metadata(file), "Read file: {} metadata filed: {}", file);
if !metadata.is_file() {
return simple_error!("Not a file: {}", file);
}
if metadata.len() > 1024 * 1024 * 1024 {
return simple_error!("File: {} too large", file);
}
if metadata.len() > 100 * 1024 * 1024 {
warning!("File: {} is a large file, size: {} byte(s)", file, metadata.len());
}
let mut f = opt_result!(File::open(file), "Open file: {} failed: {}", file);
let mut content = vec![];
opt_result!(f.read_to_end(&mut content), "Read file: {} failed: {}", file);
Ok(digest.digest(&content))
} else if let Some(input) = sub_arg_matches.value_of("input") {
Ok(digest.digest_str(input))
} else if let Some(hash_hex) = sub_arg_matches.value_of("hash-hex") {
Ok(opt_result!(hex::decode(hash_hex), "Parse hash-hex failed: {}"))
} else {
simple_error!("--file, --input or --hash-hex must assign at least one")
}
}

View File

@@ -1,10 +1,7 @@
use std::collections::BTreeMap;
use std::ops::Deref;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use yubico_manager::config::{Config, Mode, Slot};
use yubico_manager::Yubico; use crate::{cmdutil, hmacutil};
pub struct CommandImpl; pub struct CommandImpl;
@@ -17,75 +14,17 @@ impl Command for CommandImpl {
.arg(Arg::with_name("challenge-hex").short("x").long("challenge-hex").takes_value(true).help("Challenge HEX")) .arg(Arg::with_name("challenge-hex").short("x").long("challenge-hex").takes_value(true).help("Challenge HEX"))
.arg(Arg::with_name("sha1").short("1").long("sha1").help("Output SHA1")) .arg(Arg::with_name("sha1").short("1").long("sha1").help("Output SHA1"))
.arg(Arg::with_name("sha256").short("2").long("sha256").help("Output SHA256")) .arg(Arg::with_name("sha256").short("2").long("sha256").help("Output SHA256"))
.arg(Arg::with_name("sha384").short("3").long("sha384").help("Output SHA256")) .arg(Arg::with_name("sha384").short("3").long("sha384").help("Output SHA384"))
.arg(Arg::with_name("sha512").short("5").long("sha512").help("Output SHA256")) .arg(Arg::with_name("sha512").short("5").long("sha512").help("Output SHA512"))
.arg(Arg::with_name("json").long("json").help("JSON output")) .arg(cmdutil::build_json_arg())
} }
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json"); let json_output = cmdutil::check_json_output(sub_arg_matches);
if json_output { rust_util::util_msg::set_logger_std_out(false); }
let sha1_output = sub_arg_matches.is_present("sha1"); let challenge_bytes = hmacutil::get_challenge_bytes(sub_arg_matches)?;
let sha256_output = sub_arg_matches.is_present("sha256"); let hmac_result = hmacutil::compute_yubikey_hmac(&challenge_bytes)?;
let sha384_output = sub_arg_matches.is_present("sha384"); hmacutil::output_hmac_result(sub_arg_matches, json_output, challenge_bytes, &hmac_result);
let sha512_output = sub_arg_matches.is_present("sha512");
let challenge_bytes: Vec<u8> = if let Some(challenge) = sub_arg_matches.value_of("challenge") {
challenge.as_bytes().to_vec()
} else if let Some(challenge_hex) = sub_arg_matches.value_of("challenge-hex") {
opt_result!(hex::decode(challenge_hex), "Decode challenge hex: {}, failed: {}", challenge_hex)
} else {
return simple_error!("Challenge must assigned");
};
// Challenge can not be greater than 64 bytes
if challenge_bytes.len() > 64 {
return simple_error!("Challenge bytes is: {}, more than 64", challenge_bytes.len());
}
let mut yubi = Yubico::new();
if let Ok(device) = yubi.find_yubikey() {
success!("Found key, Vendor ID: {:?} Product ID {:?}", device.vendor_id, device.product_id);
let config = Config::default()
.set_vendor_id(device.vendor_id)
.set_product_id(device.product_id)
.set_variable_size(true)
.set_mode(Mode::Sha1)
.set_slot(Slot::Slot2);
// In HMAC Mode, the result will always be the SAME for the SAME provided challenge
let hmac_result = opt_result!(yubi.challenge_response_hmac(&challenge_bytes, config), "Challenge HMAC failed: {}");
// Just for debug, lets check the hex
let v: &[u8] = hmac_result.deref();
let hex_string = hex::encode(v);
let hex_sha1 = iff!(sha1_output, Some(crate::digest::sha1_bytes(v)), None);
let hex_sha256 = iff!(sha256_output, Some(crate::digest::sha256_bytes(v)), None);
let hex_sha384 = iff!(sha384_output, Some(crate::digest::sha384_bytes(v)), None);
let hex_sha512 = iff!(sha512_output, Some(crate::digest::sha512_bytes(v)), None);
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("challenge_hex", hex::encode(challenge_bytes));
json.insert("response_hex", hex_string);
hex_sha1.map(|hex_sha1| json.insert("response_sha1_hex", hex::encode(hex_sha1)));
hex_sha256.map(|hex_sha256| json.insert("response_sha256_hex", hex::encode(hex_sha256)));
hex_sha384.map(|hex_sha384| json.insert("response_sha384_hex", hex::encode(hex_sha384)));
hex_sha512.map(|hex_sha512| json.insert("response_sha512_hex", hex::encode(hex_sha512)));
println!("{}", serde_json::to_string_pretty(&json).expect("Convert to JSON failed!"));
} else {
success!("Challenge HEX: {}", hex::encode(challenge_bytes));
success!("Response HEX: {}", hex_string);
hex_sha256.map(|hex_sha256| success!("Response SHA256 HEX: {}", hex::encode(hex_sha256)));
hex_sha384.map(|hex_sha384| success!("Response SHA384 HEX: {}", hex::encode(hex_sha384)));
hex_sha512.map(|hex_sha512| success!("Response SHA512 HEX: {}", hex::encode(hex_sha512)));
}
} else {
warning!("YubiKey not found");
}
Ok(None) Ok(None)
} }

View File

@@ -11,7 +11,7 @@ impl Command for CommandImpl {
fn name(&self) -> &str { "chall-config" } fn name(&self) -> &str { "chall-config" }
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("YubiKey challenge-response HMAC config") SubCommand::with_name(self.name()).about("YubiKey challenge-response HMAC configuration")
.arg(Arg::with_name("secret").short("s").long("secret").takes_value(true).help("Secret")) .arg(Arg::with_name("secret").short("s").long("secret").takes_value(true).help("Secret"))
.arg(Arg::with_name("secret-hex").short("x").long("secret-hex").takes_value(true).help("Secret HEX")) .arg(Arg::with_name("secret-hex").short("x").long("secret-hex").takes_value(true).help("Secret HEX"))
.arg(Arg::with_name("button-press").long("button-press").help("Require button press")) .arg(Arg::with_name("button-press").long("button-press").help("Require button press"))
@@ -36,9 +36,15 @@ impl Command for CommandImpl {
} }
let mut yubi = Yubico::new(); let mut yubi = Yubico::new();
if let Ok(device) = yubi.find_yubikey() { let device = match yubi.find_yubikey() {
success!("Found key, Vendor ID: {:?} Product ID {:?}", device.vendor_id, device.product_id); Ok(device) => device,
Err(_) => {
warning!("YubiKey not found");
return Ok(Some(1));
}
};
success!("Found key, Vendor ID: {:?} Product ID {:?}", device.vendor_id, device.product_id);
let config = Config::default() let config = Config::default()
.set_vendor_id(device.vendor_id) .set_vendor_id(device.vendor_id)
.set_product_id(device.product_id) .set_product_id(device.product_id)
@@ -56,9 +62,6 @@ impl Command for CommandImpl {
} else { } else {
success!("Device configured"); success!("Device configured");
} }
} else {
warning!("YubiKey not found");
}
Ok(None) Ok(None)
} }

View File

@@ -0,0 +1,46 @@
use crate::util::base64_encode;
use crate::{cmdutil, ecutil, util};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"convert-jwk-to-pem"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Convert PEM to JWK")
.arg(
Arg::with_name("jwk")
.long("jwk")
.required(true)
.takes_value(true)
.help("JWK"),
)
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let jwk = sub_arg_matches.value_of("jwk").unwrap();
let json_output = cmdutil::check_json_output(sub_arg_matches);
let (public_key_pem, public_ker_der) = ecutil::convert_ec_jwk_to_public_key(jwk)?;
let mut json = BTreeMap::<&'_ str, String>::new();
if json_output {
json.insert("public_key_pem", public_key_pem);
json.insert("public_key_base64", base64_encode(&public_ker_der));
util::print_pretty_json(&json);
} else {
information!("Public key PEM:\n{}", &public_key_pem);
information!("\nPublic key base64:\n{}", base64_encode(&public_ker_der));
}
Ok(None)
}
}

View File

@@ -0,0 +1,42 @@
use crate::{ecutil, rsautil, util};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use serde_json::Value;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"convert-pem-to-jwk"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Convert PEM to JWK")
.arg(
Arg::with_name("public-key")
.long("public-key")
.required(true)
.takes_value(true)
.help("Public key (PEM, base64(DER) format)"),
)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let public_key = sub_arg_matches.value_of("public-key").unwrap();
let jwk = match ecutil::convert_ec_public_key_to_jwk(public_key) {
Ok(jwk) => jwk,
Err(_) => match rsautil::convert_rsa_to_jwk(public_key) {
Ok(jwk) => jwk,
Err(_) => return simple_error!("Invalid public key."),
},
};
let jwk_value: Value = serde_json::from_str(&jwk).unwrap();
util::print_pretty_json(&jwk_value);
Ok(None)
}
}

77
src/cmd_ec_verify.rs Normal file
View File

@@ -0,0 +1,77 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use crate::ecdsautil::EcdsaAlgorithm;
use crate::{argsutil, cmdutil, ecdsautil, util};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "ec-verify" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("ECDSA verify subcommand")
.arg(Arg::with_name("public-key-hex").short("k").long("public-key-hex").takes_value(true).help("Public key hex (starts with 04)"))
.arg(Arg::with_name("signature-hex").short("t").long("signature-hex").takes_value(true).help("Signature"))
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).help("Input file"))
.arg(Arg::with_name("input").short("i").long("input").takes_value(true).help("Input"))
.arg(Arg::with_name("hash-hex").short("x").long("hash-hex").takes_value(true).help("Hash"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let hash_bytes = argsutil::get_sha256_digest_or_hash(sub_arg_matches)?;
let public_key = if let Some(public_key_hex) = sub_arg_matches.value_of("public-key-hex") {
opt_result!(hex::decode(public_key_hex), "Parse public-key-hex failed: {}")
} else {
return simple_error!("--public-hex required.");
};
let signature = if let Some(signature_hex) = sub_arg_matches.value_of("signature-hex") {
opt_result!(hex::decode(signature_hex), "Parse signature-hex failed: {}")
} else {
return simple_error!("--signature-hex required.");
};
let ecdsa_algorithm = match public_key.len() {
65 => EcdsaAlgorithm::P256,
97 => EcdsaAlgorithm::P384,
_ => return simple_error!("Invalid public key: {}", hex::encode(&public_key)),
};
let mut json = BTreeMap::<&'_ str, String>::new();
debugging!("ECDSA public key point: {}", hex::encode(&public_key));
information!("Pre hash: {}", hex::encode(&hash_bytes));
debugging!("Signature: {}", hex::encode(&signature));
if json_output {
json.insert("public_key_hex", hex::encode(&public_key));
json.insert("hash_hex", hex::encode(&hash_bytes));
json.insert("signature_hex", hex::encode(&signature));
}
match ecdsautil::ecdsa_verify(ecdsa_algorithm, &public_key, &hash_bytes, &signature) {
Ok(_) => {
success!("Verify ECDSA succeed.");
if json_output {
json.insert("success", "true".to_string());
}
}
Err(e) => {
failure!("Verify ECDSA failed: {}", &e);
if json_output {
json.insert("success", "false".to_string());
json.insert("message", format!("{}", e));
}
}
}
if json_output {
util::print_pretty_json(&json);
}
Ok(None)
}
}

142
src/cmd_external_ecdh.rs Normal file
View File

@@ -0,0 +1,142 @@
use crate::keyutil::{parse_key_uri, KeyAlgorithmId, KeyUri, KeyUsage};
use crate::pivutil::ToStr;
use crate::{cmd_hmac_decrypt, cmd_se_ecdh, cmdutil, ecdhutil, mlkemutil, pivutil, seutil, util, yubikeyutil};
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::Value;
use std::collections::BTreeMap;
use yubikey::piv::{decrypt_data, AlgorithmId};
use crate::util::try_decode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"external_ecdh"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("External ECDH subcommand")
.arg(cmdutil::build_parameter_arg())
.arg(cmdutil::build_epk_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let parameter = sub_arg_matches.value_of("parameter").unwrap();
let epk = sub_arg_matches.value_of("epk").unwrap();
let ephemeral_public_key_der_bytes = cmd_se_ecdh::parse_epk(epk)?;
let mut json = BTreeMap::new();
let key_uri = parse_key_uri(parameter)?;
match ecdh(&ephemeral_public_key_der_bytes, key_uri, sub_arg_matches) {
Ok(shared_secret_bytes) => {
json.insert("success", Value::Bool(true));
json.insert(
"shared_secret_hex",
hex::encode(&shared_secret_bytes).into(),
);
}
Err(e) => {
json.insert("success", Value::Bool(false));
json.insert("error", e.to_string().into());
}
}
util::print_pretty_json(&json);
Ok(None)
}
}
pub fn ecdh(
ephemeral_public_key_bytes: &[u8],
key_uri: KeyUri,
sub_arg_matches: &ArgMatches,
) -> XResult<Vec<u8>> {
match key_uri {
KeyUri::SecureEnclave(key) => {
if key.usage != KeyUsage::Singing {
return simple_error!("Not singing key");
}
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.private_key)?;
seutil::secure_enclave_p256_dh(&private_key, ephemeral_public_key_bytes)
}
KeyUri::YubikeyPiv(key) => {
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
let pin_opt = pivutil::check_read_pin(&mut yk, key.slot, sub_arg_matches);
if let Some(pin) = pin_opt {
opt_result!(
yk.verify_pin(pin.as_bytes()),
"YubiKey verify pin failed: {}"
);
}
let algorithm = opt_value_result!(
KeyAlgorithmId::to_algorithm_id(key.algorithm),
"Yubikey not supported algorithm: {}",
key.algorithm.to_str()
);
let epk_bytes = match algorithm {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
return simple_error!("Algorithm is not supported: {:?}", algorithm)
}
AlgorithmId::EccP256 => {
use p256::{elliptic_curve::sec1::ToEncodedPoint, PublicKey};
use spki::DecodePublicKey;
let public_key = opt_result!(PublicKey::from_public_key_der(
ephemeral_public_key_bytes),"Parse P256 ephemeral public key failed: {}");
public_key.to_encoded_point(false).as_bytes().to_vec()
}
AlgorithmId::EccP384 => {
use p384::{elliptic_curve::sec1::ToEncodedPoint, PublicKey};
use spki::DecodePublicKey;
let public_key = opt_result!(PublicKey::from_public_key_der(
ephemeral_public_key_bytes), "Parse P384 ephemeral public key failed: {}");
public_key.to_encoded_point(false).as_bytes().to_vec()
}
};
let decrypted_shared_secret = opt_result!(
decrypt_data(&mut yk, &epk_bytes, algorithm, key.slot,),
"Decrypt piv failed: {}"
);
Ok(decrypted_shared_secret.to_vec())
}
KeyUri::YubikeyHmacEncSoft(key) => {
if key.algorithm.is_ecc() {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let private_key_bytes = try_decode(&private_key)?;
if let Ok(shared_secret) = ecdhutil::parse_p256_private_and_ecdh(&private_key_bytes, ephemeral_public_key_bytes) {
return Ok(shared_secret.to_vec());
}
if let Ok(shared_secret) = ecdhutil::parse_p384_private_and_ecdh(&private_key_bytes, ephemeral_public_key_bytes) {
return Ok(shared_secret.to_vec());
}
if let Ok(shared_secret) = ecdhutil::parse_p521_private_and_ecdh(&private_key_bytes, ephemeral_public_key_bytes) {
return Ok(shared_secret.to_vec());
}
simple_error!("Invalid EC private key and/or ephemeral public key")
} else if key.algorithm.is_mlkem() {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let private_key_bytes = try_decode(&private_key)?;
if let Ok((_, shared_secret)) = mlkemutil::try_parse_decapsulate_key_private_then_decapsulate(&private_key_bytes, ephemeral_public_key_bytes) {
return Ok(shared_secret);
}
simple_error!("Invalid ML-KEM private key and/or ephemeral public key")
} else {
simple_error!("Invalid algorithm: {}", key.algorithm.to_str())
}
}
KeyUri::ExternalCommand(key) => {
let parameter = cmd_hmac_decrypt::try_decrypt(&mut None, &key.parameter)?;
external_command_rs::external_ecdh(&key.external_command, &parameter, ephemeral_public_key_bytes)
}
}
}

View File

@@ -0,0 +1,121 @@
use crate::keyutil::{parse_key_uri, KeyUri, KeyUsage};
use crate::util::{base64_decode, base64_encode};
use crate::yubikeyutil::find_key_or_error;
use crate::{cmd_hmac_decrypt, cmdutil, ecdsautil, seutil, util, yubikeyutil};
use clap::{App, Arg, ArgMatches, SubCommand};
use ecdsa::elliptic_curve::pkcs8::der::Encode;
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::Value;
use std::collections::BTreeMap;
use rsa::RsaPrivateKey;
use spki::EncodePublicKey;
use x509_parser::parse_x509_certificate;
use crate::mlkemutil::{try_parse_decapsulate_key_private_get_encapsulate, try_parse_encapsulation_key_public_then_encapsulate};
use crate::pivutil::ToStr;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"external_public_key"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("External public key subcommand")
.arg(cmdutil::build_parameter_arg())
.arg(cmdutil::build_serial_arg())
.arg(Arg::with_name("x-generate-shared-secret").long("x-generate-shared-secret").help("Generate a shared secret"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let parameter = sub_arg_matches.value_of("parameter").unwrap();
let serial_opt = sub_arg_matches.value_of("serial");
let generate_shared_secret = sub_arg_matches.is_present("x-generate-shared-secret");
let mut json = BTreeMap::new();
match fetch_public_key(parameter, &serial_opt, generate_shared_secret, &mut json) {
Ok(public_key_bytes) => {
json.insert("success", Value::Bool(true));
json.insert("public_key_base64", base64_encode(&public_key_bytes).into());
}
Err(e) => {
json.insert("success", Value::Bool(false));
json.insert("error", e.to_string().into());
}
}
util::print_pretty_json(&json);
Ok(None)
}
}
fn fetch_public_key(parameter: &str, serial_opt: &Option<&str>, generate_shared_secret: bool, json: &mut BTreeMap<&str, Value>) -> XResult<Vec<u8>> {
let key_uri = parse_key_uri(parameter)?;
match key_uri {
KeyUri::SecureEnclave(key) => {
if key.usage != KeyUsage::Singing {
simple_error!("Not singing key")
} else {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.private_key)?;
let (_, public_key_der, _) =
seutil::recover_secure_enclave_p256_public_key(&private_key, true)?;
Ok(public_key_der)
}
}
KeyUri::YubikeyPiv(key) => {
let mut yk = yubikeyutil::open_yubikey_with_serial(serial_opt)?;
if let Some(key) = find_key_or_error(&mut yk, &key.slot)? {
let cert_der = key.certificate().cert.to_der()?;
let x509_certificate = parse_x509_certificate(cert_der.as_slice()).unwrap().1;
let public_key_bytes = x509_certificate.public_key().raw;
return Ok(public_key_bytes.to_vec());
}
simple_error!("Slot {} not found", key.slot)
}
KeyUri::YubikeyHmacEncSoft(key) => {
if key.algorithm.is_ecc() {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let p256_public_key = ecdsautil::parse_p256_private_key_to_public_key(&private_key).ok();
let p384_public_key = ecdsautil::parse_p384_private_key_to_public_key(&private_key).ok();
let p521_public_key = ecdsautil::parse_p521_private_key_to_public_key(&private_key).ok();
if let Some(p256_public_key) = p256_public_key {
return Ok(p256_public_key);
}
if let Some(p384_public_key) = p384_public_key {
return Ok(p384_public_key);
}
if let Some(p521_public_key) = p521_public_key {
return Ok(p521_public_key);
}
simple_error!("Invalid hmac enc private key")
} else if key.algorithm.is_rsa() {
use rsa::pkcs8::DecodePrivateKey;
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let private_key_der = base64_decode(&private_key)?;
let rsa_private_key = RsaPrivateKey::from_pkcs8_der(&private_key_der)?;
Ok(rsa_private_key.to_public_key().to_public_key_der()?.to_vec())
} else if key.algorithm.is_mlkem() {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let private_key_der = base64_decode(&private_key)?;
let (_, ek_public) = try_parse_decapsulate_key_private_get_encapsulate(&private_key_der)?;
if generate_shared_secret {
if let Ok((mlkem_len, ciphertext, shared_secret)) = try_parse_encapsulation_key_public_then_encapsulate(&ek_public) {
json.insert("algorithm", mlkem_len.to_str().into());
json.insert("ciphertext", base64_encode(ciphertext).into());
json.insert("shared_secret", hex::encode(&shared_secret).into());
}
}
Ok(ek_public)
} else {
simple_error!("Invalid algorithm: {}", key.algorithm.to_str())
}
}
KeyUri::ExternalCommand(key) => {
let parameter = cmd_hmac_decrypt::try_decrypt(&mut None, &key.parameter)?;
external_command_rs::external_public_key(&key.external_command, &parameter)
}
}
}

169
src/cmd_external_sign.rs Normal file
View File

@@ -0,0 +1,169 @@
use crate::cmd_sign_jwt_piv::digest_by_jwt_algorithm;
use crate::cmd_sign_jwt_soft::{convert_jwt_algorithm_to_ecdsa_algorithm, parse_ecdsa_private_key};
use crate::ecdsautil::EcdsaSignType;
use crate::keyutil::{parse_key_uri, KeyAlgorithmId, KeyUri, KeyUsage, YubikeyPivKey};
use crate::pivutil::ToStr;
use crate::rsautil::RsaSignAlgorithm;
use crate::util::{base64_decode, base64_encode};
use crate::{cmd_hmac_decrypt, cmdutil, ecdsautil, pivutil, rsautil, seutil, util, yubikeyutil};
use clap::{App, ArgMatches, SubCommand};
use jwt::AlgorithmType;
use rsa::RsaPrivateKey;
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::Value;
use std::collections::BTreeMap;
use swift_secure_enclave_tool_rs::DigestType;
use yubikey::piv::sign_data;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"external_sign"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("External sign subcommand")
.arg(cmdutil::build_alg_arg())
.arg(cmdutil::build_parameter_arg())
.arg(cmdutil::build_message_arg())
.arg(cmdutil::build_message_type_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let alg = sub_arg_matches.value_of("alg").unwrap();
let parameter = sub_arg_matches.value_of("parameter").unwrap();
let message_base64 = sub_arg_matches.value_of("message-base64").unwrap();
let message_type = sub_arg_matches.value_of("message-type");
let message_bytes = base64_decode(message_base64)?;
let mut json = BTreeMap::new();
let key_uri = parse_key_uri(parameter)?;
match sign(alg, &message_bytes, message_type, key_uri, sub_arg_matches) {
Ok(signature_bytes) => {
json.insert("success", Value::Bool(true));
json.insert("signature_base64", base64_encode(&signature_bytes).into());
}
Err(e) => {
json.insert("success", Value::Bool(false));
json.insert("error", e.to_string().into());
}
}
util::print_pretty_json(&json);
Ok(None)
}
}
pub fn sign(alg: &str, message: &[u8], message_type: Option<&str>, key_uri: KeyUri, sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> {
let digest_type = DigestType::parse(message_type)?;
if let Some(bytes_len) = digest_type.bytes() {
if message.len() != bytes_len as usize {
return simple_error!("Invalid message length, requires: {}, actual: {}", bytes_len, message.len());
}
}
let is_raw = DigestType::Raw == digest_type;
match key_uri {
KeyUri::SecureEnclave(key) => {
if "ES256" != alg {
return simple_error!("Invalid alg: {}", alg);
}
if key.usage != KeyUsage::Singing {
return simple_error!("Not singing key");
}
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.private_key)?;
seutil::secure_enclave_p256_sign(&private_key, message, digest_type)
}
KeyUri::YubikeyPiv(key) => {
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
let pin_opt = pivutil::check_read_pin(&mut yk, key.slot, sub_arg_matches);
// FIXME Check YubiKey slot algorithm
let jwt_algorithm = get_jwt_algorithm(&key, alg)?;
if let Some(pin) = pin_opt {
opt_result!(
yk.verify_pin(pin.as_bytes()),
"YubiKey verify pin failed: {}"
);
}
let algorithm = opt_value_result!(
KeyAlgorithmId::to_algorithm_id(key.algorithm),
"Yubikey not supported algorithm: {}",
key.algorithm.to_str()
);
let raw_in = iff!(is_raw, digest_by_jwt_algorithm(jwt_algorithm, message)?, message.to_vec());
let signed_data = opt_result!(
sign_data(&mut yk, &raw_in, algorithm, key.slot),
"Sign YubiKey failed: {}"
);
Ok(signed_data.to_vec())
}
KeyUri::YubikeyHmacEncSoft(key) => {
if key.algorithm.is_ecc() {
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let (jwt_algorithm, private_key_d) = parse_ecdsa_private_key(&private_key)?;
let raw_in = iff!(is_raw, digest_by_jwt_algorithm(jwt_algorithm, message)?, message.to_vec());
let ecdsa_algorithm = convert_jwt_algorithm_to_ecdsa_algorithm(jwt_algorithm)?;
let signed_data = ecdsautil::ecdsa_sign(
ecdsa_algorithm,
&private_key_d,
&raw_in,
EcdsaSignType::Der,
)?;
Ok(signed_data)
} else if key.algorithm.is_rsa() {
use rsa::pkcs8::DecodePrivateKey;
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &key.hmac_enc_private_key)?;
let private_key_der = base64_decode(&private_key)?;
let rsa_private_key = RsaPrivateKey::from_pkcs8_der(&private_key_der)?;
let rsa_sign_algorithm =
opt_value_result!(RsaSignAlgorithm::from_str(alg), "Invalid --alg: {}", alg);
rsautil::sign(&rsa_private_key, rsa_sign_algorithm, message, is_raw)
} else {
simple_error!("Invalid algorithm: {}", key.algorithm.to_str())
}
}
KeyUri::ExternalCommand(key) => {
let parameter = cmd_hmac_decrypt::try_decrypt(&mut None, &key.parameter)?;
let alg = key.algorithm.as_jwa_name();
let signature = external_command_rs::external_sign_digested(
&key.external_command, &parameter, alg, message, digest_type.to_str())?;
Ok(signature)
}
}
}
fn get_jwt_algorithm(key: &YubikeyPivKey, alg: &str) -> XResult<AlgorithmType> {
let jwt_algorithm = match alg {
"ES256" => AlgorithmType::Es256,
"ES384" => AlgorithmType::Es384,
"ES512" => AlgorithmType::Es512,
"RS256" => AlgorithmType::Rs256,
_ => return simple_error!("Invalid alg: {}", alg),
};
if key.algorithm == KeyAlgorithmId::Rsa1024 {
return simple_error!("Invalid algorithm: RSA1024");
}
let is_p256_mismatch =
key.algorithm == KeyAlgorithmId::EccP256 && jwt_algorithm != AlgorithmType::Es256;
let is_p384_mismatch =
key.algorithm == KeyAlgorithmId::EccP384 && jwt_algorithm != AlgorithmType::Es384;
let is_p521_mismatch =
key.algorithm == KeyAlgorithmId::EccP521 && jwt_algorithm != AlgorithmType::Es512;
let is_rsa_mismatch =
key.algorithm == KeyAlgorithmId::Rsa2048 && jwt_algorithm != AlgorithmType::Rs256;
if is_p256_mismatch || is_p384_mismatch || is_p521_mismatch || is_rsa_mismatch {
return simple_error!("Invalid algorithm: {} vs {}", key.algorithm.to_str(), alg);
}
Ok(jwt_algorithm)
}

40
src/cmd_external_spec.rs Normal file
View File

@@ -0,0 +1,40 @@
use crate::util;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use serde_json::Value;
use std::collections::BTreeMap;
pub struct CommandImpl;
// https://openwebstandard.org/rfc1
impl Command for CommandImpl {
fn name(&self) -> &str {
"external_spec"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("External spec subcommand")
.arg(Arg::with_name("external-command").long("external-command").takes_value(true).help("External command"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let external_command_opt = sub_arg_matches.value_of("external-command");
if let Some(external_command) = external_command_opt {
let spec = external_command_rs::external_spec(external_command)?;
util::print_pretty_json(&spec);
} else {
let mut json = BTreeMap::new();
json.insert("success", Value::Bool(true));
json.insert(
"agent",
format!("card-external-provider/{}", env!("CARGO_PKG_VERSION")).into(),
);
json.insert("specification", "External/1.0.0-alpha".into());
json.insert("commands", vec!["external_public_key", "external_sign", "external_ecdh"].into());
util::print_pretty_json(&json);
}
Ok(None)
}
}

193
src/cmd_file_sign.rs Normal file
View File

@@ -0,0 +1,193 @@
use std::fs;
use std::time::SystemTime;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::{util_msg, XResult};
use rust_util::util_clap::{Command, CommandError};
use serde::Deserialize;
use spki::der::Encode;
use x509_parser::nom::AsBytes;
use yubikey::{Key, YubiKey};
use yubikey::piv::{sign_data, SlotId};
use crate::{argsutil, cmdutil, pinutil, pivutil};
use crate::digestutil::sha256_bytes;
use crate::signfile::{CERTIFICATES_SEARCH_URL, HASH_ALGORITHM_SHA256, SIGNATURE_ALGORITHM_SHA256_WITH_ECDSA, SignFileRequest, SIMPLE_SIG_SCHEMA, SimpleSignFile, SimpleSignFileSignature};
use crate::util::base64_encode;
pub struct CommandImpl;
// Format:
// {
// "schema": "https://openwebstandard.org/simple-sign-file/v1",
// "version": "v1",
// "filename": "example.zip",
// "digest": "sha256-HEX(SHA256(filename-content))",
// "timestamp": 1700964163340,
// "attributes": "****",
// "comment": "***",
// "signatures": [{
// "algorithm": "SHA256withECDSA",
// "signature": "Base64(Sign(SHA256("v1"||TLV(filename)||TLV(timestamp)||TLV(attributes)||TLV(comment)||TLV(digest))))",
// "certificates": ["-----BEGIN CERTIFICATE-----\n*****\n-----END CERTIFICATE-----", ...]
// }]
// }
// v1 only support SHA256
// all hex is in lower case default
// file ext: *.simple-sig
impl Command for CommandImpl {
fn name(&self) -> &str { "file-sign" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV sign(with SHA256) subcommand")
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).required(true).help("Input file"))
.arg(Arg::with_name("filename").short("n").long("filename").takes_value(true).help("Filename"))
.arg(Arg::with_name("sign-file").short("S").long("sign-file").takes_value(false).help("Sign file"))
.arg(Arg::with_name("comment").short("c").long("comment").takes_value(true).help("Comment"))
.arg(Arg::with_name("attributes").short("a").long("attributes").takes_value(true).help("Attributes"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
util_msg::set_logger_std_out(false);
let filename_opt = sub_arg_matches.value_of("filename").map(ToString::to_string);
let comment_opt = sub_arg_matches.value_of("comment").map(ToString::to_string);
let attributes_opt = sub_arg_matches.value_of("attributes").map(ToString::to_string);
let pin_opt = pinutil::read_pin(sub_arg_matches);
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
// TODO read from stream not in memory
let file_digest = argsutil::get_sha256_digest_or_hash(sub_arg_matches)?;
debugging!("File digest: {}", hex::encode(&file_digest));
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let slot_id = pivutil::get_slot_id(slot)?;
let key = find_key(&mut yk, &slot_id)?;
let key = opt_value_result!(key, "Cannot find key in slot: {}", slot_id);
let certificate = key.certificate();
let tbs_certificate = &certificate.cert.tbs_certificate;
let spki_der = opt_result!(tbs_certificate.subject_public_key_info.to_der(), "SPKI to DER failed: {}");
debugging!("Slot public DER: {}", hex::encode(&spki_der));
let spki_der_fingerprint = hex::encode(sha256_bytes(&spki_der));
debugging!("Slot public fingerprint: {}", &spki_der_fingerprint);
let certificates = fetch_certificates(&spki_der_fingerprint)?;
let algorithm_id = opt_result!(
pivutil::get_algorithm_id_by_certificate(certificate), "Get slot key algorithm failed: {}");
debugging!("PIV algorithm: {:?}", algorithm_id);
if let Some(pin) = &pin_opt {
debugging!("PIN is assigned.");
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
}
let filename_opt = match filename_opt {
Some(filename) => Some(filename),
None => sub_arg_matches.value_of("file").map(|f| {
if f.contains('/') {
f.split('/').last().unwrap().to_string()
} else {
f.to_string()
}
}),
};
let sign_file = sub_arg_matches.value_of("sign-file").map(ToString::to_string).or_else(|| {
filename_opt.clone().map(|f| format!("{}.simple-sig", f))
});
let sign_file_request = SignFileRequest {
filename: match filename_opt {
None => None,
Some(filename) => iff!(filename.is_empty(), None, Some(filename)),
},
digest: file_digest.clone(),
timestamp: SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as i64,
attributes: attributes_opt,
comment: comment_opt,
};
let tobe_signed = sign_file_request.get_tobe_signed();
debugging!("Tobe signed: {}", hex::encode(&tobe_signed));
let tobe_signed_digest = sha256_bytes(&tobe_signed);
debugging!("Tobe signed digest: {}", hex::encode(&tobe_signed_digest));
let signed_data = opt_result!(sign_data(&mut yk, &tobe_signed_digest, algorithm_id, slot_id), "Sign PIV failed: {}");
let signature_bytes = signed_data.as_slice();
debugging!("Tobe signed signature: {}", hex::encode(signature_bytes));
let signature = SimpleSignFileSignature {
algorithm: SIGNATURE_ALGORITHM_SHA256_WITH_ECDSA.to_string(),
signature: base64_encode(signature_bytes),
certificates,
};
let simple_sig = SimpleSignFile {
schema: SIMPLE_SIG_SCHEMA.to_string(),
filename: sign_file_request.filename.clone(),
digest: format!("{}-{}", HASH_ALGORITHM_SHA256, hex::encode(&sign_file_request.digest)),
timestamp: sign_file_request.timestamp,
attributes: sign_file_request.attributes.clone(),
comment: sign_file_request.comment.clone(),
signatures: vec![signature],
};
let sign_file_content = serde_json::to_string_pretty(&simple_sig).unwrap();
if let Some(sign_file) = sign_file {
if fs::read(&sign_file).is_ok() {
warning!("Simple sign file: {} exists", sign_file);
} else {
match fs::write(&sign_file, &sign_file_content) {
Ok(_) => success!("Write simple sign file: {} succeed", sign_file),
Err(e) => failure!("Write simple sign file: {} failed: {}", sign_file, e),
}
}
}
println!("{}", sign_file_content);
Ok(None)
}
}
#[derive(Deserialize)]
struct FetchCertificateResponseData {
pub certificates: Vec<String>,
}
#[derive(Deserialize)]
struct FetchCertificateResponse {
pub status: i32,
pub message: String,
pub data: Option<FetchCertificateResponseData>,
}
fn fetch_certificates(fingerprint: &str) -> XResult<Vec<String>> {
let url = format!("{}{}", CERTIFICATES_SEARCH_URL, fingerprint);
let certificates_response = opt_result!( reqwest::blocking::get(url), "Fetch certificates failed: {}");
let certificates_response_bytes = opt_result!(certificates_response.bytes(), "Fetch certificates failed: {}");
let response = opt_result!(
serde_json::from_slice::<FetchCertificateResponse>(certificates_response_bytes.as_bytes()),
"Parse fetch certificates response failed: {}");
if response.status != 200 {
return simple_error!("Fetch certificates failed, status: {}, message: {}", response.status, response.message);
}
match response.data {
None => simple_error!("Fetch certificates failed, empty."),
Some(data) => Ok(data.certificates),
}
}
fn find_key(yk: &mut YubiKey, slot_id: &SlotId) -> XResult<Option<Key>> {
match Key::list(yk) {
Err(e) => warning!("List keys failed: {}", e),
Ok(keys) => for k in keys {
let slot_str = format!("{:x}", Into::<u8>::into(k.slot()));
if pivutil::slot_equals(slot_id, &slot_str) {
return Ok(Some(k));
}
}
}
Ok(None)
}

129
src/cmd_file_verify.rs Normal file
View File

@@ -0,0 +1,129 @@
use std::fs;
use std::ops::Add;
use std::time::{Duration, SystemTime};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use x509_parser::parse_x509_certificate;
use x509_parser::pem::parse_x509_pem;
use x509_parser::public_key::PublicKey;
use x509_parser::time::ASN1Time;
use crate::argsutil;
use crate::digestutil::sha256_bytes;
use crate::signfile::{SignFileRequest, SIMPLE_SIG_SCHEMA, SimpleSignFile};
use crate::util::base64_decode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "file-verify" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV verify(with SHA256) subcommand")
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).required(false).help("Input file"))
.arg(Arg::with_name("sign-file").short("S").long("sign-file").takes_value(true).help("Sign file"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
util_msg::set_logger_std_out(false);
let sign_file = sub_arg_matches.value_of("sign-file").map(ToString::to_string).unwrap();
let sign_file_content = opt_result!(fs::read_to_string(&sign_file), "Read file: {}, failed: {}", sign_file);
let simple_sign_file: SimpleSignFile = opt_result!(serde_json::from_str(&sign_file_content), "Parse file: {}, failed: {}", sign_file);
if SIMPLE_SIG_SCHEMA != simple_sign_file.schema {
return simple_error!("File: {} format error: bad schema", sign_file);
}
information!("File name: {}", simple_sign_file.filename.as_deref().unwrap_or("<none>"));
information!("Digest: {}", &simple_sign_file.digest);
let sign_time = SystemTime::UNIX_EPOCH.add(Duration::from_millis(simple_sign_file.timestamp as u64));
let format_time = simpledateformat::fmt("yyyy-MM-dd HH:mm:ssz").unwrap().format_local(sign_time);
information!("Timestamp: {}", format_time);
if let Some(attributes) = &simple_sign_file.attributes {
information!("Attributes: {}", attributes);
}
if let Some(comment) = &simple_sign_file.comment {
information!("Comment: {}", comment);
}
let file_digest = argsutil::get_sha256_digest_or_hash_with_file_opt(sub_arg_matches, &simple_sign_file.filename)?;
debugging!("File digest: {}", hex::encode(&file_digest));
let file_digest_with_prefix = format!("sha256-{}", hex::encode(&file_digest));
if file_digest_with_prefix != simple_sign_file.digest {
failure!("File digest mismatch\nexpected: {}\nactual : {}", simple_sign_file.digest, file_digest_with_prefix);
return simple_error!("File digest mismatch");
}
let sign_file_request = SignFileRequest {
filename: simple_sign_file.filename.clone(),
digest: file_digest.clone(),
timestamp: simple_sign_file.timestamp,
attributes: simple_sign_file.attributes.clone(),
comment: simple_sign_file.comment.clone(),
};
let tobe_signed = sign_file_request.get_tobe_signed();
debugging!("Tobe signed: {}", hex::encode(&tobe_signed));
let tobe_signed_digest = sha256_bytes(&tobe_signed);
debugging!("Tobe signed digest: {}", hex::encode(&tobe_signed_digest));
if simple_sign_file.signatures.is_empty() {
failure!("No signatures found.");
return simple_error!("No signatures found");
}
information!("Found {} signature(s)", simple_sign_file.signatures.len());
for (i, signature) in simple_sign_file.signatures.iter().enumerate() {
// check tobe_signed_digest by signature_bytes
information!("Check signature #{} of {}", i, simple_sign_file.signatures.len());
let signature_bytes = opt_result!(base64_decode(&signature.signature), "Parse signatures.signature failed: {}");
debugging!("Signature #{}: {}", i, hex::encode(&signature_bytes));
let mut cert_pems = vec![];
for certificate in &signature.certificates {
let (_, cert_pem) = opt_result!(parse_x509_pem(certificate.as_bytes()), "Parse certificate PEM failed: {}");
cert_pems.push(cert_pem);
}
let mut certificates = vec![];
for cert_pem in &cert_pems {
let (_, cert) = opt_result!(parse_x509_certificate(&cert_pem.contents), "Parse certificate failed: {}");
debugging!("Found certificate, subject: {}, issuer : {}", cert.subject.to_string(), cert.issuer.to_string());
let asn1_timestamp = opt_result!(ASN1Time::from_timestamp(simple_sign_file.timestamp/1000), "ASN1Time failed: {}");
if !cert.validity.is_valid_at(asn1_timestamp) {
failure!("Certificate validity is out of cate: {:?}", cert.validity);
return simple_error!("Certificate is invalid: {}, out of date", cert.subject.to_string());
}
certificates.push(cert);
}
let certificates_count = certificates.len();
for i in 0..certificates.len() {
let cert1 = &certificates[i];
let cert2_public_key = iff!(i < certificates_count -1, Some(certificates[i + 1].public_key()), None);
match cert1.verify_signature(cert2_public_key) {
Ok(_) => success!("Cert #{}: {} verify success", i, cert1.subject.to_string()),
Err(e) => failure!("Cert #{}: {} verify failed: {}", i, cert1.subject.to_string(), e),
}
}
let leaf_certificate = &certificates[0];
let leaf_public_key = opt_result!(leaf_certificate.public_key().parsed(), "Parse leaf certificate public key failed: {}");
match leaf_public_key {
// PublicKey::RSA(_) => {}
PublicKey::EC(ec_point) => {
if ec_point.key_size() != 384 {
return simple_error!("Current only support p384");
}
use p384::ecdsa::{DerSignature, signature::hazmat::PrehashVerifier, VerifyingKey};
let p384_verifying_key = opt_result!(VerifyingKey::from_sec1_bytes(ec_point.data()), "Parse public key failed: {}");
let sig = opt_result!(DerSignature::from_bytes(&signature_bytes), "Parse signature failed: {}");
match p384_verifying_key.verify_prehash(&tobe_signed_digest, &sig) {
Ok(_) => success!("Verify leaf certificate signature success"),
Err(e) => return simple_error!("Verify leaf certificate signature failed: {}", e),
}
}
_ => return simple_error!("Not supported public key: {:?}", leaf_public_key),
}
}
Ok(None)
}
}

70
src/cmd_hmac_decrypt.rs Normal file
View File

@@ -0,0 +1,70 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
use rust_util::XResult;
use crate::{cmdutil, pbeutil, util};
use crate::hmacutil::{hmac_decrypt_to_string, is_hmac_encrypted};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"hmac-decrypt"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("YubiKey HMAC decrypt")
.arg(Arg::with_name("ciphertext").long("ciphertext").short("t").takes_value(true).required(true).help("Ciphertext"), )
.arg(Arg::with_name("auto-pbe").long("auto-pbe").help("Auto PBE decryption"))
.arg(Arg::with_name("password").long("password").short("P").takes_value(true).help("Password"))
.arg(Arg::with_name("outputs-password").long("outputs-password").help("Outputs password"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let ciphertext = sub_arg_matches.value_of("ciphertext").unwrap();
let mut pin_opt = sub_arg_matches.value_of("password").map(|p| p.to_string());
let auto_pbe = sub_arg_matches.is_present("auto-pbe");
let outputs_password = sub_arg_matches.is_present("outputs-password");
let text = try_decrypt_with_pbe_option(&mut pin_opt, ciphertext, auto_pbe)?;
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("plaintext", text);
if let (true, Some(pin)) = (outputs_password, pin_opt.as_ref()) {
json.insert("password", pin.to_string());
}
util::print_pretty_json(&json);
} else {
success!("Plaintext: {}", text);
}
Ok(None)
}
}
pub fn try_decrypt(pin_opt: &mut Option<String>,ciphertext: &str) -> XResult<String> {
try_decrypt_with_pbe_option(pin_opt, ciphertext, true)
}
pub fn try_decrypt_with_pbe_option(pin_opt: &mut Option<String>, ciphertext: &str, auto_pbe: bool) -> XResult<String> {
if is_hmac_encrypted(ciphertext) {
hmac_decrypt(pin_opt, ciphertext, auto_pbe)
} else if pbeutil::is_simple_pbe_encrypted(ciphertext) {
pbeutil::simple_pbe_decrypt_with_prompt_to_string(pin_opt, ciphertext)
} else {
Ok(ciphertext.to_string())
}
}
pub fn hmac_decrypt(pin_opt: &mut Option<String>, ciphertext: &str, auto_pbe: bool) -> XResult<String> {
let text = hmac_decrypt_to_string(ciphertext)?;
if auto_pbe && pbeutil::is_simple_pbe_encrypted(&text) {
pbeutil::simple_pbe_decrypt_with_prompt_to_string(pin_opt, &text)
} else {
Ok(text)
}
}

81
src/cmd_hmac_encrypt.rs Normal file
View File

@@ -0,0 +1,81 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
use rust_util::XResult;
use crate::{cmdutil, hmacutil, pbeutil, util};
use crate::pinutil::get_pin;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"hmac-encrypt"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("YubiKey HMAC encrypt")
.arg(Arg::with_name("plaintext").long("plaintext").short("t").takes_value(true).required(true).help("Plaintext, @@PIN_ENTRY@@ means read from pin entry"))
.arg(Arg::with_name("password").long("password").short("P").takes_value(true).help("Password"))
.arg(cmdutil::build_with_pbe_encrypt_arg())
.arg(cmdutil::build_double_pin_check_arg())
.arg(cmdutil::build_pbe_iteration_arg())
.arg(Arg::with_name("without-hmac-encrypt").long("without-hmac-encrypt").help("Without HMAC encrypt"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let without_hmac_encrypt = sub_arg_matches.is_present("without-hmac-encrypt");
if without_hmac_encrypt && !sub_arg_matches.is_present("with-pbe-encrypt") {
return simple_error!("hmac and pbe encryption must present at least one");
}
let text = sub_arg_matches.value_of("plaintext").unwrap().to_string();
let text = if text == "@@PIN_ENTRY@@" {
match get_pin(None) {
None => return simple_error!(""),
Some(text) => text,
}
} else {
text
};
let mut pin_opt = sub_arg_matches.value_of("password").map(|p| p.to_string());
let ciphertext = do_encrypt(&text, &mut pin_opt, sub_arg_matches)?;
let ciphertext = if without_hmac_encrypt {
ciphertext
} else {
hmacutil::hmac_encrypt_from_string(&ciphertext)?
};
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("ciphertext", ciphertext);
util::print_pretty_json(&json);
} else {
success!("HMAC encrypt ciphertext: {}", ciphertext);
}
Ok(None)
}
}
pub fn do_encrypt(text: &str, password_opt: &mut Option<String>, sub_arg_matches: &ArgMatches) -> XResult<String> {
let with_hmac_encrypt = sub_arg_matches.is_present("with-hmac-encrypt");
let with_pbe_encrypt = sub_arg_matches.is_present("with-pbe-encrypt");
let text = if with_pbe_encrypt {
let double_pin_check = sub_arg_matches.is_present("double-pin-check");
let iteration = sub_arg_matches.value_of("pbe-iteration")
.map(|x| x.parse::<u32>().unwrap()).unwrap_or(100000);
pbeutil::simple_pbe_encrypt_with_prompt_from_string(iteration, text, password_opt, double_pin_check)?
} else {
text.to_string()
};
if with_hmac_encrypt {
Ok(hmacutil::hmac_encrypt_from_string(&text)?)
} else {
Ok(text)
}
}

49
src/cmd_hmac_sha1.rs Normal file
View File

@@ -0,0 +1,49 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::XResult;
use rust_util::util_clap::{Command, CommandError};
use crate::{cmdutil, hmacutil};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "hmac-sha1" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("YubiKey HMAC-SHA1")
.arg(Arg::with_name("secret").short("s").long("secret").takes_value(true).help("Secret in HEX"))
.arg(Arg::with_name("variable").long("variable").help("Variable"))
.arg(Arg::with_name("challenge").short("c").long("challenge").takes_value(true).help("Challenge"))
.arg(Arg::with_name("challenge-hex").short("x").long("challenge-hex").takes_value(true).help("Challenge HEX"))
.arg(Arg::with_name("sha1").short("1").long("sha1").help("Output SHA1"))
.arg(Arg::with_name("sha256").short("2").long("sha256").help("Output SHA256"))
.arg(Arg::with_name("sha384").short("3").long("sha384").help("Output SHA256"))
.arg(Arg::with_name("sha512").short("5").long("sha512").help("Output SHA256"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let variable = sub_arg_matches.is_present("variable");
let secret_bytes = get_secret_bytes(sub_arg_matches)?;
let challenge_bytes = hmacutil::get_challenge_bytes(sub_arg_matches)?;
let hmac_result = hmacutil::calculate_hmac_sha1_result(&secret_bytes, &challenge_bytes, variable);
hmacutil::output_hmac_result(sub_arg_matches, json_output, challenge_bytes, &hmac_result);
Ok(None)
}
}
fn get_secret_bytes(sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> {
let secret_bytes: Vec<u8> = if let Some(secret) = sub_arg_matches.value_of("secret") {
opt_result!(hex::decode(secret), "Decode secret hex: {}, failed: {}", secret)
} else {
return simple_error!("Secret must assigned");
};
if secret_bytes.len() != 20 {
return simple_error!("Secret length must be 20, actual is: {}", secret_bytes.len());
}
Ok(secret_bytes)
}

169
src/cmd_keypair_generate.rs Normal file
View File

@@ -0,0 +1,169 @@
use crate::ecdsautil::EcdsaAlgorithm;
use crate::keychain::{KeychainKey, KeychainKeyValue};
use crate::keyutil::{KeyAlgorithmId, KeyUri, YubikeyHmacEncSoftKey};
use crate::pivutil::FromStr;
use crate::util::base64_encode;
use crate::{cmd_hmac_encrypt, cmdutil, ecdsautil, hmacutil, mlkemutil, pbeutil, rsautil, util, yubikeyutil};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"keypair-generate"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Generate software keypair")
.arg(
Arg::with_name("type")
.long("type")
.required(true)
.takes_value(true)
.help("Key type (e.g. p256, p384, p521, rsa1024, rsa2048, rsa3072, rsa4096, mlkem512, mlkem768, mlkem1024)"),
)
.arg(cmdutil::build_with_hmac_encrypt_arg())
.arg(cmdutil::build_with_pbe_encrypt_arg())
.arg(cmdutil::build_double_pin_check_arg())
.arg(cmdutil::build_pbe_iteration_arg())
.arg(cmdutil::build_keychain_name_arg())
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let key_type = sub_arg_matches.value_of("type").unwrap().to_lowercase();
let keychain_name = sub_arg_matches.value_of("keychain-name");
if let Some(keychain_name) = keychain_name {
let keychain_key = KeychainKey::from_key_name_default(keychain_name);
if keychain_key.get_password()?.is_some() {
return simple_error!("Keychain key URI: {} exists", keychain_key.to_key_uri());
}
}
let ecdsa_algorithm = match key_type.as_str() {
"p256" => Some(EcdsaAlgorithm::P256),
"p384" => Some(EcdsaAlgorithm::P384),
"p521" => Some(EcdsaAlgorithm::P521),
_ => None,
};
let rsa_bit_size: Option<usize> = match key_type.as_str() {
"rsa1024" => Some(1024),
"rsa2048" => Some(2048),
"rsa3072" => Some(3072),
"rsa4096" => Some(4096),
_ => None,
};
let mlkem_len: Option<usize> = match key_type.as_str() {
"mlkem512" => Some(512),
"mlkem768" => Some(768),
"mlkem1024" => Some(1024),
_ => None,
};
let (pkcs8_base64, secret_key_pem, public_key_pem, public_key_der, jwk_key) =
if let Some(ecdsa_algorithm) = ecdsa_algorithm {
ecdsautil::generate_ecdsa_keypair(ecdsa_algorithm)?
} else if let Some(rsa_bit_size) = rsa_bit_size {
rsautil::generate_rsa_keypair(rsa_bit_size)?
} else if let Some(mlkem_len) = mlkem_len {
mlkemutil::generate_mlkem_keypair(mlkem_len)?
} else {
return simple_error!("Unsupported key type: {}", key_type);
};
let mut password_opt = None;
let (pkcs8_base64, secret_key_pem) = (
cmd_hmac_encrypt::do_encrypt(&pkcs8_base64, &mut password_opt, sub_arg_matches)?,
cmd_hmac_encrypt::do_encrypt(&secret_key_pem, &mut password_opt, sub_arg_matches)?,
);
let public_key_base64 = base64_encode(&public_key_der);
let keychain_key_uri = if let Some(keychain_name) = keychain_name {
let keychain_key_value = KeychainKeyValue {
keychain_name: keychain_name.to_string(),
pkcs8_base64: pkcs8_base64.clone(),
secret_key_pem: secret_key_pem.clone(),
public_key_pem: public_key_pem.clone(),
public_key_jwk: jwk_key.clone(),
};
let keychain_key_value_json = serde_json::to_string(&keychain_key_value)?;
let keychain_key = KeychainKey::from_key_name_default(keychain_name);
keychain_key.set_password(keychain_key_value_json.as_bytes())?;
Some(keychain_key.to_key_uri())
} else {
None
};
let algorithm_id = KeyAlgorithmId::from_str(&key_type);
let with_encrypt = hmacutil::is_hmac_encrypted(&pkcs8_base64)
|| pbeutil::is_simple_pbe_encrypted(&pkcs8_base64);
let yubikey_hmac_enc_soft_key_uri =
if let (true, Some(algorithm_id)) = (with_encrypt, algorithm_id) {
let yubikey_name = match yubikeyutil::open_yubikey() {
Ok(yk) => format!("yubikey{}-{}", yk.version().major, yk.serial().0),
Err(_) => "yubikey-unknown".to_string(),
};
let yubikey_hmac_enc_soft_key = YubikeyHmacEncSoftKey {
key_name: yubikey_name,
algorithm: algorithm_id,
hmac_enc_private_key: pkcs8_base64.clone(),
};
Some(KeyUri::YubikeyHmacEncSoft(yubikey_hmac_enc_soft_key).to_string())
} else {
None
};
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
match keychain_key_uri {
None => {
json.insert("private_key_base64", pkcs8_base64);
json.insert("private_key_pem", secret_key_pem);
if let Some(yubikey_hmac_enc_soft_key_uri) = yubikey_hmac_enc_soft_key_uri {
json.insert("key_uri", yubikey_hmac_enc_soft_key_uri.to_string());
}
}
Some(keychain_key_uri) => {
json.insert("keychain_key_uri", keychain_key_uri);
}
}
json.insert("public_key_pem", public_key_pem);
json.insert("public_key_base64", public_key_base64);
if !jwk_key.is_empty() {
json.insert("public_key_jwk", jwk_key);
}
util::print_pretty_json(&json);
} else {
match keychain_key_uri {
None => {
information!("Private key base64:\n{}\n", pkcs8_base64);
information!("Private key PEM:\n{}\n", secret_key_pem);
if let Some(yubikey_hmac_enc_soft_key_uri) = yubikey_hmac_enc_soft_key_uri {
information!("Key URI:\n{}\n", yubikey_hmac_enc_soft_key_uri);
}
}
Some(keychain_key_uri) => {
information!("Keychain key URI:\n{}\n", keychain_key_uri);
}
}
information!("Public key PEM:\n{}", public_key_pem);
information!("Public key Base64:\n{}\n", public_key_base64);
if !jwk_key.is_empty() {
information!("Public key JWK:\n{}", jwk_key);
}
}
Ok(None)
}
}

View File

@@ -0,0 +1,38 @@
use crate::{cmdutil, util};
use crate::keychain::{KeychainKey, KeychainKeyValue};
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"keypair-keychain-export"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Export software keypair from keychain")
.arg(cmdutil::build_keychain_name_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let keychain_name = sub_arg_matches.value_of("keychain-name");
if let Some(keychain_name) = keychain_name {
let keychain_key = KeychainKey::from_key_name_default(keychain_name);
if let Some(keychain_key_value_bytes) = keychain_key.get_password()? {
let keychain_key_value: KeychainKeyValue =
serde_json::from_slice(&keychain_key_value_bytes)?;
util_msg::set_logger_std_out(false);
information!("Keychain key URI: {}", keychain_key.to_key_uri());
util::print_pretty_json(&keychain_key_value);
} else {
return simple_error!("Keychain key URI: {} not found", keychain_key.to_key_uri());
}
}
Ok(None)
}
}

View File

@@ -0,0 +1,42 @@
use crate::cmdutil;
use crate::keychain::KeychainKey;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"keypair-keychain-import"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Import software keypair to keychain")
.arg(cmdutil::build_keychain_name_arg())
.arg(
Arg::with_name("import-key-value")
.long("import-key-value")
.takes_value(true)
.help("Import key value"),
)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let keychain_name = sub_arg_matches.value_of("keychain-name");
let import_key_value = sub_arg_matches.value_of("import-key-value");
if let Some(keychain_name) = keychain_name {
let keychain_key = KeychainKey::from_key_name_default(keychain_name);
if keychain_key.get_password()?.is_some() {
return simple_error!("Keychain key URI: {} exists", keychain_key.to_key_uri());
}
if let Some(import_key_value) = import_key_value {
keychain_key.set_password(import_key_value.as_bytes())?;
}
}
Ok(None)
}
}

77
src/cmd_list.rs Normal file
View File

@@ -0,0 +1,77 @@
use std::collections::BTreeMap;
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use serde_json::Value;
use crate::{cmdutil, util, yubikeyutil};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "list" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("YubiKey list")
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
if json_output {
let mut json = BTreeMap::<&'_ str, Value>::new();
json.insert("name", yk.name().into());
json.insert("version", yk.version().to_string().into());
json.insert("serial", yk.serial().0.into());
if let Ok(pin_retries) = yk.get_pin_retries() {
json.insert("pin_retries", pin_retries.into());
}
if let Ok(chuid) = yk.chuid() {
json.insert("chuid", chuid.to_string().into());
}
if let Ok(ccuid) = yk.cccid() {
json.insert("ccuid", ccuid.to_string().into());
}
if let Ok(piv_keys) = yk.piv_keys() {
let key_list = piv_keys.iter().map(|k| Value::String(format!("{}", k.slot()))).collect::<Vec<_>>();
json.insert("key_list", key_list.into());
let keys = piv_keys.iter().map(|k| format!("{}", k.slot())).collect::<Vec<_>>().join(", ");
json.insert("keys", keys.into());
}
util::print_pretty_json(&json);
} else {
success!("Name: {}", yk.name());
success!("Version: {}", yk.version());
success!("Serial: {}", yk.serial().0);
// success!("{:?}", yk.config());
if let Ok(pin_retries) = yk.get_pin_retries() {
success!("PIN retries: {}", pin_retries);
}
if let Ok(config) = yk.config() {
information!("Protected data available: {}", config.protected_data_available);
information!("PIN last changed: {:?}", config.pin_last_changed);
information!("PUK blocked: {}", config.puk_blocked);
information!("PUK noblock on upgrade: {}", config.puk_noblock_on_upgrade);
information!("PUK mgm type: {:?}", config.mgm_type);
}
if let Ok(chuid) = yk.chuid() {
information!("Chuid: {}", chuid)
}
if let Ok(ccuid) = yk.cccid() {
information!("Ccuid: {}", ccuid)
}
if let Ok(piv_keys) = yk.piv_keys() {
information!("PIV keys: {}, slots: [{}]",
piv_keys.len(),
piv_keys.iter().map(|k| format!("{}", k.slot())).collect::<Vec<_>>().join(", ")
);
}
}
Ok(None)
}
}

View File

@@ -0,0 +1,58 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use crate::{cmdutil, util};
use crate::ecdsautil::parse_ecdsa_r_and_s;
use crate::util::try_decode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"parse-ecdsa-signature"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Parse ECDSA signature")
.arg(
Arg::with_name("signature")
.long("signature")
.required(true)
.takes_value(true)
.help("ECDSA signature"),
)
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let mut json = BTreeMap::<&'_ str, String>::new();
let signature = sub_arg_matches.value_of("signature").unwrap();
let signature_der = try_decode(signature)?;
let (r, s) = parse_ecdsa_r_and_s(&signature_der)?;
let mut r_and_s = r.clone();
r_and_s.extend_from_slice(&s);
if json_output {
json.insert("r", hex::encode(&r));
json.insert("s", hex::encode(&s));
json.insert("rs", hex::encode(&r_and_s));
} else {
information!("R: {}", hex::encode(&r));
information!("S: {}", hex::encode(&s));
information!("RS: {}", hex::encode(&r_and_s));
}
if json_output {
util::print_pretty_json(&json);
}
Ok(None)
}
}

View File

@@ -1,18 +1,14 @@
use std::ops::Deref;
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use openssl::bn::BigNum;
use openssl::rsa::Rsa;
use pem::Pem;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use sequoia_openpgp::crypto::mpi::PublicKey;
use sequoia_openpgp::Packet; use sequoia_openpgp::Packet;
use sequoia_openpgp::packet::{Body, Key, PKESK, SEIP, Signature}; use sequoia_openpgp::packet::{Body, Key, PKESK, SEIP, Signature};
use sequoia_openpgp::packet::signature::subpacket::{SubpacketTag, SubpacketValue}; use sequoia_openpgp::packet::signature::subpacket::{SubpacketTag, SubpacketValue};
use sequoia_openpgp::parse::{PacketParser, PacketParserResult}; use sequoia_openpgp::parse::{PacketParser, PacketParserResult};
use sequoia_openpgp::parse::Parse; use sequoia_openpgp::parse::Parse;
use crate::pkiutil::sequoia_openpgp_public_key_pem as public_key_pem;
pub struct CommandImpl; pub struct CommandImpl;
impl Command for CommandImpl { impl Command for CommandImpl {
@@ -23,7 +19,7 @@ impl Command for CommandImpl {
.arg(Arg::with_name("in").short("i").long("in").takes_value(true).help("File input, *.pgp or *.asc")) .arg(Arg::with_name("in").short("i").long("in").takes_value(true).help("File input, *.pgp or *.asc"))
.arg(Arg::with_name("detail").long("detail").help("Detail output")) .arg(Arg::with_name("detail").long("detail").help("Detail output"))
.arg(Arg::with_name("verbose").long("verbose").help("Verbose output")) .arg(Arg::with_name("verbose").long("verbose").help("Verbose output"))
.arg(Arg::with_name("json").long("json").help("JSON output")) // .arg(Arg::with_name("json").long("json").help("JSON output"))
} }
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
@@ -91,7 +87,8 @@ impl Command for CommandImpl {
public_key4.push_str(&format!("\n\tCreation time {}", creation_time_str)); public_key4.push_str(&format!("\n\tCreation time {}", creation_time_str));
public_key4.push_str(&format!("\n\tPublic algo: {:?}", key4.pk_algo())); public_key4.push_str(&format!("\n\tPublic algo: {:?}", key4.pk_algo()));
if show_detail { if show_detail {
if let Some(pubkey_pem) = public_key_pem(key4.mpis()) { if let Some((pubkey_sha256, pubkey_pem)) = public_key_pem(key4.mpis()) {
public_key4.push_str(&format!("\n\tPublic key sha256: {}", hex::encode(pubkey_sha256)));
public_key4.push_str(&format!("\n\tPublic key PEM: {}", pubkey_pem)); public_key4.push_str(&format!("\n\tPublic key PEM: {}", pubkey_pem));
} }
} }
@@ -114,7 +111,8 @@ impl Command for CommandImpl {
public_key4.push_str(&format!("\n\tCreation time {}", creation_time_str)); public_key4.push_str(&format!("\n\tCreation time {}", creation_time_str));
public_key4.push_str(&format!("\n\tPublic algo: {:?}", key4.pk_algo())); public_key4.push_str(&format!("\n\tPublic algo: {:?}", key4.pk_algo()));
if show_detail { if show_detail {
if let Some(pub_key_pem) = public_key_pem(key4.mpis()) { if let Some((pubkey_sha256, pub_key_pem)) = public_key_pem(key4.mpis()) {
public_key4.push_str(&format!("\n\tPublic key sha256: {}", hex::encode(pubkey_sha256)));
public_key4.push_str(&format!("\n\tPublic key PEM: {}", pub_key_pem)); public_key4.push_str(&format!("\n\tPublic key PEM: {}", pub_key_pem));
} }
} }
@@ -151,7 +149,7 @@ impl Command for CommandImpl {
debugging!("Found PKESK: {:?}", pkesk); debugging!("Found PKESK: {:?}", pkesk);
match pkesk { match pkesk {
PKESK::V3(pkesk3) => { PKESK::V3(pkesk3) => {
information!("Found public key encrypted session key, key ID: {}, alog: {}", pkesk3.recipient(), pkesk3.pk_algo()); information!("Found public key encrypted session key, key ID: {:?}, alog: {}", pkesk3.recipient(), pkesk3.pk_algo());
} }
unknown => warning!("Unknown PKESK: {:?}", unknown), unknown => warning!("Unknown PKESK: {:?}", unknown),
} }
@@ -162,18 +160,21 @@ impl Command for CommandImpl {
Packet::SEIP(seip) => { Packet::SEIP(seip) => {
debugging!("Found SEIP: {:?}", seip); debugging!("Found SEIP: {:?}", seip);
match seip { match seip {
SEIP::V1(seip1) => match seip1.deref().body() { SEIP::V1(seip1) => match seip1.body() {
Body::Processed(b) | Body::Unprocessed(b) => information!("Found encrypted data, len: {} byte(s)", b.len()), Body::Processed(b) | Body::Unprocessed(b) => information!("Found encrypted data, len: {} byte(s)", b.len()),
Body::Structured(b) => information!("Found encrypted data packages, len: {}", b.len()), Body::Structured(b) => information!("Found encrypted data packages, len: {}", b.len()),
} }
SEIP::V2(seip2) => match seip2.body() {
Body::Processed(b) | Body::Unprocessed(b) => information!("Found encrypted data, len: {} byte(s)", b.len()),
Body::Structured(b) => information!("Found encrypted data packages, len: {}", b.len()),
}
_ => {}
} }
} }
#[allow(deprecated)]
Packet::MDC(mdc) => { Packet::MDC(mdc) => {
information!("Found MDC: {:?}", mdc); information!("Found MDC: {:?}", mdc);
} }
Packet::AED(aed) => {
information!("Found AED: {:?}", aed);
}
Packet::Unknown(unknown) => { Packet::Unknown(unknown) => {
warning!("Found unknown: {:?}", unknown); warning!("Found unknown: {:?}", unknown);
} }
@@ -200,23 +201,3 @@ impl Command for CommandImpl {
Ok(None) Ok(None)
} }
} }
fn public_key_pem(public_key: &PublicKey) -> Option<String> {
match public_key {
PublicKey::RSA { e, n } => {
let rsa_pub_key = Rsa::from_public_components(
BigNum::from_slice(n.value()).unwrap(),
BigNum::from_slice(e.value()).unwrap(),
);
let pub_key_pem_obj = Pem {
tag: String::from("PUBLIC KEY"),
contents: rsa_pub_key.unwrap().public_key_to_der().unwrap(),
};
Some(pem::encode(&pub_key_pem_obj))
}
_ => {
warning!("Not RSA public key: {:?}", public_key);
None
}
}
}

View File

@@ -0,0 +1,73 @@
use bech32::{ToBase32, Variant};
use clap::{App, ArgMatches, SubCommand};
use openpgp_card::algorithm::{Algo, Curve};
use openpgp_card::crypto_data::{EccType, PublicKeyMaterial};
use openpgp_card::{KeyType, OpenPgp};
use openpgp_card_pcsc::PcscBackend;
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
use crate::{cmdutil, util};
const AGE_PUBLIC_KEY_PREFIX: &str = "age";
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "pgp-age-address" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("OpenPGP Card encryption key to age address")
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let cards = opt_result!(PcscBackend::cards(None), "Failed to list OpenPGP cards: {}");
let mut cards_output: Vec<BTreeMap<&str, String>> = vec![];
information!("Found {} card(s)", cards.len());
for (i, card) in cards.into_iter().enumerate() {
let mut card_output = BTreeMap::new();
let mut pgp = OpenPgp::new(card);
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
if let Ok(application_related_data) = trans.application_related_data() {
success!("Found card #{}: {:?}", i, application_related_data.application_id());
if let Ok(application_id) = application_related_data.application_id() {
card_output.insert("application_id", format!("{}", application_id));
}
}
let encryption_public_key = match trans.public_key(KeyType::Decryption) {
Ok(pub_key) => pub_key,
Err(e) => {
failure!("Get decryption public key failed: {}", e);
continue;
}
};
if let PublicKeyMaterial::E(ecc_pub) = &encryption_public_key {
if let Algo::Ecc(ecc) = ecc_pub.algo() {
if let (EccType::ECDH, Curve::Cv25519) = (ecc.ecc_type(), ecc.curve()) {
let pub_key_bytes = ecc_pub.data();
let age_address = opt_result!(bech32::encode(
AGE_PUBLIC_KEY_PREFIX,
pub_key_bytes.to_base32(),
Variant::Bech32,
), "Generate age address failed: {}");
success!("Age address: {}", age_address);
card_output.insert("age_address", age_address);
}
}
} else {
failure!("Not supported encryption key: {}", encryption_public_key);
}
cards_output.push(card_output);
}
if json_output {
util::print_pretty_json(&cards_output);
}
Ok(None)
}
}

View File

@@ -1,46 +1,62 @@
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use openpgp_card::Sex; use openpgp_card::card_do::{Lang, Sex};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use crate::{pgpcardutil, pinutil};
pub struct CommandImpl; pub struct CommandImpl;
impl Command for CommandImpl { impl Command for CommandImpl {
fn name(&self) -> &str { "pgp-card-admin" } fn name(&self) -> &str { "pgp-card-admin" }
fn subcommand<'a>(&self) -> App<'a, 'a> { fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("OpenPGP Card Admin subcommand") SubCommand::with_name(self.name()).about("OpenPGP Card admin subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).default_value("12345678").help("OpenPGP card admin pin")) .arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("OpenPGP card admin pin"))
.arg(Arg::with_name("pass").long("pass").takes_value(true).help("[deprecated] now OpenPGP card admin pin")) .arg(Arg::with_name("pass").long("pass").takes_value(true).help("[deprecated] now OpenPGP card admin pin"))
.arg(Arg::with_name("name").short("n").long("name").takes_value(true).required(false).help("Set name")) .arg(Arg::with_name("name").short("n").long("name").takes_value(true).required(false).help("Set name"))
.arg(Arg::with_name("url").long("url").takes_value(true).required(false).help("Set URL")) .arg(Arg::with_name("url").long("url").takes_value(true).required(false).help("Set URL"))
.arg(Arg::with_name("lang").long("lang").takes_value(true).required(false).help("Set lang")) .arg(Arg::with_name("lang").long("lang").takes_value(true).required(false).help("Set lang"))
.arg(Arg::with_name("sex").long("sex").takes_value(true).required(false).help("Set sex, f or m")) .arg(Arg::with_name("sex").long("sex").takes_value(true).required(false).help("Set sex, f or m"))
.arg(Arg::with_name("reset").long("reset").help("Reset card"))
} }
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let pin_opt = sub_arg_matches.value_of("pass").or(sub_arg_matches.value_of("pin")); let pin_opt = sub_arg_matches.value_of("pass").or_else(|| sub_arg_matches.value_of("pin"));
let pin = opt_value_result!(pin_opt, "Pass must be assigned"); let pin_opt = pinutil::get_pin(pin_opt);
let pin_opt = pin_opt.as_deref();
let pin = opt_value_result!(pin_opt, "Pin must be assigned");
if pin.len() < 8 { return simple_error!("Admin pin length:{}, must >= 8!", pin.len()); } if pin.len() < 8 { return simple_error!("Admin pin length:{}, must >= 8!", pin.len()); }
let card_admin = crate::pgpcardutil::get_card_admin(pin)?; let mut pgp = pgpcardutil::get_openpgp_card()?;
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
if sub_arg_matches.is_present("reset") {
warning!("Start reset card...");
opt_result!(trans.factory_reset(), "Reset failed: {}");
success!("Reset success");
return Ok(None);
}
opt_result!(trans.verify_pw3(pin.as_ref()), "Admin pin verify failed: {}");
success!("Admin pin verify success!"); success!("Admin pin verify success!");
if let Some(name) = sub_arg_matches.value_of("name") { if let Some(name) = sub_arg_matches.value_of("name") {
information!("Set name to: {}", name); information!("Set name to: {}", name);
let response = opt_result!(card_admin.set_name(name), "Set name failed: {}"); opt_result!(trans.set_name(name.as_bytes()), "Set name failed: {}");
success!("Set name success: {:?}", response); success!("Set name success");
} }
if let Some(url) = sub_arg_matches.value_of("url") { if let Some(url) = sub_arg_matches.value_of("url") {
information!("Set URL to: {}", url); information!("Set URL to: {}", url);
let response = opt_result!(card_admin.set_url(url), "Set URL failed: {}"); opt_result!(trans.set_url(url.as_bytes()), "Set URL failed: {}");
success!("Set URL success: {:?}", response); success!("Set URL success");
} }
if let Some(lang) = sub_arg_matches.value_of("lang") { if let Some(lang) = sub_arg_matches.value_of("lang") {
information!("Set lang to: {}", lang); information!("Set lang to: {}", lang);
let response = opt_result!(card_admin.set_lang(lang), "Set lang failed: {}"); let lang_bytes = lang.as_bytes();
success!("Set lang success: {:?}", response); opt_result!(trans.set_lang(&[Lang::Value([lang_bytes[0], lang_bytes[1]])]), "Set lang failed: {}");
success!("Set lang success");
} }
if let Some(sex) = sub_arg_matches.value_of("sex") { if let Some(sex) = sub_arg_matches.value_of("sex") {
@@ -55,13 +71,11 @@ impl Command for CommandImpl {
}; };
if let Some(s) = s { if let Some(s) = s {
information!("Set sex to: {:?}", s); information!("Set sex to: {:?}", s);
let response = opt_result!(card_admin.set_sex(s), "Set sex failed: {}"); opt_result!(trans.set_sex(s), "Set lang failed: {}");
success!("Set sex success: {:?}", response); success!("Set sex success");
} }
} }
// TODO Upload Key!
Ok(None) Ok(None)
} }
} }

View File

@@ -0,0 +1,95 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use openpgp_card::crypto_data::Cryptogram;
use rust_util::XResult;
use rust_util::util_clap::{Command, CommandError};
use crate::{cmdutil, pgpcardutil, pinutil, util};
use crate::util::{base64_encode, read_stdin, try_decode};
#[derive(Debug, Clone, Copy)]
enum EncryptAlgo {
Rsa,
Ecdh,
}
impl EncryptAlgo {
fn from_str(algo: &str) -> XResult<Self> {
match algo {
"rsa" => Ok(Self::Rsa),
"x25519" | "ecdh" => Ok(Self::Ecdh),
_ => simple_error!("Unknown algo: {}", algo),
}
}
}
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "pgp-card-decrypt" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("OpenPGP Card decrypt subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("OpenPGP card user pin"))
.arg(Arg::with_name("pass").long("pass").takes_value(true).help("[deprecated] now OpenPGP card user pin"))
.arg(Arg::with_name("ciphertext").short("c").long("ciphertext").takes_value(true).help("Cipher text (HEX or Base64)"))
.arg(Arg::with_name("stdin").long("stdin").help("Standard input (Ciphertext)"))
.arg(Arg::with_name("algo").long("algo").takes_value(true).help("Algo: RSA, X25519/ECDH"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let pin_opt = sub_arg_matches.value_of("pass").or_else(|| sub_arg_matches.value_of("pin"));
let pin_opt = pinutil::get_pin(pin_opt);
let pin_opt = pin_opt.as_deref();
let pin = opt_value_result!(pin_opt, "User pin must be assigned");
if pin.len() < 6 { return simple_error!("User pin length:{}, must >= 6!", pin.len()); }
let ciphertext = sub_arg_matches.value_of("ciphertext");
let algo = sub_arg_matches.value_of("algo").unwrap_or("rsa").to_lowercase();
let algo = EncryptAlgo::from_str(&algo)?;
let ciphertext_bytes = if let Some(ciphertext) = ciphertext {
opt_result!(try_decode(ciphertext), "Decode cipher failed: {}")
} else if sub_arg_matches.is_present("stdin") {
read_stdin()?
} else {
return simple_error!("--ciphertext must be assigned");
};
let mut pgp = pgpcardutil::get_openpgp_card()?;
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
opt_result!(trans.verify_pw1_user(pin.as_ref()), "User pin verify failed: {}");
success!("User pin verify success!");
let text = match algo {
EncryptAlgo::Rsa => trans.decipher(Cryptogram::RSA(&ciphertext_bytes))?,
EncryptAlgo::Ecdh => trans.decipher(Cryptogram::ECDH(&ciphertext_bytes))?,
};
success!("Clear text HEX: {}", hex::encode(&text));
success!("Clear text base64: {}", base64_encode(&text));
let text_opt = String::from_utf8(text.clone()).ok();
if let Some(text) = &text_opt {
success!("Clear text UTF-8: {}", text);
}
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("cipher_hex", hex::encode(&ciphertext_bytes));
json.insert("cipher_base64", base64_encode(&ciphertext_bytes));
json.insert("text_hex", hex::encode(&text));
json.insert("text_base64", base64_encode(&text));
if let Some(text) = text_opt {
json.insert("text_utf8", text);
}
util::print_pretty_json(&json);
}
Ok(None)
}
}

142
src/cmd_pgp_card_list.rs Normal file
View File

@@ -0,0 +1,142 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use openpgp_card::{KeyType, OpenPgp};
use openpgp_card_pcsc::PcscBackend;
use rust_util::util_clap::{Command, CommandError};
use crate::{cmdutil, util};
use crate::pkiutil::openpgp_card_public_key_pem as public_key_pem;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "pgp-card-list" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("OpenPGP Card list subcommand")
.arg(Arg::with_name("detail").long("detail").help("Detail output"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let detail_output = sub_arg_matches.is_present("detail");
let mut jsons = vec![];
let cards = opt_result!(PcscBackend::cards(None), "Failed to list OpenPGP cards: {}");
information!("Found {} card(s)", cards.len());
for (i, card) in cards.into_iter().enumerate() {
let mut json = BTreeMap::new();
let mut pgp = OpenPgp::new(card);
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
if let Ok(application_related_data) = trans.application_related_data() {
success!("Found card #{}: {:?}", i, application_related_data.application_id());
debugging!("Historical: {:?}", application_related_data.historical_bytes());
debugging!("Extended length information: {:?}", application_related_data.extended_length_information());
debugging!("Extended capabilities: {:?}", application_related_data.extended_capabilities());
debugging!("Key generation times: {:?}", application_related_data.key_generation_times());
debugging!("PW status bytes: {:?}", application_related_data.pw_status_bytes());
debugging!(r#"Algorithm attributes:
- Signing: {:?}
- Decryption: {:?}
- Authentication: {:?}
- Attestation: {:?}"#,
application_related_data.algorithm_attributes(KeyType::Signing),
application_related_data.algorithm_attributes(KeyType::Decryption),
application_related_data.algorithm_attributes(KeyType::Authentication),
application_related_data.algorithm_attributes(KeyType::Attestation));
debugging!("Fingerprints: {:?}", application_related_data.fingerprints());
if json_output {
if let Ok(application_identifier) = application_related_data.application_id() {
json.insert("application".to_string(), format!("{}", application_identifier.application()));
json.insert("version".to_string(), format!("{}", application_identifier.version()));
json.insert("serial".to_string(), format!("{}", application_identifier.serial()));
json.insert("manufacturer".to_string(), format!("{}", application_identifier.manufacturer()));
json.insert("ident".to_string(), application_identifier.ident());
}
}
}
information!("Feature pin pad, verify: {}, modify: {}",
trans.feature_pinpad_verify(), trans.feature_pinpad_modify());
if json_output {
json.insert("feature_pinpad_verify".to_string(), format!("{}", trans.feature_pinpad_verify()));
json.insert("feature_pinpad_modify".to_string(), format!("{}", trans.feature_pinpad_modify()));
}
if let Ok(security_supported_template) = trans.security_support_template() {
debugging!("Security support template: {:?}", security_supported_template);
}
if let Ok(cardholder_certificate) = trans.cardholder_certificate() {
debugging!("Cardholder certificate: {:?}", cardholder_certificate);
}
// debugging!("Security support template: {:?}", trans.security_support_template());
// debugging!("Security support template: {:?}", trans.cardholder_certificate());
if let Ok(url) = trans.url() {
information!("URL: {}", iff!(url.is_empty(), "<empty>".to_string(), String::from_utf8_lossy(&url).to_string()));
}
if let Ok(card_holder) = trans.cardholder_related_data() {
debugging!("Card holder: {:?}", card_holder);
let mut card_holder_outputs = vec![];
if let Some(name) = card_holder.name() {
card_holder_outputs.push(format!("name: {}", String::from_utf8_lossy(name)));
}
if let Some(lang) = card_holder.lang() {
card_holder_outputs.push(format!(
"lang: {}",
lang.iter().map(|l| l.to_string()).collect::<Vec<String>>().join(" ")));
}
if let Some(sex) = card_holder.sex() {
card_holder_outputs.push(format!("sex: {:?}", sex));
}
information!("Card holder, {}",
iff!(card_holder_outputs.is_empty(), "".to_string(), card_holder_outputs.join(", ")));
}
if let Ok(Some(algo_info)) = trans.algorithm_information() {
debugging!("Algo info: {}", algo_info);
}
if let Ok(application_related_data) = trans.application_related_data() {
if let Ok(fingerprints) = application_related_data.fingerprints() {
let fingerprints = vec![
("Authentication", "authentication", KeyType::Authentication, fingerprints.authentication()),
("Decryption", "encryption", KeyType::Decryption, fingerprints.decryption()),
("Signature", "signature", KeyType::Signing, fingerprints.signature()),
];
for (tag1, tag2, key_type, fingerprint) in fingerprints {
let fingerprint = match fingerprint {
Some(fingerprint) => fingerprint,
None => continue
};
if let Ok(algo) = application_related_data.algorithm_attributes(key_type) {
information!("{} algo: {:?}", tag1, algo);
}
information!("{} fingerprint: {}", tag1, fingerprint);
if json_output {
json.insert(format!("{}_fingerprint", tag2), fingerprint.to_string());
}
if detail_output {
if let Ok(public_key) = trans.public_key(key_type) {
if let Some((public_key_sha256, public_key_pem)) = public_key_pem(&public_key) {
information!("{} public key sha256: {}", tag1, hex::encode(&public_key_sha256));
information!("{} public key: {}", tag1, public_key_pem.trim());
information!("{} public key: {}", tag1, public_key);
if json_output {
json.insert(format!("{}_public_key_sha256", tag2), hex::encode(&public_key_sha256));
json.insert(format!("{}_public_key_pem", tag2), public_key_pem);
}
}
}
}
}
}
}
jsons.push(json);
}
if json_output {
util::print_pretty_json(&jsons);
}
Ok(None)
}
}

316
src/cmd_pgp_card_make.rs Normal file
View File

@@ -0,0 +1,316 @@
use std::time::SystemTime;
use clap::{App, Arg, ArgMatches, SubCommand};
use openpgp::crypto::mpi::ProtectedMPI;
use openpgp::crypto::mpi::PublicKey as MpiPublicKey;
use openpgp::crypto::Password;
use openpgp::Fingerprint;
use openpgp::KeyID;
use openpgp::Packet;
use openpgp::packet::Key;
use openpgp::packet::key::{SecretKeyMaterial, SecretParts};
use openpgp::packet::key::SubordinateRole;
use openpgp::packet::Signature;
use openpgp::packet::signature::subpacket::{SubpacketTag, SubpacketValue};
use openpgp::parse::{PacketParser, PacketParserResult, Parse};
use openpgp_card::{Error, KeyType, OpenPgp};
use openpgp_card::card_do::KeyGenerationTime;
use openpgp_card::crypto_data::{CardUploadableKey, PrivateKeyMaterial, RSAKey};
use openssl::bn::BigNum;
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use sequoia_openpgp as openpgp;
use crate::pinutil;
use crate::rsautil::RsaCrt;
#[derive(Debug)]
struct PgpRsaPrivateKeySet {
signing: Option<PgpRsaPrivateKey>,
encryption: Option<PgpRsaPrivateKey>,
authentication: Option<PgpRsaPrivateKey>,
}
impl PgpRsaPrivateKeySet {
fn new() -> Self {
PgpRsaPrivateKeySet {
signing: None,
encryption: None,
authentication: None,
}
}
}
#[derive(Debug)]
struct PgpRsaPrivateKey {
creation_time_secs: u32,
key_id: KeyID,
fingerprint: Fingerprint,
rsa_private_key: RsaCrt,
}
#[derive(Debug)]
struct RsaKeyCrt {
e: Vec<u8>,
p: Vec<u8>,
q: Vec<u8>,
pq: Vec<u8>,
dp1: Vec<u8>,
dq1: Vec<u8>,
n: Vec<u8>,
}
impl RsaKeyCrt {
fn from(rsa_crt: &RsaCrt) -> Self {
Self {
e: rsa_crt.public_exponent.to_vec(),
p: rsa_crt.prime1.to_vec(),
q: rsa_crt.prime2.to_vec(),
pq: rsa_crt.coefficient.to_vec(),
dp1: rsa_crt.exponent1.to_vec(),
dq1: rsa_crt.exponent2.to_vec(),
n: rsa_crt.modulus.to_vec(),
}
}
}
impl RSAKey for RsaKeyCrt {
fn e(&self) -> &[u8] {
self.e.as_slice()
}
fn p(&self) -> &[u8] {
self.p.as_slice()
}
fn q(&self) -> &[u8] {
self.q.as_slice()
}
fn pq(&self) -> Box<[u8]> {
self.pq.clone().into()
}
fn dp1(&self) -> Box<[u8]> {
self.dp1.clone().into()
}
fn dq1(&self) -> Box<[u8]> {
self.dq1.clone().into()
}
fn n(&self) -> &[u8] {
self.n.as_slice()
}
}
impl CardUploadableKey for PgpRsaPrivateKey {
fn private_key(&self) -> Result<PrivateKeyMaterial, Error> {
Ok(PrivateKeyMaterial::R(Box::new(RsaKeyCrt::from(&self.rsa_private_key))))
}
fn timestamp(&self) -> KeyGenerationTime {
KeyGenerationTime::from(self.creation_time_secs)
}
fn fingerprint(&self) -> Result<openpgp_card::card_do::Fingerprint, Error> {
if let Fingerprint::V4(fingerprint_v4) = self.fingerprint {
Ok(openpgp_card::card_do::Fingerprint::from(fingerprint_v4))
} else {
Err(Error::InternalError("Not supported fingerprint version".to_string()))
}
}
}
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "pgp-card-make" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("OpenPGP Card make subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("OpenPGP card admin pin"))
.arg(Arg::with_name("pass").long("pass").takes_value(true).required(false).help("Password for PGP secret key"))
.arg(Arg::with_name("in").long("in").takes_value(true).required(false).help("PGP file in"))
.arg(Arg::with_name("force-make").long("force-make").help("Force make OpenPGP card"))
.arg(Arg::with_name("print-public-keys").long("print-public-keys").help("Print public keys"))
.arg(Arg::with_name("print-private-keys").long("print-private-keys").help("Print private keys"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let pin_opt = sub_arg_matches.value_of("pin");
let pin_opt = pinutil::get_pin(pin_opt);
let pin_opt = pin_opt.as_deref();
let pin = opt_value_result!(pin_opt, "Pin must be assigned");
if pin.len() < 8 { return simple_error!("Admin pin length:{}, must >= 8!", pin.len()); }
let pass = opt_value_result!(sub_arg_matches.value_of("pass"), "Pass must assigned!");
let password = Password::from(pass);
let pgp_in_file = opt_value_result!(sub_arg_matches.value_of("in"), "PGP in file must assigned!");
let mut pgp_rsa_private_key_set = PgpRsaPrivateKeySet::new();
let mut ppr = PacketParser::from_file(pgp_in_file)?;
let mut last_pgp_rsa_private_key = None;
while let PacketParserResult::Some(pp) = ppr {
if let Packet::SecretKey(key) = &pp.packet {
let key = key.role_as_subordinate();
let pgp_rsa_private_key = parse_security_sub_key_to_pgp_rsa_private_key(key, &password)?;
if last_pgp_rsa_private_key.is_some() { return simple_error!("Last PGP RSA private key is not none"); }
last_pgp_rsa_private_key.replace(pgp_rsa_private_key);
} else if let Packet::SecretSubkey(key) = &pp.packet {
let pgp_rsa_private_key = parse_security_sub_key_to_pgp_rsa_private_key(key, &password)?;
if last_pgp_rsa_private_key.is_some() { return simple_error!("Last PGP RSA private key is not none"); }
last_pgp_rsa_private_key.replace(pgp_rsa_private_key);
} else if let Packet::Signature(signature) = &pp.packet {
if let Signature::V3(_signature_v3) = signature {
failure!("Signature v3 is not supported.");
}
if let Signature::V4(signature_v4) = signature {
if let Some(sub_package) = signature_v4.hashed_area().subpacket(SubpacketTag::KeyFlags) {
if let SubpacketValue::KeyFlags(key_flags) = sub_package.value() {
if last_pgp_rsa_private_key.is_none() {
return simple_error!("Last PGP RSA private key is none, for signature flag: {:?}", key_flags);
}
if key_flags.for_certification() || key_flags.for_signing() {
pgp_rsa_private_key_set.signing = last_pgp_rsa_private_key.take();
} else if key_flags.for_transport_encryption() || key_flags.for_storage_encryption() {
pgp_rsa_private_key_set.encryption = last_pgp_rsa_private_key.take();
} else if key_flags.for_authentication() {
pgp_rsa_private_key_set.authentication = last_pgp_rsa_private_key.take();
} else {
return simple_error!("Unknown signature flags: {:?}", key_flags);
}
}
}
}
}
// Start parsing the next packet, recursing.
ppr = pp.recurse()?.1;
}
debugging!("Found PGP RSA private key set: {:?}", pgp_rsa_private_key_set);
if pgp_rsa_private_key_set.signing.is_none()
|| pgp_rsa_private_key_set.encryption.is_none()
|| pgp_rsa_private_key_set.authentication.is_none() {
warning!("PGP RSA private keys is not complete!");
}
success!("Found PGP RSA private keys, signing: {}, encryption: {}, authentication: {}",
pgp_rsa_private_key_set.signing.is_some(),
pgp_rsa_private_key_set.encryption.is_some(),
pgp_rsa_private_key_set.authentication.is_some());
let print_private_keys = sub_arg_matches.is_present("print-private-keys");
let print_public_keys = sub_arg_matches.is_present("print-public-keys");
if let Some(signing_key) = &pgp_rsa_private_key_set.signing {
if print_private_keys {
let signing_key_pem = opt_result!(signing_key.rsa_private_key.to_pem(), "Signing private key to pem failed: {}");
information!("Signing key: {}", signing_key_pem);
}
if print_public_keys {
let signing_public_key_pem = opt_result!(signing_key.rsa_private_key.to_public_key_pem(), "Signing public key to pem failed: {}");
information!("Signing public key: {}", signing_public_key_pem);
}
}
if let Some(encryption_key) = &pgp_rsa_private_key_set.encryption {
if print_private_keys {
let encryption_key_pem = opt_result!(encryption_key.rsa_private_key.to_pem(), "Encryption private key to pem failed: {}");
information!("Encryption key: {}", encryption_key_pem);
}
if print_public_keys {
let encryption_public_key_pem = opt_result!(encryption_key.rsa_private_key.to_public_key_pem(), "Encryption public key to pem failed: {}");
information!("Encryption public key: {}", encryption_public_key_pem);
}
}
if let Some(authentication_key) = &pgp_rsa_private_key_set.authentication {
if print_private_keys {
let authentication_key_pem = opt_result!(authentication_key.rsa_private_key.to_pem(), "Authentication private key to pem failed: {}");
information!("Authentication key: {}", authentication_key_pem);
}
if print_public_keys {
let authentication_public_key_pem = opt_result!(authentication_key.rsa_private_key.to_public_key_pem(), "Authentication public key to pem failed: {}");
information!("Authentication public key: {}", authentication_public_key_pem);
}
}
let force_make = sub_arg_matches.is_present("force-make");
if !force_make {
warning!("Force make is OFF, add argument --force-make to open, skip write private keys to card!");
return Ok(None);
}
warning!("Force make is ON, try to write private keys to card!");
let card = crate::pgpcardutil::get_card()?;
let mut pgp = OpenPgp::new(card);
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
opt_result!(trans.verify_pw3(pin.as_ref()), "Admin pin verify failed: {}");
success!("Admin pin verify success!");
if let Some(signing_key) = pgp_rsa_private_key_set.signing {
let signing_key_id = signing_key.key_id.clone();
information!("Prepare write PGP signing key, key id: {}", signing_key_id);
opt_result!(trans.key_import(Box::new(signing_key), KeyType::Signing), "Write PGP signing key failed: {}");
success!("Write PGP signing key success, key id: {}", signing_key_id);
}
if let Some(encryption_key) = pgp_rsa_private_key_set.encryption {
let encryption_key_id = encryption_key.key_id.clone();
information!("Prepare write PGP encryption key, key id: {}", encryption_key_id);
opt_result!(trans.key_import(Box::new(encryption_key), KeyType::Decryption), "Write PGP encryption key failed: {}");
success!("Write PGP encryption key success, key id: {}", encryption_key_id);
}
if let Some(authentication_key) = pgp_rsa_private_key_set.authentication {
let authentication_key_id = authentication_key.key_id.clone();
information!("Prepare write PGP authentication key, key id: {}", authentication_key_id);
opt_result!(trans.key_import(Box::new(authentication_key), KeyType::Authentication), "Write PGP authentication key failed: {}");
success!("Write PGP authentication key success, key id: {}", authentication_key_id);
}
Ok(None)
}
}
fn parse_security_sub_key_to_pgp_rsa_private_key(key: &Key<SecretParts, SubordinateRole>, password: &Password) -> XResult<PgpRsaPrivateKey> {
information!("Public key, key id: {}, fingerprint: {}", key.keyid(), key.fingerprint());
let e = if let MpiPublicKey::RSA { e, n: _ } = key.mpis() {
e.clone()
} else {
return simple_error!("Not RSA public key");
};
// default PGP implementation SHOULD encrypt private keys
// TODO information!("{:?}", key.secret());
let private_key = if key.has_unencrypted_secret() {
key.clone()
} else {
opt_result!(key.clone().decrypt_secret(password), "Decrypt private key failed: {}")
};
let (p, q) = if let Key::V4(private_key4) = private_key {
if let SecretKeyMaterial::Unencrypted(unencrypted) = private_key4.secret() {
unencrypted.map(|f| {
let p_and_q_result: XResult<(ProtectedMPI, ProtectedMPI)> = if let openpgp::crypto::mpi::SecretKeyMaterial::RSA { d: _, p, q, u: _ } = f {
Ok((p.clone(), q.clone()))
} else {
simple_error!("Not RSA private key")
};
p_and_q_result
})?
} else {
return simple_error!("Not unencrypted private key");
}
} else {
return simple_error!("Not Key::V4 private key");
};
let p = BigNum::from_slice(p.value()).unwrap();
let q = BigNum::from_slice(q.value()).unwrap();
let e = BigNum::from_slice(e.value()).unwrap();
let rsa_crt = opt_result!(RsaCrt::from(p, q, e), "Parse RSA crt failed: {}");
let creation_time_secs = key.creation_time().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as u32;
Ok(PgpRsaPrivateKey {
creation_time_secs,
key_id: key.keyid(),
fingerprint: key.fingerprint(),
rsa_private_key: rsa_crt,
})
}

199
src/cmd_pgp_card_sign.rs Normal file
View File

@@ -0,0 +1,199 @@
use std::collections::BTreeMap;
use std::fs::File;
use std::io::{ErrorKind, Read};
use std::ops::Deref;
use clap::{App, Arg, ArgMatches, SubCommand};
use digest::Digest;
use openpgp_card::crypto_data::Hash;
use rust_util::XResult;
use rust_util::util_clap::{Command, CommandError};
use sha2::{Sha256, Sha384, Sha512};
use crate::{cmdutil, pgpcardutil, pinutil, util};
use crate::util::base64_encode;
const BUFF_SIZE: usize = 512 * 1024;
#[derive(Debug, Clone, Copy)]
enum SignAlgo {
Rsa,
Ecdsa,
EdDsa,
}
impl SignAlgo {
fn from_str(algo: &str) -> XResult<Self> {
match algo {
"rsa" => Ok(Self::Rsa),
"ecdsa" => Ok(Self::Ecdsa),
"eddsa" => Ok(Self::EdDsa),
_ => simple_error!("Unknown algo: {}", algo),
}
}
}
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "pgp-card-sign" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("OpenPGP Card sign subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).help("OpenPGP card user pin"))
.arg(Arg::with_name("pass").long("pass").takes_value(true).help("[deprecated] now OpenPGP card user pin"))
.arg(Arg::with_name("sha256").short("2").long("sha256").takes_value(true).help("Digest SHA256 HEX"))
.arg(Arg::with_name("sha384").short("3").long("sha384").takes_value(true).help("Digest SHA384 HEX"))
.arg(Arg::with_name("sha512").short("5").long("sha512").takes_value(true).help("Digest SHA512 HEX"))
.arg(Arg::with_name("in").short("i").long("in").takes_value(true).help("File in"))
.arg(Arg::with_name("use-sha256").long("use-sha256").help("Use SHA256 for file in"))
.arg(Arg::with_name("use-sha384").long("use-sha384").help("Use SHA384 for file in"))
.arg(Arg::with_name("use-sha512").long("use-sha512").help("Use SHA512 for file in"))
.arg(Arg::with_name("algo").long("algo").takes_value(true).help("Algorithm, rsa, ecdsa, eddsa"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let pin_opt = sub_arg_matches.value_of("pass").or_else(|| sub_arg_matches.value_of("pin"));
let pin_opt = pinutil::get_pin(pin_opt);
let pin_opt = pin_opt.as_deref();
let pin = opt_value_result!(pin_opt, "User pin must be assigned");
if pin.len() < 6 { return simple_error!("User pin length:{}, must >= 6!", pin.len()); }
let mut sha256 = sub_arg_matches.value_of("sha256").map(|s| s.to_string());
let mut sha384 = sub_arg_matches.value_of("sha384").map(|s| s.to_string());
let mut sha512 = sub_arg_matches.value_of("sha512").map(|s| s.to_string());
let file_in_opt = sub_arg_matches.value_of("in");
let algo = sub_arg_matches.value_of("algo").unwrap_or("rsa").to_lowercase();
let algo = SignAlgo::from_str(&algo)?;
let mut json = BTreeMap::new();
if let Some(file_in) = file_in_opt {
if sha256.is_some() || sha384.is_some() || sha512.is_some() { return simple_error!("Conflict --in vs --sha256, --sha384, --sha512 args"); }
let use_sha256 = sub_arg_matches.is_present("use-sha256");
let use_sha384 = sub_arg_matches.is_present("use-sha384");
let use_sha512 = sub_arg_matches.is_present("use-sha512");
if !use_sha256 && !use_sha384 && !use_sha512 {
return simple_error!("Must has one option --use-sha256, --use-sha384, --use-sha512");
}
if use_sha256 {
let hash = opt_result!(calc_file_digest::<Sha256>(file_in), "Calc file: {} SHA256 failed: {}", file_in);
let hash_hex = hex::encode(hash);
information!("File SHA256 hex: {}", &hash_hex);
sha256 = Some(hash_hex);
}
if use_sha384 {
let hash = opt_result!(calc_file_digest::<Sha384>(file_in), "Calc file: {} SHA384 failed: {}", file_in);
let hash_hex = hex::encode(hash);
information!("File SHA384 hex: {}", &hash_hex);
sha384 = Some(hash_hex);
}
if use_sha512 {
let hash = opt_result!(calc_file_digest::<Sha512>(file_in), "Calc file: {} SHA512 failed: {}", file_in);
let hash_hex = hex::encode(hash);
information!("File SHA512 hex: {}", &hash_hex);
sha512 = Some(hash_hex);
}
let mut entry = BTreeMap::new();
entry.insert("file", file_in.to_string());
json.insert("meta", entry);
}
if sha256.is_none() && sha384.is_none() && sha512.is_none() {
return simple_error!("SHA256, SHA384 or SHA512 must assign at least one");
}
let mut pgp = pgpcardutil::get_openpgp_card()?;
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
if let Some(sha256) = sha256 {
let sha256_hex = opt_result!(hex::decode(sha256.trim()), "Decode sha256 failed: {}");
let sha256_hex = crate::digestutil::copy_sha256(&sha256_hex)?;
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
success!("User sign pin verify success!");
let sig = match algo {
SignAlgo::Rsa => trans.signature_for_hash(Hash::SHA256(sha256_hex))?,
SignAlgo::Ecdsa => trans.signature_for_hash(Hash::ECDSA(&sha256_hex))?,
SignAlgo::EdDsa => trans.signature_for_hash(Hash::EdDSA(&sha256_hex))?,
};
success!("SHA256 signature HEX: {}", hex::encode(&sig));
success!("SHA256 signature base64: {}", base64_encode(&sig));
if json_output {
let mut entry = BTreeMap::new();
entry.insert("digest", hex::encode(sha256_hex));
entry.insert("signature", hex::encode(&sig));
json.insert("sha256", entry);
}
}
if let Some(sha384) = sha384 {
let sha384_hex = opt_result!(hex::decode(sha384.trim()), "Decode sha384 failed: {}");
let sha384_hex = crate::digestutil::copy_sha384(&sha384_hex)?;
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
success!("User sign pin verify success!");
let sig = match algo {
SignAlgo::Rsa => trans.signature_for_hash(Hash::SHA384(sha384_hex))?,
SignAlgo::Ecdsa => trans.signature_for_hash(Hash::ECDSA(&sha384_hex))?,
SignAlgo::EdDsa => trans.signature_for_hash(Hash::EdDSA(&sha384_hex))?,
};
success!("SHA384 signature HEX: {}", hex::encode(&sig));
success!("SHA384 signature base64: {}", base64_encode(&sig));
if json_output {
let mut entry = BTreeMap::new();
entry.insert("digest", hex::encode(sha384_hex));
entry.insert("signature", hex::encode(&sig));
json.insert("sha384", entry);
}
}
if let Some(sha512) = sha512 {
let sha512_hex = opt_result!(hex::decode(sha512.trim()), "Decode sha512 failed: {}");
let sha512_hex = crate::digestutil::copy_sha512(&sha512_hex)?;
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
success!("User sign pin verify success!");
let sig = match algo {
SignAlgo::Rsa => trans.signature_for_hash(Hash::SHA512(sha512_hex))?,
SignAlgo::Ecdsa => trans.signature_for_hash(Hash::ECDSA(&sha512_hex))?,
SignAlgo::EdDsa => trans.signature_for_hash(Hash::EdDSA(&sha512_hex))?,
};
success!("SHA512 signature HEX: {}", hex::encode(&sig));
success!("SHA512 signature base64: {}", base64_encode(&sig));
if json_output {
let mut entry = BTreeMap::new();
entry.insert("digest", hex::encode(sha512_hex));
entry.insert("signature", hex::encode(&sig));
json.insert("sha512", entry);
}
}
if json_output {
util::print_pretty_json(&json);
}
Ok(None)
}
}
fn calc_file_digest<D>(file_name: &str) -> XResult<Vec<u8>>
where
D: Digest,
{
let mut hasher = D::new();
let mut buf: [u8; BUFF_SIZE] = [0u8; BUFF_SIZE];
let mut f = File::open(file_name)?;
let file_len = f.metadata()?.len();
debugging!("File: {}, length: {}", file_name, file_len);
loop {
let len = match f.read(&mut buf) {
Ok(0) => return Ok(hasher.finalize().deref().to_vec()),
Ok(len) => len,
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return simple_error!("Calc file digest failed: {}", e),
};
hasher.update(&buf[..len]);
}
}

View File

@@ -1,58 +0,0 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use openpgp_card::DecryptMe;
use rust_util::util_clap::{Command, CommandError};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "pgp-card-decrypt" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("OpenPGP Card Decrypt subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).default_value("123456").help("OpenPGP card user pin"))
.arg(Arg::with_name("pass").long("pass").takes_value(true).help("[deprecated] now OpenPGP card user pin"))
.arg(Arg::with_name("cipher").short("c").long("cipher").takes_value(true).help("Cipher text HEX"))
.arg(Arg::with_name("cipher-base64").short("b").long("cipher-base64").takes_value(true).help("Cipher text base64"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { rust_util::util_msg::set_logger_std_out(false); }
let pin_opt = sub_arg_matches.value_of("pass").or(sub_arg_matches.value_of("pin"));
let pin = opt_value_result!(pin_opt, "User pin must be assigned");
if pin.len() < 6 { return simple_error!("User pin length:{}, must >= 6!", pin.len()); }
let cipher = sub_arg_matches.value_of("cipher");
let cipher_base64 = sub_arg_matches.value_of("cipher-base64");
let cipher_bytes = if let Some(cipher) = cipher {
opt_result!(hex::decode(cipher), "Decode cipher failed: {}")
} else if let Some(cipher_base64) = cipher_base64 {
opt_result!(base64::decode(cipher_base64), "Decode cipher-base64 failed: {}")
} else {
return simple_error!("cipher or cipher-base64 must assign one");
};
let user = crate::pgpcardutil::get_card_user_sw1_82(pin)?;
let text = user.decrypt(DecryptMe::RSA(&cipher_bytes))?;
success!("Clear text HEX: {}", hex::encode(&text));
success!("Clear text base64: {}", base64::encode(&text));
success!("Clear text UTF-8: {}", String::from_utf8_lossy(&text));
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("cipher_hex", hex::encode(&cipher_bytes));
json.insert("cipher_base64", base64::encode(&cipher_bytes));
json.insert("text_hex", hex::encode(&text));
json.insert("text_base64", base64::encode(&text));
json.insert("text_utf8", String::from_utf8_lossy(&text).to_string());
println!("{}", serde_json::to_string_pretty(&json).unwrap());
}
Ok(None)
}
}

View File

@@ -1,76 +0,0 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use openpgp_card::{KeyType, OpenPGPCard};
use rust_util::util_clap::{Command, CommandError};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "pgp-card-list" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("OpenPGP Card List subcommand")
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { rust_util::util_msg::set_logger_std_out(false); }
let mut json = BTreeMap::new();
let cards = opt_result!(OpenPGPCard::list_cards(), "Failed to list OpenPGP cards: {}");
information!("Found {} card(s)", cards.len());
for (i, card) in cards.iter().enumerate() {
success!("Found card #{}: {:?}", i, card.get_aid());
debugging!("Historical: {:?}", card.get_historical());
debugging!("Extended length information{:?}", card.get_extended_length_information());
debugging!("Extended capabilities{:?}", card.get_extended_capabilities());
debugging!("Security support template{:?}", card.get_security_support_template());
if let Ok(url) = card.get_url() {
information!("URL: {}", iff!(url.is_empty(), "<empty>", &url));
}
if let Ok(card_holder) = card.get_cardholder_related_data() {
information!("Card holder: {:?}", card_holder);
}
if let Ok(supported_algo) = card.list_supported_algo() {
information!("Supported algo: {:?}", supported_algo);
}
if let Ok(fingerprints) = card.get_fingerprints() {
if let Some(a) = fingerprints.authentication() {
if let Ok(algo) = card.get_algorithm_attributes(KeyType::Authentication) {
information!("Authentication algo: {:?}", algo);
}
information!("Authentication fingerprint: {}", a);
if json_output {
json.insert("authentication_fingerprint", a.to_string());
}
}
if let Some(d) = fingerprints.decryption() {
if let Ok(algo) = card.get_algorithm_attributes(KeyType::Decryption) {
information!("Encryption algo: {:?}", algo);
}
information!("Encryption fingerprint: {}", d);
if json_output {
json.insert("encryption_fingerprint", d.to_string());
}
}
if let Some(s) = fingerprints.signature() {
if let Ok(algo) = card.get_algorithm_attributes(KeyType::Signing) {
information!("Signature algo: {:?}", algo);
}
information!("Signature fingerprint: {}", s);
if json_output {
json.insert("signature_fingerprint", s.to_string());
}
}
}
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
}
Ok(None)
}
}

View File

@@ -1,108 +0,0 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use openpgp_card::Hash;
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use crate::pgpcardutil;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "pgp-card-sign" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("OpenPGP Card Sign subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).default_value("123456").help("OpenPGP card user pin"))
.arg(Arg::with_name("pass").long("pass").takes_value(true).help("[deprecated] now OpenPGP card user pin"))
.arg(Arg::with_name("sha256").short("2").long("sha256").takes_value(true).help("Digest SHA256 HEX"))
.arg(Arg::with_name("sha384").short("3").long("sha384").takes_value(true).help("Digest SHA384 HEX"))
.arg(Arg::with_name("sha512").short("5").long("sha512").takes_value(true).help("Digest SHA512 HEX"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { rust_util::util_msg::set_logger_std_out(false); }
let pin_opt = sub_arg_matches.value_of("pass").or(sub_arg_matches.value_of("pin"));
let pin = opt_value_result!(pin_opt, "User pin must be assigned");
if pin.len() < 6 { return simple_error!("User pin length:{}, must >= 6!", pin.len()); }
let sha256 = sub_arg_matches.value_of("sha256");
let sha384 = sub_arg_matches.value_of("sha384");
let sha512 = sub_arg_matches.value_of("sha512");
if sha256.is_none() && sha384.is_none() && sha512.is_none() {
return simple_error!("SHA256, SHA384 or SHA512 must assign at least one");
}
let user = opt_result!(pgpcardutil::get_card_user_sw1_81(pin), "Verify user pin failed: {}");
let mut json = BTreeMap::new();
if let Some(sha256) = sha256 {
let sha256_hex = opt_result!(hex::decode(sha256.trim()), "Decode sha256 failed: {}");
let sha256_hex = copy_sha256(&sha256_hex)?;
let sig = user.signature_for_hash(Hash::SHA256(sha256_hex))?;
success!("SHA256 signature HEX: {}", hex::encode(&sig));
success!("SHA256 signature base64: {}", base64::encode(&sig));
if json_output {
let mut entry = BTreeMap::new();
entry.insert("digest", hex::encode(&sha256_hex));
entry.insert("signature", hex::encode(&sig));
json.insert("sha256", entry);
}
}
if let Some(sha384) = sha384 {
let sha384_hex = opt_result!(hex::decode(sha384.trim()), "Decode sha384 failed: {}");
let sha384_hex = copy_sha384(&sha384_hex)?;
let sig = user.signature_for_hash(Hash::SHA384(sha384_hex))?;
success!("SHA384 signature HEX: {}", hex::encode(&sig));
success!("SHA384 signature base64: {}", base64::encode(&sig));
if json_output {
let mut entry = BTreeMap::new();
entry.insert("digest", hex::encode(&sha384_hex));
entry.insert("signature", hex::encode(&sig));
json.insert("sha384", entry);
}
}
if let Some(sha512) = sha512 {
let sha512_hex = opt_result!(hex::decode(sha512.trim()), "Decode sha512 failed: {}");
let sha512_hex = copy_sha512(&sha512_hex)?;
let sig = user.signature_for_hash(Hash::SHA512(sha512_hex))?;
success!("SHA512 signature HEX: {}", hex::encode(&sig));
success!("SHA512 signature base64: {}", base64::encode(&sig));
if json_output {
let mut entry = BTreeMap::new();
entry.insert("digest", hex::encode(&sha512_hex));
entry.insert("signature", hex::encode(&sig));
json.insert("sha512", entry);
}
}
if json_output {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
}
Ok(None)
}
}
macro_rules! define_copy_array {
($fn_name: ident, $len: tt) => (
fn $fn_name(in_arr: &[u8]) -> XResult<[u8; $len]> {
if in_arr.len() != $len {
return simple_error!("Array length is not: {}, but is: {}", $len, in_arr.len());
}
let mut out_arr = [0_u8; $len];
for i in 0..$len {
out_arr[i] = in_arr[i];
}
Ok(out_arr)
}
)
}
define_copy_array!(copy_sha256, 0x20);
define_copy_array!(copy_sha384, 0x30);
define_copy_array!(copy_sha512, 0x40);

View File

@@ -3,14 +3,16 @@ use std::time::Duration;
use chrono::Local; use chrono::Local;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use digest::Digest; use digest::Digest;
use pem::Pem;
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg::MessageType;
use rust_util::XResult; use rust_util::XResult;
use sha2::Sha256; use sha2::Sha256;
use spki::der::Encode;
use x509_parser::parse_x509_certificate; use x509_parser::parse_x509_certificate;
use yubikey::{Certificate, YubiKey}; use yubikey::{Certificate, YubiKey};
use yubikey::piv::SlotId; use yubikey::piv::SlotId;
use crate::{cmdutil, yubikeyutil};
use crate::pivutil::get_algorithm_id;
use crate::pkiutil::{bytes_to_pem, get_pki_algorithm};
pub struct CommandImpl; pub struct CommandImpl;
@@ -21,43 +23,59 @@ impl Command for CommandImpl {
SubCommand::with_name(self.name()).about("PIV subcommand") SubCommand::with_name(self.name()).about("PIV subcommand")
.arg(Arg::with_name("detail").long("detail").help("Detail output")) .arg(Arg::with_name("detail").long("detail").help("Detail output"))
.arg(Arg::with_name("show-config").long("show-config").help("Show config output")) .arg(Arg::with_name("show-config").long("show-config").help("Show config output"))
.arg(cmdutil::build_serial_arg())
// .arg(Arg::with_name("json").long("json").help("JSON output")) // .arg(Arg::with_name("json").long("json").help("JSON output"))
} }
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let detail_output = sub_arg_matches.is_present("detail"); let detail_output = sub_arg_matches.is_present("detail");
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}"); let show_config = sub_arg_matches.is_present("show-config");
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
success!("Name: {}", yk.name()); success!("Name: {}", yk.name());
success!("Version: {}", yk.version()); information!("Version: {}", yk.version());
success!("Serial: {}", yk.serial()); information!("Serial: {}", yk.serial());
match yk.chuid() { match yk.chuid() {
Ok(chuid) => success!("CHUID: {}",chuid.to_string()), Ok(chuid) => information!("CHUID: {}",chuid.to_string()),
Err(e) => warning!("CHUID: <none> {}", e), Err(e) => warning!("CHUID: <none> {}", e),
} }
match yk.cccid() { match yk.cccid() {
Ok(cccid) => success!("CCCID: {}",cccid.to_string()), Ok(cccid) => information!("CCCID: {}",cccid.to_string()),
Err(e) => warning!("CCCID: <none> {}", e), Err(e) => warning!("CCCID: <none> {}", e),
} }
match yk.get_pin_retries() { match yk.get_pin_retries() {
Ok(pin_retries) => success!("PIN retries: {}",pin_retries), Ok(pin_retries) => information!("PIN retries: {}",pin_retries),
Err(e) => warning!("PIN retries: <none> {}", e), Err(e) => warning!("PIN retries: <none> {}", e),
} }
if sub_arg_matches.is_present("show-config") { if show_config {
let config = yk.config(); let config = yk.config();
information!("Config: {:#?}", config); information!("Config: {:#?}", config);
} }
match yk.piv_keys() { match yk.piv_keys() {
Ok(keys) => { Ok(mut keys) => {
information!("Found {} PIV keys", keys.len()); information!("Found {} PIV keys", keys.len());
keys.sort_by_key(|a| u8::from(a.slot()));
for k in keys {
information!(
"Found PIV #{:x} @{}, subject: {}, signature: {}",
u8::from(k.slot()),
k.slot(),
k.certificate().subject(),
k.certificate().cert.signature_algorithm.oid
);
if detail_output { if detail_output {
keys.iter().for_each(|k| information!("Found key: {:?}", k)); debugging!("Found key: {:?}", k);
}
} }
} }
Err(e) => failure!("Get PIV keys failed: {}", e) Err(e) => failure!("Get PIV keys failed: {}", e)
} }
for slot in yubikey::piv::SLOTS.iter().cloned() { // replace of yubikey::piv::SLOTS
let slots = vec![SlotId::Authentication, SlotId::Signature,
SlotId::KeyManagement, SlotId::CardAuthentication];
for slot in slots {
print_cert_info(&mut yk, slot, detail_output).ok(); print_cert_info(&mut yk, slot, detail_output).ok();
} }
Ok(None) Ok(None)
@@ -73,35 +91,40 @@ fn print_cert_info(yubikey: &mut YubiKey, slot: SlotId, detail_output: bool) ->
return simple_error!("error reading certificate in slot {:?}: {}", slot, e); return simple_error!("error reading certificate in slot {:?}: {}", slot, e);
} }
}; };
let buf = cert.into_buffer(); let buf_vec = cert.cert.to_der()?;
let buf: &[u8] = buf_vec.as_ref();
if !buf.is_empty() { if !buf.is_empty() {
information!("{}", "-".repeat(88)); information!("{}", "-".repeat(88));
let fingerprint_sha256 = Sha256::digest(&buf); let certificate_fingerprint_sha256 = Sha256::digest(buf);
let slot_id: u8 = slot.into(); let slot_id: u8 = slot.into();
success!("Slot: {:?}, id: {:x}", slot, slot_id); let algorithm_id = get_algorithm_id(&cert.cert.tbs_certificate.subject_public_key_info)
.map(|aid| format!("{:?}", aid))
.unwrap_or_else(|e| format!("Error: {}", e));
success!("Slot: {:?}, id: {:x}, algorithm: {}", slot, slot_id, algorithm_id);
let cert_pem_obj = Pem {
tag: String::from("CERTIFICATE"),
contents: buf.to_vec(),
};
if detail_output { if detail_output {
information!("{}", pem::encode(&cert_pem_obj).trim()); information!("{}", bytes_to_pem("CERTIFICATE", buf));
} else {
rust_util::util_msg::when(MessageType::DEBUG, || {
debugging!("{}", pem::encode(&cert_pem_obj).trim());
});
} }
match parse_x509_certificate(&buf) { match parse_x509_certificate(buf) {
Ok((_rem, cert)) => { Ok((_rem, cert)) => {
success!("Algorithm: {}", cert.tbs_certificate.subject_pki.algorithm.algorithm); debugging!("Algorithm: {:?}", &cert.tbs_certificate.subject_pki.algorithm);
information!("Algorithm: {:?}", get_pki_algorithm(&cert.tbs_certificate.subject_pki.algorithm));
success!("Subject: {}", cert.tbs_certificate.subject); debugging!("Public key: {}", hex::encode(&cert.tbs_certificate.subject_pki.subject_public_key));
success!("Issuer: {}", cert.tbs_certificate.issuer);
success!("Fingerprint(SHA256): {}", hex::encode(fingerprint_sha256)); let public_key_fingerprint_sha256 = Sha256::digest(cert.tbs_certificate.subject_pki.raw);
success!("Not Before: {}", cert.tbs_certificate.validity.not_before.to_rfc2822());
if detail_output {
information!("{}", bytes_to_pem("PUBLIC KEY", cert.tbs_certificate.subject_pki.raw));
}
information!("Subject: {}", cert.tbs_certificate.subject);
information!("Issuer: {}", cert.tbs_certificate.issuer);
information!("Certificate fingerprint(SHA256): {}", hex::encode(certificate_fingerprint_sha256));
information!("Public key fingerprint(SHA256): {}", hex::encode(public_key_fingerprint_sha256));
information!("Not Before: {}", cert.tbs_certificate.validity.not_before.to_rfc2822().unwrap_or_else(|e| format!("Err: {}", e)));
let mut not_after_desc = String::new(); let mut not_after_desc = String::new();
let not_after_timestamp = cert.tbs_certificate.validity.not_after.timestamp(); let not_after_timestamp = cert.tbs_certificate.validity.not_after.timestamp();
@@ -113,7 +136,10 @@ fn print_cert_info(yubikey: &mut YubiKey, slot: SlotId, detail_output: bool) ->
let valid_time = simpledateformat::format_human(Duration::from_secs((not_after_timestamp - now_timestamp) as u64)); let valid_time = simpledateformat::format_human(Duration::from_secs((not_after_timestamp - now_timestamp) as u64));
not_after_desc.push_str(&format!("(left {})", valid_time)); not_after_desc.push_str(&format!("(left {})", valid_time));
} }
success!("Not After: {} {}", cert.tbs_certificate.validity.not_after.to_rfc2822(), not_after_desc); information!("Not After: {} {}",
cert.tbs_certificate.validity.not_after.to_rfc2822().unwrap_or_else(|e| format!("Err: {}", e)),
not_after_desc
);
} }
_ => { _ => {
warning!("Failed to parse certificate"); warning!("Failed to parse certificate");

81
src/cmd_piv_decrypt.rs Normal file
View File

@@ -0,0 +1,81 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use yubikey::piv::AlgorithmId;
use crate::{cmdutil, pinutil, pivutil, util, yubikeyutil};
use crate::util::{read_stdin, try_decode};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "piv-decrypt" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV decrypt(RSA) subcommand")
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("ciphertext").long("ciphertext").short("c").takes_value(true).help("Encrypted data (HEX or Base64)"))
.arg(Arg::with_name("stdin").long("stdin").help("Standard input (Ciphertext)"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
let pin_opt = pinutil::read_pin(sub_arg_matches);
let encrypted_data = if let Some(ciphertext) = sub_arg_matches.value_of("ciphertext") {
opt_result!(try_decode(ciphertext), "Decode --ciphertext failed: {}")
} else if sub_arg_matches.is_present("stdin") {
read_stdin()?
} else {
return simple_error!("Argument --ciphertext must be assigned");
};
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
if let Some(pin) = &pin_opt {
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
}
let slot_id = pivutil::get_slot_id(slot)?;
let decrypt_result = yubikey::piv::decrypt_data(&mut yk, &encrypted_data,
AlgorithmId::Rsa2048, slot_id);
let decrypted_data = opt_result!(decrypt_result, "Decrypt data failed: {}");
let decrypted_data_bytes = decrypted_data.as_slice();
information!("Decrypted raw data: {}", hex::encode(decrypted_data_bytes));
if !(decrypted_data_bytes[0] == 0x00 && decrypted_data_bytes[1] == 0x02) {
return simple_error!("Not valid encrypted data, prefix: {}", hex::encode(&decrypted_data_bytes[0..2]));
}
let mut index_of_00_from_index_1 = 0;
for (i, byte) in decrypted_data_bytes.iter().enumerate().skip(1) {
if *byte == 0x00 {
index_of_00_from_index_1 = i + 1;
break;
}
}
if index_of_00_from_index_1 == 0 {
return simple_error!("Not valid encrypted data, cannot find 0x00");
}
let clear_data = &decrypted_data_bytes[index_of_00_from_index_1..];
success!("Decrypt data: {}", hex::encode(clear_data));
success!("Decrypt data in UTF-8: {}", String::from_utf8_lossy(clear_data));
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("encrypted_data_hex", hex::encode(&encrypted_data));
json.insert("decrypted_data_hex", hex::encode(decrypted_data_bytes));
json.insert("clear_data_hex", hex::encode(clear_data));
json.insert("clear_data", String::from_utf8_lossy(clear_data).to_string());
util::print_pretty_json(&json);
}
Ok(None)
}
}

134
src/cmd_piv_ecdh.rs Normal file
View File

@@ -0,0 +1,134 @@
use std::collections::BTreeMap;
use std::fs;
use clap::{App, Arg, ArgMatches, SubCommand};
use rand::rngs::OsRng;
use rust_util::util_clap::{Command, CommandError};
use yubikey::PinPolicy;
use yubikey::piv::{AlgorithmId, decrypt_data, metadata};
use crate::{cmdutil, ecdhutil, pinutil, pivutil, util, yubikeyutil};
use crate::pivutil::get_algorithm_id;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "piv-ecdh" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV ECDH subcommand")
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("public-256").long("public-256").help("Public key (P-256)"))
.arg(Arg::with_name("public-384").long("public-384").help("Public key (P-384)"))
.arg(Arg::with_name("private").long("private").help("Private key(PIV)"))
.arg(Arg::with_name("epk").long("epk").takes_value(true).help("E-Public key"))
.arg(Arg::with_name("public-key").long("public-key").takes_value(true).help("Public key"))
.arg(Arg::with_name("public-key-file").long("public-key-file").takes_value(true).help("Public key"))
.arg(Arg::with_name("public-key-point-hex").long("public-key-point-hex").takes_value(true).help("Public key point hex"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let public256 = sub_arg_matches.is_present("public-256");
let public384 = sub_arg_matches.is_present("public-384");
let public = public256 || public384;
if public256 && public384 {
failure_and_exit!("--public-256 and --public-384 only allow one");
}
let private = sub_arg_matches.is_present("private");
if !public && !private {
failure_and_exit!("--public-256, --public-384 or --private requires one");
} else if public && private {
failure_and_exit!("--public-256, --public-384 and --private only allow one");
}
let mut json = BTreeMap::<&'_ str, String>::new();
if public {
let public_key_pem_opt = sub_arg_matches.value_of("public-key").map(ToString::to_string)
.or_else(|| match sub_arg_matches.value_of("public-key-file") {
None => None,
Some(file) => match fs::read_to_string(file) {
Err(e) => failure_and_exit!("Read from file: {}, failed: {}", file, e),
Ok(key) => Some(key),
}
});
if let Some(public_key_pem) = &public_key_pem_opt {
debugging!("Public key: {}", public_key_pem);
}
if public256 {
ecdhutil::piv_ecdh!(p256, public_key_pem_opt, sub_arg_matches, json, json_output);
} else {
ecdhutil::piv_ecdh!(p384, public_key_pem_opt, sub_arg_matches, json, json_output);
}
}
if private {
let pin_opt = pinutil::read_pin(sub_arg_matches);
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ...");
let epk = opt_value_result!(sub_arg_matches.value_of("epk"), "--epk must assigned");
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
let slot_id = pivutil::get_slot_id(slot)?;
debugging!("Slot id: {}", slot_id);
if let Ok(meta) = metadata(&mut yk, slot_id) {
debugging!("PIV meta: {:?}", meta);
if let Some((pin_policy, _touch_policy)) = meta.policy {
match pin_policy {
PinPolicy::Never => {}
_ => if pin_opt.is_none() {
failure_and_exit!("Slot pin is required");
}
}
}
if let Some(public_key) = &meta.public {
let algorithm_id = opt_result!(get_algorithm_id(public_key), "Get algorithm id failed: {}");
match algorithm_id {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
failure_and_exit!("Not supported algorithm: {:?}", algorithm_id);
}
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
if let Some(public) = &meta.public {
json.insert("pk_point_hex", hex::encode(public.subject_public_key.raw_bytes()));
}
}
}
}
} else {
warning!("Get slot: {} meta data failed", slot);
}
if let Some(pin) = &pin_opt {
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
}
let epk_bytes = opt_result!(hex::decode(epk), "Parse epk failed: {}");
let epk_bits = ((epk_bytes.len() - 1) / 2) * 8;
debugging!("Epk {} bits", epk_bits);
let decrypted_shared_secret = opt_result!(decrypt_data(
&mut yk,
&epk_bytes,
iff!(epk_bits == 256, AlgorithmId::EccP256, AlgorithmId::EccP384),
slot_id,
), "Decrypt piv failed: {}");
if json_output {
json.insert("shared_secret_hex", hex::encode(&decrypted_shared_secret));
} else {
information!("Shared secret: {}", hex::encode(&decrypted_shared_secret));
}
}
if json_output {
util::print_pretty_json(&json);
}
Ok(None)
}
}

88
src/cmd_piv_ecsign.rs Normal file
View File

@@ -0,0 +1,88 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use x509_parser::nom::AsBytes;
use yubikey::piv::{metadata, sign_data, AlgorithmId, ManagementAlgorithmId};
use crate::util::base64_encode;
use crate::{argsutil, cmdutil, pivutil, util, yubikeyutil};
use crate::digestutil::DigestAlgorithm;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "piv-ecsign" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV EC sign(with SHA256/SHA384) subcommand")
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("algorithm").short("a").long("algorithm").takes_value(true).help("Algorithm, p256 or p384"))
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).help("Input file"))
.arg(Arg::with_name("input").short("i").long("input").takes_value(true).help("Input"))
.arg(Arg::with_name("hash-hex").short("x").long("hash-hex").takes_value(true).help("Hash"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let mut json = BTreeMap::<&'_ str, String>::new();
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
let (algorithm, algorithm_str, digest_algorithm) = match sub_arg_matches.value_of("algorithm") {
None | Some("p256") => (AlgorithmId::EccP256, "ecdsa_p256_with_sha256", DigestAlgorithm::Sha256),
Some("p384") => (AlgorithmId::EccP384, "ecdsa_p384_with_sha384", DigestAlgorithm::Sha384),
Some(unknown_algorithm) => return simple_error!("Unknown algorithm {}, e.g. p256 or p384", unknown_algorithm),
};
let hash_bytes = argsutil::get_digest_or_hash(sub_arg_matches, digest_algorithm)?;
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
let slot_id = pivutil::get_slot_id(slot)?;
let pin_opt = pivutil::check_read_pin(&mut yk, slot_id, sub_arg_matches);
if let Some(pin) = &pin_opt {
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
}
if let Ok(slot_metadata) = metadata(&mut yk, slot_id) {
match slot_metadata.algorithm {
ManagementAlgorithmId::PinPuk | ManagementAlgorithmId::ThreeDes => {
return simple_error!("Slot not supports PIV sign: {:?}", slot_metadata.algorithm);
}
ManagementAlgorithmId::Asymmetric(slot_algorithm) => {
if AlgorithmId::Rsa1024 == slot_algorithm || AlgorithmId::Rsa2048 == algorithm {
return simple_error!("Slot supports PIV RSA sign: {:?}, but requires ECDSA", slot_metadata.algorithm);
}
if slot_algorithm != algorithm {
return simple_error!("Slot supported PIV sign not match: {:?}", slot_metadata.algorithm);
}
}
}
}
let signed_data = opt_result!(sign_data(&mut yk, &hash_bytes, algorithm, slot_id), "Sign PIV failed: {}");
if json_output {
json.insert("slot", slot_id.to_string());
json.insert("algorithm", algorithm_str.to_string());
json.insert("hash_hex", hex::encode(&hash_bytes));
json.insert("signed_data_hex", hex::encode(signed_data.as_bytes()));
json.insert("signed_data_base64", base64_encode(signed_data.as_bytes()));
} else {
information!("Slot: {:?}", slot_id);
information!("Algorithm: {}", algorithm_str);
information!("Hash hex: {}", hex::encode(&hash_bytes));
information!("Signed data base64: {}", base64_encode(signed_data.as_bytes()));
information!("Signed data hex: {}", hex::encode(signed_data.as_bytes()));
}
if json_output {
util::print_pretty_json(&json);
}
Ok(None)
}
}

42
src/cmd_piv_generate.rs Normal file
View File

@@ -0,0 +1,42 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use yubikey::{PinPolicy, piv, TouchPolicy};
use yubikey::piv::{AlgorithmId, SlotId};
use crate::{cmdutil, pinutil, yubikeyutil};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "piv-generate" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV generate subcommand")
.arg(cmdutil::build_pin_arg())
.arg(Arg::with_name("force").long("force").help("Force generate"))
.arg(cmdutil::build_serial_arg())
// .arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
warning!("This feature is not works");
let pin_opt = sub_arg_matches.value_of("pin");
let pin_opt = pinutil::get_pin(pin_opt);
let pin_opt = pin_opt.as_deref();
let pin = opt_value_result!(pin_opt, "User pin must be assigned");
if !sub_arg_matches.is_present("force") {
failure_and_exit!("--force must be assigned");
}
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
let public_key_info = opt_result!(piv::generate(&mut yk,SlotId::Signature, AlgorithmId::Rsa2048,
PinPolicy::Default, TouchPolicy::Default), "Generate key failed: {}");
success!("Generate key success: {:?}", public_key_info);
Ok(None)
}
}

150
src/cmd_piv_meta.rs Normal file
View File

@@ -0,0 +1,150 @@
use std::collections::BTreeMap;
use clap::{App, ArgMatches, SubCommand};
use p256::pkcs8::der::Encode;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use rust_util::util_msg::MessageType;
use x509_parser::parse_x509_certificate;
use yubikey::Key;
use yubikey::piv::{AlgorithmId, metadata};
use crate::{cmdutil, pivutil, util, yubikeyutil};
use crate::keyutil::{KeyAlgorithmId, KeyUri, YubikeyPivKey};
use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
use crate::pkiutil::bytes_to_pem;
use crate::sshutil::SshVecWriter;
use crate::util::base64_encode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "piv-meta" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV meta subcommand")
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let mut json = BTreeMap::<&'_ str, String>::new();
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
let slot_id = pivutil::get_slot_id(slot)?;
json.insert("slot", pivutil::to_slot_hex(&slot_id));
if let Ok(meta) = metadata(&mut yk, slot_id) {
debugging!("PIV meta: {:?}", meta);
let algorithm_str = meta.algorithm.to_str();
if json_output {
json.insert("algorithm", algorithm_str.to_string());
} else {
information!("Algorithm: {}", algorithm_str);
}
if let Some((pin_policy, touch_policy)) = meta.policy {
let pin_policy_str = pin_policy.to_str();
let touch_policy_str = touch_policy.to_str();
if json_output {
json.insert("pin_policy", pin_policy_str.to_string());
json.insert("touch_policy", touch_policy_str.to_string());
} else {
information!("PIN policy: {}", pin_policy_str);
information!("Touch policy: {}", touch_policy_str);
}
}
let origin_str = meta.origin.to_str();
if json_output {
json.insert("origin", origin_str.to_string());
} else {
information!("Origin: {}", origin_str);
}
} else {
warning!("Get slot: {} meta data failed", slot);
}
match Key::list(&mut yk) {
Err(e) => warning!("List keys failed: {}", e),
Ok(keys) => for k in &keys {
let cert = &k.certificate().cert.tbs_certificate;
let slot_str = format!("{:x}", Into::<u8>::into(k.slot()));
if slot_equals(&slot_id, &slot_str) {
if let Ok(algorithm_id) = get_algorithm_id_by_certificate(k.certificate()) {
let algorithm_str = algorithm_id.to_str();
json.insert("algorithm", algorithm_str.to_string());
let public_key_bit_string = &cert.subject_public_key_info.subject_public_key;
match algorithm_id {
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
let ec_bit_len = iff!(matches!(algorithm_id, AlgorithmId::EccP256), 256, 384);
let pk_point_hex = public_key_bit_string.raw_bytes();
json.insert("pk_point_hex", hex::encode(pk_point_hex));
if pk_point_hex[0] == 0x04 {
json.insert(
"pk_point_hex_compressed",
format!("02{}", hex::encode(&pk_point_hex[1..(pk_point_hex.len() / 2) + 1])),
);
}
let mut ssh_public_key = vec![];
ssh_public_key.write_string(format!("ecdsa-sha2-nistp{}", ec_bit_len).as_bytes());
ssh_public_key.write_string(format!("nistp{}", ec_bit_len).as_bytes());
ssh_public_key.write_string(pk_point_hex);
let ssh_public_key_str = format!(
"ecdsa-sha2-nistp{} {} YubiKey-PIV-{}", ec_bit_len, base64_encode(ssh_public_key), slot_id);
json.insert("ssh_public_key", ssh_public_key_str.to_string());
}
_ => {}
}
let yubikey_piv_key = YubikeyPivKey {
key_name: format!("yubikey{}-{}", yk.version().major, yk.serial().0),
algorithm: KeyAlgorithmId::from_algorithm_id(algorithm_id),
slot: slot_id,
};
json.insert("key_uri", KeyUri::YubikeyPiv(yubikey_piv_key).to_string());
}
let serial_lower = cert.serial_number.to_string().to_lowercase();
json.insert("serial", if serial_lower.starts_with("00:") { serial_lower.chars().skip(3).collect() } else { serial_lower });
let cert_der = k.certificate().cert.to_der()?;
json.insert("certificate_hex", hex::encode(&cert_der));
json.insert("certificate_pem", bytes_to_pem("CERTIFICATE", cert_der.as_slice()));
let x509_certificate = parse_x509_certificate(cert_der.as_slice()).unwrap().1;
let public_key_bytes = x509_certificate.public_key().raw;
json.insert("subject", x509_certificate.subject.to_string());
json.insert("issuer", x509_certificate.issuer.to_string());
json.insert("public_key_hex", hex::encode(public_key_bytes));
json.insert("public_key_pem", bytes_to_pem("PUBLIC KEY", public_key_bytes));
if !json_output {
information!("Subject: {}", x509_certificate.subject.to_string());
information!("Certificate: {}", bytes_to_pem("CERTIFICATE", cert_der.as_slice()));
information!("Public key: {}", bytes_to_pem("PUBLIC KEY", public_key_bytes));
}
} else {
util_msg::when(MessageType::DEBUG, || {
let cert_der = cert.to_der().unwrap();
debugging!("Slot: {:x}", Into::<u8>::into(k.slot()));
let public_key_bytes = cert.subject_public_key_info.subject_public_key.raw_bytes();
debugging!("Certificate: {}", hex::encode(&cert_der));
debugging!("Public key: {}", hex::encode(public_key_bytes));
});
}
},
}
if json_output {
util::print_pretty_json(&json);
}
Ok(None)
}
}

67
src/cmd_piv_rsasign.rs Normal file
View File

@@ -0,0 +1,67 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use yubikey::piv;
use yubikey::piv::{AlgorithmId, SlotId};
use crate::{cmdutil, pinutil, pivutil, rsautil, util, yubikeyutil};
use crate::util::base64_encode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "piv-sign" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV RSA sign(with SHA256) subcommand")
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("sha256").short("2").long("sha256").takes_value(true).help("Digest SHA256 HEX"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let pin_opt = pinutil::read_pin(sub_arg_matches);
let sha256_hex_opt = sub_arg_matches.value_of("sha256").map(|s| s.to_string());
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
if let Some(pin) = &pin_opt {
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
}
let slot_id = match sub_arg_matches.value_of("slot") {
None => SlotId::Signature,
Some(slot) => pivutil::get_slot_id(slot)?,
};
information!("Using slot: {}", slot_id);
if let Some(sha256_hex) = sha256_hex_opt {
let sha256 = opt_result!(hex::decode(sha256_hex), "Decode sha256 failed: {}");
let raw_in = rsautil::pkcs15_sha256_rsa_2048_padding_for_sign(&sha256);
let sign_result = piv::sign_data(&mut yk, &raw_in, AlgorithmId::Rsa2048, slot_id);
let sign = opt_result!(sign_result, "Sign data failed: {}");
let sign_bytes = sign.as_slice();
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("slot", pivutil::to_slot_hex(&slot_id));
json.insert("hash_hex", hex::encode(&sha256));
json.insert("sign_hex", hex::encode(sign_bytes));
json.insert("sign_base64", base64_encode(sign_bytes));
util::print_pretty_json(&json);
} else {
success!("Signature HEX: {}", hex::encode(sign_bytes));
success!("Signature base64: {}", base64_encode(sign_bytes));
}
}
Ok(None)
}
}

182
src/cmd_piv_summary.rs Normal file
View File

@@ -0,0 +1,182 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde::Serialize;
use serde_json::{Map, Value};
use spki::der::Encode;
use tabled::settings::Style;
use tabled::{Table, Tabled};
use x509_parser::parse_x509_certificate;
use yubikey::piv::{metadata, SlotId};
use yubikey::{Certificate, YubiKey};
use crate::{cmdutil, util, yubikeyutil};
use crate::pivutil::{get_algorithm_id_by_certificate, ToStr, ORDERED_SLOTS};
const NA: &str = "N/A";
#[derive(Tabled, Serialize)]
struct PivSlot {
name: String,
id: String,
algorithm: String,
origin: String,
retries: String,
subject: String,
pin_policy: String,
touch_policy: String,
}
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "piv-summary" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV subcommand")
.arg(Arg::with_name("table").long("table").help("Show table"))
.arg(Arg::with_name("all").long("all").help("Show all"))
.arg(Arg::with_name("ordered").long("ordered").help("Show ordered"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let show_table = sub_arg_matches.is_present("table");
let show_all = sub_arg_matches.is_present("all");
let show_ordered = sub_arg_matches.is_present("ordered");
let mut output = Map::new();
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
success!("Name: {}", yk.name());
information!("Version: {}", yk.version());
information!("Serial: {}", yk.serial());
output.insert("name".to_string(), Value::String(yk.name().to_string()));
output.insert("version".to_string(), Value::String(yk.version().to_string()));
output.insert("serial".to_string(), Value::String(yk.serial().to_string()));
match yk.chuid() {
Ok(chuid) => {
information!("CHUID: {}",chuid.to_string());
output.insert("chuid".to_string(), Value::String(chuid.to_string()));
}
Err(e) => warning!("CHUID: <none> {}", e),
}
match yk.cccid() {
Ok(cccid) => {
information!("CCCID: {}",cccid.to_string());
output.insert("cccid".to_string(), Value::String(cccid.to_string()));
}
Err(e) => warning!("CCCID: <none> {}", e),
}
match yk.get_pin_retries() {
Ok(pin_retries) => {
information!("PIN retries: {}",pin_retries);
output.insert("pin_retries".to_string(), Value::String(pin_retries.to_string()));
}
Err(e) => warning!("PIN retries: <none> {}", e),
}
match yk.piv_keys() {
Ok(keys) => information!("Found {} PIV keys of {}", keys.len(), ORDERED_SLOTS.len()),
Err(e) => failure!("Get PIV keys failed: {}", e)
}
let mut piv_slots = vec![];
for slot in iff!(show_ordered, ORDERED_SLOTS, yubikey::piv::SLOTS) {
print_summary_info(&mut yk, slot, &mut piv_slots, show_all, show_table, json_output).ok();
}
if show_table {
let mut table = Table::new(piv_slots);
table.with(Style::rounded());
println!("{}", table);
} else if json_output {
let piv_slots_json = serde_json::to_string(&piv_slots).unwrap();
let piv_slots_values: Vec<Value> = serde_json::from_str(&piv_slots_json).unwrap();
output.insert("piv_slots".to_string(), Value::Array(piv_slots_values));
}
if json_output {
util::print_pretty_json(&output);
}
Ok(None)
}
}
fn print_summary_info(yubikey: &mut YubiKey, slot: SlotId, piv_slots: &mut Vec<PivSlot>, show_all: bool, show_table: bool, json_output: bool) -> XResult<()> {
let slot_id: u8 = slot.into();
let mut origin = NA.to_string();
let mut retries = NA.to_string();
let mut pin_policy = NA.to_string();
let mut touch_policy = NA.to_string();
if let Ok(metadata) = metadata(yubikey, slot) {
if let Some((p_policy, t_policy)) = &metadata.policy {
pin_policy = p_policy.to_str().to_string();
touch_policy = t_policy.to_str().to_string();
}
if let Some(o) = &metadata.origin {
origin = o.to_str().to_string();
}
if let Some(r) = &metadata.retries {
retries = format!("{}/{}", r.retry_count, r.remaining_count);
}
}
let cert = match Certificate::read(yubikey, slot) {
Ok(c) => c,
Err(e) => {
if show_all {
if show_table || json_output {
piv_slots.push(PivSlot {
name: slot.to_string(),
id: format!("{:x}", slot_id),
algorithm: NA.to_string(),
origin: origin.to_string(),
retries: retries.to_string(),
subject: NA.to_string(),
pin_policy: pin_policy.to_string(),
touch_policy: touch_policy.to_string(),
});
} else {
warning!("Slot: {:?}, id: {:x}, certificate not found", slot, slot_id);
}
}
return simple_error!("error reading certificate in slot {:?}: {}", slot, e);
}
};
let buf_vec = cert.cert.to_der()?;
let algorithm_id = get_algorithm_id_by_certificate(&cert)
.map(|aid| format!("{:?}", aid))
.unwrap_or_else(|e| format!("Error: {}", e));
let cert_subject = match parse_x509_certificate(&buf_vec) {
Ok((_rem, cert)) => cert.subject.to_string(),
_ => cert.cert.tbs_certificate.subject.to_string(),
};
if show_table || json_output {
piv_slots.push(PivSlot {
name: slot.to_string(),
id: format!("{:x}", slot_id),
algorithm: algorithm_id,
origin: origin.to_string(),
retries: retries.to_string(),
subject: cert_subject,
pin_policy: pin_policy.to_string(),
touch_policy: touch_policy.to_string(),
});
} else {
success!("Slot: {:x}, algorithm: {}, name: {:?}, origin: {}, subject: {}, pin policy: {}, touch policy: {}",
slot_id,
algorithm_id,
slot,
&origin,
&cert_subject,
&pin_policy,
&touch_policy,
);
}
Ok(())
}

104
src/cmd_piv_verify.rs Normal file
View File

@@ -0,0 +1,104 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use openssl::rsa::{Padding, Rsa};
use rust_util::util_clap::{Command, CommandError};
use yubikey::piv::AlgorithmId;
use crate::{argsutil, cmdutil, ecdsautil, pivutil, util, yubikeyutil};
use crate::ecdsautil::EcdsaAlgorithm;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "piv-verify" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV verify subcommand")
.arg(cmdutil::build_slot_arg())
.arg(Arg::with_name("signature-hex").short("t").long("signature-hex").takes_value(true).help("Signature"))
.arg(Arg::with_name("file").short("f").long("file").takes_value(true).help("Input file"))
.arg(Arg::with_name("input").short("i").long("input").takes_value(true).help("Input"))
.arg(Arg::with_name("hash-hex").short("x").long("hash-hex").takes_value(true).help("Hash"))
.arg(cmdutil::build_json_arg())
.arg(cmdutil::build_serial_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let hash_bytes = argsutil::get_sha256_digest_or_hash(sub_arg_matches)?;
let signature = if let Some(signature_hex) = sub_arg_matches.value_of("signature-hex") {
opt_result!(hex::decode(signature_hex), "Parse signature-hex failed: {}")
} else {
return simple_error!("--signature-hex required.");
};
let mut json = BTreeMap::<&'_ str, String>::new();
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
let slot_id = pivutil::get_slot_id(slot)?;
json.insert("slot", pivutil::to_slot_hex(&slot_id));
if let Some(key) = yubikeyutil::open_and_find_key(&slot_id, sub_arg_matches)? {
let certificate = key.certificate();
let tbs_certificate = &certificate.cert.tbs_certificate;
if let Ok(algorithm_id) = pivutil::get_algorithm_id_by_certificate(certificate) {
let public_key_bit_string = &tbs_certificate.subject_public_key_info.subject_public_key;
match algorithm_id {
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
let pk_point = public_key_bit_string.raw_bytes();
debugging!("ECDSA public key point: {}", hex::encode(pk_point));
information!("Pre hash: {}", hex::encode(&hash_bytes));
debugging!("Signature: {}", hex::encode(&signature));
if json_output {
json.insert("public_key_hex", hex::encode(pk_point));
json.insert("hash_hex", hex::encode(&hash_bytes));
json.insert("signature_hex", hex::encode(&signature));
}
let algorithm = iff!(algorithm_id == AlgorithmId::EccP256, EcdsaAlgorithm::P256, EcdsaAlgorithm::P384);
match ecdsautil::ecdsa_verify(algorithm, pk_point, &hash_bytes, &signature) {
Ok(_) => {
success!("Verify ECDSA succeed.");
if json_output {
json.insert("success", "true".to_string());
}
}
Err(e) => {
failure!("Verify ECDSA failed: {}", &e);
if json_output {
json.insert("success", "false".to_string());
json.insert("message", format!("{}", e));
}
}
}
}
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => {
let pk_rsa = public_key_bit_string.raw_bytes();
let keypair = opt_result!(Rsa::public_key_from_der_pkcs1(pk_rsa), "Parse RSA failed: {}");
// let pub_key_der = opt_result!(keypair.public_key_to_der(), "RSA public key to der failed: {}");
// let pub_key_fingerprint = hex::encode(sha256_bytes(&pub_key_der));
let mut dmesg = vec![0; ((keypair.n().num_bits() + 7) / 8) as usize];
let len = opt_result!(keypair.public_decrypt(&signature, &mut dmesg, Padding::NONE), "RSA public key calc failed: {}");
debugging!("RSA public key pem: {}", hex::encode(pk_rsa));
debugging!("Public key calc: {}, len: {}", hex::encode(&dmesg), len);
// TODO SHOULD IMPROVE VERIFICATION METHOD IN THE FUTURE
if hex::encode(dmesg).ends_with(&hex::encode(&hash_bytes)) {
success!("Verify RSA Sign succeed.");
} else {
failure!("Verify RSA Sign failed.");
}
}
}
}
}
if json_output {
util::print_pretty_json(&json);
}
Ok(None)
}
}

View File

@@ -1,47 +0,0 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use yubikey::piv::{AlgorithmId, SlotId};
use yubikey::YubiKey;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "piv-sign" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("PIV Sign subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").takes_value(true).default_value("123456").help("OpenPGP card user pin"))
.arg(Arg::with_name("pass").long("pass").takes_value(true).help("[deprecated] now OpenPGP card user pin"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output {
rust_util::util_msg::set_logger_std_out(false);
}
warning!("This feature is not complete");
let pin_opt = sub_arg_matches.value_of("pass").or(sub_arg_matches.value_of("pin"));
let pin = opt_value_result!(pin_opt, "User pin must be assigned");
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
let raw_in = [1_u8; 256];
let sign_result = yubikey::piv::sign_data(&mut yk, &raw_in, AlgorithmId::Rsa2048, SlotId::Signature);
let sign = opt_result!(sign_result, "Sign data failed: {}");
let sign_bytes = sign.as_slice();
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("sign_hex", hex::encode(&sign_bytes));
json.insert("sign_base64", base64::encode(&sign_bytes));
println!("{}", serde_json::to_string_pretty(&json).unwrap());
} else {
success!("Signature HEX: {}", hex::encode(sign_bytes));
success!("Signature base64: {}", base64::encode(sign_bytes));
}
Ok(None)
}
}

91
src/cmd_rsa_decrypt.rs Normal file
View File

@@ -0,0 +1,91 @@
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use openssl::bn::{BigNum, BigNumContext};
use openssl::encrypt::Decrypter;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use rust_util::util_msg::MessageType;
use crate::{cmdutil, util};
use crate::util::{read_stdin, try_decode};
pub struct CommandImpl;
// https://docs.rs/openssl/0.10.36/openssl/encrypt/index.html
impl Command for CommandImpl {
fn name(&self) -> &str { "rsa-decrypt" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("RSA decrypt subcommand")
.arg(Arg::with_name("pri-key-in").long("pri-key-in").takes_value(true).help("Private key in"))
.arg(Arg::with_name("encrypted").long("encrypted").takes_value(true).help("Encrypted data"))
.arg(Arg::with_name("ciphertext").long("ciphertext").takes_value(true).help("Encrypted data"))
.arg(Arg::with_name("stdin").long("stdin").help("Standard input (Ciphertext)"))
.arg(Arg::with_name("padding").long("padding").takes_value(true)
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let pri_key_in = opt_value_result!(sub_arg_matches.value_of("pri-key-in"), "Require private key in");
let pri_key_bytes = opt_result!(std::fs::read(pri_key_in), "Read file: {}, failed: {}", pri_key_in);
let padding_opt = sub_arg_matches.value_of("padding");
let padding = crate::rsautil::parse_padding(padding_opt)?;
let padding_str = crate::rsautil::padding_to_string(padding);
let keypair = opt_result!(Rsa::private_key_from_pem(&pri_key_bytes), "Parse RSA failed: {}");
let keypair = opt_result!(PKey::from_rsa(keypair), "RSA to PKey failed: {}");
let ciphertext_opt = sub_arg_matches.value_of("encrypted")
.or_else(|| sub_arg_matches.value_of("ciphertext"));
let ciphertext = if let Some(ciphertext) = ciphertext_opt {
opt_result!(try_decode(ciphertext), "Decode ciphertext HEX or Base64 failed: {}")
} else if sub_arg_matches.is_present("stdin") {
read_stdin()?
} else {
return simple_error!("Data is required, --ciphertext or --encrypted argument!");
};
util_msg::when(MessageType::DEBUG, || {
let rsa = keypair.rsa().unwrap();
let n = rsa.n();
let d = rsa.d();
let m = BigNum::from_slice(&ciphertext).unwrap();
let mut r = BigNum::new().unwrap();
r.mod_exp(&m, d, n, &mut BigNumContext::new().unwrap()).unwrap();
let v = r.to_vec();
debugging!("Encrypted raw HEX: 00{}", hex::encode(&v));
let pos = v.iter().position(|b| *b == 0x00);
if let Some(pos) = pos {
debugging!("Encrypted text HEX: {}", hex::encode(&v[pos+1..]));
}
});
let mut decrypter = opt_result!(Decrypter::new(&keypair), "Decrypter new failed: {}");
opt_result!(decrypter.set_rsa_padding(padding), "Set RSA padding failed: {}");
let buffer_len = opt_result!(decrypter.decrypt_len(&ciphertext), "Decrypt len failed: {}");
let mut data = vec![0; buffer_len];
let decrypted_len = opt_result!(decrypter.decrypt(&ciphertext, &mut data), "Decrypt failed: {}");
data.truncate(decrypted_len);
let encrypted_hex = hex::encode(&ciphertext);
information!("Padding: {}", padding_str);
success!("Message HEX: {}", hex::encode(&data));
success!("Message: {}", String::from_utf8_lossy(&data));
if json_output {
let mut json = BTreeMap::new();
json.insert("data", hex::encode(&data));
json.insert("padding", padding_str.to_string());
json.insert("encrypted", encrypted_hex);
util::print_pretty_json(&json);
}
Ok(None)
}
}

76
src/cmd_rsa_encrypt.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::fs;
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use openssl::encrypt::Encrypter;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use rust_util::util_clap::{Command, CommandError};
use crate::{cmdutil, util};
use crate::digestutil::sha256_bytes;
pub struct CommandImpl;
// https://docs.rs/openssl/0.10.36/openssl/encrypt/index.html
impl Command for CommandImpl {
fn name(&self) -> &str { "rsa-encrypt" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("RSA encrypt subcommand")
.arg(Arg::with_name("pub-key-in").long("pub-key-in").takes_value(true).help("Public key in"))
.arg(Arg::with_name("data").long("data").takes_value(true).help("Data"))
.arg(Arg::with_name("data-hex").long("data-hex").takes_value(true).help("Data in HEX"))
.arg(Arg::with_name("padding").long("padding").takes_value(true)
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let pub_key_in = opt_value_result!(sub_arg_matches.value_of("pub-key-in"), "Require public key in");
let pub_key_bytes = opt_result!(fs::read(pub_key_in), "Read file: {}, failed: {}", pub_key_in);
let padding_opt = sub_arg_matches.value_of("padding");
let padding = crate::rsautil::parse_padding(padding_opt)?;
let padding_str = crate::rsautil::padding_to_string(padding);
let keypair = opt_result!(Rsa::public_key_from_pem(&pub_key_bytes), "Parse RSA failed: {}");
let pub_key_der = opt_result!(keypair.public_key_to_der(), "RSA public key to der failed: {}");
let pub_key_fingerprint = hex::encode(sha256_bytes(&pub_key_der));
let keypair = opt_result!(PKey::from_rsa(keypair), "RSA to PKey failed: {}");
let data = if let Some(data_hex) = sub_arg_matches.value_of("data-hex") {
opt_result!(hex::decode(data_hex), "Decode data HEX failed: {}")
} else if let Some(data) = sub_arg_matches.value_of("data") {
data.as_bytes().to_vec()
} else {
return simple_error!("Data is required, --data-hex or --data argument!");
};
let mut encrypter = opt_result!(Encrypter::new(&keypair), "Encrypter new failed: {}");
opt_result!(encrypter.set_rsa_padding(padding), "Set RSA padding failed: {}");
let buffer_len = opt_result!(encrypter.encrypt_len(&data), "Encrypt len failed: {}");
let mut encrypted = vec![0; buffer_len];
let encrypted_len = opt_result!(encrypter.encrypt(&data, &mut encrypted), "Encrypt failed: {}");
encrypted.truncate(encrypted_len);
let encrypted_hex = hex::encode(&encrypted);
information!("Message: {}", String::from_utf8_lossy(&data));
information!("Message HEX: {}", hex::encode(&data));
information!("Padding: {}", padding_str);
information!("Public key fingerprint: {}", pub_key_fingerprint);
success!("Encrypted message: {}", encrypted_hex);
if json_output {
let mut json = BTreeMap::new();
json.insert("data", hex::encode(&data));
json.insert("public_key_fingerprint", pub_key_fingerprint);
json.insert("padding", padding_str.to_string());
json.insert("encrypted", encrypted_hex);
util::print_pretty_json(&json);
}
Ok(None)
}
}

97
src/cmd_rsa_verify.rs Normal file
View File

@@ -0,0 +1,97 @@
use std::fs;
use std::io;
use std::fs::File;
use clap::{App, Arg, ArgMatches, SubCommand};
use openssl::bn::{BigNum, BigNumContext};
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use openssl::sign::Verifier;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg::MessageType;
use rust_util::{util_msg, XResult};
use crate::digestutil::sha256_bytes;
pub struct CommandImpl;
// https://docs.rs/openssl/0.10.36/openssl/encrypt/index.html
impl Command for CommandImpl {
fn name(&self) -> &str { "rsa-verify" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("RSA verify subcommand")
.arg(Arg::with_name("pub-key-in").long("pub-key-in").takes_value(true).help("Public key in"))
.arg(Arg::with_name("signature").long("signature").takes_value(true).help("Signature HEX"))
.arg(Arg::with_name("in").short("i").long("in").takes_value(true).help("File in"))
.arg(Arg::with_name("hash").long("hash").takes_value(true).possible_values(&[
"sha256", "sha384", "sha512"
]).default_value("sha256").help("Hash"))
// .arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
// let json_output = sub_arg_matches.is_present("json");
// if json_output { rust_util::util_msg::set_logger_std_out(false); }
let pub_key_in = opt_value_result!(sub_arg_matches.value_of("pub-key-in"), "Require public key in");
let pub_key_bytes = opt_result!(fs::read(pub_key_in), "Read file: {}, failed: {}", pub_key_in);
// let mut json = BTreeMap::new();
let keypair = opt_result!(Rsa::public_key_from_pem(&pub_key_bytes), "Parse RSA failed: {}");
let pub_key_der = opt_result!(keypair.public_key_to_der(), "RSA public key to der failed: {}");
let pub_key_fingerprint = hex::encode(sha256_bytes(&pub_key_der));
let keypair = opt_result!(PKey::from_rsa(keypair), "RSA to PKey failed: {}");
let signature = if let Some(signature) = sub_arg_matches.value_of("signature") {
opt_result!(hex::decode(signature), "Decode signature HEX failed: {}")
} else {
return simple_error!("Signature is required, --signature argument!");
};
util_msg::when(MessageType::DEBUG, || {
let rsa = keypair.rsa().unwrap();
let n = rsa.n();
let e = rsa.e();
let m = BigNum::from_slice(&signature).unwrap();
let mut r = BigNum::new().unwrap();
r.mod_exp(&m, e, n, &mut BigNumContext::new().unwrap()).unwrap();
debugging!("Signature raw HEX: 00{}", hex::encode(r.to_vec()));
});
let file_in = opt_value_result!(sub_arg_matches.value_of("in"), "File in --in required");
information!("File in: {}", file_in);
information!("Public key fingerprint: {}", pub_key_fingerprint);
let hashes = sub_arg_matches.values_of("hash").expect("Cannot get hashes");
for hash in hashes {
information!("Hash: {}", hash);
let digest = get_digest(hash)?;
let mut verifier = opt_result!(Verifier::new(digest, &keypair), "Verifier new failed: {}");
let mut f = opt_result!(File::open(file_in), "Open file: {}, failed: {}", file_in);
opt_result!(io::copy(&mut f, &mut verifier), "Verifier failed: {}");
let result = opt_result!(verifier.verify(&signature), "Verifier verify failed: {}");
if result {
success!("Verify success");
} else {
failure!("Verify failed")
}
}
// if json_output {
// println!("{}", serde_json::to_string_pretty(&json).unwrap());
// }
Ok(None)
}
}
fn get_digest(hash: &str) -> XResult<MessageDigest> {
Ok(match hash {
"sha256" => MessageDigest::sha256(),
"sha384" => MessageDigest::sha384(),
"sha512" => MessageDigest::sha512(),
_ => return simple_error!("Unknown hash: {}", hash),
})
}

39
src/cmd_se.rs Normal file
View File

@@ -0,0 +1,39 @@
use crate::{cmdutil, seutil, util};
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"se"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Secure Enclave subcommand")
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
if which::which("swift-secure-enclave-tool").is_err() {
failure!("Secure Enclave tool not found.");
}
if json_output {
let mut json = BTreeMap::new();
json.insert("se_supported", seutil::is_support_se());
util::print_pretty_json(&json);
} else {
success!(
"Secure Enclave is {}supported.",
iff!(seutil::is_support_se(), "", "NOT ")
);
}
Ok(None)
}
}

85
src/cmd_se_ecdh.rs Normal file
View File

@@ -0,0 +1,85 @@
use crate::keyutil::parse_key_uri;
use crate::{cmd_hmac_decrypt, cmdutil, seutil, util};
use clap::{App, ArgMatches, SubCommand};
use p256::elliptic_curve::sec1::FromEncodedPoint;
use p256::{EncodedPoint, PublicKey};
use rust_util::util_clap::{Command, CommandError};
use spki::EncodePublicKey;
use std::collections::BTreeMap;
use rust_util::XResult;
use crate::util::base64_decode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"se-ecdh"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Secure Enclave ECDH subcommand")
.arg(cmdutil::build_key_uri_arg())
.arg(cmdutil::build_epk_arg())
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
seutil::check_se_supported()?;
let key = sub_arg_matches.value_of("key").unwrap();
let epk = sub_arg_matches.value_of("epk").unwrap();
let key_uri = parse_key_uri(key)?;
let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri);
let ephemeral_public_key_der_bytes = parse_epk(epk)?;
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &se_key_uri.private_key)?;
let dh = seutil::secure_enclave_p256_dh(
&private_key,
&ephemeral_public_key_der_bytes,
)?;
let dh_hex = hex::encode(&dh);
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("shared_secret_hex", dh_hex);
util::print_pretty_json(&json);
} else {
information!("Shared secret: {}", dh_hex);
}
Ok(None)
}
}
pub fn parse_epk(epk: &str) -> XResult<Vec<u8>> {
if epk.starts_with("04") {
let ephemeral_public_key_point_bytes = opt_result!(
hex::decode(epk),
"Decode public key point from hex failed: {}"
);
let encoded_point = opt_result!(
EncodedPoint::from_bytes(ephemeral_public_key_point_bytes),
"Parse public key point failed: {}"
);
let public_key_opt = PublicKey::from_encoded_point(&encoded_point);
if public_key_opt.is_none().into() {
return simple_error!("Parse public key failed.");
}
let public_key = public_key_opt.unwrap();
Ok(public_key.to_public_key_der()?.as_bytes().to_vec())
} else {
match hex::decode(epk) {
Ok(epk_bytes) => Ok(epk_bytes),
Err(e) => match base64_decode(epk) {
Ok(epk_bytes) => Ok(epk_bytes),
Err(_) => simple_error!("Decode public key from hex failed: {}", e)
}
}
}
}

68
src/cmd_se_ecsign.rs Normal file
View File

@@ -0,0 +1,68 @@
use crate::keyutil::parse_key_uri;
use crate::{cmd_hmac_decrypt, cmdutil, seutil, util};
use crate::util::{base64_decode, base64_encode};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
use swift_secure_enclave_tool_rs::DigestType;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"se-ecsign"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Secure Enclave EC sign subcommand")
.arg(cmdutil::build_key_uri_arg())
.arg(
Arg::with_name("input")
.short("i")
.long("input")
.takes_value(true)
.help("Input"),
)
.arg(
Arg::with_name("input-base64")
.long("input-base64")
.takes_value(true)
.help("Input in base64"),
)
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
seutil::check_se_supported()?;
let key = sub_arg_matches.value_of("key").unwrap();
let input_bytes = match sub_arg_matches.value_of("input") {
None => match sub_arg_matches.value_of("input-base64") {
None => return simple_error!("Argument --input or --input-base64 is required"),
Some(input_base64) => base64_decode(input_base64)?,
},
Some(input) => input.as_bytes().to_vec(),
};
let key_uri = parse_key_uri(key)?;
let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri);
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &se_key_uri.private_key)?;
let signature = seutil::secure_enclave_p256_sign(&private_key, &input_bytes, DigestType::Raw)?;
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("signature_base64", base64_encode(&signature));
json.insert("signature_hex", hex::encode(&signature));
util::print_pretty_json(&json);
} else {
success!("Signature: {}", base64_encode(&signature));
}
Ok(None)
}
}

137
src/cmd_se_generate.rs Normal file
View File

@@ -0,0 +1,137 @@
use crate::cmd_hmac_encrypt;
use crate::pkiutil::bytes_to_pem;
use crate::util::base64_encode;
use crate::{cmdutil, seutil, util};
use clap::{App, Arg, ArgMatches, SubCommand};
use p256::PublicKey;
use rust_util::util_clap::{Command, CommandError};
use spki::DecodePublicKey;
use std::collections::BTreeMap;
use swift_secure_enclave_tool_rs::{ControlFlag, KeyMlKem};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"se-generate"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Secure Enclave generate subcommand")
.arg(
Arg::with_name("type")
.long("type")
.required(true)
.takes_value(true)
.help("Type signing or key_agreement"),
)
.arg(
Arg::with_name("host")
.long("host")
.required(false)
.takes_value(true)
.help("Host name"),
)
.arg(
Arg::with_name("control-flag")
.long("control-flag")
.required(true)
.takes_value(true)
.help("Control flag, e.g. none, user-presence, device-passcode, biometry-any, biometry-current-set"),
)
.arg(
Arg::with_name("algorithm")
.long("algorithm")
.required(true)
.takes_value(true)
.default_value("p256")
.help("Algorithm, e.g. p256, mlkem768, mlkem1024"),
)
.arg(cmdutil::build_with_hmac_encrypt_arg())
.arg(cmdutil::build_with_pbe_encrypt_arg())
.arg(cmdutil::build_double_pin_check_arg())
.arg(cmdutil::build_pbe_iteration_arg())
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
seutil::check_se_supported()?;
let ty = sub_arg_matches.value_of("type").unwrap();
let host = sub_arg_matches.value_of("host").unwrap_or("macbook");
let sign = match ty {
"signing" | "ecsign" | "sign" => true,
"key_agreement" | "ecdh" | "dh" => false,
_ => return simple_error!("Invalid type: {}", ty),
};
let control_flag = sub_arg_matches.value_of("control-flag").unwrap();
let control_flag = match control_flag {
"none" => ControlFlag::None,
"user-presence" | "up" => ControlFlag::UserPresence,
"device-passcode" | "passcode" | "pass" => ControlFlag::DevicePasscode,
"biometry-any" | "bio-any" => ControlFlag::BiometryAny,
"biometry-current-set" | "bio-current" => ControlFlag::BiometryCurrentSet,
_ => return simple_error!("Invalid control flag: {}", control_flag),
};
let algorithm = sub_arg_matches.value_of("algorithm").unwrap();
let (public_key_point, public_key_der, private_key) = match algorithm {
"p256" => {
seutil::generate_secure_enclave_p256_keypair(sign, control_flag)?
}
"mlkem768" | "mlkem1024" => {
if sign {
return simple_error!("Algorithm: {} only supports key_agreement", algorithm);
}
seutil::generate_secure_enclave_mlkem_keypair(
iff!(algorithm == "mlkem768", KeyMlKem::MlKem768, KeyMlKem::MlKem1024), control_flag)?
}
_ => return simple_error!("Unknown algorithm: {}", algorithm),
};
let private_key = cmd_hmac_encrypt::do_encrypt(&private_key, &mut None, sub_arg_matches)?;
let key_uri = format!(
"key://{}:se/{}:{}:{}",
host,
algorithm,
iff!(sign, "signing", "key_agreement"),
private_key,
);
print_se_key(json_output, &public_key_point, &public_key_der, &key_uri);
Ok(None)
}
}
pub fn print_se_key(
json_output: bool,
public_key_point: &[u8],
public_key_der: &[u8],
key_uri: &str,
) {
let public_key_point_hex = hex::encode(public_key_point);
let public_key_pem = bytes_to_pem("PUBLIC KEY", public_key_der);
let public_key = PublicKey::from_public_key_pem(&public_key_pem).ok();
let public_key_jwk = public_key.map(|key| key.to_jwk_string());
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("public_key_point", public_key_point_hex);
json.insert("public_key_pem", base64_encode(public_key_der));
if let Some(public_key_jwk) = public_key_jwk {
json.insert("public_key_jwk", base64_encode(public_key_jwk));
}
json.insert("key_uri", key_uri.to_string());
util::print_pretty_json(&json);
} else {
success!("Public key(point): {}", public_key_point_hex);
success!("Public key PEM: \n{}", public_key_pem);
if let Some(public_key_jwk) = public_key_jwk {
success!("Public key JWK: \n{}", &public_key_jwk);
}
success!("Key: {}", key_uri);
}
}

41
src/cmd_se_recover.rs Normal file
View File

@@ -0,0 +1,41 @@
use crate::cmd_se_generate::print_se_key;
use crate::keyutil::{parse_key_uri, KeyUsage};
use crate::{cmd_hmac_decrypt, cmdutil, seutil};
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"se-recover"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Secure Enclave recover subcommand")
.arg(cmdutil::build_key_uri_arg())
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
seutil::check_se_supported()?;
let key = sub_arg_matches.value_of("key").unwrap();
let key_uri = parse_key_uri(key)?;
let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri);
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, &se_key_uri.private_key)?;
let (public_key_point, public_key_der, _private_key) =
seutil::recover_secure_enclave_p256_public_key(
&private_key,
se_key_uri.usage == KeyUsage::Singing,
)?;
print_se_key(json_output, &public_key_point, &public_key_der, key);
Ok(None)
}
}

85
src/cmd_sign_jwt.rs Normal file
View File

@@ -0,0 +1,85 @@
use crate::cmd_sign_jwt_piv::{
build_jwt_parts, fill_sign_jwt_app_args, merge_header_claims,
merge_payload_claims, print_jwt_token,
};
use crate::ecdsautil::parse_ecdsa_to_rs;
use crate::keyutil::parse_key_uri;
use crate::{cmd_external_sign, cmdutil, util};
use clap::{App, ArgMatches, SubCommand};
use jwt::ToBase64;
use jwt::{AlgorithmType, Header};
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::{Map, Value};
use crate::pivutil::ToStr;
const SEPARATOR: &str = ".";
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"sign-jwt"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
let app = SubCommand::with_name(self.name())
.about("Sign JWT subcommand")
.arg(cmdutil::build_key_uri_arg().required(false))
.arg(cmdutil::build_parameter_arg().required(false))
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_serial_arg())
.arg(cmdutil::build_json_arg());
fill_sign_jwt_app_args(app)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?;
let token_string = sign_jwt(header, &payload, &jwt_claims, sub_arg_matches)?;
print_jwt_token(json_output, token_string);
Ok(None)
}
}
fn sign_jwt(
mut header: Header,
payload: &Option<String>,
claims: &Map<String, Value>,
sub_arg_matches: &ArgMatches,
) -> XResult<String> {
let key = match sub_arg_matches.value_of("key") {
Some(key) => key,
None => match sub_arg_matches.value_of("parameter") {
Some(parameter) => parameter,
None => return simple_error!("Parameter --key or --parameter required"),
}
};
let key_uri = parse_key_uri(key)?;
let jwt_algorithm = key_uri.get_preferred_algorithm_type();
header.algorithm = jwt_algorithm;
debugging!("Header: {:?}", header);
debugging!("Claims: {:?}", claims);
let header = opt_result!(header.to_base64(), "Header to base64 failed: {}");
let claims = merge_payload_claims(payload, claims)?;
let tobe_signed = merge_header_claims(header.as_bytes(), claims.as_bytes());
let signature = cmd_external_sign::sign(jwt_algorithm.to_str(), &tobe_signed, None, key_uri, sub_arg_matches)?;
let signed_data = match jwt_algorithm {
AlgorithmType::Rs256 => signature,
AlgorithmType::Es256 | AlgorithmType::Es384 | AlgorithmType::Es512 => {
parse_ecdsa_to_rs(signature.as_slice())?
}
_ => return simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
};
let signature = util::base64_encode_url_safe_no_pad(&signed_data);
Ok([&*header, &*claims, &signature].join(SEPARATOR))
}

302
src/cmd_sign_jwt_piv.rs Normal file
View File

@@ -0,0 +1,302 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use clap::{App, Arg, ArgMatches, SubCommand};
use jwt::header::HeaderType;
use jwt::{AlgorithmType, Header, ToBase64};
use rust_util::util_clap::{Command, CommandError};
use rust_util::{util_time, XResult};
use serde_json::{Map, Number, Value};
use yubikey::piv::{sign_data, AlgorithmId, SlotId};
use yubikey::{Certificate, YubiKey};
use crate::ecdsautil::parse_ecdsa_to_rs;
use crate::{cmdutil, digestutil, pivutil, rsautil, util};
const SEPARATOR: &str = ".";
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"sign-jwt-piv"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
let app = SubCommand::with_name(self.name()).about("Sign JWT(PIV) subcommand")
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(cmdutil::build_json_arg());
fill_sign_jwt_app_args(app)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let slot = opt_value_result!(
sub_arg_matches.value_of("slot"),
"--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"
);
let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?;
let mut yk = opt_result!(YubiKey::open(), "Find YubiKey failed: {}");
let slot_id = opt_result!(pivutil::get_slot_id(slot), "Get slot id failed: {}");
let pin_opt = pivutil::check_read_pin(&mut yk, slot_id, sub_arg_matches);
let token_string = sign_jwt(&mut yk, slot_id, &pin_opt, header, &payload, &jwt_claims)?;
print_jwt_token(json_output, token_string);
Ok(None)
}
}
pub fn print_jwt_token(json_output: bool, token_string: String) {
if json_output {
debugging!("Singed JWT: {}", token_string);
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("token", token_string.clone());
util::print_pretty_json(&json);
} else {
success!("Singed JWT: {}", token_string);
}
}
pub fn fill_sign_jwt_app_args<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
app.arg(Arg::with_name("key-id").short("K").long("key-id").takes_value(true).help("Header key ID"))
.arg(Arg::with_name("claims").short("C").long("claims").takes_value(true).multiple(true).help("Claims, key:value"))
.arg(Arg::with_name("payload").short("P").long("payload").takes_value(true).help("Claims in JSON"))
.arg(Arg::with_name("jti").long("jti").help("Claims jti"))
.arg(Arg::with_name("validity").long("validity").takes_value(true)
.help("Claims validity period e.g. 10m means 10 minutes (s - second, m - minute, h - hour, d - day)"))
}
fn sign_jwt(
yk: &mut YubiKey,
slot_id: SlotId,
pin_opt: &Option<String>,
mut header: Header,
payload: &Option<String>,
claims: &Map<String, Value>,
) -> XResult<String> {
if let Some(pin) = pin_opt {
opt_result!(
yk.verify_pin(pin.as_bytes()),
"YubiKey verify pin failed: {}"
);
}
let cert = match Certificate::read(yk, slot_id) {
Ok(c) => c,
Err(e) => return simple_error!("Read YubiKey certificate failed: {}", e),
};
let piv_algorithm_id =
pivutil::get_algorithm_id(&cert.cert.tbs_certificate.subject_public_key_info)?;
let (jwt_algorithm, yk_algorithm) = match piv_algorithm_id {
AlgorithmId::Rsa1024 => return simple_error!("RSA 1024 bits not supported."),
AlgorithmId::Rsa2048 => (AlgorithmType::Rs256, AlgorithmId::Rsa2048),
AlgorithmId::EccP256 => (AlgorithmType::Es256, AlgorithmId::EccP256),
AlgorithmId::EccP384 => (AlgorithmType::Es384, AlgorithmId::EccP384),
};
header.algorithm = jwt_algorithm;
debugging!("Header: {:?}", header);
debugging!("Claims: {:?}", claims);
let header = opt_result!(header.to_base64(), "Header to base64 failed: {}");
let claims = merge_payload_claims(payload, claims)?;
let tobe_signed = merge_header_claims(header.as_bytes(), claims.as_bytes());
let raw_in = digest_by_jwt_algorithm(jwt_algorithm, &tobe_signed)?;
let signed_data = opt_result!(
sign_data(yk, &raw_in, yk_algorithm, slot_id),
"Sign YubiKey failed: {}"
);
let signed_data = match jwt_algorithm {
AlgorithmType::Rs256 => signed_data.to_vec(),
AlgorithmType::Es256 | AlgorithmType::Es384 => parse_ecdsa_to_rs(signed_data.as_slice())?,
_ => return simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
};
let signature = util::base64_encode_url_safe_no_pad(&signed_data);
Ok([&*header, &*claims, &signature].join(SEPARATOR))
}
pub fn digest_by_jwt_algorithm(jwt_algorithm: AlgorithmType, tobe_signed: &[u8]) -> XResult<Vec<u8>> {
Ok(match jwt_algorithm {
AlgorithmType::Rs256 => {
rsautil::pkcs15_sha256_rsa_2048_padding_for_sign(&digestutil::sha256_bytes(tobe_signed))
}
AlgorithmType::Es256 => digestutil::sha256_bytes(tobe_signed),
AlgorithmType::Es384 => digestutil::sha384_bytes(tobe_signed),
AlgorithmType::Es512 => digestutil::sha512_bytes(tobe_signed),
_ => return simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
})
}
pub fn merge_header_claims(header: &[u8], claims: &[u8]) -> Vec<u8> {
let mut tobe_signed = vec![];
tobe_signed.extend_from_slice(header);
tobe_signed.extend_from_slice(SEPARATOR.as_bytes());
tobe_signed.extend_from_slice(claims);
tobe_signed
}
pub fn merge_payload_claims<'a>(
payload: &'a Option<String>,
claims: &'a Map<String, Value>,
) -> XResult<Cow<'a, str>> {
Ok(match (payload, claims.is_empty()) {
(Some(payload), true) => {
Cow::Owned(util::base64_encode_url_safe_no_pad(payload.as_bytes()))
}
(_, _) => opt_result!(claims.to_base64(), "Claims to base64 failed: {}"),
})
}
pub fn build_jwt_parts(
sub_arg_matches: &ArgMatches,
) -> XResult<(Header, Option<String>, Map<String, Value>)> {
let key_id = sub_arg_matches.value_of("key-id");
let claims = sub_arg_matches.values_of("claims");
let payload = sub_arg_matches.value_of("payload");
let validity = sub_arg_matches.value_of("validity");
let jti = sub_arg_matches.is_present("jti");
let header = Header {
key_id: key_id.map(ToString::to_string),
type_: Some(HeaderType::JsonWebToken),
..Default::default()
};
let mut jwt_claims = Map::new();
if let Some(payload) = payload {
match serde_json::from_str::<Value>(payload) {
Ok(Value::Object(claims_map)) => {
claims_map.into_iter().for_each(|(k, v)| {
jwt_claims.insert(k, v);
});
}
Ok(value) => {
warning!("Not valid payload map: {}", value);
}
Err(e) => {
warning!("Not valid payload value: {}", e);
}
};
}
match (payload, claims) {
(Some(_), None) => {}
(_, Some(claims)) => {
for claim in claims {
match split_claim(claim) {
None => {
warning!("Claim '{}' do not contains ':'", claim);
}
Some((k, v)) => {
match jwt_claims.get_mut(&k) {
None => { jwt_claims.insert(k, v); },
Some(val) => {
match val {
Value::Array(arr) => {
arr.push(v);
}
_ => {
let mut arr = vec![];
arr.push(val.clone());
arr.push(v);
jwt_claims.insert(k, Value::Array(arr));
}
}
}
}
// jwt_claims.insert(k, v);
}
}
}
if !jwt_claims.contains_key("sub") {
return simple_error!("Claim sub is not assigned.");
}
}
_ => return simple_error!("Payload or Claims is required."),
}
// set jti, iat and sub
if jti && !jwt_claims.contains_key("jti") {
jwt_claims.insert(
"jti".to_string(),
Value::String(format!("jti-{}", util_time::get_current_millis())),
);
}
if let Some(validity) = validity {
match util_time::parse_duration(validity) {
None => {
warning!("Bad validity: {}", validity)
}
Some(validity) => {
let current_secs = (util_time::get_current_millis() / 1000) as u64;
jwt_claims.insert("iat".to_string(), Value::Number(Number::from(current_secs)));
jwt_claims.insert(
"exp".to_string(),
Value::Number(Number::from(current_secs + validity.as_secs())),
);
}
}
}
Ok((header, payload.map(ToString::to_string), jwt_claims))
}
pub fn split_claim(claim: &str) -> Option<(String, Value)> {
let mut k = String::new();
let mut v = String::new();
let mut claim_chars = claim.chars().peekable();
let ty = if let Some('^') = claim_chars.peek() {
let _ = claim_chars.next();
claim_chars.next()
} else {
None
};
let mut is_k = true;
for c in claim_chars {
if is_k {
if c == ':' {
is_k = false;
} else {
k.push(c);
}
} else {
v.push(c);
}
}
if is_k {
return None;
}
match ty {
None | Some('s') => Some((k, Value::String(v))),
Some('b') => Some((k, Value::Bool(["true", "yes", "1"].contains(&v.as_str())))),
Some('i') | Some('n') => {
if let Ok(i) = v.parse::<i64>() {
return Some((k, Value::Number(Number::from(i))));
}
if let Ok(f) = v.parse::<f64>() {
if let Some(number_f64) = Number::from_f64(f) {
return Some((k, Value::Number(number_f64)));
}
}
warning!("Bad number: {} in claim: {}", v, claim);
None
}
_ => {
warning!("Unknown type: {} in claim: {}", ty.unwrap(), claim);
None
}
}
}

74
src/cmd_sign_jwt_se.rs Normal file
View File

@@ -0,0 +1,74 @@
use clap::{App, ArgMatches, SubCommand};
use jwt::{AlgorithmType, Header, ToBase64};
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::{Map, Value};
use crate::cmd_sign_jwt_piv::{build_jwt_parts, merge_header_claims, merge_payload_claims, print_jwt_token};
use crate::ecdsautil::parse_ecdsa_to_rs;
use crate::keyutil::parse_key_uri;
use crate::{cmd_hmac_decrypt, cmd_sign_jwt_piv, cmdutil, util};
use crate::util::base64_decode;
const SEPARATOR: &str = ".";
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"sign-jwt-se"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
let app = SubCommand::with_name(self.name()).about("Sign JWT(SE) subcommand")
.arg(cmdutil::build_key_uri_arg())
.arg(cmdutil::build_json_arg());
cmd_sign_jwt_piv::fill_sign_jwt_app_args(app)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let private_key = opt_value_result!(
sub_arg_matches.value_of("key"),
"Private key PKCS#8 DER base64 encoded or PEM"
);
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, private_key)?;
let key_uri = parse_key_uri(&private_key)?;
let se_key_uri = key_uri.as_secure_enclave_key()?;
debugging!("Secure enclave key URI: {:?}", se_key_uri);
let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?;
let token_string = sign_jwt(&se_key_uri.private_key, header, &payload, &jwt_claims)?;
print_jwt_token(json_output, token_string);
Ok(None)
}
}
fn sign_jwt(
private_key: &str,
mut header: Header,
payload: &Option<String>,
claims: &Map<String, Value>,
) -> XResult<String> {
header.algorithm = AlgorithmType::Es256;
debugging!("Header: {:?}", header);
debugging!("Claims: {:?}", claims);
let header = opt_result!(header.to_base64(), "Header to base64 failed: {}");
let claims = merge_payload_claims(payload, claims)?;
let tobe_signed = merge_header_claims(header.as_bytes(), claims.as_bytes());
let private_key_representation = base64_decode(private_key)?;
let signed_data_der =
swift_secure_enclave_tool_rs::private_key_sign(&private_key_representation, &tobe_signed)?;
let signed_data = parse_ecdsa_to_rs(signed_data_der.as_slice())?;
let signature = util::base64_encode_url_safe_no_pad(&signed_data);
Ok([&*header, &*claims, &signature].join(SEPARATOR))
}

109
src/cmd_sign_jwt_soft.rs Normal file
View File

@@ -0,0 +1,109 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use jwt::{AlgorithmType, Header, ToBase64};
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use serde_json::{Map, Value};
use crate::cmd_sign_jwt_piv::{build_jwt_parts, digest_by_jwt_algorithm, merge_header_claims, merge_payload_claims, print_jwt_token};
use crate::keychain::{KeychainKey, KeychainKeyValue};
use crate::{cmd_hmac_decrypt, cmd_sign_jwt_piv, cmdutil, ecdsautil, keychain, util};
use crate::ecdsautil::{EcdsaAlgorithm, EcdsaSignType};
const SEPARATOR: &str = ".";
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"sign-jwt-soft"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
let app = SubCommand::with_name(self.name()).about("Sign JWT(Soft EC) subcommand")
.arg(Arg::with_name("private-key").short("k").long("private-key").takes_value(true).help("Private key PKCS#8"))
.arg(cmdutil::build_json_arg());
cmd_sign_jwt_piv::fill_sign_jwt_app_args(app)
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let private_key = opt_value_result!(
sub_arg_matches.value_of("private-key"),
"Private key PKCS#8 DER base64 encoded or PEM"
);
let private_key = cmd_hmac_decrypt::try_decrypt(&mut None, private_key)?;
let private_key = if keychain::is_keychain_key_uri(&private_key) {
debugging!("Private key keychain key URI: {}", &private_key);
let keychain_key = KeychainKey::parse_key_uri(&private_key)?;
let keychain_key_value_bytes = opt_value_result!(
keychain_key.get_password()?,
"Keychain key URI: {} not found",
&private_key
);
let keychain_key_value: KeychainKeyValue =
serde_json::from_slice(&keychain_key_value_bytes)?;
debugging!("Keychain key value {:?}", &keychain_key_value);
keychain_key_value.pkcs8_base64
} else {
private_key
};
let (header, payload, jwt_claims) = build_jwt_parts(sub_arg_matches)?;
let token_string = sign_jwt(&private_key, header, &payload, &jwt_claims)?;
print_jwt_token(json_output, token_string);
Ok(None)
}
}
fn sign_jwt(
private_key: &str,
mut header: Header,
payload: &Option<String>,
claims: &Map<String, Value>,
) -> XResult<String> {
let (jwt_algorithm, private_key_d) = parse_ecdsa_private_key(private_key)?;
header.algorithm = jwt_algorithm;
debugging!("Header: {:?}", header);
debugging!("Claims: {:?}", claims);
let header = opt_result!(header.to_base64(), "Header to base64 failed: {}");
let claims = merge_payload_claims(payload, claims)?;
let tobe_signed = merge_header_claims(header.as_bytes(), claims.as_bytes());
let raw_in = digest_by_jwt_algorithm(jwt_algorithm, &tobe_signed)?;
let ecdsa_algorithm = convert_jwt_algorithm_to_ecdsa_algorithm(jwt_algorithm)?;
let signed_data = ecdsautil::ecdsa_sign(ecdsa_algorithm, &private_key_d, &raw_in, EcdsaSignType::Rs)?;
let signature = util::base64_encode_url_safe_no_pad(&signed_data);
Ok([&*header, &*claims, &signature].join(SEPARATOR))
}
pub fn convert_jwt_algorithm_to_ecdsa_algorithm(jwt_algorithm: AlgorithmType) -> XResult<EcdsaAlgorithm> {
match jwt_algorithm {
AlgorithmType::Es256 => Ok(EcdsaAlgorithm::P256),
AlgorithmType::Es384 => Ok(EcdsaAlgorithm::P384),
AlgorithmType::Es512 => Ok(EcdsaAlgorithm::P521),
_ => simple_error!("SHOULD NOT HAPPEN: {:?}", jwt_algorithm),
}
}
pub fn parse_ecdsa_private_key(private_key: &str) -> XResult<(AlgorithmType, Vec<u8>)> {
let p256_private_key_d = ecdsautil::parse_p256_private_key(private_key).ok();
let p384_private_key_d = ecdsautil::parse_p384_private_key(private_key).ok();
let p521_private_key_d = ecdsautil::parse_p521_private_key(private_key).ok();
let (jwt_algorithm, private_key_d) = match (p256_private_key_d, p384_private_key_d, p521_private_key_d) {
(Some(p256_private_key_d), None, None) => (AlgorithmType::Es256, p256_private_key_d),
(None, Some(p384_private_key_d), None) => (AlgorithmType::Es384, p384_private_key_d),
(None, None, Some(p521_private_key_d)) => (AlgorithmType::Es512, p521_private_key_d),
_ => return simple_error!("Invalid private key: {}", private_key),
};
Ok((jwt_algorithm, private_key_d))
}

209
src/cmd_ssh_agent.rs Normal file
View File

@@ -0,0 +1,209 @@
use std::fs::remove_file;
use std::path::PathBuf;
use crate::cmdutil;
use crate::ecdsautil::{parse_ec_public_key_to_point, parse_ecdsa_r_and_s};
use crate::util::base64_encode;
use clap::{App, Arg, ArgMatches, SubCommand};
use rsa::RsaPublicKey;
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use spki::DecodePublicKey;
use ssh_agent_lib::agent::{listen, Session};
use ssh_agent_lib::error::AgentError;
use ssh_agent_lib::proto::{Extension, Identity, SignRequest};
use ssh_agent_lib::ssh_encoding::Encode;
use ssh_agent_lib::ssh_key::public::KeyData;
use ssh_agent_lib::ssh_key::{Algorithm, Signature};
use ssh_key::public::EcdsaPublicKey;
use ssh_key::{EcdsaCurve, Mpint};
use std::convert::TryFrom;
use tokio::net::UnixListener as Listener;
#[derive(Default, Clone)]
struct MySshAgent {
parameter: String,
comment: String,
}
impl MySshAgent {
fn new(parameter: &str) -> XResult<Self> {
Ok(MySshAgent {
parameter: parameter.to_string(),
comment: parameter.to_string(),
})
}
}
#[ssh_agent_lib::async_trait]
impl Session for MySshAgent {
async fn request_identities(&mut self) -> Result<Vec<Identity>, AgentError> {
information!("request_identities");
let identity = match get_identity(&self.parameter, &self.comment) {
Ok(identity) => identity,
Err(e) => {
failure!("Get identity failed: {}", e);
return Err(AgentError::Failure);
}
};
let mut writer = vec![];
identity.pubkey.encode(&mut writer).unwrap();
println!("{}", base64_encode(&writer));
Ok(vec![identity])
}
async fn sign(&mut self, request: SignRequest) -> Result<Signature, AgentError> {
information!("sign, request: {:?}", request);
let algorithm = &request.pubkey.algorithm();
match algorithm {
Algorithm::Ecdsa { curve: _ } => {}
Algorithm::Rsa { hash: _ } => {}
Algorithm::Ed25519 => {
debugging!("Algorithm::Ed25519 not supported");
return Err(AgentError::Failure);
}
Algorithm::Dsa => {
debugging!("Algorithm::Dsa not supported");
return Err(AgentError::Failure);
}
Algorithm::SkEcdsaSha2NistP256 => {
debugging!("Algorithm::SkEcdsaSha2NistP256 not supported");
return Err(AgentError::Failure);
}
Algorithm::SkEd25519 => {
debugging!("Algorithm::SkEd25519 not supported");
return Err(AgentError::Failure);
}
Algorithm::Other(algorithm_name) => {
debugging!(
"Algorithm::Other not supported, name: {}",
algorithm_name.as_str()
);
return Err(AgentError::Failure);
}
&_ => {
debugging!("Algorithm::Unknown not supported");
return Err(AgentError::Failure);
}
}
let signature = external_command_rs::external_sign(
"card-cli",
self.parameter.as_str(),
"ES384",
&request.data,
)
.unwrap();
information!("{}", hex::encode(&signature));
let (r, s) = parse_ecdsa_r_and_s(signature.as_slice()).unwrap();
let mut ssh_signature = vec![];
let r_mpint = Mpint::from_bytes(&r).unwrap();
let s_mpint = Mpint::from_bytes(&s).unwrap();
r_mpint.encode(&mut ssh_signature).unwrap();
s_mpint.encode(&mut ssh_signature).unwrap();
Ok(Signature::new(
Algorithm::Ecdsa {
curve: EcdsaCurve::NistP384,
},
ssh_signature,
)
.map_err(AgentError::other)?)
}
async fn extension(&mut self, extension: Extension) -> Result<Option<Extension>, AgentError> {
information!("extension: {:?}", extension);
Ok(None)
}
}
fn get_identity(uri: &str, comment: &str) -> XResult<Identity> {
let public_key_bytes = external_command_rs::external_public_key("card-cli", uri)?;
if let Ok(ec_point) = parse_ec_public_key_to_point(&public_key_bytes) {
let identity = Identity {
pubkey: KeyData::Ecdsa(EcdsaPublicKey::from_sec1_bytes(&ec_point).unwrap()),
comment: comment.to_string(),
};
return Ok(identity);
}
if let Ok(rsa_public_key) = RsaPublicKey::from_public_key_der(&public_key_bytes) {
let identity = Identity {
pubkey: KeyData::Rsa(ssh_key::public::RsaPublicKey::try_from(&rsa_public_key).unwrap()),
comment: comment.to_string(),
};
return Ok(identity);
}
simple_error!("Unknown uri algorithm: {}", uri)
}
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"ssh-agent"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("SSH-Agent subcommand")
.arg(
Arg::with_name("sock-file")
.long("sock-file")
.default_value("connect.ssh")
.help("Sock file, usage SSH_AUTH_SOCK=sock-file ssh ..."),
)
.arg(cmdutil::build_parameter_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
warning!("Not works yet.");
debugging!("Sub args: {:?}", sub_arg_matches);
let parameter = sub_arg_matches.value_of("parameter").unwrap();
let sock_file = sub_arg_matches.value_of("sock-file").unwrap();
information!("Sock file: {}", sock_file);
let sock_file_path = PathBuf::from(".");
match std::fs::canonicalize(sock_file_path) {
Ok(canonicalized_sock_file_path) => information!(
"SSH_AUTH_SOCK={}/{}",
canonicalized_sock_file_path.to_str().unwrap_or("-"),
sock_file
),
Err(e) => warning!("Get canonicalized sock file path failed: {}", e),
}
// let ssh_agent = SshAgent::new()?;
// // TODO information!("{}", &ssh_agent.ssh_string);
let _ = remove_file(sock_file);
information!("Start unix socket: {}", sock_file);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
listen(
Listener::bind(sock_file).unwrap(),
MySshAgent::new(parameter).unwrap(),
)
.await
.unwrap();
});
// opt_result!(
// ssh_agent.run_unix(sock_file),
// "Run unix socket: {}, failed: {}",
// sock_file
// );
Ok(None)
}
}

193
src/cmd_ssh_agent_gpg.rs Normal file
View File

@@ -0,0 +1,193 @@
use std::error::Error;
use std::fs::remove_file;
use std::path::PathBuf;
use std::sync::Mutex;
use clap::{App, Arg, ArgMatches, SubCommand};
use openpgp_card::{KeyType, OpenPgp};
use openpgp_card::crypto_data::{Hash, PublicKeyMaterial};
use openssl::hash::MessageDigest;
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use ssh_agent::Agent;
use ssh_agent::proto::{from_bytes, RsaPublicKey, signature, Signature, to_bytes};
use ssh_agent::proto::message::{self, Message};
use ssh_agent::proto::public_key::PublicKey;
use crate::digestutil::{copy_sha256, copy_sha512};
use crate::pinutil;
use crate::sshutil::{generate_ssh_string, with_sign};
struct SshAgent {
open_pgp: Mutex<OpenPgp>,
use_sign: bool,
pin: String,
public_key: PublicKey,
comment: String,
ssh_string: String,
}
impl SshAgent {
fn new(pin: String, use_sign: bool) -> XResult<Self> {
let card = crate::pgpcardutil::get_card()?;
let (public_key, comment, ssh_string, open_pgp) = {
let mut pgp = OpenPgp::new(card);
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
let serial = trans.application_related_data()
.map(|d| d.application_id().map(|i| i.serial()))
.unwrap_or_else(|_| Ok(0)).unwrap_or(0);
let serial = hex::encode(serial.to_be_bytes());
let public_key = opt_result!(trans.public_key(iff!(use_sign, KeyType::Signing, KeyType::Authentication)), "Cannot find signing key: {}");
let rsa_public_key = match public_key {
PublicKeyMaterial::E(_) => return simple_error!("Not supports ec key"),
PublicKeyMaterial::R(rsa_public_key) => rsa_public_key,
_ => return simple_error!("Unknown key type"),
};
let e = rsa_public_key.v();
let n = rsa_public_key.n();
let public_key = PublicKey::Rsa(RsaPublicKey {
e: with_sign(e.to_vec()),
n: with_sign(n.to_vec()),
});
let comment = format!("pgp-card:{}:{}", iff!(use_sign, "sign", "auth"), serial);
drop(trans);
(public_key, comment.clone(), generate_ssh_string(e, n, &comment), pgp)
};
Ok(Self {
open_pgp: Mutex::new(open_pgp),
use_sign,
pin,
public_key,
comment,
ssh_string,
})
}
fn handle_message(&self, request: Message) -> Result<Message, Box<dyn Error>> {
debugging!("Request: {:?}", request);
let response = match request {
Message::RequestIdentities => {
let identities = vec![message::Identity {
pubkey_blob: to_bytes(&self.public_key)?,
comment: self.comment.clone(),
}];
Ok(Message::IdentitiesAnswer(identities))
}
Message::RemoveIdentity(ref _identity) => {
Err(From::from(format!("Not supported message: {:?}", request)))
}
Message::AddIdentity(ref _identity) => {
Err(From::from(format!("Not supported message: {:?}", request)))
}
Message::SignRequest(sign_request) => {
let pubkey: PublicKey = from_bytes(&sign_request.pubkey_blob)?;
if self.public_key != pubkey {
return Err(From::from(format!("Unknown public key: {:?}", sign_request)));
}
debugging!("To be signed data: {:?}", &sign_request.data);
let (algorithm, hash) = if sign_request.flags & signature::RSA_SHA2_512 != 0 {
let hash = opt_result!(openssl::hash::hash(MessageDigest::sha512(), &sign_request.data), "Calc digest failed: {}");
("rsa-sha2-512", Hash::SHA512(copy_sha512(&hash).unwrap()))
} else if sign_request.flags & signature::RSA_SHA2_256 != 0 {
let hash = opt_result!(openssl::hash::hash(MessageDigest::sha256(), &sign_request.data), "Calc digest failed: {}");
("rsa-sha2-256", Hash::SHA256(copy_sha256(&hash).unwrap()))
} else {
return Err(From::from(format!("Not supported sign flags: {:?}", sign_request.flags)));
};
information!("SSH request, algorithm: {}", algorithm);
let mut pgp = self.open_pgp.lock().unwrap();
// let mut pgp = OpenPgp::new(*card_mut);
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
let sig = if self.use_sign {
debugging!("User pin verify for pw1 sign, use sign: {}", self.use_sign);
opt_result!(trans.verify_pw1_sign(self.pin.as_bytes()), "User sign pin verify failed: {}");
opt_result!(trans.signature_for_hash(hash), "Sign OpenPGP card failed: {}")
} else {
debugging!("User pin verify for pw1 user, use sign: {}", self.use_sign);
opt_result!(trans.verify_pw1_user(self.pin.as_bytes()), "User user pin verify failed: {}");
opt_result!(trans.authenticate_for_hash(hash), "Auth OpenPGP card failed: {}")
};
debugging!("Signature: {:?}", sig);
success!("SSH request sign success");
Ok(Message::SignResponse(to_bytes(&Signature {
algorithm: algorithm.to_string(),
blob: sig,
})?))
}
_ => Err(From::from(format!("Unknown message: {:?}", request)))
};
debugging!("Response {:?}", response);
response
}
}
impl Agent for SshAgent {
type Error = ();
fn handle(&self, message: Message) -> Result<Message, ()> {
self.handle_message(message).or_else(|error| {
warning!("Error handling message - {:?}", error);
Ok(Message::Failure)
})
}
}
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "ssh-agent-gpg" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("SSH-Agent OpenPGP card subcommand")
.arg(Arg::with_name("pin").short("p").long("pin").default_value("123456").help("OpenPGP card user pin"))
.arg(Arg::with_name("pgp").long("pgp").help("Use PGP"))
.arg(Arg::with_name("pgp-sign").long("pgp-sign").help("Use PGP sign"))
.arg(Arg::with_name("pgp-auth").long("pgp-auth").help("Use PGP auth"))
.arg(Arg::with_name("piv").long("piv").help("Use PIV"))
.arg(Arg::with_name("sock-file").long("sock-file").default_value("connect.ssh").help("Sock file, usage SSH_AUTH_SOCK=sock-file ssh ..."))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let use_pgp = sub_arg_matches.is_present("pgp");
let use_piv = sub_arg_matches.is_present("piv");
if !(use_pgp ^ use_piv) {
return simple_error!("Args --pgp or --piv must have one selection");
}
let use_pgp_sign = sub_arg_matches.is_present("pgp-sign");
let use_pgp_auth = sub_arg_matches.is_present("pgp-auth");
if use_pgp && !(use_pgp_sign ^ use_pgp_auth) {
return simple_error!("Args --pgp-sign or --pgp-auth must have one selection when use --pgp");
}
let pin_opt = sub_arg_matches.value_of("pin");
let pin_opt = pinutil::get_pin(pin_opt);
let pin = pin_opt.as_deref().unwrap();
let sock_file = sub_arg_matches.value_of("sock-file").unwrap();
information!("Sock file: {}", sock_file);
let sock_file_path = PathBuf::from(".");
match std::fs::canonicalize(sock_file_path) {
Ok(canonicalized_sock_file_path) => information!("SSH_AUTH_SOCK={}/{}",
canonicalized_sock_file_path.to_str().unwrap_or("-"), sock_file),
Err(e) => warning!("Get canonicalized sock file path failed: {}", e),
}
if use_pgp {
let ssh_agent = SshAgent::new(pin.to_string(), use_pgp_sign)?;
information!("{}", &ssh_agent.ssh_string);
let _ = remove_file(sock_file);
information!("Start unix socket: {}", sock_file);
opt_result!(ssh_agent.run_unix(sock_file), "Run unix socket: {}, failed: {}", sock_file);
}
information!("card-cli ssh-agent...");
Ok(None)
}
}

61
src/cmd_ssh_parse.rs Normal file
View File

@@ -0,0 +1,61 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use sshcerts_hatter_fork::ssh::PublicKeyKind;
use sshcerts_hatter_fork::{Certificate, PrivateKey, PublicKey};
use std::fs;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "ssh-parse" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("SSH public key subcommand")
.arg(Arg::with_name("file").short("f").long("file").required(true).takes_value(true).help("SSH file"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let file = sub_arg_matches.value_of("file").unwrap();
let content = opt_result!(fs::read(file), "Read file: {} failed: {}", file);
let content_str = opt_result!(String::from_utf8(content), "Read file(UTF-8): {} failed: {}", file);
let mut parsed = false;
if let Ok(private_key) = PrivateKey::from_string(&content_str) {
parsed = true;
success!("Parse private key success.");
println!("{:?}", private_key);
}
if let Ok(public_key) = PublicKey::from_string(&content_str) {
parsed = true;
success!("Parse public key success.");
println!("Key type: {}", public_key.key_type);
match public_key.kind {
PublicKeyKind::Rsa(rsa) => {
println!(" - e: {}", hex::encode(&rsa.e));
println!(" - n: {}", hex::encode(&rsa.n));
}
PublicKeyKind::Ecdsa(ecdsa) => {
println!(" - curve: {} ({:?})", ecdsa.curve.identifier, ecdsa.curve.kind);
println!(" - key: {}", hex::encode(&ecdsa.key));
println!(" - sk application: {:?}", ecdsa.sk_application);
}
PublicKeyKind::Ed25519(ed12219) => {
println!(" - key: {}", hex::encode(&ed12219.key));
println!(" - sk application: {:?}", ed12219.sk_application);
}
}
println!("Comment: {:?}", public_key.comment);
}
if let Ok(certificate) = Certificate::from_string(&content_str) {
parsed = true;
success!("Parse certificate success.");
println!("{:#?}", certificate);
}
if !parsed {
return simple_error!("Parse SSH file failed, not private key, public key or certificate.");
}
Ok(None)
}
}

119
src/cmd_ssh_parse_sign.rs Normal file
View File

@@ -0,0 +1,119 @@
use std::io::{Cursor, Read};
use std::str::FromStr;
use clap::{App, Arg, ArgMatches, SubCommand};
use pem::Pem;
use rust_util::util_clap::{Command, CommandError};
use rust_util::XResult;
use crate::util;
trait CursorReader {
fn read_bytes(&mut self, len: usize) -> XResult<Vec<u8>>;
fn read_u32(&mut self) -> XResult<u32>;
fn read_string(&mut self) -> XResult<Vec<u8>>;
}
impl CursorReader for Cursor<Vec<u8>> {
fn read_bytes(&mut self, len: usize) -> XResult<Vec<u8>> {
let mut buff = vec![0_u8; len];
Cursor::read_exact(self, &mut buff)?;
Ok(buff)
}
fn read_u32(&mut self) -> XResult<u32> {
let mut num = [0_u8; 4];
num.copy_from_slice(&self.read_bytes(4)?);
Ok(u32::from_be_bytes(num))
}
fn read_string(&mut self) -> XResult<Vec<u8>> {
let len = self.read_u32()?;
if len == 0 {
return Ok(Vec::new());
}
self.read_bytes(len as usize)
}
}
pub struct CommandImpl;
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig
impl Command for CommandImpl {
fn name(&self) -> &str { "ssh-parse-sign" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("SSH parse sign subcommand")
.arg(Arg::with_name("in").long("in").required(true).takes_value(true).help("In file, - for stdin"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let file_in = sub_arg_matches.value_of("in").unwrap();
let bytes_in = util::read_file_or_stdin(file_in)?;
let pem_in = opt_result!(String::from_utf8(bytes_in), "Parse SSH sign failed: {}");
let pem = opt_result!( Pem::from_str(&pem_in), "Parse SSH sign pem failed: {}");
debugging!("PEM: {:?}", pem);
if pem.tag() != "SSH SIGNATURE" {
return simple_error!("Not SSH signature file.");
}
let ssh_signature = pem.contents().to_vec();
let mut cursor = Cursor::new(ssh_signature);
let magic_preamble = String::from_utf8(cursor.read_bytes(6)?)?;
if magic_preamble != "SSHSIG" {
return simple_error!("Bad SSH signature file: magic");
}
let ssh_signature_version = cursor.read_u32()?;
if ssh_signature_version != 1 {
return simple_error!("Bad SSH signature file: version");
}
let public_key = cursor.read_string()?;
debugging!("Public key: {}", hex::encode(&public_key));
let namespace = String::from_utf8(cursor.read_string()?)?;
debugging!("Namespace: {}", namespace);
let reserved = cursor.read_string()?;
debugging!("Reserved: {}", hex::encode(&reserved));
let hash_algorithm = String::from_utf8(cursor.read_string()?)?;
debugging!("Hash algorithm: {}", hash_algorithm);
let signature = cursor.read_string()?;
debugging!("Signature: {}", hex::encode(&signature));
let mut public_key_cursor = Cursor::new(public_key);
let public_key_algorithm = String::from_utf8(public_key_cursor.read_string()?)?;
debugging!("Public key algorithm: {}", public_key_algorithm);
let public_key_algorithm2 = String::from_utf8(public_key_cursor.read_string()?)?;
debugging!("Public key algorithm(2): {}", public_key_algorithm2);
let public_key_value = public_key_cursor.read_string()?;
debugging!("Public key value: {}", hex::encode(&public_key_value));
let mut signature_cursor = Cursor::new(signature);
let signature_algorithm = String::from_utf8(signature_cursor.read_string()?)?;
debugging!("Signature algorithm: {}", signature_algorithm);
let signature_value = signature_cursor.read_string()?;
debugging!("Signature value: {}", hex::encode(&signature_value));
println!("Public Key:\n> {}", public_key_algorithm);
println!(" > {}", public_key_algorithm2);
println!(" > {}", hex::encode(public_key_value));
println!("Namespace: {}", namespace);
println!("Reserved: {}", hex::encode(&reserved));
println!("Hash Algorithm: {}", &hash_algorithm);
println!("Signature:\n> {}", signature_algorithm);
if signature_algorithm.starts_with("ecdsa-") {
let mut signature_value_cursor = Cursor::new(signature_value);
let signature_value_x = signature_value_cursor.read_string()?;
let signature_value_y = signature_value_cursor.read_string()?;
println!(" > {}", hex::encode(signature_value_x));
println!(" > {}", hex::encode(signature_value_y));
} else if signature_algorithm.starts_with("rsa-") {
println!(" > {}", hex::encode(signature_value));
} else {
failure!("Unknown signature algorithm: {}", signature_algorithm);
}
Ok(None)
}
}

195
src/cmd_ssh_piv_cert.rs Normal file
View File

@@ -0,0 +1,195 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use ecdsa::elliptic_curve::pkcs8::der::Encode;
use rand::random;
use rust_util::util_clap::{Command, CommandError};
use rust_util::{util_time, XResult};
use sshcerts_hatter_fork::ssh::{CurveKind, PublicKeyKind, SSHCertificateSigner};
use sshcerts_hatter_fork::utils::format_signature_for_ssh;
use sshcerts_hatter_fork::x509::extract_ssh_pubkey_from_x509_certificate;
use sshcerts_hatter_fork::{CertType, Certificate, PublicKey};
use std::fs;
use std::sync::Mutex;
use std::time::SystemTime;
use yubikey::piv::{sign_data, AlgorithmId, SlotId};
use yubikey::{Key, YubiKey};
use crate::digestutil::{sha256_bytes, sha384_bytes};
use crate::pivutil::slot_equals;
use crate::{cmdutil, pinutil, pivutil, util};
pub struct CommandImpl;
// https://github.com/RustCrypto/SSH
// https://github.com/obelisk/sshcerts/
impl Command for CommandImpl {
fn name(&self) -> &str { "ssh-piv-cert" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("SSH PIV sign cert subcommand")
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("key-id").short("k").long("key-id").takes_value(true).default_value("default_key_id").help("SSH user CA key id"))
.arg(Arg::with_name("principal").short("P").long("principal").takes_value(true).default_value("root").multiple(true).help("SSH user CA principal"))
.arg(Arg::with_name("pub").short("f").long("pub").alias("pub-file").required(true).takes_value(true).help("SSH public key file"))
.arg(Arg::with_name("out").short("o").long("out").takes_value(true).help("CA out key file"))
.arg(Arg::with_name("type").short("t").long("type").takes_value(true).default_value("user").help("CA type (user or host)"))
.arg(Arg::with_name("validity").long("validity").takes_value(true).default_value("1h").help("CA validity period e.g. 10m means 10 minutes (s - second, m - minute, h - hour, d - day)"))
.arg(Arg::with_name("force").long("force").help("Force write SSH CA file"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let ssh_pub_file = sub_arg_matches.value_of("pub").unwrap();
let ssh_pub_bytes = util::read_file_or_stdin(ssh_pub_file)?;
let ssh_pub_str = String::from_utf8(ssh_pub_bytes).expect("Read SSh pub file failed: {}");
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let slot_id = pivutil::get_slot_id(slot)?;
let serial: u64 = random();
let key_id = sub_arg_matches.value_of("key-id").unwrap();
let principals = sub_arg_matches.values_of("principal")
.map(|ps| ps.map(|p| p.to_string()).collect::<Vec<_>>())
.unwrap_or_else(|| vec!["root".to_string()]);
let ca_type = sub_arg_matches.value_of("type").unwrap();
let cert_type = match ca_type.to_lowercase().as_str() {
"user" => CertType::User,
"host" => CertType::Host,
_ => return simple_error!("Invalid CA type: {}",ca_type),
};
let now_secs = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
let validity = sub_arg_matches.value_of("validity").unwrap();
let validity_u64 = util_time::parse_duration(validity).map(|t| t.as_secs()).unwrap();
let output = sub_arg_matches.value_of("out");
let ssh_cert_file = match output {
None if ssh_pub_file.ends_with(".pub") => {
Some(format!("{}-cert.pub", String::from_utf8(ssh_pub_file.as_bytes()[0..ssh_pub_file.len() - 4].to_vec()).unwrap()))
}
None => None,
Some("-") => None,
Some(output) => Some(output.to_string()),
};
if let Some(ssh_cert_file) = &ssh_cert_file {
let force_write = sub_arg_matches.is_present("force");
if fs::metadata(ssh_cert_file).is_ok() && !force_write {
return simple_error!("Target file: {} exists", & ssh_cert_file);
}
}
information!("Serial: {}", serial);
information!("Key ID: {}", key_id);
information!("Principals: {:?}", principals);
information!("Validity: {} seconds", validity_u64);
let pin_opt = pinutil::read_pin(sub_arg_matches);
let cert_der = find_cert(&mut yk, slot_id)?;
let ca_ssh_pub_key = opt_result!(extract_ssh_pubkey_from_x509_certificate(&cert_der), "Extract SSH public key failed: {}");
debugging!("SSH CA: {}", ca_ssh_pub_key);
debugging!("SSH CA fingerprint: {}", ca_ssh_pub_key.fingerprint());
let ca_ssh_pub_algorithm_id = get_ssh_key_type(&ca_ssh_pub_key)?;
let ssh_yubikey_signer = SshYubikeySinger::new(
yk,
pin_opt,
slot_id,
ca_ssh_pub_key.clone(),
ca_ssh_pub_algorithm_id,
);
let tobe_signed_ssh_pub_key = opt_result!(PublicKey::from_string(&ssh_pub_str), "Parse tobe signed SSH public key failed: {}");
let user_cert_result = Certificate::builder(&tobe_signed_ssh_pub_key, cert_type, &ca_ssh_pub_key)
.unwrap()
.serial(serial)
.key_id(key_id)
.set_principals(&principals)
.valid_after(now_secs - 1)
.valid_before(now_secs + validity_u64)
.set_extensions(Certificate::standard_extensions())
.sign(&ssh_yubikey_signer);
// View *-cert.pub:
// ssh-keygen -L -f *-cert.pub
let user_cert = opt_result!(user_cert_result, "Sign SSH user CA failed: {}");
match ssh_cert_file {
None => println!("{}", user_cert),
Some(ssh_cert_file) => {
opt_result!(fs::write( & ssh_cert_file, user_cert.to_string()), "Write file: {} failed: {}", &ssh_cert_file);
}
}
Ok(None)
}
}
fn find_cert(yk: &mut YubiKey, slot_id: SlotId) -> XResult<Vec<u8>> {
match Key::list(yk) {
Err(e) => warning!("List keys failed: {}", e),
Ok(keys) => {
for k in &keys {
let slot_str = format!("{:x}", Into::<u8>::into(k.slot()));
if slot_equals(&slot_id, &slot_str) {
let cert_der = k.certificate().cert.to_der()?;
return Ok(cert_der);
}
}
}
}
simple_error!("Cannot find slot: {}", slot_id)
}
pub fn get_ssh_key_type(public_key: &PublicKey) -> XResult<AlgorithmId> {
match &public_key.kind {
PublicKeyKind::Ecdsa(x) => match x.curve.kind {
CurveKind::Nistp256 => Ok(AlgorithmId::EccP256),
CurveKind::Nistp384 => Ok(AlgorithmId::EccP384),
CurveKind::Nistp521 => simple_error!("NIST P521 is not supported."),
},
PublicKeyKind::Rsa(_) => simple_error!("RSA is not supported."),
PublicKeyKind::Ed25519(_) => simple_error!("Ed25519 is not supported."),
}
}
struct SshYubikeySinger {
yubikey: Mutex<YubiKey>,
pin_opt: Option<String>,
ca_ssh_pub_slot_id: SlotId,
ca_ssh_pub_key: PublicKey,
ca_ssh_pub_algorithm_id: AlgorithmId,
}
impl SshYubikeySinger {
fn new(yubikey: YubiKey, pin_opt: Option<String>, ca_ssh_pub_slot_id: SlotId, ca_ssh_pub_key: PublicKey, ca_ssh_pub_algorithm_id: AlgorithmId) -> Self {
Self {
yubikey: Mutex::new(yubikey),
pin_opt,
ca_ssh_pub_slot_id,
ca_ssh_pub_key,
ca_ssh_pub_algorithm_id,
}
}
}
impl SSHCertificateSigner for SshYubikeySinger {
fn sign(&self, buffer: &[u8]) -> Option<Vec<u8>> {
let digest = match self.ca_ssh_pub_algorithm_id {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => { panic!("Should not reach here.") }
AlgorithmId::EccP256 => sha256_bytes(buffer),
AlgorithmId::EccP384 => sha384_bytes(buffer),
};
let mut yubikey = self.yubikey.lock().unwrap();
if let Some(pin) = &self.pin_opt {
yubikey.verify_pin(pin.as_bytes()).expect("Verify PIN failed: {}");
}
let signature = sign_data(&mut yubikey, &digest, self.ca_ssh_pub_algorithm_id, self.ca_ssh_pub_slot_id).expect("SSH user CA sign failed: {}");
format_signature_for_ssh(&self.ca_ssh_pub_key, signature.as_ref())
}
}

140
src/cmd_ssh_piv_sign.rs Normal file
View File

@@ -0,0 +1,140 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use der_parser::ber::BerObjectContent;
use pem::Pem;
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use yubikey::{Key, YubiKey};
use yubikey::piv::{AlgorithmId, sign_data};
use crate::{cmdutil, pinutil, pivutil, util};
use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
use crate::sshutil::SshVecWriter;
pub struct CommandImpl;
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig
impl Command for CommandImpl {
fn name(&self) -> &str { "ssh-piv-sign" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("SSH piv sign subcommand")
.arg(cmdutil::build_slot_arg())
.arg(cmdutil::build_pin_arg())
.arg(cmdutil::build_no_pin_arg())
.arg(Arg::with_name("namespace").short("n").long("namespace").takes_value(true).help("Namespace"))
.arg(Arg::with_name("in").long("in").required(true).takes_value(true).help("In file, - for stdin"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
util_msg::set_logger_std_out(false);
let namespace_opt = sub_arg_matches.value_of("namespace");
let namespace = match namespace_opt {
None => return simple_error!("Namespace required"),
Some(namespace) => namespace,
};
let data = util::read_file_or_stdin(sub_arg_matches.value_of("in").unwrap())?;
let slot = opt_value_result!(sub_arg_matches.value_of("slot"), "--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e");
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let slot_id = pivutil::get_slot_id(slot)?;
let pin_opt = pinutil::read_pin(sub_arg_matches);
let mut algorithm_id_opt = None;
let mut ec_key_point = vec![];
match Key::list(&mut yk) {
Err(e) => warning!("List keys failed: {}", e),
Ok(keys) => for k in &keys {
let slot_str = format!("{:x}", Into::<u8>::into(k.slot()));
if slot_equals(&slot_id, &slot_str) {
let cert = &k.certificate().cert.tbs_certificate;
let certificate = k.certificate();
if let Ok(algorithm_id) = get_algorithm_id_by_certificate(certificate) {
match algorithm_id {
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
let public_key_bit_string = &cert.subject_public_key_info.subject_public_key;
ec_key_point.extend_from_slice(public_key_bit_string.raw_bytes());
algorithm_id_opt = Some(algorithm_id);
}
_ => return simple_error!("Not P256/384 key: {}", algorithm_id.to_str()),
}
}
}
}
}
let algorithm_id = match algorithm_id_opt {
None => return simple_error!("Slot key not found!"),
Some(algorithm_id) => algorithm_id,
};
let ec_bit_len = iff!(matches!(algorithm_id, AlgorithmId::EccP256), 256, 384);
let mut buffer = vec![];
buffer.write_bytes("SSHSIG".as_bytes());
buffer.write_u32(1);
let mut public_key = vec![];
public_key.write_string(format!("ecdsa-sha2-nistp{}", ec_bit_len).as_bytes());
public_key.write_string(format!("nistp{}", ec_bit_len).as_bytes());
public_key.write_string(&ec_key_point);
buffer.write_string(&public_key);
buffer.write_string(namespace.as_bytes());
buffer.write_string("".as_bytes());
// The supported hash algorithms are "sha256" and "sha512".
buffer.write_string("sha512".as_bytes());
let mut signature = vec![];
signature.write_string(format!("ecdsa-sha2-nistp{}", ec_bit_len).as_bytes());
let mut sign_message = vec![];
sign_message.write_bytes("SSHSIG".as_bytes());
sign_message.write_string(namespace.as_bytes());
sign_message.write_string("".as_bytes());
sign_message.write_string("sha512".as_bytes());
let data_digest = crate::digestutil::sha512_bytes(&data);
debugging!("Data digest: {} (sha512)", hex::encode(&data_digest));
sign_message.write_string(&data_digest);
debugging!("Singed message: {}", hex::encode(&sign_message));
let tobe_signed_data = if ec_bit_len == 256 {
crate::digestutil::sha256_bytes(&sign_message)
} else {
crate::digestutil::sha384_bytes(&sign_message)
};
debugging!("Digest of signed message: {}", hex::encode(&tobe_signed_data));
if let Some(pin) = &pin_opt {
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
}
let mut signature_value = vec![];
let signed_data = opt_result!(sign_data(&mut yk, &tobe_signed_data, algorithm_id, slot_id), "Sign PIV failed: {}");
debugging!("Signature: {}", hex::encode(signed_data.as_slice()));
let (_, parsed_signature) = opt_result!(der_parser::parse_der(signed_data.as_slice()), "Parse signature failed: {}");
match parsed_signature.content {
BerObjectContent::Sequence(seq) => {
match &seq[0].content {
BerObjectContent::Integer(r) => {
debugging!("Signature r: {}", hex::encode(r));
signature_value.write_string(r);
}
_ => return simple_error!("Parse signature failed: [0]not integer"),
}
match &seq[1].content {
BerObjectContent::Integer(s) => {
debugging!("Signature s: {}", hex::encode(s));
signature_value.write_string(s);
}
_ => return simple_error!("Parse signature failed: [1]not integer"),
}
}
_ => return simple_error!("Parse signature failed: not sequence"),
}
signature.write_string(&signature_value);
buffer.write_string(&signature);
let ssh_sig = Pem::new("SSH SIGNATURE", buffer);
let ssh_sig_pem = ssh_sig.to_string();
println!("{}", ssh_sig_pem);
Ok(None)
}
}

129
src/cmd_ssh_pub_key.rs Normal file
View File

@@ -0,0 +1,129 @@
use crate::digestutil::sha256_bytes;
use crate::pivutil::{get_algorithm_id_by_certificate, slot_equals, ToStr};
use crate::sshutil::SshVecWriter;
use crate::{cmdutil, pivutil, util};
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use std::collections::BTreeMap;
use yubikey::piv::AlgorithmId;
use yubikey::{Key, YubiKey};
use crate::util::base64_encode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"ssh-pub-key"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("SSH public key subcommand")
.arg(cmdutil::build_slot_arg())
.arg(Arg::with_name("ca").long("ca").help("SSH cert-authority"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let slot = opt_value_result!(
sub_arg_matches.value_of("slot"),
"--slot must assigned, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e"
);
let ca = sub_arg_matches.is_present("ca");
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
let slot_id = pivutil::get_slot_id(slot)?;
let mut algorithm_id_opt = None;
let mut ec_key_point = vec![];
match Key::list(&mut yk) {
Err(e) => warning!("List keys failed: {}", e),
Ok(keys) => {
for k in &keys {
let slot_str = format!("{:x}", Into::<u8>::into(k.slot()));
if slot_equals(&slot_id, &slot_str) {
let cert = &k.certificate().cert.tbs_certificate;
let certificate = k.certificate();
if let Ok(algorithm_id) = get_algorithm_id_by_certificate(certificate) {
match algorithm_id {
AlgorithmId::EccP256 | AlgorithmId::EccP384 => {
let public_key_bit_string =
&cert.subject_public_key_info.subject_public_key;
ec_key_point
.extend_from_slice(public_key_bit_string.raw_bytes());
algorithm_id_opt = Some(algorithm_id);
}
_ => {
return simple_error!(
"Not P256/384 key: {}",
algorithm_id.to_str()
)
}
}
}
}
}
}
}
let algorithm_id = match algorithm_id_opt {
None => return simple_error!("Slot key not found!"),
Some(algorithm_id) => algorithm_id,
};
let ssh_algorithm = match algorithm_id {
AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 => panic!("Not supported."),
AlgorithmId::EccP256 => "nistp256",
AlgorithmId::EccP384 => "nistp384",
};
information!("SSH algorithm: {}", ssh_algorithm);
information!("ECDSA public key: {}", hex::encode(&ec_key_point));
// ECDSA SSH public key format:
// string ecdsa-sha2-[identifier]
// byte[n] ecc_key_blob
//
// ecc_key_blob:
// string [identifier]
// string Q
//
// [identifier] will be nistp256 or nistp384
let mut ssh_pub_key = vec![];
ssh_pub_key.write_string(format!("ecdsa-sha2-{}", ssh_algorithm).as_bytes());
let mut ecc_key_blob = vec![];
ecc_key_blob.write_string(ssh_algorithm.as_bytes());
ecc_key_blob.write_string(&ec_key_point);
ssh_pub_key.write_bytes(&ecc_key_blob);
let ssh_pub_key_sha256 = sha256_bytes(&ssh_pub_key);
information!(
"SSH key SHA256: {} (base64)",
base64_encode(&ssh_pub_key_sha256)
);
information!("SSH key SHA256: {} (hex)", hex::encode(&ssh_pub_key_sha256));
eprintln!();
let ssh_pub_key = format!(
"{}ecdsa-sha2-{} {} YubiKey-PIV-{}",
if ca {
"cert-authority,principals=\"root\" "
} else {
""
},
ssh_algorithm,
base64_encode(&ssh_pub_key),
slot_id
);
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("ssh_pub_key", ssh_pub_key);
util::print_pretty_json(&json);
} else {
println!("{}", &ssh_pub_key);
}
Ok(None)
}
}

154
src/cmd_u2f_register.rs Normal file
View File

@@ -0,0 +1,154 @@
use std::collections::BTreeMap;
use std::sync::mpsc::channel;
use authenticator::authenticatorservice::AuthenticatorService;
use authenticator::RegisterFlags;
use authenticator::statecallback::StateCallback;
use clap::{App, Arg, ArgMatches, SubCommand};
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::sign::Verifier;
use rust_util::util_clap::{Command, CommandError};
use x509_parser::certificate::X509Certificate;
use x509_parser::prelude::FromDer;
use crate::{cmdutil, digestutil, util};
use crate::fidoutil;
use crate::fidoutil::{U2fRegistrationData, U2fV2Challenge};
use crate::util::base64_encode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "u2f-register" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("FIDO U2F register subcommand")
.arg(Arg::with_name("app-id").short("a").long("app-id").default_value("https://example.com").help("App id"))
.arg(Arg::with_name("timeout").short("t").long("timeout").default_value("30").help("Timeout in seconds"))
.arg(Arg::with_name("challenge").long("challenge").takes_value(true).help("Challenge HEX"))
.arg(Arg::with_name("challenge-with-timestamp-prefix").long("challenge-with-timestamp-prefix").help("Challenge with timestamp prefix"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
Ok(t) => (t * 1000) as u64,
Err(e) => return simple_error!("Timeout should be a number: {}", e),
};
let app_id = sub_arg_matches.value_of("app-id").unwrap();
let challenge_hex = sub_arg_matches.value_of("challenge");
let challenge_with_timestamp_prefix = sub_arg_matches.is_present("challenge-with-timestamp-prefix");
let u2fv2_challenge = U2fV2Challenge::new_challenge(challenge_hex, app_id, challenge_with_timestamp_prefix)?;
let u2fv2_challenge_str = u2fv2_challenge.to_json();
let app_id_hash = digestutil::sha256(app_id);
let challenge_hash = digestutil::sha256(&u2fv2_challenge_str);
let flags = RegisterFlags::empty();
let status_tx = fidoutil::start_status_updater();
let (register_tx, register_rx) = channel();
let callback = StateCallback::new(Box::new(move |rv| {
register_tx.send(rv).unwrap();
}));
information!("App id: {}, Start U2F register...", app_id);
debugging!("Wait timeout: {} ms", timeout_ms);
let mut manager = opt_result!(AuthenticatorService::new(), "Create authenticator service failed: {}");
manager.add_u2f_usb_hid_platform_transports();
if let Err(e) = manager.register(
flags,
timeout_ms,
challenge_hash.clone(),
app_id_hash.clone(),
vec![],
status_tx,
callback,
) {
return simple_error!("Couldn't register: {:?}", e);
};
let register_result = opt_result!(register_rx.recv()?, "Register U2F failed: {}");
let u2f_registration_data = opt_result!(
U2fRegistrationData::from(app_id, &u2fv2_challenge_str, &register_result), "Parse registration data failed: {}");
// +------+-------------------+-----------------+------------+--------------------+
// + 0x00 | application (32B) | challenge (32B) | key handle | User pub key (65B) |
// +------+-------------------+-----------------+------------+--------------------+
let mut signed_message = Vec::with_capacity(200);
signed_message.push(0x00);
signed_message.extend_from_slice(&app_id_hash);
signed_message.extend_from_slice(&challenge_hash);
signed_message.extend_from_slice(&u2f_registration_data.key_handle);
signed_message.extend_from_slice(&u2f_registration_data.pub_key);
// +------+--------------------+---------------------+------------+------------+------+
// + 0x05 | User pub key (65B) | key handle len (1B) | key handle | X.509 Cert | Sign |
// +------+--------------------+---------------------+------------+------------+------+
let sign_prefix_len = 1 + 65 + 1
+ u2f_registration_data.key_handle.len()
+ u2f_registration_data.attestation_cert.as_ref().map(|c| c.len()).unwrap_or(0);
let sign = &register_result.0[sign_prefix_len..];
if json_output {
let mut json = BTreeMap::new();
// println!("{}", serde_json::to_string_pretty(&u2f_registration_data).unwrap());
if let Some(device_name) = u2f_registration_data.device_name {
json.insert("device_name", device_name);
}
if let Some(attestation_cert_pem) = u2f_registration_data.attestation_cert_pem {
json.insert("attestation_cert_pem", attestation_cert_pem);
}
json.insert("device_info", format!("{}", u2f_registration_data.device_info));
json.insert("pub_key", hex::encode(&u2f_registration_data.pub_key));
json.insert("key_handle", hex::encode(&u2f_registration_data.key_handle));
json.insert("signature", hex::encode(sign));
json.insert("signed_message", hex::encode(&signed_message));
json.insert("registration_data", hex::encode(&register_result.0));
json.insert("app_id", app_id.to_string());
json.insert("app_id_hash", hex::encode(&app_id_hash));
json.insert("challenge", u2fv2_challenge_str);
json.insert("challenge_hash", hex::encode(&challenge_hash));
util::print_pretty_json(&json);
} else {
success!("Device info: {}", u2f_registration_data.device_info);
information!("Register challenge: {}", u2fv2_challenge_str);
information!("Register challenge base64: {}", base64_encode(&u2fv2_challenge_str));
if let Some(cert) = u2f_registration_data.attestation_cert_pem {
information!("Attestation certificate: {}", cert);
}
if let Some(device_name) = u2f_registration_data.device_name {
information!("Device name: {}", device_name);
}
success!("Public key: {}", hex::encode(&u2f_registration_data.pub_key));
success!("Key handle: {}", hex::encode(&u2f_registration_data.key_handle));
debugging!("Registration data: {}", hex::encode(&register_result.0));
information!("Signed message: {}", hex::encode(&signed_message));
information!("Signature: {}", hex::encode(sign));
if let Some(attestation_cert) = &u2f_registration_data.attestation_cert {
let cert = opt_result!(X509Certificate::from_der(attestation_cert), "Parse attestation cert failed: {}");
debugging!("Attestation public key: {:?}", cert.1.public_key().subject_public_key);
let pkey = opt_result!(PKey::public_key_from_der(cert.1.public_key().raw), "Parse public key failed: {}");
let mut verifier = opt_result!(Verifier::new(MessageDigest::sha256(), &pkey), "Verifier new failed: {}");
verifier.update(&signed_message)?;
let verify_result = opt_result!(verifier.verify(sign), "Verifier verify failed: {}");
if verify_result {
success!("Verify success");
} else {
failure!("Verify failed");
}
} else {
warning!("Cannot find attestation cert!");
}
}
Ok(None)
}
}

169
src/cmd_u2f_sign.rs Normal file
View File

@@ -0,0 +1,169 @@
use std::collections::BTreeMap;
use std::sync::mpsc::channel;
use authenticator::{AuthenticatorTransports, KeyHandle, SignFlags};
use authenticator::authenticatorservice::AuthenticatorService;
use authenticator::statecallback::StateCallback;
use clap::{App, Arg, ArgMatches, SubCommand};
use openssl::bn::BigNumContext;
use openssl::ec::{EcGroup, EcKey, EcPoint};
use openssl::hash::MessageDigest;
use openssl::nid::Nid;
use openssl::pkey::PKey;
use openssl::sign::Verifier;
use rust_util::util_clap::{Command, CommandError};
use crate::{cmdutil, digestutil, util};
use crate::fidoutil;
use crate::fidoutil::U2fV2Challenge;
use crate::util::base64_encode;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "u2f-sign" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("FIDO U2F sign subcommand")
.arg(Arg::with_name("app-id").short("a").long("app-id").default_value("https://example.com").help("App id"))
.arg(Arg::with_name("timeout").short("t").long("timeout").default_value("30").help("Timeout in seconds"))
.arg(Arg::with_name("public-key-hex").long("public-key-hex").takes_value(true).help("Public key hex"))
.arg(Arg::with_name("challenge").long("challenge").takes_value(true).help("Challenge HEX"))
.arg(Arg::with_name("challenge-with-timestamp-prefix").long("challenge-with-timestamp-prefix").help("Challenge with timestamp prefix"))
.arg(Arg::with_name("key-handle").short("k").long("key-handle").takes_value(true).multiple(true).help("Key handle"))
.arg(cmdutil::build_json_arg())
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = cmdutil::check_json_output(sub_arg_matches);
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
Ok(t) => (t * 1000) as u64,
Err(e) => return simple_error!("Timeout should be a number: {}", e),
};
let app_id = sub_arg_matches.value_of("app-id").unwrap();
let key_handles = opt_value_result!( sub_arg_matches.values_of("key-handle"), "Key handle is required");
let mut request_key_handles = vec![];
for kh in key_handles {
match hex::decode(kh) {
Ok(k) => request_key_handles.push(KeyHandle {
credential: k,
transports: AuthenticatorTransports::empty(),
}),
Err(e) => warning!("Parse key handle: {}, failed: {}", kh, e),
}
}
if request_key_handles.is_empty() {
return simple_error!("No valid key handle provided");
}
let flags = SignFlags::empty();
let (sign_tx, sign_rx) = channel();
let callback = StateCallback::new(Box::new(move |rv| {
sign_tx.send(rv).unwrap();
}));
let challenge_hex = sub_arg_matches.value_of("challenge");
let challenge_with_timestamp_prefix = sub_arg_matches.is_present("challenge-with-timestamp-prefix");
let u2fv2_challenge = U2fV2Challenge::new_challenge(challenge_hex, app_id, challenge_with_timestamp_prefix)?;
let u2fv2_challenge_str = u2fv2_challenge.to_json();
let app_id_hash = digestutil::sha256(app_id);
let challenge_hash = digestutil::sha256(&u2fv2_challenge_str);
let status_tx = fidoutil::start_status_updater();
information!("App id: {}, Start sign...", app_id);
debugging!("Wait timeout: {} ms", timeout_ms);
let mut manager = opt_result!(AuthenticatorService::new(), "Create authenticator service failed: {}");
manager.add_u2f_usb_hid_platform_transports();
if let Err(e) = manager.sign(
flags,
timeout_ms,
challenge_hash.clone(),
vec![app_id_hash.clone()],
request_key_handles,
status_tx,
callback,
) {
return simple_error!("Couldn't sign: {:?}", e);
}
let sign_result = opt_result!(sign_rx.recv(), "Problem receiving, unable to continue: {}");
let (_, handle_used, sign_data, device_info) = opt_result!(sign_result, "Sign failed: {}");
let user_presence_flag = &sign_data[0];
let counter = &sign_data[1..=4];
let signature = &sign_data[5..];
let counter_u32 = u32::from_be_bytes([counter[0], counter[1], counter[2], counter[3]]);
// application (32B) + user presence (1B) + counter (4B) + client data hash (32B)
let mut signed_message = Vec::with_capacity(128);
signed_message.extend_from_slice(&app_id_hash);
signed_message.push(*user_presence_flag);
signed_message.extend_from_slice(counter);
signed_message.extend_from_slice(&challenge_hash);
success!("Device info: {}", &device_info);
let mut json = BTreeMap::new();
if json_output {
json.insert("app_id", app_id.to_string());
json.insert("app_id_hash", hex::encode(&app_id_hash));
json.insert("challenge", u2fv2_challenge_str.to_string());
json.insert("challenge_hash", hex::encode(&challenge_hash));
json.insert("device_info", format!("{}", &device_info));
json.insert("signature", hex::encode(signature));
json.insert("signed_message", hex::encode(&signed_message));
json.insert("key_handle", hex::encode(&handle_used));
json.insert("sign_data", hex::encode(&sign_data));
json.insert("user_presence_flag", format!("{}", *user_presence_flag));
json.insert("counter", format!("{}", counter_u32));
} else {
information!("Sign challenge: {}", u2fv2_challenge_str);
information!("Sign challenge base64: {}", base64_encode(&u2fv2_challenge_str));
information!("Sign result : {}", base64_encode(&sign_data));
information!("- presence : {}", user_presence_flag);
information!("- counter : {}", counter_u32);
information!("- signature: {}", hex::encode(signature));
information!("Key handle: {}", hex::encode(&handle_used));
information!("Signed message: {}", hex::encode(&signed_message));
}
if let Some(public_key_hex) = sub_arg_matches.value_of("public-key-hex") {
let public_key = opt_result!(hex::decode(public_key_hex), "Parse public key hex failed: {}");
if json_output {
json.insert("pub_key", hex::encode(&public_key));
} else {
information!("Public key: {}", hex::encode(&public_key));
let authorization_result = u2f::authorization::parse_sign_response(
app_id.to_string(),
u2fv2_challenge_str.as_bytes().to_vec(),
public_key.clone(),
sign_data.clone(),
);
let authorization = opt_result!(authorization_result, "Parse authorization failed: {}");
success!("Parse authorization success, counter: {}", authorization.counter);
let ec_group = opt_result!(EcGroup::from_curve_name(Nid::X9_62_PRIME256V1), "New secp256r1 EC group failed: {}");
let ec_point = opt_result!(EcPoint::from_bytes(&ec_group, &public_key, &mut BigNumContext::new().unwrap()), "Parse from secp256r1 point failed: {}");
let ec_key = opt_result!(EcKey::from_public_key(&ec_group, &ec_point), "Parse secp256r1 public key failed: {}");
let ec_pkey = opt_result!(PKey::from_ec_key(ec_key), "EC secp256r1 key to PKey failed: {}");
let mut verifier = opt_result!(Verifier::new(MessageDigest::sha256(), &ec_pkey), "Verifier new failed: {}");
verifier.update(&signed_message)?;
let verify_result = opt_result!(verifier.verify(signature), "Verifier verify failed: {}");
if verify_result {
success!("Verify success");
} else {
failure!("Verify failed");
}
}
}
if json_output {
util::print_pretty_json(&json);
}
Ok(None)
}
}

View File

@@ -1,113 +0,0 @@
use std::sync::mpsc::channel;
use authenticator::authenticatorservice::AuthenticatorService;
use authenticator::RegisterFlags;
use authenticator::statecallback::StateCallback;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use crate::digest;
use crate::fido;
use crate::fido::{U2fRegistrationData, U2fV2Challenge};
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "u2f-register" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("FIDO U2F Register subcommand")
.arg(Arg::with_name("app-id").short("a").long("app-id").default_value("https://example.com").help("App id"))
.arg(Arg::with_name("timeout").short("t").long("timeout").default_value("10").help("Timeout in seconds"))
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output { rust_util::util_msg::set_logger_std_out(false); }
let app_id = sub_arg_matches.value_of("app-id").unwrap();
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
Ok(t) => (t * 1000) as u64,
Err(e) => return simple_error!("Timeout should be a number: {}", e),
};
let u2fv2_challenge = U2fV2Challenge::new_random(app_id);
let u2fv2_challenge_str = u2fv2_challenge.to_json();
let chall_bytes = digest::sha256(&u2fv2_challenge_str);
let app_bytes = digest::sha256(app_id);
let flags = RegisterFlags::empty();
let status_tx = fido::start_status_updater();
let (register_tx, register_rx) = channel();
let callback = StateCallback::new(Box::new(move |rv| {
register_tx.send(rv).unwrap();
}));
information!("App id: {}, Start U2F register...", app_id);
debugging!("Wait timeout: {} ms", timeout_ms);
let mut manager = opt_result!(AuthenticatorService::new(), "Create authenticator service failed: {}");
manager.add_u2f_usb_hid_platform_transports();
if let Err(e) = manager.register(
flags,
timeout_ms,
chall_bytes,
app_bytes,
vec![],
status_tx,
callback,
) {
return simple_error!("Couldn't register: {:?}", e);
};
let register_result = opt_result!(register_rx.recv()?, "Register U2F failed: {}");
let u2f_registration_data = opt_result!(
U2fRegistrationData::from(app_id, &u2fv2_challenge_str, register_result), "Parse registration data failed: {}");
if json_output {
println!("{}", serde_json::to_string_pretty(&u2f_registration_data).unwrap());
} else {
success!("Device info: {}", u2f_registration_data.device_info);
success!("Register challenge: {}", u2fv2_challenge_str);
success!("Register challenge base64: {}", base64::encode(&u2fv2_challenge_str));
if let Some(cert) = u2f_registration_data.attestation_cert_pem {
success!("Attestation certificate: {}", cert);
}
if let Some(device_name) = u2f_registration_data.device_name {
success!("Device name: {}", device_name);
}
success!("Public key: {}", u2f_registration_data.pub_key);
success!("Key handle: {}", u2f_registration_data.key_handle);
}
Ok(None)
}
}
//
// // U2F raw message format specification (version 20170411) section 4.3
// // In case of success we need to send back the following reply
// // (excluding ISO7816 success code)
// // +------+--------------------+---------------------+------------+------------+------+
// // + 0x05 | User pub key (65B) | key handle len (1B) | key handle | X.509 Cert | Sign |
// // +------+--------------------+---------------------+------------+------------+------+
// //
// // Where Sign is an ECDSA signature over the following structure:
// // +------+-------------------+-----------------+------------+--------------------+
// // + 0x00 | application (32B) | challenge (32B) | key handle | User pub key (65B) |
// // +------+-------------------+-----------------+------------+--------------------+
// // see https://github.com/google/OpenSK/blob/stable/src/ctap/ctap1.rs
// fn u2f_get_key_handle_from_register_response(register_response: &[u8]) -> XResult<Vec<u8>> {
// if register_response[0] != 0x05 {
// return simple_error!("Reserved byte not set correctly");
// }
//
// let key_handle_len = register_response[66] as usize;
// let mut public_key = register_response.to_owned();
// let mut key_handle = public_key.split_off(67);
// let _attestation = key_handle.split_off(key_handle_len);
//
// Ok(key_handle)
// }

View File

@@ -1,122 +0,0 @@
use std::sync::mpsc::channel;
use authenticator::{AuthenticatorTransports, KeyHandle, SignFlags};
use authenticator::authenticatorservice::AuthenticatorService;
use authenticator::statecallback::StateCallback;
use clap::{App, Arg, ArgMatches, SubCommand};
use openssl::sha::sha256;
use rust_util::util_clap::{Command, CommandError};
use crate::digest;
use crate::fido;
use crate::fido::U2fV2Challenge;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str { "u2f-sign" }
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("FIDO U2F Sign subcommand")
.arg(Arg::with_name("app-id").short("a").long("app-id").default_value("https://example.com").help("App id"))
.arg(Arg::with_name("timeout").short("t").long("timeout").default_value("10").help("Timeout in seconds"))
.arg(Arg::with_name("public-key-hex").long("public-key-hex").takes_value(true).help("Public key hex"))
.arg(Arg::with_name("key-handle").short("k").long("key-handle").takes_value(true).multiple(true).help("Key handle"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let app_id = sub_arg_matches.value_of("app-id").unwrap();
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
Ok(t) => (t * 1000) as u64,
Err(e) => return simple_error!("Timeout should be a number: {}", e),
};
let key_handles = opt_value_result!( sub_arg_matches.values_of("key-handle"), "Key handle is required");
let mut request_key_handles = vec![];
for kh in key_handles {
match hex::decode(kh) {
Ok(k) => request_key_handles.push(KeyHandle {
credential: k,
transports: AuthenticatorTransports::empty(),
}),
Err(e) => warning!("Parse key handle: {}, failed: {}", kh, e),
}
}
if request_key_handles.is_empty() {
return simple_error!("No valid key handle provided");
}
let flags = SignFlags::empty();
let (sign_tx, sign_rx) = channel();
let callback = StateCallback::new(Box::new(move |rv| {
sign_tx.send(rv).unwrap();
}));
let u2fv2_challenge = U2fV2Challenge::new_random(app_id);
let u2fv2_challenge_str = u2fv2_challenge.to_json();
let chall_bytes = digest::sha256(&u2fv2_challenge_str);
let app_bytes = digest::sha256(app_id);
let status_tx = fido::start_status_updater();
information!("App id: {}, Start sign...", app_id);
debugging!("Wait timeout: {} ms", timeout_ms);
let mut manager = opt_result!(AuthenticatorService::new(), "Create authenticator service failed: {}");
manager.add_u2f_usb_hid_platform_transports();
if let Err(e) = manager.sign(
flags,
timeout_ms,
chall_bytes,
vec![app_bytes],
request_key_handles,
status_tx,
callback,
) {
return simple_error!("Couldn't sign: {:?}", e);
}
let sign_result = opt_result!(sign_rx.recv(), "Problem receiving, unable to continue: {}");
let (_, handle_used, sign_data, device_info) = opt_result!(sign_result, "Sign failed: {}");
let user_presence_flag = &sign_data[0];
let counter = &sign_data[1..=4];
let signature = &sign_data[5..];
success!("Device info: {}", &device_info);
success!("Sign challenge: {}", u2fv2_challenge_str);
success!("Sign challenge base64: {}", base64::encode(&u2fv2_challenge_str));
success!("Sign result : {}", base64::encode(&sign_data));
success!("- presence : {}", user_presence_flag);
success!("- counter : {}", u32::from_be_bytes([counter[0], counter[1], counter[2], counter[3]]));
success!("- signature: {}", base64::encode(&signature));
// success!("Key handle used: {}", base64::encode(&handle_used));
success!("Key handle: {}", hex::encode(&handle_used));
if let Some(public_key_hex) = sub_arg_matches.value_of("public-key-hex") {
let public_key = opt_result!(hex::decode(public_key_hex), "Parse public key hex failed: {}");
let client_data = u2fv2_challenge_str.as_bytes().to_vec();
let app_id_hash = sha256(app_id.as_bytes());
let client_data_hash = sha256(&client_data[..]);
let mut msg = Vec::with_capacity(128);
msg.extend_from_slice(&app_id_hash);
msg.push(*user_presence_flag);
msg.extend_from_slice(counter);
msg.extend_from_slice(&client_data_hash);
information!("Public key: {}", base64::encode(&public_key));
information!("Signed message: {}", base64::encode(&msg));
let authorization_result = u2f::authorization::parse_sign_response(
app_id.to_string(),
client_data,
public_key,
sign_data,
);
let authorization = opt_result!(authorization_result, "Parse authorization failed: {}");
success!("Parse authorization success, counter: {}", authorization.counter);
}
Ok(None)
}
}

44
src/cmd_yubikey.rs Normal file
View File

@@ -0,0 +1,44 @@
use crate::util;
use clap::{App, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use serde_json::Value;
use std::collections::BTreeMap;
use yubikey::Context;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"yubikey"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name()).about("Yubikey subcommand")
}
fn run(&self, _arg_matches: &ArgMatches, _sub_arg_matches: &ArgMatches) -> CommandError {
util_msg::set_logger_std_out(false);
let mut list = vec![];
let mut readers = Context::open()?;
for reader in readers.iter()? {
let yubikey = match reader.open() {
Ok(yk) => yk,
Err(e) => {
warning!("Error opening YubiKey: {}", e);
continue;
}
};
let mut key = BTreeMap::new();
key.insert("serial", Value::Number(yubikey.serial().0.into()));
key.insert("version", yubikey.version().to_string().into());
key.insert("name", yubikey.name().into());
list.push(key);
}
util::print_pretty_json(&list);
Ok(None)
}
}

82
src/cmdutil.rs Normal file
View File

@@ -0,0 +1,82 @@
use clap::{Arg, ArgMatches};
use rust_util::util_msg;
pub fn build_slot_arg() -> Arg<'static, 'static> {
Arg::with_name("slot")
.short("s")
.long("slot")
.takes_value(true)
.help("PIV slot, e.g. 82, 83 ... 95, 9a, 9c, 9d, 9e")
}
pub fn build_with_hmac_encrypt_arg() -> Arg<'static, 'static> {
Arg::with_name("with-hmac-encrypt").long("with-hmac-encrypt").help("With HMAC encrypt")
}
pub fn build_with_pbe_encrypt_arg() -> Arg<'static, 'static> {
Arg::with_name("with-pbe-encrypt").long("with-pbe-encrypt").help("With PBE encryption")
}
pub fn build_double_pin_check_arg() -> Arg<'static, 'static> {
Arg::with_name("double-pin-check").long("double-pin-check").help("Double PIN check")
}
pub fn build_pbe_iteration_arg() -> Arg<'static, 'static> {
Arg::with_name("pbe-iteration").long("pbe-iteration").takes_value(true).help("PBE iteration, default 100000")
}
pub fn build_serial_arg() -> Arg<'static, 'static> {
Arg::with_name("serial").long("serial").takes_value(true).help("Serial number")
}
pub fn build_key_uri_arg() -> Arg<'static, 'static> {
Arg::with_name("key").long("key").required(true).takes_value(true).help("Key uri")
}
pub fn build_pin_arg() -> Arg<'static, 'static> {
Arg::with_name("pin").short("p").long("pin").takes_value(true).help("PIV card user PIN")
}
pub fn build_alg_arg() -> Arg<'static, 'static> {
Arg::with_name("alg").long("alg").takes_value(true).required(true).help("Algorithm, e.g. RS256, ES256, ES384")
}
pub fn build_parameter_arg() -> Arg<'static, 'static> {
Arg::with_name("parameter").long("parameter").takes_value(true).required(true).help("Parameter")
}
pub fn build_epk_arg() -> Arg<'static, 'static> {
Arg::with_name("epk").long("epk").required(true).takes_value(true).help("E-Public key")
}
pub fn build_message_arg() -> Arg<'static, 'static> {
Arg::with_name("message-base64").long("message-base64").takes_value(true).required(true).help("Message in base64")
}
pub fn build_message_type_arg() -> Arg<'static, 'static> {
Arg::with_name("message-type").long("message-type").takes_value(true).help("Optional, message type, raw, sha256, sha384 or sha512")
}
pub fn build_no_pin_arg() -> Arg<'static, 'static> {
Arg::with_name("no-pin").long("no-pin").help("No PIN")
}
pub fn build_keychain_name_arg() -> Arg<'static, 'static> {
Arg::with_name("keychain-name")
.long("keychain-name")
.takes_value(true)
.help("Key chain name")
}
pub fn build_json_arg() -> Arg<'static, 'static> {
Arg::with_name("json").long("json").help("JSON output")
}
pub fn check_json_output(sub_arg_matches: &ArgMatches) -> bool {
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
json_output
}

View File

@@ -1,38 +0,0 @@
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha384, Sha512};
// pub fn sha1(input: &str) -> Vec<u8> {
// let mut challenge = Sha1::default();
// challenge.update(input.as_bytes());
// challenge.digest().bytes().to_vec()
// }
pub fn sha256(input: &str) -> Vec<u8> {
let mut challenge = Sha256::default();
Digest::update(&mut challenge, input.as_bytes());
challenge.finalize().to_vec()
}
pub fn sha1_bytes(input: &[u8]) -> Vec<u8> {
let mut challenge = Sha1::default();
challenge.update(input);
challenge.digest().bytes().to_vec()
}
pub fn sha256_bytes(input: &[u8]) -> Vec<u8> {
let mut challenge = Sha256::default();
Digest::update(&mut challenge, input);
challenge.finalize().to_vec()
}
pub fn sha384_bytes(input: &[u8]) -> Vec<u8> {
let mut challenge = Sha384::default();
Digest::update(&mut challenge, input);
challenge.finalize().to_vec()
}
pub fn sha512_bytes(input: &[u8]) -> Vec<u8> {
let mut challenge = Sha512::default();
Digest::update(&mut challenge, input);
challenge.finalize().to_vec()
}

70
src/digestutil.rs Normal file
View File

@@ -0,0 +1,70 @@
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha384, Sha512};
pub enum DigestAlgorithm {
Sha256,
#[allow(dead_code)]
Sha384,
}
impl DigestAlgorithm {
pub fn digest(&self, data: &[u8]) -> Vec<u8> {
match self {
DigestAlgorithm::Sha256 => sha256_bytes(data),
DigestAlgorithm::Sha384 => sha384_bytes(data),
}
}
pub fn digest_str(&self, s: &str) -> Vec<u8> {
self.digest(s.as_bytes())
}
}
pub fn sha256(input: &str) -> Vec<u8> {
sha256_bytes(input.as_bytes())
}
pub fn sha1_bytes(input: &[u8]) -> Vec<u8> {
let mut digest = Sha1::default();
digest.update(input);
digest.finalize().to_vec()
}
pub fn sha256_bytes(input: &[u8]) -> Vec<u8> {
digest_bytes::<Sha256>(input)
}
pub fn sha384_bytes(input: &[u8]) -> Vec<u8> {
digest_bytes::<Sha384>(input)
}
pub fn sha512_bytes(input: &[u8]) -> Vec<u8> {
digest_bytes::<Sha512>(input)
}
pub fn digest_bytes<D>(input: &[u8]) -> Vec<u8> where D: Digest + Default {
let mut digest: D = Default::default();
Digest::update(&mut digest, input);
digest.finalize().to_vec()
}
macro_rules! define_copy_array {
($fn_name: ident, $len: tt) => (
pub fn $fn_name(in_arr: &[u8]) -> rust_util::XResult<[u8; $len]> {
if in_arr.len() != $len {
return simple_error!("Array length is not: {}, but is: {}", $len, in_arr.len());
}
let mut out_arr = [0_u8; $len];
for i in 0..$len {
out_arr[i] = in_arr[i];
}
Ok(out_arr)
}
)
}
define_copy_array!(copy_sha256, 0x20);
define_copy_array!(copy_sha384, 0x30);
define_copy_array!(copy_sha512, 0x40);
// define_copy_array!(copy_rsa2048, 0x100);

63
src/ecdhutil.rs Normal file
View File

@@ -0,0 +1,63 @@
macro_rules! piv_ecdh {
($p_algo: tt, $public_key_pem_opt: expr, $sub_arg_matches: expr, $json: expr, $json_output: expr) => ({
use $p_algo::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
use $p_algo::{EncodedPoint, PublicKey, ecdh::EphemeralSecret};
let public_key;
if let Some(public_key_pem) = $public_key_pem_opt {
public_key = opt_result!(public_key_pem.parse::<PublicKey>(), "Parse public key failed: {}");
} else {
let public_key_point_hex = $sub_arg_matches.value_of("public-key-point-hex").unwrap_or_else(||
failure_and_exit!("--public-key, --public-key-file or --public-key-point-hex must require one"));
let public_key_point_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse public key point hex failed: {}");
let encoded_point = opt_result!(EncodedPoint::from_bytes(public_key_point_bytes), "Parse public key point failed: {}");
public_key = PublicKey::from_encoded_point(&encoded_point).unwrap();
};
let public_key_encoded_point = public_key.to_encoded_point(false);
let esk = EphemeralSecret::random(&mut OsRng);
let epk = esk.public_key();
let epk_compress_point = epk.to_encoded_point(true);
let epk_point = PublicKey::from_encoded_point(&epk_compress_point).unwrap();
let epk_uncompressed_point = epk_point.to_encoded_point(false);
let shared_secret = esk.diffie_hellman(&public_key);
if $json_output {
$json.insert("shared_secret_hex", hex::encode(shared_secret.raw_secret_bytes()));
$json.insert("epk_point_hex", hex::encode(epk_uncompressed_point.as_bytes()));
$json.insert("pk_point_hex", hex::encode(public_key_encoded_point.as_bytes()));
} else {
information!("Shared secret: {}", hex::encode(shared_secret.raw_secret_bytes()));
information!("EPK point: {}", hex::encode(epk_uncompressed_point.as_bytes()));
information!("Public key point: {}", hex::encode(public_key_encoded_point.as_bytes()));
}
})
}
macro_rules! parse_private_and_ecdh {
($algo: tt, $private_key_bytes: tt, $ephemeral_public_key_bytes: tt) => ({
use $algo::{SecretKey, PublicKey, ecdh::diffie_hellman, pkcs8::DecodePrivateKey};
use spki::DecodePublicKey;
let secret_key= SecretKey::from_pkcs8_der($private_key_bytes)?;
let public_key = opt_result!(PublicKey::from_public_key_der(
$ephemeral_public_key_bytes),"Parse ephemeral public key failed: {}");
let shared_secret = diffie_hellman(secret_key.to_nonzero_scalar(), public_key.as_affine());
Ok(shared_secret.raw_secret_bytes().to_vec())
})
}
pub fn parse_p256_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
parse_private_and_ecdh!(p256, private_key_bytes, ephemeral_public_key_bytes)
}
pub fn parse_p384_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
parse_private_and_ecdh!(p384, private_key_bytes, ephemeral_public_key_bytes)
}
pub fn parse_p521_private_and_ecdh(private_key_bytes: &[u8], ephemeral_public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
parse_private_and_ecdh!(p521, private_key_bytes, ephemeral_public_key_bytes)
}
use rust_util::XResult;
pub(crate) use piv_ecdh;

238
src/ecdsautil.rs Normal file
View File

@@ -0,0 +1,238 @@
use der_parser::ber::BerObjectContent;
use ecdsa::elliptic_curve::pkcs8::LineEnding;
use ecdsa::VerifyingKey;
use p256::NistP256;
use p256::ecdsa::signature::hazmat::PrehashVerifier;
use p384::NistP384;
use p256::pkcs8::EncodePrivateKey;
use p521::NistP521;
use rust_util::XResult;
use spki::EncodePublicKey;
use crate::util::{base64_encode, try_decode};
#[derive(Copy, Clone)]
pub enum EcdsaAlgorithm {
P256,
P384,
P521,
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum EcdsaSignType {
Der, Rs,
}
pub fn parse_ecdsa_to_rs(signature_der: &[u8]) -> XResult<Vec<u8>> {
let (mut r, s)= parse_ecdsa_r_and_s(signature_der)?;
r.extend_from_slice(&s);
Ok(r)
}
pub fn parse_ecdsa_r_and_s(signature_der: &[u8]) -> XResult<(Vec<u8>, Vec<u8>)> {
let vec_r: Vec<u8>;
let vec_s: Vec<u8>;
let (_, parsed_signature) = opt_result!(der_parser::parse_der(signature_der), "Parse signature failed: {}");
match parsed_signature.content {
BerObjectContent::Sequence(seq) => {
match &seq[0].content {
BerObjectContent::Integer(r) => {
debugging!("Signature r: {}", hex::encode(r));
vec_r = trim_ecdsa_point_coord(r);
}
_ => return simple_error!("Parse signature failed: [0]not integer"),
}
match &seq[1].content {
BerObjectContent::Integer(s) => {
debugging!("Signature s: {}", hex::encode(s));
vec_s = trim_ecdsa_point_coord(s);
}
_ => return simple_error!("Parse signature failed: [1]not integer"),
}
}
_ => return simple_error!("Parse signature failed: not sequence"),
}
Ok((vec_r, vec_s))
}
const P256_LEN: usize = 32;
const P384_LEN: usize = 48;
const P521_LEN: usize = 66;
fn trim_ecdsa_point_coord(p: &[u8]) -> Vec<u8> {
if p.len() == (P256_LEN + 1) || p.len() == (P384_LEN + 1) || p.len() == (P521_LEN + 1) {
p[1..].to_vec()
} else if p.len() == (P256_LEN - 1) || p.len() == (P384_LEN - 1) || p.len() == (P521_LEN - 1) {
let mut v = vec![];
v.push(0_u8);
v.extend_from_slice(p);
v
} else {
p.to_vec()
}
}
macro_rules! generate_inner_ecdsa_keypair {
($algo: tt) => ({
use $algo::SecretKey;
let secret_key = SecretKey::random(&mut rand::thread_rng());
let secret_key_der_base64 = base64_encode(secret_key.to_pkcs8_der()?.as_bytes());
let secret_key_pem = secret_key.to_pkcs8_pem(LineEnding::LF)?.to_string();
let public_key_pem = secret_key.public_key().to_public_key_pem(LineEnding::LF)?;
let public_key_der = secret_key.public_key().to_public_key_der()?.to_vec();
let jwk_ec_key = secret_key.public_key().to_jwk().to_string();
Ok((secret_key_der_base64, secret_key_pem, public_key_pem, public_key_der, jwk_ec_key))
})
}
pub fn generate_ecdsa_keypair(algo: EcdsaAlgorithm) -> XResult<(String, String, String, Vec<u8>, String)> {
match algo {
EcdsaAlgorithm::P256 => generate_inner_ecdsa_keypair!(p256),
EcdsaAlgorithm::P384 => generate_inner_ecdsa_keypair!(p384),
EcdsaAlgorithm::P521 => generate_inner_ecdsa_keypair!(p521),
}
}
pub fn parse_ec_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
match parse_p521_public_key_to_point(public_key_bytes) {
Ok(point) => Ok(point),
Err(_) => match parse_p384_public_key_to_point(public_key_bytes) {
Ok(point) => Ok(point),
Err(_) => parse_p256_public_key_to_point(public_key_bytes),
}
}
}
pub fn parse_p256_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
use p256::{PublicKey, elliptic_curve::sec1::ToEncodedPoint};
use spki::DecodePublicKey;
let public_key = PublicKey::from_public_key_der(public_key_bytes)?;
Ok(public_key.to_encoded_point(false).as_bytes().to_vec())
}
pub fn parse_p384_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
use p384::{PublicKey, elliptic_curve::sec1::ToEncodedPoint};
use spki::DecodePublicKey;
let public_key = PublicKey::from_public_key_der(public_key_bytes)?;
Ok(public_key.to_encoded_point(false).as_bytes().to_vec())
}
pub fn parse_p521_public_key_to_point(public_key_bytes: &[u8]) -> XResult<Vec<u8>> {
use p521::{PublicKey, elliptic_curve::sec1::ToEncodedPoint};
use spki::DecodePublicKey;
let public_key = PublicKey::from_public_key_der(public_key_bytes)?;
Ok(public_key.to_encoded_point(false).as_bytes().to_vec())
}
macro_rules! parse_ecdsa_private_key_to_public_key {
($algo: tt, $parse_ecdsa_private_key: tt) => ({
use $algo::{SecretKey, pkcs8::DecodePrivateKey};
let secret_key = match SecretKey::from_pkcs8_pem($parse_ecdsa_private_key) {
Ok(secret_key) => secret_key,
Err(_) => match try_decode($parse_ecdsa_private_key) {
Ok(private_key_der) => match SecretKey::from_pkcs8_der(&private_key_der) {
Ok(secret_key) => secret_key,
Err(e) => return simple_error!("Invalid PKCS#8 private key {}, error: {}", $parse_ecdsa_private_key, e),
}
Err(_) => return simple_error!("Invalid PKCS#8 private key: {}", $parse_ecdsa_private_key),
}
};
let public_key_document = opt_result!(secret_key.public_key().to_public_key_der(), "Conver to public key failed: {}");
Ok(public_key_document.to_vec())
})
}
pub fn parse_p256_private_key_to_public_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key_to_public_key!(p256, private_key_pkcs8)
}
pub fn parse_p384_private_key_to_public_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key_to_public_key!(p384, private_key_pkcs8)
}
pub fn parse_p521_private_key_to_public_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key_to_public_key!(p521, private_key_pkcs8)
}
macro_rules! parse_ecdsa_private_key {
($algo: tt, $parse_ecdsa_private_key: tt) => ({
use $algo::{SecretKey, pkcs8::DecodePrivateKey};
let secret_key = match SecretKey::from_pkcs8_pem($parse_ecdsa_private_key) {
Ok(secret_key) => secret_key,
Err(_) => match try_decode($parse_ecdsa_private_key) {
Ok(private_key_der) => match SecretKey::from_pkcs8_der(&private_key_der) {
Ok(secret_key) => secret_key,
Err(e) => return simple_error!("Invalid PKCS#8 private key {}, error: {}", $parse_ecdsa_private_key, e),
}
Err(_) => return simple_error!("Invalid PKCS#8 private key: {}", $parse_ecdsa_private_key),
}
};
Ok(secret_key.to_bytes().to_vec())
})
}
pub fn parse_p256_private_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key!(p256, private_key_pkcs8)
}
pub fn parse_p384_private_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key!(p384, private_key_pkcs8)
}
pub fn parse_p521_private_key(private_key_pkcs8: &str) -> XResult<Vec<u8>> {
parse_ecdsa_private_key!(p521, private_key_pkcs8)
}
macro_rules! sign_ecdsa_rs_or_der {
($algo: tt, $private_key_d: tt, $pre_hash: tt, $is_rs: tt) => ({
use $algo::ecdsa::{SigningKey, Signature, signature::hazmat::PrehashSigner};
let signing_key = SigningKey::from_slice($private_key_d)?;
let signature: Signature = signing_key.sign_prehash($pre_hash)?;
if $is_rs {
Ok(signature.to_bytes().to_vec())
} else {
Ok(signature.to_der().as_bytes().to_vec())
}
})
}
pub fn ecdsa_sign(algo: EcdsaAlgorithm, private_key_d: &[u8], pre_hash: &[u8], sign_type: EcdsaSignType) -> XResult<Vec<u8>> {
let is_rs = sign_type == EcdsaSignType::Rs;
match algo {
EcdsaAlgorithm::P256 => sign_ecdsa_rs_or_der!(p256, private_key_d, pre_hash, is_rs),
EcdsaAlgorithm::P384 => sign_ecdsa_rs_or_der!(p384, private_key_d, pre_hash, is_rs),
EcdsaAlgorithm::P521 => sign_ecdsa_rs_or_der!(p521, private_key_d, pre_hash, is_rs),
}
}
macro_rules! ecdsa_verify_signature {
($algo: tt, $pk_point: tt, $prehash: tt, $signature: tt) => ({
use ecdsa::Signature;
let verifying_key: VerifyingKey<$algo> = opt_result!(VerifyingKey::<$algo>::from_sec1_bytes($pk_point), "Parse public key failed: {}");
let sign = if let Ok(signature) = Signature::from_der($signature) {
signature
} else if let Ok(signature) = Signature::from_slice($signature) {
signature
} else {
return simple_error!("Parse signature failed: {}", hex::encode($signature));
};
opt_result!(verifying_key.verify_prehash($prehash, &sign), "Verify signature failed: {}");
})
}
pub fn ecdsa_verify(algo: EcdsaAlgorithm, pk_point: &[u8], prehash: &[u8], signature: &[u8]) -> XResult<()> {
match algo {
EcdsaAlgorithm::P256 => ecdsa_verify_signature!(NistP256, pk_point, prehash, signature),
EcdsaAlgorithm::P384 => ecdsa_verify_signature!(NistP384, pk_point, prehash, signature),
EcdsaAlgorithm::P521 => ecdsa_verify_signature!(NistP521, pk_point, prehash, signature),
}
Ok(())
}

66
src/ecutil.rs Normal file
View File

@@ -0,0 +1,66 @@
use crate::util::base64_decode;
use p256::pkcs8::LineEnding;
use rust_util::XResult;
use spki::{DecodePublicKey, EncodePublicKey};
pub fn convert_ec_public_key_to_jwk(public_key: &str) -> XResult<String> {
if let Ok(jwk) = convert_ec_public_key_p256_to_jwk(public_key) {
return Ok(jwk);
}
if let Ok(jwk) = convert_ec_public_key_p384_to_jwk(public_key) {
return Ok(jwk);
}
simple_error!("Parse public key failed, MUST be pem or base64 encoded DER.")
}
pub fn convert_ec_public_key_p256_to_jwk(public_key: &str) -> XResult<String> {
let public_key_p256 = if public_key.contains("BEGIN PUBLIC KEY") {
debugging!("Try parse P256 public key PEM.");
p256::PublicKey::from_public_key_pem(public_key)?
} else {
debugging!("Try parse P256 public key DER.");
let der = base64_decode(public_key)?;
p256::PublicKey::from_public_key_der(&der)?
};
Ok(public_key_p256.to_jwk_string())
}
pub fn convert_ec_public_key_p384_to_jwk(public_key: &str) -> XResult<String> {
let public_key_p384 = if public_key.contains("BEGIN PUBLIC KEY") {
debugging!("Try parse P384 public key PEM.");
p384::PublicKey::from_public_key_pem(public_key)?
} else {
debugging!("Try parse P384 public key DER.");
let der = base64_decode(public_key)?;
p384::PublicKey::from_public_key_der(&der)?
};
Ok(public_key_p384.to_jwk_string())
}
pub fn convert_ec_jwk_to_public_key(jwk: &str) -> XResult<(String, Vec<u8>)> {
if let Ok(public_key) = convert_ec_jwk_p256_to_public_key(jwk) {
return Ok(public_key);
}
if let Ok(public_key) = convert_ec_jwk_p384_to_public_key(jwk) {
return Ok(public_key);
}
simple_error!("Parse JWK failed, MUST be P256 or P384.")
}
pub fn convert_ec_jwk_p256_to_public_key(jwk: &str) -> XResult<(String, Vec<u8>)> {
debugging!("Try parse P256 JWK.");
let public_key = p256::PublicKey::from_jwk_str(jwk)?;
Ok((
public_key.to_public_key_pem(LineEnding::LF)?,
public_key.to_public_key_der()?.to_vec(),
))
}
pub fn convert_ec_jwk_p384_to_public_key(jwk: &str) -> XResult<(String, Vec<u8>)> {
debugging!("Try parse P384 JWK.");
let public_key = p384::PublicKey::from_jwk_str(jwk)?;
Ok((
public_key.to_public_key_pem(LineEnding::LF)?,
public_key.to_public_key_der()?.to_vec(),
))
}

View File

@@ -1,13 +1,16 @@
use std::fmt; use std::fmt;
use std::sync::mpsc::{channel, Sender}; use std::sync::mpsc::{channel, Sender};
use std::thread; use std::thread;
use std::time::SystemTime;
use authenticator::{RegisterResult, StatusUpdate}; use authenticator::{RegisterResult, StatusUpdate};
use base64::URL_SAFE_NO_PAD;
use rand::Rng; use rand::Rng;
use rust_util::XResult; use rust_util::XResult;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::pkiutil::bytes_to_pem;
use crate::util::{base64_encode, base64_encode_url_safe_no_pad};
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct U2FDeviceInfo { pub struct U2FDeviceInfo {
pub vendor_name: String, pub vendor_name: String,
@@ -57,27 +60,29 @@ pub struct U2fRegistrationData {
pub device_name: Option<String>, pub device_name: Option<String>,
pub client_data: String, pub client_data: String,
pub registration_data: String, pub registration_data: String,
pub attestation_cert: Option<Vec<u8>>,
pub attestation_cert_pem: Option<String>, pub attestation_cert_pem: Option<String>,
pub pub_key: String, pub pub_key: Vec<u8>,
pub key_handle: String, pub key_handle: Vec<u8>,
} }
impl U2fRegistrationData { impl U2fRegistrationData {
pub fn from(app_id: &str, client_data: &str, register_result: RegisterResult) -> XResult<Self> { pub fn from(app_id: &str, client_data: &str, register_result: &RegisterResult) -> XResult<Self> {
let registration = opt_result!( let registration = opt_result!(
u2f::register::parse_registration(app_id.to_string(), client_data.as_bytes().to_vec(), register_result.0.to_vec()), u2f::register::parse_registration(app_id.to_string(), client_data.as_bytes().to_vec(), register_result.0.to_vec()),
"Parse registration data failed: {}"); "Parse registration data failed: {}");
Ok(Self { Ok(Self {
app_id: app_id.to_string(), app_id: app_id.to_string(),
device_info: U2FDeviceInfo::from(&register_result), device_info: U2FDeviceInfo::from(register_result),
device_name: registration.device_name, device_name: registration.device_name,
client_data: client_data.into(), client_data: client_data.into(),
registration_data: base64::encode(&register_result.0), registration_data: base64_encode(&register_result.0),
attestation_cert_pem: registration.attestation_cert.map(|c| { attestation_cert: registration.attestation_cert.clone(),
to_pem(&c, "CERTIFICATE", 64) attestation_cert_pem: registration.attestation_cert.map(|cert| {
bytes_to_pem("CERTIFICATE", cert)
}), }),
pub_key: hex::encode(registration.pub_key), pub_key: registration.pub_key,
key_handle: hex::encode(registration.key_handle), key_handle: registration.key_handle,
}) })
} }
} }
@@ -91,15 +96,31 @@ pub struct U2fV2Challenge {
} }
impl U2fV2Challenge { impl U2fV2Challenge {
pub fn new_random<S>(app_id: S) -> Self where S: Into<String> { pub fn new_challenge(challenge_hex: Option<&str>, app_id: &str, with_time_stamp_prefix: bool) -> XResult<U2fV2Challenge> {
Ok(match challenge_hex {
None => U2fV2Challenge::new_random(app_id, with_time_stamp_prefix),
Some(challenge_hex) => {
let challenge_bytes = opt_result!(hex::decode(challenge_hex), "Decode challenge hex failed: {}");
let challenge = base64_encode_url_safe_no_pad(challenge_bytes);
U2fV2Challenge::new(challenge, app_id)
}
})
}
pub fn new_random<S>(app_id: S, with_time_stamp_prefix: bool) -> Self where S: Into<String> {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let mut rand_bytes = [0_u8; 32]; let mut rand_bytes = [0_u8; 32];
for c in &mut rand_bytes { for c in &mut rand_bytes {
let b: u8 = rng.gen(); let b: u8 = rng.gen();
*c = b; *c = b;
} }
if with_time_stamp_prefix {
let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as u64;
let timestamp_be_bytes = timestamp.to_be_bytes();
rand_bytes[..8].clone_from_slice(&timestamp_be_bytes[..8]);
}
let challenge = base64::encode_config(&rand_bytes, URL_SAFE_NO_PAD); let challenge = base64_encode_url_safe_no_pad(rand_bytes);
Self::new(challenge, app_id) Self::new(challenge, app_id)
} }
@@ -138,29 +159,6 @@ pub fn start_status_updater() -> Sender<StatusUpdate> {
status_tx status_tx
} }
pub fn to_pem(bs: &[u8], sub: &str, w: usize) -> String {
let mut s = String::with_capacity(bs.len() * 2);
s.push_str(&format!("-----BEGIN {}-----", sub));
let b64 = base64::encode(bs).chars().collect::<Vec<char>>();
let mut b64 = b64.as_slice();
while !b64.is_empty() {
s.push('\n');
if b64.len() >= w {
for c in b64.iter().take(w) {
s.push(*c);
}
b64 = &b64[w..];
} else {
for c in b64 {
s.push(*c);
}
b64 = &[];
}
}
s.push_str(&format!("\n-----END {}-----", sub));
s
}
pub fn to_hex(data: &[u8], joiner: &str) -> String { pub fn to_hex(data: &[u8], joiner: &str) -> String {
let parts: Vec<String> = data.iter().map(|byte| format!("{:02x}", byte)).collect(); let parts: Vec<String> = data.iter().map(|byte| format!("{:02x}", byte)).collect();
parts.join(joiner) parts.join(joiner)

157
src/hmacutil.rs Normal file
View File

@@ -0,0 +1,157 @@
use clap::ArgMatches;
use rust_util::XResult;
use std::collections::BTreeMap;
use std::ops::Deref;
use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor};
use rand::random;
use yubico_manager::config::{Config, Mode, Slot};
use yubico_manager::hmacmode::HmacKey;
use yubico_manager::sec::hmac_sha1;
use yubico_manager::Yubico;
use crate::digestutil::{copy_sha256, sha256_bytes};
use crate::util;
use crate::util::{base64_decode, base64_encode, base64_encode_url_safe_no_pad, base64_uri_decode};
const HMAC_ENC_PREFIX: &str = "hmac_enc:";
// hmac encrypt, format: hmac_enc:<HMAC-NONCE>:<AES-GCM-NONCE>:<ENCRYPTED>
pub fn hmac_encrypt_from_string(plaintext: &str) -> XResult<String> {
hmac_encrypt(plaintext.as_bytes())
}
pub fn hmac_encrypt(plaintext: &[u8]) -> XResult<String> {
let hmac_nonce: [u8; 16] = random();
let aes_gcm_nonce: [u8; 16] = random();
let hmac_key = compute_yubikey_hmac(&hmac_nonce)?;
let key = copy_sha256(&sha256_bytes(&hmac_key))?;
let mut encryptor = Aes256GcmStreamEncryptor::new(key, &aes_gcm_nonce);
let mut ciphertext = encryptor.update(plaintext);
let (final_part, tag) = encryptor.finalize();
ciphertext.extend_from_slice(&final_part);
ciphertext.extend_from_slice(&tag);
Ok(format!("{}{}:{}:{}",
HMAC_ENC_PREFIX,
base64_encode_url_safe_no_pad(hmac_nonce),
base64_encode_url_safe_no_pad(aes_gcm_nonce),
base64_encode(&ciphertext)
))
}
pub fn is_hmac_encrypted(ciphertext: &str) -> bool {
ciphertext.starts_with(HMAC_ENC_PREFIX)
}
pub fn hmac_decrypt_to_string(ciphertext: &str) -> XResult<String> {
let plaintext = hmac_decrypt(ciphertext)?;
Ok(String::from_utf8(plaintext)?)
}
pub fn hmac_decrypt(ciphertext: &str) -> XResult<Vec<u8>> {
if !is_hmac_encrypted(ciphertext) {
return simple_error!("Invalid ciphertext: {}", ciphertext);
}
let parts = ciphertext.split(":").collect::<Vec<_>>();
let hmac_nonce = try_decode_hmac_val(parts[1])?;
let aes_gcm_nonce = try_decode_hmac_val(parts[2])?;
let ciphertext = base64_decode(parts[3])?;
let hmac_key = compute_yubikey_hmac(&hmac_nonce)?;
let key = copy_sha256(&sha256_bytes(&hmac_key))?;
let mut decryptor = Aes256GcmStreamDecryptor::new(key, &aes_gcm_nonce);
let mut plaintext = decryptor.update(&ciphertext);
let final_part = decryptor.finalize()?;
plaintext.extend_from_slice(&final_part);
Ok(plaintext)
}
pub fn try_decode_hmac_val(s: &str) -> XResult<Vec<u8>> {
match hex::decode(s) {
Ok(v) => Ok(v),
Err(e) => match base64_uri_decode(s) {
Ok(v) => Ok(v),
Err(_) => simple_error!("Try decode failed: {}", e)
}
}
}
pub fn compute_yubikey_hmac(challenge_bytes: &[u8]) -> XResult<Vec<u8>> {
let mut yubi = Yubico::new();
let device = match yubi.find_yubikey() {
Ok(device) => device,
Err(_) => {
return simple_error!("YubiKey not found");
}
};
debugging!("Found key, Vendor ID: {:?}, Product ID: {:?}", device.vendor_id, device.product_id);
let config = Config::default()
.set_vendor_id(device.vendor_id)
.set_product_id(device.product_id)
.set_variable_size(true)
.set_mode(Mode::Sha1)
.set_slot(Slot::Slot2);
let hmac_result = opt_result!(yubi.challenge_response_hmac(challenge_bytes, config), "Challenge HMAC failed: {}");
Ok(hmac_result.deref().to_vec())
}
pub fn get_challenge_bytes(sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> {
let challenge_bytes: Vec<u8> = if let Some(challenge) = sub_arg_matches.value_of("challenge") {
challenge.as_bytes().to_vec()
} else if let Some(challenge_hex) = sub_arg_matches.value_of("challenge-hex") {
opt_result!(hex::decode(challenge_hex), "Decode challenge hex: {}, failed: {}", challenge_hex)
} else {
return simple_error!("Challenge must assigned");
};
if challenge_bytes.len() > 64 {
return simple_error!("Challenge bytes is: {}, more than 64", challenge_bytes.len());
}
Ok(challenge_bytes)
}
pub fn calculate_hmac_sha1_result(secret_bytes: &[u8], challenge_bytes: &[u8], variable: bool) -> [u8; 20] {
let hmac_key = HmacKey::from_slice(secret_bytes);
let mut challenge = [0; 64];
if variable && challenge_bytes.last() == Some(&0) {
challenge = [0xff; 64];
}
challenge[..challenge_bytes.len()].copy_from_slice(challenge_bytes);
hmac_sha1(&hmac_key, &challenge)
}
pub fn output_hmac_result(sub_arg_matches: &ArgMatches, json_output: bool, challenge_bytes: Vec<u8>, result: &[u8]) {
let sha1_output = sub_arg_matches.is_present("sha1");
let sha256_output = sub_arg_matches.is_present("sha256");
let sha384_output = sub_arg_matches.is_present("sha384");
let sha512_output = sub_arg_matches.is_present("sha512");
let hex_string = hex::encode(result);
let hex_sha1 = iff!(sha1_output, Some(crate::digestutil::sha1_bytes(result)), None);
let hex_sha256 = iff!(sha256_output, Some(crate::digestutil::sha256_bytes(result)), None);
let hex_sha384 = iff!(sha384_output, Some(crate::digestutil::sha384_bytes(result)), None);
let hex_sha512 = iff!(sha512_output, Some(crate::digestutil::sha512_bytes(result)), None);
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("challenge_hex", hex::encode(challenge_bytes));
json.insert("response_hex", hex_string);
hex_sha1.map(|hex_sha1| json.insert("response_sha1_hex", hex::encode(hex_sha1)));
hex_sha256.map(|hex_sha256| json.insert("response_sha256_hex", hex::encode(hex_sha256)));
hex_sha384.map(|hex_sha384| json.insert("response_sha384_hex", hex::encode(hex_sha384)));
hex_sha512.map(|hex_sha512| json.insert("response_sha512_hex", hex::encode(hex_sha512)));
util::print_pretty_json(&json);
} else {
success!("Challenge HEX: {}", hex::encode(challenge_bytes));
success!("Response HEX: {}", hex_string);
if let Some(hex_sha256) = hex_sha256 { success!("Response SHA256 HEX: {}", hex::encode(hex_sha256)); }
if let Some(hex_sha384) = hex_sha384 { success!("Response SHA384 HEX: {}", hex::encode(hex_sha384)); }
if let Some(hex_sha512) = hex_sha512 { success!("Response SHA512 HEX: {}", hex::encode(hex_sha512)); }
}
}

168
src/keychain.rs Normal file
View File

@@ -0,0 +1,168 @@
use rust_util::{util_file, XResult};
use security_framework::os::macos::keychain::{CreateOptions, SecKeychain};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
const KEYCHAIN_KEY_PREFIX: &str = "keychain:";
const DEFAULT_SERVICE_NAME: &str = "card-cli";
pub struct KeychainKey {
pub keychain_name: String,
pub service_name: String,
pub key_name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KeychainKeyValue {
pub keychain_name: String,
pub pkcs8_base64: String,
pub secret_key_pem: String,
pub public_key_pem: String,
pub public_key_jwk: String,
}
pub fn is_keychain_key_uri(name: &str) -> bool {
name.starts_with(KEYCHAIN_KEY_PREFIX)
}
impl KeychainKey {
pub fn from_key_name_default(key_name: &str) -> Self {
Self::from("", DEFAULT_SERVICE_NAME, key_name)
}
pub fn from(keychain_name: &str, service_name: &str, key_name: &str) -> Self {
debugging!(
"Keychain key: {} - {} - {}",
keychain_name,
service_name,
key_name
);
Self {
keychain_name: keychain_name.to_string(),
service_name: service_name.to_string(),
key_name: key_name.to_string(),
}
}
pub fn parse_key_uri(keychain_key: &str) -> XResult<Self> {
if !keychain_key.starts_with(KEYCHAIN_KEY_PREFIX) {
return simple_error!("Not a valid keychain key: {}", keychain_key);
}
//keychain:keychain_name:service_name:key_name
let keychain_key_parts = keychain_key.split(':').collect::<Vec<_>>();
if keychain_key_parts.len() != 4 {
return simple_error!("Not a valid keychain key: {}", keychain_key);
}
Ok(Self {
keychain_name: keychain_key_parts[1].to_string(),
service_name: keychain_key_parts[2].to_string(),
key_name: keychain_key_parts[3].to_string(),
})
}
pub fn to_key_uri(&self) -> String {
let mut s = String::new();
s.push_str(KEYCHAIN_KEY_PREFIX);
s.push_str(&self.keychain_name);
s.push(':');
s.push_str(&self.service_name);
s.push(':');
s.push_str(&self.key_name);
s
}
pub fn get_password(&self) -> XResult<Option<Vec<u8>>> {
let sec_keychain = self.get_keychain()?;
debugging!(
"Try find generic password: {}.{}",
&self.service_name,
&self.key_name
);
match sec_keychain.find_generic_password(&self.service_name, &self.key_name) {
Ok((item_password, _keychain_item)) => Ok(Some(item_password.as_ref().to_vec())),
Err(e) => {
debugging!("Get password: {} failed: {}", &self.to_key_uri(), e);
Ok(None)
}
}
}
pub fn set_password(&self, password: &[u8]) -> XResult<()> {
let sec_keychain = self.get_keychain()?;
if sec_keychain
.find_generic_password(&self.service_name, &self.key_name)
.is_ok()
{
return simple_error!("Password {}.{} exists", &self.service_name, &self.key_name);
}
opt_result!(
sec_keychain.set_generic_password(&self.service_name, &self.key_name, password),
"Set password {}.{} error: {}",
&self.service_name,
&self.key_name
);
Ok(())
}
fn get_keychain(&self) -> XResult<SecKeychain> {
if !self.keychain_name.is_empty() {
let keychain_file_name = format!("{}.keychain", &self.keychain_name);
debugging!("Open or create keychain: {}", &keychain_file_name);
let keychain_exists = check_keychain_exists(&keychain_file_name);
if keychain_exists {
Ok(opt_result!(
SecKeychain::open(&keychain_file_name),
"Open keychain: {}, failed: {}",
&keychain_file_name
))
} else {
match CreateOptions::new()
.prompt_user(true)
.create(&keychain_file_name)
{
Ok(sec_keychain) => Ok(sec_keychain),
Err(ce) => match SecKeychain::open(&keychain_file_name) {
Ok(sec_keychain) => Ok(sec_keychain),
Err(oe) => simple_error!(
"Create keychain: {}, failed: {}, open also failed: {}",
&self.keychain_name,
ce,
oe
),
},
}
}
} else {
Ok(opt_result!(
SecKeychain::default(),
"Get keychain failed: {}"
))
}
}
}
fn check_keychain_exists(keychain_file_name: &str) -> bool {
let keychain_path = PathBuf::from(util_file::resolve_file_path("~/Library/Keychains/"));
match keychain_path.read_dir() {
Ok(read_dir) => {
for dir in read_dir {
match dir {
Ok(dir) => {
if let Some(file_name) = dir.file_name().to_str() {
if file_name.starts_with(keychain_file_name) {
debugging!("Found key chain file: {:?}", dir);
return true;
}
}
}
Err(e) => {
debugging!("Read path sub dir: {:?} failed: {}", keychain_path, e);
}
}
}
}
Err(e) => {
debugging!("Read path: {:?} failed: {}", keychain_path, e);
}
}
false
}

445
src/keyutil.rs Normal file
View File

@@ -0,0 +1,445 @@
use crate::pivutil::{FromStr, ToStr};
use jwt::AlgorithmType;
use percent_encoding::NON_ALPHANUMERIC;
use regex::Regex;
use rust_util::XResult;
use yubikey::piv::{AlgorithmId, SlotId};
// reference: https://git.hatter.ink/hatter/card-cli/issues/6
#[derive(Debug)]
pub enum KeyUri {
SecureEnclave(SecureEnclaveKey),
YubikeyPiv(YubikeyPivKey),
YubikeyHmacEncSoft(YubikeyHmacEncSoftKey),
ExternalCommand(ExternalCommandKey),
}
impl KeyUri {
pub fn as_secure_enclave_key(&self) -> XResult<&SecureEnclaveKey> {
match self {
KeyUri::SecureEnclave(key) => Ok(key),
_ => simple_error!("Not a secure enclave key."),
}
}
pub fn get_preferred_algorithm_type(&self) -> AlgorithmType {
let algorithm_id = match &self {
KeyUri::SecureEnclave(_) => return AlgorithmType::Es256,
KeyUri::YubikeyPiv(key) => key.algorithm,
KeyUri::YubikeyHmacEncSoft(key) => key.algorithm,
KeyUri::ExternalCommand(key) => key.algorithm,
};
match algorithm_id {
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => AlgorithmType::Rs256,
KeyAlgorithmId::EccP256 => AlgorithmType::Es256,
KeyAlgorithmId::EccP384 => AlgorithmType::Es384,
KeyAlgorithmId::EccP521 => AlgorithmType::Es512,
// ML-KEM not supports JWS
KeyAlgorithmId::MlKem512
| KeyAlgorithmId::MlKem768
| KeyAlgorithmId::MlKem1024 => AlgorithmType::None,
}
}
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for KeyUri {
fn to_string(&self) -> String {
let mut key_uri = String::with_capacity(64);
key_uri.push_str("key://");
match self {
// key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)
// key://hatter-mac-pro:se/p256:key_agreement:BASE64(dataRepresentation)
KeyUri::SecureEnclave(key) => {
key_uri.push_str(&key.host);
key_uri.push_str(":se/p256:");
key_uri.push_str(&key.usage.to_string());
key_uri.push(':');
key_uri.push_str(&key.private_key);
}
// key://yubikey-5n:piv/p256::9a
KeyUri::YubikeyPiv(key) => {
key_uri.push_str(&key.key_name);
key_uri.push_str(":piv/");
key_uri.push_str(key.algorithm.to_str());
key_uri.push_str("::");
key_uri.push_str(key.slot.to_str());
}
// key://-:soft/p256::hmac_enc:...
KeyUri::YubikeyHmacEncSoft(key) => {
key_uri.push_str(&key.key_name);
key_uri.push_str(":soft/");
key_uri.push_str(key.algorithm.to_str());
key_uri.push_str("::");
key_uri.push_str(key.hmac_enc_private_key.as_str());
}
// key://external-command-file-name:external_command/p256::parameter
KeyUri::ExternalCommand(key) => {
let encoded_external_command =
percent_encoding::utf8_percent_encode(&key.external_command, NON_ALPHANUMERIC)
.to_string();
key_uri.push_str(&encoded_external_command);
key_uri.push_str(":external_command/");
key_uri.push_str(key.algorithm.to_str());
key_uri.push_str("::");
key_uri.push_str(&key.parameter);
}
}
key_uri
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyAlgorithmId {
Rsa1024,
Rsa2048,
Rsa3072,
Rsa4096,
EccP256,
EccP384,
EccP521,
MlKem512,
MlKem768,
MlKem1024,
}
impl KeyAlgorithmId {
pub fn from_algorithm_id(algorithm_id: AlgorithmId) -> Self {
match algorithm_id {
AlgorithmId::Rsa1024 => Self::Rsa1024,
AlgorithmId::Rsa2048 => Self::Rsa2048,
AlgorithmId::EccP256 => Self::EccP256,
AlgorithmId::EccP384 => Self::EccP384,
}
}
pub fn to_algorithm_id(self) -> Option<AlgorithmId> {
match self {
KeyAlgorithmId::Rsa1024 => Some(AlgorithmId::Rsa1024),
KeyAlgorithmId::Rsa2048 => Some(AlgorithmId::Rsa2048),
KeyAlgorithmId::Rsa3072 => None,
KeyAlgorithmId::Rsa4096 => None,
KeyAlgorithmId::EccP256 => Some(AlgorithmId::EccP256),
KeyAlgorithmId::EccP384 => Some(AlgorithmId::EccP384),
KeyAlgorithmId::EccP521 => None,
KeyAlgorithmId::MlKem512 => None,
KeyAlgorithmId::MlKem768 => None,
KeyAlgorithmId::MlKem1024 => None,
}
}
pub fn is_rsa(&self) -> bool {
match self {
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => true,
KeyAlgorithmId::EccP256
| KeyAlgorithmId::EccP384
| KeyAlgorithmId::EccP521
| KeyAlgorithmId::MlKem512
| KeyAlgorithmId::MlKem768
| KeyAlgorithmId::MlKem1024 => false,
}
}
pub fn is_ecc(&self) -> bool {
match self {
KeyAlgorithmId::EccP256 | KeyAlgorithmId::EccP384 | KeyAlgorithmId::EccP521 => true,
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096
| KeyAlgorithmId::MlKem512
| KeyAlgorithmId::MlKem768
| KeyAlgorithmId::MlKem1024 => false,
}
}
pub fn is_mlkem(&self) -> bool {
match self {
| KeyAlgorithmId::MlKem512
| KeyAlgorithmId::MlKem768
| KeyAlgorithmId::MlKem1024 => true,
KeyAlgorithmId::EccP256
| KeyAlgorithmId::EccP384
| KeyAlgorithmId::EccP521
| KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => false,
}
}
pub fn as_jwa_name(&self) -> &str {
match self {
KeyAlgorithmId::Rsa1024
| KeyAlgorithmId::Rsa2048
| KeyAlgorithmId::Rsa3072
| KeyAlgorithmId::Rsa4096 => "RS256",
KeyAlgorithmId::EccP256 => "ES256,",
KeyAlgorithmId::EccP384 => "ES384",
KeyAlgorithmId::EccP521 => "ES512",
KeyAlgorithmId::MlKem512
| KeyAlgorithmId::MlKem768
| KeyAlgorithmId::MlKem1024 => "__UNKNOWN__",
}
}
}
impl FromStr for KeyAlgorithmId {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
match s {
"rsa1024" => Some(KeyAlgorithmId::Rsa1024),
"rsa2048" => Some(KeyAlgorithmId::Rsa2048),
"rsa3072" => Some(KeyAlgorithmId::Rsa3072),
"rsa4096" => Some(KeyAlgorithmId::Rsa4096),
"p256" => Some(KeyAlgorithmId::EccP256),
"p384" => Some(KeyAlgorithmId::EccP384),
"p521" => Some(KeyAlgorithmId::EccP521),
"mlkem512" => Some(KeyAlgorithmId::MlKem512),
"mlkem768" => Some(KeyAlgorithmId::MlKem768),
"mlkem1024" => Some(KeyAlgorithmId::MlKem1024),
_ => None,
}
}
}
impl ToStr for KeyAlgorithmId {
fn to_str(&self) -> &str {
match self {
KeyAlgorithmId::Rsa1024 => "rsa1024",
KeyAlgorithmId::Rsa2048 => "rsa2048",
KeyAlgorithmId::Rsa3072 => "rsa3072",
KeyAlgorithmId::Rsa4096 => "rsa4096",
KeyAlgorithmId::EccP256 => "p256",
KeyAlgorithmId::EccP384 => "p384",
KeyAlgorithmId::EccP521 => "p521",
KeyAlgorithmId::MlKem512 => "mlkem512",
KeyAlgorithmId::MlKem768 => "mlkem768",
KeyAlgorithmId::MlKem1024 => "mlkem1024",
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum KeyUsage {
Any,
Singing,
KeyAgreement,
}
impl KeyUsage {
pub fn from(usage: &str) -> Option<Self> {
match usage {
"signing" => Some(Self::Singing),
"key_agreement" => Some(Self::KeyAgreement),
"*" => Some(Self::Any),
_ => None,
}
}
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for KeyUsage {
fn to_string(&self) -> String {
match self {
KeyUsage::Any => "*",
KeyUsage::Singing => "signing",
KeyUsage::KeyAgreement => "key_agreement",
}
.to_string()
}
}
#[derive(Debug)]
pub struct SecureEnclaveKey {
pub host: String,
pub usage: KeyUsage,
pub private_key: String,
}
#[derive(Debug)]
pub struct YubikeyPivKey {
pub key_name: String,
pub algorithm: KeyAlgorithmId,
pub slot: SlotId,
}
#[derive(Debug)]
pub struct YubikeyHmacEncSoftKey {
pub key_name: String,
pub algorithm: KeyAlgorithmId,
pub hmac_enc_private_key: String,
}
#[derive(Debug)]
pub struct ExternalCommandKey {
pub external_command: String,
pub algorithm: KeyAlgorithmId,
pub parameter: String,
}
pub fn parse_key_uri(key_uri: &str) -> XResult<KeyUri> {
let regex = Regex::new(r##"^key://([0-9a-zA-Z\-\._]*):(\w+)/(\w+):((?:\w+)?):(.*)$"##).unwrap();
let captures = match regex.captures(key_uri) {
None => return simple_error!("Invalid key uri: {}", key_uri),
Some(captures) => captures,
};
let host_or_name = captures.get(1).unwrap().as_str();
let module = captures.get(2).unwrap().as_str();
let algorithm = captures.get(3).unwrap().as_str();
let usage = captures.get(4).unwrap().as_str();
let left_part = captures.get(5).unwrap().as_str();
match module {
"se" => {
if "p256" != algorithm {
return simple_error!("Key uri's algorithm must be p256.");
}
let key_usage = match KeyUsage::from(usage) {
None | Some(KeyUsage::Any) => {
return simple_error!("Key uri's usage must be signing or key_agreement.")
}
Some(key_usage) => key_usage,
};
let parsed_key_uri = KeyUri::SecureEnclave(SecureEnclaveKey {
host: host_or_name.to_string(),
usage: key_usage,
private_key: left_part.to_string(),
});
debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(parsed_key_uri)
}
"piv" => {
if !usage.is_empty() {
return simple_error!("Key uri's usage must be empty.");
}
let algorithm = opt_value_result!(
KeyAlgorithmId::from_str(algorithm),
"Invalid algorithm id: {}",
algorithm
);
let slot = opt_value_result!(
SlotId::from_str(left_part),
"Invalid slot id: {}",
left_part
);
let parsed_key_uri = KeyUri::YubikeyPiv(YubikeyPivKey {
key_name: host_or_name.to_string(),
algorithm,
slot,
});
debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(parsed_key_uri)
}
"soft" => {
if !usage.is_empty() {
return simple_error!("Key uri's usage must be empty.");
}
let algorithm = opt_value_result!(
KeyAlgorithmId::from_str(algorithm),
"Invalid algorithm id: {}",
algorithm
);
let hmac_enc_private_key = left_part.to_string();
let parsed_key_uri = KeyUri::YubikeyHmacEncSoft(YubikeyHmacEncSoftKey {
key_name: host_or_name.to_string(),
algorithm,
hmac_enc_private_key,
});
debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(parsed_key_uri)
}
"external_command" => {
if !usage.is_empty() {
return simple_error!("Key uri's usage must be empty.");
}
let external_command = opt_result!(
percent_encoding::percent_decode_str(host_or_name).decode_utf8(),
"Decode external command failed: {}"
);
let algorithm = opt_value_result!(
KeyAlgorithmId::from_str(algorithm),
"Invalid algorithm id: {}",
algorithm
);
let parameter = left_part.to_string();
let parsed_key_uri = KeyUri::ExternalCommand(ExternalCommandKey {
external_command: external_command.to_string(),
algorithm,
parameter,
});
debugging!("Parsed key uri: {:?}", parsed_key_uri);
Ok(parsed_key_uri)
}
_ => simple_error!("Key uri's module must be se."),
}
}
#[test]
fn test_parse_key_uri_01() {
let se_key_uri =
parse_key_uri("key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)").unwrap();
assert_eq!(
"key://hatter-mac-pro:se/p256:signing:BASE64(dataRepresentation)",
se_key_uri.to_string()
);
match se_key_uri {
KeyUri::SecureEnclave(se_key_uri) => {
assert_eq!("hatter-mac-pro", se_key_uri.host);
assert_eq!(KeyUsage::Singing, se_key_uri.usage);
assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key);
}
_ => {
panic!("Key uri not parsed")
}
}
}
#[test]
fn test_parse_key_uri_02() {
let se_key_uri =
parse_key_uri("key://hatter-mac-m1:se/p256:key_agreement:BASE64(dataRepresentation)")
.unwrap();
assert_eq!(
"key://hatter-mac-m1:se/p256:key_agreement:BASE64(dataRepresentation)",
se_key_uri.to_string()
);
match se_key_uri {
KeyUri::SecureEnclave(se_key_uri) => {
assert_eq!("hatter-mac-m1", se_key_uri.host);
assert_eq!(KeyUsage::KeyAgreement, se_key_uri.usage);
assert_eq!("BASE64(dataRepresentation)", se_key_uri.private_key);
}
_ => {
panic!("Key uri not parsed")
}
}
}
#[test]
fn test_parse_key_uri_03() {
let se_key_uri = parse_key_uri("key://yubikey-5n:piv/p256::9a").unwrap();
assert_eq!(
"key://yubikey-5n:piv/p256::authentication",
se_key_uri.to_string()
);
match se_key_uri {
KeyUri::YubikeyPiv(piv_key_uri) => {
assert_eq!("yubikey-5n", piv_key_uri.key_name);
assert_eq!(KeyAlgorithmId::EccP256, piv_key_uri.algorithm);
assert_eq!(SlotId::Authentication, piv_key_uri.slot);
}
_ => {
panic!("Key uri not parsed")
}
}
}

View File

@@ -4,20 +4,92 @@ extern crate rust_util;
use clap::{App, AppSettings, ArgMatches}; use clap::{App, AppSettings, ArgMatches};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
mod fido; mod argsutil;
mod digest;
mod cmd_u2fregister;
mod cmd_u2fsign;
mod cmd_pgp;
mod pgpcardutil;
mod cmd_pgpcardadmin;
mod cmd_pgpcardlist;
mod cmd_pgpcardsign;
mod cmd_pgpcarddecrypt;
mod cmd_piv;
mod cmd_pivsign;
mod cmd_chall; mod cmd_chall;
mod cmd_challconfig; mod cmd_chall_config;
mod cmd_convert_jwk_to_pem;
mod cmd_convert_pem_to_jwk;
mod cmd_ec_verify;
mod cmd_external_ecdh;
mod cmd_external_public_key;
mod cmd_external_sign;
mod cmd_external_spec;
mod cmd_file_sign;
mod cmd_file_verify;
mod cmd_hmac_decrypt;
mod cmd_hmac_encrypt;
mod cmd_hmac_sha1;
mod cmd_keypair_generate;
mod cmd_keypair_keychain_export;
mod cmd_keypair_keychain_import;
mod cmd_list;
mod cmd_parseecdsasignature;
#[cfg(feature = "with-sequoia-openpgp")]
mod cmd_pgp;
mod cmd_pgp_age_address;
mod cmd_pgp_card_admin;
mod cmd_pgp_card_decrypt;
mod cmd_pgp_card_list;
#[cfg(feature = "with-sequoia-openpgp")]
mod cmd_pgp_card_make;
mod cmd_pgp_card_sign;
mod cmd_piv;
mod cmd_piv_decrypt;
mod cmd_piv_ecdh;
mod cmd_piv_ecsign;
mod cmd_piv_generate;
mod cmd_piv_meta;
mod cmd_piv_rsasign;
mod cmd_piv_summary;
mod cmd_piv_verify;
mod cmd_rsa_decrypt;
mod cmd_rsa_encrypt;
mod cmd_rsa_verify;
mod cmd_se;
mod cmd_se_ecdh;
mod cmd_se_ecsign;
mod cmd_se_generate;
mod cmd_se_recover;
mod cmd_sign_jwt;
mod cmd_sign_jwt_piv;
mod cmd_sign_jwt_se;
mod cmd_sign_jwt_soft;
mod cmd_ssh_agent;
mod cmd_ssh_agent_gpg;
mod cmd_ssh_parse;
mod cmd_ssh_parse_sign;
mod cmd_ssh_piv_cert;
mod cmd_ssh_piv_sign;
mod cmd_ssh_pub_key;
mod cmd_u2f_register;
mod cmd_u2f_sign;
mod cmdutil;
mod digestutil;
mod ecdhutil;
mod ecdsautil;
mod ecutil;
mod fidoutil;
mod hmacutil;
mod keychain;
mod keyutil;
mod pbeutil;
mod pgpcardutil;
mod pinutil;
mod pivutil;
mod pkiutil;
mod rsautil;
mod seutil;
mod signfile;
mod sshutil;
mod util;
mod yubikeyutil;
mod cmd_yubikey;
mod mlkemutil;
use zeroizing_alloc::ZeroAlloc;
#[global_allocator]
static ALLOC: ZeroAlloc<std::alloc::System> = ZeroAlloc(std::alloc::System);
pub struct DefaultCommandImpl; pub struct DefaultCommandImpl;
@@ -32,29 +104,97 @@ impl DefaultCommandImpl {
} }
fn main() { fn main() {
if let Err(e) = inner_main() { // Run with: RUST_LOG=debug, for more: https://docs.rs/env_logger/0.10.0/env_logger/
failure_and_exit!("Run cli error: {}", e); #[cfg(debug_assertions)]
env_logger::init();
match inner_main() {
Err(e) => failure_and_exit!("Run cli error: {}", e),
Ok(Some(code)) => std::process::exit(code),
Ok(None) => (),
} }
} }
fn inner_main() -> CommandError { fn inner_main() -> CommandError {
let commands: Vec<Box<dyn Command>> = vec![ let commands: Vec<Box<dyn Command>> = vec![
Box::new(cmd_list::CommandImpl),
Box::new(cmd_chall::CommandImpl), Box::new(cmd_chall::CommandImpl),
Box::new(cmd_challconfig::CommandImpl), Box::new(cmd_hmac_sha1::CommandImpl),
Box::new(cmd_hmac_encrypt::CommandImpl),
Box::new(cmd_hmac_decrypt::CommandImpl),
Box::new(cmd_chall_config::CommandImpl),
Box::new(cmd_rsa_encrypt::CommandImpl),
Box::new(cmd_rsa_decrypt::CommandImpl),
Box::new(cmd_rsa_verify::CommandImpl),
#[cfg(feature = "with-sequoia-openpgp")]
Box::new(cmd_pgp::CommandImpl), Box::new(cmd_pgp::CommandImpl),
Box::new(cmd_pgpcardadmin::CommandImpl), Box::new(cmd_pgp_card_admin::CommandImpl),
Box::new(cmd_pgpcardlist::CommandImpl), Box::new(cmd_pgp_card_list::CommandImpl),
Box::new(cmd_pgpcardsign::CommandImpl), Box::new(cmd_pgp_card_sign::CommandImpl),
Box::new(cmd_pgpcarddecrypt::CommandImpl), Box::new(cmd_pgp_card_decrypt::CommandImpl),
#[cfg(feature = "with-sequoia-openpgp")]
Box::new(cmd_pgp_card_make::CommandImpl),
Box::new(cmd_piv::CommandImpl), Box::new(cmd_piv::CommandImpl),
Box::new(cmd_pivsign::CommandImpl), Box::new(cmd_piv_summary::CommandImpl),
Box::new(cmd_u2fregister::CommandImpl), Box::new(cmd_piv_meta::CommandImpl),
Box::new(cmd_u2fsign::CommandImpl), Box::new(cmd_piv_verify::CommandImpl),
Box::new(cmd_piv_rsasign::CommandImpl),
Box::new(cmd_piv_ecdh::CommandImpl),
Box::new(cmd_piv_ecsign::CommandImpl),
Box::new(cmd_piv_decrypt::CommandImpl),
Box::new(cmd_piv_generate::CommandImpl),
Box::new(cmd_u2f_register::CommandImpl),
Box::new(cmd_u2f_sign::CommandImpl),
Box::new(cmd_ssh_agent::CommandImpl),
Box::new(cmd_ssh_agent_gpg::CommandImpl),
Box::new(cmd_ssh_parse_sign::CommandImpl),
Box::new(cmd_ssh_piv_sign::CommandImpl),
Box::new(cmd_ssh_piv_cert::CommandImpl),
Box::new(cmd_ssh_pub_key::CommandImpl),
Box::new(cmd_ssh_parse::CommandImpl),
Box::new(cmd_pgp_age_address::CommandImpl),
Box::new(cmd_sign_jwt_piv::CommandImpl),
Box::new(cmd_sign_jwt_soft::CommandImpl),
Box::new(cmd_sign_jwt_se::CommandImpl),
Box::new(cmd_sign_jwt::CommandImpl),
Box::new(cmd_file_sign::CommandImpl),
Box::new(cmd_file_verify::CommandImpl),
Box::new(cmd_se::CommandImpl),
Box::new(cmd_se_generate::CommandImpl),
Box::new(cmd_se_recover::CommandImpl),
Box::new(cmd_se_ecsign::CommandImpl),
Box::new(cmd_se_ecdh::CommandImpl),
Box::new(cmd_ec_verify::CommandImpl),
Box::new(cmd_parseecdsasignature::CommandImpl),
Box::new(cmd_keypair_generate::CommandImpl),
Box::new(cmd_keypair_keychain_import::CommandImpl),
Box::new(cmd_keypair_keychain_export::CommandImpl),
Box::new(cmd_convert_pem_to_jwk::CommandImpl),
Box::new(cmd_convert_jwk_to_pem::CommandImpl),
Box::new(cmd_external_spec::CommandImpl),
Box::new(cmd_external_public_key::CommandImpl),
Box::new(cmd_external_sign::CommandImpl),
Box::new(cmd_external_ecdh::CommandImpl),
Box::new(cmd_yubikey::CommandImpl),
]; ];
#[allow(clippy::vec_init_then_push)]
let features = {
let mut features: Vec<&str> = vec![];
#[cfg(feature = "with-sequoia-openpgp")]
features.push("sequoia-openpgp");
features
};
let about = format!(
"{}, features: [{}]",
"Card Cli is a command tool for WebAuthn, OpenPGP, YubiKey ... smart cards",
features.join(", "),
);
let mut app = App::new(env!("CARGO_PKG_NAME")) let mut app = App::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION")) .version(env!("CARGO_PKG_VERSION"))
.about(env!("CARGO_PKG_DESCRIPTION")) .about(env!("CARGO_PKG_DESCRIPTION"))
.long_about("Card Cli is a command tool for WebAuthn, OpenPGP, YubiKey ... smart cards") .long_about(about.as_str())
.setting(AppSettings::ColoredHelp); .setting(AppSettings::ColoredHelp);
app = DefaultCommandImpl::process_command(app); app = DefaultCommandImpl::process_command(app);
for command in &commands { for command in &commands {

241
src/mlkemutil.rs Normal file
View File

@@ -0,0 +1,241 @@
use crate::util::{base64_encode, to_pem};
use ml_kem::kem::{Decapsulate, Encapsulate};
use ml_kem::{EncodedSizeUser, KemCore, MlKem1024, MlKem512, MlKem768};
use rust_util::XResult;
use std::convert::TryInto;
use crate::pivutil::ToStr;
#[derive(Debug, Clone, Copy)]
pub enum MlKemLen {
Len512,
Len768,
Len1024,
}
impl ToStr for MlKemLen {
fn to_str(&self) -> &str {
match self {
MlKemLen::Len512 => "mlkem512",
MlKemLen::Len768 => "mlkem768",
MlKemLen::Len1024 => "mlkem1024",
}
}
}
pub fn generate_mlkem_keypair(len: usize) -> XResult<(String, String, String, Vec<u8>, String)> {
let (dk_private, ek_public) = match len {
512 => generate_ml_kem_512(),
768 => generate_ml_kem_768(),
1024 => generate_ml_kem_1024(),
_ => return simple_error!("Invalid ML-KEM={}", len),
};
let secret_key_der_base64 = base64_encode(&dk_private);
let secret_key_pem = to_pem(&format!("ML-KEM-{} PRIVATE KEY", len), &dk_private);
let public_key_pem = to_pem(&format!("ML-KEM-{} PUBLIC KEY", len), &ek_public);
let public_key_der = ek_public;
let jwk_ec_key = "".to_string();
Ok((
secret_key_der_base64,
secret_key_pem,
public_key_pem,
public_key_der,
jwk_ec_key,
))
}
pub fn try_parse_decapsulate_key_private_then_decapsulate(
key_bytes: &[u8],
ciphertext_bytes: &[u8],
) -> XResult<(MlKemLen, Vec<u8>)> {
if let Ok(shared_secret) =
parse_decapsulate_key_512_private_then_decapsulate(key_bytes, ciphertext_bytes)
{
return Ok((MlKemLen::Len512, shared_secret.to_vec()));
}
if let Ok(shared_secret) =
parse_decapsulate_key_768_private_then_decapsulate(key_bytes, ciphertext_bytes)
{
return Ok((MlKemLen::Len768, shared_secret.to_vec()));
}
if let Ok(shared_secret) =
parse_decapsulate_key_1024_private_then_decapsulate(key_bytes, ciphertext_bytes)
{
return Ok((MlKemLen::Len1024, shared_secret.to_vec()));
}
simple_error!("Invalid decapsulation key, only allow MK-KEM-512, ML-KEM-768, ML-KEM-1024")
}
pub fn try_parse_decapsulate_key_private_get_encapsulate(
key_bytes: &[u8],
) -> XResult<(MlKemLen, Vec<u8>)> {
if let Ok(encapsulate_key) = parse_decapsulate_key_512_private_get_encapsulate(key_bytes) {
return Ok((MlKemLen::Len512, encapsulate_key));
}
if let Ok(encapsulate_key) = parse_decapsulate_key_768_private_get_encapsulate(key_bytes) {
return Ok((MlKemLen::Len768, encapsulate_key));
}
if let Ok(encapsulate_key) = parse_decapsulate_key_1024_private_get_encapsulate(key_bytes) {
return Ok((MlKemLen::Len1024, encapsulate_key));
}
simple_error!("Invalid decapsulation key, only allow MK-KEM-512, ML-KEM-768, ML-KEM-1024")
}
pub fn generate_ml_kem_512() -> (Vec<u8>, Vec<u8>) {
let mut rng = rand::thread_rng();
let (dk_private, ek_public) = <MlKem512 as KemCore>::generate(&mut rng);
(
dk_private.as_bytes().0.to_vec(),
ek_public.as_bytes().0.to_vec(),
)
}
pub fn generate_ml_kem_768() -> (Vec<u8>, Vec<u8>) {
let mut rng = rand::thread_rng();
let (dk_private, ek_public) = <MlKem768 as KemCore>::generate(&mut rng);
(
dk_private.as_bytes().0.to_vec(),
ek_public.as_bytes().0.to_vec(),
)
}
pub fn generate_ml_kem_1024() -> (Vec<u8>, Vec<u8>) {
let mut rng = rand::thread_rng();
let (dk_private, ek_public) = <MlKem1024 as KemCore>::generate(&mut rng);
(
dk_private.as_bytes().0.to_vec(),
ek_public.as_bytes().0.to_vec(),
)
}
pub fn try_parse_encapsulation_key_public_then_encapsulate(bytes: &[u8]) -> XResult<(MlKemLen, Vec<u8>, Vec<u8>)> {
if let Ok((ciphertext, shared_key)) = parse_encapsulation_key_512_public_then_encapsulate(bytes) {
return Ok((MlKemLen::Len512, ciphertext, shared_key));
}
if let Ok((ciphertext, shared_key)) = parse_encapsulation_key_768_public_then_encapsulate(bytes) {
return Ok((MlKemLen::Len768, ciphertext, shared_key));
}
if let Ok((ciphertext, shared_key)) = parse_encapsulation_key_1024_public_then_encapsulate(bytes) {
return Ok((MlKemLen::Len1024, ciphertext, shared_key));
}
simple_error!("Invalid encapsulation key, only allow MK-KEM-512, ML-KEM-768, ML-KEM-1024")
}
pub fn parse_encapsulation_key_512_public_then_encapsulate(
bytes: &[u8],
) -> XResult<(Vec<u8>, Vec<u8>)> {
let ek = <MlKem512 as KemCore>::EncapsulationKey::from_bytes(&opt_result!(
bytes.try_into(),
"Parse encapsulation key 512 failed: {}"
));
let (ciphertext, shared_key) = opt_result!(
ek.encapsulate(&mut rand::thread_rng()),
"Encapsulation key 512 encapsulate failed: {:?}"
);
Ok((ciphertext.0.to_vec(), shared_key.0.to_vec()))
}
pub fn parse_encapsulation_key_768_public_then_encapsulate(
bytes: &[u8],
) -> XResult<(Vec<u8>, Vec<u8>)> {
let ek = <MlKem768 as KemCore>::EncapsulationKey::from_bytes(&opt_result!(
bytes.try_into(),
"Parse encapsulation key 768 failed: {}"
));
let (ciphertext, shared_key) = opt_result!(
ek.encapsulate(&mut rand::thread_rng()),
"Encapsulation key 768 encapsulate failed: {:?}"
);
Ok((ciphertext.0.to_vec(), shared_key.0.to_vec()))
}
pub fn parse_encapsulation_key_1024_public_then_encapsulate(
bytes: &[u8],
) -> XResult<(Vec<u8>, Vec<u8>)> {
let ek = <MlKem1024 as KemCore>::EncapsulationKey::from_bytes(&opt_result!(
bytes.try_into(),
"Parse encapsulation key 1024 failed: {}"
));
let (ciphertext, shared_key) = opt_result!(
ek.encapsulate(&mut rand::thread_rng()),
"Encapsulation key 1024 encapsulate failed: {:?}"
);
Ok((ciphertext.0.to_vec(), shared_key.0.to_vec()))
}
pub fn parse_decapsulate_key_512_private_then_decapsulate(
key_bytes: &[u8],
ciphertext_bytes: &[u8],
) -> XResult<Vec<u8>> {
let dk = <MlKem512 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 512 failed: {}"
));
let shared_key = opt_result!(
dk.decapsulate(opt_result!(
ciphertext_bytes.try_into(),
"Parse encoded ciphertext 512 failed: {}"
)),
"Decapsulation key 512 decapsulate failed: {:?}"
);
Ok(shared_key.0.to_vec())
}
pub fn parse_decapsulate_key_768_private_then_decapsulate(
key_bytes: &[u8],
ciphertext_bytes: &[u8],
) -> XResult<Vec<u8>> {
let dk = <MlKem768 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 768 failed: {}"
));
let shared_key = opt_result!(
dk.decapsulate(opt_result!(
ciphertext_bytes.try_into(),
"Parse encoded ciphertext 768 failed: {}"
)),
"Decapsulation key 768 decapsulate failed: {:?}"
);
Ok(shared_key.0.to_vec())
}
pub fn parse_decapsulate_key_1024_private_then_decapsulate(
key_bytes: &[u8],
ciphertext_bytes: &[u8],
) -> XResult<Vec<u8>> {
let dk = <MlKem1024 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 1024 failed: {}"
));
let shared_key = opt_result!(
dk.decapsulate(opt_result!(
ciphertext_bytes.try_into(),
"Parse encoded ciphertext 1024 failed: {}"
)),
"Decapsulation key 1024 decapsulate failed: {:?}"
);
Ok(shared_key.0.to_vec())
}
pub fn parse_decapsulate_key_512_private_get_encapsulate(key_bytes: &[u8]) -> XResult<Vec<u8>> {
let dk = <MlKem512 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 512 failed: {}"
));
Ok(dk.encapsulation_key().as_bytes().0.to_vec())
}
pub fn parse_decapsulate_key_768_private_get_encapsulate(key_bytes: &[u8]) -> XResult<Vec<u8>> {
let dk = <MlKem768 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 768 failed: {}"
));
Ok(dk.encapsulation_key().as_bytes().0.to_vec())
}
pub fn parse_decapsulate_key_1024_private_get_encapsulate(key_bytes: &[u8]) -> XResult<Vec<u8>> {
let dk = <MlKem1024 as KemCore>::DecapsulationKey::from_bytes(&opt_result!(
key_bytes.try_into(),
"Parse decapsulation key 1024 failed: {}"
));
Ok(dk.encapsulation_key().as_bytes().0.to_vec())
}

119
src/pbeutil.rs Normal file
View File

@@ -0,0 +1,119 @@
use crate::digestutil::{copy_sha256, sha256_bytes};
use crate::pinutil;
use crate::util::{base64_decode, base64_encode, base64_encode_url_safe_no_pad};
use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor};
use rand::random;
use rust_util::XResult;
use zeroize::Zeroize;
const PBE_ENC_PREFIX: &str = "pbe_enc:";
pub fn simple_pbe_encrypt_with_prompt_from_string(iteration: u32, plaintext: &str, passowrd: &mut Option<String>, password_double_check: bool) -> XResult<String> {
simple_pbe_encrypt_with_prompt(iteration, plaintext.as_bytes(), passowrd, password_double_check)
}
pub fn simple_pbe_decrypt_with_prompt_to_string(pin_opt: &mut Option<String>, ciphertext: &str) -> XResult<String> {
let plaintext = simple_pbe_decrypt_with_prompt(pin_opt, ciphertext)?;
Ok(String::from_utf8(plaintext)?)
}
pub fn simple_pbe_encrypt_with_prompt(iteration: u32, plaintext: &[u8], password_opt: &mut Option<String>, password_double_check: bool) -> XResult<String> {
let mut pin = match password_opt {
None => {
let pin1 = opt_value_result!(pinutil::get_pin(None), "Simple PBE password required");
if password_double_check {
let mut pin2 = opt_value_result!(pinutil::get_pin(None), "Simple PBE password required");
if pin1 != pin2 {
return simple_error!("Two PINs mismatch");
}
pin2.zeroize();
}
*password_opt = Some(pin1.clone());
pin1
}
Some(pin) => pin.clone(),
};
let encrypt_result = simple_pbe_encrypt(&pin, iteration, plaintext);
pin.zeroize();
encrypt_result
}
pub fn simple_pbe_decrypt_with_prompt(pin_opt: &mut Option<String>, ciphertext: &str) -> XResult<Vec<u8>> {
let mut pin = opt_value_result!(pinutil::get_pin(pin_opt.clone().as_deref()), "Simple PBE password required");
pin_opt.zeroize();
*pin_opt = Some(pin.clone());
let decrypt_result = simple_pbe_decrypt(&pin, ciphertext);
pin.zeroize();
decrypt_result
}
pub fn simple_pbe_encrypt(password: &str, iteration: u32, plaintext: &[u8]) -> XResult<String> {
let pbe_salt: [u8; 16] = random();
let key = simple_pbe_kdf(password, &pbe_salt, iteration)?;
let aes_gcm_nonce: [u8; 16] = random();
let mut encryptor = Aes256GcmStreamEncryptor::new(key, &aes_gcm_nonce);
let mut ciphertext = encryptor.update(plaintext);
let (final_part, tag) = encryptor.finalize();
ciphertext.extend_from_slice(&final_part);
ciphertext.extend_from_slice(&tag);
Ok(format!(
"{}{}:{}:{}:{}",
PBE_ENC_PREFIX,
iteration,
base64_encode_url_safe_no_pad(pbe_salt),
base64_encode_url_safe_no_pad(aes_gcm_nonce),
base64_encode(&ciphertext)
))
}
pub fn simple_pbe_decrypt(password: &str, ciphertext: &str) -> XResult<Vec<u8>> {
if !is_simple_pbe_encrypted(ciphertext) {
return simple_error!("Invalid ciphertext: {}", ciphertext);
}
let parts = ciphertext.split(":").collect::<Vec<_>>();
let iteration: u32 = parts[1].parse()?;
let pbe_salt = crate::hmacutil::try_decode_hmac_val(parts[2])?;
let aes_gcm_nonce = crate::hmacutil::try_decode_hmac_val(parts[3])?;
let ciphertext = base64_decode(parts[4])?;
let key = simple_pbe_kdf(password, &pbe_salt, iteration)?;
let mut decryptor = Aes256GcmStreamDecryptor::new(key, &aes_gcm_nonce);
let mut plaintext = decryptor.update(&ciphertext);
let final_part = decryptor.finalize()?;
plaintext.extend_from_slice(&final_part);
Ok(plaintext)
}
pub fn is_simple_pbe_encrypted(ciphertext: &str) -> bool {
ciphertext.starts_with(PBE_ENC_PREFIX)
}
fn simple_pbe_kdf(password: &str, pbe_salt: &[u8], iteration: u32) -> XResult<[u8; 32]> {
let mut init_data = password.as_bytes().to_vec();
init_data.extend_from_slice(pbe_salt);
let mut loop_hash = sha256_bytes(&init_data);
for i in 0..iteration {
let i_to_bytes = i.to_be_bytes();
loop_hash[..4].copy_from_slice(&i_to_bytes);
loop_hash = sha256_bytes(&loop_hash);
}
let key = copy_sha256(&sha256_bytes(&loop_hash))?;
Ok(key)
}
#[test]
fn test_simple_pbe_kdf() {
assert_eq!("cea7eb6424132aa4d6b1880c3cb570df17c41d766826e144088fd16b334c80c5",
hex::encode(simple_pbe_kdf("hello_world", b"salt", 1).unwrap()));
assert_eq!("e3ed4a0a2f451180cfa4a0f8f3e1181d8863fc192d161e4c1480e3612135ca27",
hex::encode(simple_pbe_kdf("hello_world", b"salt", 2).unwrap()));
assert_eq!("e552900ad3f14a96629c8056bd7bc4b2431250ef4a47e78856626f45807cd87e",
hex::encode(simple_pbe_kdf("hello_world 2", b"salt", 2).unwrap()));
assert_eq!("37e71484f00033c99db444c77553364b31614d5145e38aad5bd5caa6676d40f9",
hex::encode(simple_pbe_kdf("hello_world", b"salt 2", 2).unwrap()));
}

View File

@@ -1,47 +1,19 @@
use openpgp_card::{OpenPGPCard, OpenPGPCardAdmin, OpenPGPCardUser}; use openpgp_card::OpenPgp;
use openpgp_card_pcsc::PcscBackend;
use rust_util::XResult; use rust_util::XResult;
pub fn get_card_user_sw1_81(pin: &str) -> XResult<OpenPGPCardUser> { pub fn get_openpgp_card() -> XResult<OpenPgp> {
// pw1_81 for signature
// openssl dgst -sha256 -verify aa -signature sig LICENSE
get_card_user(|open_pgp_card: OpenPGPCard| open_pgp_card.verify_pw1_81(pin), "pw1_81")
}
pub fn get_card_user_sw1_82(pin: &str) -> XResult<OpenPGPCardUser> {
// pw1_82 for decrypt
// PKCSv1.5
get_card_user(|open_pgp_card: OpenPGPCard| open_pgp_card.verify_pw1_82(pin), "pw1_82")
}
pub fn get_card_admin(pin: &str) -> XResult<OpenPGPCardAdmin> {
let card = get_card()?; let card = get_card()?;
match card.verify_pw3(pin) { Ok(OpenPgp::new(card))
Result::Ok(admin) => Ok(admin),
Result::Err(_) => simple_error!("Verify pw3 OpenPGP card failed"),
}
} }
fn get_card_user(process_fn: impl Fn(OpenPGPCard) -> Result<OpenPGPCardUser, OpenPGPCard>, tag: &str) -> XResult<OpenPGPCardUser> { pub fn get_card() -> XResult<PcscBackend> {
let card = get_card()?; let card_list = opt_result!(PcscBackend::cards(None), "Read OpenPGP card list failed: {}");
match process_fn(card) {
Result::Ok(user) => Ok(user),
Result::Err(_) => simple_error!("Verify {} OpenPGP card failed", tag),
}
}
fn get_card() -> XResult<OpenPGPCard> {
let card_list = opt_result!(OpenPGPCard::list_cards(),
"Read OpenPGP card list failed: {}");
if card_list.is_empty() { if card_list.is_empty() {
return simple_error!("Cannot find any card"); return simple_error!("Cannot find any card");
} }
if card_list.len() > 1 { if card_list.len() > 1 {
warning!("Find {} OpenPGP cards, will use first card", card_list.len()); warning!("Find {} OpenPGP cards, will use first card", card_list.len());
} }
let card = card_list.into_iter().next(); Ok(opt_value_result!(card_list.into_iter().next(), "SHOULD NOT HAPPEN, CANNOT FIND ANY CARD"))
card.as_ref().map(|card|
card.get_aid().map(|card|
success!("Found card: {:?}", card)));
Ok(opt_value_result!(card, "Get first card failed"))
} }

58
src/pinutil.rs Normal file
View File

@@ -0,0 +1,58 @@
use std::{env, fs};
use clap::ArgMatches;
use pinentry::PassphraseInput;
use secrecy::ExposeSecret;
const PIN_ENTRY_ENV: &str = "PIN_ENTRY_CMD";
const PIN_ENTRY_1: &str = "/usr/local/MacGPG2/libexec/pinentry-mac.app/Contents/MacOS/pinentry-mac";
const PIN_ENTRY_DEFAULT: &str = "pinentry";
pub fn read_pin(sub_arg_matches: &ArgMatches) -> Option<String> {
if sub_arg_matches.is_present("no-pin") {
None
} else {
let pin_opt = sub_arg_matches.value_of("pin");
get_pin(pin_opt)
}
}
pub fn get_pin(pin_opt: Option<&str>) -> Option<String> {
if let Some(pin) = pin_opt {
return Some(pin.to_string());
}
let pin_entry = get_pin_entry();
if let Some(mut input) = PassphraseInput::with_binary(pin_entry) {
let secret = input
.with_description("Please input PIN.")
.with_prompt("PIN: ")
.interact();
match secret {
Ok(secret_string) => Some(secret_string.expose_secret().to_string()),
Err(e) => {
warning!("Input PIN failed: {}", e);
None
}
}
} else {
match rpassword::prompt_password("Please input PIN: ") {
Ok(pin) => Some(pin),
Err(e) => {
warning!("Input PIN failed: {}", e);
None
}
}
}
}
fn get_pin_entry() -> String {
if let Ok(pin_entry) = env::var(PIN_ENTRY_ENV) {
return pin_entry;
}
if let Ok(m) = fs::metadata(PIN_ENTRY_1) {
if m.is_file() {
return PIN_ENTRY_1.to_string();
}
}
PIN_ENTRY_DEFAULT.to_string()
}

308
src/pivutil.rs Normal file
View File

@@ -0,0 +1,308 @@
use clap::ArgMatches;
use jwt::AlgorithmType;
use rust_util::XResult;
use spki::{ObjectIdentifier, SubjectPublicKeyInfoOwned};
use spki::der::{Decode, Encode};
use x509_parser::prelude::FromDer;
use x509_parser::public_key::RSAPublicKey;
use yubikey::{Certificate, PinPolicy, TouchPolicy, YubiKey};
use yubikey::piv::{metadata, AlgorithmId, ManagementAlgorithmId, ManagementSlotId, Origin, RetiredSlotId};
use yubikey::piv::SlotId;
use crate::pinutil;
const RSA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
const ECC: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
// NIST recommended curves
// secp192r1 {1.2.840.10045.3.1.1}
// secp224r1 {1.3.132.0.33}
// secp256r1 {1.2.840.10045.3.1.7}
// secp384r1 {1.3.132.0.34}
// secp521r1 {1.3.132.0.35}
const ECC_P256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7");
const ECC_P384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.34");
pub const ORDERED_SLOTS: [SlotId; 28] = [
SlotId::Management(ManagementSlotId::Pin),
SlotId::Management(ManagementSlotId::Puk),
SlotId::Retired(RetiredSlotId::R1),
SlotId::Retired(RetiredSlotId::R2),
SlotId::Retired(RetiredSlotId::R3),
SlotId::Retired(RetiredSlotId::R4),
SlotId::Retired(RetiredSlotId::R5),
SlotId::Retired(RetiredSlotId::R6),
SlotId::Retired(RetiredSlotId::R7),
SlotId::Retired(RetiredSlotId::R8),
SlotId::Retired(RetiredSlotId::R9),
SlotId::Retired(RetiredSlotId::R10),
SlotId::Retired(RetiredSlotId::R11),
SlotId::Retired(RetiredSlotId::R12),
SlotId::Retired(RetiredSlotId::R13),
SlotId::Retired(RetiredSlotId::R14),
SlotId::Retired(RetiredSlotId::R15),
SlotId::Retired(RetiredSlotId::R16),
SlotId::Retired(RetiredSlotId::R17),
SlotId::Retired(RetiredSlotId::R18),
SlotId::Retired(RetiredSlotId::R19),
SlotId::Retired(RetiredSlotId::R20),
SlotId::Authentication,
SlotId::Management(ManagementSlotId::Management),
SlotId::Signature,
SlotId::KeyManagement,
SlotId::CardAuthentication,
SlotId::Attestation,
];
pub trait ToStr {
fn to_str(&self) -> &str;
}
pub trait FromStr {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized;
}
impl ToStr for AlgorithmType {
fn to_str(&self) -> &str {
match self {
AlgorithmType::Hs256 => "HS256",
AlgorithmType::Hs384 => "HS384",
AlgorithmType::Hs512 => "HS512",
AlgorithmType::Rs256 => "RS256",
AlgorithmType::Rs384 => "RS384",
AlgorithmType::Rs512 => "RS512",
AlgorithmType::Es256 => "ES256",
AlgorithmType::Es384 => "ES384",
AlgorithmType::Es512 => "ES512",
AlgorithmType::Ps256 => "PS256",
AlgorithmType::Ps384 => "PS384",
AlgorithmType::Ps512 => "PS512",
AlgorithmType::None => "NONE",
}
}
}
impl ToStr for PinPolicy {
fn to_str(&self) -> &str {
match self {
PinPolicy::Default => "default",
PinPolicy::Never => "never",
PinPolicy::Once => "once",
PinPolicy::Always => "always",
}
}
}
impl ToStr for TouchPolicy {
fn to_str(&self) -> &str {
match self {
TouchPolicy::Default => "default",
TouchPolicy::Never => "never",
TouchPolicy::Always => "always",
TouchPolicy::Cached => "cached",
}
}
}
impl FromStr for AlgorithmId {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
match s {
"rsa1024" => Some(AlgorithmId::Rsa1024),
"rsa2048" => Some(AlgorithmId::Rsa2048),
"p256" => Some(AlgorithmId::EccP256),
"p384" => Some(AlgorithmId::EccP384),
_ => None,
}
}
}
impl ToStr for AlgorithmId {
fn to_str(&self) -> &str {
match self {
AlgorithmId::Rsa1024 => "rsa1024",
AlgorithmId::Rsa2048 => "rsa2048",
AlgorithmId::EccP256 => "p256",
AlgorithmId::EccP384 => "p384",
}
}
}
impl ToStr for ManagementAlgorithmId {
fn to_str(&self) -> &str {
match self {
ManagementAlgorithmId::PinPuk => "pin_puk",
ManagementAlgorithmId::ThreeDes => "three_des",
ManagementAlgorithmId::Asymmetric(algo_id) => algo_id.to_str(),
}
}
}
impl ToStr for Origin {
fn to_str(&self) -> &str {
match self {
Origin::Imported => "imported",
Origin::Generated => "generated",
}
}
}
impl ToStr for Option<Origin> {
fn to_str(&self) -> &str {
match self {
None => "none",
Some(origin) => origin.to_str(),
}
}
}
pub fn get_algorithm_id_by_certificate(certificate: &Certificate) -> XResult<AlgorithmId> {
let tbs_certificate = &certificate.cert.tbs_certificate;
get_algorithm_id(&tbs_certificate.subject_public_key_info)
}
pub fn get_algorithm_id(public_key_info: &SubjectPublicKeyInfoOwned) -> XResult<AlgorithmId> {
if public_key_info.algorithm.oid == RSA {
let rsa_public_key = opt_result!(
RSAPublicKey::from_der(public_key_info.subject_public_key.raw_bytes()), "Parse public key failed: {}");
let starts_with_0 = rsa_public_key.1.modulus.starts_with(&[0]);
let public_key_bits = (rsa_public_key.1.modulus.len() - if starts_with_0 { 1 } else { 0 }) * 8;
if public_key_bits == 1024 {
return Ok(AlgorithmId::Rsa1024);
}
if public_key_bits == 2048 {
return Ok(AlgorithmId::Rsa2048);
}
return simple_error!("Unknown rsa bits: {}", public_key_bits);
}
if public_key_info.algorithm.oid == ECC {
if let Some(any) = &public_key_info.algorithm.parameters {
let any_parameter_der = opt_result!(any.to_der(), "Bad any parameter: {}");
let any_parameter_oid = opt_result!(ObjectIdentifier::from_der(&any_parameter_der), "Bad any parameter der: {}");
if any_parameter_oid == ECC_P256 {
return Ok(AlgorithmId::EccP256);
}
if any_parameter_oid == ECC_P384 {
return Ok(AlgorithmId::EccP384);
}
return simple_error!("Unknown any parameter oid: {}", any_parameter_oid);
}
}
simple_error!("Unknown algorithm: {}", public_key_info.algorithm.oid)
}
pub fn slot_equals(slot_id: &SlotId, slot: &str) -> bool {
get_slot_id(slot).map(|sid| &sid == slot_id).unwrap_or(false)
}
pub fn to_slot_hex(slot: &SlotId) -> String {
let slot_id: u8 = (*slot).into();
format!("{:x}", slot_id)
}
pub fn get_slot_id(slot: &str) -> XResult<SlotId> {
let slot_lower = slot.to_lowercase();
Ok(match slot_lower.as_str() {
"9a" | "auth" | "authentication" => SlotId::Authentication,
"9b" | "management" => SlotId::Management(ManagementSlotId::Management),
"9c" | "sign" | "signature" => SlotId::Signature,
"9d" | "keym" | "keymanagement" => SlotId::KeyManagement,
"9e" | "card" | "cardauthentication" => SlotId::CardAuthentication,
"f9" | "attest" | "attestation" => SlotId::Attestation,
"r1" | "82" => SlotId::Retired(RetiredSlotId::R1),
"r2" | "83" => SlotId::Retired(RetiredSlotId::R2),
"r3" | "84" => SlotId::Retired(RetiredSlotId::R3),
"r4" | "85" => SlotId::Retired(RetiredSlotId::R4),
"r5" | "86" => SlotId::Retired(RetiredSlotId::R5),
"r6" | "87" => SlotId::Retired(RetiredSlotId::R6),
"r7" | "88" => SlotId::Retired(RetiredSlotId::R7),
"r8" | "89" => SlotId::Retired(RetiredSlotId::R8),
"r9" | "8a" => SlotId::Retired(RetiredSlotId::R9),
"r10" | "8b" => SlotId::Retired(RetiredSlotId::R10),
"r11" | "8c" => SlotId::Retired(RetiredSlotId::R11),
"r12" | "8d" => SlotId::Retired(RetiredSlotId::R12),
"r13" | "8e" => SlotId::Retired(RetiredSlotId::R13),
"r14" | "8f" => SlotId::Retired(RetiredSlotId::R14),
"r15" | "90" => SlotId::Retired(RetiredSlotId::R15),
"r16" | "91" => SlotId::Retired(RetiredSlotId::R16),
"r17" | "92" => SlotId::Retired(RetiredSlotId::R17),
"r18" | "93" => SlotId::Retired(RetiredSlotId::R18),
"r19" | "94" => SlotId::Retired(RetiredSlotId::R19),
"r20" | "95" => SlotId::Retired(RetiredSlotId::R20),
_ => return simple_error!("Unknown slot: {}", slot),
})
}
impl FromStr for SlotId {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
get_slot_id(s).ok()
}
}
impl ToStr for SlotId {
fn to_str(&self) -> &str {
match self {
SlotId::Authentication => "authentication",
SlotId::Signature => "signature",
SlotId::KeyManagement => "keymanagement",
SlotId::CardAuthentication => "cardauthentication",
SlotId::Retired(retried) => match retried {
RetiredSlotId::R1 => "r1",
RetiredSlotId::R2 => "r2",
RetiredSlotId::R3 => "r3",
RetiredSlotId::R4 => "r4",
RetiredSlotId::R5 => "r5",
RetiredSlotId::R6 => "r6",
RetiredSlotId::R7 => "r7",
RetiredSlotId::R8 => "r8",
RetiredSlotId::R9 => "r9",
RetiredSlotId::R10 => "r10",
RetiredSlotId::R11 => "r11",
RetiredSlotId::R12 => "r12",
RetiredSlotId::R13 => "r13",
RetiredSlotId::R14 => "r14",
RetiredSlotId::R15 => "r15",
RetiredSlotId::R16 => "r16",
RetiredSlotId::R17 => "r17",
RetiredSlotId::R18 => "r18",
RetiredSlotId::R19 => "r19",
RetiredSlotId::R20 => "r20",
}
SlotId::Attestation => "attestation",
SlotId::Management(management) => match management {
ManagementSlotId::Pin => "pin",
ManagementSlotId::Puk => "puk",
ManagementSlotId::Management => "management",
}
}
}
}
pub fn check_read_pin(yk: &mut YubiKey, slot_id: SlotId, sub_arg_matches: &ArgMatches) -> Option<String> {
if never_use_pin(yk, slot_id) {
None
} else {
pinutil::read_pin(sub_arg_matches)
}
}
pub fn never_use_pin(yk: &mut YubiKey, slot_id: SlotId) -> bool {
match get(yk, slot_id) {
None => false,
Some((pin_policy, _)) => pin_policy == PinPolicy::Never,
}
}
pub fn get(yk: &mut YubiKey, slot_id: SlotId) -> Option<(PinPolicy, TouchPolicy)> {
let slot_metadata = match metadata(yk, slot_id){
Ok(meta) => meta,
Err(_) => return None,
};
slot_metadata.policy
}

84
src/pkiutil.rs Normal file
View File

@@ -0,0 +1,84 @@
use openpgp_card::crypto_data::PublicKeyMaterial;
use openssl::bn::BigNum;
use openssl::rsa::Rsa;
use pem::Pem;
use rust_util::XResult;
#[cfg(feature = "with-sequoia-openpgp")]
use sequoia_openpgp::crypto::mpi::PublicKey;
use x509_parser::x509::AlgorithmIdentifier;
use crate::digestutil::sha256_bytes;
#[derive(Clone, Copy, Debug)]
pub enum PkiAlgorithm {
Rsa,
P256,
P384,
P521,
}
pub fn get_pki_algorithm(algorithm_identifier: &AlgorithmIdentifier) -> XResult<PkiAlgorithm> {
let algorithm_id_string = algorithm_identifier.algorithm.to_id_string();
if "1.2.840.113549.1.1.1" == algorithm_id_string {
return Ok(PkiAlgorithm::Rsa);
}
if "1.2.840.10045.2.1" == algorithm_id_string {
if let Some(parameters) = &algorithm_identifier.parameters {
if let Ok(parameter_oid) = parameters.as_oid() {
let parameter_oid_id_string = parameter_oid.to_id_string();
return match parameter_oid_id_string.as_str() {
"1.2.840.10045.3.1.7" => Ok(PkiAlgorithm::P256),
"1.3.132.0.34" => Ok(PkiAlgorithm::P384),
"1.3.132.0.35" => Ok(PkiAlgorithm::P521),
unknown_ec_oid => simple_error!("Unknown EC curve: {}", unknown_ec_oid),
};
}
}
}
simple_error!("Unknown pki algorithm: {}", algorithm_id_string)
}
pub fn bytes_to_pem<T>(tag: &str, contents: T) -> String where T: Into<Vec<u8>> {
let cert_public_key_pem_obj = Pem::new(tag, contents);
pem::encode(&cert_public_key_pem_obj).trim().to_string()
}
#[cfg(feature = "with-sequoia-openpgp")]
pub fn sequoia_openpgp_public_key_pem(public_key: &PublicKey) -> Option<(Vec<u8>, String)> {
match public_key {
PublicKey::RSA { e, n } => {
Some(rsa_public_key_pem(n.value(), e.value()))
}
_ => {
warning!("Not RSA public key: {:?}", public_key);
None
}
}
}
pub fn openpgp_card_public_key_pem(public_key: &PublicKeyMaterial) -> Option<(Vec<u8>, String)> {
match public_key {
PublicKeyMaterial::R(rsa_pub) => {
Some(rsa_public_key_pem(rsa_pub.n(), rsa_pub.v()))
}
PublicKeyMaterial::E(ecc_pub) => {
let ecc_pub_key_bytes_sha256 = sha256_bytes(ecc_pub.data());
Some((ecc_pub_key_bytes_sha256, format!("hex:{}", hex::encode(ecc_pub.data()))))
}
_ => {
warning!("Unknown public key: {:?}", public_key);
None
}
}
}
pub fn rsa_public_key_pem(n: &[u8], e: &[u8]) -> (Vec<u8>, String) {
let rsa_pub_key = Rsa::from_public_components(
BigNum::from_slice(n).unwrap(),
BigNum::from_slice(e).unwrap(),
);
let rsa_pub_key_bytes = rsa_pub_key.unwrap().public_key_to_der().unwrap();
let rsa_pub_key_bytes_sha256 = sha256_bytes(&rsa_pub_key_bytes);
(rsa_pub_key_bytes_sha256, bytes_to_pem("PUBLIC KEY", rsa_pub_key_bytes))
}

250
src/rsautil.rs Normal file
View File

@@ -0,0 +1,250 @@
use std::collections::HashMap;
use ecdsa::elliptic_curve::rand_core::OsRng;
use openssl::bn::{BigNum, BigNumContext};
use openssl::pkey::PKey;
use openssl::rsa::{Padding, Rsa};
use rsa::{Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
use rust_util::{util_msg, XResult};
use rust_util::util_msg::MessageType;
use spki::DecodePublicKey;
use rsa::pkcs1::DecodeRsaPublicKey;
use rsa::traits::PublicKeyParts;
use spki::EncodePublicKey;
use rsa::pkcs1::LineEnding;
use rsa::pkcs8::EncodePrivateKey;
use sha2::{Sha256, Sha384, Sha512};
use crate::digestutil;
use crate::util::{base64_decode, base64_encode};
pub enum RsaSignAlgorithm {
Rs256,
Rs384,
Rs512,
}
impl RsaSignAlgorithm {
pub fn from_str(alg: &str) -> Option<RsaSignAlgorithm> {
match alg {
"RS256" => Some(RsaSignAlgorithm::Rs256),
"RS384" => Some(RsaSignAlgorithm::Rs384),
"RS512" => Some(RsaSignAlgorithm::Rs512),
_ => None
}
}
}
pub fn sign(rsa_private_key: &RsaPrivateKey, rsa_sign_algorithm: RsaSignAlgorithm, message: &[u8], is_raw: bool) -> XResult<Vec<u8>> {
match rsa_sign_algorithm {
RsaSignAlgorithm::Rs256 => {
let raw_in = iff!(is_raw, digestutil::sha256_bytes(message), message.to_vec());
Ok(rsa_private_key.sign(Pkcs1v15Sign::new::<Sha256>(), &raw_in)?)
}
RsaSignAlgorithm::Rs384 => {
let raw_in = iff!(is_raw, digestutil::sha384_bytes(message), message.to_vec());
Ok(rsa_private_key.sign(Pkcs1v15Sign::new::<Sha384>(), &raw_in)?)
}
RsaSignAlgorithm::Rs512 => {
let raw_in = iff!(is_raw, digestutil::sha512_bytes(message), message.to_vec());
Ok(rsa_private_key.sign(Pkcs1v15Sign::new::<Sha512>(), &raw_in)?)
}
}
}
pub fn generate_rsa_keypair(bit_size: usize) -> XResult<(String, String, String, Vec<u8>, String)> {
let rsa_private_key = opt_result!(RsaPrivateKey::new(&mut OsRng, bit_size), "Generate RSA private key failed: {}");
let rsa_public_key = rsa_private_key.to_public_key();
let secret_key_der_base64 = base64_encode(rsa_private_key.to_pkcs8_der()?.as_bytes());
let secret_key_pem = rsa_private_key.to_pkcs8_pem(LineEnding::LF)?.to_string();
let public_key_pem = rsa_public_key.to_public_key_pem(LineEnding::LF)?;
let public_key_der = rsa_public_key.to_public_key_der()?.to_vec();
let jwk_ec_key = rsa_public_key_to_jwk(&rsa_public_key)?;
Ok((secret_key_der_base64, secret_key_pem, public_key_pem, public_key_der, jwk_ec_key))
}
#[derive(Debug)]
pub struct RsaCrt {
// n = p * q
pub modulus: BigNum,
// e
pub public_exponent: BigNum,
// d = e mod inverse ((p - 1) * (q - 1))
pub private_exponent: BigNum,
// p
pub prime1: BigNum,
// q
pub prime2: BigNum,
// dp = d mod (p1)
pub exponent1: BigNum,
// dq = d mod (q1)
pub exponent2: BigNum,
// qinv = q^1 mod p
pub coefficient: BigNum,
}
impl RsaCrt {
pub fn from(p: BigNum, q: BigNum, e: BigNum) -> XResult<RsaCrt> {
Ok(opt_result!( inner_from(p, q, e), "Calc RsaCrt failed: {}"))
}
pub fn to_public_key_pem(&self) -> XResult<String> {
Ok(crate::pkiutil::rsa_public_key_pem(
clone_big_num(&self.modulus)?.to_vec().as_slice(),
clone_big_num(&self.public_exponent)?.to_vec().as_slice(),
).1)
}
pub fn to_pem(&self) -> XResult<String> {
let private_key = opt_result!(Rsa::from_private_components(
clone_big_num(&self.modulus)?,
clone_big_num(&self.public_exponent)?,
clone_big_num(&self.private_exponent)?,
clone_big_num(&self.prime1)?,
clone_big_num(&self.prime2)?,
clone_big_num(&self.exponent1)?,
clone_big_num(&self.exponent2)?,
clone_big_num(&self.coefficient)?,
), "From private components failed: {}");
let private_pkey = opt_result!(PKey::from_rsa(private_key), "From rsa to pkey failed: {}");
// let k = private_key.private_key_to_pem_passphrase(Cipher::aes_128_gcm(), passphrase);
let private_key_pem = opt_result!(private_pkey.private_key_to_pem_pkcs8(), "Private key to pem failed: {}");
Ok(opt_result!(String::from_utf8(private_key_pem), "Pem to string failed: {}").trim().to_string())
}
}
pub fn clone_big_num(n: &BigNum) -> XResult<BigNum> {
Ok(opt_result!(BigNum::from_slice(n.to_vec().as_slice()), "Clone big num:{}, failed: {}", n))
}
pub fn parse_padding(padding_opt: Option<&str>) -> XResult<Padding> {
Ok(match padding_opt {
Some("oaep") | Some("pkcs1_oaep") => Padding::PKCS1_OAEP,
Some("pss") | Some("pkcs1_pss") => Padding::PKCS1_PSS,
Some("none") => Padding::NONE,
Some("pkcs1") | None => Padding::PKCS1,
Some(p) => return simple_error!("Not supported padding: {}", p),
})
}
pub fn padding_to_string(padding: Padding) -> &'static str {
match padding {
Padding::NONE => "none",
Padding::PKCS1 => "pkcs1",
Padding::PKCS1_PSS => "pkcs1_pss",
Padding::PKCS1_OAEP => "pkcs1_oaep",
_ => "unknown",
}
}
fn inner_from(p: BigNum, q: BigNum, e: BigNum) -> XResult<RsaCrt> {
let mut n = BigNum::new()?;
n.checked_mul(&p, &q, &mut BigNumContext::new().unwrap())?;
let mut p_m1 = clone_big_num(&p)?;
p_m1.sub_word(1)?;
let mut q_m1 = clone_big_num(&q)?;
q_m1.sub_word(1)?;
let mut m = BigNum::new()?;
m.checked_mul(&p_m1, &q_m1, &mut BigNumContext::new().unwrap())?;
let mut d = BigNum::new()?;
d.mod_inverse(&e, &m, &mut BigNumContext::new().unwrap())?;
let mut dp = BigNum::new()?;
dp.nnmod(&d, &p_m1, &mut BigNumContext::new().unwrap())?;
let mut dq = BigNum::new()?;
dq.nnmod(&d, &q_m1, &mut BigNumContext::new().unwrap())?;
let mut qinv = BigNum::new()?;
qinv.mod_inverse(&q, &p, &mut BigNumContext::new().unwrap())?;
Ok(RsaCrt {
modulus: n,
public_exponent: e,
private_exponent: d,
prime1: p,
prime2: q,
exponent1: dp,
exponent2: dq,
coefficient: qinv,
})
}
pub fn pkcs15_sha256_rsa_2048_padding_for_sign(sha256: &[u8]) -> Vec<u8> {
// https://www.ibm.com/docs/en/zos/2.2.0?topic=cryptography-pkcs-1-formats
// MD5 X3020300C 06082A86 4886F70D 02050500 0410 || 16-byte hash value
// SHA-1 X'30213009 06052B0E 03021A05 000414 || 20-byte hash value
// SHA-224 X302D300D 06096086 48016503 04020405 00041C || 28-byte hash value
// SHA-256 X3031300D 06096086 48016503 04020105 000420 || 32-byte hash value
// SHA-384 X3041300D 06096086 48016503 04020205 000430 || 48-byte hash value
// SHA-512 X3051300D 06096086 48016503 04020305 000440 || 64-byte hash value
let sha256_der_prefix = hex::decode("3031300d060960864801650304020105000420").unwrap();
let mut hash_with_oid = Vec::with_capacity(128);
hash_with_oid.extend_from_slice(&sha256_der_prefix);
hash_with_oid.extend_from_slice(sha256);
let hash_padding = pkcs1_padding_for_sign(&hash_with_oid, 2048).unwrap();
util_msg::when(MessageType::DEBUG, || {
debugging!("Hash: {}", hex::encode(sha256));
debugging!("Hash with OID: {}", hex::encode(&hash_with_oid));
debugging!("PKCS1 padding: {}", hex::encode(&hash_padding));
});
hash_padding
}
fn pkcs1_padding_for_sign(bs: &[u8], bit_len: usize) -> XResult<Vec<u8>> {
let byte_len = bit_len / 8;
let max_len = byte_len - (1 + 1 + 8 + 2);
if bs.len() > max_len {
return simple_error!("Length is too large: {} > {}", bs.len(), max_len);
}
let mut output = Vec::<u8>::with_capacity(byte_len);
output.push(0x00);
output.push(0x01);
let ps_len = byte_len - bs.len() - (1 + 1 + 1);
output.extend_from_slice(&vec![0xff_u8; ps_len]);
output.push(0x00);
output.extend_from_slice(bs);
Ok(output)
}
pub fn convert_rsa_to_jwk(public_key: &str) -> XResult<String> {
let rsa_public_key = try_parse_rsa(public_key)?;
rsa_public_key_to_jwk(&rsa_public_key)
}
pub fn rsa_public_key_to_jwk(rsa_public_key: &RsaPublicKey) -> XResult<String> {
let e_bytes = rsa_public_key.e().to_bytes_be();
let n_bytes = rsa_public_key.n().to_bytes_be();
let mut jwk = HashMap::new();
jwk.insert("kty", "RSA".to_string());
jwk.insert("n", base64_encode(&n_bytes));
jwk.insert("e", base64_encode(&e_bytes));
Ok(serde_json::to_string(&jwk).unwrap())
}
pub fn try_parse_rsa(public_key: &str) -> XResult<RsaPublicKey> {
debugging!("Try parse RSA public key PEM.");
// parse RSA public key PEM not works? why?
if let Ok(rsa_public_key) = RsaPublicKey::from_public_key_pem(public_key) {
return Ok(rsa_public_key);
}
debugging!("Try parse RSA PKCS#1 public key PEM.");
if let Ok(rsa_public_key) = RsaPublicKey::from_pkcs1_pem(public_key) {
return Ok(rsa_public_key);
}
if let Ok(public_key_der) = base64_decode(public_key) {
debugging!("Try parse RSA public key DER.");
if let Ok(rsa_public_key) = RsaPublicKey::from_public_key_der(&public_key_der) {
return Ok(rsa_public_key);
}
debugging!("Try parse RSA PKCS#1 public key DER.");
if let Ok(rsa_public_key) = RsaPublicKey::from_pkcs1_der(&public_key_der) {
return Ok(rsa_public_key);
}
}
simple_error!("Invalid RSA public key.")
}

81
src/seutil.rs Normal file
View File

@@ -0,0 +1,81 @@
use crate::util::{base64_decode, base64_encode};
use rust_util::XResult;
use se_tool::KeyPurpose;
use swift_secure_enclave_tool_rs as se_tool;
use swift_secure_enclave_tool_rs::{ControlFlag, DigestType, KeyMlKem};
pub fn is_support_se() -> bool {
se_tool::is_secure_enclave_supported().unwrap_or_else(|e| {
failure!("Invoke command swift-secure-enclave-tool failed: {}", e);
false
})
}
pub fn check_se_supported() -> XResult<()> {
if !is_support_se() {
simple_error!("Secure Enclave is NOT supported.")
} else {
Ok(())
}
}
pub fn generate_secure_enclave_p256_keypair(
sign: bool,
control_flag: ControlFlag,
) -> XResult<(Vec<u8>, Vec<u8>, String)> {
let key_material = if sign {
se_tool::generate_keypair(KeyPurpose::Signing, control_flag)?
} else {
se_tool::generate_keypair(KeyPurpose::KeyAgreement, control_flag)?
};
Ok((
key_material.public_key_point,
key_material.public_key_der,
base64_encode(&key_material.private_key_representation),
))
}
pub fn generate_secure_enclave_mlkem_keypair(
key_ml_kem: KeyMlKem,
control_flag: ControlFlag,
) -> XResult<(Vec<u8>, Vec<u8>, String)> {
let key_material = se_tool::generate_mlkem_keypair(key_ml_kem, control_flag)?;
Ok((
key_material.public_key_point,
key_material.public_key_der,
base64_encode(&key_material.private_key_representation),
))
}
pub fn recover_secure_enclave_p256_public_key(
private_key: &str,
sign: bool,
) -> XResult<(Vec<u8>, Vec<u8>, String)> {
let private_key_representation = base64_decode(private_key)?;
let key_material = if sign {
se_tool::recover_keypair(KeyPurpose::Signing, &private_key_representation)
} else {
se_tool::recover_keypair(KeyPurpose::KeyAgreement, &private_key_representation)
}?;
Ok((
key_material.public_key_point,
key_material.public_key_der,
base64_encode(&key_material.private_key_representation),
))
}
pub fn secure_enclave_p256_dh(
private_key: &str,
ephemeral_public_key_bytes: &[u8],
) -> XResult<Vec<u8>> {
let private_key_representation = base64_decode(private_key)?;
let shared_secret =
se_tool::private_key_ecdh(&private_key_representation, ephemeral_public_key_bytes)?;
Ok(shared_secret)
}
pub fn secure_enclave_p256_sign(private_key: &str, content: &[u8], digest_type: DigestType) -> XResult<Vec<u8>> {
let private_key_representation = base64_decode(private_key)?;
let signature = se_tool::private_key_sign_digested(&private_key_representation, content, digest_type)?;
Ok(signature)
}

111
src/signfile.rs Normal file
View File

@@ -0,0 +1,111 @@
use serde::{Deserialize, Serialize};
pub const SIMPLE_SIG_V1: &str = "v1";
pub const SIMPLE_SIG_SCHEMA: &str = "https://openwebstandard.org/simple-sign-file/v1";
pub const HASH_ALGORITHM_SHA256: &str = "sha256";
pub const SIGNATURE_ALGORITHM_SHA256_WITH_ECDSA: &str = "SHA256withECDSA";
pub const CERTIFICATES_SEARCH_URL: &str = "https://hatter.ink/ca/fetch_certificates.json?fingerprint=";
pub struct SignFileRequest {
pub filename: Option<String>,
pub digest: Vec<u8>,
pub timestamp: i64,
pub attributes: Option<String>,
pub comment: Option<String>,
}
impl SignFileRequest {
pub fn get_tobe_signed(&self) -> Vec<u8> {
let mut tobe_signed = vec![];
// "v1"||TLV(filename)||TLV(timestamp)||TLV(attributes)||TLV(comment)||TLV(digest)
debugging!("Tobe signed version: {}", SIMPLE_SIG_V1);
tobe_signed.extend_from_slice(SIMPLE_SIG_V1.as_bytes());
let tobe_signed_filename = SignFileTlv::Filename(self.filename.clone()).to_byes();
debugging!("Tobe signed filename: {} ({:?})", hex::encode(&tobe_signed_filename), &self.filename);
tobe_signed.extend_from_slice(&tobe_signed_filename);
let tobe_signed_timestamp = SignFileTlv::Timestamp(self.timestamp).to_byes();
debugging!("Tobe signed timestamp: {} ({})", hex::encode(&tobe_signed_timestamp), &self.timestamp);
tobe_signed.extend_from_slice(&tobe_signed_timestamp);
let tobe_signed_attributes = SignFileTlv::Attributes(self.attributes.clone()).to_byes();
debugging!("Tobe signed attributes: {} ({:?})", hex::encode(&tobe_signed_attributes), &self.attributes);
tobe_signed.extend_from_slice(&tobe_signed_attributes);
let tobe_signed_comment = SignFileTlv::Comment(self.comment.clone()).to_byes();
debugging!("Tobe signed comment: {} ({:?})", hex::encode(&tobe_signed_comment), &self.comment);
tobe_signed.extend_from_slice(&tobe_signed_comment);
let tobe_signed_digest = SignFileTlv::Digest(self.digest.clone()).to_byes();
debugging!("Tobe signed file digest: {}", hex::encode(&tobe_signed_digest));
tobe_signed.extend_from_slice(&tobe_signed_digest);
tobe_signed
}
}
pub enum SignFileTlv {
Filename(Option<String>),
Timestamp(i64),
Attributes(Option<String>),
Comment(Option<String>),
Digest(Vec<u8>),
}
impl SignFileTlv {
pub fn tag(&self) -> u8 {
match self {
SignFileTlv::Filename(_) => 0,
SignFileTlv::Timestamp(_) => 1,
SignFileTlv::Attributes(_) => 2,
SignFileTlv::Comment(_) => 3,
SignFileTlv::Digest(_) => 254,
}
}
pub fn to_byes(&self) -> Vec<u8> {
let mut bytes = vec![];
bytes.push(self.tag());
match self {
SignFileTlv::Timestamp(timestamp) => {
bytes.extend_from_slice(&timestamp.to_be_bytes());
}
SignFileTlv::Filename(value)
| SignFileTlv::Attributes(value)
| SignFileTlv::Comment(value) => {
Self::write_bytes(&mut bytes, match value {
None => &[],
Some(value) => value.as_bytes(),
});
}
SignFileTlv::Digest(digest) => {
Self::write_bytes(&mut bytes, digest);
}
}
bytes
}
fn write_bytes(bytes: &mut Vec<u8>, b: &[u8]) {
if b.len() > u16::MAX as usize {
panic!("Cannot more than: {}", u16::MAX);
}
bytes.extend_from_slice(&(b.len() as u16).to_be_bytes());
bytes.extend_from_slice(b);
}
}
#[derive(Serialize, Deserialize)]
pub struct SimpleSignFileSignature {
pub algorithm: String,
pub signature: String,
pub certificates: Vec<String>,
}
#[derive(Serialize, Deserialize)]
pub struct SimpleSignFile {
pub schema: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
pub digest: String,
pub timestamp: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
pub signatures: Vec<SimpleSignFileSignature>,
}

42
src/sshutil.rs Normal file
View File

@@ -0,0 +1,42 @@
use crate::util::base64_encode;
pub fn with_sign(mut vec: Vec<u8>) -> Vec<u8> {
if !vec.is_empty() && vec[0] >= 128 {
vec.insert(0, 0x00);
}
vec
}
pub fn generate_ssh_string(e: &[u8], n: &[u8], comment: &str) -> String {
let mut ssh_key = vec![];
append_slice_with_len(&mut ssh_key, "ssh-rsa".as_bytes());
append_slice_with_len(&mut ssh_key, &with_sign(e.to_vec()));
append_slice_with_len(&mut ssh_key, &with_sign(n.to_vec()));
format!("ssh-rsa {} {}", base64_encode(&ssh_key), comment)
}
pub fn append_slice_with_len(v: &mut Vec<u8>, s: &[u8]) {
v.extend_from_slice(&(s.len() as u32).to_be_bytes()[..]);
v.extend_from_slice(s);
}
pub trait SshVecWriter {
fn write_bytes(&mut self, bytes: &[u8]);
fn write_u32(&mut self, num: u32);
fn write_string(&mut self, bytes: &[u8]);
}
impl SshVecWriter for Vec<u8> {
fn write_bytes(&mut self, bytes: &[u8]) {
self.extend_from_slice(bytes);
}
fn write_u32(&mut self, num: u32) {
self.write_bytes(&num.to_be_bytes());
}
fn write_string(&mut self, bytes: &[u8]) {
self.write_u32(bytes.len() as u32);
self.write_bytes(bytes);
}
}

77
src/util.rs Normal file
View File

@@ -0,0 +1,77 @@
use std::fs;
use std::io::Read;
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use base64::{DecodeError, Engine};
use rust_util::XResult;
use serde::Serialize;
pub fn base64_encode<T: AsRef<[u8]>>(input: T) -> String {
STANDARD.encode(input)
}
pub fn base64_encode_url_safe_no_pad<T: AsRef<[u8]>>(input: T) -> String {
URL_SAFE_NO_PAD.encode(input)
}
pub fn base64_decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
STANDARD.decode(input)
}
pub fn base64_uri_decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
URL_SAFE_NO_PAD.decode(input)
}
pub fn to_pem(header: &str, bytes: &[u8]) -> String {
let mut buf = String::new();
buf.push_str(&format!("-----BEGIN {}-----\n", header));
let bas64ed = base64_encode(bytes);
let len = bas64ed.len();
for (i, c) in bas64ed.chars().enumerate() {
buf.push(c);
if i > 0 && i < len && i % 64 == 0 {
buf.push('\n');
}
}
buf.push_str(&format!("\n-----END {}-----\n", header));
buf
}
pub fn try_decode(input: &str) -> XResult<Vec<u8>> {
match hex::decode(input) {
Ok(v) => Ok(v),
Err(_) => match base64_decode(input) {
Ok(v) => Ok(v),
Err(_) => match base64_uri_decode(input) {
Ok(v) => Ok(v),
Err(e) => simple_error!("decode hex or base64 error: {}", e),
},
},
}
}
pub fn read_stdin() -> XResult<Vec<u8>> {
let mut buffer = vec![];
let mut stdin = std::io::stdin();
opt_result!(stdin.read_to_end(&mut buffer), "Read stdin failed: {}");
Ok(buffer)
}
pub fn read_file_or_stdin(file: &str) -> XResult<Vec<u8>> {
if file == "-" {
read_stdin()
} else {
Ok(opt_result!(
fs::read(file),
"Read file: {} failed: {}",
file
))
}
}
pub fn print_pretty_json<T>(value: &T)
where
T: ?Sized + Serialize,
{
println!("{}", serde_json::to_string_pretty(value).unwrap());
}

58
src/yubikeyutil.rs Normal file
View File

@@ -0,0 +1,58 @@
use crate::pivutil::slot_equals;
use clap::ArgMatches;
use rust_util::XResult;
use yubikey::piv::SlotId;
use yubikey::{Key, Serial, YubiKey};
pub fn open_yubikey_with_args(sub_arg_matches: &ArgMatches) -> XResult<YubiKey> {
let serial_opt = sub_arg_matches.value_of("serial");
open_yubikey_with_serial(&serial_opt)
}
pub fn open_yubikey_with_serial(serial_opt: &Option<&str>) -> XResult<YubiKey> {
match serial_opt {
None => open_yubikey(),
Some(serial) => {
let serial_no: u32 = opt_result!(serial.parse(), "{}");
Ok(opt_result!(
YubiKey::open_by_serial(Serial(serial_no)),
"YubiKey with serial: {} not found: {}",
serial
))
}
}
}
pub fn open_yubikey() -> XResult<YubiKey> {
Ok(opt_result!(YubiKey::open(), "YubiKey not found: {}"))
}
pub fn open_and_find_key(slot_id: &SlotId, sub_arg_matches: &ArgMatches) -> XResult<Option<Key>> {
let mut yk = open_yubikey_with_args(sub_arg_matches)?;
find_key(&mut yk, slot_id)
}
pub fn find_key(yk: &mut YubiKey, slot_id: &SlotId) -> XResult<Option<Key>> {
match Key::list(yk) {
Err(e) => warning!("List keys failed: {}", e),
Ok(keys) => return Ok(filter_key(keys, slot_id)),
}
Ok(None)
}
pub fn find_key_or_error(yk: &mut YubiKey, slot_id: &SlotId) -> XResult<Option<Key>> {
match Key::list(yk) {
Err(e) => simple_error!("List keys failed: {}", e),
Ok(keys) => Ok(filter_key(keys, slot_id)),
}
}
fn filter_key(keys: Vec<Key>, slot_id: &SlotId) -> Option<Key> {
for k in keys {
let slot_str = format!("{:x}", Into::<u8>::into(k.slot()));
if slot_equals(slot_id, &slot_str) {
return Some(k);
}
}
None
}