126 lines
4.0 KiB
Python
126 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import secrets
|
|
from pathlib import Path
|
|
|
|
from Crypto.Cipher import AES
|
|
from Crypto.Hash import SHA256
|
|
from Crypto.Protocol.KDF import HKDF
|
|
from Crypto.PublicKey import ECC
|
|
from Crypto.Signature import eddsa
|
|
|
|
|
|
def _derive_wrap_material(master_secret: bytes, kid: str, name: str, version: str) -> tuple[bytes, bytes]:
|
|
wrap_key = HKDF(
|
|
master=master_secret,
|
|
key_len=32,
|
|
salt=kid.encode("utf-8"),
|
|
hashmod=SHA256,
|
|
num_keys=1,
|
|
context=f"secspider:{name}:{version}:wrap-key".encode("utf-8"),
|
|
)
|
|
wrap_nonce = HKDF(
|
|
master=master_secret,
|
|
key_len=12,
|
|
salt=kid.encode("utf-8"),
|
|
hashmod=SHA256,
|
|
num_keys=1,
|
|
context=f"secspider:{name}:{version}:wrap-nonce".encode("utf-8"),
|
|
)
|
|
return wrap_key, wrap_nonce
|
|
|
|
|
|
def _signing_bytes(headers: dict[str, str], payload_b64: str) -> bytes:
|
|
return "\n".join(
|
|
[
|
|
f"//@name:{headers['name']}",
|
|
f"//@version:{headers['version']}",
|
|
f"//@remark:{headers['remark']}",
|
|
f"//@format:{headers['format']}",
|
|
f"//@alg:{headers['alg']}",
|
|
f"//@wrap:{headers['wrap']}",
|
|
f"//@sign:{headers['sign']}",
|
|
f"//@kid:{headers['kid']}",
|
|
f"//@nonce:{headers['nonce']}",
|
|
f"//@ek:{headers['ek']}",
|
|
f"//@hash:{headers['hash']}",
|
|
f"payload.base64:{payload_b64}",
|
|
]
|
|
).encode("utf-8")
|
|
|
|
|
|
def build_secspider_package(
|
|
*,
|
|
source_text: str,
|
|
name: str,
|
|
version: str,
|
|
remark: str,
|
|
kid: str,
|
|
signing_private_key,
|
|
master_secret: bytes,
|
|
) -> str:
|
|
source_bytes = source_text.encode("utf-8")
|
|
source_hash = SHA256.new(source_bytes).hexdigest()
|
|
content_key = secrets.token_bytes(32)
|
|
payload_nonce = secrets.token_bytes(12)
|
|
wrap_key, wrap_nonce = _derive_wrap_material(master_secret, kid, name, version)
|
|
|
|
wrap_cipher = AES.new(wrap_key, AES.MODE_GCM, nonce=wrap_nonce)
|
|
wrapped_key, wrapped_tag = wrap_cipher.encrypt_and_digest(content_key)
|
|
|
|
payload_cipher = AES.new(content_key, AES.MODE_GCM, nonce=payload_nonce)
|
|
payload_ciphertext, payload_tag = payload_cipher.encrypt_and_digest(source_bytes)
|
|
payload_b64 = base64.b64encode(payload_ciphertext + payload_tag).decode("ascii")
|
|
|
|
headers = {
|
|
"name": name,
|
|
"version": version,
|
|
"remark": remark,
|
|
"format": "secspider/1",
|
|
"alg": "aes-256-gcm",
|
|
"wrap": "hkdf-aes-keywrap",
|
|
"sign": "ed25519",
|
|
"kid": kid,
|
|
"nonce": "base64:" + base64.b64encode(payload_nonce).decode("ascii"),
|
|
"ek": "base64:" + base64.b64encode(wrapped_key + wrapped_tag).decode("ascii"),
|
|
"hash": f"sha256:{source_hash}",
|
|
}
|
|
|
|
signer = eddsa.new(signing_private_key, "rfc8032")
|
|
headers["sig"] = "base64:" + base64.b64encode(signer.sign(_signing_bytes(headers, payload_b64))).decode("ascii")
|
|
|
|
return "\n".join(
|
|
[
|
|
"// ignore",
|
|
f"//@name:{headers['name']}",
|
|
f"//@version:{headers['version']}",
|
|
f"//@remark:{headers['remark']}",
|
|
f"//@format:{headers['format']}",
|
|
f"//@alg:{headers['alg']}",
|
|
f"//@wrap:{headers['wrap']}",
|
|
f"//@sign:{headers['sign']}",
|
|
f"//@kid:{headers['kid']}",
|
|
f"//@nonce:{headers['nonce']}",
|
|
f"//@ek:{headers['ek']}",
|
|
f"//@hash:{headers['hash']}",
|
|
f"//@sig:{headers['sig']}",
|
|
"// ignore",
|
|
f"payload.base64:{payload_b64}",
|
|
]
|
|
)
|
|
|
|
|
|
def generate_signing_keypair() -> tuple[str, str]:
|
|
private_key = ECC.generate(curve="Ed25519")
|
|
public_key = private_key.public_key()
|
|
return private_key.export_key(format="PEM"), public_key.export_key(format="PEM")
|
|
|
|
|
|
def load_signing_private_key(path: str | Path):
|
|
return ECC.import_key(Path(path).read_text(encoding="utf-8"))
|
|
|
|
|
|
def load_master_secret(path: str | Path) -> bytes:
|
|
return Path(path).read_text(encoding="utf-8").strip().encode("utf-8")
|