Files
dev-ops-agent/scripts/import_volcengine_certificate.py
T

189 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Import a local certificate/key pair into Volcengine Certificate Service.
This script deliberately reads certificate material from files at runtime so
private keys do not need to appear in shell arguments.
"""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import hmac
import json
import sys
import urllib.error
import urllib.request
from pathlib import Path
def read_dotenv(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
name, value = line.split("=", 1)
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
value = value[1:-1]
values[name.strip()] = value
return values
def sha256_hex(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def hmac_bytes(key: bytes, text: str) -> bytes:
return hmac.new(key, text.encode("utf-8"), hashlib.sha256).digest()
def hmac_hex(key: bytes, text: str) -> str:
return hmac.new(key, text.encode("utf-8"), hashlib.sha256).hexdigest()
def sign_headers(
*,
access_key: str,
secret_key: str,
region: str,
service: str,
host: str,
method: str,
uri: str,
query: str,
body: str,
) -> dict[str, str]:
payload_hash = sha256_hex(body)
now = dt.datetime.now(dt.UTC)
x_date = now.strftime("%Y%m%dT%H%M%SZ")
short_date = now.strftime("%Y%m%d")
canonical_headers = (
f"content-type:application/json\n"
f"host:{host}\n"
f"x-content-sha256:{payload_hash}\n"
f"x-date:{x_date}\n"
)
signed_headers = "content-type;host;x-content-sha256;x-date"
canonical_request = "\n".join(
[
method,
uri,
query,
canonical_headers,
signed_headers,
payload_hash,
]
)
scope = f"{short_date}/{region}/{service}/request"
string_to_sign = "\n".join(
[
"HMAC-SHA256",
x_date,
scope,
sha256_hex(canonical_request),
]
)
# Volcengine OpenAPI uses the raw SK as the first HMAC key for this API.
k_date = hmac_bytes(secret_key.encode("utf-8"), short_date)
k_region = hmac_bytes(k_date, region)
k_service = hmac_bytes(k_region, service)
k_signing = hmac_bytes(k_service, "request")
signature = hmac_hex(k_signing, string_to_sign)
authorization = (
f"HMAC-SHA256 Credential={access_key}/{scope}, "
f"SignedHeaders={signed_headers}, Signature={signature}"
)
return {
"Authorization": authorization,
"Content-Type": "application/json",
"Host": host,
"X-Content-Sha256": payload_hash,
"X-Date": x_date,
}
def import_certificate(args: argparse.Namespace) -> dict:
env = read_dotenv(Path(args.env_path))
access_key = env.get(f"{args.profile_prefix}_VOLCENGINE_ACCESS_KEY", "")
secret_key = env.get(f"{args.profile_prefix}_VOLCENGINE_SECRET_KEY", "")
if not access_key or not secret_key:
raise RuntimeError(
f"Missing {args.profile_prefix}_VOLCENGINE_ACCESS_KEY or "
f"{args.profile_prefix}_VOLCENGINE_SECRET_KEY in {args.env_path}"
)
certificate = Path(args.certificate_path).read_text(encoding="utf-8")
private_key = Path(args.private_key_path).read_text(encoding="utf-8")
body = json.dumps(
{
"CertificateInfo": {
"CertificateChain": certificate,
"PrivateKey": private_key,
},
"ProjectName": args.project_name,
"Repeatable": True,
"Tag": args.domain,
},
ensure_ascii=False,
separators=(",", ":"),
)
service = "certificate_service"
host = "open.volcengineapi.com"
query = "Action=ImportCertificate&Version=2024-10-01"
headers = sign_headers(
access_key=access_key,
secret_key=secret_key,
region=args.region,
service=service,
host=host,
method="POST",
uri="/",
query=query,
body=body,
)
request = urllib.request.Request(
f"https://{host}/?{query}",
data=body.encode("utf-8"),
headers=headers,
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=args.timeout) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
# Do not print Authorization, AK, SK, certificate, or private key.
raise RuntimeError(f"Volcengine API request failed with HTTP status {exc.code}") from None
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--domain", required=True)
parser.add_argument("--certificate-path", required=True)
parser.add_argument("--private-key-path", required=True)
parser.add_argument("--env-path", default=".env")
parser.add_argument("--profile-prefix", default="INTELLIGROW")
parser.add_argument("--region", default="cn-beijing")
parser.add_argument("--project-name", default="default")
parser.add_argument("--timeout", type=int, default=30)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
result = import_certificate(args)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())