From e507bd28081665e1e2571c31291b39abbd31866d Mon Sep 17 00:00:00 2001 From: Julik Tarkhanov Date: Tue, 11 Jun 2024 13:51:18 +0200 Subject: [PATCH 1/4] Add Ruby request signing example --- .../message-signing/ruby/.gitignore | 118 ++++++++++++++++++ ps-api-examples/message-signing/ruby/Gemfile | 3 + .../message-signing/ruby/Gemfile.lock | 21 ++++ .../message-signing/ruby/signing_example.rb | 96 ++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 ps-api-examples/message-signing/ruby/.gitignore create mode 100644 ps-api-examples/message-signing/ruby/Gemfile create mode 100644 ps-api-examples/message-signing/ruby/Gemfile.lock create mode 100644 ps-api-examples/message-signing/ruby/signing_example.rb diff --git a/ps-api-examples/message-signing/ruby/.gitignore b/ps-api-examples/message-signing/ruby/.gitignore new file mode 100644 index 0000000..77f7daf --- /dev/null +++ b/ps-api-examples/message-signing/ruby/.gitignore @@ -0,0 +1,118 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* + +*.key +*.pem \ No newline at end of file diff --git a/ps-api-examples/message-signing/ruby/Gemfile b/ps-api-examples/message-signing/ruby/Gemfile new file mode 100644 index 0000000..2960cae --- /dev/null +++ b/ps-api-examples/message-signing/ruby/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "faraday", "~>2", "<3" diff --git a/ps-api-examples/message-signing/ruby/Gemfile.lock b/ps-api-examples/message-signing/ruby/Gemfile.lock new file mode 100644 index 0000000..a0ddd4a --- /dev/null +++ b/ps-api-examples/message-signing/ruby/Gemfile.lock @@ -0,0 +1,21 @@ +GEM + remote: https://rubygems.org/ + specs: + base64 (0.2.0) + faraday (2.8.1) + base64 + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.2) + openssl (3.2.0) + ruby2_keywords (0.0.5) + +PLATFORMS + ruby + +DEPENDENCIES + faraday (~> 2, < 3) + openssl (~> 3) + +BUNDLED WITH + 2.1.4 diff --git a/ps-api-examples/message-signing/ruby/signing_example.rb b/ps-api-examples/message-signing/ruby/signing_example.rb new file mode 100644 index 0000000..edd755e --- /dev/null +++ b/ps-api-examples/message-signing/ruby/signing_example.rb @@ -0,0 +1,96 @@ +require "digest" # For SHA-512 +require "base64" # for encoding +require "openssl" # for cryptography +require "time" # ISO8601 formatting +require "faraday" # Adapter for most HTTP clients + +class RequestSigningMiddleware < Faraday::Middleware + # @option key_id[String] UID of the signing key; found in the Starling dev portal + # @option key[OpenSSL::PKey::RSA] the private key used to sign requests + # def initialize(app, key_id:, key:)... + def on_request(env) + env[:request_headers].merge!(generate_auth_headers(env, Time.now)) + end + + def generate_auth_headers(env, at_request_time) + body_to_digest = env[:request_body].nil? || env[:request_body].empty? ? "X" : env[:request_body] + raise "The request_body must be a String - this middleware has to be mounted below :json" unless body_to_digest.is_a?(String) + + body_digest = Base64.strict_encode64(Digest::SHA512.digest(body_to_digest)) + date = at_request_time.utc.iso8601 + string_to_sign = "(request-target): #{request_target(env)}\nDate: #{date}\nDigest: #{body_digest}" + msg_digest = Base64.strict_encode64(key.sign("SHA256", string_to_sign)) + + { + "Authorization" => "Signature keyid=\"#{key_id}\",algorithm=\"rsa-sha256\",headers=\"(request-target) Date Digest\",signature=\"#{msg_digest}\"", + "Digest" => body_digest, + "Date" => date + } + end + + private + + # Should be the request path + eventually query string if set + def request_target(env) + path_and_query = [env[:url].path, env[:url].query].compact.join("?") + "#{env[:method].downcase} #{path_and_query}" + end + + def key_id + options.fetch(:key_id) + end + + def key + options.fetch(:private_key) + end +end +Faraday::Request.register_middleware(starling_request_signing: RequestSigningMiddleware) + + +# You can generate a new keypair using +# keypair = OpenSSL::PKey::RSA.new(2048) +# For the signing you need the private key only +# pem = keypair.private_to_pem (if you use Ruby's openssl version 3 or above) +# or +# pem = keypair.to_pem (older "openssl" versions) +private_key = OpenSSL::PKey::RSA.new(<<~PEM) + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAyQtOhiSj+7+UblUGADTPDGTuXP1YuLSE25+R+Lj71AmQUD+6 + qcAu4CUdfJ48p13Bg7veY0Dxk9VmSnZ12IST8dLfM80625ILMpLEdESihPzaCnjE + /Y4eGlFCefIL9b4DUasOaXOJzyvloS1/tQX/cwT8yAIfK/hXlsq38dt50w+9i58C + k9dhrq5nxAXwIfkqytcSyQipIYUkp8qyKTpsswO0m/LOk8KRd/NybIxCD+cVm4eQ + 0xfFFa8jhN9CpP5onUDGugVm2zlqheaAYS9GxEJc/Z5oh78tuyuH+PGv+cRxNUCq + y0pZHsK+qp8tZGRqsBprgqAV4itZ7M6UiKPHBQIDAQABAoIBACSjbl6M++OLwPmw + fgT4mskX9ca1lv8mStYZiQkqcR5t1cKCMrrv3rsTmIGW9tfLgtJGoRs2gTAfYmJs + n0JjuvCFrQ6sNq9AONExJSNJRNL2n6fr5X6N8Vd7eqFtppdU1xcBlQFLwJAkYFdU + yuLLIogsHwM2O8cQHapJ7GbjyBpZ/6LkF6Cz80Nnud5mKrJIOIwzcS5/rTszlVdA + cdtwAfHZIKWqccMxd/ukZ3s3cFgub+DtW49uYdfTOXbPsWbZbyTugBIPvP7rldeI + hm+AExHmq7/q0m3baJuIFm7ISNd/noS4guRAbtgk1pvVemwOad3gq1geKTo1uStO + tXvJz8sCgYEA8mHBNH7iezDpA1IMgsKaZQwuWgVWKp9OVzMXyYuQ6Be57YQr2d1O + skMNcCV+wJGGDX79PEJvp/t6kZDrAq7duYJqKtRdRfu+99R3tsYz6eY4G4uNp0mF + gFdRdMtOTZHVhtk3E4zEi2I7i/I8Ce4WbPVB27kR4gTNx7tNKhRELKMCgYEA1Fb7 + CnotoHf76kyoEkyrq+WP/TdLkpsabDZRrjiET4cbNj6f8nWLdN5yIfcrRBe5b5wU + bnZQsuxgofwo+To9ECa7McvVI5z3UX/mq4Lbf85CYMUUKouz8GURxcgvbCRMXlJ/ + ozWp0mWQwJx/2t0TH6SxYAaRZk1hyRX50Qc8EDcCgYAJXjfedI0CX+iRpUkwgJ8B + CtB70Dr9WLzpZ+Mieg92uPwJrxMWz5PsFeVeEUTt4nIA8YiOHK8+Gd0p5SUALIwL + UHwT/bNBMjK2V3LtEIoPH0PJ5MHr1k6foEBYuEblfp53IMwdKFKsZHaSuSES7S3W + tj/+Yw/K4Y6mipm356Ke6wKBgQC6tvFwuRa98EOYN2fjD4A1W1tN8f2GINUPKoSQ + iinuNIN9I3xKG4pRbfk2XL2y1pm8xqZAq9EyRCCEz9LHtKpVNXmNxArbkf73r1wK + nLqem6RKq4GcF9RWIsmJ/QmWMiTlG+4YeeumkqDCfdr/fT5/qLZAFgZsysadp7FQ + WOg76QKBgQCrDvcFaCVM+GipRyxaOG+TZttAoaaFTrhVrZbEPIH1rP+7aTbFTZt0 + Sm37XdDoRlnUtF1CbLWSEeBsGZ2qIiQQ4MKcwU4Bw+Pp4wKpe7sRBrDeZAzbVOqe + zSZPRlOta72qhsE+lDeks63CRZpuEXYvQOXqgVxGGMoUzJZ1U1VdVg== + -----END RSA PRIVATE KEY----- +PEM + +key_id = "a937ba2a-7b67-4903-9355-ae90f6bf015d" +base_url = "https://payment-api-sandbox.starlingbank.com" +faraday ||= Faraday.new(base_url, _connection_opts = {}) do |conn| + conn.request :json # JSON has to be serialized before the request gets signed + conn.request :starling_request_signing, private_key: private_key, key_id: key_id + conn.response :json, parser_options: {symbolize_names: true} +end + +response = faraday.get("/api/v1/7d0b3a0a-f0f9-4579-b7fa-9c091d243d48") +warn response.status +warn response.body \ No newline at end of file From c41baba1803bd7ba2315412088d90d0ca36e1844 Mon Sep 17 00:00:00 2001 From: Julik Tarkhanov Date: Tue, 11 Jun 2024 13:52:10 +0200 Subject: [PATCH 2/4] Remove gitignore there --- .../message-signing/ruby/.gitignore | 118 ------------------ 1 file changed, 118 deletions(-) delete mode 100644 ps-api-examples/message-signing/ruby/.gitignore diff --git a/ps-api-examples/message-signing/ruby/.gitignore b/ps-api-examples/message-signing/ruby/.gitignore deleted file mode 100644 index 77f7daf..0000000 --- a/ps-api-examples/message-signing/ruby/.gitignore +++ /dev/null @@ -1,118 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 - -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.pnp.* - -*.key -*.pem \ No newline at end of file From f0075f4b5267080df2a76a3a4d5666d3e1301ce1 Mon Sep 17 00:00:00 2001 From: Julik Tarkhanov Date: Tue, 11 Jun 2024 13:53:52 +0200 Subject: [PATCH 3/4] Regenerate Gemfile.lock --- ps-api-examples/message-signing/ruby/Gemfile.lock | 2 -- 1 file changed, 2 deletions(-) diff --git a/ps-api-examples/message-signing/ruby/Gemfile.lock b/ps-api-examples/message-signing/ruby/Gemfile.lock index a0ddd4a..5546c3e 100644 --- a/ps-api-examples/message-signing/ruby/Gemfile.lock +++ b/ps-api-examples/message-signing/ruby/Gemfile.lock @@ -7,7 +7,6 @@ GEM faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-net_http (3.0.2) - openssl (3.2.0) ruby2_keywords (0.0.5) PLATFORMS @@ -15,7 +14,6 @@ PLATFORMS DEPENDENCIES faraday (~> 2, < 3) - openssl (~> 3) BUNDLED WITH 2.1.4 From 184e627215401b4d022dd5f0f3bd173804600049 Mon Sep 17 00:00:00 2001 From: Julik Tarkhanov Date: Tue, 11 Jun 2024 16:11:38 +0200 Subject: [PATCH 4/4] Fix binding of a conditional --- ps-api-examples/message-signing/ruby/signing_example.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ps-api-examples/message-signing/ruby/signing_example.rb b/ps-api-examples/message-signing/ruby/signing_example.rb index edd755e..f4441ca 100644 --- a/ps-api-examples/message-signing/ruby/signing_example.rb +++ b/ps-api-examples/message-signing/ruby/signing_example.rb @@ -13,7 +13,7 @@ def on_request(env) end def generate_auth_headers(env, at_request_time) - body_to_digest = env[:request_body].nil? || env[:request_body].empty? ? "X" : env[:request_body] + body_to_digest = (env[:request_body].nil? || env[:request_body].empty?) ? "X" : env[:request_body] raise "The request_body must be a String - this middleware has to be mounted below :json" unless body_to_digest.is_a?(String) body_digest = Base64.strict_encode64(Digest::SHA512.digest(body_to_digest))