diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 41541bc4..5171a62c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,15 +1,63 @@ name: tests -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: + build_opensc: + name: Build opensc package + #if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Cache deb files + uses: actions/cache@v3 + env: + cache-name: opensc-deb + with: + path: opensc*.deb + key: ${{ runner.os }}-${{ env.cache-name }} + - name: Check file existence + id: check_deb_files + uses: andstor/file-existence-action@v1 + with: + files: "opensc*.deb" + - name: Package Install + if: steps.check_deb_files.outputs.files_exists == 'false' + run: | + sudo sed -i 's/^# deb-src/deb-src/' /etc/apt/sources.list + sudo apt-get update + sudo apt-get install -q -y curl git gcc g++ cmake swig psmisc procps debian-keyring devscripts + sudo apt-get build-dep -q -y opensc + sudo rm -f /usr/bin/clang-tidy + - name: Build the package + if: steps.check_deb_files.outputs.files_exists == 'false' + run: | + dget http://archive.ubuntu.com/ubuntu/pool/universe/o/opensc/opensc_0.23.0-0.1ubuntu1.dsc + cd opensc-0.23.0 + curl https://github.com/OpenSC/OpenSC/commit/a0aef25c7f2ce0ec2c7e1014f959f0fe86ff0479.diff | patch -p1 + dch --local ppa~jammy --distribution jammy "Apply a patch. Backports to Jammy." + DEB_BUILD_OPTIONS='parallel=2' debuild --no-sign -b + - name: Upload package files + uses: actions/upload-artifact@v3 + with: + name: opensc-deb + path: opensc*.deb + + + build_test: name: Build and Test runs-on: ubuntu-latest + needs: build_opensc steps: + - name: Download backport OpenSC package + uses: actions/download-artifact@v3 + with: + name: opensc-deb + - name: Package Install run: | sudo apt-add-repository ppa:yubico/stable sudo apt-get update - sudo apt-get install -q -y git gcc g++ cmake swig psmisc procps pcscd pcsc-tools yubico-piv-tool libhidapi-dev libassuan-dev libgcrypt20-dev libksba-dev libnpth0-dev opensc openssl openssh-server libpcsclite-dev libudev-dev libcmocka-dev python3-pip python3-setuptools python3-wheel lcov yubikey-manager libcbor-dev + sudo apt-get install -q -y git gcc g++ cmake swig psmisc procps pcscd pcsc-tools yubico-piv-tool libhidapi-dev libassuan-dev libgcrypt20-dev libksba-dev libnpth0-dev libssl3 zlib1g libglib2.0-0 openssl openssh-server libpcsclite-dev libudev-dev libcmocka-dev python3-pip python3-setuptools python3-wheel lcov yubikey-manager libcbor-dev + sudo dpkg -i opensc*.deb pip3 install --upgrade pip - name: Set up Go 1.16 @@ -324,6 +372,7 @@ jobs: - name: Test the PIV run: | set -o xtrace + go test -v test-via-pcsc/piv_test.go RDID="Canokey [OpenPGP PIV OATH] 00 00" yubico-piv-tool -r "$RDID" -a status -a set-ccc -a set-chuid -a status opensc-tool -r "$RDID" -s '00 F8 00 00' | grep 'SW1=0x90, SW2=0x00' # PIV_INS_GET_SERIAL, Yubico @@ -340,9 +389,10 @@ jobs: yubico-piv-tool -r "$RDID" -a verify-pin -P 654321 yubico-piv-tool -r "$RDID" -a set-mgm-key -n F1F2F3F4F5F6F7F8F1F2F3F4F5F6F7F8F1F2F3F4F5F6F7F8 yubico-piv-tool -r "$RDID" -a set-mgm-key --key=F1F2F3F4F5F6F7F8F1F2F3F4F5F6F7F8F1F2F3F4F5F6F7F8 -n 010203040506070801020304050607080102030405060708 - #export PIV_EXT_AUTH_KEY=test-via-pcsc/PIV_EXT_AUTH_KEY.txt - #piv-tool --reader "$RDID" --admin A:9B:03 # External Auth - #piv-tool --reader "$RDID" --admin M:9B:03 # Mutual Auth + export PIV_EXT_AUTH_KEY=$PWD/test-via-pcsc/PIV_EXT_AUTH_KEY.txt + # opensc 0.22.0~0.23.0 has a bug on External Auth. See opensc commit: a0aef25c7f2ce0ec2c7e1014f959f0fe86ff0479 + piv-tool --reader "$RDID" --admin A:9B:03 # External Auth + piv-tool --reader "$RDID" --admin M:9B:03 # Mutual Auth ## Key generation PIVGenKeyCert() { key=$1 diff --git a/applets/admin/admin.c b/applets/admin/admin.c index bfbcedf5..1fafecfe 100644 --- a/applets/admin/admin.c +++ b/applets/admin/admin.c @@ -39,6 +39,8 @@ uint8_t cfg_is_webusb_landing_enable(void) { return current_config.webusb_landin uint8_t cfg_is_kbd_with_return_enable(void) { return current_config.kbd_with_return_en; } +uint8_t cfg_is_piv_algo_extension_enable(void) { return current_config.piv_algo_ext_en; } + void admin_poweroff(void) { pin.is_validated = 0; } int admin_install(uint8_t reset) { @@ -114,6 +116,9 @@ static int admin_config(const CAPDU *capdu, RAPDU *rapdu) { case ADMIN_P1_CFG_KBD_WITH_RETURN: current_config.kbd_with_return_en = P2 & 1; break; + case ADMIN_P1_CFG_PIV_ALGO_EXT: + current_config.piv_algo_ext_en = P2 & 1; + break; default: EXCEPT(SW_WRONG_P1P2); } diff --git a/applets/piv/piv.c b/applets/piv/piv.c index b49add4c..a620c076 100644 --- a/applets/piv/piv.c +++ b/applets/piv/piv.c @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include #include @@ -39,13 +40,11 @@ #define ALG_ECC_256 0x11 #define ALG_ECC_384 0x14 #define ALG_ED25519 0x22 // Not defined in NIST SP 800-78-4, defined in https://github.com/go-piv/piv-go/pull/69 -#ifdef PIV_CUSTOM_ALG_EXT #define ALG_RSA_3072 0x50 // Not defined in NIST SP 800-78-4 #define ALG_RSA_4096 0x51 // Not defined in NIST SP 800-78-4 #define ALG_X25519 0x52 // Not defined in NIST SP 800-78-4 #define ALG_SECP256K1 0x53 // Not defined in NIST SP 800-78-4 #define ALG_SM2 0x54 // Not defined in NIST SP 800-78-4 -#endif #define TDEA_BLOCK_SIZE 8 @@ -70,6 +69,19 @@ #define AUTH_STATE_EXTERNAL 1 #define AUTH_STATE_MUTUAL 2 +#define PIV_TOUCH(cached) \ + do { \ + if (is_nfc()) break; \ + uint32_t current_tick = device_get_tick(); \ + if ((cached) && current_tick > last_touch && current_tick - last_touch < 15000) break; \ + switch (wait_for_user_presence(WAIT_ENTRY_CCID)) { \ + case USER_PRESENCE_CANCEL: \ + case USER_PRESENCE_TIMEOUT: \ + EXCEPT(SW_ERROR_WHILE_RECEIVING); \ + } \ + last_touch = device_get_tick(); \ + } while (0) + static const uint8_t DEFAULT_MGMT_KEY[] = {1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8}; static const char *DEFAULT_PIN = "123456\xFF\xFF"; static const char *DEFAULT_PUK = "12345678"; @@ -83,6 +95,7 @@ static uint8_t pin_is_consumed; static char piv_do_path[MAX_DO_PATH_LEN]; // data object file path during chaining read/write static int piv_do_write; // -1: not in chaining write, otherwise: count of remaining bytes static int piv_do_read; // -1: not in chaining read mode, otherwise: data object offset +static uint32_t last_touch = UINT32_MAX; static pin_t pin = {.min_length = 8, .max_length = 8, .is_validated = 0, .path = "piv-pin"}; static pin_t puk = {.min_length = 8, .max_length = 8, .is_validated = 0, .path = "piv-puk"}; @@ -99,7 +112,6 @@ static int create_key(const char *path, key_usage_t usage, pin_policy_t pin_poli .pin_policy = pin_policy, .touch_policy = TOUCH_POLICY_NEVER}}; if (ck_write_key(path, &key) < 0) { - ERR_MSG("Create key %s failed\n", path); return -1; } return 0; @@ -115,38 +127,42 @@ static key_type_t algo_id_to_key_type(uint8_t id) { return RSA2048; case ALG_ED25519: return ED25519; -#ifdef PIV_CUSTOM_ALG_EXT - case ALG_X25519: - return X25519; - case ALG_SECP256K1: - return SECP256K1; - case ALG_SM2: - return SM2; - case ALG_RSA_3072: - return RSA3072; - case ALG_RSA_4096: - return RSA4096; -#endif case ALG_DEFAULT: case ALG_TDEA_3KEY: return TDEA; default: - return KEY_TYPE_PKC_END; + + if (!cfg_is_piv_algo_extension_enable()) return KEY_TYPE_PKC_END; + + switch (id) { + case ALG_X25519: + return X25519; + case ALG_SECP256K1: + return SECP256K1; + case ALG_SM2: + return SM2; + case ALG_RSA_3072: + return RSA3072; + case ALG_RSA_4096: + return RSA4096; + default: + return KEY_TYPE_PKC_END; + } } } static uint8_t key_type_to_algo_id[] = { [SECP256R1] = ALG_ECC_256, [SECP384R1] = ALG_ECC_384, - [ED25519] = ALG_ED25519, [RSA2048] = ALG_RSA_2048, -#ifdef PIV_CUSTOM_ALG_EXT - [SM2] = ALG_SM2, - [SECP256K1] = ALG_SECP256K1, + [ED25519] = ALG_ED25519, [X25519] = ALG_X25519, + [SECP256K1] = ALG_SECP256K1, + [SM2] = ALG_SM2, [RSA3072] = ALG_RSA_3072, [RSA4096] = ALG_RSA_4096, -#endif + [TDEA] = ALG_TDEA_3KEY, + [KEY_TYPE_PKC_END] = ALG_DEFAULT, }; int piv_security_status_check(uint8_t id, const key_meta_t *meta) { @@ -216,7 +232,6 @@ int piv_install(uint8_t reset) { .touch_policy = TOUCH_POLICY_NEVER}}; memcpy(admin_key.data, DEFAULT_MGMT_KEY, 24); if (ck_write_key(CARD_ADMIN_KEY_PATH, &admin_key) < 0) { - ERR_MSG("Write admin key failed\n"); return -1; } uint8_t tmp = 0x01; @@ -310,7 +325,6 @@ static int piv_get_large_data(const CAPDU *capdu, RAPDU *rapdu, const char *path int read = read_file(path, RDATA, 0, LE); // return first chunk if (read < 0) { - ERR_MSG("read file %s error: %d\n", path, read); return -1; } LL = read; @@ -370,7 +384,6 @@ static int piv_get_data(const CAPDU *capdu, RAPDU *rapdu) { if (path == NULL) EXCEPT(SW_FILE_NOT_FOUND); int size = get_file_size(path); if (size < 0) { - ERR_MSG("read file size %s error: %d\n", path, size); return -1; } if (size == 0) EXCEPT(SW_FILE_NOT_FOUND); @@ -386,12 +399,10 @@ static int piv_get_data_response(const CAPDU *capdu, RAPDU *rapdu) { int size = get_file_size(piv_do_path); if (size < 0) { - ERR_MSG("read file size %s error: %d\n", piv_do_path, size); return -1; } int read = read_file(piv_do_path, RDATA, piv_do_read, LE); if (read < 0) { - ERR_MSG("read file %s error: %d\n", piv_do_path, read); return -1; } DBG_MSG("continue to read file %s, expected: %d, read: %d\n", piv_do_path, LE, read); @@ -514,11 +525,12 @@ static int piv_general_authenticate(const CAPDU *capdu, RAPDU *rapdu) { EXCEPT(SW_REFERENCE_DATA_NOT_FOUND); } if (ck_read_key_metadata(key_path, &key.meta) < 0) { - ERR_MSG("Read metadata of %s failed\n", key_path); return -1; } DBG_KEY_META(&key.meta); + // empty slot after reset + if (key.meta.type == KEY_TYPE_PKC_END) EXCEPT(SW_CONDITIONS_NOT_SATISFIED); if (algo_id_to_key_type(P1) != key.meta.type) { DBG_MSG("The value of P1 mismatches the key specified by P2\n"); EXCEPT(SW_WRONG_P1P2); @@ -541,6 +553,9 @@ static int piv_general_authenticate(const CAPDU *capdu, RAPDU *rapdu) { DBG_MSG("Tag %02X, pos: %d, len: %d\n", tag, pos[tag - 0x80], len[tag - 0x80]); } + // User presence test + if (key.meta.touch_policy == TOUCH_POLICY_CACHED || key.meta.touch_policy == TOUCH_POLICY_ALWAYS) PIV_TOUCH(key.meta.touch_policy == TOUCH_POLICY_CACHED); + // // CASE 1 - INTERNAL AUTHENTICATE (Key ID = 9A / 9E) // Authenticates the CARD to the CLIENT and is also used for KEY ESTABLISHMENT @@ -568,7 +583,6 @@ static int piv_general_authenticate(const CAPDU *capdu, RAPDU *rapdu) { EXCEPT(SW_WRONG_LENGTH); } if (ck_read_key(key_path, &key) < 0) { - ERR_MSG("Read key failed\n"); return -1; } DBG_KEY_META(&key.meta); @@ -652,13 +666,11 @@ static int piv_general_authenticate(const CAPDU *capdu, RAPDU *rapdu) { auth_ctx[OFFSET_AUTH_STATE] = AUTH_STATE_EXTERNAL; if (ck_read_key(key_path, &key) < 0) { - ERR_MSG("Read key failed\n"); return -1; } DBG_KEY_META(&key.meta); if (tdes_enc(RDATA + 4, auth_ctx + OFFSET_AUTH_CHALLENGE, key.data) < 0) { - ERR_MSG("TDEA failed\n"); memzero(&key, sizeof(key)); return -1; } @@ -706,13 +718,11 @@ static int piv_general_authenticate(const CAPDU *capdu, RAPDU *rapdu) { LL = TDEA_BLOCK_SIZE + 4; if (ck_read_key(key_path, &key) < 0) { - ERR_MSG("Read key failed\n"); return -1; } DBG_KEY_META(&key.meta); if (tdes_enc(auth_ctx + OFFSET_AUTH_CHALLENGE, RDATA + 4, key.data) < 0) { - ERR_MSG("TDEA failed\n"); memzero(&key, sizeof(key)); return -1; } @@ -747,13 +757,11 @@ static int piv_general_authenticate(const CAPDU *capdu, RAPDU *rapdu) { LL = TDEA_BLOCK_SIZE + 4; if (ck_read_key(key_path, &key) < 0) { - ERR_MSG("Read key failed\n"); return -1; } DBG_KEY_META(&key.meta); if (tdes_enc(DATA + pos[IDX_CHALLENGE], RDATA + 4, key.data) < 0) { - ERR_MSG("TDEA failed\n"); memzero(&key, sizeof(key)); return -1; } @@ -780,7 +788,6 @@ static int piv_general_authenticate(const CAPDU *capdu, RAPDU *rapdu) { EXCEPT(SW_WRONG_DATA); } if (ck_read_key(key_path, &key) < 0) { - ERR_MSG("Read key failed\n"); return -1; } DBG_KEY_META(&key.meta); @@ -833,7 +840,6 @@ static int piv_put_data(const CAPDU *capdu, RAPDU *rapdu) { DBG_MSG("write file %s, first chunk length %d\n", path, size); int rc = write_file(path, DATA + 5, 0, size, 1); if (rc < 0) { - ERR_MSG("write file %s error: %d\n", path, rc); return -1; } if ((CLA & 0x10) != 0 && size < max_len) { @@ -855,7 +861,6 @@ static int piv_put_data(const CAPDU *capdu, RAPDU *rapdu) { DBG_MSG("write file %s, continuous chunk length %d\n", piv_do_path, LC); int rc = append_file(piv_do_path, DATA, LC); if (rc < 0) { - ERR_MSG("write file %s error: %d\n", piv_do_path, rc); return -1; } if ((CLA & 0x10) == 0) { // last chunk @@ -884,7 +889,6 @@ static int piv_generate_asymmetric_key_pair(const CAPDU *capdu, RAPDU *rapdu) { const char *key_path = get_key_path(P2); ck_key_t key; if (ck_read_key(key_path, &key) < 0) { - ERR_MSG("Fail to read key %s\n", key_path); return -1; } @@ -892,7 +896,6 @@ static int piv_generate_asymmetric_key_pair(const CAPDU *capdu, RAPDU *rapdu) { if (key.meta.type == KEY_TYPE_PKC_END) EXCEPT(SW_WRONG_DATA); start_quick_blinking(0); if (ck_generate_key(&key) < 0) { - ERR_MSG("Generate key %s failed\n", key_path); return -1; } int err = ck_parse_piv_policies(&key, &DATA[5], LC - 5); @@ -902,7 +905,6 @@ static int piv_generate_asymmetric_key_pair(const CAPDU *capdu, RAPDU *rapdu) { EXCEPT(SW_WRONG_DATA); } if (ck_write_key(key_path, &key) < 0) { - ERR_MSG("Write key %s failed\n", key_path); return -1; } DBG_MSG("Generate key %s successful\n", key_path); @@ -950,7 +952,6 @@ static int piv_import_asymmetric_key(const CAPDU *capdu, RAPDU *rapdu) { } ck_key_t key; if (ck_read_key(key_path, &key) < 0) { - ERR_MSG("Fail to read key %s\n", key_path); return -1; } @@ -1039,7 +1040,6 @@ static int piv_get_metadata(const CAPDU *capdu, RAPDU *rapdu) { ck_key_t key; if (ck_read_key(key_path, &key) < 0) { - ERR_MSG("Read key failed\n"); return -1; } DBG_KEY_META(&key.meta); @@ -1057,7 +1057,6 @@ static int piv_get_metadata(const CAPDU *capdu, RAPDU *rapdu) { RDATA[pos++] = 0x04; // Public int len = ck_encode_public_key(&key, &RDATA[pos], true); if (len < 0) { - ERR_MSG("Encode public key failed\n"); memzero(&key, sizeof(key)); return -1; } diff --git a/canokey-crypto b/canokey-crypto index 75192fdc..9dd712b7 160000 --- a/canokey-crypto +++ b/canokey-crypto @@ -1 +1 @@ -Subproject commit 75192fdc78a62b16d4238876bd626affd47289c8 +Subproject commit 9dd712b79da8e28a6ff3b9cb8aeb165223971645 diff --git a/include/admin.h b/include/admin.h index 3a0a21a4..d54b740c 100644 --- a/include/admin.h +++ b/include/admin.h @@ -28,6 +28,7 @@ #define ADMIN_P1_CFG_NDEF 0x04 #define ADMIN_P1_CFG_WEBUSB_LANDING 0x05 #define ADMIN_P1_CFG_KBD_WITH_RETURN 0x06 +#define ADMIN_P1_CFG_PIV_ALGO_EXT 0x07 typedef struct { uint32_t reserved; @@ -37,6 +38,7 @@ typedef struct { uint32_t ndef_en : 1; uint32_t webusb_landing_en : 1; uint32_t kbd_with_return_en : 1; + uint32_t piv_algo_ext_en : 1; } __packed admin_device_config_t; void admin_poweroff(void); @@ -52,5 +54,6 @@ uint8_t cfg_is_kbd_interface_enable(void); uint8_t cfg_is_ndef_enable(void); uint8_t cfg_is_webusb_landing_enable(void); uint8_t cfg_is_kbd_with_return_enable(void); +uint8_t cfg_is_piv_algo_extension_enable(void); #endif // CANOKEY_CORE_ADMIN_ADMIN_H_ diff --git a/littlefs b/littlefs index 9c7e2320..40dba4a5 160000 --- a/littlefs +++ b/littlefs @@ -1 +1 @@ -Subproject commit 9c7e232086f865cff0bb96fe753deb66431d91fd +Subproject commit 40dba4a556e0d81dfbe64301a6aa4e18ceca896c diff --git a/src/key.c b/src/key.c index a99aa684..d41ad281 100644 --- a/src/key.c +++ b/src/key.c @@ -71,38 +71,38 @@ int ck_encode_public_key(const ck_key_t *key, uint8_t *buf, bool include_length) int ck_parse_piv_policies(ck_key_t *key, const uint8_t *buf, size_t buf_len) { const uint8_t *end = buf + buf_len; - if (buf < end) { - DBG_MSG("May have pin policy\n"); - if (buf < end && *buf++ != 0xAA) { - DBG_MSG("Wrong tag for pin policy\n"); - return KEY_ERR_DATA; - } - if (buf < end && *buf++ != 0x01) { - DBG_MSG("Wrong length for pin policy\n"); - return KEY_ERR_LENGTH; - } - if (buf < end && (*buf > PIN_POLICY_ALWAYS || *buf < PIN_POLICY_NEVER)) { - DBG_MSG("Wrong data for pin policy\n"); - return KEY_ERR_DATA; - } - key->meta.pin_policy = *buf++; - } - - if (buf < end) { - DBG_MSG("May have touch policy\n"); - if (buf < end && *buf++ != 0xAB) { - DBG_MSG("Wrong tag for touch policy\n"); - return KEY_ERR_DATA; - } - if (buf < end && *buf++ != 0x01) { - DBG_MSG("Wrong length for touch policy\n"); - return KEY_ERR_LENGTH; - } - if (buf < end && (*buf > TOUCH_POLICY_CACHED || *buf < TOUCH_POLICY_NEVER)) { - DBG_MSG("Wrong data for touch policy\n"); - return KEY_ERR_DATA; + while (buf < end) { + switch (*buf++) { + case 0xAA: + DBG_MSG("May have pin policy\n"); + if (buf < end && *buf++ != 0x01) { + DBG_MSG("Wrong length for pin policy\n"); + return KEY_ERR_LENGTH; + } + if (buf < end && (*buf > PIN_POLICY_ALWAYS || *buf < PIN_POLICY_NEVER)) { + DBG_MSG("Wrong data for pin policy\n"); + return KEY_ERR_DATA; + } + key->meta.pin_policy = *buf++; + break; + + case 0xAB: + DBG_MSG("May have touch policy\n"); + if (buf < end && *buf++ != 0x01) { + DBG_MSG("Wrong length for touch policy\n"); + return KEY_ERR_LENGTH; + } + if (buf < end && (*buf > TOUCH_POLICY_CACHED || *buf < TOUCH_POLICY_NEVER)) { + DBG_MSG("Wrong data for touch policy\n"); + return KEY_ERR_DATA; + } + key->meta.touch_policy = *buf++; + break; + + default: + buf = end; + break; } - key->meta.touch_policy = *buf++; } return 0; diff --git a/test-via-pcsc/oath_test.go b/test-via-pcsc/oath_test.go index bfa2711a..6b94bcb3 100644 --- a/test-via-pcsc/oath_test.go +++ b/test-via-pcsc/oath_test.go @@ -42,6 +42,23 @@ func otpCodeShouldEqual(actual interface{}, expected ...interface{}) string { expected[0], actual, hex.EncodeToString(b), expected[2], expected[3]) } +func clearRecords(oath *ykoath.OATH, NKeys int) { + lResult, err := oath.List() + So(err, ShouldBeNil) + if NKeys != -1 { + So(len(lResult), ShouldEqual, NKeys) + } + + for _, item := range lResult { + err := oath.Delete(item.Name) + So(err, ShouldBeNil) + } + + lResult, err = oath.List() + So(err, ShouldBeNil) + So(lResult, ShouldBeEmpty) +} + func TestOath(t *testing.T) { Convey("OATH should work", t, func(ctx C) { @@ -62,23 +79,6 @@ func TestOath(t *testing.T) { NumKeys := 100 - clearRecords := func(NKeys int) { - lResult, err := oath.List() - So(err, ShouldBeNil) - if NKeys != -1 { - So(len(lResult), ShouldEqual, NKeys) - } - - for _, item := range lResult { - err := oath.Delete(item.Name) - So(err, ShouldBeNil) - } - - lResult, err = oath.List() - So(err, ShouldBeNil) - So(lResult, ShouldBeEmpty) - } - Convey("With invalid parameters", func(ctx C) { name := strings.Repeat("O", 64) err = oath.Put(name, ykoath.HmacSha1, ykoath.Totp, 6, make([]byte, 65), false, false, 0) @@ -87,7 +87,7 @@ func TestOath(t *testing.T) { }) Convey("When deleting all keys", func(ctx C) { - clearRecords(-1) + clearRecords(oath, -1) }) Convey("When name is too long or empty", func(ctx C) { @@ -247,7 +247,7 @@ func TestOath(t *testing.T) { key2Alg[name] = alg2 } - defer clearRecords(CurKeys) + defer clearRecords(oath, CurKeys) validateTotp := func(name string, otp string) { // fmt.Printf("%s %s\n", otp, name) @@ -379,14 +379,34 @@ func TestOath(t *testing.T) { } }) }) + }) + +} + +func TestFullOath(t *testing.T) { + Convey("OATH should work", t, func(ctx C) { + oath, err := ykoath.New() + So(err, ShouldBeNil) + defer oath.Close() + + // enable OATH for this session + _, err = oath.Select() + So(err, ShouldBeNil) - Convey("Fill all slots in the end", func(ctx C) { + clearRecords(oath, -1) + + NumKeys := 100 + + Convey("If we fill all slots in the end", func(ctx C) { var name string - type1 := ykoath.Hotp + type1 := ykoath.Totp alg1, _ := chooseAlgorithm() key := make([]byte, 64) for i := 0; i < NumKeys; i++ { alg1, _ = chooseAlgorithm() + if i == NumKeys-1 { + type1 = ykoath.Hotp + } name = fmt.Sprintf("Index%054dHmac%d", i, alg1) // len=5+54+4+1 _, err := crand.Read(key) @@ -412,5 +432,4 @@ func TestOath(t *testing.T) { }) }) }) - } diff --git a/test-via-pcsc/openpgp_test.go b/test-via-pcsc/openpgp_test.go index fce0e0c6..48533b93 100644 --- a/test-via-pcsc/openpgp_test.go +++ b/test-via-pcsc/openpgp_test.go @@ -484,6 +484,55 @@ func TestOpenPGPCerts(t *testing.T) { }) } +func TestOpenPGPED25519(t *testing.T) { + doGenerate := true + + Convey("Connecting to applet", t, func(ctx C) { + + app, err := New() + So(err, ShouldBeNil) + defer app.Close() + _, code, err := app.Send([]byte{0x00, 0xA4, 0x04, 0x00, 0x06, 0xD2, 0x76, 0x00, 0x01, 0x24, 0x01}) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x9000) + + if doGenerate { + // Admin PIN + _, code, err = app.Send([]byte{0x00, 0x20, 0x00, 0x83, 0x08, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38}) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x9000) + // ED25519 key-attr + _, code, err = app.Send([]byte{0x00, 0xDA, 0x00, 0xC1, 0x0A, 0x16, 0x2B, 0x06, 0x01, 0x04, 0x01, 0xDA, 0x47, 0x0F, 0x01}) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x9000) + // generate + _, code, err = app.Send([]byte{0x00, 0x47, 0x80, 0x00, 0x02, 0xB6, 0x00}) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x9000) + } + verifyPin := []byte{0x00, 0x20, 0x00, 0x81, 0x06, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36} + // compute digital signature + signAPDU := []byte{0x00, 0x2A, 0x9E, 0x9A, 0x20, 0x61, 0x1C, 0xE6, 0x47, 0x96, 0xBC, 0x6A, 0xC0, 0x0F, 0x3D, 0xD7, 0x0A, 0x73, 0x67, 0xD2, 0xAD, 0xA6, 0xEB, 0x34, 0xCD, 0x28, 0xA2, 0xA2, 0x52, 0x52, 0xF1, 0xB3, 0xD4, 0x63, 0xEA, 0x3A, 0xAF, 0x00} + _, code, err = app.Send(verifyPin) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x9000) + rsp1, code, err := app.Send(signAPDU) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x9000) + _, code, err = app.Send(verifyPin) + So(err, ShouldBeNil) + rsp2, code, err := app.Send(signAPDU) + So(code, ShouldEqual, 0x9000) + _, code, err = app.Send(verifyPin) + So(err, ShouldBeNil) + rsp3, code, err := app.Send(signAPDU) + So(code, ShouldEqual, 0x9000) + So(rsp3, ShouldResemble, rsp2) + So(rsp1, ShouldResemble, rsp2) + + }) +} + func TestAppletReset(t *testing.T) { Convey("Connecting to applet", t, func(ctx C) { diff --git a/test-via-pcsc/piv_test.go b/test-via-pcsc/piv_test.go new file mode 100644 index 00000000..1a835843 --- /dev/null +++ b/test-via-pcsc/piv_test.go @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "crypto/des" + "fmt" + "strings" + "testing" + + "github.com/ebfe/scard" + "github.com/pkg/errors" + . "github.com/smartystreets/goconvey/convey" +) + +const ( + errFailedToConnect = "failed to connect to reader" + errFailedToDisconnect = "failed to disconnect from reader" + errFailedToEstablishContext = "failed to establish context" + errFailedToListReaders = "failed to list readers" + errFailedToListSuitableReader = "no suitable reader found (out of %d readers)" + errFailedToReleaseContext = "failed to release context" + errFailedToTransmit = "failed to transmit APDU" + errUnknownTag = "unknown tag (%x)" +) + +const ADMIN_P1_CFG_PIV_ALGO_EXT = 7 + +type PIVApplet struct { + context *scard.Context + card *scard.Card +} + +func New() (*PIVApplet, error) { + context, err := scard.EstablishContext() + if err != nil { + return nil, errors.Wrapf(err, errFailedToEstablishContext) + } + readers, err := context.ListReaders() + if err != nil { + context.Release() + return nil, errors.Wrapf(err, errFailedToListReaders) + } + for _, reader := range readers { + if strings.Contains(strings.ToLower(reader), "canokey") && strings.Contains(strings.ToLower(reader), "piv") { + card, err := context.Connect(reader, scard.ShareShared, scard.ProtocolAny) + if err != nil { + context.Release() + return nil, errors.Wrapf(err, errFailedToConnect) + } + + return &PIVApplet{ + card: card, + context: context, + }, nil + } + } + context.Release() + return nil, fmt.Errorf(errFailedToListSuitableReader, len(readers)) +} +func (o *PIVApplet) Close() error { + if err := o.card.Disconnect(scard.LeaveCard); err != nil { + return errors.Wrapf(err, errFailedToDisconnect) + } + o.card = nil + if err := o.context.Release(); err != nil { + return errors.Wrapf(err, errFailedToReleaseContext) + } + o.context = nil + return nil +} +func (o *PIVApplet) Send(apdu []byte) ([]byte, uint16, error) { + res, err := o.card.Transmit(apdu) + + if err != nil { + return nil, 0, errors.Wrapf(err, errFailedToTransmit) + } + return res[0 : len(res)-2], uint16(res[len(res)-2])<<8 | uint16(res[len(res)-1]), nil +} +func (app *PIVApplet) ConfigPIVAlgoExt(enable uint8) { + verifyPin := func(pin []byte) (code uint16) { + _, code, err := app.Send(append([]byte{0x00, 0x20, 0x00, 0x00, byte(len(pin))}, pin...)) + So(err, ShouldBeNil) + return + } + + _, code, err := app.Send([]byte{0x00, 0xA4, 0x04, 0x00, 0x05, 0xF0, 0x00, 0x00, 0x00, 0x00}) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x9000) + So(verifyPin([]byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x36}), ShouldEqual, 0x9000) + + apdu := []byte{0x00, 0x40, uint8(ADMIN_P1_CFG_PIV_ALGO_EXT), enable} + _, code, err = app.Send(apdu) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x9000) +} +func (app *PIVApplet) Select() { + _, code, err := app.Send([]byte{0x00, 0xA4, 0x04, 0x00, 0x05, 0xA0, 0x00, 0x00, 0x03, 0x08}) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x9000) +} +func (app *PIVApplet) Authenticate() { + key := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} + ci, err := des.NewTripleDESCipher(key) + So(err, ShouldBeNil) + + chal, code, err := app.Send([]byte{0x00, 0x87, 0x03, 0x9B, 0x04, 0x7C, 0x02, 0x81, 0x00, 0x00}) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x9000) + So(chal[0], ShouldEqual, 0x7C) + l := chal[1] + chal = chal[2 : 2+l] + So(chal[0], ShouldEqual, 0x81) + l = chal[1] + chal = chal[2 : 2+l] + + rsp := make([]byte, l) + ci.Encrypt(rsp, chal) + + _, code, err = app.Send(append([]byte{0x00, 0x87, 0x03, 0x9B, byte(l + 4), 0x7C, byte(l + 2), 0x82, byte(l)}, rsp...)) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x9000) +} + +func TestPIVExtensions(t *testing.T) { + + Convey("Connecting to applet", t, func(ctx C) { + + app, err := New() + So(err, ShouldBeNil) + defer app.Close() + + Convey("Enable algorithm extension", func(ctx C) { + app.ConfigPIVAlgoExt(1) + }) + + Convey("Select the Applet and Authenticate", func(ctx C) { + app.Select() + app.Authenticate() + }) + + Convey("Generate the key", func(ctx C) { + for keyID := 0x50; keyID <= 0x54; keyID++ { + _, code, err := app.Send([]byte{0x00, 0x47, 0x00, 0x9E, 0x05, 0xAC, 0x03, 0x80, 0x01, byte(keyID)}) + So(err, ShouldBeNil) + if code&0xFF00 != 0x6100 { + So(code, ShouldEqual, 0x9000) + } + } + }) + + Convey("Disable algorithm extension", func(ctx C) { + app.ConfigPIVAlgoExt(0) + }) + + Convey("Select the Applet and Authenticate again", func(ctx C) { + app.Select() + app.Authenticate() + }) + + Convey("Generate the key again", func(ctx C) { + for keyID := 0x50; keyID <= 0x54; keyID++ { + _, code, err := app.Send([]byte{0x00, 0x47, 0x00, 0x9E, 0x05, 0xAC, 0x03, 0x80, 0x01, byte(keyID)}) + So(err, ShouldBeNil) + So(code, ShouldEqual, 0x6A80) + } + }) + }) +}