diff --git a/README.md b/README.md index 629eac07..4e73deb5 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ sudo pacman -S tor python-pip python-pysocks python-pycryptodome \ imagemagick python-pillow python-requests \ perl-image-exiftool python-numpy python-dateutil \ certbot flake8 +suso pip3 install pyLD ``` Or on Debian: @@ -34,6 +35,7 @@ sudo apt install -y \ python3-crypto python3-cryptodome \ python3-dateutil python3-pil.imagetk python3-idna python3-requests \ + python3-pyld \ libimage-exiftool-perl python3-flake8 \ certbot nginx ``` diff --git a/gemini/EN/install.gmi b/gemini/EN/install.gmi index 7013b3e3..b46ff795 100644 --- a/gemini/EN/install.gmi +++ b/gemini/EN/install.gmi @@ -4,7 +4,7 @@ You will need python version 3.7 or later. On a Debian based system: - sudo apt install -y tor python3-socks imagemagick python3-numpy python3-setuptools python3-crypto python3-cryptodome python3-dateutil python3-pil.imagetk python3-idna python3-requests python3-flake8 libimage-exiftool-perl certbot nginx + sudo apt install -y tor python3-socks imagemagick python3-numpy python3-setuptools python3-crypto python3-cryptodome python3-dateutil python3-pil.imagetk python3-idna python3-requests python3-flake8 python3-pyld libimage-exiftool-perl certbot nginx The following instructions install Epicyon to the /opt directory. It's not essential that it be installed there, and it could be in any other preferred directory. diff --git a/jsonldsig.py b/jsonldsig.py new file mode 100644 index 00000000..2c50f1c9 --- /dev/null +++ b/jsonldsig.py @@ -0,0 +1,149 @@ +__filename__ = "jsonldsig.py" +__author__ = "Bob Mottram" +__credits__ = ['Based on ' + + 'https://github.com/WebOfTrustInfo/ld-signatures-python'] +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +from copy import deepcopy +from datetime import datetime + +import pytz + +try: + from Cryptodome.PublicKey import RSA + from Cryptodome.Hash import SHA256 + from Cryptodome.Signature import pkcs1_15 as PKCS1_v1_5 +except ImportError: + from Crypto.PublicKey import RSA + from Crypto.Hash import SHA256 + from Crypto.Signature import PKCS1_v1_5 + +from pyld import jsonld + +import base64 +import json + + +def b64safeEncode(payload): + """ + b64 url safe encoding with the padding removed. + """ + return base64.urlsafe_b64encode(payload).rstrip(b'=') + + +def b64safeDecode(payload): + """ + b64 url safe decoding with the padding added. + """ + return base64.urlsafe_b64decode(payload + b'=' * (4 - len(payload) % 4)) + + +def normalizeJson(payload): + return json.dumps(payload, separators=(',', ':'), + sort_keys=True).encode('utf-8') + + +def signRs256(payload, private_key): + """ + Produce a RS256 signature of the payload + """ + key = RSA.importKey(private_key) + signer = PKCS1_v1_5.new(key) + signature = signer.sign(SHA256.new(payload)) + return signature + + +def verifyRs256(payload, signature, public_key): + """ + Verifies a RS256 signature + """ + key = RSA.importKey(public_key) + verifier = PKCS1_v1_5.new(key) + return verifier.verify(SHA256.new(payload), signature) + + +def signJws(payload, private_key): + """ Prepare payload to sign + """ + header = { + 'alg': 'RS256', + 'b64': False, + 'crit': ['b64'] + } + normalizedJson = normalizeJson(header) + encodedHeader = b64safeEncode(normalizedJson) + preparedPayload = b'.'.join([encodedHeader, payload]) + + signature = signRs256(preparedPayload, private_key) + encodedSignature = b64safeEncode(signature) + jwsSignature = b'..'.join([encodedHeader, encodedSignature]) + + return jwsSignature + + +def verifyJws(payload, jws_signature, public_key): + # remove the encoded header from the signature + encodedHeader, encodedSignature = jws_signature.split(b'..') + signature = b64safeDecode(encodedSignature) + payload = b'.'.join([encodedHeader, payload]) + return verifyRs256(payload, signature, public_key) + + +def jsonldNormalize(jldDocument: str): + """ + Normalize and hash the json-ld document + """ + options = { + 'algorithm': 'URDNA2015', + 'format': 'application/nquads' + } + normalized = jsonld.normalize(jldDocument, options=options) + normalizedHash = SHA256.new(data=normalized.encode('utf-8')).digest() + return normalizedHash + + +def jsonldSign(jldDocument: {}, privateKeyPem: str) -> {}: + """ + Produces a signed JSON-LD document with a Json Web Signature + """ + jldDocument = deepcopy(jldDocument) + normalizedJldHash = jsonldNormalize(jldDocument) + jwsSignature = signJws(normalizedJldHash, privateKeyPem) + + # construct the signature document and add it to jsonld + signature = { + 'type': 'RsaSignatureSuite2017', + 'created': datetime.now(tz=pytz.utc).strftime('%Y-%m-%dT%H:%M:%SZ'), + 'signatureValue': jwsSignature.decode('utf-8') + } + jldDocument.update({'signature': signature}) + + return jldDocument + + +def jsonldVerify(signedJldDocument: {}, publicKeyPem: str) -> bool: + """ + Verifies the Json Web Signature of a signed JSON-LD Document + """ + signedJldDocument = deepcopy(signedJldDocument) + signature = signedJldDocument.pop('signature') + jwsSignature = signature['signatureValue'].encode('utf-8') + normalizedJldHash = jsonldNormalize(signedJldDocument) + + return verifyJws(normalizedJldHash, jwsSignature, publicKeyPem) + + +def testSignJsonld(jldDocument: {}, privateKeyPem: str, + expectedJldDocumentSigned=None): + signedJldDocument = jsonldSign(jldDocument, privateKeyPem) + # pop the created time key since its dynamic + signedJldDocument['signature'].pop('created') + + if expectedJldDocumentSigned: + assert signedJldDocument == expectedJldDocumentSigned + else: + return signedJldDocument diff --git a/tests.py b/tests.py index 86d16aac..b67ded78 100644 --- a/tests.py +++ b/tests.py @@ -47,6 +47,7 @@ from follow import sendFollowRequest from person import createPerson from person import setDisplayNickname from person import setBio +# from person import generateRSAKey from skills import setSkillLevel from roles import setRole from roles import outboxDelegate @@ -70,6 +71,8 @@ from content import replaceContentDuplicates from content import removeTextFormatting from theme import setCSSparam from semantic import isAccusatory +from jsonldsig import testSignJsonld +from jsonldsig import jsonldVerify testServerAliceRunning = False testServerBobRunning = False @@ -1809,8 +1812,61 @@ def testRemoveTextFormatting(): assert(resultStr == '
Text with formatting
') +def testJsonld(): + print("testJsonld") + jldDocument = { + "description": "My json document", + "numberField": 83582, + "object": { + "content": "Some content" + } + } + # privateKeyPem, publicKeyPem = generateRSAKey() + privateKeyPem = '-----BEGIN RSA PRIVATE KEY-----\n' \ + 'MIIEowIBAAKCAQEAod9iHfIn4ugY/2byFrFjUprrFLkkH5bCrjiBq2/MdHFg99IQ\n' \ + '7li2x2mg5fkBMhU5SJIxlN8kiZMFq7JUXSA97Yo4puhVubqTSHihIh6Xn2mTjTgs\n' \ + 'zNo9SBbmN3YiyBPTcr0rF4jGWZAduJ8u6i7Eky2QH+UBKyUNRZrcfoVq+7grHUIA\n' \ + '45pE7vAfEEWtgRiw32Nwlx55N3hayHax0y8gMdKEF/vfYKRLcM7rZgEASMtlCpgy\n' \ + 'fsyHwFCDzl/BP8AhP9u3dM+SEundeAvF58AiXx1pKvBpxqttDNAsKWCRQ06/WI/W\n' \ + '2Rwihl9yCjobqRoFsZ/cTEi6FG9AbDAds5YjTwIDAQABAoIBAERL3rbpy8Bl0t43\n' \ + 'jh7a+yAIMvVMZBxb3InrV3KAug/LInGNFQ2rKnsaawN8uu9pmwCuhfLc7yqIeJUH\n' \ + 'qaadCuPlNJ/fWQQC309tbfbaV3iv78xejjBkSATZfIqb8nLeQpGflMXaNG3na1LQ\n' \ + '/tdZoiDC0ZNTaNnOSTo765oKKqhHUTQkwkGChrwG3Js5jekV4zpPMLhUafXk6ksd\n' \ + '8XLlZdCF3RUnuguXAg2xP/duxMYmTCx3eeGPkXBPQl0pahu8/6OtBoYvBrqNdQcx\n' \ + 'jnEtYX9PCqDY3hAXW9GWsxNfu02DKhWigFHFNRUQtMI++438+QIfzXPslE2bTQIt\n' \ + '0OXUlwECgYEAxTKUZ7lwIBb5XKPJq53RQmX66M3ArxI1RzFSKm1+/CmxvYiN0c+5\n' \ + '2Aq62WEIauX6hoZ7yQb4zhdeNRzinLR7rsmBvIcP12FidXG37q9v3Vu70KmHniJE\n' \ + 'TPbt5lHQ0bNACFxkar4Ab/JZN4CkMRgJdlcZ5boYNmcGOYCvw9izuM8CgYEA0iQ1\n' \ + 'khIFZ6fCiXwVRGvEHmqSnkBmBHz8MY8fczv2Z4Gzfq3Tlh9VxpigK2F2pFt7keWc\n' \ + '53HerYFHFpf5otDhEyRwA1LyIcwbj5HopumxsB2WG+/M2as45lLfWa6KO73OtPpU\n' \ + 'wGZYW+i/otdk9eFphceYtw19mxI+3lYoeI8EjYECgYBxOtTKJkmCs45lqkp/d3QT\n' \ + '2zjSempcXGkpQuG6KPtUUaCUgxdj1RISQj792OCbeQh8PDZRvOYaeIKInthkQKIQ\n' \ + 'P/Z1yVvIQUvmwfBqZmQmR6k1bFLJ80UiqFr7+BiegH2RD3Q9cnIP1aly3DPrWLD+\n' \ + 'OY9OQKfsfQWu+PxzyTeRMwKBgD8Zjlh5PtQ8RKcB8mTkMzSq7bHFRpzsZtH+1wPE\n' \ + 'Kp40DRDp41H9wMTsiZPdJUH/EmDh4LaCs8nHuu/m3JfuPtd/pn7pBjntzwzSVFji\n' \ + 'bW+jwrJK1Gk8B87pbZXBWlLMEOi5Dn/je37Fqd2c7f0DHauFHq9AxsmsteIPXwGs\n' \ + 'eEKBAoGBAIzJX/5yFp3ObkPracIfOJ/U/HF1UdP6Y8qmOJBZOg5s9Y+JAdY76raK\n' \ + '0SbZPsOpuFUdTiRkSI3w/p1IuM5dPxgCGH9MHqjqogU5QwXr3vLF+a/PFhINkn1x\n' \ + 'lozRZjDcF1y6xHfExotPC973UZnKEviq9/FqOsovZpvSQkzAYSZF\n' \ + '-----END RSA PRIVATE KEY-----' + publicKeyPem = '-----BEGIN PUBLIC KEY-----\n' \ + 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAod9iHfIn4ugY/2byFrFj\n' \ + 'UprrFLkkH5bCrjiBq2/MdHFg99IQ7li2x2mg5fkBMhU5SJIxlN8kiZMFq7JUXSA9\n' \ + '7Yo4puhVubqTSHihIh6Xn2mTjTgszNo9SBbmN3YiyBPTcr0rF4jGWZAduJ8u6i7E\n' \ + 'ky2QH+UBKyUNRZrcfoVq+7grHUIA45pE7vAfEEWtgRiw32Nwlx55N3hayHax0y8g\n' \ + 'MdKEF/vfYKRLcM7rZgEASMtlCpgyfsyHwFCDzl/BP8AhP9u3dM+SEundeAvF58Ai\n' \ + 'Xx1pKvBpxqttDNAsKWCRQ06/WI/W2Rwihl9yCjobqRoFsZ/cTEi6FG9AbDAds5Yj\n' \ + 'TwIDAQAB\n' \ + '-----END PUBLIC KEY-----' + + signedDocument = testSignJsonld(jldDocument, privateKeyPem) + assert(signedDocument) + assert(jsonldVerify(signedDocument, publicKeyPem)) + + def runAllTests(): print('Running tests...') + testJsonld() testRemoveTextFormatting() testAccusatory() testWebLinks() diff --git a/website/EN/index.html b/website/EN/index.html index 86a1bdec..c2c18f86 100644 --- a/website/EN/index.html +++ b/website/EN/index.html @@ -1264,7 +1264,7 @@You will need python version 3.7 or later.
On a Debian based system:
sudo apt install -y tor python3-socks imagemagick python3-numpy python3-setuptools python3-crypto python3-cryptodome python3-dateutil python3-pil.imagetk python3-idna python3-requests python3-flake8 libimage-exiftool-perl certbot nginx
+sudo apt install -y tor python3-socks imagemagick python3-numpy python3-setuptools python3-crypto python3-cryptodome python3-dateutil python3-pil.imagetk python3-idna python3-requests python3-flake8 python3-pyld libimage-exiftool-perl certbot nginx