Files
HP-Laserjet-Pro-Certificate…/refresh-certificate.py
2025-12-30 11:02:43 +01:00

673 lines
27 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Module pour la gestion de certificats et la communication avec les serveurs HP CDM.
Ce module fournit des fonctions pour créer des demandes de signature de certificat (CSR),
installer des certificats, et communiquer avec les serveurs HP CDM.
Fonctions principales :
- `get_bearer` : obtenir un jeton d'accès pour la communication avec le serveur HP CDM
- `get_csr` : créer une demande de signature de certificat (CSR)
- `install_certificate` : installer un certificat sur un serveur HP CDM
- `main` : fonction principale pour exécuter le programme
Dépendances :
- `argparse` pour la gestion des arguments de ligne de commande
- `configparser` pour la lecture des fichiers de configuration
- `getpass` pour la saisie de mots de passe
- `logging` pour la gestion des messages de log
- `requests` pour les requêtes HTTP
- `coloredlogs` pour la coloration des messages de log
Notes :
- Ce module est conçu pour être utilisé dans un environnement de gestion de certificats et
de communication avec les serveurs HP CDM.
- Les fonctions de ce module peuvent être utilisées séparément pour des tâches spécifiques,
ou ensemble pour créer un programme complet de gestion de certificats et de communication
avec les serveurs HP CDM.
"""
import configparser
import argparse
import logging
import sys
import json
import getpass
import random
import base64
from cryptography import x509
from cryptography.hazmat.primitives.serialization import Encoding
from dateutil.parser import parse
from typeguard import typechecked
from datetime import datetime, timedelta
import requests
import coloredlogs
@typechecked
def create_nonce(length: int=45) -> str:
"""
Generate a random nonce string.
This function creates a random string of a specified length, consisting only of valid
characters as defined in the URL specification (RFC 7230).
The generated string can be used as a nonce, for example in HTTP requests.
Code is inspired by main.js sent by printer
Args:
length (int, optional): The length of the nonce string. Defaults to 45.
Returns:
str: A random nonce string of the specified length.
Notes:
- The generated string only contains characters that are valid in URLs, as defined in
RFC 7230.
- The function uses a random number generator to select characters from the set of valid
characters.
"""
valid_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
nonce = ''
for _ in range(0, length):
pos = random.randint(0, len(valid_chars)-1)
nonce+= valid_chars[pos]
return nonce
@typechecked
def get_bearer(hostname: str, verify: bool, username: str, password:str) -> str:
"""
Retrieve an OAuth bearer token for authentication with a HP CDM server.
This function performs the necessary steps to obtain an OAuth bearer token, including:
- Retrieving the base content and redirection to the login URL
- Authenticating with the provided username and password
- Exchanging the authentication code for an access token
Args:
hostname (str): The hostname of the HP CDM server.
verify (bool): Whether to verify the SSL certificate of the server.
username (str): The username to use for authentication.
password (str): The password to use for authentication.
Returns:
str: The OAuth bearer token.
Notes:
- This function uses the `requests` library to make HTTP requests to the server.
- The function logs information and errors using the `logging` module.
- If an error occurs during the authentication process, the function exits with a non-zero
status code.
"""
logger = logging.getLogger(__name__)
logger.info("Retrieving base content")
base_url = f'https://{hostname}'
logger.info('Retrieving redirection to login URL.')
auth_url = base_url+'/cdm/oauth2/v1/authorize'
certificate_management_url = base_url+'/security/certificateManagement/certificates'
nonce = create_nonce()
params = { 'response_type':'code', 'client_id':'com.hp.cdm.client.hpEws', 'state':nonce,
'redirect_uri':certificate_management_url,
'scope':'com.hp.cdm.auth.alias.deviceRole.deviceAdmin'}
try:
r = requests.get(auth_url, params=params, allow_redirects=False, verify=verify, timeout=10)
except requests.exceptions.RequestException as e:
logger.error('Impossible to retrieve URL: %s. Error: %s', auth_url, e)
sys.exit(-1)
if r.status_code != 302:
logger.error('Impossible to retrieve redirection to login URL. Status code: %d',
r.status_code)
sys.exit(-1)
login_url = base_url+r.headers['Location']
logger.debug('UR to login: %s', login_url)
logger.info('Loading login page.')
r = requests.get(login_url, verify=verify, timeout=10)
if r.status_code != 200:
logger.error('')
sys.exit(-1)
# admin_url = base_url+'/cdm/security/v1/deviceAdminConfig'
authentication_url = base_url+'/cdm/security/v1/authenticate'
# Agent ID is useless
payload = { 'username':username, 'password':password, 'client_id':'com.hp.cdm.client.hpEws',
'scope':'com.hp.cdm.auth.alias.deviceRole.deviceAdmin',
'grant_type':'authorization_code' , 'state':nonce }
logger.info('Authenticating to receive authentication code.')
r = requests.post(authentication_url, data=json.dumps(payload), verify=verify, timeout=10)
if r.status_code != 200:
logger.error('Impossible to authenticate.')
sys.exit(-1)
response = json.loads(r.content.decode('utf8'))
code = response['code']
logger.debug('Authentication code: %s', code)
token_url = base_url+'/cdm/oauth2/v1/token'
logger.debug('Token URL: %s', token_url)
logger.info('Retrieving authentication bearer.')
payload = { 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': base_url,
'client_id': 'com.hp.cdm.client.hpEws' }
r = requests.post(token_url, data=payload, verify=verify, timeout=10)
if r.status_code != 200:
logger.error('Impossible to obtain Oauth bearer.')
sys.exit(-1)
response = json.loads(r.content.decode('utf8'))
bearer = response['access_token']
logger.debug('Bearer: %s', bearer)
return bearer
@typechecked
def get_csr(hostname: str, verify: bool, username: str, password: str, ou: str, org: str, city:str,
state: str, country: str, filename: str) -> None:
"""
Generate a Certificate Signing Request (CSR) for a HP CDM server.
This function performs the necessary steps to generate a CSR, including:
- Retrieving the base configuration for the CSR
- Updating the CSR description with the provided information
- Sending the updated CSR description to the server
- Waiting for the CSR generation to complete
- Saving the generated CSR to a file
Args:
hostname (str): The hostname of the HP CDM server.
verify (bool): Whether to verify the SSL certificate of the server.
username (str): The username to use for authentication.
password (str): The password to use for authentication.
ou (str): The organizational unit.
org (str): The organization.
city (str): The city.
state (str): The state.
country (str): The country.
filename (str): The filename to save the generated CSR to.
Returns:
None
Notes:
- This function uses the `requests` library to make HTTP requests to the server.
- The function logs information and errors using the `logging` module.
- If an error occurs during the CSR generation process, the function exits with a non-zero
status code.
"""
logger = logging.getLogger(__name__)
base_url = f'https://{hostname}'
bearer = get_bearer(hostname, verify, username, password)
logger.info('Retrieving CSR base configuration.')
csr_url = base_url+'/cdm/certificate/v1/certificateSigningRequest'
headers = { 'Authorization' : f'Bearer {bearer}'}
r = requests.get(csr_url, headers=headers, verify=verify, timeout=10)
if r.status_code != 200:
logger.error('Impossible to obtain CSR info')
sys.exit(-1)
csr_description = json.loads(r.content.decode('utf8'))
logger.debug('Defaut CSR values received: %s', csr_description)
csr_description['state'] = 'processing'
csr_description['links'][0]['hints'] = None
csr_description['certificateAttributes'] = {'commonName':hostname, 'organization':org,
'organizationalUnit':[ou],'cityOrLocality':city,
'stateOrProvince':state,'countryOrRegion':country}
logger.info('Sending CSR updated informations.')
r = requests.patch(csr_url, headers=headers, data=json.dumps(csr_description), verify=verify,
timeout=10)
if r.status_code != 204:
logger.error('Impossible to send CSR description')
sys.exit(-1)
finished = False
while not finished:
logger.info('Waiting for CSR generation.')
r = requests.get(csr_url, headers=headers, verify=verify, timeout=10)
if r.status_code != 200:
logger.error('Impossible to receive CSR. Status code: %d', r.status_code)
sys.exit(-1)
csr = json.loads(r.content.decode('utf8'))
logger.debug(csr)
if csr['state'] == 'idle':
finished = True
csr = csr['certificateData']
logger.debug('CSR:\n %s', csr)
with open(filename, 'w+', encoding='utf-8') as f:
f.write(csr)
@typechecked
def install_certificate(hostname: str, verify: bool, username: str, password: str, filename:str,
bearer=None) -> None:
"""
Install a certificate on a HP CDM server.
This function performs the necessary steps to install a certificate, including:
- Reading the certificate from a file
- Encoding the certificate in base64
- Authenticating with the server using a bearer token
- Sending the certificate to the server for installation
Args:
hostname (str): The hostname of the HP CDM server.
verify (bool): Whether to verify the SSL certificate of the server.
username (str): The username to use for authentication.
password (str): The password to use for authentication.
filename (str): The filename of the certificate to install.
bearer (str, optional): The access token for authentication. If None, the token is
generated automatically. Defaults to None.
Returns:
None
Notes:
- This function uses the `requests` library to make HTTP requests to the server.
- The function logs information and errors using the `logging` module.
- If an error occurs during the certificate installation process, the function exits with
a non-zero status code.
"""
logger = logging.getLogger(__name__)
base_url = f'https://{hostname}'
if bearer is None:
bearer = get_bearer(hostname, verify, username, password)
with open(filename, 'rb') as f:
data = f.read()
certs = x509.load_pem_x509_certificates(data)
for cert in certs:
print(f'Subject: {cert.subject}. Issuer: {cert.issuer}.')
extensions = cert.extensions
ca = False
for ext in extensions:
if isinstance(ext.value, x509.extensions.BasicConstraints):
ca = ext.value.ca
break
cert = cert.public_bytes(Encoding.PEM)
cert = base64.b64encode(cert)
cert = cert.decode('ascii')
logger.debug('Installing certificate: %s. CA: %s', cert, ca)
url = base_url+'/cdm/certificate/v1/certificates'
# May be cert should be in base 64
# Request type:
# installId for identity certificate
# importCA for CA for certificate
# certificateData: Base 64 encoding of the PEM certificate itself.
if ca:
request_type = 'importCa'
else:
request_type = 'installId'
certificate = { 'version':'1.1.0', 'requestType':request_type, 'certificateFormat':
'pem', 'certificateData':cert }
headers = { 'Authorization': f'Bearer {bearer}' }
r = requests.post(url, headers=headers, data=json.dumps(certificate), verify=verify,
timeout=10)
if r.status_code not in [200, 201]:
logger.error('Impossible to install certificate. Status code: %d.', r.status_code)
sys.exit(-1)
logger.info('Certificate successfully installed.')
@typechecked
def self_signed(hostname:str, verify:bool, username: str, password: str, bearer=None) -> None:
logger = logging.getLogger(__name__)
base_url = f'https://{hostname}'
if bearer is None:
bearer = get_bearer(hostname, verify, username, password)
now = datetime.now()
end = now + timedelta(days=365)
end = end.strftime('%Y-%m-%dT%H:%M:%S.000Z')
url = base_url+'/cdm/certificate/v1/certificates/selfSignedCertificate'
certificate = {"version":"1.1.0","signatureAlgorithm":"sha256WithRsa",
"subjectAlternativeNameList":[hostname], "privateKeyExportable": False,
"links":[{"rel":"certificateConstraints",
"href":"/cdm/certificate/v1/certificates/selfSignedCertificate/constraints",
"hints":None}],
"certificateAttributes":{"commonName":hostname},
"keyInfo":{"keyType":"rsa","keyStrength":"bits2048"},
"validity":{"toDate":end},"state":"processing"}
headers = { 'Authorization': f'Bearer {bearer}' }
r = requests.patch(url, headers=headers, data=json.dumps(certificate), verify=verify,
timeout=10)
if r.status_code != 204:
logger.error('Impossible to create a self-signed certificate. Error code: %d',
r.status_code)
sys.exit(-1)
logger.info('Self-signed certificate created successfully.')
@typechecked
def get_certificates(hostname:str, verify:bool, username: str, password: str,
bearer=None) -> dict[int, dict]:
"""
Retrieve a list of certificates from an HP CDM server.
This function uses the HP CDM server API to retrieve a list of certificates and returns them
as a dictionary.
Args:
hostname (str): The hostname of the HP CDM server.
verify (bool): Verify the SSL certificate validity of the server.
username (str): The username for authentication.
password (str): The password for authentication.
bearer (str, optional): The access token for authentication. If None, the token is
generated automatically. Defaults to None.
Returns:
dict: A dictionary where each key is a certificate index (starting from 1) and each value
is the corresponding certificate.
Notes:
- This function uses the `requests` library to make the certificate retrieval request.
- If the certificate retrieval fails, the function logs an error and exits with a
non-zero status code.
- If no certificates are found, the function logs an error and exits with a non-zero
status code.
"""
logger = logging.getLogger(__name__)
base_url = f'https://{hostname}'
res = {}
if bearer is None:
bearer = get_bearer(hostname, verify, username, password)
url = base_url+'/cdm/certificate/v1/certificates'
headers = { 'Authorization': f'Bearer {bearer}' }
r = requests.get(url, headers=headers, verify=verify, timeout=10)
if r.status_code != 200:
logger.error('Impossible to obtain certificates list')
sys.exit(-1)
certificates = json.loads(r.content.decode('utf8'))
if 'certificates' not in certificates:
logger.error('No certificates found')
sys.exit(-1)
certificates = certificates['certificates']
num = 1
for certificate in certificates:
res[num] = certificate
num+=1
return res
@typechecked
def delete_certificate(hostname, verify, username, password, certificates, certid, bearer=None):
"""
Delete a certificate from an HP CDM server.
This function uses the HP CDM server API to delete a specified certificate.
Args:
hostname (str): The hostname of the HP CDM server.
verify (bool): Verify the SSL certificate validity of the server.
username (str): The username for authentication.
password (str): The password for authentication.
certificates (list): The list of available certificates on the server.
certid (int): The index of the certificate to delete in the list of certificates.
bearer (str, optional): The access token for authentication. If None, the token is
generated automatically. Defaults to None.
Returns:
None
Notes:
- This function uses the `requests` library to make the certificate deletion request.
- If the certificate deletion fails, the function logs an error and exits with a non-zero
status code.
Raises:
sys.exit(-1) if the certificate deletion fails.
"""
logger = logging.getLogger(__name__)
base_url = f'https://{hostname}'
if bearer is None:
bearer = get_bearer(hostname, verify, username, password)
logger.debug('Certificates: %s', certificates)
cert = certificates[certid]
certid = cert.get('certificateId')
url = base_url+f'/cdm/certificate/v1/certificates/{certid}'
headers = { 'Authorization': f'Bearer {bearer}' }
r = requests.delete(url, headers=headers, verify=verify, timeout=10)
if r.status_code != 200:
logger.error('Impossible to delete certificate')
sys.exit(-1)
logger.info('Certificate deleted with success.')
def main():
"""
Main entry point of the program.
This function parses command-line arguments, reads configuration files, and performs the
necessary actions based on the provided commands.
Args:
None (command-line arguments are parsed using argparse)
Returns:
None (the program exits with a status code)
Notes:
- The program uses the following commands:
- `csr`: create a Certificate Signing Request (CSR)
- `pem`: install a certificate
- The program uses the following options:
- `--debug`: activate debug mode
- `--config`: specify a configuration file
- `--user`: specify a username
- `--password`: specify a password
- `--host`: specify a hostname
- `--no-tls-verification`: disable TLS verification
- The program logs information and errors using the `logging` module.
- If an error occurs during the execution of the program, the program exits with a
non-zero status code.
"""
logger = logging.getLogger(__name__)
coloredlogs.install()
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--debug", dest='debug', action='store_true', required=False,
help="Activate debug.")
parser.add_argument("-c", "--config", dest='config_filename', required=False, default=None,
help="Configuration file.")
parser.add_argument("-u", "--user", dest='username', required=False, default='admin',
help="Username.")
parser.add_argument("-p", "--password", dest='password', nargs='?', required=False,
default=None, help="Password.")
parser.add_argument("-H", "--host", dest='hostname', required=False, default=None,
help="Hostname.")
parser.add_argument("-n", "--no-tls-verification", dest='verify', default=None,
action='store_const', const=False,
required=False, help="Verify certificate validity.")
subparsers = parser.add_subparsers(dest='command', required=True, help='command help')
subparsers.add_parser('list', help='List certificates.')
subparsers.add_parser('self', help='Create a self-signed certificate.')
parser_delete = subparsers.add_parser('del', help='Delete a certificate.')
parser_delete.add_argument("-#", "--number", dest='certid', required=True, type=int,
help="Certificate number as given by list command.")
parser_create_csr = subparsers.add_parser('csr', help='Create a certificate signing request.')
parser_create_csr.add_argument("-C", "--country", dest='country', required=False, default='US',
help="Country.")
parser_create_csr.add_argument("-s", "--state", dest='state', required=False, help="State.")
parser_create_csr.add_argument("-c", "--city", dest='city', required=False, help="State.")
parser_create_csr.add_argument("--ou", dest='organizational_unit', required=False,
help="Organizational Unit.")
parser_create_csr.add_argument("--org", dest='organization', required=False,
help="Organization.")
parser_create_csr.add_argument("-o", "--output", dest='output', required=False, default=None,
help="Output file.")
parser_install_certificate = subparsers.add_parser('pem', help='Install a certificate.')
parser_install_certificate.add_argument("-i", "--input", dest='input', required=True,
default=None, help="Input file.")
args = parser.parse_args()
if not hasattr(args, 'country'):
args.country = None
if not hasattr(args, 'state'):
args.state = None
if not hasattr(args, 'city'):
args.city = None
if not hasattr(args, 'organizational_unit'):
args.organizational_unit = None
if not hasattr(args, 'organization'):
args.organization = None
logger.info("Arguments: %s", args)
if args.config_filename is not None:
try:
with open(args.config_filename, 'r', encoding='utf-8') as config_file:
config = configparser.ConfigParser()
config.read_file(config_file)
except OSError as e:
logger.info('Impossible to open configuration file. Error: %s', e)
sys.exit(-1)
sections = config.sections()
if 'TLS' in sections:
options = config.options('TLS')
if 'verify' in options and args.verify is None:
args.verify = config.getboolean('TLS', 'verify')
if 'Login' in sections:
options = config.options('Login')
if 'user' in options and args.username is None:
args.username = config.get('Login','user')
if 'password' in options and args.password is None:
args.password = config.get('Login','password')
if 'host' in options and args.hostname is None:
args.hostname = config.get('Login','host')
if 'CSR' in sections:
options = config.options('CSR')
if 'org' in options and args.organization is None :
args.organization = config.get('CSR','org')
if 'ou' in options and args.organizational_unit is None :
args.organizational_unit = config.get('CSR','ou')
if 'country' in options and args.country is None:
args.country = config.get('CSR','country')
if 'state' in options and args.state is None:
args.state = config.get('CSR','state')
if 'city' in options and args.city is None:
args.city = config.get('CSR','city')
if args.verify is None:
args.verification = False
if args.username is None:
logger.error('Username is mandatory')
parser.print_help()
sys.exit(-1)
if args.hostname is None:
logger.error('Hostname is mandatory')
parser.print_help()
sys.exit(-1)
if args.password is None:
args.password = getpass.getpass()
if args.debug:
logger.info('Setting logging to debug mode')
coloredlogs.set_level(level=logging.DEBUG)
logger.info("Arguments: %s", args)
if args.command == 'csr':
if args.output is None:
args.output = f'{args.hostname}.csr'
logger.info('Final arguments: %s', args)
match args.command:
case 'list':
certs = get_certificates(hostname=args.hostname, verify=args.verify,
username=args.username,
password=args.password)
logger.info('List of certificates:')
for certid, cert in certs.items():
subject = cert.get('subject')
issuer = cert.get('issuer')
validity = cert.get('validity')
begin = parse(validity.get('fromDate')).strftime('%Y-%m-%d (%H:%M)')
end = parse(validity.get('toDate')).strftime('%Y-%m-%d (%H:%M)')
print(f'{certid} - {subject} issued by {issuer}. From: {begin} to {end}')
case 'del':
bearer = get_bearer(hostname=args.hostname, verify=args.verify, username=args.username,
password=args.password)
certs = get_certificates(hostname=args.hostname, verify=args.verify,
username=args.username,
password=args.password, bearer=bearer)
if args.certid not in certs:
logger.error('Certificate #%d does not exist', args.certid)
sys.exit(-1)
delete_certificate(hostname=args.hostname, verify=args.verify, username=args.username,
password=args.password, certificates=certs, certid=args.certid,
bearer=bearer)
case 'csr':
get_csr(hostname=args.hostname, verify=args.verify, username=args.username,
password=args.password, org=args.organization, ou=args.organizational_unit,
city=args.city, state=args.state, country=args.country, filename=args.output)
case 'pem':
install_certificate(hostname=args.hostname, verify=args.verify, username=args.username,
password=args.password, filename=args.input)
case 'self':
self_signed(hostname=args.hostname, verify=args.verify, username=args.username,
password=args.password)
case _:
logger.error('Unknown command: %s', args.command)
sys.exit(-1)
if __name__ == "__main__":
main()