Compare commits

...

6 Commits

Author SHA1 Message Date
Frédéric Tronel
0d772f59de Improve display of help and list certificate. 2025-12-30 11:02:43 +01:00
Frédéric Tronel
9f98cd1e70 Add a command to create a self-signed certificate. 2025-12-30 10:57:46 +01:00
Frédéric Tronel
8d709213d3 Improve README. 2025-12-29 15:48:56 +01:00
Frédéric Tronel
54d2338da6 Improve logging when operations are successful. 2025-12-29 15:10:21 +01:00
Frédéric Tronel
c286b5147f Add typeguard and dateutils as requirements. 2025-12-29 15:09:52 +01:00
Frédéric Tronel
115fa39618 Add typechecking. 2025-12-29 15:08:07 +01:00
3 changed files with 122 additions and 34 deletions

View File

@@ -2,43 +2,67 @@
I wanted to install a certificate generated by Let's encrypt on my brand new laser printer I wanted to install a certificate generated by Let's encrypt on my brand new laser printer
(HP Laserjet MFJ 4302) for the integrated administration webserver. (HP Laserjet MFJ 4302) for the integrated administration webserver.
After research on the web, I discovered After research on the web, I discovered that although there were projects supporting older models
nothing was existing for newer models.
Hence I decided to write my own tool in Python.
It was designed by reverse engineering the integrated administration webserver following the
network exchange when using the menu dedicated to certificate management
(Security -> Certicates management)
# Arguments # Command
positional arguments: ```bash
{csr,pem} command help {list, del, csr,pem}
csr Create CSR list List certificates known by the printer
pem Install certificate del Delete a certificate
csr Create a certificate signing request (CSR)
options: pem Install a PEM certificate
-h, --help show this help message and exit
-d, --debug Activate debug.
-c, --config CONFIG_FILENAME
Configuration file.
-u, --user USERNAME Username.
-p, --password [PASSWORD]
Password.
-H, --host HOSTNAME Hostname.
-n, --no-tls-verification
Verify certificate validity.
Common options:
-h, --help Show this help message and exit
-d, --debug Activate debug.
-c, --config CONFIG_FILENAME Configuration file.
-u, --user USERNAME Username (admin).
-p, --password [PASSWORD] Specify admin password.
-H, --host HOSTNAME Hostname.
-n, --no-tls-verification Do not verify certificate validity.
```
# How to use it ? # How to use it ?
To obtain a CSR: After playing a lot with the administration web server, I discover a reproducible way to convince
``` the printer to accept a Let's Encrypt certificate.
First you need to ask a CSR generated by the printer:
```bash
./refresh-certificate.py -c ./config.ini -n -p PASSWORD csr ./refresh-certificate.py -c ./config.ini -n -p PASSWORD csr
``` ```
This way the private key part of the certificate is only known by the printer.
I was not able to import the private key of a Let's Encrypt certificate as obtained when generating
the certificate directly from ACME.
To obtain a PEM certificate from Let's encrypt: Next you need to ask Let's Encrypt to sign the CSR:
``` ```
certbot certonly --webroot -w /var/www/letsencrypt/ --csr printer.csr certbot certonly --webroot -w /var/www/letsencrypt/ --csr printer.csr
``` ```
To install the PEM file on the printer: You should obtain three PEM files in response:
1. _cert.pem_: the certificate itself (PEM format)
2. _chain.pem_: the intermediate CA that signed the certificate (PEM format)
3. _fullchain.pem_: the two previous files concatenated.
Finally you can install the certificate PEM file on the printer:
``` ```
./refresh-certificate.py -c ./config.ini -n -p PASSWORD pem ./refresh-certificate.py -c ./config.ini -n -p PASSWORD pem -i fullchain.pem
``` ```
Please note that during the last step, you can add either the _cert.pem_ file or
the _fullchain.pem_ files. In both cases, you should be able to connect to the printer webserver
without warning message from your navigator (tested on Firefox and Chromium).
This is possible because the main navigators comes with a certificates store that contains
not only the root certificate of Let's Encrypt but also the intermediate certificates.
However, note that most others tools will fail to connect to the Web server of the printer
(including this tool) because the printer will only present the final certificate.
You really have to install the _fullchain.pem_ to remove TLS connexion errors from all tools.

View File

@@ -39,11 +39,15 @@ import random
import base64 import base64
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.primitives.serialization import Encoding from cryptography.hazmat.primitives.serialization import Encoding
from dateutil.parser import parse
from typeguard import typechecked
from datetime import datetime, timedelta
import requests import requests
import coloredlogs import coloredlogs
def create_nonce(length: int=45): @typechecked
def create_nonce(length: int=45) -> str:
""" """
Generate a random nonce string. Generate a random nonce string.
@@ -72,7 +76,9 @@ def create_nonce(length: int=45):
return nonce return nonce
def get_bearer(hostname: str, verify: bool, username: str, password:str):
@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. Retrieve an OAuth bearer token for authentication with a HP CDM server.
@@ -170,8 +176,10 @@ def get_bearer(hostname: str, verify: bool, username: str, password:str):
return bearer return bearer
@typechecked
def get_csr(hostname: str, verify: bool, username: str, password: str, ou: str, org: str, city:str, def get_csr(hostname: str, verify: bool, username: str, password: str, ou: str, org: str, city:str,
state: str, country: str, filename: str): state: str, country: str, filename: str) -> None:
""" """
Generate a Certificate Signing Request (CSR) for a HP CDM server. Generate a Certificate Signing Request (CSR) for a HP CDM server.
@@ -252,12 +260,16 @@ def get_csr(hostname: str, verify: bool, username: str, password: str, ou: str,
finished = True finished = True
csr = csr['certificateData'] csr = csr['certificateData']
print(csr)
logger.debug('CSR:\n %s', csr)
with open(filename, 'w+', encoding='utf-8') as f: with open(filename, 'w+', encoding='utf-8') as f:
f.write(csr) f.write(csr)
def install_certificate(hostname, verify, username, password, filename, bearer=None):
@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. Install a certificate on a HP CDM server.
@@ -333,7 +345,44 @@ def install_certificate(hostname, verify, username, password, filename, bearer=N
logger.info('Certificate successfully installed.') logger.info('Certificate successfully installed.')
def get_certificates(hostname, verify, username, password, bearer=None): @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. Retrieve a list of certificates from an HP CDM server.
@@ -386,6 +435,8 @@ def get_certificates(hostname, verify, username, password, bearer=None):
return res return res
@typechecked
def delete_certificate(hostname, verify, username, password, certificates, certid, bearer=None): def delete_certificate(hostname, verify, username, password, certificates, certid, bearer=None):
""" """
Delete a certificate from an HP CDM server. Delete a certificate from an HP CDM server.
@@ -430,6 +481,8 @@ def delete_certificate(hostname, verify, username, password, certificates, certi
logger.error('Impossible to delete certificate') logger.error('Impossible to delete certificate')
sys.exit(-1) sys.exit(-1)
logger.info('Certificate deleted with success.')
def main(): def main():
""" """
Main entry point of the program. Main entry point of the program.
@@ -478,13 +531,15 @@ def main():
subparsers = parser.add_subparsers(dest='command', required=True, help='command help') subparsers = parser.add_subparsers(dest='command', required=True, help='command help')
subparsers.add_parser('list', help='List certificates') subparsers.add_parser('list', help='List certificates.')
parser_delete = subparsers.add_parser('del', help='Delete a certificate') 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, parser_delete.add_argument("-#", "--number", dest='certid', required=True, type=int,
help="Certificate number as given by list command.") help="Certificate number as given by list command.")
parser_create_csr = subparsers.add_parser('csr', help='Create CSR') 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', parser_create_csr.add_argument("-C", "--country", dest='country', required=False, default='US',
help="Country.") help="Country.")
parser_create_csr.add_argument("-s", "--state", dest='state', required=False, help="State.") parser_create_csr.add_argument("-s", "--state", dest='state', required=False, help="State.")
@@ -496,7 +551,7 @@ def main():
parser_create_csr.add_argument("-o", "--output", dest='output', required=False, default=None, parser_create_csr.add_argument("-o", "--output", dest='output', required=False, default=None,
help="Output file.") help="Output file.")
parser_install_certificate = subparsers.add_parser('pem', help='Install certificate') parser_install_certificate = subparsers.add_parser('pem', help='Install a certificate.')
parser_install_certificate.add_argument("-i", "--input", dest='input', required=True, parser_install_certificate.add_argument("-i", "--input", dest='input', required=True,
default=None, help="Input file.") default=None, help="Input file.")
@@ -579,10 +634,14 @@ def main():
certs = get_certificates(hostname=args.hostname, verify=args.verify, certs = get_certificates(hostname=args.hostname, verify=args.verify,
username=args.username, username=args.username,
password=args.password) password=args.password)
logger.info('List of certificates:')
for certid, cert in certs.items(): for certid, cert in certs.items():
subject = cert.get('subject') subject = cert.get('subject')
issuer = cert.get('issuer') issuer = cert.get('issuer')
print(f'{certid} - {subject} issued by {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': case 'del':
bearer = get_bearer(hostname=args.hostname, verify=args.verify, username=args.username, bearer = get_bearer(hostname=args.hostname, verify=args.verify, username=args.username,
password=args.password) password=args.password)
@@ -602,6 +661,9 @@ def main():
case 'pem': case 'pem':
install_certificate(hostname=args.hostname, verify=args.verify, username=args.username, install_certificate(hostname=args.hostname, verify=args.verify, username=args.username,
password=args.password, filename=args.input) password=args.password, filename=args.input)
case 'self':
self_signed(hostname=args.hostname, verify=args.verify, username=args.username,
password=args.password)
case _: case _:
logger.error('Unknown command: %s', args.command) logger.error('Unknown command: %s', args.command)
sys.exit(-1) sys.exit(-1)

View File

@@ -1,3 +1,5 @@
requests requests
coloredlogs coloredlogs
cryptography cryptography
typeguard
dateutils