diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76ac2c5..52bfdb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,10 +8,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.41.0 + toolchain: 1.44.0 override: true - name: Install build dependencies run: sudo apt install libpcsclite-dev @@ -49,10 +49,10 @@ jobs: os: macos-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.41.0 + toolchain: 1.44.0 override: true - name: Install build dependencies run: sudo apt install ${{ matrix.build_deps }} @@ -77,7 +77,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable @@ -98,7 +98,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: nightly diff --git a/Cargo.lock b/Cargo.lock index 1420969..5ebe693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,12 +14,12 @@ name = "age-core" version = "0.5.0" source = "git+https://github.com/str4d/rage.git?rev=d8fd951e059d9f7116b2b9dd0d176798a11b49f3#d8fd951e059d9f7116b2b9dd0d176798a11b49f3" dependencies = [ - "base64", + "base64 0.12.3", "c2-chacha", "chacha20poly1305", "cookie-factory", "hkdf", - "nom", + "nom 5.1.2", "rand", "secrecy", "sha2", @@ -42,7 +42,14 @@ version = "0.0.0" dependencies = [ "age-core", "age-plugin", + "bech32", + "console", + "elliptic-curve", "gumdrop", + "p256", + "sha2", + "x509-parser 0.9.0", + "yubikey-piv", ] [[package]] @@ -51,6 +58,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + [[package]] name = "autocfg" version = "1.0.1" @@ -63,6 +76,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bech32" version = "0.7.2" @@ -75,6 +94,29 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bitvec" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2838fdd79e8776dbe07a106c784b0f8dda571a21b2750a092cc4cbaa653c8e" +dependencies = [ + "funty", + "radium 0.4.1", + "wyz", +] + +[[package]] +name = "bitvec" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" +dependencies = [ + "funty", + "radium 0.5.3", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -84,6 +126,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "byteorder" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" + [[package]] name = "c2-chacha" version = "0.3.0" @@ -140,6 +188,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "console" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc80946b3480f421c2f17ed1cb841753a371c7c5104f51d507e13f532c856aa" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] +name = "const-oid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d9162b7289a46e86208d6af2c686ca5bfde445878c41a458a9fac706252d0b" + [[package]] name = "cookie-factory" version = "0.3.1" @@ -168,6 +237,75 @@ dependencies = [ "subtle", ] +[[package]] +name = "data-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993a608597367c6377b258c25d7120740f00ed23a2252b729b1932dd7866f908" + +[[package]] +name = "der-oid-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66558629d772c3be040566b7be07be8c8f5aecee95e4a092dfe2efc313277ad" +dependencies = [ + "nom 5.1.2", + "num-bigint 0.3.1", + "num-traits", + "proc-macro-hack", +] + +[[package]] +name = "der-oid-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd17d13ecf875e704369fdbde242483ac769fc18f6af21e43d5a692a079732fc" +dependencies = [ + "nom 6.0.1", + "num-bigint 0.3.1", + "num-traits", + "proc-macro-hack", +] + +[[package]] +name = "der-parser" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caca07c50eaae94d43e21f4d14eca5543b6f5f5ce64715e9b7665ac5f5185b4e" +dependencies = [ + "der-oid-macro 0.2.0", + "nom 5.1.2", + "num-bigint 0.3.1", + "num-traits", + "proc-macro-hack", + "rusticata-macros 2.1.0", +] + +[[package]] +name = "der-parser" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4b1e27396f46037881c39d821660f2ff48797aaa7152a45ded7a93b368a819" +dependencies = [ + "der-oid-macro 0.3.0", + "nom 6.0.1", + "num-bigint 0.3.1", + "num-traits", + "proc-macro-hack", + "rusticata-macros 3.0.1", +] + +[[package]] +name = "des" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b24e7c748888aa2fa8bce21d8c64a52efc810663285315ac7476f7197a982fae" +dependencies = [ + "byteorder", + "cipher", + "opaque-debug", +] + [[package]] name = "digest" version = "0.9.0" @@ -177,6 +315,44 @@ dependencies = [ "generic-array", ] +[[package]] +name = "elliptic-curve" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396db09c483e7fca5d4fdb9112685632b3e76c9a607a2649c1bf904404a01366" +dependencies = [ + "bitvec 0.18.4", + "const-oid", + "ff", + "generic-array", + "group", + "rand_core", + "subtle", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "ff" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01646e077d4ebda82b73f1bca002ea1e91561a77df2431a9e79729bcc31950ef" +dependencies = [ + "bitvec 0.18.4", + "rand_core", + "subtle", +] + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "generic-array" version = "0.14.4" @@ -198,6 +374,17 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "group" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc11f9f5fbf1943b48ae7c2bf6846e7d827a512d1be4f23af708f5ca5d01dde1" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "gumdrop" version = "0.8.0" @@ -238,6 +425,15 @@ dependencies = [ "digest", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] + [[package]] name = "lexical-core" version = "0.7.4" @@ -257,6 +453,21 @@ version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + +[[package]] +name = "log" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2" +dependencies = [ + "cfg-if 0.1.10", +] + [[package]] name = "memchr" version = "2.3.4" @@ -274,13 +485,77 @@ dependencies = [ "version_check", ] +[[package]] +name = "nom" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88034cfd6b4a0d54dd14f4a507eceee36c0b70e5a02236c4e4df571102be17f0" +dependencies = [ + "bitvec 0.19.4", + "lexical-core", + "memchr", + "version_check", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d51546d704f52ef14b3c962b5776e53d5b862e5790e40a350d366c209bd7f7a" +dependencies = [ + "autocfg 0.1.7", + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "serde", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ - "autocfg", + "autocfg 1.0.1", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg 1.0.1", + "num-integer", "num-traits", ] @@ -290,15 +565,99 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "autocfg", + "autocfg 1.0.1", ] +[[package]] +name = "oid-registry" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2508c8f170e55be68508b1113956a760a82684f42022f8834fb16ca198621211" +dependencies = [ + "der-parser 5.0.0", +] + +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + [[package]] name = "opaque-debug" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "p256" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280ed58e7e5f3052b6e2f596fa40c7eff4c27c4b6b6deecb5d685ba5c2080980" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "p384" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06de0548166c258c22bb6bdcff3074eac4b07125040aa74db3f61db87fe5f275" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "pbkdf2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b8c0d71734018084da0c0354193a5edfb81b20d2d57a92c5b154aefc554a4a" +dependencies = [ + "base64 0.13.0", + "crypto-mac", + "hmac", + "rand", + "rand_core", + "sha2", + "subtle", +] + +[[package]] +name = "pcsc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e09a8d8705a2c9b1ffe1f9dd9580efe3f8e80c19fc9f99038fe99b7bb56c83" +dependencies = [ + "bitflags", + "pcsc-sys", +] + +[[package]] +name = "pcsc-sys" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b7bfecba2c0f1b5efb0e7caf7533ab1c295024165bcbb066231f60d33e23ea" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "pem" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c220d01f863d13d96ca82359d1e81e64a7c6bf0637bcde7b2349630addf0c6" +dependencies = [ + "base64 0.13.0", + "once_cell", + "regex", +] + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + [[package]] name = "poly1305" version = "0.6.2" @@ -315,6 +674,12 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + [[package]] name = "proc-macro2" version = "1.0.24" @@ -333,6 +698,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64de9a0c5361e034f1aefc9f71a86871ec870e766fe31a009734a989b329286a" + +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "rand" version = "0.7.3" @@ -374,6 +751,67 @@ dependencies = [ "rand_core", ] +[[package]] +name = "regex" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" + +[[package]] +name = "rsa" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3648b669b10afeab18972c105e284a7b953a669b0be3514c27f9b17acab2f9cd" +dependencies = [ + "byteorder", + "digest", + "lazy_static", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pem", + "rand", + "sha2", + "simple_asn1", + "subtle", + "thiserror", + "zeroize", +] + +[[package]] +name = "rusticata-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a9050636e8a1b487ba1fbe99114021cd7594dde3ce6ed95bfc1691e5b5367b" +dependencies = [ + "nom 5.1.2", +] + +[[package]] +name = "rusticata-macros" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7390af60e66c44130b4c5ea85f2555b7ace835d73b4b889c704dc3cb4c0468c8" +dependencies = [ + "nom 6.0.1", +] + +[[package]] +name = "rustversion" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd" + [[package]] name = "ryu" version = "1.0.5" @@ -389,6 +827,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "serde" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" + +[[package]] +name = "sha-1" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool 0.1.2", + "digest", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.9.2" @@ -402,6 +859,29 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "simple_asn1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b" +dependencies = [ + "chrono", + "num-bigint 0.2.6", + "num-traits", +] + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "static_assertions" version = "1.1.0" @@ -414,6 +894,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + [[package]] name = "syn" version = "1.0.58" @@ -425,6 +914,54 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e" + +[[package]] +name = "terminal_size" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd2d183bd3fac5f5fe38ddbeb4dc9aec4a39a9d7d59e7491d900302da01cbe1" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.1.44" @@ -442,6 +979,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.1" @@ -498,8 +1041,111 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "x509" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9335b8ff50b6a0de184b3eeb11fdce74224e3af90ca7265012512e73fc999d1a" +dependencies = [ + "chrono", + "cookie-factory", +] + +[[package]] +name = "x509-parser" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76245c48460d72a3e17ad3a01855c3cae98601bb992091c1c1421c77d1cb27c" +dependencies = [ + "base64 0.13.0", + "chrono", + "data-encoding", + "der-oid-macro 0.2.0", + "der-parser 4.1.0", + "lazy_static", + "nom 5.1.2", + "num-bigint 0.3.1", + "rusticata-macros 2.1.0", + "rustversion", + "thiserror", +] + +[[package]] +name = "x509-parser" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22c80f083d860f8e77f44762e9df8c92de7defeb70219ec37f32968cab53e90" +dependencies = [ + "base64 0.13.0", + "chrono", + "data-encoding", + "der-oid-macro 0.3.0", + "der-parser 5.0.0", + "lazy_static", + "nom 6.0.1", + "num-bigint 0.3.1", + "oid-registry", + "rusticata-macros 3.0.1", + "rustversion", + "thiserror", +] + +[[package]] +name = "yubikey-piv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568f3f194d91d4f5bd624983d371927c4d72c1d63be2e702cab2d09cb630a28f" +dependencies = [ + "chrono", + "cookie-factory", + "der-parser 4.1.0", + "des", + "elliptic-curve", + "getrandom", + "hmac", + "log", + "nom 5.1.2", + "num-bigint-dig", + "num-integer", + "num-traits", + "p256", + "p384", + "pbkdf2", + "pcsc", + "rsa", + "secrecy", + "sha-1", + "sha2", + "subtle", + "subtle-encoding", + "x509", + "x509-parser 0.8.2", + "zeroize", +] + [[package]] name = "zeroize" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f369ddb18862aba61aa49bf31e74d29f0f162dec753063200e1dc084345d16" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml index bec3907..b1b9aa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,14 @@ edition = "2018" [dependencies] age-core = "0.5" age-plugin = "0.0" +bech32 = "0.7.2" +console = "0.14" +elliptic-curve = "0.6" gumdrop = "0.8" +p256 = "0.5" +sha2 = "0.9" +x509-parser = "0.9" +yubikey-piv = { version = "0.1", features = ["untested"] } [patch.crates-io] age-core = { git = "https://github.com/str4d/rage.git", rev = "d8fd951e059d9f7116b2b9dd0d176798a11b49f3" } diff --git a/src/error.rs b/src/error.rs index 7875b13..6f2a5dc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,20 @@ use std::fmt; use std::io; +use yubikey_piv::{key::RetiredSlotId, Serial}; + +use crate::USABLE_SLOTS; pub enum Error { + InvalidSlot(u8), Io(io::Error), MultipleCommands, + MultipleIdentities, + MultipleYubiKeys, + NoIdentities, + NoMatchingSerial(Serial), + SlotHasNoIdentity(RetiredSlotId), + TimedOut, + YubiKey(yubikey_piv::Error), } impl From for Error { @@ -12,16 +23,61 @@ impl From for Error { } } +impl From for Error { + fn from(e: yubikey_piv::error::Error) -> Self { + Error::YubiKey(e) + } +} + // Rust only supports `fn main() -> Result<(), E: Debug>`, so we implement `Debug` // manually to provide the error output we want. impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Error::InvalidSlot(slot) => writeln!( + f, + "Invalid slot '{}' (expected number between 1 and 20).", + slot + )?, Error::Io(e) => writeln!(f, "Failed to set up YubiKey: {}", e)?, Error::MultipleCommands => writeln!( f, "Only one of --generate, --identity, --list, --list-all can be specified." )?, + Error::MultipleIdentities => writeln!( + f, + "This YubiKey has multiple age identities. Use --slot to select a single identity." + )?, + Error::MultipleYubiKeys => writeln!( + f, + "Multiple YubiKeys are plugged in. Use --serial to select a single YubiKey." + )?, + Error::NoIdentities => { + writeln!(f, "This YubiKey does not contain any age identities.")? + } + Error::NoMatchingSerial(serial) => { + writeln!(f, "Could not find YubiKey with serial {}.", serial)? + } + Error::SlotHasNoIdentity(slot) => writeln!( + f, + "Slot {} does not contain an age identity or compatible key.", + USABLE_SLOTS.iter().position(|s| s == slot).unwrap() + 1 + )?, + Error::TimedOut => { + writeln!(f, "Timed out while waiting for a YubiKey to be inserted.")? + } + Error::YubiKey(e) => match e { + yubikey_piv::error::Error::NotFound => { + writeln!(f, "Please insert the YubiKey you want to set up")? + } + e => { + writeln!(f, "Error while communicating with YubiKey: {}", e)?; + use std::error::Error; + if let Some(inner) = e.source() { + writeln!(f, "Cause: {}", inner)?; + } + } + }, } writeln!(f)?; writeln!( diff --git a/src/main.rs b/src/main.rs index 9df723b..10246e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,46 @@ use age_plugin::run_state_machine; use gumdrop::Options; +use yubikey_piv::{ + certificate::PublicKeyInfo, + key::{RetiredSlotId, SlotId}, + Key, Readers, +}; mod error; +mod p256; mod plugin; +mod util; +mod yubikey; use error::Error; +const PLUGIN_NAME: &str = "age-plugin-yubikey"; +const RECIPIENT_PREFIX: &str = "age1yubikey"; +const IDENTITY_PREFIX: &str = "age-plugin-yubikey-"; + +const USABLE_SLOTS: [RetiredSlotId; 20] = [ + RetiredSlotId::R1, + RetiredSlotId::R2, + RetiredSlotId::R3, + RetiredSlotId::R4, + RetiredSlotId::R5, + RetiredSlotId::R6, + RetiredSlotId::R7, + RetiredSlotId::R8, + RetiredSlotId::R9, + RetiredSlotId::R10, + RetiredSlotId::R11, + RetiredSlotId::R12, + RetiredSlotId::R13, + RetiredSlotId::R14, + RetiredSlotId::R15, + RetiredSlotId::R16, + RetiredSlotId::R17, + RetiredSlotId::R18, + RetiredSlotId::R19, + RetiredSlotId::R20, +]; + #[derive(Debug, Options)] struct PluginOptions { #[options(help = "Print this help message and exit.")] @@ -29,6 +64,133 @@ struct PluginOptions { #[options(help = "List all YubiKey keys that are compatible with age.", no_short)] list_all: bool, + + #[options( + help = "Specify which YubiKey to use, if more than one is plugged in.", + no_short + )] + serial: Option, + + #[options( + help = "Specify which slot to use. Defaults to first usable slot.", + no_short + )] + slot: Option, +} + +fn identity(opts: PluginOptions) -> Result<(), Error> { + let serial = opts.serial.map(|s| s.into()); + let slot = opts + .slot + .map(|slot| { + USABLE_SLOTS + .get(slot as usize - 1) + .cloned() + .ok_or(Error::InvalidSlot(slot)) + }) + .transpose()?; + + let mut yubikey = yubikey::open(serial)?; + + let mut keys = Key::list(&mut yubikey)?.into_iter().filter_map(|key| { + // - We only use the retired slots. + // - Only P-256 keys are compatible with us. + match (key.slot(), key.certificate().subject_pki()) { + (SlotId::Retired(slot), PublicKeyInfo::EcP256(pubkey)) => { + p256::Recipient::from_pubkey(*pubkey).map(|r| (key, slot, r)) + } + _ => None, + } + }); + + let (key, slot, recipient) = if let Some(slot) = slot { + keys.find(|(_, s, _)| s == &slot) + .ok_or(Error::SlotHasNoIdentity(slot)) + } else { + let mut keys = keys.filter(|(key, _, _)| { + let cert = x509_parser::parse_x509_certificate(key.certificate().as_ref()) + .map(|(_, cert)| cert) + .ok(); + match cert + .as_ref() + .and_then(|cert| cert.subject().iter_organization().next()) + { + Some(org) => org.as_str() == Ok(PLUGIN_NAME), + _ => false, + } + }); + match (keys.next(), keys.next()) { + (None, None) => Err(Error::NoIdentities), + (Some(key), None) => Ok(key), + (Some(_), Some(_)) => Err(Error::MultipleIdentities), + (None, Some(_)) => unreachable!(), + } + }?; + + let stub = yubikey::Stub::new(yubikey.serial(), slot, &recipient); + let created = x509_parser::parse_x509_certificate(key.certificate().as_ref()) + .ok() + .map(|(_, cert)| cert.validity().not_before.to_rfc2822()) + .unwrap_or_else(|| "Unknown".to_owned()); + + util::print_identity(stub, recipient, &created); + + Ok(()) +} + +fn list(all: bool) -> Result<(), Error> { + let mut readers = Readers::open()?; + + for reader in readers.iter()? { + let mut yubikey = reader.open()?; + + for key in Key::list(&mut yubikey)? { + // We only use the retired slots. + let slot = match key.slot() { + SlotId::Retired(slot) => slot, + _ => continue, + }; + + // Only P-256 keys are compatible with us. + let recipient = match key.certificate().subject_pki() { + PublicKeyInfo::EcP256(pubkey) => match p256::Recipient::from_pubkey(*pubkey) { + Some(recipient) => recipient, + None => continue, + }, + _ => continue, + }; + + let ((name, pin_policy, touch_policy), created) = + match x509_parser::parse_x509_certificate(key.certificate().as_ref()) + .ok() + .and_then(|(_, cert)| { + util::extract_name_and_policies(&mut yubikey, &key, &cert, all) + .map(|res| (res, cert.validity().not_before.to_rfc2822())) + }) { + Some(res) => res, + None => continue, + }; + + println!( + "# Serial: {}, Slot: {}", + yubikey.serial(), + // Use 1-indexing in the UI for niceness + USABLE_SLOTS.iter().position(|s| s == &slot).unwrap() + 1, + ); + println!("# Name: {}", name); + println!("# Created: {}", created); + println!("# PIN policy: {}", util::pin_policy_to_str(pin_policy)); + println!( + "# Touch policy: {}", + util::touch_policy_to_str(touch_policy) + ); + println!("{}", recipient.to_string()); + println!(); + } + println!(); + } + + Ok(()) } fn main() -> Result<(), Error> { @@ -53,11 +215,11 @@ fn main() -> Result<(), Error> { } else if opts.generate { todo!() } else if opts.identity { - todo!() + identity(opts) } else if opts.list { - todo!() + list(false) } else if opts.list_all { - todo!() + list(true) } else { // TODO: CLI identity generation Ok(()) diff --git a/src/p256.rs b/src/p256.rs new file mode 100644 index 0000000..5f6534b --- /dev/null +++ b/src/p256.rs @@ -0,0 +1,55 @@ +use bech32::ToBase32; +use elliptic_curve::sec1::EncodedPoint; +use p256::NistP256; +use sha2::{Digest, Sha256}; +use std::convert::TryInto; +use std::fmt; + +use crate::RECIPIENT_PREFIX; + +pub(crate) const TAG_BYTES: usize = 4; + +/// Wrapper around a compressed secp256r1 curve point. +#[derive(Clone)] +pub struct Recipient(EncodedPoint); + +impl fmt::Debug for Recipient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Recipient({:?})", self.as_bytes()) + } +} + +impl fmt::Display for Recipient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str( + bech32::encode(RECIPIENT_PREFIX, self.as_bytes().to_base32()) + .expect("HRP is valid") + .as_str(), + ) + } +} + +impl Recipient { + /// Attempts to parse a valid secp256r1 public key from its SEC-1 encoding. + pub(crate) fn from_pubkey(pubkey: EncodedPoint) -> Option { + if pubkey.is_compressed() { + if pubkey.decompress().is_some().into() { + Some(Recipient(pubkey)) + } else { + None + } + } else { + Some(Recipient(pubkey.compress())) + } + } + + /// Returns the compressed SEC-1 encoding of this public key. + pub(crate) fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + pub(crate) fn tag(&self) -> [u8; TAG_BYTES] { + let tag = Sha256::digest(self.to_string().as_bytes()); + (&tag[0..TAG_BYTES]).try_into().expect("length is correct") + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..c296e08 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,124 @@ +use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid}; +use yubikey_piv::{ + policy::{PinPolicy, TouchPolicy}, + Key, YubiKey, +}; + +use crate::{p256::Recipient, yubikey::Stub, PLUGIN_NAME}; + +const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8]; + +pub(crate) fn pin_policy_to_str(policy: Option) -> &'static str { + match policy { + Some(PinPolicy::Always) => "Always (A PIN is required for every decryption, if set)", + Some(PinPolicy::Once) => "Once (A PIN is required once per session, if set)", + Some(PinPolicy::Never) => "Never (A PIN is NOT required to decrypt)", + _ => "Unknown", + } +} + +pub(crate) fn touch_policy_to_str(policy: Option) -> &'static str { + match policy { + Some(TouchPolicy::Always) => "Always (A physical touch is required for every decryption)", + Some(TouchPolicy::Cached) => { + "Cached (A physical touch is required for decryption, and is cached for 15 seconds)" + } + Some(TouchPolicy::Never) => "Never (A physical touch is NOT required to decrypt)", + _ => "Unknown", + } +} + +pub(crate) fn extract_name(cert: &X509Certificate, all: bool) -> Option<(String, bool)> { + // Look at Subject Organization to determine if we created this. + match cert.subject().iter_organization().next() { + Some(org) if org.as_str() == Ok(PLUGIN_NAME) => { + // We store the identity name as a Common Name attribute. + let name = cert + .subject() + .iter_common_name() + .next() + .and_then(|cn| cn.as_str().ok()) + .map(|s| s.to_owned()) + .unwrap_or_default(); // TODO: This should always be present. + + Some((name, true)) + } + _ => { + // Not one of ours, but we've already filtered for compatibility. + if !all { + return None; + } + + // Display the entire subject. + let name = cert.subject().to_string(); + + Some((name, false)) + } + } +} + +pub(crate) fn extract_name_and_policies( + yubikey: &mut YubiKey, + key: &Key, + cert: &X509Certificate, + all: bool, +) -> Option<(String, Option, Option)> { + // We store the PIN and touch policies for identities in their certificates + // using the same certificate extension as PIV attestations. + // https://developers.yubico.com/PIV/Introduction/PIV_attestation.html + let policies = |c: &X509Certificate| { + c.extensions() + .get(&Oid::from(POLICY_EXTENSION_OID).unwrap()) + // If the encoded extension doesn't have 2 bytes, we assume it is invalid. + .filter(|policy| policy.value.len() >= 2) + .map(|policy| { + // We should only ever see one of three values for either policy, but + // handle unknown values just in case. + let pin_policy = match policy.value[0] { + 0x01 => Some(PinPolicy::Never), + 0x02 => Some(PinPolicy::Once), + 0x03 => Some(PinPolicy::Always), + _ => None, + }; + let touch_policy = match policy.value[1] { + 0x01 => Some(TouchPolicy::Never), + 0x02 => Some(TouchPolicy::Always), + 0x03 => Some(TouchPolicy::Cached), + _ => None, + }; + (pin_policy, touch_policy) + }) + .unwrap_or((None, None)) + }; + + extract_name(cert, all).map(|(name, ours)| { + if ours { + let (pin_policy, touch_policy) = policies(&cert); + (name, pin_policy, touch_policy) + } else { + // We can extract the PIN and touch policies via an attestation. This + // is slow, but the user has asked for all compatible keys, so... + let (pin_policy, touch_policy) = yubikey_piv::key::attest(yubikey, key.slot()) + .ok() + .and_then(|buf| { + x509_parser::parse_x509_certificate(&buf) + .map(|(_, c)| policies(&c)) + .ok() + }) + .unwrap_or((None, None)); + + (name, pin_policy, touch_policy) + } + }) +} + +pub(crate) fn print_identity(stub: Stub, recipient: Recipient, created: &str) { + let recipient = recipient.to_string(); + if !console::user_attended() { + eprintln!("Recipient: {}", recipient); + } + + println!("# created: {}", created); + println!("# recipient: {}", recipient); + println!("{}", stub.to_string()); +} diff --git a/src/yubikey.rs b/src/yubikey.rs new file mode 100644 index 0000000..937a9bc --- /dev/null +++ b/src/yubikey.rs @@ -0,0 +1,113 @@ +//! Structs for handling YubiKeys. + +use bech32::ToBase32; +use std::fmt; +use std::thread::sleep; +use std::time::{Duration, SystemTime}; +use yubikey_piv::{key::RetiredSlotId, yubikey::Serial, Readers, YubiKey}; + +use crate::{ + error::Error, + p256::{Recipient, TAG_BYTES}, + IDENTITY_PREFIX, +}; + +const ONE_SECOND: Duration = Duration::from_secs(1); +const FIFTEEN_SECONDS: Duration = Duration::from_secs(15); + +pub(crate) fn wait_for_readers() -> Result { + // Start a 15-second timer waiting for a YubiKey to be inserted (if necessary). + let start = SystemTime::now(); + loop { + let mut readers = Readers::open()?; + if readers.iter()?.len() > 0 { + break Ok(readers); + } + + match SystemTime::now().duration_since(start) { + Ok(end) if end >= FIFTEEN_SECONDS => return Err(Error::TimedOut), + _ => sleep(ONE_SECOND), + } + } +} + +pub(crate) fn open(serial: Option) -> Result { + if Readers::open()?.iter()?.len() == 0 { + if let Some(serial) = serial { + eprintln!("⏳ Please insert the YubiKey with serial {}.", serial); + } else { + eprintln!("⏳ Please insert the YubiKey."); + } + } + let mut readers = wait_for_readers()?; + let mut readers_iter = readers.iter()?; + + // --serial selects the YubiKey to use. If not provided, and more than one YubiKey is + // connected, an error is returned. + let yubikey = match (readers_iter.len(), serial) { + (0, _) => unreachable!(), + (1, None) => readers_iter.next().unwrap().open()?, + (1, Some(serial)) => { + let yubikey = readers_iter.next().unwrap().open()?; + if yubikey.serial() != serial { + return Err(Error::NoMatchingSerial(serial)); + } + yubikey + } + (_, Some(serial)) => { + let reader = readers_iter + .find(|reader| match reader.open() { + Ok(yk) => yk.serial() == serial, + _ => false, + }) + .ok_or(Error::NoMatchingSerial(serial))?; + reader.open()? + } + (_, None) => return Err(Error::MultipleYubiKeys), + }; + + Ok(yubikey) +} + +/// A reference to an age key stored in a YubiKey. +#[derive(Debug)] +pub struct Stub { + pub(crate) serial: Serial, + pub(crate) slot: RetiredSlotId, + pub(crate) tag: [u8; TAG_BYTES], + identity_index: usize, +} + +impl fmt::Display for Stub { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str( + bech32::encode(IDENTITY_PREFIX, self.to_bytes().to_base32()) + .expect("HRP is valid") + .to_uppercase() + .as_str(), + ) + } +} + +impl Stub { + /// Returns a key stub and recipient for this `(Serial, SlotId, PublicKey)` tuple. + /// + /// Does not check that the `PublicKey` matches the given `(Serial, SlotId)` tuple; + /// this is checked at decryption time. + pub(crate) fn new(serial: Serial, slot: RetiredSlotId, recipient: &Recipient) -> Self { + Stub { + serial, + slot, + tag: recipient.tag(), + identity_index: 0, + } + } + + fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(9); + bytes.extend_from_slice(&self.serial.0.to_le_bytes()); + bytes.push(self.slot.into()); + bytes.extend_from_slice(&self.tag); + bytes + } +}