* Add handling of Linked Data Signatures in payloads * Add a way to sign JSON, fix canonicalization of signature options * Fix signatureValue encoding, send out signed JSON when distributing * Add missing security contextclosed-social-v3
@ -0,0 +1,56 @@ | |||||
# frozen_string_literal: true | |||||
class ActivityPub::LinkedDataSignature | |||||
include JsonLdHelper | |||||
CONTEXT = 'https://w3id.org/identity/v1' | |||||
def initialize(json) | |||||
@json = json | |||||
end | |||||
def verify_account! | |||||
return unless @json['signature'].is_a?(Hash) | |||||
type = @json['signature']['type'] | |||||
creator_uri = @json['signature']['creator'] | |||||
signature = @json['signature']['signatureValue'] | |||||
return unless type == 'RsaSignature2017' | |||||
creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account) | |||||
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri) | |||||
return if creator.nil? | |||||
options_hash = hash(@json['signature'].without('type', 'id', 'signatureValue').merge('@context' => CONTEXT)) | |||||
document_hash = hash(@json.without('signature')) | |||||
to_be_verified = options_hash + document_hash | |||||
if creator.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), to_be_verified) | |||||
creator | |||||
end | |||||
end | |||||
def sign!(creator) | |||||
options = { | |||||
'type' => 'RsaSignature2017', | |||||
'creator' => [ActivityPub::TagManager.instance.uri_for(creator), '#main-key'].join, | |||||
'created' => Time.now.utc.iso8601, | |||||
} | |||||
options_hash = hash(options.without('type', 'id', 'signatureValue').merge('@context' => CONTEXT)) | |||||
document_hash = hash(@json.without('signature')) | |||||
to_be_signed = options_hash + document_hash | |||||
signature = Base64.strict_encode64(creator.keypair.sign(OpenSSL::Digest::SHA256.new, to_be_signed)) | |||||
@json.merge('@context' => merge_context(@json['@context'], CONTEXT), 'signature' => options.merge('signatureValue' => signature)) | |||||
end | |||||
private | |||||
def hash(obj) | |||||
Digest::SHA256.hexdigest(canonicalize(obj)) | |||||
end | |||||
end |
@ -0,0 +1,4 @@ | |||||
# frozen_string_literal: true | |||||
require_relative '../../lib/json_ld/identity' | |||||
require_relative '../../lib/json_ld/security' |
@ -0,0 +1,86 @@ | |||||
# -*- encoding: utf-8 -*- | |||||
# frozen_string_literal: true | |||||
# This file generated automatically from https://w3id.org/identity/v1 | |||||
require 'json/ld' | |||||
class JSON::LD::Context | |||||
add_preloaded("https://w3id.org/identity/v1") do | |||||
new(processingMode: "json-ld-1.0", term_definitions: { | |||||
"Credential" => TermDefinition.new("Credential", id: "https://w3id.org/credentials#Credential", simple: true), | |||||
"CryptographicKey" => TermDefinition.new("CryptographicKey", id: "https://w3id.org/security#Key", simple: true), | |||||
"CryptographicKeyCredential" => TermDefinition.new("CryptographicKeyCredential", id: "https://w3id.org/credentials#CryptographicKeyCredential", simple: true), | |||||
"EncryptedMessage" => TermDefinition.new("EncryptedMessage", id: "https://w3id.org/security#EncryptedMessage", simple: true), | |||||
"GraphSignature2012" => TermDefinition.new("GraphSignature2012", id: "https://w3id.org/security#GraphSignature2012", simple: true), | |||||
"Group" => TermDefinition.new("Group", id: "https://www.w3.org/ns/activitystreams#Group", simple: true), | |||||
"Identity" => TermDefinition.new("Identity", id: "https://w3id.org/identity#Identity", simple: true), | |||||
"LinkedDataSignature2015" => TermDefinition.new("LinkedDataSignature2015", id: "https://w3id.org/security#LinkedDataSignature2015", simple: true), | |||||
"Organization" => TermDefinition.new("Organization", id: "http://schema.org/Organization", simple: true), | |||||
"Person" => TermDefinition.new("Person", id: "http://schema.org/Person", simple: true), | |||||
"PostalAddress" => TermDefinition.new("PostalAddress", id: "http://schema.org/PostalAddress", simple: true), | |||||
"about" => TermDefinition.new("about", id: "http://schema.org/about", type_mapping: "@id"), | |||||
"accessControl" => TermDefinition.new("accessControl", id: "https://w3id.org/permissions#accessControl", type_mapping: "@id"), | |||||
"address" => TermDefinition.new("address", id: "http://schema.org/address", type_mapping: "@id"), | |||||
"addressCountry" => TermDefinition.new("addressCountry", id: "http://schema.org/addressCountry", simple: true), | |||||
"addressLocality" => TermDefinition.new("addressLocality", id: "http://schema.org/addressLocality", simple: true), | |||||
"addressRegion" => TermDefinition.new("addressRegion", id: "http://schema.org/addressRegion", simple: true), | |||||
"cipherAlgorithm" => TermDefinition.new("cipherAlgorithm", id: "https://w3id.org/security#cipherAlgorithm", simple: true), | |||||
"cipherData" => TermDefinition.new("cipherData", id: "https://w3id.org/security#cipherData", simple: true), | |||||
"cipherKey" => TermDefinition.new("cipherKey", id: "https://w3id.org/security#cipherKey", simple: true), | |||||
"claim" => TermDefinition.new("claim", id: "https://w3id.org/credentials#claim", type_mapping: "@id"), | |||||
"comment" => TermDefinition.new("comment", id: "http://www.w3.org/2000/01/rdf-schema#comment", simple: true), | |||||
"created" => TermDefinition.new("created", id: "http://purl.org/dc/terms/created", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"), | |||||
"creator" => TermDefinition.new("creator", id: "http://purl.org/dc/terms/creator", type_mapping: "@id"), | |||||
"cred" => TermDefinition.new("cred", id: "https://w3id.org/credentials#", simple: true, prefix: true), | |||||
"credential" => TermDefinition.new("credential", id: "https://w3id.org/credentials#credential", type_mapping: "@id"), | |||||
"dc" => TermDefinition.new("dc", id: "http://purl.org/dc/terms/", simple: true, prefix: true), | |||||
"description" => TermDefinition.new("description", id: "http://schema.org/description", simple: true), | |||||
"digestAlgorithm" => TermDefinition.new("digestAlgorithm", id: "https://w3id.org/security#digestAlgorithm", simple: true), | |||||
"digestValue" => TermDefinition.new("digestValue", id: "https://w3id.org/security#digestValue", simple: true), | |||||
"domain" => TermDefinition.new("domain", id: "https://w3id.org/security#domain", simple: true), | |||||
"email" => TermDefinition.new("email", id: "http://schema.org/email", simple: true), | |||||
"expires" => TermDefinition.new("expires", id: "https://w3id.org/security#expiration", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"), | |||||
"familyName" => TermDefinition.new("familyName", id: "http://schema.org/familyName", simple: true), | |||||
"givenName" => TermDefinition.new("givenName", id: "http://schema.org/givenName", simple: true), | |||||
"id" => TermDefinition.new("id", id: "@id", simple: true), | |||||
"identity" => TermDefinition.new("identity", id: "https://w3id.org/identity#", simple: true, prefix: true), | |||||
"identityService" => TermDefinition.new("identityService", id: "https://w3id.org/identity#identityService", type_mapping: "@id"), | |||||
"idp" => TermDefinition.new("idp", id: "https://w3id.org/identity#idp", type_mapping: "@id"), | |||||
"image" => TermDefinition.new("image", id: "http://schema.org/image", type_mapping: "@id"), | |||||
"initializationVector" => TermDefinition.new("initializationVector", id: "https://w3id.org/security#initializationVector", simple: true), | |||||
"issued" => TermDefinition.new("issued", id: "https://w3id.org/credentials#issued", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"), | |||||
"issuer" => TermDefinition.new("issuer", id: "https://w3id.org/credentials#issuer", type_mapping: "@id"), | |||||
"label" => TermDefinition.new("label", id: "http://www.w3.org/2000/01/rdf-schema#label", simple: true), | |||||
"member" => TermDefinition.new("member", id: "http://schema.org/member", type_mapping: "@id"), | |||||
"memberOf" => TermDefinition.new("memberOf", id: "http://schema.org/memberOf", type_mapping: "@id"), | |||||
"name" => TermDefinition.new("name", id: "http://schema.org/name", simple: true), | |||||
"nonce" => TermDefinition.new("nonce", id: "https://w3id.org/security#nonce", simple: true), | |||||
"normalizationAlgorithm" => TermDefinition.new("normalizationAlgorithm", id: "https://w3id.org/security#normalizationAlgorithm", simple: true), | |||||
"owner" => TermDefinition.new("owner", id: "https://w3id.org/security#owner", type_mapping: "@id"), | |||||
"password" => TermDefinition.new("password", id: "https://w3id.org/security#password", simple: true), | |||||
"paymentProcessor" => TermDefinition.new("paymentProcessor", id: "https://w3id.org/payswarm#processor", simple: true), | |||||
"perm" => TermDefinition.new("perm", id: "https://w3id.org/permissions#", simple: true, prefix: true), | |||||
"postalCode" => TermDefinition.new("postalCode", id: "http://schema.org/postalCode", simple: true), | |||||
"preferences" => TermDefinition.new("preferences", id: "https://w3id.org/payswarm#preferences", type_mapping: "@vocab"), | |||||
"privateKey" => TermDefinition.new("privateKey", id: "https://w3id.org/security#privateKey", type_mapping: "@id"), | |||||
"privateKeyPem" => TermDefinition.new("privateKeyPem", id: "https://w3id.org/security#privateKeyPem", simple: true), | |||||
"ps" => TermDefinition.new("ps", id: "https://w3id.org/payswarm#", simple: true, prefix: true), | |||||
"publicKey" => TermDefinition.new("publicKey", id: "https://w3id.org/security#publicKey", type_mapping: "@id"), | |||||
"publicKeyPem" => TermDefinition.new("publicKeyPem", id: "https://w3id.org/security#publicKeyPem", simple: true), | |||||
"publicKeyService" => TermDefinition.new("publicKeyService", id: "https://w3id.org/security#publicKeyService", type_mapping: "@id"), | |||||
"rdf" => TermDefinition.new("rdf", id: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", simple: true, prefix: true), | |||||
"rdfs" => TermDefinition.new("rdfs", id: "http://www.w3.org/2000/01/rdf-schema#", simple: true, prefix: true), | |||||
"recipient" => TermDefinition.new("recipient", id: "https://w3id.org/credentials#recipient", type_mapping: "@id"), | |||||
"revoked" => TermDefinition.new("revoked", id: "https://w3id.org/security#revoked", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"), | |||||
"schema" => TermDefinition.new("schema", id: "http://schema.org/", simple: true, prefix: true), | |||||
"sec" => TermDefinition.new("sec", id: "https://w3id.org/security#", simple: true, prefix: true), | |||||
"signature" => TermDefinition.new("signature", id: "https://w3id.org/security#signature", simple: true), | |||||
"signatureAlgorithm" => TermDefinition.new("signatureAlgorithm", id: "https://w3id.org/security#signatureAlgorithm", simple: true), | |||||
"signatureValue" => TermDefinition.new("signatureValue", id: "https://w3id.org/security#signatureValue", simple: true), | |||||
"streetAddress" => TermDefinition.new("streetAddress", id: "http://schema.org/streetAddress", simple: true), | |||||
"title" => TermDefinition.new("title", id: "http://purl.org/dc/terms/title", simple: true), | |||||
"type" => TermDefinition.new("type", id: "@type", simple: true), | |||||
"url" => TermDefinition.new("url", id: "http://schema.org/url", type_mapping: "@id"), | |||||
"writePermission" => TermDefinition.new("writePermission", id: "https://w3id.org/permissions#writePermission", type_mapping: "@id"), | |||||
"xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true) | |||||
}) | |||||
end | |||||
end |
@ -0,0 +1,50 @@ | |||||
# -*- encoding: utf-8 -*- | |||||
# frozen_string_literal: true | |||||
# This file generated automatically from https://w3id.org/security/v1 | |||||
require 'json/ld' | |||||
class JSON::LD::Context | |||||
add_preloaded("https://w3id.org/security/v1") do | |||||
new(processingMode: "json-ld-1.0", term_definitions: { | |||||
"CryptographicKey" => TermDefinition.new("CryptographicKey", id: "https://w3id.org/security#Key", simple: true), | |||||
"EcdsaKoblitzSignature2016" => TermDefinition.new("EcdsaKoblitzSignature2016", id: "https://w3id.org/security#EcdsaKoblitzSignature2016", simple: true), | |||||
"EncryptedMessage" => TermDefinition.new("EncryptedMessage", id: "https://w3id.org/security#EncryptedMessage", simple: true), | |||||
"GraphSignature2012" => TermDefinition.new("GraphSignature2012", id: "https://w3id.org/security#GraphSignature2012", simple: true), | |||||
"LinkedDataSignature2015" => TermDefinition.new("LinkedDataSignature2015", id: "https://w3id.org/security#LinkedDataSignature2015", simple: true), | |||||
"LinkedDataSignature2016" => TermDefinition.new("LinkedDataSignature2016", id: "https://w3id.org/security#LinkedDataSignature2016", simple: true), | |||||
"authenticationTag" => TermDefinition.new("authenticationTag", id: "https://w3id.org/security#authenticationTag", simple: true), | |||||
"canonicalizationAlgorithm" => TermDefinition.new("canonicalizationAlgorithm", id: "https://w3id.org/security#canonicalizationAlgorithm", simple: true), | |||||
"cipherAlgorithm" => TermDefinition.new("cipherAlgorithm", id: "https://w3id.org/security#cipherAlgorithm", simple: true), | |||||
"cipherData" => TermDefinition.new("cipherData", id: "https://w3id.org/security#cipherData", simple: true), | |||||
"cipherKey" => TermDefinition.new("cipherKey", id: "https://w3id.org/security#cipherKey", simple: true), | |||||
"created" => TermDefinition.new("created", id: "http://purl.org/dc/terms/created", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"), | |||||
"creator" => TermDefinition.new("creator", id: "http://purl.org/dc/terms/creator", type_mapping: "@id"), | |||||
"dc" => TermDefinition.new("dc", id: "http://purl.org/dc/terms/", simple: true, prefix: true), | |||||
"digestAlgorithm" => TermDefinition.new("digestAlgorithm", id: "https://w3id.org/security#digestAlgorithm", simple: true), | |||||
"digestValue" => TermDefinition.new("digestValue", id: "https://w3id.org/security#digestValue", simple: true), | |||||
"domain" => TermDefinition.new("domain", id: "https://w3id.org/security#domain", simple: true), | |||||
"encryptionKey" => TermDefinition.new("encryptionKey", id: "https://w3id.org/security#encryptionKey", simple: true), | |||||
"expiration" => TermDefinition.new("expiration", id: "https://w3id.org/security#expiration", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"), | |||||
"expires" => TermDefinition.new("expires", id: "https://w3id.org/security#expiration", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"), | |||||
"id" => TermDefinition.new("id", id: "@id", simple: true), | |||||
"initializationVector" => TermDefinition.new("initializationVector", id: "https://w3id.org/security#initializationVector", simple: true), | |||||
"iterationCount" => TermDefinition.new("iterationCount", id: "https://w3id.org/security#iterationCount", simple: true), | |||||
"nonce" => TermDefinition.new("nonce", id: "https://w3id.org/security#nonce", simple: true), | |||||
"normalizationAlgorithm" => TermDefinition.new("normalizationAlgorithm", id: "https://w3id.org/security#normalizationAlgorithm", simple: true), | |||||
"owner" => TermDefinition.new("owner", id: "https://w3id.org/security#owner", type_mapping: "@id"), | |||||
"password" => TermDefinition.new("password", id: "https://w3id.org/security#password", simple: true), | |||||
"privateKey" => TermDefinition.new("privateKey", id: "https://w3id.org/security#privateKey", type_mapping: "@id"), | |||||
"privateKeyPem" => TermDefinition.new("privateKeyPem", id: "https://w3id.org/security#privateKeyPem", simple: true), | |||||
"publicKey" => TermDefinition.new("publicKey", id: "https://w3id.org/security#publicKey", type_mapping: "@id"), | |||||
"publicKeyPem" => TermDefinition.new("publicKeyPem", id: "https://w3id.org/security#publicKeyPem", simple: true), | |||||
"publicKeyService" => TermDefinition.new("publicKeyService", id: "https://w3id.org/security#publicKeyService", type_mapping: "@id"), | |||||
"revoked" => TermDefinition.new("revoked", id: "https://w3id.org/security#revoked", type_mapping: "http://www.w3.org/2001/XMLSchema#dateTime"), | |||||
"salt" => TermDefinition.new("salt", id: "https://w3id.org/security#salt", simple: true), | |||||
"sec" => TermDefinition.new("sec", id: "https://w3id.org/security#", simple: true, prefix: true), | |||||
"signature" => TermDefinition.new("signature", id: "https://w3id.org/security#signature", simple: true), | |||||
"signatureAlgorithm" => TermDefinition.new("signatureAlgorithm", id: "https://w3id.org/security#signingAlgorithm", simple: true), | |||||
"signatureValue" => TermDefinition.new("signatureValue", id: "https://w3id.org/security#signatureValue", simple: true), | |||||
"type" => TermDefinition.new("type", id: "@type", simple: true), | |||||
"xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true) | |||||
}) | |||||
end | |||||
end |
@ -0,0 +1,86 @@ | |||||
require 'rails_helper' | |||||
RSpec.describe ActivityPub::LinkedDataSignature do | |||||
include JsonLdHelper | |||||
let!(:sender) { Fabricate(:account, uri: 'http://example.com/alice') } | |||||
let(:raw_json) do | |||||
{ | |||||
'@context' => 'https://www.w3.org/ns/activitystreams', | |||||
'id' => 'http://example.com/hello-world', | |||||
} | |||||
end | |||||
let(:json) { raw_json.merge('signature' => signature) } | |||||
subject { described_class.new(json) } | |||||
describe '#verify_account!' do | |||||
context 'when signature matches' do | |||||
let(:raw_signature) do | |||||
{ | |||||
'creator' => 'http://example.com/alice', | |||||
'created' => '2017-09-23T20:21:34Z', | |||||
} | |||||
end | |||||
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) } | |||||
it 'returns creator' do | |||||
expect(subject.verify_account!).to eq sender | |||||
end | |||||
end | |||||
context 'when signature is missing' do | |||||
let(:signature) { nil } | |||||
it 'returns nil' do | |||||
expect(subject.verify_account!).to be_nil | |||||
end | |||||
end | |||||
context 'when signature is tampered' do | |||||
let(:raw_signature) do | |||||
{ | |||||
'creator' => 'http://example.com/alice', | |||||
'created' => '2017-09-23T20:21:34Z', | |||||
} | |||||
end | |||||
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => 's69F3mfddd99dGjmvjdjjs81e12jn121Gkm1') } | |||||
it 'returns nil' do | |||||
expect(subject.verify_account!).to be_nil | |||||
end | |||||
end | |||||
end | |||||
describe '#sign!' do | |||||
subject { described_class.new(raw_json).sign!(sender) } | |||||
it 'returns a hash' do | |||||
expect(subject).to be_a Hash | |||||
end | |||||
it 'contains signature context' do | |||||
expect(subject['@context']).to include('https://www.w3.org/ns/activitystreams', 'https://w3id.org/identity/v1') | |||||
end | |||||
it 'contains signature' do | |||||
expect(subject['signature']).to be_a Hash | |||||
expect(subject['signature']['signatureValue']).to be_present | |||||
end | |||||
it 'can be verified again' do | |||||
expect(described_class.new(subject).verify_account!).to eq sender | |||||
end | |||||
end | |||||
def sign(from_account, options, document) | |||||
options_hash = Digest::SHA256.hexdigest(canonicalize(options.merge('@context' => ActivityPub::LinkedDataSignature::CONTEXT))) | |||||
document_hash = Digest::SHA256.hexdigest(canonicalize(document)) | |||||
to_be_verified = options_hash + document_hash | |||||
Base64.strict_encode64(from_account.keypair.sign(OpenSSL::Digest::SHA256.new, to_be_verified)) | |||||
end | |||||
end |
@ -1,9 +1,10 @@ | |||||
require 'rails_helper' | require 'rails_helper' | ||||
RSpec.describe ActivityPub::ProcessCollectionService do | RSpec.describe ActivityPub::ProcessCollectionService do | ||||
subject { ActivityPub::ProcessCollectionService.new } | |||||
subject { described_class.new } | |||||
describe '#call' do | describe '#call' do | ||||
pending | |||||
context 'when actor is the sender' | |||||
context 'when actor differs from sender' | |||||
end | end | ||||
end | end |