Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Applied Cryptography for Developers  Module 4 – Hash functions and their uses

Integrity checks, SRI and canonical hashing

Tell integrity from authenticity, pin scripts with Subresource Integrity, hash structured data without ambiguity, and see why XOR sums and CRC-32 fail.

  • Beginner
  • 30 minutes
  • Examples run with Node.js 24.21.0, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 30 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Distinguish integrity (a matching checksum) from authenticity (who produced it)
  • Generate and verify Subresource Integrity values for scripts and styles
  • Hash structured data unambiguously with canonical encodings, length prefixes and domain separation
  • Choose between cryptographic and non-cryptographic hashes for the job
  • Show why XOR-of-blocks hashes and CRC-32 checksums fail against an attacker, and what a keyed MAC changes

Before you start

What you’ll learn · Before you start

What you will learn

  • Distinguish integrity (a matching checksum) from authenticity (who produced it)
  • Generate and verify Subresource Integrity values for scripts and styles
  • Hash structured data unambiguously with canonical encodings, length prefixes and domain separation
  • Choose between cryptographic and non-cryptographic hashes for the job
  • Show why XOR-of-blocks hashes and CRC-32 checksums fail against an attacker, and what a keyed MAC changes

Before you start

On this page

A build that downloads a tool and its SHA-256 checksum from the same mirror accepts a replaced tool without a word: whoever could change one file could change both. A digest proves that bytes arrived as somebody intended, never who that somebody was, and most mistakes with hashes in real code come from forgetting the difference.

This lesson puts hashes to work and marks their limits. You separate integrity from authenticity, pin a script to its digest with Subresource Integrity, hash structured data so that one digest means exactly one message, see on messages made here why XOR sums and CRC-32 protect nothing against an attacker, and choose the right hash or check for a job. A two-step code lab at the end makes SRI values and unambiguous digests.

A matching checksum proves integrity, not who made the file

A digest from SHA-256 tells you whether bytes changed since the digest was made. That is data integrity: The guarantee that data has not been changed without authorisation since it was created, sent or stored. A MAC, an authenticated cipher's tag or a signature detects deliberate changes; a checksum catches only accidents. More in “What cryptography can and cannot do”. It says nothing about who made the bytes or the digest, which is source authentication: Assurance about who created or sent a piece of data, also called authenticity. A MAC shows that a holder of the shared key made it; a signature shows which private key made it. More in “What cryptography can and cannot do”, because anyone can compute a digest for any file: a hash has no key. Step through a build that trusts a checksum from the same place as its download:

Interactive · message by message A checksum from the same place as the file Step through a build that downloads a tool and its checksum from a mirror. The mirror's copy has been changed; watch which check notices.
A checksum from the same place as the file, step 5 of 5. Messages so far: Your build to A download mirror: GET tool.tar.gz; A download mirror to Your build: file + its own digest; The project's site to Your build: the expected digest.Your builddownloads and checksA download mirrorserves file + .sha256The project's siteHTTPS, its own keyGET tool.tar.gzfile + its own digestthe expected digestmismatch: build stops

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The download. Your build fetches tool.tar.gz from a mirror, then the file tool.tar.gz.sha256 from the same mirror.
  2. A changed copy. The mirror's copy of the file has been replaced. Whoever replaced it computed a new SHA-256 for the new file and replaced the checksum file too: nothing stops them, because a digest needs no key.
  3. The check passes. The build computes SHA-256 of the file it received and compares it with the checksum it received. They match. The check proved integrity, that the bytes arrived as the mirror sent them, and nothing about who made them.
  4. A digest from elsewhere. The fix is to take the expected digest from a source the mirror cannot change: the project's own site over HTTPS, a lock file committed to your repository, or a signature made with the project's key.
  5. The check fails, as it should. Compared with the project's digest, the mirror's file no longer matches, and the build stops before using it. Authenticity came from the channel or the signature, not from the hash.

The check passed on a changed file because both answers came from the same place. It started protecting the build only when the expected digest came from a source the mirror could not change. In practice that is one of three: the project’s own site over HTTPS or a lock file committed to your repository, which fix the digest you expect; a MAC with a key you share with the sender; or a digital signature, which anyone can check against the sender’s public key. The next module builds MACs, and the public-key module builds signatures.

HTTP has the same split. RFC 9530 defines the Content-Digest and Repr-Digest fields, which carry a body’s digest as in Content-Digest: sha-256=:…:, and says plainly that they are not meant as general protection against deliberate tampering: someone who can change the body in transit can replace the field too. For that it points to TLS or to HTTP Message Signatures, RFC 9421, which the api-design track covers.

Hashing a token is fine; hashing a password is not

Storing the SHA-256 of a 128-bit random reset token instead of the token works: the table that leaks gives back nothing, because a preimage of a random value cannot be guessed. A password is not random, so its SHA-256 can be guessed by hashing likely passwords; passwords need the slow, salted functions of the password module.

Checkpoint

Check: Integrity is not authenticity

  1. Question 1 of 2 A build downloads a tool and its .sha256 file from the same mirror, and the digests match. What has the build learned?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: That the file arrived as the mirror sent it, and nothing about who made it

    A matching digest proves integrity relative to the digest you hold. When the digest comes from the same place as the file, both can be changed together, so the check proves nothing about authenticity.

  2. Question 2 of 2 An API adds Content-Digest to its responses, as RFC 9530 describes. Does that protect against a party who can change messages in transit?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No; RFC 9530 says the fields are not meant as general protection against tampering, and points to TLS or HTTP Message Signatures

    Digest fields catch corruption. Against deliberate changes they need a key behind them: TLS for the connection, or a signature over the message such as RFC 9421's.

Subresource Integrity pins a script to its digest

A page that loads a script from another server runs whatever that server sends. Subresource Integrity (SRI): A W3C mechanism that lets a page pin a script or stylesheet to a SHA-256, SHA-384 or SHA-512 digest in its integrity attribute. A response with another digest is refused as a network error., a W3C specification, lets the page name the exact bytes it expects: the integrity attribute of a script or link element holds an algorithm, a hyphen and the Base64 of a digest, and browsers must support SHA-256, SHA-384 and SHA-512. Step through what the browser does with it:

Interactive · stage by stage What the browser does with integrity="sha384-…" Step through the check a browser runs before it uses a script or a stylesheet that carries an integrity attribute.
What the browser does with integrity="sha384-…", step 6 of 6. Showing Read the attribute, Pick the strongest, Fetch with CORS, Hash the response, Compare, Run or refuse. In focus: Run or refuse.page loads a fallbackRead the attributesha384-rDst8WSL…Pick the strongestsha512 > sha384 > sha256Fetch with CORScrossorigin="anonymous"Hash the responsesame algorithmCompareany listed value may matchRun or refuserefuse = network errorone changed byte is enough

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Parse. The attribute holds one or more values, each an algorithm name, a hyphen and the Base64 of a digest. Browsers must support sha256, sha384 and sha512; values with other names, such as md5, are ignored.
  2. The strongest algorithm. When several algorithms appear, only the strongest is used: sha512 before sha384 before sha256. An attribute with no supported value at all gives no protection, so check it when you write it.
  3. Fetch, with CORS. A script from another server is fetched in CORS mode, which is what crossorigin="anonymous" asks for; the specification notes that integrity checks need CORS.
  4. Hash what arrived. The browser hashes the bytes of the response with the chosen algorithm, here SHA-384.
  5. Compare. If the digest equals any value listed for that algorithm, the resource passes. Listing two values lets a page accept either of two versions during an update.
  6. Run, or refuse. A match runs the script. A mismatch is treated as a network error: the script never runs, and an error event lets the page load a fallback copy.

The program computes the value for the lesson’s small script with Web Crypto, compares it with the value its page pins, then changes one byte of the script, as a changed copy on the other server would:

An integrity value made, checked and broken by one byte JavaScript · sri-js/sri.mjs
// Subresource Integrity: compute the integrity value of a script with Web Crypto, check it against the value the page
// carries, and show what one changed byte does. The page and the script are files beside this program.
import { readFileSync } from 'node:fs';

async function integrityOf(bytes, algorithm = 'SHA-384') {
  const digest = await crypto.subtle.digest(algorithm, bytes);
  return `${algorithm.toLowerCase().replace('-', '')}-${Buffer.from(digest).toString('base64')}`;
}

const script = readFileSync('app.js');
const page = readFileSync('page.html', 'utf8');
const pinned = /integrity="([^"]+)"/.exec(page)[1];

const value = await integrityOf(script);
console.log(`integrity of app.js: ${value}`);
console.log(`the page pins:       ${pinned}`);
console.log(`a browser runs it:   ${value === pinned}`);

const changed = Buffer.from(script);
changed[changed.indexOf('ran')] = 'R'.charCodeAt(0); // one byte changed on the other server
const after = await integrityOf(changed);
console.log(`after one changed byte: ${after.slice(0, 20)}…`);
console.log(`a browser runs it:   ${after === pinned} (it reports a network error instead)`);
console.log(`the value for SHA-256 would be ${(await integrityOf(script, 'SHA-256')).length} characters, for SHA-512 ${(await integrityOf(script, 'SHA-512')).length}`);

Output

integrity of app.js: sha384-rDst8WSLk6rpAjGeULUERiKE1i87H21JKbSP+JD3rdKoPDqMQkx+fZxtxMql3bkE
the page pins:       sha384-rDst8WSLk6rpAjGeULUERiKE1i87H21JKbSP+JD3rdKoPDqMQkx+fZxtxMql3bkE
a browser runs it:   true
after one changed byte: sha384-6lr6I+D8uIoIu…
a browser runs it:   false (it reports a network error instead)
the value for SHA-256 would be 51 characters, for SHA-512 95

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node sri.mjs

The value matched the page, so a browser would run the script. After one changed byte the SHA-384 digest shares nothing with the pinned one, and the specification has the browser refuse the response as a network error, with an error event the page can use to load a fallback. SHA-384 values take 71 characters, SHA-256 values 51 and SHA-512 values 95. Because the script comes from another origin, the element needs crossorigin="anonymous": the specification notes that integrity checks need CORS.

An integrity value the browser cannot use

Wrong: integrity="md5-…" or integrity="sha1-…", copied from an old build tool. Browsers ignore algorithms other than SHA-256, SHA-384 and SHA-512, and an attribute with no usable value means no check at all, so the page loads any script. Right: generate sha384- values in the build and fail the build when a value is missing or unusable.

Checkpoint

Check: Subresource Integrity

  1. Question 1 of 2 The server that hosts a pinned script changes one byte of it. What does the browser do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It refuses the response as a network error and does not run the script; an error event lets the page load a fallback

    With integrity="sha384-…", any change to the bytes makes the digest differ, and the browser refuses the response instead of running it.

  2. Question 2 of 2 A script tag carries integrity="sha256-A sha384-B", where A is right for the script and B is wrong. What does a browser decide?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It refuses the script, because only the strongest algorithm present, sha384, is checked

    Browsers pick the strongest algorithm among those listed, here SHA-384, and the resource passes only if it matches one of the values for that algorithm. Several values of one algorithm are allowed, for example during an update.

One digest should mean one message: framing and canonical forms

A digest is only unambiguous if the bytes it covers are. Code that hashes several fields by joining them loses the boundaries between them, so two different requests can hash alike. This program builds a download-link digest from a user name and a path, first naively, then with each part framed by its length:

Joined fields collide; framed fields do not Python · ambiguity/ambiguity.py
"""Hashing fields joined together is ambiguous; length prefixes and a purpose label make one digest mean one message."""
import hashlib


def naive(fields):
    return hashlib.sha256("".join(fields).encode()).hexdigest()


def framed(fields, label):
    """SHA-256 over the label and every field, each preceded by its length in bytes as 4 bytes, big-endian."""
    h = hashlib.sha256()
    for part in [label, *fields]:
        data = part.encode()
        h.update(len(data).to_bytes(4, "big"))
        h.update(data)
    return h.hexdigest()


a = ["user 12", "3/report.pdf"]  # user "user 12" asks for "3/report.pdf"
b = ["user 123", "/report.pdf"]  # user "user 123" asks for "/report.pdf"
print(f"joined:   {''.join(a)!r} and {''.join(b)!r}")
print(f"naive digests equal:  {naive(a) == naive(b)}")
print(f"framed digests equal: {framed(a, 'download-link/v1') == framed(b, 'download-link/v1')}")

print(f"two empty fields vs one: naive equal {naive(['', '']) == naive([''])}, framed equal {framed(['', ''], 'x') == framed([''], 'x')}")
same = ["user 12", "3/report.pdf"]
print(f"same fields, labels 'download-link/v1' and 'audit-log/v1': equal {framed(same, 'download-link/v1') == framed(same, 'audit-log/v1')}")
print(f"'é' counts as {len('é'.encode())} bytes in its prefix, not 1 character")

Output

joined:   'user 123/report.pdf' and 'user 123/report.pdf'
naive digests equal:  True
framed digests equal: False
two empty fields vs one: naive equal True, framed equal False
same fields, labels 'download-link/v1' and 'audit-log/v1': equal False
'é' counts as 2 bytes in its prefix, not 1 character

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ambiguity.py

user 12 with 3/report.pdf and user 123 with /report.pdf join to the same text, so the naive digests are equal, and a link signed for one user would work for the other. Putting each part’s length in bytes in front of it, as 4 bytes, makes the encoding reversible, so different lists can no longer meet; it also keeps two empty fields apart from one. The label in front, download-link/v1, adds domain separation: Making the purpose part of what is hashed or signed, for example with a label such as download-link/v1 in front of the fields, so that a value made for one purpose is never accepted for another.: the same fields hashed for the audit log under audit-log/v1 give another digest, so a value made for one purpose is never accepted for another. Change the fields in the program and try to make the framed digests meet.

Structured formats bring a second problem: the same data can be written in many texts. JSON allows any key order, any spacing and several spellings of one number, so two systems that agree on the data can still disagree on the bytes. RFC 8785, the JSON Canonicalization Scheme, fixes one text per value: no whitespace, object keys sorted by their UTF-16 code units at every level, and numbers and strings written by the ECMAScript rules. This program applies those rules to the JSON it uses:

Two JSON texts, one canonical form and one digest JavaScript · jcs-js/jcs.mjs
// The JSON Canonicalization Scheme (RFC 8785) for the JSON this example uses: object keys sorted by UTF-16 code units,
// no whitespace, numbers and strings written as JSON.stringify writes them (the ECMAScript rules RFC 8785 adopts).
// A minimal canonicaliser for the example; production code uses a tested library.
import { createHash } from 'node:crypto';

function canonical(value) {
  if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
  if (value !== null && typeof value === 'object') {
    const keys = Object.keys(value).sort(); // default sort compares UTF-16 code units
    return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`;
  }
  if (typeof value === 'number' && !Number.isFinite(value)) throw new Error('NaN and Infinity are not JSON');
  return JSON.stringify(value);
}

const first = '{"amount": 500.00, "to": "Asha", "currency": "INR", "tags": ["rent", "monthly"]}';
const second = '{\n  "tags": ["rent", "monthly"],\n  "currency": "INR",\n  "to": "Asha",\n  "amount": 5e2\n}';
const sha256 = (text) => createHash('sha256').update(text).digest('hex');

console.log(`the two texts are equal as text: ${first === second}`);
console.log(`their SHA-256 digests are equal: ${sha256(first) === sha256(second)}`);
const a = canonical(JSON.parse(first));
const b = canonical(JSON.parse(second));
console.log(`canonical form: ${a}`);
console.log(`canonical forms equal: ${a === b}; digest ${sha256(a).slice(0, 16)}… for both`);

Output

the two texts are equal as text: false
their SHA-256 digests are equal: false
canonical form: {"amount":500,"currency":"INR","tags":["rent","monthly"],"to":"Asha"}
canonical forms equal: true; digest bcde00c092895c39… for both

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node jcs.mjs

The two texts differ in key order, spacing and in 500.00 against 5e2, so their digests differ. Both canonicalise to the same 69-character text, which has one digest. Hash or sign the canonical form, and send the JSON as it is.

Key idea

Hash an encoding that can be read back only one way: a length in front of every field, a label that names the purpose, and a canonical form for formats such as JSON that allow many texts for one value.

Checkpoint

Check: Unambiguous encodings

  1. Question 1 of 2 A cache key is SHA-256(tenant + "/" + path). Which two requests can share a key?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: tenant "a/b" with path "c", and tenant "a" with path "b/c"

    A separator only frames fields that can never contain it. Length prefixes frame any content, which is why the lesson's framed digest kept "user 12" + "3/report.pdf" apart from "user 123" + "/report.pdf".

  2. Question 2 of 2 Why does the lesson put a label such as "download-link/v1" in front of the hashed fields?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: So that a digest made for one purpose is never accepted for another, which is called domain separation

    Domain separation makes the purpose part of the input. A tag or digest made for download links then means nothing to the audit log, even for identical fields.

XOR sums and CRC-32 catch accidents, never attackers

A fixed-size output and speed are not what makes a hash cryptographic. The Handbook of Applied Cryptography gives the counterexamples: checksums such as a running XOR of 32-bit words or a CRC detect transmission errors, but a data string with any chosen checksum is easy to build, for the XOR sum by appending one block that corrects the total. CRCs are linear codes, the Handbook says, with one-wayness comparable to an XOR sum. This program runs those checks, and the same checks on SHA-256 and HMAC-SHA-256, on messages made here:

The weaknesses of simple hashes and CRC-32, checked Python · simple-hashes/simple_hashes.py
"""Textbook simple hashes and CRC-32 put to the checks an attacker would make, on messages made here, and the same
checks on SHA-256 and HMAC-SHA-256. "weak: True" means the check found the weakness."""
import hashlib
import hmac
import random
import zlib

BLOCK = 8
MASK = 2**64 - 1


def blocks(message):
    message = message + bytes(-len(message) % BLOCK)  # pad with zeros to whole 8-byte blocks
    return [int.from_bytes(message[i : i + BLOCK], "big") for i in range(0, len(message), BLOCK)]


def rotate(h):
    return ((h << 1) | (h >> 63)) & MASK


def xor_hash(message):
    h = 0
    for b in blocks(message):
        h ^= b
    return h


def rotate_xor_hash(message):
    h = 0
    for b in blocks(message):
        h = rotate(h) ^ b  # rotate the running value one bit left, then XOR the block in
    return h


results = []


def check(name, weak, expected):
    results.append(weak == expected)
    print(f"weak: {str(weak):<6}{name}")


original = b"ship 12 boxes to the warehouse in Pune"
other = b"ship 99 boxes to the yard in Kochi".ljust(40)  # a different message of five whole blocks
print(f"original: {original.decode()!r}\n")

reordered = b"".join(x.to_bytes(BLOCK, "big") for x in reversed(blocks(original)))
check("XOR hash: the blocks in reverse order keep the value", xor_hash(reordered) == xor_hash(original), True)

fix = xor_hash(original)
for b in blocks(other):
    fix ^= b
check("XOR hash: one chosen block gives another message the same value", xor_hash(other + fix.to_bytes(BLOCK, "big")) == xor_hash(original), True)

running = 0
for b in blocks(other):
    running = rotate(running) ^ b
last = rotate(running) ^ rotate_xor_hash(original)
check("rotate-XOR hash: the same works", rotate_xor_hash(other + last.to_bytes(BLOCK, "big")) == rotate_xor_hash(original), True)

check("SHA-256: the blocks in reverse order keep the digest", hashlib.sha256(reordered).digest() == hashlib.sha256(original).digest(), False)

print(f"\nCRC-32 check value of '123456789': {zlib.crc32(b'123456789'):08x}")
rng = random.Random(7)  # seeded, so the 1,000 cases repeat on every run
affine = 0
for _ in range(1000):
    size = rng.randrange(1, 200)
    m, d = rng.randbytes(size), rng.randbytes(size)
    flipped = bytes(x ^ y for x, y in zip(m, d))
    affine += zlib.crc32(flipped) == zlib.crc32(m) ^ zlib.crc32(d) ^ zlib.crc32(bytes(size))
check(f"CRC-32: crc(m ^ d) = crc(m) ^ crc(d) ^ crc(zeros) held in {affine:,} of 1,000 cases", affine == 1000, True)

d = bytearray(len(original))
d[5], d[6] = ord("1") ^ ord("9"), ord("2") ^ ord("9")  # the change that turns "12" into "99"
changed = bytes(x ^ y for x, y in zip(original, d))
patched = zlib.crc32(original) ^ zlib.crc32(bytes(d)) ^ zlib.crc32(bytes(len(original)))
check(f"CRC-32: the checksum of {changed[:12].decode()!r}… follows without reading it", patched == zlib.crc32(changed), True)

key = b"a key that only the sender and the receiver hold"
tag = lambda message: int.from_bytes(hmac.new(key, message, "sha256").digest(), "big")
check("HMAC-SHA-256: the same patch gives a valid tag", tag(original) ^ tag(bytes(d)) ^ tag(bytes(len(original))) == tag(changed), False)

print(f"\n{sum(results)} of {len(results)} checks came out as the lesson says")

Output

original: 'ship 12 boxes to the warehouse in Pune'

weak: True  XOR hash: the blocks in reverse order keep the value
weak: True  XOR hash: one chosen block gives another message the same value
weak: True  rotate-XOR hash: the same works
weak: False SHA-256: the blocks in reverse order keep the digest

CRC-32 check value of '123456789': cbf43926
weak: True  CRC-32: crc(m ^ d) = crc(m) ^ crc(d) ^ crc(zeros) held in 1,000 of 1,000 cases
weak: True  CRC-32: the checksum of 'ship 99 boxe'… follows without reading it
weak: False HMAC-SHA-256: the same patch gives a valid tag

7 of 7 checks came out as the lesson says

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 simple_hashes.py

The XOR hash keeps its value when the message’s blocks are reversed, and one chosen block gave a different message the original’s value; rotating the running value before each XOR did not help. CRC-32 reproduced its standard check value cbf43926 for 123456789 and showed the property that sinks it: for messages of equal length, crc(m ⊕ d) = crc(m) ⊕ crc(d) ⊕ crc(zeros), in 1,000 of 1,000 random cases. So the checksum of a changed message follows from the old checksum and the change alone, without reading the message. Against HMAC-SHA-256 the same patch fails, because a tag cannot be computed without the key.

This is not a toy problem. The original Wi-Fi encryption, WEP, put a CRC-32 inside its encrypted frames as the integrity check. The 2001 analysis by Borisov, Goldberg and Wagner showed that the checksum’s linearity let frames be modified without detection, and named a keyed MAC such as HMAC as the remedy.

GTU unit 5: simple hash functions

Unit 5 of Gujarat Technological University’s Cryptography and Network Security syllabus (3161606) lists simple hash functions and their requirements and security. The XOR and rotate-and-XOR hashes above are simple hash functions of that kind; the program’s checks show which of the requirements of the previous lesson each one fails.

Checkpoint

Check: Why checksums fail attackers

  1. Question 1 of 2 For two messages m and d of the same length, what does CRC-32's affine property let anyone compute without reading m?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The CRC-32 of m ⊕ d, from crc(m), crc(d) and the CRC-32 of zeros of the same length

    Because CRC-32 is affine, someone who flips bits d in a message can fix the checksum to match without knowing the rest of the message. This is how WEP's CRC-32 integrity check failed.

  2. Question 2 of 2 The XOR-of-blocks hash of a message is computed. What happens to it if two blocks swap places?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing; XOR does not depend on the order of its inputs

    XOR is commutative, so the hash cannot see block order, and one extra block can force any value. Rotating before each XOR adds order but not security: one chosen block still matched a target.

Choosing a hash or a check for the job

The right choice follows from two questions: could anyone change the data on purpose, and must the value show who made it? A hash table adds a third, because a table that hashes keys chosen by users can be flooded with keys that collide. Python met that problem in its own dictionaries: PEP 456 moved the hashing of strings and bytes to SipHash, a keyed function with a 128-bit seed, to stop hash-collision denial of service, and the interpreter reports it:

The keyed hash behind Python's dictionaries Python · which-hash/which_hash.py
"""Python's own dictionaries hash str and bytes with a keyed function, so that crafted keys cannot be made to collide."""
import sys

print(f"sys.hash_info.algorithm: {sys.hash_info.algorithm}")
print(f"its key (seed) is {sys.hash_info.seed_bits} bits, its output {sys.hash_info.hash_bits} bits")

Output

sys.hash_info.algorithm: siphash13
its key (seed) is 128 bits, its output 64 bits

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 which_hash.py

The recorded run reports siphash13, with a 128-bit key and a 64-bit output. Answer the three questions in the table to see which hash or check fits your case:

Interactive · answer and compare Which hash or checksum fits the job? Answer three questions about who could change the data and what the value must prove. The table marks what fits and says why the others do not.
Could someone change the data on purpose, on its way or where it is stored?
Must the value show who produced the data?
Is the hash for a hash table that stores keys from users?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five kinds of hash and check against what each can and cannot defend
Choice Catches accidents Stops a deliberate change Shows who made it Speed
CRC-32 or another checksum Needs: no key Good built for random errors and bursts Poor linear: a change and its new checksum follow without the data Poor anyone can compute it Good very fast
SipHash (keyed) Needs: a secret key per process Mixed not made for this; its key differs per process Mixed keeps crafted dictionary keys from colliding Poor its key never leaves the process, so nobody else can check it Good fast enough for every dictionary lookup
SHA-256 digest Needs: no key Good any change changes about half the bits Mixed only if the digest comes from a channel the attacker cannot change Poor anyone can compute a digest for any file Good fast; hashes gigabytes in pieces
HMAC-SHA-256 Needs: a key both sides hold Good like SHA-256 Good no valid tag without the key; the CRC patch failed on it Mixed someone holding the shared key Good about the cost of SHA-256
A digital signature Needs: the sender's private key Good covers the digest of the data Good no valid signature without the private key Good the holder of one private key Mixed slower than a MAC; keys to manage
How the answers decide
  • “Could someone change the data on purpose, on its way or where it is stored?” Yes: rules out CRC-32 or another checksum. A checksum can be recomputed or patched by whoever changes the data: CRC-32 is affine, as the lesson's run found in 1,000 of 1,000 cases.
  • “Could someone change the data on purpose, on its way or where it is stored?” Yes and “Must the value show who produced the data?” No, only that it is unchanged: adds a caveat to SHA-256 digest. A digest proves integrity only if the expected value arrives over a channel the attacker cannot change, such as the project's own HTTPS page.
  • “Must the value show who produced the data?” One of us two, who share a key: rules out CRC-32 or another checksum, SHA-256 digest and SipHash (keyed). Without a key anyone can compute the value. HMAC with a shared key shows that one of the key holders made it.
  • “Must the value show who produced the data?” Yes, to anyone, even a third party: rules out CRC-32 or another checksum, SHA-256 digest, SipHash (keyed) and HMAC-SHA-256. With a shared key either side could have made the tag, so only a signature shows a third party which key holder made it.
  • “Is the hash for a hash table that stores keys from users?” Yes: adds a caveat to CRC-32 or another checksum and SHA-256 digest. Unkeyed hashes let users craft keys that all land in one bucket; PEP 456 moved Python to SipHash with a secret seed for exactly this.
  • “Could someone change the data on purpose, on its way or where it is stored?” No, only accidents such as a bad disk or a dropped packet and “Must the value show who produced the data?” No, only that it is unchanged and “Is the hash for a hash table that stores keys from users?” No: adds a caveat to HMAC-SHA-256 and A digital signature. They work, but a checksum or a digest does the job without keys to manage.

In short: a checksum such as CRC-32 for accidents, a keyed hash such as SipHash inside hash tables, SHA-256 for integrity when the expected digest arrives over a trusted channel, HMAC when both sides share a key, and a signature when a third party must be able to check who made the data. ASVS 5.0’s Appendix C lists every CRC as disallowed for cryptographic use, whatever its length.

Checkpoint

Check: Choosing a hash or a check

  1. Question 1 of 2 A web service stores request headers from users in a hash table. Which hash belongs in the table?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A keyed hash such as SipHash with a secret seed, as Python's dictionaries use

    A hash table needs speed and keys that users cannot make collide. A fast keyed function with a secret seed gives both, which is why PEP 456 moved Python to SipHash.

  2. Question 2 of 2 An invoice must let an auditor, who holds no shared key, check which company issued it. What fits?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A digital signature with the company's private key

    Showing a third party who made the data needs a key only the maker holds: a digital signature, the subject of the public-key module.

Practise: SRI values and unambiguous digests

Two exercises, with sample tests that run on this page, turn the lesson into build tools. The first makes and checks Subresource Integrity values as a browser decides, strongest algorithm first, and refuses attributes that would check nothing. The second hashes a list of fields under a purpose label with length prefixes, held to the exact bytes of the encoding:

Code lab · 2 steps

Build integrity values that mean one thing

Make and check Subresource Integrity values as a browser does, then hash lists of fields under a label so that one digest means one list.

  1. Make and check Subresource Integrity values To do
  2. One digest for one list of fields To do

Exercise · Easy · Python

Make and check Subresource Integrity values

Write two functions in sri.py for a build step that pins the scripts a page loads:

  • sri_for(content, algorithm="sha384") returns the integrity value of content (bytes): the algorithm's name, a hyphen and the standard Base64 of its digest, with padding, such as sha384-rDst8WSL…. Accept sha256, sha384 and sha512 only; raise ValueError for anything else.
  • check_sri(content, integrity) decides as a browser does: the attribute holds values separated by spaces; only values whose algorithm is sha256, sha384 or sha512 count; of those, only the strongest algorithm present is used (sha512 before sha384 before sha256); the content passes if its digest equals any value listed for that algorithm. Unlike a browser, which applies no check at all when no value is usable, raise ValueError then, so that an attribute such as md5-… is caught when the page is built.

The starter code writes hex instead of Base64 and checks only the first value.

Starter code · sri.py

import hashlib


def sri_for(content, algorithm="sha384"):
    """The integrity value of content: '<algorithm>-<Base64 of the digest>'."""
    return f"{algorithm}-{hashlib.new(algorithm, content).hexdigest()}"  # hex, not Base64


def check_sri(content, integrity):
    """True if content matches the integrity attribute, as a browser decides."""
    first = integrity.split()[0]  # only the first value is looked at
    algorithm = first.split("-", 1)[0]
    return sri_for(content, algorithm) == first
The sample tests · test_sri.py
import base64
import hashlib

from sri import check_sri, sri_for

SCRIPT = b"// A small script that a page loads from another server.\ndocument.querySelector('#status').textContent = 'The script ran.';\n"
PINNED = "sha384-rDst8WSLk6rpAjGeULUERiKE1i87H21JKbSP+JD3rdKoPDqMQkx+fZxtxMql3bkE"  # the lesson's page


def raises_value_error(call):
    try:
        call()
    except ValueError:
        return True
    return False


def test_the_lessons_value():
    """the lesson's app.js gives the value its page pins"""
    assert sri_for(SCRIPT) == PINNED


def test_other_algorithms():
    """sha256 and sha512 values, with Base64 padding"""
    assert sri_for(b"abc", "sha256") == "sha256-ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0="
    value = sri_for(b"abc", "sha512")
    assert value.startswith("sha512-") and len(value) == 7 + 88


def test_only_three_algorithms():
    """md5, sha1 and sha3_256 are refused when making a value"""
    for name in ["md5", "sha1", "sha3_256"]:
        assert raises_value_error(lambda: sri_for(b"abc", name)), name


def test_check_matches_and_mismatches():
    """the pinned script passes, one changed byte fails"""
    assert check_sri(SCRIPT, PINNED)
    assert not check_sri(SCRIPT.replace(b"ran", b"Ran"), PINNED)


def test_strongest_algorithm_decides():
    """a right sha256 value does not help when a sha512 value is present and wrong"""
    wrong512 = "sha512-" + base64.b64encode(hashlib.sha512(b"other").digest()).decode()
    assert not check_sri(SCRIPT, f"{sri_for(SCRIPT, 'sha256')} {wrong512}")
    assert check_sri(SCRIPT, f"{wrong512} {sri_for(SCRIPT, 'sha512')}")


def test_two_versions_of_one_algorithm():
    """either of two sha384 values may match, as during an update"""
    other = sri_for(b"the next version", "sha384")
    assert check_sri(SCRIPT, f"{other} {PINNED}")


def test_nothing_usable():
    """an attribute with only md5 or unknown values is refused"""
    for attribute in ["md5-kAFQmDzST7DWlj99KOF/cg==", "sha1-qZk+NkcGgWq6PiVxeFDCbJzQ2J0=", ""]:
        assert raises_value_error(lambda: check_sri(SCRIPT, attribute)), attribute
A hint

base64.b64encode(hashlib.new(algorithm, content).digest()).decode() gives the Base64 part. In check_sri, split the attribute on whitespace, split each value once at the first -, keep the ones with a supported name, then take the highest-ranked name among them before comparing.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Easy · Python

One digest for one list of fields

Write tuple_digest(fields, label) in tuple_digest.py. It returns the SHA-256 hex digest of a list of text fields under a purpose label, encoded so that different lists, or the same list under different labels, never share an encoding:

  • Encode the label and then each field as UTF-8, and put in front of each one its length in bytes as 4 bytes, big-endian. Hash the label's part first, then the fields' parts in order.
  • Raise ValueError for an empty label, and for a part longer than 2³² − 1 bytes, which 4 bytes cannot count.

With this encoding ["ab", "c"] and ["a", "bc"] differ, an empty field is not the same as no field, and the label "download-link/v1" keeps a digest from being accepted where "audit-log/v1" is expected. The starter code joins the fields and ignores the label.

Starter code · tuple_digest.py

import hashlib


def tuple_digest(fields, label):
    """SHA-256 of a list of text fields under a purpose label, with every part length-prefixed."""
    return hashlib.sha256("".join(fields).encode()).hexdigest()  # ambiguous, and the label is ignored
The sample tests · test_tuple_digest.py
import hashlib

from tuple_digest import tuple_digest


def raises_value_error(call):
    try:
        call()
    except ValueError:
        return True
    return False


def test_the_encoding_exactly():
    """label 'v1' and fields 'ab', 'c': 4-byte lengths in front of each part, then SHA-256"""
    encoded = b"\x00\x00\x00\x02v1" + b"\x00\x00\x00\x02ab" + b"\x00\x00\x00\x01c"
    assert tuple_digest(["ab", "c"], "v1") == hashlib.sha256(encoded).hexdigest()


def test_shifted_boundaries_differ():
    """['ab', 'c'] and ['a', 'bc']; the lesson's 'user 12' + '3/report.pdf' and 'user 123' + '/report.pdf'"""
    assert tuple_digest(["ab", "c"], "v1") != tuple_digest(["a", "bc"], "v1")
    assert tuple_digest(["user 12", "3/report.pdf"], "v1") != tuple_digest(["user 123", "/report.pdf"], "v1")


def test_empty_fields_count():
    """two empty fields, one empty field and no field are three different lists"""
    digests = {tuple_digest(fields, "v1") for fields in [["", ""], [""], []]}
    assert len(digests) == 3


def test_labels_separate_purposes():
    """the same fields under two labels give two digests"""
    assert tuple_digest(["user 12"], "download-link/v1") != tuple_digest(["user 12"], "audit-log/v1")


def test_lengths_count_bytes():
    """'é' is 2 bytes in UTF-8, so its prefix says 2"""
    encoded = b"\x00\x00\x00\x02v1" + b"\x00\x00\x00\x02" + "é".encode()
    assert tuple_digest(["é"], "v1") == hashlib.sha256(encoded).hexdigest()


def test_empty_label_is_refused():
    """a label is required"""
    assert raises_value_error(lambda: tuple_digest(["a"], ""))
A hint

For each part, data = part.encode(), then h.update(len(data).to_bytes(4, "big")) and h.update(data). Measure the length of the encoded bytes, not of the string: "é" is one character and two bytes.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

CRC Calculator (CRC-8/16/32/64) Compute CRC-32 of your own text and check the value cbf43926 for 123456789, then compare it with a SHA-256 digest.

The self-check below covers the five ideas. It begins with the idea you know least, raises the level as your answers come out right, and brings back what you miss:

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 Which statement describes data integrity?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: The data has not changed since a reference point, such as when its digest was made

    Integrity is unchanged data; authenticity is known origin; confidentiality is secrecy. A digest gives the first, and only against accidents unless the digest itself is protected.

  2. Question 2 of 15 A team pins the SHA-256 of every dependency in a lock file committed to its repository. Against what does that protect the build?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: A dependency that changes on the registry or a mirror after it was pinned

    A pinned digest moves trust to where the pin lives. It stops later changes elsewhere, but not a bad first version or someone with write access to the repository.

  3. Question 3 of 15 A service stores SHA-256 of reset tokens and also of user passwords. Why is one fine and the other not?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: Reset tokens are 128 random bits, so no guess list exists; passwords are guessable, so their digests can be matched by hashing likely passwords

    Hashing is safe storage only for high-entropy values. Passwords need slow, salted password hashing, which the password module covers.

  4. Question 4 of 15 How many characters long is a sha384 integrity value, including the "sha384-" prefix?

    Type a number.

    A hint

    A SHA-384 digest is 48 bytes, and Base64 writes 3 bytes as 4 characters.

    Show the answer: Question 4 of 15

    Answer: 71 characters

    48 bytes are 64 Base64 characters with no padding, and the prefix adds 7: 71, the length of the lesson's pinned value.

  5. Question 5 of 15 An old page has integrity="md5-…" on a script from another server. What protection does it get?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: None; browsers ignore md5 values, and with no usable value no check is applied

    An attribute without a supported algorithm silently checks nothing. A build step that refuses such values, as the exercise does, catches the mistake before the page ships.

  6. Question 6 of 15 You pin a library from a public CDN with SRI. The CDN later serves a new patch version at the same URL. What happens, and what should the page do?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: The browser refuses the new bytes; pin a URL that names an exact version, and update the integrity value with the URL

    SRI and moving targets do not mix: pin versioned URLs, and change the URL and the value together when you choose to update.

  7. Question 7 of 15 In the lesson's framing, how many bytes does the field "naïve" add to the hashed input, prefix included?

    Type a number.

    A hint

    The prefix is 4 bytes, and ï takes 2 bytes in UTF-8.

    Show the answer: Question 7 of 15

    Answer: 10 bytes

    "naïve" is 5 characters but 6 bytes in UTF-8, because ï takes 2. With the 4-byte length prefix the field adds 4 + 6 = 10 bytes, and the prefix holds 6.

  8. Question 8 of 15 What does RFC 8785, the JSON Canonicalization Scheme, do?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: It fixes one text for each JSON value, with sorted keys, no whitespace and ECMAScript number and string rules, so that it can be hashed or signed

    Canonicalisation gives every value one byte sequence. The application can still send and store ordinary JSON and hash or sign the canonical form.

  9. Question 9 of 15 A signed download link covers HMAC(key, user + path). How can a user abuse it, and what is the fix?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: A user named "user 12" could get a tag that also fits user "user 123" with a shorter path; frame each field with its length and add a purpose label

    A MAC or a signature is only as precise as the bytes it covers. Length prefixes and a label give one input per request, so one tag can never authorise two.

  10. Question 10 of 15 What does the standard CRC-32 check value cbf43926 test?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: That an implementation computes CRC-32 of "123456789" as the standard says

    A check value is a known-answer test for the implementation, the same discipline as the hash vectors of the previous lesson.

  11. Question 11 of 15 What did the analysis of WEP by Borisov, Goldberg and Wagner name as the remedy for its CRC-32 integrity check?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: A keyed message authentication code such as HMAC

    The paper's point is general: integrity against an attacker needs a secret, so the check must be a keyed MAC. The same patch that worked on CRC-32 failed on HMAC-SHA-256 in the program.

  12. Question 12 of 15 A developer replaces the XOR hash with rotate-then-XOR, so that block order matters. Is it now fit to protect a file against deliberate changes?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: No; the program still made a different message match with one chosen last block, so any value can be forced

    A hash for security must resist preimages, second preimages and collisions. Rotate-and-XOR fails second preimages in one step, so it is a checksum, not a cryptographic hash.

  13. Question 13 of 15 For which job is CRC-32 the right choice?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: Detecting accidental corruption in a file format or a network packet

    CRC-32 is a good error-detecting code and a poor security tool: ASVS 5.0 disallows every CRC for cryptographic use.

  14. Question 14 of 15 A partner and your service share a secret and exchange webhooks. What should prove that a webhook came from the partner unchanged?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: HMAC-SHA-256 over the raw body with the shared secret

    Two parties with a shared key need a MAC. A signature adds the ability for outsiders to verify, at the cost of key pairs to manage.

  15. Question 15 of 15 A team argues that SHA-256 is "safer" than SipHash for its in-memory hash table of user-supplied keys. What is the weakness in that argument?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Without a secret key, users know the function and can search for keys that land in the same bucket of a small table; SipHash's secret seed prevents that

    A hash table uses only a few bits of each hash to pick a bucket. With an unkeyed function those bits are easy to match on purpose; a keyed function makes them unpredictable to users, which PEP 456 adopted for Python.

Interview questions

Warm-up (fresher to mid level): how do integrity and authenticity differ, and which tools give each?

Integrity means the data has not changed since some reference point; authenticity means you know who produced it. A checksum or a hash such as SHA-256 gives integrity against accidents, and against deliberate changes only when the expected digest reaches you over a channel the attacker cannot touch, because anyone can compute a digest for any data. Authenticity needs a key: a MAC such as HMAC-SHA-256 shows that someone holding the shared key made the data, and a digital signature shows which private key made it, in a way a third party can check. So a checksum downloaded from the same mirror as the file proves almost nothing, while a signature from the project’s key, or a digest from its own site over HTTPS, does.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Your checkout page loads a script from a partner's content delivery network. How do you protect the page if that network is ever compromised, what breaks when the partner ships a new version, and what do you do about it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Service A signs JSON payloads and service B verifies them, and about 3 % of valid payloads fail verification after a library upgrade in B. Find the cause and fix the design so that it cannot come back.

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why does a build that fetches a tool and its SHA-256 checksum from the same mirror gain nothing?

    A digest proves integrity, that the bytes are unchanged since it was made, never who made them. Whoever can change the file on the mirror can change the checksum too. The expected digest must come from a source the mirror cannot change: the project's own site over HTTPS or a committed lock file, a MAC with a shared key, or a digital signature.

  2. What do HTTP's Content-Digest and Repr-Digest fields protect against?

    Accidents, not deliberate tampering. RFC 9530 says they are not meant as general protection, because someone who can change the body in transit can replace the field too. For that, use TLS or HTTP Message Signatures (RFC 9421).

  3. Why is storing the SHA-256 of a reset token fine, but storing the SHA-256 of a password is not?

    A 128-bit random token cannot be guessed, so a leaked table of hashes gives back nothing. A password is not random: its SHA-256 can be found by hashing likely passwords, so passwords need slow, salted password-hashing functions.

  4. How does Subresource Integrity pin a script, and what makes the attribute useless?

    The integrity attribute holds an algorithm, a hyphen and the Base64 of a digest, and browsers must support SHA-2 at 256, 384 and 512 bits. If one byte of the file changes, the browser refuses the response, and a cross-origin element needs crossorigin="anonymous". An md5- or sha1- value is ignored, and an attribute with no usable value checks nothing. Generate sha384- values in the build and fail it when one is missing.

  5. Why does joining fields before hashing give ambiguous digests, and how do you fix it?

    user 12 with 3/report.pdf and user 123 with /report.pdf join to the same text, so a link signed for one user works for the other. Put each field's length in bytes in front of it, so the encoding reads back only one way, and add a label such as download-link/v1 for domain separation, so that a value made for one purpose cannot pass as another's.

  6. What does the JSON Canonicalization Scheme (RFC 8785) fix?

    One text per JSON value: no whitespace, object keys sorted by their UTF-16 code units at every level, and numbers and strings written by the ECMAScript rules. Texts that differ in key order, spacing and 500.00 against 5e2 then give one canonical text and one digest. Hash or sign the canonical form; send the JSON as it is.

  7. Why do XOR sums and CRC-32 protect nothing against an attacker?

    They detect transmission errors, but a message with any chosen checksum is easy to build. CRC-32 is affine: for messages of equal length crc(m ⊕ d) = crc(m) ⊕ crc(d) ⊕ crc(zeros), so the checksum of a changed message follows from the old one. WEP put a CRC-32 inside its encrypted frames and frames could be modified undetected; against HMAC the same patch fails.

Key takeaways

  • A matching digest proves integrity, not authenticity: a checksum from the same place as the file can be changed with it, so the expected digest must come from a trusted channel, a MAC or a signature.
  • Subresource Integrity pins a script or stylesheet to a SHA-256, SHA-384 or SHA-512 digest; one changed byte makes the browser refuse it, and an attribute with no usable value checks nothing.
  • Hash structured data through an encoding with one reading: lengths in front of fields, a label for the purpose, and RFC 8785’s canonical form for JSON.
  • XOR sums and CRC-32 catch accidents only: CRC-32 is affine, so a change and its new checksum follow without the data, while the same patch fails against HMAC-SHA-256.
  • Choose by the threat: checksums for accidents, SipHash in hash tables, SHA-256 with a trusted digest, HMAC with a shared key, and signatures when others must check who made the data.

Terms in this lesson

domain separation
Making the purpose part of what is hashed or signed, for example with a label such as download-link/v1 in front of the fields, so that a value made for one purpose is never accepted for another.
Subresource Integrity (SRI)
A W3C mechanism that lets a page pin a script or stylesheet to a SHA-256, SHA-384 or SHA-512 digest in its integrity attribute. A response with another digest is refused as a network error.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.