@@ -41,10 +41,21 @@ Response shape (one entry): |
| 41 | 41 | "name": "Alice", |
| 42 | 42 | "email": "alice@example.com", |
| 43 | 43 | "date": "2026-05-12T00:00:00Z" |
| 44 | + }, |
| 45 | + "verification": { |
| 46 | + "verified": false, |
| 47 | + "reason": "unsigned", |
| 48 | + "signature": null, |
| 49 | + "payload": null, |
| 50 | + "verified_at": null |
| 44 | 51 | } |
| 45 | 52 | } |
| 46 | 53 | ``` |
| 47 | 54 | |
| 55 | +The `verification` object mirrors GitHub's documented shape and |
| 56 | +is always present. See [Signature verification](#signature-verification) |
| 57 | +below for the field semantics and the `reason` enum. |
| 58 | + |
| 48 | 59 | Empty / uninitialised repos return `[]` rather than `404`. |
| 49 | 60 | |
| 50 | 61 | ## Get a commit |
@@ -82,3 +93,54 @@ that `git rev-parse` resolves. Unknown SHAs `404`. |
| 82 | 93 | `files[].status` is git's letter code (`A` added, `M` modified, |
| 83 | 94 | `D` deleted, `R` renamed, `C` copied, `T` type-changed). Renames |
| 84 | 95 | and copies surface the original path on `old_path`. |
| 96 | + |
| 97 | +## Signature verification |
| 98 | + |
| 99 | +Every commit response carries a `verification` object. shithub |
| 100 | +runs the signature check server-side against the bytes git |
| 101 | +stored in the commit object; the result is cached per |
| 102 | +`(repo, commit_oid)` and surfaced here. |
| 103 | + |
| 104 | +```json |
| 105 | +{ |
| 106 | + "verified": true, |
| 107 | + "reason": "valid", |
| 108 | + "signature": "-----BEGIN PGP SIGNATURE-----\n…", |
| 109 | + "payload": "tree abc…\nparent def…\n…", |
| 110 | + "verified_at": "2026-05-12T04:00:00Z" |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +| Field | Type | Notes | |
| 115 | +|---------------|------------------|--------------------------------------------------------------------------------| |
| 116 | +| `verified` | bool | `true` only when `reason == "valid"`. Always `false` otherwise. | |
| 117 | +| `reason` | string | One of the enum values below. | |
| 118 | +| `signature` | string \| null | The armored signature block as stored on the commit object. | |
| 119 | +| `payload` | string \| null | The bytes the signature was computed over (commit object minus `gpgsig`). | |
| 120 | +| `verified_at` | string \| null | RFC3339 timestamp of the cache row; `null` for unsigned / not-yet-stamped. | |
| 121 | + |
| 122 | +### `reason` enum |
| 123 | + |
| 124 | +The values mirror gh's documented enum exactly: |
| 125 | + |
| 126 | +| Value | Meaning | |
| 127 | +|-----------------------|----------------------------------------------------------------------------------------------------------| |
| 128 | +| `valid` | Signature parsed, cryptographically valid, signing email matches a verified email on the key's account. | |
| 129 | +| `unsigned` | Commit object carried no signature header. Default for cache misses; clients render no badge. | |
| 130 | +| `unknown_key` | Signature parsed but no uploaded GPG key matches the signing subkey's fingerprint. | |
| 131 | +| `unverified_email` | Signature is valid for an uploaded key, but the signing email isn't verified on that key's account. | |
| 132 | +| `bad_email` | Signature is valid for an uploaded key, but the signing email isn't on the key at all. | |
| 133 | +| `expired_key` | Signature parsed but the key was expired at signing time. | |
| 134 | +| `not_signing_key` | The key referenced isn't a signing key (capability bits missing). | |
| 135 | +| `malformed_signature` | The `gpgsig` header didn't parse as an OpenPGP signature block. | |
| 136 | +| `invalid` | Signature parsed but the cryptographic check failed. | |
| 137 | + |
| 138 | +### Cache freshness |
| 139 | + |
| 140 | +Verification rows are populated by an asynchronous backfill that |
| 141 | +runs whenever a user uploads a GPG key (and once-off at deploy |
| 142 | +time via `shithubd gpg-backfill-all`). Between key upload and |
| 143 | +backfill completion, affected commits report `unsigned` (the |
| 144 | +conservative default); the badge appears once the row is |
| 145 | +stamped. Revoking a key invalidates affected cache rows; |
| 146 | +clients see `unsigned` until another matching key is uploaded. |