mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-12 09:46:09 -05:00
Add support for expires in Linked Data Signatures and Object Integrity Proofs (#39701)
This commit is contained in:
@@ -122,10 +122,10 @@ module JsonLdHelper
|
||||
graph.dump(:normalize)
|
||||
end
|
||||
|
||||
def compact(json)
|
||||
compacted = JSON::LD::API.compact(json.without('signature'), full_context, documentLoader: method(:load_jsonld_context))
|
||||
compacted['signature'] = json['signature']
|
||||
compacted
|
||||
def compact(json, context = full_context)
|
||||
JSON::LD::API.compact(json.without('signature'), context, documentLoader: method(:load_jsonld_context)).tap do |compacted|
|
||||
compacted['signature'] = json['signature']
|
||||
end
|
||||
end
|
||||
|
||||
def unsupported_jsonld_features?(json)
|
||||
|
||||
@@ -14,9 +14,13 @@ class ActivityPub::LinkedDataSignature
|
||||
return unless @json['signature'].is_a?(Hash)
|
||||
return if unsupported_jsonld_features?(@json)
|
||||
|
||||
type = @json['signature']['type']
|
||||
creator_uri = @json['signature']['creator']
|
||||
signature = @json['signature']['signatureValue']
|
||||
signature_options = compact(@json['signature'].merge({ '@context' => CONTEXT }), CONTEXT)
|
||||
|
||||
type = signature_options['type']
|
||||
creator_uri = signature_options['creator']
|
||||
signature = signature_options['signatureValue']
|
||||
|
||||
return if signature_options['expires']&.to_datetime&.past?
|
||||
|
||||
return unless type == 'RsaSignature2017'
|
||||
|
||||
@@ -24,7 +28,7 @@ class ActivityPub::LinkedDataSignature
|
||||
keypair = ActivityPub::FetchRemoteKeyService.new.call(creator_uri) if keypair&.public_key.blank?
|
||||
return if keypair.nil? || !keypair.usable? || keypair.type != 'rsa'
|
||||
|
||||
options_hash = hash(@json['signature'].without('type', 'id', 'signatureValue').merge('@context' => CONTEXT))
|
||||
options_hash = hash(signature_options.without('type', 'id', 'signatureValue'))
|
||||
document_hash = hash(@json.without('signature'))
|
||||
to_be_verified = options_hash + document_hash
|
||||
|
||||
@@ -33,13 +37,14 @@ class ActivityPub::LinkedDataSignature
|
||||
false
|
||||
end
|
||||
|
||||
def sign!(creator, sign_with: nil)
|
||||
def sign!(creator, sign_with: nil, expires_in: 2.days)
|
||||
keypair = sign_with.presence || creator.keypair(type: :rsa)
|
||||
|
||||
options = {
|
||||
'type' => 'RsaSignature2017',
|
||||
'creator' => keypair.full_uri,
|
||||
'created' => Time.now.utc.iso8601,
|
||||
'expires' => expires_in.from_now.utc.iso8601,
|
||||
}
|
||||
|
||||
options_hash = hash(options.without('type', 'id', 'signatureValue').merge('@context' => CONTEXT))
|
||||
|
||||
@@ -17,9 +17,8 @@ class ActivityPub::ObjectIntegrityProof
|
||||
proof = @json['proof']
|
||||
return unless proof['type'].present? && proof['verificationMethod'].present? && proof['proofPurpose'].present?
|
||||
|
||||
return if proof_purpose != proof['proofPurpose']
|
||||
|
||||
return if proof['type'] != 'DataIntegrityProof'
|
||||
return if proof_purpose != proof['proofPurpose'] || proof['type'] != 'DataIntegrityProof'
|
||||
return if proof['expires']&.to_datetime&.past?
|
||||
|
||||
cryptosuite = proof['cryptosuite']
|
||||
key_uri = proof['verificationMethod']
|
||||
|
||||
@@ -38,6 +38,38 @@ RSpec.describe ActivityPub::LinkedDataSignature do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signature matches and has an expiration date set in the future' do
|
||||
let(:raw_signature) do
|
||||
{
|
||||
'creator' => keyid,
|
||||
'created' => '2017-09-23T20:21:34Z',
|
||||
'expires' => '3000-01-01T00:00:00Z',
|
||||
}
|
||||
end
|
||||
|
||||
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
|
||||
|
||||
it 'returns creator' do
|
||||
expect(subject.verify_actor!).to eq sender
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signature matches but has an expiration date set in the past' do
|
||||
let(:raw_signature) do
|
||||
{
|
||||
'creator' => keyid,
|
||||
'created' => '2017-09-23T20:21:34Z',
|
||||
'expires' => '2017-09-23T20:21:35Z',
|
||||
}
|
||||
end
|
||||
|
||||
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.verify_actor!).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when local account record is missing a public key' do
|
||||
let(:raw_signature) do
|
||||
{
|
||||
|
||||
@@ -45,23 +45,135 @@ RSpec.describe ActivityPub::ObjectIntegrityProof do
|
||||
JSON
|
||||
end
|
||||
|
||||
before do
|
||||
asn1 = OpenSSL::ASN1::Sequence(
|
||||
[
|
||||
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('ED25519')]),
|
||||
OpenSSL::ASN1::BitString(Multibase.decode_multicodec('z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2')[1]),
|
||||
]
|
||||
)
|
||||
keypair = OpenSSL::PKey.read(asn1.to_der)
|
||||
|
||||
Fabricate(:keypair, account: actor, uri: 'https://server.example/users/alice#ed25519-key', type: :ed25519, public_key: keypair.public_to_pem)
|
||||
end
|
||||
|
||||
context 'when the signature is correct' do
|
||||
before do
|
||||
asn1 = OpenSSL::ASN1::Sequence(
|
||||
[
|
||||
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('ED25519')]),
|
||||
OpenSSL::ASN1::BitString(Multibase.decode_multicodec('z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2')[1]),
|
||||
]
|
||||
)
|
||||
keypair = OpenSSL::PKey.read(asn1.to_der)
|
||||
|
||||
Fabricate(:keypair, account: actor, uri: 'https://server.example/users/alice#ed25519-key', type: :ed25519, public_key: keypair.public_to_pem)
|
||||
end
|
||||
|
||||
it 'returns the actor' do
|
||||
expect(described_class.new(json).verify_actor!).to eq actor
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the signature is incorrect' do
|
||||
before do
|
||||
Fabricate(:keypair, account: actor, uri: 'https://server.example/users/alice#ed25519-key', type: :ed25519, public_key: OpenSSL::PKey.generate_key('Ed25519').public_to_pem)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(described_class.new(json).verify_actor!).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the signature is correct and has an expiration date set in the future' do
|
||||
let(:json) do
|
||||
JSON.parse(<<~JSON)
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/data-integrity/v2"
|
||||
],
|
||||
"id": "https://server.example/activities/1",
|
||||
"type": "Create",
|
||||
"actor": "https://server.example/users/alice",
|
||||
"object": {
|
||||
"id": "https://server.example/objects/1",
|
||||
"type": "Note",
|
||||
"attributedTo": "https://server.example/users/alice",
|
||||
"content": "Hello world"
|
||||
},
|
||||
"proof": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/data-integrity/v2"
|
||||
],
|
||||
"type": "DataIntegrityProof",
|
||||
"cryptosuite": "eddsa-jcs-2022",
|
||||
"verificationMethod": "https://server.example/users/alice#ed25519-key",
|
||||
"proofPurpose": "assertionMethod",
|
||||
"proofValue": "z4bp44KNvTEeb7h8gY7tpbF9EaJxzbcYc6JNPxMv1wdWDakBt2vVEC3UzjctghA5XP2NevDnDCPzph2oNNb3qRJ8K",
|
||||
"created": "2023-02-24T23:36:38Z",
|
||||
"expires": "3000-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
end
|
||||
|
||||
before do
|
||||
asn1 = OpenSSL::ASN1::Sequence(
|
||||
[
|
||||
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('ED25519')]),
|
||||
OpenSSL::ASN1::BitString(Multibase.decode_multicodec('z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2')[1]),
|
||||
]
|
||||
)
|
||||
keypair = OpenSSL::PKey.read(asn1.to_der)
|
||||
|
||||
Fabricate(:keypair, account: actor, uri: 'https://server.example/users/alice#ed25519-key', type: :ed25519, public_key: keypair.public_to_pem)
|
||||
end
|
||||
|
||||
it 'returns the actor' do
|
||||
expect(described_class.new(json).verify_actor!).to eq actor
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the signature is correct and has an expiration date set in the past' do
|
||||
let(:json) do
|
||||
JSON.parse(<<~JSON)
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/data-integrity/v2"
|
||||
],
|
||||
"id": "https://server.example/activities/1",
|
||||
"type": "Create",
|
||||
"actor": "https://server.example/users/alice",
|
||||
"object": {
|
||||
"id": "https://server.example/objects/1",
|
||||
"type": "Note",
|
||||
"attributedTo": "https://server.example/users/alice",
|
||||
"content": "Hello world"
|
||||
},
|
||||
"proof": {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/data-integrity/v2"
|
||||
],
|
||||
"type": "DataIntegrityProof",
|
||||
"cryptosuite": "eddsa-jcs-2022",
|
||||
"verificationMethod": "https://server.example/users/alice#ed25519-key",
|
||||
"proofPurpose": "assertionMethod",
|
||||
"proofValue": "z25og1nNL6TLjQ8fHK4WmzWZzjb8BhcVvaoXToxE9qpcB6iqCWmWhmSAifmcWA11rtdomgGpkRNeEPDMXWJY5BDVQ",
|
||||
"created": "2023-02-24T23:36:38Z",
|
||||
"expires": "2023-02-24T23:36:39Z"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
end
|
||||
|
||||
before do
|
||||
asn1 = OpenSSL::ASN1::Sequence(
|
||||
[
|
||||
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('ED25519')]),
|
||||
OpenSSL::ASN1::BitString(Multibase.decode_multicodec('z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2')[1]),
|
||||
]
|
||||
)
|
||||
keypair = OpenSSL::PKey.read(asn1.to_der)
|
||||
|
||||
Fabricate(:keypair, account: actor, uri: 'https://server.example/users/alice#ed25519-key', type: :ed25519, public_key: keypair.public_to_pem)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(described_class.new(json).verify_actor!).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'verify_eddsa_jcs_2022' do
|
||||
|
||||
Reference in New Issue
Block a user