From b4e326adfaded077c2c850a158d05d2a955feb7f Mon Sep 17 00:00:00 2001 From: Danilo Bispo Date: Wed, 17 Jul 2024 11:45:27 -0300 Subject: [PATCH 1/3] Added sample for signature cloudhub rest and added single line on samples/init.py directly referencing flask env, since this variable is now deprecated in flask newer versions --- python/flask/requirements.txt | 3 +- python/flask/sample/__init__.py | 1 + python/flask/sample/config.py | 4 + python/flask/sample/templates/home/index.html | 3 + .../discover.html | 69 ++++++ .../pades_signature_cloudhub_rest/index.html | 21 ++ .../signature-info.html | 17 ++ python/flask/sample/utils.py | 8 +- python/flask/sample/views/__init__.py | 2 + .../views/pades_signature_cloudhub_rest.py | 231 ++++++++++++++++++ 10 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 python/flask/sample/templates/pades_signature_cloudhub_rest/discover.html create mode 100644 python/flask/sample/templates/pades_signature_cloudhub_rest/index.html create mode 100644 python/flask/sample/templates/pades_signature_cloudhub_rest/signature-info.html create mode 100644 python/flask/sample/views/pades_signature_cloudhub_rest.py diff --git a/python/flask/requirements.txt b/python/flask/requirements.txt index ff445467..4e16f12d 100644 --- a/python/flask/requirements.txt +++ b/python/flask/requirements.txt @@ -9,4 +9,5 @@ wheel>=0.31.1 docopt>=0.6.1 restpki-client>=1.1.1 pkiexpress>=1.10.0 -amplia-client>=1.0.0 \ No newline at end of file +amplia-client>=1.0.0 +cloudhub-client >= 1.0.1 \ No newline at end of file diff --git a/python/flask/sample/__init__.py b/python/flask/sample/__init__.py index c1b36af6..32933580 100644 --- a/python/flask/sample/__init__.py +++ b/python/flask/sample/__init__.py @@ -26,6 +26,7 @@ def create_app(): app = Flask(__name__) + app.env = os.getenv('FLASK_ENV') # Select configuration based on the application environment. if app.env not in config: diff --git a/python/flask/sample/config.py b/python/flask/sample/config.py index 0f8f05f7..ff98e1f7 100644 --- a/python/flask/sample/config.py +++ b/python/flask/sample/config.py @@ -60,6 +60,10 @@ class Config(object): # Web PKI # -------------------------------------------------------------------------- WEB_PKI_LICENSE = None + # -------------------------------------------------------------------------- + # Cloudhub + # -------------------------------------------------------------------------- + CLOUDHUB_API_KEY = 'mR1j0v7L12lBHnxpgxVkIdikCN9Gm89rn8I9qet3UHo=' class ProductionConfig(Config): diff --git a/python/flask/sample/templates/home/index.html b/python/flask/sample/templates/home/index.html index 2647bcff..eb368cfa 100644 --- a/python/flask/sample/templates/home/index.html +++ b/python/flask/sample/templates/home/index.html @@ -91,6 +91,9 @@ signPdfCloudOAuth: { express: '/check-express/upload/pades-cloud-oauth' }, + signPdfCloudhubServer: { + restpki: 'check-rest-token/server-files/pades-signature-cloudhub/signPdf' + }, signXmlServer: { restpki: '/check-rest-token/xml-signature' }, diff --git a/python/flask/sample/templates/pades_signature_cloudhub_rest/discover.html b/python/flask/sample/templates/pades_signature_cloudhub_rest/discover.html new file mode 100644 index 00000000..e3346f3d --- /dev/null +++ b/python/flask/sample/templates/pades_signature_cloudhub_rest/discover.html @@ -0,0 +1,69 @@ +{% extends "layout.html" %} + +{% block content %} + +

PAdES Signature using cloud certificate with PKI Express (OAuth Flow)

+ + {% if services.services|length > 0 %} +
Trusted services were found with this CPF
+
+
+
+
+ + +
+
+ +
+
+ {% else %} +
No trusted services were found with this CPF
+
+

Possible reasons include: +

+

+

To configure the PKI Express you need to configure all providers by running the + following commands on the terminal:
+ pkie config --set trustServices:{provider}:clientId={value}
+ pkie config --set trustServices:{provider}:clientSecret={value} +

+

For customized providers you neet to also configure the endpoint:
+ pkie config --set trustServices:{provider}:endpoint={value} +

+

The standard providers are: +

+

+ +
+ {% endif %} + +{% endblock %} + +{% block scripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/python/flask/sample/templates/pades_signature_cloudhub_rest/index.html b/python/flask/sample/templates/pades_signature_cloudhub_rest/index.html new file mode 100644 index 00000000..90fde804 --- /dev/null +++ b/python/flask/sample/templates/pades_signature_cloudhub_rest/index.html @@ -0,0 +1,21 @@ +{% extends "layout.html" %} + +{% block content %} + +

PAdES Signature using cloud certificate with Cloudhub

+ +
+
+
+ + +
+ + + +
+
+ +{% endblock %} diff --git a/python/flask/sample/templates/pades_signature_cloudhub_rest/signature-info.html b/python/flask/sample/templates/pades_signature_cloudhub_rest/signature-info.html new file mode 100644 index 00000000..efb3a29b --- /dev/null +++ b/python/flask/sample/templates/pades_signature_cloudhub_rest/signature-info.html @@ -0,0 +1,17 @@ +{% extends "layout.html" %} + +{% block content %} + +

PAdES Signature using cloud certificate with Cloudhub

+ +

File signed successfully!

+ +

Actions:

+ + +{% endblock %} \ No newline at end of file diff --git a/python/flask/sample/utils.py b/python/flask/sample/utils.py index d8af3012..dc008598 100644 --- a/python/flask/sample/utils.py +++ b/python/flask/sample/utils.py @@ -11,6 +11,7 @@ from pkiexpress import TimestampAuthority from restpki_client import RestPkiClient from restpki_client import StandardSecurityContexts +from cloudhub_client import Configuration # region REST PKI @@ -69,6 +70,12 @@ def get_security_context_id(): else: # In production, accepting only certificates from ICP-Brasil return StandardSecurityContexts.PKI_BRAZIL + +def get_cloudhub_client(): + configuration = Configuration() + configuration.api_key['X-Api-Key'] = 'mR1j0v7L12lBHnxpgxVkIdikCN9Gm89rn8I9qet3UHo=' + return configuration + # endregion @@ -141,7 +148,6 @@ def set_pki_defaults(operator): # endregion - def format_date(date): return date.strftime("%m-%d-%Y") diff --git a/python/flask/sample/views/__init__.py b/python/flask/sample/views/__init__.py index cb66baa1..98558795 100644 --- a/python/flask/sample/views/__init__.py +++ b/python/flask/sample/views/__init__.py @@ -38,6 +38,7 @@ from .xml_nfe_signature_rest import blueprint as xml_nfe_signature_rest from .xml_signature_rest import blueprint as xml_signature_rest from .server_files import blueprint as server_files +from .pades_signature_cloudhub_rest import blueprint as pades_signature_cloudhub_rest blueprints = { authentication_express, @@ -64,6 +65,7 @@ open_pades_rest, pades_server_key_express, pades_signature_express, + pades_signature_cloudhub_rest, pades_signature_restpki, pades_cloud_pwd_express, pades_cloud_oauth_express, diff --git a/python/flask/sample/views/pades_signature_cloudhub_rest.py b/python/flask/sample/views/pades_signature_cloudhub_rest.py new file mode 100644 index 00000000..da39ef20 --- /dev/null +++ b/python/flask/sample/views/pades_signature_cloudhub_rest.py @@ -0,0 +1,231 @@ +import base64 +import os +import uuid + +from os.path import basename +from os.path import exists +from os.path import join + +from flask import request +from flask import Blueprint +from flask import current_app +from flask import make_response +from flask import render_template +from flask import redirect + +import cloudhub_client as client + +from sample.pades_visual_elements_express import PadesVisualElementsExpress +from sample.storage_mock import create_app_data +from sample.storage_mock import get_pdf_stamp_path +from sample.utils import get_expired_page_headers, get_security_context_id +from sample.utils import get_cloudhub_client + +from sample.pades_visual_elements_rest import PadesVisualElementsRest +from restpki_client import PadesSignatureStarter +from restpki_client import PadesSignatureFinisher +from restpki_client import StandardSignaturePolicies + +from sample.utils import get_rest_pki_client + +# 26-08-2022 +# By further inspecting in the latest Blueprint documentation (https://flask.palletsprojects.com/en/2.2.x/api/#blueprint-objects), +# when creating a Blueprint object, the first parameter (name) is prepend to the URL endpoint. Therefore, Blueprint no longer +# allows dots in the name since it would break the URL entirely. +__name__ = __name__.replace(".", "/") +blueprint = Blueprint(basename(__name__), __name__, + url_prefix='/pades-signature-cloudhub-rest') + +# This sample is responsible to perform a OAuth flow to communicate with PSCs to perform a +# signature. To perform this sample it's necessary to configure PKI Express with the credentials of +# the services by executing the following sample: +# +# pkie config --set trustServices:: +# +# All standard providers: +# - BirdId +# - ViDaaS +# - NeoId +# - RemoteId +# - SafeId +# It's possible to create a custom provider if necessary. +# +# All configuration available: +# - clientId +# - clientSecret +# - endpoint +# - provider +# - badgeUrl +# - protocolVariant (error handling, normally it depends on the used provider) +# +# This sample will only show the PSCs that are configured. + +# Call cloudhub client library and start a session to retrieve the user's certificate +configuration = get_cloudhub_client() +sessions_api = client.SessionsApi(client.ApiClient(configuration)) + + +@blueprint.route('/') +def index(file_id): + """ + + This action will render a page that request a CPF to the user. This CPF is used to discover + which PSCs have a certificate containing that CPF. + + """ + # Verify if the provided userfile exists. + file_path = join(current_app.config['APPDATA_FOLDER'], file_id) + if not exists(file_path): + return render_template('error.html', msg='File not found') + + return render_template('pades_signature_cloudhub_rest/index.html') + + +@blueprint.route('/discover/', methods=['POST']) +def discover(file_id): + """ + + This action will be called after the user press the button "Search" on index page. It will + search for all PSCs that have a certificate with the provided CPF. Thus, it will start the + authentication process and return a URL to redirect the user to perform the authentication. + + After this action the user will be redirected, and to store the local data (fileId) to be user + after the user returns to your application. We use the parameter "customState", the last + parameter of the method discoverByCpfAndStartAuth(). This parameter will be recovered in the + next action. + + """ + try: + # Recover CPF from the POST argument. + cpf = request.form['cpf'] + + # Process cpf, removing all formatting. + plainCpf = cpf.replace(".", "").replace("-", "") + + # create an instance of the API class + create_session_request = client.SessionCreateRequest( + identifier=plainCpf, + type=client.TrustServiceSessionTypes.SingleSignature, + redirect_uri=f"http://localhost:5000/pades-signature-cloudhub-rest/complete/fileId={file_id}" + ) + api_response = sessions_api.api_sessions_post( + body=create_session_request) + print(api_response) + + # Render complete page. + return render_template('pades_signature_cloudhub_rest/discover.html', cpf=cpf, services=api_response) + + except Exception as e: + return render_template('error.html', msg=e) + + +@blueprint.route('/complete/fileId=', methods=['GET']) +def complete(file_id): + """ + + This action will complete the authentication process and create a signature using a session + token returned by user. Also, we recover the parameter "customState" containing the id of the + file that will be signed. + + """ + try: + # Extract fileId and session from query parameters + session = request.args.get('session') + + if not file_id or not session: + return render_template('error.html', msg='FileId or session parameter is missing') + + # Verify if the provided file_id exists. + file_path = join(current_app.config['APPDATA_FOLDER'], file_id) + if not exists(file_path): + return render_template('error.html', msg='File not found') + + # Recover variables from query parameters. + session = request.args.get('session') + + # Get the certificate from the Sessions API + cert = sessions_api.api_sessions_certificate_get(session=session) + + # Start the signature process + + # Get an instantiate of the PadesSignatureStarter class, responsible for + # receiving the signature elements and start the signature process. + signature_starter = PadesSignatureStarter(get_rest_pki_client()) + + # Set the PDF to be signed. + signature_starter.set_pdf_to_sign( + '%s/%s' % (current_app.config['APPDATA_FOLDER'], file_id)) + + # Set the signature policy. + signature_starter.signature_policy =\ + StandardSignaturePolicies.PADES_BASIC + + # Set a security context to be used to determine trust in the + # certificate chain. We have encapsulated the security context choice on + # util.py. + signature_starter.security_context = get_security_context_id() + + # Set the visual representation for the signature. We have encapsulated + # this code (on util-pades.py) to be used on various PAdES examples. + signature_starter.visual_representation = \ + PadesVisualElementsRest.get_visual_representation() + + signature_starter.signer_certificate = cert + + # Call the start() method, which initiates the signature. + # This yields the token, a 43-character case-sensitive URL-safe string, + # which identifies this signature process. We'll use this value to call + # the signWithRestPki() method on the Web PKI component (see + # signature-form.js javascript) and also to complete the signature after + # the form is submitted (see method pades_signature_action()). This + # should not be mistaken with the API access token. + result = signature_starter.start() + + # Perform the hash signature + sign_hash_request = client.SignHashRequest( + session=session, + hash=result.to_sign_hash, + digest_algorithm_oid=result.digest_algorithm_oid + ) + signed_hash = sessions_api.api_sessions_sign_hash_post(body=sign_hash_request) + + # Finish the signature process + + # Get an intance of the PadesSignatureFinisher class, responsible for + # completing the signature process. + signature_finisher = PadesSignatureFinisher(get_rest_pki_client()) + + # Set the token. + signature_finisher.token = result.token + signed_hash_bytes = base64.b64decode(signed_hash) + + # Set the signed hash previously signed by cloudhub + signature_finisher.signature = signed_hash_bytes + + signature_finisher.force_blob_result = False + + + # Call the finish() method, which finalizes the signature process. The + # return value is the signed PDF content. + sig_result = signature_finisher.finish() + + # At this point, you'd typically store the signed PDF on your database. + # For demonstration purposes, we'll store the PDF on a temporary folder + # publicly accessible and render a link to it. + + create_app_data() # Guarantees that "app data" folder exists. + filename = '%s.pdf' % (str(uuid.uuid4())) + sig_result.write_to_file(os.path.join( + current_app.config['APPDATA_FOLDER'], filename)) + + # Perform the signature. + # signer_cert = signer.sign(get_cert=False) + + response = make_response(render_template( + 'pades_signature_cloudhub_rest/signature-info.html', + signed_pdf=filename)) + + return response + + except Exception as e: + return render_template('error.html', msg=e) From 385537c1d4e0176398ad6b676ef0dd3fba78565e Mon Sep 17 00:00:00 2001 From: Danilo Bispo Date: Mon, 22 Jul 2024 15:44:05 +0000 Subject: [PATCH 2/3] Some refactoring to make the code more reusable and typo fix in discover.html + removed useless prints --- .../discover.html | 2 +- python/flask/sample/utils.py | 8 +++---- .../views/pades_signature_cloudhub_rest.py | 22 +++++++++---------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/python/flask/sample/templates/pades_signature_cloudhub_rest/discover.html b/python/flask/sample/templates/pades_signature_cloudhub_rest/discover.html index e3346f3d..326232a2 100644 --- a/python/flask/sample/templates/pades_signature_cloudhub_rest/discover.html +++ b/python/flask/sample/templates/pades_signature_cloudhub_rest/discover.html @@ -2,7 +2,7 @@ {% block content %} -

PAdES Signature using cloud certificate with PKI Express (OAuth Flow)

+

PAdES Signature using cloud certificate with Cloudhub

{% if services.services|length > 0 %}
Trusted services were found with this CPF
diff --git a/python/flask/sample/utils.py b/python/flask/sample/utils.py index dc008598..ab1f1730 100644 --- a/python/flask/sample/utils.py +++ b/python/flask/sample/utils.py @@ -11,7 +11,7 @@ from pkiexpress import TimestampAuthority from restpki_client import RestPkiClient from restpki_client import StandardSecurityContexts -from cloudhub_client import Configuration +from cloudhub_client import Configuration, SessionsApi, ApiClient # region REST PKI @@ -71,12 +71,10 @@ def get_security_context_id(): # In production, accepting only certificates from ICP-Brasil return StandardSecurityContexts.PKI_BRAZIL -def get_cloudhub_client(): +def get_cloudhub_client_api(): configuration = Configuration() configuration.api_key['X-Api-Key'] = 'mR1j0v7L12lBHnxpgxVkIdikCN9Gm89rn8I9qet3UHo=' - return configuration - - + return SessionsApi(ApiClient(configuration)) # endregion diff --git a/python/flask/sample/views/pades_signature_cloudhub_rest.py b/python/flask/sample/views/pades_signature_cloudhub_rest.py index da39ef20..638f3288 100644 --- a/python/flask/sample/views/pades_signature_cloudhub_rest.py +++ b/python/flask/sample/views/pades_signature_cloudhub_rest.py @@ -13,19 +13,21 @@ from flask import render_template from flask import redirect -import cloudhub_client as client + from sample.pades_visual_elements_express import PadesVisualElementsExpress from sample.storage_mock import create_app_data from sample.storage_mock import get_pdf_stamp_path from sample.utils import get_expired_page_headers, get_security_context_id -from sample.utils import get_cloudhub_client +from sample.utils import get_cloudhub_client_api from sample.pades_visual_elements_rest import PadesVisualElementsRest from restpki_client import PadesSignatureStarter from restpki_client import PadesSignatureFinisher from restpki_client import StandardSignaturePolicies +from cloudhub_client import SessionCreateRequest, TrustServiceSessionTypes, SignHashRequest + from sample.utils import get_rest_pki_client # 26-08-2022 @@ -60,9 +62,6 @@ # # This sample will only show the PSCs that are configured. -# Call cloudhub client library and start a session to retrieve the user's certificate -configuration = get_cloudhub_client() -sessions_api = client.SessionsApi(client.ApiClient(configuration)) @blueprint.route('/') @@ -96,6 +95,8 @@ def discover(file_id): """ try: + # Call cloudhub client library and start a session to retrieve the user's certificate + sessions_api = get_cloudhub_client_api() # Recover CPF from the POST argument. cpf = request.form['cpf'] @@ -103,14 +104,13 @@ def discover(file_id): plainCpf = cpf.replace(".", "").replace("-", "") # create an instance of the API class - create_session_request = client.SessionCreateRequest( + create_session_request = SessionCreateRequest( identifier=plainCpf, - type=client.TrustServiceSessionTypes.SingleSignature, + type=TrustServiceSessionTypes.SingleSignature, redirect_uri=f"http://localhost:5000/pades-signature-cloudhub-rest/complete/fileId={file_id}" ) api_response = sessions_api.api_sessions_post( body=create_session_request) - print(api_response) # Render complete page. return render_template('pades_signature_cloudhub_rest/discover.html', cpf=cpf, services=api_response) @@ -129,6 +129,8 @@ def complete(file_id): """ try: + # Call cloudhub client library + sessions_api = get_cloudhub_client_api() # Extract fileId and session from query parameters session = request.args.get('session') @@ -182,7 +184,7 @@ def complete(file_id): result = signature_starter.start() # Perform the hash signature - sign_hash_request = client.SignHashRequest( + sign_hash_request = SignHashRequest( session=session, hash=result.to_sign_hash, digest_algorithm_oid=result.digest_algorithm_oid @@ -203,8 +205,6 @@ def complete(file_id): signature_finisher.signature = signed_hash_bytes signature_finisher.force_blob_result = False - - # Call the finish() method, which finalizes the signature process. The # return value is the signed PDF content. sig_result = signature_finisher.finish() From 43defbce10f71299ce9da4360c11579468bfd00b Mon Sep 17 00:00:00 2001 From: Danilo Bispo Date: Mon, 22 Jul 2024 17:03:30 +0000 Subject: [PATCH 3/3] Further refactoring --- python/flask/sample/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/flask/sample/utils.py b/python/flask/sample/utils.py index ab1f1730..a0ea3b6c 100644 --- a/python/flask/sample/utils.py +++ b/python/flask/sample/utils.py @@ -73,7 +73,7 @@ def get_security_context_id(): def get_cloudhub_client_api(): configuration = Configuration() - configuration.api_key['X-Api-Key'] = 'mR1j0v7L12lBHnxpgxVkIdikCN9Gm89rn8I9qet3UHo=' + configuration.api_key['X-Api-Key'] = current_app.config['CLOUDHUB_API_KEY'] return SessionsApi(ApiClient(configuration)) # endregion