From 5a5dd1e016bf69833eee6bad778e875cd6025060 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 5 Jan 2021 17:27:11 +0000 Subject: [PATCH 1/8] Comments --- linked_data_sig.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/linked_data_sig.py b/linked_data_sig.py index f36cc288f..abe4a9348 100644 --- a/linked_data_sig.py +++ b/linked_data_sig.py @@ -24,47 +24,65 @@ except ImportError: from pyjsonld import normalize -def _options_hash(doc): - doc = dict(doc["signature"]) +def _options_hash(doc: {}) -> str: + """Returns a hash of the signature, with a few fields removed + """ + docSig = dict(doc["signature"]) + + # remove fields from signature for k in ["type", "id", "signatureValue"]: - if k in doc: - del doc[k] - doc["@context"] = "https://w3id.org/identity/v1" + if k in docSig: + del docSig[k] + + docSig["@context"] = "https://w3id.org/identity/v1" options = { "algorithm": "URDNA2015", "format": "application/nquads" } - normalized = normalize(doc, options) + + normalized = normalize(docSig, options) h = hashlib.new("sha256") h.update(normalized.encode("utf-8")) return h.hexdigest() -def _doc_hash(doc): +def _doc_hash(doc: {}) -> str: + """Returns a hash of the ActivityPub post + """ doc = dict(doc) + + # remove the signature if "signature" in doc: del doc["signature"] + options = { "algorithm": "URDNA2015", "format": "application/nquads" } + normalized = normalize(doc, options) h = hashlib.new("sha256") h.update(normalized.encode("utf-8")) return h.hexdigest() -def verifyJsonSignature(doc: {}, publicKeyPem: str): +def verifyJsonSignature(doc: {}, publicKeyPem: str) -> bool: + """Returns True if the given ActivityPub post was sent + by an actor having the given public key + """ key = RSA.importKey(publicKeyPem) to_be_signed = _options_hash(doc) + _doc_hash(doc) signature = doc["signature"]["signatureValue"] signer = PKCS1_v1_5.new(key) # type: ignore digest = SHA256.new() digest.update(to_be_signed.encode("utf-8")) - return signer.verify(digest, base64.b64decode(signature)) # type: ignore + base64sig = base64.b64decode(signature) + return signer.verify(digest, base64sig) # type: ignore -def generateJsonSignature(doc: {}, privateKeyPem: str): +def generateJsonSignature(doc: {}, privateKeyPem: str) -> None: + """Adds a json signature to the given ActivityPub post + """ if not doc.get('actor'): return From 5c23fe5cf823e9e5167cd94eddc00faa978cf3d2 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 5 Jan 2021 17:35:40 +0000 Subject: [PATCH 2/8] Debug --- pyjsonld.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyjsonld.py b/pyjsonld.py index 1826cf82f..9133929dc 100644 --- a/pyjsonld.py +++ b/pyjsonld.py @@ -1457,6 +1457,7 @@ class JsonLdProcessor(object): if 'format' in opts: del opts['format'] opts['produceGeneralizedRdf'] = False + print('to_rdf: ' + str(input_)) dataset = self.to_rdf(input_, opts) except JsonLdError as cause: raise JsonLdError( From 52159d3817a4d60a089944490fad20959ca64243 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 5 Jan 2021 17:36:50 +0000 Subject: [PATCH 3/8] Debug --- inbox.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/inbox.py b/inbox.py index bde7388ee..1ba6bf8d2 100644 --- a/inbox.py +++ b/inbox.py @@ -2734,9 +2734,12 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, queue.pop(0) continue - if checkJsonSignature and verifyAllSignatures: + if checkJsonSignature: # use the original json message received, not one which may have # been modified along the way + print('Test: jsonld inbox signature check test ' + + keyId + ' ' + pubKey + ' ' + + str(queueJson['original'])) if not verifyJsonSignature(queueJson['original'], pubKey): if debug: print('WARN: jsonld inbox signature check failed ' + From 6b9f30cbd2d2e36406bf533c7d937c055662d64c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 5 Jan 2021 20:11:16 +0000 Subject: [PATCH 4/8] Check context before json signature verify --- context.py | 593 +++++++++++++++++++++++++++++++++++++++++++++ inbox.py | 34 +-- linked_data_sig.py | 6 +- pyjsonld.py | 491 ++----------------------------------- 4 files changed, 634 insertions(+), 490 deletions(-) create mode 100644 context.py diff --git a/context.py b/context.py new file mode 100644 index 000000000..ba130e423 --- /dev/null +++ b/context.py @@ -0,0 +1,593 @@ +__filename__ = "inbox.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + + +validContexts = ( + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/identity/v1", + "https://w3id.org/security/v1", + "https://raitisoja.com/apschema/v1.21" +) + + +def hasValidContext(postJsonObject: {}) -> bool: + """Are the links within the @context of a post recognised? + """ + if not postJsonObject.get('@context'): + return False + if isinstance(postJsonObject['@context'], list): + for url in postJsonObject['@context']: + if not isinstance(url, str): + continue + if url not in validContexts: + print('Invalid @context: ' + url) + return False + elif isinstance(postJsonObject['@context'], str): + url = postJsonObject['@context'] + if url not in validContexts: + print('Invalid @context: ' + url) + return False + else: + # not a list or string + return False + return True + + +def getApschemaV1_21() -> {}: + # https://raitisoja.com/apschema/v1.21 + return { + "@context": { + "zot": "https://raitisoja.com/apschema#", + "as": "https://www.w3.org/ns/activitystreams#", + "toot": "http://joinmastodon.org/ns#", + "ostatus": "http://ostatus.org#", + "schema": "http://schema.org#", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "movedTo": "as:movedTo", + "copiedTo": "as:copiedTo", + "alsoKnownAs": "as:alsoKnownAs", + "inheritPrivacy": "as:inheritPrivacy", + "EmojiReact": "as:EmojiReact", + "commentPolicy": "zot:commentPolicy", + "topicalCollection": "zot:topicalCollection", + "eventRepeat": "zot:eventRepeat", + "emojiReaction": "zot:emojiReaction", + "expires": "zot:expires", + "directMessage": "zot:directMessage", + "Category": "zot:Category", + "replyTo": "zot:replyTo", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "discoverable": "toot:discoverable" + } + } + + +def getV1SecuritySchema() -> {}: + # https://w3id.org/security/v1 + return { + "@context": { + "id": "@id", + "type": "@type", + "dc": "http://purl.org/dc/terms/", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016", + "Ed25519Signature2018": "sec:Ed25519Signature2018", + "EncryptedMessage": "sec:EncryptedMessage", + "GraphSignature2012": "sec:GraphSignature2012", + "LinkedDataSignature2015": "sec:LinkedDataSignature2015", + "LinkedDataSignature2016": "sec:LinkedDataSignature2016", + "CryptographicKey": "sec:Key", + "authenticationTag": "sec:authenticationTag", + "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm", + "cipherAlgorithm": "sec:cipherAlgorithm", + "cipherData": "sec:cipherData", + "cipherKey": "sec:cipherKey", + "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, + "creator": {"@id": "dc:creator", "@type": "@id"}, + "digestAlgorithm": "sec:digestAlgorithm", + "digestValue": "sec:digestValue", + "domain": "sec:domain", + "encryptionKey": "sec:encryptionKey", + "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "initializationVector": "sec:initializationVector", + "iterationCount": "sec:iterationCount", + "nonce": "sec:nonce", + "normalizationAlgorithm": "sec:normalizationAlgorithm", + "owner": {"@id": "sec:owner", "@type": "@id"}, + "password": "sec:password", + "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, + "privateKeyPem": "sec:privateKeyPem", + "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, + "publicKeyBase58": "sec:publicKeyBase58", + "publicKeyPem": "sec:publicKeyPem", + "publicKeyWif": "sec:publicKeyWif", + "publicKeyService": { + "@id": "sec:publicKeyService", "@type": "@id" + }, + "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, + "salt": "sec:salt", + "signature": "sec:signature", + "signatureAlgorithm": "sec:signingAlgorithm", + "signatureValue": "sec:signatureValue" + } + } + + +def getV1Schema() -> {}: + # https://w3id.org/identity/v1 + return { + "@context": { + "id": "@id", + "type": "@type", + "cred": "https://w3id.org/credentials#", + "dc": "http://purl.org/dc/terms/", + "identity": "https://w3id.org/identity#", + "perm": "https://w3id.org/permissions#", + "ps": "https://w3id.org/payswarm#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "sec": "https://w3id.org/security#", + "schema": "http://schema.org/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "Group": "https://www.w3.org/ns/activitystreams#Group", + "claim": {"@id": "cred:claim", "@type": "@id"}, + "credential": {"@id": "cred:credential", "@type": "@id"}, + "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"}, + "issuer": {"@id": "cred:issuer", "@type": "@id"}, + "recipient": {"@id": "cred:recipient", "@type": "@id"}, + "Credential": "cred:Credential", + "CryptographicKeyCredential": "cred:CryptographicKeyCredential", + "about": {"@id": "schema:about", "@type": "@id"}, + "address": {"@id": "schema:address", "@type": "@id"}, + "addressCountry": "schema:addressCountry", + "addressLocality": "schema:addressLocality", + "addressRegion": "schema:addressRegion", + "comment": "rdfs:comment", + "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, + "creator": {"@id": "dc:creator", "@type": "@id"}, + "description": "schema:description", + "email": "schema:email", + "familyName": "schema:familyName", + "givenName": "schema:givenName", + "image": {"@id": "schema:image", "@type": "@id"}, + "label": "rdfs:label", + "name": "schema:name", + "postalCode": "schema:postalCode", + "streetAddress": "schema:streetAddress", + "title": "dc:title", + "url": {"@id": "schema:url", "@type": "@id"}, + "Person": "schema:Person", + "PostalAddress": "schema:PostalAddress", + "Organization": "schema:Organization", + "identityService": { + "@id": "identity:identityService", "@type": "@id" + }, + "idp": {"@id": "identity:idp", "@type": "@id"}, + "Identity": "identity:Identity", + "paymentProcessor": "ps:processor", + "preferences": {"@id": "ps:preferences", "@type": "@vocab"}, + "cipherAlgorithm": "sec:cipherAlgorithm", + "cipherData": "sec:cipherData", + "cipherKey": "sec:cipherKey", + "digestAlgorithm": "sec:digestAlgorithm", + "digestValue": "sec:digestValue", + "domain": "sec:domain", + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "initializationVector": "sec:initializationVector", + "member": {"@id": "schema:member", "@type": "@id"}, + "memberOf": {"@id": "schema:memberOf", "@type": "@id"}, + "nonce": "sec:nonce", + "normalizationAlgorithm": "sec:normalizationAlgorithm", + "owner": {"@id": "sec:owner", "@type": "@id"}, + "password": "sec:password", + "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, + "privateKeyPem": "sec:privateKeyPem", + "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, + "publicKeyPem": "sec:publicKeyPem", + "publicKeyService": { + "@id": "sec:publicKeyService", "@type": "@id" + }, + "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, + "signature": "sec:signature", + "signatureAlgorithm": "sec:signatureAlgorithm", + "signatureValue": "sec:signatureValue", + "CryptographicKey": "sec:Key", + "EncryptedMessage": "sec:EncryptedMessage", + "GraphSignature2012": "sec:GraphSignature2012", + "LinkedDataSignature2015": "sec:LinkedDataSignature2015", + "accessControl": {"@id": "perm:accessControl", "@type": "@id"}, + "writePermission": {"@id": "perm:writePermission", "@type": "@id"} + } + } + + +def getActivitystreamsSchema() -> {}: + # https://www.w3.org/ns/activitystreams + return { + "@context": { + "@vocab": "_:", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "as": "https://www.w3.org/ns/activitystreams#", + "ldp": "http://www.w3.org/ns/ldp#", + "vcard": "http://www.w3.org/2006/vcard/ns#", + "id": "@id", + "type": "@type", + "Accept": "as:Accept", + "Activity": "as:Activity", + "IntransitiveActivity": "as:IntransitiveActivity", + "Add": "as:Add", + "Announce": "as:Announce", + "Application": "as:Application", + "Arrive": "as:Arrive", + "Article": "as:Article", + "Audio": "as:Audio", + "Block": "as:Block", + "Collection": "as:Collection", + "CollectionPage": "as:CollectionPage", + "Relationship": "as:Relationship", + "Create": "as:Create", + "Delete": "as:Delete", + "Dislike": "as:Dislike", + "Document": "as:Document", + "Event": "as:Event", + "Follow": "as:Follow", + "Flag": "as:Flag", + "Group": "as:Group", + "Ignore": "as:Ignore", + "Image": "as:Image", + "Invite": "as:Invite", + "Join": "as:Join", + "Leave": "as:Leave", + "Like": "as:Like", + "Link": "as:Link", + "Mention": "as:Mention", + "Note": "as:Note", + "Object": "as:Object", + "Offer": "as:Offer", + "OrderedCollection": "as:OrderedCollection", + "OrderedCollectionPage": "as:OrderedCollectionPage", + "Organization": "as:Organization", + "Page": "as:Page", + "Person": "as:Person", + "Place": "as:Place", + "Profile": "as:Profile", + "Question": "as:Question", + "Reject": "as:Reject", + "Remove": "as:Remove", + "Service": "as:Service", + "TentativeAccept": "as:TentativeAccept", + "TentativeReject": "as:TentativeReject", + "Tombstone": "as:Tombstone", + "Undo": "as:Undo", + "Update": "as:Update", + "Video": "as:Video", + "View": "as:View", + "Listen": "as:Listen", + "Read": "as:Read", + "Move": "as:Move", + "Travel": "as:Travel", + "IsFollowing": "as:IsFollowing", + "IsFollowedBy": "as:IsFollowedBy", + "IsContact": "as:IsContact", + "IsMember": "as:IsMember", + "subject": { + "@id": "as:subject", + "@type": "@id" + }, + "relationship": { + "@id": "as:relationship", + "@type": "@id" + }, + "actor": { + "@id": "as:actor", + "@type": "@id" + }, + "attributedTo": { + "@id": "as:attributedTo", + "@type": "@id" + }, + "attachment": { + "@id": "as:attachment", + "@type": "@id" + }, + "bcc": { + "@id": "as:bcc", + "@type": "@id" + }, + "bto": { + "@id": "as:bto", + "@type": "@id" + }, + "cc": { + "@id": "as:cc", + "@type": "@id" + }, + "context": { + "@id": "as:context", + "@type": "@id" + }, + "current": { + "@id": "as:current", + "@type": "@id" + }, + "first": { + "@id": "as:first", + "@type": "@id" + }, + "generator": { + "@id": "as:generator", + "@type": "@id" + }, + "icon": { + "@id": "as:icon", + "@type": "@id" + }, + "image": { + "@id": "as:image", + "@type": "@id" + }, + "inReplyTo": { + "@id": "as:inReplyTo", + "@type": "@id" + }, + "items": { + "@id": "as:items", + "@type": "@id" + }, + "instrument": { + "@id": "as:instrument", + "@type": "@id" + }, + "orderedItems": { + "@id": "as:items", + "@type": "@id", + "@container": "@list" + }, + "last": { + "@id": "as:last", + "@type": "@id" + }, + "location": { + "@id": "as:location", + "@type": "@id" + }, + "next": { + "@id": "as:next", + "@type": "@id" + }, + "object": { + "@id": "as:object", + "@type": "@id" + }, + "oneOf": { + "@id": "as:oneOf", + "@type": "@id" + }, + "anyOf": { + "@id": "as:anyOf", + "@type": "@id" + }, + "closed": { + "@id": "as:closed", + "@type": "xsd:dateTime" + }, + "origin": { + "@id": "as:origin", + "@type": "@id" + }, + "accuracy": { + "@id": "as:accuracy", + "@type": "xsd:float" + }, + "prev": { + "@id": "as:prev", + "@type": "@id" + }, + "preview": { + "@id": "as:preview", + "@type": "@id" + }, + "replies": { + "@id": "as:replies", + "@type": "@id" + }, + "result": { + "@id": "as:result", + "@type": "@id" + }, + "audience": { + "@id": "as:audience", + "@type": "@id" + }, + "partOf": { + "@id": "as:partOf", + "@type": "@id" + }, + "tag": { + "@id": "as:tag", + "@type": "@id" + }, + "target": { + "@id": "as:target", + "@type": "@id" + }, + "to": { + "@id": "as:to", + "@type": "@id" + }, + "url": { + "@id": "as:url", + "@type": "@id" + }, + "altitude": { + "@id": "as:altitude", + "@type": "xsd:float" + }, + "content": "as:content", + "contentMap": { + "@id": "as:content", + "@container": "@language" + }, + "name": "as:name", + "nameMap": { + "@id": "as:name", + "@container": "@language" + }, + "duration": { + "@id": "as:duration", + "@type": "xsd:duration" + }, + "endTime": { + "@id": "as:endTime", + "@type": "xsd:dateTime" + }, + "height": { + "@id": "as:height", + "@type": "xsd:nonNegativeInteger" + }, + "href": { + "@id": "as:href", + "@type": "@id" + }, + "hreflang": "as:hreflang", + "latitude": { + "@id": "as:latitude", + "@type": "xsd:float" + }, + "longitude": { + "@id": "as:longitude", + "@type": "xsd:float" + }, + "mediaType": "as:mediaType", + "published": { + "@id": "as:published", + "@type": "xsd:dateTime" + }, + "radius": { + "@id": "as:radius", + "@type": "xsd:float" + }, + "rel": "as:rel", + "startIndex": { + "@id": "as:startIndex", + "@type": "xsd:nonNegativeInteger" + }, + "startTime": { + "@id": "as:startTime", + "@type": "xsd:dateTime" + }, + "summary": "as:summary", + "summaryMap": { + "@id": "as:summary", + "@container": "@language" + }, + "totalItems": { + "@id": "as:totalItems", + "@type": "xsd:nonNegativeInteger" + }, + "units": "as:units", + "updated": { + "@id": "as:updated", + "@type": "xsd:dateTime" + }, + "width": { + "@id": "as:width", + "@type": "xsd:nonNegativeInteger" + }, + "describes": { + "@id": "as:describes", + "@type": "@id" + }, + "formerType": { + "@id": "as:formerType", + "@type": "@id" + }, + "deleted": { + "@id": "as:deleted", + "@type": "xsd:dateTime" + }, + "inbox": { + "@id": "ldp:inbox", + "@type": "@id" + }, + "outbox": { + "@id": "as:outbox", + "@type": "@id" + }, + "following": { + "@id": "as:following", + "@type": "@id" + }, + "followers": { + "@id": "as:followers", + "@type": "@id" + }, + "streams": { + "@id": "as:streams", + "@type": "@id" + }, + "preferredUsername": "as:preferredUsername", + "endpoints": { + "@id": "as:endpoints", + "@type": "@id" + }, + "uploadMedia": { + "@id": "as:uploadMedia", + "@type": "@id" + }, + "proxyUrl": { + "@id": "as:proxyUrl", + "@type": "@id" + }, + "liked": { + "@id": "as:liked", + "@type": "@id" + }, + "oauthAuthorizationEndpoint": { + "@id": "as:oauthAuthorizationEndpoint", + "@type": "@id" + }, + "oauthTokenEndpoint": { + "@id": "as:oauthTokenEndpoint", + "@type": "@id" + }, + "provideClientKey": { + "@id": "as:provideClientKey", + "@type": "@id" + }, + "signClientKey": { + "@id": "as:signClientKey", + "@type": "@id" + }, + "sharedInbox": { + "@id": "as:sharedInbox", + "@type": "@id" + }, + "Public": { + "@id": "as:Public", + "@type": "@id" + }, + "source": "as:source", + "likes": { + "@id": "as:likes", + "@type": "@id" + }, + "shares": { + "@id": "as:shares", + "@type": "@id" + }, + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + } + } + } diff --git a/inbox.py b/inbox.py index 1ba6bf8d2..e1604d7f7 100644 --- a/inbox.py +++ b/inbox.py @@ -74,6 +74,7 @@ from happening import saveEventPost from delete import removeOldHashtags from follow import isFollowingActor from categories import guessHashtagCategory +from context import hasValidContext def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: @@ -2714,20 +2715,23 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, # check if a json signature exists on this post checkJsonSignature = False - if queueJson['original'].get('@context') and \ - queueJson['original'].get('signature'): - if isinstance(queueJson['original']['signature'], dict): + originalJson = queueJson['original'] + if originalJson.get('@context') and \ + originalJson.get('signature'): + if isinstance(originalJson['signature'], dict): # see https://tools.ietf.org/html/rfc7515 - jwebsig = queueJson['original']['signature'] + jwebsig = originalJson['signature'] # signature exists and is of the expected type if jwebsig.get('type') and jwebsig.get('signatureValue'): if jwebsig['type'] == 'RsaSignature2017': - checkJsonSignature = True + if hasValidContext(originalJson): + checkJsonSignature = True # strict enforcement of json signatures if verifyAllSignatures and \ not checkJsonSignature: - print('inbox post does not have a jsonld signature ' + keyId) + print('inbox post does not have a jsonld signature ' + + keyId + ' ' + str(originalJson)) if os.path.isfile(queueFilename): os.remove(queueFilename) if len(queue) > 0: @@ -2737,22 +2741,18 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, if checkJsonSignature: # use the original json message received, not one which may have # been modified along the way - print('Test: jsonld inbox signature check test ' + - keyId + ' ' + pubKey + ' ' + - str(queueJson['original'])) - if not verifyJsonSignature(queueJson['original'], pubKey): + if not verifyJsonSignature(originalJson, pubKey): if debug: print('WARN: jsonld inbox signature check failed ' + - keyId + ' ' + pubKey + ' ' + - str(queueJson['original'])) + keyId + ' ' + pubKey + ' ' + str(originalJson)) else: print('WARN: jsonld inbox signature check failed ' + keyId) - if os.path.isfile(queueFilename): - os.remove(queueFilename) - if len(queue) > 0: - queue.pop(0) - continue + # if os.path.isfile(queueFilename): + # os.remove(queueFilename) + # if len(queue) > 0: + # queue.pop(0) + # continue else: print('jsonld inbox signature check success ' + keyId) diff --git a/linked_data_sig.py b/linked_data_sig.py index abe4a9348..2410e9c2d 100644 --- a/linked_data_sig.py +++ b/linked_data_sig.py @@ -22,6 +22,7 @@ except ImportError: from Crypto.Signature import PKCS1_v1_5 from pyjsonld import normalize +from context import hasValidContext def _options_hash(doc: {}) -> str: @@ -70,6 +71,8 @@ def verifyJsonSignature(doc: {}, publicKeyPem: str) -> bool: """Returns True if the given ActivityPub post was sent by an actor having the given public key """ + if not hasValidContext(doc): + return False key = RSA.importKey(publicKeyPem) to_be_signed = _options_hash(doc) + _doc_hash(doc) signature = doc["signature"]["signatureValue"] @@ -85,7 +88,8 @@ def generateJsonSignature(doc: {}, privateKeyPem: str) -> None: """ if not doc.get('actor'): return - + if not hasValidContext(doc): + return options = { "type": "RsaSignature2017", "creator": doc["actor"] + "#main-key", diff --git a/pyjsonld.py b/pyjsonld.py index 9133929dc..35ebdf7a3 100644 --- a/pyjsonld.py +++ b/pyjsonld.py @@ -37,6 +37,11 @@ import traceback from collections import deque, namedtuple from numbers import Integral, Real +from context import getApschemaV1_21 +from context import getV1Schema +from context import getV1SecuritySchema +from context import getActivitystreamsSchema + try: from functools import cmp_to_key except ImportError: @@ -350,477 +355,6 @@ def parse_link_header(header): return rval -def getV1Schema() -> {}: - # https://w3id.org/identity/v1 - return { - "@context": { - "id": "@id", - "type": "@type", - "cred": "https://w3id.org/credentials#", - "dc": "http://purl.org/dc/terms/", - "identity": "https://w3id.org/identity#", - "perm": "https://w3id.org/permissions#", - "ps": "https://w3id.org/payswarm#", - "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - "rdfs": "http://www.w3.org/2000/01/rdf-schema#", - "sec": "https://w3id.org/security#", - "schema": "http://schema.org/", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "Group": "https://www.w3.org/ns/activitystreams#Group", - "claim": {"@id": "cred:claim", "@type": "@id"}, - "credential": {"@id": "cred:credential", "@type": "@id"}, - "issued": {"@id": "cred:issued", "@type": "xsd:dateTime"}, - "issuer": {"@id": "cred:issuer", "@type": "@id"}, - "recipient": {"@id": "cred:recipient", "@type": "@id"}, - "Credential": "cred:Credential", - "CryptographicKeyCredential": "cred:CryptographicKeyCredential", - "about": {"@id": "schema:about", "@type": "@id"}, - "address": {"@id": "schema:address", "@type": "@id"}, - "addressCountry": "schema:addressCountry", - "addressLocality": "schema:addressLocality", - "addressRegion": "schema:addressRegion", - "comment": "rdfs:comment", - "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, - "creator": {"@id": "dc:creator", "@type": "@id"}, - "description": "schema:description", - "email": "schema:email", - "familyName": "schema:familyName", - "givenName": "schema:givenName", - "image": {"@id": "schema:image", "@type": "@id"}, - "label": "rdfs:label", - "name": "schema:name", - "postalCode": "schema:postalCode", - "streetAddress": "schema:streetAddress", - "title": "dc:title", - "url": {"@id": "schema:url", "@type": "@id"}, - "Person": "schema:Person", - "PostalAddress": "schema:PostalAddress", - "Organization": "schema:Organization", - "identityService": { - "@id": "identity:identityService", "@type": "@id" - }, - "idp": {"@id": "identity:idp", "@type": "@id"}, - "Identity": "identity:Identity", - "paymentProcessor": "ps:processor", - "preferences": {"@id": "ps:preferences", "@type": "@vocab"}, - "cipherAlgorithm": "sec:cipherAlgorithm", - "cipherData": "sec:cipherData", - "cipherKey": "sec:cipherKey", - "digestAlgorithm": "sec:digestAlgorithm", - "digestValue": "sec:digestValue", - "domain": "sec:domain", - "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, - "initializationVector": "sec:initializationVector", - "member": {"@id": "schema:member", "@type": "@id"}, - "memberOf": {"@id": "schema:memberOf", "@type": "@id"}, - "nonce": "sec:nonce", - "normalizationAlgorithm": "sec:normalizationAlgorithm", - "owner": {"@id": "sec:owner", "@type": "@id"}, - "password": "sec:password", - "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, - "privateKeyPem": "sec:privateKeyPem", - "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, - "publicKeyPem": "sec:publicKeyPem", - "publicKeyService": { - "@id": "sec:publicKeyService", "@type": "@id" - }, - "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, - "signature": "sec:signature", - "signatureAlgorithm": "sec:signatureAlgorithm", - "signatureValue": "sec:signatureValue", - "CryptographicKey": "sec:Key", - "EncryptedMessage": "sec:EncryptedMessage", - "GraphSignature2012": "sec:GraphSignature2012", - "LinkedDataSignature2015": "sec:LinkedDataSignature2015", - "accessControl": {"@id": "perm:accessControl", "@type": "@id"}, - "writePermission": {"@id": "perm:writePermission", "@type": "@id"} - } - } - - -def getActivitystreamsSchema() -> {}: - # https://www.w3.org/ns/activitystreams - return { - "@context": { - "@vocab": "_:", - "xsd": "http://www.w3.org/2001/XMLSchema#", - "as": "https://www.w3.org/ns/activitystreams#", - "ldp": "http://www.w3.org/ns/ldp#", - "vcard": "http://www.w3.org/2006/vcard/ns#", - "id": "@id", - "type": "@type", - "Accept": "as:Accept", - "Activity": "as:Activity", - "IntransitiveActivity": "as:IntransitiveActivity", - "Add": "as:Add", - "Announce": "as:Announce", - "Application": "as:Application", - "Arrive": "as:Arrive", - "Article": "as:Article", - "Audio": "as:Audio", - "Block": "as:Block", - "Collection": "as:Collection", - "CollectionPage": "as:CollectionPage", - "Relationship": "as:Relationship", - "Create": "as:Create", - "Delete": "as:Delete", - "Dislike": "as:Dislike", - "Document": "as:Document", - "Event": "as:Event", - "Follow": "as:Follow", - "Flag": "as:Flag", - "Group": "as:Group", - "Ignore": "as:Ignore", - "Image": "as:Image", - "Invite": "as:Invite", - "Join": "as:Join", - "Leave": "as:Leave", - "Like": "as:Like", - "Link": "as:Link", - "Mention": "as:Mention", - "Note": "as:Note", - "Object": "as:Object", - "Offer": "as:Offer", - "OrderedCollection": "as:OrderedCollection", - "OrderedCollectionPage": "as:OrderedCollectionPage", - "Organization": "as:Organization", - "Page": "as:Page", - "Person": "as:Person", - "Place": "as:Place", - "Profile": "as:Profile", - "Question": "as:Question", - "Reject": "as:Reject", - "Remove": "as:Remove", - "Service": "as:Service", - "TentativeAccept": "as:TentativeAccept", - "TentativeReject": "as:TentativeReject", - "Tombstone": "as:Tombstone", - "Undo": "as:Undo", - "Update": "as:Update", - "Video": "as:Video", - "View": "as:View", - "Listen": "as:Listen", - "Read": "as:Read", - "Move": "as:Move", - "Travel": "as:Travel", - "IsFollowing": "as:IsFollowing", - "IsFollowedBy": "as:IsFollowedBy", - "IsContact": "as:IsContact", - "IsMember": "as:IsMember", - "subject": { - "@id": "as:subject", - "@type": "@id" - }, - "relationship": { - "@id": "as:relationship", - "@type": "@id" - }, - "actor": { - "@id": "as:actor", - "@type": "@id" - }, - "attributedTo": { - "@id": "as:attributedTo", - "@type": "@id" - }, - "attachment": { - "@id": "as:attachment", - "@type": "@id" - }, - "bcc": { - "@id": "as:bcc", - "@type": "@id" - }, - "bto": { - "@id": "as:bto", - "@type": "@id" - }, - "cc": { - "@id": "as:cc", - "@type": "@id" - }, - "context": { - "@id": "as:context", - "@type": "@id" - }, - "current": { - "@id": "as:current", - "@type": "@id" - }, - "first": { - "@id": "as:first", - "@type": "@id" - }, - "generator": { - "@id": "as:generator", - "@type": "@id" - }, - "icon": { - "@id": "as:icon", - "@type": "@id" - }, - "image": { - "@id": "as:image", - "@type": "@id" - }, - "inReplyTo": { - "@id": "as:inReplyTo", - "@type": "@id" - }, - "items": { - "@id": "as:items", - "@type": "@id" - }, - "instrument": { - "@id": "as:instrument", - "@type": "@id" - }, - "orderedItems": { - "@id": "as:items", - "@type": "@id", - "@container": "@list" - }, - "last": { - "@id": "as:last", - "@type": "@id" - }, - "location": { - "@id": "as:location", - "@type": "@id" - }, - "next": { - "@id": "as:next", - "@type": "@id" - }, - "object": { - "@id": "as:object", - "@type": "@id" - }, - "oneOf": { - "@id": "as:oneOf", - "@type": "@id" - }, - "anyOf": { - "@id": "as:anyOf", - "@type": "@id" - }, - "closed": { - "@id": "as:closed", - "@type": "xsd:dateTime" - }, - "origin": { - "@id": "as:origin", - "@type": "@id" - }, - "accuracy": { - "@id": "as:accuracy", - "@type": "xsd:float" - }, - "prev": { - "@id": "as:prev", - "@type": "@id" - }, - "preview": { - "@id": "as:preview", - "@type": "@id" - }, - "replies": { - "@id": "as:replies", - "@type": "@id" - }, - "result": { - "@id": "as:result", - "@type": "@id" - }, - "audience": { - "@id": "as:audience", - "@type": "@id" - }, - "partOf": { - "@id": "as:partOf", - "@type": "@id" - }, - "tag": { - "@id": "as:tag", - "@type": "@id" - }, - "target": { - "@id": "as:target", - "@type": "@id" - }, - "to": { - "@id": "as:to", - "@type": "@id" - }, - "url": { - "@id": "as:url", - "@type": "@id" - }, - "altitude": { - "@id": "as:altitude", - "@type": "xsd:float" - }, - "content": "as:content", - "contentMap": { - "@id": "as:content", - "@container": "@language" - }, - "name": "as:name", - "nameMap": { - "@id": "as:name", - "@container": "@language" - }, - "duration": { - "@id": "as:duration", - "@type": "xsd:duration" - }, - "endTime": { - "@id": "as:endTime", - "@type": "xsd:dateTime" - }, - "height": { - "@id": "as:height", - "@type": "xsd:nonNegativeInteger" - }, - "href": { - "@id": "as:href", - "@type": "@id" - }, - "hreflang": "as:hreflang", - "latitude": { - "@id": "as:latitude", - "@type": "xsd:float" - }, - "longitude": { - "@id": "as:longitude", - "@type": "xsd:float" - }, - "mediaType": "as:mediaType", - "published": { - "@id": "as:published", - "@type": "xsd:dateTime" - }, - "radius": { - "@id": "as:radius", - "@type": "xsd:float" - }, - "rel": "as:rel", - "startIndex": { - "@id": "as:startIndex", - "@type": "xsd:nonNegativeInteger" - }, - "startTime": { - "@id": "as:startTime", - "@type": "xsd:dateTime" - }, - "summary": "as:summary", - "summaryMap": { - "@id": "as:summary", - "@container": "@language" - }, - "totalItems": { - "@id": "as:totalItems", - "@type": "xsd:nonNegativeInteger" - }, - "units": "as:units", - "updated": { - "@id": "as:updated", - "@type": "xsd:dateTime" - }, - "width": { - "@id": "as:width", - "@type": "xsd:nonNegativeInteger" - }, - "describes": { - "@id": "as:describes", - "@type": "@id" - }, - "formerType": { - "@id": "as:formerType", - "@type": "@id" - }, - "deleted": { - "@id": "as:deleted", - "@type": "xsd:dateTime" - }, - "inbox": { - "@id": "ldp:inbox", - "@type": "@id" - }, - "outbox": { - "@id": "as:outbox", - "@type": "@id" - }, - "following": { - "@id": "as:following", - "@type": "@id" - }, - "followers": { - "@id": "as:followers", - "@type": "@id" - }, - "streams": { - "@id": "as:streams", - "@type": "@id" - }, - "preferredUsername": "as:preferredUsername", - "endpoints": { - "@id": "as:endpoints", - "@type": "@id" - }, - "uploadMedia": { - "@id": "as:uploadMedia", - "@type": "@id" - }, - "proxyUrl": { - "@id": "as:proxyUrl", - "@type": "@id" - }, - "liked": { - "@id": "as:liked", - "@type": "@id" - }, - "oauthAuthorizationEndpoint": { - "@id": "as:oauthAuthorizationEndpoint", - "@type": "@id" - }, - "oauthTokenEndpoint": { - "@id": "as:oauthTokenEndpoint", - "@type": "@id" - }, - "provideClientKey": { - "@id": "as:provideClientKey", - "@type": "@id" - }, - "signClientKey": { - "@id": "as:signClientKey", - "@type": "@id" - }, - "sharedInbox": { - "@id": "as:sharedInbox", - "@type": "@id" - }, - "Public": { - "@id": "as:Public", - "@type": "@id" - }, - "source": "as:source", - "likes": { - "@id": "as:likes", - "@type": "@id" - }, - "shares": { - "@id": "as:shares", - "@type": "@id" - }, - "alsoKnownAs": { - "@id": "as:alsoKnownAs", - "@type": "@id" - } - } - } - - def load_document(url): """ Retrieves JSON-LD at the given URL. @@ -849,6 +383,13 @@ def load_document(url): 'document': getV1Schema() } return doc + if url == 'https://w3id.org/security/v1': + doc = { + 'contextUrl': None, + 'documentUrl': url, + 'document': getV1SecuritySchema() + } + return doc elif url == 'https://www.w3.org/ns/activitystreams': doc = { 'contextUrl': None, @@ -856,6 +397,13 @@ def load_document(url): 'document': getActivitystreamsSchema() } return doc + elif url == 'https://raitisoja.com/apschema/v1.21': + doc = { + 'contextUrl': None, + 'documentUrl': url, + 'document': getApschemaV1_21() + } + return doc return None except JsonLdError as e: raise e @@ -1457,7 +1005,6 @@ class JsonLdProcessor(object): if 'format' in opts: del opts['format'] opts['produceGeneralizedRdf'] = False - print('to_rdf: ' + str(input_)) dataset = self.to_rdf(input_, opts) except JsonLdError as cause: raise JsonLdError( From 99903072d27fe5914add4f8333d9fd0c6d410bb5 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 5 Jan 2021 20:15:52 +0000 Subject: [PATCH 5/8] Debug --- inbox.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inbox.py b/inbox.py index e1604d7f7..5ea5b348f 100644 --- a/inbox.py +++ b/inbox.py @@ -2726,6 +2726,9 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, if jwebsig['type'] == 'RsaSignature2017': if hasValidContext(originalJson): checkJsonSignature = True + else: + print('unrecognised @context: ' + + originalJson['@context']) # strict enforcement of json signatures if verifyAllSignatures and \ From 2ce7456e28aeebe507556c9130035699021deb2e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 5 Jan 2021 20:16:58 +0000 Subject: [PATCH 6/8] Convert to string --- inbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inbox.py b/inbox.py index 5ea5b348f..6636535ed 100644 --- a/inbox.py +++ b/inbox.py @@ -2728,7 +2728,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, checkJsonSignature = True else: print('unrecognised @context: ' + - originalJson['@context']) + str(originalJson['@context'])) # strict enforcement of json signatures if verifyAllSignatures and \ From e5c4d9e69bce511fcf0e7856593fd4edd7b6a2f9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 5 Jan 2021 20:55:11 +0000 Subject: [PATCH 7/8] Only use json signatures on outgoing posts unless verify all signatures is enabled --- inbox.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/inbox.py b/inbox.py index 6636535ed..bc499f8bb 100644 --- a/inbox.py +++ b/inbox.py @@ -2741,7 +2741,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, queue.pop(0) continue - if checkJsonSignature: + if checkJsonSignature and verifyAllSignatures: # use the original json message received, not one which may have # been modified along the way if not verifyJsonSignature(originalJson, pubKey): @@ -2751,11 +2751,11 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, else: print('WARN: jsonld inbox signature check failed ' + keyId) - # if os.path.isfile(queueFilename): - # os.remove(queueFilename) - # if len(queue) > 0: - # queue.pop(0) - # continue + if os.path.isfile(queueFilename): + os.remove(queueFilename) + if len(queue) > 0: + queue.pop(0) + continue else: print('jsonld inbox signature check success ' + keyId) From 6c44c8fcaa42bb8945ac132263acb32fd3a50437 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 5 Jan 2021 21:30:56 +0000 Subject: [PATCH 8/8] Don't show clearnet http favicons --- webapp_column_right.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webapp_column_right.py b/webapp_column_right.py index 88f0513e0..d52717fec 100644 --- a/webapp_column_right.py +++ b/webapp_column_right.py @@ -198,6 +198,9 @@ def _getNewswireFavicon(url: str) -> str: """ if '://' not in url: return None + if url.startswith('http://'): + if not (url.endswith('.onion') or url.endswith('.i2p')): + return None domain = url.split('://')[1] if '/' not in domain: return url + '/favicon.ico'