ruby-paseto 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +549 -0
  6. data/lib/paseto/asn1/algorithm_identifier.rb +17 -0
  7. data/lib/paseto/asn1/curve_private_key.rb +22 -0
  8. data/lib/paseto/asn1/ec_private_key.rb +27 -0
  9. data/lib/paseto/asn1/ecdsa_full_r.rb +26 -0
  10. data/lib/paseto/asn1/ecdsa_sig_value.rb +23 -0
  11. data/lib/paseto/asn1/ecdsa_signature.rb +49 -0
  12. data/lib/paseto/asn1/ed25519_identifier.rb +15 -0
  13. data/lib/paseto/asn1/named_curve.rb +17 -0
  14. data/lib/paseto/asn1/one_asymmetric_key.rb +32 -0
  15. data/lib/paseto/asn1/private_key.rb +17 -0
  16. data/lib/paseto/asn1/private_key_algorithm_identifier.rb +17 -0
  17. data/lib/paseto/asn1/public_key.rb +17 -0
  18. data/lib/paseto/asn1/subject_public_key_info.rb +28 -0
  19. data/lib/paseto/asn1.rb +101 -0
  20. data/lib/paseto/asymmetric_key.rb +100 -0
  21. data/lib/paseto/configuration/box.rb +23 -0
  22. data/lib/paseto/configuration/decode_configuration.rb +68 -0
  23. data/lib/paseto/configuration.rb +18 -0
  24. data/lib/paseto/interface/i_d.rb +23 -0
  25. data/lib/paseto/interface/key.rb +113 -0
  26. data/lib/paseto/interface/pbkd.rb +83 -0
  27. data/lib/paseto/interface/pie.rb +59 -0
  28. data/lib/paseto/interface/pke.rb +86 -0
  29. data/lib/paseto/interface/serializer.rb +19 -0
  30. data/lib/paseto/interface/version.rb +161 -0
  31. data/lib/paseto/interface/wrapper.rb +20 -0
  32. data/lib/paseto/operations/i_d.rb +48 -0
  33. data/lib/paseto/operations/id/i_dv3.rb +20 -0
  34. data/lib/paseto/operations/id/i_dv4.rb +20 -0
  35. data/lib/paseto/operations/pbkd/p_b_k_dv3.rb +85 -0
  36. data/lib/paseto/operations/pbkd/p_b_k_dv4.rb +94 -0
  37. data/lib/paseto/operations/pbkw.rb +73 -0
  38. data/lib/paseto/operations/pke/p_k_ev3.rb +97 -0
  39. data/lib/paseto/operations/pke/p_k_ev4.rb +95 -0
  40. data/lib/paseto/operations/pke.rb +57 -0
  41. data/lib/paseto/operations/wrap.rb +29 -0
  42. data/lib/paseto/paserk.rb +55 -0
  43. data/lib/paseto/paserk_types.rb +46 -0
  44. data/lib/paseto/protocol/version3.rb +100 -0
  45. data/lib/paseto/protocol/version4.rb +99 -0
  46. data/lib/paseto/result.rb +9 -0
  47. data/lib/paseto/serializer/optional_json.rb +30 -0
  48. data/lib/paseto/serializer/raw.rb +23 -0
  49. data/lib/paseto/sodium/curve_25519.rb +46 -0
  50. data/lib/paseto/sodium/safe_ed25519_loader.rb +19 -0
  51. data/lib/paseto/sodium/stream/base.rb +82 -0
  52. data/lib/paseto/sodium/stream/x_cha_cha20_xor.rb +31 -0
  53. data/lib/paseto/sodium.rb +5 -0
  54. data/lib/paseto/symmetric_key.rb +119 -0
  55. data/lib/paseto/token.rb +127 -0
  56. data/lib/paseto/token_types.rb +29 -0
  57. data/lib/paseto/util.rb +105 -0
  58. data/lib/paseto/v3/local.rb +63 -0
  59. data/lib/paseto/v3/public.rb +204 -0
  60. data/lib/paseto/v4/local.rb +56 -0
  61. data/lib/paseto/v4/public.rb +169 -0
  62. data/lib/paseto/validator.rb +154 -0
  63. data/lib/paseto/verifiers/footer.rb +30 -0
  64. data/lib/paseto/verifiers/payload.rb +42 -0
  65. data/lib/paseto/verify.rb +48 -0
  66. data/lib/paseto/version.rb +6 -0
  67. data/lib/paseto/versions.rb +25 -0
  68. data/lib/paseto/wrappers/pie/pie_v3.rb +72 -0
  69. data/lib/paseto/wrappers/pie/pie_v4.rb +72 -0
  70. data/lib/paseto/wrappers/pie.rb +71 -0
  71. data/lib/paseto.rb +99 -0
  72. data/paseto.gemspec +58 -0
  73. data/sorbet/config +3 -0
  74. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  75. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  76. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
  77. data/sorbet/rbi/gems/docile@1.4.0.rbi +376 -0
  78. data/sorbet/rbi/gems/ffi@1.15.5.rbi +1994 -0
  79. data/sorbet/rbi/gems/io-console@0.5.11.rbi +8 -0
  80. data/sorbet/rbi/gems/irb@1.5.1.rbi +342 -0
  81. data/sorbet/rbi/gems/json@2.6.3.rbi +1541 -0
  82. data/sorbet/rbi/gems/multi_json@1.15.0.rbi +267 -0
  83. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  84. data/sorbet/rbi/gems/oj@3.13.23.rbi +603 -0
  85. data/sorbet/rbi/gems/openssl@3.0.1.rbi +1735 -0
  86. data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
  87. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +407 -0
  88. data/sorbet/rbi/gems/rake@13.0.6.rbi +3021 -0
  89. data/sorbet/rbi/gems/rbnacl@7.1.1.rbi +3218 -0
  90. data/sorbet/rbi/gems/regexp_parser@2.6.1.rbi +3481 -0
  91. data/sorbet/rbi/gems/reline@0.3.1.rbi +8 -0
  92. data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
  93. data/sorbet/rbi/gems/rspec-core@3.12.0.rbi +10887 -0
  94. data/sorbet/rbi/gems/rspec-expectations@3.12.0.rbi +8090 -0
  95. data/sorbet/rbi/gems/rspec-mocks@3.12.0.rbi +5300 -0
  96. data/sorbet/rbi/gems/rspec-support@3.12.0.rbi +1617 -0
  97. data/sorbet/rbi/gems/rspec@3.12.0.rbi +88 -0
  98. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1239 -0
  99. data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +219 -0
  100. data/sorbet/rbi/gems/simplecov@0.21.2.rbi +2135 -0
  101. data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +8 -0
  102. data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
  103. data/sorbet/rbi/gems/timecop@0.9.6.rbi +350 -0
  104. data/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi +48 -0
  105. data/sorbet/rbi/gems/webrick@1.7.0.rbi +2555 -0
  106. data/sorbet/rbi/gems/yard-sorbet@0.7.0.rbi +391 -0
  107. data/sorbet/rbi/gems/yard@0.9.28.rbi +17816 -0
  108. data/sorbet/rbi/gems/zeitwerk@2.6.6.rbi +950 -0
  109. data/sorbet/rbi/shims/multi_json.rbi +19 -0
  110. data/sorbet/rbi/shims/openssl.rbi +111 -0
  111. data/sorbet/rbi/shims/rbnacl.rbi +65 -0
  112. data/sorbet/rbi/shims/zeitwerk.rbi +6 -0
  113. data/sorbet/rbi/todo.rbi +7 -0
  114. data/sorbet/tapioca/config.yml +30 -0
  115. data/sorbet/tapioca/require.rb +12 -0
  116. metadata +376 -0
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Paseto
5
+ class TokenTypes < T::Enum
6
+ extend T::Sig
7
+
8
+ enums do
9
+ V3Local = new('v3.local')
10
+ V3Public = new('v3.public')
11
+ V4Local = new('v4.local')
12
+ V4Public = new('v4.public')
13
+ end
14
+
15
+ sig { returns(T.nilable(T.class_of(Interface::Key))) }
16
+ def key_klass
17
+ case self
18
+ in V3Local then V3::Local
19
+ in V3Public then V3::Public
20
+ in V4Local if Paseto.rbnacl?
21
+ V4::Local
22
+ in V4Public if Paseto.rbnacl?
23
+ V4::Public
24
+ else
25
+ nil
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,105 @@
1
+ # encoding: binary
2
+ # typed: strict
3
+ # frozen_string_literal: true
4
+
5
+ module Paseto
6
+ module Util
7
+ extend T::Sig
8
+
9
+ sig { params(str: String).returns(String) }
10
+ def self.encode64(str)
11
+ Base64.urlsafe_encode64(str, padding: false)
12
+ end
13
+
14
+ sig { params(str: String).returns(String) }
15
+ def self.decode64(str)
16
+ # Ruby's Base64 library does not care about whether or not padding is present,
17
+ # but the PASETO test vectors do.
18
+ return '' if str.include?('=')
19
+
20
+ Base64.urlsafe_decode64(str).b
21
+ rescue ArgumentError
22
+ ''
23
+ end
24
+
25
+ sig { params(str: String).returns(String) }
26
+ def self.decode_hex(str)
27
+ [str].pack('H*')
28
+ end
29
+
30
+ sig { params(num: Integer).returns(String) }
31
+ def self.le64(num)
32
+ raise ArgumentError, 'num too large' if num.bit_length > 64
33
+ raise ArgumentError, 'num must not be signed' if num.negative?
34
+
35
+ [num].pack('Q<')
36
+ end
37
+
38
+ sig { params(num: Integer).returns(String) }
39
+ def self.int_to_be32(num)
40
+ raise ArgumentError, 'num too large' if num.bit_length > 32
41
+ raise ArgumentError, 'num must not be signed' if num.negative?
42
+
43
+ [num].pack('N')
44
+ end
45
+
46
+ sig { params(num: Integer).returns(String) }
47
+ def self.int_to_be64(num)
48
+ raise ArgumentError, 'num too large' if num.bit_length > 64
49
+ raise ArgumentError, 'num must not be signed' if num.negative?
50
+
51
+ [num].pack('Q>')
52
+ end
53
+
54
+ sig { params(val: String).returns(Integer) }
55
+ def self.be64_to_int(val)
56
+ raise ArgumentError, 'input size incorrect' unless val.bytesize == 8
57
+
58
+ val.unpack1('Q>')
59
+ end
60
+
61
+ sig { params(val: String).returns(Integer) }
62
+ def self.be32_to_int(val)
63
+ raise ArgumentError, 'input size incorrect' unless val.bytesize == 4
64
+
65
+ val.unpack1('N')
66
+ end
67
+
68
+ sig { params(parts: String).returns(String) }
69
+ def self.pre_auth_encode(*parts)
70
+ parts.inject(le64(parts.size)) do |memo, part|
71
+ "#{memo}#{le64(part.bytesize)}#{part}"
72
+ end
73
+ end
74
+
75
+ # rubocop:disable Naming/MethodParameterName, Style/IdenticalConditionalBranches
76
+ # Moving the sig out of the conditional triggers a bug in rubocop-sorbet
77
+
78
+ # Use a faster comparison when RbNaCl is available
79
+ if Paseto.rbnacl?
80
+ sig { params(a: String, b: String).returns(T::Boolean) }
81
+ def self.constant_compare(a, b)
82
+ h_a = RbNaCl::Hash.blake2b(a)
83
+ h_b = RbNaCl::Hash.blake2b(b)
84
+ RbNaCl::Util.verify64(h_a, h_b)
85
+ end
86
+ else
87
+ sig { params(a: String, b: String).returns(T::Boolean) }
88
+ def self.constant_compare(a, b)
89
+ OpenSSL.secure_compare(a, b)
90
+ end
91
+ end
92
+
93
+ # rubocop:enable Naming/MethodParameterName, Style/IdenticalConditionalBranches
94
+
95
+ # Check if the libcrypto version that's running is actually openssl, and that the version
96
+ # is at least the provided major/minor/fix/patch level.
97
+ sig { params(major: Integer, minor: Integer, fix: Integer, patch: Integer).returns(T::Boolean) }
98
+ def self.openssl?(major, minor = 0, fix = 0, patch = 0)
99
+ return false if OpenSSL::OPENSSL_VERSION.include?('LibreSSL')
100
+
101
+ OpenSSL::OPENSSL_VERSION_NUMBER >=
102
+ (major * 0x10000000) + (minor * 0x100000) + (fix * 0x1000) + (patch * 0x10)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,63 @@
1
+ # encoding: binary
2
+ # typed: strict
3
+ # frozen_string_literal: true
4
+
5
+ module Paseto
6
+ module V3
7
+ # PASETOv3 `local` token interface providing symmetric encryption of tokens.
8
+ class Local < SymmetricKey
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ # Size in bytes of a SHA384 digest
13
+ SHA384_DIGEST_LEN = 48
14
+
15
+ # String initialized to \x00 for use in key derivation
16
+ NULL_SALT = T.let(0.chr * SHA384_DIGEST_LEN, String)
17
+
18
+ final!
19
+
20
+ sig(:final) { override.returns(Protocol::Version3) }
21
+ attr_reader :protocol
22
+
23
+ # Create a new Local instance with a randomly generated key.
24
+ sig(:final) { returns(T.attached_class) }
25
+ def self.generate
26
+ new(ikm: SecureRandom.random_bytes(32))
27
+ end
28
+
29
+ sig(:final) { params(ikm: String).void }
30
+ def initialize(ikm:)
31
+ @protocol = T.let(Protocol::Version3.new, Paseto::Protocol::Version3)
32
+
33
+ super(ikm)
34
+ end
35
+
36
+ private
37
+
38
+ # Derive an encryption key, nonce, and authentication key from an input nonce.
39
+ sig(:final) { override.params(nonce: String).returns([String, String, String]) }
40
+ def calc_keys(nonce)
41
+ tmp = OpenSSL::KDF.hkdf(key, info: "paseto-encryption-key#{nonce}", salt: NULL_SALT, length: 48, hash: 'SHA384')
42
+ ek = T.must(tmp[0, 32])
43
+ n2 = T.must(tmp[-16, 16])
44
+ ak = OpenSSL::KDF.hkdf(key, info: "paseto-auth-key-for-aead#{nonce}", salt: NULL_SALT, length: 48, hash: 'SHA384')
45
+ [ek, n2, ak]
46
+ end
47
+
48
+ # Split `payload` into the following parts:
49
+ # - nonce, 32 leftmost bytes
50
+ # - tag, 48 rightmost bytes
51
+ # - ciphertext, everything in between
52
+ sig(:final) { override.params(payload: String).returns([String, String, String]) }
53
+ def split_payload(payload)
54
+ n = T.must(payload.slice(0, 32))
55
+ c = T.must(payload.slice(32, payload.size - 80))
56
+ t = T.must(payload.slice(-48, 48))
57
+ [n, c, t]
58
+ rescue TypeError
59
+ raise ParseError
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,204 @@
1
+ # encoding: binary
2
+ # typed: strict
3
+ # frozen_string_literal: true
4
+
5
+ module Paseto
6
+ module V3
7
+ # PASETOv3 `public` token interface providing asymmetric signature signing and verification of tokens.
8
+ class Public < AsymmetricKey
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ final!
13
+
14
+ # Size of (r || s) in an ECDSA secp384r1 signature
15
+ SIGNATURE_BYTE_LEN = 96
16
+
17
+ # Size of r | s in an ECDSA secp384r1 signature
18
+ SIGNATURE_PART_LEN = T.let(SIGNATURE_BYTE_LEN / 2, Integer)
19
+
20
+ sig(:final) { override.returns(Protocol::Version3) }
21
+ attr_reader :protocol
22
+
23
+ # Create a new Public instance with a brand new EC key.
24
+ sig(:final) { returns(T.attached_class) }
25
+ def self.generate
26
+ OpenSSL::PKey::EC.generate('secp384r1')
27
+ .then(&:to_der)
28
+ .then { |der| new(der) }
29
+ end
30
+
31
+ sig(:final) { params(bytes: String).returns(T.attached_class) }
32
+ def self.from_public_bytes(bytes)
33
+ ASN1.p384_public_bytes_to_spki_der(bytes)
34
+ .then { |der| new(der) }
35
+ end
36
+
37
+ sig(:final) { params(bytes: String).returns(T.attached_class) }
38
+ def self.from_scalar_bytes(bytes)
39
+ ASN1.p384_scalar_bytes_to_oak_der(bytes)
40
+ .then { |der| new(der) }
41
+ end
42
+
43
+ # `key` must be either a DER or PEM encoded secp384r1 key.
44
+ # Encrypted PEM inputs are not supported.
45
+ sig(:final) { params(key: String).void }
46
+ def initialize(key)
47
+ @key = T.let(OpenSSL::PKey::EC.new(key), OpenSSL::PKey::EC)
48
+ @private = T.let(@key.private?, T::Boolean)
49
+
50
+ raise LucidityError unless @key.group.curve_name == 'secp384r1'
51
+ raise InvalidKeyPair unless custom_check_key
52
+
53
+ @protocol = T.let(Protocol::Version3.new, Protocol::Version3)
54
+
55
+ super
56
+ rescue OpenSSL::PKey::ECError => e
57
+ raise CryptoError, e.message
58
+ end
59
+
60
+ # Sign `message` and optional non-empty `footer` and return a Token.
61
+ # The resulting token may be bound to a particular use by passing a non-empty `implicit_assertion`.
62
+ sig(:final) { override.params(message: String, footer: String, implicit_assertion: String).returns(Token) }
63
+ def sign(message:, footer: '', implicit_assertion: '')
64
+ raise ArgumentError, 'no private key available' unless private?
65
+
66
+ Util.pre_auth_encode(public_bytes, pae_header, message, footer, implicit_assertion)
67
+ .then { |m2| protocol.digest(m2) }
68
+ .then { |data| @key.sign_raw(nil, data) }
69
+ .then { |sig_asn| ASN1::ECDSASignature.from_asn1(sig_asn) }
70
+ .then { |ecdsa_sig| ecdsa_sig.to_rs(SIGNATURE_PART_LEN) }
71
+ .then { |sig| Token.new(payload: "#{message}#{sig}", purpose: purpose, version: version, footer: footer) }
72
+ rescue Encoding::CompatibilityError
73
+ raise ParseError, 'invalid message encoding, must be UTF-8'
74
+ end
75
+
76
+ # Verify the signature of `token`, with an optional binding `implicit_assertion`. `token` must be a `v3.public` type Token.
77
+ # Returns the verified payload if successful, otherwise raises an exception.
78
+ sig(:final) { override.params(token: Token, implicit_assertion: String).returns(String) }
79
+ def verify(token:, implicit_assertion: '') # rubocop:disable Metrics/AbcSize
80
+ raise LucidityError unless header == token.header
81
+
82
+ payload = token.raw_payload
83
+ raise ParseError, 'message too short' if payload.bytesize < SIGNATURE_BYTE_LEN
84
+
85
+ m = T.must(payload.slice(0, payload.bytesize - SIGNATURE_BYTE_LEN))
86
+
87
+ s = T.must(payload.slice(-SIGNATURE_BYTE_LEN, SIGNATURE_BYTE_LEN))
88
+ .then { |bytes| ASN1::ECDSASignature.from_rs(bytes, SIGNATURE_PART_LEN).to_der }
89
+
90
+ Util.pre_auth_encode(public_bytes, pae_header, m, token.raw_footer, implicit_assertion)
91
+ .then { |m2| protocol.digest(m2) }
92
+ .then { |data| @key.verify_raw(nil, s, data) }
93
+ .then { |valid| raise InvalidSignature unless valid }
94
+ .then { m.encode(Encoding::UTF_8) }
95
+ rescue Encoding::UndefinedConversionError
96
+ raise ParseError, 'invalid payload encoding'
97
+ end
98
+
99
+ sig(:final) { override.returns(String) }
100
+ def public_to_pem = @key.public_to_pem
101
+
102
+ sig(:final) { override.returns(String) }
103
+ def private_to_pem
104
+ raise ArgumentError, 'no private key available' unless private?
105
+
106
+ @key.to_pem
107
+ end
108
+
109
+ sig(:final) { override.returns(String) }
110
+ def to_bytes
111
+ raise ArgumentError, 'no private key available' unless private?
112
+
113
+ @key.private_key.to_s(2).rjust(48, "\x00")
114
+ end
115
+
116
+ sig(:final) { override.returns(T::Boolean) }
117
+ def private? = @private
118
+
119
+ sig(:final) { override.returns(String) }
120
+ def public_bytes = @key.public_key.to_octet_string(:compressed)
121
+
122
+ sig(:final) { override.params(other: T.any(OpenSSL::PKey::EC, OpenSSL::PKey::EC::Point)).returns(String) }
123
+ def ecdh(other)
124
+ case other
125
+ when OpenSSL::PKey::EC::Point
126
+ @key.dh_compute_key(other)
127
+ when OpenSSL::PKey::EC
128
+ other.dh_compute_key(@key.public_key)
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ # TODO: Figure out how to get SimpleCov to cover this consistently. With OSSL1.1.1, most of
135
+ # this doesn't run. With OSSL3, check_key never raises...
136
+ # :nocov:
137
+
138
+ # The openssl gem as of 3.0.0 will prefer EVP_PKEY_public_check over EC_KEY_check_key
139
+ # whenever the EVP api is available, which is always for the library here as we're requiring
140
+ # 3.0.0 or greater. However, this has some problems.
141
+ #
142
+ # The behavior of EVP_PKEY_public_check is different between 1.1.1 and 3.x. Specifically,
143
+ # it no longer calls the custom verifier method in EVP_PKEY_METHOD, and only checks the
144
+ # correctness of the public component. This leads to a problem when calling EC#key_check,
145
+ # as the private component is NEVER verified for an ECDSA key through the APIs that the gem
146
+ # makes available to us.
147
+ #
148
+ # Until this is fixed in ruby/openssl, I am working around this by implementing the algorithm
149
+ # used by EVP_PKEY_pairwise_check through the OpenSSL API.
150
+ #
151
+ # BUG: https://github.com/ruby/openssl/issues/563
152
+ # https://www.openssl.org/docs/man1.1.1/man3/EVP_PKEY_public_check.html
153
+ # https://www.openssl.org/docs/man3.0/man3/EVP_PKEY_public_check.html
154
+ sig(:final) { returns(T::Boolean) }
155
+ def custom_check_key
156
+ begin
157
+ @key.check_key
158
+ rescue StandardError
159
+ return false
160
+ end
161
+
162
+ return true unless private? && Util.openssl?(3)
163
+
164
+ priv_key = @key.private_key
165
+ group = @key.group
166
+
167
+ # int ossl_ec_key_private_check(const EC_KEY *eckey)
168
+ # {
169
+ # ...
170
+ # if (BN_cmp(eckey->priv_key, BN_value_one()) < 0
171
+ # || BN_cmp(eckey->priv_key, eckey->group->order) >= 0) {
172
+ # ERR_raise(ERR_LIB_EC, EC_R_INVALID_PRIVATE_KEY);
173
+ # return 0;
174
+ # }
175
+ # ...
176
+ # }
177
+ #
178
+ # https://github.com/openssl/openssl/blob/5ac7cfb56211d18596e3c35baa942542f3c0189a/crypto/ec/ec_key.c#L510
179
+ # private keys must be in range [1, order-1]
180
+ return false if priv_key < OpenSSL::BN.new(1) || priv_key > group.order
181
+
182
+ # int ossl_ec_key_pairwise_check(const EC_KEY *eckey, BN_CTX *ctx)
183
+ # {
184
+ # ...
185
+ # if (!EC_POINT_mul(eckey->group, point, eckey->priv_key, NULL, NULL, ctx)) {
186
+ # ERR_raise(ERR_LIB_EC, ERR_R_EC_LIB);
187
+ # goto err;
188
+ # }
189
+ # if (EC_POINT_cmp(eckey->group, point, eckey->pub_key, ctx) != 0) {
190
+ # ERR_raise(ERR_LIB_EC, EC_R_INVALID_PRIVATE_KEY);
191
+ # goto err;
192
+ # }
193
+ # ...
194
+ # }
195
+ #
196
+ # https://github.com/openssl/openssl/blob/5ac7cfb56211d18596e3c35baa942542f3c0189a/crypto/ec/ec_key.c#L529
197
+ # Check generator * priv_key = pub_key
198
+ @key.public_key == group.generator.mul(priv_key)
199
+ end
200
+
201
+ # :nocov:
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: binary
2
+ # typed: strict
3
+ # frozen_string_literal: true
4
+
5
+ module Paseto
6
+ module V4
7
+ # PASETOv4 `local` token interface providing symmetric encryption of tokens.
8
+ class Local < SymmetricKey
9
+ extend T::Sig
10
+
11
+ final!
12
+
13
+ sig(:final) { override.returns(Protocol::Version4) }
14
+ attr_reader :protocol
15
+
16
+ # Create a new Local instance with a randomly generated key.
17
+ sig(:final) { returns(T.attached_class) }
18
+ def self.generate
19
+ new(ikm: RbNaCl::Random.random_bytes(32))
20
+ end
21
+
22
+ sig(:final) { params(ikm: String).void }
23
+ def initialize(ikm:)
24
+ @protocol = T.let(Protocol::Version4.new, Paseto::Protocol::Version4)
25
+
26
+ super(ikm)
27
+ end
28
+
29
+ private
30
+
31
+ # Derive an encryption key, nonce, and authentication key from an input nonce.
32
+ sig(:final) { override.params(nonce: String).returns([String, String, String]) }
33
+ def calc_keys(nonce)
34
+ tmp = protocol.hmac("paseto-encryption-key#{nonce}", key: key, digest_size: 56)
35
+ ek = T.must(tmp[0, 32])
36
+ n2 = T.must(tmp[-24, 24])
37
+ ak = protocol.hmac("paseto-auth-key-for-aead#{nonce}", key: key, digest_size: 32)
38
+ [ek, n2, ak]
39
+ end
40
+
41
+ # Separate a token payload into:
42
+ # - nonce, 32 leftmost bytes
43
+ # - tag, 32 rightmost bytes
44
+ # - ciphertext, everything in between
45
+ sig(:final) { override.params(payload: String).returns([String, String, String]) }
46
+ def split_payload(payload)
47
+ n = T.must(payload.slice(0, 32))
48
+ c = T.must(payload.slice(32, payload.size - 64))
49
+ t = T.must(payload.slice(-32, 32))
50
+ [n, c, t]
51
+ rescue TypeError
52
+ raise ParseError
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,169 @@
1
+ # encoding: binary
2
+ # typed: strict
3
+ # frozen_string_literal: true
4
+
5
+ module Paseto
6
+ module V4
7
+ # PASETOv4 `public` token interface providing asymmetric signature signing and verification of tokens.
8
+ class Public < AsymmetricKey
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ final!
13
+
14
+ # Number of bytes in an Ed25519 signature
15
+ SIGNATURE_BYTES = 64
16
+
17
+ sig(:final) { returns(T.any(RbNaCl::SigningKey, RbNaCl::VerifyKey)) }
18
+ attr_reader :key
19
+
20
+ sig(:final) { override.returns(Protocol::Version4) }
21
+ attr_reader :protocol
22
+
23
+ # Create a new Public instance with a brand new Ed25519 key.
24
+ sig(:final) { returns(T.attached_class) }
25
+ def self.generate
26
+ new(RbNaCl::SigningKey.generate)
27
+ end
28
+
29
+ sig(:final) { params(keypair: String).returns(T.attached_class) }
30
+ def self.from_keypair(keypair)
31
+ new(Sodium::SafeEd25519Loader.from_keypair(keypair))
32
+ end
33
+
34
+ sig(:final) { params(bytes: String).returns(T.attached_class) }
35
+ def self.from_public_bytes(bytes)
36
+ new(RbNaCl::VerifyKey.new(bytes))
37
+ end
38
+
39
+ # If `key` is a String, it must be a PEM- or DER- encoded ED25519 key.
40
+ sig(:final) { params(key: T.any(String, RbNaCl::SigningKey, RbNaCl::VerifyKey)).void }
41
+ def initialize(key)
42
+ key = ed25519_pkey_ossl_to_nacl(key) if key.is_a?(String)
43
+
44
+ @key = T.let(key, T.any(RbNaCl::SigningKey, RbNaCl::VerifyKey))
45
+
46
+ @private = T.let(@key.is_a?(RbNaCl::SigningKey), T::Boolean)
47
+ @protocol = T.let(Protocol::Version4.new, Paseto::Protocol::Version4)
48
+
49
+ super
50
+ end
51
+
52
+ # Sign `message` and optional non-empty `footer` and return a Token.
53
+ # The resulting token may be bound to a particular use by passing a non-empty `implicit_assertion`.
54
+ sig(:final) { override.params(message: String, footer: String, implicit_assertion: String).returns(Token) }
55
+ def sign(message:, footer: '', implicit_assertion: '')
56
+ raise ArgumentError, 'no private key available' unless @key.is_a?(RbNaCl::SigningKey)
57
+
58
+ Util.pre_auth_encode(pae_header, message, footer, implicit_assertion)
59
+ .then { |m2| @key.sign(m2) }
60
+ .then { |sig| "#{message}#{sig}" }
61
+ .then { |payload| Token.new(payload: payload, purpose: purpose, version: version, footer: footer) }
62
+ end
63
+
64
+ # Verify the signature of `token`, with an optional binding `implicit_assertion`. `token` must be a `v4.public`` type Token.
65
+ # Returns the verified payload if successful, otherwise raises an exception.
66
+ sig(:final) { override.params(token: Token, implicit_assertion: String).returns(String) }
67
+ def verify(token:, implicit_assertion: '') # rubocop:disable Metrics/AbcSize
68
+ raise LucidityError unless header == token.header
69
+
70
+ payload = token.raw_payload
71
+ raise ParseError, 'message too short' if payload.bytesize < SIGNATURE_BYTES
72
+
73
+ m = T.must(payload.slice(0, payload.size - SIGNATURE_BYTES))
74
+ s = T.must(payload.slice(-SIGNATURE_BYTES, SIGNATURE_BYTES))
75
+
76
+ Util.pre_auth_encode(pae_header, m, token.raw_footer, implicit_assertion)
77
+ .then { |m2| public_key.verify(s, m2) }
78
+ .then { m.encode(Encoding::UTF_8) }
79
+ rescue RbNaCl::BadSignatureError
80
+ raise InvalidSignature
81
+ rescue Encoding::UndefinedConversionError
82
+ raise ParseError, 'invalid payload encoding'
83
+ end
84
+
85
+ sig(:final) { override.returns(String) }
86
+ def public_to_pem
87
+ ASN1.ed25519_pubkey_nacl_to_pem(public_bytes)
88
+ end
89
+
90
+ sig(:final) { override.returns(String) }
91
+ def private_to_pem
92
+ raise ArgumentError, 'no private key available' unless @key.is_a? RbNaCl::SigningKey
93
+
94
+ ASN1.ed25519_rs_to_oak_pem(@key.keypair_bytes)
95
+ end
96
+
97
+ sig(:final) { override.returns(String) }
98
+ def to_bytes
99
+ raise ArgumentError, 'no private key available' unless @key.is_a? RbNaCl::SigningKey
100
+
101
+ @key.keypair_bytes
102
+ end
103
+
104
+ sig(:final) { override.returns(T::Boolean) }
105
+ def private? = @private
106
+
107
+ sig(:final) { override.returns(String) }
108
+ def public_bytes
109
+ public_key.to_bytes
110
+ end
111
+
112
+ sig(:final) { override.params(other: T.any(RbNaCl::PrivateKey, RbNaCl::PublicKey)).returns(String) }
113
+ def ecdh(other)
114
+ case other
115
+ when RbNaCl::PrivateKey
116
+ RbNaCl::GroupElement.new(x25519_public_key).mult(other).to_bytes
117
+ when RbNaCl::PublicKey
118
+ RbNaCl::GroupElement.new(other).mult(x25519_private_key).to_bytes
119
+ end
120
+ end
121
+
122
+ sig(:final) { returns(RbNaCl::PrivateKey) }
123
+ def x25519_private_key
124
+ Sodium::Curve25519.new(self).to_x25519_private_key
125
+ end
126
+
127
+ sig(:final) { returns(RbNaCl::PublicKey) }
128
+ def x25519_public_key
129
+ Sodium::Curve25519.new(self).to_x25519_public_key
130
+ end
131
+
132
+ private
133
+
134
+ # Convert a PEM- or DER- encoded ED25519 key into either a `RbNaCl::VerifyKey`` or `RbNaCl::SigningKey`
135
+ sig(:final) { params(pem_or_der: String).returns(T.any(RbNaCl::VerifyKey, RbNaCl::SigningKey)) }
136
+ def ed25519_pkey_ossl_to_nacl(pem_or_der)
137
+ key = OpenSSL::PKey.read(pem_or_der)
138
+
139
+ if ossl_ed25519_private_key?(key)
140
+ bytes = OpenSSL::ASN1.decode(key.private_to_der).value[2].value[2..]
141
+ RbNaCl::SigningKey.new(bytes)
142
+ else
143
+ bytes = OpenSSL::ASN1.decode(key.public_to_der).value[1].value
144
+ RbNaCl::VerifyKey.new(bytes)
145
+ end
146
+ rescue OpenSSL::PKey::PKeyError => e
147
+ raise ParseError, e.message
148
+ end
149
+
150
+ # ruby/openssl doesn't give us any API to detect if a PKey has a private component
151
+ sig(:final) { params(key: OpenSSL::PKey::PKey).returns(T::Boolean) }
152
+ def ossl_ed25519_private_key?(key)
153
+ raise LucidityError, "expected Ed25519 key, got #{key.oid}" unless key.oid == 'ED25519'
154
+
155
+ return key.to_text.start_with?('ED25519 Private-Key') if Util.openssl?(3)
156
+ return key.to_text != "<INVALID PRIVATE KEY>\n" if Util.openssl?(1, 1, 1)
157
+
158
+ false
159
+ end
160
+
161
+ sig(:final) { returns(RbNaCl::VerifyKey) }
162
+ def public_key
163
+ return @key.verify_key if @key.is_a?(RbNaCl::SigningKey)
164
+
165
+ @key
166
+ end
167
+ end
168
+ end
169
+ end