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 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: