[PATCH gnupg] scd:piv: Let the card decide whether signing needs a PIN
David Seifert
soap at gentoo.org
Tue Nov 3 13:17:24 CET 2026
* scd/app-piv.c (YKPIV_METADATA_POLICY_TAG): Remove.
(get_cached_data): Remove support for special tag 0xF7xx.
(check_pin_policy): Remove.
(do_sign): Do not verify the PIN before signing. If GENERAL
AUTHENTICATE fails with GPG_ERR_BAD_PIN, force a verify and retry
once.
--
Deciding up front whether to ask for the PIN requires knowing the
card's per-key PIN policy. The YubiKey GET METADATA command (0xF7)
only exists on YubiKey 5.3 and later and was only used for cards
detected as YubiKeys. Older YubiKeys and other cards implementing
the same per-key policies (e.g. TOKEN2 PIV) thus fall back to
forcing a verify for 9C on every signature, ignoring a configured
ONCE or NEVER policy.
Instead, just try the operation first. If the card refuses it with
6982 (mapped to GPG_ERR_BAD_PIN), force a fresh PIN entry and retry
once. This works for any card that enforces its own policy, needs
no vendor detection, and still prompts for every signature with a
"PIN Always" key, because a forced verify bypasses the PIN cache.
This changes signing for all keys handled by do_sign (and thus
do_auth), not only 9C: e.g. 9E now signs without a PIN, as intended
by SP 800-73.
I have tested the change against various smartcards with PIV
functionality. A fake pinentry counted the prompts - each scenario
starts with a fresh P-256 key in 9C and a card reset.
TOKEN2 PIV card (PIN+ R3.3, policy read back via GET METADATA):
9C policy Operations Prompts Result
NEVER sign, sign 0, 0 both signed
ONCE sign, sign 1, 0 both signed
ALWAYS sign, sign 1, 1 both signed
ALWAYS cancel 1 canceled, retries kept (6)
ALWAYS wrong, good PIN 1, 1 Bad PIN (6 -> 5), then
signed, retries back to 6
YubiKey 4 (firmware 4.3.7, policy confirmed via attestation):
9C policy Operations Prompts Result
NEVER sign, sign 0, 0 both signed
ONCE sign, sign 1, 0 both signed
ALWAYS sign, sign 1, 1 both signed
ALWAYS cancel 1 canceled, retries kept (3)
ALWAYS wrong, good PIN 1, 1 Bad PIN (3 -> 2), then
signed, retries back to 3
YubiKey 5 NFC (firmware 5.1.2, policy confirmed via attestation):
9C policy Operations Prompts Result
NEVER sign, sign 0, 0 both signed
ONCE sign, sign 1, 0 both signed
ALWAYS sign, sign 1, 1 both signed
ALWAYS cancel 1 canceled, retries kept (3)
ALWAYS wrong, good PIN 1, 1 Bad PIN (3 -> 2), then
signed, retries back to 3
Nitrokey 3 (accepts but ignores the policy at key generation and
enforces "PIN Always" for 9C):
9C policy Operations Prompts Result
NEVER sign, sign 1, 1 both signed
ONCE sign, sign 1, 1 both signed
ALWAYS sign, sign 1, 1 both signed
ALWAYS cancel 1 canceled, retries kept (3)
ALWAYS wrong, good PIN 1, 1 Bad PIN (3 -> 2), then
signed, retries back to 3
Signed-off-by: David Seifert <soap at gentoo.org>
---
scd/app-piv.c | 109 ++++++++------------------------------------------
1 file changed, 16 insertions(+), 93 deletions(-)
diff --git a/scd/app-piv.c b/scd/app-piv.c
index 9652ff68e..d9ff8be06 100644
--- a/scd/app-piv.c
+++ b/scd/app-piv.c
@@ -266,14 +266,9 @@ do_deinit (app_t app)
}
-#define YKPIV_METADATA_POLICY_TAG 0x02
-
/* Wrapper around iso7816_get_data which first tries to get the data
* from the cache. With GET_IMMEDIATE passed as true, the cache is
* bypassed. The tag-53 container is also removed.
- *
- * A special tag F7xx is also supported to get PIN Policy and Touch
- * Policy.
*/
static gpg_error_t
get_cached_data (app_t app, int tag,
@@ -311,43 +306,7 @@ get_cached_data (app_t app, int tag,
}
}
- if (app->app_local->flags.yubikey && (tag >> 8) == 0xF7)
- /* Get policies from metadata for a key slot. */
- {
- unsigned char apdu[4];
-
- /* We use a proprietary Yubikey command to get metadata for KEYREF. */
- apdu[0] = 0;
- apdu[1] = 0xf7; /* Yubikey: Get metadata. */
- apdu[2] = 0;
- apdu[3] = (tag & 0xff);
- err = iso7816_apdu_direct (app_get_slot (app), apdu, 4, 1,
- NULL, &p, &len);
- if (!err)
- {
- unsigned char *policies = NULL;
-
- s = find_tlv (p, len, YKPIV_METADATA_POLICY_TAG, &len);
- if (s && len == 2)
- {
- policies = xtrymalloc (len);
- if (!policies)
- err = gpg_error_from_syserror ();
- else
- memcpy (policies, s, len);
- }
- xfree (p);
- if (policies)
- p = policies;
- else
- {
- p = NULL;
- len = 0;
- }
- }
- }
- else
- err = iso7816_get_data_odd (app_get_slot (app), 0, tag, &p, &len);
+ err = iso7816_get_data_odd (app_get_slot (app), 0, tag, &p, &len);
if (err)
return err;
@@ -356,8 +315,6 @@ get_cached_data (app_t app, int tag,
* (SP 800-73-5 Part 2, section 3.1.2) */
if (tag == 0x7E || tag == 0x7F61)
;
- else if (app->app_local->flags.yubikey && (tag >> 8) == 0xF7)
- ;
else if (len && *p == 0x53 && (s = find_tlv (p, len, 0x53, &n)))
{
memmove (p, s, n);
@@ -2191,48 +2148,6 @@ do_check_chv (app_t app, ctrl_t ctrl, const char *pwidstr)
}
-/* Examining the per slot PIN policy for KEYREF, return 1 if the
- * verification of PIN is always required. Return 0, otherwise. */
-static int
-check_pin_policy (app_t app, int keyref)
-{
- unsigned char *buffer;
- size_t buflen;
- gpg_error_t err;
- int value;
- int force;
-
- if (!app->app_local->flags.yubikey)
- {
- no_f7xx:
- switch (keyref)
- {
- case 0x9c: force = 1; break;
- default: force = 0; break;
- }
- return force;
- }
-
- /* Use special tag F7xx to get PIN Policy and Touch Policy. */
- err = get_cached_data (app, ((0xf7 << 8) | keyref), &buffer, &buflen, 0);
- if (err)
- goto no_f7xx;
-
- if (buflen == 2)
- value = buffer[0]; /* 0: Default, 1: Never, 2: Once, 3: Always */
- else
- goto no_f7xx;
-
- switch (keyref)
- {
- case 0x9c: force = (value == 0 || value == 3); break;
- default: force = (value == 3); break;
- }
-
- xfree (buffer);
- return force;
-}
-
/* Compute a digital signature using the GENERAL AUTHENTICATE command
* on INDATA which is expected to be the raw message digest. The
* KEYIDSTR has the key reference or its OID (e.g. "PIV.9A"). The
@@ -2261,7 +2176,6 @@ do_sign (app_t app, ctrl_t ctrl, const char *keyidstr, int hashalgo,
unsigned char *indata_buffer = NULL; /* Malloced helper. */
unsigned char *apdudata = NULL;
size_t apdudatalen;
- int force_verify;
(void)ctrl;
@@ -2284,7 +2198,9 @@ do_sign (app_t app, ctrl_t ctrl, const char *keyidstr, int hashalgo,
* for each key. It's user's control.
* https://developers.yubico.com/PIV/Introduction/Yubico_extensions.html
*/
- force_verify = check_pin_policy (app, keyref);
+ /* Don't hardcode per-key PIN rules: let the card enforce its own
+ * policy (SP 800-73 "PIN Always", Yubico per-key policies, ...).
+ * We try the operation first and only VERIFY if the card refuses. */
err = get_key_algorithm_by_dobj (app, dobj, &mechanism);
if (err)
@@ -2446,11 +2362,6 @@ do_sign (app_t app, ctrl_t ctrl, const char *keyidstr, int hashalgo,
goto leave;
}
- /* Now verify the Application PIN. */
- err = verify_chv (app, ctrl, 0x80, force_verify);
- if (err)
- goto leave;
-
/* Build the Dynamic Authentication Template. */
err = concat_tlv_list (0, &apdudata, &apdudatalen,
(int)0x7c, (size_t)0, NULL, /* Constructed. */
@@ -2465,6 +2376,18 @@ do_sign (app_t app, ctrl_t ctrl, const char *keyidstr, int hashalgo,
mechanism, keyref,
apdudata, (int)apdudatalen, 0,
&outdata, &outdatalen);
+ /* GPG_ERR_BAD_PIN (SW 6982 or 63Cx): the card wants a VERIFY first.
+ * Force it even if the PIN is already verified, because a "PIN
+ * Always" key needs a fresh one. Retry only once. */
+ if (gpg_err_code (err) == GPG_ERR_BAD_PIN)
+ {
+ err = verify_chv (app, ctrl, 0x80, 1);
+ if (!err)
+ err = iso7816_general_authenticate (app_get_slot (app), -1,
+ mechanism, keyref,
+ apdudata, (int)apdudatalen, 0,
+ &outdata, &outdatalen);
+ }
if (err)
goto leave;
--
2.56.0
More information about the Gnupg-devel
mailing list