truelayer-signing 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|