Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add transaction_id in pdf keywords. #42

Merged
merged 1 commit into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"cSpell.words": [
"addsig",
"asyncpg",
"certvalidator",
"fastapi",
"HEALTHCHECK",
"jsonschema",
Expand All @@ -17,5 +18,9 @@
"secp",
"subfilter",
"timestamper"
]
],
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none"
}
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ ci: docker-build docker-push

vscode_venv:
$(info Creating virtualenv in devcontainer)
python3 -m venv .venv

vscode_pip: vscode_venv
$(info Installing pip packages in devcontainer)
pip3 install --upgrade pip
pip3 install pip-tools
pip3 install -r requirements.txt
.venv/bin/pip install -r requirements.txt
# .venv/bin/mypy --install-types

vscode_packages:
$(info Installing apt packages in devcontainer)
Expand Down
1 change: 1 addition & 0 deletions src/pkcs11_ca_service/common/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
"""common"""
__author__ = "masv"
1 change: 1 addition & 0 deletions src/pkcs11_ca_service/common/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""helpers"""
import time
from datetime import datetime, timezone

Expand Down
1 change: 1 addition & 0 deletions src/pkcs11_ca_service/pdf/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
"""pdf"""
__author__ = "masv"
28 changes: 11 additions & 17 deletions src/pkcs11_ca_service/pdf/app.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
"""pdf app"""

import logging
# from logging import Logger, getLogger

from fastapi import FastAPI
from typing import Optional
from pyhanko.sign import signers, SimpleSigner, timestamps
from pyhanko.keys import load_cert_from_pemder
from pyhanko_certvalidator import ValidationContext
from pkcs11_ca_service.common.helpers import unix_ts
from pkcs11_ca_service.pdf.context import ContextRequestRoute
from pkcs11_ca_service.pdf.routers.pdf import pdf_router
from pkcs11_ca_service.pdf.routers.status import status_router
from .exceptions import (
from pkcs11_ca_service.pdf.exceptions import (
RequestValidationError,
validation_exception_handler,
HTTPErrorDetail,
Expand All @@ -22,10 +22,7 @@
class PDFAPI(FastAPI):
"""PDF API"""

def __init__(self,
service_name: str = "pdf_api",
timestamp_url: str = "http://ca:8005/timestamp01"
):
def __init__(self, service_name: str = "pdf_api", timestamp_url: str = "http://ca:8005/timestamp01"):
self.service_name = service_name
self.logger = logging.getLogger(self.service_name)
self.logger.setLevel(logging.DEBUG)
Expand All @@ -35,9 +32,7 @@ def __init__(self,
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")

ch.setFormatter(formatter)

Expand All @@ -60,11 +55,12 @@ def __init__(self,
self.logger.info(msg=f"cert_path: {self.cert_path}")
self.logger.info(msg=f"key_path: {self.key_path}")

self.cms_signer: Optional[SimpleSigner] = signers.SimpleSigner.load(
self.simple_signer: SimpleSigner = signers.SimpleSigner.load(
key_file=self.key_path,
cert_file=self.cert_path,
# ca_chain_files=(self.chain_path),
signature_mechanism=None)
# signature_mechanism=None
)

self.cert_pemder = load_cert_from_pemder(self.cert_path)

Expand All @@ -85,10 +81,8 @@ def init_api(service_name: str = "pdf_api") -> PDFAPI:
app.include_router(status_router)

# Exception handling
app.add_exception_handler(RequestValidationError,
validation_exception_handler)
app.add_exception_handler(
HTTPErrorDetail, http_error_detail_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(HTTPErrorDetail, http_error_detail_handler)
app.add_exception_handler(Exception, unexpected_error_handler)

app.logger.info(msg="app running...")
Expand Down
2 changes: 2 additions & 0 deletions src/pkcs11_ca_service/pdf/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""pdf exceptions"""

from __future__ import annotations

import logging
Expand Down
23 changes: 12 additions & 11 deletions src/pkcs11_ca_service/pdf/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pydantic import BaseModel, validator
"""pdf model"""

from typing import Optional
from datetime import datetime
from pydantic import BaseModel, validator


class PDFSignRequest(BaseModel):
Expand All @@ -24,29 +25,29 @@ class PDFSignReply(BaseModel):
"""Class to represent reply"""

transaction_id: str
data: str
error: str
data: Optional[str] = None
error: Optional[str] = None
create_ts: Optional[int]


class PDFValidateRequest(BaseModel):
"""Class to represent request"""
data: str


class PDFValidateData(BaseModel):
"""Class to represent validation data"""
valid: bool
data: str


class PDFValidateReply(BaseModel):
"""Class to represent reply"""
data: PDFValidateData
error: str

valid_signature: bool = False
transaction_id: Optional[str] = None
is_revoked: bool = False
error: Optional[str] = None


class StatusReply(BaseModel):
"""Class to represent status reply"""

status: str
message: Optional[str] = None
last_check: int
Expand Down
1 change: 1 addition & 0 deletions src/pkcs11_ca_service/pdf/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
"""http router"""
__author__ = "masv"
22 changes: 12 additions & 10 deletions src/pkcs11_ca_service/pdf/routers/pdf.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""pdf router"""

from typing import Any
from fastapi import APIRouter
from pkcs11_ca_service.pdf.routers.utils.pdf import sign, validate
Expand All @@ -19,24 +21,24 @@

@pdf_router.post("/sign", response_model=PDFSignReply)
def endpoint_sign_pdf(req: ContextRequest, in_data: PDFSignRequest) -> Any:
""" endpoint for signing a base64 encoded PDF """
"""endpoint for signing a base64 encoded PDF"""

req.app.logger.info(
f"Received a base64 PDF, transaction_id: {in_data.transaction_id}")
req.app.logger.info(f"Received a base64 PDF, transaction_id: {in_data.transaction_id}")

reply = sign(req=req,
transaction_id=in_data.transaction_id,
base64_pdf=in_data.data,
reason=in_data.reason,
location=in_data.location,
)
reply = sign(
req=req,
transaction_id=in_data.transaction_id,
base64_pdf=in_data.data,
reason=in_data.reason,
location=in_data.location,
)

return reply


@pdf_router.post("/validate", response_model=PDFValidateReply)
def endpoint_validate_pdf(req: ContextRequest, in_data: PDFValidateRequest) -> Any:
""" endpoint for validation of a base64 encoded PDF """
"""endpoint for validation of a base64 encoded PDF"""

req.app.logger.info("Validate a signed base64 PDF")

Expand Down
2 changes: 2 additions & 0 deletions src/pkcs11_ca_service/pdf/routers/status.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""status router"""

from typing import Any
from fastapi import APIRouter
from pkcs11_ca_service.pdf.routers.utils.status import healthy
Expand Down
85 changes: 56 additions & 29 deletions src/pkcs11_ca_service/pdf/routers/utils/pdf.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
"""pdf utils"""

import base64
from io import BytesIO
from pkcs11_ca_service.common.helpers import unix_ts
from typing import Optional

from pyhanko.sign import signers
from pyhanko.sign.fields import SigSeedSubFilter
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.sign.validation import validate_pdf_signature
from pyhanko.pdf_utils.reader import PdfFileReader
from pkcs11_ca_service.pdf.models import PDFSignReply, PDFValidateReply, PDFValidateData
from pyhanko.pdf_utils.crypt.api import PdfKeyNotAvailableError
from pkcs11_ca_service.pdf.models import PDFSignReply, PDFValidateReply
from pkcs11_ca_service.pdf.context import ContextRequest
from pkcs11_ca_service.common.helpers import unix_ts



def sign(req: ContextRequest, transaction_id: str, base64_pdf: str, reason: str, location: str) -> PDFSignReply:
"""sign a PDF"""

req.app.logger.info(
msg=f"Trying to sign the PDF, transaction_id: {transaction_id}"
)
pdf_writer = IncrementalPdfFileWriter(
input_stream=BytesIO(base64.b64decode(base64_pdf))
)
req.app.logger.info(msg=f"Trying to sign the PDF, transaction_id: {transaction_id}")
pdf_writer = IncrementalPdfFileWriter(input_stream=BytesIO(base64.urlsafe_b64decode(base64_pdf)), strict=False)

pdf_writer.document_meta.keywords = [f"transaction_id:{transaction_id}"]

f = BytesIO()
signed_pdf = BytesIO()

signature_meta = signers.PdfSignatureMetadata(
field_name='Signature1',
field_name="Signature1",
location=location,
reason=reason,
subfilter=SigSeedSubFilter.PADES,
Expand All @@ -33,21 +36,31 @@ def sign(req: ContextRequest, transaction_id: str, base64_pdf: str, reason: str,
validation_context=req.app.validator_context,
)

signers.sign_pdf(
pdf_writer,
signature_meta=signature_meta,
signer=req.app.cms_signer,
output=f,
# timestamper=req.app.tst_client,
)
try:
signers.sign_pdf(
pdf_writer,
signature_meta=signature_meta,
signer=req.app.simple_signer,
output=signed_pdf,
# timestamper=req.app.tst_client,
)
except PdfKeyNotAvailableError as _e:
err_msg = f"ca_pdfsign: input pdf is encrypted, err: {_e}"

base64_encoded = base64.b64encode(f.getvalue()).decode("utf-8")
req.app.logger.warn(err_msg)

req.app.logger.info(
msg=f"Successfully signed the PDF, transaction_id: {transaction_id}"
)
return PDFSignReply(
transaction_id=transaction_id,
data=None,
create_ts=unix_ts(),
error=err_msg,
)

f.close()
base64_encoded = base64.b64encode(signed_pdf.getvalue()).decode("utf-8")

req.app.logger.info(msg=f"Successfully signed the PDF, transaction_id: {transaction_id}")

signed_pdf.close()

return PDFSignReply(
transaction_id=transaction_id,
Expand All @@ -57,21 +70,35 @@ def sign(req: ContextRequest, transaction_id: str, base64_pdf: str, reason: str,
)


def get_transaction_id_from_keywords(req: ContextRequest, pdf: PdfFileReader) -> Optional[str]:
"""simple function to get transaction_id from a list of keywords"""
for keyword in pdf.document_meta_view.keywords:
entry = keyword.split(sep=":")
if entry[0] == "transaction_id":
req.app.logger.info(msg=f"found transaction_id: {entry[1]}")
return entry[1]
return None

def validate(req: ContextRequest, base64_pdf: str) -> PDFValidateReply:
"""validate a PDF"""

req.app.logger.info(msg="Trying to validate the PDF")

pdf = PdfFileReader(
BytesIO(base64.b64decode(base64_pdf.encode("utf-8"), validate=True))
)
pdf = PdfFileReader(BytesIO(base64.b64decode(base64_pdf.encode("utf-8"), validate=True)))

if len(pdf.embedded_signatures) == 0:
return PDFValidateReply(error="No signature found")

sig = pdf.embedded_signatures[0]
status = validate_pdf_signature(
embedded_sig=sig,
signer_validation_context=req.app.validator_context,
)

transaction_id = get_transaction_id_from_keywords(req=req, pdf=pdf)

req.app.logger.info(msg=f"status: {status}")

# status_ltv = validate_pdf_ltv_signature(
# sig,
# RevocationInfoValidationType.PADES_LTA,
Expand All @@ -80,9 +107,9 @@ def validate(req: ContextRequest, base64_pdf: str) -> PDFValidateReply:

# req.app.logger.info(msg=status_ltv.pretty_print_details())

req.app.logger.info(msg="Successfully validate PDF")

return PDFValidateReply(
data=PDFValidateData(
valid=status.valid,
),
error="",
valid_signature=status.valid,
transaction_id= transaction_id,
)
11 changes: 6 additions & 5 deletions src/pkcs11_ca_service/pdf/routers/utils/status.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import timedelta
"""status utils"""
from pkcs11_ca_service.common.helpers import unix_ts
from pkcs11_ca_service.pdf.models import StatusReply
from pkcs11_ca_service.pdf.context import ContextRequest
Expand All @@ -13,20 +13,21 @@ def healthy(req: ContextRequest) -> StatusReply:

if now > req.app.status_storage.next_check:
pdf = "JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YWxvZw0KL091dGxpbmVzIDIgMCBSDQovUGFnZXMgMyAwIFINCj4+DQplbmRvYmoNCg0KMiAwIG9iag0KPDwNCi9UeXBlIC9PdXRsaW5lcw0KL0NvdW50IDANCj4+DQplbmRvYmoNCg0KMyAwIG9iag0KPDwNCi9UeXBlIC9QYWdlcw0KL0NvdW50IDINCi9LaWRzIFsgNCAwIFIgNiAwIFIgXSANCj4+DQplbmRvYmoNCg0KNCAwIG9iag0KPDwNCi9UeXBlIC9QYWdlDQovUGFyZW50IDMgMCBSDQovUmVzb3VyY2VzIDw8DQovRm9udCA8PA0KL0YxIDkgMCBSIA0KPj4NCi9Qcm9jU2V0IDggMCBSDQo+Pg0KL01lZGlhQm94IFswIDAgNjEyLjAwMDAgNzkyLjAwMDBdDQovQ29udGVudHMgNSAwIFINCj4+DQplbmRvYmoNCg0KNSAwIG9iag0KPDwgL0xlbmd0aCAxMDc0ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBBIFNpbXBsZSBQREYgRmlsZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIFRoaXMgaXMgYSBzbWFsbCBkZW1vbnN0cmF0aW9uIC5wZGYgZmlsZSAtICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjY0LjcwNDAgVGQNCigganVzdCBmb3IgdXNlIGluIHRoZSBWaXJ0dWFsIE1lY2hhbmljcyB0dXRvcmlhbHMuIE1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NTIuNzUyMCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDYyOC44NDgwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjE2Ljg5NjAgVGQNCiggdGV4dC4gQW5kIG1vcmUgdGV4dC4gQm9yaW5nLCB6enp6ei4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjA0Ljk0NDAgVGQNCiggbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDU5Mi45OTIwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNTY5LjA4ODAgVGQNCiggQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA1NTcuMTM2MCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBFdmVuIG1vcmUuIENvbnRpbnVlZCBvbiBwYWdlIDIgLi4uKSBUag0KRVQNCmVuZHN0cmVhbQ0KZW5kb2JqDQoNCjYgMCBvYmoNCjw8DQovVHlwZSAvUGFnZQ0KL1BhcmVudCAzIDAgUg0KL1Jlc291cmNlcyA8PA0KL0ZvbnQgPDwNCi9GMSA5IDAgUiANCj4+DQovUHJvY1NldCA4IDAgUg0KPj4NCi9NZWRpYUJveCBbMCAwIDYxMi4wMDAwIDc5Mi4wMDAwXQ0KL0NvbnRlbnRzIDcgMCBSDQo+Pg0KZW5kb2JqDQoNCjcgMCBvYmoNCjw8IC9MZW5ndGggNjc2ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBTaW1wbGUgUERGIEZpbGUgMiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIC4uLmNvbnRpbnVlZCBmcm9tIHBhZ2UgMS4gWWV0IG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NzYuNjU2MCBUZA0KKCBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY2NC43MDQwIFRkDQooIHRleHQuIE9oLCBob3cgYm9yaW5nIHR5cGluZyB0aGlzIHN0dWZmLiBCdXQgbm90IGFzIGJvcmluZyBhcyB3YXRjaGluZyApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY1Mi43NTIwIFRkDQooIHBhaW50IGRyeS4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NDAuODAwMCBUZA0KKCBCb3JpbmcuICBNb3JlLCBhIGxpdHRsZSBtb3JlIHRleHQuIFRoZSBlbmQsIGFuZCBqdXN0IGFzIHdlbGwuICkgVGoNCkVUDQplbmRzdHJlYW0NCmVuZG9iag0KDQo4IDAgb2JqDQpbL1BERiAvVGV4dF0NCmVuZG9iag0KDQo5IDAgb2JqDQo8PA0KL1R5cGUgL0ZvbnQNCi9TdWJ0eXBlIC9UeXBlMQ0KL05hbWUgL0YxDQovQmFzZUZvbnQgL0hlbHZldGljYQ0KL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcNCj4+DQplbmRvYmoNCg0KMTAgMCBvYmoNCjw8DQovQ3JlYXRvciAoUmF2ZSBcKGh0dHA6Ly93d3cubmV2cm9uYS5jb20vcmF2ZVwpKQ0KL1Byb2R1Y2VyIChOZXZyb25hIERlc2lnbnMpDQovQ3JlYXRpb25EYXRlIChEOjIwMDYwMzAxMDcyODI2KQ0KPj4NCmVuZG9iag0KDQp4cmVmDQowIDExDQowMDAwMDAwMDAwIDY1NTM1IGYNCjAwMDAwMDAwMTkgMDAwMDAgbg0KMDAwMDAwMDA5MyAwMDAwMCBuDQowMDAwMDAwMTQ3IDAwMDAwIG4NCjAwMDAwMDAyMjIgMDAwMDAgbg0KMDAwMDAwMDM5MCAwMDAwMCBuDQowMDAwMDAxNTIyIDAwMDAwIG4NCjAwMDAwMDE2OTAgMDAwMDAgbg0KMDAwMDAwMjQyMyAwMDAwMCBuDQowMDAwMDAyNDU2IDAwMDAwIG4NCjAwMDAwMDI1NzQgMDAwMDAgbg0KDQp0cmFpbGVyDQo8PA0KL1NpemUgMTENCi9Sb290IDEgMCBSDQovSW5mbyAxMCAwIFINCj4+DQoNCnN0YXJ0eHJlZg0KMjcxNA0KJSVFT0YNCg=="
signed_pdf = sign(req=req, transaction_id="trans_id_status-check",
base64_pdf=pdf, reason="status_check", location="tidan")
signed_pdf = sign(
req=req, transaction_id="trans_id_status-check", base64_pdf=pdf, reason="status_check", location="tidan"
)

validate_pdf = validate(req=req, base64_pdf=signed_pdf.data)

req.app.status_storage.next_check = now+5
req.app.status_storage.next_check = now + 5

req.app.status_storage = StatusReply(
status="STATUS_FAIL",
last_check=now,
next_check=req.app.status_storage.next_check,
)

if validate_pdf.data.valid:
if validate_pdf.valid_signature:
req.app.status_storage.status = "STATUS_OK"

return req.app.status_storage
Loading