. "$BST_LIB" # BST-TIMEOUT: 900 # the interpreter is one large static image and this suite # # starts it four times; on a loaded guest a single start has # # been minutes, and the runner's 60s default would report the # # port as broken when what ran out was the clock. # # python3-pyu2f 0.1.5 (meta-openembedded meta-python, # recipes-devtools/python/python3-pyu2f_0.1.5.bb -- `inherit pypi setuptools3', # PyPI sdist) on wasm32-hwjs. Google's U2F HOST library: the side that speaks the # FIDO U2F raw message protocol to a security key over USB HID. # # THERE IS NO SECURITY KEY BEHIND THIS MACHINE, AND THAT SPLITS THE PACKAGE IN # TWO. The wire encoders and decoders -- the ISO 7816 extended APDUs, the U2FHID # fragmentation layer, the raw-message field layout, the status-word map -- are # pure functions of bytes and need no device at all; they are the deliverable, # and they are exercised in full below over a scripted fake transport, with every # layer above that transport running unmodified. Discovery is the one part that # touches hardware, and its documented answer here is "none": that half is # measured against this image's real /sys and asserted as such, not stubbed. # # WHY THE PROTOCOL VECTORS ARE PINNABLE AT ALL. Every request below is built # from FIXED challenge, application and key-handle patterns rather than random # bytes, so its encoding is a constant. The constants are not invented here -- # they are the encoding the FIDO U2F Raw Message Formats specification defines, # and the suite asserts the bytes rather than the shape, so a rewrite that # produced a self-consistent but non-conforming encoding is a red. # # HOW THE SEMANTIC CASES ARE PINNED. They are not pinned in this file. One probe # under $BST_DATA is run by the interpreter under test and each of its # ` ' lines is compared against the value the SAME probe printed on # an x86-64 control -- CPython 3.12.3 with pyu2f 0.1.5 and six 1.16.0, recorded # 2026-09-09. Nothing in the probe depends on pointer width, byte order, the # platform string, the clock or hash randomisation, which is what makes # "identical to x86-64" a claim rather than a transcription of whatever this arm # happened to do. One interpreter start covers all of them, deliberately: a # `python3 -c' per case would be a hundred start-ups of a large static image on a # guest where one has taken minutes. # # PURE PYTHON, WHICH IS WHY IT PORTS. This image's python3 is ONE STATIC BINARY # with an empty lib-dynload and no dynamic loader (BRINTOS-1476), so a module # carrying a compiled extension could not be imported here whatever it built to. # pyu2f ships none -- it is `inherit pypi setuptools3' over a tree of .py files # -- and the no-.so case below asserts that negative directly, with the empty # lib-dynload beside it as the MECHANISM, so a reader can see why the negative # matters without leaving the file. # # NOT COVERED HERE, on purpose: # * upstream's own unittests. setup.py excludes `pyu2f.tests' from the wheel, # so they are not on the image to run. # * pyu2f/hid/macos.py and pyu2f/hid/windows.py. Neither is importable on this # platform and neither is reached; they are the sole importers of ctypes in # this package, which is why the probe asserts ctypes is NOT in sys.modules # rather than asserting anything about them. # * pyu2f.convenience.customauthenticator, which shells out to an external # plugin binary over subprocess. There is no plugin on this image, so what it # would exercise is `subprocess' rather than pyu2f. # # THE ENUMERATION SECTION IS THE ONE THAT CAN LEGITIMATELY DIFFER BY IMAGE, and # it is written so that it decides its own expectation from what it measures # rather than from a guess -- see the comment above it. require python3 python3-pyu2f/present SITE=/usr/lib/python3.12/site-packages SCRATCH=${TMPDIR:-/tmp}/bst-python3-pyu2f.$$ mkdir -p "$SCRATCH" 2>/dev/null # --------------------------------------------------------- the deliverable --- # The package is a directory of modules plus two subpackages. Asserted as files # rather than as an import, so a red separates "not installed" from "installed # and does not work". check python3-pyu2f/site-packages-dir test -d "$SITE" check python3-pyu2f/package-dir test -d "$SITE/pyu2f" check python3-pyu2f/module-apdu test -f "$SITE/pyu2f/apdu.py" check python3-pyu2f/module-errors test -f "$SITE/pyu2f/errors.py" check python3-pyu2f/module-hardware test -f "$SITE/pyu2f/hardware.py" check python3-pyu2f/module-hidtransport test -f "$SITE/pyu2f/hidtransport.py" check python3-pyu2f/module-model test -f "$SITE/pyu2f/model.py" check python3-pyu2f/module-u2f test -f "$SITE/pyu2f/u2f.py" check python3-pyu2f/subpackage-hid test -f "$SITE/pyu2f/hid/__init__.py" check python3-pyu2f/subpackage-convenience \ test -f "$SITE/pyu2f/convenience/__init__.py" check python3-pyu2f/backend-linux test -f "$SITE/pyu2f/hid/linux.py" # THE NO-.so ASSERTION, and the mechanism that makes it matter. The first case # is the ticket's requirement: nothing pyu2f owns may be a compiled extension. # The second is why -- lib-dynload is empty on this image, so there is no place # an extension could be loaded from even if one shipped. # # Both are `find' over a directory, so both would pass VACUOUSLY against a # directory that is not there, which is the one way this pair could report a # clean port on an image carrying no python3 at all. Each is preceded by the # existence case that makes its emptiness mean something. check_eq python3-pyu2f/no-so "" \ "$(find "$SITE/pyu2f" -name '*.so' 2>/dev/null)" check python3-pyu2f/lib-dynload-dir test -d /usr/lib/python3.12/lib-dynload check_eq python3-pyu2f/lib-dynload-empty "" \ "$(find /usr/lib/python3.12/lib-dynload -name '*.so' 2>/dev/null)" # The package resolves to the tree the recipe installed, from the interpreter's # own side. `import pyu2f' succeeding from some other path -- a stray copy, a # namespace package -- would satisfy every case below while the port had # installed nothing. check_eq python3-pyu2f/import-path "$SITE/pyu2f/__init__.py" \ "$(python3 -c 'import pyu2f; print(pyu2f.__file__)' 2>&1 | tail -1)" # ------------------------------------------------------------ the probe run --- # Forked, so a failure to stage the exec costs this file its cases rather than # the run its terminator. PROBE_OUT=$SCRATCH/probe.out ( python3 "$BST_DATA/python3-pyu2f-probe.py" > "$PROBE_OUT" 2>&1 ) # pv : the value the probe printed for , empty if it never got # there. Empty is a red against every expectation below, which is the intent -- # a probe that died half-way must not leave the cases it never reached looking # untested. pv() { sed -n "s/^$1 //p" "$PROBE_OUT" | head -1; } P=python3-pyu2f # THE VERSION THE RECIPE PINS. pyu2f carries no __version__ of its own, so this # is the installed distribution's metadata -- which is also the artifact that # would be missing if setuptools3 had installed the modules and not the # dist-info, and importlib.metadata.version would then raise rather than answer. check_eq $P/version "0.1.5" "$(pv version)" check_eq $P/module-is-source "py" "$(pv module-suffix)" check_eq $P/submodules "apdu,errors,hardware,hid,hidtransport,model,u2f" \ "$(pv submodules)" # ctypes is imported by the macOS and Windows backends and by NOTHING on the # linux path. The recipe declares python3-ctypes anyway, which is right for a # recipe built for whatever platform the distro targets, and unreached here. # Asserted as a negative so that a future version that DID reach ctypes on linux # shows up as a changed value rather than as a mystery at import time -- on this # image that would be an unimportable module, not a slow one. check_eq $P/ctypes-not-imported "False" "$(pv ctypes-imported)" # threading is the opposite shape: it IS imported, but by `logging', not by # pyu2f -- and threading.py lives in python3-core, so the python3-threading # split the recipe declares (queue.py, _threading_local.py) is still unreached. check_eq $P/threading-imported "True" "$(pv threading-imported)" # ----------------------------------------------------- the U2F command APDU --- # The registration request: CLA=0x00, INS=0x01 (U2F_REGISTER), P1=0x03, P2=0x00, # a three-byte EXTENDED Lc of 0x000040, the 32-byte challenge parameter followed # by the 32-byte application parameter, and a two-byte Le of 0x0000. 73 bytes, # and every one of them is asserted -- this is the vector the whole port exists # to get right. check_eq $P/apdu-register \ "00010300000040000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f0000" \ "$(pv apdu.register)" check_eq $P/apdu-register-len "73" "$(pv apdu.register-len)" # The authentication request. INS=0x02 (U2F_AUTHENTICATE) and P1 is the control # byte -- 0x03 "enforce user presence and sign", 0x07 "check-only" -- with the # key handle carried behind its ONE-BYTE length, which is what makes a handle # longer than 255 unrepresentable at this layer. check_eq $P/apdu-authenticate \ "00020300000051000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f10404142434445464748494a4b4c4d4e4f0000" \ "$(pv apdu.authenticate)" check_eq $P/apdu-authenticate-check-only \ "00020700000051000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f10404142434445464748494a4b4c4d4e4f0000" \ "$(pv apdu.authenticate-check-only)" # The version request carries NO data, and that is the one case where the two # encodings differ: the ISO 7816-4 form omits Lc and sends a three-byte trailer, # the U2F variant sends Lc anyway as three zero bytes. That difference is the # whole of "the U2F variant of ISO 7816" this module implements, and it is what # the legacy fallback further down switches between. check_eq $P/apdu-version "00030000000000" "$(pv apdu.version)" check_eq $P/apdu-version-legacy "000300000000000000" "$(pv apdu.version-legacy)" # With data present the two encodings coincide, which is why the fallback is # safe to take for any command. check_eq $P/apdu-register-legacy-same "$(pv apdu.register)" \ "$(pv apdu.register-legacy)" # Lc is three bytes for EVERY length, including the ones that would fit in one. # That is what "extended encoding only" means here, and a short-form encoder # would still produce a valid-looking APDU for the first two of these. check_eq $P/apdu-lc-1 "000001" "$(pv apdu.lc-1)" check_eq $P/apdu-lc-255 "0000ff" "$(pv apdu.lc-255)" check_eq $P/apdu-lc-65535 "00ffff" "$(pv apdu.lc-65535)" # 65536 is one past what a 16-bit Lc can express, and it is refused in the # constructor -- not in the encoder, which would silently truncate. check_eq $P/apdu-too-long "InvalidCommandError" "$(pv apdu.too-long)" # ---------------------------------------------------- the U2F response APDU --- check_eq $P/apdu-resp-body "dead" "$(pv apdu.resp-body)" check_eq $P/apdu-resp-sw "9000" "$(pv apdu.resp-sw)" check_eq $P/apdu-resp-success "True" "$(pv apdu.resp-success)" # The shortest legal response is the two status bytes with no body at all, and # its body is None rather than an empty bytearray -- a caller that tested # truthiness would not tell the two apart, so the value is pinned. check_eq $P/apdu-resp-empty-body "None" "$(pv apdu.resp-empty-body)" check_eq $P/apdu-resp-empty-success "True" "$(pv apdu.resp-empty-success)" # TRUNCATION. One byte cannot carry a status word, so it is not a response with # an empty body -- it is not a response. This is the ticket's truncated-response # requirement, asserted at the layer that decides it. check_eq $P/apdu-resp-truncated "InvalidResponseError" "$(pv apdu.resp-truncated)" check_eq $P/apdu-resp-none "InvalidResponseError" "$(pv apdu.resp-none)" # THE STATUS-WORD MAP, which is the part of this package a caller reasons about # by exception class rather than by number. Three words get their own class and # everything else that is not 0x9000 is a bare ApduError carrying the two bytes # -- the ticket's bad-status-word requirement. check_eq $P/apdu-sw-9000 "none" "$(pv apdu.sw-9000)" check_eq $P/apdu-sw-6985 "TUPRequiredError" "$(pv apdu.sw-6985)" check_eq $P/apdu-sw-6a80 "InvalidKeyHandleError" "$(pv apdu.sw-6a80)" check_eq $P/apdu-sw-6700 "InvalidKeyHandleError" "$(pv apdu.sw-6700)" check_eq $P/apdu-sw-6a86 "ApduError" "$(pv apdu.sw-6a86)" check_eq $P/apdu-sw-6a86-fields "106 134" "$(pv apdu.sw-6a86-fields)" # ---------------------------------------------------------- the U2FHID layer --- # The transport that fragments an APDU across 64-byte HID reports. Driven over a # fake hid device that answers from a script and RECORDS every packet written, # so what is asserted below is the actual wire bytes rather than the transport's # own account of them. # # The INIT handshake first: the transport opens on the broadcast channel id # ff:ff:ff:ff, sends an 8-byte nonce, and keeps the channel id the device # allocates for every packet after it. check_eq $P/hid-init-broadcast-cid "ffffffff" "$(pv hid.init-broadcast-cid)" check_eq $P/hid-init-cmd "86" "$(pv hid.init-cmd)" check_eq $P/hid-init-size "8" "$(pv hid.init-size)" check_eq $P/hid-init-packet-len "64" "$(pv hid.init-packet-len)" check_eq $P/hid-cid "01020304" "$(pv hid.cid)" check_eq $P/hid-u2fhid-version "2" "$(pv hid.u2fhid-version)" check_eq $P/hid-packet-size "64" "$(pv hid.packet-size)" # FRAGMENTATION, which is the whole reason this layer exists. An init packet # carries 7 bytes of header and so at most 57 of payload, so the 73-byte # registration APDU goes out as one init packet plus one continuation carrying # the remaining 16 bytes behind a 5-byte header whose sequence number starts at # zero. The payloads are asserted byte for byte, and then asserted again as a # round trip, so a boundary that moved by one shows up as both. check_eq $P/hid-frag-frames "2" "$(pv hid.frag-frames)" check_eq $P/hid-frag-init-cmd "83" "$(pv hid.frag-init-cmd)" check_eq $P/hid-frag-init-size "73" "$(pv hid.frag-init-size)" check_eq $P/hid-frag-init-payload \ "00010300000040000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031" \ "$(pv hid.frag-init-payload)" check_eq $P/hid-frag-cont-cid "01020304" "$(pv hid.frag-cont-cid)" check_eq $P/hid-frag-cont-seq "0" "$(pv hid.frag-cont-seq)" check_eq $P/hid-frag-cont-payload "32333435363738393a3b3c3d3e3f0000" \ "$(pv hid.frag-cont-payload)" check_eq $P/hid-frag-roundtrip "True" "$(pv hid.frag-roundtrip)" # Every frame is a FULL report, zero-padded. A short write would be a protocol # error on a real device, and it is the kind of thing a rewrite of this layer # gets wrong while every payload assertion above still passes. check_eq $P/hid-frag-frame-lens "64,64" "$(pv hid.frag-frame-lens)" check_eq $P/hid-frag-cont-tail-zero "True" "$(pv hid.frag-cont-tail-zero)" check_eq $P/hid-msg-reply "aabb9000" "$(pv hid.msg-reply)" # DEFRAGMENTATION, the same thing in the other direction: a 100-byte reply does # not fit in one report, so the transport must read the continuation and stop at # the size the INIT packet declared rather than at the end of the last frame -- # which is 128 bytes of frames for 100 bytes of message. check_eq $P/hid-defrag-len "100" "$(pv hid.defrag-len)" check_eq $P/hid-defrag-equal "True" "$(pv hid.defrag-equal)" # A reply whose command byte is not the one that was sent is a protocol error, # not a payload -- the ticket's unknown-command requirement at the layer that # owns it -- and a U2FHID_ERROR frame is raised rather than returned. check_eq $P/hid-cmd-mismatch "HidError" "$(pv hid.cmd-mismatch)" check_eq $P/hid-error-frame "HidError" "$(pv hid.error-frame)" # Packet-level validation, asserted on the constructors rather than through a # device: an over-long init payload, a sequence number above 127 and a frame # that is not exactly one report are each an InvalidPacketError. check_eq $P/hid-init-payload-too-big "InvalidPacketError" \ "$(pv hid.init-payload-too-big)" check_eq $P/hid-cont-seq-too-big "InvalidPacketError" "$(pv hid.cont-seq-too-big)" check_eq $P/hid-short-frame "InvalidPacketError" "$(pv hid.short-frame)" # --------------------------------------------------- the SecurityKey commands --- # The layer that turns a method call into one of the APDUs above, driven over a # recording transport. The first case is the registration APDU arriving at the # transport unchanged -- i.e. that the command layer and the encoder agree. check_eq $P/sk-register-sent "$(pv apdu.register)" "$(pv sk.register-sent)" check_eq $P/sk-auth-sent "$(pv apdu.authenticate)" "$(pv sk.auth-sent)" check_eq $P/sk-auth-check-only-p1 "07" "$(pv sk.auth-check-only-p1)" # THE COMMITTED REGISTRATION RESPONSE, split into the fields the FIDO U2F raw # format defines: a 0x05 reserved byte, the 65-byte uncompressed user public key # (0x04 followed by the two 32-byte coordinates), the one-byte key-handle length, # the key handle, the attestation certificate and the signature. pyu2f returns # the body whole, so the split is this suite's -- and what it asserts is that # every byte survived the APDU and U2FHID layers in order. check_eq $P/sk-register-body-len "121" "$(pv sk.register-body-len)" check_eq $P/sk-register-reserved "05" "$(pv sk.register-reserved)" check_eq $P/sk-register-pubkey-len "65" "$(pv sk.register-pubkey-len)" check_eq $P/sk-register-pubkey \ "040102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40" \ "$(pv sk.register-pubkey)" check_eq $P/sk-register-kh-len "16" "$(pv sk.register-kh-len)" check_eq $P/sk-register-kh "404142434445464748494a4b4c4d4e4f" "$(pv sk.register-kh)" check_eq $P/sk-register-cert "30820102101112131415161718191a1b1c1d1e1f" \ "$(pv sk.register-cert)" check_eq $P/sk-register-sig "3044808182838485868788898a8b8c8d8e8f" \ "$(pv sk.register-sig)" # THE AUTHENTICATION RESPONSE: a user-presence byte, a 4-byte BIG-ENDIAN counter # and the signature. The counter is the only multi-byte integer in the U2F raw # format, so it is the only field a byte-order mistake could break -- and on a # little-endian host a wrong parse of 0x0001e240 yields 1074528256, which is a # perfectly plausible counter. It is therefore asserted BOTH as the number and # as its bytes: the pair is what makes the arm's endianness the thing under test # rather than an assumption. check_eq $P/sk-auth-user-presence "01" "$(pv sk.auth-user-presence)" check_eq $P/sk-auth-counter "123456" "$(pv sk.auth-counter)" check_eq $P/sk-auth-counter-bytes "0001e240" "$(pv sk.auth-counter-bytes)" check_eq $P/sk-auth-sig "3044808182838485868788898a8b8c8d8e8f" "$(pv sk.auth-sig)" # The two parameters are hashes and are 32 bytes by definition; a caller that # passes something else is stopped here rather than on the wire. check_eq $P/sk-register-short-challenge "InvalidRequestError" \ "$(pv sk.register-short-challenge)" check_eq $P/sk-register-short-app "InvalidRequestError" \ "$(pv sk.register-short-app)" check_eq $P/sk-auth-short-challenge "InvalidRequestError" \ "$(pv sk.auth-short-challenge)" # CmdVersion AND THE LEGACY FALLBACK, which is the one piece of protocol # negotiation in the package. A device that does not speak the ISO 7816-4 # encoding answers SW_WRONG_LENGTH; pyu2f then retries the SAME command in the # U2F variant and remembers the choice for every command after it. Both # encodings are asserted, in order, off the recording transport -- the second # send is the legacy form of the first, and the flag proves the choice stuck # rather than being re-derived per command. check_eq $P/sk-version "U2F_V2" "$(pv sk.version)" check_eq $P/sk-version-sent "00030000000000" "$(pv sk.version-sent)" check_eq $P/sk-version-legacy-flag "False" "$(pv sk.version-legacy-flag)" check_eq $P/sk-version-fallback "U2F_V2" "$(pv sk.version-fallback)" check_eq $P/sk-version-fallback-sends "2" "$(pv sk.version-fallback-sends)" check_eq $P/sk-version-fallback-first "00030000000000" \ "$(pv sk.version-fallback-first)" check_eq $P/sk-version-fallback-second "000300000000000000" \ "$(pv sk.version-fallback-second)" check_eq $P/sk-version-fallback-flag "True" "$(pv sk.version-fallback-flag)" check_eq $P/sk-register-bad-sw "ApduError" "$(pv sk.register-bad-sw)" check_eq $P/sk-register-tup "TUPRequiredError" "$(pv sk.register-tup)" # ------------------------------------------------------------- the model layer --- # ClientData is the JSON the server signs over, and the U2F spec pins its # encoding twice: the challenge is URL-SAFE base64 with the padding REMOVED, and # the keys are sorted. The challenge in the probe is chosen so that the standard # base64 alphabet would render `+' and `/' where the URL-safe one renders `-' and # `_' -- so a standard-alphabet encode is a DIFFERENT string here, not an # equivalent one, and the two spellings cannot both pass. check_eq $P/model-clientdata-json \ '{"challenge": "-----v_wAAE", "origin": "https://example.com", "typ": "navigator.id.finishEnrollment"}' \ "$(pv model.clientdata-json)" check_eq $P/model-clientdata-auth-json \ '{"challenge": "-----v_wAAE", "origin": "https://example.com", "typ": "navigator.id.getAssertion"}' \ "$(pv model.clientdata-auth-json)" check_eq $P/model-clientdata-repr-is-json "True" \ "$(pv model.clientdata-repr-is-json)" check_eq $P/model-typ-registration "navigator.id.finishEnrollment" \ "$(pv model.typ-registration)" check_eq $P/model-typ-authentication "navigator.id.getAssertion" \ "$(pv model.typ-authentication)" check_eq $P/model-bad-typ "InvalidModelError" "$(pv model.bad-typ)" check_eq $P/model-registered-key-default-version "U2F_V2" \ "$(pv model.registered-key-default-version)" # -------------------------------------------------------- the high-level flow --- # U2FInterface over a scripted SecurityKey -- the layer a caller actually writes # against. Two things are worth pinning about it: that it hashes the right # strings into the two APDU parameters (SHA-256 of the ClientData JSON and of the # app id, per the spec, and the probe recomputes both rather than comparing them # with themselves), and that it hands back the registration data untouched. check_eq $P/u2f-register-type "RegisterResponse" "$(pv u2f.register-type)" check_eq $P/u2f-register-app-param "True" "$(pv u2f.register-app-param)" check_eq $P/u2f-register-challenge-param "True" \ "$(pv u2f.register-challenge-param)" check_eq $P/u2f-register-data \ "05040102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4010404142434445464748494a4b4c4d4e4f30820102101112131415161718191a1b1c1d1e1f3044808182838485868788898a8b8c8d8e8f" \ "$(pv u2f.register-data)" check_eq $P/u2f-register-client-data-json \ '{"challenge": "-----v_wAAE", "origin": "example.com", "typ": "navigator.id.finishEnrollment"}' \ "$(pv u2f.register-client-data-json)" check_eq $P/u2f-sign-type "SignResponse" "$(pv u2f.sign-type)" check_eq $P/u2f-sign-key-handle "404142434445464748494a4b4c4d4e4f" \ "$(pv u2f.sign-key-handle)" check_eq $P/u2f-sign-data "010001e2403044808182838485868788898a8b8c8d8e8f" \ "$(pv u2f.sign-data)" check_eq $P/u2f-sign-counter "123456" "$(pv u2f.sign-counter)" check_eq $P/u2f-sign-client-data-typ "True" "$(pv u2f.sign-client-data-typ)" # A device reporting a version this library does not speak is refused at # construction, before any command is sent. check_eq $P/u2f-bad-version "UnsupportedVersionException" "$(pv u2f.bad-version)" # All key handles invalid is DEVICE_INELIGIBLE, and the code is a field on the # exception rather than only a number inside its message. check_eq $P/u2f-all-keys-invalid-code "4" "$(pv u2f.all-keys-invalid-code)" check_eq $P/u2f-error-codes "0 1 2 3 4 5" "$(pv u2f.error-code-names)" # ---------------------------------------------------------- the six dependency --- # python3-six is the recipe's one non-stdlib RDEPENDS, and on this platform it is # reached from exactly one module: pyu2f.convenience.localauthenticator, which # uses six.moves.input to prompt for the touch. Importing that module is the # assertion that the declared dependency is installed AND that it is what the # import resolves to -- an image missing python3-six fails here and nowhere else # in this suite. check_eq $P/six-localauthenticator-imports "True" \ "$(pv six.localauthenticator-imported)" check_eq $P/six-imported "True" "$(pv six.imported)" check_eq $P/six-version "1.16.0" "$(pv six.version)" # ---------------------------------------------------------------- enumeration --- # THE ONE SECTION WHOSE EXPECTED VALUES ARE NOT COPIED FROM AN x86-64 CONTROL, # because they are a fact about THIS image's /sys rather than about pyu2f. # # pyu2f/hid/linux.py:Enumerate is `os.listdir("/sys/class/hidraw")' and nothing # else, so the outcome follows from one measurable thing and this section # measures it rather than guessing: # # hidraw class ABSENT -> os.listdir raises FileNotFoundError, and because # DiscoverLocalHIDU2FDevices is a GENERATOR the error # surfaces at the first iteration, i.e. out of # GetLocalU2FInterface's own `for' rather than out of a # call the caller can see. # hidraw class PRESENT-> enumeration returns a list; with no U2F device among # the entries GetLocalU2FInterface falls through to the # documented NoDeviceFoundError. # # Both are correct answers to "is there a security key here", and the port's # requirement is the same either way: PROMPTLY, with no hang and no traceback out # of a ctypes backend. Those three are asserted unconditionally below; only the # outcome NAME is conditioned on what /sys carries. DISC_OUT=$SCRATCH/discovery.out ( python3 "$BST_DATA/python3-pyu2f-discovery.py" > "$DISC_OUT" 2>&1 ) dv() { sed -n "s/^$1 //p" "$DISC_OUT" | head -1; } # The platform switch. wasm32-hwjs reports `linux', so the hidraw backend is what # gets selected and the two ctypes backends are never imported -- which is the # load-bearing fact on an image with no dynamic loader, not a triviality. check_eq $P/sys-platform "linux" "$(dv sys-platform)" check_eq $P/backend "LinuxHidDevice" "$(dv backend)" check_eq $P/enumerate-no-ctypes "False" "$(dv ctypes-imported)" # Promptness and non-explosion, asserted whichever /sys this is. check_eq $P/enumerate-prompt "True" "$(dv enumerate-under-10s)" check_eq $P/getlocal-prompt "True" "$(dv getlocal-under-10s)" check_eq $P/gethostname-nonempty "True" "$(dv gethostname-nonempty)" check_eq $P/default-origin-is-hostname "True" \ "$(dv default-origin-is-hostname)" # ...and the outcome names, against the expectation this image's own /sys picks. if [ "$(dv hidraw-class-present)" = "True" ]; then _want_enum=list _want_getlocal=NoDeviceFoundError else _want_enum=FileNotFoundError _want_getlocal=FileNotFoundError fi # The branch above is only meaningful if the discovery run actually reported the # presence. Without this case a discovery script that died before its first line # would silently take the else arm, and the two cases below would then be # asserting the absent-/sys answer against nothing at all. _seen=$(dv hidraw-class-present) case "$_seen" in True|False) _seen=reported ;; *) _seen="unreported:$_seen" ;; esac check_eq $P/hidraw-class-reported "reported" "$_seen" check_eq $P/enumerate-outcome "$_want_enum" "$(dv enumerate-outcome)" check_eq $P/getlocal-outcome "$_want_getlocal" "$(dv getlocal-outcome)" rm -rf "$SCRATCH" 2>/dev/null