. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. # # # They are: a bare `import passlib', a second whose STDERR # # is the assertion (`import-quiet'), and the probe, which # # takes ONE start for 160 measurements. Every other case # # below reads the filesystem or dpkg with shell builtins, or # # greps the probe's output. # # # There is no fourth for a shipped executable: setup.py # # passes neither `scripts' nor `entry_points', so nothing # # lands in /usr/bin. `no-bin-script' asserts that absence # # rather than leaving it unstated. # # # EVERY COST PARAMETER INSIDE THE PROBE IS PINNED LOW -- # # 1000 rounds for pbkdf2_sha256 and the two SHA2-crypt # # schemes against passlib's own defaults of 29000 and # # 535000, bcrypt at cost 4, scrypt at n=2^8. The cases are # # about whether these KDFs compute the right function, and # # a default-cost hash would put minutes of pure-Python block # # mixing inside a budget that exists for interpreter starts. # # The whole probe took 1.4s on an x86-64 control. # # # 1200 is 400s per start, matching 99-python3-olefile's # # budget for its own three and roughly six times the ~60s a # # python3 start has measured at on this arm. # # # A suite-timeout red is a batch-wide cost, so if the three # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed replaces # # this note in the test plan rather than in this comment, # # which would cost an image build to keep the shipped suite # # byte-identical. # # python3-passlib 1.7.4 (meta-openembedded/meta-python # recipes-devtools/python/python3-passlib_1.7.4.bb -- `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # A PASSWORD-HASHING FRAMEWORK. Two layers, and the suite exercises both. The # lower one is a registry of 77 hash handlers -- everything from the $6$ # SHA-512 crypt in a modern /etc/shadow, through $1$ md5_crypt and the 13-byte # DES form, to LDAP, Django, Cisco and MSSQL dialects -- each of which can hash a # password, verify one, and identify whether a given string is its own. The upper # one is `CryptContext': a policy object that names which schemes an application # accepts, which of them are deprecated, and what a new hash is written in, so # that stored hashes MIGRATE as the policy moves rather than being rewritten by # hand. # # ============================================================================ # WHY IT PORTS, AND WHAT IT WAITS ON: NOTHING. # ============================================================================ # # PURE PYTHON, WITH THE DEPENDENCY SET COMPLETE AS WRITTEN. 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 installs cleanly and # cannot be imported. passlib declares no ext_modules and the built .deb ships # no `.so' at all -- `pkg-no-so' asserts that off the shipped file list rather # than off the source -- so this port carries no red attributable to # BRINTOS-2046. # # meta-python's recipe declares `python3-core python3-crypt python3-logging # python3-misc python3-netclient', every one a sub-package of the ported # interpreter, so this port needs NO bbappend and waits on nothing. Each is # asserted by name below: a bare "the Depends line exists" would pass on a stanza # that had lost one, and the loss would surface as an ImportError at the first # use that reached that stdlib module. # # `python3-crypt' is the one worth a sentence. It is the split carrying the # `crypt' module, which wraps crypt(3) -- and crypt(3) reaches this image only # because python3_%.bbappend depends on libxcrypt BY NAME, answers CPython's # `ac_cv_crypt_crypt' probe by hand, and names libcrypt.a by absolute path # because wasmld eats a bare `-lcrypt'. That work is BRINTOS-1476's, not this # port's, and what it buys passlib is measured directly by the `*-vs-oscrypt' # cases below. # # ============================================================================ # WHAT IS BEING ASSERTED, AND AGAINST WHAT. # ============================================================================ # # THREE INDEPENDENT KINDS OF EVIDENCE, because "passlib agrees with passlib" is # not a measurement: # # 1. PUBLISHED VECTORS. sha512_crypt's and sha256_crypt's are Ulrich Drepper's # own specification vectors (`Hello world!' under the salt `saltstring' at the # format's default 5000 rounds); md5_crypt's and des_crypt's are the classical # ones; bcrypt's is the widely republished $2b$ cost-12 hash of `password'. # Each is asserted TWICE -- once by verifying it, and once by REPRODUCING the # exact string from the same salt and cost, which is the stronger claim: a # parser can verify a hash it cannot compute. # # 2. AN INDEPENDENT IMPLEMENTATION IN-PROCESS. The `*-vs-oscrypt' cases compare # passlib's PURE-PYTHON backend against the guest's own crypt(3) -- libxcrypt's # C, sharing no source with passlib's Python -- for the same salt and password. # `pbkdf2-vs-hashlib' recomputes pbkdf2_sha256's checksum with # hashlib.pbkdf2_hmac, which is OpenSSL's PBKDF2. # # 3. THE ROUND TRIP, four ways: hash, verify, verify a WRONG password, and hand # the verifier a CORRUPTED hash string. # # ============================================================================ # BACKENDS ARE MEASURED, NEVER PREDICTED -- AND THAT IS WHY TWO ARMS EXIST. # ============================================================================ # # Several schemes pick an implementation at import. sha512_crypt, sha256_crypt, # md5_crypt and des_crypt each prefer the OS crypt(3) and fall back to a # pure-Python `builtin'; bcrypt prefers three C libraries, then crypt(3), then a # builtin that is OFF unless PASSLIB_BUILTIN_BCRYPT says otherwise; argon2 wants # argon2_cffi or argon2pure and has no fallback at all. # # WHICH ONE ANSWERS ON THIS IMAGE IS A PROPERTY OF THE IMAGE, so this suite # reads the name the probe got and asserts the behaviour that name implies. The # `bcrypt-*' cases below are written both ways for that reason: with a backend, # bcrypt must verify its published vector and round-trip; with none, it must # raise passlib's documented MissingBackendError rather than silently hashing # with something weaker. Neither arm is a skip, and the case NAME says which one # ran. # # This ticket predicted the second arm, on the grounds that python3-bcrypt # `inherit cargo' and Rust does not target this arm. That is true of the PyPI # bcrypt module and does not settle the question: passlib's fourth choice is # crypt(3), and this image HAS one. An x86-64 control with the three C modules # blocked selected `os_crypt' and verified the published vector, so whether # bcrypt works here turns on whether the image's libxcrypt was built with the # $2b$ hash enabled -- which the verify measures and this suite does not guess. # # THE PURE-PYTHON PATH IS ASSERTED SEPARATELY AND UNCONDITIONALLY, by forcing # `builtin' and reproducing the published vector under it (`*-builtin-repro'). # That path is what makes this package portable to this arm at all, and it must # hold whether or not the image happens to have a faster crypt(3). # # ============================================================================ # THE THREE FINDINGS, ALL UPSTREAM'S OR THE FORMAT'S, AND NONE OF THEM THIS # ARM'S. # ============================================================================ # # Each was MEASURED on an x86-64 python3.12 control against the SAME .deb this # image installs -- unpacked and put on sys.path -- before it was written down # here. None is filed: they are properties of passlib 1.7.4 and of the hash # formats on every platform, and this port neither introduces nor can fix them. # See docs/conventions/no-public-upstream-reports.md. # # 1. des_crypt TRUNCATES THE PASSWORD AT EIGHT BYTES. DES crypt takes a 56-bit # key from the first eight bytes and discards the rest, so `abcdefgh' and # `abcdefghIGNORED' have the same hash and `verify' answers True for either. # `des-truncates-at-8' asserts it on purpose. It is why des_crypt is in this # suite as a legacy READER and not as something to write new hashes with -- # a caller who ships it without knowing this has an eight-character password # policy and does not know it. # # It is also why every round-trip case here uses a wrong password differing in # its FIRST character. An earlier version of this probe differed in the last # one and measured des_crypt verifying it, correctly; a generic case that # stumbles into this property tells a reader the round trip is broken. # # 2. A MALFORMED HASH IS A PLAIN ValueError, not one of passlib's own exception # classes. `passlib.exc' defines MalformedHashError and its siblings, and # `verify' against a truncated string, an empty string or a foreign scheme # raises `ValueError' itself -- so a caller cannot tell "this is not my hash # format" from any other bad argument by type. Asserted at all three shapes, # per scheme, so a release that started returning False instead -- which would # turn a parse bug into a wrong-password answer, silently -- is visible here. # # 3. THE TEST SUITE AND THE SETUP HELPERS SHIP IN THE RUNTIME PACKAGE. # setup.py's `find_packages()' takes `passlib.tests' and `passlib._setup' # along with the library, so 70 of the .deb's 204 files are upstream's own # tests. They are not run here -- python3-pytest is not ported and this distro # carries no `ptest' in DISTRO_FEATURES, so there is no ptest package at all # and the build emitted python3-passlib, -dev and -dbg and nothing else. The # files are asserted as files. # # ============================================================================ # WHAT THIS SUITE DOES NOT TOUCH. # ============================================================================ # # No network, no device, no peer, no terminal, and nothing outside the probe's # own address space: every case is a hash of a string literal. Nothing is # written to disk. The one file read is the CryptContext policy the suite ships # (data/python3-passlib-policy.cfg), which exists so that passlib's .ini policy # loader is exercised as well as its keyword constructor, and so that a reader # can check the context assertions against the policy without reading Python. # # passlib.totp (TOTP tokens) and passlib.apache (htpasswd/htdigest files) are # deliberately out of scope: the first wants a clock the suite would have to pin # and the second wants files on disk, and neither is what this ticket ports the # package for. P=python3-passlib # `suite-canary' is emitted FIRST and unconditionally. A suite that dies before # its first result line is indistinguishable from one that was never scheduled, # so this one line is what separates "ran and found nothing" from "did not run". bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/passlib DI=$SITE/passlib-1.7.4.dist-info PKGLIST=/var/lib/dpkg/info/python3-passlib.list PROBE=$BST_DATA/python3-passlib-probe.py POLICY=$BST_DATA/python3-passlib-policy.cfg tmp=${TMPDIR:-/tmp} W=$tmp/bst-passlib.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- packaging -- # WHERE THE PACKAGING SAYS THE MODULE LANDS. These run before any import, so a # red here says the .deb is wrong rather than that the module misbehaves. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/package-init test -s "$PKG/__init__.py" check $P/no-flat-module test ! -f "$SITE/passlib.py" # THE FOUR SUBPACKAGES THE SUITE'S CASES REACH THROUGH, asserted as directories # so that a release which flattened one is visible here rather than as an # ImportError inside the probe. `crypto/_blowfish' is passlib's pure-Python # bcrypt, which is why the bcrypt cases below have a `builtin' arm at all. check $P/dir-handlers test -d "$PKG/handlers" check $P/dir-crypto test -d "$PKG/crypto" check $P/dir-blowfish test -d "$PKG/crypto/_blowfish" check $P/dir-utils test -d "$PKG/utils" check $P/module-context test -s "$PKG/context.py" check $P/module-registry test -s "$PKG/registry.py" check $P/module-hash test -s "$PKG/hash.py" check $P/module-exc test -s "$PKG/exc.py" # FINDING 3. Upstream's own test suite and its setup helpers ship inside the # RUNTIME package, because setup.py's find_packages() takes them. check $P/ships-tests test -d "$PKG/tests" check $P/ships-test-module test -s "$PKG/tests/test_handlers.py" check $P/ships-setup-pkg test -d "$PKG/_setup" # THE WORDSETS ARE package_data, not code, and they are what passlib.pwd's # passphrase generator reads. A setuptools change that dropped package_data # would leave the module importable and that generator broken, which nothing # else here would see. check $P/data-wordsets test -d "$PKG/_data/wordsets" check $P/data-eff-long test -s "$PKG/_data/wordsets/eff_long.txt" check $P/data-bip39 test -s "$PKG/_data/wordsets/bip39.txt" # NO EXECUTABLE. setup.py passes neither `scripts' nor `entry_points', so # nothing lands in /usr/bin. Asserted so that a future release which started # shipping one is noticed here. check $P/no-bin-script test ! -e /usr/bin/passlib # --------------------------------------------------------------- dist-info --- # The version is read from METADATA rather than from the module, so a mismatch # between what the packaging claims and what imports is visible as a mismatch. # `dist-version' below reads the module's own `__version__' for the other half. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 1.7.4" cat "$DI/METADATA" check_out $P/dist-info-name "Name: passlib" cat "$DI/METADATA" check $P/dist-info-wheel test -f "$DI/WHEEL" check $P/dist-info-record test -f "$DI/RECORD" check_out $P/dist-info-toplevel "passlib" cat "$DI/top_level.txt" # PURE PYTHON, stated by the packaging itself. `Root-Is-Purelib: true' is the # wheel's own claim that nothing in it is architecture-specific, which is the # property that makes this package portable to this arm at all; the `py3-none-any' # tag is the same claim in the filename's coordinates. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" # ------------------------------------------------------------ the .deb file --- # The installed file list, read once into $W. The copy is what the loop below # redirects from: a stdin redirect the top-level shell performs on the virtiofs # root makes dup2(2) return 513 and ENDS the suite silently (brintos/linux#73), # and $W is on tmpfs. cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # NO COMPILED EXTENSION, asserted off the shipped file list rather than off the # source. This image's python3 has an empty lib-dynload and no dynamic loader # (BRINTOS-1476), so a single .so here would mean a package that installs # cleanly and cannot import -- the BRINTOS-2046 shape. check_eq $P/pkg-no-so "" \ "$(grep -c '\.so$' "$W/pkgf" 2>/dev/null | grep -v '^0$')" # Every payload path the package ships is under site-packages, and is a .py, a # .pyc, package_data, or dist-info metadata. Anything else is a file this port # did not expect to be shipping and wants looking at. _bad="" while IFS= read -r _f; do case "$_f" in ''|/usr/lib/python3.12/site-packages) continue ;; */) continue ;; esac [ -d "$_f" ] && continue case "$_f" in *.py|*.pyc) ;; # package_data: the four wordsets, and the .cfg fixtures upstream's own tests # read. Both are declared in setup.py's package_data and are the only # non-Python payload this package has. */_data/wordsets/*.txt|*/tests/*.cfg) ;; # dist-info metadata. Which of these the build backend writes is a property # of SETUPTOOLS rather than of this package, so the whole standard set is # allowed: pinning the exact list would red on a setuptools change that says # nothing about passlib. `pkg-no-so' above carries the real claim. */METADATA|*/WHEEL|*/RECORD|*/INSTALLER|*/LICENSE*|*/entry_points.txt|*/top_level.txt|*/zip-safe) ;; *) _bad="$_bad $_f" ;; esac done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-passlib$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" check_out $P/pkg-version "Version: 1.7.4-r0" cat "$W/st" check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" # THE DEPENDENCY SET IS THE PORT'S CLAIM THAT IT WAITS ON NOTHING. Every name # here is a sub-package of the ported python3 (BRINTOS-1476), so each is # asserted individually. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" for _d in python3-core python3-crypt python3-logging python3-misc python3-netclient; do check_out $P/pkg-depends-$_d "$_d" cat "$W/dep" done # ------------------------------------------------------------------ import --- # THE FIRST OF THREE PYTHON STARTS. If the Depends set above were short by one # sub-package, this is the case that would say so. check $P/import python3 -c 'import passlib' # THE SECOND. A package that imports but WARNS on the way in is a different # defect from one that fails, and it is invisible to the case above because the # exit status is 0 either way. The assertion is that stderr is EMPTY. # # This is not a formality for THIS package: passlib 1.7.4 reaches the `crypt' # module, which python3.12 deprecates with a DeprecationWarning. That warning is # not shown by default -- it is raised at import of `crypt' itself, where the # default filter is silent for a non-__main__ importer -- and this case is what # says so on target rather than in a comment. check_eq $P/import-quiet "" \ "$(python3 -c 'import passlib' 2>&1 >/dev/null)" # ------------------------------------------------------------------- probe --- check $P/policy-present test -s "$POLICY" if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # THE THIRD START, and the one that carries the suite. Bounded at 900s inside # the 1200s suite budget so that a hung probe is reported as a probe overrun # with the last line it printed, rather than as a suite timeout that says # nothing about where it stopped. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" "$POLICY" OUT="$W/probe" # pv : the value the probe emitted for , or empty. `head -n 1' # because a key is emitted once and a duplicate would otherwise concatenate; # `cut -f2-' because several values legitimately contain `='. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # The probe reached its end. Every case below reads a value the probe printed, # so a probe that stopped early would otherwise turn into a run of empty-string # mismatches that all name the wrong thing. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "160" "$(pv emitted_count)" # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. Without this every behavioural case below would be a # statement about whatever was first on sys.path. check_eq $P/module-answered "$PKG/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/dist-version "1.7.4" "$(pv version)" # THE REGISTRY IS THE PACKAGE'S OWN INVENTORY, and it is asserted WHOLE rather # than sampled. "Over 30 schemes" is this package's headline claim and the # reason the ticket ports it; a release that dropped one would still pass every # behavioural case below. Sorted, so the assertion does not depend on # registration order. check_eq $P/handler-count "77" "$(pv handler_count)" check_eq $P/handlers \ "apr_md5_crypt,argon2,atlassian_pbkdf2_sha1,bcrypt,bcrypt_sha256,bigcrypt,bsd_nthash,bsdi_crypt,cisco_asa,cisco_pix,cisco_type7,crypt16,cta_pbkdf2_sha1,des_crypt,django_argon2,django_bcrypt,django_bcrypt_sha256,django_des_crypt,django_disabled,django_pbkdf2_sha1,django_pbkdf2_sha256,django_salted_md5,django_salted_sha1,dlitz_pbkdf2_sha1,fshp,grub_pbkdf2_sha512,hex_md4,hex_md5,hex_sha1,hex_sha256,hex_sha512,htdigest,ldap_bcrypt,ldap_bsdi_crypt,ldap_des_crypt,ldap_hex_md5,ldap_hex_sha1,ldap_md5,ldap_md5_crypt,ldap_pbkdf2_sha1,ldap_pbkdf2_sha256,ldap_pbkdf2_sha512,ldap_plaintext,ldap_salted_md5,ldap_salted_sha1,ldap_salted_sha256,ldap_salted_sha512,ldap_sha1,ldap_sha1_crypt,ldap_sha256_crypt,ldap_sha512_crypt,lmhash,md5_crypt,msdcc,msdcc2,mssql2000,mssql2005,mysql323,mysql41,nthash,oracle10,oracle11,pbkdf2_sha1,pbkdf2_sha256,pbkdf2_sha512,phpass,plaintext,postgres_md5,roundup_plaintext,scram,scrypt,sha1_crypt,sha256_crypt,sha512_crypt,sun_md5_crypt,unix_disabled,unix_fallback" \ "$(pv handlers)" # ----------------------------------------------- the five pure-Python schemes -- # The common shape, per scheme: the handler answers to its own name; whichever # backend it selected is one of its OWN (never a fixed name -- see the header); # the round trip holds at a pinned cost; the cost really went into the string; # two hashes of one password differ, so the handler is salting; and a corrupted # hash is refused the documented way. for _s in pbkdf2_sha256 sha512_crypt sha256_crypt md5_crypt des_crypt; do check_eq $P/$_s-name "$_s" "$(pv ${_s}_name)" check_eq $P/$_s-roundtrip "yes" "$(pv ${_s}_roundtrip)" check_eq $P/$_s-roundtrip-wrong "no" "$(pv ${_s}_roundtrip_wrong)" check_eq $P/$_s-identify-self "yes" "$(pv ${_s}_roundtrip_identify)" check_eq $P/$_s-salted "yes" "$(pv ${_s}_salted)" # FINDING 2, per scheme. A wrong CHECKSUM is a failed verification; a string # that is not well-formed for this scheme is a plain ValueError, not one of # passlib.exc's own classes. check_eq $P/$_s-corrupt-flip "no" "$(pv ${_s}_corrupt_flip)" check_eq $P/$_s-corrupt-trunc-exc "ValueError" "$(pv ${_s}_corrupt_trunc_exc)" check_eq $P/$_s-corrupt-empty-exc "ValueError" "$(pv ${_s}_corrupt_empty_exc)" check_eq $P/$_s-foreign-exc "ValueError" "$(pv ${_s}_foreign_exc)" check_eq $P/$_s-identify-foreign "no" "$(pv ${_s}_identify_foreign)" done # The four crypt(3) schemes carry a backend layer and a published vector; # pbkdf2_sha256 has neither -- its string format is passlib's own, so it is # cross-checked against OpenSSL further down instead. for _s in sha512_crypt sha256_crypt md5_crypt des_crypt; do check_eq $P/$_s-backends "os_crypt,builtin" "$(pv ${_s}_backends)" check_eq $P/$_s-backend-is-own "yes" "$(pv ${_s}_backend_is_own)" check_eq $P/$_s-has-builtin "yes" "$(pv ${_s}_has_builtin)" check_eq $P/$_s-published-verify "yes" "$(pv ${_s}_published_verify)" check_eq $P/$_s-published-wrong "no" "$(pv ${_s}_published_wrong)" check_eq $P/$_s-identify-published "yes" "$(pv ${_s}_identify_published)" # THE STRONGER CLAIM: the exact published string, reproduced from its own salt # and cost. A parser can verify a hash it cannot compute; this says passlib # computes the same function the specification does. check_eq $P/$_s-published-repro "yes" "$(pv ${_s}_published_reproduced)" # ...and reproduced by the PURE-PYTHON backend specifically. This is the case # that has to hold on an image with no crypt(3) for the scheme, and it is # asserted whether or not this image has one. check_eq $P/$_s-builtin-repro "yes" "$(pv ${_s}_builtin_reproduced)" done # The cost parameter, where the format has one. des_crypt's and md5_crypt's # formats carry none at all, which is asserted rather than left out. check_eq $P/pbkdf2-sha256-has-rounds "yes" "$(pv pbkdf2_sha256_has_rounds)" check_eq $P/sha512-crypt-has-rounds "yes" "$(pv sha512_crypt_has_rounds)" check_eq $P/sha256-crypt-has-rounds "yes" "$(pv sha256_crypt_has_rounds)" check_eq $P/md5-crypt-has-rounds "no" "$(pv md5_crypt_has_rounds)" check_eq $P/des-crypt-has-rounds "no" "$(pv des_crypt_has_rounds)" # THE COST WENT INTO THE STRING. A `using(rounds=N)' that were silently ignored # would still round-trip, so the produced hash is parsed back and its rounds read # out of it. check_eq $P/pbkdf2-sha256-rounds-in-hash "1000" "$(pv pbkdf2_sha256_rounds_in_hash)" check_eq $P/sha512-crypt-rounds-in-hash "1000" "$(pv sha512_crypt_rounds_in_hash)" check_eq $P/sha256-crypt-rounds-in-hash "1000" "$(pv sha256_crypt_rounds_in_hash)" # FINDING 1. des_crypt takes its 56-bit key from the first EIGHT bytes and # discards the rest, so a password sharing those eight verifies. Asserted on # purpose: a caller who ships des_crypt without knowing this has an # eight-character password policy and does not know it. check_eq $P/des-truncates-at-8 "yes" "$(pv des_truncates_at_8)" check_eq $P/des-first-8-matter "no" "$(pv des_first_8_matter)" check_eq $P/des-hash-len "13" "$(pv des_hash_len)" # ---------------------------------------------------- the independent checks -- # THE GUEST'S OWN crypt(3), which reaches this image only because # python3_%.bbappend goes to some length to give the interpreter one. If that # work regressed, `crypt-module' is the case that says so -- and every # `*-vs-oscrypt' case below would go `na' rather than red, which is why this one # is asserted separately. check_eq $P/crypt-module "yes" "$(pv crypt_module)" # PASSLIB'S PURE PYTHON AGAINST libxcrypt'S C, for the same salt and password. # Two implementations sharing no source. A scheme the guest's crypt(3) cannot do # would answer `unsupported', which is a fact about the image rather than a # failure -- but all four are expected to answer, since libxcrypt does DES, MD5 # and both SHA2 forms. check_eq $P/sha512-crypt-vs-oscrypt "yes" "$(pv sha512_crypt_vs_oscrypt)" check_eq $P/sha256-crypt-vs-oscrypt "yes" "$(pv sha256_crypt_vs_oscrypt)" check_eq $P/md5-crypt-vs-oscrypt "yes" "$(pv md5_crypt_vs_oscrypt)" check_eq $P/des-crypt-vs-oscrypt "yes" "$(pv des_crypt_vs_oscrypt)" # pbkdf2_sha256 HAS NO PUBLISHED VECTOR, because the string format is passlib's # own. Its checksum is recomputed instead with hashlib.pbkdf2_hmac -- OpenSSL's # PBKDF2 on this image, sharing no code with passlib's -- from the salt and # rounds parsed back out of passlib's own output. check_eq $P/pbkdf2-parsed-rounds "1000" "$(pv pbkdf2_parsed_rounds)" check_eq $P/pbkdf2-checksum-len "32" "$(pv pbkdf2_checksum_len)" check_eq $P/pbkdf2-vs-hashlib "yes" "$(pv pbkdf2_vs_hashlib)" check_eq $P/pbkdf2-prefix "pbkdf2-sha256" "$(pv pbkdf2_prefix)" # ------------------------------------------- the backend-dependent schemes ---- # ARGON2 HAS NO FALLBACK AT ALL: its two backends are argon2_cffi (a C library # through cffi, which needs the _ctypes this interpreter does not have -- # hwjs-cc#15) and argon2pure (a separate PyPI package, not ported). So the # documented "no backend available" answer is the one this image gives, and it # is asserted as behaviour rather than assumed: passlib must RAISE # MissingBackendError, not silently hash with something weaker. check_eq $P/argon2-present "yes" "$(pv argon2_present)" check_eq $P/argon2-backends "argon2_cffi,argon2pure" "$(pv argon2_backends)" check_eq $P/argon2-backend "none" "$(pv argon2_backend)" check_eq $P/argon2-missing-exc "MissingBackendError" "$(pv argon2_missing_exc)" check_eq $P/argon2-hash-exc "error:MissingBackendError" "$(pv argon2_hash_exc)" # BCRYPT AND SCRYPT ARE MEASURED, THEN ASSERTED ACCORDINGLY -- see the header for # why this is two arms rather than a prediction. The backend name is asserted to # be one of the handler's own either way, and then the arm the image landed in # gets the assertion that arm implies. `bcrypt-backend-measured' carries the name # in one line on every run, so a reader never has to infer which arm ran. check_eq $P/bcrypt-present "yes" "$(pv bcrypt_present)" check_eq $P/bcrypt-backends "bcrypt,pybcrypt,bcryptor,os_crypt,builtin" \ "$(pv bcrypt_backends)" check_eq $P/scrypt-present "yes" "$(pv scrypt_present)" check_eq $P/scrypt-backends "stdlib,scrypt,builtin" "$(pv scrypt_backends)" for _s in bcrypt scrypt; do _b="$(pv ${_s}_backend)" bst_ok $P/$_s-backend-measured "backend: ${_b:-}" case "$_b" in none) # NO BACKEND: passlib's documented answer, asserted as behaviour. check_eq $P/$_s-missing-exc "MissingBackendError" "$(pv ${_s}_missing_exc)" check_eq $P/$_s-hash-exc "error:MissingBackendError" \ "$(pv ${_s}_hash_exc)" ;; "") bst_notok $P/$_s-backend-reported "probe emitted no ${_s}_backend" ;; *) # A BACKEND ANSWERED: then it must do the whole job, at the same standard # the pure-Python schemes are held to. check_eq $P/$_s-hash-exc "none" "$(pv ${_s}_hash_exc)" check_eq $P/$_s-roundtrip "yes" "$(pv ${_s}_roundtrip)" ;; esac done # bcrypt's PUBLISHED VECTOR, and only where a backend answered. The `$2b$' cost-12 # hash of `password' is the widely republished OpenBSD-format one, cross-checked # against libxcrypt's crypt(3) on an x86-64 control. case "$(pv bcrypt_backend)" in none|"") bst_ok $P/bcrypt-published-skipped "no bcrypt backend on this image" ;; *) check_eq $P/bcrypt-published-verify "yes" "$(pv bcrypt_published_verify)" ;; esac # ------------------------------------------------------------ CryptContext ---- # THE POLICY IS READ FROM THE FILE THIS SUITE SHIPS, so these cases exercise # passlib's .ini loader as well as the context. Every value below can be checked # against data/python3-passlib-policy.cfg by eye. check_eq $P/ctx-schemes "pbkdf2_sha256,sha256_crypt,md5_crypt" "$(pv ctx_schemes)" check_eq $P/ctx-default "pbkdf2_sha256" "$(pv ctx_default)" check_eq $P/ctx-deprecated "md5_crypt,sha256_crypt" "$(pv ctx_deprecated)" # The per-scheme cost the policy pins, read back off the loaded context. A # policy key the loader silently ignored would leave the context at passlib's # own 29000/535000 defaults, which no other case here would notice -- except as # a suite that took minutes. check_eq $P/ctx-pbkdf2-rounds "1000" "$(pv ctx_pbkdf2_rounds)" check_eq $P/ctx-sha256-rounds "1000" "$(pv ctx_sha256_rounds)" # A NEW HASH IS WRITTEN IN THE DEFAULT SCHEME, at the policy's cost, and is not # itself in need of updating. check_eq $P/ctx-hash-scheme "pbkdf2_sha256" "$(pv ctx_hash_scheme)" check_eq $P/ctx-hash-verify "yes" "$(pv ctx_hash_verify)" check_eq $P/ctx-hash-wrong "no" "$(pv ctx_hash_wrong)" check_eq $P/ctx-hash-needs-update "no" "$(pv ctx_hash_needs_update)" check_eq $P/ctx-hash-rounds "1000" "$(pv ctx_hash_rounds)" # THE MIGRATION PATH, which is the whole reason to hold a CryptContext rather # than a bare handler: a stored hash in a DEPRECATED scheme still verifies, is # reported as needing update, and verify_and_update hands back its replacement in # the preferred scheme -- which verifies and is itself current. check_eq $P/ctx-stored-identify "md5_crypt" "$(pv ctx_stored_identify)" check_eq $P/ctx-stored-verify "yes" "$(pv ctx_stored_verify)" check_eq $P/ctx-stored-needs-update "yes" "$(pv ctx_stored_needs_update)" check_eq $P/ctx-vu-ok "yes" "$(pv ctx_vu_ok)" check_eq $P/ctx-vu-replaced "yes" "$(pv ctx_vu_replaced)" check_eq $P/ctx-vu-new-scheme "pbkdf2_sha256" "$(pv ctx_vu_new_scheme)" check_eq $P/ctx-vu-new-verifies "yes" "$(pv ctx_vu_new_verifies)" check_eq $P/ctx-vu-new-needs-update "no" "$(pv ctx_vu_new_needs_update)" # ...and the same call against a hash that is ALREADY current returns NO # replacement, which is what keeps verify_and_update cheap in the common case. check_eq $P/ctx-vu-current-ok "yes" "$(pv ctx_vu_current_ok)" check_eq $P/ctx-vu-current-replaced "no" "$(pv ctx_vu_current_replaced)" # A WRONG PASSWORD IS NOT AN UPGRADE. verify_and_update must not hand back a # replacement hash for a password it has just rejected -- a caller that stored # one would have locked the account out with a hash of the wrong secret. check_eq $P/ctx-vu-wrong-ok "no" "$(pv ctx_vu_wrong_ok)" check_eq $P/ctx-vu-wrong-replaced "no" "$(pv ctx_vu_wrong_replaced)" # A SCHEME THE POLICY DOES NOT NAME IS NOT SILENTLY ACCEPTED. sha512_crypt is a # perfectly valid hash that this context is not configured for: identify() # returns None and verify() raises UnknownHashError. That is a different error # from the plain ValueError a MALFORMED string gets (finding 2), and the pair is # asserted together so the distinction stays visible. check_eq $P/ctx-foreign-identify "None" "$(pv ctx_foreign_identify)" check_eq $P/ctx-foreign-exc "UnknownHashError" "$(pv ctx_foreign_exc)" rm -rf "$W" :