Class: Faraday::Middlewares::BuildService::SshSigner
- Inherits:
-
Object
- Object
- Faraday::Middlewares::BuildService::SshSigner
- Includes:
- HttpHelpers
- Defined in:
- lib/faraday/middlewares/build_service/ssh_signer.rb
Instance Method Summary collapse
-
#generate ⇒ Object
Generate Signature.
-
#initialize(challenge_params, headers, credentials: {}, challenge_context: {}) ⇒ SshSigner
constructor
Initalizes the signer.
-
#ssh_sign(payload, realm, ssh_key) ⇒ Object
Sign a given payload pertaining to a specific realm.
Methods included from HttpHelpers
#pairs_to_payload, #pairs_to_quoted_string, #parse_authorization_header, #parse_list_header, #unquote
Constructor Details
#initialize(challenge_params, headers, credentials: {}, challenge_context: {}) ⇒ SshSigner
Initalizes the signer
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/faraday/middlewares/build_service/ssh_signer.rb', line 20 def initialize(challenge_params, headers, credentials: {}, challenge_context: {}) @headers = headers @credentials = credentials # The challenge is formatted according to RFC7235 section 4.1 # https://datatracker.ietf.org/doc/html/rfc7235#section-4.1 # www-authenticate: 'Signature realm="Use your developer account",headers="(created)"' # # This signer does only process Signature challenge @challenge_params = challenge_params created = Time.now.to_i # funny tip: you can bucket requests into 5 min blocks and the signature # will still be constant & valid. # created = created - (created % 300) @challenge_context = { created: created }.update(challenge_context) # As per RFC Signing HTTP Messages draft-cavage-http-signatures-12 section # 2.3. At the very least `(created)` header (and `(request-target)` but Build # Service implementation doesn't include it) should be provided to the # signature string. # (created) represents a unix timestamp that must be provided into the # final signature for the server to validate. end |
Instance Method Details
#generate ⇒ Object
Generate Signature
as per datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1
a ‘signing_string` is created based on the headers that the challenge demands the format for such signing_string (also referred as payload) is:
(calculated header): %value%n host: your.host
the n is to show that a newline is needed there. Then this full string is passed to the signing algorithm, in the build service case is:
ssh-keygen -Y sign -f “%path-to-privk%” -n “%realm%” -q <<< “signing string”
the result of that signature is in OpenSSH Signature format. Delimiters are removed & newlines stripped.
This final result along with the parameters used to generate the signature are concatenated in an HTTP List format and returned to the caller to use.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/faraday/middlewares/build_service/ssh_signer.rb', line 67 def generate # first listify the headers in the challenge. As the spec @challenge_params[:headers] ||= "(created)" headers = @challenge_params[:headers].split # Iterate through it to build the singing string payload_pairs = headers.map do |header| value = if header.include?("(") # if it's a calculated header (surrounded by parenthesis), look for it # in the challenge_context @challenge_context[header.tr("()", "").to_sym] else # else look for it in the given headers @headers[header] end [header, value] end # Build the signing string signing_string = pairs_to_payload(payload_pairs) # remove parenthesis from the headers to append to the signature parameters payload_pairs = payload_pairs.map do |key, value| key = key.tr("()", "") [key, value] end # sign with the provided realm (or empty if not present) signature = ssh_sign(signing_string, @challenge_params[:realm] || "", @credentials[:ssh_key]) # Build all signature parameters signature_parameters = [ ["keyId", @credentials[:username]], ["algorithm", "ssh"], ["signature", signature], ["headers", @challenge_params[:headers]], *payload_pairs ] # build the HTTP quoted list & return pairs_to_quoted_string(signature_parameters) end |
#ssh_sign(payload, realm, ssh_key) ⇒ Object
Sign a given payload pertaining to a specific realm
The SSH Privatekey will briefly be materialized as a file in the filesystem for SSH to be able to pick it up and sign the payload.
Then it’ll be removed
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/faraday/middlewares/build_service/ssh_signer.rb', line 122 def ssh_sign(payload, realm, ssh_key) Tempfile.create("ssh-key", Rails.root.join("tmp/")) do |file| file.write(ssh_key) file.flush # force IO flush cmd = ["ssh-keygen", "-Y", "sign", "-f", file.path.to_s, "-q", "-n", realm] stdout, stderr, process_status = Open3.capture3({}, *cmd, stdin_data: payload) raise "cannot sign: #{stderr}" unless process_status.to_i.zero? # remove the surrounding --- blocks & join in one single line signature = stdout.split("\n").slice(1..-2).join.presence # this should never happen, ssh-keygen ALWAYS returns an armored format or fail unless signature raise "cannot sign: bad output" end signature end end |