ruby-paseto 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|