| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef DEVICE_FIDO_CABLE_V2_HANDSHAKE_H_ |
| #define DEVICE_FIDO_CABLE_V2_HANDSHAKE_H_ |
| |
| #include <stdint.h> |
| |
| #include <array> |
| #include <memory> |
| #include <optional> |
| #include <string_view> |
| |
| #include "base/component_export.h" |
| #include "base/containers/span.h" |
| #include "components/cbor/values.h" |
| #include "device/fido/cable/cable_discovery_data.h" |
| #include "device/fido/cable/noise.h" |
| #include "device/fido/cable/v2_constants.h" |
| #include "device/fido/fido_constants.h" |
| #include "third_party/boringssl/src/include/openssl/base.h" |
| |
| class GURL; |
| |
| namespace device::cablev2 { |
| |
| namespace tunnelserver { |
| |
| // ToKnownDomainID creates a KnownDomainID from a raw 16-bit value, or returns |
| // |nullopt| if the value maps to an assigned, but unknown, domain. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| std::optional<KnownDomainID> ToKnownDomainID(uint16_t domain); |
| |
| // DecodeDomain converts a 16-bit tunnel server domain into a string in dotted |
| // form. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| std::string DecodeDomain(KnownDomainID domain); |
| |
| // GetNewTunnelURL converts a tunnel server domain and a tunnel ID, into a |
| // WebSockets-based URL for creating a new tunnel. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| GURL GetNewTunnelURL(KnownDomainID domain, base::span<const uint8_t, 16> id); |
| |
| // GetConnectURL converts a tunnel server domain, a routing-ID, and a tunnel ID, |
| // into a WebSockets-based URL for connecting to an existing tunnel. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| GURL GetConnectURL(KnownDomainID domain, |
| std::array<uint8_t, kRoutingIdSize> routing_id, |
| base::span<const uint8_t, 16> id); |
| |
| // GetContactURL gets a URL for contacting a previously-paired authenticator. |
| // The |tunnel_server| is assumed to be a valid domain name and should have been |
| // taken from a previous call to |DecodeDomain|. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| GURL GetContactURL(KnownDomainID tunnel_server, |
| base::span<const uint8_t> contact_id); |
| |
| } // namespace tunnelserver |
| |
| namespace eid { |
| |
| // Encrypt turns an EID into a BLE advert payload by encrypting and |
| // authenticating with |key|. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| std::array<uint8_t, kAdvertSize> Encrypt( |
| const CableEidArray& eid, |
| base::span<const uint8_t, kEIDKeySize> key); |
| |
| // Decrypt turns a BLE advert payload into a plaintext EID (suitable for passing |
| // to |ToComponents|) by decrypting with |key|. It ensures that the encoded |
| // tunnel server domain is recognised. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| std::optional<CableEidArray> Decrypt( |
| const std::array<uint8_t, kAdvertSize>& advert, |
| base::span<const uint8_t, kEIDKeySize> key); |
| |
| // TODO(agl): this could probably be a class. |
| |
| // Components contains the parts of a decrypted EID. |
| struct COMPONENT_EXPORT(DEVICE_FIDO) Components { |
| Components(); |
| Components(const Components&); |
| ~Components(); |
| |
| tunnelserver::KnownDomainID tunnel_server_domain; |
| std::array<uint8_t, kRoutingIdSize> routing_id; |
| std::array<uint8_t, kNonceSize> nonce; |
| }; |
| |
| // FromComponents constructs a valid EID from the given components. The result |
| // will produce a non-nullopt value if given to |ToComponents|. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| CableEidArray FromComponents(const Components& components); |
| |
| // ToComponents explodes a decrypted EID into its components. It's the |
| // inverse of |FromComponents|. This will CHECK if the |eid| array is invalid; |
| // eids from |Decrypt| are always valid. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| Components ToComponents(const CableEidArray& eid); |
| |
| } // namespace eid |
| |
| namespace qr { |
| |
| // Components contains the parsed elements of a QR code. |
| struct COMPONENT_EXPORT(DEVICE_FIDO) Components { |
| std::array<uint8_t, device::kP256X962Length> peer_identity; |
| std::array<uint8_t, 16> secret; |
| |
| // num_known_domains is the number of registered tunnel server domains known |
| // to the device showing the QR code. Authenticators can use this to fallback |
| // to a hashed domain if their registered domain isn't going to work with this |
| // client. |
| int64_t num_known_domains = 0; |
| |
| // supports_linking is true if the device showing the QR code supports storing |
| // and later using linking information. If this is false or absent, an |
| // authenticator may wish to avoid bothering the user about linking. |
| std::optional<bool> supports_linking; |
| |
| // request_type contains the hinted type of the request. This can |
| // be used to guide UI ahead of receiving the actual request. This defaults to |
| // `kGetAssertion` if not present or if the value in the QR code is unknown. |
| FidoRequestType request_type = FidoRequestType::kGetAssertion; |
| }; |
| |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| std::optional<Components> Parse(const std::string& qr_url); |
| |
| // Encode returns the contents of a QR code that represents |qr_key|. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| std::string Encode(base::span<const uint8_t, kQRKeySize> qr_key, |
| FidoRequestType request_type); |
| |
| // BytesToDigits returns a base-10 encoding of |in|. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| std::string BytesToDigits(base::span<const uint8_t> in); |
| |
| // DigitsToBytes reverses the actions of |BytesToDigits|. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| std::optional<std::vector<uint8_t>> DigitsToBytes(std::string_view in); |
| |
| } // namespace qr |
| |
| namespace sync { |
| |
| // IDNow returns the current pairing ID for Sync. This is a very rough |
| // timestamp. |
| COMPONENT_EXPORT(DEVICE_FIDO) uint32_t IDNow(); |
| |
| // IDIsMoreThanNPeriodsOld returns true iff |candidate| is a pairing ID that |
| // was generated more than `periods` time periods before the current time. A |
| // time period is 86400 seconds, i.e. basically a day. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| bool IDIsMoreThanNPeriodsOld(uint32_t candidate, unsigned periods); |
| |
| } // namespace sync |
| |
| // DerivedValueType enumerates the different types of values that might be |
| // derived in caBLEv2 from some secret. The values this this enum are protocol |
| // constants and thus must not change over time. |
| enum class DerivedValueType : uint32_t { |
| kEIDKey = 1, |
| kTunnelID = 2, |
| kPSK = 3, |
| kPairedSecret = 4, |
| kIdentityKeySeed = 5, |
| kPerContactIDSecret = 6, |
| }; |
| |
| namespace internal { |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| void Derive(uint8_t* out, |
| size_t out_len, |
| base::span<const uint8_t> secret, |
| base::span<const uint8_t> nonce, |
| DerivedValueType type); |
| } // namespace internal |
| |
| // RequestTypeToString maps |request_type| to either "ga" (for getAssertion) or |
| // "mc" (for makeCredential). These strings are encoded in the QR code and |
| // client payload to give the phone an early hint about the type of request. |
| // This lets it craft better UI. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| const char* RequestTypeToString(FidoRequestType request_type); |
| |
| // RequestTypeFromString performs the inverse of `RequestTypeToString`. If the |
| // value of `s` is unknown, `kGetAssertion` is returned. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| FidoRequestType RequestTypeFromString(const std::string& s); |
| |
| // Derive derives a sub-secret from a secret and nonce. It is not possible to |
| // learn anything about |secret| from the value of the sub-secret, assuming that |
| // |secret| has sufficient size to prevent full enumeration of the |
| // possibilities. |
| template <size_t N> |
| std::array<uint8_t, N> Derive(base::span<const uint8_t> secret, |
| base::span<const uint8_t> nonce, |
| DerivedValueType type) { |
| std::array<uint8_t, N> ret; |
| internal::Derive(ret.data(), N, secret, nonce, type); |
| return ret; |
| } |
| |
| // IdentityKey returns a P-256 private key derived from |root_secret|. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| bssl::UniquePtr<EC_KEY> IdentityKey(base::span<const uint8_t, 32> root_secret); |
| |
| // IdentityKey returns a P-256 private key derived from |seed|. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| bssl::UniquePtr<EC_KEY> ECKeyFromSeed( |
| base::span<const uint8_t, kQRSeedSize> seed); |
| |
| // EncodePaddedCBORMap encodes the given map and pads it to |
| // |kPostHandshakeMsgPaddingGranularity| bytes in such a way that |
| // |DecodePaddedCBORMap| can decode it. The padding is done on the assumption |
| // that the returned bytes will be encrypted and the encoded size of the map |
| // should be hidden. The function can fail if the CBOR encoding fails or, |
| // somehow, the size overflows. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| std::optional<std::vector<uint8_t>> EncodePaddedCBORMap( |
| cbor::Value::MapValue map); |
| |
| // DecodePaddedCBORMap unpads and decodes a CBOR map as produced by |
| // |EncodePaddedCBORMap|. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| std::optional<cbor::Value> DecodePaddedCBORMap(base::span<const uint8_t> input); |
| |
| // Crypter handles the post-handshake encryption of CTAP2 messages. |
| class COMPONENT_EXPORT(DEVICE_FIDO) Crypter { |
| public: |
| Crypter(base::span<const uint8_t, 32> read_key, |
| base::span<const uint8_t, 32> write_key); |
| ~Crypter(); |
| |
| // Encrypt encrypts |message_to_encrypt| and overrides it with the |
| // ciphertext. It returns true on success and false on error. |
| bool Encrypt(std::vector<uint8_t>* message_to_encrypt); |
| |
| // Decrypt decrypts |ciphertext|, which was received as the payload of a |
| // message with the given command, and writes the plaintext to |
| // |out_plaintext|. It returns true on success and false on error. |
| // |
| // (In practice, command must always be |kMsg|. But passing it here makes it |
| // less likely that other code will forget to check that.) |
| bool Decrypt(base::span<const uint8_t> ciphertext, |
| std::vector<uint8_t>* out_plaintext); |
| |
| // Encrypt and decrypt with big-endian nonces and no additional data. This |
| // is the format in the spec and that we want to transition to. |
| void UseNewConstruction(); |
| |
| // IsCounterpartyOfForTesting returns true if |other| is the mirror-image of |
| // this object. (I.e. read/write keys are equal but swapped.) |
| bool IsCounterpartyOfForTesting(const Crypter& other) const; |
| |
| bool& GetNewConstructionFlagForTesting(); |
| |
| private: |
| const std::array<uint8_t, 32> read_key_, write_key_; |
| uint32_t read_sequence_num_ = 0; |
| uint32_t write_sequence_num_ = 0; |
| }; |
| |
| // HandshakeHash is the hashed transcript of a handshake. This can be used as a |
| // channel-binding value. See |
| // http://www.noiseprotocol.org/noise.html#channel-binding. |
| using HandshakeHash = std::array<uint8_t, 32>; |
| |
| // HandshakeResult is the output of the handshaking process on both sides: a |
| // |Crypter| that can encrypt and decrypt future messages on the connection, and |
| // the handshake hash that can be used to tie signatures to the connection. |
| using HandshakeResult = |
| std::optional<std::pair<std::unique_ptr<Crypter>, HandshakeHash>>; |
| |
| // HandshakeInitiator starts a caBLE v2 handshake and processes the single |
| // response message from the other party. The handshake is always initiated from |
| // the desktop. |
| class COMPONENT_EXPORT(DEVICE_FIDO) HandshakeInitiator { |
| public: |
| // The size of a valid response message. Messages of a different length |
| // may be passed to `ProcessResponse` but will always be rejected. |
| static inline constexpr size_t kResponseSize = |
| kP256X962Length + /* empty AES-GCM ciphertext length */ 16; |
| |
| HandshakeInitiator( |
| // psk is derived from the connection nonce and either QR-code secrets |
| // pairing secrets. nullopt for enclave handshakes. |
| std::optional<base::span<const uint8_t, 32>> psk, |
| // peer_identity, if not nullopt, specifies that this is a paired |
| // handshake and then contains a P-256 public key for the peer. Otherwise |
| // this is a QR handshake. |
| std::optional<base::span<const uint8_t, kP256X962Length>> peer_identity, |
| // identity_seed, if not nullopt, specifies that this is a QR handshake |
| // and contains the seed for QR key for this client. identity_seed must be |
| // provided iff |peer_identity| is not. |
| std::optional<base::span<const uint8_t, kQRSeedSize>> identity_seed); |
| |
| ~HandshakeInitiator(); |
| |
| // BuildInitialMessage returns the handshake message to send to the peer to |
| // start a handshake. |
| std::vector<uint8_t> BuildInitialMessage(); |
| |
| // ProcessResponse processes the handshake response from the peer. If |
| // successful it returns a |Crypter| for protecting future messages on the |
| // connection and a handshake transcript for signing over if needed. |
| HandshakeResult ProcessResponse(base::span<const uint8_t> response); |
| |
| private: |
| Noise noise_; |
| std::optional<std::array<uint8_t, 32>> psk_; |
| |
| std::optional<std::array<uint8_t, kP256X962Length>> peer_identity_; |
| bssl::UniquePtr<EC_KEY> local_identity_; |
| bssl::UniquePtr<EC_KEY> ephemeral_key_; |
| }; |
| |
| // RespondToHandshake responds to a caBLE v2 handshake started by a peer. Since |
| // the desktop speaks first in caBLE, this is called by the phone. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| HandshakeResult RespondToHandshake( |
| // psk is derived from the connection nonce and either QR-code secrets or |
| // pairing secrets. |
| std::optional<base::span<const uint8_t, 32>> psk, |
| // identity, if not nullptr, specifies that this is a paired handshake and |
| // contains the phone's private key. |
| bssl::UniquePtr<EC_KEY> identity, |
| // peer_identity, which must be non-nullopt iff |identity| is nullptr, |
| // contains the peer's public key as taken from the QR code. |
| std::optional<base::span<const uint8_t, kP256X962Length>> peer_identity, |
| // in contains the initial handshake message from the peer. |
| base::span<const uint8_t> in, |
| // out_response is set to the response handshake message, if successful. |
| std::vector<uint8_t>* out_response); |
| |
| // VerifyPairingSignature checks that |signature| is a valid signature of |
| // |handshake_hash| by |peer_public_key_x962|. This is used by a phone to prove |
| // possession of |peer_public_key_x962| since the |handshake_hash| encloses |
| // random values generated by the desktop and thus is a fresh value. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| bool VerifyPairingSignature( |
| base::span<const uint8_t, kQRSeedSize> identity_seed, |
| base::span<const uint8_t, kP256X962Length> peer_public_key_x962, |
| base::span<const uint8_t, std::tuple_size<HandshakeHash>::value> |
| handshake_hash, |
| base::span<const uint8_t> signature); |
| |
| // CalculatePairingSignature generates a value that will satisfy |
| // |VerifyPairingSignature|. |
| COMPONENT_EXPORT(DEVICE_FIDO) |
| std::vector<uint8_t> CalculatePairingSignature( |
| const EC_KEY* identity_key, |
| base::span<const uint8_t, kP256X962Length> peer_public_key_x962, |
| base::span<const uint8_t, std::tuple_size<HandshakeHash>::value> |
| handshake_hash); |
| |
| } // namespace device::cablev2 |
| |
| #endif // DEVICE_FIDO_CABLE_V2_HANDSHAKE_H_ |