ssh_data 1.2.0 → 1.3.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.
- 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:
|