mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 07:00:26 +02:00
Add optional SHA-256 verification of downloaded manifest pieces
Each Sony manifest piece carries a SHA-256 (hashValue). When the new "Verify downloaded pieces" setting is on, every piece is hashed while it streams to disk (OpenSSL EVP, already linked) and compared against the manifest value; a mismatch aborts the download, deletes the partial, and reports piece_verify_failed instead of handing a corrupt 60 GB package to the installer. Off by default: TLS already protects the bytes in transit and the PS5 installer verifies the whole packageDigest before applying, so this is a fail-fast belt-and-suspenders check. It is also unverified on hardware yet, so it stays opt-in (persisted in config.json) until confirmed on-device; the hex compare is case-insensitive since Sony mixes cases.
This commit is contained in:
1 parent
9006965a75
commit
ea1328de79
5 files changed
+117
-27
No files matched your search
+76
-13
@@ -7,12 +7,14 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef PATCHDL_HAVE_CURL
|
||||
#include <curl/curl.h>
|
||||
#include <openssl/evp.h>
|
||||
#include "patchdl_ca.h"
|
||||
#endif
|
||||
|
||||
@@ -290,9 +292,32 @@ patchdl_http_get(const char *url, patchdl_buf_t *out) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Write sink: tees the body to the file and, when verifying, into a running
|
||||
SHA-256. curl always calls with size==1, so nmemb is the byte count. */
|
||||
typedef struct {
|
||||
FILE *fp;
|
||||
EVP_MD_CTX *md; /* NULL when not verifying */
|
||||
} write_sink_t;
|
||||
|
||||
static size_t
|
||||
file_write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||
return fwrite(ptr, size, nmemb, (FILE *)userdata);
|
||||
write_sink_t *s = (write_sink_t *)userdata;
|
||||
size_t written = fwrite(ptr, size, nmemb, s->fp);
|
||||
if (s->md && written)
|
||||
EVP_DigestUpdate(s->md, ptr, written * size);
|
||||
return written;
|
||||
}
|
||||
|
||||
/* Hex-encode a digest, lowercase. */
|
||||
static void
|
||||
hex_encode(const unsigned char *d, unsigned int len, char *out, size_t out_sz) {
|
||||
static const char hexd[] = "0123456789abcdef";
|
||||
unsigned int i;
|
||||
for (i = 0; i < len && (2u * i + 2u) < out_sz; i++) {
|
||||
out[2 * i] = hexd[(d[i] >> 4) & 0xf];
|
||||
out[2 * i + 1] = hexd[d[i] & 0xf];
|
||||
}
|
||||
out[2 * i] = '\0';
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
@@ -318,21 +343,32 @@ curl_progress_cb(void *clientp, curl_off_t dltotal, curl_off_t dlnow,
|
||||
return p->cb(p->ctx, p->base + (long long)dlnow, total);
|
||||
}
|
||||
|
||||
/* Returns 0 on success, -1 on download/network failure, -2 when an expected
|
||||
SHA-256 was given and the downloaded bytes did not match it. */
|
||||
static int
|
||||
http_download_to_file_progress(const char *url, FILE *fp, long long *bytes_out,
|
||||
progress_state_t *progress) {
|
||||
progress_state_t *progress,
|
||||
const char *expected_sha256_hex) {
|
||||
CURL *curl;
|
||||
CURLcode res;
|
||||
char host[256], ip[INET_ADDRSTRLEN], rs443[512], rs80[512];
|
||||
struct curl_slist *rl = NULL;
|
||||
struct curl_blob ca_blob;
|
||||
curl_off_t dl = 0;
|
||||
write_sink_t sink = { fp, NULL };
|
||||
int verify = (expected_sha256_hex && expected_sha256_hex[0]);
|
||||
|
||||
if (bytes_out) *bytes_out = 0;
|
||||
if (url_host(url, host, sizeof(host))) return -1;
|
||||
if (!host_allowed(host)) return -1;
|
||||
if (dns_lookup(host, ip, sizeof(ip))) return -1;
|
||||
|
||||
if (verify) {
|
||||
sink.md = EVP_MD_CTX_new();
|
||||
if (sink.md)
|
||||
EVP_DigestInit_ex(sink.md, EVP_sha256(), NULL);
|
||||
}
|
||||
|
||||
snprintf(rs443, sizeof(rs443), "%s:443:%s", host, ip);
|
||||
rl = curl_slist_append(NULL, rs443);
|
||||
snprintf(rs80, sizeof(rs80), "%s:80:%s", host, ip);
|
||||
@@ -343,12 +379,16 @@ http_download_to_file_progress(const char *url, FILE *fp, long long *bytes_out,
|
||||
ca_blob.flags = CURL_BLOB_COPY;
|
||||
|
||||
curl = curl_easy_init();
|
||||
if (!curl) { curl_slist_free_all(rl); return -1; }
|
||||
if (!curl) {
|
||||
curl_slist_free_all(rl);
|
||||
if (sink.md) EVP_MD_CTX_free(sink.md);
|
||||
return -1;
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_RESOLVE, rl);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, file_write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &sink);
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &ca_blob);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
@@ -371,7 +411,22 @@ http_download_to_file_progress(const char *url, FILE *fp, long long *bytes_out,
|
||||
curl_easy_cleanup(curl);
|
||||
curl_slist_free_all(rl);
|
||||
|
||||
if (res != CURLE_OK) return -1;
|
||||
if (res != CURLE_OK) {
|
||||
if (sink.md) EVP_MD_CTX_free(sink.md);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (sink.md) {
|
||||
unsigned char dig[EVP_MAX_MD_SIZE];
|
||||
unsigned int dlen = 0;
|
||||
char hex[2 * EVP_MAX_MD_SIZE + 1];
|
||||
EVP_DigestFinal_ex(sink.md, dig, &dlen);
|
||||
EVP_MD_CTX_free(sink.md);
|
||||
hex_encode(dig, dlen, hex, sizeof(hex));
|
||||
if (strcasecmp(hex, expected_sha256_hex) != 0)
|
||||
return -2; /* integrity mismatch */
|
||||
}
|
||||
|
||||
if (bytes_out) *bytes_out = (long long)dl;
|
||||
return 0;
|
||||
}
|
||||
@@ -385,7 +440,7 @@ patchdl_http_download_progress(const char *url, const char *dest_path,
|
||||
int rc;
|
||||
|
||||
if (!fp) return -1;
|
||||
rc = http_download_to_file_progress(url, fp, bytes_out, &progress);
|
||||
rc = http_download_to_file_progress(url, fp, bytes_out, &progress, NULL);
|
||||
fclose(fp);
|
||||
|
||||
if (rc) {
|
||||
@@ -449,7 +504,7 @@ patchdl_http_download_manifest_progress(const char *manifest_url,
|
||||
const char *dest_path,
|
||||
long long *bytes_out,
|
||||
patchdl_download_progress_cb cb,
|
||||
void *ctx) {
|
||||
void *ctx, int verify) {
|
||||
patchdl_buf_t manifest;
|
||||
const char *pieces;
|
||||
const char *p;
|
||||
@@ -486,10 +541,11 @@ patchdl_http_download_manifest_progress(const char *manifest_url,
|
||||
p = pieces;
|
||||
while ((p = strstr(p, "\"url\"")) && (!pieces_end || p < pieces_end)) {
|
||||
char url[768];
|
||||
char hash[80] = {0};
|
||||
long long got = 0;
|
||||
unsigned long long expected = 0;
|
||||
unsigned long long offset = 0;
|
||||
int have_offset;
|
||||
int have_offset, drc;
|
||||
const char *obj_end = strchr(p, '}');
|
||||
progress_state_t progress = {
|
||||
cb,
|
||||
@@ -502,6 +558,8 @@ patchdl_http_download_manifest_progress(const char *manifest_url,
|
||||
break;
|
||||
json_u64_after(p, "fileSize", &expected);
|
||||
have_offset = (json_u64_after(p, "fileOffset", &offset) == 0);
|
||||
if (verify)
|
||||
json_string_after(p, "hashValue", hash, sizeof(hash));
|
||||
|
||||
/* Pieces are concatenated in array order; each one's fileOffset must
|
||||
equal the bytes written so far. A manifest that lists them out of
|
||||
@@ -509,8 +567,13 @@ patchdl_http_download_manifest_progress(const char *manifest_url,
|
||||
if (have_offset && offset != (unsigned long long)total)
|
||||
goto done;
|
||||
|
||||
if (http_download_to_file_progress(url, fp, &got, &progress))
|
||||
goto done; /* network error or cancel mid-piece */
|
||||
/* drc: 0 ok, -1 network/cancel, -2 SHA-256 mismatch (propagated out). */
|
||||
drc = http_download_to_file_progress(url, fp, &got, &progress,
|
||||
hash[0] ? hash : NULL);
|
||||
if (drc) {
|
||||
if (drc == -2) rc = -2;
|
||||
goto done;
|
||||
}
|
||||
if (expected && (unsigned long long)got != expected)
|
||||
goto done;
|
||||
|
||||
@@ -538,7 +601,7 @@ int
|
||||
patchdl_http_download_manifest(const char *manifest_url, const char *dest_path,
|
||||
long long *bytes_out) {
|
||||
return patchdl_http_download_manifest_progress(manifest_url, dest_path,
|
||||
bytes_out, NULL, NULL);
|
||||
bytes_out, NULL, NULL, 0);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -633,8 +696,8 @@ patchdl_http_download_manifest_progress(const char *manifest_url,
|
||||
const char *dest_path,
|
||||
long long *bytes_out,
|
||||
patchdl_download_progress_cb cb,
|
||||
void *ctx) {
|
||||
(void)cb; (void)ctx;
|
||||
void *ctx, int verify) {
|
||||
(void)cb; (void)ctx; (void)verify;
|
||||
return patchdl_http_download_manifest(manifest_url, dest_path, bytes_out);
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -28,14 +28,15 @@ int patchdl_http_download_progress(const char *url, const char *dest_path,
|
||||
patchdl_download_progress_cb cb, void *ctx);
|
||||
|
||||
/* Download a Sony JSON package manifest by concatenating every entry in
|
||||
"pieces" into one installable PKG. */
|
||||
"pieces" into one installable PKG. When `verify` is non-zero each piece is
|
||||
checked against its manifest SHA-256 (a mismatch returns -2). */
|
||||
int patchdl_http_download_manifest(const char *manifest_url, const char *dest_path,
|
||||
long long *bytes_out);
|
||||
int patchdl_http_download_manifest_progress(const char *manifest_url,
|
||||
const char *dest_path,
|
||||
long long *bytes_out,
|
||||
patchdl_download_progress_cb cb,
|
||||
void *ctx);
|
||||
void *ctx, int verify);
|
||||
|
||||
/* Diagnostic: run the GET pipeline for `url` and write a JSON report
|
||||
(dns result/ip, curl code, http status, bytes) into `out_json`. */
|
||||
|
||||
+25
-12
@@ -37,7 +37,8 @@ static struct {
|
||||
char default_policy[8]; /* "deny" | "allow" */
|
||||
int install_after_download;
|
||||
int delete_pkg_after_install;
|
||||
} g_cfg = { "deny", 0, 1 };
|
||||
int verify_downloads; /* SHA-256 each manifest piece (default off) */
|
||||
} g_cfg = { "deny", 0, 1, 0 };
|
||||
|
||||
static struct {
|
||||
int active;
|
||||
@@ -436,10 +437,12 @@ build_config_json(void) {
|
||||
snprintf(head, sizeof(head),
|
||||
"{\"default_policy\":\"%s\","
|
||||
"\"install_after_download\":%s,"
|
||||
"\"delete_pkg_after_install\":%s,",
|
||||
"\"delete_pkg_after_install\":%s,"
|
||||
"\"verify_downloads\":%s,",
|
||||
g_cfg.default_policy[0] ? g_cfg.default_policy : "deny",
|
||||
g_cfg.install_after_download ? "true" : "false",
|
||||
g_cfg.delete_pkg_after_install ? "true" : "false");
|
||||
g_cfg.delete_pkg_after_install ? "true" : "false",
|
||||
g_cfg.verify_downloads ? "true" : "false");
|
||||
pthread_mutex_unlock(&g_mutex);
|
||||
|
||||
char *out = malloc(strlen(head) + sizeof(config_tail_json));
|
||||
@@ -463,10 +466,12 @@ save_config(void) {
|
||||
fprintf(f,
|
||||
"{\n\"default_policy\":\"%s\",\n"
|
||||
"\"install_after_download\":%s,\n"
|
||||
"\"delete_pkg_after_install\":%s,\n\"titles\":{",
|
||||
"\"delete_pkg_after_install\":%s,\n"
|
||||
"\"verify_downloads\":%s,\n\"titles\":{",
|
||||
g_cfg.default_policy[0] ? g_cfg.default_policy : "deny",
|
||||
g_cfg.install_after_download ? "true" : "false",
|
||||
g_cfg.delete_pkg_after_install ? "true" : "false");
|
||||
g_cfg.delete_pkg_after_install ? "true" : "false",
|
||||
g_cfg.verify_downloads ? "true" : "false");
|
||||
for (size_t i = 0; i < g_title_count; i++)
|
||||
fprintf(f, "%s\"%s\":%s", i ? "," : "",
|
||||
g_titles[i].title_id, g_titles[i].enabled ? "true" : "false");
|
||||
@@ -500,6 +505,8 @@ load_config(void) {
|
||||
json_get_bool(buf, "install_after_download", g_cfg.install_after_download);
|
||||
g_cfg.delete_pkg_after_install =
|
||||
json_get_bool(buf, "delete_pkg_after_install", g_cfg.delete_pkg_after_install);
|
||||
g_cfg.verify_downloads =
|
||||
json_get_bool(buf, "verify_downloads", g_cfg.verify_downloads);
|
||||
|
||||
/* per-title overrides live under "titles": { "<id>": true|false, ... } */
|
||||
const char *titles = strstr(buf, "\"titles\"");
|
||||
@@ -835,6 +842,7 @@ do_download(struct MHD_Connection *conn, const char *title_id,
|
||||
const char *name, const char *version, int enabled) {
|
||||
char dir[256], dest[320], resp[640];
|
||||
long long bytes = 0;
|
||||
int verify = 0, dlrc;
|
||||
|
||||
if (!enabled)
|
||||
return queue_json(conn, MHD_HTTP_FORBIDDEN,
|
||||
@@ -866,13 +874,15 @@ do_download(struct MHD_Connection *conn, const char *title_id,
|
||||
strncpy(g_dl.name, name && name[0] ? name : title_id, sizeof(g_dl.name) - 1);
|
||||
strncpy(g_dl.version, version ? version : "", sizeof(g_dl.version) - 1);
|
||||
strncpy(g_dl.path, dest, sizeof(g_dl.path) - 1);
|
||||
verify = g_cfg.verify_downloads;
|
||||
pthread_mutex_unlock(&g_mutex);
|
||||
|
||||
if ((url_is_manifest(patch_url)
|
||||
? patchdl_http_download_manifest_progress(patch_url, dest, &bytes,
|
||||
download_progress_cb, NULL)
|
||||
: patchdl_http_download_progress(patch_url, dest, &bytes,
|
||||
download_progress_cb, NULL))) {
|
||||
dlrc = url_is_manifest(patch_url)
|
||||
? patchdl_http_download_manifest_progress(patch_url, dest, &bytes,
|
||||
download_progress_cb, NULL, verify)
|
||||
: patchdl_http_download_progress(patch_url, dest, &bytes,
|
||||
download_progress_cb, NULL);
|
||||
if (dlrc) {
|
||||
int was_cancel;
|
||||
pthread_mutex_lock(&g_mutex);
|
||||
was_cancel = g_dl.cancel;
|
||||
@@ -889,8 +899,9 @@ do_download(struct MHD_Connection *conn, const char *title_id,
|
||||
return queue_json(conn, MHD_HTTP_OK,
|
||||
"{\"ok\":false,\"cancelled\":true,"
|
||||
"\"reason\":\"download_cancelled\"}");
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"ok\":false,\"reason\":\"download_failed\"}");
|
||||
/* -2 = a piece failed its SHA-256 (only possible when verify is on). */
|
||||
snprintf(resp, sizeof(resp), "{\"ok\":false,\"reason\":\"%s\"}",
|
||||
dlrc == -2 ? "piece_verify_failed" : "download_failed");
|
||||
return queue_json_owned(conn, MHD_HTTP_BAD_GATEWAY, strdup(resp));
|
||||
}
|
||||
|
||||
@@ -1064,6 +1075,8 @@ handle_config_post(struct MHD_Connection *conn, const char *body) {
|
||||
json_get_bool(body, "install_after_download", g_cfg.install_after_download);
|
||||
g_cfg.delete_pkg_after_install =
|
||||
json_get_bool(body, "delete_pkg_after_install", g_cfg.delete_pkg_after_install);
|
||||
g_cfg.verify_downloads =
|
||||
json_get_bool(body, "verify_downloads", g_cfg.verify_downloads);
|
||||
pthread_mutex_unlock(&g_mutex);
|
||||
|
||||
save_config();
|
||||
|
||||
@@ -47,6 +47,7 @@ const fallback = {
|
||||
download_dir: "/data/patchdl (internal)",
|
||||
install_after_download: false,
|
||||
delete_pkg_after_install: true,
|
||||
verify_downloads: false,
|
||||
source_policy: {
|
||||
official: { allow_check: true, allow_download: true, allow_install: true },
|
||||
external: { allow_check: true, allow_download: true, allow_install: true },
|
||||
@@ -168,6 +169,7 @@ function bindElements() {
|
||||
downloadDir: document.getElementById("downloadDir"),
|
||||
installAfterDownload: document.getElementById("installAfterDownload"),
|
||||
deleteAfterInstall: document.getElementById("deleteAfterInstall"),
|
||||
verifyDownloads: document.getElementById("verifyDownloads"),
|
||||
refreshBtn: document.getElementById("refreshBtn"),
|
||||
saveBtn: document.getElementById("saveBtn"),
|
||||
pauseAllBtn: document.getElementById("pauseAllBtn"),
|
||||
@@ -262,6 +264,8 @@ function renderSettings() {
|
||||
els.downloadDir.value = state.config.download_dir || "";
|
||||
els.installAfterDownload.checked = Boolean(state.config.install_after_download);
|
||||
els.deleteAfterInstall.checked = Boolean(state.config.delete_pkg_after_install);
|
||||
if (els.verifyDownloads)
|
||||
els.verifyDownloads.checked = Boolean(state.config.verify_downloads);
|
||||
els.allowlistHosts.replaceChildren(...(state.config.cdn_allowlist || []).map((host) => {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "host-chip";
|
||||
@@ -573,6 +577,7 @@ async function saveConfig() {
|
||||
download_dir: els.downloadDir.value.trim(),
|
||||
install_after_download: els.installAfterDownload.checked,
|
||||
delete_pkg_after_install: els.deleteAfterInstall.checked,
|
||||
verify_downloads: els.verifyDownloads ? els.verifyDownloads.checked : Boolean(state.config.verify_downloads),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -782,6 +787,7 @@ const REASON_TEXT = {
|
||||
no_compatible_patch: "No compatible patch available.",
|
||||
download_failed: "Download failed.",
|
||||
download_in_progress: "Another download is already running.",
|
||||
piece_verify_failed: "A downloaded piece failed its SHA-256 check.",
|
||||
};
|
||||
function reasonText(error) {
|
||||
const r = error && error.body && error.body.reason;
|
||||
|
||||
@@ -184,6 +184,13 @@
|
||||
<em>Only after a successful install</em>
|
||||
</span>
|
||||
</label>
|
||||
<label class="switch-row">
|
||||
<input id="verifyDownloads" type="checkbox" />
|
||||
<span>
|
||||
<strong>Verify downloaded pieces (SHA-256)</strong>
|
||||
<em>Checks each manifest piece; off by default, verify on-device first</em>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="allowlist">
|
||||
|
||||
Reference in new issue
Block a user