Compare commits
6 Commits
2cf4c996f9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d772f59de | ||
|
|
9f98cd1e70 | ||
|
|
8d709213d3 | ||
|
|
54d2338da6 | ||
|
|
c286b5147f | ||
|
|
115fa39618 |
70
README.md
70
README.md
@@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
requests
|
requests
|
||||||
coloredlogs
|
coloredlogs
|
||||||
cryptography
|
cryptography
|
||||||
|
typeguard
|
||||||
|
dateutils
|
||||||
|
|||||||
Reference in New Issue
Block a user