device/fido: add PIN support to |VirtualCtap2Device|

This will be needed for testing PIN support.

Change-Id: I274226528fe5fd3616d4e81233516e08f66406d9
Reviewed-on: https://chromium-review.googlesource.com/c/1484080
Commit-Queue: Adam Langley <agl@chromium.org>
Auto-Submit: Adam Langley <agl@chromium.org>
Reviewed-by: Martin Kreichgauer <martinkr@google.com>
Cr-Commit-Position: refs/heads/master@{#636113}
diff --git a/content/browser/webauth/scoped_virtual_authenticator_environment.cc b/content/browser/webauth/scoped_virtual_authenticator_environment.cc
index abd4326..a3e43ca 100644
--- a/content/browser/webauth/scoped_virtual_authenticator_environment.cc
+++ b/content/browser/webauth/scoped_virtual_authenticator_environment.cc
@@ -108,8 +108,8 @@
     // If no bindings are active then create a virtual device. This is useful
     // for web-platform tests which assume that they can make webauthn calls,
     // but which don't implement the Chromium-specific mock Mojo interfaces.
-    auto device =
-        std::make_unique<device::VirtualCtap2Device>(virtual_device_state_);
+    auto device = std::make_unique<device::VirtualCtap2Device>(
+        virtual_device_state_, false /* no PIN support */);
     discovery->AddVirtualDevice(std::move(device));
   } else {
     for (auto& authenticator : authenticators_) {
diff --git a/device/fido/features.cc b/device/fido/features.cc
index ea36b7b..0b623bd 100644
--- a/device/fido/features.cc
+++ b/device/fido/features.cc
@@ -19,4 +19,7 @@
 extern const base::Feature kWebAuthProxyCryptotoken{
     "WebAuthenticationProxyCryptotoken", base::FEATURE_ENABLED_BY_DEFAULT};
 
+extern const base::Feature kWebAuthPINSupport{
+    "WebAuthenticationPINSupport", base::FEATURE_DISABLED_BY_DEFAULT};
+
 }  // namespace device
diff --git a/device/fido/features.h b/device/fido/features.h
index 1ed614c7..00129f1 100644
--- a/device/fido/features.h
+++ b/device/fido/features.h
@@ -20,6 +20,10 @@
 COMPONENT_EXPORT(DEVICE_FIDO)
 extern const base::Feature kWebAuthProxyCryptotoken;
 
+// Enable support for PIN-based user-verification.
+COMPONENT_EXPORT(DEVICE_FIDO)
+extern const base::Feature kWebAuthPINSupport;
+
 }  // namespace device
 
 #endif  // DEVICE_FIDO_FEATURES_H_
diff --git a/device/fido/scoped_virtual_fido_device.cc b/device/fido/scoped_virtual_fido_device.cc
index 9aa0c74..3bba1ce2 100644
--- a/device/fido/scoped_virtual_fido_device.cc
+++ b/device/fido/scoped_virtual_fido_device.cc
@@ -26,19 +26,22 @@
   explicit VirtualFidoDeviceDiscovery(
       FidoTransportProtocol transport,
       scoped_refptr<VirtualFidoDevice::State> state,
-      ProtocolVersion supported_protocol)
+      ProtocolVersion supported_protocol,
+      bool enable_pin)
       : FidoDeviceDiscovery(transport),
         state_(std::move(state)),
-        supported_protocol_(supported_protocol) {}
+        supported_protocol_(supported_protocol),
+        enable_pin_(enable_pin) {}
   ~VirtualFidoDeviceDiscovery() override = default;
 
  protected:
   void StartInternal() override {
     std::unique_ptr<FidoDevice> device;
-    if (supported_protocol_ == ProtocolVersion::kCtap)
-      device = std::make_unique<VirtualCtap2Device>(state_);
-    else
+    if (supported_protocol_ == ProtocolVersion::kCtap) {
+      device = std::make_unique<VirtualCtap2Device>(state_, enable_pin_);
+    } else {
       device = std::make_unique<VirtualU2fDevice>(state_);
+    }
 
     AddDevice(std::move(device));
     base::SequencedTaskRunnerHandle::Get()->PostTask(
@@ -49,7 +52,8 @@
 
  private:
   scoped_refptr<VirtualFidoDevice::State> state_;
-  ProtocolVersion supported_protocol_;
+  const ProtocolVersion supported_protocol_;
+  const bool enable_pin_;
   DISALLOW_COPY_AND_ASSIGN(VirtualFidoDeviceDiscovery);
 };
 
@@ -66,6 +70,11 @@
   transport_ = transport;
 }
 
+void ScopedVirtualFidoDevice::EnablePINSupport() {
+  supported_protocol_ = ProtocolVersion::kCtap;
+  enable_pin_ = true;
+}
+
 VirtualFidoDevice::State* ScopedVirtualFidoDevice::mutable_state() {
   return state_.get();
 }
@@ -76,8 +85,8 @@
   if (transport != transport_) {
     return nullptr;
   }
-  return std::make_unique<VirtualFidoDeviceDiscovery>(transport_, state_,
-                                                      supported_protocol_);
+  return std::make_unique<VirtualFidoDeviceDiscovery>(
+      transport_, state_, supported_protocol_, enable_pin_);
 }
 
 }  // namespace test
diff --git a/device/fido/scoped_virtual_fido_device.h b/device/fido/scoped_virtual_fido_device.h
index 24765bec..ab4426f 100644
--- a/device/fido/scoped_virtual_fido_device.h
+++ b/device/fido/scoped_virtual_fido_device.h
@@ -34,6 +34,9 @@
   void SetTransport(FidoTransportProtocol transport);
 
   void SetSupportedProtocol(ProtocolVersion supported_protocol);
+  // EnablePINSupport causes the virtual devices to support PINs and sets the
+  // protocol version to CTAP2.
+  void EnablePINSupport();
   VirtualFidoDevice::State* mutable_state();
 
  protected:
@@ -45,6 +48,7 @@
   ProtocolVersion supported_protocol_ = ProtocolVersion::kU2f;
   FidoTransportProtocol transport_ =
       FidoTransportProtocol::kUsbHumanInterfaceDevice;
+  bool enable_pin_ = false;
   scoped_refptr<VirtualFidoDevice::State> state_;
   DISALLOW_COPY_AND_ASSIGN(ScopedVirtualFidoDevice);
 };
diff --git a/device/fido/virtual_ctap2_device.cc b/device/fido/virtual_ctap2_device.cc
index ba91245..b42ecd5 100644
--- a/device/fido/virtual_ctap2_device.cc
+++ b/device/fido/virtual_ctap2_device.cc
@@ -23,6 +23,17 @@
 #include "device/fido/fido_constants.h"
 #include "device/fido/fido_parsing_utils.h"
 #include "device/fido/opaque_attestation_statement.h"
+#include "device/fido/pin.h"
+#include "device/fido/pin_internal.h"
+#include "third_party/boringssl/src/include/openssl/aes.h"
+#include "third_party/boringssl/src/include/openssl/digest.h"
+#include "third_party/boringssl/src/include/openssl/ec.h"
+#include "third_party/boringssl/src/include/openssl/ec_key.h"
+#include "third_party/boringssl/src/include/openssl/hmac.h"
+#include "third_party/boringssl/src/include/openssl/mem.h"
+#include "third_party/boringssl/src/include/openssl/obj.h"
+#include "third_party/boringssl/src/include/openssl/rand.h"
+#include "third_party/boringssl/src/include/openssl/sha.h"
 
 namespace device {
 
@@ -50,6 +61,22 @@
                                        data.value_or(std::vector<uint8_t>{}))));
 }
 
+// CheckPINToken returns true iff |pin_auth| is a valid authentication of
+// |client_data_hash| given that the PIN token in effect is |pin_token|.
+bool CheckPINToken(base::span<const uint8_t> pin_token,
+                   base::span<const uint8_t> pin_auth,
+                   base::span<const uint8_t> client_data_hash) {
+  uint8_t calculated_pin_auth[SHA256_DIGEST_LENGTH];
+  unsigned hmac_bytes;
+  CHECK(HMAC(EVP_sha256(), pin_token.data(), pin_token.size(),
+             client_data_hash.data(), client_data_hash.size(),
+             calculated_pin_auth, &hmac_bytes));
+  DCHECK_EQ(sizeof(calculated_pin_auth), static_cast<size_t>(hmac_bytes));
+
+  return pin_auth.size() == 16 &&
+         CRYPTO_memcmp(pin_auth.data(), calculated_pin_auth, 16) == 0;
+}
+
 // CheckUserVerification implements the first, common steps of
 // makeCredential and getAssertion from the CTAP2 spec.
 CtapDeviceResponseCode CheckUserVerification(
@@ -57,6 +84,8 @@
     const AuthenticatorSupportedOptions& options,
     const base::Optional<std::vector<uint8_t>>& pin_auth,
     const base::Optional<uint8_t>& pin_protocol,
+    base::span<const uint8_t> pin_token,
+    base::span<const uint8_t> client_data_hash,
     UserVerificationRequirement user_verification,
     base::RepeatingCallback<void(void)> simulate_press_callback,
     bool* out_user_verified) {
@@ -88,7 +117,7 @@
   // and the pinProtocol is not supported, return CTAP2_ERR_PIN_AUTH_INVALID
   // error."
   if (supports_pin && pin_auth && (!pin_protocol || *pin_protocol != 1)) {
-    return CtapDeviceResponseCode::kCtap2ErrPinInvalid;
+    return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid;
   }
 
   // 3. "If authenticator is not protected by some form of user verification and
@@ -110,7 +139,9 @@
   // Step 4.
   bool uv = false;
   if (can_do_uv) {
-    if (user_verification == UserVerificationRequirement::kRequired) {
+    if (options.user_verification_availability ==
+        AuthenticatorSupportedOptions::UserVerificationAvailability::
+            kSupportedAndConfigured) {
       // Internal UV is assumed to always succeed.
       if (simulate_press_callback) {
         simulate_press_callback.Run();
@@ -118,10 +149,15 @@
       uv = true;
     }
 
-    if (pin_auth) {
+    if (pin_auth && options.client_pin_availability ==
+                        AuthenticatorSupportedOptions::ClientPinAvailability::
+                            kSupportedAndPinSet) {
       DCHECK(pin_protocol && *pin_protocol == 1);
-      // The pin_auth argument is assumed to be correct.
-      uv = true;
+      if (CheckPINToken(pin_token, *pin_auth, client_data_hash)) {
+        uv = true;
+      } else {
+        return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid;
+      }
     }
 
     if (is_make_credential && !uv) {
@@ -256,19 +292,149 @@
                      });
 }
 
+base::Optional<std::vector<uint8_t>> GetPINBytestring(
+    const cbor::Value::MapValue& request,
+    pin::RequestKey key) {
+  const auto it = request.find(cbor::Value(static_cast<int>(key)));
+  if (it == request.end() || !it->second.is_bytestring()) {
+    return base::nullopt;
+  }
+  return it->second.GetBytestring();
+}
+
+base::Optional<bssl::UniquePtr<EC_POINT>> GetPINKey(
+    const cbor::Value::MapValue& request,
+    pin::RequestKey map_key) {
+  const auto it = request.find(cbor::Value(static_cast<int>(map_key)));
+  if (it == request.end() || !it->second.is_map()) {
+    return base::nullopt;
+  }
+  const auto& cose_key = it->second.GetMap();
+  auto response = pin::KeyAgreementResponse::ParseFromCOSE(cose_key);
+  if (!response) {
+    return base::nullopt;
+  }
+
+  bssl::UniquePtr<EC_GROUP> group(
+      EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
+  return pin::PointFromKeyAgreementResponse(group.get(), *response).value();
+}
+
+// ConfirmPresentedPIN checks whether |encrypted_pin_hash| is a valid proof-of-
+// possession of the PIN, given that |shared_key| is the result of the ECDH key
+// agreement.
+CtapDeviceResponseCode ConfirmPresentedPIN(
+    VirtualCtap2Device::State* state,
+    const uint8_t shared_key[SHA256_DIGEST_LENGTH],
+    const std::vector<uint8_t>& encrypted_pin_hash) {
+  if (state->retries == 0) {
+    return CtapDeviceResponseCode::kCtap2ErrPinBlocked;
+  }
+  if (state->soft_locked) {
+    return CtapDeviceResponseCode::kCtap2ErrPinAuthBlocked;
+  }
+
+  state->retries--;
+  state->retries_since_insertion++;
+
+  DCHECK((encrypted_pin_hash.size() % AES_BLOCK_SIZE) == 0);
+  uint8_t pin_hash[AES_BLOCK_SIZE];
+  pin::Decrypt(shared_key, encrypted_pin_hash, pin_hash);
+
+  uint8_t calculated_pin_hash[SHA256_DIGEST_LENGTH];
+  SHA256(reinterpret_cast<const uint8_t*>(state->pin.data()), state->pin.size(),
+         calculated_pin_hash);
+
+  if (state->pin.empty() ||
+      CRYPTO_memcmp(pin_hash, calculated_pin_hash, sizeof(pin_hash)) != 0) {
+    if (state->retries == 0) {
+      return CtapDeviceResponseCode::kCtap2ErrPinBlocked;
+    }
+    if (state->retries_since_insertion == 3) {
+      state->soft_locked = true;
+      return CtapDeviceResponseCode::kCtap2ErrPinAuthBlocked;
+    }
+    return CtapDeviceResponseCode::kCtap2ErrPinInvalid;
+  }
+
+  state->retries = 8;
+  state->retries_since_insertion = 0;
+
+  return CtapDeviceResponseCode::kSuccess;
+}
+
+// SetPIN sets the current PIN based on the ciphertext in |encrypted_pin|, given
+// that |shared_key| is the result of the ECDH key agreement.
+CtapDeviceResponseCode SetPIN(VirtualCtap2Device::State* state,
+                              const uint8_t shared_key[SHA256_DIGEST_LENGTH],
+                              const std::vector<uint8_t>& encrypted_pin,
+                              const std::vector<uint8_t>& pin_auth) {
+  // See
+  // https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#settingNewPin
+  uint8_t calculated_pin_auth[SHA256_DIGEST_LENGTH];
+  unsigned hmac_bytes;
+  CHECK(HMAC(EVP_sha256(), shared_key, SHA256_DIGEST_LENGTH,
+             encrypted_pin.data(), encrypted_pin.size(), calculated_pin_auth,
+             &hmac_bytes));
+  DCHECK_EQ(sizeof(calculated_pin_auth), static_cast<size_t>(hmac_bytes));
+
+  if (pin_auth.size() != sizeof(calculated_pin_auth) ||
+      CRYPTO_memcmp(calculated_pin_auth, pin_auth.data(),
+                    sizeof(calculated_pin_auth)) != 0) {
+    return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid;
+  }
+
+  if (encrypted_pin.size() < 64) {
+    return CtapDeviceResponseCode::kCtap2ErrPinPolicyViolation;
+  }
+
+  std::vector<uint8_t> plaintext_pin(encrypted_pin.size());
+  pin::Decrypt(shared_key, encrypted_pin, plaintext_pin.data());
+
+  size_t padding_len = 0;
+  while (padding_len < plaintext_pin.size() &&
+         plaintext_pin[plaintext_pin.size() - padding_len - 1] == 0) {
+    padding_len++;
+  }
+
+  plaintext_pin.resize(plaintext_pin.size() - padding_len);
+  if (padding_len == 0 || plaintext_pin.size() < 4 ||
+      plaintext_pin.size() > 63) {
+    return CtapDeviceResponseCode::kCtap2ErrPinPolicyViolation;
+  }
+
+  state->pin = std::string(reinterpret_cast<const char*>(plaintext_pin.data()),
+                           plaintext_pin.size());
+  state->retries = 8;
+
+  return CtapDeviceResponseCode::kSuccess;
+}
+
 }  // namespace
 
 VirtualCtap2Device::VirtualCtap2Device()
-    : VirtualFidoDevice(),
-      device_info_(AuthenticatorGetInfoResponse({ProtocolVersion::kCtap},
-                                                kDeviceAaguid)),
-      weak_factory_(this) {}
+    : VirtualFidoDevice(), weak_factory_(this) {
+  device_info_ =
+      AuthenticatorGetInfoResponse({ProtocolVersion::kCtap}, kDeviceAaguid);
+}
 
-VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state)
-    : VirtualFidoDevice(std::move(state)),
-      device_info_(AuthenticatorGetInfoResponse({ProtocolVersion::kCtap},
-                                                kDeviceAaguid)),
-      weak_factory_(this) {}
+VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state,
+                                       bool enable_pin)
+    : VirtualFidoDevice(std::move(state)), weak_factory_(this) {
+  device_info_ =
+      AuthenticatorGetInfoResponse({ProtocolVersion::kCtap}, kDeviceAaguid);
+  if (enable_pin) {
+    AuthenticatorSupportedOptions options;
+    if (mutable_state()->pin.empty()) {
+      options.client_pin_availability = AuthenticatorSupportedOptions::
+          ClientPinAvailability::kSupportedButPinNotSet;
+    } else {
+      options.client_pin_availability = AuthenticatorSupportedOptions::
+          ClientPinAvailability::kSupportedAndPinSet;
+    }
+    device_info_->SetOptions(options);
+  }
+}
 
 VirtualCtap2Device::~VirtualCtap2Device() = default;
 
@@ -304,6 +470,9 @@
     case CtapRequestCommand::kAuthenticatorGetAssertion:
       response_code = OnGetAssertion(request_bytes, &response_data);
       break;
+    case CtapRequestCommand::kAuthenticatorClientPin:
+      response_code = OnPINCommand(request_bytes, &response_data);
+      break;
     default:
       break;
   }
@@ -319,7 +488,7 @@
 
 void VirtualCtap2Device::SetAuthenticatorSupportedOptions(
     const AuthenticatorSupportedOptions& options) {
-  device_info_.SetOptions(options);
+  device_info_->SetOptions(options);
 }
 
 CtapDeviceResponseCode VirtualCtap2Device::OnMakeCredential(
@@ -333,13 +502,14 @@
   CtapMakeCredentialRequest request = std::get<0>(*request_and_hash);
   CtapMakeCredentialRequest::ClientDataHash client_data_hash =
       std::get<1>(*request_and_hash);
-  const AuthenticatorSupportedOptions& options = device_info_.options();
+  const AuthenticatorSupportedOptions& options = device_info_->options();
 
   bool user_verified;
   const CtapDeviceResponseCode uv_error = CheckUserVerification(
       true /* is makeCredential */, options, request.pin_auth(),
-      request.pin_protocol(), request.user_verification(),
-      mutable_state()->simulate_press_callback, &user_verified);
+      request.pin_protocol(), mutable_state()->pin_token, client_data_hash,
+      request.user_verification(), mutable_state()->simulate_press_callback,
+      &user_verified);
   if (uv_error != CtapDeviceResponseCode::kSuccess) {
     return uv_error;
   }
@@ -451,13 +621,14 @@
   CtapGetAssertionRequest request = std::get<0>(*request_and_hash);
   CtapGetAssertionRequest::ClientDataHash client_data_hash =
       std::get<1>(*request_and_hash);
-  const AuthenticatorSupportedOptions& options = device_info_.options();
+  const AuthenticatorSupportedOptions& options = device_info_->options();
 
   bool user_verified;
   const CtapDeviceResponseCode uv_error = CheckUserVerification(
       false /* not makeCredential */, options, request.pin_auth(),
-      request.pin_protocol(), request.user_verification(),
-      mutable_state()->simulate_press_callback, &user_verified);
+      request.pin_protocol(), mutable_state()->pin_token, client_data_hash,
+      request.user_verification(), mutable_state()->simulate_press_callback,
+      &user_verified);
   if (uv_error != CtapDeviceResponseCode::kSuccess) {
     return uv_error;
   }
@@ -513,9 +684,180 @@
   return CtapDeviceResponseCode::kSuccess;
 }
 
+CtapDeviceResponseCode VirtualCtap2Device::OnPINCommand(
+    base::span<const uint8_t> request_bytes,
+    std::vector<uint8_t>* response) {
+  if (device_info_->options().client_pin_availability ==
+      AuthenticatorSupportedOptions::ClientPinAvailability::kNotSupported) {
+    return CtapDeviceResponseCode::kCtap1ErrInvalidCommand;
+  }
+
+  const auto& cbor_request = cbor::Reader::Read(request_bytes);
+  if (!cbor_request || !cbor_request->is_map()) {
+    return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType;
+  }
+  const auto& request_map = cbor_request->GetMap();
+
+  const auto protocol_it = request_map.find(
+      cbor::Value(static_cast<int>(pin::RequestKey::kProtocol)));
+  if (protocol_it == request_map.end() || !protocol_it->second.is_unsigned()) {
+    return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType;
+  }
+  if (protocol_it->second.GetUnsigned() != pin::kProtocolVersion) {
+    return CtapDeviceResponseCode::kCtap1ErrInvalidCommand;
+  }
+
+  const auto subcommand_it = request_map.find(
+      cbor::Value(static_cast<int>(pin::RequestKey::kSubcommand)));
+  if (subcommand_it == request_map.end() ||
+      !subcommand_it->second.is_unsigned()) {
+    return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType;
+  }
+  const int64_t subcommand = subcommand_it->second.GetUnsigned();
+
+  cbor::Value::MapValue response_map;
+  switch (subcommand) {
+    case static_cast<int>(device::pin::Subcommand::kGetRetries):
+      response_map.emplace(static_cast<int>(pin::ResponseKey::kRetries),
+                           mutable_state()->retries);
+      break;
+
+    case static_cast<int>(device::pin::Subcommand::kGetKeyAgreement): {
+      bssl::UniquePtr<EC_KEY> key(
+          EC_KEY_new_by_curve_name(NID_X9_62_prime256v1));
+      CHECK(EC_KEY_generate_key(key.get()));
+      response_map.emplace(static_cast<int>(pin::ResponseKey::kKeyAgreement),
+                           pin::EncodeCOSEPublicKey(key.get()));
+      mutable_state()->ecdh_key = std::move(key);
+      break;
+    }
+
+    case static_cast<int>(device::pin::Subcommand::kSetPIN): {
+      const auto encrypted_pin =
+          GetPINBytestring(request_map, pin::RequestKey::kNewPINEnc);
+      const auto pin_auth =
+          GetPINBytestring(request_map, pin::RequestKey::kPINAuth);
+      const auto peer_key =
+          GetPINKey(request_map, pin::RequestKey::kKeyAgreement);
+
+      if (!encrypted_pin || (encrypted_pin->size() % AES_BLOCK_SIZE) != 0 ||
+          !pin_auth || !peer_key) {
+        return CtapDeviceResponseCode::kCtap2ErrMissingParameter;
+      }
+
+      if (!mutable_state()->pin.empty()) {
+        return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid;
+      }
+
+      uint8_t shared_key[SHA256_DIGEST_LENGTH];
+      if (!mutable_state()->ecdh_key) {
+        // kGetKeyAgreement should have been called first.
+        NOTREACHED();
+        return CtapDeviceResponseCode::kCtap2ErrPinTokenExpired;
+      }
+      pin::CalculateSharedKey(mutable_state()->ecdh_key.get(), peer_key->get(),
+                              shared_key);
+
+      CtapDeviceResponseCode err =
+          SetPIN(mutable_state(), shared_key, *encrypted_pin, *pin_auth);
+      if (err != CtapDeviceResponseCode::kSuccess) {
+        return err;
+      };
+
+      AuthenticatorSupportedOptions options = device_info_->options();
+      options.client_pin_availability = AuthenticatorSupportedOptions::
+          ClientPinAvailability::kSupportedAndPinSet;
+      device_info_->SetOptions(options);
+
+      break;
+    }
+
+    case static_cast<int>(device::pin::Subcommand::kChangePIN): {
+      const auto encrypted_new_pin =
+          GetPINBytestring(request_map, pin::RequestKey::kNewPINEnc);
+      const auto encrypted_pin_hash =
+          GetPINBytestring(request_map, pin::RequestKey::kPINHashEnc);
+      const auto pin_auth =
+          GetPINBytestring(request_map, pin::RequestKey::kPINAuth);
+      const auto peer_key =
+          GetPINKey(request_map, pin::RequestKey::kKeyAgreement);
+
+      if (!encrypted_pin_hash || encrypted_pin_hash->size() != AES_BLOCK_SIZE ||
+          !encrypted_new_pin ||
+          (encrypted_new_pin->size() % AES_BLOCK_SIZE) != 0 || !pin_auth ||
+          !peer_key) {
+        return CtapDeviceResponseCode::kCtap2ErrMissingParameter;
+      }
+
+      uint8_t shared_key[SHA256_DIGEST_LENGTH];
+      if (!mutable_state()->ecdh_key) {
+        // kGetKeyAgreement should have been called first.
+        NOTREACHED();
+        return CtapDeviceResponseCode::kCtap2ErrPinTokenExpired;
+      }
+      pin::CalculateSharedKey(mutable_state()->ecdh_key.get(), peer_key->get(),
+                              shared_key);
+
+      CtapDeviceResponseCode err =
+          ConfirmPresentedPIN(mutable_state(), shared_key, *encrypted_pin_hash);
+      if (err != CtapDeviceResponseCode::kSuccess) {
+        return err;
+      };
+
+      err = SetPIN(mutable_state(), shared_key, *encrypted_new_pin, *pin_auth);
+      if (err != CtapDeviceResponseCode::kSuccess) {
+        return err;
+      };
+
+      break;
+    }
+
+    case static_cast<int>(device::pin::Subcommand::kGetPINToken): {
+      const auto encrypted_pin_hash =
+          GetPINBytestring(request_map, pin::RequestKey::kPINHashEnc);
+      const auto peer_key =
+          GetPINKey(request_map, pin::RequestKey::kKeyAgreement);
+
+      if (!encrypted_pin_hash || encrypted_pin_hash->size() != AES_BLOCK_SIZE ||
+          !peer_key) {
+        return CtapDeviceResponseCode::kCtap2ErrMissingParameter;
+      }
+
+      uint8_t shared_key[SHA256_DIGEST_LENGTH];
+      if (!mutable_state()->ecdh_key) {
+        // kGetKeyAgreement should have been called first.
+        NOTREACHED();
+        return CtapDeviceResponseCode::kCtap2ErrPinTokenExpired;
+      }
+      pin::CalculateSharedKey(mutable_state()->ecdh_key.get(), peer_key->get(),
+                              shared_key);
+
+      CtapDeviceResponseCode err =
+          ConfirmPresentedPIN(mutable_state(), shared_key, *encrypted_pin_hash);
+      if (err != CtapDeviceResponseCode::kSuccess) {
+        return err;
+      };
+
+      RAND_bytes(mutable_state()->pin_token,
+                 sizeof(mutable_state()->pin_token));
+      uint8_t encrypted_pin_token[sizeof(mutable_state()->pin_token)];
+      pin::Encrypt(shared_key, mutable_state()->pin_token, encrypted_pin_token);
+      response_map.emplace(static_cast<int>(pin::ResponseKey::kPINToken),
+                           base::span<const uint8_t>(encrypted_pin_token));
+      break;
+    }
+
+    default:
+      return CtapDeviceResponseCode::kCtap1ErrInvalidCommand;
+  }
+
+  *response = cbor::Writer::Write(cbor::Value(std::move(response_map))).value();
+  return CtapDeviceResponseCode::kSuccess;
+}
+
 CtapDeviceResponseCode VirtualCtap2Device::OnAuthenticatorGetInfo(
     std::vector<uint8_t>* response) const {
-  *response = EncodeToCBOR(device_info_);
+  *response = EncodeToCBOR(*device_info_);
   return CtapDeviceResponseCode::kSuccess;
 }
 
@@ -672,7 +1014,7 @@
         pin_protocol_it->second.GetUnsigned() >
             std::numeric_limits<uint8_t>::max())
       return base::nullopt;
-    request.SetPinProtocol(pin_auth_it->second.GetUnsigned());
+    request.SetPinProtocol(pin_protocol_it->second.GetUnsigned());
   }
 
   return std::make_pair(std::move(request),
@@ -761,7 +1103,7 @@
         pin_protocol_it->second.GetUnsigned() >
             std::numeric_limits<uint8_t>::max())
       return base::nullopt;
-    request.SetPinProtocol(pin_auth_it->second.GetUnsigned());
+    request.SetPinProtocol(pin_protocol_it->second.GetUnsigned());
   }
 
   return std::make_pair(std::move(request),
diff --git a/device/fido/virtual_ctap2_device.h b/device/fido/virtual_ctap2_device.h
index 9925fb5..1745026 100644
--- a/device/fido/virtual_ctap2_device.h
+++ b/device/fido/virtual_ctap2_device.h
@@ -30,7 +30,7 @@
     : public VirtualFidoDevice {
  public:
   VirtualCtap2Device();
-  explicit VirtualCtap2Device(scoped_refptr<State> state);
+  explicit VirtualCtap2Device(scoped_refptr<State> state, bool enable_pin);
   ~VirtualCtap2Device() override;
 
   // FidoDevice:
@@ -48,6 +48,9 @@
   CtapDeviceResponseCode OnGetAssertion(base::span<const uint8_t> request,
                                         std::vector<uint8_t>* response);
 
+  CtapDeviceResponseCode OnPINCommand(base::span<const uint8_t> request,
+                                      std::vector<uint8_t>* response);
+
   CtapDeviceResponseCode OnAuthenticatorGetInfo(
       std::vector<uint8_t>* response) const;
 
@@ -58,7 +61,6 @@
       base::Optional<AttestedCredentialData> attested_credential_data,
       base::Optional<cbor::Value> extensions);
 
-  AuthenticatorGetInfoResponse device_info_;
   base::WeakPtrFactory<FidoDevice> weak_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(VirtualCtap2Device);
diff --git a/device/fido/virtual_fido_device.cc b/device/fido/virtual_fido_device.cc
index 4ce0786..38dfa9c 100644
--- a/device/fido/virtual_fido_device.cc
+++ b/device/fido/virtual_fido_device.cc
@@ -10,6 +10,7 @@
 
 #include "crypto/ec_signature_creator.h"
 #include "device/fido/fido_parsing_utils.h"
+#include "third_party/boringssl/src/include/openssl/ec_key.h"
 
 namespace device {
 
diff --git a/device/fido/virtual_fido_device.h b/device/fido/virtual_fido_device.h
index c6cfb191..e24b3e2 100644
--- a/device/fido/virtual_fido_device.h
+++ b/device/fido/virtual_fido_device.h
@@ -23,6 +23,7 @@
 #include "device/fido/fido_device.h"
 #include "device/fido/fido_parsing_utils.h"
 #include "net/cert/x509_util.h"
+#include "third_party/boringssl/src/include/openssl/base.h"
 
 namespace crypto {
 class ECPrivateKey;
@@ -89,6 +90,21 @@
     // zero, in violation of the rules for self-attestation.
     bool non_zero_aaguid_with_self_attestation = false;
 
+    // Number of PIN retries remaining.
+    int retries = 8;
+    // The number of failed PIN attempts since the token was "inserted".
+    int retries_since_insertion = 0;
+    // True if the token is soft-locked due to too many failed PIN attempts
+    // since "insertion".
+    bool soft_locked = false;
+    // The PIN for the device, or an empty string if no PIN is set.
+    std::string pin;
+    // The elliptic-curve key. (Not expected to be set externally.)
+    bssl::UniquePtr<EC_KEY> ecdh_key;
+    // The random PIN token that is returned as a placeholder for the PIN
+    // itself.
+    uint8_t pin_token[32];
+
     FidoTransportProtocol transport =
         FidoTransportProtocol::kUsbHumanInterfaceDevice;