objectstore · API

objectstore.crypto

objectstore.crypto — SHA-256, HMAC-SHA256 and hex, in pure Mojo.

AWS SigV4 is built entirely out of these two primitives, and nothing in the Mojo ecosystem provides either: hashes.mojo covers the non-cryptographic hashes Iceberg needs for partition transforms (murmur3, xxhash, CRC32), and no conda-resolvable package offers SHA-2. Rather than pull OpenSSL in through a second FFI shim — libcurl already links it, but its EVP surface is a moving target across conda builds — this module implements FIPS 180-4 and RFC 2104 directly.

SigV4 hashes the whole payload of every signed PUT, so this is not a negligible cost: a purely scalar compression function caps a multipart upload at the speed of the hash. The block loop therefore has three backends, chosen at compile time by _BACKEND:

  • armv8-crypto — the ARMv8 SHA-2 extension (sha256h/sha256h2/sha256su0 /sha256su1), which every Apple Silicon and every server aarch64 part has.
  • x86-sha-ni — Intel SHA extensions (sha256rnds2/sha256msg1 /sha256msg2), present on AMD Zen and on Intel from Goldmont/Ice Lake client onwards, but not on Ice Lake / Sapphire Rapids Xeons.
  • scalar — portable FIPS 180-4, fully unrolled with a 16-word rolling schedule so the whole working set stays in registers.

The choice is a comptime query of the target's CPU features, and mojo compiles for the host CPU unless told otherwise, so on a normal build (pixi source dependency, pixi run, CI) the compile-time answer is the run-time truth. A binary deliberately cross-built with --target-cpu for a CPU it will not run on is the one case that would fault, and that is the same contract as every other -march=native build.

If a general-purpose home ever appears, this belongs in hashes.mojo.

Structs

Sha256

struct Sha256

Streaming FIPS 180-4 SHA-256.

Streaming rather than one-shot because a SigV4 PUT has to hash the whole payload, which for a data file is larger than anything worth copying into one contiguous buffer just to hash it.

Implements Copyable, Movable

__init__

def __init__(out self)

update

def update(mut self, data: Span[UInt8])

digest

def digest(self) -> List[UInt8]

The digest of everything fed so far. Does not consume self, so a caller can keep hashing afterwards.

Functions

sha256_backend

def sha256_backend() -> String

Which compression backend this build is using: armv8-crypto, armv8-crypto (asm), x86-sha-ni or scalar. Benchmarks and tests print it so a CI log says which path it actually exercised.

sha256_target_features

def sha256_target_features() -> String

The CPU features this build was compiled against, as the backend choice saw them. A scalar backend on hardware that plainly has the extension means mojo did not resolve the host CPU — this is the line that says so.

sha256_scalar

def sha256_scalar(data: Span[UInt8]) -> List[UInt8]

SHA-256 forced through the portable backend, whatever _BACKEND is.

Exists so the test suite can cross-check the hardware path against the reference implementation on the very machine that runs it — the two are entirely separate code, and the FIPS vectors alone would not catch a schedule bug that only shows up past the first block.

sha256

def sha256(data: Span[UInt8]) -> List[UInt8]

sha256_hex

def sha256_hex(data: Span[UInt8]) -> String
def sha256_hex(s: String) -> String

hmac_sha256

def hmac_sha256(key: Span[UInt8], msg: Span[UInt8]) -> List[UInt8]

RFC 2104 HMAC with SHA-256. Keys longer than the 64-byte block are hashed first; shorter keys are zero-padded.

def hmac_sha256(key: Span[UInt8], msg: String) -> List[UInt8]

hmac_sha256_hex

def hmac_sha256_hex(key: Span[UInt8], msg: String) -> String

to_hex

def to_hex(data: Span[UInt8]) -> String

Lowercase hex — the case SigV4's canonical request requires.

from_hex

def from_hex(s: String) -> List[UInt8]

Aliases

comptime SHA256_DIGEST_SIZE = 32
comptime SHA256_BLOCK_SIZE = 64
comptime AT_HWCAP = 16
comptime HWCAP_SHA2 = 64