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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +8 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +549 -0
- data/lib/paseto/asn1/algorithm_identifier.rb +17 -0
- data/lib/paseto/asn1/curve_private_key.rb +22 -0
- data/lib/paseto/asn1/ec_private_key.rb +27 -0
- data/lib/paseto/asn1/ecdsa_full_r.rb +26 -0
- data/lib/paseto/asn1/ecdsa_sig_value.rb +23 -0
- data/lib/paseto/asn1/ecdsa_signature.rb +49 -0
- data/lib/paseto/asn1/ed25519_identifier.rb +15 -0
- data/lib/paseto/asn1/named_curve.rb +17 -0
- data/lib/paseto/asn1/one_asymmetric_key.rb +32 -0
- data/lib/paseto/asn1/private_key.rb +17 -0
- data/lib/paseto/asn1/private_key_algorithm_identifier.rb +17 -0
- data/lib/paseto/asn1/public_key.rb +17 -0
- data/lib/paseto/asn1/subject_public_key_info.rb +28 -0
- data/lib/paseto/asn1.rb +101 -0
- data/lib/paseto/asymmetric_key.rb +100 -0
- data/lib/paseto/configuration/box.rb +23 -0
- data/lib/paseto/configuration/decode_configuration.rb +68 -0
- data/lib/paseto/configuration.rb +18 -0
- data/lib/paseto/interface/i_d.rb +23 -0
- data/lib/paseto/interface/key.rb +113 -0
- data/lib/paseto/interface/pbkd.rb +83 -0
- data/lib/paseto/interface/pie.rb +59 -0
- data/lib/paseto/interface/pke.rb +86 -0
- data/lib/paseto/interface/serializer.rb +19 -0
- data/lib/paseto/interface/version.rb +161 -0
- data/lib/paseto/interface/wrapper.rb +20 -0
- data/lib/paseto/operations/i_d.rb +48 -0
- data/lib/paseto/operations/id/i_dv3.rb +20 -0
- data/lib/paseto/operations/id/i_dv4.rb +20 -0
- data/lib/paseto/operations/pbkd/p_b_k_dv3.rb +85 -0
- data/lib/paseto/operations/pbkd/p_b_k_dv4.rb +94 -0
- data/lib/paseto/operations/pbkw.rb +73 -0
- data/lib/paseto/operations/pke/p_k_ev3.rb +97 -0
- data/lib/paseto/operations/pke/p_k_ev4.rb +95 -0
- data/lib/paseto/operations/pke.rb +57 -0
- data/lib/paseto/operations/wrap.rb +29 -0
- data/lib/paseto/paserk.rb +55 -0
- data/lib/paseto/paserk_types.rb +46 -0
- data/lib/paseto/protocol/version3.rb +100 -0
- data/lib/paseto/protocol/version4.rb +99 -0
- data/lib/paseto/result.rb +9 -0
- data/lib/paseto/serializer/optional_json.rb +30 -0
- data/lib/paseto/serializer/raw.rb +23 -0
- data/lib/paseto/sodium/curve_25519.rb +46 -0
- data/lib/paseto/sodium/safe_ed25519_loader.rb +19 -0
- data/lib/paseto/sodium/stream/base.rb +82 -0
- data/lib/paseto/sodium/stream/x_cha_cha20_xor.rb +31 -0
- data/lib/paseto/sodium.rb +5 -0
- data/lib/paseto/symmetric_key.rb +119 -0
- data/lib/paseto/token.rb +127 -0
- data/lib/paseto/token_types.rb +29 -0
- data/lib/paseto/util.rb +105 -0
- data/lib/paseto/v3/local.rb +63 -0
- data/lib/paseto/v3/public.rb +204 -0
- data/lib/paseto/v4/local.rb +56 -0
- data/lib/paseto/v4/public.rb +169 -0
- data/lib/paseto/validator.rb +154 -0
- data/lib/paseto/verifiers/footer.rb +30 -0
- data/lib/paseto/verifiers/payload.rb +42 -0
- data/lib/paseto/verify.rb +48 -0
- data/lib/paseto/version.rb +6 -0
- data/lib/paseto/versions.rb +25 -0
- data/lib/paseto/wrappers/pie/pie_v3.rb +72 -0
- data/lib/paseto/wrappers/pie/pie_v4.rb +72 -0
- data/lib/paseto/wrappers/pie.rb +71 -0
- data/lib/paseto.rb +99 -0
- data/paseto.gemspec +58 -0
- data/sorbet/config +3 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
- data/sorbet/rbi/gems/docile@1.4.0.rbi +376 -0
- data/sorbet/rbi/gems/ffi@1.15.5.rbi +1994 -0
- data/sorbet/rbi/gems/io-console@0.5.11.rbi +8 -0
- data/sorbet/rbi/gems/irb@1.5.1.rbi +342 -0
- data/sorbet/rbi/gems/json@2.6.3.rbi +1541 -0
- data/sorbet/rbi/gems/multi_json@1.15.0.rbi +267 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
- data/sorbet/rbi/gems/oj@3.13.23.rbi +603 -0
- data/sorbet/rbi/gems/openssl@3.0.1.rbi +1735 -0
- data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +407 -0
- data/sorbet/rbi/gems/rake@13.0.6.rbi +3021 -0
- data/sorbet/rbi/gems/rbnacl@7.1.1.rbi +3218 -0
- data/sorbet/rbi/gems/regexp_parser@2.6.1.rbi +3481 -0
- data/sorbet/rbi/gems/reline@0.3.1.rbi +8 -0
- data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
- data/sorbet/rbi/gems/rspec-core@3.12.0.rbi +10887 -0
- data/sorbet/rbi/gems/rspec-expectations@3.12.0.rbi +8090 -0
- data/sorbet/rbi/gems/rspec-mocks@3.12.0.rbi +5300 -0
- data/sorbet/rbi/gems/rspec-support@3.12.0.rbi +1617 -0
- data/sorbet/rbi/gems/rspec@3.12.0.rbi +88 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1239 -0
- data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +219 -0
- data/sorbet/rbi/gems/simplecov@0.21.2.rbi +2135 -0
- data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +8 -0
- data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
- data/sorbet/rbi/gems/timecop@0.9.6.rbi +350 -0
- data/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi +48 -0
- data/sorbet/rbi/gems/webrick@1.7.0.rbi +2555 -0
- data/sorbet/rbi/gems/yard-sorbet@0.7.0.rbi +391 -0
- data/sorbet/rbi/gems/yard@0.9.28.rbi +17816 -0
- data/sorbet/rbi/gems/zeitwerk@2.6.6.rbi +950 -0
- data/sorbet/rbi/shims/multi_json.rbi +19 -0
- data/sorbet/rbi/shims/openssl.rbi +111 -0
- data/sorbet/rbi/shims/rbnacl.rbi +65 -0
- data/sorbet/rbi/shims/zeitwerk.rbi +6 -0
- data/sorbet/rbi/todo.rbi +7 -0
- data/sorbet/tapioca/config.yml +30 -0
- data/sorbet/tapioca/require.rb +12 -0
- 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
|
data/lib/paseto/util.rb
ADDED
|
@@ -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
|