ssh_data 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/ssh_data/encoding.rb +54 -0
- data/lib/ssh_data/private_key/dsa.rb +13 -1
- data/lib/ssh_data/public_key/security_key.rb +40 -0
- data/lib/ssh_data/public_key/skecdsa.rb +20 -2
- data/lib/ssh_data/public_key/sked25519.rb +26 -3
- data/lib/ssh_data/public_key.rb +1 -0
- data/lib/ssh_data/signature.rb +122 -0
- data/lib/ssh_data/version.rb +1 -1
- data/lib/ssh_data.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1d17ebf6c761d5e15b35c4fe273dd0431b327c41456e36971c402763a5ddcdfa
|
4
|
+
data.tar.gz: c48ac30c8c9d53b022b8f06bfe5deaf8a16954f2fcdc91cfd3178fbd6804cc88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c262a0c7c00ebab56b9c302625b3de59ced3b140e362c96cfddc4fb7d150d0d6515eab600177cb18b068a526b4e5d72314508a0ceec7bd89422321c85289dfbb
|
7
|
+
data.tar.gz: 0b794f39c77676fcbab5c545a7b172c95fd37cd72048d9eb1d9e5de26efd70e971ab318b32fa428752e364e1f24c8afa2841b9d582aab59fc4ea66abb396123a
|
data/lib/ssh_data/encoding.rb
CHANGED
@@ -3,6 +3,19 @@ module SSHData
|
|
3
3
|
# Fields in an OpenSSL private key
|
4
4
|
# https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
|
5
5
|
OPENSSH_PRIVATE_KEY_MAGIC = "openssh-key-v1\x00"
|
6
|
+
|
7
|
+
OPENSSH_SIGNATURE_MAGIC = "SSHSIG"
|
8
|
+
OPENSSH_SIGNATURE_VERSION = 0x01
|
9
|
+
|
10
|
+
OPENSSH_SIGNATURE_FIELDS = [
|
11
|
+
[:sigversion, :uint32],
|
12
|
+
[:publickey, :string],
|
13
|
+
[:namespace, :string],
|
14
|
+
[:reserved, :string],
|
15
|
+
[:hash_algorithm, :string],
|
16
|
+
[:signature, :string],
|
17
|
+
]
|
18
|
+
|
6
19
|
OPENSSH_PRIVATE_KEY_FIELDS = [
|
7
20
|
[:ciphername, :string],
|
8
21
|
[:kdfname, :string],
|
@@ -313,6 +326,21 @@ module SSHData
|
|
313
326
|
[key, str_read]
|
314
327
|
end
|
315
328
|
|
329
|
+
def decode_openssh_signature(raw, offset=0)
|
330
|
+
total_read = 0
|
331
|
+
|
332
|
+
magic = raw.byteslice(offset, OPENSSH_SIGNATURE_MAGIC.bytesize)
|
333
|
+
unless magic == OPENSSH_SIGNATURE_MAGIC
|
334
|
+
raise DecodeError, "bad OpenSSH signature"
|
335
|
+
end
|
336
|
+
|
337
|
+
total_read += OPENSSH_SIGNATURE_MAGIC.bytesize
|
338
|
+
offset += total_read
|
339
|
+
data, read = decode_fields(raw, OPENSSH_SIGNATURE_FIELDS, offset)
|
340
|
+
total_read += read
|
341
|
+
[data, total_read]
|
342
|
+
end
|
343
|
+
|
316
344
|
# Decode the fields in a certificate.
|
317
345
|
#
|
318
346
|
# raw - Binary String certificate as described by RFC4253 section 6.6.
|
@@ -680,6 +708,32 @@ module SSHData
|
|
680
708
|
[value].pack("L>")
|
681
709
|
end
|
682
710
|
|
711
|
+
# Read a uint8 from the provided raw data.
|
712
|
+
#
|
713
|
+
# raw - A binary String.
|
714
|
+
# offset - The offset into raw at which to read (default 0).
|
715
|
+
#
|
716
|
+
# Returns an Array including the decoded uint8 as an Integer and the
|
717
|
+
# Integer number of bytes read.
|
718
|
+
def decode_uint8(raw, offset=0)
|
719
|
+
if raw.bytesize < offset + 1
|
720
|
+
raise DecodeError, "data too short"
|
721
|
+
end
|
722
|
+
|
723
|
+
uint8 = raw.byteslice(offset, 1).unpack("C").first
|
724
|
+
|
725
|
+
[uint8, 1]
|
726
|
+
end
|
727
|
+
|
728
|
+
# Encoding an integer as a uint8.
|
729
|
+
#
|
730
|
+
# value - The Integer value to encode.
|
731
|
+
#
|
732
|
+
# Returns an encoded representation of the value.
|
733
|
+
def encode_uint8(value)
|
734
|
+
[value].pack("C")
|
735
|
+
end
|
736
|
+
|
683
737
|
extend self
|
684
738
|
end
|
685
739
|
end
|
@@ -7,7 +7,19 @@
|
|
7
7
|
#
|
8
8
|
# Returns a PublicKey::Base subclass instance.
|
9
9
|
def self.generate
|
10
|
-
|
10
|
+
openssl_key =
|
11
|
+
if defined?(OpenSSL::PKey.generate_parameters)
|
12
|
+
dsa_parameters = OpenSSL::PKey.generate_parameters("DSA", {
|
13
|
+
dsa_paramgen_bits: 1024,
|
14
|
+
dsa_paramgen_q_bits: 160
|
15
|
+
})
|
16
|
+
|
17
|
+
OpenSSL::PKey.generate_key(dsa_parameters)
|
18
|
+
else
|
19
|
+
OpenSSL::PKey::DSA.generate(1024)
|
20
|
+
end
|
21
|
+
|
22
|
+
from_openssl(openssl_key)
|
11
23
|
end
|
12
24
|
|
13
25
|
# Import an openssl private key.
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module SSHData
|
2
|
+
module PublicKey
|
3
|
+
module SecurityKey
|
4
|
+
|
5
|
+
# Defaults to match OpenSSH, user presence is required by verification is not.
|
6
|
+
DEFAULT_SK_VERIFY_OPTS = {
|
7
|
+
user_presence_required: true,
|
8
|
+
user_verification_required: false
|
9
|
+
}
|
10
|
+
|
11
|
+
SK_FLAG_USER_PRESENCE = 0b001
|
12
|
+
SK_FLAG_USER_VERIFICATION = 0b100
|
13
|
+
|
14
|
+
def build_signing_blob(application, signed_data, signature)
|
15
|
+
read = 0
|
16
|
+
sig_algo, raw_sig, signature_read = Encoding.decode_signature(signature)
|
17
|
+
read += signature_read
|
18
|
+
sk_flags, sk_flags_read = Encoding.decode_uint8(signature, read)
|
19
|
+
read += sk_flags_read
|
20
|
+
counter, counter_read = Encoding.decode_uint32(signature, read)
|
21
|
+
read += counter_read
|
22
|
+
|
23
|
+
if read != signature.bytesize
|
24
|
+
raise DecodeError, "unexpected trailing data"
|
25
|
+
end
|
26
|
+
|
27
|
+
application_hash = OpenSSL::Digest::SHA256.digest(application)
|
28
|
+
message_hash = OpenSSL::Digest::SHA256.digest(signed_data)
|
29
|
+
|
30
|
+
blob =
|
31
|
+
application_hash +
|
32
|
+
Encoding.encode_uint8(sk_flags) +
|
33
|
+
Encoding.encode_uint32(counter) +
|
34
|
+
message_hash
|
35
|
+
|
36
|
+
[sig_algo, raw_sig, sk_flags, blob]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module SSHData
|
2
2
|
module PublicKey
|
3
3
|
class SKECDSA < ECDSA
|
4
|
+
include SecurityKey
|
4
5
|
attr_reader :application
|
5
6
|
|
6
7
|
OPENSSL_CURVE_NAME_FOR_CURVE = {
|
@@ -34,8 +35,25 @@ module SSHData
|
|
34
35
|
)
|
35
36
|
end
|
36
37
|
|
37
|
-
def verify(signed_data, signature)
|
38
|
-
|
38
|
+
def verify(signed_data, signature, **opts)
|
39
|
+
opts = DEFAULT_SK_VERIFY_OPTS.merge(opts)
|
40
|
+
unknown_opts = opts.keys - DEFAULT_SK_VERIFY_OPTS.keys
|
41
|
+
raise UnsupportedError, "Verification options #{unknown_opts.inspect} are not supported." unless unknown_opts.empty?
|
42
|
+
|
43
|
+
sig_algo, raw_sig, sk_flags, blob = build_signing_blob(application, signed_data, signature)
|
44
|
+
self.class.check_algorithm!(sig_algo, curve)
|
45
|
+
|
46
|
+
openssl_sig = self.class.openssl_signature(raw_sig)
|
47
|
+
digest = DIGEST_FOR_CURVE[curve]
|
48
|
+
|
49
|
+
result = openssl.verify(digest.new, openssl_sig, blob)
|
50
|
+
|
51
|
+
# We don't know that the flags are correct until after we've validated the signature
|
52
|
+
# which embeds the flags, so always verify the signature first.
|
53
|
+
return false if opts[:user_presence_required] && (sk_flags & SK_FLAG_USER_PRESENCE != SK_FLAG_USER_PRESENCE)
|
54
|
+
return false if opts[:user_verification_required] && (sk_flags & SK_FLAG_USER_VERIFICATION != SK_FLAG_USER_VERIFICATION)
|
55
|
+
|
56
|
+
result
|
39
57
|
end
|
40
58
|
|
41
59
|
def ==(other)
|
@@ -1,13 +1,14 @@
|
|
1
1
|
module SSHData
|
2
2
|
module PublicKey
|
3
3
|
class SKED25519 < ED25519
|
4
|
+
include SecurityKey
|
4
5
|
attr_reader :application
|
5
6
|
|
6
7
|
def initialize(algo:, pk:, application:)
|
7
8
|
@application = application
|
8
9
|
super(algo: algo, pk: pk)
|
9
10
|
end
|
10
|
-
|
11
|
+
|
11
12
|
def self.algorithm_identifier
|
12
13
|
ALGO_SKED25519
|
13
14
|
end
|
@@ -23,8 +24,30 @@ module SSHData
|
|
23
24
|
)
|
24
25
|
end
|
25
26
|
|
26
|
-
def verify(signed_data, signature)
|
27
|
-
|
27
|
+
def verify(signed_data, signature, **opts)
|
28
|
+
self.class.ed25519_gem_required!
|
29
|
+
opts = DEFAULT_SK_VERIFY_OPTS.merge(opts)
|
30
|
+
unknown_opts = opts.keys - DEFAULT_SK_VERIFY_OPTS.keys
|
31
|
+
raise UnsupportedError, "Verification options #{unknown_opts.inspect} are not supported." unless unknown_opts.empty?
|
32
|
+
|
33
|
+
sig_algo, raw_sig, sk_flags, blob = build_signing_blob(application, signed_data, signature)
|
34
|
+
|
35
|
+
if sig_algo != self.class.algorithm_identifier
|
36
|
+
raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}"
|
37
|
+
end
|
38
|
+
|
39
|
+
result = begin
|
40
|
+
ed25519_key.verify(raw_sig, blob)
|
41
|
+
rescue Ed25519::VerifyError
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
# We don't know that the flags are correct until after we've validated the signature
|
46
|
+
# which embeds the flags, so always verify the signature first.
|
47
|
+
return false if opts[:user_presence_required] && (sk_flags & SK_FLAG_USER_PRESENCE != SK_FLAG_USER_PRESENCE)
|
48
|
+
return false if opts[:user_verification_required] && (sk_flags & SK_FLAG_USER_VERIFICATION != SK_FLAG_USER_VERIFICATION)
|
49
|
+
|
50
|
+
result
|
28
51
|
end
|
29
52
|
|
30
53
|
def ==(other)
|
data/lib/ssh_data/public_key.rb
CHANGED
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SSHData
|
4
|
+
class Signature
|
5
|
+
PEM_TYPE = "SSH SIGNATURE"
|
6
|
+
SIGNATURE_PREAMBLE = "SSHSIG"
|
7
|
+
MIN_SUPPORTED_VERSION = 1
|
8
|
+
MAX_SUPPORTED_VERSION = 1
|
9
|
+
|
10
|
+
# Spec: no SHA1 or SHA384. In practice, OpenSSH is always going to use SHA512.
|
11
|
+
# Note the actual signing / verify primitive may use a different hash algorithm.
|
12
|
+
# https://github.com/openssh/openssh-portable/blob/b7ffbb17e37f59249c31f1ff59d6c5d80888f689/PROTOCOL.sshsig#L67
|
13
|
+
SUPPORTED_HASH_ALGORITHMS = {
|
14
|
+
"sha256" => OpenSSL::Digest::SHA256,
|
15
|
+
"sha512" => OpenSSL::Digest::SHA512,
|
16
|
+
}
|
17
|
+
|
18
|
+
PERMITTED_RSA_SIGNATURE_ALGORITHMS = [
|
19
|
+
PublicKey::ALGO_RSA_SHA2_256,
|
20
|
+
PublicKey::ALGO_RSA_SHA2_512,
|
21
|
+
]
|
22
|
+
|
23
|
+
attr_reader :sigversion, :namespace, :signature, :reserved, :hash_algorithm
|
24
|
+
|
25
|
+
# Parses a PEM armored SSH signature.
|
26
|
+
# pem - A PEM encoded SSH signature.
|
27
|
+
#
|
28
|
+
# Returns a Signature instance.
|
29
|
+
def self.parse_pem(pem)
|
30
|
+
pem_type = Encoding.pem_type(pem)
|
31
|
+
|
32
|
+
if pem_type != PEM_TYPE
|
33
|
+
raise DecodeError, "Mismatched PEM type. Expecting '#{PEM_TYPE}', actually '#{pem_type}'."
|
34
|
+
end
|
35
|
+
|
36
|
+
blob = Encoding.decode_pem(pem, pem_type)
|
37
|
+
self.parse_blob(blob)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.parse_blob(blob)
|
41
|
+
data, read = Encoding.decode_openssh_signature(blob)
|
42
|
+
|
43
|
+
if read != blob.bytesize
|
44
|
+
raise DecodeError, "unexpected trailing data"
|
45
|
+
end
|
46
|
+
|
47
|
+
new(**data)
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize(sigversion:, publickey:, namespace:, reserved:, hash_algorithm:, signature:)
|
51
|
+
if sigversion > MAX_SUPPORTED_VERSION || sigversion < MIN_SUPPORTED_VERSION
|
52
|
+
raise UnsupportedError, "Signature version is not supported"
|
53
|
+
end
|
54
|
+
|
55
|
+
unless SUPPORTED_HASH_ALGORITHMS.has_key?(hash_algorithm)
|
56
|
+
raise UnsupportedError, "Hash algorithm #{hash_algorithm} is not supported."
|
57
|
+
end
|
58
|
+
|
59
|
+
# Spec: empty namespaces are not permitted.
|
60
|
+
# https://github.com/openssh/openssh-portable/blob/b7ffbb17e37f59249c31f1ff59d6c5d80888f689/PROTOCOL.sshsig#L57
|
61
|
+
raise UnsupportedError, "A namespace is required." if namespace.empty?
|
62
|
+
|
63
|
+
# Spec: ignore 'reserved', don't need to validate that it is empty.
|
64
|
+
|
65
|
+
@sigversion = sigversion
|
66
|
+
@publickey = publickey
|
67
|
+
@namespace = namespace
|
68
|
+
@reserved = reserved
|
69
|
+
@hash_algorithm = hash_algorithm
|
70
|
+
@signature = signature
|
71
|
+
end
|
72
|
+
|
73
|
+
def verify(signed_data, **opts)
|
74
|
+
signing_key = public_key
|
75
|
+
|
76
|
+
# Unwrap the signing key if this signature was created from a certificate.
|
77
|
+
key = signing_key.is_a?(Certificate) ? signing_key.public_key : signing_key
|
78
|
+
|
79
|
+
digest_algorithm = SUPPORTED_HASH_ALGORITHMS[@hash_algorithm]
|
80
|
+
|
81
|
+
if key.is_a?(PublicKey::RSA)
|
82
|
+
sig_algo, * = Encoding.decode_signature(@signature)
|
83
|
+
|
84
|
+
# Spec: If the signature is an RSA signature, the legacy 'ssh-rsa'
|
85
|
+
# identifer is not permitted.
|
86
|
+
# https://github.com/openssh/openssh-portable/blob/b7ffbb17e37f59249c31f1ff59d6c5d80888f689/PROTOCOL.sshsig#L72
|
87
|
+
unless PERMITTED_RSA_SIGNATURE_ALGORITHMS.include?(sig_algo)
|
88
|
+
raise UnsupportedError, "RSA signature #{sig_algo} is not supported."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
message_digest = digest_algorithm.digest(signed_data)
|
93
|
+
blob =
|
94
|
+
SIGNATURE_PREAMBLE +
|
95
|
+
Encoding.encode_string(@namespace) +
|
96
|
+
Encoding.encode_string(@reserved || "") +
|
97
|
+
Encoding.encode_string(@hash_algorithm) +
|
98
|
+
Encoding.encode_string(message_digest)
|
99
|
+
|
100
|
+
if key.class.include?(::SSHData::PublicKey::SecurityKey)
|
101
|
+
key.verify(blob, @signature, **opts)
|
102
|
+
else
|
103
|
+
key.verify(blob, @signature)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Gets the public key from the signature.
|
108
|
+
# If the signature was created from a certificate, this will be an
|
109
|
+
# SSHData::Certificate. Otherwise, this will be a PublicKey algorithm.
|
110
|
+
def public_key
|
111
|
+
public_key_algorithm, _ = Encoding.decode_string(@publickey)
|
112
|
+
|
113
|
+
if PublicKey::ALGOS.include?(public_key_algorithm)
|
114
|
+
PublicKey.parse_rfc4253(@publickey)
|
115
|
+
elsif Certificate::ALGOS.include?(public_key_algorithm)
|
116
|
+
Certificate.parse_rfc4253(@publickey)
|
117
|
+
else
|
118
|
+
raise UnsupportedError, "Public key algorithm #{public_key_algorithm} is not supported."
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/lib/ssh_data/version.rb
CHANGED
data/lib/ssh_data.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ssh_data
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mastahyeti
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ed25519
|
@@ -89,8 +89,10 @@ files:
|
|
89
89
|
- "./lib/ssh_data/public_key/ecdsa.rb"
|
90
90
|
- "./lib/ssh_data/public_key/ed25519.rb"
|
91
91
|
- "./lib/ssh_data/public_key/rsa.rb"
|
92
|
+
- "./lib/ssh_data/public_key/security_key.rb"
|
92
93
|
- "./lib/ssh_data/public_key/skecdsa.rb"
|
93
94
|
- "./lib/ssh_data/public_key/sked25519.rb"
|
95
|
+
- "./lib/ssh_data/signature.rb"
|
94
96
|
- "./lib/ssh_data/version.rb"
|
95
97
|
homepage: https://github.com/github/ssh_data
|
96
98
|
licenses:
|