
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

 <title>ImperialViolet</title>
 <link href="http://www.imperialviolet.org/iv-rss.xml" rel="self"/>
 <link href="http://www.imperialviolet.org/"/>
 <updated>2025-07-27T22:35:43+00:00</updated>
 <id>http://www.imperialviolet.org/</id>
 <author>
   <name>Adam Langley</name>
 </author>

 
 <entry>
   <title>TRMNL</title>
   <link href="http://www.imperialviolet.org/2025/07/27/trmnl.html"/>
   <updated>2025-07-27T00:00:00+00:00</updated>
   <id>http://www.imperialviolet.org/2025/07/27/trmnl</id>
   <content type="html">&lt;p&gt;The &lt;a href=&quot;https://usetrmnl.com/&quot;&gt;TRMNL&lt;/a&gt; is an 800×600, 1-bit e-ink display connected to a battery and a microcontroller, all housed in a nice but unremarkable plastic case. Because the microcontroller spends the vast majority of the time sleeping, and because e-ink displays don't require power unless they're updating, the battery can last six or more months. It charges over USB-C.&lt;/p&gt;

&lt;p&gt;When the microcontroller wakes up, it connects to a Wi-Fi network and communicates with a pre-configured server to fetch an 800×600 image to display, and the duration of the next sleep. You can flash your own firmware on the device, or point the &lt;a href=&quot;https://github.com/usetrmnl/trmnl-firmware&quot;&gt;standard firmware&lt;/a&gt; at a custom server. The company provides an &lt;a href=&quot;https://github.com/usetrmnl/byos_hanami&quot;&gt;example server&lt;/a&gt;, although you can implement the (HTTP-based) protocol in whatever way you wish.&lt;/p&gt;

&lt;p&gt;I considered running my own server, but thought I would give the easy path a try first to see if it would suffice. The default service lets you split the display into several tiles, and there are a number of pre-built and community-built things that can display in each. None of them worked well for me, but that's okay because you can create your own private ones. They get data either by polling a given URL, or by having data posted to a webhook. The layout is rendered using the &lt;a href=&quot;https://shopify.github.io/liquid/&quot;&gt;Liquid&lt;/a&gt; templating system, which I had not used before, but it's reasonably straightforward.&lt;p&gt;

&lt;p&gt;I wrote a Go program hosted on &lt;a href=&quot;https://cloud.google.com/run&quot;&gt;Cloud Run&lt;/a&gt; which fetches the family shared calendar and converts events from the next week into a JSON format designed to make it trivial to render in the templating system.&lt;/p&gt;

&lt;p&gt;With a &lt;a href=&quot;https://makerworld.com/en/models/1045586-trmnl-magnetic-fridge-mount#profileId-1031360&quot;&gt;3D-printed holder&lt;/a&gt;, super glue, and some &lt;a href=&quot;https://www.amazon.com/dp/B0DXSF9JJJ&quot;&gt;magnets&lt;/a&gt;, it's now happily stuck to the fridge where it displays the current date and the family events for the next week.&lt;/p&gt;

&lt;p&gt;The most awkward part of the default service is managing the refreshes. The device has a sleep schedule, and so do the tiles, which are only updated periodically. So the combination can easily leave the wrong day showing. It would be helpful if the service told you when the device would next update, and when a given tile would next update. But it's not a huge deal and, after a little bit of head scratching, I managed to configure things such that the device updates in the early hours of the morning and the tiles are ready for it.&lt;/p&gt;

&lt;p&gt;The price has gone up a bit since I ordered one, and you have to pay an extra $20 for the Developer Edition to do interesting things with it. So it ends up a little expensive for something that's neat, but hardly life-changing. But maybe you'll figure out something interesting for it! (Or you can &lt;a href=&quot;https://usetrmnl.com/guides/turn-your-amazon-kindle-into-a-trmnl&quot;&gt;repurpose an old Kindle&lt;/a&gt; into a TRMNL device.)&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Continuous Glucose Monitoring</title>
   <link href="http://www.imperialviolet.org/2025/06/29/cgm.html"/>
   <updated>2025-06-29T00:00:00+00:00</updated>
   <id>http://www.imperialviolet.org/2025/06/29/cgm</id>
   <content type="html">&lt;p&gt;Continuous glucose monitoring has been a thing for a while. It's a probe that sits just inside your body and measures blood glucose levels frequently. Obviously this is most useful for type 1 diabetics, who need to regulate their blood glucose manually.&lt;/p&gt;

&lt;p&gt;(At this point, I would be amiss not to give a nod to the book &lt;a href=&quot;https://www.amazon.com/Systems-Medicine-Physiological-Circuits-Computational/dp/1032411856&quot;&gt;Systems Medicine&lt;/a&gt;, which I think most readers would find fascinating. I can't judge whether it's correct or not, but it is a delightful exploration of a bunch of maladies from the perspective of differential equations.)&lt;/p&gt;

&lt;p&gt;But CGMs have been both expensive and prescription-only. And I am not a diabetic, type 1 or otherwise. But technology and, more importantly, regulation have apparently marched on, and even in America I &lt;a href=&quot;https://www.stelo.com/&quot;&gt;can now buy a CGM&lt;/a&gt; for $50 that lasts for two weeks, over the counter. So CGM technology is now available to the mildly curious, like me.&lt;/p&gt;

&lt;p&gt;The device itself looks like a thick guitar pick, and it comes encased inside a much larger lump of plastic that has a pretty serious-looking spring inside. It takes readings every 5 minutes but only transmits every 15 minutes. You need a phone to receive the data and, if the phone is not nearby, it will buffer some number of samples and catch up when it can. The instructions say to keep the phone nearby at all times, so I didn't test how much it will buffer beyond an hour or so.&lt;/p&gt;

&lt;p&gt;I've got both an Android and an iPhone, but for this the iPhone was a more convenient device. So everything following probably applies to both ecosystems, but I've only tested it in one.&lt;/p&gt;

&lt;p&gt;The app is well made, although you can feel the lawyers &amp;amp; regulators hovering over every part of it. It gives you instructions about how to &amp;ldquo;install&amp;rdquo; the sensor, which you do by holding the big lump of plastic with the spring over a suitable spot on your body and then pressing the button. &lt;/p&gt;

&lt;p&gt;It's not a &lt;em&gt;large&lt;/em&gt; needle, but it's not trivial either. There is a soupçon of cyberpunk about applying it to yourself in the bathroom but, honestly, my first thought after pressing the button and hearing the bang of the spring releasing was, &amp;ldquo;oh, it didn't work.&amp;rdquo; Because I didn't feel anything at all. But when I lifted the applicator away, there it was. And after a little while it started providing readings.&lt;/p&gt;

&lt;p&gt;It's held in place with some sticky plastic, and you can shower with it on. After a week or two the plastic does start to get a bit messed up. Honestly, I would have preferred to have replaced cover every few days, but I only got one in the box.&lt;/p&gt;

&lt;p&gt;I placed it on the upper arm as suggested in the instructions. I put it a little bit further around and I didn't have any problems laying down on that side.&lt;/p&gt;

&lt;p&gt;What did I learn? In a couple of cases, meals that I thought would be fairly healthy (or at least not terrible) were pretty terrible. There'll be some things that I'll avoid eating more than I had before. In the bucket of &amp;ldquo;things that should have been obvious but the effect is still stronger than I thought&amp;rdquo;: exercise really works. Even a brisk walk resets my blood sugar quite significantly. And the &lt;a href=&quot;https://en.wikipedia.org/wiki/Hawthorne_effect&quot;&gt;Hawthorne effect&lt;/a&gt; works even when you're doing it to yourself.&lt;/p&gt;

&lt;p&gt;The app does not seem to let you export the data. However, at least on iOS you can connect it to Apple Health. And Apple Health does let you export all of your data as a big XML file. So a little bit of Go code later, I have a CSV of everything it recorded and per-day averages and variations.&lt;/p&gt;

&lt;p&gt;The sensor will stop working after 15 and a half days. It says exactly 15, but I think it will give you another half day to switch over to another sensor. It comes out easily, although the sticky residue takes some effort to get off the skin.&lt;/p&gt;

&lt;p&gt;I did not switch to another sensor. I will probably do it again, but I'll give it a while since, as I expected, most of the insights that I think I'm going to get, I got fairly rapidly. Honestly, I think the gamification of not wanting to spike my blood sugar was perhaps the most effective part of it. I still think it's cool that this is a thing now.&lt;/p&gt;

</content>
 </entry>
 
 <entry>
   <title>A Tour of WebAuthn</title>
   <link href="http://www.imperialviolet.org/2024/12/23/tourofwebauthn.html"/>
   <updated>2024-12-23T00:00:00+00:00</updated>
   <id>http://www.imperialviolet.org/2024/12/23/tourofwebauthn</id>
   <content type="html">&lt;p&gt;I've done a bunch of posts about WebAuthn/passkeys over time. This year I decided to flesh them out a bit into a longer work on understanding and using WebAuthn. If you were at the FIDO conference in Carlsbad this year, you may have received a physical, printed booklet of the result. It took a while to get around to converting to HTML, but the text is now available &lt;a href=&quot;https://www.imperialviolet.org/tourofwebauthn/tourofwebauthn.html&quot;&gt;online&lt;/a&gt;.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Let's Kerberos</title>
   <link href="http://www.imperialviolet.org/2024/04/07/letskerberos.html"/>
   <updated>2024-04-07T00:00:00+00:00</updated>
   <id>http://www.imperialviolet.org/2024/04/07/letskerberos</id>
   <content type="html">&lt;p&gt;(I think this is worth pondering, but I don’t mean it &lt;i&gt;too&lt;/i&gt; seriously—don’t panic.)&lt;/p&gt;

&lt;p&gt;Are the sizes of post-quantum signatures &lt;a href=&quot;https://dadrian.io/blog/posts/pqc-signatures-2024/&quot;&gt;getting you down&lt;/a&gt;? Are you despairing of deploying a post-quantum Web PKI? Don’t fret! Symmetric cryptography is post-quantum too!&lt;/p&gt;

&lt;p&gt;When you connect to a site, also fetch a record from DNS that contains a handful of “CA” records. Each contains:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;a UUID that identifies a CA&lt;/li&gt;
  &lt;li&gt;E&lt;sub&gt;CA-key&lt;/sub&gt;(server-CA-key, AAD=server-hostname)&lt;/li&gt;
  &lt;li&gt;A key ID so that the CA can find “CA-key” from the previous field.&lt;/li&gt;
&lt;/ul&gt;
  
&lt;p&gt;“CA-key” is a symmetric key known only to the CA, and “server-CA-key” is a symmetric key known to the server and the CA.&lt;/p&gt;

&lt;p&gt;The client finds three of these CA records where the UUID matches a CA that the client trusts. It then sends a message to each CA containing:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;E&lt;sub&gt;CA-key’&lt;/sub&gt;(client-CA-key) — i.e. a key that the client and CA share, encrypted to a key that only the CA knows. We’ll get to how the client has such a value later.&lt;/li&gt;
  &lt;li&gt; A key ID for CA-key’.&lt;/li&gt;
  &lt;li&gt;E&lt;sub&gt;client-CA-key&lt;/sub&gt;(client-server-key) — the client randomly generates a client–server key for each CA.&lt;/li&gt;
  &lt;li&gt;The CA record from the server’s DNS.&lt;/li&gt;
  &lt;li&gt;The hostname that the client is connecting to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CA can decrypt “client-CA-key” and then it can decrypt “server-CA-key” (from the DNS information that the client sent) using an AAD that’s either the client’s specified hostname, or else that hostname with the first label replaced with &lt;tt&gt;*&lt;/tt&gt;, for wildcard records.&lt;/p&gt;

&lt;p&gt;The CA replies with E&lt;sub&gt;server-CA-key&lt;/sub&gt;(client-server-key), i.e. the client’s chosen key, encrypted to the server. The client can then start a TLS connection with the server, send it the three encrypted client–server keys, and  the client and server can authenticate a Kyber key-agreement using the three shared keys concatenated.&lt;/p&gt;

&lt;p&gt;Both the client and server need symmetric keys established with each CA for this to work. To do this, they’ll need to establish a public-key authenticated connection to the CA. So these connections will need large post-quantum signatures, but that cost can be amortised over many connections between clients and servers. (And the servers will have to pass standard challenges in order to prove that they can legitimately speak for a given hostname.)&lt;/p&gt;

&lt;p&gt;Some points:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The CAs get to see which servers clients are talking to, like OCSP servers used to. Technical and policy controls will be needed to prevent that information from being misused. E.g. CAs run audited code in at least SEV/TDX.&lt;/li&gt;
  &lt;li&gt;You need to compromise at least three CAs in order to achieve anything. While we have Certificate Transparency today, that’s a post-hoc auditing mechanism and a single CA compromise is still a problem in the current WebPKI.&lt;/li&gt;
  &lt;li&gt;The CAs can be required to publish a log of server key IDs that they recognise for each hostname. They could choose not to log a record, but three of them need to be evil to compromise anything.&lt;/li&gt;
  &lt;li&gt;There’s additional latency from having to contact the CAs. However, one might be able to overlap that with doing the Kyber exchange with the server. Certainly clients could cache and reuse client-server keys for a while.&lt;/li&gt;
  &lt;li&gt;CAs can generate new keys every day. Old keys can continue to work for a few days. Servers are renewing shared keys with the CAs daily. (ACME-like automation is very much assumed here.)&lt;/li&gt;
  &lt;li&gt;The public-keys that parties use to establish shared keys are very long term, however. Like roots are today.&lt;/li&gt;
  &lt;li&gt;Distrusting a CA in this model needn’t be a Whole Big Thing like it is today: Require sites to be set up with at least five trusted CAs so that any CA can be distrusted without impact. I.e. it’s like distrusting a Certificate Transparency log. &lt;/li&gt;
  &lt;li&gt;Revocation by CAs is easy and can be immediately effective.&lt;/li&gt;
  &lt;li&gt;CAs should be highly available, but the system can handle a CA being unavailable by using other ones. The high-availability part of CA processing is designed to be nearly stateless so should scale very well and be reasonably robust using anycast addresses.&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>Chrome support for passkeys in iCloud Keychain</title>
   <link href="http://www.imperialviolet.org/2023/10/18/icloudkeychain.html"/>
   <updated>2023-10-18T00:00:00+00:00</updated>
   <id>http://www.imperialviolet.org/2023/10/18/icloudkeychain</id>
   <content type="html">&lt;p&gt;Chrome 118 (which is rolling out to the Stable channel now) contains support for creating and accessing passkeys in iCloud Keychain.&lt;/p&gt;

&lt;p&gt;Firstly, I’d like to thank Apple for creating an API for this that browsers can use: it’s a bunch of work, and they didn’t have to. Chrome has long had support for creating WebAuthn credentials on macOS that were protected by the macOS Keychain and stored in the local Chrome profile. If you’ve used WebAuthn in Chrome and it asked you for Touch ID (or your unlock password) then it was this. It has worked great for a long time.&lt;/p&gt;

&lt;p&gt;But passkeys are supposed to be durable, and something that’s forever trapped in a local profile on disk is not durable. Also, if you’re a macOS + iOS user then it’s very convenient to have passkeys sync between your different devices, but Google Password Manager doesn’t cover passkeys on those platforms yet. (We’re working on it.)&lt;/p&gt;

&lt;p&gt;So having iCloud Keychain support is hopefully useful for a number of people. With Chrome 118 you’ll see an “iCloud Keychain” option appear in Chrome’s WebAuthn UI if you’re running macOS 13.5 or later:&lt;/p&gt;

&lt;div style=&quot;text-align: center&quot;&gt;&lt;img src=&quot;/binary/ickc-1.png&quot; alt=&quot;An image of the iCloud Keychain option in Chrome's WebAuthn UI&quot; style=&quot;width: 100%; max-width: 40em&quot;&gt;&lt;/div&gt;

&lt;p&gt;You won’t, at first, see iCloud Keychain credentials appear in autofill. That’s because you need to grant Chrome permission to access the metadata of iCloud Keychain passkeys before it can display them. So the first time you select iCloud Keychain as an option, you’ll see this:&lt;/p&gt;

&lt;div style=&quot;text-align: center&quot;&gt;&lt;img src=&quot;/binary/ickc-2.png&quot; alt=&quot;An image of the macOS passkeys permission dialog&quot; style=&quot;width: 25em&quot;/&gt;&lt;/div&gt;

&lt;p&gt;If you accept, then iCloud Keychain credentials will appear in autofill, and in Chrome’s account picker when you click a button to use passkeys. If you decline, then you won’t be asked again. You can still use iCloud Keychain, but you’ll have to go though some extra clicks every time.&lt;/p&gt;

&lt;p&gt;You can change your mind in System Settings → Passkeys Access for Web Browsers, or you can run &lt;kbd&gt;tccutil reset WebBrowserPublicKeyCredential&lt;/kbd&gt; from a terminal to reset that permission system wide. (Restart Chrome after doing either of those things.)&lt;/p&gt;

&lt;p style=&quot;margin-top: 1em&quot;&gt;Saving a passkey in iCloud Keychain requires having an iCloud account and having iCloud Keychain sync enabled. If you’re missing either of those, the iCloud Keychain passkey UI will prompt you to enable them to continue. It’s not possible for a regular process on macOS to tell whether iCloud Keychain syncing is enabled, at least not without gross tricks that we’re not going to try. The closest that we can cleanly detect is whether iCloud &lt;i&gt;Drive&lt;/i&gt; is enabled. If it is, Chrome will trigger iCloud Keychain for passkey creation by default when a site requests a “platform” credential in the hope that iCloud Keychain sync is also enabled. (Chrome will default to iCloud Keychain for passkey creations on accounts.google.com whatever the status of iCloud Drive, however&amp;mdash;there are complexities to also being a password manager.)&lt;/p&gt;

&lt;p&gt;If you opt into statistics collection in Chrome, thank you, and we’ll be watching those numbers to see how successful people are being in aggregate with this. If the numbers look reasonable, we may try making iCloud Keychain the default for more groups of users.&lt;/p&gt;

&lt;p&gt;If you don’t want creation to default to iCloud Keychain, there’s a control in &lt;kbd&gt;chrome://password-manager/settings&lt;/kbd&gt;:&lt;/p&gt;

&lt;div style=&quot;text-align: center&quot;&gt;&lt;img src=&quot;/binary/ickc-3.png&quot; alt=&quot;An image of Chrome settings&quot; style=&quot;width: 100%; max-width: 40em&quot;/&gt;&lt;/div&gt;

&lt;p&gt;I’ve described above how things are a little complex, but the setting is just a boolean. So, if you’ve never changed it, it reflects an approximation of what Chrome is doing. But if you set it, then every case will respect that. The enterprise policy &lt;kbd&gt;CreatePasskeysInICloudKeychain&lt;/kbd&gt; controls the same setting if you need to control this fleet-wide.&lt;/p&gt;

&lt;p&gt;With macOS 14, other password managers are able to provide passkeys into the system on macOS and iOS. This iCloud Keychain integration was written prior to Chromium building with the macOS 14 SDK so, if you happen to install such a password manager on macOS 14, its passkeys will be labeled as “iCloud Keychain” in Chrome until we can do another update. Sorry.&lt;/p&gt;

</content>
 </entry>
 
 <entry>
   <title>Signature counters</title>
   <link href="http://www.imperialviolet.org/2023/08/05/signature-counters.html"/>
   <updated>2023-08-05T00:00:00+00:00</updated>
   <id>http://www.imperialviolet.org/2023/08/05/signature-counters</id>
   <content type="html">&lt;p&gt;If you look at the structure of the &lt;a
href=&quot;https://w3c.github.io/webauthn/#sctn-authenticator-data&quot;&gt;signed
messages&lt;/a&gt; in WebAuthn you’ll notice that one of the fields is called
the “signature counter”. In the previous &lt;a
href=&quot;https://www.imperialviolet.org/2023/07/23/u2f-to-passkeys.html&quot;&gt;long
post&lt;/a&gt; I said to ignore it, which is still correct, but here’s
why.&lt;/p&gt;
&lt;p&gt;Signature counters are optional for the authenticator to implement:
it’s valid for a security key not to have a signature counter, although
the vast majority of them do. In that case, the counter value is always
zero. But once a website has seen a non-zero value, then the security
key has to ensure that the counter, for all future assertions from a
given credential, is strictly increasing.&lt;/p&gt;
&lt;p&gt;The motivation of the signature counter is that it might allow
websites to detect when a security key has been cloned. Cloning a
security key is supposed to be very difficult. At the very least, you
should need physical access to it, and hopefully you need to spend a
substantial amount of time invasively interrogating it. But, if you
assume all that happened, then one could clone a security key (probably
destroying it in the process), get the private key of a credential out
of it, and create a working replica which could be slipped back into the
possession of the legitimate user, leaving them unaware that anything
has happened. At this point, the attacker can create assertions at will
because they know the credential’s private key.&lt;/p&gt;
&lt;p&gt;If all that has happened, then the signature counter might uncover
it. Unless the attacker can know exactly when the legitimate user has
created an assertion, and thus incremented the counter, then eventually
either they or the real user will create an assertion where the counter
didn’t increase.&lt;/p&gt;
&lt;p&gt;You might be able to tell, but I consider this a rather far-fetched
scenario. Nevertheless, if a website wants to use the signature
counters, then it must treat any non-incrementing counter as a signal to
lock the account and trigger an investigation. At a minimum, the
security key in question should be replaced. Simply rejecting the
assertion is meaningless: the attacker will just increment the counter
and try again, and a regular user will assume that it’s some temporary
glitch and do the same.&lt;/p&gt;
&lt;p&gt;However, where I’ve seen sites bothering to check the signature
counter, they’ve always just treated it as a transient error. And I’ve
never heard of a signature counter actually being used to catch an
attack.&lt;/p&gt;
&lt;p&gt;On the other hand, many security keys only have a single, global
signature counter, and this allows different websites to correlate the
use of the same security key between them. That is, the current counter
value of your security key is somewhat identifying and can be combined
with information about how often you use it. For that reason, some
security keys implement more granular signature counters, and good for
them, but I consider it rather a waste.&lt;/p&gt;
&lt;p&gt;When passkeys are synced between machines, they never implement
signature counters because that would require that the set of machines
maintain a coherent value. So, over time, you’ll probably observe that
the majority of credentials don’t have them.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Voice recognition</title>
   <link href="http://www.imperialviolet.org/2023/07/29/voice-recognition.html"/>
   <updated>2023-07-29T00:00:00+00:00</updated>
   <id>http://www.imperialviolet.org/2023/07/29/voice-recognition</id>
   <content type="html">&lt;p&gt;&lt;b&gt;Update&lt;/b&gt;: &lt;a href=&quot;https://neugierig.org/software/blog/&quot;&gt;Evan&lt;/a&gt; let me know that &lt;a href=&quot;https://github.com/ggerganov/whisper.cpp&quot;&gt;Whisper&lt;/a&gt; solved the voice recognition problem. He has a wrapper that records from a microphone and prints the transcription &lt;a href=&quot;https://github.com/evmar/whisper&quot;&gt;here&lt;/a&gt;. Whisper is very impressive and the only caveat is that it sometimes inserts whole fabricated sentences at the end. The words always sort of make sense in context, but there were no sounds that could possibly have caused it. It's always at the very end in my experience, and it's no problem to remove it so, with that noted, you should ignore everything below because Whisper is a better answer.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Last week’s blog post was rather long, and had a greater than normal
number of typos. (Thanks to people who pointed them out. I think I’ve
fixed all the ones that were reported.)&lt;/p&gt;
&lt;p&gt;This was because I saw in reviews that iOS 17’s voice recognition was
supposed to be much improved, and I figured that I’d give it a try. I’ve
always found iOS’s recognition to be superior to Google Docs and I have
an old iPad Pro that’s good for betas.&lt;/p&gt;
&lt;p&gt;iOS’s performance remains good and, yes, I think it’s better than iOS
16. But it’s still hardly at the level of “magic”, especially when using
technical terms. Here’s a paragraph taken directly from the raw output
of last week’s post (I’ve highlighted errors with italics):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It is integrated into the W3C credential management specification and
so it is called via &lt;em&gt;navigator . credentials . create&lt;/em&gt; and
&lt;em&gt;navigator .credentials. get&lt;/em&gt;. This document is about
understanding the deeper structures that underpin &lt;em&gt;web orphan&lt;/em&gt;
rather than being a &lt;em&gt;guy&lt;/em&gt; as to its details. So we will leave a
great many details to the numerous guides to &lt;em&gt;Web Oran&lt;/em&gt; that
already exist on the web and instead focus on how structures from
&lt;em&gt;UF&lt;/em&gt; were carried over into &lt;em&gt;Web orphan&lt;/em&gt; and updated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;While it’s nice that many of the words are there, with that density
of errors doing all the corrections means that it’s not clearly better
than typing things out. However, the world is all aflutter about LLMs
these days. Can they help?&lt;/p&gt;
&lt;p&gt;I wrote a script to chunk paragraphs of text so that the chunks were
at least 500 bytes, and then to send them to &lt;code&gt;gpt-3.5-turbo&lt;/code&gt;
with the following instructions prefixed, and a temperature of 0.6:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The following is the output of a poor speech recognition system.
Please correct its errors and reply with the corrected version without
any preamble. Some of the uncommon words that the system can’t recognise
are U2F, CTAP, FIDO, CTAP2, JSON, FIDO2, WebAuthn, bearer token, hash,
phishing, RP ID, passkey, caBLE, and password manager.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here’s the cleaned up version of that paragraph.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It is integrated into the W3C credential management specification and
so it is called via navigator.credentials.create and
navigator.credentials.get. This document is about understanding the
deeper structures that underpin &lt;em&gt;web authentication&lt;/em&gt; rather than
being a guide as to its details. So we will leave a great many details
to the numerous guides to WebAuthn that already exist on the web and
instead focus on how structures from U2F were carried over into WebAuthn
and updated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There’s one case where it wrote “web authentication” rather than
“WebAuthn”, but it fixed all the other problems!&lt;/p&gt;
&lt;p&gt;So that’s what I tried: I dictated long chunks to iOS, then ran a
script to clean it up with GPT, then edited it manually in Obsidian.
From Obsidian, pandoc converted to HTML and EPUB formats.&lt;/p&gt;
&lt;p&gt;That prompt is the result of some experimentation. Initially, I asked
GPT to fix “errors and grammar” but, when reading the results, some
sentences were incorrect and I found that it had “fixed” them into
nonsense. Therefore I dropped “and grammar”. You can ask it to output in
Markdown format, and I probably should have done that, but I was too far
into manual editing by the time that I thought of it.&lt;/p&gt;
&lt;p&gt;An oddity was that I wrote the instructions with the word “recognise”
(English spelling) but then thought that it might work better with the
more common American spelling (“recognize”). But that seemed to make it
worse!&lt;/p&gt;
&lt;p&gt;An obvious thing to try was to use GPT 4. However, I misread the &lt;a
href=&quot;https://openai.com/pricing&quot;&gt;costs of OpenAI’s API&lt;/a&gt; and thought
that their charges were per-token, not per 1000 tokens. So with
estimates that were off by three orders of magnitude, GPT 4 seemed a bit
too expensive for a random experiment and I used GPT 3.5 for
everything.&lt;/p&gt;
&lt;p&gt;I didn’t write this post the same way, but this experimental worked
well enough that I might try it again in the future for longer public
writing.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>From U2F to passkeys</title>
   <link href="http://www.imperialviolet.org/2023/07/23/u2f-to-passkeys.html"/>
   <updated>2023-07-23T00:00:00+00:00</updated>
   <id>http://www.imperialviolet.org/2023/07/23/u2f-to-passkeys</id>
   <content type="html">&lt;p&gt;(This post is nearing 8&amp;thinsp;000 words. If you want to throw it onto an ereader there's an &lt;a href=&quot;/binary/passkeys.epub&quot;&gt;EPUB&lt;/a&gt; version too.)&lt;/p&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Over more than a decade, a handful of standards have developed into
passkeys—a plausible replacement for passwords. They picked up a lot
of complexity on the way, and this post tries to give a chronological
account of the development of the core of these technologies. Nothing
here is secret; it’s all described in various published standards.
However, it can be challenging to read these standards and understand
how it’s meant to fit together.&lt;/p&gt;
&lt;h2 id=&quot;the-beginning-u2f&quot;&gt;The beginning: U2F&lt;/h2&gt;
&lt;p&gt;U2F stands for “Universal Second Factor”. It was a pair of standards,
one for computers to talk to small removable devices called security
keys, and the second a JavaScript API for websites to use them. The
first standard of the pair is also called the Client to Authenticator
Protocol (CTAP1), and when the term “U2F” is used in isolation, it
usually refers to that. The JavaScript API, now obsolete, was generally
referred to as the “U2F API”.&lt;/p&gt;
&lt;p&gt;The goal of U2F was to eliminate “bearer tokens” in user
authentication. A “bearer token” is a term of art in authentication that
refers to any secret that is passed around to prove identity. A password
is the most common example of such a secret. It’s a bearer token because
you prove who you are by disclosing it, on the assumption that nobody
else knows the secret. Passwords are not the only bearer tokens involved
in computer security by a long way—the infamous cookies that all web
users are constantly bothered about are another example. But U2F was
focused on user authentication, while cookies identify computers, so U2F
was primarily trying to augment passwords.&lt;/p&gt;
&lt;p&gt;The problem with bearer tokens is that to use them, you have to
disclose them. And knowledge of the token is how you prove your
identity. So every time you prove your identity, you are handing another
entity the power to impersonate you. Hopefully, the other entity is the
intended counterparty and so would gain nothing from impersonating you
to itself. But websites are very complicated counterparties, made up of
many different parts, any one of which could be compromised to leak
these tokens.&lt;/p&gt;
&lt;p&gt;Digital signatures are preferable to bearer tokens because it’s
possible to prove possession of a private key, using a signature,
without disclosing that private key. So U2F allowed signatures to be
used for authentication on the web.&lt;/p&gt;
&lt;p&gt;While U2F is generally obsolete these days, it defined the core
concepts that shaped how everything that came after it worked. (And
there remain plenty of U2F security keys in use.) It’s also the clearest
demonstration of those concepts, before things got more complex, so
we’ll cover it in some detail although the following sections will use
modern terminology where things have been renamed, so you’ll see
different names if you look at the U2F specs.&lt;/p&gt;
&lt;h3 id=&quot;creating-a-credential&quot;&gt;Creating a credential&lt;/h3&gt;
&lt;p&gt;CTAP1 only includes two different commands: one to create a
credential and one to get a signature from a credential. Websites make
requests using the U2F JavaScript API and the browser translates them
into CTAP1 commands.&lt;/p&gt;
&lt;p&gt;Here’s the structure of the CTAP1 request for creating a
credential:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr class=&quot;header&quot;&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Command code, 0x01 to register&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Flags, always zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Length of following data, always 64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;SHA-256 hash of client data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;SHA-256 hash of AppID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;There are two important inputs here: the two hashes. The first is the
hash of the “client data”, a JSON structure built by the browser. The
security key includes this hash in its signed output and it’s what
allows the browser (or operating system) to put data into the signed
message. The JSON is provided to the website by the browser and can
include a variety of things, but there are two that are worth
highlighting:&lt;/p&gt;
&lt;p&gt;Firstly, the origin that made the JavaScript call. (An origin is the
protocol, hostname, and port number of a URL.) This allows the website’s
server to know what origin the user was interacting with when they were
using their security key, and that allows it to stop phishing attacks by
rejecting unknown origins. For example, if all sign-in and account
actions are done on &lt;code&gt;https://accounts.example.com&lt;/code&gt;, then the
server needs to permit that as a valid origin. But, by rejecting all
other origins, phishing attacks are easily defeated.&lt;/p&gt;
&lt;p&gt;When used outside of a web context, for example by an Android app,
the “origin” will be a special URL scheme that includes the hash of the
public key that signed the app. If a backend server expects users to be
signing in with an app, then it must recognize that app as a valid
origin value too. (You might see in some documentation that there’s an
iOS scheme similarly defined but, in fact, iOS incorrectly puts a web
origin into the JSON string even when the request comes from an
app.)&lt;/p&gt;
&lt;p&gt;The second value from the client data worth highlighting is called
the “challenge”. This value is provided by the website and it’s a large
random number. Large enough that the website that created it can be sure
that any value derived from it must have been created afterwards. This
ensures that any reply derived from it is “fresh” and this prevents
replay attacks, where an old response is repeated and presented as being
new.&lt;/p&gt;
&lt;p&gt;There are other values in the JSON string too (e.g. the type of the
message, to provide domain separation), but they’re peripheral to this
discussion.&lt;/p&gt;
&lt;p&gt;Now we’ll discuss the second hash in the request: the AppID hash. The
AppID is specified by the website and its hash is forever associated
with the newly created credential. The same value must be presented
every time the credential is used.&lt;/p&gt;
&lt;p&gt;A privacy goal of U2F and the protocols that followed it was to
prevent the creation of credentials that span websites, and thus could
be a form of “super cookie”. So the AppID hash identifies the site that
created a credential and, if some other site tries to use it, it
prevents them from doing so. Clearly, to be effective, the browser has
to limit what AppIDs a website is allowed to use—otherwise all websites
could just decide to use the same AppID and share credentials!&lt;/p&gt;
&lt;p&gt;U2F envisioned a process where browsers could fetch the AppID (which
is a URL) and parse a JSON document from it that would list other sorts
of entities, like apps, that would be allowed to use an AppID. But in
practice, I don’t believe any of the browsers ever implemented that.
Instead, a website was allowed to use an AppID if the host part of the
AppID could be formed by removing labels from the website’s origin
without hitting an eTLD. That was a complicated sentence, but don’t
worry about it for now. AppIDs are defunct, and we will cover this logic
in more detail when we discuss their replacement in a later section.&lt;/p&gt;
&lt;p&gt;What you should take away is that credentials have access controls,
so that websites can only use their own credentials. This happens to
stop most phishing attacks, but that’s incidental: the hash of the JSON
from the browser is what is supposed to stop phishing attacks. Rather,
the AppID should be seen as a constraint on websites.&lt;/p&gt;
&lt;p&gt;Given those inputs, the security key generates a new credential,
consisting of an ID and public–private key pair.&lt;/p&gt;
&lt;h4 id=&quot;registration-errors&quot;&gt;Registration errors&lt;/h4&gt;
&lt;p&gt;Assuming that the request is well-formed, there is only one plausible
error that the security key can return, but it happens a lot! The error
is called “test of user presence required”. It means that a human needs
to touch a sensor on the security key. U2F was designed so that security
keys could be implemented in a Java-based framework that did not allow
requests to block, so the computer is expected to repeatedly send
requests and, if they result in this error, to wait a short amount of
time and to send them again. Security keys will generally blink an LED
while the stream of requests is ongoing, and that’s a signal to the user
to physically touch the security key. If a touch was registered within a
short time before the request was received, then the request will be
processed successfully.&lt;/p&gt;
&lt;p&gt;This shows “user presence”, i.e. that some human actually authorised
the operation. Security keys don’t (generally) have a trusted display
that says &lt;em&gt;what&lt;/em&gt; operation is being performed, but this check
does stop malware from getting the security key to perform operations
silently.&lt;/p&gt;
&lt;h4 id=&quot;the-registration-response&quot;&gt;The registration response&lt;/h4&gt;
&lt;p&gt;Here’s what comes back from a U2F security key after creating a
credential:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr class=&quot;header&quot;&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Reserved, always has value 0x05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;Public key (uncompressed X9.62)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Length of credential ID (“L”)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;&lt;em&gt;variable&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Credential ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;67 + L&lt;/td&gt;
&lt;td&gt;variable&lt;/td&gt;
&lt;td&gt;X.509 attestation certificate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;&lt;em&gt;variable&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;variable&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;ECDSA signature&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The public key field is hopefully pretty obvious: it’s the public key
of the newly created credential. U2F always uses ECDSA with P-256 and
SHA-256, and a P-256 point in uncompressed X9.62 format is 65 bytes
long.&lt;/p&gt;
&lt;p&gt;Next, the credential ID is an opaque identifier for the credential
(although we will have more to say about it later).&lt;/p&gt;
&lt;p&gt;Then comes the attestation certificate. Every U2F security key has an
X.509 certificate that (usually) identifies the make and model of the
security key. The private key corresponding to the certificate is
embedded within the security key and, hopefully, is hard to extract.
Every new credential is signed by this attestation certificate to attest
that it was created within a specific make and model of security
key.&lt;/p&gt;
&lt;p&gt;But a unique attestation certificate would obviously become a
tracking vector that identifies a given security key every time it
creates a credential! Since we don’t want that, the same attestation
certificate is used in many security keys and manufacturers are supposed
to use the same certificate for batches of at least 100,000 security
keys.&lt;/p&gt;
&lt;p&gt;Finally, the response contains the signature, from that attestation
certificate, over several fields of the request and response.&lt;/p&gt;
&lt;p&gt;Note that there’s no self-signature from the credential. That was
probably a mistake in the design, but it’s a mistake that is still with
us today. In fact, if you don’t check the attestation signature then
nothing is signed and you needn’t have bothered with the challenge
parameter at all! That’s why you might see a challenge during
registration being set to a single zero byte or other such placeholder
value.&lt;/p&gt;
&lt;h4 id=&quot;statelessness&quot;&gt;Statelessness&lt;/h4&gt;
&lt;p&gt;The vast majority (probably all?) U2F security keys don’t actually
store anything when they create a credential. The credential ID that
they return is actually an encrypted seed that allows the security key
to regenerate the private key as needed. So the security key has a
single root key that it uses to encrypt generated seeds, and those
encrypted seeds are the credential IDs. Since you always need to send
the credential ID to a U2F security key when getting a signature from
it, no per-credential storage is necessary.&lt;/p&gt;
&lt;p&gt;The key handle won’t just be an encryption of the seed because you
want the security key to be able to ignore key handles that it didn’t
generate. Also, the AppID hash needs to be mixed into the ciphertext
somehow so that the security key can check it. But any authenticated
encryption scheme can manage these needs.&lt;/p&gt;
&lt;p&gt;Whenever you reset a stateless security key, it just regenerates its
root key, thus invalidating all previous credentials.&lt;/p&gt;
&lt;h3 id=&quot;getting-assertions&quot;&gt;Getting assertions&lt;/h3&gt;
&lt;p&gt;An “assertion” is a signature from a credential. Like we did when we
covered credential creation, let’s look at the structure of a CTAP1
assertion request because it still carries the core concepts that we see
in passkeys today:&lt;/p&gt;
&lt;table&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 9%&quot; /&gt;
&lt;col style=&quot;width: 15%&quot; /&gt;
&lt;col style=&quot;width: 75%&quot; /&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr class=&quot;header&quot;&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Command code, 0x02 to get an assertion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Flags: 0x0700 for “check-only”, 0x0300 otherwise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Length of following data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;SHA-256 hash of Client Data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;SHA-256 hash of AppID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Length of credential ID (“L”)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;&lt;em&gt;variable&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Credential ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We already know what the client data and AppID hashes are. (Although
this time you &lt;em&gt;definitely&lt;/em&gt; need a random challenge in the client
data!)&lt;/p&gt;
&lt;p&gt;The security key will attempt to decrypt the credential ID and
authenticate the AppID hash. If unsuccessful, perhaps because the
credential ID is from a different security key, it will return an error.
Otherwise, it will check to see whether its touch sensor has been
touched recently and, if so, it will return the requested assertion. (If
the touch sensor hasn’t been triggered then the platform does the same
polling as when creating a credential, as detailed above.)&lt;/p&gt;
&lt;p&gt;The bytes signed by an assertion look like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr class=&quot;header&quot;&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;SHA-256 hash of the AppID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0x1 if user-presence was confirmed, zero otherwise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Signature counter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;SHA-256 hash of the Client Data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The signature covers the client data hash, and thus it covers the
challenge from the website. So the website can be convinced that it is a
fresh signature from the security key. Since the client data also
includes the origin, the website can check that the user hasn’t been
phished.&lt;/p&gt;
&lt;p&gt;There’s also a “signature counter” field. All you need to know is
that you should ignore it—the field will generally be zero these days
anyway.&lt;/p&gt;
&lt;h4 id=&quot;transports&quot;&gt;Transports&lt;/h4&gt;
&lt;p&gt;Most security keys are USB devices. They appear on the USB bus as a
Human Interface Device (HID) and they have a special usage-page number
to identify themselves.&lt;/p&gt;
&lt;p&gt;NFC capable security keys are also quite common and frequently offer
a USB connection too. When using the security key via NFC, the touch
sensor isn’t used. Merely having the security key in the NFC field is
considered to satisfy user presence.&lt;/p&gt;
&lt;p&gt;There are also Bluetooth security keys. They work over the GATT
protocol and their major downside is that they need a battery. For a
long time, Bluetooth security keys were the only way to get a security
key to work with iOS, but since iOS added native support, they’ve become
much less common. (And Yubico now makes a security key with a Lightning
connector.)&lt;/p&gt;
&lt;h3 id=&quot;connecting-u2f-to-the-web&quot;&gt;Connecting U2F to the web&lt;/h3&gt;
&lt;p&gt;FIDO defined a web API for U2F. I’m not going to go into the details
because it’s obsolete now (and Chromium never actually implemented it,
instead shipping an internal extension that sites could communicate with
via &lt;code&gt;postMessage&lt;/code&gt;), but it’s important to understand how
browsers translated requests from websites into U2F commands because
it’s still the core of how things work now.&lt;/p&gt;
&lt;p&gt;When registering a security key, a website could provide a list of
already registered credential IDs. The idea was that the user should not
mistakenly register the same security key twice, so any security key
that recognised one of the already known credential IDs should not be
used to complete the registration request.&lt;/p&gt;
&lt;p&gt;Browsers implement this by sending a series of assertion requests to
each security key to see whether any of the credential IDs are valid for
them. That’s why there’s a “check only” mode in the assertion request:
it causes the security key to report whether the credential ID was
recognised without requiring a touch.&lt;/p&gt;
&lt;p&gt;When Chrome first implemented U2F support, any security keys excluded
by this check were ignored. But this meant that they never flashed and
users found that confusing—they assumed that the security key was
broken. So Chrome started sending dummy registration requests to those
security keys, which made them flash. If the user touched them, the
created credential would be discarded. (That was presumably a strong
incentive for U2F security keys to be stateless!)&lt;/p&gt;
&lt;p&gt;When signing in, a site sends a list of known credential IDs for the
current user. The browser sends a series of “check only” requests to the
security keys until it finds a credential recognised by each key. Then
it repeatedly sends a normal request for that credential ID until the
user touches a security key. The security key that the user touches
first “wins” and that assertion is returned to the website.&lt;/p&gt;
&lt;p&gt;The need for the website to send a list of credential IDs determines
the standard U2F sign-in experience: the user enters their username and
password and, if recognised, &lt;em&gt;then&lt;/em&gt; the site asks them to tap
their security key. A desire to move away from this model motivated the
development of the next iteration of the standards.&lt;/p&gt;
&lt;h2 id=&quot;fido2&quot;&gt;FIDO2&lt;/h2&gt;
&lt;p&gt;The U2F ecosystem described above satisfied the needs of
second-factor authentication. But that doesn’t get rid of passwords: you
still have to enter your password first and then use your security key.
If passwords were to be eliminated, more was needed. So an effort to
develop a new security key protocol, CTAP2, was started.&lt;/p&gt;
&lt;p&gt;Concurrent with the development of CTAP2, an updated web API was also
started. That ended up moving to the W3C (the usual venue for web
standards) and became the “Web Authentication” spec, or WebAuthn for
short.&lt;/p&gt;
&lt;p&gt;Together, CTAP2 and WebAuthn constituted the FIDO2 effort.&lt;/p&gt;
&lt;h3 id=&quot;discoverable-credentials&quot;&gt;Discoverable credentials&lt;/h3&gt;
&lt;p&gt;U2F credentials are called “non-discoverable”. This means that, in
order to use them, you have to know their credential ID. “Discoverable”
credentials are ones that a security key can find by itself, and thus
they can also replace usernames.&lt;/p&gt;
&lt;p&gt;A security key with discoverable credentials must dedicate storage
for each of them. Because of this, you sometimes see discoverable
credentials called “resident credentials”, but there is a distinction
between whether the security key keeps state for a credential vs whether
it’s discoverable. A U2F security key doesn’t have to be stateless, it
could keep state for every credential, and its credential IDs could
simply be identifiers. But those credentials are still non-discoverable
if they can only be used when their credential ID is presented.&lt;/p&gt;
&lt;p&gt;With discoverable credentials comes the need for credential metadata:
if the user is going to select their account entirely client-side, then
the client needs to know something like a username. So in the FIDO2
model, each credential gets three new pieces of metadata: a username, a
user display name, and a user ID. The username is a human-readable
string that uniquely identifies an account on a website (it often has
the form of an email address). The user display name can be a more
friendly name and might not be unique (it often has the form of a legal
name). The user ID is an opaque binary identifier for an account.&lt;/p&gt;
&lt;p&gt;The user ID is different from the other two pieces of metadata.
Firstly, it is returned to the website when signing in, while the other
metadata is purely client-side once it has been set. Also, the user ID
is semantically important because a given security key will only store a
single discoverable credential per website for a given user ID.
Attempting to create a second discoverable credential for a website with
a user ID that matches an existing one will cause the existing one to be
overwritten.&lt;/p&gt;
&lt;p&gt;Storing all this takes space on the security key, of course. And, if
your security key needs to be able to run within the tight power budget
of an NFC device, space might be limited. Also, the interface to manage
discoverable credentials didn’t make it into CTAP 2.0 and had to wait
for CTAP 2.1, so some early CTAP2 security keys only let you erase
discoverable credentials by resetting the whole key!&lt;/p&gt;
&lt;h3 id=&quot;user-verification&quot;&gt;User verification&lt;/h3&gt;
&lt;p&gt;You probably don’t want somebody to be able to find your lost
security key and sign in as you. So, to replace passwords, security keys
are going to have to verify that the &lt;em&gt;correct&lt;/em&gt; user is present,
not just that any user is present.&lt;/p&gt;
&lt;p&gt;So, FIDO2 has an upgraded form of user presence called “user
verification”. Different security keys can verify users in different
ways. The most basic method is a PIN entered on the computer and sent to
the security key. The PIN doesn’t have to be numeric—it can include
letters and other symbols too—one might even call it a password if the
aim of FIDO wasn’t to replace passwords. But, whatever you call it, it
is stronger than typical password authentication because the secret is
only sent to the security key, so it can’t leak from some far away
password database, and the security key can enforce a limited number of
attempts to guess it.&lt;/p&gt;
&lt;p&gt;Some security keys do user verification in other ways. They can &lt;a
href=&quot;https://www.yubico.com/products/yubikey-bio-series/&quot;&gt;incorporate a
fingerprint reader&lt;/a&gt;, or they can have an integrated PIN pad for more
secure PIN entry.&lt;/p&gt;
&lt;h3 id=&quot;rp-ids&quot;&gt;RP IDs&lt;/h3&gt;
&lt;p&gt;FIDO2 replaces AppIDs with “relying party IDs” (RP IDs). AppIDs were
URLs, but RP IDs are bare domain names. But otherwise, RP IDs serve the
same purpose as AppIDs did in CTAP1.&lt;/p&gt;
&lt;p&gt;We only briefly covered the rules for which websites can set which
AppIDs before because AppIDs are obsolete, but it’s worth covering the
rules for RP IDs in detail because of how important they are in
deployments.&lt;/p&gt;
&lt;p&gt;A site may use any RP ID formed by discarding zero or more labels
from the left of its domain name until it hits an &lt;a
href=&quot;https://en.wikipedia.org/wiki/Public_Suffix_List&quot;&gt;eTLD&lt;/a&gt;. So say
that you’re &lt;code&gt;https://www.foo.co.uk&lt;/code&gt;: you can specify an RP ID
of &lt;code&gt;www.foo.co.uk&lt;/code&gt; (discarding zero
labels), &lt;code&gt;foo.co.uk&lt;/code&gt; (discarding one label), but
not &lt;code&gt;co.uk&lt;/code&gt; because that’s an eTLD. If you don’t set an RP ID
in a request then the default is the site’s full domain.&lt;/p&gt;
&lt;p&gt;Our &lt;code&gt;www.foo.co.uk&lt;/code&gt; example might happily be creating
credentials with its default RP ID but later decide that it wants to
move all sign-in activity to an isolated
origin, &lt;code&gt;https://accounts.foo.co.uk&lt;/code&gt;. But none of the
passkeys could be used from that origin! The site would have needed to
create them with an RP ID of &lt;code&gt;foo.co.uk&lt;/code&gt; from the beginning
to allow that.&lt;/p&gt;
&lt;p&gt;So it’s important to carefully consider your RP ID from the outset.
But the rule is not to always use the most general RP ID possible. Going
back to our example, if &lt;code&gt;usercontent.foo.co.uk&lt;/code&gt; existed, then
any credentials with an RP ID of &lt;code&gt;foo.co.uk&lt;/code&gt; could be
overwritten by pages on &lt;code&gt;usercontent.foo.co.uk&lt;/code&gt;. We can
assume that &lt;code&gt;foo.co.uk&lt;/code&gt; is checking the origin of any
assertions, so &lt;code&gt;usercontent.foo.co.uk&lt;/code&gt; can’t use its ability
to set an RP ID of &lt;code&gt;foo.co.uk&lt;/code&gt; to generate valid assertions,
but it can still try to get the user to create new credentials which
could overwrite the legitimate ones.&lt;/p&gt;
&lt;h3 id=&quot;ctap-protocol-changes&quot;&gt;CTAP protocol changes&lt;/h3&gt;
&lt;p&gt;In addition to the high-level semantic changes outlined above, the
syntax of CTAP2 is thoroughly different from the U2F. Rather than being
a binary protocol with fixed or ad-hoc field lengths, it uses &lt;a
href=&quot;https://datatracker.ietf.org/doc/html/rfc8949&quot;&gt;CBOR&lt;/a&gt;. CBOR,
when reasonably subset, is a &lt;a
href=&quot;https://msgpack.org/&quot;&gt;MessagePack&lt;/a&gt;-like encoding that can
represent the JSON data model in a compact binary format, but
it also supports a bytestring type to avoid having to
base64-encode binary values.&lt;/p&gt;
&lt;p&gt;CTAP2 replaces the polling-based model of U2F with one where a
security key would wait to process a request until it was able. It also
tried to create a model where the entire request would be sent by the
platform in a single message, rather than having the platform iterate
through credential IDs to find ones that a security key recognised.
However, due to limited buffer sizes of security keys, this did not work
out: the messages could end up too large, especially when dealing with
large lists of credential IDs, so many requests will still involve
multiple round trips between the computer and the security key to
process.&lt;/p&gt;
&lt;p&gt;While I’m not going to cover CTAP2 in any detail, let’s have a look
at a couple of examples. Here’s a credential creation request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  # SHA-256 hash of client data
  1: h&amp;#39;60EACC608F20422888C8E363FE35C9544A58B8920989D060021BC30F7323A423&amp;#39;,
  # RP ID and friendly name of website
  2: {
    &amp;quot;id&amp;quot;: &amp;quot;webauthn.io&amp;quot;,
    &amp;quot;name&amp;quot;: &amp;quot;webauthn.io&amp;quot;
  },
  3: {
    # User ID
    &amp;quot;id&amp;quot;: h&amp;#39;526E4A6C5A41&amp;#39;,
    # Username
    &amp;quot;name&amp;quot;: &amp;quot;Fred&amp;quot;,
    # User Display Name
    &amp;quot;displayName&amp;quot;: &amp;quot;Fred&amp;quot;
  },
  4: [
    # ECDSA with P-256 is acceptable to the website
    {&amp;quot;alg&amp;quot;: -7, &amp;quot;type&amp;quot;: &amp;quot;public-key&amp;quot;},
    # And so is RSA.
    {&amp;quot;alg&amp;quot;: -257, &amp;quot;type&amp;quot;: &amp;quot;public-key&amp;quot;}
  ],
  # Create a discoverable credential.
  7: {&amp;quot;rk&amp;quot;: true},
  # A MAC showing that the user has entered the correct PIN and thus
  # This request has verified the user with &amp;quot;PIN protocol&amp;quot; v1.
  8: h&amp;#39;4153542771C1BF6586718BCD0ECA8E96&amp;#39;, 9: 1
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CBOR is a binary format, but it defines a &lt;a
href=&quot;https://datatracker.ietf.org/doc/html/rfc8949#name-diagnostic-notation&quot;&gt;diagnostic
notation&lt;/a&gt; for debugging, and that’s how we’ll present CBOR messages
here. If you scan down the fields in the message, you’ll see
similarities and differences with U2F:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The hash of the client data is still there.
&lt;li&gt;The AppID is replaced by an RP ID, but the RP ID is included verbatim rather than hashed.
&lt;li&gt;There’s metadata for the user because the request is creating a discoverable credential.
&lt;li&gt;The website can list the public key formats that it recognises so that there’s some algorithm agility.
&lt;li&gt;User verification was done by entering a PIN on the computer and there’s some communication about that (which we won’t go into).
&lt;/ul&gt;

&lt;p&gt;Likewise, here’s an assertion request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  # RP ID of the requesting website.
  1: &amp;quot;webauthn.io&amp;quot;,
  # Hash of the client data
  2: h&amp;#39;E7870DBBA212581A536D29D38831B2B8192076BAAEC76A4B34918B4222B79616&amp;#39;,
  # List of credential IDs
  3: [
    {&amp;quot;id&amp;quot;: h&amp;#39;D64875A5A7C642667745245E118FCD6A&amp;#39;, &amp;quot;type&amp;quot;: &amp;quot;public-key&amp;quot;}
  ],
  # A MAC showing that the user has entered the correct PIN and thus
  # This request has verified the user with &amp;quot;PIN protocol&amp;quot; one.
  6: h&amp;#39;6459AF24BBDA323231CF42AECABA51CF&amp;#39;, 7: 1
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again, it’s structurally similar to the U2F request, except that the
list of credential IDs is included in the request rather than having the
computer poll for each in turn. Since the credential that we created was
discoverable, critically that list could also be empty and the request
would still work! That’s why discoverable credentials can be used before
a username has been entered.&lt;/p&gt;
&lt;p&gt;With management of discoverable credentials, fingerprint enrollment,
enterprise attestation support, and more, CTAP2 is quite complex. But
it’s a very capable authentication ecosystem for enterprises and
experts.&lt;/p&gt;
&lt;h3 id=&quot;webauthn&quot;&gt;WebAuthn&lt;/h3&gt;
&lt;p&gt;As part of the FIDO2 effort, the WebAuthn API was completely
replaced. If you recall, the U2F web API was not a W3C standard, and it
was only ever implemented in Chromium as a hidden extension. The
replacement, called WebAuthn, is a real W3C spec and is now implemented
in all browsers.&lt;/p&gt;
&lt;p&gt;It is substantially more complicated than the old API!&lt;/p&gt;
&lt;p&gt;WebAuthn is integrated into the &lt;a
href=&quot;https://www.w3.org/TR/credential-management-1/&quot;&gt;W3C credential
management specification&lt;/a&gt; and so it is invoked in JavaScript via
&lt;code&gt;navigator.credentials.create&lt;/code&gt; and
&lt;code&gt;navigator.credentials.get&lt;/code&gt;. This document is about
understanding the deeper structures that underpin WebAuthn rather than
being a guide to its details. So we’ll leave them to the numerous
tutorials that already exist on the web and instead focus on how
structures from U2F were carried over into WebAuthn and updated.&lt;/p&gt;
&lt;p&gt;Firstly, we’ll look at the &lt;a
href=&quot;https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data&quot;&gt;structure
of a signed assertion&lt;/a&gt; in WebAuthn.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr class=&quot;header&quot;&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;SHA-256 hash of the RP ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.w3.org/TR/webauthn-2/#flags&quot;&gt;Flags&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Signature counter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;even&quot;&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;&lt;em&gt;varies&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;CBOR-encoded extension outputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr class=&quot;odd&quot;&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;SHA-256 hash of the client data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;It should look familiar because it’s a superset of the CTAP signed
message format. This was chosen deliberately so that U2F security keys
would function with WebAuthn. This wasn’t a given—there were discussions
about whether it should be a fresh start–but ultimately there were lots
of perfectly functional U2F security keys out in the world, and it
seemed too much of a shame to leave them behind.&lt;/p&gt;
&lt;p&gt;But there are changes in the details. Firstly, what was the AppID
hash is now the RP ID hash. We discussed RP IDs above and, importantly,
the space of AppIDs and the space of RP IDs is distinct. So since U2F
security keys compare the hashes of these strings, no credential
registered with the old U2F API could function with WebAuthn. From the
security keys’ perspective, the hash is incorrect and so the credential
can’t be used. Some complicated workarounds were needed for this, which
we will touch on later.&lt;/p&gt;
&lt;p&gt;The other changes in the assertion format come from defining
additional flag bits and adding an extensions block. The most important
new flag bit is the one that indicates that user verification was
performed in an assertion. (WebAuthn and CTAP2 were co-developed, and so
the new concept of user verification from the latter was exposed in the
former.)&lt;/p&gt;
&lt;p&gt;The extensions block was added to make the assertion format more
flexible. While U2F’s binary format was pleasantly simple, it was
difficult to change. Since CTAP2 was embracing CBOR throughout, it made
sense that security keys be able to return any future fields that needed
to be added to the assertion in CBOR format.&lt;/p&gt;
&lt;p&gt;Correspondingly, an extension block was added into the WebAuthn
requests too (although those are JavaScript objects rather than CBOR).
The initial intent was that browsers would transcode extensions into
CBOR, send them to the authenticator, and the authenticator could return
the result in its output. However, exposing arbitrary and unknown
functionality from whatever USB devices were plugged into the computer
to the open web was too much for browsers, and no browser ever allowed
arbitrary extensions to be passed through like that. Nonetheless,
several important pieces of functionality have been implemented via
extensions in the subsequent years.&lt;/p&gt;
&lt;p&gt;The first major extension was a workaround for the transition to RP
IDs mentioned above. The &lt;a
href=&quot;https://www.w3.org/TR/webauthn-2/#sctn-appid-extension&quot;&gt;&lt;code&gt;appid&lt;/code&gt;
extension&lt;/a&gt; to WebAuthn allowed a website to assert a U2F AppID when
requesting an assertion, so that credentials registered with the old U2F
API could still be used. Similarly, the &lt;a
href=&quot;https://www.w3.org/TR/webauthn-2/#sctn-appid-exclude-extension&quot;&gt;&lt;code&gt;appidExclude&lt;/code&gt;
extension&lt;/a&gt; could specify an AppID in a WebAuthn registration request
so that a security key registered under the old API couldn’t be
accidentally registered twice.&lt;/p&gt;
&lt;p&gt;Overall, the transition to RP IDs probably wasn’t worth it, but we’ve
done it now so it’s only a question of learning for the future.&lt;/p&gt;
&lt;p&gt;Extensions in the signed response allow the authenticator to add
extra data into the response, but the last field in the signed message,
the &lt;a href=&quot;https://w3c.github.io/webauthn/#client-data&quot;&gt;client
data&lt;/a&gt; hash, is carried over directly from U2F and remains the way
that the browser/platform adds extra data. It gained some more fields in
WebAuthn:&lt;/p&gt;
&lt;pre class=&quot;idl&quot;&gt;&lt;code&gt;dictionary CollectedClientData {
    required DOMString           type;
    required DOMString           challenge;
    required DOMString           origin;
    DOMString                    topOrigin;
    boolean                      crossOrigin;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The centrally-important &lt;code&gt;origin&lt;/code&gt; and &lt;code&gt;challenge&lt;/code&gt;
are still there, and &lt;code&gt;type&lt;/code&gt; for domain separation, but the
modern web is complex and often involves layers of iframes and so some
more fields have been added to ensure that backends have a clear and
correct picture of where the purposed sign-in is happening.&lt;/p&gt;
&lt;h2 id=&quot;other-types-of-authenticator&quot;&gt;Other types of authenticator&lt;/h2&gt;
&lt;p&gt;Until now, we have been dealing only with security keys as
authenticators. But WebAuthn does not require that all authenticators be
security keys. Although aspects of CTAP2 poke through in the WebAuthn
data structures, anything that formats messages correctly can be an
authenticator, and so laptops and desktops themselves can be
authenticators.&lt;/p&gt;
&lt;p&gt;These devices are known as “platform authenticators”. At this point
in our evolution, they are aimed at a different use case than security
keys. Security keys are called “cross-platform authenticators” because
they can be moved between devices, and so they can be used to
authenticate on a brand-new device. A platform authenticator is for when
you need to re-authenticate a user, that is, to establish that the
correct human is still behind the keyboard. Since we want to validate a
specific human, platform authenticators must support user
verification to be useful for this.&lt;/p&gt;
&lt;p&gt;And so there is a specific feature detection function called &lt;a
href=&quot;https://www.w3.org/TR/webauthn-2/#sctn-isUserVerifyingPlatformAuthenticatorAvailable&quot;&gt;&lt;code&gt;isUserVerifyingPlatformAuthenticatorAvailable&lt;/code&gt;&lt;/a&gt;
(usually shortened to “isUVPAA” for obvious reasons). Any website can
call this and it will return true if there is a platform authenticator
on the current device that can do user verification.&lt;/p&gt;
&lt;p&gt;The majority of WebAuthn credentials are created on platform
authenticators now because they’re so readily available and easy to
use.&lt;/p&gt;
&lt;h3 id=&quot;cable-hybrid&quot;&gt;caBLE / hybrid&lt;/h3&gt;
&lt;p&gt;While platform authenticators were great for reauthenticating on the
same computer, they could never work for signing in on a different
computer. And the set of people who were going to go out and buy
security keys was always going to be rather small. So, to broaden the
reach of WebAuthn, allowing people to use their phones as authenticators
was an obvious step.&lt;/p&gt;
&lt;p&gt;CTAP over BLE was already defined, but Bluetooth pairing was an
awkward and error-prone process. Could we make phones usable as
authenticators without it?&lt;/p&gt;
&lt;p&gt;The first attempt was called cloud-assisted BLE (caBLE) and it
involved the website and the phone having a shared key. A WebAuthn
extension allowed the website to request that a computer start
broadcasting a byte string over BLE. The idea was that the phone would
be listening for these BLE adverts, would trial decrypt their contents
against the set of shared keys it knew about, and (if it found a match)
it would start advertising in response. When the computer saw a matching
reply, it would make a Generic Attribute Profile (GATT) connection to
that phone, do encryption at the application level, and then CTAP could
continue as normal, all without having to do Bluetooth pairing.&lt;/p&gt;
&lt;p&gt;This was launched as a feature specific to
&lt;code&gt;accounts.google.com&lt;/code&gt; and Chrome. For several years you could
enable “Phone as a Security Key” for your Google account and it did
something like that. But, despite a bunch of effort, there were
persistent problems:&lt;/p&gt;
&lt;p&gt;Firstly, listening for Bluetooth adverts in the background was
difficult in the Android ecosystem. To work around this,
&lt;code&gt;accounts.google.com&lt;/code&gt; would send a notification to the phone
over the network to tell it when to start listening. This was fine for
&lt;code&gt;accounts.google.com&lt;/code&gt;, but most websites can’t do that.&lt;/p&gt;
&lt;p&gt;Second, the quality of Bluetooth hardware in desktops varies
considerably, and getting a desktop to send more than one BLE
advert never worked well. So you could only have one phone
enrolled for this service, per account.&lt;/p&gt;
&lt;p&gt;Lastly, but most critically, BLE GATT connections were just too
unreliable. Even after a considerable amount of work to try and debug
issues, the most reliable combination of phone and desktop achieved only
95% connection success—and that’s after the two devices had managed to
exchange BLE adverts. In common configurations, the success rate
was closer to 80% and it would randomly fail even for the people
developing it. So despite trying for years to make this design work, it
had to be abandoned.&lt;/p&gt;
&lt;p&gt;The next attempt was called caBLEv2. Given all the issues with BLE in
the previous iteration, caBLEv2 was designed to use the least amount of
Bluetooth possible: a single advert sent from the phone to the
desktop. This means that the rest of the communication went over the
internet, which requires that both phone and desktop have an internet
connection. This is unfortunate, but there were no other viable options.
Using Bluetooth Classic presents a host of problems, and BLE L2CAP does
not work from user space on Windows.&lt;/p&gt;
&lt;p&gt;Still, using Bluetooth somewhere in the protocol is critical because
it proves proximity between the two devices. If &lt;em&gt;all&lt;/em&gt;
communication was done over the Internet, then the phone has no proof
that the computer it is sending the assertion to is nearby. It could be an
attacker’s computer on the other side of the world. But if we can send
one Bluetooth message from the phone and make the computer prove that it
has received it, then all other communication can be routed over the
Internet. And that is what caBLEv2 does.&lt;/p&gt;
&lt;p&gt;It also changed the relationship between the parties. While caBLEv1
required that a key be shared between the website and the phone, caBLEv2
was a relationship between a &lt;em&gt;computer&lt;/em&gt; and a phone. This made
some user flows less smooth, but it made it much easier for smaller
websites to take advantage of the capability.&lt;/p&gt;
&lt;p&gt;In practice, caBLEv2 has worked far better, although Bluetooth
problems still occur. (And not every desktop has Bluetooth.)&lt;/p&gt;
&lt;p&gt;A caBLEv2 transaction is often triggered by a browser showing a QR
code. That QR code contains a public key for the browser and a shared
secret. When a phone scans it, it starts sending a BLE advert that is
encrypted with the shared secret and which contains a nonce and the
location of an internet server that communication can be routed through.
The desktop decrypts this advert, connects to that server (which
forwards messages to the phone and back), and starts a cryptographic
handshake to prove that it holds the keys from the QR code and that it
received the BLE advert. Once that communication channel is established,
CTAP2 is run over it so that the phone can be used as an
authenticator.&lt;/p&gt;
&lt;p&gt;caBLEv2 also allows the phone to send information to the desktop that
allows the desktop to contact it in the future without scanning a QR
code. This depends on that same internet service, which must be able to
send a notification to the phone, rather than constant BLE listening.
(Although a BLE advert is sent for every transaction to prove
proximity.) &lt;/p&gt;
&lt;p&gt;But ultimately, while the name caBLE was cute, it was also confusing.
And so FIDO renamed it to “hybrid” when it was included in CTAP 2.2. So
you’ll now see this called “&lt;a
href=&quot;https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-hybrid&quot;&gt;hybrid
CTAP&lt;/a&gt;” and the transport name in WebAuthn is &lt;code&gt;hybrid&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-webauthn-family-of-apis&quot;&gt;The WebAuthn-family of APIs&lt;/h2&gt;
&lt;p&gt;WebAuthn is a web API, but people also use their computers and phones
outside of a web browser sometimes. So while these contexts can’t use
WebAuthn itself, a number of APIs for native apps that are similar to
WebAuthn have popped up. These APIs aren’t WebAuthn, but if they produce
signed messages in the same format as WebAuthn, a backend server needn’t
know the difference. It’s a term that I’ve made up, but I call them
“WebAuthn-family” APIs.&lt;/p&gt;
&lt;p&gt;On Windows, &lt;a
href=&quot;https://github.com/microsoft/webauthn&quot;&gt;&lt;code&gt;webauthn.dll&lt;/code&gt;&lt;/a&gt;
is a system service that reproduces most of WebAuthn for apps. (Browsers
on Windows use this to implement WebAuthn, so it has to be pretty
complete.) On iOS and macOS, &lt;a
href=&quot;https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication&quot;&gt;Authentication
Services&lt;/a&gt; does much the same. On Android, &lt;a
href=&quot;https://developer.android.com/training/sign-in/passkeys&quot;&gt;Credential
Manager&lt;/a&gt; allows apps to pass in JSON-encoded WebAuthn requests and
get JSON responses back. WebAuthn Level Three also includes &lt;a
href=&quot;https://w3c.github.io/webauthn/#sctn-parseCreationOptionsFromJSON&quot;&gt;support&lt;/a&gt;
for the same JSON encoding so that backends should seamlessly be able to
handle sign-ins from the web and Android apps. (WebAuthn should never
have used ArrayBuffers.)&lt;/p&gt;
&lt;h2 id=&quot;passkeys&quot;&gt;Passkeys&lt;/h2&gt;
&lt;p&gt;With hybrid and platform authenticators, people had lots of access to
WebAuthn authenticators. But if you reset or lost your phone/laptop you
still lost all of your credentials, same as if you reset or lost a
security key. In an enterprise situation, losing a security key is
resolved by going to the helpdesk. In a personal context, the advice had
long been to register at least two security keys and to keep one of them
locked away in a safe. But it’s awfully inconvenient to register a
security key everywhere when it’s locked in a safe. So while this advice
worked for protecting a tiny number of high-value accounts, if WebAuthn
credentials were ever going to make a serious dent in the regular
authentication ecosystem, they had to do better.&lt;/p&gt;
&lt;p&gt;“Better” has to mean “recoverable”. People do lose and reset their
phones, and so a heretofore sacred property of FIDO would have to be
relaxed so that it could expand its scope beyond enterprises and
experts: private keys would have to be backed up.&lt;/p&gt;
&lt;p&gt;In 2021, with iOS 15, Apple included the ability to save WebAuthn
private keys into iCloud Keychain, and Android Play Services got support
for hybrid. At the end of 2022, iOS 16 added support for hybrid and, on
Android, Google Password Manager added support for backing up and
syncing private keys.&lt;/p&gt;
&lt;p&gt;People now had common access to authenticators, the ability to assert
credentials across devices with them, and fair assurance that they could
recover those credentials. To bundle that together and give it a more
friendly name, Apple introduced better branding: passkeys.&lt;/p&gt;
&lt;p&gt;With passkeys, the world now has a widely available authentication
mechanism that isn’t subject to phishing, isn’t subject to password reuse nor
credential stuffing, can’t be sniffed and replayed by malicious
3rd-party JavaScript on the website, and doesn’t cause a mess when the
server-side &lt;a href=&quot;https://haveibeenpwned.com/&quot;&gt;password database
leaks&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There is some ambiguity about the definition of passkeys. Passkeys
are synced, discoverable WebAuthn credentials. But we don’t want to
exclude people who really want to use a security key, so if you would
like to create a credential on a security key, we assume you know what
you’re doing and the UI will refer to them as passkeys even though they
aren’t synced. Also, we’re still building the ecosystem of syncing,
which is quite fragmented presently: Windows Hello doesn’t sync at all,
Google Password Manager can only sync between Android devices, and
iCloud Keychain only works on Apple devices. So there is a fair chance
that if you create a credential that gets called a passkey, it might not
actually be backed up anywhere. So the definition is a little bit
aspirational for the moment, but we’re working on it.&lt;/p&gt;
&lt;p&gt;Another feature that came with the introduction of passkeys was
integration into browser autofill. (This is also called “conditional UI”
because of the name of a value in the W3C credential management spec.)
So websites can now opt to have passkeys listed in autofill, as
passwords are. This is not a long-term design! It would be weird if in
20 years websites had to have a pair of text boxes on their front page
for signing in, in the same way that we use an icon of a floppy disk to
denote saving. But conditional UI hopefully makes it much easier for
websites to adopt passkeys, given that they are starting with a user
base that is 100% password users.&lt;/p&gt;
&lt;p&gt;If you want to understand how passkey support works on a website, see
&lt;a
href=&quot;https://www.imperialviolet.org/2022/09/22/passkeys.html&quot;&gt;here&lt;/a&gt;.
But remember that the core concepts stretch back to U2F: passkeys are
still partitioned by an RP ID, they still have credential IDs, and
there’s still the client data containing a server-provided
challenge.&lt;/p&gt;
&lt;h2 id=&quot;the-future&quot;&gt;The future&lt;/h2&gt;
&lt;p&gt;The initial launch of passkeys didn’t have any provision for
third-party password managers. On iOS and macOS, you had to use iCloud
Keychain, and on Android you had to use Google Password Manager. That
was expedient but never the intended end state, and with iOS 17 and
Android 14, third-party password managers can save and provide
passkeys.&lt;/p&gt;
&lt;p&gt;At the time of writing, in 2023, most of the work is in building out
the ecosystem that we have sketched. Passkeys need to sync to more
places, and third-party password manager support needs to get fleshed
out.&lt;/p&gt;
&lt;p&gt;There are a number of topics on the horizon, however. With FIDO2,
CTAP, and WebAuthn, we are asking websites to trust password managers a
lot more. While password managers have long existed, usage is far from
universal. But with FIDO2, by design, users have to use a password
manager. We are also suggesting that with passkeys, websites might not
need to use a second authentication factor. Two-factor authentication
has become commonplace, but that’s because the first factor (the
password) was such rubbish. With passkeys, that’s no longer the case.
That brings many benefits! But it means that websites are outsourcing
their authentication to password managers, and some would like some
attestation that they’re doing a good job.&lt;/p&gt;
&lt;p&gt;Next, the concept of an RP ID is central to passkeys, but it’s a very
web-centric concept. Some services are mobile-only and don’t have a
strong brand in the form of a domain name. But passkeys are forever
associated with an RP ID, which forces apps to commit to the domain name
that might well appear in the UI.&lt;/p&gt;
&lt;p&gt;The purpose of the RP ID was to stop credentials from being shared
across websites and thus becoming a tracking vector. But now that we
have a more elaborate UI, perhaps we could show the user the places
where credentials are being used and let the RP ID be a hash of a public
key, or something else not tied to DNS.&lt;/p&gt;
&lt;p&gt;We also need to think about the problem of users transitioning
between ecosystems. People switch from Android to iOS and vice versa,
and they should be able to bring their passkeys along with them.&lt;/p&gt;
&lt;p&gt;There is a big pile of corpses labeled “tried to replace passwords”.
Passkeys are the best attempt so far. Here's hoping that in five years’
time, that they’re not a cautionary tale.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Books, 2022</title>
   <link href="http://www.imperialviolet.org/2022/12/18/books.html"/>
   <updated>2022-12-18T00:00:00+00:00</updated>
   <id>http://www.imperialviolet.org/2022/12/18/books</id>
   <content type="html">&lt;p&gt;As Twitter is having a thing (&lt;tt&gt;agl@infosec.exchange&lt;/tt&gt;, by the way) it's nice that RSS is still ticking along. To mark that fact as we reach the end of the year, I decided to write up a list of books that I've read in the past 12 months that feel worthy of recommendation to a general audience.&lt;/p&gt;

&lt;h3&gt;&lt;a href=&quot;https://www.google.com/search?q=9780593460177&quot;&gt;Flying Blind&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Boeing was once a standard-bearer for American engineering and manufacturing abilities and now it's better known for the groundings of the 737 MAX 8 and 787. This is a history of how Boeing brought McDonnell Douglas and found that it contained lethal parasites.&lt;/p&gt;

&lt;h3&gt;&lt;a href=&quot;https://www.google.com/search?q=9780262545044&quot;&gt;Electrify&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Burning things produces CO&lt;sub&gt;2&lt;/sub&gt; and air pollution. Both slowly hurt people so we should stop doing it where possible. This is a data-filled book on how to get a long way towards that goal, and is an optimistic respite from some of the other books on this list.

&lt;h3&gt;&lt;a href=&quot;https://www.google.com/search?q=978-0385348713&quot;&gt;The Splendid &amp;amp; The Vile&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Do we really need another book about WWII? Perhaps not, but I enjoyed this one which focuses on the members of the Churchill family in the first couple of years of the war.&lt;/p&gt;

&lt;h3&gt;&lt;a href=&quot;https://www.google.com/search?q=978-0393651485&quot;&gt;Transformer&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Ignorant as I am about microbiology, I love Nick Lane books because they make me feel otherwise and I cross my fingers that they're actually accurate. This book is avowedly presenting a wild theory—which is probably false—but is a wonderful tour of the landscape either way.&lt;/p&gt;

&lt;h3&gt;&lt;a href=&quot;https://www.google.com/search?q=978-0544236042&quot;&gt;Stuff Matters&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A pop-science book, but a good one that focuses on a number of materials in the modern world. If you know anything about the topic, this is probably too light-weight. But if, like me, you know little, it's a fun introduction.&lt;/p&gt;

&lt;h3&gt;&lt;a href=&quot;https://www.google.com/search?q=978-0143036531&quot;&gt;Amusing Ourselves to Death&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This is a book from 1985 about the horrors of television. Its arguments carry more force when transposed to the modern internet but are also placed into a larger context because they were made 40 years ago and TV hasn't destroyed civilisation (… probably).&lt;/p&gt;

&lt;h3&gt;&lt;a href=&quot;https://www.worksinprogress.co/issue/the-story-of-vaccinateca/&quot;&gt;The Story of VaccinateCA&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I'm cheating: this isn't a book, although it is quite long. As we collectively fail to deal with several slow crises, here's a tale of what failed and what succeeded in a recent short, sharp crisis. It is very California focused, and less useful to those outside of America, but I feel that it's quite important.&lt;/p&gt;

&lt;h3&gt;&lt;a href=&quot;https://www.google.com/search?q=978-0134397603&quot;&gt;Art of Computer Programming, Satisfiability&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This isn't “general audience”, but if you read this far perhaps neither are you. SMT solvers are volatile magic and, while this is only about SAT, it's a great introduction to the area. I would love  to say that I read it in the depth that it deserves, but even skimming the text and skipping the exercises is worth a lot.&lt;/p&gt;

&lt;div style=&quot;height: 1em&quot;&gt;&lt;/div&gt;

&lt;p&gt;The Economist also has its &lt;a href=&quot;https://www.economist.com/culture/2022/12/06/these-are-the-economists-best-books-of-2022&quot;&gt;books of the year&lt;/a&gt; list. I've only read one of them, &lt;a href=&quot;https://www.google.com/search?q=1399803425&quot;&gt;Slouching Towards Utopia&lt;/a&gt;, and it didn't make my list above!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Passkeys</title>
   <link href="http://www.imperialviolet.org/2022/09/22/passkeys.html"/>
   <updated>2022-09-22T00:00:00+00:00</updated>
   <id>http://www.imperialviolet.org/2022/09/22/passkeys</id>
   <content type="html">&lt;p&gt;This is an opinionated, “quick-start” guide to using passkeys as a
web developer. It’s hopefully broadly applicable, but one size will
never fit all authentication needs and this guide ignores everything
that’s optional. So take it as a worked example, but not as gospel.&lt;/p&gt;
&lt;p&gt;It doesn't use any WebAuthn libraries, it just assumes that you have access to functions for verifying signatures. That mightn't be optimal&amp;mdash;maybe finding a good library is better idea&amp;mdash;but passkeys aren't so complex that it's unreasonable for people to know what's going on.&lt;/p&gt;
&lt;p&gt;This is probably a post that'll need updating over time, making it a bad fit for a blog, so maybe I'll move it in the future. But it's here for now.&lt;/p&gt;
&lt;p&gt;Platforms for developing with passkeys include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Safari on iOS 16 or macOS 13.&lt;/li&gt;
&lt;li&gt;Chrome Canary (with
&lt;code&gt;chrome://flags#webauthn-conditional-ui&lt;/code&gt; set) on Win­dows
22H2.&lt;/li&gt;
&lt;li&gt;Chrome Canary (with
&lt;code&gt;chrome://flags#webauthn-conditional-ui&lt;/code&gt; set) on mac­OS.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;database-changes&quot;&gt;Database changes&lt;/h2&gt;
&lt;p&gt;Each user will need a passkey &lt;a
href=&quot;https://www.w3.org/TR/webauthn-2/#user-handle&quot;&gt;user ID&lt;/a&gt;. The
user ID identifies an account, but &lt;a
href=&quot;https://www.w3.org/TR/webauthn-2/#sctn-user-handle-privacy&quot;&gt;should
not contain&lt;/a&gt; any personally identifiable information (PII). You
probably already have a user ID in your system, but you should make one
specifically for passkeys to more easily keep it PII-free. Create a new
column in your &lt;code&gt;users&lt;/code&gt; table and populate it with large
random values for this purpose. (The following is in SQLite syntax so
you’ll need to adjust for other databases.)&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb1&quot;&gt;&lt;pre
class=&quot;sourceCode sql&quot;&gt;&lt;code class=&quot;sourceCode sql&quot;&gt;&lt;span id=&quot;cb1-1&quot;&gt;&lt;a href=&quot;#cb1-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;/* SQLite can&amp;#39;t set a non-constant DEFAULT when altering a table, only&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-2&quot;&gt;&lt;a href=&quot;#cb1-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt; * when creating it, but this is what we would like to write. */&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-3&quot;&gt;&lt;a href=&quot;#cb1-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;TABLE&lt;/span&gt; users &lt;span class=&quot;kw&quot;&gt;ADD&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;COLUMN&lt;/span&gt; passkey_id &lt;span class=&quot;dt&quot;&gt;blob&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;DEFAULT&lt;/span&gt;(randomblob(&lt;span class=&quot;dv&quot;&gt;16&lt;/span&gt;));&lt;/span&gt;
&lt;span id=&quot;cb1-4&quot;&gt;&lt;a href=&quot;#cb1-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-5&quot;&gt;&lt;a href=&quot;#cb1-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;/* The CASE expression causes the function to be non-constant. */&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-6&quot;&gt;&lt;a href=&quot;#cb1-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;UPDATE&lt;/span&gt; USERS &lt;span class=&quot;kw&quot;&gt;SET&lt;/span&gt; passkey_id&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;hex(randomblob(&lt;span class=&quot;cf&quot;&gt;CASE&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;rowid&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-7&quot;&gt;&lt;a href=&quot;#cb1-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;                                                      &lt;span class=&quot;cf&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;16&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-8&quot;&gt;&lt;a href=&quot;#cb1-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;                                                      &lt;span class=&quot;cf&quot;&gt;ELSE&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;16&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;END&lt;/span&gt;));&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A user can only have a single pass&lt;em&gt;word&lt;/em&gt; but can have multiple
passkeys. So create a table for them:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb2&quot;&gt;&lt;pre
class=&quot;sourceCode sql&quot;&gt;&lt;code class=&quot;sourceCode sql&quot;&gt;&lt;span id=&quot;cb2-1&quot;&gt;&lt;a href=&quot;#cb2-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;TABLE&lt;/span&gt; passkeys (&lt;/span&gt;
&lt;span id=&quot;cb2-2&quot;&gt;&lt;a href=&quot;#cb2-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;kw&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;BLOB&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;PRIMARY&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;KEY&lt;/span&gt;,&lt;/span&gt;
&lt;span id=&quot;cb2-3&quot;&gt;&lt;a href=&quot;#cb2-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  username STRING &lt;span class=&quot;kw&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;NULL&lt;/span&gt;,&lt;/span&gt;
&lt;span id=&quot;cb2-4&quot;&gt;&lt;a href=&quot;#cb2-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  public_key_spki &lt;span class=&quot;dt&quot;&gt;BLOB&lt;/span&gt;,&lt;/span&gt;
&lt;span id=&quot;cb2-5&quot;&gt;&lt;a href=&quot;#cb2-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  backed_up &lt;span class=&quot;dt&quot;&gt;BOOLEAN&lt;/span&gt;,&lt;/span&gt;
&lt;span id=&quot;cb2-6&quot;&gt;&lt;a href=&quot;#cb2-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;kw&quot;&gt;FOREIGN&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;KEY&lt;/span&gt;(username) &lt;span class=&quot;kw&quot;&gt;REFERENCES&lt;/span&gt; users(username));&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;secure-contexts&quot;&gt;Secure contexts&lt;/h2&gt;
&lt;p&gt;Nothing in WebAuthn works outside of a &lt;a
href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts&quot;&gt;secure
context&lt;/a&gt;, so if you’re not using HTTPS, go fix that first.&lt;/p&gt;
&lt;h2 id=&quot;enrolling-existing-users&quot;&gt;Enrolling existing users&lt;/h2&gt;
&lt;p&gt;When a user signs in with a password, you might want to prompt them
to create a passkey on the local device for easier sign-in next time.
First, check to see if their device has a local authenticator and that
the browser is going to support passkeys:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb3&quot;&gt;&lt;pre class=&quot;sourceCode js&quot;&gt;&lt;code class=&quot;sourceCode javascript&quot;&gt;&lt;span id=&quot;cb3-1&quot;&gt;&lt;a href=&quot;#cb3-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;op&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;PublicKeyCredential&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;||&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-2&quot;&gt;&lt;a href=&quot;#cb3-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;!&lt;/span&gt;(PublicKeyCredential &lt;span class=&quot;im&quot;&gt;as&lt;/span&gt; any)&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;isConditionalMediationAvailable&lt;/span&gt;) {&lt;/span&gt;
&lt;span id=&quot;cb3-3&quot;&gt;&lt;a href=&quot;#cb3-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-4&quot;&gt;&lt;a href=&quot;#cb3-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;}&lt;/span&gt;
&lt;span id=&quot;cb3-5&quot;&gt;&lt;a href=&quot;#cb3-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-6&quot;&gt;&lt;a href=&quot;#cb3-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;all&lt;/span&gt;([&lt;/span&gt;
&lt;span id=&quot;cb3-7&quot;&gt;&lt;a href=&quot;#cb3-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    (PublicKeyCredential &lt;span class=&quot;im&quot;&gt;as&lt;/span&gt; any)&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isConditionalMediationAvailable&lt;/span&gt;()&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-8&quot;&gt;&lt;a href=&quot;#cb3-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    PublicKeyCredential&lt;/span&gt;
&lt;span id=&quot;cb3-9&quot;&gt;&lt;a href=&quot;#cb3-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isUserVerifyingPlatformAuthenticatorAvailable&lt;/span&gt;()])&lt;/span&gt;
&lt;span id=&quot;cb3-10&quot;&gt;&lt;a href=&quot;#cb3-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;then&lt;/span&gt;((values) &lt;span class=&quot;kw&quot;&gt;=&amp;gt;&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb3-11&quot;&gt;&lt;a href=&quot;#cb3-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; (values&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;every&lt;/span&gt;(x &lt;span class=&quot;kw&quot;&gt;=&amp;gt;&lt;/span&gt; x &lt;span class=&quot;op&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;true&lt;/span&gt;)) {&lt;/span&gt;
&lt;span id=&quot;cb3-12&quot;&gt;&lt;a href=&quot;#cb3-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;fu&quot;&gt;promptUserToCreatePlatformCredential&lt;/span&gt;()&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-13&quot;&gt;&lt;a href=&quot;#cb3-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    }&lt;/span&gt;
&lt;span id=&quot;cb3-14&quot;&gt;&lt;a href=&quot;#cb3-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  })&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(The snippets here are in TypeScript. It should be easy to convert
them to plain Javascript if that’s what you need. You might notice
several places where TypeScript’s DOM types are getting overridden
because lib.dom.d.ts hasn’t caught up. I hope these cases will disappear
in time.)&lt;/p&gt;
&lt;p&gt;If the user accepts, ask the browser to create a local
credential:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb4&quot;&gt;&lt;pre class=&quot;sourceCode js&quot;&gt;&lt;code class=&quot;sourceCode javascript&quot;&gt;&lt;span id=&quot;cb4-1&quot;&gt;&lt;a href=&quot;#cb4-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;var&lt;/span&gt; createOptions &lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; CredentialCreationOptions &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb4-2&quot;&gt;&lt;a href=&quot;#cb4-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;dt&quot;&gt;publicKey&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb4-3&quot;&gt;&lt;a href=&quot;#cb4-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;rp&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb4-4&quot;&gt;&lt;a href=&quot;#cb4-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;co&quot;&gt;// The RP ID. This needs some thought. See comments below.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-5&quot;&gt;&lt;a href=&quot;#cb4-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; SEE_BELOW&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-6&quot;&gt;&lt;a href=&quot;#cb4-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;co&quot;&gt;// This field is required to be set to something, but you can&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-7&quot;&gt;&lt;a href=&quot;#cb4-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;co&quot;&gt;// ignore it.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-8&quot;&gt;&lt;a href=&quot;#cb4-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-9&quot;&gt;&lt;a href=&quot;#cb4-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    }&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-10&quot;&gt;&lt;a href=&quot;#cb4-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-11&quot;&gt;&lt;a href=&quot;#cb4-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb4-12&quot;&gt;&lt;a href=&quot;#cb4-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;co&quot;&gt;// `userIdBase64` is the user&amp;#39;s passkey ID, from the database,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-13&quot;&gt;&lt;a href=&quot;#cb4-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;co&quot;&gt;// base64-encoded.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-14&quot;&gt;&lt;a href=&quot;#cb4-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;from&lt;/span&gt;(&lt;span class=&quot;fu&quot;&gt;atob&lt;/span&gt;(userIdBase64)&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; c &lt;span class=&quot;kw&quot;&gt;=&amp;gt;&lt;/span&gt; c&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;charCodeAt&lt;/span&gt;(&lt;span class=&quot;dv&quot;&gt;0&lt;/span&gt;))&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-15&quot;&gt;&lt;a href=&quot;#cb4-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;co&quot;&gt;// `username` is the user&amp;#39;s username. Whatever they would type&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-16&quot;&gt;&lt;a href=&quot;#cb4-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;co&quot;&gt;// when signing in with a password.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-17&quot;&gt;&lt;a href=&quot;#cb4-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; username&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-18&quot;&gt;&lt;a href=&quot;#cb4-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;co&quot;&gt;// `displayName` can be a more human name for the user, or&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-19&quot;&gt;&lt;a href=&quot;#cb4-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;co&quot;&gt;// just leave it blank.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-20&quot;&gt;&lt;a href=&quot;#cb4-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;displayName&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-21&quot;&gt;&lt;a href=&quot;#cb4-21&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    }&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-22&quot;&gt;&lt;a href=&quot;#cb4-22&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-23&quot;&gt;&lt;a href=&quot;#cb4-23&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// This lists the ids of the user&amp;#39;s existing credentials. I.e.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-24&quot;&gt;&lt;a href=&quot;#cb4-24&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;//   SELECT id FROM passkeys WHERE username = ?&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-25&quot;&gt;&lt;a href=&quot;#cb4-25&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// and supply the resulting list of values, base64-encoded, as&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-26&quot;&gt;&lt;a href=&quot;#cb4-26&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// existingCredentialIdsBase64 here.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-27&quot;&gt;&lt;a href=&quot;#cb4-27&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;excludeCredentials&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; existingCredentialIdsBase64&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;map&lt;/span&gt;(id &lt;span class=&quot;kw&quot;&gt;=&amp;gt;&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb4-28&quot;&gt;&lt;a href=&quot;#cb4-28&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb4-29&quot;&gt;&lt;a href=&quot;#cb4-29&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;dt&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;public-key&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-30&quot;&gt;&lt;a href=&quot;#cb4-30&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;dt&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;from&lt;/span&gt;(&lt;span class=&quot;fu&quot;&gt;atob&lt;/span&gt;(id)&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; c &lt;span class=&quot;kw&quot;&gt;=&amp;gt;&lt;/span&gt; c&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;charCodeAt&lt;/span&gt;(&lt;span class=&quot;dv&quot;&gt;0&lt;/span&gt;))&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-31&quot;&gt;&lt;a href=&quot;#cb4-31&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      }&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-32&quot;&gt;&lt;a href=&quot;#cb4-32&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    })&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-33&quot;&gt;&lt;a href=&quot;#cb4-33&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-34&quot;&gt;&lt;a href=&quot;#cb4-34&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// Boilerplate that advertises support for P-256 ECDSA and RSA&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-35&quot;&gt;&lt;a href=&quot;#cb4-35&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// PKCS#1v1.5. Supporting these key types results in universal&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-36&quot;&gt;&lt;a href=&quot;#cb4-36&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// coverage so far.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-37&quot;&gt;&lt;a href=&quot;#cb4-37&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;pubKeyCredParams&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; [{&lt;/span&gt;
&lt;span id=&quot;cb4-38&quot;&gt;&lt;a href=&quot;#cb4-38&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;public-key&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-39&quot;&gt;&lt;a href=&quot;#cb4-39&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;alg&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;7&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-40&quot;&gt;&lt;a href=&quot;#cb4-40&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    }&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb4-41&quot;&gt;&lt;a href=&quot;#cb4-41&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;public-key&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-42&quot;&gt;&lt;a href=&quot;#cb4-42&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;alg&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;257&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-43&quot;&gt;&lt;a href=&quot;#cb4-43&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    }]&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-44&quot;&gt;&lt;a href=&quot;#cb4-44&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-45&quot;&gt;&lt;a href=&quot;#cb4-45&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// Unused during registrations, except in some enterprise&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-46&quot;&gt;&lt;a href=&quot;#cb4-46&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// deployments. But don&amp;#39;t do this during sign-in!&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-47&quot;&gt;&lt;a href=&quot;#cb4-47&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;challenge&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Uint8Array&lt;/span&gt;([&lt;span class=&quot;dv&quot;&gt;0&lt;/span&gt;])&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-48&quot;&gt;&lt;a href=&quot;#cb4-48&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-49&quot;&gt;&lt;a href=&quot;#cb4-49&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;authenticatorSelection&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb4-50&quot;&gt;&lt;a href=&quot;#cb4-50&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;authenticatorAttachment&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;platform&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-51&quot;&gt;&lt;a href=&quot;#cb4-51&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;requireResidentKey&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-52&quot;&gt;&lt;a href=&quot;#cb4-52&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    }&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-53&quot;&gt;&lt;a href=&quot;#cb4-53&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-54&quot;&gt;&lt;a href=&quot;#cb4-54&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// Three minutes.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-55&quot;&gt;&lt;a href=&quot;#cb4-55&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;180000&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-56&quot;&gt;&lt;a href=&quot;#cb4-56&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  }&lt;/span&gt;
&lt;span id=&quot;cb4-57&quot;&gt;&lt;a href=&quot;#cb4-57&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;}&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-58&quot;&gt;&lt;a href=&quot;#cb4-58&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-59&quot;&gt;&lt;a href=&quot;#cb4-59&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;credentials&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;create&lt;/span&gt;(createOptions)&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;then&lt;/span&gt;(&lt;/span&gt;
&lt;span id=&quot;cb4-60&quot;&gt;&lt;a href=&quot;#cb4-60&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  handleCreation&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; handleCreationError)&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;rp-ids&quot;&gt;RP IDs&lt;/h3&gt;
&lt;p&gt;There are two levels of controls that prevent passkeys from being
used on the wrong website. You need to know about this upfront to
prevent getting stuck later.&lt;/p&gt;
&lt;p&gt;“RP” stands for “relying party”. You (the website) are a “relying
party” in authentication-speak. An RP ID is a domain name and every
passkey has one that’s fixed at creation time. Every passkey operation
asserts an RP ID and, if a passkey’s RP ID doesn’t match, then it
doesn’t exist for that operation.&lt;/p&gt;
&lt;p&gt;This prevents one site from using another’s passkeys. A passkey with
an RP ID of &lt;tt&gt;foo.com&lt;/tt&gt; can’t be used on &lt;tt&gt;bar.com&lt;/tt&gt; because
&lt;tt&gt;bar.com&lt;/tt&gt; can’t assert an RP ID of &lt;tt&gt;foo.com&lt;/tt&gt;. A site may
use any RP ID formed by discarding zero or more labels from the left of
its domain name until it hits an eTLD. So say that you’re
&lt;tt&gt;https://www.foo.co.uk&lt;/tt&gt;: you can assert &lt;tt&gt;www.foo.co.uk&lt;/tt&gt;
(discarding zero labels), &lt;tt&gt;foo.co.uk&lt;/tt&gt; (discarding one label), but
not &lt;tt&gt;co.uk&lt;/tt&gt; because that hits an eTLD. If you don’t set an RP ID
in a request then the default is the site’s full domain.&lt;/p&gt;
&lt;p&gt;Our &lt;tt&gt;www.foo.co.uk&lt;/tt&gt; example might happily be creating passkeys
with the default RP ID but later decide that it wants to move all
sign-in activity to an isolated origin,
&lt;tt&gt;https://accounts.foo.co.uk&lt;/tt&gt;. But none of the passkeys could be
used from that origin! If would have needed to create them with an RP ID
of &lt;tt&gt;foo.co.uk&lt;/tt&gt; in the first place to allow that.&lt;/p&gt;
&lt;p&gt;But you might want to be careful about always setting the most
general RP ID because then &lt;tt&gt;usercontent.foo.co.uk&lt;/tt&gt; could access
and overwrite them too. That brings us to the second control mechanism.
As you’ll see later, when a passkey is used to sign in, the browser
includes the origin that made the request in the signed data. So
&lt;tt&gt;accounts.foo.co.uk&lt;/tt&gt; would be able to see that a request was
triggered by &lt;tt&gt;usercontent.foo.co.uk&lt;/tt&gt; and reject it, even if the
passkey’s RP ID allowed &lt;tt&gt;usercontent.foo.co.uk&lt;/tt&gt; to use it. But
that mechanism can’t do anything about &lt;tt&gt;usercontent.foo.co.uk&lt;/tt&gt;
being able to overwrite them.&lt;/p&gt;
&lt;p&gt;So either pick an RP ID and put it in the “SEE BELOW” placeholder,
above. Or else don’t include the &lt;tt&gt;rp.id&lt;/tt&gt; field at all and use the
default.&lt;/p&gt;
&lt;h3 id=&quot;recording-a-passkey&quot;&gt;Recording a passkey&lt;/h3&gt;
&lt;p&gt;When the promise from &lt;code&gt;navigator.credentials.create&lt;/code&gt;
resolves successfully, you have a newly created passkey! Now you have to
ensure that it gets recorded by the server.&lt;/p&gt;
&lt;p&gt;The promise will result in a &lt;a
href=&quot;https://www.w3.org/TR/webauthn-2/#iface-pkcredential&quot;&gt;&lt;code&gt;PublicKeyCredential&lt;/code&gt;&lt;/a&gt;
object, the &lt;code&gt;response&lt;/code&gt; field of which is an &lt;a
href=&quot;https://www.w3.org/TR/webauthn-2/#authenticatorattestationresponse&quot;&gt;&lt;code&gt;AuthenticatorAttestationResponse&lt;/code&gt;&lt;/a&gt;.
First, sanity check some data from the browser. Since this data isn’t
signed over in the configuration that we’re using, it’s fine to do this
check client-side.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb5&quot;&gt;&lt;pre class=&quot;sourceCode js&quot;&gt;&lt;code class=&quot;sourceCode javascript&quot;&gt;&lt;span id=&quot;cb5-1&quot;&gt;&lt;a href=&quot;#cb5-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;const&lt;/span&gt; cdj &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;parse&lt;/span&gt;(&lt;/span&gt;
&lt;span id=&quot;cb5-2&quot;&gt;&lt;a href=&quot;#cb5-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;TextDecoder&lt;/span&gt;()&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;decode&lt;/span&gt;(cred&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;clientDataJSON&lt;/span&gt;))&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-3&quot;&gt;&lt;a href=&quot;#cb5-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; (cdj&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;webauthn.create&amp;#39;&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;||&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-4&quot;&gt;&lt;a href=&quot;#cb5-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    ((&lt;span class=&quot;st&quot;&gt;&amp;#39;crossOrigin&amp;#39;&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;in&lt;/span&gt; cdj) &lt;span class=&quot;op&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; cdj&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;crossOrigin&lt;/span&gt;) &lt;span class=&quot;op&quot;&gt;||&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-5&quot;&gt;&lt;a href=&quot;#cb5-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    cdj&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;origin&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;https://YOURSITEHERE&amp;#39;&lt;/span&gt;) {&lt;/span&gt;
&lt;span id=&quot;cb5-6&quot;&gt;&lt;a href=&quot;#cb5-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;co&quot;&gt;// handle error&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-7&quot;&gt;&lt;a href=&quot;#cb5-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Call &lt;code&gt;getAuthenticatorData()&lt;/code&gt; and
&lt;code&gt;getPublicKey()&lt;/code&gt; on &lt;code&gt;response&lt;/code&gt; and send those
ArrayBuffers to the server.&lt;/p&gt;
&lt;p&gt;At the server, we want to insert a row into the &lt;code&gt;passkeys&lt;/code&gt;
table for this user. The &lt;a
href=&quot;https://w3c.github.io/webauthn/#authenticator-data&quot;&gt;authenticator
data&lt;/a&gt; is a fairly simple, binary format. Offset 32 contains the flags
byte. Sanity check that bit 7 is set and then extract:&lt;/p&gt;
&lt;ol type=&quot;1&quot;&gt;
&lt;li&gt;Bit 4 as the value of &lt;code&gt;backed_up&lt;/code&gt;. (I.e.
&lt;code&gt;(authData[32] &amp;gt;&amp;gt; 4) &amp;amp; 1&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;The big-endian, uint16 at offset 53 as the length of the credential
ID.&lt;/li&gt;
&lt;li&gt;That many bytes from offset 55 as the value of &lt;code&gt;id&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The ArrayBuffer that came from &lt;code&gt;getPublicKey()&lt;/code&gt; is the
value for &lt;code&gt;public_key_spki&lt;/code&gt;. That should be all the values
needed to insert the row.&lt;/p&gt;
&lt;h3 id=&quot;handling-a-registration-exception&quot;&gt;Handling a registration
exception&lt;/h3&gt;
&lt;p&gt;The promise from &lt;code&gt;create()&lt;/code&gt; might also result in an
exception. &lt;code&gt;InvalidStateError&lt;/code&gt; is special and means that a
passkey already exists for the local device. This is not an error, and
no error will have been shown to the user. They’ll have seen a UI just
like they were registering a passkey but the server doesn’t need to
update anything.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NotAllowedError&lt;/code&gt; means that the user canceled the
operation. Other exceptions mean that something more unexpected
happened.&lt;/p&gt;
&lt;p&gt;To test whether an exception is one of these values do something
like:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb6&quot;&gt;&lt;pre class=&quot;sourceCode js&quot;&gt;&lt;code class=&quot;sourceCode javascript&quot;&gt;&lt;span id=&quot;cb6-1&quot;&gt;&lt;a href=&quot;#cb6-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;handleCreationError&lt;/span&gt;(e&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Error&lt;/span&gt;) {&lt;/span&gt;
&lt;span id=&quot;cb6-2&quot;&gt;&lt;a href=&quot;#cb6-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; (e &lt;span class=&quot;kw&quot;&gt;instanceof&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;DOMException&lt;/span&gt;) {&lt;/span&gt;
&lt;span id=&quot;cb6-3&quot;&gt;&lt;a href=&quot;#cb6-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;switch&lt;/span&gt; (e&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;name&lt;/span&gt;) {&lt;/span&gt;
&lt;span id=&quot;cb6-4&quot;&gt;&lt;a href=&quot;#cb6-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;cf&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;InvalidStateError&amp;#39;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-5&quot;&gt;&lt;a href=&quot;#cb6-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;bu&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;st&quot;&gt;&amp;#39;InvalidStateError&amp;#39;&lt;/span&gt;)&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-6&quot;&gt;&lt;a href=&quot;#cb6-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-7&quot;&gt;&lt;a href=&quot;#cb6-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-8&quot;&gt;&lt;a href=&quot;#cb6-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;cf&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;NotAllowedError&amp;#39;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-9&quot;&gt;&lt;a href=&quot;#cb6-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;bu&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;st&quot;&gt;&amp;#39;NotAllowedError&amp;#39;&lt;/span&gt;)&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-10&quot;&gt;&lt;a href=&quot;#cb6-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-11&quot;&gt;&lt;a href=&quot;#cb6-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    }&lt;/span&gt;
&lt;span id=&quot;cb6-12&quot;&gt;&lt;a href=&quot;#cb6-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  }&lt;/span&gt;
&lt;span id=&quot;cb6-13&quot;&gt;&lt;a href=&quot;#cb6-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-14&quot;&gt;&lt;a href=&quot;#cb6-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;bu&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;log&lt;/span&gt;(e)&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-15&quot;&gt;&lt;a href=&quot;#cb6-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(But obviously don’t just log them to the console in real code.)&lt;/p&gt;
&lt;h2 id=&quot;signing-in-with-autocomplete&quot;&gt;Signing in with autocomplete&lt;/h2&gt;
&lt;p&gt;Somewhere on your site you have username &amp;amp; password inputs. On
the username &lt;code&gt;input&lt;/code&gt; element, add &lt;code&gt;webauthn&lt;/code&gt; to
the &lt;code&gt;autocomplete&lt;/code&gt; attribute. So if you have:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;username&amp;quot; autocomplete=&amp;quot;username&amp;quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;… then change that to …&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&amp;quot;text&amp;quot; name=&amp;quot;username&amp;quot; autocomplete=&amp;quot;username webauthn&amp;quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Autocomplete for passkeys works differently than for passwords. For
the latter, when the user selects a username &amp;amp; password from the
pop-up, the input fields are filled for them. Then they can click a
button to submit the form and sign in. With passkeys, no fields are
filled, but rather a pending &lt;a
href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise&quot;&gt;promise&lt;/a&gt;
is resolved. It’s then the &lt;em&gt;site’s responsibility&lt;/em&gt; to
navigate/update the page so that the user is signed in.&lt;/p&gt;
&lt;p&gt;That pending promise must be set up by the site before the user
focuses the username field and triggers autocomplete. (Just adding the
&lt;code&gt;webauthn&lt;/code&gt; tag doesn’t do anything if there’s not a pending
promise for the browser to resolve.) To create it, run a function at
page load that:&lt;/p&gt;
&lt;ol type=&quot;1&quot;&gt;
&lt;li&gt;Does feature detection and, if supported,&lt;/li&gt;
&lt;li&gt;Starts a “conditional” WebAuthn request to produce the promise that
will be resolved if the user selects a credential.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s how to do the feature detection:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb9&quot;&gt;&lt;pre class=&quot;sourceCode js&quot;&gt;&lt;code class=&quot;sourceCode javascript&quot;&gt;&lt;span id=&quot;cb9-1&quot;&gt;&lt;a href=&quot;#cb9-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;op&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;PublicKeyCredential&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;||&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-2&quot;&gt;&lt;a href=&quot;#cb9-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;!&lt;/span&gt;(PublicKeyCredential &lt;span class=&quot;im&quot;&gt;as&lt;/span&gt; any)&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;isConditionalMediationAvailable&lt;/span&gt;) {&lt;/span&gt;
&lt;span id=&quot;cb9-3&quot;&gt;&lt;a href=&quot;#cb9-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-4&quot;&gt;&lt;a href=&quot;#cb9-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;}&lt;/span&gt;
&lt;span id=&quot;cb9-5&quot;&gt;&lt;a href=&quot;#cb9-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-6&quot;&gt;&lt;a href=&quot;#cb9-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;(PublicKeyCredential &lt;span class=&quot;im&quot;&gt;as&lt;/span&gt; any)&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isConditionalMediationAvailable&lt;/span&gt;()&lt;/span&gt;
&lt;span id=&quot;cb9-7&quot;&gt;&lt;a href=&quot;#cb9-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;then&lt;/span&gt;((result&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; boolean) &lt;span class=&quot;kw&quot;&gt;=&amp;gt;&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb9-8&quot;&gt;&lt;a href=&quot;#cb9-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;op&quot;&gt;!&lt;/span&gt;result) {&lt;/span&gt;
&lt;span id=&quot;cb9-9&quot;&gt;&lt;a href=&quot;#cb9-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-10&quot;&gt;&lt;a href=&quot;#cb9-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    }&lt;/span&gt;
&lt;span id=&quot;cb9-11&quot;&gt;&lt;a href=&quot;#cb9-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-12&quot;&gt;&lt;a href=&quot;#cb9-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;startConditionalRequest&lt;/span&gt;()&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-13&quot;&gt;&lt;a href=&quot;#cb9-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  })&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then, to start the conditional request:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb10&quot;&gt;&lt;pre
class=&quot;sourceCode js&quot;&gt;&lt;code class=&quot;sourceCode javascript&quot;&gt;&lt;span id=&quot;cb10-1&quot;&gt;&lt;a href=&quot;#cb10-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;var&lt;/span&gt; getOptions &lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; CredentialRequestOptions &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb10-2&quot;&gt;&lt;a href=&quot;#cb10-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;co&quot;&gt;// This is the critical option that tells the browser not to show&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-3&quot;&gt;&lt;a href=&quot;#cb10-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;co&quot;&gt;// modal UI.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-4&quot;&gt;&lt;a href=&quot;#cb10-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;dt&quot;&gt;mediation&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;conditional&amp;quot;&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;as&lt;/span&gt; CredentialMediationRequirement&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-5&quot;&gt;&lt;a href=&quot;#cb10-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-6&quot;&gt;&lt;a href=&quot;#cb10-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;dt&quot;&gt;publicKey&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; {&lt;/span&gt;
&lt;span id=&quot;cb10-7&quot;&gt;&lt;a href=&quot;#cb10-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;challenge&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;from&lt;/span&gt;(&lt;span class=&quot;fu&quot;&gt;atob&lt;/span&gt;(CHALLENGE_SEE_BELOW)&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; c &lt;span class=&quot;kw&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-8&quot;&gt;&lt;a href=&quot;#cb10-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;                                 c&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;charCodeAt&lt;/span&gt;(&lt;span class=&quot;dv&quot;&gt;0&lt;/span&gt;))&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-9&quot;&gt;&lt;a href=&quot;#cb10-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-10&quot;&gt;&lt;a href=&quot;#cb10-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;rpId&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; SAME_AS_YOU_USED_FOR_REGISTRATION&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-11&quot;&gt;&lt;a href=&quot;#cb10-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  }&lt;/span&gt;
&lt;span id=&quot;cb10-12&quot;&gt;&lt;a href=&quot;#cb10-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;}&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-13&quot;&gt;&lt;a href=&quot;#cb10-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-14&quot;&gt;&lt;a href=&quot;#cb10-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;credentials&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;(getOptions)&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;then&lt;/span&gt;(&lt;/span&gt;
&lt;span id=&quot;cb10-15&quot;&gt;&lt;a href=&quot;#cb10-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  handleSignIn&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; handleSignInError)&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;challenges&quot;&gt;Challenges&lt;/h3&gt;
&lt;p&gt;Challenges are random values, generated by the server, that are
signed over when using a passkey. Because they are large random values,
the server knows that the signature must have been generated after it
generated the challenge. This stops “replay” attacks where a signature
is captured and used multiple times.&lt;/p&gt;
&lt;p&gt;Challenges are a little like a &lt;a
href=&quot;https://portswigger.net/web-security/csrf/tokens&quot;&gt;CSRF token&lt;/a&gt;:
they should be large (16- or 32-byte), cryptographically-random values
and stored in the session object. They should only be used once: when a
sign-in attempt is received, the challenge should be invalidated. Future
sign-in attempts will have to use a fresh challenge.&lt;/p&gt;
&lt;p&gt;The snippet above has a value &lt;code&gt;CHALLENGE_SEE_BELOW&lt;/code&gt; which
is assumed to be the base64-encoded challenge for the sign-in. The
sign-in page might &lt;a
href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest&quot;&gt;XHR&lt;/a&gt;
to get the challenge, or the challenge might be injected into the page’s
template. Either way, it must be generated at the server!&lt;/p&gt;
&lt;h3 id=&quot;handling-sign-in&quot;&gt;Handling sign-in&lt;/h3&gt;
&lt;p&gt;If the user selects a passkey then &lt;code&gt;handle­Sign­In&lt;/code&gt; will
be called with a &lt;a
href=&quot;https://www.w3.org/TR/webauthn-2/#iface-pkcredential&quot;&gt;&lt;code&gt;Public­Key­Credential&lt;/code&gt;&lt;/a&gt;
object, the &lt;code&gt;response&lt;/code&gt; field of which is a &lt;a
href=&quot;https://www.w3.org/TR/webauthn-2/#authenticatorassertionresponse&quot;&gt;&lt;code&gt;Authenticator­Assertion­Response&lt;/code&gt;&lt;/a&gt;.
Send the Array­Buffers &lt;code&gt;raw­Id&lt;/code&gt;,
&lt;code&gt;response.­client­Data­JSON&lt;/code&gt;,
&lt;code&gt;response.­authenticator­Data&lt;/code&gt;, and
&lt;code&gt;response.­signature&lt;/code&gt; to the server.&lt;/p&gt;
&lt;p&gt;At the server, first look up the passkey:
&lt;code&gt;SELECT (username, public_key_spki, backed_up) FROM passkey WHERE id = ?&lt;/code&gt;
and give the value of &lt;code&gt;rawId&lt;/code&gt; for matching. The
&lt;code&gt;id&lt;/code&gt; column is a primary key, so there can either be zero or
one matching rows. If there are zero rows then the user is signing in
with a passkey that the server doesn’t know about—perhaps they deleted
it. This is an error, reject the sign-in.&lt;/p&gt;
&lt;p&gt;Otherwise, the server now knows the claimed username and public key.
To validate the signature you’ll need to construct the signed data and
parse the public key. The &lt;code&gt;public_key_spki&lt;/code&gt; values from the
database are stored in &lt;a
href=&quot;https://datatracker.ietf.org/doc/html/rfc5280#section-4.1&quot;&gt;SubjectPublicKeyInfo&lt;/a&gt;
format and most languages will have some way to ingest them. Here are
some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Java&lt;/em&gt;: java.security.spec.X509EncodedKeySpec&lt;/li&gt;
&lt;li&gt;&lt;em&gt;.NET&lt;/em&gt;:
System.Security.Cryptography.ECDsa.ImportSubjectPublicKeyInfo&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Go&lt;/em&gt;: crypto/x509.ParsePKIXPublicKey&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your languages’s crypto library should provide a function that takes
a signature and some signed data and tells you whether that signature is
valid for a given public key. For the signature, pass in the value of
the &lt;code&gt;signature&lt;/code&gt; ArrayBuffer that the client sent. For the
signed data, calculate the SHA-256 hash of &lt;code&gt;clientDataJSON&lt;/code&gt;
and append it to the contents of &lt;code&gt;authenticatorData&lt;/code&gt;. If the
signature isn’t valid, reject the sign-in.&lt;/p&gt;
&lt;p&gt;But there are still a bunch of things that you need to check!&lt;/p&gt;
&lt;p&gt;Parse the &lt;code&gt;clientDataJSON&lt;/code&gt; as UTF-8 JSON and check
that:&lt;/p&gt;
&lt;ol type=&quot;1&quot;&gt;
&lt;li&gt;The &lt;code&gt;type&lt;/code&gt; member is “webauthn.get”.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;challenge&lt;/code&gt; member is equal to the base64url encoding
of the challenge that the server gave for this sign-in.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;origin&lt;/code&gt; member is equal to your site’s sign-in
origin (e.g. a string like “https://www.example.com”).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;crossOrigin&lt;/code&gt; member, if present, is false.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There’s more! Take the &lt;code&gt;authenticatorData&lt;/code&gt; and check
that:&lt;/p&gt;
&lt;ol type=&quot;1&quot;&gt;
&lt;li&gt;The first 32 bytes are equal to the SHA-256 hash of the RP ID that
you’re using.&lt;/li&gt;
&lt;li&gt;That bit zero of the byte at offset 32 is one. I.e.
&lt;code&gt;(authData[32] &amp;amp; 1) == 1&lt;/code&gt;. This is the &lt;a
href=&quot;https://www.w3.org/TR/webauthn-2/#test-of-user-presence&quot;&gt;user
presence bit&lt;/a&gt; that indicates that a user approved the signature.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If all those checks work out then sign in the user whose passkey it
was. I.e. set a cookie and respond to the running Javascript so that it
can update the page.&lt;/p&gt;
&lt;p&gt;If the stored value of &lt;code&gt;backed_up&lt;/code&gt; is not equal to
&lt;code&gt;(authData[32] &amp;gt;&amp;gt; 4) &amp;amp; 1&lt;/code&gt; then update that in the
database.&lt;/p&gt;
&lt;h2 id=&quot;removing-passwords&quot;&gt;Removing passwords&lt;/h2&gt;
&lt;p&gt;Once a user is using passkeys to sign in, great! But if they were
upgraded from a password then that password is hanging around on the
account, doing nothing useful yet creating risk. It would be good to ask
the user about removing the password.&lt;/p&gt;
&lt;p&gt;Doing this is reasonable if the account has a backed-up passkey. I.e.
if
&lt;code&gt;SELECT 1 FROM passkeys WHERE username = ? AND backed_up = TRUE&lt;/code&gt;
has results. A site might consider prompting the user to remove the
password on an account when they sign in with a passkey and have a
backed-up one registered.&lt;/p&gt;
&lt;h2 id=&quot;registering-new-passkey-only-users&quot;&gt;Registering new,
passkey-only users&lt;/h2&gt;
&lt;p&gt;For sign ups of new users, consider making them passkey-only if the
feature detection (from the section on enrolling users) is happy.&lt;/p&gt;
&lt;p&gt;When enrolling users where a passkey will be their only sign-in
method you really want the passkey to end up “in their pocket”, i.e. on
their phone. Otherwise they could have a passkey on the computer that
they signed-up with but, if it’s not syncing to their phone, that’s not
very convenient. There is not, currently, a great answer for this I’m
afraid! Hopefully, in a few months, calling
&lt;code&gt;navigator­.credentials.­create()&lt;/code&gt; with
&lt;code&gt;authenticator­Selection.­authenticator­Attachment&lt;/code&gt; set to
&lt;code&gt;cross-plat­form&lt;/code&gt; will do the right thing. But with iOS 16
it’ll exclude the platform authenticator.&lt;/p&gt;
&lt;p&gt;So, for now, do that on all platforms except for iOS/iPadOS, where
&lt;code&gt;authenticator­Attachment&lt;/code&gt; should continue to be
&lt;code&gt;plat­form&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;(I’ll try and update this section when the answer is simplier!)&lt;/p&gt;
&lt;h2 id=&quot;settings&quot;&gt;Settings&lt;/h2&gt;
&lt;p&gt;If you’ve used security keys with any sites then you’ll have noticed
that they tend to list registered security keys in their account
settings, have users name each one, show the last-used time, and let
them be individually removed. You can do that with passkeys too if you
like, but it’s quite a lot of complexity. Instead, I think you can have
just two buttons:&lt;/p&gt;
&lt;p&gt;First, a button to add a passkey that uses the
&lt;code&gt;createOptions&lt;/code&gt; object from above, but with
&lt;code&gt;authenticatorAttachment&lt;/code&gt; deleted in order to allow other
devices to be registered.&lt;/p&gt;
&lt;p&gt;Second, a “reset passkeys” button (like a “reset password” button).
It would prompt for a new passkey registration, delete all other
passkeys, and invalidate all other active sessions for the user.&lt;/p&gt;
&lt;h2 id=&quot;test-vectors&quot;&gt;Test vectors&lt;/h2&gt;
&lt;p&gt;Connecting up to your language’s crypto libraries is one of the
trickier parts of this. To help, here are some test vectors to give you
a ground truth to check against, in the format of Python 3 code that
checks an assertion signature.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb11&quot;&gt;&lt;pre
class=&quot;sourceCode py&quot;&gt;&lt;code class=&quot;sourceCode python&quot;&gt;&lt;span id=&quot;cb11-1&quot;&gt;&lt;a href=&quot;#cb11-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;im&quot;&gt;import&lt;/span&gt; codecs&lt;/span&gt;
&lt;span id=&quot;cb11-2&quot;&gt;&lt;a href=&quot;#cb11-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-3&quot;&gt;&lt;a href=&quot;#cb11-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;im&quot;&gt;from&lt;/span&gt; cryptography.hazmat.primitives.asymmetric &lt;span class=&quot;im&quot;&gt;import&lt;/span&gt; ec&lt;/span&gt;
&lt;span id=&quot;cb11-4&quot;&gt;&lt;a href=&quot;#cb11-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;im&quot;&gt;from&lt;/span&gt; cryptography.hazmat.primitives &lt;span class=&quot;im&quot;&gt;import&lt;/span&gt; hashes&lt;/span&gt;
&lt;span id=&quot;cb11-5&quot;&gt;&lt;a href=&quot;#cb11-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;im&quot;&gt;from&lt;/span&gt; cryptography.hazmat.primitives.serialization &lt;span class=&quot;im&quot;&gt;import&lt;/span&gt; (&lt;/span&gt;
&lt;span id=&quot;cb11-6&quot;&gt;&lt;a href=&quot;#cb11-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;  load_der_public_key)&lt;/span&gt;
&lt;span id=&quot;cb11-7&quot;&gt;&lt;a href=&quot;#cb11-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-8&quot;&gt;&lt;a href=&quot;#cb11-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# This is the public key in SPKI format, as obtained from the&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-9&quot;&gt;&lt;a href=&quot;#cb11-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# `getPublicKey` call at registration time.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-10&quot;&gt;&lt;a href=&quot;#cb11-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;public_key_spki_hex &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-11&quot;&gt;&lt;a href=&quot;#cb11-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;3059301306072a8648ce3d020106082a8648ce3d03010703420004dfacc605c6&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-12&quot;&gt;&lt;a href=&quot;#cb11-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;e1192f4ab89671edff7dff80c8d5e2d4d44fa284b8d1453fe34ccc5742e48286&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-13&quot;&gt;&lt;a href=&quot;#cb11-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;d39ec681f46e3f38fe127ce27c30941252430bd373b0a12b3e94c8&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-14&quot;&gt;&lt;a href=&quot;#cb11-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-15&quot;&gt;&lt;a href=&quot;#cb11-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-16&quot;&gt;&lt;a href=&quot;#cb11-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# This is the contents of the `clientDataJSON` field at assertion&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-17&quot;&gt;&lt;a href=&quot;#cb11-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# time. This is UTF-8 JSON that you also need to validate in several&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-18&quot;&gt;&lt;a href=&quot;#cb11-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# ways; see the main body of the text.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-19&quot;&gt;&lt;a href=&quot;#cb11-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;client_data_json_hex &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-20&quot;&gt;&lt;a href=&quot;#cb11-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;7b2274797065223a22776562617574686e2e676574222c226368616c6c656e67&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-21&quot;&gt;&lt;a href=&quot;#cb11-21&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;65223a22594934476c4170525f6653654b4d455a444e36326d74624a73345878&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-22&quot;&gt;&lt;a href=&quot;#cb11-22&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;47316e6f757642445a483664436141222c226f726967696e223a226874747073&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-23&quot;&gt;&lt;a href=&quot;#cb11-23&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;3a2f2f73656375726974796b6579732e696e666f222c2263726f73734f726967&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-24&quot;&gt;&lt;a href=&quot;#cb11-24&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;696e223a66616c73657d&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-25&quot;&gt;&lt;a href=&quot;#cb11-25&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-26&quot;&gt;&lt;a href=&quot;#cb11-26&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-27&quot;&gt;&lt;a href=&quot;#cb11-27&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# This is the `authenticatorData` field at assertion time. You also&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-28&quot;&gt;&lt;a href=&quot;#cb11-28&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# need to validate this in several ways; see the main body of the&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-29&quot;&gt;&lt;a href=&quot;#cb11-29&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# text.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-30&quot;&gt;&lt;a href=&quot;#cb11-30&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;authenticator_data_hex &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-31&quot;&gt;&lt;a href=&quot;#cb11-31&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;26bd7278be463761f1faa1b10ab4c4f82670269c410c726a1fd6e05855e19b46&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-32&quot;&gt;&lt;a href=&quot;#cb11-32&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;0100000cc7&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-33&quot;&gt;&lt;a href=&quot;#cb11-33&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-34&quot;&gt;&lt;a href=&quot;#cb11-34&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-35&quot;&gt;&lt;a href=&quot;#cb11-35&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# This is the signature at assertion time.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-36&quot;&gt;&lt;a href=&quot;#cb11-36&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;signature_hex &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-37&quot;&gt;&lt;a href=&quot;#cb11-37&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;3046022100af548d9095e22e104197f2810ee9563135316609bc810877d1685b&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-38&quot;&gt;&lt;a href=&quot;#cb11-38&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;cff62dcd5b022100b31a97961a94b4983088386fd2b7edb09117f4546cf8a5c1&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-39&quot;&gt;&lt;a href=&quot;#cb11-39&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;732420b2370384fd&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-40&quot;&gt;&lt;a href=&quot;#cb11-40&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;st&quot;&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-41&quot;&gt;&lt;a href=&quot;#cb11-41&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-42&quot;&gt;&lt;a href=&quot;#cb11-42&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;def&lt;/span&gt; from_hex(h):&lt;/span&gt;
&lt;span id=&quot;cb11-43&quot;&gt;&lt;a href=&quot;#cb11-43&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; codecs.decode(h.replace(&lt;span class=&quot;st&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;#39;&lt;/span&gt;, &lt;span class=&quot;st&quot;&gt;&amp;#39;&amp;#39;&lt;/span&gt;), &lt;span class=&quot;st&quot;&gt;&amp;#39;hex&amp;#39;&lt;/span&gt;)&lt;/span&gt;
&lt;span id=&quot;cb11-44&quot;&gt;&lt;a href=&quot;#cb11-44&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-45&quot;&gt;&lt;a href=&quot;#cb11-45&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;def&lt;/span&gt; sha256(m):&lt;/span&gt;
&lt;span id=&quot;cb11-46&quot;&gt;&lt;a href=&quot;#cb11-46&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    digest &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; hashes.Hash(hashes.SHA256())&lt;/span&gt;
&lt;span id=&quot;cb11-47&quot;&gt;&lt;a href=&quot;#cb11-47&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    digest.update(m)&lt;/span&gt;
&lt;span id=&quot;cb11-48&quot;&gt;&lt;a href=&quot;#cb11-48&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; digest.finalize()&lt;/span&gt;
&lt;span id=&quot;cb11-49&quot;&gt;&lt;a href=&quot;#cb11-49&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-50&quot;&gt;&lt;a href=&quot;#cb11-50&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# The signed message is calculated from the authenticator data and&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-51&quot;&gt;&lt;a href=&quot;#cb11-51&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# clientDataJSON, but the latter is hashed first.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-52&quot;&gt;&lt;a href=&quot;#cb11-52&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;signed_message &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; (from_hex(authenticator_data_hex) &lt;span class=&quot;op&quot;&gt;+&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-53&quot;&gt;&lt;a href=&quot;#cb11-53&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;                  sha256(from_hex(client_data_json_hex)))&lt;/span&gt;
&lt;span id=&quot;cb11-54&quot;&gt;&lt;a href=&quot;#cb11-54&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-55&quot;&gt;&lt;a href=&quot;#cb11-55&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;public_key &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; load_der_public_key(from_hex(public_key_spki_hex))&lt;/span&gt;
&lt;span id=&quot;cb11-56&quot;&gt;&lt;a href=&quot;#cb11-56&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;public_key.verify(from_hex(signature_hex),&lt;/span&gt;
&lt;span id=&quot;cb11-57&quot;&gt;&lt;a href=&quot;#cb11-57&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;                  signed_message,&lt;/span&gt;
&lt;span id=&quot;cb11-58&quot;&gt;&lt;a href=&quot;#cb11-58&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;                  ec.ECDSA(hashes.SHA256()))&lt;/span&gt;
&lt;span id=&quot;cb11-59&quot;&gt;&lt;a href=&quot;#cb11-59&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# `verify` throws an exception if the signature isn&amp;#39;t valid.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-60&quot;&gt;&lt;a href=&quot;#cb11-60&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;print&lt;/span&gt;(&lt;span class=&quot;st&quot;&gt;&amp;#39;ok&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;where-to-ask-questions&quot;&gt;Where to ask questions&lt;/h2&gt;
&lt;p&gt;&lt;a
href=&quot;https://stackoverflow.com/questions/tagged/passkey&quot;&gt;StackOverflow&lt;/a&gt;
is a reasonable place, with the &lt;code&gt;passkey&lt;/code&gt; tag.&lt;/p&gt;
</content>
 </entry>
 

</feed>
