84 lines
3.4 KiB
Python
84 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate an Ed25519 keypair for signing GovOPlaN release catalogs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
from datetime import UTC, datetime
|
|
import json
|
|
from pathlib import Path
|
|
import stat
|
|
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--key-id", required=True, help="Public key id recorded in catalog signatures.")
|
|
parser.add_argument("--private-key", type=Path, required=True, help="Private PEM output path. Keep outside git.")
|
|
parser.add_argument("--public-key", type=Path, help="Optional base64 public key output path.")
|
|
parser.add_argument("--keyring", type=Path, help="Optional public keyring JSON output path.")
|
|
parser.add_argument("--status", default="active", choices=("active", "next", "retired", "revoked", "disabled"))
|
|
parser.add_argument("--force", action="store_true", help="Overwrite existing key files.")
|
|
args = parser.parse_args()
|
|
|
|
private_path = args.private_key.expanduser()
|
|
public_path = args.public_key.expanduser() if args.public_key else None
|
|
keyring_path = args.keyring.expanduser() if args.keyring else None
|
|
if private_path.exists() and not args.force:
|
|
raise SystemExit(f"Private key already exists: {private_path}")
|
|
if public_path is not None and public_path.exists() and not args.force:
|
|
raise SystemExit(f"Public key already exists: {public_path}")
|
|
|
|
private_key = Ed25519PrivateKey.generate()
|
|
private_bytes = private_key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.PKCS8,
|
|
encryption_algorithm=serialization.NoEncryption(),
|
|
)
|
|
public_bytes = private_key.public_key().public_bytes(
|
|
encoding=serialization.Encoding.Raw,
|
|
format=serialization.PublicFormat.Raw,
|
|
)
|
|
public_base64 = base64.b64encode(public_bytes).decode("ascii")
|
|
|
|
private_path.parent.mkdir(parents=True, exist_ok=True)
|
|
private_path.write_bytes(private_bytes)
|
|
private_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
|
|
if public_path is not None:
|
|
public_path.parent.mkdir(parents=True, exist_ok=True)
|
|
public_path.write_text(public_base64 + "\n", encoding="utf-8")
|
|
|
|
if keyring_path is not None:
|
|
keyring_path.parent.mkdir(parents=True, exist_ok=True)
|
|
keyring = {
|
|
"keyring_version": "1",
|
|
"purpose": "govoplan module package catalog signatures",
|
|
"generated_at": datetime.now(tz=UTC).isoformat().replace("+00:00", "Z"),
|
|
"keys": [
|
|
{
|
|
"key_id": args.key_id,
|
|
"status": args.status,
|
|
"public_key": public_base64,
|
|
"not_before": datetime.now(tz=UTC).date().isoformat() + "T00:00:00Z",
|
|
}
|
|
],
|
|
}
|
|
keyring_path.write_text(json.dumps(keyring, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
|
|
print(f"private_key={private_path}")
|
|
if public_path is not None:
|
|
print(f"public_key={public_path}")
|
|
if keyring_path is not None:
|
|
print(f"keyring={keyring_path}")
|
|
print(f"key_id={args.key_id}")
|
|
print(f"public_key_base64={public_base64}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|