# One interpreter start for every semantic fact 99-python3-pyu2f.test asserts # about the pyu2f PROTOCOL layer. # # Each line is ` `; the suite extracts a case by name and compares # the value with check_eq, so a probe that dies part-way leaves the cases it # never reached with an empty value rather than with a plausible one. # # WHY EVERY CASE HERE IS DEVICE-FREE, and why that is the port rather than a # limitation of it. pyu2f is a HOST library: the wire encoders and decoders -- # ISO 7816 extended APDUs, the U2FHID fragmentation layer, the FIDO U2F raw # message fields -- are pure functions of bytes, and the only part of the # package that touches hardware is `pyu2f.hid`, which this machine has no # security key behind. So the transport is stubbed with a FakeHidDevice that # answers from a script, and EVERY LAYER ABOVE IT RUNS UNMODIFIED: the packets # below really are the packets pyu2f would put on the wire, byte for byte, # checked against the vectors in the FIDO U2F Raw Message Formats spec. # # What is NOT stubbed is the enumeration; that is measured in the .test, on this # image's real /sys, because "there is no device here" is the answer the port # has to give and asserting it against a fake would assert nothing. # # Nothing here is allowed to depend on pointer width, byte order, the platform # string, the clock or hash randomisation: this file's whole output must be # identical on x86-64 and on wasm32, and what legitimately differs about this # arm is asserted in the .test instead. import sys def emit(case, value): print("%s %s" % (case, value)) sys.stdout.flush() def hexs(b): return "".join("%02x" % x for x in bytearray(b)) def raises(fn): """The exception class fn raises, or `none' if it returns.""" try: fn() except BaseException as e: # the point is to name whatever comes out return type(e).__name__ return "none" import pyu2f from pyu2f import apdu from pyu2f import errors from pyu2f import hardware from pyu2f import hidtransport from pyu2f import model from pyu2f import u2f # --------------------------------------------------------------- the package --- # The version the recipe pins. pyu2f itself carries no __version__, so this is # the installed distribution's metadata -- which is also the thing that would be # missing if setuptools3 had installed the modules and not the dist-info. import importlib.metadata emit("version", importlib.metadata.version("pyu2f")) emit("module-suffix", pyu2f.__file__.rsplit(".", 1)[-1]) emit("submodules", ",".join(sorted( m.split(".", 1)[1] for m in sys.modules if m.startswith("pyu2f.")))) # ctypes is imported by pyu2f/hid/macos.py and pyu2f/hid/windows.py and by # NOTHING on the linux path. The recipe declares python3-ctypes anyway, which is # right for the recipe -- it is built for whatever platform the distro targets -- # and unreached here; this records the negative, so a future version that DID # reach ctypes on linux shows up as a changed value rather than as a mystery at # import time. # # threading is the opposite shape and is reported for the same reason. It IS # imported, but not by pyu2f: `logging' pulls it in, and threading.py lives in # python3-core, so the python3-threading split the recipe declares -- queue.py # and _threading_local.py -- is still unreached. Its one importer is the macOS # backend's `from six.moves import queue'. emit("ctypes-imported", "ctypes" in sys.modules) emit("threading-imported", "threading" in sys.modules) # ------------------------------------------------------- the U2F command APDU --- # The registration request, encoded against the FIDO U2F Raw Message Formats # spec: CLA=0x00, INS=0x01 (U2F_REGISTER), P1=0x03, P2=0x00, then the ISO 7816-4 # EXTENDED Lc -- three bytes, a leading 0x00 followed by a 16-bit big-endian # length -- then the 64-byte request (32-byte challenge parameter followed by # 32-byte application parameter), then a two-byte Le of 0x0000 meaning "send # everything you have". # # The two parameters are fixed patterns rather than random bytes, so the # expected encoding is a constant this suite can pin. CHAL = bytearray(range(0x00, 0x20)) APP = bytearray(range(0x20, 0x40)) reg = apdu.CommandApdu(0, apdu.CMD_REGISTER, 0x03, 0x00, CHAL + APP) emit("apdu.register", hexs(reg.ToByteArray())) emit("apdu.register-len", len(reg.ToByteArray())) # The authentication request. Same shape, INS=0x02 (U2F_AUTHENTICATE), and P1 is # the control byte: 0x03 "enforce user presence and sign", 0x07 "check-only". # The body carries the key handle preceded by its ONE-BYTE length, which is what # makes a handle longer than 255 unrepresentable at this layer. KH = bytearray(range(0x40, 0x50)) auth_body = CHAL + APP + bytearray([len(KH)]) + KH emit("apdu.authenticate", hexs(apdu.CommandApdu(0, apdu.CMD_AUTH, 0x03, 0x00, auth_body).ToByteArray())) emit("apdu.authenticate-check-only", hexs(apdu.CommandApdu(0, apdu.CMD_AUTH, 0x07, 0x00, auth_body).ToByteArray())) # The version request carries NO data, and that is the one case where the two # encodings differ. The ISO 7816-4 form omits Lc entirely and sends a three-byte # trailer; the U2F variant sends Lc anyway, as three zero bytes, which is the # whole of "the U2F variant of ISO 7816" this module's docstring names. A device # that answers SW_WRONG_LENGTH to the first is retried with the second. ver = apdu.CommandApdu(0, apdu.CMD_VERSION, 0x00, 0x00) emit("apdu.version", hexs(ver.ToByteArray())) emit("apdu.version-legacy", hexs(ver.ToLegacyU2FByteArray())) emit("apdu.register-legacy", hexs(reg.ToLegacyU2FByteArray())) # The extended Lc is three bytes for every length, including the ones that fit # in one -- that is what "extended encoding only" means here. emit("apdu.lc-1", hexs(apdu.CommandApdu(0, 1, 0, 0, bytearray([0xaa])).InternalEncodeLc())) emit("apdu.lc-255", hexs(apdu.CommandApdu(0, 1, 0, 0, bytearray(255)).InternalEncodeLc())) emit("apdu.lc-65535", hexs(apdu.CommandApdu(0, 1, 0, 0, bytearray(65535)).InternalEncodeLc())) # 65536 bytes is one past what a 16-bit Lc can express, and the constructor is # where that is caught -- not the encoder, which would silently truncate. emit("apdu.too-long", raises(lambda: apdu.CommandApdu(0, 1, 0, 0, bytearray(65536)))) # ------------------------------------------------------ the U2F response APDU --- # A response is a body followed by the two status bytes, so the shortest legal # response is two bytes with no body at all. ok = apdu.ResponseApdu(bytearray([0xde, 0xad, 0x90, 0x00])) emit("apdu.resp-body", hexs(ok.body)) emit("apdu.resp-sw", "%02x%02x" % (ok.sw1, ok.sw2)) emit("apdu.resp-success", ok.IsSuccess()) empty = apdu.ResponseApdu(bytearray([0x90, 0x00])) emit("apdu.resp-empty-body", empty.body) emit("apdu.resp-empty-success", empty.IsSuccess()) # TRUNCATION. One byte cannot carry a status word, so it is not a response with # an empty body -- it is not a response. emit("apdu.resp-truncated", raises(lambda: apdu.ResponseApdu(bytearray([0x90])))) emit("apdu.resp-none", raises(lambda: apdu.ResponseApdu(bytearray()))) # THE STATUS-WORD MAP, which is the part of this package a caller reasons about # by exception class rather than by number. Each of the three special words gets # its own class; everything else that is not 0x9000 is a bare ApduError carrying # the two bytes. def sw(a, b): return raises(apdu.ResponseApdu(bytearray([a, b])).CheckSuccessOrRaise) emit("apdu.sw-9000", sw(0x90, 0x00)) emit("apdu.sw-6985", sw(0x69, 0x85)) # SW_CONDITIONS_NOT_SATISFIED -> touch me emit("apdu.sw-6a80", sw(0x6a, 0x80)) # SW_WRONG_DATA -> not my key handle emit("apdu.sw-6700", sw(0x67, 0x00)) # SW_WRONG_LENGTH -> also a bad handle emit("apdu.sw-6a86", sw(0x6a, 0x86)) # anything else try: apdu.ResponseApdu(bytearray([0x6a, 0x86])).CheckSuccessOrRaise() except errors.ApduError as e: emit("apdu.sw-6a86-fields", "%d %d" % (e.sw1, e.sw2)) # ---------------------------------------------------------- the U2FHID layer --- # A FakeHidDevice: 64-byte reports, and a scripted reply queue. It records every # packet written, so the fragmentation below is asserted on the actual wire # bytes rather than on what the transport says it sent. class FakeHidDevice(object): def __init__(self, cid, replies=None, packet_size=64): self.cid = cid self.packet_size = packet_size self.written = [] self.replies = list(replies or []) # The INIT handshake is answered from the nonce the transport sends, so # it is scripted here rather than queued: os.urandom(8) is not something # this probe is allowed to depend on. self.init_answered = False self.u2fhid_version = 2 def GetInReportDataLength(self): return self.packet_size def GetOutReportDataLength(self): return self.packet_size def Write(self, packet): self.written.append(bytearray(packet)) def Read(self): last = self.written[-1] cmd = last[4] if cmd == hidtransport.UsbHidTransport.U2FHID_INIT: nonce = last[7:15] body = bytearray(nonce) + self.cid + bytearray( [self.u2fhid_version, 1, 0, 0, 0]) return self._frame(hidtransport.UsbHidTransport.U2FHID_INIT, body, bytearray([0xff, 0xff, 0xff, 0xff])) cmd_out, body = self.replies.pop(0) return self._frame(cmd_out, bytearray(body), self.cid) def _frame(self, cmd, body, cid): """One init packet; continuations are queued for the reads that follow.""" head = body[0:self.packet_size - 7] pkt = hidtransport.UsbHidTransport.InitPacket( self.packet_size, cid, cmd, len(body), head) rest = body[self.packet_size - 7:] seq = 0 while rest: chunk = rest[0:self.packet_size - 5] rest = rest[self.packet_size - 5:] self.pending = getattr(self, "pending", []) self.pending.append(hidtransport.UsbHidTransport.ContPacket( self.packet_size, cid, seq, chunk).ToWireFormat()) seq += 1 return pkt.ToWireFormat() class FramingHidDevice(FakeHidDevice): """As above, but hands out queued continuation frames on later reads.""" def Read(self): pending = getattr(self, "pending", []) if pending: return pending.pop(0) return FakeHidDevice.Read(self) CID = bytearray([0x01, 0x02, 0x03, 0x04]) # THE INIT HANDSHAKE. The transport opens on the broadcast channel id # ff:ff:ff:ff, sends an 8-byte nonce, and the device answers with that nonce, # the channel id it has allocated, and the protocol version. The transport keeps # the allocated cid for every packet after this one. dev = FramingHidDevice(CID, replies=[]) t = hidtransport.UsbHidTransport(dev) emit("hid.cid", hexs(t.cid)) emit("hid.u2fhid-version", t.u2fhid_version) emit("hid.packet-size", t.packet_size) emit("hid.init-broadcast-cid", hexs(dev.written[0][0:4])) emit("hid.init-cmd", "%02x" % dev.written[0][4]) emit("hid.init-size", "%d" % ((dev.written[0][5] << 8) | dev.written[0][6])) emit("hid.init-packet-len", len(dev.written[0])) # FRAGMENTATION, which is the whole reason this layer exists. An init packet # carries 7 bytes of header and so at most 57 bytes of payload; the 73-byte # registration APDU therefore goes out as one init packet plus one continuation # with the remaining 16 bytes, and the continuation's header is 5 bytes with a # sequence number that starts at zero. msg = reg.ToByteArray() dev = FramingHidDevice(CID, replies=[(hidtransport.UsbHidTransport.U2FHID_MSG, bytearray([0xaa, 0xbb, 0x90, 0x00]))]) t = hidtransport.UsbHidTransport(dev) before = len(dev.written) resp = t.SendMsgBytes(msg) frames = dev.written[before:] emit("hid.frag-frames", len(frames)) emit("hid.frag-init-cmd", "%02x" % frames[0][4]) emit("hid.frag-init-size", "%d" % ((frames[0][5] << 8) | frames[0][6])) emit("hid.frag-init-payload", hexs(frames[0][7:64])) emit("hid.frag-cont-cid", hexs(frames[1][0:4])) emit("hid.frag-cont-seq", frames[1][4]) emit("hid.frag-cont-payload", hexs(frames[1][5:5 + 16])) # Every frame is a full report, zero-padded -- a short write would be a protocol # error on a real device and is the kind of thing a rewrite of this layer breaks. emit("hid.frag-frame-lens", ",".join(str(len(f)) for f in frames)) emit("hid.frag-cont-tail-zero", hexs(frames[1][21:64]) == "00" * 43) # Reassembled, the two frames are exactly the APDU that went in. emit("hid.frag-roundtrip", hexs(bytearray(frames[0][7:64]) + bytearray(frames[1][5:5 + 16])) == hexs(msg)) emit("hid.msg-reply", hexs(resp)) # 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. long_body = bytearray(range(100)) dev = FramingHidDevice(CID, replies=[(hidtransport.UsbHidTransport.U2FHID_MSG, long_body)]) t = hidtransport.UsbHidTransport(dev) got = t.SendMsgBytes(bytearray([0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00])) emit("hid.defrag-len", len(got)) emit("hid.defrag-equal", hexs(got) == hexs(long_body)) # A reply whose command is not the one that was sent is a protocol error, not a # payload -- this is the "unknown command" arm. dev = FramingHidDevice(CID, replies=[(hidtransport.UsbHidTransport.U2FHID_PING, bytearray([0x01]))]) t = hidtransport.UsbHidTransport(dev) emit("hid.cmd-mismatch", raises(lambda: t.SendMsgBytes(bytearray([0x00] * 7)))) # A U2FHID_ERROR frame carries a one-byte reason and is raised as a HidError. dev = FramingHidDevice(CID, replies=[(hidtransport.UsbHidTransport.U2FHID_ERROR, bytearray([0x01]))]) t = hidtransport.UsbHidTransport(dev) emit("hid.error-frame", raises(lambda: t.SendMsgBytes(bytearray([0x00] * 7)))) # Packet-level validation, on the constructors rather than on a device: a # too-large init payload, a sequence number above 127 and a wrong-length frame # are each an InvalidPacketError. emit("hid.init-payload-too-big", raises( lambda: hidtransport.UsbHidTransport.InitPacket(64, CID, 0x83, 58, bytearray(58)))) emit("hid.cont-seq-too-big", raises( lambda: hidtransport.UsbHidTransport.ContPacket(64, CID, 128, bytearray(1)))) emit("hid.short-frame", raises( lambda: hidtransport.UsbHidTransport.InitPacket.FromWireFormat(64, bytearray(63)))) # ---------------------------------------------------- the SecurityKey commands --- # The layer that turns a method call into one of the APDUs above. Driven over # the fake transport, so what is asserted is the bytes it hands down. class RecordingTransport(object): def __init__(self, replies): self.sent = [] self.replies = list(replies) def SendMsgBytes(self, msg): self.sent.append(bytearray(msg)) return self.replies.pop(0) # A committed registration response, in the FIDO U2F raw format: a 0x05 reserved # byte, the 65-byte uncompressed user public key, the one-byte key-handle # length, the key handle, the attestation certificate and the signature. pyu2f # returns it whole, so the field split below is this suite's, and the assertion # is that every byte survives the APDU and U2FHID layers in order. PUBKEY = bytearray([0x04] + list(range(0x01, 0x41))) CERT = bytearray([0x30, 0x82, 0x01, 0x02] + list(range(0x10, 0x20))) SIG = bytearray([0x30, 0x44] + list(range(0x80, 0x90))) REG_BODY = bytearray([0x05]) + PUBKEY + bytearray([len(KH)]) + KH + CERT + SIG tr = RecordingTransport([REG_BODY + bytearray([0x90, 0x00])]) key = hardware.SecurityKey(tr) body = key.CmdRegister(CHAL, APP) emit("sk.register-sent", hexs(tr.sent[0])) emit("sk.register-body-len", len(body)) emit("sk.register-reserved", "%02x" % body[0]) emit("sk.register-pubkey", hexs(body[1:66])) emit("sk.register-pubkey-len", len(body[1:66])) emit("sk.register-kh-len", body[66]) emit("sk.register-kh", hexs(body[67:67 + body[66]])) emit("sk.register-cert", hexs(body[67 + body[66]:67 + body[66] + len(CERT)])) emit("sk.register-sig", hexs(body[67 + body[66] + len(CERT):])) # The authentication response: a user-presence byte, a 4-byte BIG-ENDIAN # counter, then the signature. The counter is the one multi-byte integer in the # U2F raw format, so it is the one field whose parse a byte-order mistake would # break -- and it is asserted as a number, not as its bytes, for exactly that # reason. import struct AUTH_BODY = bytearray([0x01]) + bytearray(struct.pack(">I", 0x0001e240)) + SIG tr = RecordingTransport([AUTH_BODY + bytearray([0x90, 0x00])]) key = hardware.SecurityKey(tr) body = key.CmdAuthenticate(CHAL, APP, KH) emit("sk.auth-sent", hexs(tr.sent[0])) emit("sk.auth-user-presence", "%02x" % body[0]) emit("sk.auth-counter", struct.unpack(">I", bytes(body[1:5]))[0]) emit("sk.auth-counter-bytes", hexs(body[1:5])) emit("sk.auth-sig", hexs(body[5:])) tr = RecordingTransport([AUTH_BODY + bytearray([0x90, 0x00])]) key = hardware.SecurityKey(tr) key.CmdAuthenticate(CHAL, APP, KH, check_only=True) emit("sk.auth-check-only-p1", "%02x" % tr.sent[0][2]) # 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. tr = RecordingTransport([bytearray([0x90, 0x00])]) key = hardware.SecurityKey(tr) emit("sk.register-short-challenge", raises(lambda: key.CmdRegister(CHAL[0:31], APP))) emit("sk.register-short-app", raises(lambda: key.CmdRegister(CHAL, APP[0:31]))) emit("sk.auth-short-challenge", raises(lambda: key.CmdAuthenticate(CHAL[0:31], APP, KH))) # CmdVersion, and the LEGACY FALLBACK. 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. Both encodings are asserted, in # order, off the recording transport -- this is the one piece of protocol # negotiation in the package. tr = RecordingTransport([bytearray(b"U2F_V2") + bytearray([0x90, 0x00])]) key = hardware.SecurityKey(tr) emit("sk.version", bytes(key.CmdVersion()).decode()) emit("sk.version-sent", hexs(tr.sent[0])) emit("sk.version-legacy-flag", key.use_legacy_format) tr = RecordingTransport([bytearray([0x67, 0x00]), bytearray(b"U2F_V2") + bytearray([0x90, 0x00])]) key = hardware.SecurityKey(tr) emit("sk.version-fallback", bytes(key.CmdVersion()).decode()) emit("sk.version-fallback-sends", len(tr.sent)) emit("sk.version-fallback-first", hexs(tr.sent[0])) emit("sk.version-fallback-second", hexs(tr.sent[1])) emit("sk.version-fallback-flag", key.use_legacy_format) # A status word the map does not special-case comes out of CmdRegister as an # ApduError, so the caller sees the device's own two bytes. tr = RecordingTransport([bytearray([0x6a, 0x86])]) key = hardware.SecurityKey(tr) emit("sk.register-bad-sw", raises(lambda: key.CmdRegister(CHAL, APP))) tr = RecordingTransport([bytearray([0x69, 0x85])]) key = hardware.SecurityKey(tr) emit("sk.register-tup", raises(lambda: key.CmdRegister(CHAL, APP))) # ------------------------------------------------------------- the model layer --- # ClientData is the JSON the server signs over, and its encoding is pinned by # the U2F spec twice over: the challenge is URL-SAFE base64 with the padding # REMOVED, and the keys are sorted. The challenge below is chosen to contain a # byte pair that base64's standard alphabet renders as `+' and `/', so a # standard-alphabet encode would be a different string here rather than an # equivalent one. RAW_CHALLENGE = bytearray([0xfb, 0xef, 0xbe, 0xfa, 0xff, 0xf0, 0x00, 0x01]) cd = model.ClientData(model.ClientData.TYP_REGISTRATION, RAW_CHALLENGE, "https://example.com") emit("model.clientdata-json", cd.GetJson()) emit("model.clientdata-repr-is-json", repr(cd) == cd.GetJson()) emit("model.typ-registration", model.ClientData.TYP_REGISTRATION) emit("model.typ-authentication", model.ClientData.TYP_AUTHENTICATION) emit("model.clientdata-auth-json", model.ClientData(model.ClientData.TYP_AUTHENTICATION, RAW_CHALLENGE, "https://example.com").GetJson()) emit("model.bad-typ", raises( lambda: model.ClientData("navigator.id.nonsense", RAW_CHALLENGE, "o"))) emit("model.registered-key-default-version", model.RegisteredKey(KH).version) # ------------------------------------------------------- the high-level flow --- # U2FInterface over a scripted SecurityKey. This is the layer a caller actually # uses, and the two things worth pinning about it are 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 that it returns the raw registration data # untouched. class ScriptedKey(object): def __init__(self, version=b"U2F_V2", register=None, authenticate=None): self._version = version self._register = register self._authenticate = authenticate self.calls = [] def CmdVersion(self): return self._version def CmdRegister(self, challenge_param, app_param): self.calls.append(("register", hexs(challenge_param), hexs(app_param))) return self._register def CmdAuthenticate(self, challenge_param, app_param, key_handle, check_only=False): self.calls.append(("authenticate", hexs(challenge_param), hexs(app_param), hexs(key_handle), check_only)) if check_only: raise errors.InvalidKeyHandleError() return self._authenticate def CmdWink(self): self.calls.append(("wink",)) import hashlib APP_ID = "https://example.com" sk = ScriptedKey(register=REG_BODY) iface = u2f.U2FInterface(security_key=sk, origin="example.com") rr = iface.Register(APP_ID, RAW_CHALLENGE, []) emit("u2f.register-type", type(rr).__name__) emit("u2f.register-data", hexs(rr.registration_data)) emit("u2f.register-client-data-json", rr.client_data.GetJson()) emit("u2f.register-app-param", sk.calls[0][2] == hashlib.sha256(APP_ID.encode()).hexdigest()) emit("u2f.register-challenge-param", sk.calls[0][1] == hashlib.sha256( rr.client_data.GetJson().encode()).hexdigest()) sk = ScriptedKey(authenticate=AUTH_BODY) iface = u2f.U2FInterface(security_key=sk, origin="example.com") sr = iface.Authenticate(APP_ID, RAW_CHALLENGE, [model.RegisteredKey(KH)]) emit("u2f.sign-type", type(sr).__name__) emit("u2f.sign-key-handle", hexs(sr.key_handle)) emit("u2f.sign-data", hexs(sr.signature_data)) emit("u2f.sign-counter", struct.unpack(">I", bytes(sr.signature_data[1:5]))[0]) emit("u2f.sign-client-data-typ", model.ClientData.TYP_AUTHENTICATION in sr.client_data.GetJson()) # A device that reports a version this library does not speak is refused at # construction, before any command is sent. emit("u2f.bad-version", raises( lambda: u2f.U2FInterface(security_key=ScriptedKey(version=b"U2F_V1"), origin="example.com"))) # All key handles invalid is DEVICE_INELIGIBLE (4), and the code is on the # exception rather than only in its message. sk = ScriptedKey(authenticate=AUTH_BODY) sk.CmdAuthenticate = lambda *a, **k: (_ for _ in ()).throw( errors.InvalidKeyHandleError()) iface = u2f.U2FInterface(security_key=sk, origin="example.com") try: iface.Authenticate(APP_ID, RAW_CHALLENGE, [model.RegisteredKey(KH)]) except errors.U2FError as e: emit("u2f.all-keys-invalid-code", e.code) emit("u2f.error-code-names", "%d %d %d %d %d %d" % ( errors.U2FError.OK, errors.U2FError.OTHER_ERROR, errors.U2FError.BAD_REQUEST, errors.U2FError.CONFIGURATION_UNSUPPORTED, errors.U2FError.DEVICE_INELIGIBLE, errors.U2FError.TIMEOUT)) # ---------------------------------------------------------- the six dependency --- # python3-six is the recipe's one non-stdlib RDEPENDS and it is reached from # exactly one module on this platform: pyu2f.convenience.localauthenticator, # which uses six.moves.input to prompt for the touch. Importing it is the # assertion that the declared dependency is installed AND that it is what the # import resolves to. from pyu2f.convenience import localauthenticator emit("six.localauthenticator-imported", "pyu2f.convenience.localauthenticator" in sys.modules) emit("six.imported", "six" in sys.modules) import six emit("six.version", six.__version__)