truelayer-signing 0.1.2 → 0.2.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +12 -2
- data/examples/webhook-server/Gemfile +1 -0
- data/examples/webhook-server/Gemfile.lock +28 -3
- data/examples/webhook-server/main.rb +15 -14
- data/lib/truelayer-signing/verifier.rb +72 -20
- data/lib/truelayer-signing.rb +9 -1
- data/test/resources/missing-zero-padding-test-jwks.json +19 -0
- data/test/resources/missing-zero-padding-test-payload.json +22 -0
- data/test/resources/missing-zero-padding-test-signature.txt +1 -0
- data/test/test-truelayer-signing.rb +118 -6
- data/truelayer-signing.gemspec +2 -2
- metadata +9 -75
- data/doc/CHANGELOG_md.html +0 -132
- data/doc/JWT/Decode.html +0 -97
- data/doc/JWT/Encode.html +0 -97
- data/doc/JWT/JWK/EC.html +0 -169
- data/doc/JWT/JWK.html +0 -91
- data/doc/JWT.html +0 -95
- data/doc/LICENSE-APACHE.html +0 -177
- data/doc/LICENSE-MIT.html +0 -105
- data/doc/README_md.html +0 -197
- data/doc/Rakefile.html +0 -106
- data/doc/TrueLayerSigning/Config.html +0 -211
- data/doc/TrueLayerSigning/Error.html +0 -97
- data/doc/TrueLayerSigning/JwsBase.html +0 -317
- data/doc/TrueLayerSigning/JwsHeader.html +0 -268
- data/doc/TrueLayerSigning/Signer.html +0 -186
- data/doc/TrueLayerSigning/Verifier.html +0 -327
- data/doc/TrueLayerSigning.html +0 -226
- data/doc/TrueLayerSigningExamples.html +0 -217
- data/doc/created.rid +0 -21
- data/doc/css/fonts.css +0 -167
- data/doc/css/rdoc.css +0 -662
- data/doc/examples/sign-request/Gemfile.html +0 -99
- data/doc/examples/sign-request/Gemfile_lock.html +0 -143
- data/doc/examples/sign-request/README_md.html +0 -138
- data/doc/examples/webhook-server/Gemfile.html +0 -99
- data/doc/examples/webhook-server/Gemfile_lock.html +0 -142
- data/doc/examples/webhook-server/README_md.html +0 -139
- data/doc/fonts/Lato-Light.ttf +0 -0
- data/doc/fonts/Lato-LightItalic.ttf +0 -0
- data/doc/fonts/Lato-Regular.ttf +0 -0
- data/doc/fonts/Lato-RegularItalic.ttf +0 -0
- data/doc/fonts/SourceCodePro-Bold.ttf +0 -0
- data/doc/fonts/SourceCodePro-Regular.ttf +0 -0
- data/doc/images/add.png +0 -0
- data/doc/images/arrow_up.png +0 -0
- data/doc/images/brick.png +0 -0
- data/doc/images/brick_link.png +0 -0
- data/doc/images/bug.png +0 -0
- data/doc/images/bullet_black.png +0 -0
- data/doc/images/bullet_toggle_minus.png +0 -0
- data/doc/images/bullet_toggle_plus.png +0 -0
- data/doc/images/date.png +0 -0
- data/doc/images/delete.png +0 -0
- data/doc/images/find.png +0 -0
- data/doc/images/loadingAnimation.gif +0 -0
- data/doc/images/macFFBgHack.png +0 -0
- data/doc/images/package.png +0 -0
- data/doc/images/page_green.png +0 -0
- data/doc/images/page_white_text.png +0 -0
- data/doc/images/page_white_width.png +0 -0
- data/doc/images/plugin.png +0 -0
- data/doc/images/ruby.png +0 -0
- data/doc/images/tag_blue.png +0 -0
- data/doc/images/tag_green.png +0 -0
- data/doc/images/transparent.png +0 -0
- data/doc/images/wrench.png +0 -0
- data/doc/images/wrench_orange.png +0 -0
- data/doc/images/zoom.png +0 -0
- data/doc/index.html +0 -118
- data/doc/js/darkfish.js +0 -84
- data/doc/js/navigation.js +0 -105
- data/doc/js/navigation.js.gz +0 -0
- data/doc/js/search.js +0 -110
- data/doc/js/search_index.js +0 -1
- data/doc/js/search_index.js.gz +0 -0
- data/doc/js/searcher.js +0 -229
- data/doc/js/searcher.js.gz +0 -0
- data/doc/table_of_contents.html +0 -269
- data/examples/sign-request/Gemfile.lock +0 -40
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7675d871fb8de41624e5f92fb6b0252fac90900f2f52c6bd9b182ea0f80cde44
|
|
4
|
+
data.tar.gz: 2345bf7cfc1619ef8c5e1bb20ed56f966ec9a71263798bde713aca0ba92b0811
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: df411a9c02a97ab165f5a47063ec0ca3fae67fe437f96af20b1f71eab4f88105bd8259a3c9685056c91210c3dca0134721f5c3b94e9cc39691756f6b64c17279
|
|
7
|
+
data.tar.gz: a39b865f5c4a1a3226cc8071f3c5971ff8c32c536d4b7b88ddab8cf7ffcc71fa2e36c3d3409b5fa7f3d25647f8ea8fb202ef6f74ac51661e85c2b089f6a4e2c4
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
- ...
|
|
11
11
|
|
|
12
|
+
## [0.2.0] – 2023-06-13
|
|
13
|
+
|
|
14
|
+
- Add support for signature verification using a JWKS: `TrueLayerSigning.verify_with_jwks(jwks)`
|
|
15
|
+
and `TrueLayerSigning.extract_jws_header(signature)`.
|
|
16
|
+
|
|
12
17
|
## [0.1.2] – 2023-05-19
|
|
13
18
|
|
|
14
19
|
- Fix conflict with JWT library
|
data/README.md
CHANGED
|
@@ -59,10 +59,20 @@ See full example of [request signing].
|
|
|
59
59
|
|
|
60
60
|
## Verifying webhooks
|
|
61
61
|
|
|
62
|
-
The `
|
|
62
|
+
The `verify_with_jwks` method may be used to verify webhook `Tl-Signature` header signatures.
|
|
63
63
|
|
|
64
64
|
```ruby
|
|
65
|
-
|
|
65
|
+
# The `jku` field is included in webhook signatures
|
|
66
|
+
jku = TrueLayerSigning.extract_jws_header(webhook_signature).jku
|
|
67
|
+
|
|
68
|
+
# You should check that the `jku` is a valid TrueLayer URL (not provided by this library)
|
|
69
|
+
ensure_jku_allowed(jku)
|
|
70
|
+
|
|
71
|
+
# Then fetch JSON Web Key Set from the public URL (not provided by this library)
|
|
72
|
+
jwks = fetch_jwks(jku)
|
|
73
|
+
|
|
74
|
+
# The raw JWKS value may be used directly to verify a signature
|
|
75
|
+
TrueLayerSigning.verify_with_jwks(jwks)
|
|
66
76
|
.set_method(method)
|
|
67
77
|
.set_path(path)
|
|
68
78
|
.set_headers(headers)
|
|
@@ -1,15 +1,40 @@
|
|
|
1
1
|
GEM
|
|
2
2
|
remote: https://rubygems.org/
|
|
3
3
|
specs:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
addressable (2.8.4)
|
|
5
|
+
public_suffix (>= 2.0.2, < 6.0)
|
|
6
|
+
domain_name (0.5.20190701)
|
|
7
|
+
unf (>= 0.0.5, < 1.0.0)
|
|
8
|
+
ffi (1.15.5)
|
|
9
|
+
ffi-compiler (1.0.1)
|
|
10
|
+
ffi (>= 1.0.0)
|
|
11
|
+
rake
|
|
12
|
+
http (5.1.1)
|
|
13
|
+
addressable (~> 2.8)
|
|
14
|
+
http-cookie (~> 1.0)
|
|
15
|
+
http-form_data (~> 2.2)
|
|
16
|
+
llhttp-ffi (~> 0.4.0)
|
|
17
|
+
http-cookie (1.0.5)
|
|
18
|
+
domain_name (~> 0.5)
|
|
19
|
+
http-form_data (2.3.0)
|
|
20
|
+
jwt (2.7.0)
|
|
21
|
+
llhttp-ffi (0.4.0)
|
|
22
|
+
ffi-compiler (~> 1.0)
|
|
23
|
+
rake (~> 13.0)
|
|
24
|
+
public_suffix (5.0.1)
|
|
25
|
+
rake (13.0.6)
|
|
26
|
+
truelayer-signing (0.2.0)
|
|
27
|
+
jwt (~> 2.7)
|
|
28
|
+
unf (0.1.4)
|
|
29
|
+
unf_ext
|
|
30
|
+
unf_ext (0.0.8.2)
|
|
7
31
|
webrick (1.8.1)
|
|
8
32
|
|
|
9
33
|
PLATFORMS
|
|
10
34
|
arm64-darwin-21
|
|
11
35
|
|
|
12
36
|
DEPENDENCIES
|
|
37
|
+
http
|
|
13
38
|
truelayer-signing
|
|
14
39
|
webrick
|
|
15
40
|
|
|
@@ -1,23 +1,14 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
3
|
-
require
|
|
1
|
+
require "http"
|
|
2
|
+
require "truelayer-signing"
|
|
3
|
+
require "webrick"
|
|
4
4
|
|
|
5
5
|
class TrueLayerSigningExamples
|
|
6
6
|
# Note: the webhook path can be whatever is configured for your application.
|
|
7
7
|
# Here a unique path is used, matching the example signature in the README.
|
|
8
8
|
WEBHOOK_PATH = "/hook/d7a2c49d-110a-4ed2-a07d-8fdb3ea6424b".freeze
|
|
9
|
-
PUBLIC_KEY_PEM = <<~TXT.freeze
|
|
10
|
-
-----BEGIN PUBLIC KEY-----
|
|
11
|
-
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBJ6ET9XeVCyMy+yOetZaNNCXPhwr5
|
|
12
|
-
BlyDDg1CLmyNM5SvqOs8RveL6dYl4lpPur4xrPQl04ggYlVd9wnHkZnp3jcBlXw8
|
|
13
|
-
Lc5phyYF1q2/QV/5wp2WHIhKDqUiXC0TvlE8d7MdTAN9yolcwrh6aWZ3kesTMZif
|
|
14
|
-
BgItyT6PXUab8mMdI8k=
|
|
15
|
-
-----END PUBLIC KEY-----
|
|
16
|
-
TXT
|
|
17
9
|
|
|
18
10
|
class << self
|
|
19
11
|
def run_webhook_server
|
|
20
|
-
TrueLayerSigning.certificate_id ||= SecureRandom.uuid
|
|
21
12
|
server = WEBrick::HTTPServer.new(Port: 4567)
|
|
22
13
|
|
|
23
14
|
puts "Server running at http://localhost:4567"
|
|
@@ -61,9 +52,19 @@ class TrueLayerSigningExamples
|
|
|
61
52
|
|
|
62
53
|
return ["400", "Bad Request – Header `Tl-Signature` missing"] unless tl_signature
|
|
63
54
|
|
|
55
|
+
jku = TrueLayerSigning.extract_jws_header(tl_signature).jku
|
|
56
|
+
|
|
57
|
+
return ["400", "Bad Request – Signature missing `jku`"] unless jku
|
|
58
|
+
return ["401", "Unauthorized – Unpermitted `jku`"] \
|
|
59
|
+
unless jku == "https://webhooks.truelayer.com/.well-known/jwks" ||
|
|
60
|
+
jku == "https://webhooks.truelayer-sandbox.com/.well-known/jwks"
|
|
61
|
+
|
|
62
|
+
jwks = HTTP.get(jku)
|
|
63
|
+
|
|
64
|
+
return ["401", "Unauthorized – Unavailable `jwks` resource"] unless jwks.status.success?
|
|
65
|
+
|
|
64
66
|
begin
|
|
65
|
-
TrueLayerSigning
|
|
66
|
-
.verify_with_pem(PUBLIC_KEY_PEM)
|
|
67
|
+
TrueLayerSigning.verify_with_jwks(jwks.to_s)
|
|
67
68
|
.set_method(:post)
|
|
68
69
|
.set_path(path)
|
|
69
70
|
.set_headers(headers)
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
module TrueLayerSigning
|
|
2
2
|
class Verifier < JwsBase
|
|
3
|
-
|
|
3
|
+
EXPECTED_COORDS_LENGTH = 66.freeze
|
|
4
|
+
|
|
5
|
+
attr_reader :required_headers, :key_type, :key_value
|
|
4
6
|
|
|
5
7
|
def initialize(args)
|
|
6
8
|
super
|
|
9
|
+
@key_type = args[:key_type]
|
|
7
10
|
@key_value = args[:key_value]
|
|
8
11
|
end
|
|
9
12
|
|
|
@@ -11,7 +14,6 @@ module TrueLayerSigning
|
|
|
11
14
|
ensure_verifier_config!
|
|
12
15
|
|
|
13
16
|
jws_header, jws_header_b64, signature_b64 = self.class.parse_tl_signature(tl_signature)
|
|
14
|
-
public_key = OpenSSL::PKey.read(key_value)
|
|
15
17
|
|
|
16
18
|
raise(Error, "Unexpected `alg` header value") if jws_header.alg != TrueLayerSigning.algorithm
|
|
17
19
|
|
|
@@ -22,24 +24,7 @@ module TrueLayerSigning
|
|
|
22
24
|
raise(Error, "Signature missing required header(s)") if required_headers &&
|
|
23
25
|
required_headers.any? { |key| !normalised_headers.has_key?(key.downcase) }
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
full_signature = [jws_header_b64, payload_b64, signature_b64].join(".")
|
|
27
|
-
jwt_options = { algorithm: TrueLayerSigning.algorithm }
|
|
28
|
-
|
|
29
|
-
begin
|
|
30
|
-
JWT.truelayer_decode(full_signature, public_key, true, jwt_options)
|
|
31
|
-
rescue JWT::VerificationError
|
|
32
|
-
@path = path.end_with?("/") && path[0...-1] || path + "/"
|
|
33
|
-
payload_b64 = Base64.urlsafe_encode64(build_signing_payload(ordered_headers),
|
|
34
|
-
padding: false)
|
|
35
|
-
full_signature = [jws_header_b64, payload_b64, signature_b64].join(".")
|
|
36
|
-
|
|
37
|
-
begin
|
|
38
|
-
JWT.truelayer_decode(full_signature, public_key, true, jwt_options)
|
|
39
|
-
rescue
|
|
40
|
-
raise(Error, "Signature verification failed")
|
|
41
|
-
end
|
|
42
|
-
end
|
|
27
|
+
verify_signature_flex(ordered_headers, jws_header, jws_header_b64, signature_b64)
|
|
43
28
|
end
|
|
44
29
|
|
|
45
30
|
def require_header(name)
|
|
@@ -69,6 +54,73 @@ module TrueLayerSigning
|
|
|
69
54
|
[jws_header, jws_header_b64, signature_b64]
|
|
70
55
|
end
|
|
71
56
|
|
|
57
|
+
private def verify_signature_flex(ordered_headers, jws_header, jws_header_b64, signature_b64)
|
|
58
|
+
full_signature = build_full_signature(ordered_headers, jws_header_b64, signature_b64)
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
verify_signature(jws_header, full_signature)
|
|
62
|
+
rescue JWT::VerificationError
|
|
63
|
+
@path = path.end_with?("/") && path[0...-1] || path + "/"
|
|
64
|
+
full_signature = build_full_signature(ordered_headers, jws_header_b64, signature_b64)
|
|
65
|
+
|
|
66
|
+
begin
|
|
67
|
+
verify_signature(jws_header, full_signature)
|
|
68
|
+
rescue JWT::VerificationError
|
|
69
|
+
raise(Error, "Signature verification failed")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private def build_full_signature(ordered_headers, jws_header_b64, signature_b64)
|
|
75
|
+
payload_b64 = Base64.urlsafe_encode64(build_signing_payload(ordered_headers), padding: false)
|
|
76
|
+
|
|
77
|
+
[jws_header_b64, payload_b64, signature_b64].join(".")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private def verify_signature(jws_header, full_signature)
|
|
81
|
+
case key_type
|
|
82
|
+
when :pem
|
|
83
|
+
public_key = OpenSSL::PKey.read(key_value)
|
|
84
|
+
when :jwks
|
|
85
|
+
public_key = retrieve_public_key(:jwks, key_value, jws_header)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
jwt_options = { algorithm: TrueLayerSigning.algorithm }
|
|
89
|
+
JWT.truelayer_decode(full_signature, public_key, true, jwt_options)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private def retrieve_public_key(key_type, key_value, jws_header)
|
|
93
|
+
case key_type
|
|
94
|
+
when :pem
|
|
95
|
+
OpenSSL::PKey.read(key_value)
|
|
96
|
+
when :jwks
|
|
97
|
+
jwks_hash = JSON.parse(key_value, symbolize_names: true)
|
|
98
|
+
jwk = jwks_hash[:keys].find { |key| key[:kid] == jws_header.kid }
|
|
99
|
+
|
|
100
|
+
raise(Error, "JWKS does not include given `kid` value") unless jwk
|
|
101
|
+
|
|
102
|
+
valid_jwk = apply_zero_padding_as_needed(jwk)
|
|
103
|
+
|
|
104
|
+
JWT::JWK::EC.import(valid_jwk).public_key
|
|
105
|
+
else
|
|
106
|
+
raise(Error, "Type of public key not recognised")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private def apply_zero_padding_as_needed(jwk)
|
|
111
|
+
valid_jwk = jwk.clone
|
|
112
|
+
|
|
113
|
+
%i(x y).each do |elem|
|
|
114
|
+
coords = Base64.urlsafe_decode64(valid_jwk[elem])
|
|
115
|
+
length = coords.length
|
|
116
|
+
diff = EXPECTED_COORDS_LENGTH - length
|
|
117
|
+
|
|
118
|
+
valid_jwk[elem] = Base64.urlsafe_encode64(("\x00" * diff) + coords) if diff > 0
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
valid_jwk
|
|
122
|
+
end
|
|
123
|
+
|
|
72
124
|
private def ensure_verifier_config!
|
|
73
125
|
raise(Error, "Key value missing") unless key_value
|
|
74
126
|
end
|
data/lib/truelayer-signing.rb
CHANGED
|
@@ -28,8 +28,16 @@ module TrueLayerSigning
|
|
|
28
28
|
Signer.new
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def extract_jws_header(signature)
|
|
32
|
+
Verifier.parse_tl_signature(signature).first
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def verify_with_jwks(jwks)
|
|
36
|
+
Verifier.new(key_type: :jwks, key_value: jwks)
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
def verify_with_pem(pem)
|
|
32
|
-
Verifier.new(key_value: pem)
|
|
40
|
+
Verifier.new(key_type: :pem, key_value: pem)
|
|
33
41
|
end
|
|
34
42
|
end
|
|
35
43
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"keys": [
|
|
3
|
+
{
|
|
4
|
+
"kty": "RSA",
|
|
5
|
+
"alg": "RS512",
|
|
6
|
+
"kid": "c9034c0f-bd06-4dd1-98fe-f67a5a1a0601",
|
|
7
|
+
"n": "yWTtfMrBLrVaMgUO2Oz83kMHbJX0aCSwkQ4gZoXruicEBfhWKSC4VV6WB3CZIBGfxGhJhSBFkFizdZzjSTQYIB-OIQcFR2FtP5tSSpK2K7d4-CvBs78_rOFyA4Vz4aoYLlJOWmFurMod27BmQe1UrDRm0SWJbE9L8TqtQYKDva0sWeQPuxj2Stv4BBf7Z5Zq8NyCAiW-TMmhV7silLT8eN7MLp6X_-BFoHgZSL26ECd5KkQxVE8kl9Cl-GelmcbT8CIM4LQ8vTSYB6ADHcAte24xtP3IUv5_K-gFaNucbC1i95X0Ha2UNx13uFfaBVOngwFyAnU7eFlZh_MJS_kCXw",
|
|
8
|
+
"e": "AQAB"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"kty": "EC",
|
|
12
|
+
"alg": "ES512",
|
|
13
|
+
"kid": "d3534b83-bdc7-4066-adb5-c8d4bf616601",
|
|
14
|
+
"crv": "P-521",
|
|
15
|
+
"x": "AQO9n3CGUtsvQEWivE7KTkbaqY-xxsa0EPhplLxj0Nak6dS400ug0VhMqgTXfyc3nV3UA7Mz7x5dDL13YyUtfiOq",
|
|
16
|
+
"y": "fRU9nhcoS3FUZD7UzBUt9AOOpShMlV8yXC3VXn0FTRB7ySDqZnr1th0ZYnt0uihMDKYKB0-TrptWg1hgA75aeiI"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "payment_executed",
|
|
3
|
+
"event_version": 1,
|
|
4
|
+
"event_id": "2606ed6d-5c89-4bd9-8f95-40e42c95ca71",
|
|
5
|
+
"payment_id": "a62ba556-4b35-4628-9d8c-42302d2e5d02",
|
|
6
|
+
"payment_method": {
|
|
7
|
+
"type": "bank_transfer",
|
|
8
|
+
"provider_id": "mock-payments-gb-redirect",
|
|
9
|
+
"scheme_id": "faster_payments_service"
|
|
10
|
+
},
|
|
11
|
+
"executed_at": "2023-06-09T15:40:30.561Z",
|
|
12
|
+
"payment_source": {
|
|
13
|
+
"account_identifiers": [
|
|
14
|
+
{
|
|
15
|
+
"type": "sort_code_account_number",
|
|
16
|
+
"sort_code": "040668",
|
|
17
|
+
"account_number": "00000871"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"account_holder_name": "JOHN SANDBRIDGE"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
eyJhbGciOiJFUzUxMiIsImtpZCI6ImQzNTM0YjgzLWJkYzctNDA2Ni1hZGI1LWM4ZDRiZjYxNjYwMSIsInRsX3ZlcnNpb24iOiIyIiwidGxfaGVhZGVycyI6IngtdGwtd2ViaG9vay10aW1lc3RhbXAiLCJqa3UiOiJodHRwczovL3dlYmhvb2tzLnRydWVsYXllci1zYW5kYm94LmNvbS8ud2VsbC1rbm93bi9qd2tzIn0..AVKH1WQK4Z4tIF3I-gU1AkLI7o4Dk-ZpALG7rKuMPQdksVwzUVBa8zq3LdvV2SHNzlH50NhGVYi-j4nC8G23Qd5UAW8DCvMB1ynTBVE_3vKzbmbfh8Dg3TAPvZCdajQiDLrLJp5iiUtcRgML2EI_zt9SEAkpIaSxFU8DHnMm8CN3YEQT
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
require "minitest/autorun"
|
|
2
2
|
require "truelayer-signing"
|
|
3
3
|
|
|
4
|
+
def read_file(path)
|
|
5
|
+
File.read(File.expand_path(path, File.dirname(__FILE__)))
|
|
6
|
+
end
|
|
7
|
+
|
|
4
8
|
CERTIFICATE_ID = "45fc75cf-5649-4134-84b3-192c2c78e990".freeze
|
|
5
|
-
PRIVATE_KEY =
|
|
6
|
-
|
|
7
|
-
PUBLIC_KEY = File.read(File.expand_path("../../test-resources/ec512-public.pem",
|
|
8
|
-
File.dirname(__FILE__))).freeze
|
|
9
|
+
PRIVATE_KEY = read_file("../../test-resources/ec512-private.pem").freeze
|
|
10
|
+
PUBLIC_KEY = read_file("../../test-resources/ec512-public.pem").freeze
|
|
9
11
|
|
|
10
12
|
TrueLayerSigning.certificate_id = CERTIFICATE_ID.freeze
|
|
11
13
|
TrueLayerSigning.private_key = PRIVATE_KEY.freeze
|
|
@@ -59,6 +61,24 @@ class TrueLayerSigningTest < Minitest::Test
|
|
|
59
61
|
refute(result.first.include?("\nIdempotency-Key: "))
|
|
60
62
|
end
|
|
61
63
|
|
|
64
|
+
def test_full_request_signature_without_method_should_default_to_post_and_succeed
|
|
65
|
+
body = { currency: "GBP", max_amount_in_minor: 50_000_00 }.to_json
|
|
66
|
+
path = "/merchant_accounts/a61acaef-ee05-4077-92f3-25543a11bd8d/sweeping"
|
|
67
|
+
|
|
68
|
+
tl_signature = TrueLayerSigning.sign_with_pem
|
|
69
|
+
.set_path(path)
|
|
70
|
+
.set_body(body)
|
|
71
|
+
.sign
|
|
72
|
+
|
|
73
|
+
result = TrueLayerSigning.verify_with_pem(PUBLIC_KEY)
|
|
74
|
+
.set_path(path)
|
|
75
|
+
.set_body(body)
|
|
76
|
+
.verify(tl_signature)
|
|
77
|
+
|
|
78
|
+
assert(result.first
|
|
79
|
+
.start_with?("POST /merchant_accounts/a61acaef-ee05-4077-92f3-25543a11bd8d/sweeping\n"))
|
|
80
|
+
end
|
|
81
|
+
|
|
62
82
|
def test_mismatched_signature_with_attached_valid_body_should_fail
|
|
63
83
|
# Signature for `/bar` but with a valid jws-body pre-attached.
|
|
64
84
|
# If we run a simple jws verify on this unchanged, it'll work!
|
|
@@ -101,8 +121,7 @@ class TrueLayerSigningTest < Minitest::Test
|
|
|
101
121
|
body = { currency: "GBP", max_amount_in_minor: 50_000_00, name: "Foo???" }.to_json
|
|
102
122
|
idempotency_key = "idemp-2076717c-9005-4811-a321-9e0787fa0382"
|
|
103
123
|
path = "/merchant_accounts/a61acaef-ee05-4077-92f3-25543a11bd8d/sweeping"
|
|
104
|
-
tl_signature =
|
|
105
|
-
File.dirname(__FILE__)))
|
|
124
|
+
tl_signature = read_file("../../test-resources/tl-signature.txt")
|
|
106
125
|
|
|
107
126
|
result = TrueLayerSigning.verify_with_pem(PUBLIC_KEY)
|
|
108
127
|
.set_method(:post)
|
|
@@ -370,6 +389,99 @@ class TrueLayerSigningTest < Minitest::Test
|
|
|
370
389
|
.start_with?("POST /merchant_accounts/a61acaef-ee05-4077-92f3-25543a11bd8d/sweeping\n"))
|
|
371
390
|
end
|
|
372
391
|
|
|
392
|
+
def test_extract_jws_header_should_succeed
|
|
393
|
+
hook_signature = read_file("../../test-resources/webhook-signature.txt")
|
|
394
|
+
jws_header = TrueLayerSigning.extract_jws_header(hook_signature)
|
|
395
|
+
|
|
396
|
+
assert_equal("ES512", jws_header.alg)
|
|
397
|
+
assert_equal(CERTIFICATE_ID, jws_header.kid)
|
|
398
|
+
assert_equal("2", jws_header.tl_version)
|
|
399
|
+
assert_equal("X-Tl-Webhook-Timestamp,Content-Type", jws_header.tl_headers)
|
|
400
|
+
assert_equal("https://webhooks.truelayer.com/.well-known/jwks", jws_header.jku)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def test_verify_with_jwks_should_succeed
|
|
404
|
+
hook_signature = read_file("../../test-resources/webhook-signature.txt")
|
|
405
|
+
jwks = read_file("../../test-resources/jwks.json")
|
|
406
|
+
body = { event_type: "example", event_id: "18b2842b-a57b-4887-a0a6-d3c7c36f1020" }.to_json
|
|
407
|
+
|
|
408
|
+
TrueLayerSigning.verify_with_jwks(jwks)
|
|
409
|
+
.set_method(:post)
|
|
410
|
+
.set_path("/tl-webhook")
|
|
411
|
+
.add_header("x-tl-webhook-timestamp", "2021-11-29T11:42:55Z")
|
|
412
|
+
.add_header("content-type", "application/json")
|
|
413
|
+
.set_body(body)
|
|
414
|
+
.verify(hook_signature)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def test_verify_with_jwks_with_zero_padding_missing_should_succeed
|
|
418
|
+
jwks = read_file("resources/missing-zero-padding-test-jwks.json")
|
|
419
|
+
|
|
420
|
+
# JWKS with EC key missing zero padded coords is not supported by `jwt` gem
|
|
421
|
+
|
|
422
|
+
jwks_as_json = JSON.parse(jwks, symbolize_names: true)
|
|
423
|
+
jwk_missing_padding = jwks_as_json[:keys].find { |e| e[:kty] == "EC" }
|
|
424
|
+
imported_jwk = JWT::JWK::EC.import(jwk_missing_padding)
|
|
425
|
+
|
|
426
|
+
error = assert_raises(OpenSSL::PKey::EC::Point::Error) { imported_jwk.public_key.check_key }
|
|
427
|
+
assert_equal("EC_POINT_bn2point: invalid encoding", error.message)
|
|
428
|
+
|
|
429
|
+
# But supported by `truelayer-signing` using zero padding (prepend)
|
|
430
|
+
|
|
431
|
+
payload = read_file("resources/missing-zero-padding-test-payload.json")
|
|
432
|
+
body = JSON.parse(payload).to_json
|
|
433
|
+
|
|
434
|
+
TrueLayerSigning.verify_with_jwks(jwks)
|
|
435
|
+
.set_method(:post)
|
|
436
|
+
.set_path("/a147f26a-f07e-47e3-9526-d52f1f1fdd55")
|
|
437
|
+
.add_header("x-tl-webhook-timestamp", "2023-06-09T15:40:30Z")
|
|
438
|
+
.set_body(body)
|
|
439
|
+
.verify(read_file("resources/missing-zero-padding-test-signature.txt"))
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def test_verify_with_jwks_with_wrong_timestamp_should_fail
|
|
443
|
+
hook_signature = read_file("../../test-resources/webhook-signature.txt")
|
|
444
|
+
jwks = read_file("../../test-resources/jwks.json")
|
|
445
|
+
body = { event_type: "example", event_id: "18b2842b-a57b-4887-a0a6-d3c7c36f1020" }.to_json
|
|
446
|
+
|
|
447
|
+
verifier = TrueLayerSigning.verify_with_jwks(jwks)
|
|
448
|
+
.set_method(:post)
|
|
449
|
+
.set_path("/tl-webhook")
|
|
450
|
+
.add_header("x-tl-webhook-timestamp", "2021-12-29T11:42:55Z")
|
|
451
|
+
.add_header("content-type", "application/json")
|
|
452
|
+
.set_body(body)
|
|
453
|
+
|
|
454
|
+
error = assert_raises(TrueLayerSigning::Error) { verifier.verify(hook_signature) }
|
|
455
|
+
assert_equal("Signature verification failed", error.message)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def test_sign_with_pem_and_custom_jku_should_succeed
|
|
459
|
+
body = { currency: "GBP", max_amount_in_minor: 50_000_00 }.to_json
|
|
460
|
+
idempotency_key = "idemp-2076717c-9005-4811-a321-9e0787fa0382"
|
|
461
|
+
path = "/merchant_accounts/a61acaef-ee05-4077-92f3-25543a11bd8d/sweeping"
|
|
462
|
+
|
|
463
|
+
tl_signature_1 = TrueLayerSigning.sign_with_pem
|
|
464
|
+
.set_path(path)
|
|
465
|
+
.add_header("Idempotency-Key", idempotency_key)
|
|
466
|
+
.set_body(body)
|
|
467
|
+
.sign
|
|
468
|
+
|
|
469
|
+
jws_header_1 = TrueLayerSigning.extract_jws_header(tl_signature_1)
|
|
470
|
+
|
|
471
|
+
assert_nil(jws_header_1.jku)
|
|
472
|
+
|
|
473
|
+
tl_signature_2 = TrueLayerSigning.sign_with_pem
|
|
474
|
+
.set_path(path)
|
|
475
|
+
.add_header("Idempotency-Key", idempotency_key)
|
|
476
|
+
.set_body(body)
|
|
477
|
+
.set_jku("https://webhooks.truelayer.com/.well-known/jwks")
|
|
478
|
+
.sign
|
|
479
|
+
|
|
480
|
+
jws_header_2 = TrueLayerSigning.extract_jws_header(tl_signature_2)
|
|
481
|
+
|
|
482
|
+
assert_equal("https://webhooks.truelayer.com/.well-known/jwks", jws_header_2.jku)
|
|
483
|
+
end
|
|
484
|
+
|
|
373
485
|
# TODO: remove if/when we get rid of `lib/truelayer-signing/jwt.rb`
|
|
374
486
|
def test_jwt_encode_and_decode_should_succeed
|
|
375
487
|
payload_object = { currency: "GBP", max_amount_in_minor: 50_000_00 }
|
data/truelayer-signing.gemspec
CHANGED
|
@@ -2,7 +2,7 @@ $LOAD_PATH.unshift(::File.join(::File.dirname(__FILE__), "lib"))
|
|
|
2
2
|
|
|
3
3
|
Gem::Specification.new do |s|
|
|
4
4
|
s.name = "truelayer-signing"
|
|
5
|
-
s.version = "0.
|
|
5
|
+
s.version = "0.2.0"
|
|
6
6
|
s.summary = "Ruby gem to produce and verify TrueLayer API requests signatures"
|
|
7
7
|
s.description = "TrueLayer provides instant access to open banking to " \
|
|
8
8
|
"easily integrate next-generation payments and financial data into any app." \
|
|
@@ -21,5 +21,5 @@ Gem::Specification.new do |s|
|
|
|
21
21
|
s.require_paths = ["lib"]
|
|
22
22
|
|
|
23
23
|
s.required_ruby_version = ">= 2.7"
|
|
24
|
-
s.add_runtime_dependency("jwt", "2.
|
|
24
|
+
s.add_runtime_dependency("jwt", "~> 2.7")
|
|
25
25
|
end
|
metadata
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: truelayer-signing
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kevin Plattret
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-
|
|
11
|
+
date: 2023-06-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jwt
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- -
|
|
17
|
+
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '2.
|
|
19
|
+
version: '2.7'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
|
-
- -
|
|
24
|
+
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '2.
|
|
26
|
+
version: '2.7'
|
|
27
27
|
description: TrueLayer provides instant access to open banking to easily integrate
|
|
28
28
|
next-generation payments and financial data into any app.This helps easily sign
|
|
29
29
|
TrueLayer API requests using a JSON web signature.
|
|
@@ -37,76 +37,7 @@ files:
|
|
|
37
37
|
- "./LICENSE-MIT"
|
|
38
38
|
- "./README.md"
|
|
39
39
|
- "./Rakefile"
|
|
40
|
-
- "./doc/CHANGELOG_md.html"
|
|
41
|
-
- "./doc/JWT.html"
|
|
42
|
-
- "./doc/JWT/Decode.html"
|
|
43
|
-
- "./doc/JWT/Encode.html"
|
|
44
|
-
- "./doc/JWT/JWK.html"
|
|
45
|
-
- "./doc/JWT/JWK/EC.html"
|
|
46
|
-
- "./doc/LICENSE-APACHE.html"
|
|
47
|
-
- "./doc/LICENSE-MIT.html"
|
|
48
|
-
- "./doc/README_md.html"
|
|
49
|
-
- "./doc/Rakefile.html"
|
|
50
|
-
- "./doc/TrueLayerSigning.html"
|
|
51
|
-
- "./doc/TrueLayerSigning/Config.html"
|
|
52
|
-
- "./doc/TrueLayerSigning/Error.html"
|
|
53
|
-
- "./doc/TrueLayerSigning/JwsBase.html"
|
|
54
|
-
- "./doc/TrueLayerSigning/JwsHeader.html"
|
|
55
|
-
- "./doc/TrueLayerSigning/Signer.html"
|
|
56
|
-
- "./doc/TrueLayerSigning/Verifier.html"
|
|
57
|
-
- "./doc/TrueLayerSigningExamples.html"
|
|
58
|
-
- "./doc/created.rid"
|
|
59
|
-
- "./doc/css/fonts.css"
|
|
60
|
-
- "./doc/css/rdoc.css"
|
|
61
|
-
- "./doc/examples/sign-request/Gemfile.html"
|
|
62
|
-
- "./doc/examples/sign-request/Gemfile_lock.html"
|
|
63
|
-
- "./doc/examples/sign-request/README_md.html"
|
|
64
|
-
- "./doc/examples/webhook-server/Gemfile.html"
|
|
65
|
-
- "./doc/examples/webhook-server/Gemfile_lock.html"
|
|
66
|
-
- "./doc/examples/webhook-server/README_md.html"
|
|
67
|
-
- "./doc/fonts/Lato-Light.ttf"
|
|
68
|
-
- "./doc/fonts/Lato-LightItalic.ttf"
|
|
69
|
-
- "./doc/fonts/Lato-Regular.ttf"
|
|
70
|
-
- "./doc/fonts/Lato-RegularItalic.ttf"
|
|
71
|
-
- "./doc/fonts/SourceCodePro-Bold.ttf"
|
|
72
|
-
- "./doc/fonts/SourceCodePro-Regular.ttf"
|
|
73
|
-
- "./doc/images/add.png"
|
|
74
|
-
- "./doc/images/arrow_up.png"
|
|
75
|
-
- "./doc/images/brick.png"
|
|
76
|
-
- "./doc/images/brick_link.png"
|
|
77
|
-
- "./doc/images/bug.png"
|
|
78
|
-
- "./doc/images/bullet_black.png"
|
|
79
|
-
- "./doc/images/bullet_toggle_minus.png"
|
|
80
|
-
- "./doc/images/bullet_toggle_plus.png"
|
|
81
|
-
- "./doc/images/date.png"
|
|
82
|
-
- "./doc/images/delete.png"
|
|
83
|
-
- "./doc/images/find.png"
|
|
84
|
-
- "./doc/images/loadingAnimation.gif"
|
|
85
|
-
- "./doc/images/macFFBgHack.png"
|
|
86
|
-
- "./doc/images/package.png"
|
|
87
|
-
- "./doc/images/page_green.png"
|
|
88
|
-
- "./doc/images/page_white_text.png"
|
|
89
|
-
- "./doc/images/page_white_width.png"
|
|
90
|
-
- "./doc/images/plugin.png"
|
|
91
|
-
- "./doc/images/ruby.png"
|
|
92
|
-
- "./doc/images/tag_blue.png"
|
|
93
|
-
- "./doc/images/tag_green.png"
|
|
94
|
-
- "./doc/images/transparent.png"
|
|
95
|
-
- "./doc/images/wrench.png"
|
|
96
|
-
- "./doc/images/wrench_orange.png"
|
|
97
|
-
- "./doc/images/zoom.png"
|
|
98
|
-
- "./doc/index.html"
|
|
99
|
-
- "./doc/js/darkfish.js"
|
|
100
|
-
- "./doc/js/navigation.js"
|
|
101
|
-
- "./doc/js/navigation.js.gz"
|
|
102
|
-
- "./doc/js/search.js"
|
|
103
|
-
- "./doc/js/search_index.js"
|
|
104
|
-
- "./doc/js/search_index.js.gz"
|
|
105
|
-
- "./doc/js/searcher.js"
|
|
106
|
-
- "./doc/js/searcher.js.gz"
|
|
107
|
-
- "./doc/table_of_contents.html"
|
|
108
40
|
- "./examples/sign-request/Gemfile"
|
|
109
|
-
- "./examples/sign-request/Gemfile.lock"
|
|
110
41
|
- "./examples/sign-request/README.md"
|
|
111
42
|
- "./examples/sign-request/main.rb"
|
|
112
43
|
- "./examples/webhook-server/Gemfile"
|
|
@@ -120,6 +51,9 @@ files:
|
|
|
120
51
|
- "./lib/truelayer-signing/signer.rb"
|
|
121
52
|
- "./lib/truelayer-signing/utils.rb"
|
|
122
53
|
- "./lib/truelayer-signing/verifier.rb"
|
|
54
|
+
- "./test/resources/missing-zero-padding-test-jwks.json"
|
|
55
|
+
- "./test/resources/missing-zero-padding-test-payload.json"
|
|
56
|
+
- "./test/resources/missing-zero-padding-test-signature.txt"
|
|
123
57
|
- "./test/test-truelayer-signing.rb"
|
|
124
58
|
- "./truelayer-signing.gemspec"
|
|
125
59
|
homepage: https://github.com/TrueLayer/truelayer-signing/tree/main/ruby
|