Skip to content

Commit df17ff4

Browse files
committed
feat: support allowed signers parsing
This just adds the lowest level API to match the golang.org/x/crypto/ssh package. The library itself should offer a nicer API addition to also provide proper verification while utilizing the principals and options from the allowed_signers file. Signed-off-by: Hidde Beydals <[email protected]>
1 parent 0c27509 commit df17ff4

File tree

1 file changed

+115
-0
lines changed

1 file changed

+115
-0
lines changed

allowed_signers.go

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package sshsig
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"errors"
7+
"io"
8+
"strings"
9+
10+
"golang.org/x/crypto/ssh"
11+
)
12+
13+
// ParseAllowedSigner parses an entry in the format of the allowed_signers file.
14+
//
15+
// The allowed_signers format is documented in the ssh-keygen(1) manual page.
16+
// This function will parse a single entry from in. On successful return,
17+
// principals will contain the list of principals that this entry matches,
18+
// options will contain the list of options that this entry matches (i.e.
19+
// "cert-authority", "namespaces=file,git"), and pubKey will contain the
20+
// public key. See the ssh-keygen(1) manual page for the various forms that a
21+
// principal string can take, and further details on the options.
22+
//
23+
// The unparsed remainder of the input will be returned in rest. This function
24+
// can be called repeatedly to parse multiple entries.
25+
//
26+
// If no entries were found in the input then err will be io.EOF. Otherwise, a
27+
// non-nil err value indicates a parse error.
28+
//
29+
// This function is an addition to the golang.org/x/crypto/ssh package, which
30+
// does offer ssh.ParseAuthorizedKey and ssh.ParseKnownHosts, but not a parser
31+
// for allowed_signers files which has a slightly different format.
32+
func ParseAllowedSigner(in []byte) (principals []string, options []string, pubKey ssh.PublicKey, rest []byte, err error) {
33+
for len(in) > 0 {
34+
end := bytes.IndexByte(in, '\n')
35+
if end != -1 {
36+
rest = in[end+1:]
37+
in = in[:end]
38+
} else {
39+
rest = nil
40+
}
41+
42+
end = bytes.IndexByte(in, '\r')
43+
if end != -1 {
44+
in = in[:end]
45+
}
46+
47+
in = bytes.TrimSpace(in)
48+
if len(in) == 0 || in[0] == '#' {
49+
in = rest
50+
continue
51+
}
52+
53+
i := bytes.IndexAny(in, " \t")
54+
if i == -1 {
55+
in = rest
56+
continue
57+
}
58+
59+
// Split the line into the principal list, options, and key.
60+
// The options are not required, and may not be present.
61+
keyFields := bytes.Fields(in)
62+
if len(keyFields) < 3 || len(keyFields) > 4 {
63+
return nil, nil, nil, nil, errors.New("ssh: invalid entry in allowed_signers data")
64+
}
65+
66+
// The first field is the principal list.
67+
principals := string(keyFields[0])
68+
69+
// If there are 4 fields, the second field is the options list.
70+
var options string
71+
if len(keyFields) == 4 {
72+
options = string(keyFields[1])
73+
}
74+
75+
// keyFields[len(keyFields)-2] contains the key type (e.g. "sha-rsa").
76+
// This information is also available in the base64-encoded key, and
77+
// thus ignored here.
78+
key := bytes.Join(keyFields[len(keyFields)-1:], []byte(" "))
79+
if pubKey, err = parseAuthorizedKey(key); err != nil {
80+
return nil, nil, nil, nil, err
81+
}
82+
return strings.Split(principals, ","), strings.Split(options, ","), pubKey, rest, nil
83+
}
84+
return nil, nil, nil, nil, io.EOF
85+
}
86+
87+
// parseAuthorizedKey parses a public key in OpenSSH authorized_keys format
88+
// (see sshd(8) manual page) once the options and key type fields have been
89+
// removed.
90+
//
91+
// This function is a modified copy of the parseAuthorizedKey function from the
92+
// golang.org/x/crypto/ssh package, and does not return any comments.
93+
//
94+
// xref: https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.7.0:ssh/keys.go;l=88?q=parseAuthorizedKey
95+
func parseAuthorizedKey(in []byte) (out ssh.PublicKey, err error) {
96+
in = bytes.TrimSpace(in)
97+
98+
i := bytes.IndexAny(in, " \t")
99+
if i == -1 {
100+
i = len(in)
101+
}
102+
base64Key := in[:i]
103+
104+
key := make([]byte, base64.StdEncoding.DecodedLen(len(base64Key)))
105+
n, err := base64.StdEncoding.Decode(key, base64Key)
106+
if err != nil {
107+
return nil, err
108+
}
109+
key = key[:n]
110+
out, err = ssh.ParsePublicKey(key)
111+
if err != nil {
112+
return nil, err
113+
}
114+
return out, nil
115+
}

0 commit comments

Comments
 (0)