Skip to content

Commit 40fe82d

Browse files
committed
ssh: sign and verify
Initial implementation of proposal golang/go#68197. Want to make sure the API is all right before adding more tests. Also seeking feedback on how to best test this - is it OK to sign and verify in the same test, or do you have other ideas? (maybe a fixed rand reader?).
1 parent d0a798f commit 40fe82d

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed

ssh/sig.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package ssh
2+
3+
import (
4+
"crypto/sha512"
5+
"encoding/pem"
6+
"fmt"
7+
"io"
8+
)
9+
10+
// blob according to the SSHSIG protocol.
11+
type blob struct {
12+
Namespace string
13+
Reserved string
14+
HashAlgorithm string
15+
Hash []byte
16+
}
17+
18+
// signedData according to the SSHSIG protocol.
19+
type signedData struct {
20+
MagicPreamble [6]byte
21+
Version uint32
22+
PublicKey []byte
23+
Namespace string
24+
Reserved string
25+
HashAlgorithm string
26+
Signature []byte
27+
}
28+
29+
const (
30+
sigMagicPreamble = "SSHSIG"
31+
sigVersion = 1
32+
signHashAlgorithm = "sha512"
33+
)
34+
35+
func createSignBlob(message []byte, namespace string) ([]byte, error) {
36+
hash := sha512.New()
37+
if _, err := hash.Write(message); err != nil {
38+
return nil, err
39+
}
40+
return append([]byte(sigMagicPreamble), Marshal(blob{
41+
Namespace: namespace,
42+
HashAlgorithm: signHashAlgorithm,
43+
Hash: hash.Sum(nil),
44+
})...), nil
45+
}
46+
47+
// Sign returns a detached SSH Signature for the provided message.
48+
//
49+
// The namespace is a domain-specific identifier for the context in which the
50+
// signature will be used. It must match between the Sign and [Verify] calls. A
51+
// fully-qualified suffix is recommended, e.g. "[email protected]".
52+
//
53+
// These signatures are compatible with those generated by "ssh-keygen -Y sign",
54+
// and can be verified with [Verify] or "ssh-keygen -Y verify". The returned
55+
// bytes are usually PEM encoded with [encoding/pem] and type "SSH SIGNATURE".
56+
//
57+
// If the Signer has an RSA PublicKey, it must also implement [AlgorithmSigner].
58+
// If it also implements [MultiAlgorithmSigner], the first algorithm returned by
59+
// Algorithms will be used, otherwise "rsa-sha2-512" is used.
60+
func Sign(s Signer, rand io.Reader, message []byte, namespace string) ([]byte, error) {
61+
signer, ok := s.(AlgorithmSigner)
62+
if !ok {
63+
return nil, fmt.Errorf("invalid signer")
64+
}
65+
66+
algorithm := KeyAlgoRSASHA512
67+
multiSigner, ok := s.(MultiAlgorithmSigner)
68+
if ok {
69+
algorithm = multiSigner.Algorithms()[0]
70+
}
71+
72+
data, err := createSignBlob(message, namespace)
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
sig, err := signer.SignWithAlgorithm(rand, data, algorithm)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
signedData := signedData{
83+
Version: sigVersion,
84+
PublicKey: s.PublicKey().Marshal(),
85+
Namespace: namespace,
86+
HashAlgorithm: signHashAlgorithm,
87+
Signature: Marshal(sig),
88+
}
89+
copy(signedData.MagicPreamble[:], []byte(sigMagicPreamble))
90+
91+
return pem.EncodeToMemory(&pem.Block{
92+
Type: "SSH SIGNATURE",
93+
Bytes: Marshal(signedData),
94+
}), nil
95+
}
96+
97+
// Verify verifies a detached SSH Signature for the provided message.
98+
//
99+
// The namespace is a domain-specific identifier for the context in which the
100+
// signature will be used. It must match between the [Sign] and Verify calls. A
101+
// fully-qualified suffix is recommended, e.g. "[email protected]".
102+
//
103+
// The provided signature is usually decoded from a PEM block of type "SSH
104+
// SIGNATURE" using [encoding/pem].
105+
func Verify(pub PublicKey, message, signature []byte, namespace string) error {
106+
var sig signedData
107+
if err := Unmarshal(signature, &sig); err != nil {
108+
return err
109+
}
110+
if sig.Version != sigVersion {
111+
return fmt.Errorf("invalid version: %d", sig.Version)
112+
}
113+
if s := string(sig.MagicPreamble[:]); s != sigMagicPreamble {
114+
return fmt.Errorf("invalid header: %s", s)
115+
}
116+
if sig.Namespace != namespace {
117+
return fmt.Errorf("invalid namespace: %s", sig.Namespace)
118+
}
119+
if sig.HashAlgorithm != signHashAlgorithm {
120+
return fmt.Errorf("invalid hash algorithm: %s", sig.HashAlgorithm)
121+
}
122+
123+
var sshSig Signature
124+
if err := Unmarshal(sig.Signature, &sshSig); err != nil {
125+
return err
126+
}
127+
128+
data, err := createSignBlob(message, namespace)
129+
if err != nil {
130+
return err
131+
}
132+
133+
return pub.Verify(data, &sshSig)
134+
}

ssh/sig_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package ssh
2+
3+
import (
4+
"crypto/rand"
5+
"encoding/pem"
6+
"testing"
7+
)
8+
9+
func TestEd25519SignVerify(t *testing.T) {
10+
signer, ok := testSigners["ed25519"]
11+
if !ok {
12+
t.Fatalf("cannot find signer: ed25519")
13+
}
14+
15+
const message = "gopher test message"
16+
const namespace = "gopher@test"
17+
18+
signature, err := Sign(signer, rand.Reader, []byte(message), namespace)
19+
if err != nil {
20+
t.Fatalf("could not sign: %v", err)
21+
}
22+
23+
block, _ := pem.Decode(signature)
24+
err = Verify(signer.PublicKey(), []byte(message), block.Bytes, namespace)
25+
if err != nil {
26+
t.Fatalf("could not verify signature: %v", err)
27+
}
28+
}

0 commit comments

Comments
 (0)