ActivityPub 協定的基本踩坑心得(下)

(最近更新:2022-11-23,勘誤)

前文

作爲前文的續篇,程式碼仍然改自 Timmot的作品,使用 MIT授權。

如何發送 follow request

說明

我們這裏示範如何發送 following request。這裏需要進行加密技術的簽署驗證 (HTTP Signature),還有製作 digest。所以動作會比較繁瑣。

先備安裝

我們進去 ~/my_folder目錄,安裝加密和發送 HTTP request 的套件:

pip install cryptography requests

嘗試做法

建立~/my_folder/fo_req.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# 因為要加密,所以要 import 一堆東西。
from cryptography.hazmat.backends import default_backend as crypto_default_backend
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding

from urllib.parse import urlparse # 分析 url
import base64 #用 base64 轉碼用
import datetime #時間日期
import requests #送 http request
import hashlib #產生雜湊用
import json # 處理 json
import os.path # 處理路徑
default_sender = "alice" # 寄件者名稱
sender_domain = "activitypub_test.ddns.net" #寄件者域名

receiver_name = "bob" #收件人名稱
recipient_domain = "example.social" #收件者域名

'''產生一些連結'''
recipient_url = f"https://{recipient_domain}/users/{receiver_name}" #對方的代表網址
recipient_inbox = f"https://{recipient_domain}/users/{receiver_name}/inbox" #對方的寄件匣

sender_url = f"https://{sender_domain}/users/{default_sender}" # 發信者用戶網址
sender_key = f"https://{sender_domain}/users/{default_sender}#main-key" # 發信者的 key

# 這個舉動(發送 follow request)的 identified(獨一)的識別網址,這裏使用 test,實際可以用亂數
activity_id = f"https://{sender_domain}/users/{default_sender}/follows/test"


'''簽署 http request'''
home_folder = os.path.expanduser("~") # 家目錄
private_key_path = os.path.join(home_folder, "my_folder/my_keys/private.pem")

private_key_path = f"/etc/letsencrypt/live/{sender_domain}/privkey.pem" # 私鑰路徑
private_key_text = open(private_key_path, 'rb').read() # load from file

# 將 private_key_text 的文字存成 private_key
private_key = crypto_serialization.load_pem_private_key(
private_key_text,
password=None,
backend=crypto_default_backend()
)

# 產生格式如 Mon, 21 Nov 2022 14:47:28 GMT 的現在時間(UTC 時區)
current_date = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')


recipient_parsed = urlparse(recipient_inbox) #分析收件方的地址
recipient_path = recipient_parsed.path # '/users/bob/inbox'



# following 的訊息內容 (dict)
follow_request_message = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": activity_id,
"type": "Follow",
"actor": sender_url, # 收件者地址
"object": recipient_url # 寄件者地址
}

'''製作digest摘要資訊'''
# 將follow_request_message 轉 json
follow_request_json = json.dumps(follow_request_message)

#產生上述 json 的 hash 值,並用 base64 編碼,存成 digest(摘要)
digest = base64.b64encode(hashlib.sha256(follow_request_json.__str__().encode('utf-8')).digest())


# 建立簽署用文字
# 格式為:
#(request-target): post [recipient_path]
#digest: [digest]
#host: [recipient_domain]
#date: [current_date]
# 方框[]內的值用 utf-8 編碼
signature_text = b'(request-target): post %s\ndigest: SHA-256=%s\nhost: %s\ndate: %s' % \
(recipient_path.encode('utf-8'),
digest,
recipient_domain.encode('utf-8'),
current_date.encode('utf-8'))

# 用私鑰產生 signature_text 簽署得出的值
raw_signature = private_key.sign(
signature_text,
padding.PKCS1v15(),
hashes.SHA256()
)

# 將簽署值轉成 base64
raw_signature_in_base64 = base64.b64encode(raw_signature).decode('utf-8')
print(signature_text)

# 提供寄件者公鑰 sender_key 和轉成 base64 的簽署值,存到 signature_header
# headers="(request-target) digest host date 和上面提到的簽署用文字格式相似
signature_header = f'keyId="{sender_key}",algorithm="rsa-sha256",headers="(request-target) digest host date",signature="{raw_signature_in_base64}"'

# 設定 request 的 http 表頭
headers = {
'Date': current_date, # 日期
'Content-Type': 'application/activity+json',
'Host': recipient_domain, # 收件者域名
'Digest': "SHA-256="+digest.decode('utf-8'), # digest 用 UTF-8 解碼
'Signature': signature_header
}


# 以 POST 方法送出 request,回應存在 r
r = requests.post(recipient_inbox,
headers=headers, # 表頭
json=follow_request_message # 內容
)

print(r, r.content) # 顯示回應內容

這樣傳輸,會給出 <200> code,代表成功:

1
<Response [202]> b''

另可參這個 Microblog 的程式片段,但是使用的加密函式庫不一樣,就留給讀者參考吧。