1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
//! Identity and grant levels as the SSH front end names them (the ACL model).
//!
//! # There is nothing defined here any more, and that is the point
//!
//! This module used to carry its own `KeyFingerprint`, `Principal`,
//! `PrincipalKind` and `Access`: a second definition of identity, sitting
//! beside `gunnar-core`'s, written at the same time. One implementation forbids exactly that,
//! and the `KeyFingerprint` copy was the dangerous one: it was the *value the
//! auth decision is keyed on*, so two spellings of it meant the SSH layer could
//! compute a fingerprint the identity table would never match.
//!
//! Everything here is now a re-export of the one definition in `gunnar-core`.
//! The module survives as a name (`gunnar_ssh::access::Access` reads better at
//! a call site than `gunnar_core::Level` does inside an SSH dispatch), but there
//! is no second type behind it.
//!
//! | this crate says | it is |
//! |---|---|
//! | [`Access`] | [`gunnar_core::Level`] |
//! | [`Principal`], [`PrincipalKind`] | [`gunnar_core::Principal`], [`gunnar_core::PrincipalKind`] |
//! | [`Fingerprint`] | [`gunnar_core::Fingerprint`] |
//!
//! # Fingerprinting a key that arrived through russh
//!
//! [`fingerprint_of_key`] is the only function this module still contains, and
//! it is four lines. It exists because russh hands over an
//! `ssh-key 0.7.0-rc.10` `PublicKey` and `gunnar-core` links `ssh-key 0.6.7`
//! The key itself cannot cross, but its wire blob can. The blob goes
//! to [`gunnar_core::fingerprint_of_blob`], which is where the definition lives
//! and where the known-answer tests against real `ssh-keygen -lf` output are.
use ssh_key;
/// A grant level, ordered. `Admin` implies `Write` implies `Read`.
///
/// The ordering is what lets [`command::SERVICES`](crate::command::SERVICES)
/// record the level each verb requires and the dispatch *compare* rather than
/// match, so adding a third verb (LFS over SSH) cannot forget a case. See
/// [`gunnar_core::Level::permits`].
pub type Access = Level;
/// Who the session belongs to.
///
/// The SSH username is deliberately not in here: it is decorative and must
/// never reach an authorisation decision (the ACL model).
pub type Principal = Principal;
/// What kind of thing holds a key: a human, a machine account, a deploy key.
pub type PrincipalKind = PrincipalKind;
/// A public key's SHA-256 fingerprint in OpenSSH's printed form,
/// `SHA256:<unpadded-base64>`, the value the identity table is keyed on.
pub type Fingerprint = Fingerprint;
/// Fingerprint a key russh handed over, as [`gunnar-core`](gunnar_core) would.
///
/// # Why this is not `Fingerprint::of_key`
///
/// It cannot be. `russh 0.61.2` pins `ssh-key = "=0.7.0-rc.10"` and
/// `Handler::auth_publickey` gives us *that* crate's `PublicKey`;
/// `gunnar-core` links the workspace's `ssh-key 0.6.7`. The two are
/// semver-incompatible and both are in this crate's graph, so no key value can
/// be passed from one to the other, and the workspace is deliberately not
/// bumped to a release candidate to hide it.
///
/// The SSH wire blob is bytes, and bytes cross. `to_bytes()` on ssh-key 0.7
/// produces the same encoding `to_bytes()` on ssh-key 0.6 does. That encoding
/// is the `string` from RFC 4253, the base64 payload of an `authorized_keys`
/// line, so the fingerprint this returns is the fingerprint the
/// [`AuthTable`](gunnar_core::AuthTable) stored when the key was registered.
/// `tests/auth.rs` asserts that on a key that made a real handshake, which is
/// the integration the whole merge exists to make possible.
/// # `None` is a refusal, not a panic
///
/// This runs in `auth_publickey`, on every SSH connection, over bytes the
/// remote supplied. It used to be
/// `key.to_bytes().expect("… always re-encodes")`. The reasoning behind that
/// `expect` is probably sound for a key russh has already parsed and verified
/// a signature against, but "probably sound" is not a thing to abort an auth
/// handler on, and the version split puts **two incompatible `ssh-key` copies** in this
/// graph, which is exactly the situation where a future point release changes
/// an edge-case encoding.
///
/// So a key that will not re-encode is **rejected and logged**, never accepted
/// and never panicked on. The caller maps `None` to `Auth::reject()`.
/// The seam the no-panic guard needs.
///
/// `to_bytes()` cannot be made to fail on a key russh has verified, so the
/// error arm is unreachable from any test that goes through
/// [`fingerprint_of_key`]. Taking the `Result` instead of the key lets a test
/// drive that arm directly; without it the `None` branch is dead code and the
/// guard could never go red, which is the whole reason this entry stayed open.
pub