Let users upload their OpenPGP public key, and serve WKD response #205

Merged
raucao merged 16 commits from feature/191-gpg_keys_wkd into master 2024-10-14 14:08:32 +00:00
5 changed files with 135 additions and 3 deletions
Showing only changes of commit 4a677178e8 - Show all commits

View File

@ -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
raucao marked this conversation as resolved
Review

Are any user emails other than the Kosmos address cleared somewhere in the process or are they exposed here?

Found this in the spec under "Security Considerations":

The mail provider MUST make sure to publish a key in a way that only
the mail address belonging to the requested user is part of the User
ID packets included in the returned key. Other User ID packets and
their associated binding signatures MUST be removed before
publication.

Are any user emails other than the Kosmos address cleared somewhere in the process or are they exposed here? Found this [in the spec](https://www.ietf.org/archive/id/draft-koch-openpgp-webkey-service-18.html#name-security-considerations) under "Security Considerations": > The mail provider MUST make sure to publish a key in a way that only the mail address belonging to the requested user is part of the User ID packets included in the returned key. Other User ID packets and their associated binding signatures MUST be removed before publication.
Review

Good catch. Currently, it offers whatever key is uploaded in its entirety, i.e. including any UIDs that do not contain the user's account address. I'll open a new issue for it, since it's a bit of an edge case IMO.

Good catch. Currently, it offers whatever key is uploaded in its entirety, i.e. including any UIDs that do not contain the user's account address. I'll open a new issue for it, since it's a bit of an edge case IMO.
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

View File

@ -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'

13
db/seeds/admin.asc Normal file
View File

@ -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-----

Binary file not shown.

View File

@ -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