ssh_data 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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