Add incremental verification, and fix verifying binary files
minisign_verify_file read the message with read_file and passed
strlen() as its length, so verification of any file containing a NUL
byte hashed only the bytes up to the first one and failed. Every binary
file was affected.
Signatures here are always prehashed, so the digest can be built as the
message arrives. Split verify() into begin/update/end, expose that as
minisign_verify_begin/update/end/free, and re-express verify() and
minisign_verify_file on top of it. Verifying a file now streams in 64K
chunks instead of holding it in memory whole.
Also add minisign_signature_key_id, which reads the key number out of a
signature without the matching public key so a caller can name the key
it is missing, and stop minisign_init(NULL) from dereferencing NULL.7 files changed +316 −39
- + .gitignore
- • CMakeLists.txt
- • README.md
- • include/minisign/minisign.h
- • src/core.c
- • src/core.h
- • src/minisign.c
| 1 | diff --git a/.gitignore b/.gitignore |
| 2 | new file mode 100644 |
| 3 | index 0000000..567609b |
| 4 | --- /dev/null |
| 5 | +++ b/.gitignore |
| 6 | @@ -0,0 +1 @@ |
| 7 | +build/ |
| 8 | diff --git a/CMakeLists.txt b/CMakeLists.txt |
| 9 | index b028801..e67d6de 100644 |
| 10 | --- a/CMakeLists.txt |
| 11 | +++ b/CMakeLists.txt |
| 12 | @@ -1,7 +1,7 @@ |
| 13 | cmake_minimum_required(VERSION 3.14) |
| 14 | project(minisign |
| 15 | LANGUAGES C CXX |
| 16 | - VERSION 1.3.1) |
| 17 | + VERSION 1.4.0) |
| 18 | |
| 19 | set(COMMENT_PREFIX_DEFAULT "untrusted comment: ") |
| 20 | set(TRUSTED_COMMENT_PREFIX_DEFAULT "trusted comment: ") |
| 21 | diff --git a/README.md b/README.md |
| 22 | index 321d9f2..b5ba2b5 100644 |
| 23 | --- a/README.md |
| 24 | +++ b/README.md |
| 25 | @@ -23,6 +23,12 @@ The header is documented, see [minisign.h](include/minisign/minisign.h) for more |
| 26 | | `minisign_sign` | Signs an in-memory message buffer and returns the generated signature. Optional comments and trusted comments can be included, and the signature can be verified before returning. | |
| 27 | | `minisign_sign_file` | Signs a file and writes the signature to a separate file. Optional comments and trusted comments can be included, and verification can be performed before writing. | |
| 28 | | `minisign_verify` | Verifies an in-memory message buffer against a signature string using the provided public key. Returns 1 if valid, 0 otherwise. | |
| 29 | +| `minisign_verify_begin` | Starts verifying a message that will be supplied in chunks. Returns an opaque state, or NULL if the signature is malformed or was made by a different key. | |
| 30 | +| `minisign_verify_update` | Feeds the next chunk of the message. | |
| 31 | +| `minisign_verify_end` | Completes verification. Returns 1 if the message and its trusted comment are both valid. | |
| 32 | +| `minisign_verify_free` | Releases a verification state. | |
| 33 | +| `minisign_verify_trusted_comment` | Returns the trusted comment carried by the signature. | |
| 34 | +| `minisign_signature_key_id` | Reads the key number a signature was made with, without needing the matching public key. | |
| 35 | | `minisign_verify_file` | Verifies a file against a given signature file using the provided public key. Returns 1 if valid, 0 otherwise. | |
| 36 | |
| 37 | ## Tools |
| 38 | diff --git a/include/minisign/minisign.h b/include/minisign/minisign.h |
| 39 | index 00c5bfe..3d65224 100644 |
| 40 | --- a/include/minisign/minisign.h |
| 41 | +++ b/include/minisign/minisign.h |
| 42 | @@ -10,6 +10,7 @@ extern "C" { |
| 43 | #define MINISIGN_API |
| 44 | #endif |
| 45 | |
| 46 | +#include <stdint.h> |
| 47 | #include <sodium.h> |
| 48 | |
| 49 | #include "globals.h" |
| 50 | @@ -160,6 +161,67 @@ minisign_read_seckey( |
| 51 | const char* key_name, |
| 52 | const char* key_dir); |
| 53 | |
| 54 | +/** |
| 55 | + * Opaque state for incremental verification. |
| 56 | + */ |
| 57 | +typedef struct VerifyState_ minisign_verify_state; |
| 58 | + |
| 59 | +/** |
| 60 | + * Begins verifying a message that will be supplied in chunks. |
| 61 | + * |
| 62 | + * Signatures are always prehashed, so the digest can be built up as the |
| 63 | + * message arrives instead of buffering the whole thing. |
| 64 | + * |
| 65 | + * @param pubkey_string Public key in string format. |
| 66 | + * @param sig_contents Signature contents to verify against. |
| 67 | + * |
| 68 | + * @return An opaque state to feed with minisign_verify_update, or NULL if the |
| 69 | + * signature is malformed or was made by a different key. Free it with |
| 70 | + * minisign_verify_free. |
| 71 | + */ |
| 72 | +MINISIGN_API minisign_verify_state* |
| 73 | +minisign_verify_begin(const char* pubkey_string, const char* sig_contents); |
| 74 | + |
| 75 | +/** |
| 76 | + * Feeds the next chunk of the message. |
| 77 | + * |
| 78 | + * @return 1 on success, 0 on failure. |
| 79 | + */ |
| 80 | +MINISIGN_API int |
| 81 | +minisign_verify_update(minisign_verify_state* state, const unsigned char* chunk, size_t chunk_len); |
| 82 | + |
| 83 | +/** |
| 84 | + * Completes verification. The state cannot be updated afterwards. |
| 85 | + * |
| 86 | + * @return 1 if the message and its trusted comment are both valid, 0 otherwise. |
| 87 | + */ |
| 88 | +MINISIGN_API int |
| 89 | +minisign_verify_end(minisign_verify_state* state); |
| 90 | + |
| 91 | +/** |
| 92 | + * Releases a verification state. |
| 93 | + */ |
| 94 | +MINISIGN_API void |
| 95 | +minisign_verify_free(minisign_verify_state* state); |
| 96 | + |
| 97 | +/** |
| 98 | + * Returns the trusted comment carried by the signature, or NULL. |
| 99 | + */ |
| 100 | +MINISIGN_API const char* |
| 101 | +minisign_verify_trusted_comment(const minisign_verify_state* state); |
| 102 | + |
| 103 | +/** |
| 104 | + * Reads the key number a signature was made with, without needing the matching |
| 105 | + * public key. Lets a caller name the key it is missing. |
| 106 | + * |
| 107 | + * @param sig_contents Signature contents. |
| 108 | + * @param out_key_id Receives the key number. |
| 109 | + * |
| 110 | + * @return 1 on success, 0 on failure. |
| 111 | + */ |
| 112 | +MINISIGN_API int |
| 113 | +minisign_signature_key_id(const char* sig_contents, uint64_t* out_key_id); |
| 114 | + |
| 115 | MINISIGN_API int safe_copy(char *dst, size_t n, const char *src); |
| 116 | MINISIGN_API int safe_join(char *dst, size_t n, const char *a, const char *b); |
| 117 | |
| 118 | diff --git a/src/core.c b/src/core.c |
| 119 | index 2bc84a4..e6d82e0 100644 |
| 120 | --- a/src/core.c |
| 121 | +++ b/src/core.c |
| 122 | @@ -443,64 +443,174 @@ seckey_load_file(const char* key_path, char *const password, char *const sk_comm |
| 123 | return seckey_struct; |
| 124 | } |
| 125 | |
| 126 | -int |
| 127 | -verify(const PubkeyStruct* pubkey_struct, const unsigned char* message_contents, const unsigned int message_size, |
| 128 | - const char* sig_contents) { |
| 129 | - char trusted_comment[TRUSTEDCOMMENTMAXBYTES]; |
| 130 | - unsigned char global_sig[crypto_sign_BYTES]; |
| 131 | - FILE *info_fp = stdout; |
| 132 | - unsigned char *sig_and_trusted_comment = NULL; |
| 133 | - SigStruct *sig_struct = NULL; |
| 134 | - unsigned char *message_hashed = NULL; |
| 135 | - size_t message_hashed_len = crypto_generichash_BYTES_MAX; |
| 136 | - size_t trusted_comment_len = 0; |
| 137 | +struct VerifyState_ { |
| 138 | + PubkeyStruct pubkey; |
| 139 | + SigStruct *sig_struct; |
| 140 | + unsigned char global_sig[crypto_sign_BYTES]; |
| 141 | + char trusted_comment[TRUSTEDCOMMENTMAXBYTES]; |
| 142 | + crypto_generichash_state hs; |
| 143 | + int done; |
| 144 | +}; |
| 145 | |
| 146 | - sig_struct = sig_load(sig_contents, global_sig, trusted_comment, sizeof trusted_comment); |
| 147 | - if (!sig_struct) { |
| 148 | - free(message_hashed); |
| 149 | - minisign_err("Could not load signature"); |
| 150 | +int |
| 151 | +sig_key_id(const char *sig_contents, uint64_t *out_key_id) { |
| 152 | + char trusted_comment[TRUSTEDCOMMENTMAXBYTES]; |
| 153 | + unsigned char global_sig[crypto_sign_BYTES]; |
| 154 | + SigStruct *sig_struct; |
| 155 | + |
| 156 | + if (sig_contents == NULL || out_key_id == NULL) { |
| 157 | + minisign_err("sig_key_id: missing argument"); |
| 158 | return 0; |
| 159 | } |
| 160 | |
| 161 | - message_hashed = message_load_hashed(message_contents, message_size); |
| 162 | + sig_struct = sig_load(sig_contents, global_sig, trusted_comment, sizeof trusted_comment); |
| 163 | + if (!sig_struct) { |
| 164 | + return 0; |
| 165 | + } |
| 166 | |
| 167 | - if (memcmp(sig_struct->keynum, pubkey_struct->keynum_pk.keynum, sizeof sig_struct->keynum) != 0) { |
| 168 | + *out_key_id = le64_load(sig_struct->keynum); |
| 169 | + free(sig_struct); |
| 170 | + return 1; |
| 171 | +} |
| 172 | + |
| 173 | +VerifyState * |
| 174 | +verify_stream_begin(const PubkeyStruct *pubkey_struct, const char *sig_contents) { |
| 175 | + VerifyState *state; |
| 176 | + |
| 177 | + if (pubkey_struct == NULL || sig_contents == NULL) { |
| 178 | + minisign_err("verify_stream_begin: missing argument"); |
| 179 | + return NULL; |
| 180 | + } |
| 181 | + |
| 182 | + state = xmalloc(sizeof *state); |
| 183 | + if (!state) { |
| 184 | + minisign_err("memory allocation failed"); |
| 185 | + return NULL; |
| 186 | + } |
| 187 | + memset(state, 0, sizeof *state); |
| 188 | + state->pubkey = *pubkey_struct; |
| 189 | + |
| 190 | + state->sig_struct = sig_load(sig_contents, state->global_sig, state->trusted_comment, |
| 191 | + sizeof state->trusted_comment); |
| 192 | + if (!state->sig_struct) { |
| 193 | + minisign_err("Could not load signature"); |
| 194 | + free(state); |
| 195 | + return NULL; |
| 196 | + } |
| 197 | + |
| 198 | + if (memcmp(state->sig_struct->keynum, state->pubkey.keynum_pk.keynum, |
| 199 | + sizeof state->sig_struct->keynum) != 0) { |
| 200 | minisign_err( |
| 201 | "Signature key id is %016" PRIX64 |
| 202 | "\n" |
| 203 | "but the key id in the public key is %016" PRIX64, |
| 204 | - le64_load(sig_struct->keynum), |
| 205 | - le64_load(pubkey_struct->keynum_pk.keynum)); |
| 206 | - free(message_hashed); |
| 207 | - free(sig_struct); |
| 208 | + le64_load(state->sig_struct->keynum), |
| 209 | + le64_load(state->pubkey.keynum_pk.keynum)); |
| 210 | + free(state->sig_struct); |
| 211 | + free(state); |
| 212 | + return NULL; |
| 213 | + } |
| 214 | + |
| 215 | + crypto_generichash_init(&state->hs, NULL, 0U, crypto_generichash_BYTES_MAX); |
| 216 | + return state; |
| 217 | +} |
| 218 | + |
| 219 | +int |
| 220 | +verify_stream_update(VerifyState *state, const unsigned char *chunk, const size_t chunk_len) { |
| 221 | + if (state == NULL || state->done) { |
| 222 | + minisign_err("verify_stream_update: invalid state"); |
| 223 | + return 0; |
| 224 | + } |
| 225 | + if (chunk_len == 0U) { |
| 226 | + return 1; |
| 227 | + } |
| 228 | + if (chunk == NULL) { |
| 229 | + minisign_err("verify_stream_update: null chunk"); |
| 230 | return 0; |
| 231 | } |
| 232 | |
| 233 | - if (crypto_sign_verify_detached(sig_struct->sig, message_hashed, message_hashed_len, |
| 234 | - pubkey_struct->keynum_pk.pk) != 0) { |
| 235 | + crypto_generichash_update(&state->hs, chunk, chunk_len); |
| 236 | + return 1; |
| 237 | +} |
| 238 | + |
| 239 | +int |
| 240 | +verify_stream_end(VerifyState *state) { |
| 241 | + unsigned char message_hashed[crypto_generichash_BYTES_MAX]; |
| 242 | + unsigned char *sig_and_trusted_comment; |
| 243 | + size_t trusted_comment_len; |
| 244 | + int ok; |
| 245 | + |
| 246 | + if (state == NULL || state->done) { |
| 247 | + minisign_err("verify_stream_end: invalid state"); |
| 248 | + return 0; |
| 249 | + } |
| 250 | + state->done = 1; |
| 251 | + |
| 252 | + crypto_generichash_final(&state->hs, message_hashed, sizeof message_hashed); |
| 253 | + |
| 254 | + if (crypto_sign_verify_detached(state->sig_struct->sig, message_hashed, sizeof message_hashed, |
| 255 | + state->pubkey.keynum_pk.pk) != 0) { |
| 256 | minisign_err("Signature verification failed"); |
| 257 | - free(message_hashed); |
| 258 | - free(sig_struct); |
| 259 | return 0; |
| 260 | } |
| 261 | - free(message_hashed); |
| 262 | |
| 263 | - trusted_comment_len = strlen(trusted_comment); |
| 264 | - sig_and_trusted_comment = xmalloc((sizeof sig_struct->sig) + trusted_comment_len); |
| 265 | - memcpy(sig_and_trusted_comment, sig_struct->sig, sizeof sig_struct->sig); |
| 266 | - memcpy(sig_and_trusted_comment + sizeof sig_struct->sig, trusted_comment, trusted_comment_len); |
| 267 | - if (crypto_sign_verify_detached(global_sig, sig_and_trusted_comment, |
| 268 | - (sizeof sig_struct->sig) + trusted_comment_len, |
| 269 | - pubkey_struct->keynum_pk.pk) != 0) { |
| 270 | + trusted_comment_len = strlen(state->trusted_comment); |
| 271 | + sig_and_trusted_comment = xmalloc((sizeof state->sig_struct->sig) + trusted_comment_len); |
| 272 | + if (!sig_and_trusted_comment) { |
| 273 | + minisign_err("memory allocation failed"); |
| 274 | + return 0; |
| 275 | + } |
| 276 | + memcpy(sig_and_trusted_comment, state->sig_struct->sig, sizeof state->sig_struct->sig); |
| 277 | + memcpy(sig_and_trusted_comment + sizeof state->sig_struct->sig, state->trusted_comment, |
| 278 | + trusted_comment_len); |
| 279 | + |
| 280 | + ok = crypto_sign_verify_detached(state->global_sig, sig_and_trusted_comment, |
| 281 | + (sizeof state->sig_struct->sig) + trusted_comment_len, |
| 282 | + state->pubkey.keynum_pk.pk) == 0; |
| 283 | + free(sig_and_trusted_comment); |
| 284 | + |
| 285 | + if (!ok) { |
| 286 | minisign_err("Comment signature verification failed"); |
| 287 | return 0; |
| 288 | } |
| 289 | - |
| 290 | - free(sig_and_trusted_comment); |
| 291 | - free(sig_struct); |
| 292 | return 1; |
| 293 | } |
| 294 | |
| 295 | +void |
| 296 | +verify_stream_free(VerifyState *state) { |
| 297 | + if (state == NULL) { |
| 298 | + return; |
| 299 | + } |
| 300 | + free(state->sig_struct); |
| 301 | + free(state); |
| 302 | +} |
| 303 | + |
| 304 | +const char * |
| 305 | +verify_stream_trusted_comment(const VerifyState *state) { |
| 306 | + return state == NULL ? NULL : state->trusted_comment; |
| 307 | +} |
| 308 | + |
| 309 | +int |
| 310 | +verify(const PubkeyStruct* pubkey_struct, const unsigned char* message_contents, const unsigned int message_size, |
| 311 | + const char* sig_contents) { |
| 312 | + VerifyState *state; |
| 313 | + int ok; |
| 314 | + |
| 315 | + if (message_contents == NULL) { |
| 316 | + minisign_err("message_contents is NULL"); |
| 317 | + return 0; |
| 318 | + } |
| 319 | + |
| 320 | + state = verify_stream_begin(pubkey_struct, sig_contents); |
| 321 | + if (!state) { |
| 322 | + return 0; |
| 323 | + } |
| 324 | + |
| 325 | + ok = verify_stream_update(state, message_contents, message_size) && verify_stream_end(state); |
| 326 | + verify_stream_free(state); |
| 327 | + return ok; |
| 328 | +} |
| 329 | + |
| 330 | static char * |
| 331 | append_sig_suffix(const char *message_file) { |
| 332 | char *sig_file; |
| 333 | diff --git a/src/core.h b/src/core.h |
| 334 | index ba77808..d6fd98d 100644 |
| 335 | --- a/src/core.h |
| 336 | +++ b/src/core.h |
| 337 | @@ -32,4 +32,19 @@ int verify( |
| 338 | unsigned int message_size, |
| 339 | const char* sig_contents); |
| 340 | |
| 341 | +/* Incremental verification. Signatures are always prehashed (SIGALG_HASHED), |
| 342 | + * so the message digest can be fed in chunks as they arrive rather than |
| 343 | + * requiring the whole message in memory. */ |
| 344 | +typedef struct VerifyState_ VerifyState; |
| 345 | + |
| 346 | +VerifyState *verify_stream_begin(const PubkeyStruct *pubkey_struct, const char *sig_contents); |
| 347 | +int verify_stream_update(VerifyState *state, const unsigned char *chunk, size_t chunk_len); |
| 348 | +int verify_stream_end(VerifyState *state); |
| 349 | +void verify_stream_free(VerifyState *state); |
| 350 | +const char *verify_stream_trusted_comment(const VerifyState *state); |
| 351 | + |
| 352 | +/* Reads the 8-byte key number out of a signature without needing the matching |
| 353 | + * public key, so a caller can report which key a package expects. */ |
| 354 | +int sig_key_id(const char *sig_contents, uint64_t *out_key_id); |
| 355 | + |
| 356 | char* read_file(const char* path); |
| 357 | \ No newline at end of file |
| 358 | diff --git a/src/minisign.c b/src/minisign.c |
| 359 | index e0c8cfd..43c245b 100644 |
| 360 | --- a/src/minisign.c |
| 361 | +++ b/src/minisign.c |
| 362 | @@ -25,6 +25,10 @@ ensure_minisign_init() { |
| 363 | |
| 364 | static int |
| 365 | ensure_valid_key_dir_name(const char *key_dir) { |
| 366 | + if (key_dir == NULL) { |
| 367 | + minisign_err("argument 'key_dir' must not be NULL"); |
| 368 | + return 0; |
| 369 | + } |
| 370 | const size_t len = strlen(key_dir); |
| 371 | if (len == 0 || key_dir[len-1] != '/') { |
| 372 | minisign_err("argument 'key_dir' must end with /"); |
| 373 | @@ -314,18 +318,48 @@ minisign_verify_file( |
| 374 | return 0; |
| 375 | } |
| 376 | |
| 377 | - char* message_contents = read_file(path); |
| 378 | - if (!message_contents) return 0; |
| 379 | - |
| 380 | char* sig_contents = read_file(path_sig); |
| 381 | if (!sig_contents) { |
| 382 | - free(message_contents); |
| 383 | + sodium_free(pubkey_struct); |
| 384 | return 0; |
| 385 | } |
| 386 | |
| 387 | - const int res = verify(pubkey_struct, (unsigned char*)message_contents, strlen(message_contents), sig_contents); |
| 388 | - free(message_contents); |
| 389 | + /* Stream the message rather than slurping it: read_file returns a C string, |
| 390 | + * so a NUL byte anywhere in a binary file used to truncate the message that |
| 391 | + * was hashed, and any large file had to be held in memory whole. */ |
| 392 | + FILE* fp = fopen(path, "rb"); |
| 393 | + if (!fp) { |
| 394 | + minisign_err("unable to open %s", path); |
| 395 | + free(sig_contents); |
| 396 | + sodium_free(pubkey_struct); |
| 397 | + return 0; |
| 398 | + } |
| 399 | + |
| 400 | + VerifyState* state = verify_stream_begin(pubkey_struct, sig_contents); |
| 401 | free(sig_contents); |
| 402 | + sodium_free(pubkey_struct); |
| 403 | + if (!state) { |
| 404 | + fclose(fp); |
| 405 | + return 0; |
| 406 | + } |
| 407 | + |
| 408 | + unsigned char buf[65536]; |
| 409 | + size_t n; |
| 410 | + int ok = 1; |
| 411 | + while ((n = fread(buf, 1U, sizeof buf, fp)) > 0U) { |
| 412 | + if (!verify_stream_update(state, buf, n)) { |
| 413 | + ok = 0; |
| 414 | + break; |
| 415 | + } |
| 416 | + } |
| 417 | + if (ok && ferror(fp)) { |
| 418 | + minisign_err("read error on %s", path); |
| 419 | + ok = 0; |
| 420 | + } |
| 421 | + fclose(fp); |
| 422 | + |
| 423 | + const int res = ok && verify_stream_end(state); |
| 424 | + verify_stream_free(state); |
| 425 | return res; |
| 426 | } |
| 427 | |
| 428 | @@ -436,4 +470,53 @@ safe_join(char *dst, const size_t n, const char *a, const char *b) { |
| 429 | memmove(dst + al, b, bl); |
| 430 | dst[al + bl] = 0; |
| 431 | return 1; |
| 432 | -} |
| 433 | \ No newline at end of file |
| 434 | +} |
| 435 | + |
| 436 | +minisign_verify_state* |
| 437 | +minisign_verify_begin(const char* pubkey_string, const char* sig_contents) { |
| 438 | + if (!ensure_minisign_init()) return NULL; |
| 439 | + |
| 440 | + if (pubkey_string == NULL) { |
| 441 | + minisign_err("you did not provide a public key"); |
| 442 | + return NULL; |
| 443 | + } |
| 444 | + if (sig_contents == NULL) { |
| 445 | + minisign_err("you did not provide the signature contents"); |
| 446 | + return NULL; |
| 447 | + } |
| 448 | + |
| 449 | + PubkeyStruct* pubkey_struct = pubkey_load(pubkey_string); |
| 450 | + if (!pubkey_struct) { |
| 451 | + return NULL; |
| 452 | + } |
| 453 | + |
| 454 | + minisign_verify_state* state = verify_stream_begin(pubkey_struct, sig_contents); |
| 455 | + sodium_free(pubkey_struct); |
| 456 | + return state; |
| 457 | +} |
| 458 | + |
| 459 | +int |
| 460 | +minisign_verify_update(minisign_verify_state* state, const unsigned char* chunk, const size_t chunk_len) { |
| 461 | + return verify_stream_update(state, chunk, chunk_len); |
| 462 | +} |
| 463 | + |
| 464 | +int |
| 465 | +minisign_verify_end(minisign_verify_state* state) { |
| 466 | + return verify_stream_end(state); |
| 467 | +} |
| 468 | + |
| 469 | +void |
| 470 | +minisign_verify_free(minisign_verify_state* state) { |
| 471 | + verify_stream_free(state); |
| 472 | +} |
| 473 | + |
| 474 | +const char* |
| 475 | +minisign_verify_trusted_comment(const minisign_verify_state* state) { |
| 476 | + return verify_stream_trusted_comment(state); |
| 477 | +} |
| 478 | + |
| 479 | +int |
| 480 | +minisign_signature_key_id(const char* sig_contents, uint64_t* out_key_id) { |
| 481 | + if (!ensure_minisign_init()) return 0; |
| 482 | + return sig_key_id(sig_contents, out_key_id); |
| 483 | +} |