def piv_change_pin(self, old_pin, new_pin): with self._open_device([SmartCardConnection]) as conn: session = PivSession(conn) try: session.change_pin(old_pin, new_pin) logger.debug('PIN change successful!') return success() except InvalidPinError as e: attempts = e.attempts_remaining if attempts: logger.debug("Failed to change PIN, %d tries left", attempts, exc_info=e) return failure('wrong_pin', {'tries_left': attempts}) else: logger.debug("PIN is blocked.", exc_info=e) return failure('pin_blocked') except ApduError as e: if e.sw == SW.INCORRECT_PARAMETERS: return failure('incorrect_parameters') tries_left = e.attempts_remaining logger.debug('PIN change failed. %s tries left.', tries_left, exc_info=e) return { 'success': False, 'tries_left': tries_left, }
def refresh_piv(self): with self._open_device([SmartCardConnection]) as conn: session = PivSession(conn) pivman = get_pivman_data(session) try: key_type = session.get_management_key_metadata().key_type except NotSupportedError: key_type = MANAGEMENT_KEY_TYPE.TDES return success({ 'piv_data': { 'certs': self._piv_list_certificates(session), 'has_derived_key': pivman.has_derived_key, 'has_protected_key': pivman.has_protected_key, 'has_stored_key': pivman.has_stored_key, 'pin_tries': session.get_pin_attempts(), 'puk_blocked': pivman.puk_blocked, 'supported_algorithms': _supported_algorithms( self._dev_info['version'].split('.')), 'key_type': key_type, }, })
def sign(self, message): if 'mplock' in globals(): mplock.acquire() try: conn = self._connect() with conn: session = PivSession(conn) if self.pin: try: session.verify_pin(self.pin) except InvalidPinError as err: controlflow.system_error_exit(7, f'YubiKey - {err}') try: signed = session.sign(slot=self.slot, key_type=self.key_type, message=message, hash_algorithm=hashes.SHA256(), padding=padding.PKCS1v15()) except ApduError as err: controlflow.system_error_exit(8, f'YubiKey - {err}') except ValueError as err: controlflow.system_error_exit(9, f'YubiKey - {err}') if 'mplock' in globals(): mplock.release() return signed
def piv_export_certificate(self, slot, file_url): file_path = self._get_file_path(file_url) with self._open_device([SmartCardConnection]) as conn: session = PivSession(conn) cert = session.get_certificate(SLOT[slot]) with open(file_path, 'wb') as file: file.write( cert.public_bytes(encoding=serialization.Encoding.PEM)) return success()
def piv_unblock_pin(self, puk, new_pin): with self._open_device([SmartCardConnection]) as conn: session = PivSession(conn) try: session.unblock_pin(puk, new_pin) return success() except InvalidPinError as e: attempts = e.attempts_remaining if attempts: logger.debug("Failed to unblock PIN, %d tries left", attempts, exc_info=e) return failure('wrong_puk', {'tries_left': attempts}) else: logger.debug("PUK is blocked.", exc_info=e) return failure('puk_blocked')
def piv(ctx): """ Manage PIV Application. Examples: \b Generate an ECC P-256 private key and a self-signed certificate in slot 9a: $ ykman piv generate-key --algorithm ECCP256 9a pubkey.pem $ ykman piv generate-certificate --subject "yubico" 9a pubkey.pem \b Change the PIN from 123456 to 654321: $ ykman piv change-pin --pin 123456 --new-pin 654321 \b Reset all PIV data and restore default settings: $ ykman piv reset """ try: app = PivSession(ctx.obj["conn"]) ctx.obj["controller"] = PivController(app) except ApduError as e: if e.sw == SW.FILE_NOT_FOUND: ctx.fail("The PIV application can't be found on this YubiKey.") raise
def pivman_change_pin(session: PivSession, old_pin: str, new_pin: str) -> None: """Change the PIN, while keeping PivmanData in sync.""" session.change_pin(old_pin, new_pin) pivman = get_pivman_data(session) if pivman.has_derived_key: session.authenticate( MANAGEMENT_KEY_TYPE.TDES, derive_management_key(old_pin, cast(bytes, pivman.salt)), ) session.verify_pin(new_pin) new_salt = os.urandom(16) new_key = derive_management_key(new_pin, new_salt) session.set_management_key(MANAGEMENT_KEY_TYPE.TDES, new_key) pivman.salt = new_salt session.put_object(OBJECT_ID_PIVMAN_DATA, pivman.get_bytes())
def sign_csr_builder( session: PivSession, slot: SLOT, public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey], builder: x509.CertificateSigningRequestBuilder, hash_algorithm: Type[hashes.HashAlgorithm] = hashes.SHA256, ) -> x509.CertificateSigningRequest: """Sign a CSR.""" key_type = KEY_TYPE.from_public_key(public_key) dummy_key = _dummy_key(key_type) csr = builder.sign(dummy_key, hash_algorithm(), default_backend()) seq = Tlv.parse_list(Tlv.unpack(0x30, csr.public_bytes(Encoding.DER))) # Replace public key pub_format = (PublicFormat.PKCS1 if key_type.algorithm == ALGORITHM.RSA else PublicFormat.SubjectPublicKeyInfo) dummy_bytes = dummy_key.public_key().public_bytes(Encoding.DER, pub_format) pub_bytes = public_key.public_bytes(Encoding.DER, pub_format) seq[0] = Tlv(seq[0].replace(dummy_bytes, pub_bytes)) sig = session.sign( slot, key_type, seq[0], hash_algorithm(), padding.PKCS1v15(), # Only used for RSA ) # Replace signature, add unused bits = 0 seq[2] = Tlv(seq[2].tag, b"\0" + sig) # Re-assemble sequence der = Tlv(0x30, b"".join(seq)) return x509.load_der_x509_csr(der, default_backend())
def piv_change_mgm_key(self, pin, current_key_hex, new_key_hex, key_type, store_on_device=False): with self._open_device([SmartCardConnection]) as conn: session = PivSession(conn) pivman = get_pivman_data(session) if pivman.has_protected_key or store_on_device: pin_failed = self._piv_verify_pin( session, pin=pin) if pin_failed: return pin_failed with PromptTimeout(): auth_failed = self._piv_ensure_authenticated( session, pin=pin, mgm_key_hex=current_key_hex) if auth_failed: return auth_failed try: new_key = a2b_hex(new_key_hex) if new_key_hex else None except Exception as e: logger.debug('Failed to parse new management key', exc_info=e) return failure('new_mgm_key_bad_hex') if new_key is not None and len(new_key) != MANAGEMENT_KEY_TYPE(key_type).key_len: logger.debug('Wrong length for new management key: %d', len(new_key)) return failure('new_mgm_key_bad_length') pivman_set_mgm_key( session, new_key, MANAGEMENT_KEY_TYPE(key_type), touch=False, store_on_device=store_on_device ) return success()
def sign_certificate_builder( session: PivSession, slot: SLOT, key_type: KEY_TYPE, builder: x509.CertificateBuilder, hash_algorithm: Type[hashes.HashAlgorithm] = hashes.SHA256, ) -> x509.Certificate: """Sign a Certificate.""" dummy_key = _dummy_key(key_type) cert = builder.sign(dummy_key, hash_algorithm(), default_backend()) sig = session.sign( slot, key_type, cert.tbs_certificate_bytes, hash_algorithm(), padding.PKCS1v15(), # Only used for RSA ) seq = Tlv.parse_list(Tlv.unpack(0x30, cert.public_bytes(Encoding.DER))) # Replace signature, add unused bits = 0 seq[2] = Tlv(seq[2].tag, b"\0" + sig) # Re-assemble sequence der = Tlv(0x30, b"".join(seq)) return x509.load_der_x509_certificate(der, default_backend())
def piv_info(conn): try: piv = PivSession(conn) return ["\tPIV"] + [ f"\t\t{ln}" for ln in get_piv_info(piv).splitlines() if ln ] except Exception as e: return [f"\tPIV not accessible {e}"]
def get_pivman_data(session: PivSession) -> PivmanData: """Reads out the Pivman data from a YubiKey.""" try: return PivmanData(session.get_object(OBJECT_ID_PIVMAN_DATA)) except ApduError as e: if e.sw == SW.FILE_NOT_FOUND: # No data there, initialise a new object. return PivmanData() raise
def get_pivman_protected_data(session: PivSession) -> PivmanProtectedData: """Reads out the Pivman protected data from a YubiKey. This function requires PIN verification prior to being called. """ try: return PivmanProtectedData(session.get_object(OBJECT_ID_PIVMAN_PROTECTED_DATA)) except ApduError as e: if e.sw == SW.FILE_NOT_FOUND: # No data there, initialise a new object. return PivmanProtectedData() raise
def get_certificate(self): try: conn = self._connect() with conn: session = PivSession(conn) if self.pin: try: session.verify_pin(self.pin) except InvalidPinError as err: controlflow.system_error_exit(7, f'YubiKey - {err}') try: cert = session.get_certificate(self.slot) except ApduError as err: controlflow.system_error_exit(9, f'YubiKey - {err}') cert_pem = cert.public_bytes(serialization.Encoding.PEM).decode() publicKeyData = b64encode(cert_pem.encode()) if isinstance(publicKeyData, bytes): publicKeyData = publicKeyData.decode() return publicKeyData except ValueError as err: controlflow.system_error_exit(9, f'YubiKey - {err}')
def piv_import_file(self, slot, file_url, password=None, pin=None, mgm_key=None): is_cert = False is_private_key = False file_path = self._get_file_path(file_url) if password: password = password.encode() with open(file_path, 'r+b') as file: data = file.read() try: certs = parse_certificates(data, password) is_cert = True except (ValueError, TypeError): pass try: private_key = parse_private_key(data, password) is_private_key = True except (ValueError, TypeError, InvalidPasswordError): pass if not (is_cert or is_private_key): return failure('failed_parsing') with self._open_device([SmartCardConnection]) as conn: session = PivSession(conn) with PromptTimeout(): auth_failed = self._piv_ensure_authenticated( session, pin, mgm_key) if auth_failed: return auth_failed if is_private_key: session.put_key(SLOT[slot], private_key) if is_cert: if len(certs) > 1: leafs = get_leaf_certificates(certs) cert_to_import = leafs[0] else: cert_to_import = certs[0] session.put_certificate(SLOT[slot], cert_to_import) session.put_object(OBJECT_ID.CHUID, generate_chuid()) return success({ 'imported_cert': is_cert, 'imported_key': is_private_key })
def pivman_set_mgm_key( session: PivSession, new_key: bytes, algorithm: MANAGEMENT_KEY_TYPE, touch: bool = False, store_on_device: bool = False, ) -> None: """Set a new management key, while keeping PivmanData in sync.""" pivman = get_pivman_data(session) if store_on_device or (not store_on_device and pivman.has_stored_key): # Ensure we have access to protected data before overwriting key try: pivman_prot = get_pivman_protected_data(session) except Exception as e: logger.debug("Failed to initialize protected pivman data", exc_info=e) if store_on_device: raise # Set the new management key session.set_management_key(algorithm, new_key, touch) if pivman.has_derived_key: # Clear salt for old derived keys. pivman.salt = None # Set flag for stored or not stored key. pivman.mgm_key_protected = store_on_device # Update readable pivman data session.put_object(OBJECT_ID_PIVMAN_DATA, pivman.get_bytes()) if store_on_device: # Store key in protected pivman data pivman_prot.key = new_key session.put_object(OBJECT_ID_PIVMAN_PROTECTED_DATA, pivman_prot.get_bytes()) elif not store_on_device and pivman.has_stored_key: # If new key should not be stored and there is an old stored key, # try to clear it. try: pivman_prot.key = None session.put_object( OBJECT_ID_PIVMAN_PROTECTED_DATA, pivman_prot.get_bytes(), ) except ApduError as e: logger.debug("No PIN provided, can't clear key...", exc_info=e)
def list_certificates(session: PivSession) -> Mapping[SLOT, Optional[x509.Certificate]]: """Reads out and parses stored certificates. Only certificates which are successfully parsed are returned. """ certs = OrderedDict() for slot in set(SLOT) - {SLOT.ATTESTATION}: try: certs[slot] = session.get_certificate(slot) except ApduError: pass except BadResponseError: certs[slot] = None # type: ignore return certs
def check_key( session: PivSession, slot: SLOT, public_key: Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey], ) -> bool: """Check that a given public key corresponds to the private key in a slot. This will create a signature using the private key, so the PIN must be verified prior to calling this function if the PIN policy requires it. """ try: test_data = b"test" test_sig = session.sign( slot, KEY_TYPE.from_public_key(public_key), test_data, hashes.SHA256(), padding.PKCS1v15(), # Only used for RSA ) if isinstance(public_key, rsa.RSAPublicKey): public_key.verify( test_sig, test_data, padding.PKCS1v15(), hashes.SHA256(), ) elif isinstance(public_key, ec.EllipticCurvePublicKey): public_key.verify(test_sig, test_data, ec.ECDSA(hashes.SHA256())) else: raise ValueError("Unknown key type: " + type(public_key)) return True except ApduError as e: if e.sw in (SW.INCORRECT_PARAMETERS, SW.WRONG_PARAMETERS_P1P2): return False raise except InvalidSignature: return False
def piv(ctx): """ Manage the PIV application. Examples: \b Generate an ECC P-256 private key and a self-signed certificate in slot 9a: $ ykman piv keys generate --algorithm ECCP256 9a pubkey.pem $ ykman piv certificates generate --subject "yubico" 9a pubkey.pem \b Change the PIN from 123456 to 654321: $ ykman piv access change-pin --pin 123456 --new-pin 654321 \b Reset all PIV data and restore default settings: $ ykman piv reset """ session = PivSession(ctx.obj["conn"]) ctx.obj["session"] = session ctx.obj["pivman_data"] = get_pivman_data(session)
def piv_delete_certificate(self, slot_name, pin=None, mgm_key_hex=None): logger.debug('piv_delete_certificate %s', slot_name) with self._open_device([SmartCardConnection]) as conn: session = PivSession(conn) with PromptTimeout(): auth_failed = self._piv_ensure_authenticated( session, pin=pin, mgm_key_hex=mgm_key_hex) if auth_failed: return auth_failed try: session.delete_certificate(SLOT[slot_name]) session.put_object(OBJECT_ID.CHUID, generate_chuid()) return success() except ApduError as e: if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: logger.debug("Wrong management key", exc_info=e) return failure('wrong_mgm_key')
def reset_piv(self): '''Resets YubiKey PIV app and generates new key for GAM to use.''' reply = str( input( 'This will wipe all PIV keys and configuration from your YubiKey. Are you sure? (y/N) ' ).lower().strip()) if reply != 'y': sys.exit(1) try: conn = self._connect() with conn: piv = PivSession(conn) piv.reset() rnd = SystemRandom() pin_puk_chars = string.ascii_letters + string.digits + string.punctuation new_puk = ''.join(rnd.choice(pin_puk_chars) for _ in range(8)) new_pin = ''.join(rnd.choice(pin_puk_chars) for _ in range(8)) piv.change_puk('12345678', new_puk) piv.change_pin('123456', new_pin) print(f'PIN set to: {new_pin}') piv.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) piv.verify_pin(new_pin) print('YubiKey is generating a non-exportable private key...') pubkey = piv.generate_key(SLOT.AUTHENTICATION, KEY_TYPE.RSA2048, PIN_POLICY.ALWAYS, TOUCH_POLICY.NEVER) now = datetime.datetime.utcnow() valid_to = now + datetime.timedelta(days=36500) subject = 'CN=GAM Created Key' piv.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) piv.verify_pin(new_pin) cert = generate_self_signed_certificate( piv, SLOT.AUTHENTICATION, pubkey, subject, now, valid_to) piv.put_certificate(SLOT.AUTHENTICATION, cert) piv.put_object(OBJECT_ID.CHUID, generate_chuid()) except ValueError as err: controlflow.system_error_exit(8, f'YubiKey - {err}')
def get_piv_info(session: PivSession) -> str: """Get human readable information about the PIV configuration.""" pivman = get_pivman_data(session) lines = [] lines.append("PIV version: %d.%d.%d" % session.version) try: pin_data = session.get_pin_metadata() if pin_data.default_value: lines.append("WARNING: Using default PIN!") tries_str = "%d/%d" % (pin_data.attempts_remaining, pin_data.total_attempts) except NotSupportedError: # Largest possible number of PIN tries to get back is 15 tries = session.get_pin_attempts() tries_str = "15 or more." if tries == 15 else str(tries) lines.append(f"PIN tries remaining: {tries_str}") if pivman.puk_blocked: lines.append("PUK blocked.") try: metadata = session.get_management_key_metadata() if metadata.default_value: lines.append("WARNING: Using default Management key!") key_type = metadata.key_type except NotSupportedError: key_type = MANAGEMENT_KEY_TYPE.TDES lines.append(f"Management key algorithm: {key_type.name}") if pivman.has_derived_key: lines.append("Management key is derived from PIN.") if pivman.has_stored_key: lines.append( "Management key is stored on the YubiKey, protected by PIN.") try: chuid = session.get_object(OBJECT_ID.CHUID).hex() except ApduError as e: if e.sw == SW.FILE_NOT_FOUND: chuid = "No data available." lines.append("CHUID:\t" + chuid) try: ccc = session.get_object(OBJECT_ID.CAPABILITY).hex() except ApduError as e: if e.sw == SW.FILE_NOT_FOUND: ccc = "No data available." lines.append("CCC: \t" + ccc) for (slot, cert) in list_certificates(session).items(): lines.append(f"Slot {slot:02x}:") if isinstance(cert, x509.Certificate): try: # Try to read out full DN, fallback to only CN. # Support for DN was added in crytography 2.5 subject_dn = cert.subject.rfc4514_string() issuer_dn = cert.issuer.rfc4514_string() print_dn = True except AttributeError: print_dn = False logger.debug("Failed to read DN, falling back to only CNs") cn = cert.subject.get_attributes_for_oid( x509.NameOID.COMMON_NAME) subject_cn = cn[0].value if cn else "None" cn = cert.issuer.get_attributes_for_oid( x509.NameOID.COMMON_NAME) issuer_cn = cn[0].value if cn else "None" except ValueError as e: # Malformed certificates may throw ValueError logger.debug("Failed parsing certificate", exc_info=e) lines.append(f"\tMalformed certificate: {e}") continue fingerprint = cert.fingerprint(hashes.SHA256()).hex() try: key_algo = KEY_TYPE.from_public_key(cert.public_key()).name except ValueError: key_algo = "Unsupported" serial = cert.serial_number try: not_before: Optional[datetime] = cert.not_valid_before except ValueError as e: logger.debug("Failed reading not_valid_before", exc_info=e) not_before = None try: not_after: Optional[datetime] = cert.not_valid_after except ValueError as e: logger.debug("Failed reading not_valid_after", exc_info=e) not_after = None # Print out everything lines.append(f"\tAlgorithm:\t{key_algo}") if print_dn: lines.append(f"\tSubject DN:\t{subject_dn}") lines.append(f"\tIssuer DN:\t{issuer_dn}") else: lines.append(f"\tSubject CN:\t{subject_cn}") lines.append(f"\tIssuer CN:\t{issuer_cn}") lines.append(f"\tSerial:\t\t{serial}") lines.append(f"\tFingerprint:\t{fingerprint}") if not_before: lines.append(f"\tNot before:\t{not_before}") if not_after: lines.append(f"\tNot after:\t{not_after}") else: lines.append("\tError: Failed to parse certificate.") return "\n".join(lines)
def setUpClass(cls): with open_device()[0] as conn: controller = PivController(PivSession(conn)) controller.reset()
def reconnect(self): self.conn.close() self.conn = open_device()[0] self.controller = PivController(PivSession(self.conn))
def session(ccid_connection): piv = PivSession(ccid_connection) piv.reset() yield piv reset_state(piv)
def piv_generate_certificate( self, slot_name, algorithm, subject, expiration_date, self_sign=True, csr_file_url=None, pin=None, mgm_key_hex=None): logger.debug('slot_name=%s algorithm=%s common_name=%s ' 'expiration_date=%s self_sign=%s csr_file_url=%s', slot_name, algorithm, subject, expiration_date, self_sign, csr_file_url) if csr_file_url: file_path = self._get_file_path(csr_file_url) with self._open_device([SmartCardConnection]) as conn: session = PivSession(conn) with PromptTimeout(): auth_failed = self._piv_ensure_authenticated( session, pin=pin, mgm_key_hex=mgm_key_hex) if auth_failed: return auth_failed pin_failed = self._piv_verify_pin(session, pin) if pin_failed: return pin_failed if self_sign: now = datetime.datetime.utcnow() try: year = int(expiration_date[0:4]) month = int(expiration_date[(4+1):(4+1+2)]) day = int(expiration_date[(4+1+2+1):(4+1+2+1+2)]) valid_to = datetime.datetime(year, month, day) except ValueError as e: logger.debug( 'Failed to parse date: ' + expiration_date, exc_info=e) return failure( 'invalid_iso8601_date', {'date': expiration_date}) try: public_key = session.generate_key( SLOT[slot_name], KEY_TYPE[algorithm]) except ApduError as e: if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: logger.debug("Wrong management key", exc_info=e) return failure('wrong_mgm_key') pin_failed = self._piv_verify_pin(session, pin) if pin_failed: return pin_failed if "=" not in subject: # Old style, common name only. subject = "CN=" + subject try: if self_sign: cert = generate_self_signed_certificate(session, SLOT[slot_name], public_key, subject, now, valid_to) session.put_certificate(SLOT[slot_name], cert) session.put_object(OBJECT_ID.CHUID, generate_chuid()) else: csr = generate_csr(session, SLOT[slot_name], public_key, subject) with open(file_path, 'w+b') as csr_file: csr_file.write(csr.public_bytes( encoding=serialization.Encoding.PEM)) except ApduError as e: if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: return failure('pin_required') raise return success()
def setUp(self): self.conn = open_device()[0] self.controller = PivController(PivSession(self.conn))
def piv_reset(self): with self._open_device([SmartCardConnection]) as conn: session = PivSession(conn) session.reset() return success()