Add support for expires in Linked Data Signatures and Object Integrity Proofs (#39701)

This commit is contained in:
Claire
2026-07-03 11:20:35 +02:00
committed by GitHub
parent 2e486d29b5
commit 0ec9ef12d5
5 changed files with 172 additions and 24 deletions

View File

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

View File

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

View File

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

View File

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

View File

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