ruby-paseto 0.1.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.
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