ssh_data 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ module SSHData
2
+ module PublicKey
3
+ # Public key algorithm identifiers
4
+ ALGO_RSA = "ssh-rsa"
5
+ ALGO_DSA = "ssh-dss"
6
+ ALGO_ECDSA256 = "ecdsa-sha2-nistp256"
7
+ ALGO_ECDSA384 = "ecdsa-sha2-nistp384"
8
+ ALGO_ECDSA521 = "ecdsa-sha2-nistp521"
9
+ ALGO_ED25519 = "ssh-ed25519"
10
+
11
+ # RSA SHA2 *signature* algorithms used with ALGO_RSA keys.
12
+ # https://tools.ietf.org/html/draft-rsa-dsa-sha2-256-02
13
+ ALGO_RSA_SHA2_256 = "rsa-sha2-256"
14
+ ALGO_RSA_SHA2_512 = "rsa-sha2-512"
15
+
16
+ ALGOS = [
17
+ ALGO_RSA, ALGO_DSA, ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521,
18
+ ALGO_ED25519
19
+ ]
20
+
21
+ # Parse an OpenSSH public key in authorized_keys format (see sshd(8) manual
22
+ # page).
23
+ #
24
+ # key - An OpenSSH formatted public key, including algo, base64 encoded key
25
+ # and optional comment.
26
+ #
27
+ # Returns a PublicKey::Base subclass instance.
28
+ def self.parse_openssh(key)
29
+ algo, raw, _ = SSHData.key_parts(key)
30
+ parsed = parse_rfc4253(raw)
31
+
32
+ if parsed.algo != algo
33
+ raise DecodeError, "algo mismatch: #{parsed.algo.inspect}!=#{algo.inspect}"
34
+ end
35
+
36
+ parsed
37
+ end
38
+
39
+ # Deprecated
40
+ singleton_class.send(:alias_method, :parse, :parse_openssh)
41
+
42
+ # Parse an RFC 4253 binary SSH public key.
43
+ #
44
+ # key - A RFC 4253 binary public key String.
45
+ #
46
+ # Returns a PublicKey::Base subclass instance.
47
+ def self.parse_rfc4253(raw)
48
+ data, read = Encoding.decode_public_key(raw)
49
+
50
+ if read != raw.bytesize
51
+ raise DecodeError, "unexpected trailing data"
52
+ end
53
+
54
+ from_data(data)
55
+ end
56
+
57
+ def self.from_data(data)
58
+ case data[:algo]
59
+ when ALGO_RSA
60
+ RSA.new(**data)
61
+ when ALGO_DSA
62
+ DSA.new(**data)
63
+ when ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521
64
+ ECDSA.new(**data)
65
+ when ALGO_ED25519
66
+ ED25519.new(**data)
67
+ else
68
+ raise DecodeError, "unkown algo: #{data[:algo].inspect}"
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ require "ssh_data/public_key/base"
75
+ require "ssh_data/public_key/rsa"
76
+ require "ssh_data/public_key/dsa"
77
+ require "ssh_data/public_key/ecdsa"
78
+ require "ssh_data/public_key/ed25519"
@@ -0,0 +1,71 @@
1
+ module SSHData
2
+ module PublicKey
3
+ class Base
4
+ attr_reader :algo
5
+
6
+ def initialize(**kwargs)
7
+ @algo = kwargs[:algo]
8
+ end
9
+
10
+ # Calculate the fingerprint of this public key.
11
+ #
12
+ # md5: - Bool of whether to generate an MD5 fingerprint instead of the
13
+ # default SHA256.
14
+ #
15
+ # Returns a String fingerprint.
16
+ def fingerprint(md5: false)
17
+ if md5
18
+ # colon separated, hex encoded md5 digest
19
+ OpenSSL::Digest::MD5.digest(rfc4253).unpack("H2" * 16).join(":")
20
+ else
21
+ # base64 encoded sha256 digest with b64 padding stripped
22
+ Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(rfc4253))[0...-1]
23
+ end
24
+ end
25
+
26
+ # Make an SSH signature.
27
+ #
28
+ # signed_data - The String message over which to calculated the signature.
29
+ #
30
+ # Returns a binary String signature.
31
+ def sign(signed_data)
32
+ raise "implement me"
33
+ end
34
+
35
+ # Verify an SSH signature.
36
+ #
37
+ # signed_data - The String message that the signature was calculated over.
38
+ # signature - The binary String signature with SSH encoding.
39
+ #
40
+ # Returns boolean.
41
+ def verify(signed_data, signature)
42
+ raise "implement me"
43
+ end
44
+
45
+ # RFC4253 binary encoding of the public key.
46
+ #
47
+ # Returns a binary String.
48
+ def rfc4253
49
+ raise "implement me"
50
+ end
51
+
52
+ # OpenSSH public key in authorized_keys format (see sshd(8) manual page).
53
+ #
54
+ # comment - Optional String comment to append.
55
+ #
56
+ # Returns a String key.
57
+ def openssh(comment: nil)
58
+ [algo, Base64.strict_encode64(rfc4253), comment].compact.join(" ")
59
+ end
60
+
61
+ # Is this public key equal to another public key?
62
+ #
63
+ # other - Another SSHData::PublicKey::Base instance to compare with.
64
+ #
65
+ # Returns boolean.
66
+ def ==(other)
67
+ other.class == self.class
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,122 @@
1
+ module SSHData
2
+ module PublicKey
3
+ class DSA < Base
4
+ attr_reader :p, :q, :g, :y, :openssl
5
+
6
+ # Convert an SSH encoded DSA signature to DER encoding for verification with
7
+ # OpenSSL.
8
+ #
9
+ # sig - A binary String signature from an SSH packet.
10
+ #
11
+ # Returns a binary String signature, as expected by OpenSSL.
12
+ def self.openssl_signature(sig)
13
+ if sig.bytesize != 40
14
+ raise DecodeError, "bad DSA signature size"
15
+ end
16
+
17
+ r = OpenSSL::BN.new(sig.byteslice(0, 20), 2)
18
+ s = OpenSSL::BN.new(sig.byteslice(20, 20), 2)
19
+
20
+ OpenSSL::ASN1::Sequence.new([
21
+ OpenSSL::ASN1::Integer.new(r),
22
+ OpenSSL::ASN1::Integer.new(s)
23
+ ]).to_der
24
+ end
25
+
26
+ # Convert an DER encoded DSA signature, as generated by OpenSSL to SSH
27
+ # encoding.
28
+ #
29
+ # sig - A binary String signature, as generated by OpenSSL.
30
+ #
31
+ # Returns a binary String signature, as found in an SSH packet.
32
+ def self.ssh_signature(sig)
33
+ a1 = OpenSSL::ASN1.decode(sig)
34
+ if a1.tag_class != :UNIVERSAL || a1.tag != OpenSSL::ASN1::SEQUENCE || a1.value.count != 2
35
+ raise DecodeError, "bad asn1 signature"
36
+ end
37
+
38
+ r, s = a1.value
39
+ if r.tag_class != :UNIVERSAL || r.tag != OpenSSL::ASN1::INTEGER || s.tag_class != :UNIVERSAL || s.tag != OpenSSL::ASN1::INTEGER
40
+ raise DecodeError, "bad asn1 signature"
41
+ end
42
+
43
+ # left pad big endian representations to 20 bytes and concatenate
44
+ [
45
+ "\x00" * (20 - r.value.num_bytes),
46
+ r.value.to_s(2),
47
+ "\x00" * (20 - s.value.num_bytes),
48
+ s.value.to_s(2)
49
+ ].join
50
+ end
51
+
52
+ def initialize(algo:, p:, q:, g:, y:)
53
+ unless algo == ALGO_DSA
54
+ raise DecodeError, "bad algorithm: #{algo.inspect}"
55
+ end
56
+
57
+ @p = p
58
+ @q = q
59
+ @g = g
60
+ @y = y
61
+
62
+ @openssl = OpenSSL::PKey::DSA.new(asn1.to_der)
63
+
64
+ super(algo: algo)
65
+ end
66
+
67
+ # Verify an SSH signature.
68
+ #
69
+ # signed_data - The String message that the signature was calculated over.
70
+ # signature - The binarty String signature with SSH encoding.
71
+ #
72
+ # Returns boolean.
73
+ def verify(signed_data, signature)
74
+ sig_algo, ssh_sig, _ = Encoding.decode_signature(signature)
75
+ if sig_algo != ALGO_DSA
76
+ raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}"
77
+ end
78
+
79
+ openssl_sig = self.class.openssl_signature(ssh_sig)
80
+ openssl.verify(OpenSSL::Digest::SHA1.new, openssl_sig, signed_data)
81
+ end
82
+
83
+ # RFC4253 binary encoding of the public key.
84
+ #
85
+ # Returns a binary String.
86
+ def rfc4253
87
+ Encoding.encode_fields(
88
+ [:string, algo],
89
+ [:mpint, p],
90
+ [:mpint, q],
91
+ [:mpint, g],
92
+ [:mpint, y],
93
+ )
94
+ end
95
+
96
+ # Is this public key equal to another public key?
97
+ #
98
+ # other - Another SSHData::PublicKey::Base instance to compare with.
99
+ #
100
+ # Returns boolean.
101
+ def ==(other)
102
+ super && other.p == p && other.q == q && other.g == g && other.y == y
103
+ end
104
+
105
+ private
106
+
107
+ def asn1
108
+ OpenSSL::ASN1::Sequence.new([
109
+ OpenSSL::ASN1::Sequence.new([
110
+ OpenSSL::ASN1::ObjectId.new("DSA"),
111
+ OpenSSL::ASN1::Sequence.new([
112
+ OpenSSL::ASN1::Integer.new(p),
113
+ OpenSSL::ASN1::Integer.new(q),
114
+ OpenSSL::ASN1::Integer.new(g),
115
+ ]),
116
+ ]),
117
+ OpenSSL::ASN1::BitString.new(OpenSSL::ASN1::Integer.new(y).to_der),
118
+ ])
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,151 @@
1
+ module SSHData
2
+ module PublicKey
3
+ class ECDSA < Base
4
+ attr_reader :curve, :public_key_bytes, :openssl
5
+
6
+ NISTP256 = "nistp256"
7
+ NISTP384 = "nistp384"
8
+ NISTP521 = "nistp521"
9
+
10
+ OPENSSL_CURVE_NAME_FOR_CURVE = {
11
+ NISTP256 => "prime256v1",
12
+ NISTP384 => "secp384r1",
13
+ NISTP521 => "secp521r1",
14
+ }
15
+
16
+ CURVE_FOR_OPENSSL_CURVE_NAME = {
17
+ "prime256v1" => NISTP256,
18
+ "secp384r1" => NISTP384,
19
+ "secp521r1" => NISTP521,
20
+ }
21
+
22
+ DIGEST_FOR_CURVE = {
23
+ NISTP256 => OpenSSL::Digest::SHA256,
24
+ NISTP384 => OpenSSL::Digest::SHA384,
25
+ NISTP521 => OpenSSL::Digest::SHA512,
26
+ }
27
+
28
+ # Convert an SSH encoded ECDSA signature to DER encoding for verification with
29
+ # OpenSSL.
30
+ #
31
+ # sig - A binary String signature from an SSH packet.
32
+ #
33
+ # Returns a binary String signature, as expected by OpenSSL.
34
+ def self.openssl_signature(sig)
35
+ r, rlen = Encoding.decode_mpint(sig, 0)
36
+ s, slen = Encoding.decode_mpint(sig, rlen)
37
+
38
+ if rlen + slen != sig.bytesize
39
+ raise DecodeError, "unexpected trailing data"
40
+ end
41
+
42
+ OpenSSL::ASN1::Sequence.new([
43
+ OpenSSL::ASN1::Integer.new(r),
44
+ OpenSSL::ASN1::Integer.new(s)
45
+ ]).to_der
46
+ end
47
+
48
+ # Convert an DER encoded ECDSA signature, as generated by OpenSSL to SSH
49
+ # encoding.
50
+ #
51
+ # sig - A binary String signature, as generated by OpenSSL.
52
+ #
53
+ # Returns a binary String signature, as found in an SSH packet.
54
+ def self.ssh_signature(sig)
55
+ a1 = OpenSSL::ASN1.decode(sig)
56
+ if a1.tag_class != :UNIVERSAL || a1.tag != OpenSSL::ASN1::SEQUENCE || a1.value.count != 2
57
+ raise DecodeError, "bad asn1 signature"
58
+ end
59
+
60
+ r, s = a1.value
61
+ if r.tag_class != :UNIVERSAL || r.tag != OpenSSL::ASN1::INTEGER || s.tag_class != :UNIVERSAL || s.tag != OpenSSL::ASN1::INTEGER
62
+ raise DecodeError, "bad asn1 signature"
63
+ end
64
+
65
+ [Encoding.encode_mpint(r.value), Encoding.encode_mpint(s.value)].join
66
+ end
67
+
68
+ def initialize(algo:, curve:, public_key:)
69
+ unless [ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521].include?(algo)
70
+ raise DecodeError, "bad algorithm: #{algo.inspect}"
71
+ end
72
+
73
+ unless algo == "ecdsa-sha2-#{curve}"
74
+ raise DecodeError, "bad curve: #{curve.inspect}"
75
+ end
76
+
77
+ @curve = curve
78
+ @public_key_bytes = public_key
79
+
80
+ @openssl = begin
81
+ OpenSSL::PKey::EC.new(asn1.to_der)
82
+ rescue ArgumentError
83
+ raise DecodeError, "bad key data"
84
+ end
85
+
86
+ super(algo: algo)
87
+ end
88
+
89
+ # Verify an SSH signature.
90
+ #
91
+ # signed_data - The String message that the signature was calculated over.
92
+ # signature - The binarty String signature with SSH encoding.
93
+ #
94
+ # Returns boolean.
95
+ def verify(signed_data, signature)
96
+ sig_algo, ssh_sig, _ = Encoding.decode_signature(signature)
97
+ if sig_algo != "ecdsa-sha2-#{curve}"
98
+ raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}"
99
+ end
100
+
101
+ openssl_sig = self.class.openssl_signature(ssh_sig)
102
+ digest = DIGEST_FOR_CURVE[curve]
103
+
104
+ openssl.verify(digest.new, openssl_sig, signed_data)
105
+ end
106
+
107
+ # RFC4253 binary encoding of the public key.
108
+ #
109
+ # Returns a binary String.
110
+ def rfc4253
111
+ Encoding.encode_fields(
112
+ [:string, algo],
113
+ [:string, curve],
114
+ [:string, public_key_bytes],
115
+ )
116
+ end
117
+
118
+ # Is this public key equal to another public key?
119
+ #
120
+ # other - Another SSHData::PublicKey::Base instance to compare with.
121
+ #
122
+ # Returns boolean.
123
+ def ==(other)
124
+ super && other.curve == curve && other.public_key_bytes == public_key_bytes
125
+ end
126
+
127
+ # The digest algorithm to use with this key's curve.
128
+ #
129
+ # Returns an OpenSSL::Digest.
130
+ def digest
131
+ DIGEST_FOR_CURVE[curve]
132
+ end
133
+
134
+ private
135
+
136
+ def asn1
137
+ unless name = OPENSSL_CURVE_NAME_FOR_CURVE[curve]
138
+ raise DecodeError, "unknown curve: #{curve.inspect}"
139
+ end
140
+
141
+ OpenSSL::ASN1::Sequence.new([
142
+ OpenSSL::ASN1::Sequence.new([
143
+ OpenSSL::ASN1::ObjectId.new("id-ecPublicKey"),
144
+ OpenSSL::ASN1::ObjectId.new(name),
145
+ ]),
146
+ OpenSSL::ASN1::BitString.new(public_key_bytes),
147
+ ])
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,74 @@
1
+ module SSHData
2
+ module PublicKey
3
+ class ED25519 < Base
4
+ attr_reader :pk, :ed25519_key
5
+
6
+ # ed25519 isn't a hard requirement for using this Gem. We only do actual
7
+ # validation with the key if the ed25519 Gem has been loaded.
8
+ def self.enabled?
9
+ Object.const_defined?(:Ed25519)
10
+ end
11
+
12
+ # Assert that the ed25519 gem has been loaded.
13
+ #
14
+ # Returns nothing, raises AlgorithmError.
15
+ def self.ed25519_gem_required!
16
+ raise AlgorithmError, "the ed25519 gem is not loaded" unless enabled?
17
+ end
18
+
19
+ def initialize(algo:, pk:)
20
+ unless algo == ALGO_ED25519
21
+ raise DecodeError, "bad algorithm: #{algo.inspect}"
22
+ end
23
+
24
+ @pk = pk
25
+
26
+ if self.class.enabled?
27
+ @ed25519_key = Ed25519::VerifyKey.new(pk)
28
+ end
29
+
30
+ super(algo: algo)
31
+ end
32
+
33
+ # Verify an SSH signature.
34
+ #
35
+ # signed_data - The String message that the signature was calculated over.
36
+ # signature - The binarty String signature with SSH encoding.
37
+ #
38
+ # Returns boolean.
39
+ def verify(signed_data, signature)
40
+ self.class.ed25519_gem_required!
41
+
42
+ sig_algo, raw_sig, _ = Encoding.decode_signature(signature)
43
+ if sig_algo != ALGO_ED25519
44
+ raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}"
45
+ end
46
+
47
+ begin
48
+ ed25519_key.verify(raw_sig, signed_data)
49
+ rescue Ed25519::VerifyError
50
+ false
51
+ end
52
+ end
53
+
54
+ # RFC4253 binary encoding of the public key.
55
+ #
56
+ # Returns a binary String.
57
+ def rfc4253
58
+ Encoding.encode_fields(
59
+ [:string, algo],
60
+ [:string, pk],
61
+ )
62
+ end
63
+
64
+ # Is this public key equal to another public key?
65
+ #
66
+ # other - Another SSHData::PublicKey::Base instance to compare with.
67
+ #
68
+ # Returns boolean.
69
+ def ==(other)
70
+ super && other.pk == pk
71
+ end
72
+ end
73
+ end
74
+ end