From 3be52d7968a30bedd5840bf09f05119ad010e780 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 18 Jun 2026 20:01:41 +0200 Subject: [PATCH] Add support for importing Ed25519 public keys --- app/lib/activitypub/linked_data_signature.rb | 2 +- app/models/keypair.rb | 7 +- .../activitypub/process_account_service.rb | 8 +++ .../fetch_remote_key_service_spec.rb | 64 ++++++++++++++++++- 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/app/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb index d2533e0364a..e0c0606e446 100644 --- a/app/lib/activitypub/linked_data_signature.rb +++ b/app/lib/activitypub/linked_data_signature.rb @@ -22,7 +22,7 @@ class ActivityPub::LinkedDataSignature keypair = Keypair.from_keyid(creator_uri) keypair = ActivityPub::FetchRemoteKeyService.new.call(creator_uri) if keypair&.public_key.blank? - return if keypair.nil? || !keypair.usable? + return if keypair.nil? || !keypair.usable? || keypair.type != 'rsa' options_hash = hash(@json['signature'].without('type', 'id', 'signatureValue').merge('@context' => CONTEXT)) document_hash = hash(@json.without('signature')) diff --git a/app/models/keypair.rb b/app/models/keypair.rb index 80c313f4df7..891d8dca246 100644 --- a/app/models/keypair.rb +++ b/app/models/keypair.rb @@ -25,7 +25,10 @@ class Keypair < ApplicationRecord belongs_to :account - enum :type, { rsa: 0 } + enum :type, { + rsa: 0, + ed25519: 1, + }, validate: true attr_accessor :require_private_key @@ -46,6 +49,8 @@ class Keypair < ApplicationRecord case type when 'rsa' OpenSSL::PKey::RSA.new(private_key || public_key) + when 'ed25519' + OpenSSL::PKey.read(private_key || public_key) end end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 045d3019cc7..e4bebabff07 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -341,6 +341,14 @@ class ActivityPub::ProcessAccountService < BaseService case tag when :'rsa-pub' [:rsa, OpenSSL::PKey::RSA.new(key).to_pem] + when :'ed25519-pub' + asn1 = OpenSSL::ASN1::Sequence( + [ + OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('ED25519')]), + OpenSSL::ASN1::BitString(key), + ] + ) + [:ed25519, OpenSSL::PKey.read(asn1.to_der).public_to_pem] end rescue ArgumentError nil diff --git a/spec/services/activitypub/fetch_remote_key_service_spec.rb b/spec/services/activitypub/fetch_remote_key_service_spec.rb index 7780d0353ec..45156c4f0ec 100644 --- a/spec/services/activitypub/fetch_remote_key_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_key_service_spec.rb @@ -110,7 +110,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService do stub_request(:get, public_key_id).to_return(body: key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }).to_json, headers: { 'Content-Type': 'application/activity+json' }) end - it 'returns the nil' do + it 'returns nil' do expect(keypair).to be_nil end end @@ -127,6 +127,13 @@ RSpec.describe ActivityPub::FetchRemoteKeyService do publicKeyMultibase: 'z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK', } end + let(:ed25519_key_pem) do + <<~TEXT + -----BEGIN PUBLIC KEY----- + MCowBQYDK2VwAyEALm/M42cB3HkUiODQsXRcweM6TByfzEHGO9ND274JcOY= + -----END PUBLIC KEY----- + TEXT + end let(:rsa_key_id) { 'https://example.com/alice#rsa-key' } let(:actor_rsa_key) { rsa_multikey } @@ -218,10 +225,63 @@ RSpec.describe ActivityPub::FetchRemoteKeyService do stub_request(:get, rsa_key_id).to_return(body: rsa_multikey.merge({ '@context': ['https://www.w3.org/ns/cid/v1'] }).to_json, headers: { 'Content-Type': 'application/activity+json' }) end - it 'returns the nil' do + it 'returns nil' do expect(keypair).to be_nil end end + + context 'with an Ed25519 key' do + let(:keypair) { subject.call(ed25519_key_id) } + + context 'when the key is a sub-object from the actor' do + before do + stub_request(:get, ed25519_key_id).to_return(body: actor.to_json, headers: { 'Content-Type': 'application/activity+json' }) + end + + it 'returns the expected account' do + expect(keypair.account.uri).to eq 'https://example.com/alice' + + expect(keypair) + .to have_attributes( + uri: ed25519_key_id, + type: 'ed25519', + public_key: ed25519_key_pem + ) + end + end + + context 'when the key is a separate document' do + let(:ed25519_key_id) { 'https://example.com/alice-public-key.json' } + let(:actor_ed25519_key) { ed25519_key_id } + + before do + stub_request(:get, ed25519_key_id).to_return(body: ed25519_multikey.merge({ '@context': ['https://www.w3.org/ns/cid/v1'] }).to_json, headers: { 'Content-Type': 'application/activity+json' }) + end + + it 'returns the expected account' do + expect(keypair.account.uri).to eq 'https://example.com/alice' + expect(keypair) + .to have_attributes( + uri: ed25519_key_id, + type: 'ed25519', + public_key: ed25519_key_pem + ) + end + end + + context 'when the key and owner do not match' do + let(:ed25519_key_id) { 'https://example.com/fake-public-key.json' } + let(:actor_ed25519_key) { 'https://example.com/alice-public-key.json' } + + before do + stub_request(:get, ed25519_key_id).to_return(body: ed25519_multikey.merge({ '@context': ['https://www.w3.org/ns/cid/v1'] }).to_json, headers: { 'Content-Type': 'application/activity+json' }) + end + + it 'returns nil' do + expect(keypair).to be_nil + end + end + end end end end