scanning directories for unit tests...
scanning present working directory for unit tests...
shuffled the files with seed 571422038 (replay with VERITAS_SEED=571422038)
found unit tests: (bewaking-spec.scm crypto-spec.scm cli-spec.scm web-spec.scm password-spec.scm)
running unit tests from file: bewaking-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 571422038 (replay with VERITAS_SEED=571422038)
+----------------------------------------------+
[📑 suite] Sealed envelope (versioned, authenticated)
[🧪 test] tokens and objects are envelopes and reject tampering
[⚙️ assert] generated tokens are enveloped
✅ got: #t
[⚙️ assert] sealed objects are enveloped
✅ got: #t
[⚙️ assert] a tampered token is rejected
✅ got: #t
[⚙️ assert] a tampered object is rejected
✅ got: #t
[🧪 test] key rotation with a keyring
[⚙️ assert] a keyring seals and opens by key id
✅ got: #t
[🧪 test] no reader is left for the earlier formats
[⚙️ assert] a BWK1 envelope is not recognised
✅ got: #f
[⚙️ assert] a BWK2 envelope is not recognised
✅ got: #f
[⚙️ assert] a BWK2 envelope with no expiry is not recognised
✅ got: #f
[⚙️ assert] no metadata is read out of a BWK1 envelope
✅ got: #f
[⚙️ assert] no metadata is read out of a BWK2 envelope
✅ got: #f
[⚙️ assert] an unreadable envelope is never reported as live
✅ got: #f
[⚙️ assert] a BWK2 token is refused even with the keys that sealed it
✅ got: #f
[⚙️ assert] a BWK1 object is refused likewise
✅ got: #f
[🧪 test] the requested key id is embedded in generated tokens
[⚙️ assert] the token carries the key id
✅ got: #t
[🧪 test] data that is not a BWK3 envelope is refused
[⚙️ assert] arbitrary text is not an envelope
✅ got: #f
[⚙️ assert] an unknown envelope version is not recognised
✅ got: #f
[⚙️ assert] a truncated envelope is refused when it is opened
✅ got: #t
[⚙️ assert] recognition is a version-tag check, not a structure check
✅ got: #t
[⚙️ assert] a BWK3 envelope carrying an extra MAC field is refused
✅ got: #t
[⚙️ assert] an unsupported version raises rather than being guessed at
✅ got: #t
[⚙️ assert] a pre-versioning bare Base16 token is refused
✅ got: #f
[⚙️ assert] a bare Base16 object is refused
✅ got: #f
[🧪 test] seal-envelope and open-envelope
[⚙️ assert] the result is a versioned envelope
✅ got: #t
[⚙️ assert] the envelope starts with the version tag
✅ got: #t
[⚙️ assert] the module exposes the current format version
✅ got: BWK3
[⚙️ assert] BWK3 is the only version produced and read
✅ got: (BWK3)
[⚙️ assert] the plaintext round-trips
✅ got: top secret payload
[⚙️ assert] a tampered envelope fails the integrity check
✅ got: #t
[⚙️ assert] a wrong signing key fails the cipher check
✅ got: #t
[⚙️ assert] passing a MAC key to open-envelope is refused, not ignored
✅ got: #t
[🧪 test] knowing the format is not knowing the plaintext
[⚙️ assert] the plaintext, its Base16 and its Base64 do not appear in the envelope
✅ got: #t
[⚙️ assert] the format is not hidden, only the content is
✅ got: #t
[⚙️ assert] the public metadata is exactly what is authenticated
✅ got: (version key-id created-at expires-at)
[🧪 test] the same plaintext never seals to the same value
[⚙️ assert] a fresh salt and nonce make each envelope unique
✅ got: #t
[⚙️ assert] equal objects seal to different strings
✅ got: #f
[🧪 test] a refusal never says which key id the keyring configures
[⚙️ assert] the refusal is a refusal, not an accidental success
✅ got: #t
[⚙️ assert] an unknown key id and a tampered body fail identically
✅ got: (misc-error #f ~A (bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.) #f)
[⚙️ assert] a keyring with no matching entry fails identically
✅ got: (misc-error #f ~A (bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.) #f)
[⚙️ assert] a retired key fails identically
✅ got: (misc-error #f ~A (bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.) #f)
[⚙️ assert] a key that is not active yet fails identically
✅ got: (misc-error #f ~A (bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.) #f)
[⚙️ assert] the refusal does not name the key id it could not resolve
✅ got: #f
[⚙️ assert] and it does not name a configured key id either
✅ got: #f
[⚙️ assert] nor does it carry any key material
✅ got: #f
[⚙️ assert] an expired genuine envelope is still told apart, since it authenticated first
✅ got: #f
[🧪 test] editing a genuine envelope never yields a plaintext
[⚙️ assert] every single-digit mutation of a real envelope is refused
✅ got: #t
[⚙️ assert] and every mutation of a real sealed object too
✅ got: #t
+----------------------------------------------+
[📑 suite] Payload primitives
[🧪 test] token-payload-valid?
[⚙️ assert] a decrypted token payload is valid
✅ got: #t
[⚙️ assert] a missing token type is invalid
✅ got: #f
[⚙️ assert] an empty payload is invalid
✅ got: #f
[🧪 test] unsafe-generate-token
[⚙️ assert] incomplete owner details raise instead of returning #f
✅ success catching error: misc-error: (#f Could not generate token from given payload! () #f)
[⚙️ assert] valid details produce a token string
✅ got: #t
[🧪 test] encrypt-payload and decrypt-payload
[⚙️ assert] a payload round-trips under the same key
✅ got: mysecretdata
[⚙️ assert] a supplied salt makes the output reproducible
✅ got: U2FsdGVkX18BAQEBAQEBAbwE/xss9SSpZh5gLj72hSU=
[⚙️ assert] a different key does not open the payload
✅ got: #t
+----------------------------------------------+
[📑 suite] Permissions
[🧪 test] has-needed-permissions?
✅ got: #f
✅ got: #t
✅ got: #f
✅ got: #t
✅ got: #t
✅ got: #t
✅ got: #f
✅ got: #f
+----------------------------------------------+
[📑 suite] Lossless Token Decryption
[🧪 test] lossless-token
owner-details: ((id . "444959d3-7a8e-455e-b608-31e35145a2fa") (email . "someone@jointhefreeworld.org") (name . "Legolas son of Thandruil") (permissions byggsteg:modify-permissions byggsteg:job-create byggsteg:job-delete byggsteg:profile-create byggsteg:profile-delete byggsteg:user-create byggsteg:user-delete))
generate-token: BWK3.default.1794340808.-.01cb223273ca5d26ace3d723614598abe590439e90a035ea09384b73f0a2e1ac96dee013ad83cf4947bd676ce8c0a3d2a394a3abac2ad9b80df26fd7ba0041fc8d083a6777a3afcdf038fe657c2b8245358bb3695bd709f5e60d1489bdd003e24aaa989719546ff5ebe65c53420ce744c21a743293b5f894c32b8fb190ecfd115715ad4778a15d4b1ee7725430707e589b37bad8703026fd96fc803b0e241147d089eee0fb0214c61e4146d2a57d2e2271be06e8cbaa8cb8a5553bef133d92f353edf60b8305f205b0149531a85924d4d072f7d0825272de39993351eb2c31ce2fc0f036abb281e8f118a165666f60c0d7ab57d2082d07a0b408813ece09c4c448246504b6b092c76079a9415a8e5817a6ba2aa5c8e790ad74cd2d682a2ed3f7a031ffe81bf6dc34209e09b8c95d5503eed16396223b1171037548e66e7d548819a85fd40407d90aff81fc5660e3c470238aefd7e8a178d5aba45f57d1afdbe0cd642cddfc1c1fe266fe5ba3a03cf679c13b82e07e5b2e6541ef7bbb1edb48367794b223ac426f967e2b5cffec811d6e9fabed29b90a57a447c9e018219f5be98f407cae31ed7ec58596e70def267cad5865c223ae700878ff5091eabb3199e460cc28d2a355e8119322ecd7e21bcf0ba9fcd52ed5a1f7724413fd41260c1b808074e4027d5ad4d254b0a219359b854b15e6611fc84ca5c42e477aa0286a370d694c956f01974e2d46493e3b1f2b20e3f706019b3113806c8ce3716f3771f7a7452ef900a1b3d0eeef209a21e903b54e8972381c9fda240c3cdd1c8493228b420c8913017b9afbf41d1fe08bff3ddd6a171c33e3da61caa2054b03037d76903218875e3394aba4f0776de297f4725db570be82fdff162cae8cac217798ac63ac1e536637f056f6dde287aeb921aceed043d5f3eb7915a19a1250f79704f1dfcfa833a2b79aad8c3ff18c6fb352b01afef3ab510f38ef72886849f4c0b210cd7d3d960f1cc9715f47ddfb95cb85afcf25414c9e6c80daa4bb9dcc3dce0de1bf8c380b0e5b59a737c15132f3b502adfbfe84b9c2ac2a8efe25925aca983ea6ea833e60ff9ae4d11a5bbd184dc92741b066baeb611d178d773d554eca6f914320c6e8b6ee21d120f7505b939c34101e3d7c613dff2251cf0306a297dfdd8df9e574decf42907269b30989efcc8b319d5969296f9bca07582daf12adbf097314a4dc0d441243637a0ba80abd295fd59c2973da27d686539c759d3e052982f30ef37df61004c4e1ce11dcc4e3c18e59912e02866f3bf0aa0df1d51e
decoded: ((token-type . bewaking-token) (token-id . 52b575d0-4bcf-4fab-b956-f73cc4c2b664) (token-created-at . 1794340808) (token-expires-at . 1794945608) (owner-id . 444959d3-7a8e-455e-b608-31e35145a2fa) (owner-email . someone@jointhefreeworld.org) (owner-name . Legolas son of Thandruil) (owner-permissions byggsteg:modify-permissions byggsteg:job-create byggsteg:job-delete byggsteg:profile-create byggsteg:profile-delete byggsteg:user-create byggsteg:user-delete))
[⚙️ assert] works properly with correct details
✅ got: #t
+----------------------------------------------+
[📑 suite] Token validity helpers
[🧪 test] token-remaining-seconds and token-valid?
[⚙️ assert] remaining seconds is positive for a live token
✅ got: #t
[⚙️ assert] remaining seconds is negative for an expired token
✅ got: #t
[⚙️ assert] a missing expiry raises an error
✅ success catching error: misc-error: (#f token-remaining-seconds: Token is invalid. Missing or malformed 'token-expires-at' () #f)
[⚙️ assert] a freshly generated token is valid
✅ got: #t
[⚙️ assert] a freshly generated token is not expired
✅ got: #f
+----------------------------------------------+
[📑 suite] generate-token
[🧪 test] works properly with correct details
owner-details: ((id . "444959d3-7a8e-455e-b608-31e35145a2fa") (email . "someone@jointhefreeworld.org") (name . "Legolas son of Thandruil") (permissions byggsteg:modify-permissions byggsteg:job-create byggsteg:job-delete byggsteg:profile-create byggsteg:profile-delete byggsteg:user-create byggsteg:user-delete))
generate-token: BWK3.default.1794340808.-.0133e423ae7fb843dc4f7137ff2e5b5d3d4ef5ff65bc47896b3925103e007e4944cd308210072ad51f008a6fb867d14e10ccda90803551c8618044edd09f6f7cbebdd5913393e76b9335b1be7080568e6badf565e49bc38b0492d152f80b0476d87e1687b295dc5abd994637ba51afdc131ced555cc32fe17022409c8aef94f5df006ea9ec2cb1756297a38961370c4a2bd9bc16c43ee773321e80c891f9509a9b8869f1b615bf3eb9f0473dfe6d91a5890e0c206af0384d2c553d702015877897858ecb685c6b1ec035f087add4ba4bb6858d2322a336b0982bd7407f3b84dc14a31dde91772f28d6b6ac300a919bbf0367f0d0b44989cd6068790369796aeb39d83311d7e1a4e1a18389218d287665683ac5cda1bc032531b8c2ec3a81e10fe87976d36a57be8d6e19f12b41f0d4c76162969ff89c7a2717c059d310954d5c601bf7e78bc0ba3dc2e28d0730ccf0530d7f542934445ca29d3dade5b135fa27e9db6604dd132ff26a5755746c7f5146c5b398657de7192bb1fb6353a43650d3446b2cc4ca5c2b848db3b97280e8546d6aa3f7fd6d217ab4c88ef5f4f65e213574679808cd8290570f30c6e8fada83bb780141f01d16d12a939dcc71cb05e4c0c4ecebf05d3a23e2ea1c03e7ace4bd6e31a6fd0d9802368aacd8f80d9448e706d036357dd2a45ab4a9ab5d6e133335ccea6a730dea7b74da0038265f99ccdf9f6e14ab550ad9aca457dbf029a18d8576ac716705314eb89a81bb2c5c03de7144993a46efe51fca90bc11a6c8cb3a3820312d73754b8a6176f1aab18a69aa567aa3def6c78526e12f2c26dd0ff4e50812adb47edf37b3999117bd8ffba5af538b3ee9b3c4c71f2cc64d5392d6ce32a3533a8572eaa71c197f675ac53b44346b5af5f816862bac99f14937b6dc8f32b44e83a0bae30895a47124a38a7331ed08115318d8ff8c3e54b04ceb674b5567ee3b09c986933b6a57d228418d0af65b4def30db36df92ca38d0325505031808dd11ee4b629623643f1c3dc74bf7523994b6c80df8c0b0dcaca1b2ad05de685fcdf67e818970b7c754336c017646c5258624a18e7653d9f64938ee4f5b72f3f076d45980696a80797ec6f47ec145ac1cf50592417c42991e7eedf7e8327aba18435905904c7125a2d0aa8726edcdf45fbe1588eb03ce70140c2ab8237ad3c6f13ea34598c517a53863a961e64e9ce6b85abf7c0a1195df94fec6aa7ac3adb96f1a4b33439c37376a940e2e2487ef43091c9132f9701d7717f656d839d55c2e8797bd7648b0c07668ec2d6d
✅ got: #t
+----------------------------------------------+
[📑 suite] Envelope metadata (key id, created-at, expiry)
[🧪 test] ttl-seconds records an absolute expiry
[⚙️ assert] created-at is the requested timestamp
✅ got: 1000
[⚙️ assert] expires-at is created-at plus the ttl
✅ got: 4600
[⚙️ assert] the envelope is live before its expiry
✅ got: #f
[⚙️ assert] the envelope is expired after its expiry
✅ got: #t
[🧪 test] a malformed envelope is rejected
[⚙️ assert] a missing body is refused
✅ got: #t
[⚙️ assert] a non-numeric created-at is refused
✅ got: #t
[⚙️ assert] a body that is not hexadecimal is refused
✅ got: #t
[⚙️ assert] a truncated body is refused
✅ got: #t
[⚙️ assert] sealing a non-string plaintext is refused
✅ got: #t
[⚙️ assert] sealing without a signing key is refused
✅ got: #t
[⚙️ assert] an empty key id is refused
✅ got: #t
[🧪 test] expiry is enforced when opening
[⚙️ assert] an expired envelope is refused
✅ got: #t
[⚙️ assert] enforce-expiry? #f still opens it
✅ got: ttl payload
[⚙️ assert] max-age-seconds enforces a rotation policy
✅ got: #t
[🧪 test] the metadata is authenticated
[⚙️ assert] editing created-at in a BWK3 envelope breaks its tag
✅ got: #t
[⚙️ assert] editing the key id breaks the tag
✅ got: #t
[⚙️ assert] extending the expiry in the envelope breaks the tag
✅ got: #t
[⚙️ assert] removing the expiry marker breaks the tag
✅ got: #t
[⚙️ assert] relabelling the version is refused
✅ got: #t
[⚙️ assert] backdating created-at breaks the tag
✅ got: #t
[⚙️ assert] editing the body breaks the tag
✅ got: #t
[🧪 test] invalid sealing options are rejected
[⚙️ assert] expires-at and ttl-seconds are mutually exclusive
✅ got: #t
[⚙️ assert] a non-integer ttl-seconds is rejected
✅ got: #t
[⚙️ assert] a key id containing a dot is rejected
✅ got: #t
[🧪 test] expired tokens and objects are refused
[⚙️ assert] a token sealed with a past expiry is refused
✅ got: #t
[⚙️ assert] an object sealed with a past expiry is refused
✅ got: #t
[⚙️ assert] expiry auditing ignores non-envelopes
✅ got: #f
[🧪 test] metadata is reported without any key
[⚙️ assert] created-at is a Unix timestamp
✅ got: #t
[⚙️ assert] the key id is reported
✅ got: default
[⚙️ assert] there is no expiry unless one was requested
✅ got: #f
[⚙️ assert] the format version is reported
✅ got: BWK3
[⚙️ assert] metadata is only read from envelopes
✅ got: #f
[⚙️ assert] an envelope of a dropped format reports no metadata
✅ got: #f
[⚙️ assert] a malformed BWK3 envelope reports no metadata
✅ got: #f
[🧪 test] keyring validity windows enforce rotation
[⚙️ assert] a key inside its window seals and opens
✅ got: #t
[⚙️ assert] a key past not-after cannot open data
✅ got: #t
[⚙️ assert] a key before not-before cannot seal data
✅ got: #t
[⚙️ assert] no keyring is needed to read metadata
✅ got: #t
+----------------------------------------------+
[📑 suite] Deterministic sealing and diagnostics
[🧪 test] an explicit clock drives the age policy
[⚙️ assert] max-age-seconds is measured against the given clock
✅ got: #t
[🧪 test] a failure is reported on standard error
[⚙️ assert] a failing decrypt-token prints nothing on standard output
✅ got:
[⚙️ assert] its diagnosis is written to standard error
✅ got: #t
[🧪 test] a fixed clock and random source make sealing reproducible
[⚙️ assert] two envelopes sealed the same way are identical
✅ got: BWK3.default.1800000000.-.01000000000000000000000000000000000000000000000000000000009da1750a6251b3b6c6b6ba54dbac33b0125ee5be5eb41ba79b5ffade9baf14f12d6a300e0976b3ed8fafb1c425514c2c5a76d0bea37f42cf3c826054
[⚙️ assert] two tokens generated the same way are identical
✅ got: BWK3.default.1800000000.-.01000000000000000000000000000000000000000000000000000000009d8f043c7c6f8e8ae2b1837fbcbf03ce3c44d9bf22a7059e992bd2a9b9b805f91d2046061577adbb9bdcdae0799c99928222bf8b58c4f0d1d649cd180ac6161d169d7fd3784605f7fa5aa62f3fa03df80b6883c6fe8536d72d645111c902842637d281224b99702789b051d292b5f60989572576c0700007b9331fd6087f08731644a0252a70b785c94271b8c1def0178d24f612cfa21b54fe644d4d9f6d23c3959162eb2a0df9fc6daf110e677b7a1a219804488fbb5afb1f3707b63ed09c36d485c3b93bea585d4495ebf6c97b6a4276f4365145aadf305c67fe3b107abbbe756be6859ac321bb55cb4441063f2a1e2ee92dbec7cf943cf49d55a2f376bec33028a94a129765ce6aace2254fdb2d65382757c938bfe81bcba8cdaf4589ae4da4241c7db2cdd4d81ae139e7eaee4a273a28cc03f2251616c90e90cff7426bc3c62d042a21b45225d094a73c1da8c857c40373832cda1416244f7527fa8635fa36ea1eadd86c490db1c2731a738baf676fdead89cbeb3eff85d32fdd840dac65f9c82df6ddf035da266d2f0b1106da6cb05b8dcb2cfef6b1ca1334650e2ecef032a00dc3dd9ee419641260132af7d0f627adc1df35a174cff2d803a746f18622bf5e7a5ab68902346982d7489907c1a918402da33fb92bea0b53ed6575b813d280ab59e508d71c4d6481113ed807fdd7b9832be8d8e794068aa8dc9c
[⚙️ assert] the envelope header records the supplied clock
✅ got: 1800000000
[⚙️ assert] the supplied token id is the one read back
✅ got: 123e4567-e89b-12d3-a456-426655440000
[⚙️ assert] the supplied clock is the payload's creation time
✅ got: 1800000000
+----------------------------------------------+
[📑 suite] Token expiry
[🧪 test] is-token-expired?
[⚙️ assert] a token whose expiry is in the past is expired
✅ got: #t
[⚙️ assert] a token whose expiry is in the future is not expired
✅ got: #f
[⚙️ assert] a far-future token is not expired
✅ got: #f
[⚙️ assert] a missing token-expires-at raises an error
✅ success catching error: misc-error: (#f is-token-expired?: Token is invalid. Missing or malformed 'token-expires-at' () #f)
[⚙️ assert] a malformed token-expires-at raises an error
✅ success catching error: misc-error: (#f is-token-expired?: Token is invalid. Missing or malformed 'token-expires-at' () #f)
+----------------------------------------------+
[📑 suite] Garbling and Ungarbling
[🧪 test] garble-ungarble
✅ got: dlrow
✅ got: lrow
✅ got: row
✅ got: ow
✅ got: w
✅ got: dlrow olleh
✅ got: hello world
✅ got: 7myludvsci59ixumnw54l5v5m81512ap788fe3c5e545e19wu0f2baffrij92ku3zfcm7d9gulk6vqtifiue6x49ej4ncjvuhnkmvx2r4g2106jl80l95s5hvn1f3103
+----------------------------------------------+
[📑 suite] Decryption
[🧪 test] decrypt-token
[⚙️ assert] fails gracefully when signing key is not valid
✅ got: #f
[⚙️ assert] fails gracefully when token is not valid
✅ got: #f
[⚙️ assert] works properly with valid token
✅ got: #t
+----------------------------------------------+
[📑 suite] Generic object encryption
[🧪 test] decrypted data is data, never code
[⚙️ assert] Guile refuses the #. read-eval syntax
✅ got: #t
[⚙️ assert] a sealed payload that looks like code stays a string
✅ got: #.(+ 1 2)
[🧪 test] encrypt-object and decrypt-object
[⚙️ assert] sealing produces a non-empty string
✅ got: #t
[⚙️ assert] round-trips an arbitrary alist payload
✅ got: ((user . joe) (age . 42) (roles admin editor) (tags a b) (nested (x . 1) (y . 2)))
[⚙️ assert] fails gracefully with a wrong signing key
✅ got: #f
[⚙️ assert] round-trips a plain string payload
✅ got: just a string
[⚙️ assert] round-trips a number payload
✅ got: 1234
[⚙️ assert] round-trips a list payload
✅ got: (1 2 3)
ℹ️ info - ran 12 tasks in: 2 seconds, 612007 microseconds
running unit tests from file: crypto-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 571422038 (replay with VERITAS_SEED=571422038)
+----------------------------------------------+
[📑 suite] Argon2id key derivation (RFC 9106)
[🧪 test] invalid parameters are rejected
[⚙️ assert] less than 8 KiB per lane is refused
✅ got: #t
[⚙️ assert] zero passes is refused
✅ got: #t
[⚙️ assert] zero lanes is refused
✅ got: #t
[⚙️ assert] a non-integer memory cost is refused
✅ got: #t
[⚙️ assert] a non-bytevector secret is refused
✅ got: #t
[⚙️ assert] this libgcrypt provides Argon2
✅ got: #t
[🧪 test] the RFC 9106 Argon2id vector is reproduced
[⚙️ assert] password 32x01, salt 16x02, secret 8x03, data 12x04, m=32, t=3, p=4
✅ got: 0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659
[🧪 test] every parameter changes the derived bytes
[⚙️ assert] a different pass count gives a different tag
✅ got: #f
[⚙️ assert] a different salt gives a different tag
✅ got: #f
[⚙️ assert] a different passphrase gives a different tag
✅ got: #f
+----------------------------------------------+
[📑 suite] Sealed payloads (key derivation is recorded)
[🧪 test] tampering with the recorded parameters is not honoured
[⚙️ assert] a changed pass count no longer decrypts
✅ got: #t
[🧪 test] the default KDF keeps the OpenSSL layout byte for byte
[⚙️ assert] a fixed-salt PBKDF2 payload equals OpenSSL's own output
✅ got: #t
[🧪 test] sizes and headers are validated
[⚙️ assert] an 8-byte Argon2id salt is refused
✅ got: #t
[⚙️ assert] a 16-byte PBKDF2 salt is refused
✅ got: #t
[⚙️ assert] a payload with no known header is refused
✅ got: #t
[🧪 test] both readers keep their contracts
[⚙️ assert] openssl-decrypt refuses what the OpenSSL CLI could not read
✅ got: #t
[⚙️ assert] kdf-decrypt opens payloads produced by OpenSSL
✅ got: #t
[🧪 test] an Argon2id payload records its derivation and parameters
[⚙️ assert] the header names the derivation
✅ got: Argon2id
[⚙️ assert] the memory cost is recorded, in KiB
✅ got: 8192
[⚙️ assert] the pass count is recorded
✅ got: 1
[⚙️ assert] the lane count is recorded
✅ got: 1
[🧪 test] an Argon2id payload round-trips and resists the wrong key
[⚙️ assert] the same key opens it
✅ got: #t
[⚙️ assert] another key does not
✅ got: #t
+----------------------------------------------+
[📑 suite] Key derivation salt lengths
[🧪 test] kdf-salt-length
[⚙️ assert] the OpenSSL-compatible layout uses an 8-byte salt
✅ got: 8
[⚙️ assert] the Argon2id layout uses a 16-byte salt
✅ got: 16
+----------------------------------------------+
[📑 suite] Error handling helper
[🧪 test] call-with-error-handler
[⚙️ assert] a successful thunk's value is returned unchanged
✅ got: 42
[⚙️ assert] an ordinary error is handed to the handler as a message
✅ got: boom
[⚙️ assert] a signal is rethrown, so Ctrl-C keeps working
✅ got: 2
[⚙️ assert] a quit is rethrown, so exit codes survive the helper
✅ got: rethrown
+----------------------------------------------+
[📑 suite] Derived key material
[🧪 test] an unknown derivation is refused
[⚙️ assert] no other function can be named
✅ got: #t
[🧪 test] Argon2id key material honours its parameters
[⚙️ assert] the full key-and-IV length can be derived
✅ got: 48
[⚙️ assert] changing the pass count changes the material
✅ got: #t
[⚙️ assert] changing the passphrase changes the material
✅ got: #t
[🧪 test] PBKDF2 key material is the PBKDF2 derivation's
[⚙️ assert] derive-key-material agrees with the primitive it names
✅ got: 25ff9d4852ea4ea0ff9caf7aa33ed9e25f8d8a4eb2915445e0ff398292d67473
[⚙️ assert] the requested length is produced
✅ got: 32
+----------------------------------------------+
[📑 suite] HMAC-SHA256 (RFC 4231)
[🧪 test] RFC 4231 test case 2
[⚙️ assert] the key "Jefe" over its test message
✅ got: 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
[🧪 test] the digest is 32 bytes and key-sensitive
[⚙️ assert] 64 hexadecimal digits, i.e. 32 bytes
✅ got: 64
[⚙️ assert] changing the key changes the MAC
✅ got: #f
[🧪 test] RFC 4231 test case 1
[⚙️ assert] a 20-byte key of 0x0b over "Hi There"
✅ got: b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7
+----------------------------------------------+
[📑 suite] Randomness
[🧪 test] random-bytes returns genuinely fresh bytes
[⚙️ assert] the default length is the OpenSSL salt length
✅ got: 8
[⚙️ assert] the requested length is honoured
✅ got: 32
[⚙️ assert] two calls do not return the same bytes
✅ got: #t
[⚙️ assert] a draw is not a constant
✅ got: #f
[⚙️ assert] a draw is not all zeroes
✅ got: #t
+----------------------------------------------+
[📑 suite] Base16 encoding
[🧪 test] known encodings
[⚙️ assert] the empty string encodes to nothing
✅ got:
[⚙️ assert] lowercase hex digits are produced
✅ got: 68656c6c6f
[⚙️ assert] a word encodes byte per byte
✅ got: 626577616b696e67
[🧪 test] decoding accepts both cases
[⚙️ assert] lowercase hex decodes
✅ got: hello
[⚙️ assert] uppercase hex decodes too
✅ got: hello
[⚙️ assert] encoding then decoding is lossless
✅ got: hello
[🧪 test] malformed input is rejected
[⚙️ assert] an odd number of digits raises
✅ success catching error: misc-error: (#f bewaking: malformed hexadecimal input (odd number of digits). () #f)
[⚙️ assert] non-hexadecimal characters raise
✅ success catching error: misc-error: (#f bewaking: malformed hexadecimal input. () #f)
[⚙️ assert] embedded whitespace raises
✅ success catching error: misc-error: (#f bewaking: malformed hexadecimal input (odd number of digits). () #f)
+----------------------------------------------+
[📑 suite] Authenticated payload hygiene
[🧪 test] an empty plaintext is still authenticated
[⚙️ assert] the empty plaintext round-trips
✅ got: #t
[⚙️ assert] even an empty payload carries a salt, a nonce and a tag
✅ got: #t
[🧪 test] the recorded derivation cost is bounded before it is spent
[⚙️ assert] a memory cost above the cap is refused, not allocated
✅ got: #t
[⚙️ assert] a lane count above the cap is refused
✅ got: #t
[⚙️ assert] a zero pass count is refused
✅ got: #t
[⚙️ assert] less than 8 KiB per lane is refused
✅ got: #t
[⚙️ assert] a cost inside the bounds is derived, then fails the tag: the bound is not a blanket refusal
✅ got: #t
[🧪 test] the salt and the nonce are used, not decorative
[⚙️ assert] editing a salt digit breaks the tag
✅ got: #t
[⚙️ assert] editing a nonce digit breaks the tag
✅ got: #t
[⚙️ assert] editing an Argon2id parameter digit is refused
✅ got: #t
[🧪 test] every payload uses a fresh salt and nonce
[⚙️ assert] sealing the same plaintext twice does not repeat the payload
✅ got: #f
[⚙️ assert] the recorded salts differ, so the derived keys do too
✅ got: #f
[⚙️ assert] the recorded nonces differ as well
✅ got: #f
[🧪 test] the associated data is authenticated but not stored
[⚙️ assert] the associated data does not appear in the payload
✅ got: #f
[⚙️ assert] one extra associated-data byte breaks the tag
✅ got: #t
+----------------------------------------------+
[📑 suite] Constant-time comparison
[🧪 test] constant-time-equal?
[⚙️ assert] equal strings compare equal
✅ got: #t
[⚙️ assert] one differing byte is not equal
✅ got: #f
[⚙️ assert] different lengths are not equal
✅ got: #f
[⚙️ assert] equal bytevectors compare equal
✅ got: #t
[⚙️ assert] a difference in the last byte is still found
✅ got: #f
[⚙️ assert] values that are neither strings nor bytevectors are refused
✅ success catching error: misc-error: (#f bewaking: constant-time-equal? compares two strings or two bytevectors. () #f)
+----------------------------------------------+
[📑 suite] OpenSSL-compatible sealing
[🧪 test] the salt must be 8 bytes
[⚙️ assert] a short salt raises
✅ success catching error: misc-error: (#f bewaking: a PBKDF2 salt must be 8 bytes long. () #f)
[🧪 test] a fixed-salt vector matches OpenSSL byte for byte
[⚙️ assert] the ciphertext of a known salt and key
✅ got: U2FsdGVkX18AAQIDBAUGByAGpafcjamTQUX3eOyvQUE=
[⚙️ assert] and it decrypts back
✅ got: bewaking
[🧪 test] failures are reported rather than ignored
[⚙️ assert] the wrong key raises
✅ got: #t
[⚙️ assert] the wrong key raises for OpenSSL-produced data too
✅ got: #t
[⚙️ assert] input that is not Base64 raises
✅ got: #t
[⚙️ assert] Base64 without a Salted__ header raises
✅ got: #t
[⚙️ assert] a truncated body raises rather than returning garbage
✅ got: #t
[🧪 test] data produced by OpenSSL itself can be opened
[⚙️ assert] an openssl-encrypted string, random salt and all
✅ got: a secret that openssl sealed
[🧪 test] round-trips
[⚙️ assert] the empty string round-trips
✅ got:
[⚙️ assert] a longer payload round-trips
✅ got: a secret that openssl sealed
[⚙️ assert] one padded block follows the 16-byte Salted__ header
✅ got: 32
+----------------------------------------------+
[📑 suite] Key-derivation specifications
[🧪 test] unsupported specifications are refused
[⚙️ assert] an unknown KDF name is refused
✅ got: #t
[⚙️ assert] a string is not a KDF specification
✅ got: #t
[⚙️ assert] a PBKDF2 cost the OpenSSL format cannot record is refused
✅ got: #t
[⚙️ assert] an out-of-range Argon2id memory cost is refused
✅ got: #t
[🧪 test] the default is PBKDF2 with its fixed cost
[⚙️ assert] the module default names PBKDF2
✅ got: pbkdf2
[⚙️ assert] its iteration count is the OpenSSL one
✅ got: 10000
[🧪 test] Argon2id defaults are filled in and reported
[⚙️ assert] the defaults are 64 MiB, 3 passes, 1 lane
✅ got: ((kdf . argon2id) (memory-kib . 65536) (iterations . 3) (parallelism . 1))
[⚙️ assert] given parameters are kept and the rest defaulted
✅ got: ((kdf . argon2id) (memory-kib . 262144) (iterations . 4) (parallelism . 1))
+----------------------------------------------+
[📑 suite] PBKDF2-HMAC-SHA256
[🧪 test] the key and IV material OpenSSL derives
[⚙️ assert] 48 bytes over 10000 iterations, as `openssl kdf' reports
✅ got: cb14e1e1f7ac2fe57f2fcaa462d4ad903867450d9778d2ccf939d8ad3cb99982e0edbd06820c0a636f6fd53d4be339fd
[🧪 test] published password/salt vectors
[⚙️ assert] one iteration
✅ got: 120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b
[⚙️ assert] two iterations
✅ got: ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43
[⚙️ assert] 4096 iterations
✅ got: c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a
+----------------------------------------------+
[📑 suite] Base64 encoding
[🧪 test] malformed input is rejected
[⚙️ assert] non-alphabet characters raise
✅ success catching error: misc-error: (#f bewaking: malformed base64 input. () #f)
[⚙️ assert] a truncated group raises
✅ success catching error: misc-error: (#f bewaking: malformed base64 input (bad length or padding). () #f)
[⚙️ assert] data after padding raises
✅ success catching error: misc-error: (#f bewaking: malformed base64 input (data after padding). () #f)
[⚙️ assert] an impossible amount of padding raises
✅ success catching error: misc-error: (#f bewaking: malformed base64 input (bad length or padding). () #f)
[🧪 test] RFC 4648 test vectors
[⚙️ assert] empty
✅ got:
[⚙️ assert] one byte of input
✅ got: Zg==
[⚙️ assert] two bytes of input
✅ got: Zm8=
[⚙️ assert] three bytes of input
✅ got: Zm9v
[⚙️ assert] four bytes of input
✅ got: Zm9vYg==
[⚙️ assert] five bytes of input
✅ got: Zm9vYmE=
[⚙️ assert] six bytes of input
✅ got: Zm9vYmFy
[🧪 test] decoding is tolerant of line breaks
[⚙️ assert] a trailing newline is ignored
✅ got: hello
[⚙️ assert] encoding then decoding is lossless
✅ got: hello world
+----------------------------------------------+
[📑 suite] Format constants
[🧪 test] layout lengths and Argon2id defaults
[⚙️ assert] the GCM outer layer uses a 16-byte salt
✅ got: 16
[⚙️ assert] the GCM nonce is 12 bytes
✅ got: 12
[⚙️ assert] PBKDF2 keeps the OpenSSL 8-byte salt
✅ got: 8
[⚙️ assert] the default Argon2id spec names its derivation
✅ got: argon2id
[⚙️ assert] the default memory cost is recorded
✅ got: 65536
[⚙️ assert] the default pass count is recorded
✅ got: 3
[⚙️ assert] the default lane count is recorded
✅ got: 1
✅ got: 65536
✅ got: 3
✅ got: 1
[⚙️ assert] argon2id-available? always answers, never raises
✅ got: #t
+----------------------------------------------+
[📑 suite] Error messages
[🧪 test] a diagnostic is the error's own message
[⚙️ assert] the message's format arguments are applied
✅ got: failed: 42
[⚙️ assert] a message without arguments is returned as is
✅ got: boom
[⚙️ assert] a plain string argument is used when there is no data list
✅ got: custom
+----------------------------------------------+
[📑 suite] Encodings of raw bytes
[🧪 test] Base16 round-trips arbitrary bytevectors
[⚙️ assert] the empty bytevector round-trips
✅ got: #t
[⚙️ assert] bytes that are not valid UTF-8 survive the round trip
✅ got: #t
[🧪 test] Base64 round-trips arbitrary bytevectors
[⚙️ assert] bytes that are not valid UTF-8 survive the round trip
✅ got: 3
+----------------------------------------------+
[📑 suite] Authenticated payloads (AES-256-GCM)
[🧪 test] the payload is byte for byte the reference implementation's;;; note: source file ./src/bewaking/cli.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/cli.go
;;; note: source file ./src/bewaking/cli.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/cli.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/cli.scm.go
;;; note: source file ./src/bewaking/crypto.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/crypto.go
;;; note: source file ./src/bewaking/crypto.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/crypto.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/crypto.scm.go
;;; note: source file ./src/bewaking/bewaking.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/bewaking.go
;;; note: source file ./src/bewaking/bewaking.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/bewaking.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/bewaking.scm.go
;;; note: source file ./src/bewaking/password.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/password.go
;;; note: source file ./src/bewaking/password.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/password.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/password.scm.go
;;; note: source file ./src/bewaking/cli.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/cli.go
;;; note: source file ./src/bewaking/cli.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/cli.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/cli.scm.go
;;; note: source file ./src/bewaking/crypto.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/crypto.go
;;; note: source file ./src/bewaking/crypto.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/crypto.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/crypto.scm.go
;;; note: source file ./src/bewaking/bewaking.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/bewaking.go
;;; note: source file ./src/bewaking/bewaking.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/bewaking.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/bewaking.scm.go
;;; note: source file ./src/bewaking/password.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/password.go
;;; note: source file ./src/bewaking/password.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/password.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/password.scm.go
[⚙️ assert] PBKDF2-HMAC-SHA256, as Python's cryptography seals it
✅ got: 010707070707070707070707070707070709090909090909090909090915d0ed647041883a00a39c1a98153992e43396ca2ef78f30cf603552820a8ed81e320d38a87aee14ad4a7f41b2df7b369d6614500294e59e58
[⚙️ assert] Argon2id with its parameters recorded, likewise
✅ got: 020000200000000001000000010707070707070707070707070707070709090909090909090909090996b424d50c29974261cc56650752a5237f7528271202d70046f0bc49c8c0bfefb86150c0b946a554a1590bec6d5f03409bbff197af7d782651
[🧪 test] fresh salt and nonce still round-trip
[⚙️ assert] a payload sealed with random salt and nonce opens
✅ got: #t
[⚙️ assert] and so does an Argon2id one
✅ got: #t
[⚙️ assert] associated data is optional: empty means none
✅ got: #t
[⚙️ assert] a payload sealed without it does not open with it
✅ got: #t
[🧪 test] the tag refuses every alteration
[⚙️ assert] a flipped ciphertext digit is refused
✅ got: #t
[⚙️ assert] a flipped tag digit is refused
✅ got: #t
[⚙️ assert] an unknown key-derivation id is refused, not guessed at
✅ got: #t
[⚙️ assert] different associated data is refused
✅ got: #t
[⚙️ assert] the wrong key is refused
✅ got: #t
[⚙️ assert] a truncated payload is refused
✅ got: #t
[🧪 test] the payload records which derivation to use
[⚙️ assert] PBKDF2 is identified by its own id
✅ got: 1
[⚙️ assert] Argon2id is identified by a different id
✅ got: 2
[⚙️ assert] the memory cost is recorded, in KiB
✅ got: 8192
[⚙️ assert] the pass count is recorded
✅ got: 1
[⚙️ assert] the lane count is recorded
✅ got: 1
[🧪 test] hexadecimal that stands for bytes decodes to bytes
[⚙️ assert] base16->bytes returns a bytevector
✅ got: #t
[⚙️ assert] and keeps the raw octets, which base16-decode cannot
✅ got: 255
ℹ️ info - ran 18 tasks in: 1 seconds, -309572 microseconds
running unit tests from file: cli-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 571422038 (replay with VERITAS_SEED=571422038)
+----------------------------------------------+
[📑 suite] CLI object commands
[🧪 test] object expiry policy
[⚙️ assert] an object sealed with a past expiry is refused
✅ got: #t
[🧪 test] object round-trip
[⚙️ assert] object encrypt then decrypt round-trips a datum
✅ got: #t
+----------------------------------------------+
[📑 suite] CLI dispatch
[🧪 test] help and exit codes
[⚙️ assert] help exits successfully
✅ got: 0
[⚙️ assert] help prints the usage banner
✅ got: #t
[⚙️ assert] help lists the token generate command
✅ got: #t
[⚙️ assert] help documents the keyring option
✅ got: #t
[⚙️ assert] help documents the rotation metadata options
✅ got: #t
[⚙️ assert] help lists the token inspect command
✅ got: #t
[⚙️ assert] version exits successfully
✅ got: 0
[⚙️ assert] an unknown command exits with 1
✅ got: 1
[⚙️ assert] a token subcommand is required
✅ got: 1
[⚙️ assert] decrypting without keys fails
✅ got: 1
+----------------------------------------------+
[📑 suite] CLI rotation metadata
[🧪 test] token inspect audits the header without keys
[⚙️ assert] inspect reports version, key id and expiry
✅ got: #t
[⚙️ assert] inspect reports an expired envelope
✅ got: #t
[⚙️ assert] inspect rejects a non-envelope token
✅ got: #t
[🧪 test] expiry is sealed into tokens and enforced
[⚙️ assert] an expired token exits with 1
✅ got: #t
[⚙️ assert] --allow-expired opens it anyway
✅ got: #t
[⚙️ assert] --max-age-seconds accepts fresh data
✅ got: #t
[⚙️ assert] --ttl-seconds and --expires-at conflict
✅ got: #t
+----------------------------------------------+
[📑 suite] CLI key derivation and dropped formats
[🧪 test] a refusal does not reveal which key ids the keyring configures
[⚙️ assert] an unknown key id and an edited body give the same message and exit code
✅ got: #t
[🧪 test] envelopes of dropped formats are refused
[⚙️ assert] a BWK1 token cannot be decrypted or inspected
✅ got: #t
[⚙️ assert] a BWK2 token cannot be decrypted or inspected
✅ got: #t
[⚙️ assert] an empty token is refused
✅ got: #t
[🧪 test] tampering and missing arguments are refused
[⚙️ assert] an edited token exits with 1
✅ got: #t
[⚙️ assert] generating without an owner id exits with 1
✅ got: #t
[⚙️ assert] sealing an object without keys exits with 1
✅ got: #t
[🧪 test] the key derivation is chosen when sealing and read back when opening
[⚙️ assert] an Argon2id token round-trips through the CLI
✅ got: #t
[⚙️ assert] a KDF parameter without --kdf is refused
✅ got: #t
+----------------------------------------------+
[📑 suite] CLI token commands
[🧪 test] token round-trip
[⚙️ assert] generate then decrypt preserves the payload
✅ got: #t
[🧪 test] wrong keys are rejected
[⚙️ assert] a wrong signing key exits with 1
✅ got: #t
[🧪 test] key id and keyring rotation
[⚙️ assert] a keyring round-trips and rejects the wrong key
✅ got: #t
+----------------------------------------------+
[📑 suite] CLI password hashing
[🧪 test] password hash and verify
[⚙️ assert] the command prints a PHC Argon2id hash
✅ got: #t
[⚙️ assert] the same password verifies with exit code 0
✅ got: #t
[⚙️ assert] a wrong password exits with 1
✅ got: #t
[⚙️ assert] a failed verification writes nothing to standard output
✅ got:
[⚙️ assert] a malformed hash exits with 1
✅ got: 1
+----------------------------------------------+
[📑 suite] CLI password commands
[🧪 test] password strength
[⚙️ assert] strength exits successfully
✅ got: 0
[⚙️ assert] a common password is reported weak
✅ got: #t
[🧪 test] password generate
[⚙️ assert] generation exits successfully
✅ got: 0
[⚙️ assert] the requested length is honoured
✅ got: 32
[⚙️ assert] symbols are excluded on request
✅ got: (lowercase uppercase digit)
+----------------------------------------------+
[📑 suite] CLI surface
[🧪 test] help, parallel Argon2id and environment keys
[⚙️ assert] --help prints the usage text and exits 0
✅ got: #t
[⚙️ assert] --kdf-parallelism seals and opens an Argon2id token
✅ got: #t
[⚙️ assert] BEWAKING_ENCRYPTION_KEY and BEWAKING_SIGNING_KEY are picked up
✅ got: #t
ℹ️ info - ran 8 tasks in: 2 seconds, 203131 microseconds
running unit tests from file: web-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 571422038 (replay with VERITAS_SEED=571422038)
+----------------------------------------------+
[📑 suite] Web chapter: sign-in seals a session into a cookie
[🧪 test] bad credentials get no cookie at all
[⚙️ assert] a wrong password is a plain 401
✅ got: 401
[⚙️ assert] no session is created for a failed sign-in
✅ got: #f
[⚙️ assert] the failure is reported, not swallowed
✅ got: invalid credentials
[🧪 test] the cookie lifetime can be aligned with the token's
[⚙️ assert] the requested lifetime is recorded in the cookie
✅ got: 86400
[⚙️ assert] the envelope is sealed now
✅ got: #t
[⚙️ assert] the payload lifetime is separate from the cookie's
✅ got: #f
[🧪 test] a verified user gets a sealed session cookie
[⚙️ assert] sign-in redirects, as the README's controller does
✅ got: 302
[⚙️ assert] the redirect target is the dashboard
✅ got: /dashboard
[⚙️ assert] the cookie value is a BWK3 envelope
✅ got: #t
[⚙️ assert] the cookie is named as documented
✅ got: x-bewaking-session
[⚙️ assert] (a) HttpOnly puts the cookie out of reach of JavaScript
✅ got: #t
[⚙️ assert] (b) Secure keeps it off plain HTTP
✅ got: #t
[⚙️ assert] (c) path "/" applies the cookie to the whole site
✅ got: /
[⚙️ assert] (d) the cookie's lifetime matches #:token-validity-seconds
✅ got: 604800
+----------------------------------------------+
[📑 suite] Web chapter: sealed objects as stateless state
[🧪 test] a cursor or half-finished form can be sealed the same way
[⚙️ assert] a sealed cursor round-trips
✅ got: page-3
[⚙️ assert] a client cannot page anywhere it was not sent
✅ got: #t
[🧪 test] an edited link is worth nothing
[⚙️ assert] the link was edited in the mail client
✅ got: #f
[⚙️ assert] and garbage is not a link either
✅ got: #f
[🧪 test] the link answers only to the key that sealed it
[⚙️ assert] a link sealed elsewhere does not open here
✅ got: #f
[🧪 test] a password-reset link round-trips with no table row
[⚙️ assert] the link is a sealed envelope
✅ got: #t
[⚙️ assert] the state comes back as an ordinary alist
✅ got: ((user . u-1) (action . reset-password))
[⚙️ assert] the user id is not readable in the link
✅ got: #f
[⚙️ assert] the hour-long lifetime is recorded in the header
✅ got: #t
[🧪 test] an expired link is refused by the server, not by the client
[⚙️ assert] before the expiry the link opens
✅ got: ((user . u-1) (action . reset-password))
[⚙️ assert] after the expiry the server refuses it
✅ got: #f
[⚙️ assert] and the header says so, for the log
✅ got: #t
+----------------------------------------------+
[📑 suite] Web chapter: protecting a route
[🧪 test] every failure mode is a refusal, never an admission
[⚙️ assert] a request with no cookie is redirected to /login
✅ got: 302
[⚙️ assert] an empty cookie is refused
✅ got: 302
[⚙️ assert] a tampered cookie is refused
✅ got: 302
[⚙️ assert] a cookie that is not a real envelope is refused
✅ got: 302
[⚙️ assert] arbitrary text is refused
✅ got: 302
[⚙️ assert] a second user's genuine session is admitted, as that user
✅ got: 200
[⚙️ assert] and it admits the *second* user, not the first
✅ got: u-2
[⚙️ assert] a keyring that cannot resolve the key id is refused
✅ got: 302
[🧪 test] the payload is the identity, not the request
[⚙️ assert] the owner id comes from the sealed payload
✅ got: u-1
[⚙️ assert] a client-supplied user id is a hint, never the identity
✅ got: #f
[🧪 test] an expired session is refused, and sign-out clears the cookie
[⚙️ assert] a token whose payload lifetime has passed is refused
✅ got: 302
[⚙️ assert] and the helper returns #f for it
✅ got: #f
[⚙️ assert] sign-out redirects to the login page
✅ got: 302
[⚙️ assert] sign-out deletes the cookie, the only cleanup there is
✅ got:
[⚙️ assert] the deletion cookie expires immediately
✅ got: 0
[🧪 test] the same session works as an API bearer token
[⚙️ assert] the API route admits the header token
✅ got: 200
[⚙️ assert] the token identifies the caller without a database round-trip
✅ got: u-1
[⚙️ assert] the granted permissions are what the token says
✅ got: (read-only access-files)
[🧪 test] a permission that was never granted is refused on the server
[⚙️ assert] the browser route refuses a route the user may not use
✅ got: 302
[⚙️ assert] the API route answers 401 rather than 403-by-omission
✅ got: 401
[⚙️ assert] the helper returns #f, not a partially authorized payload
✅ got: #f
[⚙️ assert] the permission the user does have is enough
✅ got: #t
[⚙️ assert] a route needing nothing is open to a valid session
✅ got: #t
[🧪 test] the signed-in user is admitted
[⚙️ assert] the browser route runs the handler
✅ got: 200
[⚙️ assert] the handler sees the owner id from the payload
✅ got: u-1
[⚙️ assert] the thunk's 'email' came back as 'owner-email'
✅ got: joe@example.com
[⚙️ assert] and 'name' came back as 'owner-name'
✅ got: Joe
[⚙️ assert] permissions travel inside the token
✅ got: (read-only access-files)
[⚙️ assert] the payload carries its own token id
✅ got: #t
[⚙️ assert] and its own payload expiry
✅ got: #t
+----------------------------------------------+
[📑 suite] Web chapter: auditing what is safe to log
[🧪 test] an expiry is reported as a time, and its absence as nothing
[⚙️ assert] the audit reports no envelope expiry by default
✅ got: #f
[⚙️ assert] a token sealed with a TTL records its expiry
✅ got: #t
[⚙️ assert] and the audit helper agrees it is live
✅ got: #f
[🧪 test] auditing garbage is #f, not an exception
[⚙️ assert] an unparseable cookie yields no metadata
✅ got: #f
[⚙️ assert] a missing cookie yields no metadata
✅ got: #f
[⚙️ assert] a malformed BWK3 value yields no metadata
✅ got: #f
[🧪 test] the envelope header is readable with no keys at all
[⚙️ assert] the version is reported for the log
✅ got: BWK3
[⚙️ assert] the key id is reported, which is what rotation is keyed on
✅ got: default
[⚙️ assert] the creation time is reported
✅ got: #t
[⚙️ assert] a fresh session is not expired
✅ got: #f
[⚙️ assert] the audit output carries no key material
✅ got: #f
[⚙️ assert] not the encryption key and not the signing key either
✅ got: #f
+----------------------------------------------+
[📑 suite] Web chapter: keys, rotation and revocation
[🧪 test] keys stay secret and distinct, which is the whole requirement
[⚙️ assert] the encryption key does not appear in the token
✅ got: #f
[⚙️ assert] nor does the signing key
✅ got: #f
[⚙️ assert] swapping the two keys does not open the session
✅ got: #f
[🧪 test] rotation needs no downtime and no forced logout
[⚙️ assert] the key id is embedded in the envelope
✅ got: #t
[⚙️ assert] new logins are sealed with the current key
✅ got: #t
[⚙️ assert] an old token still opens: nothing to migrate
✅ got: #t
[⚙️ assert] and so does a new one
✅ got: #t
[🧪 test] retiring a key logs every session sealed with it out at once
[⚙️ assert] the retired key id stops opening sessions
✅ got: #f
[⚙️ assert] the current key keeps working
✅ got: #t
ℹ️ info - ran 5 tasks in: 1 seconds, 84934 microseconds
running unit tests from file: password-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 571422038 (replay with VERITAS_SEED=571422038)
+----------------------------------------------+
[📑 suite] Password character classes
[🧪 test] password-character-classes
[⚙️ assert] lowercase letters only
✅ got: (lowercase)
[⚙️ assert] uppercase letters only
✅ got: (uppercase)
[⚙️ assert] digits only
✅ got: (digit)
[⚙️ assert] all four classes are detected
✅ got: (lowercase uppercase digit symbol)
[⚙️ assert] an empty password has no classes
✅ got: ()
+----------------------------------------------+
[📑 suite] Password strength
[🧪 test] strong-password?
[⚙️ assert] a common word is not strong
✅ got: #f
[⚙️ assert] a short mixed password is not strong
✅ got: #f
[⚙️ assert] a long passphrase is strong
✅ got: #t
[⚙️ assert] min-score lowers the bar
✅ got: #t
[🧪 test] password-strength
[⚙️ assert] a common numeric password scores 0
✅ got: 0
[⚙️ assert] score 0 is labelled very weak
✅ got: very weak
[⚙️ assert] a very short password scores 0
✅ got: 0
[⚙️ assert] a long passphrase scores 4
✅ got: 4
[⚙️ assert] score 4 is labelled very strong
✅ got: very strong
[⚙️ assert] an empty password scores 0
✅ got: 0
[⚙️ assert] stronger passwords have higher entropy
✅ got: #t
[⚙️ assert] feedback is returned as a list
✅ got: #t
[⚙️ assert] common passwords get a specific warning
✅ got: #t
[⚙️ assert] sequential runs are flagged in the feedback
✅ got: #t
+----------------------------------------------+
[📑 suite] Password hashing
[🧪 test] hash-password and verify-password
[⚙️ assert] a hash is a well-formed PHC Argon2id string
✅ got: #t
[⚙️ assert] the requested parameters are the ones recorded
✅ got: #t
[⚙️ assert] the right password verifies
✅ got: #t
[⚙️ assert] a wrong password does not verify
✅ got: #f
[⚙️ assert] plain text is not a password hash
✅ got: #f
[⚙️ assert] a malformed hash verifies nothing
✅ got: #f
[⚙️ assert] the empty string is not a password hash
✅ got: #f
[🧪 test] salts and cost ceilings
[⚙️ assert] two hashes of one password differ, so each carries a fresh salt
✅ got: #f
[⚙️ assert] a recorded memory cost above the ceiling is refused
✅ got: #f
[⚙️ assert] a recorded pass count above the ceiling is refused
✅ got: #f
+----------------------------------------------+
[📑 suite] Password entropy
[🧪 test] password-entropy
[⚙️ assert] an empty password has zero entropy
✅ got: 0
[⚙️ assert] eight lowercase letters are ~37.6 bits
✅ got: #t
[⚙️ assert] adding a character class increases the entropy
✅ got: #t
+----------------------------------------------+
[📑 suite] Strength labels
[🧪 test] password-strength-label
✅ got: very weak
✅ got: weak
✅ got: fair
✅ got: strong
✅ got: very strong;;; note: source file ./src/bewaking/bewaking.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/bewaking.go
;;; note: source file ./src/bewaking/bewaking.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/bewaking.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/bewaking.scm.go
;;; note: source file ./src/bewaking/crypto.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/crypto.go
;;; note: source file ./src/bewaking/crypto.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/crypto.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/crypto.scm.go
error decrypting token: bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.
error decrypting object: bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.
error decrypting object: bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.
error decrypting object: bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.
error decrypting token: open-envelope: not a bewaking envelope (expected BWK3).
error decrypting object: open-envelope: not a bewaking envelope (expected BWK3).
error decrypting token: open-envelope: not a bewaking envelope (expected BWK3).
error decrypting object: open-envelope: not a bewaking envelope (expected BWK3).
error decrypting token: open-envelope: the sealed data expired at 1000 (key 'default').
error decrypting object: open-envelope: the sealed data expired at 1000 (key 'default').
error decrypting token: bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.
error decrypting token: open-envelope: not a bewaking envelope (expected BWK3).
error decrypting object: bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.
;;; note: source file ./src/bewaking/cli.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/cli.go
;;; note: source file ./src/bewaking/cli.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/cli.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/cli.scm.go
;;; note: source file ./src/bewaking/password.scm
;;; newer than compiled /gnu/store/21afi1i81zcsj3g9fjxxaxrkfxm3gfjp-profile/lib/guile/3.0/site-ccache/bewaking/password.go
;;; note: source file ./src/bewaking/password.scm
;;; newer than compiled /run/current-system/profile/lib/guile/3.0/site-ccache/bewaking/password.go
;;; found fresh local cache at /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/bewaking/trunk/src/bewaking/password.scm.go
error decrypting object: bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.
error decrypting token: bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.
error decrypting token: bewaking: malformed BWK3 envelope (created-at)
error decrypting token: open-envelope: not a bewaking envelope (expected BWK3).
error decrypting token: bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.
error decrypting token: bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.
error decrypting token: bewaking: the sealed data could not be opened: unknown, retired or wrong key, or the data was altered.
[⚙️ assert] a score outside 0-4 is labelled unknown
✅ got: unknown
+----------------------------------------------+
[📑 suite] Password generation
[🧪 test] generate-strong-password
[⚙️ assert] default length is 24
✅ got: 24
[⚙️ assert] a generated password is strong
✅ got: #t
[⚙️ assert] default generated passwords use every class
✅ got: (lowercase uppercase digit symbol)
[⚙️ assert] length is respected
✅ got: 32
[⚙️ assert] symbols can be excluded
✅ got: (lowercase uppercase digit)
[⚙️ assert] too-short a request raises an error
✅ success catching error: misc-error: (#f ~A (generate-strong-password: length must be at least 4 to cover every character class.) #f)
ℹ️ info - ran 6 tasks in: 1 seconds, -957776 microseconds
finished running tests
--------------------------------------------------------------
Maak 0.14.0: the infinitely extensible command runner
Loading tasks from Maak file: /etc/byggsteg/job-clone/bewaking/trunk/maak.scm
Executing Maak tasks: test
--------------------------------------------------------------
=> Running Maak task: test
Run the project's tests.
Execute command:
guix time-machine --channels=channels.scm -- shell -m manifest.scm -- guile -L ./src -L ./test -c '((@ (veritas runner) run-tests))'
Executing: guix time-machine --channels=channels.scm -- shell -m manifest.scm -- guile -L ./src -L ./test -c '((@ (veritas runner) run-tests))'