Skip to content

About

Lightweight cryptographic primitives for Mach

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

332 Commits

Folders and files

Repository files navigation

mach-crypto

Lightweight cryptographic primitives for Mach.

This repository owns reusable algorithms and their assurance evidence. Protocol state machines belong in protocol repositories such as mach-tls.

Modules

  • crypto.secret owns allocated secret storage, bounded views, explicit moves, allocating clones, entropy initialization, and deterministic destruction.
  • crypto.hash provides incremental SHA-256 and SHA-384 transcript hashing, and BLAKE2b with keyed, salted, personalized and tree modes.
  • crypto.hmac provides one-shot HMAC-SHA-256 and HMAC-SHA-384, with public or secret tags.
  • crypto.hkdf provides SHA-256 and SHA-384 extract and expand operations.
  • crypto.aead.aes_gcm provides AES-128-GCM and AES-256-GCM record protection.
  • crypto.cipher.aes provides the AES-128 and AES-256 block cipher under one expanded key.
  • crypto.aead.chacha20_poly1305 provides ChaCha20-Poly1305 record protection.
  • crypto.group.x25519 provides X25519 key agreement.
  • crypto.group.p256 provides SEC1 P-256 public keys and ECDH.
  • crypto.signature provides ECDSA verification over P-256, P-384, and P-521 with any of SHA-256, SHA-384, and SHA-512, Ed25519, RSA-PSS, and strict RSA PKCS#1 v1.5 signatures.
  • crypto.encoding.der, crypto.encoding.pem, and crypto.encoding.keys provide strict TLS key-container parsing and exact serialization.
  • crypto.vectors defines a common test vector contract.
  • crypto.assurance publishes the validation state of this package.

use crypto; binds crypto.lib.crypto, which re-exports these modules for consumers that prefer one import.

Secret ownership

crypto.secret.Secret is move-only by contract. Initialize every owner with secret.empty(), then allocate it with secret.init or secret.init_random. Use secret.move to transfer the allocation and secret.clone when an independent copy is required. Direct record copies are invalid because they duplicate ownership.

secret.init returns fully zeroed storage, and exactly one layer zeroes it. An allocator whose zeroes_on_allocate is set returns zeroed storage itself, and secret.init uses it as returned. The system allocators set it, because std.memory.secret already allocates zero-filled storage. For any other allocator secret.init zeroes the allocation.

secret.bytes and secret.buffer return bounded borrowed views. A view must not outlive its owner or any move or destroy operation. Exactly one layer wipes each release. An allocator whose wipes_on_release is set wipes the whole span before every release attempt and leaves zeroed storage when the attempt fails, so secret.destroy hands it the storage untouched. The system allocators set it, because std.memory.secret already wipes on release. For any other allocator secret.destroy wipes storage immediately before every release attempt, and if the deallocator mutates the allocation and fails while retaining ownership, wipes it again before returning. Either way the owner remains in a wiped cleanup-pending state, and a later destroy retries with fully zeroed storage.

crypto.secret.SecretArray[T] owns a dynamically sized typed allocation while preserving every field's secrecy shape. Initialize an owner with secret.empty_array[T], construct the native allocator with secret.system_array_allocator[T], and call secret.init_array[T] with the element count. secret.array_data[T] returns *T only while the owner is active. No raw pointer or declassification cast is needed for directly secret values or records with deeply nested secret fields.

Array initialization checks count * $size_of(T), supplies $align_of(T) to the typed allocator, and returns the complete allocation zeroed, zeroing it itself unless the allocator sets zeroes_on_allocate. A zero-count array is active with nil data and acquires no allocation. destroy_array[T] splits the release wipe the same way. A wiping allocator zeroes every element itself, and for any other allocator destroy_array[T] wipes all elements before every release attempt and rewipes any callback mutation after a failed one. A failed release then retains the typed pointer, count, byte size, allocator, and cleanup-pending state for retry. Custom ArrayAllocator[T] callbacks follow the same ownership contract. A nonnil output transfers the full requested allocation even when allocation reports failure.

The native allocator and entropy source delegate to mach-std secret-welded OS primitives. Storage remains secret-welded through every native boundary, std's allocation begins zeroed, which is the only allocation zeroing on this path, entropy fills are complete or leave a fully wiped destination, and std's release wipes before returning storage to the operating system, which is the only release wipe on this path. Custom allocators follow the same ownership rule. A nonnil allocation result transfers ownership even when allocation reports failure, allowing partial storage to be wiped and released deterministically.

Key containers

encoding.der accepts only definite, minimally encoded lengths and rejects noncanonical BOOLEAN, INTEGER, BIT STRING, NULL, and OBJECT IDENTIFIER values. Complete-document parsing rejects trailing bytes and validates every nested constructed value to a maximum depth of 32, which covers CMS SignedData carrying an RFC 3161 time-stamp token. Cursor failures are transactional. The public writer supports overlapping content through move semantics and validates constructed content before changing output.

encoding.pem uses standard base64 with canonical padding and 64-character lines. Labels are bounded to 64 uppercase ASCII letters, digits, and single interior spaces. Decoding accepts LF or CRLF and an optional final newline, but rejects leading or trailing data, mismatched labels, nonfinal short lines, noncanonical padding bits, URL-safe base64, and embedded whitespace. Exact-base in-place decoding is supported. Public encoding rejects every overlap. Secret encoding rejects exact-base overlap, and nonidentical secret views are disjoint by contract.

encoding.keys supports the key containers needed by TLS:

  • RFC 8410 Ed25519 and X25519 PKCS#8 private keys and SPKI public keys
  • P-256 SEC1 and PKCS#8 private keys and uncompressed SPKI public keys
  • P-384 and P-521 uncompressed SPKI public keys
  • PKCS#1 RSA private keys, RSA PKCS#8 private keys, and RSA SPKI public keys

Algorithm identifiers and parameters are exact. P-256 private scalars are range-checked. An embedded SEC1 public point must be on the curve and match the private scalar. RSA containers require version zero, a 2048 through 4096-bit odd modulus, a canonical odd public exponent smaller than the modulus, and all eight PKCS#1 key integers. Encrypted PKCS#8, version-one OneAsymmetricKey, multi-prime RSA, compressed P-256 points, and legacy RSA PUBLIC KEY PEM are not accepted.

Initialize every private output with keys.empty_private. Successful DER decoding copies the complete canonical document into one owned secret allocation. Successful PEM decoding allocates the exact decoded DER size once and transfers that allocation directly into the key. keys.private_bytes returns a borrowed seed or scalar. keys.copy_rsa copies the public modulus and exponent and a modulus-width private exponent into caller storage. It snapshots the private exponent first, so private output may overlap the owning key. Overlapping public RSA outputs are rejected.

Private keys are move-only by contract and must be released with keys.destroy_private. A failed allocation or parse normally leaves the output empty. If cleanup deallocation fails, the output retains wiped cleanup-pending storage while the primary error is returned. Call destroy_private after that failure to retry release. Serialization reproduces the exact accepted DER, not a reconstructed variant. Public SPKI views borrow the caller's DER or PEM scratch buffer and must not outlive it.

Transcript hashes

hash.Sha256 and hash.Sha384 are separate incremental state records for public protocol transcripts. Initialize each record with hash.empty_sha256 or hash.empty_sha384, then activate it with the corresponding init function. Repeated update calls accept bounded public fragments, including an empty fragment. SHA-256 input is bounded to 2^61 - 1 bytes. SHA-384 uses its full 128-bit encoded length field and accepts totals through 2^64 - 1 bytes.

snapshot_sha256 and snapshot_sha384 write the current digest without changing the source state. The same state remains usable for later updates, which is the TLS transcript operation used at handshake checkpoints. final writes the digest and consumes the state only after successful output. Validation, capacity, and length-overflow failures leave both state and output unchanged.

destroy_sha256 and destroy_sha384 zeroize the complete state and are idempotent. A destroyed state rejects update, snapshot, and final, but can be activated again with init. This supports the TLS HelloRetryRequest transcript rewrite without exposing the underlying state. State records are opaque and must not be copied directly. SHA-2 runs on crypto.hash.sha256 and crypto.hash.sha512, and the former dispatches SHA-256 to SHA-NI or ARMv8 SHA2 where the processor has it. Secret input uses those modules' secret-typed states, and public input is lifted into them block by block. Snapshot clones, lift buffers, temporary digests, and destroyed contexts are explicitly zeroized.

BLAKE2b

crypto.hash.blake2b is the RFC 7693 primitive. blake2b.Params is the whole parameter block: digest length (1 to 64 bytes), 16-byte salt, 16-byte personalization, and the tree fields with the last-node flag. blake2b.params(n) returns sequential parameters for an n-byte digest, and blake2b.valid checks a block. The digest length is a parameter of the hash, not a truncation of a 64-byte digest. The byte counter is the full 128 bits of the specification, so no input length overflows it.

hash.Blake2b hashes public input under any parameter block. It follows the transcript contract above: empty_blake2b, init_blake2b, update_blake2b, snapshot_blake2b, final_blake2b and destroy_blake2b. snapshot and final write exactly the configured digest length. hash.digest_blake2b is the one-shot form, and its output may overlap its input.

hash.Blake2bSecret runs on the secret-typed state. init_blake2b_secret takes a secret key of 0 to 64 bytes, where an empty key is unkeyed hashing of secret input. Longer keys are rejected with INVALID_KEY. Input arrives as secret bytes through update_blake2b_secret or as public bytes through update_blake2b_public, and the two may be mixed. The digest is written to a contracts.SecretBuffer, and declassifying it is the caller's decision. hash.mac_blake2b_secret is the one-shot keyed form with a secret tag, and hash.mac_blake2b declassifies the tag into public output. The tag output may overlap the message because nothing is written until the tag is complete.

The secret state, snapshot clones, lift buffers and temporary digests are explicitly zeroized, and the compression is checked constant-time at the secret instance.

HMAC and HKDF

hmac.sha256 and hmac.sha384 accept secret keys, public messages, and caller-owned public output. The output capacity must be at least the full tag size. Successful calls write the full tag. The output may overlap the public message because no output is written until authentication is complete.

hmac.sha256_secret and hmac.sha384_secret take the same inputs and write the tag into a caller-owned contracts.SecretBuffer, for a tag that is itself secret derived material, such as a stateless reset token or a derived key. The caller decides where, or whether, to declassify it. The output may overlap the key or the message. hmac.sha256 and hmac.sha384 are these functions followed by one declassify into the public output.

hkdf.extract_sha256 and hkdf.extract_sha384 accept secret salt and input key material and write a fixed-size secret pseudorandom key. Empty salt and input key material follow RFC 5869. Extract output may overlap either input because the complete pseudorandom key is held locally before it is copied.

hkdf.expand_sha256 and hkdf.expand_sha384 treat output capacity as the requested output length. The pseudorandom key must be exactly the hash output size. Zero-length output is valid. Output is bounded to 255 hash blocks, which is 8160 bytes for SHA-256 and 12240 bytes for SHA-384. Expand output may overlap the pseudorandom key because the key is snapshotted before the first write.

Every validation failure returns written = 0 without changing output. Public buffers cannot alias nonempty secret buffers under Mach's secret-welded pointer types. Internal SHA-2 state, schedules, HMAC pads, intermediate tags, pseudorandom key snapshots, and expansion blocks are explicitly zeroized.

AES-GCM

aead.aes_gcm.Context holds one key expanded once: the AES key schedule and the GHASH key. Construct it with aes_gcm.empty(), install a 16-byte or 32-byte secret key with aes_gcm.init, then call seal_with and open_with for any number of records. Neither ever expands the key again. aes_gcm.dnit zeroizes every key-derived field and returns the context to empty. It is idempotent. init on a context that is not empty, and seal_with or open_with on one that is, return INVALID_STATE without writing. The key may be released or overwritten once init returns. A failed init leaves the context empty. Every key-derived field is secret-typed, so the context is secret-welded like the key. It is move-only by contract and must not be copied directly.

seal_with and open_with take the arguments and keep the contracts of seal and open below, with the context in place of the key. aes_gcm.seal and aes_gcm.open are the one-shot form. Each call expands the key into a temporary context, runs seal_with or open_with, and wipes it. A caller that protects more than one record under a key, as TLS and QUIC do, holds a Context instead.

aead.aes_gcm.seal accepts a 16-byte or 32-byte secret key, an exact 12-byte public nonce, public additional authenticated data, secret plaintext, and caller-owned public output. It writes ciphertext followed by the complete 16-byte tag. Output capacity must cover both. Nonce and additional data may overlap output because both are fully consumed before the first write.

aead.aes_gcm.open accepts ciphertext followed by its complete tag and authenticates the entire record before decrypting any byte. On authentication failure it returns AUTH_FAILED, writes no plaintext, and zeroizes exactly the ciphertext-length prefix of secret output. Validation and capacity failures do not change output. Successful output may overlap the secret key because the expanded key schedule is complete before plaintext release.

Plaintext is bounded to 68,719,476,704 bytes, or 2^32 - 2 counter blocks. Additional authenticated data is bounded to 2^61 - 1 bytes so its bit length is representable in GCM's 64-bit length field. The implementation uses an algebraic AES S-box and fixed-position GHASH multiplication rather than memory lookups indexed by secret values. Expanded keys, cipher state, hash products, authentication tags, and stream blocks are explicitly zeroized.

AES and GHASH each run behind an internal backend seam (crypto.internal.aes and crypto.internal.ghash). A context records the member chosen when its key is installed, and every later call runs that member. The bitsliced software member is portable and constant time. The hardware members are AES-NI and PCLMULQDQ on x86_64 and the ARMv8 AES rounds and PMULL on aarch64. init picks them where the processor has the extensions, which std.system.cpu reports (or the build target selects, in which case nothing is probed), and the AES and GHASH members are chosen independently. The hardware kernels are fixed instruction streams with no branch or secret address, checked by #[oblivious], and GHASH reduces with shifts in the bit-reflected form of Intel's carry-less multiply white paper.

AES block cipher

cipher.aes.Context holds one AES-128 or AES-256 key schedule. Construct it with aes.empty(), expand the key once with aes.init, then call aes.encrypt_block for any number of 16-byte blocks. Input must be exactly one block, output must hold one, and they may be the same storage. aes.dnit zeroizes the schedule. The lifecycle, the INVALID_STATE rules and the welding are the same as for the AES-GCM context. This is the raw forward cipher for constructions such as QUIC header protection, which derives a five-byte mask from one encrypted packet sample. Record protection belongs to crypto.aead.

ChaCha20-Poly1305

aead.chacha20_poly1305.seal accepts an exact 32-byte secret key, an exact 12-byte public nonce, public additional authenticated data, secret plaintext, and caller-owned public output. It writes ciphertext followed by the complete 16-byte tag. Nonce and additional data may overlap output because both are consumed before the first output write.

aead.chacha20_poly1305.open authenticates the complete ciphertext and tag before decrypting any byte. Authentication failure returns AUTH_FAILED and zeroizes exactly the ciphertext-length prefix of secret output. Validation and capacity failures do not change output. Successful output may overlap the secret key because the ChaCha20 context snapshots it before plaintext release.

Plaintext is bounded to 274,877,906,880 bytes, or 2^32 - 1 payload blocks. Counter zero is reserved for the Poly1305 one-time key and payload counters run from one through 0xffffffff. Additional-data and ciphertext byte lengths are encoded as the complete 64-bit RFC 8439 length fields. ChaCha20 uses fixed rotations and positions. Poly1305 runs on three 44-bit limbs, nine crypto.internal.word products and one carry chain per block, with no secret-dependent table access. Keys, stream state, one-time keys, accumulators, authentication tags, and stream blocks are explicitly zeroized.

This module protects TLS records and QUIC packet payloads. QUIC header protection is a separate primitive contract because it consumes a packet sample to generate a five-byte mask rather than an AEAD nonce and payload. For the AES suites that primitive is cipher.aes.

X25519

group.x25519.derive_public accepts an exact 32-byte secret private value and writes its 32-byte RFC 7748 public u-coordinate to caller-owned public output. group.x25519.agree accepts an exact 32-byte secret private value and 32-byte public peer encoding, then writes the shared value to caller-owned secret output. Private values are clamped internally. Consumers can create fresh private values directly with secret.init_random.

Peer decoding masks the high bit and accepts non-canonical values by reducing them modulo 2^255 - 19, as required by RFC 7748. Any peer encoding that produces the all-zero shared value is rejected with INVALID_KEY. Validation, capacity, and low-order failures return written = 0 without changing caller output. Successful agreement output may overlap the private key because the complete scalar is snapshotted before output is written.

The implementation performs the same 255-step Montgomery ladder for every private value. Conditional swaps use masks rather than secret branches. Field operations use fixed-position radix-2^51 limbs. Each field product is 25 limb products, computed with the processor's 64 x 64 widening multiply where mach admits it as constant time ($mach.build.ct_mul(low, 128): x86-64 on every OS, aarch64 on linux and darwin under PSTATE.DIT, riscv64 under Zkt) and as a fixed 255-step masked sum everywhere else. Secret division is never used. Scalar, field, inversion, encoded result, and rejected shared-secret state are explicitly zeroized.

P-256, P-384, and ECDSA

group.p256.derive_public accepts an exact 32-byte big-endian private scalar in 1..n-1 and writes the exact 65-byte uncompressed SEC1 public point. group.p256.agree accepts that scalar and an exact uncompressed SEC1 peer point, validates canonical coordinates and the curve equation, and writes the 32-byte big-endian shared x-coordinate to secret output. Compressed points, the point at infinity, off-curve points, non-canonical coordinates, and invalid private scalars are rejected. Successful agreement may overwrite its private scalar because the complete scalar is snapshotted first.

signature.sign_ecdsa_p256_sha256 hashes the public message with SHA-256 and uses RFC 6979 HMAC-SHA-256 nonce generation. It writes a strict minimal DER signature of at most 72 bytes. signature.verify_ecdsa_p256_sha256 accepts an exact uncompressed SEC1 public key and strict DER, rejects zero or out-of-range r and s, and accepts both high- and low-s mathematical signatures. All validation and capacity failures return written = 0 without changing output. Signing evaluates eight nonce candidates before selecting the first valid one, which hides the RFC rejection count and bounds all-candidate failure below the P-256 security level.

signature.verify_ecdsa_p384_sha384 hashes the public message with SHA-384 and accepts an exact 97-byte uncompressed secp384r1 public key and strict DER signature of at most 104 bytes. It validates canonical coordinates, the curve equation, and scalar ranges. P-384 private signing and ECDH are not exposed.

signature.verify_ecdsa takes the curve (CURVE_P256, CURVE_P384, or CURVE_P521) and the hash (HASH_SHA256, HASH_SHA384, or HASH_SHA512) independently, as CMS and PDF signers choose them, and hashes the public message. signature.verify_ecdsa_digest takes the curve and a 32, 48, or 64-byte digest the caller computed. A digest longer than the curve order is truncated to its leftmost bytes and a shorter one is padded on the left, per FIPS 186-5. The public key is an exact uncompressed SEC1 point (65, 97, or 133 bytes) and the signature is strict DER, up to 72, 104, or 139 bytes. The two fixed pairs above call verify_ecdsa. P-521 is verification only, so no signing or ECDH is exposed for it.

Field and scalar values are four 64-bit limbs on one Montgomery ring (crypto.internal.mont), and field coordinates stay in Montgomery form inside a point. Each product is 16 limb products and a 16-product reduction, computed with the processor's 64 x 64 widening multiply where mach admits it as constant time ($mach.build.ct_mul(low, 128)) and with a fixed 64-step masked sum everywhere else. Inversions use fixed public addition chains, scalar multiplication uses a fixed 64-window point loop with masked selection and no secret-indexed table, and the fixed-base comb table is public data read under a mask. Private scalars, nonce state, hashes, field and scalar temporaries, and projective points are explicitly zeroized.

P-384 verification uses the same ownership and constant-shape contract on six 64-bit limbs of the same ring. Verification evaluates the two ECDSA point products together in one fixed 384-step loop with masked four-way point selection and no secret-indexed table. Inversions retain fixed public exponents. Hashes, field and scalar temporaries, and projective points remain welded until explicitly zeroized.

P-521 verification (crypto.internal.p521) follows the P-384 code on nine 64-bit limbs, with a 521-step loop. A P-521 signature of 128 or more content bytes carries the DER long-form sequence length 0x81, which the parser requires and accepts only in minimal form.

Ed25519 and RSA signatures

signature.derive_ed25519_public derives an exact 32-byte RFC 8032 public key from an exact 32-byte private seed. signature.sign_ed25519 writes an exact 64-byte deterministic pure-Ed25519 signature. Verification requires canonical point encodings, S < L, and non-identity prime-subgroup public and nonce points. Malformed public keys return INVALID_KEY. Malformed or mathematically invalid signatures return AUTH_FAILED.

RSA keys use explicit RsaPublicKey and RsaPrivateKey records. A modulus is a canonical big-endian, odd, full-width value from 256 through 512 bytes in four-byte increments. This admits 2048, 3072, and 4096-bit TLS keys. A public exponent is canonical big-endian, odd, at least three, and less than the modulus. A private exponent is secret, left-zero-padded to exactly the modulus width, and is paired with its public exponent so signing can verify its own result before release.

The arithmetic runs on 64-bit limbs in a Montgomery ring derived from the modulus per operation, with every limb product a crypto.internal.word product, so a target with the hardware multiply computes one instruction per limb pair. A modulus that does not fill its top limb, such as 2080 bits, runs on the same ring. Exponentiation uses fixed 4-bit windows: four squarings and one multiply per window, with the table entry read by a masked scan over all sixteen entries so no secret indexes memory, and the same path for public and private exponents.

signature.sign_rsa_pss_sha256 requires an exact 32-byte caller-provided secret salt. signature.sign_rsa_pss_sha384 requires an exact 48-byte salt. The caller owns salt generation and should use secret.init_random for every signature. The encoded message uses emBits = modBits - 1, MGF1 with the same hash, an exact hash-length salt, and trailer 0xbc. Signatures are exactly the modulus width. Verification enforces the same salt and encoded-message policy.

signature.verify_rsa_pss_sha512 verifies RSA-PSS with SHA-512, MGF1 over SHA-512 and a 64-byte salt. signature.sign_rsa_pss_sha512 is not provided.

signature.verify_rsa_pkcs1_sha256, signature.verify_rsa_pkcs1_sha384, and signature.verify_rsa_pkcs1_sha512 accept certificate signatures encoded by EMSA-PKCS1-v1_5. Verification requires a signature exactly as wide as the modulus, at least eight 0xff padding bytes, the exact RFC 8017 SHA-256, SHA-384, or SHA-512 DigestInfo with explicit NULL parameters, and no leading or trailing bytes. Malformed encodings and digest mismatches return AUTH_FAILED without changing caller-owned key, message, or signature storage.

Ed25519 and RSA-PSS signing consume the complete message before writing, so public message and output storage may overlap. Validation, capacity, and key failures write nothing and return written = 0. RSA signing performs a public exponentiation self-check before any signature byte is released. Secret seeds, expanded scalars, nonces, salts copied into encoded messages, private exponentiation state, message hashes, masks, encoded messages, and unreleased signatures are explicitly zeroized. signature.algorithm_status returns UNSUPPORTED for an unknown TLS signature identifier, independently of INVALID_KEY and AUTH_FAILED operation results.

Status

The repository combines callable primitives with contracts for algorithms that are still being built. AES-128-GCM, AES-256-GCM, the AES-128 and AES-256 block cipher, ChaCha20-Poly1305, X25519, P-256 ECDH, ECDSA P-256 with SHA-256, ECDSA P-384 verification with SHA-384, ECDSA verification over P-256, P-384, and P-521 with SHA-256, SHA-384, and SHA-512, Ed25519, RSA-PSS and RSA PKCS#1 v1.5 with SHA-256 and SHA-384, and their verification with SHA-512, HMAC-SHA-256, HMAC-SHA-384, HKDF-Extract, HKDF-Expand, incremental SHA-256 and SHA-384, strict DER and PEM, and TLS key containers are callable in this revision. Their tests include NIST FIPS 197, NIST SP 800-38A, NIST SP 800-38D, other NIST, RFC 4231, RFC 5869, RFC 6979, RFC 7468, RFC 7748, RFC 8017, RFC 8032, RFC 8410, RFC 8439, SEC 1, and independent OpenSSL vectors. They cover strict-encoding and tamper cases, counter and output limits, invalid inputs, fixed-buffer failures, and supported overlap.

Mach constant-time support is functional. Its current limitation is assurance, not expressiveness. The package will use Mach secret types and oblivious code, official vectors, differential tests, generated-code inspection, leakage tests, and independent review as distinct evidence layers.

tools/assurance run is the fail-closed release evidence gate. It runs the functional, official-vector, differential, invalid-input, leakage, and zeroization layers independently. It then performs two clean builds of every project for all six targets in debug and release mode with verified IR and emitted assembly. The two artifact sets must be byte-for-byte identical.

A successful run publishes an immutable machine-readable evidence directory under out/assurance/ and updates out/assurance/current. The report records the exact Git commit, compiler binary hash, compiler version, dependency lock, target, profile, algorithm, vector source, logs, generated-code samples, and artifact hashes. tools/assurance verify rejects missing, modified, or stale evidence. See the assurance guide for the complete contract.

The source-only floor remains assurance.SCAFFOLD because generated evidence is deliberately not trusted merely because a source constant says it exists. The gate advances the automated level in state.tsv only after every required artifact passes. Callable algorithm availability and independent review status are separate fields. Independent review is currently NOT_RECORDED.

Local development

Dependencies are pinned Git tags. Build output uses Mach's default out/ directory inside this repository.

mach test . runs only the tests the library reaches. mach test . -a tests runs every test, through src/lib/tests.mach. A new test module goes there, and tools/test-selection fails until it does.

mach dep pull .
mach build .
mach test . -a tests
tools/test-selection
tools/assurance run
tools/assurance verify

About

Lightweight cryptographic primitives for Mach

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages