diff --git a/app/controllers/web_key_directory_controller.rb b/app/controllers/web_key_directory_controller.rb new file mode 100644 index 0000000..ffd2844 --- /dev/null +++ b/app/controllers/web_key_directory_controller.rb @@ -0,0 +1,34 @@ +class WebKeyDirectoryController < WellKnownController + before_action :allow_cross_origin_requests, only: [ :show ] + + # /.well-known/openpgpkey/hu/:hashed_username(.txt) + def show + @user = User.find_by(cn: params[:l]) + + if @user.nil? || + @user.pgp_pubkey.empty? || + !@user.pgp_pubkey_contains_user_address? + http_status :not_found and return + end + + if params[:hashed_username] != @user.wkd_hash + http_status :unprocessable_entity and return + end + + respond_to do |format| + format.text do + response.headers['Content-Type'] = 'text/plain' + render plain: @user.pgp_pubkey + end + + format.any do + key = @user.gnupg_key.export + send_data key, filename: "#{@user.wkd_hash}.pem", + type: "application/octet-stream" + end + end + end + + private + +end diff --git a/config/routes.rb b/config/routes.rb index 07e1ffe..9751f3d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -70,10 +70,11 @@ Rails.application.routes.draw do get '.well-known/webfinger', to: 'webfinger#show' get '.well-known/nostr', to: 'well_known#nostr' - get '.well-known/lnurlp/:username', to: 'lnurlpay#index', as: 'lightning_address' - get '.well-known/keysend/:username', to: 'lnurlpay#keysend', as: 'lightning_address_keysend' + get '.well-known/lnurlp/:username', to: 'lnurlpay#index', as: :lightning_address + get '.well-known/keysend/:username', to: 'lnurlpay#keysend', as: :lightning_address_keysend + get '.well-known/openpgpkey/hu/:hashed_username(.:format)', to: 'web_key_directory#show', as: :wkd_key - get 'lnurlpay/:username/invoice', to: 'lnurlpay#invoice', as: 'lnurlpay_invoice' + get 'lnurlpay/:username/invoice', to: 'lnurlpay#invoice', as: :lnurlpay_invoice post 'webhooks/lndhub', to: 'webhooks#lndhub' diff --git a/db/seeds/admin.asc b/db/seeds/admin.asc new file mode 100644 index 0000000..d3b65a1 --- /dev/null +++ b/db/seeds/admin.asc @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZvGiUxYJKwYBBAHaRw8BAQdARPZXLqyB3nylJuzuARlOJxqc9mchMKHI4Cy+ +hPWlzja0GEFkbWluIDxhZG1pbkBrb3Ntb3Mub3JnPoiZBBMWCgBBFiEE0pie1+fG +ImdZwzGnwgEYSg8AulYFAmbxolMCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsCBBYC +AwECHgcCF4AACgkQwgEYSg8AulaldAEA7yzh7XRCdIJDHgLUvKHsy2NnyLaDD1Tl +hyZWbl5og0IBAJAQ2Dm82YXMdUK3X1OGlK8KH5O4E5lSFY4+8/xx0UEJuDgEZvGi +UxIKKwYBBAGXVQEFAQEHQJc8pzzeIF7Hm5z1eseRAqGvFa+V1BIDf+1XQzuJhhxi +AwEIB4h+BBgWCgAmFiEE0pie1+fGImdZwzGnwgEYSg8AulYFAmbxolMCGwwFCQWj +moAACgkQwgEYSg8AulbLtgEApZvuDqSP77lrl1jmtCAJEEZk/ofsRFkf1g3U3Zhm +9PcA/1+AbcyqjLTcqIPjHmZyGEPiaAvEsBzbPKEPiL3JYhkG +=45sx +-----END PGP PUBLIC KEY BLOCK----- diff --git a/spec/fixtures/files/pgp_key_valid_jimmy.pem b/spec/fixtures/files/pgp_key_valid_jimmy.pem new file mode 100644 index 0000000..44c792b Binary files /dev/null and b/spec/fixtures/files/pgp_key_valid_jimmy.pem differ diff --git a/spec/requests/web_key_directory_spec.rb b/spec/requests/web_key_directory_spec.rb new file mode 100644 index 0000000..627c89a --- /dev/null +++ b/spec/requests/web_key_directory_spec.rb @@ -0,0 +1,84 @@ +require 'rails_helper' + +RSpec.describe "OpenPGP Web Key Directory", type: :request do + describe "non-existent user" do + it "returns a 404 status" do + get "/.well-known/openpgpkey/hu/fmb8gw3n4zdj4xpwaziki4mwcxr1368i?l=aristotle" + expect(response).to have_http_status(:not_found) + end + end + + describe "user without pubkey" do + let(:user) { create :user, cn: 'bernd', ou: 'kosmos.org' } + + it "returns a 404 status" do + get "/.well-known/openpgpkey/hu/kp95h369c89sx8ia1hn447i868nqyz4t?l=bernd" + expect(response).to have_http_status(:not_found) + end + end + + describe "user with pubkey" do + let(:alice) { create :user, id: 2, cn: "alice", email: "alice@example.com" } + let(:jimmy) { create :user, id: 3, cn: "jimmy", email: "jimmy@example.com" } + let(:valid_key_alice) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_alice.asc") } + let(:valid_key_jimmy) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_jimmy.asc") } + let(:fingerprint_alice) { "EB85BB5FA33A75E15E944E63F231550C4F47E38E" } + let(:fingerprint_jimmy) { "316BF516236DAF77236B15F6057D93972FB862C3" } + let(:invalid_key) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_invalid.asc") } + + before do + GPGME::Key.import(valid_key_alice) + GPGME::Key.import(valid_key_jimmy) + alice.update pgp_fpr: fingerprint_alice + jimmy.update pgp_fpr: fingerprint_jimmy + end + + after do + alice.gnupg_key.delete! + jimmy.gnupg_key.delete! + end + + describe "pubkey does not contain user address" do + before do + allow_any_instance_of(User).to receive(:ldap_entry) + .and_return({ pgp_key: valid_key_alice }) + end + + it "returns a 404 status" do + get "/.well-known/openpgpkey/hu/kei1q4tipxxu1yj79k9kfukdhfy631xe?l=alice" + expect(response).to have_http_status(:not_found) + end + end + + describe "pubkey contains user address" do + before do + allow_any_instance_of(User).to receive(:ldap_entry) + .and_return({ pgp_key: valid_key_jimmy }) + end + + it "returns the pubkey in binary format" do + get "/.well-known/openpgpkey/hu/yuca4ky39mhwkjo78qb8zjgbfj1hg3yf?l=jimmy" + expect(response).to have_http_status(:ok) + expect(response.headers['Content-Type']).to eq("application/octet-stream") + expected_binary_data = File.binread("#{Rails.root}/spec/fixtures/files/pgp_key_valid_jimmy.pem") + expect(response.body).to eq(expected_binary_data) + end + + context "with .txt extension" do + it "returns the pubkey as ASCII Armor plain text" do + get "/.well-known/openpgpkey/hu/yuca4ky39mhwkjo78qb8zjgbfj1hg3yf.txt?l=jimmy" + expect(response).to have_http_status(:ok) + expect(response.body).to eq(valid_key_jimmy) + expect(response.headers['Content-Type']).to eq("text/plain") + end + end + + context "invalid URL" do + it "returns a 422 status" do + get "/.well-known/openpgpkey/hu/123456abcdef?l=alice" + expect(response).to have_http_status(:not_found) + end + end + end + end +end