. "$BST_LIB" # BST-TIMEOUT: 1500 # THREE python3 starts and nothing else that costs: a bare # # `import M2Crypto', a second one whose STDERR is the # # assertion, and the probe, which takes ONE start for # # ninety-odd measurements. Everything else reads the # # filesystem with shell builtins or greps the probe's # # output. # # # # 500s per start, against the ~60s a python3 start has # # measured at on this arm. The margin is deliberate: this # # import pulls in a side module carrying a statically # # linked libcrypto, which is the largest .so any python3 # # port on this image loads, and no dlopen of that size has # # been timed here. # # # # THE WORK INSIDE THE PROBE IS MICROSECONDS. Measured on a # # host build of this same 0.40.1 (x86_64, openssl 3.0.13, # # swig 4.2.1): 93 keys emitted, whole run well under a # # second. No key generation -- the RSA key is committed, # # for which see the probe's header. # # # # 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-m2crypto 0.40.1 (meta-openembedded # meta-python/recipes-devtools/python/python3-m2crypto_0.40.1.bb, `inherit pypi # siteinfo setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # THE PACKAGE **IS** THE EXTENSION. THERE IS NO PURE-PYTHON HALF. # ============================================================================ # # Several python3-* ports on this image ship a C accelerator beside a # pure-Python implementation, and this distro declines the accelerator -- # python3-cbor2, python3-simplejson, python3-markupsafe each have a bbappend # saying so. M2Crypto is not that shape and no such choice exists here: # # M2Crypto/__init__.py:24 from M2Crypto import m2 # M2Crypto/m2.py:30 from M2Crypto.m2crypto import * # M2Crypto/m2crypto.py the SWIG shim, whose first act is to import # M2Crypto._m2crypto # # all unconditional, with no `try/except ImportError' anywhere on the path. If # `_m2crypto.cpython-312.so' does not load, `import M2Crypto' raises and NOTHING # in the package is reachable. `m2crypto/import' below is therefore the pivot, # and every behavioural case after it is downstream of that one answer. # # TWO DEFECTS HAD TO CLOSE BEFORE THAT IMPORT COULD SUCCEED, and both did, # within the week before this port: # # * BRINTOS-2046 (Done 2026-09-02, meta-brintos 4994fba2) -- the interpreter # exported 35 symbols and not one was `Py*', so every dlopen'd extension was # refused with `policy -> ELIBBAD -80'. It now publishes 1715, derived from # CPython's own PyAPI_FUNC/PyAPI_DATA declarations. # * BRINTOS-2323 (Done 2026-09-05) -- with the symbols exported, an extension # BOUND and then trapped at first import: `hwjs-user-rt VIOLATION[lane 0]: # set-over-set', rc=132, because a dynamic main published no suspend # partition for the API it exports. Its own measurement is `99-python3-crc32c' # going 28 pass / 4 fail / 44 skip -> 76 / 0 / 0. # # So this suite is written expecting the import to WORK. That expectation is a # PREDICTION, not a measurement -- see the header block of the test-plan section # BRINTOS-2401 for what was and was not measured before the image existed. # # ============================================================================ # THE EXPECTED VALUES ARE MEASURED, AND NOT AGAINST THIS ARM. # ============================================================================ # # Every literal the probe cases below assert was produced by RUNNING this exact # probe against a host build of M2Crypto 0.40.1 -- x86_64, glibc, openssl # 3.0.13, swig 4.2.1 from this build's own swig-native sysroot. That build is # not the target and is not offered as evidence about it; what it establishes is # that the expectations are M2Crypto's real answers rather than a reading of its # source. It is also what caught the probe's own bug, for which see the probe's # `ONE LOADED KEY AND ONE LOADED CERTIFICATE' header: `str(cert.get_subject())' # on a temporary certificate SEGFAULTS, because X509_Name carries a borrowed # pointer and no reference to the X509 it came from. # # THREE KEYS ARE ARM- OR IMAGE-DEPENDENT and are asserted against this image # rather than against the host run: `openssl-version-text', `-number' and # `-triple'. The host reported openssl 3.0.13; the image ships poky's openssl # (meta/recipes-connectivity/openssl, BRINTOS-295), so the three are asserted # against the openssl package's dpkg Version on this rootfs, not a literal. The # case that matters is `openssl-version-agrees', which compares what the binding # reports with what /usr/bin/openssl on THIS ROOTFS reports -- a binding linked # against some other host's sysroot copy fails that one and passes the literal. # # ============================================================================ # NO NETWORK, AND NO KEY GENERATION. # ============================================================================ # # The SSL layer is constructed and torn down; nothing connects and no socket is # created (SSL.Connection's constructor makes one, so it is deliberately not # exercised). The RSA key, its public half and the certificate are committed # fixtures beside the probe: generating a 2048-bit key would be minutes on this # arm, and a generated key would make every signature assertion a tautology -- # the probe would be checking M2Crypto against itself. As committed, the # expected signature is a literal openssl(1) produced. P=python3-m2crypto require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/M2Crypto DI=$SITE/M2Crypto-0.40.1.dist-info SO=$PKGDIR/_m2crypto.cpython-312.so PROBE=$BST_DATA/python3-m2crypto-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-m2crypto.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. The import further down is the functional # statement; these say WHICH copy answered, so a package that imported out of # some other path on sys.path is distinguishable from what this image installed. check $P/site-packages-dir test -d "$SITE" # A PACKAGE DIRECTORY, not a single module. check $P/package-dir test -d "$PKGDIR" check $P/init-file test -s "$PKGDIR/__init__.py" check $P/m2-file test -s "$PKGDIR/m2.py" check $P/m2crypto-shim test -s "$PKGDIR/m2crypto.py" check $P/evp-file test -s "$PKGDIR/EVP.py" check $P/rsa-file test -s "$PKGDIR/RSA.py" check $P/x509-file test -s "$PKGDIR/X509.py" check $P/bio-file test -s "$PKGDIR/BIO.py" check $P/asn1-file test -s "$PKGDIR/ASN1.py" check $P/rand-file test -s "$PKGDIR/Rand.py" check $P/smime-file test -s "$PKGDIR/SMIME.py" # The SSL layer is a SUB-PACKAGE, which is a different shape from the rest and # the one a `--exclude' in packaging would silently drop. check $P/ssl-subpackage-dir test -d "$PKGDIR/SSL" check $P/ssl-context-file test -s "$PKGDIR/SSL/Context.py" check $P/ssl-connection-file test -s "$PKGDIR/SSL/Connection.py" check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -s "$DI/METADATA" # THE EXTENSION, as a file. This is the whole difference between this port and # every pure-Python one beside it, so its presence is asserted directly rather # than inferred from the import succeeding. check $P/cext-present test -s "$SO" # ...and it is a WebAssembly module, not a host object and not an empty stub. # The first four bytes of a wasm binary are \0asm; `od' is reached through a # pipe, so the read is a forked child's and cannot end this suite (see # bst_local). check_eq $P/cext-is-wasm "0061736d" \ "$(head -c 4 "$SO" 2>/dev/null | od -An -tx1 | tr -d ' \n')" # A SIDE MODULE, which is what `dylink.0' names. A wasm object built as a normal # static input would carry no such section and would be exactly as unloadable as # a missing file, while passing the magic-number case above. check_out $P/cext-dylink "dylink" \ sh -c "head -c 64 '$SO' | tr -d '\\000' " # EXACTLY ONE. setup.py declares a single Extension, `M2Crypto._m2crypto' # (setup.py:296), so a build that started producing more, or fewer, is visible # rather than silent. check_eq $P/cext-count "1" \ "$(find "$PKGDIR" -name '*.so' 2>/dev/null | wc -l | tr -d ' ')" # THE SWIG SHIM SHIPS AS PYTHON, not as generated C left in the package. swig # writes M2Crypto/m2crypto.py beside the extension and the package is unusable # without it, so a build that ran the compiler but lost the -outdir product # fails here rather than at import. check_out $P/shim-imports-cext "_m2crypto" \ sh -c "head -c 4096 '$PKGDIR/m2crypto.py'" # ------------------------------------------------------------- the import --- # THE PIVOT. Everything below is downstream of this one answer; see the header # for the two defects that had to close before it could succeed. check $P/import python3 -c 'import M2Crypto' # ...and it is QUIET. A module that imports while printing to stderr is a # different result from one that imports cleanly, and a warning here would most # likely be the loader saying something the exit status hides. check_eq $P/import-quiet "" \ "$(python3 -c 'import M2Crypto' 2>&1)" # ---------------------------------------------------------------- the probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present check $P/key-fixture-present test -s "$BST_DATA/python3-m2crypto-key.pem" check $P/cert-fixture-present test -s "$BST_DATA/python3-m2crypto-cert.pem" check $P/pub-fixture-present test -s "$BST_DATA/python3-m2crypto-pub.pem" # ONE interpreter start for everything that follows. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. bst_probe $P/probe-run "$W/probe" 1100 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken library, when what actually happened is that no measurement was # taken. The word `unscorable' is PARSED -- brintos_local_verify re-measures a # suite that prints it on a fresh guest rather than trusting a shared boot's # numbers -- so it goes in this structural check, not a comment. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # THE IMPORT, AS THE PROBE SAW IT. If the extension did not load, this is the # one line carrying the exception text, and every case after it is that same # cause repeated. Empty is the pass. check_eq $P/probe-import-error "" "$(pv import_error)" # ...and it ran to the END. Every key is emitted in file order, so a probe that # died two thirds of the way through would leave the earlier cases green and the # later ones reading got:'' -- which looks like a partial library rather than a # partial run. This is the case that tells those apart. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "95" "$(pv emitted_count)" # ------------------------------------------------------------------ identity --- # The module answered out of the tree this image installed, not a stray copy. check_eq $P/module-file "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/version "0.40.1" "$(pv version_attr)" # __version__ and version are two names for one string (__init__.py:21-22); a # release that let them drift is a packaging error worth seeing. check_eq $P/version-dunder "0.40.1" "$(pv version_dunder)" # ...and the DIST metadata agrees with the module. These come from different # places -- the wheel's METADATA against a literal in the source -- so a .deb # built from one version's sdist over another's dist-info fails here. check_eq $P/dist-version "0.40.1" "$(pv dist_version)" # `m2' is the pure-Python namespace shim; the EXTENSION is what # `M2Crypto._m2crypto' resolves to. The payload cases above say the file # shipped; this says the RUNNING PROCESS loaded that file. check_eq $P/m2-module-file "$PKGDIR/m2.py" "$(pv cext_file)" check_eq $P/cext-module-file "$SO" "$(pv cext_module_file)" # ------------------------------------------------------------- the ENGINE gap --- # THE ONE PLACE THIS PORT DIVERGES, ASSERTED RATHER THAN ASSUMED. This image's # openssl is built `no-engine' by this layer's own openssl_%.bbappend -- the # ENGINE API is dlopen-shaped and `no-dso' is already set, so it could only ever # load the built-ins -- and a `no-engine' build ships with its # whole contents behind `#ifndef OPENSSL_NO_ENGINE'. M2Crypto's # `src/SWIG/_m2crypto.i:99' includes `_engine.i' unconditionally, so the stock # recipe generates C calling sixteen undeclared ENGINE_* functions and # do_compile dies. python3-m2crypto_%.bbappend guards the include and passes # swig `-DOPENSSL_NO_ENGINE'. # # What that costs is exactly M2Crypto.Engine. Nothing else in the package # imports it and its module scope calls m2.engine_init_error on its first line # of executable code (Engine.py:19), so it raises there rather than degrading -- # which on an image with no DSO loader is the honest state and not a loss. # # THESE TWO CASES ARE WHAT MAKE THAT A CHECKED FACT. The file still ships, so # `engine-module-file' passing beside `engine-import' NOT importing is the # expected shape; the day this arm gains ENGINE both flip and the bbappend is # re-read instead of quietly outliving its reason. check $P/engine-module-file test -s "$PKGDIR/Engine.py" check_eq $P/m2-no-engine-wrappers "no" "$(pv m2_has_engine)" check_eq $P/engine-import "AttributeError" "$(pv engine_import)" # ------------------------------------------------------------------- openssl --- # THE VERSION THE BINDING REPORTS, and the reason this is three cases rather # than one. The package's dpkg Version says which openssl this image carries; # the `agrees' case says the binding is bound to THAT one and not to a copy from # some other host's sysroot, which is the failure the version alone cannot see. ossl_v=$(sed -n '/^Package: openssl$/,/^$/s/^Version: //p' /var/lib/dpkg/status 2>/dev/null) ossl_v=${ossl_v#*:}; ossl_v=${ossl_v%%-*} case "$ossl_v" in [0-9]*.[0-9]*.[0-9]*) ossl_n=$(IFS=.; set -- $ossl_v; printf '0x%x%02x%04x0' "$1" "$2" "$3") ;; *) ossl_v=''; ossl_n='' ;; esac check_out $P/openssl-version-text "OpenSSL $ossl_v " sh -c "grep '^openssl_version_text=' '$OUT'" check_eq $P/openssl-version-number "$ossl_n" "$(pv openssl_version_number)" check_eq $P/openssl-version-triple "$ossl_v" "$(pv openssl_version_triple)" # /usr/bin/openssl's own answer, from this rootfs, against the binding's. check_eq $P/openssl-version-agrees \ "$(openssl version 2>/dev/null | cut -d' ' -f1-2)" \ "$(pv openssl_version_text | cut -d' ' -f1-2)" # --------------------------------------------------------------- EVP digests --- # PUBLISHED VECTORS, not round trips: FIPS 180-2 and RFC 1321 answers for "abc" # and for the empty string. A round trip would pass against a digest that is # merely self-consistent. check_eq $P/md-sha1-abc \ "a9993e364706816aba3e25717850c26c9cd0d89d" "$(pv md_sha1_abc)" check_eq $P/md-sha256-abc \ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" "$(pv md_sha256_abc)" check_eq $P/md-sha512-abc \ "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f" \ "$(pv md_sha512_abc)" check_eq $P/md-md5-abc \ "900150983cd24fb0d6963f7d28e17f72" "$(pv md_md5_abc)" check_eq $P/md-sha256-empty \ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" "$(pv md_sha256_empty)" # Three update() calls must agree with one -- the property a streaming digest # exists for, and the one a wrapper that re-inits per update() breaks. check_eq $P/md-sha256-chunked \ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" "$(pv md_sha256_chunked)" # ...and against the interpreter's own hashlib, which is a SECOND implementation # in the same process and does not share this code path. check_eq $P/md-agrees-hashlib "yes" "$(pv md_sha256_agrees_hashlib)" # An algorithm name OpenSSL does not know must RAISE, not answer with a digest # of something else. check_eq $P/md-unknown-alg "EVPError" "$(pv md_unknown_alg)" # ------------------------------------------------------------------- EVP HMAC --- # RFC 4231 test cases 1 and 2 for HMAC-SHA-256, RFC 2202 case 1 for HMAC-SHA-1. check_eq $P/hmac-sha256-rfc4231-1 \ "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" "$(pv hmac_sha256_rfc4231_1)" check_eq $P/hmac-sha256-rfc4231-2 \ "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843" "$(pv hmac_sha256_rfc4231_2)" check_eq $P/hmac-sha1-rfc2202-1 \ "b617318655057264e28bc0b6fb378c8ef146be00" "$(pv hmac_sha1_rfc2202_1)" # EVP.hmac() and EVP.HMAC are two entry points to one primitive. check_eq $P/hmac-oneshot-agrees "yes" "$(pv hmac_oneshot_agrees)" # A changed key must change the tag. A MAC that ignored its key would pass every # vector above by returning the vector's own answer. check_eq $P/hmac-key-sensitive "yes" "$(pv hmac_key_sensitive)" # ---------------------------------------------------------------- EVP ciphers --- # NIST SP 800-38A F.2.1 and F.2.5 -- one block, padding disabled, so the answer # is the published ciphertext and not an artifact of the padding scheme. check_eq $P/aes128-cbc-sp800-38a \ "7649abac8119b246cee98e9b12e9197d" "$(pv aes128cbc_sp800_38a)" check_eq $P/aes256-cbc-sp800-38a \ "f58c4c04d6e5f1ba779eabfb5f7bfbd6" "$(pv aes256cbc_sp800_38a)" # The decrypt direction against the same vector, so a cipher that is wrong in # one direction only is caught. check_eq $P/aes128-cbc-decrypt-sp800-38a \ "6bc1bee22e409f96e93d7e117393172a" "$(pv aes128cbc_decrypt_sp800_38a)" # A PADDED round trip over 56 bytes -- not a block multiple, which is what the # padding path exists for. PKCS#7 rounds it up to 64. check_eq $P/aes256-cbc-roundtrip "yes" "$(pv aes256cbc_roundtrip)" check_eq $P/aes256-cbc-ct-len "64" "$(pv aes256cbc_ct_len)" check_eq $P/aes128-cbc-roundtrip "yes" "$(pv aes128cbc_roundtrip)" # CTR is a STREAM mode: the ciphertext is the plaintext's length, not padded up. # The pair of length cases is what distinguishes the two modes having actually # been selected from one mode answering twice. check_eq $P/aes256-ctr-roundtrip "yes" "$(pv aes256ctr_roundtrip)" check_eq $P/aes256-ctr-ct-len "56" "$(pv aes256ctr_ct_len)" # A one-bit change in the key must not decrypt to the plaintext. Either a raise # on the padding check or a wrong answer counts; returning the plaintext does # not. check_eq $P/aes256-cbc-wrong-key-refused "yes" "$(pv aes256cbc_wrong_key_refused)" # ------------------------------------------------------------- the fixtures --- # THE BYTES THE PROBE READ, hashed. Every RSA and X.509 literal below is about # these two files, so a fixture that was regenerated, re-encoded or truncated in # packaging turns the whole block red for a reason nobody could otherwise name. check_eq $P/key-fixture-bytes \ "96e7353b59e873bc95926fd1e455cb9fe22e001df3d8ce79cc5ca91cd98a5e5d" "$(pv key_pem_sha256)" check_eq $P/cert-fixture-bytes \ "7301782b46ea5ebd4d53633b81808af4ed2ad18a0c394f504e92e5377a6e5a8c" "$(pv cert_pem_sha256)" # ------------------------------------------------------------------- RSA keys --- check_eq $P/rsa-load "yes" "$(pv rsa_load)" check_eq $P/rsa-bits "2048" "$(pv rsa_bits)" # RSA_check_key over the CRT parameters -- the private key is internally # consistent, not merely parseable. check_eq $P/rsa-check-key "1" "$(pv rsa_check_key)" # `e' and `n' come back in OpenSSL's MPINT encoding: a 4-byte big-endian length # then the bytes. 65537 is 01 00 01, so `e' is seven bytes and reads exactly # thus; `n' is 4 + 257, the extra byte being the leading zero a 2048-bit modulus # with its top bit set needs to stay unsigned. Asserting the ENCODING and not # just the value is what makes a change to that convention visible. check_eq $P/rsa-e "00000003010001" "$(pv rsa_e)" check_eq $P/rsa-n-len "261" "$(pv rsa_n_len)" # The committed public half is the private key's public half. The two files are # a pair and nothing else says so. check_eq $P/rsa-pub-matches-priv "yes" "$(pv rsa_pub_matches_priv)" # ---------------------------------------------------------- RSA sign / verify --- check_eq $P/msg-sha256 \ "e11c615eebd83e0d3f133ad3eec54c8375c5b60a2a6bca704043941831fd80ce" "$(pv msg_sha256)" check_eq $P/rsa-sig-len "256" "$(pv rsa_sig_len)" # PKCS#1 v1.5 IS DETERMINISTIC, so the signature has ONE correct value -- and # this literal is the digest of the signature `openssl dgst -sha256 -sign' # produced from the same committed key. A sign/verify round trip alone would # pass against a binding that signed and verified consistently WRONG; this is # the case that would not. check_eq $P/rsa-sig-sha256 \ "5fb0aee06a0ac30b8f4b236ed3922ccf72236e22b9d4f14a653886547ae6a020" "$(pv rsa_sig_sha256)" check_eq $P/rsa-verify "1" "$(pv rsa_verify)" # ...and the PUBLIC half alone verifies it, which is the property that matters. check_eq $P/rsa-verify-with-pub "1" "$(pv rsa_verify_with_pub)" # A TAMPERED SIGNATURE MUST BE REFUSED. Without this the verify cases above say # only that the function returns 1, not that it discriminates. check_eq $P/rsa-tampered-sig "refused" "$(pv rsa_tampered_sig)" # ...and so must a signature offered against a different digest. check_eq $P/rsa-wrong-digest "refused" "$(pv rsa_wrong_digest)" # ...and one made under sha1 offered as sha256: the DigestInfo prefix differs, # so a verifier that ignored the algorithm would accept it. check_eq $P/rsa-alg-mismatch "refused" "$(pv rsa_alg_mismatch)" # Public-key encrypt -> private-key decrypt, OAEP. check_eq $P/rsa-oaep-roundtrip "yes" "$(pv rsa_oaep_roundtrip)" check_eq $P/rsa-oaep-ct-len "256" "$(pv rsa_oaep_ct_len)" # THE EVP.PKey PATH over the same key -- a different entry point that digests # and signs in one operation where RSA.sign() takes a pre-computed digest. Both # are PKCS#1 v1.5 over sha256, so they must produce the SAME bytes, and this # pair of cases is what says the two paths agree rather than each being # separately plausible. check_eq $P/pkey-sig-sha256 \ "5fb0aee06a0ac30b8f4b236ed3922ccf72236e22b9d4f14a653886547ae6a020" "$(pv pkey_sig_sha256)" check_eq $P/pkey-sig-agrees-rsa "yes" "$(pv pkey_sig_agrees_rsa)" # --------------------------------------------------------------------- X.509 --- check_eq $P/cert-load "yes" "$(pv cert_load)" # get_version() is the ENCODED version, zero-based: 2 is X.509 v3. check_eq $P/cert-version "2" "$(pv cert_version)" # The serial is "Brintos2401" as an 11-byte big-endian integer, which is how it # was set, and reading it back as that decimal is the assertion. check_eq $P/cert-serial "80329398740849386612600881" "$(pv cert_serial)" # X509_NAME_oneline form, leading slash, RDNs in encoding order. check_eq $P/cert-subject \ "/C=RU/ST=Moscow/L=Moscow/O=Emcraft Systems/OU=brintOS/CN=brintos-selftest-m2crypto" \ "$(pv cert_subject)" check_eq $P/cert-issuer \ "/C=RU/ST=Moscow/L=Moscow/O=Emcraft Systems/OU=brintOS/CN=brintos-selftest-m2crypto" \ "$(pv cert_issuer)" # ...and the individual RDNs through X509_Name's attribute access, which is a # by-NID lookup and not a substring of the line above. check_eq $P/cert-subject-cn "brintos-selftest-m2crypto" "$(pv cert_subject_cn)" check_eq $P/cert-subject-o "Emcraft Systems" "$(pv cert_subject_o)" check_eq $P/cert-subject-ou "brintOS" "$(pv cert_subject_ou)" check_eq $P/cert-subject-c "RU" "$(pv cert_subject_c)" check_eq $P/cert-self-signed-name "yes" "$(pv cert_self_signed_name)" # The validity dates, in ASN1_TIME_print's format -- two spaces before a # single-digit day, which is the detail a re-implementation gets wrong. check_eq $P/cert-not-before "Sep 7 23:16:42 2026 GMT" "$(pv cert_not_before)" check_eq $P/cert-not-after "Sep 4 23:16:42 2036 GMT" "$(pv cert_not_after)" # FIVE extensions: basicConstraints, keyUsage, subjectAltName, # subjectKeyIdentifier and the authorityKeyIdentifier openssl adds itself. check_eq $P/cert-ext-count "5" "$(pv cert_ext_count)" check_eq $P/cert-ext-san \ "DNS:selftest.brintos.invalid, DNS:m2crypto.brintos.invalid" "$(pv cert_ext_san)" check_eq $P/cert-ext-basic "CA:TRUE, pathlen:0" "$(pv cert_ext_basic)" check_eq $P/cert-ext-keyusage "Digital Signature, Certificate Sign, CRL Sign" \ "$(pv cert_ext_keyusage)" check_eq $P/cert-ext-ski \ "9F:44:88:67:51:1A:42:68:28:61:95:50:F6:27:23:08:97:9D:01:3C" "$(pv cert_ext_ski)" # CRITICALITY IS PART OF THE EXTENSION, not decoration: two extensions were set # critical and one was not, and a parser that lost the flag would answer the # values above correctly and still be wrong. check_eq $P/cert-ext-basic-critical "yes" "$(pv cert_ext_basic_critical)" check_eq $P/cert-ext-san-critical "no" "$(pv cert_ext_san_critical)" # The DER the parse re-encodes must be the DER that went in -- byte length and # digest, both. check_eq $P/cert-der-len "1070" "$(pv cert_der_len)" check_eq $P/cert-der-sha256 \ "1afb1de4468125e937f5b7677a94402d221f428b411ed9ef8e5c294381ebc341" "$(pv cert_der_sha256)" check_eq $P/cert-der-roundtrip "yes" "$(pv cert_der_roundtrip)" # The public key the certificate carries is the committed key's. check_eq $P/cert-pubkey-bits "2048" "$(pv cert_pubkey_bits)" # SELF-SIGNATURE. It is self-signed, so it verifies under its own public key and # no trust store is needed -- which is the point: this asserts the signature # arithmetic, not a chain policy. check_eq $P/cert-verify-self "1" "$(pv cert_verify_self)" # An extension nobody put in must raise LookupError, not answer. check_eq $P/cert-ext-missing "LookupError" "$(pv cert_ext_missing)" # HALF A CERTIFICATE MUST BE REFUSED, not parsed into a partial one. check_eq $P/cert-truncated-refused "X509Error" "$(pv cert_truncated_refused)" # The WRITE side of X509_Name: one built from scratch renders in the same form. check_eq $P/name-build "/C=RU/CN=built-here" "$(pv name_build)" # ----------------------------------------------------------------------- BIO --- # A memory BIO written and read back. No file, no socket. check_eq $P/bio-roundtrip "brintOS memory bio" "$(pv bio_roundtrip)" check_eq $P/bio-len "10" "$(pv bio_len)" # A read past the end is EMPTY -- not an error, and not the payload again, which # is what a buffer that never advanced its read pointer would give. check_eq $P/bio-drained "empty" "$(pv bio_drained)" # And the BIO is what the PEM readers actually consume: a certificate loaded # THROUGH a memory BIO must be the same certificate. check_eq $P/bio-feeds-x509 "yes" "$(pv bio_feeds_x509)" # ----------------------------------------------------------------------- SSL --- # CONSTRUCTED AND TORN DOWN, connecting nowhere. There is no peer on this image # and this suite uses no network. What is measured is that the context can be # built at all -- it is the layer that pulls in the most of libssl, so a binding # that is linked but not wired fails here rather than somewhere subtler. check_eq $P/ssl-ctx-verify-mode "0" "$(pv ssl_ctx_verify_mode)" check_eq $P/ssl-ctx-cipher-list "yes" "$(pv ssl_ctx_cipher_list)" # ...and it accepts the committed certificate and key as its own credentials, # which is the one operation that crosses the SSL layer into X.509 and RSA. check_eq $P/ssl-ctx-load-cert "yes" "$(pv ssl_ctx_load_cert)" # A protocol name libssl does not know must raise. check_eq $P/ssl-ctx-unknown-proto "ValueError" "$(pv ssl_ctx_unknown_proto)" # ------------------------------------------------------------------ randomness --- # Rand is the entropy source everything above sits on, and on this arm it is # worth its own cases: a source stuck at a constant would leave every symmetric # and signature case here passing, since all of those use committed inputs. check_eq $P/rand-len "32" "$(pv rand_len)" check_eq $P/rand-varies "yes" "$(pv rand_varies)" check_eq $P/rand-status "1" "$(pv rand_status)" rm -rf "$W"