File Hashes and Digital Signatures
Separate byte integrity from publisher identity through hashes, signatures, certificates, and Windows trust checks.
Cheatsheet for Lesson 10.8
A one-page summary of “File Hashes and Digital Signatures” you can keep beside your notes. Plain Markdown.
Lesson 10.8 — File Hashes and Digital Signatures: cheatsheet
- Remember: Interpret each API’s documented return contract; for this trust check, success is exactly zero.
- Also useful: The signing key makes the approval mark; the matching public key checks it. Checking does not require handing everyone the signing secret.
Key terms
- checksum: The last four bytes are a checksum: a number computed from the other bytes and stored beside them, so the loader can detect damage.
- digital signature: A digital signature is a number computed from a file's hash with a secret key, which anyone can check with a matching key that is not secret.
- public key: The public key is given to everyone; for Authenticode it travels inside the publisher's certificate.
PCWSTR:PCWSTRis only a pointer.
Review
- Q: What can an Authenticode signature tell you that a standalone SHA-256 hash cannot? A: Whether bytes match a publisher's signature under a Windows trust policy
- Q: A publisher signs a game patch, and Windows checks the signature on your machine. Which key does each side use? A: The publisher signs with its private key; Windows checks with the public key in the certificate
Why one edited byte breaks the save
Section titled “Why one edited byte breaks the save”The book’s invented GSAV format makes the byte-integrity question concrete.
Its offline character Nia has level 3, 250 gold, and this complete 19-byte save:
47 53 41 56 01 00 FA 00 00 00 03 03 4E 69 61 4A 03 00 00The last four bytes are a checksum: a number computed from the other bytes
and stored beside them, so the loader can detect damage. This format uses the
simplest checksum there is. It adds up every byte from offset 0 to the byte
just before the checksum and stores the total as a little-endian u32. For
Nia’s save, bytes 0 to 14 add up like this:
magic 47 53 41 56 71 + 83 + 65 + 86 = 305 total 305version 01 00 1 + 0 = 1 total 306gold FA 00 00 00 250 + 0 + 0 + 0 = 250 total 556level 03 3 = 3 total 559length 03 3 = 3 total 562name 4E 69 61 78 + 105 + 97 = 280 total 842The stored checksum is the same number:
0x34A because 842 = 3 × 256 + 74.The loader repeats that sum every time it opens the file:
Validate bytes before interpreting a save. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
encode the 15-byte GSAV payload
sum the payload bytes
store the checksum, or corrupt it
check header, version, length and checksum
interpret the accepted fields
report why the record was accepted or refusedResult: accepted Nia, level 3, gold 250. A recomputable checksum is not evidence of an authorized writer.
Final values: gold = 250; corrupt = no; payload_length = 15; computed_sum = 842; stored_sum = 842; total_length = 19; accepted = true; name = Nia; level = 3; decoded_gold = 250.
Read every step of the lesson’s example
Step 1, line 1. Start with the selected inputs and follow how the next operation changes the state.
gold = 250; corrupt = no.
Step 2, line 2. The writer sums the first fifteen bytes.
gold = 250; corrupt = no; payload_length = 15; computed_sum = 842.
Step 3, line 3. The record contains four checksum bytes after its payload.
gold = 250; corrupt = no; payload_length = 15; computed_sum = 842; stored_sum = 842; total_length = 19.
Step 4, line 4. The fixture’s header and size are valid; compare both checksum values.
gold = 250; corrupt = no; payload_length = 15; computed_sum = 842; stored_sum = 842; total_length = 19; accepted = true.
Step 5, line 5. Only now interpret the fields.
gold = 250; corrupt = no; payload_length = 15; computed_sum = 842; stored_sum = 842; total_length = 19; accepted = true; name = Nia; level = 3; decoded_gold = 250.
Step 6, line 6. Result: accepted Nia, level 3, gold 250. A recomputable checksum is not evidence of an authorized writer.
gold = 250; corrupt = no; payload_length = 15; computed_sum = 842; stored_sum = 842; total_length = 19; accepted = true; name = Nia; level = 3; decoded_gold = 250.
The exact 19-byte GSAV file is checked for its header, version and length. Copies of the first 15 byte values enter an accumulator and sum to 842. The four stored checksum bytes also decode to 842. The accepted record contains Nia, level 3, and gold 250.
Read the steps
- Read the unchanged sample: 19 bytes. Offsets locate fields inside the file.
- Bytes 47 53 41 56 spell GSAV. Bytes 01 00 decode as little-endian version 1, so this loader recognizes the layout.
- Offset 11 holds length 3. Twelve fixed bytes plus three name bytes plus four checksum bytes exactly fill the 19-byte file.
- Copy each byte value into the adder. The file stays unchanged. Sum bytes 0 through 14, stopping before the stored checksum.
- The computed sum is 842. Stored bytes 4A 03 00 00 decode as 74 + 3 × 256 = 842. The totals match.
- Only after those checks does the loader accept the record: FA 00 00 00 gives 250 gold, offset 10 gives level 3, and 4E 69 61 spells Nia.
The adder consumes copies of byte values. A checksum mismatch refuses the file before the game receives a record.
Now type gold over as 9,999 and change nothing else. 9,999 is 39 × 256 + 15,
and 39 = 2 × 16 + 7 = 0x27 while 15 = 0x0F, so 9,999 is 0x270F and its
little-endian bytes are 0F 27 00 00.
Predict first
The last four bytes still say 4A 03 00 00, which is 842. What does the loader do with the edited file?
Answer: It refuses the save as corrupt. The old gold bytes FA 00 00 00 added 250 to the total; the new 0F 27 00 00 add 15 + 39 = 54. The total becomes 842 − 250 + 54 = 646 while the stored checksum still says 842, so the loader refuses the file.
The gold bytes, the total, the verdict
Gold at offsets 0x06 to 0x09
add the bytes
What these bytes add to the totalThe loader’s check
The gold bytes now add 54 instead of 250, so the loader computes 646 while the file still says 842.
The fix is to do what the game does when it saves: recompute the checksum. For
1,250 gold, the gold bytes E2 04 00 00 add 226 + 4 = 230 instead of 250, so
the total is 842 − 250 + 230 = 822. That is 3 × 256 + 54, and 54 = 3 × 16 + 6 =
0x36, so the checksum is 0x336, stored as 36 03 00 00: exactly what save B
contained. The complete edited file is:
47 53 41 56 01 00 E2 04 00 00 03 03 4E 69 61 36 03 00 00A plain sum has a blind spot: adding numbers in a different order gives the same
total, so swapping two bytes goes unnoticed. The 64,000 example above is that
swap: 00 FA 00 00 still adds up to 250, so the file loads. That is why real formats more often
use a CRC-32, a 32-bit checksum designed to change when bytes are reordered, or
a hash. The editing procedure stays the same: work out which bytes are covered,
which algorithm runs, where the result is stored, and in which byte order, as
listed in Lesson 10.2. If the
check uses a secret key, as a MAC does, or a digital signature, it cannot be
recomputed without that key; Lesson 10.10 explains why.
A hash and a signature answer different questions
Section titled “A hash and a signature answer different questions”The manifest in Lesson 10.7 records the bytes before and after a mod installation. Its hash detects an unexpected later change. To decide whether a downloaded game or mod came from the publisher you expected, we need a different question: who approved these bytes?
A SHA-256 hash is a fingerprint of exact bytes. Change one byte and the fingerprint should change dramatically. A saved baseline can answer:
“Are these bytes identical to the bytes I recorded earlier?”
It cannot answer who created the original file. A hash does not create trust; it only moves trust from wherever you obtained the hash onto the bytes now on your disk.
Changing one bit of the input changes about half of the 256 bits of a SHA-256 fingerprint, so it shows that bytes changed but not where or how much. The interactive version needs JavaScript; the lesson text around it explains the same idea.
That distinction has practical teeth. If you download mod.zip and
mod.zip.sha256 from the same page, and someone is able to change that page,
they can change both — and the two will agree perfectly:
Verification reports success and has proven nothing. A hash is only worth checking when it reached you by a route the attacker does not control: a signed release, a genuinely separate channel, or a value you recorded yourself from a copy you already had reason to trust.
If an attacker changes both mod.zip and the hash published next to it, the two agree and the check proves nothing. A hash you recorded yourself, or a publisher’s signature, would expose the swap. The interactive version needs JavaScript; the lesson text around it explains the same idea.
An Authenticode signature connects the file’s digest to a signing certificate and asks Windows to apply a trust policy. It can help answer:
“Does this file still match a signature made by a publisher whose certificate chain Windows trusts under this policy?”
Neither answer proves that software has no bugs or that you personally intended to install this version.
How a signature is made and checked
Section titled “How a signature is made and checked”A digital signature is a number computed from a file’s hash with a secret key, which anyone can check with a matching key that is not secret. It fixes the compromised-page problem above: an attacker who controls the page can replace the file and the hash, but cannot produce a signature that checks out, because they do not have the secret.
That needs public-key cryptography, where keys are made in pairs. The private key never leaves the publisher. The public key is given to everyone; for Authenticode it travels inside the publisher’s certificate. The pair has one property that makes everything work: a signature made with the private key is accepted by the matching public key and by no other, and knowing the public key does not let anyone work out the private one.
Walk the two ways it can fail. Change one byte of the file and your machine computes a different hash, so the signature no longer matches it. Replace the signature with one the attacker made using their own key pair, and it only checks out against the attacker’s public key, whose certificate Windows does not trust.
A comparison often used for public keys is an open padlock: you hand out as many as you like and keep the only key, so anyone can lock a box for you and only you can open it. That picture describes encrypting a message to someone. A signature runs the other way: only the private-key holder can make one, and anyone can check it. Nothing is hidden, either; a signed file stays perfectly readable. The signature only proves which key approved these exact bytes.
Why can nobody forge one? Real keys are numbers hundreds of digits long, chosen so that checking a signature takes a fraction of a millisecond while making one without the private key would take longer than the age of the universe. The same kind of gap shows up with small numbers. Multiplying 7 × 17 = 119 is quick. Given only 119, finding the two numbers means trying divisors: 2 fails, 3 fails, 5 fails, and 7 gives 119 ÷ 7 = 17. Checking a proposed answer is one division. RSA, a common signature scheme, builds its keys on this gap between multiplying and factoring, with numbers so large that the trying never finishes.
The same machinery sits behind the padlock icon in a web browser, driver signing in Lesson 12.7, and a console’s secure boot in Lesson 13.6. Its limits are the ones this lesson keeps returning to: a signature says who approved the bytes, not that the bytes are safe, and if a publisher’s private key is stolen, the thief can sign anything until the certificate is revoked.
Unsigned does not mean malicious
Section titled “Unsigned does not mean malicious”Many open-source game builds are unsigned, self-built, or distributed in archives without Authenticode. Wesnoth, AssaultCube, or Urban Terror files can legitimately report “no embedded signature,” depending on the exact build and distributor.
Treat the result as one fact:
| Result | Reasonable next question |
|---|---|
| Trusted signature | Does the publisher and file version match the source I expected? |
| No embedded signature | Does the official project publish hashes or reproducible release artifacts? |
| Explicitly distrusted | Why did Windows policy reject this publisher/signature? |
| Other failure | Was the file damaged, unsupported, expired, or checked under a limited classroom policy? |
WinVerifyTrust returns a status code, not a Result
Section titled “WinVerifyTrust returns a status code, not a Result”The Win32 function returns a LONG. Microsoft’s return-value contract says only exact zero means the requested trust action succeeded. Do not use HRESULT-style “success if nonnegative” logic.
The original helper treats every nonnegative status as trusted, confusing this result with status types whose nonnegative values indicate success. WinVerifyTrust has a different contract: only exactly zero reports success.
fn explain_trust(status: i32) { if status >= 0 { println!("trusted"); } // Incorrect for WinVerifyTrust. else { println!("trust check failed with status {status:#010x}"); }}Use the function’s own success contract, and keep every unknown nonzero status visible as a failure. A successful offline check still has the policy limits described below.
Improved code
fn explain_trust(status: i32) { if status == 0 { println!("trusted"); } else { println!("trust check failed with status {status:#010x}"); }}Show the diff
fn explain_trust(status: i32) { if status >= 0 { println!("trusted"); } // Incorrect for WinVerifyTrust. if status == 0 { println!("trusted"); } else { println!("trust check failed with status {status:#010x}"); }}Takeaway: do not reuse another API’s success test just because both results are integers.
Known nonzero values help us print plain-English explanations. Unknown statuses remain failures and are printed in hexadecimal for investigation.
Trust verification has state and policy choices
Section titled “Trust verification has state and policy choices”WINTRUST_FILE_INFO points to the file path. WINTRUST_DATA describes how the trust provider should work:
WTD_UI_NONEprevents pop-up dialogs;WTD_CHOICE_FILEselects the file-info union member;WTD_STATEACTION_VERIFYbegins verification and may create provider state;WTD_STATEACTION_CLOSEreleases that state afterward;WTD_DISABLE_MD2_MD4refuses two obsolete digest algorithms;WTD_REVOKE_NONEskips revocation checking in this offline classroom run;WTD_CACHE_ONLY_URL_RETRIEVALavoids network retrieval in this reproducible lab.
Those last two choices have a tradeoff: the result does not include a fresh revocation check. A release pipeline with network access should define and test a stricter revocation policy instead of treating this offline classroom result as the final word.
Build the combined inspector
Section titled “Build the combined inspector”run calls these in order; WinVerifyTrust itself is called twice, once to verify and once to release the state the first call created:
Complete lab source: signature_check.rs
signature_check.rs
Show the codeHide the code
#[cfg(windows)]
mod app {
use std::{
ffi::OsStr,
fs::File,
io::Read,
mem::size_of,
os::windows::ffi::OsStrExt,
path::{Path, PathBuf},
};
use anyhow::Context;
use sha2::{Digest, Sha256};
use windows::{
Win32::{
Foundation::{
HANDLE, HWND, TRUST_E_EXPLICIT_DISTRUST, TRUST_E_NOSIGNATURE,
TRUST_E_PROVIDER_UNKNOWN, TRUST_E_SUBJECT_FORM_UNKNOWN,
TRUST_E_SUBJECT_NOT_TRUSTED,
},
Security::WinTrust::{
WINTRUST_ACTION_GENERIC_VERIFY_V2, WINTRUST_DATA, WINTRUST_DATA_0,
WINTRUST_FILE_INFO, WTD_CACHE_ONLY_URL_RETRIEVAL, WTD_CHOICE_FILE,
WTD_DISABLE_MD2_MD4, WTD_REVOKE_NONE, WTD_STATEACTION_CLOSE,
WTD_STATEACTION_VERIFY, WTD_UI_NONE, WTD_UICONTEXT_EXECUTE, WinVerifyTrust,
},
},
core::PCWSTR,
};
fn sha256(path: &Path) -> anyhow::Result<String> {
let mut file = File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0_u8; 64 * 1_024];
loop {
let count = file.read(&mut buffer)?;
if count == 0 {
break;
}
hasher.update(&buffer[..count]);
}
Ok(format!("{:x}", hasher.finalize()))
}
fn verify_authenticode(path: &Path) -> i32 {
let wide_path: Vec<u16> = OsStr::new(path)
.encode_wide()
.chain(std::iter::once(0))
.collect();
let mut file_info = WINTRUST_FILE_INFO {
cbStruct: size_of::<WINTRUST_FILE_INFO>() as u32,
pcwszFilePath: PCWSTR(wide_path.as_ptr()),
hFile: HANDLE::default(),
pgKnownSubject: std::ptr::null_mut(),
};
let mut trust_data = WINTRUST_DATA {
cbStruct: size_of::<WINTRUST_DATA>() as u32,
dwUIChoice: WTD_UI_NONE,
fdwRevocationChecks: WTD_REVOKE_NONE,
dwUnionChoice: WTD_CHOICE_FILE,
Anonymous: WINTRUST_DATA_0 {
pFile: &mut file_info,
},
dwStateAction: WTD_STATEACTION_VERIFY,
dwProvFlags: WTD_CACHE_ONLY_URL_RETRIEVAL | WTD_DISABLE_MD2_MD4,
dwUIContext: WTD_UICONTEXT_EXECUTE,
..Default::default()
};
let mut action = WINTRUST_ACTION_GENERIC_VERIFY_V2;
// SAFETY: both structures contain valid sizes and pointers that remain
// alive through the call. UI is disabled and the file path is terminated.
let status = unsafe {
WinVerifyTrust(
HWND::default(),
&mut action,
(&mut trust_data as *mut WINTRUST_DATA).cast(),
)
};
trust_data.dwStateAction = WTD_STATEACTION_CLOSE;
// SAFETY: closes provider state created by the verification call. The
// same live structures and action identifier are supplied once more.
let _ = unsafe {
WinVerifyTrust(
HWND::default(),
&mut action,
(&mut trust_data as *mut WINTRUST_DATA).cast(),
)
};
status
}
fn meaning(status: i32) -> &'static str {
if status == 0 {
"trusted Authenticode signature"
} else if status == TRUST_E_NOSIGNATURE.0 {
"no embedded signature was found"
} else if status == TRUST_E_EXPLICIT_DISTRUST.0 {
"the publisher or signature is explicitly distrusted"
} else if status == TRUST_E_SUBJECT_NOT_TRUSTED.0 {
"the trust policy rejected the file"
} else if status == TRUST_E_PROVIDER_UNKNOWN.0 {
"Windows has no matching trust provider"
} else if status == TRUST_E_SUBJECT_FORM_UNKNOWN.0 {
"the provider does not understand this file form"
} else {
"verification failed with another trust-provider status"
}
}
pub fn run() -> anyhow::Result<()> {
let path = std::env::args_os()
.nth(1)
.map(PathBuf::from)
.context("usage: signature_check <game.exe-or-dll>")?;
anyhow::ensure!(path.is_file(), "{} is not a file", path.display());
let hash = sha256(&path).with_context(|| format!("could not hash {}", path.display()))?;
let status = verify_authenticode(&path);
println!("File: {}", path.display());
println!("SHA-256: {hash}");
println!(
"Trust: {} (status {:#010x})",
meaning(status),
status as u32
);
println!("The cache-only check did not modify the file.");
Ok(())
}
}
#[cfg(windows)]
fn main() -> anyhow::Result<()> {
app::run()
}
#[cfg(not(windows))]
fn main() {
eprintln!("This Authenticode lab must run on Windows.");
}#[cfg(windows)]mod app { use std::{ ffi::OsStr, fs::File, io::Read, mem::size_of, os::windows::ffi::OsStrExt, path::{Path, PathBuf}, };
use anyhow::Context; use sha2::{Digest, Sha256}; use windows::{ Win32::{ Foundation::{ HANDLE, HWND, TRUST_E_EXPLICIT_DISTRUST, TRUST_E_NOSIGNATURE, TRUST_E_PROVIDER_UNKNOWN, TRUST_E_SUBJECT_FORM_UNKNOWN, TRUST_E_SUBJECT_NOT_TRUSTED, }, Security::WinTrust::{ WINTRUST_ACTION_GENERIC_VERIFY_V2, WINTRUST_DATA, WINTRUST_DATA_0, WINTRUST_FILE_INFO, WTD_CACHE_ONLY_URL_RETRIEVAL, WTD_CHOICE_FILE, WTD_DISABLE_MD2_MD4, WTD_REVOKE_NONE, WTD_STATEACTION_CLOSE, WTD_STATEACTION_VERIFY, WTD_UI_NONE, WTD_UICONTEXT_EXECUTE, WinVerifyTrust, }, }, core::PCWSTR, };
fn sha256(path: &Path) -> anyhow::Result<String> { let mut file = File::open(path)?; let mut hasher = Sha256::new(); let mut buffer = [0_u8; 64 * 1_024]; loop { let count = file.read(&mut buffer)?; if count == 0 { break; } hasher.update(&buffer[..count]); } Ok(format!("{:x}", hasher.finalize())) }
fn verify_authenticode(path: &Path) -> i32 { let wide_path: Vec<u16> = OsStr::new(path) .encode_wide() .chain(std::iter::once(0)) .collect(); let mut file_info = WINTRUST_FILE_INFO { cbStruct: size_of::<WINTRUST_FILE_INFO>() as u32, pcwszFilePath: PCWSTR(wide_path.as_ptr()), hFile: HANDLE::default(), pgKnownSubject: std::ptr::null_mut(), }; let mut trust_data = WINTRUST_DATA { cbStruct: size_of::<WINTRUST_DATA>() as u32, dwUIChoice: WTD_UI_NONE, fdwRevocationChecks: WTD_REVOKE_NONE, dwUnionChoice: WTD_CHOICE_FILE, Anonymous: WINTRUST_DATA_0 { pFile: &mut file_info }, dwStateAction: WTD_STATEACTION_VERIFY, dwProvFlags: WTD_CACHE_ONLY_URL_RETRIEVAL | WTD_DISABLE_MD2_MD4, dwUIContext: WTD_UICONTEXT_EXECUTE, ..Default::default() }; let mut action = WINTRUST_ACTION_GENERIC_VERIFY_V2;
// SAFETY: both structures contain valid sizes and pointers that remain // alive through the call. UI is disabled and the file path is terminated. let status = unsafe { WinVerifyTrust( HWND::default(), &mut action, (&mut trust_data as *mut WINTRUST_DATA).cast(), ) };
trust_data.dwStateAction = WTD_STATEACTION_CLOSE; // SAFETY: closes provider state created by the verification call. The // same live structures and action identifier are supplied once more. let _ = unsafe { WinVerifyTrust( HWND::default(), &mut action, (&mut trust_data as *mut WINTRUST_DATA).cast(), ) }; status }
fn meaning(status: i32) -> &'static str { if status == 0 { "trusted Authenticode signature" } else if status == TRUST_E_NOSIGNATURE.0 { "no embedded signature was found" } else if status == TRUST_E_EXPLICIT_DISTRUST.0 { "the publisher or signature is explicitly distrusted" } else if status == TRUST_E_SUBJECT_NOT_TRUSTED.0 { "the trust policy rejected the file" } else if status == TRUST_E_PROVIDER_UNKNOWN.0 { "Windows has no matching trust provider" } else if status == TRUST_E_SUBJECT_FORM_UNKNOWN.0 { "the provider does not understand this file form" } else { "verification failed with another trust-provider status" } }
pub fn run() -> anyhow::Result<()> { let path = std::env::args_os().nth(1).map(PathBuf::from) .context("usage: signature_check <game.exe-or-dll>")?; anyhow::ensure!(path.is_file(), "{} is not a file", path.display());
let hash = sha256(&path) .with_context(|| format!("could not hash {}", path.display()))?; let status = verify_authenticode(&path); println!("File: {}", path.display()); println!("SHA-256: {hash}"); println!("Trust: {} (status {:#010x})", meaning(status), status as u32); println!("The cache-only check did not modify the file."); Ok(()) }}
#[cfg(windows)]fn main() -> anyhow::Result<()> { app::run()}
#[cfg(not(windows))]fn main() { eprintln!("This Authenticode lab must run on Windows.");}Why keep the wide path alive?
Section titled “Why keep the wide path alive?”PCWSTR is only a pointer. It does not own the UTF-16 vector. wide_path, file_info, and trust_data all remain local variables until both WinVerifyTrust calls finish, so every pointer stays valid.
This is why the unsafe comment names lifetimes instead of saying only “Windows API call.” A useful safety comment explains the exact facts the compiler cannot verify.
Check your installations
Section titled “Check your installations”In the Windows VM, build the checker from windows-labs and give it one EXE
path at a time. Record each reported hash beside the exact file and build;
the signature result applies only to the file you supplied.
cd windows-labscargo build --release --target i686-pc-windows-msvc --bin signature_check
.\target\i686-pc-windows-msvc\release\signature_check.exe ` "C:\Games\Wesnoth\wesnoth.exe"
.\target\i686-pc-windows-msvc\release\signature_check.exe ` "C:\Games\AssaultCube\bin_win32\ac_client.exe"
.\target\i686-pc-windows-msvc\release\signature_check.exe ` "C:\Games\UrbanTerror\Quake3-UrT.exe"A SHA-256 value belongs to one exact file, so it means something only next to the game version and download source it came from. Re-run the tool after installing a mod or update: every file that changed gets a new hash, which is expected for the files the mod or update replaced and unexplained for any other.
Some Windows components are signed through catalogs rather than an embedded signature. This lab checks the embedded signature on the selected game EXE. If you expand it to inventory every DLL loaded beside the game, add explicit catalog-signature handling instead of reporting those files as simply unsigned.
Combine this lesson with module_inventory: inventory tells you what actually loaded, the hash identifies exact bytes, and Authenticode adds one publisher-trust signal.
The buildable source is signature_check.rs.
References: WinVerifyTrust, WINTRUST_DATA, WINTRUST_FILE_INFO, and Authenticode time stamping.
Ask the right trust question
What can an Authenticode signature tell you that a standalone SHA-256 hash cannot?
A hash identifies exact bytes. Authenticode additionally connects a digest to signing identity and certificate policy, without proving the software is bug-free.
Finished Lesson 10.8?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.