{"slug": "i-built-a-fido2-hardware-key-openai-says-it-isn-t-hardware-enough", "title": "I built a FIDO2 hardware key. OpenAI says it isn't hardware enough", "summary": "A developer built a working FIDO2 hardware security key on an ESP32-S3 running Pico FIDO2 firmware, but OpenAI's Daybreak classified the registered credential as a Passkey rather than a hardware security key, even though Daybreak explicitly rejects software and synced passkeys and requires physical FIDO2 hardware keys. OpenAI Support told the developer that its documentation states no requirement that a key be listed in the FIDO Metadata Service or use attestation, yet the key is still not accepted as hardware. The device reports AAGUID 89fb94b7-06c9-3673-9b7e-30526d968145, self-derived from the first 16 bytes of SHA256(\"Pico FIDO2\"), and supports CTAP 2.1, U2F, client PIN, resident credentials, credBlob, credProtect, hmac-secret and largeBlobKey.", "body_md": "this started because OpenAI Daybreak requires physical FIDO2 hardware security keys and I had the obvious thought: fine, I'll build one.\n\nI already had an ESP32-S3. Pico FIDO2 runs on it. USB HID. CTAP 2.1. FIDO2. U2F. client PIN. resident credentials. the whole point of the thing is to be a security key.\n\nI flashed it, fixed a stupid Linux problem, registered it with OpenAI, authenticated with it, and then OpenAI filed the credential under **Passkey**.\n\nnot security key.\n\nnot hardware key.\n\nPasskey.\n\nthat matters because Daybreak explicitly rejects software/synced passkeys and requires physical FIDO2 hardware keys.\n\nthere is an ESP32-S3 physically sitting on my desk doing the cryptography, so I had questions.\n\n## first, udev wasted a chunk of my afternoon\n\ninitially Chromium could not see the key at all.\n\n```\n/dev/hidraw8 600 root:root\n```\n\nI had a sane udev rule:\n\n```\nSUBSYSTEM==\"hidraw\", ATTRS{idVendor}==\"feff\", ATTRS{idProduct}==\"fcfd\", GROUP=\"plugdev\", MODE=\"0660\", TAG+=\"uaccess\"\n```\n\n`udevadm test` said the rule worked.\n\nthe actual device did not care.\n\neventually I checked the daemon instead of rewriting the same fucking rule again.\n\n```\npid 611  udevd   ELAPSED 17-08:49   CPU TIME 1-23:59:58   11.5%   1 thread\n```\n\nudevd had burned almost two days of CPU in a single-thread spin loop and was processing no events.\n\n```\nKERNEL remove  .../hidraw8\nKERNEL add     .../hidraw8\n\n# no UDEV event\n# daemon is apparently busy contemplating death\n```\n\nrestart it:\n\n```\nsudo sv restart /var/service/udevd\n```\n\nand suddenly:\n\n```\n/dev/hidraw8 660 root:plugdev\n/dev/hidraw9 660 root:plugdev\n```\n\nfine. solved.\n\nalso a useful reminder that `udevadm test` is a simulation. it can tell you the rules on disk are perfect while the actual daemon has been dead behind the eyes for seventeen days.\n\n## the key works\n\nonce Chromium could open it, the actual FIDO side was boring in the best possible way.\n\n```\nproto: 0x02\nmajor: 0x06\nminor: 0x06\ncaps: 0x05 (wink, cbor, msg)\nversion strings: U2F_V2, FIDO_2_0, FIDO_2_1\nextension strings: credBlob, credProtect, hmac-secret, largeBlobKey,\n                   minPinLength, thirdPartyPayment\nalgorithms: es256 (public-key), es384 (public-key)\naaguid: 89fb94b706c936739b7e30526d968145\noptions: noep, rk, noalwaysUv, credMgmt, authnrCfg, clientPin, largeBlobs,\n         pinUvAuthToken, setMinPINLength\nfwversion: 0x606\nmaxmsgsiz: 1024\nmaxcredlen: 1024\npin protocols: 1, 2\n```\n\nChrome sees it.\n\nOpenAI registers a credential on it.\n\nOpenAI authenticates with it.\n\n```\nESP32-S3\n   |\n   | USB HID\n   v\nChromium\n   |\n   | WebAuthn / CTAP2\n   v\nOpenAI\n   |\n   +-- registration works\n   +-- authentication works\n   `-- classification: Passkey\n```\n\nthat last line is the problem.\n\n## the original theory: AAGUID and attestation\n\nPico FIDO2 reports this AAGUID:\n\n```\n89fb94b7-06c9-3673-9b7e-30526d968145\n```\n\nit is self-assigned by the firmware project. the source literally derives it from the first 16 bytes of SHA256(\"Pico FIDO2\").\n\nit is not a commercial authenticator model sitting in the FIDO Metadata Service, and the firmware self-attests instead of presenting a manufacturer attestation chain.\n\nso my first theory was obvious: OpenAI is probably using AAGUID metadata and/or attestation to decide whether something gets the special \"hardware security key\" classification.\n\nthat still looks plausible from the outside.\n\nbut now I have Support's answer, and it makes the whole thing more interesting.\n\n## support: no, MDS and attestation are not documented requirements\n\nI sent OpenAI the AAGUID, the CTAP output, the hardware and firmware details, the browser/device information, the time of the failure, and the fact that registration succeeds but the credential lands in the Passkey bucket.\n\nSupport came back with this:\n\nthe docs do not state any requirement that the key be MDS-listed or use attestation.\n\ngood. that is exactly the distinction I wanted clarified.\n\nexcept they still do not accept the key as hardware.\n\nthey also said there is no documented mechanism for adding an arbitrary AAGUID to an allowlist.\n\nand the documented workaround is basically:\n\n```\nif this physical FIDO2 key works but we do not recognize it as hardware,\nuse a different physical FIDO2 key.\n```\n\nwhich is a workaround. it is not a definition of compatible.\n\nat this point the facts are:\n\n```\nphysical hardware:                   yes\nFIDO2 / CTAP 2.1:                    yes\nregistration with OpenAI:            works\nauthentication with OpenAI:          works\nMDS listing documented as required:  no\nattestation documented as required:  no\narbitrary AAGUID allowlist path:      no\nOpenAI counts it as hardware:         no\ntechnical reason given:               no\n```\n\nso there is clearly another property being enforced somewhere.\n\nI just cannot tell you what it is because apparently neither the public docs nor the support answer will define it.\n\n## and then support told me I actually need two fucking keys\n\nthis part is almost funnier.\n\nAdvanced Account Security requires two sign-in methods.\n\nnormally that can be some combination of passkeys and hardware keys.\n\nDaybreak, however, disallows software/synced passkeys and wants physical hardware keys.\n\nSupport explicitly pointed out that one qualifying hardware key is not enough.\n\nso the practical requirement is:\n\n```\nAdvanced Account Security:  2 sign-in methods required\nDaybreak:                    software/synced passkeys do not qualify\ntherefore:                   2 qualifying physical hardware keys\n```\n\nI had Google Password Manager enrolled as another method.\n\nthat has to go.\n\nmy ESP32 key does not count.\n\nso I do not merely need to go buy a hardware key because OpenAI will not recognize the one I built.\n\nI effectively need to go buy **two**.\n\nthat is a materially different requirement from reading \"at least one compatible FIDO2 hardware security key\" and thinking, reasonably, that you need one fucking key.\n\n## the documentation can technically be reconciled. that does not make it good.\n\nI can see how this happened.\n\nthe Daybreak page describes its hardware requirement.\n\nthe Advanced Account Security page separately describes the two-method requirement.\n\ncombine both policies and you arrive at two hardware keys.\n\nso this is not some impossible logical contradiction.\n\nit is just a really shitty way to communicate a requirement that costs money and can lock somebody out of access if they discover it at the deadline.\n\nif the actual onboarding requirement is:\n\n```\ntwo physical FIDO2 hardware security keys recognized by OpenAI's classifier\nand no software/synced passkeys\n```\n\nput that sentence on the page.\n\ndo not make people solve a little requirements algebra problem between two help articles while a deadline is three days away.\n\n## attestation still sucks when it becomes an invisible admission ticket\n\nthe Support answer matters here because I do not want to claim something I cannot prove.\n\nI cannot say \"OpenAI requires MDS attestation.\" Support specifically says that is not a published requirement, and I cannot see their classifier.\n\nI can say the thing in my hand is physical hardware, implements the protocol, works for authentication, and is still not considered a qualifying hardware key.\n\nsomething beyond protocol compatibility is therefore being used to make that decision.\n\nattestation and authenticator metadata remain the obvious suspects because that is exactly what those mechanisms are for: establishing authenticator provenance and model identity.\n\nand this is where I dislike the whole setup.\n\nattestation has legitimate uses. if a company wants to require a specific certified device model, fine. if it wants supply-chain assurance, fine. if it wants to exclude random homemade authenticators because it does not trust their implementation quality, fine.\n\nbut those are different security properties from \"physical FIDO2 key.\"\n\nmy key does not become software because I built it.\n\nthe private key does not magically leave the ESP32 because nobody paid to put the device in a vendor metadata registry.\n\nand if the policy is really \"we only trust authenticator models we recognize,\" then the policy should say exactly that.\n\notherwise an open protocol has an invisible corporate admission layer sitting on top of it.\n\nthe protocol can be open. the firmware can be open source. I can implement the thing correctly enough for the relying party to use it. and then at the last step some opaque classifier says no because the device lacks whatever pedigree the backend expects.\n\nthat mostly benefits established authenticator vendors and relying parties that want to outsource trust to vendor identity.\n\nit does not do much for open hardware, research hardware, or some asshole with an ESP32 who actually followed the standard.\n\n## no, I am not going to make it pretend to be a yubikey\n\nthe obvious shitty hack would be to copy the AAGUID from a known commercial authenticator into the firmware.\n\nno.\n\nfirst, that is lying about what the device is.\n\nsecond, if OpenAI really verifies attestation, the fake AAGUID accomplishes nothing because I do not have the manufacturer's attestation private key and certificate chain.\n\nthird, making an authentication system happy by impersonating somebody else's authenticator is the exact opposite of what I am trying to test.\n\nI want the device accepted or rejected as what it actually is:\n\n```\na physical, self-built, standards-compatible FIDO2 authenticator\n```\n\nif that class is not acceptable for Daybreak, cool. document that.\n\n## what support actually resolved\n\nthe answer was useful in one sense.\n\nwe now know this is not supposed to be read as:\n\n```\nMDS listing is explicitly required\nattestation is explicitly required\n```\n\nbecause Support says neither requirement exists in the published guidance.\n\nwe also know there is no documented arbitrary-AAGUID allowlist path.\n\nand we know the practical Daybreak setup needs two qualifying physical keys, because AAS needs two methods and Daybreak excludes the passkey class.\n\nwhat we still do not know is the simplest question:\n\n```\nwhat technical property makes one working physical FIDO2 authenticator\n\"hardware\" to OpenAI and another one \"Passkey\"?\n```\n\nthat question remains unanswered.\n\n## where it stands now\n\n```\nudev problem:                     fixed\nESP32-S3 FIDO2 key:              works\nOpenAI registration:             works\nOpenAI authentication:           works\nOpenAI hardware classification:  nope\nMDS requirement documented:      nope\nattestation requirement documented: nope\nAAGUID allowlist path:            nope\nGoogle Password Manager:         has to be removed\nqualifying physical keys needed: two\nnumber of qualifying keys I have: zero, apparently\n```\n\nso yes, the practical answer is probably that I buy two commercial security keys before the deadline.\n\nthat does not make the documentation or classification behavior less stupid.\n\nif OpenAI wants only known commercial authenticators, say so.\n\nif it wants certified authenticators, say so.\n\nif it wants attested authenticators, say so.\n\nif it has an internal allowlist of models it considers hardware, say that.\n\nright now the requirement is effectively:\n\n```\nbuy two things that our backend decides are hardware.\nwe do not publish the exact property that makes the decision.\n```\n\nwhich is not a protocol requirement.\n\nit is a product policy hiding behind the word \"compatible.\"\n\nreferences:\n\n```\nOpenAI Advanced Account Security\nhttps://help.openai.com/en/articles/20001221-measure-results\n\nOpenAI Daybreak overview\nhttps://help.openai.com/en/articles/20001258-openai-daybreak-trusted-access-for-cyber-overview\n\nOpenAI Daybreak troubleshooting\nhttps://help.openai.com/en/articles/20001259-openai-daybreak-common-issues-and-troubleshooting\n\nFIDO Metadata Service\nhttps://fidoalliance.org/metadata/\n\nPico FIDO\nhttps://github.com/polhenarejos/pico-fido\n```\n\n", "url": "https://wpnews.pro/news/i-built-a-fido2-hardware-key-openai-says-it-isn-t-hardware-enough", "canonical_source": "https://voidnullvalue.github.io/openai-fido2/", "published_at": "2026-09-28 22:41:31+00:00", "updated_at": "2026-09-28 23:17:35.819093+00:00", "lang": "en", "topics": ["ai-products", "ai-policy"], "entities": ["OpenAI", "OpenAI Daybreak", "ESP32-S3", "Pico FIDO2", "Chromium", "FIDO2", "WebAuthn", "CTAP2"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/i-built-a-fido2-hardware-key-openai-says-it-isn-t-hardware-enough", "markdown": "https://wpnews.pro/news/i-built-a-fido2-hardware-key-openai-says-it-isn-t-hardware-enough.md", "text": "https://wpnews.pro/news/i-built-a-fido2-hardware-key-openai-says-it-isn-t-hardware-enough.txt", "jsonld": "https://wpnews.pro/news/i-built-a-fido2-hardware-key-openai-says-it-isn-t-hardware-enough.jsonld"}}