ssh_data 1.2.0 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2a37d746413f208ac72e4dca430d8e9ad71646cdab1a86a4beb1939d538ca00
4
- data.tar.gz: 37c066043665eb883f68f1a0bbff2c7b814a6c11a32dcef02d19cb24a7fd9b17
3
+ metadata.gz: a33f8e3096dba56186df0bf3288d36c0fda79f75d7a02e6a58c585b468d4cd76
4
+ data.tar.gz: 3f5f652b61e4fbbb06bc24154c51683e97be194a70565d993ee27ff87ec8eb2f
5
5
  SHA512:
6
- metadata.gz: 2e32a7671a4bebef2e1a9035a9be8692af5f8274e9b6e8ca8043421ed8adf620eb88878a8a55deca82d9b1c1fad725d6018230c5bd7a75a8bec727a39e8823f8
7
- data.tar.gz: e0b84d38f45eb30d9ac0a1940e26b70a83677f68bb399c206f6a599194ffddd77a9a77f18e27b2cad94de5004c67e7bf63f009f621ab9b9dabc2637fba85f1ad
6
+ metadata.gz: b6b1324539e847dd7e7d858cd0f64a8c369de781bdb8f5fcde7667ad9a61748e347209f7d6f06f7e5400d9147e67a9289f6f99aa187ec368bfc4a38ddccc22b9
7
+ data.tar.gz: 7c1a063701138b853aebff7adb7f7a9d8bccabdf04d58d81dffb16ab2a499706401355ac01f2f4e8e91789140fd051569ecf8056eae27a76d0ae1425128e62c7
@@ -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.
@@ -396,7 +424,7 @@ module SSHData
396
424
  [hash, total_read]
397
425
  end
398
426
 
399
- # Encode the series of fiends into a binary string.
427
+ # Encode the series of fields into a binary string.
400
428
  #
401
429
  # fields - A series of Arrays, each containing a Symbol type and a value to
402
430
  # encode.
@@ -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
- from_openssl(OpenSSL::PKey::DSA.generate(1024))
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.
@@ -13,7 +13,7 @@ module SSHData
13
13
  openssl_curve = PublicKey::ECDSA::OPENSSL_CURVE_NAME_FOR_CURVE[curve]
14
14
  raise AlgorithmError, "unknown curve: #{curve}" if openssl_curve.nil?
15
15
 
16
- openssl_key = OpenSSL::PKey::EC.new(openssl_curve).tap(&:generate_key)
16
+ openssl_key = OpenSSL::PKey::EC.generate(openssl_curve)
17
17
  from_openssl(openssl_key)
18
18
  end
19
19
 
@@ -21,9 +21,9 @@ module SSHData
21
21
 
22
22
  # Import an openssl private key.
23
23
  #
24
- # key - An OpenSSL::PKey::DSA instance.
24
+ # key - An OpenSSL::PKey::RSA instance.
25
25
  #
26
- # Returns a DSA instance.
26
+ # Returns a RSA instance.
27
27
  def self.from_openssl(key)
28
28
  new(
29
29
  algo: PublicKey::ALGO_RSA,
@@ -67,7 +67,7 @@ module SSHData
67
67
  # Verify an SSH signature.
68
68
  #
69
69
  # signed_data - The String message that the signature was calculated over.
70
- # signature - The binarty String signature with SSH encoding.
70
+ # signature - The binary String signature with SSH encoding.
71
71
  #
72
72
  # Returns boolean.
73
73
  def verify(signed_data, signature)
@@ -93,7 +93,7 @@ module SSHData
93
93
  # Verify an SSH signature.
94
94
  #
95
95
  # signed_data - The String message that the signature was calculated over.
96
- # signature - The binarty String signature with SSH encoding.
96
+ # signature - The binary String signature with SSH encoding.
97
97
  #
98
98
  # Returns boolean.
99
99
  def verify(signed_data, signature)
@@ -37,7 +37,7 @@ module SSHData
37
37
  # Verify an SSH signature.
38
38
  #
39
39
  # signed_data - The String message that the signature was calculated over.
40
- # signature - The binarty String signature with SSH encoding.
40
+ # signature - The binary String signature with SSH encoding.
41
41
  #
42
42
  # Returns boolean.
43
43
  def verify(signed_data, signature)
@@ -26,7 +26,7 @@ module SSHData
26
26
  # Verify an SSH signature.
27
27
  #
28
28
  # signed_data - The String message that the signature was calculated over.
29
- # signature - The binarty String signature with SSH encoding.
29
+ # signature - The binary String signature with SSH encoding.
30
30
  #
31
31
  # Returns boolean.
32
32
  def verify(signed_data, signature)
@@ -37,6 +37,12 @@ module SSHData
37
37
  raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}"
38
38
  end
39
39
 
40
+ # OpenSSH compatibility: if a the number of bytes in the signature is less than the number of bytes of the RSA
41
+ # modulus, prepend the signature with zeros.
42
+ # See https://github.com/openssh/openssh-portable/blob/ac383f3a5c6f529a2e8a5bc44af79a08c7da294e/ssh-rsa.c#L531
43
+ difference = n.num_bytes - raw_sig.bytesize
44
+ raw_sig = "\0" * difference + raw_sig if difference.positive?
45
+
40
46
  openssl.verify(digest.new, raw_sig, signed_data)
41
47
  end
42
48
 
@@ -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
- raise UnsupportedError, "SK-ECDSA verification is not supported."
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
- raise UnsupportedError, "SK-Ed25519 verification is not supported."
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)
@@ -78,6 +78,7 @@ module SSHData
78
78
  end
79
79
 
80
80
  require "ssh_data/public_key/base"
81
+ require "ssh_data/public_key/security_key"
81
82
  require "ssh_data/public_key/rsa"
82
83
  require "ssh_data/public_key/dsa"
83
84
  require "ssh_data/public_key/ecdsa"
@@ -0,0 +1,126 @@
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
+ @data_public_key ||= load_public_key
112
+ end
113
+
114
+ private def load_public_key
115
+ public_key_algorithm, _ = Encoding.decode_string(@publickey)
116
+
117
+ if PublicKey::ALGOS.include?(public_key_algorithm)
118
+ PublicKey.parse_rfc4253(@publickey)
119
+ elsif Certificate::ALGOS.include?(public_key_algorithm)
120
+ Certificate.parse_rfc4253(@publickey)
121
+ else
122
+ raise UnsupportedError, "Public key algorithm #{public_key_algorithm} is not supported."
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,3 +1,3 @@
1
1
  module SSHData
2
- VERSION = "1.2.0"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/ssh_data.rb CHANGED
@@ -34,3 +34,4 @@ require "ssh_data/certificate"
34
34
  require "ssh_data/public_key"
35
35
  require "ssh_data/private_key"
36
36
  require "ssh_data/encoding"
37
+ require "ssh_data/signature"
metadata CHANGED
@@ -1,15 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ssh_data
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mastahyeti
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-12-02 00:00:00.000000000 Z
10
+ date: 2025-01-06 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: ed25519
15
28
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +65,20 @@ dependencies:
52
65
  - - "~>"
53
66
  - !ruby/object:Gem::Version
54
67
  version: '3.10'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec-parameterized
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.0'
55
82
  - !ruby/object:Gem::Dependency
56
83
  name: rspec-mocks
57
84
  requirement: !ruby/object:Gem::Requirement
@@ -66,7 +93,6 @@ dependencies:
66
93
  - - "~>"
67
94
  - !ruby/object:Gem::Version
68
95
  version: '3.10'
69
- description:
70
96
  email: opensource+ssh_data@github.com
71
97
  executables: []
72
98
  extensions: []
@@ -89,14 +115,15 @@ files:
89
115
  - "./lib/ssh_data/public_key/ecdsa.rb"
90
116
  - "./lib/ssh_data/public_key/ed25519.rb"
91
117
  - "./lib/ssh_data/public_key/rsa.rb"
118
+ - "./lib/ssh_data/public_key/security_key.rb"
92
119
  - "./lib/ssh_data/public_key/skecdsa.rb"
93
120
  - "./lib/ssh_data/public_key/sked25519.rb"
121
+ - "./lib/ssh_data/signature.rb"
94
122
  - "./lib/ssh_data/version.rb"
95
123
  homepage: https://github.com/github/ssh_data
96
124
  licenses:
97
125
  - MIT
98
126
  metadata: {}
99
- post_install_message:
100
127
  rdoc_options: []
101
128
  require_paths:
102
129
  - lib
@@ -104,15 +131,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
104
131
  requirements:
105
132
  - - ">="
106
133
  - !ruby/object:Gem::Version
107
- version: '2.3'
134
+ version: '3.1'
108
135
  required_rubygems_version: !ruby/object:Gem::Requirement
109
136
  requirements:
110
137
  - - ">="
111
138
  - !ruby/object:Gem::Version
112
139
  version: '0'
113
140
  requirements: []
114
- rubygems_version: 3.1.2
115
- signing_key:
141
+ rubygems_version: 3.6.2
116
142
  specification_version: 4
117
143
  summary: Library for parsing SSH certificates
118
144
  test_files: []