76 lines
4.2 KiB
Python
76 lines
4.2 KiB
Python
"""Vendor-only offline issuer. Never include this tool or its private key in a customer installer."""
|
|
import argparse
|
|
import base64
|
|
from datetime import datetime, timezone
|
|
import getpass
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import uuid
|
|
sys.path.insert(0,str(Path(__file__).resolve().parents[2]))
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
from monitor_runtime.licensing import canonical, verify_document, timestamp
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser()
|
|
sub=parser.add_subparsers(dest='command',required=True)
|
|
keygen=sub.add_parser('keygen')
|
|
keygen.add_argument('--private-key',type=Path,required=True)
|
|
keygen.add_argument('--public-key',type=Path,required=True)
|
|
keygen.add_argument('--dpapi',action='store_true',help='Encrypt private key for current Windows account')
|
|
issue=sub.add_parser('issue')
|
|
issue.add_argument('--private-key',type=Path,required=True)
|
|
issue.add_argument('--request',type=Path,required=True)
|
|
issue.add_argument('--customer',required=True)
|
|
issue.add_argument('--expires',help='ISO 8601 time with timezone; omitted means perpetual')
|
|
issue.add_argument('--output',type=Path,required=True)
|
|
args=parser.parse_args()
|
|
if args.command=='keygen':
|
|
if args.private_key.exists() or args.public_key.exists():
|
|
parser.error('Refusing to overwrite existing issuer keys')
|
|
key=Ed25519PrivateKey.generate()
|
|
if args.dpapi:
|
|
from monitor_runtime.windows import protect
|
|
raw=key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption())
|
|
encoded=b'MONITOR-DPAPI-V1\n'+base64.b64encode(protect(raw))
|
|
else:
|
|
password=getpass.getpass('Private-key password (minimum 12 characters): ')
|
|
if len(password)<12 or password!=getpass.getpass('Confirm password: '):
|
|
parser.error('Password too short or confirmation mismatch')
|
|
encoded=key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,
|
|
serialization.BestAvailableEncryption(password.encode()))
|
|
args.private_key.parent.mkdir(parents=True,exist_ok=True)
|
|
args.public_key.parent.mkdir(parents=True,exist_ok=True)
|
|
with args.private_key.open('xb') as f:f.write(encoded)
|
|
with args.public_key.open('xb') as f:
|
|
f.write(key.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo))
|
|
print('Issuer keypair created. Keep the private key offline and backed up.')
|
|
return
|
|
raw=args.private_key.read_bytes()
|
|
if raw.startswith(b'MONITOR-DPAPI-V1\n'):
|
|
from monitor_runtime.windows import protect
|
|
raw=protect(base64.b64decode(raw.split(b'\n',1)[1]),decrypt=True)
|
|
key=serialization.load_pem_private_key(raw,password=None)
|
|
else:
|
|
key=serialization.load_pem_private_key(raw,password=getpass.getpass('Private-key password: ').encode())
|
|
request=json.loads(args.request.read_text(encoding='utf-8'))
|
|
if request.get('product')!='monitor' or request.get('schema_version')!=1:
|
|
parser.error('Unsupported machine request')
|
|
fingerprint=request.get('fingerprint','')
|
|
if not fingerprint.startswith('v1:') or len(fingerprint)!=67 or any(c not in '0123456789abcdef' for c in fingerprint[3:]):
|
|
parser.error('Invalid machine fingerprint')
|
|
now=datetime.now(timezone.utc)
|
|
payload={'schema_version':1,'product':'monitor','license_id':str(uuid.uuid4()),'customer':args.customer,
|
|
'machine_fingerprint':fingerprint,'issued_at':now.isoformat(),
|
|
'expires_at':args.expires}
|
|
document={'payload':payload,'signature':base64.b64encode(key.sign(canonical(payload))).decode('ascii')}
|
|
public=key.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo)
|
|
verify_document(document,public,fingerprint)
|
|
args.output.parent.mkdir(parents=True,exist_ok=True)
|
|
with args.output.open('x',encoding='utf-8') as f:json.dump(document,f,ensure_ascii=False,indent=2)
|
|
print('License signed:',args.output)
|
|
|
|
if __name__=='__main__':
|
|
main()
|