Compare commits
280 Commits
6be02db2a9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a50024285 | |||
|
07e1671867
|
|||
|
4537b6ece9
|
|||
|
f96144f450
|
|||
|
1d49b7c1c1
|
|||
|
10c38cda8a
|
|||
|
9f544e3cb7
|
|||
|
3d29fe6a6d
|
|||
|
1d23dba248
|
|||
|
6f556cc2d6
|
|||
|
33a6661c3f
|
|||
|
421f2e2ffe
|
|||
|
3647515321
|
|||
|
6bd4d0ba57
|
|||
|
d272904357
|
|||
|
0bc671be7b
|
|||
|
a698a852fd
|
|||
|
febdf659cd
|
|||
|
62110ed7fb
|
|||
|
149650bf15
|
|||
|
5b3e0bc8cb
|
|||
|
f870c07387
|
|||
|
ecf034376d
|
|||
|
488db38387
|
|||
|
f6b9671872
|
|||
|
87e51cc7e4
|
|||
|
bb8d804505
|
|||
|
d104d4405e
|
|||
|
f5a15ca0ae
|
|||
|
28c2f096f7
|
|||
|
9926dbf09d
|
|||
|
b23f4a3a69
|
|||
|
d42bfd4bcc
|
|||
|
f74820903a
|
|||
|
21b5cc8221
|
|||
|
7fa6aa1146
|
|||
|
b4beaa3a75
|
|||
|
fb026c9f21
|
|||
|
4431bff9e6
|
|||
|
58f665823d
|
|||
|
fdf02bc976
|
|||
|
8e4cf5cec8
|
|||
|
0b9ec436ba
|
|||
|
a1ae0ff4dc
|
|||
|
06d2da4ddf
|
|||
|
0513dd2398
|
|||
|
d6ecdb5ed4
|
|||
|
81f7a6d77e
|
|||
|
63fabc6054
|
|||
|
57c3ec57df
|
|||
|
c0ea3b773d
|
|||
|
67568f8f15
|
|||
|
9435b287c8
|
|||
|
96927e0dab
|
|||
|
8894a2156a
|
|||
|
e52e42d48c
|
|||
|
a3541e7b68
|
|||
|
d7f52530df
|
|||
|
3dae02e090
|
|||
|
86489c5d29
|
|||
|
1773186dbf
|
|||
|
cec27e0f88
|
|||
|
9a749b63eb
|
|||
|
fcb10f5efa
|
|||
|
5329108380
|
|||
|
b8f0be2023
|
|||
|
0ac9300262
|
|||
|
c270c2e369
|
|||
|
3af863762f
|
|||
|
21676451fd
|
|||
|
4dca8e0146
|
|||
|
dc56f2df77
|
|||
|
4dac890200
|
|||
|
e7b20abd6d
|
|||
|
492c434f62
|
|||
|
fe30f538ba
|
|||
|
e6409174b6
|
|||
|
bb02c7c823
|
|||
|
e9388eb164
|
|||
|
a6bff6d31c
|
|||
|
6d3298549e
|
|||
|
417e3f6a49
|
|||
|
3a40d7f0ad
|
|||
|
1be5754ed1
|
|||
|
7ec3a705cf
|
|||
|
e2fa3bba9f
|
|||
|
070161c056
|
|||
|
3fb43403aa
|
|||
|
c2b3a779c8
|
|||
|
755d61fa86
|
|||
|
af20f4c4a0
|
|||
|
1582f76cae
|
|||
|
3848b65ff1
|
|||
|
d4fce3f4fc
|
|||
|
8aeb47c66f
|
|||
|
d04038ccd9
|
|||
|
aee2f8d5d3
|
|||
|
25e80661bb
|
|||
|
c5e35fd941
|
|||
|
468701b3ff
|
|||
| 4a48e932d4 | |||
|
8b6056db34
|
|||
|
ea0b091414
|
|||
|
31e710d779
|
|||
|
0894366331
|
|||
|
7d2cf85f89
|
|||
|
04247bb846
|
|||
|
2f3bf2eec7
|
|||
|
c48493f892
|
|||
|
d27f0d5f83
|
|||
|
73b5cc1f5f
|
|||
|
5a1942e150
|
|||
|
ef1f637c83
|
|||
|
a19a7822c4
|
|||
| 3653d2f5f9 | |||
|
4824e2329d
|
|||
|
61804ab029
|
|||
|
cd6e34ffd8
|
|||
|
4406cf6d2e
|
|||
|
d686bbe767
|
|||
|
a12fdfd27f
|
|||
|
776c7eb245
|
|||
|
883e41b2cf
|
|||
|
df6e7eee55
|
|||
|
25b0efd272
|
|||
|
0cda981409
|
|||
|
bf9f228967
|
|||
|
0fec0c25e7
|
|||
|
81940853f9
|
|||
|
a5ea22f622
|
|||
|
8031fca7e7
|
|||
|
0cfe26e238
|
|||
|
81149f6aa2
|
|||
|
582ea8a9bb
|
|||
|
a61782e3fe
|
|||
|
8ec07eb01a
|
|||
|
135631df90
|
|||
|
6f2d4d2369
|
|||
|
50e40522e9
|
|||
|
7be3e51da0
|
|||
|
4fdca28679
|
|||
|
059728137b
|
|||
|
61854b7abf
|
|||
|
cbf127a297
|
|||
|
1d2a00a0c8
|
|||
|
ddc3ff98a2
|
|||
|
87d62eae4a
|
|||
|
f47a4fc90a
|
|||
|
7287193e49
|
|||
|
61f5d8c909
|
|||
|
9fa90827aa
|
|||
|
fed67019aa
|
|||
|
8a4d030d82
|
|||
|
14585af7c6
|
|||
|
6e32dc341c
|
|||
|
7d5af78078
|
|||
|
d4b9b852c1
|
|||
|
32ab2d3d6d
|
|||
|
320664bfa0
|
|||
|
9caee95711
|
|||
|
8ba43f8c6b
|
|||
|
639f87576c
|
|||
|
9404d8e5d7
|
|||
|
0dd5aa59ab
|
|||
|
266cf01930
|
|||
|
b3d8c791c4
|
|||
|
68473e2f01
|
|||
|
e13503116a
|
|||
|
44a0e3512e
|
|||
|
4c5f5b4e13
|
|||
|
bb10a24693
|
|||
|
99d478def5
|
|||
|
4eaf083e07
|
|||
|
dd2e804ee6
|
|||
|
3b3ce7b623
|
|||
|
8d163a5ec2
|
|||
|
e9afbe528e
|
|||
|
5faf29020c
|
|||
|
ed140470ae
|
|||
|
030ccd6cfb
|
|||
|
753e1b0c38
|
|||
|
ebc21e47c4
|
|||
|
4b442ff71a
|
|||
|
cda80af731
|
|||
|
6fbf0943aa
|
|||
|
116abd276b
|
|||
|
b17ab578d5
|
|||
|
f8bf21f549
|
|||
|
d5fd1ad57a
|
|||
|
1fc9b0d1ba
|
|||
|
7cfe51fa96
|
|||
|
2f0042e209
|
|||
|
66fdde9b72
|
|||
|
c43c7011e7
|
|||
|
4f015be358
|
|||
|
aadb4b0530
|
|||
|
b5e65cc54d
|
|||
|
09bb894242
|
|||
|
69529b1813
|
|||
|
7b79f6aa18
|
|||
|
362a4ca81b
|
|||
|
a544daae67
|
|||
|
4b7743a68b
|
|||
|
78824f279b
|
|||
|
81c20a6872
|
|||
|
d90c2ebfee
|
|||
|
1d0fe631a2
|
|||
|
506f34fe79
|
|||
|
c71b6bef89
|
|||
|
1a5173bba9
|
|||
|
d6ffc0fbd0
|
|||
|
fd2f3213fe
|
|||
|
1c08f1c264
|
|||
|
08581ecfa7
|
|||
|
c21a4082dc
|
|||
|
1018d408b4
|
|||
|
0679f73dae
|
|||
|
746fdbb53d
|
|||
|
8e2dce9b39
|
|||
|
4b6352759b
|
|||
|
e6bb1cf292
|
|||
|
ffd0e6233f
|
|||
|
1698e2d9f8
|
|||
|
2ada122834
|
|||
|
7f5a5a7d3c
|
|||
|
9fb0da7d33
|
|||
|
0ad8c12c38
|
|||
|
0af2940cd0
|
|||
|
6c600ae4b0
|
|||
|
709f83ce77
|
|||
|
d3c666de4c
|
|||
|
330759fcb9
|
|||
|
cdc12e38f8
|
|||
|
19a2d58bdb
|
|||
|
8177586645
|
|||
|
689cda9446
|
|||
|
a5bb172bf5
|
|||
|
599cba9bf4
|
|||
|
e08ea3995c
|
|||
|
aeb8279bfa
|
|||
|
39d159d682
|
|||
|
b275d1b58b
|
|||
|
a2e545e89a
|
|||
|
e08282870e
|
|||
|
e42a73b2c9
|
|||
|
b39edfa00b
|
|||
|
7886a0755f
|
|||
|
f2c3dbdcd5
|
|||
|
269eaa9304
|
|||
|
a28f4ddced
|
|||
|
426bc46a09
|
|||
|
7348e6d51f
|
|||
|
90d4723f86
|
|||
|
118a797d7c
|
|||
|
9955341c81
|
|||
|
d9bb0ce106
|
|||
|
8a19502400
|
|||
|
60847e8edf
|
|||
|
1d3745f44c
|
|||
|
6437b955d7
|
|||
|
19c84448fb
|
|||
|
733cd7eb67
|
|||
|
3c883f976e
|
|||
|
7902ec5655
|
|||
|
623f8fb585
|
|||
|
5aadb3f082
|
|||
|
57df2177cc
|
|||
|
07ff1d8187
|
|||
|
a95fdb5d09
|
|||
|
5a09200966
|
|||
|
30b14346e3
|
|||
|
7b847ea569
|
|||
|
74482ae918
|
|||
|
033942c2b6
|
|||
|
003ed28b37
|
|||
|
d7ae185264
|
|||
|
bae59b745e
|
|||
|
9b35e28819
|
|||
|
962750c670
|
|||
|
d597a1722c
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
__external_gitignore/
|
||||||
__*.pem
|
__*.pem
|
||||||
test.txt
|
test.txt
|
||||||
enc.txt
|
enc.txt
|
||||||
@@ -25,6 +26,7 @@ target/
|
|||||||
# Icon must end with two \r
|
# Icon must end with two \r
|
||||||
Icon
|
Icon
|
||||||
|
|
||||||
|
|
||||||
# Thumbnails
|
# Thumbnails
|
||||||
._*
|
._*
|
||||||
|
|
||||||
|
|||||||
4703
Cargo.lock
generated
4703
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
69
Cargo.toml
69
Cargo.toml
@@ -1,33 +1,76 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "card-cli"
|
name = "card-cli"
|
||||||
version = "1.2.1"
|
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"] }
|
||||||
digest = "0.10"
|
digest = "0.10"
|
||||||
sha1 = "0.6"
|
sha1 = "0.10"
|
||||||
sha2 = "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.2"
|
openpgp-card-pcsc = "0.3"
|
||||||
openpgp-card-pcsc = "0.2"
|
openpgp-card-sequoia = { version = "0.2", optional = true }
|
||||||
openpgp-card-sequoia = "0.0.8"
|
sequoia-openpgp = { version = "2.0", optional = true }
|
||||||
sequoia-openpgp = "1.3.0"
|
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
simpledateformat = "0.1"
|
simpledateformat = "0.1"
|
||||||
ring = "0.16"
|
ring = "0.17"
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
pem = "1.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.13"
|
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
17
LICENSE.simple-sig
Normal 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-----"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
172
README.md
172
README.md
@@ -1,12 +1,17 @@
|
|||||||
# 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
|
||||||
|
|
||||||
## encrypt & decrypt
|
## encrypt & decrypt
|
||||||
@@ -30,28 +35,36 @@ 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 & verify
|
## 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
|
OR
|
||||||
|
|
||||||
$ cargo r -- pgp-card-sign --in test.txt --use-sha256
|
$ 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
|
||||||
```
|
```
|
||||||
@@ -109,3 +122,144 @@ vjG+EPEF3g8ywKaS8mZQX+sCAwEAAQ==
|
|||||||
-----END PUBLIC KEY-----
|
-----END PUBLIC KEY-----
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# piv-ecdh
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ card-cli piv-ecdh --public-256 --public-key-point-hex 04dd3eebd906c9cf00b08ec29f7ed61804d1cc1d1352d9257b628191e08fc3717c4fae3298cd5c4829cec8bf3a946e7db60b7857e1287f6a0bae6b3f2342f007d0 --json
|
||||||
|
{
|
||||||
|
"epk_point_hex": "04bbb6a458e81d2c646587118abfb029ff715db366f92a1d0468887f9947f176c11961eccebd5b9cbbb8b67e33fa8d3f0010a4aaf5010d0f419f1f99b4c2d7aa56",
|
||||||
|
"pk_point_hex": "04dd3eebd906c9cf00b08ec29f7ed61804d1cc1d1352d9257b628191e08fc3717c4fae3298cd5c4829cec8bf3a946e7db60b7857e1287f6a0bae6b3f2342f007d0",
|
||||||
|
"shared_secret_hex": "58069f1b2ce85c4f2232070567bef99f71b45f69ab321c4c782e599813b56f25"
|
||||||
|
}
|
||||||
|
|
||||||
|
$ card-cli piv-ecdh --private --slot 82 --epk 04bbb6a458e81d2c646587118abfb029ff715db366f92a1d0468887f9947f176c11961eccebd5b9cbbb8b67e33fa8d3f0010a4aaf5010d0f419f1f99b4c2d7aa56 --json
|
||||||
|
[WARN ] Get slot: 82 meta data failed
|
||||||
|
{
|
||||||
|
"shared_secret_hex": "58069f1b2ce85c4f2232070567bef99f71b45f69ab321c4c782e599813b56f25"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# piv-ecsign
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ 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 & 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 <<Cryptography external command specification>><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
31
examples/hmac_sha1.rs
Normal 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(())
|
||||||
|
}
|
||||||
26
examples/rsa.rs
Normal file
26
examples/rsa.rs
Normal 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()));
|
||||||
|
}
|
||||||
225
examples/ssh_agent.rs
Normal file
225
examples/ssh_agent.rs
Normal 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
|
||||||
|
}
|
||||||
20
justfile
20
justfile
@@ -1,6 +1,22 @@
|
|||||||
_:
|
_:
|
||||||
@just --list
|
@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
|
# run --help
|
||||||
help:
|
help:
|
||||||
cargo r -- --help
|
cargo r -- --help
|
||||||
@@ -13,3 +29,7 @@ pgp-list:
|
|||||||
example-rsa-encrypt:
|
example-rsa-encrypt:
|
||||||
cargo r --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
47
src/argsutil.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
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)); }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warning!("YubiKey not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,28 +36,31 @@ 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(_) => {
|
||||||
let config = Config::default()
|
warning!("YubiKey not found");
|
||||||
.set_vendor_id(device.vendor_id)
|
return Ok(Some(1));
|
||||||
.set_product_id(device.product_id)
|
|
||||||
.set_command(yubico_manager::config::Command::Configuration2);
|
|
||||||
|
|
||||||
let hmac_key: HmacKey = HmacKey::from_slice(&secret_bytes);
|
|
||||||
let button_press = sub_arg_matches.is_present("button-press");
|
|
||||||
information!("Button press: {}", button_press);
|
|
||||||
|
|
||||||
let mut device_config = DeviceModeConfig::default();
|
|
||||||
device_config.challenge_response_hmac(&hmac_key, false, button_press);
|
|
||||||
|
|
||||||
if let Err(err) = yubi.write_config(config, &mut device_config) {
|
|
||||||
failure!("Config device failed: {:?}", err);
|
|
||||||
} else {
|
|
||||||
success!("Device configured");
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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_command(yubico_manager::config::Command::Configuration2);
|
||||||
|
|
||||||
|
let hmac_key: HmacKey = HmacKey::from_slice(&secret_bytes);
|
||||||
|
let button_press = sub_arg_matches.is_present("button-press");
|
||||||
|
information!("Button press: {}", button_press);
|
||||||
|
|
||||||
|
let mut device_config = DeviceModeConfig::default();
|
||||||
|
device_config.challenge_response_hmac(&hmac_key, false, button_press);
|
||||||
|
|
||||||
|
if let Err(err) = yubi.write_config(config, &mut device_config) {
|
||||||
|
failure!("Config device failed: {:?}", err);
|
||||||
} else {
|
} else {
|
||||||
warning!("YubiKey not found");
|
success!("Device configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
46
src/cmd_convert_jwk_to_pem.rs
Normal file
46
src/cmd_convert_jwk_to_pem.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/cmd_convert_pem_to_jwk.rs
Normal file
42
src/cmd_convert_pem_to_jwk.rs
Normal 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
77
src/cmd_ec_verify.rs
Normal 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
142
src/cmd_external_ecdh.rs
Normal 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, ¶meter, ephemeral_public_key_bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/cmd_external_public_key.rs
Normal file
121
src/cmd_external_public_key.rs
Normal 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, ¶meter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/cmd_external_sign.rs
Normal file
169
src/cmd_external_sign.rs
Normal 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, ¶meter, 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
40
src/cmd_external_spec.rs
Normal 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
193
src/cmd_file_sign.rs
Normal 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
129
src/cmd_file_verify.rs
Normal 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
70
src/cmd_hmac_decrypt.rs
Normal 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
81
src/cmd_hmac_encrypt.rs
Normal 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
49
src/cmd_hmac_sha1.rs
Normal 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
169
src/cmd_keypair_generate.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/cmd_keypair_keychain_export.rs
Normal file
38
src/cmd_keypair_keychain_export.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/cmd_keypair_keychain_import.rs
Normal file
42
src/cmd_keypair_keychain_import.rs
Normal 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
77
src/cmd_list.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/cmd_parseecdsasignature.rs
Normal file
58
src/cmd_parseecdsasignature.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
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 rust_util::util_clap::{Command, CommandError};
|
use rust_util::util_clap::{Command, CommandError};
|
||||||
@@ -21,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 {
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
73
src/cmd_pgp_age_address.rs
Normal file
73
src/cmd_pgp_age_address.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||||
use openpgp_card::card_do::{Lang, Sex};
|
use openpgp_card::card_do::{Lang, Sex};
|
||||||
use openpgp_card::OpenPgp;
|
|
||||||
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"))
|
||||||
@@ -21,11 +22,12 @@ impl Command for CommandImpl {
|
|||||||
|
|
||||||
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_else(|| 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 = pinutil::get_pin(pin_opt);
|
||||||
|
let pin_opt = pin_opt.as_deref();
|
||||||
let pin = opt_value_result!(pin_opt, "Pin must be assigned");
|
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 mut card = crate::pgpcardutil::get_card()?;
|
let mut pgp = pgpcardutil::get_openpgp_card()?;
|
||||||
let mut pgp = OpenPgp::new(&mut card);
|
|
||||||
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
|
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
|
||||||
|
|
||||||
if sub_arg_matches.is_present("reset") {
|
if sub_arg_matches.is_present("reset") {
|
||||||
95
src/cmd_pgp_card_decrypt.rs
Normal file
95
src/cmd_pgp_card_decrypt.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ use clap::{App, Arg, ArgMatches, SubCommand};
|
|||||||
use openpgp_card::{KeyType, OpenPgp};
|
use openpgp_card::{KeyType, OpenPgp};
|
||||||
use openpgp_card_pcsc::PcscBackend;
|
use openpgp_card_pcsc::PcscBackend;
|
||||||
use rust_util::util_clap::{Command, CommandError};
|
use rust_util::util_clap::{Command, CommandError};
|
||||||
|
use crate::{cmdutil, util};
|
||||||
use crate::pkiutil::openpgp_card_public_key_pem as public_key_pem;
|
use crate::pkiutil::openpgp_card_public_key_pem as public_key_pem;
|
||||||
|
|
||||||
pub struct CommandImpl;
|
pub struct CommandImpl;
|
||||||
@@ -13,23 +13,23 @@ impl Command for CommandImpl {
|
|||||||
fn name(&self) -> &str { "pgp-card-list" }
|
fn name(&self) -> &str { "pgp-card-list" }
|
||||||
|
|
||||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||||
SubCommand::with_name(self.name()).about("OpenPGP Card List subcommand")
|
SubCommand::with_name(self.name()).about("OpenPGP Card list 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("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 = cmdutil::check_json_output(sub_arg_matches);
|
||||||
|
|
||||||
let detail_output = sub_arg_matches.is_present("detail");
|
let detail_output = sub_arg_matches.is_present("detail");
|
||||||
let json_output = sub_arg_matches.is_present("json");
|
|
||||||
if json_output { rust_util::util_msg::set_logger_std_out(false); }
|
|
||||||
|
|
||||||
let mut jsons = vec![];
|
let mut jsons = vec![];
|
||||||
let cards = opt_result!(PcscBackend::cards(None), "Failed to list OpenPGP cards: {}");
|
let cards = opt_result!(PcscBackend::cards(None), "Failed to list OpenPGP cards: {}");
|
||||||
|
|
||||||
information!("Found {} card(s)", cards.len());
|
information!("Found {} card(s)", cards.len());
|
||||||
for (i, mut card) in cards.into_iter().enumerate() {
|
for (i, card) in cards.into_iter().enumerate() {
|
||||||
let mut json = BTreeMap::new();
|
let mut json = BTreeMap::new();
|
||||||
let mut pgp = OpenPgp::new(&mut card);
|
let mut pgp = OpenPgp::new(card);
|
||||||
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
|
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
|
||||||
if let Ok(application_related_data) = trans.application_related_data() {
|
if let Ok(application_related_data) = trans.application_related_data() {
|
||||||
success!("Found card #{}: {:?}", i, application_related_data.application_id());
|
success!("Found card #{}: {:?}", i, application_related_data.application_id());
|
||||||
@@ -94,7 +94,7 @@ impl Command for CommandImpl {
|
|||||||
iff!(card_holder_outputs.is_empty(), "".to_string(), card_holder_outputs.join(", ")));
|
iff!(card_holder_outputs.is_empty(), "".to_string(), card_holder_outputs.join(", ")));
|
||||||
}
|
}
|
||||||
if let Ok(Some(algo_info)) = trans.algorithm_information() {
|
if let Ok(Some(algo_info)) = trans.algorithm_information() {
|
||||||
information!("Algo info: {}", algo_info);
|
debugging!("Algo info: {}", algo_info);
|
||||||
}
|
}
|
||||||
if let Ok(application_related_data) = trans.application_related_data() {
|
if let Ok(application_related_data) = trans.application_related_data() {
|
||||||
if let Ok(fingerprints) = application_related_data.fingerprints() {
|
if let Ok(fingerprints) = application_related_data.fingerprints() {
|
||||||
@@ -120,6 +120,7 @@ impl Command for CommandImpl {
|
|||||||
if let Some((public_key_sha256, public_key_pem)) = public_key_pem(&public_key) {
|
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 sha256: {}", tag1, hex::encode(&public_key_sha256));
|
||||||
information!("{} public key: {}", tag1, public_key_pem.trim());
|
information!("{} public key: {}", tag1, public_key_pem.trim());
|
||||||
|
information!("{} public key: {}", tag1, public_key);
|
||||||
if json_output {
|
if json_output {
|
||||||
json.insert(format!("{}_public_key_sha256", tag2), hex::encode(&public_key_sha256));
|
json.insert(format!("{}_public_key_sha256", tag2), hex::encode(&public_key_sha256));
|
||||||
json.insert(format!("{}_public_key_pem", tag2), public_key_pem);
|
json.insert(format!("{}_public_key_pem", tag2), public_key_pem);
|
||||||
@@ -134,7 +135,7 @@ impl Command for CommandImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if json_output {
|
if json_output {
|
||||||
println!("{}", serde_json::to_string_pretty(&jsons).unwrap());
|
util::print_pretty_json(&jsons);
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ use rust_util::util_clap::{Command, CommandError};
|
|||||||
use rust_util::XResult;
|
use rust_util::XResult;
|
||||||
use sequoia_openpgp as openpgp;
|
use sequoia_openpgp as openpgp;
|
||||||
|
|
||||||
|
use crate::pinutil;
|
||||||
use crate::rsautil::RsaCrt;
|
use crate::rsautil::RsaCrt;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -127,8 +128,8 @@ impl Command for CommandImpl {
|
|||||||
fn name(&self) -> &str { "pgp-card-make" }
|
fn name(&self) -> &str { "pgp-card-make" }
|
||||||
|
|
||||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||||
SubCommand::with_name(self.name()).about("OpenPGP Card Make subcommand")
|
SubCommand::with_name(self.name()).about("OpenPGP Card make 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).required(false).help("Password for PGP secret key"))
|
.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("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("force-make").long("force-make").help("Force make OpenPGP card"))
|
||||||
@@ -138,6 +139,8 @@ impl Command for CommandImpl {
|
|||||||
|
|
||||||
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("pin");
|
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");
|
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()); }
|
||||||
|
|
||||||
@@ -159,6 +162,9 @@ impl Command for CommandImpl {
|
|||||||
if last_pgp_rsa_private_key.is_some() { return simple_error!("Last PGP RSA private key is not none"); }
|
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);
|
last_pgp_rsa_private_key.replace(pgp_rsa_private_key);
|
||||||
} else if let Packet::Signature(signature) = &pp.packet {
|
} 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 Signature::V4(signature_v4) = signature {
|
||||||
if let Some(sub_package) = signature_v4.hashed_area().subpacket(SubpacketTag::KeyFlags) {
|
if let Some(sub_package) = signature_v4.hashed_area().subpacket(SubpacketTag::KeyFlags) {
|
||||||
if let SubpacketValue::KeyFlags(key_flags) = sub_package.value() {
|
if let SubpacketValue::KeyFlags(key_flags) = sub_package.value() {
|
||||||
@@ -234,8 +240,8 @@ impl Command for CommandImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
warning!("Force make is ON, try to write private keys to card!");
|
warning!("Force make is ON, try to write private keys to card!");
|
||||||
let mut card = crate::pgpcardutil::get_card()?;
|
let card = crate::pgpcardutil::get_card()?;
|
||||||
let mut pgp = OpenPgp::new(&mut card);
|
let mut pgp = OpenPgp::new(card);
|
||||||
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
|
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
|
||||||
|
|
||||||
opt_result!(trans.verify_pw3(pin.as_ref()), "Admin pin verify failed: {}");
|
opt_result!(trans.verify_pw3(pin.as_ref()), "Admin pin verify failed: {}");
|
||||||
@@ -1,25 +1,45 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{ErrorKind, Read};
|
use std::io::{ErrorKind, Read};
|
||||||
|
use std::ops::Deref;
|
||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||||
use digest::Digest;
|
use digest::Digest;
|
||||||
use openpgp_card::crypto_data::Hash;
|
use openpgp_card::crypto_data::Hash;
|
||||||
use openpgp_card::OpenPgp;
|
|
||||||
use rust_util::util_clap::{Command, CommandError};
|
|
||||||
use rust_util::XResult;
|
use rust_util::XResult;
|
||||||
|
use rust_util::util_clap::{Command, CommandError};
|
||||||
use sha2::{Sha256, Sha384, Sha512};
|
use sha2::{Sha256, Sha384, Sha512};
|
||||||
|
|
||||||
|
use crate::{cmdutil, pgpcardutil, pinutil, util};
|
||||||
|
use crate::util::base64_encode;
|
||||||
|
|
||||||
const BUFF_SIZE: usize = 512 * 1024;
|
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;
|
pub struct CommandImpl;
|
||||||
|
|
||||||
impl Command for CommandImpl {
|
impl Command for CommandImpl {
|
||||||
fn name(&self) -> &str { "pgp-card-sign" }
|
fn name(&self) -> &str { "pgp-card-sign" }
|
||||||
|
|
||||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||||
SubCommand::with_name(self.name()).about("OpenPGP Card Sign subcommand")
|
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("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("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("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("sha384").short("3").long("sha384").takes_value(true).help("Digest SHA384 HEX"))
|
||||||
@@ -28,14 +48,16 @@ impl Command for CommandImpl {
|
|||||||
.arg(Arg::with_name("use-sha256").long("use-sha256").help("Use SHA256 for 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-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("use-sha512").long("use-sha512").help("Use SHA512 for file in"))
|
||||||
.arg(Arg::with_name("json").long("json").help("JSON output"))
|
.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 {
|
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 pin_opt = sub_arg_matches.value_of("pass").or_else(|| 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 = pinutil::get_pin(pin_opt);
|
||||||
|
let pin_opt = pin_opt.as_deref();
|
||||||
let pin = opt_value_result!(pin_opt, "User pin must be assigned");
|
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()); }
|
if pin.len() < 6 { return simple_error!("User pin length:{}, must >= 6!", pin.len()); }
|
||||||
|
|
||||||
@@ -44,6 +66,9 @@ impl Command for CommandImpl {
|
|||||||
let mut sha512 = sub_arg_matches.value_of("sha512").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 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();
|
let mut json = BTreeMap::new();
|
||||||
if let Some(file_in) = file_in_opt {
|
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"); }
|
if sha256.is_some() || sha384.is_some() || sha512.is_some() { return simple_error!("Conflict --in vs --sha256, --sha384, --sha512 args"); }
|
||||||
@@ -58,15 +83,21 @@ impl Command for CommandImpl {
|
|||||||
|
|
||||||
if use_sha256 {
|
if use_sha256 {
|
||||||
let hash = opt_result!(calc_file_digest::<Sha256>(file_in), "Calc file: {} SHA256 failed: {}", file_in);
|
let hash = opt_result!(calc_file_digest::<Sha256>(file_in), "Calc file: {} SHA256 failed: {}", file_in);
|
||||||
sha256 = Some(hex::encode(hash));
|
let hash_hex = hex::encode(hash);
|
||||||
|
information!("File SHA256 hex: {}", &hash_hex);
|
||||||
|
sha256 = Some(hash_hex);
|
||||||
}
|
}
|
||||||
if use_sha384 {
|
if use_sha384 {
|
||||||
let hash = opt_result!(calc_file_digest::<Sha384>(file_in), "Calc file: {} SHA384 failed: {}", file_in);
|
let hash = opt_result!(calc_file_digest::<Sha384>(file_in), "Calc file: {} SHA384 failed: {}", file_in);
|
||||||
sha384 = Some(hex::encode(hash));
|
let hash_hex = hex::encode(hash);
|
||||||
|
information!("File SHA384 hex: {}", &hash_hex);
|
||||||
|
sha384 = Some(hash_hex);
|
||||||
}
|
}
|
||||||
if use_sha512 {
|
if use_sha512 {
|
||||||
let hash = opt_result!(calc_file_digest::<Sha512>(file_in), "Calc file: {} SHA512 failed: {}", file_in);
|
let hash = opt_result!(calc_file_digest::<Sha512>(file_in), "Calc file: {} SHA512 failed: {}", file_in);
|
||||||
sha512 = Some(hex::encode(hash));
|
let hash_hex = hex::encode(hash);
|
||||||
|
information!("File SHA512 hex: {}", &hash_hex);
|
||||||
|
sha512 = Some(hash_hex);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut entry = BTreeMap::new();
|
let mut entry = BTreeMap::new();
|
||||||
@@ -78,65 +109,79 @@ impl Command for CommandImpl {
|
|||||||
return simple_error!("SHA256, SHA384 or SHA512 must assign at least one");
|
return simple_error!("SHA256, SHA384 or SHA512 must assign at least one");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut card = crate::pgpcardutil::get_card()?;
|
let mut pgp = pgpcardutil::get_openpgp_card()?;
|
||||||
let mut pgp = OpenPgp::new(&mut card);
|
|
||||||
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
|
let mut trans = opt_result!(pgp.transaction(), "Open card failed: {}");
|
||||||
|
|
||||||
if let Some(sha256) = sha256 {
|
if let Some(sha256) = sha256 {
|
||||||
let sha256_hex = opt_result!(hex::decode(sha256.trim()), "Decode sha256 failed: {}");
|
let sha256_hex = opt_result!(hex::decode(sha256.trim()), "Decode sha256 failed: {}");
|
||||||
let sha256_hex = crate::digest::copy_sha256(&sha256_hex)?;
|
let sha256_hex = crate::digestutil::copy_sha256(&sha256_hex)?;
|
||||||
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
|
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
|
||||||
success!("User sign pin verify success!");
|
success!("User sign pin verify success!");
|
||||||
let sig = trans.signature_for_hash(Hash::SHA256(sha256_hex))?;
|
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 HEX: {}", hex::encode(&sig));
|
||||||
success!("SHA256 signature base64: {}", base64::encode(&sig));
|
success!("SHA256 signature base64: {}", base64_encode(&sig));
|
||||||
if json_output {
|
if json_output {
|
||||||
let mut entry = BTreeMap::new();
|
let mut entry = BTreeMap::new();
|
||||||
entry.insert("digest", hex::encode(&sha256_hex));
|
entry.insert("digest", hex::encode(sha256_hex));
|
||||||
entry.insert("signature", hex::encode(&sig));
|
entry.insert("signature", hex::encode(&sig));
|
||||||
json.insert("sha256", entry);
|
json.insert("sha256", entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(sha384) = sha384 {
|
if let Some(sha384) = sha384 {
|
||||||
let sha384_hex = opt_result!(hex::decode(sha384.trim()), "Decode sha384 failed: {}");
|
let sha384_hex = opt_result!(hex::decode(sha384.trim()), "Decode sha384 failed: {}");
|
||||||
let sha384_hex = crate::digest::copy_sha384(&sha384_hex)?;
|
let sha384_hex = crate::digestutil::copy_sha384(&sha384_hex)?;
|
||||||
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
|
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
|
||||||
success!("User sign pin verify success!");
|
success!("User sign pin verify success!");
|
||||||
let sig = trans.signature_for_hash(Hash::SHA384(sha384_hex))?;
|
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 HEX: {}", hex::encode(&sig));
|
||||||
success!("SHA384 signature base64: {}", base64::encode(&sig));
|
success!("SHA384 signature base64: {}", base64_encode(&sig));
|
||||||
if json_output {
|
if json_output {
|
||||||
let mut entry = BTreeMap::new();
|
let mut entry = BTreeMap::new();
|
||||||
entry.insert("digest", hex::encode(&sha384_hex));
|
entry.insert("digest", hex::encode(sha384_hex));
|
||||||
entry.insert("signature", hex::encode(&sig));
|
entry.insert("signature", hex::encode(&sig));
|
||||||
json.insert("sha384", entry);
|
json.insert("sha384", entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(sha512) = sha512 {
|
if let Some(sha512) = sha512 {
|
||||||
let sha512_hex = opt_result!(hex::decode(sha512.trim()), "Decode sha512 failed: {}");
|
let sha512_hex = opt_result!(hex::decode(sha512.trim()), "Decode sha512 failed: {}");
|
||||||
let sha512_hex = crate::digest::copy_sha512(&sha512_hex)?;
|
let sha512_hex = crate::digestutil::copy_sha512(&sha512_hex)?;
|
||||||
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
|
opt_result!(trans.verify_pw1_sign(pin.as_ref()), "User sign pin verify failed: {}");
|
||||||
success!("User sign pin verify success!");
|
success!("User sign pin verify success!");
|
||||||
let sig = trans.signature_for_hash(Hash::SHA512(sha512_hex))?;
|
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 HEX: {}", hex::encode(&sig));
|
||||||
success!("SHA512 signature base64: {}", base64::encode(&sig));
|
success!("SHA512 signature base64: {}", base64_encode(&sig));
|
||||||
if json_output {
|
if json_output {
|
||||||
let mut entry = BTreeMap::new();
|
let mut entry = BTreeMap::new();
|
||||||
entry.insert("digest", hex::encode(&sha512_hex));
|
entry.insert("digest", hex::encode(sha512_hex));
|
||||||
entry.insert("signature", hex::encode(&sig));
|
entry.insert("signature", hex::encode(&sig));
|
||||||
json.insert("sha512", entry);
|
json.insert("sha512", entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if json_output {
|
if json_output {
|
||||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
util::print_pretty_json(&json);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calc_file_digest<D>(file_name: &str) -> XResult<Vec<u8>> where D: Digest {
|
fn calc_file_digest<D>(file_name: &str) -> XResult<Vec<u8>>
|
||||||
|
where
|
||||||
|
D: Digest,
|
||||||
|
{
|
||||||
let mut hasher = D::new();
|
let mut hasher = D::new();
|
||||||
let mut buf: [u8; BUFF_SIZE] = [0u8; BUFF_SIZE];
|
let mut buf: [u8; BUFF_SIZE] = [0u8; BUFF_SIZE];
|
||||||
let mut f = File::open(file_name)?;
|
let mut f = File::open(file_name)?;
|
||||||
@@ -144,7 +189,7 @@ fn calc_file_digest<D>(file_name: &str) -> XResult<Vec<u8>> where D: Digest {
|
|||||||
debugging!("File: {}, length: {}", file_name, file_len);
|
debugging!("File: {}, length: {}", file_name, file_len);
|
||||||
loop {
|
loop {
|
||||||
let len = match f.read(&mut buf) {
|
let len = match f.read(&mut buf) {
|
||||||
Ok(0) => return Ok(hasher.finalize().as_slice().to_vec()),
|
Ok(0) => return Ok(hasher.finalize().deref().to_vec()),
|
||||||
Ok(len) => len,
|
Ok(len) => len,
|
||||||
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
|
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
|
||||||
Err(e) => return simple_error!("Calc file digest failed: {}", e),
|
Err(e) => return simple_error!("Calc file digest failed: {}", e),
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
|
||||||
use openpgp_card::OpenPgp;
|
|
||||||
use openpgp_card::crypto_data::Cryptogram;
|
|
||||||
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_else(|| 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 mut card = crate::pgpcardutil::get_card()?;
|
|
||||||
let mut pgp = OpenPgp::new(&mut 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 = trans.decipher(Cryptogram::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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,11 +6,13 @@ use digest::Digest;
|
|||||||
use rust_util::util_clap::{Command, CommandError};
|
use rust_util::util_clap::{Command, CommandError};
|
||||||
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::pkiutil::bytes_to_pem;
|
use crate::pivutil::get_algorithm_id;
|
||||||
|
use crate::pkiutil::{bytes_to_pem, get_pki_algorithm};
|
||||||
|
|
||||||
pub struct CommandImpl;
|
pub struct CommandImpl;
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ 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"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +31,7 @@ impl Command for CommandImpl {
|
|||||||
let detail_output = sub_arg_matches.is_present("detail");
|
let detail_output = sub_arg_matches.is_present("detail");
|
||||||
let show_config = sub_arg_matches.is_present("show-config");
|
let show_config = sub_arg_matches.is_present("show-config");
|
||||||
|
|
||||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
let mut yk = yubikeyutil::open_yubikey_with_args(sub_arg_matches)?;
|
||||||
success!("Name: {}", yk.name());
|
success!("Name: {}", yk.name());
|
||||||
information!("Version: {}", yk.version());
|
information!("Version: {}", yk.version());
|
||||||
information!("Serial: {}", yk.serial());
|
information!("Serial: {}", yk.serial());
|
||||||
@@ -50,10 +53,20 @@ impl Command for CommandImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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());
|
||||||
if detail_output {
|
keys.sort_by_key(|a| u8::from(a.slot()));
|
||||||
keys.iter().for_each(|k| debugging!("Found key: {:?}", k));
|
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 {
|
||||||
|
debugging!("Found key: {:?}", k);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => failure!("Get PIV keys failed: {}", e)
|
Err(e) => failure!("Get PIV keys failed: {}", e)
|
||||||
@@ -78,13 +91,17 @@ 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.as_ref();
|
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 certificate_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}, algorithm: {:?}", slot, slot_id, cert.subject_pki().algorithm());
|
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);
|
||||||
|
|
||||||
if detail_output {
|
if detail_output {
|
||||||
information!("{}", bytes_to_pem("CERTIFICATE", buf));
|
information!("{}", bytes_to_pem("CERTIFICATE", buf));
|
||||||
@@ -92,7 +109,10 @@ fn print_cert_info(yubikey: &mut YubiKey, slot: SlotId, detail_output: bool) ->
|
|||||||
|
|
||||||
match parse_x509_certificate(buf) {
|
match parse_x509_certificate(buf) {
|
||||||
Ok((_rem, cert)) => {
|
Ok((_rem, cert)) => {
|
||||||
information!("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));
|
||||||
|
|
||||||
|
debugging!("Public key: {}", hex::encode(&cert.tbs_certificate.subject_pki.subject_public_key));
|
||||||
|
|
||||||
let public_key_fingerprint_sha256 = Sha256::digest(cert.tbs_certificate.subject_pki.raw);
|
let public_key_fingerprint_sha256 = Sha256::digest(cert.tbs_certificate.subject_pki.raw);
|
||||||
|
|
||||||
@@ -104,7 +124,7 @@ fn print_cert_info(yubikey: &mut YubiKey, slot: SlotId, detail_output: bool) ->
|
|||||||
information!("Issuer: {}", cert.tbs_certificate.issuer);
|
information!("Issuer: {}", cert.tbs_certificate.issuer);
|
||||||
information!("Certificate fingerprint(SHA256): {}", hex::encode(certificate_fingerprint_sha256));
|
information!("Certificate fingerprint(SHA256): {}", hex::encode(certificate_fingerprint_sha256));
|
||||||
information!("Public key fingerprint(SHA256): {}", hex::encode(public_key_fingerprint_sha256));
|
information!("Public key fingerprint(SHA256): {}", hex::encode(public_key_fingerprint_sha256));
|
||||||
information!("Not Before: {}", cert.tbs_certificate.validity.not_before.to_rfc2822());
|
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();
|
||||||
@@ -116,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));
|
||||||
}
|
}
|
||||||
information!("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
81
src/cmd_piv_decrypt.rs
Normal 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
134
src/cmd_piv_ecdh.rs
Normal 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
88
src/cmd_piv_ecsign.rs
Normal 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
42
src/cmd_piv_generate.rs
Normal 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
150
src/cmd_piv_meta.rs
Normal 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
67
src/cmd_piv_rsasign.rs
Normal 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
182
src/cmd_piv_summary.rs
Normal 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
104
src/cmd_piv_verify.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,68 +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-decrypt" }
|
|
||||||
|
|
||||||
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("encrypted-data").long("encrypted-data").takes_value(true).help("Encrypted data"))
|
|
||||||
.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("pin");
|
|
||||||
let pin = opt_value_result!(pin_opt, "User pin must be assigned");
|
|
||||||
|
|
||||||
let encrypted_data = if let Some(encrypted_data_hex) = sub_arg_matches.value_of("encrypted-data") {
|
|
||||||
opt_result!(hex::decode(encrypted_data_hex), "Decode --encrypted-data failed: {}")
|
|
||||||
} else {
|
|
||||||
return simple_error!("Argument --data or --data-hex must assign one");
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
|
||||||
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
|
|
||||||
|
|
||||||
let sign_result = yubikey::piv::sign_data(&mut yk, &encrypted_data, AlgorithmId::Rsa2048, SlotId::KeyManagement);
|
|
||||||
let decrypted_data = opt_result!(sign_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 in 1..decrypted_data_bytes.len() {
|
|
||||||
if decrypted_data_bytes[i] == 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());
|
|
||||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
|
||||||
use rust_util::util_clap::{Command, CommandError};
|
|
||||||
use yubikey::{PinPolicy, TouchPolicy, YubiKey};
|
|
||||||
use yubikey::piv::{AlgorithmId, SlotId};
|
|
||||||
|
|
||||||
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(Arg::with_name("pin").short("p").long("pin").takes_value(true).default_value("123456").help("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 works");
|
|
||||||
let pin = opt_value_result!(sub_arg_matches.value_of("pin"), "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 public_key_info = opt_result!(yubikey::piv::generate(&mut yk,SlotId::Signature, AlgorithmId::Rsa2048,
|
|
||||||
PinPolicy::Default, TouchPolicy::Default), "Generate key failed: {}");
|
|
||||||
|
|
||||||
success!("Generate key success: {:?}", public_key_info);
|
|
||||||
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
|
||||||
use rust_util::util_clap::{Command, CommandError};
|
|
||||||
use rust_util::util_msg::MessageType;
|
|
||||||
use rust_util::XResult;
|
|
||||||
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("sha256").short("2").long("sha256").takes_value(true).help("Digest SHA256 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("pin");
|
|
||||||
let pin = opt_value_result!(pin_opt, "User pin must be assigned");
|
|
||||||
|
|
||||||
let sha256_hex_opt = sub_arg_matches.value_of("sha256").map(|s| s.to_string());
|
|
||||||
|
|
||||||
let mut yk = opt_result!(YubiKey::open(), "YubiKey not found: {}");
|
|
||||||
opt_result!(yk.verify_pin(pin.as_bytes()), "YubiKey verify pin failed: {}");
|
|
||||||
|
|
||||||
// https://www.ibm.com/docs/en/zos/2.2.0?topic=cryptography-pkcs-1-formats
|
|
||||||
// MD5 X’3020300C 06082A86 4886F70D 02050500 0410’ || 16-byte hash value
|
|
||||||
// SHA-1 X'30213009 06052B0E 03021A05 000414’ || 20-byte hash value
|
|
||||||
// SHA-224 X’302D300D 06096086 48016503 04020405 00041C’ || 28-byte hash value
|
|
||||||
// SHA-256 X’3031300D 06096086 48016503 04020105 000420’ || 32-byte hash value
|
|
||||||
// SHA-384 X’3041300D 06096086 48016503 04020205 000430’ || 48-byte hash value
|
|
||||||
// SHA-512 X’3051300D 06096086 48016503 04020305 000440’ || 64-byte hash value
|
|
||||||
let sha256_der_prefix = hex::decode("3031300d060960864801650304020105000420").unwrap();
|
|
||||||
|
|
||||||
if let Some(sha256_hex) = sha256_hex_opt {
|
|
||||||
let hash = opt_result!(hex::decode(sha256_hex), "Decode sha256 failed: {}");
|
|
||||||
|
|
||||||
let mut hash_with_oid = Vec::with_capacity(128);
|
|
||||||
hash_with_oid.extend_from_slice(&sha256_der_prefix);
|
|
||||||
hash_with_oid.extend_from_slice(&hash);
|
|
||||||
let hash_padding = pkcs1_padding_for_sign(&hash_with_oid, 2048).unwrap();
|
|
||||||
rust_util::util_msg::when(MessageType::DEBUG, || {
|
|
||||||
debugging!("Hash: {}", hex::encode(&hash));
|
|
||||||
debugging!("Hash with OID: {}", hex::encode(&hash_with_oid));
|
|
||||||
debugging!("PKCS1 padding: {}", hex::encode(&hash_padding));
|
|
||||||
});
|
|
||||||
let raw_in = crate::digest::copy_rsa2048(&hash_padding).unwrap();
|
|
||||||
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("hash_hex", hex::encode(&hash));
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,10 @@ use openssl::encrypt::Decrypter;
|
|||||||
use openssl::pkey::PKey;
|
use openssl::pkey::PKey;
|
||||||
use openssl::rsa::Rsa;
|
use openssl::rsa::Rsa;
|
||||||
use rust_util::util_clap::{Command, CommandError};
|
use rust_util::util_clap::{Command, CommandError};
|
||||||
|
use rust_util::util_msg;
|
||||||
use rust_util::util_msg::MessageType;
|
use rust_util::util_msg::MessageType;
|
||||||
|
use crate::{cmdutil, util};
|
||||||
|
use crate::util::{read_stdin, try_decode};
|
||||||
|
|
||||||
pub struct CommandImpl;
|
pub struct CommandImpl;
|
||||||
|
|
||||||
@@ -15,17 +18,18 @@ impl Command for CommandImpl {
|
|||||||
fn name(&self) -> &str { "rsa-decrypt" }
|
fn name(&self) -> &str { "rsa-decrypt" }
|
||||||
|
|
||||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||||
SubCommand::with_name(self.name()).about("RSA Decrypt subcommand")
|
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("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("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)
|
.arg(Arg::with_name("padding").long("padding").takes_value(true)
|
||||||
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding"))
|
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding"))
|
||||||
.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 pri_key_in = opt_value_result!(sub_arg_matches.value_of("pri-key-in"), "Require private key in");
|
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 pri_key_bytes = opt_result!(std::fs::read(pri_key_in), "Read file: {}, failed: {}", pri_key_in);
|
||||||
@@ -34,22 +38,24 @@ impl Command for CommandImpl {
|
|||||||
let padding = crate::rsautil::parse_padding(padding_opt)?;
|
let padding = crate::rsautil::parse_padding(padding_opt)?;
|
||||||
let padding_str = crate::rsautil::padding_to_string(padding);
|
let padding_str = crate::rsautil::padding_to_string(padding);
|
||||||
|
|
||||||
let mut json = BTreeMap::new();
|
|
||||||
|
|
||||||
let keypair = opt_result!(Rsa::private_key_from_pem(&pri_key_bytes), "Parse RSA failed: {}");
|
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 keypair = opt_result!(PKey::from_rsa(keypair), "RSA to PKey failed: {}");
|
||||||
|
|
||||||
let encrypted = if let Some(encrypted) = sub_arg_matches.value_of("encrypted") {
|
let ciphertext_opt = sub_arg_matches.value_of("encrypted")
|
||||||
opt_result!(hex::decode(encrypted), "Decode encrypted HEX failed: {}")
|
.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 {
|
} else {
|
||||||
return simple_error!("Data is required, --data-hex or --data argument!");
|
return simple_error!("Data is required, --ciphertext or --encrypted argument!");
|
||||||
};
|
};
|
||||||
|
|
||||||
rust_util::util_msg::when(MessageType::DEBUG, || {
|
util_msg::when(MessageType::DEBUG, || {
|
||||||
let rsa = keypair.rsa().unwrap();
|
let rsa = keypair.rsa().unwrap();
|
||||||
let n = rsa.n();
|
let n = rsa.n();
|
||||||
let d = rsa.d();
|
let d = rsa.d();
|
||||||
let m = BigNum::from_slice(&encrypted).unwrap();
|
let m = BigNum::from_slice(&ciphertext).unwrap();
|
||||||
let mut r = BigNum::new().unwrap();
|
let mut r = BigNum::new().unwrap();
|
||||||
r.mod_exp(&m, d, n, &mut BigNumContext::new().unwrap()).unwrap();
|
r.mod_exp(&m, d, n, &mut BigNumContext::new().unwrap()).unwrap();
|
||||||
let v = r.to_vec();
|
let v = r.to_vec();
|
||||||
@@ -62,23 +68,22 @@ impl Command for CommandImpl {
|
|||||||
|
|
||||||
let mut decrypter = opt_result!(Decrypter::new(&keypair), "Decrypter new failed: {}");
|
let mut decrypter = opt_result!(Decrypter::new(&keypair), "Decrypter new failed: {}");
|
||||||
opt_result!(decrypter.set_rsa_padding(padding), "Set RSA padding failed: {}");
|
opt_result!(decrypter.set_rsa_padding(padding), "Set RSA padding failed: {}");
|
||||||
let buffer_len = opt_result!(decrypter.decrypt_len(&encrypted), "Decrypt len failed: {}");
|
let buffer_len = opt_result!(decrypter.decrypt_len(&ciphertext), "Decrypt len failed: {}");
|
||||||
let mut data = vec![0; buffer_len];
|
let mut data = vec![0; buffer_len];
|
||||||
let decrypted_len = opt_result!(decrypter.decrypt(&encrypted, &mut data), "Decrypt failed: {}");
|
let decrypted_len = opt_result!(decrypter.decrypt(&ciphertext, &mut data), "Decrypt failed: {}");
|
||||||
data.truncate(decrypted_len);
|
data.truncate(decrypted_len);
|
||||||
|
|
||||||
let encrypted_hex = hex::encode(&encrypted);
|
let encrypted_hex = hex::encode(&ciphertext);
|
||||||
information!("Padding: {}", padding_str);
|
information!("Padding: {}", padding_str);
|
||||||
success!("Message HEX: {}", hex::encode(&data));
|
success!("Message HEX: {}", hex::encode(&data));
|
||||||
success!("Message: {}", String::from_utf8_lossy(&data));
|
success!("Message: {}", String::from_utf8_lossy(&data));
|
||||||
if json_output {
|
if json_output {
|
||||||
|
let mut json = BTreeMap::new();
|
||||||
json.insert("data", hex::encode(&data));
|
json.insert("data", hex::encode(&data));
|
||||||
json.insert("padding", padding_str.to_string());
|
json.insert("padding", padding_str.to_string());
|
||||||
json.insert("encrypted", encrypted_hex);
|
json.insert("encrypted", encrypted_hex);
|
||||||
}
|
|
||||||
|
|
||||||
if json_output {
|
util::print_pretty_json(&json);
|
||||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::fs;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||||
@@ -5,8 +6,8 @@ use openssl::encrypt::Encrypter;
|
|||||||
use openssl::pkey::PKey;
|
use openssl::pkey::PKey;
|
||||||
use openssl::rsa::Rsa;
|
use openssl::rsa::Rsa;
|
||||||
use rust_util::util_clap::{Command, CommandError};
|
use rust_util::util_clap::{Command, CommandError};
|
||||||
|
use crate::{cmdutil, util};
|
||||||
use crate::digest::sha256_bytes;
|
use crate::digestutil::sha256_bytes;
|
||||||
|
|
||||||
pub struct CommandImpl;
|
pub struct CommandImpl;
|
||||||
|
|
||||||
@@ -15,28 +16,25 @@ impl Command for CommandImpl {
|
|||||||
fn name(&self) -> &str { "rsa-encrypt" }
|
fn name(&self) -> &str { "rsa-encrypt" }
|
||||||
|
|
||||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||||
SubCommand::with_name(self.name()).about("RSA Encrypt subcommand")
|
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("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").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("data-hex").long("data-hex").takes_value(true).help("Data in HEX"))
|
||||||
.arg(Arg::with_name("padding").long("padding").takes_value(true)
|
.arg(Arg::with_name("padding").long("padding").takes_value(true)
|
||||||
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding"))
|
.possible_values(&["pkcs1", "oaep", "pss", "none"]).help("Padding"))
|
||||||
.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 pub_key_in = opt_value_result!(sub_arg_matches.value_of("pub-key-in"), "Require public key in");
|
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!(std::fs::read(pub_key_in), "Read file: {}, failed: {}", pub_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_opt = sub_arg_matches.value_of("padding");
|
||||||
let padding = crate::rsautil::parse_padding(padding_opt)?;
|
let padding = crate::rsautil::parse_padding(padding_opt)?;
|
||||||
let padding_str = crate::rsautil::padding_to_string(padding);
|
let padding_str = crate::rsautil::padding_to_string(padding);
|
||||||
|
|
||||||
let mut json = BTreeMap::new();
|
|
||||||
|
|
||||||
let keypair = opt_result!(Rsa::public_key_from_pem(&pub_key_bytes), "Parse RSA failed: {}");
|
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_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 pub_key_fingerprint = hex::encode(sha256_bytes(&pub_key_der));
|
||||||
@@ -64,14 +62,13 @@ impl Command for CommandImpl {
|
|||||||
information!("Public key fingerprint: {}", pub_key_fingerprint);
|
information!("Public key fingerprint: {}", pub_key_fingerprint);
|
||||||
success!("Encrypted message: {}", encrypted_hex);
|
success!("Encrypted message: {}", encrypted_hex);
|
||||||
if json_output {
|
if json_output {
|
||||||
|
let mut json = BTreeMap::new();
|
||||||
json.insert("data", hex::encode(&data));
|
json.insert("data", hex::encode(&data));
|
||||||
json.insert("public_key_fingerprint", pub_key_fingerprint);
|
json.insert("public_key_fingerprint", pub_key_fingerprint);
|
||||||
json.insert("padding", padding_str.to_string());
|
json.insert("padding", padding_str.to_string());
|
||||||
json.insert("encrypted", encrypted_hex);
|
json.insert("encrypted", encrypted_hex);
|
||||||
}
|
|
||||||
|
|
||||||
if json_output {
|
util::print_pretty_json(&json);
|
||||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||||
@@ -8,9 +10,9 @@ use openssl::rsa::Rsa;
|
|||||||
use openssl::sign::Verifier;
|
use openssl::sign::Verifier;
|
||||||
use rust_util::util_clap::{Command, CommandError};
|
use rust_util::util_clap::{Command, CommandError};
|
||||||
use rust_util::util_msg::MessageType;
|
use rust_util::util_msg::MessageType;
|
||||||
use rust_util::XResult;
|
use rust_util::{util_msg, XResult};
|
||||||
|
|
||||||
use crate::digest::sha256_bytes;
|
use crate::digestutil::sha256_bytes;
|
||||||
|
|
||||||
pub struct CommandImpl;
|
pub struct CommandImpl;
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ impl Command for CommandImpl {
|
|||||||
fn name(&self) -> &str { "rsa-verify" }
|
fn name(&self) -> &str { "rsa-verify" }
|
||||||
|
|
||||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||||
SubCommand::with_name(self.name()).about("RSA Verify subcommand")
|
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("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("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("in").short("i").long("in").takes_value(true).help("File in"))
|
||||||
@@ -34,7 +36,7 @@ impl Command for CommandImpl {
|
|||||||
// if json_output { rust_util::util_msg::set_logger_std_out(false); }
|
// 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_in = opt_value_result!(sub_arg_matches.value_of("pub-key-in"), "Require public key in");
|
||||||
let pub_key_bytes = opt_result!(std::fs::read(pub_key_in), "Read file: {}, failed: {}", pub_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 mut json = BTreeMap::new();
|
||||||
|
|
||||||
@@ -49,14 +51,14 @@ impl Command for CommandImpl {
|
|||||||
return simple_error!("Signature is required, --signature argument!");
|
return simple_error!("Signature is required, --signature argument!");
|
||||||
};
|
};
|
||||||
|
|
||||||
rust_util::util_msg::when(MessageType::DEBUG, || {
|
util_msg::when(MessageType::DEBUG, || {
|
||||||
let rsa = keypair.rsa().unwrap();
|
let rsa = keypair.rsa().unwrap();
|
||||||
let n = rsa.n();
|
let n = rsa.n();
|
||||||
let e = rsa.e();
|
let e = rsa.e();
|
||||||
let m = BigNum::from_slice(&signature).unwrap();
|
let m = BigNum::from_slice(&signature).unwrap();
|
||||||
let mut r = BigNum::new().unwrap();
|
let mut r = BigNum::new().unwrap();
|
||||||
r.mod_exp(&m, e, n, &mut BigNumContext::new().unwrap()).unwrap();
|
r.mod_exp(&m, e, n, &mut BigNumContext::new().unwrap()).unwrap();
|
||||||
debugging!("Signature raw HEX: 00{}", hex::encode(&r.to_vec()));
|
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");
|
let file_in = opt_value_result!(sub_arg_matches.value_of("in"), "File in --in required");
|
||||||
@@ -68,7 +70,7 @@ impl Command for CommandImpl {
|
|||||||
let digest = get_digest(hash)?;
|
let digest = get_digest(hash)?;
|
||||||
let mut verifier = opt_result!(Verifier::new(digest, &keypair), "Verifier new failed: {}");
|
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);
|
let mut f = opt_result!(File::open(file_in), "Open file: {}, failed: {}", file_in);
|
||||||
opt_result!(std::io::copy(&mut f, &mut verifier), "Verifier failed: {}");
|
opt_result!(io::copy(&mut f, &mut verifier), "Verifier failed: {}");
|
||||||
let result = opt_result!(verifier.verify(&signature), "Verifier verify failed: {}");
|
let result = opt_result!(verifier.verify(&signature), "Verifier verify failed: {}");
|
||||||
if result {
|
if result {
|
||||||
success!("Verify success");
|
success!("Verify success");
|
||||||
39
src/cmd_se.rs
Normal file
39
src/cmd_se.rs
Normal 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
85
src/cmd_se_ecdh.rs
Normal 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
68
src/cmd_se_ecsign.rs
Normal 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
137
src/cmd_se_generate.rs
Normal 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
41
src/cmd_se_recover.rs
Normal 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
85
src/cmd_sign_jwt.rs
Normal 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
302
src/cmd_sign_jwt_piv.rs
Normal 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
74
src/cmd_sign_jwt_se.rs
Normal 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
109
src/cmd_sign_jwt_soft.rs
Normal 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
209
src/cmd_ssh_agent.rs
Normal 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
193
src/cmd_ssh_agent_gpg.rs
Normal 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
61
src/cmd_ssh_parse.rs
Normal 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
119
src/cmd_ssh_parse_sign.rs
Normal 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
195
src/cmd_ssh_piv_cert.rs
Normal 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
140
src/cmd_ssh_piv_sign.rs
Normal 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
129
src/cmd_ssh_pub_key.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,9 +12,10 @@ use rust_util::util_clap::{Command, CommandError};
|
|||||||
use x509_parser::certificate::X509Certificate;
|
use x509_parser::certificate::X509Certificate;
|
||||||
use x509_parser::prelude::FromDer;
|
use x509_parser::prelude::FromDer;
|
||||||
|
|
||||||
use crate::digest;
|
use crate::{cmdutil, digestutil, util};
|
||||||
use crate::fido;
|
use crate::fidoutil;
|
||||||
use crate::fido::{U2fRegistrationData, U2fV2Challenge};
|
use crate::fidoutil::{U2fRegistrationData, U2fV2Challenge};
|
||||||
|
use crate::util::base64_encode;
|
||||||
|
|
||||||
pub struct CommandImpl;
|
pub struct CommandImpl;
|
||||||
|
|
||||||
@@ -22,17 +23,16 @@ impl Command for CommandImpl {
|
|||||||
fn name(&self) -> &str { "u2f-register" }
|
fn name(&self) -> &str { "u2f-register" }
|
||||||
|
|
||||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||||
SubCommand::with_name(self.name()).about("FIDO U2F Register subcommand")
|
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("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("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").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("challenge-with-timestamp-prefix").long("challenge-with-timestamp-prefix").help("Challenge with timestamp prefix"))
|
||||||
.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 timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
|
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
|
||||||
Ok(t) => (t * 1000) as u64,
|
Ok(t) => (t * 1000) as u64,
|
||||||
@@ -46,12 +46,12 @@ impl Command for CommandImpl {
|
|||||||
let u2fv2_challenge = U2fV2Challenge::new_challenge(challenge_hex, app_id, 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 u2fv2_challenge_str = u2fv2_challenge.to_json();
|
||||||
|
|
||||||
let app_id_hash = digest::sha256(app_id);
|
let app_id_hash = digestutil::sha256(app_id);
|
||||||
let challenge_hash = digest::sha256(&u2fv2_challenge_str);
|
let challenge_hash = digestutil::sha256(&u2fv2_challenge_str);
|
||||||
|
|
||||||
let flags = RegisterFlags::empty();
|
let flags = RegisterFlags::empty();
|
||||||
|
|
||||||
let status_tx = fido::start_status_updater();
|
let status_tx = fidoutil::start_status_updater();
|
||||||
|
|
||||||
let (register_tx, register_rx) = channel();
|
let (register_tx, register_rx) = channel();
|
||||||
let callback = StateCallback::new(Box::new(move |rv| {
|
let callback = StateCallback::new(Box::new(move |rv| {
|
||||||
@@ -96,8 +96,8 @@ impl Command for CommandImpl {
|
|||||||
+ u2f_registration_data.attestation_cert.as_ref().map(|c| c.len()).unwrap_or(0);
|
+ u2f_registration_data.attestation_cert.as_ref().map(|c| c.len()).unwrap_or(0);
|
||||||
let sign = ®ister_result.0[sign_prefix_len..];
|
let sign = ®ister_result.0[sign_prefix_len..];
|
||||||
|
|
||||||
let mut json = BTreeMap::new();
|
|
||||||
if json_output {
|
if json_output {
|
||||||
|
let mut json = BTreeMap::new();
|
||||||
// println!("{}", serde_json::to_string_pretty(&u2f_registration_data).unwrap());
|
// println!("{}", serde_json::to_string_pretty(&u2f_registration_data).unwrap());
|
||||||
if let Some(device_name) = u2f_registration_data.device_name {
|
if let Some(device_name) = u2f_registration_data.device_name {
|
||||||
json.insert("device_name", device_name);
|
json.insert("device_name", device_name);
|
||||||
@@ -115,10 +115,12 @@ impl Command for CommandImpl {
|
|||||||
json.insert("app_id_hash", hex::encode(&app_id_hash));
|
json.insert("app_id_hash", hex::encode(&app_id_hash));
|
||||||
json.insert("challenge", u2fv2_challenge_str);
|
json.insert("challenge", u2fv2_challenge_str);
|
||||||
json.insert("challenge_hash", hex::encode(&challenge_hash));
|
json.insert("challenge_hash", hex::encode(&challenge_hash));
|
||||||
|
|
||||||
|
util::print_pretty_json(&json);
|
||||||
} else {
|
} else {
|
||||||
success!("Device info: {}", u2f_registration_data.device_info);
|
success!("Device info: {}", u2f_registration_data.device_info);
|
||||||
information!("Register challenge: {}", u2fv2_challenge_str);
|
information!("Register challenge: {}", u2fv2_challenge_str);
|
||||||
information!("Register challenge base64: {}", base64::encode(&u2fv2_challenge_str));
|
information!("Register challenge base64: {}", base64_encode(&u2fv2_challenge_str));
|
||||||
if let Some(cert) = u2f_registration_data.attestation_cert_pem {
|
if let Some(cert) = u2f_registration_data.attestation_cert_pem {
|
||||||
information!("Attestation certificate: {}", cert);
|
information!("Attestation certificate: {}", cert);
|
||||||
}
|
}
|
||||||
@@ -147,9 +149,6 @@ impl Command for CommandImpl {
|
|||||||
warning!("Cannot find attestation cert!");
|
warning!("Cannot find attestation cert!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if json_output {
|
|
||||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
|
||||||
}
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,9 +13,10 @@ use openssl::pkey::PKey;
|
|||||||
use openssl::sign::Verifier;
|
use openssl::sign::Verifier;
|
||||||
use rust_util::util_clap::{Command, CommandError};
|
use rust_util::util_clap::{Command, CommandError};
|
||||||
|
|
||||||
use crate::digest;
|
use crate::{cmdutil, digestutil, util};
|
||||||
use crate::fido;
|
use crate::fidoutil;
|
||||||
use crate::fido::U2fV2Challenge;
|
use crate::fidoutil::U2fV2Challenge;
|
||||||
|
use crate::util::base64_encode;
|
||||||
|
|
||||||
pub struct CommandImpl;
|
pub struct CommandImpl;
|
||||||
|
|
||||||
@@ -23,19 +24,18 @@ impl Command for CommandImpl {
|
|||||||
fn name(&self) -> &str { "u2f-sign" }
|
fn name(&self) -> &str { "u2f-sign" }
|
||||||
|
|
||||||
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
fn subcommand<'a>(&self) -> App<'a, 'a> {
|
||||||
SubCommand::with_name(self.name()).about("FIDO U2F Sign subcommand")
|
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("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("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("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").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("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(Arg::with_name("key-handle").short("k").long("key-handle").takes_value(true).multiple(true).help("Key handle"))
|
||||||
.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 timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
|
let timeout_ms = match sub_arg_matches.value_of("timeout").unwrap().parse::<u32>() {
|
||||||
Ok(t) => (t * 1000) as u64,
|
Ok(t) => (t * 1000) as u64,
|
||||||
@@ -71,10 +71,10 @@ impl Command for CommandImpl {
|
|||||||
let u2fv2_challenge = U2fV2Challenge::new_challenge(challenge_hex, app_id, 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 u2fv2_challenge_str = u2fv2_challenge.to_json();
|
||||||
|
|
||||||
let app_id_hash = digest::sha256(app_id);
|
let app_id_hash = digestutil::sha256(app_id);
|
||||||
let challenge_hash = digest::sha256(&u2fv2_challenge_str);
|
let challenge_hash = digestutil::sha256(&u2fv2_challenge_str);
|
||||||
|
|
||||||
let status_tx = fido::start_status_updater();
|
let status_tx = fidoutil::start_status_updater();
|
||||||
|
|
||||||
information!("App id: {}, Start sign...", app_id);
|
information!("App id: {}, Start sign...", app_id);
|
||||||
debugging!("Wait timeout: {} ms", timeout_ms);
|
debugging!("Wait timeout: {} ms", timeout_ms);
|
||||||
@@ -115,7 +115,7 @@ impl Command for CommandImpl {
|
|||||||
json.insert("challenge", u2fv2_challenge_str.to_string());
|
json.insert("challenge", u2fv2_challenge_str.to_string());
|
||||||
json.insert("challenge_hash", hex::encode(&challenge_hash));
|
json.insert("challenge_hash", hex::encode(&challenge_hash));
|
||||||
json.insert("device_info", format!("{}", &device_info));
|
json.insert("device_info", format!("{}", &device_info));
|
||||||
json.insert("signature", hex::encode(&signature));
|
json.insert("signature", hex::encode(signature));
|
||||||
json.insert("signed_message", hex::encode(&signed_message));
|
json.insert("signed_message", hex::encode(&signed_message));
|
||||||
json.insert("key_handle", hex::encode(&handle_used));
|
json.insert("key_handle", hex::encode(&handle_used));
|
||||||
json.insert("sign_data", hex::encode(&sign_data));
|
json.insert("sign_data", hex::encode(&sign_data));
|
||||||
@@ -123,11 +123,11 @@ impl Command for CommandImpl {
|
|||||||
json.insert("counter", format!("{}", counter_u32));
|
json.insert("counter", format!("{}", counter_u32));
|
||||||
} else {
|
} else {
|
||||||
information!("Sign challenge: {}", u2fv2_challenge_str);
|
information!("Sign challenge: {}", u2fv2_challenge_str);
|
||||||
information!("Sign challenge base64: {}", base64::encode(&u2fv2_challenge_str));
|
information!("Sign challenge base64: {}", base64_encode(&u2fv2_challenge_str));
|
||||||
information!("Sign result : {}", base64::encode(&sign_data));
|
information!("Sign result : {}", base64_encode(&sign_data));
|
||||||
information!("- presence : {}", user_presence_flag);
|
information!("- presence : {}", user_presence_flag);
|
||||||
information!("- counter : {}", counter_u32);
|
information!("- counter : {}", counter_u32);
|
||||||
information!("- signature: {}", hex::encode(&signature));
|
information!("- signature: {}", hex::encode(signature));
|
||||||
information!("Key handle: {}", hex::encode(&handle_used));
|
information!("Key handle: {}", hex::encode(&handle_used));
|
||||||
information!("Signed message: {}", hex::encode(&signed_message));
|
information!("Signed message: {}", hex::encode(&signed_message));
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ impl Command for CommandImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if json_output {
|
if json_output {
|
||||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
util::print_pretty_json(&json);
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
44
src/cmd_yubikey.rs
Normal file
44
src/cmd_yubikey.rs
Normal 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
82
src/cmdutil.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -1,11 +1,24 @@
|
|||||||
use sha1::Sha1;
|
use sha1::Sha1;
|
||||||
use sha2::{Digest, Sha256, Sha384, Sha512};
|
use sha2::{Digest, Sha256, Sha384, Sha512};
|
||||||
|
|
||||||
// pub fn sha1(input: &str) -> Vec<u8> {
|
pub enum DigestAlgorithm {
|
||||||
// let mut challenge = Sha1::default();
|
Sha256,
|
||||||
// challenge.update(input.as_bytes());
|
#[allow(dead_code)]
|
||||||
// challenge.digest().bytes().to_vec()
|
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> {
|
pub fn sha256(input: &str) -> Vec<u8> {
|
||||||
sha256_bytes(input.as_bytes())
|
sha256_bytes(input.as_bytes())
|
||||||
@@ -14,7 +27,7 @@ pub fn sha256(input: &str) -> Vec<u8> {
|
|||||||
pub fn sha1_bytes(input: &[u8]) -> Vec<u8> {
|
pub fn sha1_bytes(input: &[u8]) -> Vec<u8> {
|
||||||
let mut digest = Sha1::default();
|
let mut digest = Sha1::default();
|
||||||
digest.update(input);
|
digest.update(input);
|
||||||
digest.digest().bytes().to_vec()
|
digest.finalize().to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sha256_bytes(input: &[u8]) -> Vec<u8> {
|
pub fn sha256_bytes(input: &[u8]) -> Vec<u8> {
|
||||||
@@ -54,4 +67,4 @@ define_copy_array!(copy_sha256, 0x20);
|
|||||||
define_copy_array!(copy_sha384, 0x30);
|
define_copy_array!(copy_sha384, 0x30);
|
||||||
define_copy_array!(copy_sha512, 0x40);
|
define_copy_array!(copy_sha512, 0x40);
|
||||||
|
|
||||||
define_copy_array!(copy_rsa2048, 0x100);
|
// define_copy_array!(copy_rsa2048, 0x100);
|
||||||
63
src/ecdhutil.rs
Normal file
63
src/ecdhutil.rs
Normal 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
238
src/ecdsautil.rs
Normal 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
66
src/ecutil.rs
Normal 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(),
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -4,12 +4,12 @@ use std::thread;
|
|||||||
use std::time::SystemTime;
|
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::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 {
|
||||||
@@ -76,7 +76,7 @@ impl U2fRegistrationData {
|
|||||||
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(®ister_result.0),
|
registration_data: base64_encode(®ister_result.0),
|
||||||
attestation_cert: registration.attestation_cert.clone(),
|
attestation_cert: registration.attestation_cert.clone(),
|
||||||
attestation_cert_pem: registration.attestation_cert.map(|cert| {
|
attestation_cert_pem: registration.attestation_cert.map(|cert| {
|
||||||
bytes_to_pem("CERTIFICATE", cert)
|
bytes_to_pem("CERTIFICATE", cert)
|
||||||
@@ -101,7 +101,7 @@ impl U2fV2Challenge {
|
|||||||
None => U2fV2Challenge::new_random(app_id, with_time_stamp_prefix),
|
None => U2fV2Challenge::new_random(app_id, with_time_stamp_prefix),
|
||||||
Some(challenge_hex) => {
|
Some(challenge_hex) => {
|
||||||
let challenge_bytes = opt_result!(hex::decode(challenge_hex), "Decode challenge hex failed: {}");
|
let challenge_bytes = opt_result!(hex::decode(challenge_hex), "Decode challenge hex failed: {}");
|
||||||
let challenge = base64::encode_config(&challenge_bytes, base64::URL_SAFE_NO_PAD);
|
let challenge = base64_encode_url_safe_no_pad(challenge_bytes);
|
||||||
U2fV2Challenge::new(challenge, app_id)
|
U2fV2Challenge::new(challenge, app_id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -120,7 +120,7 @@ impl U2fV2Challenge {
|
|||||||
rand_bytes[..8].clone_from_slice(×tamp_be_bytes[..8]);
|
rand_bytes[..8].clone_from_slice(×tamp_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)
|
||||||
}
|
}
|
||||||
|
|
||||||
157
src/hmacutil.rs
Normal file
157
src/hmacutil.rs
Normal 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
168
src/keychain.rs
Normal 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
445
src/keyutil.rs
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
202
src/main.rs
202
src/main.rs
@@ -4,28 +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 rsautil;
|
|
||||||
mod pkiutil;
|
|
||||||
mod pgpcardutil;
|
|
||||||
mod cmd_u2fregister;
|
|
||||||
mod cmd_u2fsign;
|
|
||||||
mod cmd_rsaencrypt;
|
|
||||||
mod cmd_rsadecrypt;
|
|
||||||
mod cmd_rsaverify;
|
|
||||||
mod cmd_pgp;
|
|
||||||
mod cmd_pgpcardadmin;
|
|
||||||
mod cmd_pgpcardlist;
|
|
||||||
mod cmd_pgpcardsign;
|
|
||||||
mod cmd_pgpcarddecrypt;
|
|
||||||
mod cmd_pgpcardmake;
|
|
||||||
mod cmd_piv;
|
|
||||||
mod cmd_pivsign;
|
|
||||||
mod cmd_pivdecrypt;
|
|
||||||
mod cmd_pivgenerate;
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -40,35 +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_rsaencrypt::CommandImpl),
|
Box::new(cmd_hmac_encrypt::CommandImpl),
|
||||||
Box::new(cmd_rsadecrypt::CommandImpl),
|
Box::new(cmd_hmac_decrypt::CommandImpl),
|
||||||
Box::new(cmd_rsaverify::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),
|
||||||
Box::new(cmd_pgpcardmake::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_pivdecrypt::CommandImpl),
|
Box::new(cmd_piv_meta::CommandImpl),
|
||||||
Box::new(cmd_pivgenerate::CommandImpl),
|
Box::new(cmd_piv_verify::CommandImpl),
|
||||||
Box::new(cmd_u2fregister::CommandImpl),
|
Box::new(cmd_piv_rsasign::CommandImpl),
|
||||||
Box::new(cmd_u2fsign::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
241
src/mlkemutil.rs
Normal 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
119
src/pbeutil.rs
Normal 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()));
|
||||||
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
|
use openpgp_card::OpenPgp;
|
||||||
use openpgp_card_pcsc::PcscBackend;
|
use openpgp_card_pcsc::PcscBackend;
|
||||||
use rust_util::XResult;
|
use rust_util::XResult;
|
||||||
|
|
||||||
|
pub fn get_openpgp_card() -> XResult<OpenPgp> {
|
||||||
|
let card = get_card()?;
|
||||||
|
Ok(OpenPgp::new(card))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_card() -> XResult<PcscBackend> {
|
pub fn get_card() -> XResult<PcscBackend> {
|
||||||
let card_list = opt_result!(PcscBackend::cards(None), "Read OpenPGP card list failed: {}");
|
let card_list = opt_result!(PcscBackend::cards(None), "Read OpenPGP card list failed: {}");
|
||||||
if card_list.is_empty() {
|
if card_list.is_empty() {
|
||||||
|
|||||||
58
src/pinutil.rs
Normal file
58
src/pinutil.rs
Normal 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
308
src/pivutil.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -2,18 +2,48 @@ use openpgp_card::crypto_data::PublicKeyMaterial;
|
|||||||
use openssl::bn::BigNum;
|
use openssl::bn::BigNum;
|
||||||
use openssl::rsa::Rsa;
|
use openssl::rsa::Rsa;
|
||||||
use pem::Pem;
|
use pem::Pem;
|
||||||
|
use rust_util::XResult;
|
||||||
|
#[cfg(feature = "with-sequoia-openpgp")]
|
||||||
use sequoia_openpgp::crypto::mpi::PublicKey;
|
use sequoia_openpgp::crypto::mpi::PublicKey;
|
||||||
|
use x509_parser::x509::AlgorithmIdentifier;
|
||||||
|
|
||||||
use crate::digest::sha256_bytes;
|
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>> {
|
pub fn bytes_to_pem<T>(tag: &str, contents: T) -> String where T: Into<Vec<u8>> {
|
||||||
let cert_public_key_pem_obj = Pem {
|
let cert_public_key_pem_obj = Pem::new(tag, contents);
|
||||||
tag: tag.to_string(),
|
|
||||||
contents: contents.into(),
|
|
||||||
};
|
|
||||||
pem::encode(&cert_public_key_pem_obj).trim().to_string()
|
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)> {
|
pub fn sequoia_openpgp_public_key_pem(public_key: &PublicKey) -> Option<(Vec<u8>, String)> {
|
||||||
match public_key {
|
match public_key {
|
||||||
PublicKey::RSA { e, n } => {
|
PublicKey::RSA { e, n } => {
|
||||||
@@ -31,8 +61,12 @@ pub fn openpgp_card_public_key_pem(public_key: &PublicKeyMaterial) -> Option<(Ve
|
|||||||
PublicKeyMaterial::R(rsa_pub) => {
|
PublicKeyMaterial::R(rsa_pub) => {
|
||||||
Some(rsa_public_key_pem(rsa_pub.n(), rsa_pub.v()))
|
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!("Not RSA public key: {:?}", public_key);
|
warning!("Unknown public key: {:?}", public_key);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
138
src/rsautil.rs
138
src/rsautil.rs
@@ -1,7 +1,65 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use ecdsa::elliptic_curve::rand_core::OsRng;
|
||||||
use openssl::bn::{BigNum, BigNumContext};
|
use openssl::bn::{BigNum, BigNumContext};
|
||||||
use openssl::pkey::PKey;
|
use openssl::pkey::PKey;
|
||||||
use openssl::rsa::{Padding, Rsa};
|
use openssl::rsa::{Padding, Rsa};
|
||||||
use rust_util::XResult;
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct RsaCrt {
|
pub struct RsaCrt {
|
||||||
@@ -112,3 +170,81 @@ fn inner_from(p: BigNum, q: BigNum, e: BigNum) -> XResult<RsaCrt> {
|
|||||||
coefficient: qinv,
|
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 X’3020300C 06082A86 4886F70D 02050500 0410’ || 16-byte hash value
|
||||||
|
// SHA-1 X'30213009 06052B0E 03021A05 000414’ || 20-byte hash value
|
||||||
|
// SHA-224 X’302D300D 06096086 48016503 04020405 00041C’ || 28-byte hash value
|
||||||
|
// SHA-256 X’3031300D 06096086 48016503 04020105 000420’ || 32-byte hash value
|
||||||
|
// SHA-384 X’3041300D 06096086 48016503 04020205 000430’ || 48-byte hash value
|
||||||
|
// SHA-512 X’3051300D 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
81
src/seutil.rs
Normal 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
111
src/signfile.rs
Normal 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(×tamp.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
42
src/sshutil.rs
Normal 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
77
src/util.rs
Normal 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
58
src/yubikeyutil.rs
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user