:title: Cryptographic Facilities in Unicon :author: Jafar Al-Gharaibeh :trnumber: 27 :date: August 2026 :copyright: 2026, Jafar Al-Gharaibeh :abstract: Unicon has long used OpenSSL for encrypted network sockets, but hashing, HMAC, signing, and symmetric encryption were not available as language primitives. This report describes the facilities that close that gap: cryptographic operations reached through the same open(), read(), write(), and close() interface already used for files and sockets, with ``[ ]`` for status and Attrib() for idle-window mutation. Mode letter ``e`` is reused as a modifier; ``op=`` selects the operation; keys and certificates are loaded as file-typed handles and passed as attributes. Encrypted TCP is unchanged; encrypted UDP uses DTLS. The implementation is the OpenSSL EVP interface already linked for TLS. :keywords: Unicon, cryptography, OpenSSL, TLS, DTLS, hash, HMAC, sign, encrypt, runtime, technical report. :docclass: report .. _sec-intro: 1. Introduction =============== This report describes the cryptographic facilities in the Unicon runtime: how a handle is opened, how payload flows through ``write()`` / ``read()``, how key and certificate material is loaded and reused, and how the same letters and attributes compose with encrypted sockets. It is formatted as a Unicon Technical Report :cite:`Jeffery:UTR15`. The language-facing reference lives in *Programming with Unicon* (Chapter 6) and the language reference (``open`` modes ``e`` / ``er`` / ``eh`` / ``re`` / ``we``, and ``ne`` / ``nue``, subscript peek, ``key()``). Automated tests live under ``tests/crypto/``. The feature is optional in the same sense as TLS. A build with OpenSSL reports ``secure sockets layer encryption`` in ``&features`` (the ``_SSL`` flag). Without the library, the crypto paths are compiled out. .. _sec-motivation: 2. Motivation ============= Unicon already links OpenSSL :cite:`OpenSSL` for mode ``n`` with ``e`` -- encrypted TCP. Programs that needed a SHA-256 digest, an HMAC, a signature, or a file encrypted at rest had to use external tools. The facilities here expose those primitives without a second API: hashing a stream is opening a handle and writing to it; reading an encrypted file is opening the file with a transform on the way through. No new external dependency is introduced. The implementation uses OpenSSL's EVP interface, which the encrypted-socket support already requires. .. _sec-principles: 3. Design principles ==================== **``e`` is the crypto letter, reused not invented.** Unicon already uses ``e`` for encryption (``Fs_Encrypt``). New modes carry that letter rather than adding one letter per operation. ``op=`` selects what the handle does. **``e`` composes as a modifier.** Mode letters already stack, and several already branch on bits accumulated so far: ``n`` + ``e`` is an encrypted socket; ``r`` + ``e`` / ``w`` + ``e`` is a file transform; ``e`` + ``h`` is an in-memory hash; ``e`` + ``r`` is raw key material rather than a path. **The open target configures the operation; payload is I/O.** Target is never the message being hashed or encrypted. What target means depends on the mode (algorithm name, key path, data file, host:port). Payload always flows through ``write()`` / ``read()`` / ``reads()``. **Attributes are ``name=value`` strings, or handles.** A string element is parsed as always. A **handle** -- the return value of a prior ``open(..., "e")`` -- is a typed attribute that could not survive being flattened into a string. Handles are interpreted by **role** from their own content (private key, public key, cert, symmetric key), not by position or by an attribute name. **Material comes from a file, raw bytes, or a handle.** A file path is the target under mode ``e``, or ``key=`` when the target is already spoken for (``re`` / ``we``, ``n`` + ``e``). Raw bytes use mode ``er``. A handle avoids re-parsing. **Initialization is lazy.** ``open()`` does not create the underlying EVP context until the first ``write()`` (or the first transforming ``read()`` on a file). A handle that is never used costs nothing. ``read()`` finalizes and resets, so the next ``write()`` initializes again. **``Attrib()`` reconfigures only in the idle window** -- before the first ``write()``, or after a ``read()`` and before the next ``write()``. Changing ``op=``, ``alg=``, ``cipher=``, or ``iv=`` mid-operation fails. Handles are accepted by ``open()``. .. _sec-modes: 4. Modes ======== ``open()``'s mode string is scanned character by character in ``src/runtime/fsys.r``. Letters are not independent: ``u`` after ``n`` is UDP, ``r`` after ``n`` is raw, ``h`` after ``e`` is hash. Order matters. ``e`` already set ``Fs_Encrypt``; this design adds ``Fs_Crypto`` for hash / HMAC / sign / verify / encrypt-decrypt handles so they are distinct from TLS sockets that also carry ``Fs_Encrypt``. .. list-table:: :header-rows: 1 * - Mode - Meaning - Target * - ``e`` - Load material, or an operation whose target is material - Key/cert path, or a material handle * - ``er`` - Same, but the target is raw bytes - In-memory string * - ``eh`` - Hash; no ``op=`` needed - Digest name, e.g. ``sha256`` * - ``re`` - Read a file, transformed (hash or decrypt) - Data-file path * - ``we`` - Write a file, transformed (encrypt) - Data-file path * - ``ne`` - Encrypted TCP (TLS) -- unchanged - ``host:port`` * - ``nue`` / ``naue`` - Encrypted UDP (DTLS) - ``host:port`` ``e`` / ``er`` are the **data-pipe** shape: write payload in, read the result out. ``eh`` is separate because its target is an algorithm name, not material. ``re`` / ``we`` are the **file-transform** shape: the target is the data file, so key material arrives via ``key=`` or a handle attribute. ``h`` alone remains SSH. ``r`` alone remains read. The new meanings apply only when ``e`` was seen first. Mode ``-`` skips peer verification on TLS (``ne-``) and SSH (``h-``), the same as ``verifyPeer=no``. An explicit ``verifyPeer=`` overrides the mode flag. .. _sec-ops: 5. Operations ============= ``op=`` names the operation when the mode does not already imply one. Loading material (``e`` / ``er`` with a file or raw target and no ``op=``), hashing in memory (``eh``), and TLS / DTLS sockets take no ``op=``. .. list-table:: :header-rows: 1 * - ``op=`` - What it does - Typical mode - Other attributes * - ``hash`` - Digest of a file - ``re`` - ``alg=`` * - ``hmac`` - Keyed digest (RFC 2104) - ``e`` / ``er`` - ``alg=`` * - ``sign`` - Produce a signature - ``e`` / ``er`` - ``alg=`` * - ``verify`` - Check a signature - ``e`` / ``er`` - ``alg=``, ``sig=`` * - ``encrypt`` - Symmetric encrypt - ``e`` / ``er``, or ``we`` - ``cipher=``; ``key=`` or a handle for ``we`` * - ``decrypt`` - Symmetric decrypt - ``e`` / ``er``, or ``re`` - ``cipher=``; ``key=`` or a handle for ``re`` HMAC is the construction of RFC 2104 :cite:`RFC2104` over the digest named by ``alg=``. Algorithm and cipher names are OpenSSL's own (``sha256``, ``aes-256-gcm``) and are passed through rather than mapped to a Unicon-defined set. Defaults are ``alg=sha256`` and ``cipher=aes-256-gcm``. .. _sec-attrs: 6. Attributes ============= Trailing ``open()`` arguments are ``name=value`` strings or crypto handles. Empty values and unknown names fail (TLS attribute errors use 1302; crypto state/name mistakes use 1316). .. _sec-attrs-tls: 6.1 TLS / DTLS attributes ------------------------- These are the names ``create_ssl_context()`` accepts. They apply to ``ne`` / ``nue`` and are unchanged except for the two alignments in :ref:`section 6.3 `. .. list-table:: :header-rows: 1 * - Attribute - Meaning * - ``cert=`` - Certificate file path * - ``key=`` - Private key file path * - ``keypass=`` - Passphrase for an encrypted private key * - ``ca=`` / ``caDir=`` / ``caStore=`` - Trust store * - ``ciphers=`` / ``ciphers1.3=`` - TLS 1.2 list (e.g. ``HIGH``, ``ECDHE-RSA-AES256-GCM-SHA384``) and TLS 1.3 suites (e.g. ``TLS_AES_256_GCM_SHA384``) * - ``minProto=`` / ``maxProto=`` - Protocol version bounds, e.g. ``TLS1.2``, ``TLS1.3`` * - ``verifyPeer=`` - ``yes`` / ``no``; ``no`` is the same as mode ``-`` Socket attributes (``reuseaddr``, ``ttl``, ``iface``, ``join``, ``proto``, ...) are applied separately and are unaffected. Live-session status is :ref:`section 6.4 `. .. _sec-attrs-crypto: 6.2 Crypto attributes --------------------- .. list-table:: :header-rows: 1 * - Attribute - Meaning - Used with * - ``op=`` - Operation: ``hash``, ``hmac``, ``sign``, ``verify``, ``encrypt``, ``decrypt`` - All data-pipe and file-transform ops * - ``alg=`` - Digest name passed to OpenSSL (default ``sha256``; also ``sha512``, ``sha1``, ``sha3-256``, ``blake2b512``). Peek ``h["alg"]`` reports the library name (``SHA256``) - ``hash``, ``hmac``, ``sign``, ``verify`` * - ``cipher=`` - Symmetric cipher (default ``aes-256-gcm``; also ``aes-128-gcm``, ``aes-256-cbc``, ``chacha20-poly1305``) - ``encrypt``, ``decrypt`` * - ``iv=`` - Explicit IV; omit for automatic IV - ``encrypt``, ``decrypt`` * - ``sig=`` - Signature being checked - ``verify`` * - ``key=`` - Key path when the target is already a data file or host - ``re`` / ``we``, and TLS * - ``type=`` - Narrow a multi-item file: ``cert``, ``key``, ``pubkey`` - Material load (``e`` / ``er``) * - ``keypass=`` - Passphrase for an encrypted key file - Material load ``key=`` on a crypto handle is typed by content and by the operation: PEM / DER private keys for ``sign`` and TLS; raw bytes for ``encrypt``, ``decrypt``, and ``hmac``. A role mismatch fails. Without ``iv=``, ``encrypt`` generates a fresh random IV per operation and prepends it to the ciphertext; ``decrypt`` reads that prefix. Supplying ``iv=`` opts out -- useful for a wire format that does not prepend an IV, or for test vectors -- and hands uniqueness back to the caller. The default cipher is AEAD; the runtime appends the authentication tag after the ciphertext. Status peeks are :ref:`section 6.4 `. .. _sec-attrs-shared: 6.3 Shared names with SSH ------------------------- ``key=`` is a private-key path on TLS, crypto, and SSH. ``keypass=`` is the shared name for a key-file passphrase. SSH ``verifyPeer=yes|no`` and mode ``-`` mean the same thing on TLS and SSH. An explicit ``verifyPeer=`` overrides the mode flag. Trust stores stay distinct: TLS uses ``ca=`` (X.509); SSH uses ``hostkeyfile=`` (OpenSSH ``known_hosts``). .. _sec-peek: 6.4 Status peek (``[ ]`` and ``key()``) --------------------------------------- ``h["name"]`` / ``conn["name"]`` is a non-destructive get. ``Attrib()`` only assigns in the idle window (``op=``, ``alg=``, ``sig=``, ...). Unknown names raise 1302 on a crypto handle and 1326 on a TLS socket. An unpopulated field fails. A boolean field that *did* answer succeeds: ``"yes"`` for true, ``&null`` for false , which would make things like ``if \h["expired"]`` succeed on a valid certificate. ``key(h)`` generates every *answerable* field, including false booleans, so any ``k`` from ``key(h)`` makes ``h[k]`` succeed. Dump with ``image(h[k])`` so ``&null`` is visible. ``h["*"]`` returns a table of those same fields captured under one lock. No status field is named ``*``. Peeking a closed handle is error 174. ``key(h)`` that has not yet produced a name also raises 174 if the handle is already closed. After the first suspend, ``close()`` makes the generator fail instead of raising, so a walk does not turn a mid-generation close into an error. ``h["type"]`` is a role label (``key``, ``cert``, ``key,cert``), not a list. ``h["san"]`` and ``conn["san"]`` are lists of SAN strings (``DNS:host``, ``IP:1.2.3.4``). ``conn["certchain"]`` is a list of PEM strings. A single item is still a list of length 1. ``h["op"]`` is always populated on an operation handle. **Material** (mode ``e`` / ``er``, no ``op=``). .. list-table:: :header-rows: 1 * - Name - Value * - ``type`` - ``key``, ``pubkey``, ``cert``, ``symkey``, or a comma-joined combination after a merge (``key,cert``) * - ``alg`` - Public-key algorithm: ``rsaEncryption``, ``id-ecPublicKey``, ``ED25519``, ``dsaEncryption`` * - ``keysize`` - Size in bits (integer), e.g. ``2048``, ``256`` * - ``subject`` / ``issuer`` - Certificate DN, OpenSSL oneline (``/C=US/O=Example/CN=host``) * - ``san`` - List of Subject Alternative Names (``DNS:localhost``, ``IP:192.0.2.1``, ``email:user@host``); unpopulated if the cert has no SAN extension * - ``notbefore`` / ``notafter`` - Validity timestamps (``2024-01-01T00:00:00Z``) * - ``expired`` - ``"yes"`` or ``&null`` * - ``fingerprint`` - SHA-256 of the cert or public key (hex) Certificate names fail on a key-only handle. **Hash** (``eh``, or ``op=hash``). .. list-table:: :header-rows: 1 * - Name - Value * - ``op`` - ``hash`` * - ``alg`` - Digest name: ``SHA256`` (default), ``SHA512``, ``SHA1``, ``SHA3-256``. ``alg=`` at open uses the lowercase OpenSSL spelling (``sha256``) * - ``hash`` - Current digest / MAC / signature bytes without finalizing (``EVP_MD_CTX_copy_ex``). Unpopulated until the first ``write()``. Not populated on ``verify``. * - ``bytecount`` - Bytes written so far (integer) * - ``blocksize`` / ``digestsize`` - Algorithm sizes (integers); SHA-256 is 64 / 32 **HMAC, sign, verify.** .. list-table:: :header-rows: 1 * - Name - Value * - ``op`` - ``hmac``, ``sign``, or ``verify`` * - ``hash`` - Running HMAC or signature bytes (same copy-and-finalize peek). Unpopulated on ``verify`` -- use ``verified``. * - ``alg`` - Digest used for HMAC or for the signature: ``SHA256``, ``SHA512``, ``SHA1`` * - ``bytecount`` - Bytes written (integer) * - ``verified`` - ``"yes"`` or ``&null`` after a verify; unpopulated before **Encrypt / decrypt.** .. list-table:: :header-rows: 1 * - Name - Value * - ``op`` - ``encrypt`` or ``decrypt`` * - ``cipher`` - Symmetric cipher: ``AES-256-GCM`` (default), ``AES-128-GCM``, ``AES-256-CBC``, ``ChaCha20-Poly1305``. ``cipher=`` at open uses ``aes-256-gcm``, etc. * - ``iv`` - IV that was set or generated (bytes) * - ``bytecount`` - Bytes processed (integer) **TLS socket** (after handshake). These are not ``open()`` attributes. .. list-table:: :header-rows: 1 * - Name - Value * - ``cipher`` - Negotiated suite: ``TLS_AES_256_GCM_SHA384``, ``TLS_AES_128_GCM_SHA256``, ``TLS_CHACHA20_POLY1305_SHA256`` (TLS 1.3) or ``ECDHE-RSA-AES256-GCM-SHA384`` (TLS 1.2) * - ``version`` - ``TLSv1.3``, ``TLSv1.2`` * - ``alpn`` - Selected protocol: ``h2``, ``http/1.1``; unpopulated if none * - ``peercert`` - Peer certificate as PEM * - ``certchain`` - List of PEMs * - ``subject`` / ``issuer`` - Peer DN, e.g. ``/CN=host.example`` * - ``san`` - Peer SANs as a list (``DNS:host.example``) * - ``notbefore`` / ``notafter`` / ``expired`` - Peer validity; ``expired`` is ``"yes"`` or ``&null`` * - ``certverified`` - ``"yes"`` or ``&null``. Always populated: with ``verifyPeer=yes`` it is ``"yes"`` (otherwise ``open()`` failed). With ``verifyPeer=no`` it reports what strict verification would have said. * - ``verifyresult`` - ``ok``, ``expired``, ``self-signed``, ``untrusted-CA``, ``hostname-mismatch``, or ``revoked``. Always populated. * - ``hostnamematch`` - ``"yes"`` or ``&null``: did CN/SAN match the requested host, independent of chain trust. Always populated when a peer certificate and hostname are available. Handshake failure means ``open()`` failed; there is no handle to subscript. Distinguish expired / untrusted CA / hostname mismatch / no shared cipher / protocol version via ``&errornumber`` 1320--1325. .. code-block:: unicon cert := open("client.crt", "e") | stop(&errortext) write("material ", cert["type"], " ", cert["subject"]) every name := !cert["san"] do write(" SAN ", name) every k := key(cert) do write(" ", k) h := open("sha256", "eh") | stop(&errortext) write(h, "abc") write("op=", h["op"], " peek hash *", *h["hash"]) close(h) conn := open("host:443", "ne", "ca=unicon-ca.crt") | stop(&errortext) write(conn["cipher"], " ", conn["version"]) every pem := !conn["certchain"] do write(*pem, " byte PEM") close(conn) .. _sec-handles: 7. Material handles =================== Mode ``e`` with no ``op=`` loads keys and certificates. Content is auto-detected (PEM by header, DER by probe-by-parse). A file that holds several items -- a combined cert+key PEM, or a cert plus chain -- loads into one handle. ``type=`` narrows that to a single role. .. code-block:: unicon k := open("signing.pem", "e") pk := open("verify.pem", "e") c := open("client.pem", "e") kb := open(key_bytes, "er") ek := open("enc.key", "e", "keypass=" || pw) c := open("bundle.pem", "e", "type=cert") A raw key is still a file value; ``h["type"]`` reports the role: .. code-block:: unicon procedure main() local k k := open("sixteen-byte-key-material-here!!", "er") | stop(&errortext) write(type(k)) write(k["type"]) close(k) end Output:: file symkey Material split across files can be merged: .. code-block:: unicon idh := open("client.pem", "e") Attrib(idh, "key=client.key") A handle is a file value (``type(h)`` is ``"file"``) carrying ``Fs_Crypto``, the same way a window or socket is a file that does not support every file operation. The underlying ``CryptoFile`` is malloc'd so pointers stay stable across garbage collection and are freed by ``close()``. Consumers ask a handle for the roles they need. An encrypted socket wants a cert and a private key: one merged handle satisfies both, or two handles each contribute what they have. ``op=sign`` wants a private key; ``op=hmac`` wants symmetric bytes. A missing role, or two handles supplying the same role, fails at ``open()``. .. _sec-examples: 8. Examples =========== These assume a build with ``secure sockets layer encryption``. Failures set ``&errortext``; the usual form is ``h := open(...) | stop(&errortext)``. Package ``crypto`` (``uni/lib/crypto.icn``) supplies ``hexencode`` / ``hexbytes`` and base64 helpers for dumping binary digests and keys. The programs under ``tests/crypto/`` are the executable form of this section. .. _sec-ex-hash: 8.1 Hash -------- SHA-256 of the known vector ``abc``: .. code-block:: unicon import crypto procedure main() local h, digest h := open("sha256", "eh") | stop(&errortext) write(h, "abc") digest := read(h) write(hexbytes(digest, 1)) close(h) end Output:: ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad ``write()`` may be called many times; ``read()`` finalizes the digest **and resets** the handle, so the same handle can hash unrelated messages. Chunked ``"hel"`` + ``"lo"`` matches a single write of ``"hello"``. A read with nothing pending fails: .. code-block:: unicon import crypto procedure main() local h, d1, d2 h := open("sha256", "eh") | stop(&errortext) write(h, "hello") d1 := hexbytes(read(h), 1) write(h, "hel") write(h, "lo") d2 := hexbytes(read(h), 1) write(d1) write(d2) if read(h) then write("idle:fail") else write("idle:ok") close(h) end Output:: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 idle:ok A whole file uses the file-transform shape. The first ``read()`` consumes the file and returns the digest: .. code-block:: unicon import crypto procedure main() local f, h, digest f := open("hash.dat", "w") | stop(&errortext) writes(f, "abc") close(f) h := open("hash.dat", "re", "op=hash", "alg=sha256") | stop(&errortext) digest := read(h) write(hexbytes(digest, 1)) close(h) remove("hash.dat") end Output:: ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad .. _sec-ex-hmac: 8.2 HMAC -------- Raw key bytes use mode ``er``. ``read()`` returns the MAC; the same handle can MAC many messages after each finalize: .. code-block:: unicon import crypto procedure main() local hm, mac hm := open("secret-key-bytes", "er", "op=hmac", "alg=sha256") | stop(&errortext) write(hm, "message one") mac := read(hm) write(hexbytes(mac, 1)) write(*mac) close(hm) end Output:: 9fd1a900325bd173af31bf6467de8139d6615202654cf0fe2645cd2997eaf879 32 A key loaded from a file is the same, with the material handle as the ``open()`` target and ``op=hmac`` as an attribute: .. code-block:: unicon k := open("hmac.key", "e") hm := open(k, "e", "op=hmac", "alg=sha256") .. _sec-ex-sign: 8.3 Sign and verify ------------------- A private key signs; a certificate or public key verifies. ``read()`` on the signer returns the signature bytes. The listing assumes ``signing.pem`` and ``verify.pem`` in the current directory: .. code-block:: unicon procedure main() local k, sg, pk, vf, signature k := open("signing.pem", "e") | stop(&errortext) sg := open(k, "e", "op=sign", "alg=sha256") | stop(&errortext) write(sg, "hello") signature := read(sg) | stop(&errortext) close(sg) pk := open("verify.pem", "e") | stop(&errortext) vf := open(pk, "e", "op=verify", "alg=sha256", "sig=" || signature) | stop(&errortext) write(vf, "hello") if read(vf) then write("ok") else stop("bad signature: ", &errortext) close(vf) close(k) close(pk) end Output:: ok A failed verify sets ``&errornumber`` 1315 so it is distinct from "nothing written." The same handle can check many messages by setting ``sig=`` in the idle window: .. code-block:: unicon procedure main() local k, sg, pk, vf, signature, msg k := open("signing.pem", "e") | stop(&errortext) sg := open(k, "e", "op=sign", "alg=sha256") | stop(&errortext) pk := open("verify.pem", "e") | stop(&errortext) vf := open(pk, "e", "op=verify", "alg=sha256") | stop(&errortext) every msg := !["one", "two"] do { write(sg, msg) signature := read(sg) | stop(&errortext) Attrib(vf, "sig=" || signature) write(vf, msg) if read(vf) then write(msg, ":ok") else write(msg, ":fail") } every close(sg | vf | k | pk) end Output:: one:ok two:ok .. _sec-ex-enc: 8.4 Symmetric encrypt and decrypt --------------------------------- The idle-window ``Attrib(..., "op=decrypt")`` reuses the bound key. Ciphertext is binary; the recovered plaintext is what matters: .. code-block:: unicon procedure main() local ky, c, ct, pt ky := open("sixteen-byte-key-material-here!!", "er") | stop(&errortext) c := open(ky, "e", "op=encrypt") | stop(&errortext) write(c, "hello") ct := read(c) Attrib(c, "op=decrypt") write(c, ct) pt := read(c) write(pt) close(c) close(ky) end Output:: hello With no ``iv=``, each ``read()`` generates a fresh IV and prepends it, so one handle can encrypt many messages. Lengths below include the IV and AEAD tag: .. code-block:: unicon procedure main() local ky, c, msg, ct ky := open("sixteen-byte-key-material-here!!", "er") | stop(&errortext) c := open(ky, "e", "op=encrypt") | stop(&errortext) every msg := !["alpha", "beta", "gamma"] do { write(c, msg) ct := read(c) write(msg, " ", *ct, " bytes") } close(c) close(ky) end Output:: alpha 33 bytes beta 32 bytes gamma 33 bytes For a wire format that does not prepend an IV, or for test vectors, set ``iv=`` in the idle window and take uniqueness yourself: .. code-block:: unicon every plaintext := !messages do { Attrib(c, "iv=" || newiv()) write(c, plaintext) ciphertext := read(c) } .. _sec-ex-file: 8.5 File encrypt and decrypt ---------------------------- Writes encrypt and are finalized on ``close()``. Reads decrypt as a stream. AEAD authentication is checked on the **final** read: earlier ``reads()`` chunks are provisional. Nothing decrypted should be acted on until the stream completes without error. .. code-block:: unicon procedure main() local ky, f, chunk, pending ky := open("sixteen-byte-key-material-here!!", "er") | stop(&errortext) f := open("secrets.dat", "we", "op=encrypt", ky) | stop(&errortext) writes(f, "classified") close(f) f := open("secrets.dat", "re", "op=decrypt", ky) | stop(&errortext) pending := "" while chunk := reads(f, 4096) do pending ||:= chunk if \ &errornumber then stop("tampered: ", &errortext) write(pending) close(f) remove("secrets.dat") close(ky) end Output:: classified Callers who cannot defer processing should use the data-pipe shape, where a single ``read()`` returns plaintext only after the tag verifies. The same key handle can wrap many files: .. code-block:: unicon keyh := open("aes.key", "e") every name := !filenames do { f := open(name || ".enc", "we", "op=encrypt", keyh) write(f, contents(name)) close(f) } .. _sec-ex-tls: 8.6 TLS identity reuse and DTLS ------------------------------- Encrypted TCP remains ``"ne"``. Material handles may be passed as attributes so certs and keys are parsed once. The socket asks by role, not by position: one combined PEM, or two handles, both work. .. code-block:: unicon id := open("client.pem", "e") conn := open("host:443", "ne", id, "verifyPeer=no") conn := open("host:443", "ne-", id) # same as verifyPeer=no certh := open("client.pem", "e") keyh := open("client.key", "e") conn := open("host:443", "ne", certh, keyh) After a handshake, ``conn["cipher"]`` peeks the negotiated suite and ``conn["certchain"]`` is a list of peer certificates as PEM strings; see :ref:`section 6.4 ` (``tests/crypto/tlspeek.icn``). ``certh["type"]`` / ``keyh["type"]`` are material-handle peeks, not session fields. Encrypted UDP (DTLS) uses ``"nue"`` / ``"naue"``. The runtime selects a DTLS context when the socket is datagram. .. _sec-io: 9. I/O and ``Attrib()`` ======================= On a data-pipe handle, ``write()`` accumulates and ``read()`` finalizes. ``read()`` is always terminal -- one read consumes the whole result and resets the handle -- for two reasons that are easy to forget later. First, the default cipher is AEAD (``aes-256-gcm``): nothing authenticated can be released until the tag is checked at the end, so a mid-stream plaintext read would be a lie. Second, ``read()`` already means "finalize and reset" for hash / HMAC / sign; a partial encrypt read would look identical to a final one, and the handle would reset under the caller. Block ciphers such as CBC or CTR *could* emit ciphertext at a block boundary, but that would be a second meaning of ``read()`` next to the AEAD and digest rules, so it is not offered. A second ``read()`` with nothing written since the last one fails, as when reading a socket with no data. ``close()`` is idempotent: a second ``close()``, or ``close()`` on a never-written handle, releases the handle and drops its reference to any bound key. On ``we``, writes encrypt to the file and ``close()`` flushes the AEAD tag. On ``re`` with ``op=decrypt``, ``reads()`` yields plaintext chunks. On ``re`` with ``op=hash``, the first ``read()`` returns the digest. ``Attrib()`` during the idle window may change ``op=``, ``alg=``, ``cipher=``, ``iv=``, and ``sig=``. A change while a digest or cipher is in flight fails (1316). Status gets are :ref:`section 6.4 `. Handles are not valid ``Attrib()`` values. Framing is owned by the runtime: IV prepended, tag appended, both fixed-length per cipher. The same layout is used for ``e`` / ``er`` output and for ``we`` files. .. _sec-errors: 10. Error handling ================== Crypto and TLS connect failures **fail**, they do not ``runerr``. This matches the SSL connect path: set ``&errortext``, tear down what was allocated, and fail, so callers write ``if h := open(...) then ... else ...``. Runtime errors are reserved for type mistakes (a bad mode letter, a non-file where a handle is required). Error numbers, defined in ``src/runtime/data.r``: .. list-table:: :header-rows: 1 * - Number - Text - Typical cause * - 1300--1308 - SSL / certificate / cipher errors - TLS context and handshake * - 1311 - unknown cryptographic algorithm or cipher - Bad ``alg=`` or ``cipher=`` * - 1312 - cryptographic handle lacks required material - Missing key, cert, or wrong role * - 1313 - duplicate cryptographic material role - Two handles both supply the same role * - 1314 - AEAD authentication failed - Tampered or truncated ciphertext * - 1315 - signature verification failed - ``op=verify`` did not match * - 1316 - invalid cryptographic operation state - Mid-operation ``Attrib()``, bad ``op=``, empty read * - 1317 - cryptographic material could not be loaded - Unreadable path, parse failure, allocation .. _sec-build: 11. Build integration ===================== Crypto compiles under ``HAVE_LIBSSL``, the same guard as TLS. ``configure --disable-ssl`` turns both off. ``--enable-thin`` disables SSL as part of a minimal build. Otherwise ``CHECK_OPENSSL`` probes for the library and headers. ``&features`` reports ``secure sockets layer encryption``. ``tests/crypto/Makefile`` skips the suite when that string is absent. TLS echo tests also require ``concurrent threads``. Guards are ``#if HAVE_LIBSSL`` in ``rstructs.h``, ``rmacros.h``, ``feature.h``, ``fsys.r``, ``fmisc.r``, ``rposix.r``, and ``rcrypto.ri``. TLS helpers (``create_ssl_context``, ``is_ssl_attr``) and the ``CryptoFile`` implementation share ``src/runtime/rcrypto.ri``. .. _sec-status: 12. Status and remaining work ============================= The language surface described here is implemented: modes ``e`` / ``er`` / ``eh`` / ``re`` / ``we``, TLS handle reuse, DTLS context selection and handshake, ``keypass=`` / ``verifyPeer=`` alignment with SSH, and the error numbers in :ref:`section 10 `. **Separate configure switch.** Crypto cannot be compiled out while leaving TLS sockets on. A ``Unicon_Crypto`` / ``NoCrypto`` guard driven by ``--disable-crypto`` is the obvious next step if a distribution wants encrypted sockets without the new ``open()`` modes. **``~`` in ``key=``.** Not expanded, matching SSH. Callers must expand a leading ``~/`` themselves. **Asymmetric encrypt / decrypt.** The ``op=`` set is hash, HMAC, sign, verify, and symmetric encrypt/decrypt. Public-key encryption is not exposed. **Trust-on-first-use and key generation** are out of scope. Certificates and keys are created out of band. .. _sec-coverage: Appendix: test coverage ======================= Automated tests live under ``tests/crypto/``, one program per concern: ``hash``, ``hmac``, ``sha512``, ``sign``, ``encrypt``, ``iv``, ``freshiv``, ``tamper``, ``filehash``, ``filecrypt``, ``filekey``, ``filekeyattr``, ``filetamper``, ``opswitch``, ``midattrib``, ``badalg``, ``badkey``, ``badrole``, ``badverify``, plus TLS / DTLS (``tlsplain``, ``tlsverify``, ``tlsauth``, ``tlsproto``, ``tlscipher``, ``tlshandle``, ``tlspeek``, ``dtls``, ``dtlsecho``). ``tests/crypto/Makefile`` skips the suite when OpenSSL is absent, and skips the threaded TLS echo tests when concurrency is absent. Expected output is ``tests/crypto/stand/*.std``. References ========== .. bibliography:: utr27.bib