From 437fadbf926f1811a0ef6b6897b3fd841c2242d5 Mon Sep 17 00:00:00 2001 From: pdp8 Date: Thu, 4 May 2023 16:43:12 +0200 Subject: signature verification --- application.rb | 193 ++++++++++++++++++++++++++++++-------------- outbox/230401T23:00:01.json | 4 - outbox/230402T23:00:01.json | 10 --- pdp8 | 4 +- 4 files changed, 133 insertions(+), 78 deletions(-) delete mode 100644 outbox/230401T23:00:01.json delete mode 100644 outbox/230402T23:00:01.json diff --git a/application.rb b/application.rb index cd8c2ea..ab42c1f 100644 --- a/application.rb +++ b/application.rb @@ -1,8 +1,10 @@ -# TODO check POST to outbox with mastinator.com require 'json' require 'net/http' require 'uri' require 'base64' +require 'securerandom' +require 'fileutils' +require 'digest/sha2' USER = "pdp8" WWW_DOMAIN = "pdp8.info" @@ -11,47 +13,64 @@ SOCIAL_DOMAIN = "social.#{WWW_DOMAIN}" ACCOUNT = "#{USER}@#{SOCIAL_DOMAIN}" SOCIAL_URL = "https://#{SOCIAL_DOMAIN}" -ACTOR = URI.join(SOCIAL_URL, USER) +ACTOR = File.join(SOCIAL_URL, USER) MATRIX = "@#{USER}:matrix.#{WWW_DOMAIN}" class Application def call(env) code = 404 - type = "application/activity+json" + type = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + response = "not allowed" + # p env["rack.input"].read + # if env["CONTENT_TYPE"] =~ /json/ + # puts env["REMOTE_ADDR"] case env['REQUEST_METHOD'] when 'POST' + input = env["rack.input"].read case env["REQUEST_URI"] when "/inbox" # receive from server + puts "POST INBOX" if verify(env) - save JSON.parse(env["rack.input"].gets), "inbox" - code = 200 - response = "OK" + begin + # unless input.match(//) + object = JSON.parse(input) + # puts object + case object["type"] + when "Create" + File.open(File.join("inbox", SecureRandom.uuid + ".json"), "w+") { |f| f.puts input } + else + puts input + end + code = 200 + response = "OK" + # end + rescue => e + # puts e.to_s + puts "Verification ERROR: " + # puts input + response = "invalid json" + end else code = 401 response = "not verified" end when "/outbox" # receive from client + puts "POST OUTBOX" # TODO auth if auth(env) - input = JSON.parse(env["rack.input"].gets) - input["type"] ? activity = input : activity = activity(input) # expand object to create activity - save activity, "outbox" - deliver ["to", "bto", "cc", "bcc", "audience"].collect { |d| activity[d] }.flatten.uniq - case activity["type"] - when "Create" - when "Update" - when "Delete" - when "Follow" - when "Remove" - when "Like" - when "Block" - when "Undo" - end + input = JSON.parse(input) + input["type"] == "Create" ? activity = input : activity = activity(input) # expand object to create activity + add_id activity + save activity # , "outbox" + FileUtils.ln_s File.join('..', path(activity)), "outbox" + code, response = deliver activity, ["to", "bto", "cc", "bcc", "audience"].collect { |d| + activity[d] + }.flatten.uniq.compact code = 200 response = "OK" else @@ -71,6 +90,7 @@ class Application code = 200 when "/#{USER}" + # TODO serve html response = File.read(USER) code = 200 @@ -81,18 +101,25 @@ class Application end end + # else + # response = "Cannot serve Content-type: " + env["CONTENT_TYPE"] + # end [code, { "Content-Type" => type }, [response]] end + def add_id object + object["id"] = File.join(SOCIAL_URL, object["type"].downcase, SecureRandom.uuid + ".json") + end + def activity object - date = Time.now.strftime("%Y-%m-%dT%H:%M:%S.%3N") - object["id"] = "TODO" + date = Time.now.strftime("%Y-%m-%dT%H:%M:%S") object["attributedTo"] = ACTOR object["published"] = date + add_id object + save object { "@context" => "https://www.w3.org/ns/activitystreams", "type" => "Create", - "id" => "https://example.net/~mallory/87374", "actor" => ACTOR, "object" => object, "published" => date, @@ -101,40 +128,62 @@ class Application } end - def save activity, dir - date = Time.now.strftime("%Y-%m-%dT%H:%M:%S.%3N") - File.open(File.join(dir, date), "w+") { |f| f.puts activity.to_json } + def path object + object["id"].sub(SOCIAL_URL, '').sub('/', '') + end + + def save object + path = path object + FileUtils.mkdir_p File.dirname(path) + File.open(path, "w+") { |f| f.puts object.to_json } + end + + def inbox uri + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + header = { 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } + request = Net::HTTP::Get.new(uri.request_uri, header) + response = http.request(request) + JSON.parse(response.body)["inbox"] end - def deliver addr - p addr + def deliver object, urls + # https://github.com/mastodon/mastodon/blob/main/app/lib/request.rb keypair = OpenSSL::PKey::RSA.new(File.read('private.pem')) - addr.each do |url| + urls.each do |url| date = Time.now.utc.httpdate uri = URI.parse(url) - signed_string = "(request-target): post /inbox\nhost: #{uri.host}\ndate: #{date}" - signed_string = keypair.sign(OpenSSL::Digest::SHA256.new, signed_string) - signature = Base64.urlsafe_encode64(signed_string).encode("UTF-8") - signed_header = 'keyId="#{url}",headers="(request-target) host date",signature="' + signature + '"' - - inbox_url = JSON.parse(Net::HTTP.get(uri, - { 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }))["inbox"] - inbox = URI.parse(inbox_url) - http = Net::HTTP.new(inbox.host, inbox.port) + + sha256 = OpenSSL::Digest::SHA256.new + body = object.to_json + digest = "SHA-256=" + sha256.base64digest(body) + + signed_string = "(request-target): post /inbox\nhost: #{uri.host}\ndate: #{date}\ndigest: #{digest}" + signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) + signed_header = 'keyId="' + ACTOR + '#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' + signature + '"' + + uri = URI.parse(get(url)["inbox"]) + http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true - # http.verify_mode = OpenSSL::SSL::VERIFY_NONE header = { 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'Host' => inbox.host, + 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'Host' => uri.host, 'Date' => date, + 'Digest' => digest, 'Signature' => signed_header, } - request = Net::HTTP::Post.new(inbox.request_uri, header) - request.body = document.to_json + puts signed_header + request = Net::HTTP::Post.new(uri.request_uri, header) + request.body = body response = http.request(request) - puts(response.body, response.code) + # puts(response.body, response.code) + puts(response.code) + # puts(response.body["signed_string"]) + # puts(response.body["signature"]) end + # [response.code, response.body] end def ordered_collection dir @@ -150,34 +199,56 @@ class Application end def verify env + # https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/signature_verification.rb + puts env + # puts env.select { |k, v| k.match(/^HTTP_/) } + # puts env["HTTP_SIGNATURE"] # .split(',').each do |pair| begin - signature_header = env["HTTP_SIGNATURE"].split(',').each do |pair| - pair.gsub('"', '').split('=') - end.to_h - key_id = signature_header['keyId'] - headers = signature_header['headers'] - signature = Base64.urlsafe_decode64(signature_header['signature'].encode("ascii-8bit")) - - uri = URI(key_id) - res = Net::HTTP.get_response(uri) - actor = JSON.parse(res.body) + signature_params = {} + env["HTTP_SIGNATURE"].split(',').each do |pair| + k, v = pair.split('=') + signature_params[k] = v.gsub('"', '') + end + + # puts signature_params + key_id = signature_params['keyId'] + headers = signature_params['headers'] + signature = Base64.decode64(signature_params['signature']) + + actor = get key_id key = OpenSSL::PKey::RSA.new(actor['publicKey']['publicKeyPem']) - comparison = headers.split(' ').map do |signed_header_name| - if signed_header_name == '(request-target)' + comparison = headers.split(' ').map do |signed_params_name| + if signed_params_name == '(request-target)' '(request-target): post /inbox' + elsif signed_params_name == 'content-type' + "#{signed_params_name}: #{env["CONTENT_TYPE"]}" else - "#{signed_header_name}: #{env["HTTP_" + signed_header_name.upcase]}" + "#{signed_params_name}: #{env["HTTP_" + signed_params_name.upcase]}" end - end.join("\n").encode("ascii-8bit") - - key.verify(OpenSSL::Digest::SHA256.new, signature, comparison) - rescue + end.join("\n") + + puts comparison + # key.verify(OpenSSL::Digest::SHA256.new, signature, comparison) + key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison) + rescue => e + puts e.class + # puts e.message false end end - def auth + def get url + uri = URI(url) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + header = { 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } + request = Net::HTTP::Get.new(uri.request_uri, header) + response = http.request(request) + JSON.parse(response.body) + end + + def auth env true end end diff --git a/outbox/230401T23:00:01.json b/outbox/230401T23:00:01.json deleted file mode 100644 index baeb7f8..0000000 --- a/outbox/230401T23:00:01.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Note", - "name": "A Simple Note" -} diff --git a/outbox/230402T23:00:01.json b/outbox/230402T23:00:01.json deleted file mode 100644 index bd50adb..0000000 --- a/outbox/230402T23:00:01.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "Note", - "name": "Another Simple Note", - "tag": [ - { - "type": "Hashtag", - "name": "#test" - } - ] -} diff --git a/pdp8 b/pdp8 index ceb25c9..7eca15f 100644 --- a/pdp8 +++ b/pdp8 @@ -33,10 +33,8 @@ } ], "publicKey": { - "@context": "https://w3id.org/security/v1", - "@type": "Key", "id": "https://social.pdp8.info/pdp8#main-key", "owner": "https://social.pdp8.info/pdp8", - "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4pZMYXoh8G+iEguDpKGD\n+eq+uDdhx/ch2x7rq00aPDDeHp40CG8bW1ZRC4WIOTUOgK4MeMDoaXT9/vWgr7xT\n/Qm95SEyZWBKqasBsp2uGkDxl23C6dB2eeshuAwt308Qzm2DeTrKPAw/XBAyWHDD\nfan2nWrtXcDJaeXhD/QE/w7Qiz5F2GCb/E/o46SwEyOJi13WxI9Jtuzh76xmwNsd\nwVWIBSu4zn0hg/wv+xtq/c/KLO4ZL54YiJXxRwrkDN7Xdnd18FwFuZ7fT8+kfiqF\nBnvle0OTKxumW46U7ivaylnqoSOvsYK6oyop/m2rl9Nh3sGdcmOsLoFVDg4gOjDf\niQIDAQAB\n-----END PUBLIC KEY-----\n" + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArDawzSl+XcJ+96sIrx+E\nsDoUQzSvoKazCgw7qOMaOGi7XxJ8riBvdRBlJ4zOEfQaxcaQgGn5JntOofqkeWvk\nIykOAzYfwY6HoUm7i1eZME2quO+CkMMq9SX9/DOqggOYtiVC9DX5FxXe5YHK7Q/n\nbo1iB6rgVS43wT0PnI6uduY4cUlvhRkX4Iht0N1GTrBlGKloRQ96KTzp+U9xF7bp\nKO87Y4yftv+d6L3ZZBfTRgWOtDXG8E4Vdvsq0aPQNBtazq0fwtBbk2G4mZtCMqyT\nvLZh8w+YPn1ICoQsKukU/q7eG29UJCz/QdZndkuv5iIm+H/c8gicGllw9rNQP2G0\nBQIDAQAB\n-----END PUBLIC KEY-----\n" } } -- cgit v1.2.3