#Digest
Status: standard library specification draft.
This chapter defines std.digest, which lib/std writes in ordinary hd
over the language tier:
Digest, the result of a hash, with constant-time equality;Sha256, a SHA-256 computation that takes its input in pieces;Sha256::hmac, the same hasher keyed for HMAC-SHA256;sha256, the SHA-256 digest of bytes in one call.
Nothing in it is language tier. The 32-bit word arithmetic uses the wrapping methods of Num and the shifts of Shifts.
- std-digest.import
std.digestdeclaresDigest,Sha256andsha256. None is a prelude name; code imports them, as inuse std.digest.sha256.
use std.digest.sha256
fn digest_of(text: string) -> string:
sha256(text.bytes().collect()).to_hex()#Digest
A Digest holds the bytes of a hash result. It compares in constant time,
as Go's hmac.Equal does, so checking a received signature does not
reveal how many leading bytes were right.
pub struct Digest:
pub bytes: List[u8]
impl Digest:
pub fn to_hex(self) -> string
impl Eq for Digest- std-digest.digest-value
Digestis a struct, a value, with one public field,bytes: List[u8]. A copy of a digest is independent of the original. Code may build one from bytes it received, asDigest { bytes: hex_decode(text)? }. - std-digest.digest.to-hex
to_hexreturns the bytes as lowercase hex text, ashex_encodedoes. - std-digest.digest.eqTwo digests are equal when their byte lists have the same length and the same bytes.
- std-digest.digest.eq.constant-timeWhen the lengths are equal,
==reads every byte pair, and neither its control flow nor its memory accesses depend on the byte values. - std-digest.digest.eq.lengthDigests of different lengths are unequal, and the comparison may return before it reads any byte.
- std-digest.digest.eq.scopeThe property covers
==on twoDigestvalues only. Comparing theirbyteslists orto_hextexts is not constant time.
use std.digest.{Digest, sha256}
use std.encoding.hex_decode
fn signature_matches(body: List[u8], sent_hex: string) -> bool:
match hex_decode(sent_hex):
.Ok(sent) => sha256(body) == Digest { bytes: sent }
.Err(_) => falseWhy. A
List[u8]comparison stops at the first differing byte. An attacker who times many forged signatures learns the right bytes one at a time. The length is not secret: a SHA-256 digest is always 32 bytes.
#SHA-256
sha256 hashes bytes with SHA-256:
use std.digest.sha256
fn checksum(text: string) -> string:
sha256(text.bytes().collect()).to_hex()
fn same_content(left: List[u8], right: List[u8]) -> bool:
sha256(left) == sha256(right)| Rule | Function | Result |
|---|---|---|
std-digest.sha256sha256 |
pub fn sha256(bytes: List[u8]) -> Digest |
the SHA-256 digest of bytes |
- std-digest.sha256.fipsThe digest is SHA-256 as FIPS 180-4 defines it, of the message that
bytesholds in order. - std-digest.sha256.lengthThe result's
bytesholds 32 bytes: the eight words of the final hash value in order, each written high byte first. - std-digest.sha256.hexSo
sha256(bytes).to_hex()returns 64 lowercase hex digits. - std-digest.sha256.oneshot
sha256(bytes)returns the same digest asSha256::new().update(bytes).finish(), and does not copybytes. - std-digest.no-panic
sha256,Sha256::new,Sha256::hmac,update,finish,to_hexand==on digests do not panic.
| Message | to_hex gives |
|---|---|
"" |
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
"abc" |
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad |
Note. A string's message is its UTF-8 bytes, from the language-tier
bytesmethod (module.string.bytes). So the digest of a text matches thesha256sumof a file that holds exactly that text.
Why. SHA-256 covers what a script needs: checksums, content addresses, and comparison with other tools. MD5 and SHA-1 are broken for collisions and are left out.
#Streaming
Sha256 hashes input that arrives in pieces, as Go's hash.Hash and
Rust's Digest do: new, then update for each piece, then finish.
A reader of a large file or a socket feeds each chunk to update and
never holds the whole message.
pub struct Sha256: ...
impl Sha256:
pub fn new() -> *mut Sha256
pub fn update(*mut self, bytes: List[u8]) -> *mut Sha256
pub fn finish(self) -> Digestuse std.digest.{Digest, Sha256}
fn digest_of_chunks(chunks: List[List[u8]]) -> Digest:
hasher := Sha256::new()
for chunk in chunks:
hasher.update(chunk)
hasher.finish()- std-digest.sha256.decl-struct
Sha256is a struct with private fields, used behind*mut. Code cannot build one except withSha256::new. - std-digest.sha256.new-ref
Sha256::new() -> &mut Sha256returns a new object, a hasher that has seen no bytes. Copies of the reference share it. - std-digest.sha256.update-ref
h.update(bytes: List[u8]) -> *mut Sha256addsbytesto the end of the message and returnsh, so calls chain; it takes*mut self. The message is the bytes of all calls in call order, so how it is split across calls does not change the digest. - std-digest.sha256.finish
h.finish() -> Digestreturns the digest of the message added so far, assha256defines it. It takesself, so it leaveshas it was:htakes moreupdatecalls, and a secondfinishreturns the same digest. - std-digest.sha256.memory
Sha256keeps at most one partial block of the message, fewer than 64 bytes.updatehashes each full block as it arrives, andfinishpads only the last block.
Why.
sha256needs the whole message in memory. Streaming lets a script hash a file larger than memory. Keeping the hasher usable afterfinishis what Go'sSumdoes.
#HMAC
Sha256::hmac keys the hasher, as BLAKE3's Hasher::new_keyed does.
Webhooks and signed tokens carry an HMAC-SHA256 of the body; the receiver
computes the same MAC under the shared secret and compares.
impl Sha256:
pub fn hmac(key: List[u8]) -> *mut Sha256use std.digest.{Digest, Sha256}
use std.encoding.hex_decode
fn signature_ok(secret: string, body: string, sent_hex: string) -> bool:
match hex_decode(sent_hex):
.Ok(sent) =>
expected := Sha256::hmac(secret.bytes().collect()).update(body.bytes().collect()).finish()
expected == Digest { bytes: sent }
.Err(_) => false- std-digest.hmac-ref
Sha256::hmac(key: List[u8]) -> &mut Sha256returns a new object, a hasher that has seen no message bytes. Itsfinishreturns HMAC-SHA256 of the message underkey, as RFC 2104 defines it with SHA-256 as the hash. - std-digest.hmac.keyA key longer than 64 bytes is replaced by its SHA-256 digest first; a key of up to 64 bytes is used as given. The result is padded with zero bytes to 64 bytes. An empty key is allowed.
- std-digest.hmac.finish
update, andfinishleaving the hasher unchanged, behave as for a plain hasher (update,finish): the split of the message across calls does not change the MAC. - std-digest.hmac.resultThe result is a
Digestof 32 bytes. Checking a received MAC isexpected == sent, constant time by==. - std-digest.hmac.vectorsThe MACs of RFC 4231 test cases 1 to 7 are the values that RFC lists for HMAC-SHA-256 (case 5 in full, not truncated).
Cost. A keyed hasher compresses three more 64-byte blocks than a plain one: the inner key block, the outer key block, and the block that holds the inner digest.
Why. Hashing
key || messageis not a MAC: an attacker who sees the digest can extend it. HMAC's two nested hashes avoid that.