|
3 | 3 | //! ```text |
4 | 4 | //! vkey commits enable an SSH key on the key, git told to sign with it |
5 | 5 | //! vkey commits disable git stops signing; the key stays for the next enable |
| 6 | +//! vkey commits github the key on GitHub, so the commits show as verified |
6 | 7 | //! ``` |
7 | 8 | //! |
8 | 9 | //! The key is a FIDO2 authenticator (the nRF52840's passkeys), and OpenSSH keeps a |
@@ -138,42 +139,78 @@ fn make_key(key: &Path) -> Result<(), Error> { |
138 | 139 | } |
139 | 140 |
|
140 | 141 | /// GitHub shows a commit as verified only when it knows the key as a signing key. |
141 | | -/// Uploading publishes it, so it is asked, and needs `gh` with the scope for it. |
| 142 | +/// Uploading publishes it, so `enable` asks first. |
142 | 143 | fn offer_to_github(key: &Path) -> Result<(), Error> { |
143 | | - let public = pub_path(key); |
144 | | - let by_hand = format!( |
145 | | - " gh ssh-key add {} --type signing --title vkey", |
146 | | - public.display() |
147 | | - ); |
148 | | - let gh = Command::new("gh").args(["auth", "status"]).output(); |
149 | | - if !matches!(gh, Ok(ref o) if o.status.success()) { |
| 144 | + if !prompt::is_tty() |
| 145 | + || !prompt::yes_no("add the key to GitHub, so the commits show as verified?") |
| 146 | + { |
150 | 147 | println!( |
151 | | - "for GitHub to show the commits as verified, add the key as a signing key:\n{by_hand}" |
| 148 | + "to add it later: vkey commits github (or /settings → github)\n public key: {}", |
| 149 | + pub_path(key).display() |
152 | 150 | ); |
153 | 151 | return Ok(()); |
154 | 152 | } |
155 | | - if !prompt::is_tty() || !prompt::yes_no("add the key to GitHub as a signing key?") { |
156 | | - println!("to add it later:\n{by_hand}"); |
157 | | - return Ok(()); |
| 153 | + github().map(drop) |
| 154 | +} |
| 155 | + |
| 156 | +/// The scope `gh` needs to add a signing key; a plain `gh auth login` does not ask it. |
| 157 | +const SCOPE: &str = "admin:ssh_signing_key"; |
| 158 | + |
| 159 | +/// `vkey commits github`: the signing key's public half on GitHub as a signing key, |
| 160 | +/// signing `gh` in - or giving it the scope - in the browser first when it has to. |
| 161 | +pub fn github() -> Result<u8, Error> { |
| 162 | + let public = pub_path(&home()?.join(KEY)); |
| 163 | + if !public.exists() { |
| 164 | + return Err(Error::Value( |
| 165 | + "no signing key yet: vkey commits enable".into(), |
| 166 | + )); |
158 | 167 | } |
159 | | - let out = Command::new("gh") |
160 | | - .args(["ssh-key", "add"]) |
161 | | - .arg(&public) |
162 | | - .args(["--type", "signing", "--title", "vkey"]) |
163 | | - .output()?; |
164 | | - if out.status.success() { |
165 | | - println!("added to GitHub: commits signed by this key show as verified"); |
166 | | - return Ok(()); |
| 168 | + let signed_in = match Command::new("gh").args(["auth", "status"]).output() { |
| 169 | + Ok(out) => out.status.success(), |
| 170 | + Err(_) => { |
| 171 | + return Err(Error::Value( |
| 172 | + "gh, GitHub's CLI, is missing: sudo apt install gh".into(), |
| 173 | + )); |
| 174 | + } |
| 175 | + }; |
| 176 | + if !signed_in { |
| 177 | + println!("gh signs in to GitHub in the browser first"); |
| 178 | + gh_interactive(&["auth", "login", "-h", "github.com", "-s", SCOPE])?; |
167 | 179 | } |
| 180 | + let mut out = add_key(&public)?; |
| 181 | + if !out.status.success() && String::from_utf8_lossy(&out.stderr).contains(SCOPE) { |
| 182 | + println!("gh needs one more permission, to add signing keys: grant it in the browser"); |
| 183 | + gh_interactive(&["auth", "refresh", "-h", "github.com", "-s", SCOPE])?; |
| 184 | + out = add_key(&public)?; |
| 185 | + } |
| 186 | + // gh answers success for a key GitHub already has, so both read the same. |
168 | 187 | let why = String::from_utf8_lossy(&out.stderr); |
169 | | - if why.contains("admin:ssh_signing_key") { |
170 | | - println!( |
171 | | - "gh needs the scope to add signing keys first:\n gh auth refresh -h github.com -s admin:ssh_signing_key\nthen:\n{by_hand}" |
172 | | - ); |
173 | | - } else if why.contains("already") { |
174 | | - println!("GitHub already has this key"); |
| 188 | + if out.status.success() || why.contains("already") { |
| 189 | + println!("the key is on GitHub: commits signed with it show as verified"); |
175 | 190 | } else { |
176 | | - println!("GitHub refused the key: {}\n{by_hand}", why.trim()); |
| 191 | + return Err(Error::Value(format!( |
| 192 | + "GitHub refused the key: {}", |
| 193 | + why.trim() |
| 194 | + ))); |
| 195 | + } |
| 196 | + Ok(0) |
| 197 | +} |
| 198 | + |
| 199 | +fn add_key(public: &Path) -> Result<std::process::Output, Error> { |
| 200 | + Ok(Command::new("gh") |
| 201 | + .args(["ssh-key", "add"]) |
| 202 | + .arg(public) |
| 203 | + .args(["--type", "signing", "--title", "vkey"]) |
| 204 | + .output()?) |
| 205 | +} |
| 206 | + |
| 207 | +/// `gh` with the terminal: it prints a one-time code and opens the browser. |
| 208 | +fn gh_interactive(args: &[&str]) -> Result<(), Error> { |
| 209 | + if !Command::new("gh").args(args).status()?.success() { |
| 210 | + return Err(Error::Value(format!( |
| 211 | + "gh {} did not finish", |
| 212 | + args.join(" ") |
| 213 | + ))); |
177 | 214 | } |
178 | 215 | Ok(()) |
179 | 216 | } |
|
0 commit comments