ActivityPub implementation - Could not refresh public key

I’m trying to implement an AP server in Lua, but I’m stuck with the error "{“error”:"Could not refresh public key https://wildleague.org/community/ropoko/json#main-key"}"

here’s my code:

local utc_date = os.date('!%a, %d %b %Y %H:%M:%S GMT')

post.publicKey = {
	id = 'https://wildleague.org/community/ropoko/json' .. '#main-key',
	owner = 'https://wildleague.org/community/ropoko/json',
	publicKeyPem = user.public_key
}

post = json.encode(post)

local keypair = rsa:new({ private_key = user.private_key, algorithm = 'sha256', key_type = rsa.KEY_TYPE.PKCS8 })

local signed_string = "(request-target): post /inbox\nhost: mastodon.social\ndate: " .. utc_date
local signature = sha.bin_to_base64(sha.hex_to_bin(keypair:sign(signed_string)))

local headers = {
	['Host'] = 'mastodon.social',
	['Date'] = utc_date,
	['Digest'] = 'SHA-256=' .. sha.bin_to_base64(sha.hex_to_bin(sha.sha256(post))),
	['Signature'] = 'keyId="https://wildleague.org/community/ropoko/json#main-key",headers="(request-target) date digest host",signature="' .. signature .. '"',
	['Accept'] = 'application/activity+json',
}

https.request({
	url = 'https://mastodon.social/inbox',
	method = 'POST',
	headers = headers,
	source = ltn12.source.string(post),
	sink = ltn12.sink.table(response)
})

any ideas?

With the mentioned actor, it should return a valid ActivityPub media type in the Content-Type header, whereas the aforementioned URL is only presented with an application/json media type instead, even when an ActivityPub type is requested (in the Accept header).

There was a recent scramble of common server implementations tightening down their permissiveness with Content-Type, in order to prevent user-uploaded content (such as JSON files, or even plain text files) being indistinguishable as an authentic same-origin validated object (allowing impersonated posts of other local users, via an upload, etc). Therefore anything that worked a month ago with most implementations, probably won’t work anymore, unless the aforementioned changes are made (present valid ActivityPub content with a valid ActivityPub media type, not just application/json).

It worked, but now I got a different error:

{"error":"Verification failed for ropoko@wildleague.org https://wildleague.org/community/ropoko/json using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)","signed_string":"(request-target): post /inbox\ndate: Wed, 21 Feb 2024 14:07:43 GMT\ndigest: SHA-256=EJ60y5s7KkVLmHQj7gD7f0k6sD/fjk0XYqTlYUbk2Sw=\nhost: mastodon.gamedev.place","signature":"VaOcuQK1zdOKi759K0YvCtjFSAh+i5sD3lxwyzkvxhULjJCbTAtIA0ZAW3JPmSb7fLU5p+1rU3wVh9X1pAAOCrTWy7l919nctY7h+tqdVMxGq09El/ScDfYPRedvonkxDudiywc4uWODplHBa4DhRPjI/VDaCqVkCdgj2vIIc7gSOo4O6WJyGEg7UJYp9EANqk0sGyjQBIL1uyvN5wjfm6U7D363TndFBU5jWyXCL0a0rBLpUiP78Spl2Dl28zUIepZMaAfZ64z/gPMqm/5sfy0pQrbH6cxznzUqxi+Aspo4uwQr9bsYrERLnLpdsrA1jQfoM3z0q8CgqDrFTQ=="}

Your headers include a Digest, but your signed string does not include one. You can see the signed string Mastodon is using for comparison here:

"signed_string":"(request-target): post /inbox\ndate: Wed, 21 Feb 2024 14:07:43 GMT\ndigest: SHA-256=EJ60y5s7KkVLmHQj7gD7f0k6sD/fjk0XYqTlYUbk2Sw=\nhost: mastodon.gamedev.place"

but your signed string is generated as follows:

local signed_string = "(request-target): post /inbox\nhost: mastodon.social\ndate: " .. utc_date

Also, your hard-coded signed_string has the headers in the wrong order, they’re not sorted as required per spec.

Just added the digest on the signed string and sorted the headers:

local post_digest = sha.bin_to_base64(sha.hex_to_bin(sha.sha256(post)))

local keypair = rsa:new({ private_key = user.private_key, algorithm = 'sha256', key_type = rsa.KEY_TYPE.PKCS8 })

local signed_string = "(request-target): post /inbox\nhost: mastodon.gamedev.place\ndate: " .. utc_date .. '\ndigest: SHA-256=' .. post_digest
local signature = sha.bin_to_base64(sha.hex_to_bin(keypair:sign(signed_string)))

local headers = {
	['Host'] = 'mastodon.gamedev.place',
	['Date'] = utc_date,
	['Digest'] = 'SHA-256=' .. post_digest,
	['Signature'] = 'keyId="https://wildleague.org/community/ropoko/json#main-key",headers="(request-target) host date digest",signature="' .. signature .. '"',
	['Content-Type'] = 'application/ld+json',
}

I’m getting the same error:

{"error":"Verification failed for ropoko@wildleague.org https://wildleague.org/community/ropoko/json using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)","signed_string":"(request-target): post /inbox\nhost: mastodon.gamedev.place\ndate: Wed, 21 Feb 2024 17:27:41 GMT\ndigest: SHA-256=4mhTBtnbEu5YLbLqlKuDDLR8Arx0UabaOGojhq+abx8=","signature":"Df9IRLeLUw0Opv6K5YcNxnue7GuY3enM2IGGz9XwNSnofnoISVFdygr8hPlA6HsjhSJwpSUqmQ9AtY8UTqLHWhOyayihChRTG3YfdAPpU1g8iw311tAMuVsywiE5pOpYOCdkD1cITw/fa6KtiOr6TChfSB08EzRN2d3OXyhzH1D8KdIcUd7irjzwRpwWfb3Vsf4CDQLD2SiDPZ4KL9GSwD4qkPtLO8ty47F5HRiEHQny3Q9SSnfKfZmdXM4dLs0bOcAoc8tY0d3kHrxrWEbH4jX7CzD3ZxUgYscQnHf6a3RNNpVPmfpb36eVPlZ/aifbQdpvdKhWI4/uZuJLJqs="}

I’m using this document for reference: Security - Mastodon documentation

Just to update here the current state of the implementation I have with the help of @arcanicanis

post = json.encode(post)

local post_digest = sha.bin_to_base64(sha.hex_to_bin(sha.sha256(post)))
local keypair = rsa:new({ private_key = user.private_key, algorithm = 'sha256', key_type = rsa.KEY_TYPE.PKCS1 })

local signed_string = "(request-target): post /inbox\nhost: test.arcanican.is\ndate: " .. utc_date .. '\ncontent-type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"' .. '\ndigest: SHA-256=' .. post_digest

local signature = sha.bin_to_base64(sha.hex_to_bin(keypair:sign(signed_string)))

local headers = {
	['Host'] = 'test.arcanican.is',
	['Date'] = utc_date,
	['Content-Type'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
	['Digest'] = 'SHA-256=' .. post_digest,
	['Signature'] = 'keyId="https://wildleague.org/community/ropoko/json#main-key",algorithm="rsa-sha256",headers="(request-target) host date content-type digest",signature="' .. signature .. '"',
	['Content-Length'] = #post
}

I added Content-Length and Content-Type to the request also created a new key in the format pkcs1 (the older key was generated using pkcs8 format)