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
//! OpenSSH public-key and `allowed_signers` text forms for a lys Ed25519 key.
//!
//! # Why this exists
//!
//! A lys identity is an Ed25519 keypair, and so is an SSH signing key. Git
//! can sign commits and tags with an SSH key (`gpg.format = ssh`) and verify
//! them against an `allowed_signers` file, which means a lys identity can
//! sign commits that `git verify-commit` checks on plain GitHub — no
//! accounts, no third-party service, no lys installed on the verifier's
//! machine. That is the same "a stranger can check this with standard
//! tooling" property the rest of this crate exists for, obtained for the
//! price of an encoding.
//!
//! # Invariants
//!
//! - The public-key blob is the RFC 4253 §6.6 / RFC 8709 §4 form: the string
//! `"ssh-ed25519"` followed by the 32-byte key, each length-prefixed with
//! a big-endian `u32`, then standard base64 with padding. Every
//! `ssh-ed25519` key therefore begins `AAAAC3NzaC1lZDI1NTE5AAAAI`, which
//! is asserted in the tests as a cross-check against an external constant
//! rather than only against our own output.
//! - A principal is non-empty and free of whitespace. `allowed_signers` is a
//! whitespace-separated line format, so a principal containing a space
//! would silently shift every following field — the exact class of quiet
//! corruption this crate refuses elsewhere.
//! - Only public material is handled here. Nothing in this module can emit a
//! private key, and there is deliberately no private-key export: an
//! OpenSSH private key file is a second at-rest copy of the seed, and this
//! crate keeps exactly one.
//!
//! # This is not a lys wire format
//!
//! These encodings are OpenSSH's, not ours. They carry no domain-separation
//! tag and nothing here freezes under the lys versioning rules — if OpenSSH
//! changes its text form, the correct response is to follow it, because the
//! whole value of this module is agreeing with somebody else's parser.
use Engine;
use STANDARD;
use crate;
/// The SSH public-key algorithm name for Ed25519 (RFC 8709 §4).
const SSH_ED25519: & = b"ssh-ed25519";
/// The signature namespace Git uses for commit and tag signatures.
///
/// `allowed_signers` entries are scoped to a namespace; `git` is the one
/// `git verify-commit` and `git verify-tag` present.
const GIT_NAMESPACE: &str = "git";
/// Encodes a length-prefixed SSH string into `out` (RFC 4251 §5).
/// Returns the OpenSSH one-line public-key form of an Ed25519 public key.
///
/// The result is `ssh-ed25519 <base64 blob>`, byte-compatible with
/// `authorized_keys`, `allowed_signers`, and `ssh-keygen -l`. No comment
/// field is appended; callers that want one can add it, since a comment is
/// free text that no parser interprets.
/// Returns an `allowed_signers` line binding `principal` to this key for
/// Git signatures.
///
/// The form is `<principal> namespaces="git" ssh-ed25519 <base64 blob>`.
/// Write it to a file and point Git at it with
/// `git config gpg.ssh.allowedSignersFile <path>`; `git verify-commit` then
/// checks commits signed by this key with no lys tooling present.
///
/// The namespace is pinned to `git` rather than left open. An unscoped entry
/// authorises this key for every SSH signature namespace, including ones the
/// operator never intended, and a signing key for commits is not
/// automatically a signing key for anything else.
///
/// # Errors
///
/// Returns [`TrustError::KeyManagement`] if `principal` is empty or contains
/// whitespace. `allowed_signers` is whitespace-separated, so such a
/// principal would shift every following field and produce a file that
/// parses into something other than what was written.