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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2a37d746413f208ac72e4dca430d8e9ad71646cdab1a86a4beb1939d538ca00
4
- data.tar.gz: 37c066043665eb883f68f1a0bbff2c7b814a6c11a32dcef02d19cb24a7fd9b17
3
+ metadata.gz: 1d17ebf6c761d5e15b35c4fe273dd0431b327c41456e36971c402763a5ddcdfa
4
+ data.tar.gz: c48ac30c8c9d53b022b8f06bfe5deaf8a16954f2fcdc91cfd3178fbd6804cc88
5
5
  SHA512:
6
- metadata.gz: 2e32a7671a4bebef2e1a9035a9be8692af5f8274e9b6e8ca8043421ed8adf620eb88878a8a55deca82d9b1c1fad725d6018230c5bd7a75a8bec727a39e8823f8
7
- data.tar.gz: e0b84d38f45eb30d9ac0a1940e26b70a83677f68bb399c206f6a599194ffddd77a9a77f18e27b2cad94de5004c67e7bf63f009f621ab9b9dabc2637fba85f1ad
6
+ metadata.gz: c262a0c7c00ebab56b9c302625b3de59ced3b140e362c96cfddc4fb7d150d0d6515eab600177cb18b068a526b4e5d72314508a0ceec7bd89422321c85289dfbb
7
+ data.tar.gz: 0b794f39c77676fcbab5c545a7b172c95fd37cd72048d9eb1d9e5de26efd70e971ab318b32fa428752e364e1f24c8afa2841b9d582aab59fc4ea66abb396123a
@@ -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
- 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.
@@ -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,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
@@ -1,3 +1,3 @@
1
1
  module SSHData
2
- VERSION = "1.2.0"
2
+ VERSION = "1.3.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,14 +1,14 @@
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: 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: 2021-12-02 00:00:00.000000000 Z
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: