U
    gn                     @   s  d Z ddlZddlZddlZddlZddlmZmZ ddlmZm	Z	 ddl
mZmZmZmZmZ ddlZddlmZmZ ddlmZ ddlmZmZ dd	lmZ dd
lmZmZ zddlZW n ek
r   edY nX dddddddgZ e!e"Z#eddG dd dZ$eddG dd dZ%d&ej&e$e'e%dddZ(e%dddZ)e*e+e+dddZ,eddG d d dZ-G d!d dej.Z/G d"d de/Z0eG d#d$ d$Z1G d%d deZ2dS )'a  

.. versionadded:: 0.10.0

Asynchronous :class:`~pyhanko.sign.signers.pdf_cms.Signer` implementation for
interacting with a remote signing service using the Cloud Signature Consortium
(CSC) API.

This implementation is based on version 1.0.4.0 (2019-06) of the CSC API
specification.


Usage notes
-----------

This module's :class:`.CSCSigner` class supplies an implementation of the
:class:`~pyhanko.sign.signers.pdf_cms.Signer` class in pyHanko.
As such, it is flexible enough to be used either through pyHanko's high-level
API (:func:`~pyhanko.sign.signers.functions.sign_pdf` et al.), or through
the :ref:`interrupted signing API <interrupted-signing>`.

:class:`.CSCSigner` overview
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

:class:`.CSCSigner` is only directly responsible for calling the
``signatures/signHash`` endpoint in the CSC API. Other than that, it only
handles batch control. This means that the following tasks require further
action on the API user's part:

 * authenticating to the signing service (typically using OAuth2);
 * obtaining Signature Activation Data (SAD) from the signing service;
 * provisioning the certificates to embed into the document (usually
   those are supplied by the signing service as well).

The first two involve a degree of implementation/vendor dependence that is
difficult to cater to in full generality, and the third is out of scope
for :class:`~pyhanko.sign.signers.pdf_cms.Signer` subclasses in general.

However, this module still provides a number of convenient hooks and guardrails
that should allow you to fill in these blanks with relative ease. We briefly
discuss these below.

Throughout, the particulars of how pyHanko should connect to a signing
service are supplied in a :class:`.CSCServiceSessionInfo` object.
This object contains the base CSC API URL, the CSC credential ID to use,
and authentication data.


Authenticating to the signing service
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

While the authentication process itself is the API user's responsibility,
:class:`.CSCServiceSessionInfo` includes an
:attr:`~.CSCServiceSessionInfo.oauth_token` field that will (by default)
be used to populate the HTTP ``Authorization`` header for every request.

To handle OAuth-specific tasks, you might want to use a library like
`OAuthLib <https://oauthlib.readthedocs.io/en/latest/>`_.


Obtaining SAD from the signing service
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This is done by subclassing :class:`.CSCAuthorizationInfo` and passing
an instance to the :class:`.CSCSigner`. The :class:`.CSCAuthorizationInfo`
instance should call the signer's ``credentials/authorize`` endpoint with
the proper parameters required by the service.
See the documentation for :class:`.CSCAuthorizationInfo` for details and=
information about helper functions.


Certificate provisioning
^^^^^^^^^^^^^^^^^^^^^^^^

In pyHanko's API, :class:`~pyhanko.sign.signers.pdf_cms.Signer` instances
need to be initialised with the signer's certificate, preferably together
with other relevant CA certificates.
In a CSC context, these are typically retrieved from the signing service by
calling the ``credentials/info`` endpoint.

This module offers a helper function to handle that task, see
:func:`.fetch_certs_in_csc_credential`.
    N)	dataclassfield)datetime	timedelta)AnyDict	FrozenSetListOptional)algosx509)hashes)CertificateStoreSimpleCertificateStore)Signer)SigningErrorget_pyca_cryptography_hashz!Install pyHanko with [async_http]	CSCSignerCSCServiceSessionInfoCSCCredentialInfofetch_certs_in_csc_credentialCSCAuthorizationInfoCSCAuthorizationManager!PrefetchedSADAuthorizationManagerT)frozenc                   @   sR   e Zd ZU dZeed< eed< dZee ed< dZeed< dd	 Z	e
d
d ZdS )r   z`
    Information about the CSC service, together with the required authentication
    data.
    service_urlcredential_idNoauth_tokenZv1api_verc                 C   s   | j  d| j d| S )z
        Complete an endpoint name to a full URL.

        :param endpoint_name:
            Name of the endpoint (e.g. ``credentials/info``).
        :return:
            A URL.
        z/csc//)r   r   )selfZendpoint_name r!   C/tmp/pip-unpacked-wheel-owvgwkas/pyhanko/sign/signers/csc_signer.pyendpoint_url   s    	z"CSCServiceSessionInfo.endpoint_urlc                 C   s   | j }|rdd| iS i S )a=  
        HTTP Header(s) necessary for authentication, to be passed with every
        request.

        .. note::
            By default, this supplies the ``Authorization`` header
            with the value of :attr:`oauth_token` as the ``Bearer`` value.

        :return:
            A ``dict`` of headers.
        AuthorizationzBearer )r   )r    tokr!   r!   r"   auth_headers   s    z"CSCServiceSessionInfo.auth_headers)__name__
__module____qualname____doc__str__annotations__r   r
   r   r#   propertyr&   r!   r!   r!   r"   r   z   s   
	c                   @   s\   e Zd ZU dZejed< eej ed< ee	 ed< e
ed< eed< eed< edd	d
ZdS )r   z
    Information about a CSC credential, typically fetched using a
    ``credentials/info`` call. See also :func:`.fetch_certs_in_csc_credential`.
    signing_certchainsupported_mechanismsmax_batch_sizehash_pinning_requiredresponse_datareturnc                 C   s"   t  }|| j || j |S )z
        Register the relevant certificates into a :class:`.CertificateStore`
        and return it.

        :return:
            A :class:`.CertificateStore`.
        )r   registerr.   Zregister_multipler/   )r    Zscsr!   r!   r"   as_cert_store   s    	zCSCCredentialInfo.as_cert_storeN)r'   r(   r)   r*   r   Certificater,   r	   r   r+   intbooldictr   r7   r!   r!   r!   r"   r      s   

   )sessioncsc_session_infotimeoutr5   c              
      s   | d}|jddd}zB| j||j|d|d4 I dH }| I dH }W 5 Q I dH R X W n. tjk
r } ztd|W 5 d}~X Y nX t|S )	a  
    Call the ``credentials/info`` endpoint of the CSC service for a specific
    credential, and encode the result into a :class:`.CSCCredentialInfo`
    object.

    :param session:
        The ``aiohttp`` session to use when performing queries.
    :param csc_session_info:
        General information about the CSC service and the credential.
    :param timeout:
        How many seconds to allow before time-out.
    :return:
        A :class:`.CSCCredentialInfo` object with the processed response.
    zcredentials/infor/   F)credentialIDcertificatesZcertInfoTheadersjsonraise_for_statusr?   NzCredential info request failed)	r#   r   postr&   rD   aiohttpClientErrorr   "_process_certificate_info_response)r=   r>   r?   urlreq_dataresponser3   er!   r!   r"   r      s$    
"r4   c           	   
   C   s  z| d d }W n, t k
r< } ztd|W 5 d }~X Y nX zdd |D }W n, tk
r| } ztd|W 5 d }~X Y nX z0| d d }t|tsttd	d
 |D }W n2 t ttfk
r } ztd|W 5 d }~X Y nX zt| d }W n2 t tfk
r$ } ztd|W 5 d }~X Y nX | dd}zt|}|dkrJtW n tk
rj   tdY nX |dk}t	|d |dd  |||| dS )NcertrA   z-Could not retrieve certificates from responsec                 S   s   g | ]}t jt|qS r!   )r   r8   loadbase64	b64decode).0rN   r!   r!   r"   
<listcomp>  s    z6_process_certificate_info_response.<locals>.<listcomp>z)Could not decode certificates in responsekeyalgoc                 s   s   | ]}t |jV  qd S N)r   ZSignedDigestAlgorithmIdnative)rR   oidr!   r!   r"   	<genexpr>&  s    z5_process_certificate_info_response.<locals>.<genexpr>z=Could not retrieve supported signing mechanisms from responseZ	multisignz/Could not retrieve max batch size from responseZSCAL   )rZ      zSCAL value must be "1" or "2".r[   r   )r.   r/   r0   r1   r2   r3   )
KeyErrorr   
ValueError
isinstancelist	TypeError	frozensetr9   getr   )	r3   Z	b64_certsrM   certsZ	algo_oidsZsupported_algosr1   Z
scal_valuer2   r!   r!   r"   rI     sf    



rI   datadigest_algorithmr5   c                 C   s0   t |}t|}||  t| dS )z
    Digest some bytes and base64-encode the result.

    :param data:
        Data to digest.
    :param digest_algorithm:
        Name of the digest algorihtm to use.
    :return:
        A base64-encoded hash.
    ascii)r   r   ZHashupdaterP   	b64encodefinalizedecode)re   rf   Z	hash_specZmdr!   r!   r"   base64_digestK  s    

rl   c                   @   s*   e Zd ZU dZeed< dZee ed< dS )r   zv
    Authorization data to make a signing request.
    This is the result of a call to ``credentials/authorize``.
    sadN
expires_at)	r'   r(   r)   r*   r+   r,   rn   r
   r   r!   r!   r!   r"   r   ]  s   
c                	   @   s   e Zd ZdZeedddZee e	dddZ
deee ee eee  ee ee ed
ddZeee	dddZedd Zd	S )r   a  
    Abstract class that handles authorisation requests for the CSC signing
    client.

    .. note::
        Implementations may wish to make use of the
        :meth:`format_csc_auth_request` convenience method to format
        requests to the ``credentials/authorize`` endpoint.

    :param csc_session_info:
        General information about the CSC service and the credential.
    :param credential_info:
        Details about the credential.
    r>   credential_infoc                 C   s   || _ || _d S rV   ro   )r    r>   rp   r!   r!   r"   __init__  s    z CSCAuthorizationManager.__init__	hash_b64sr5   c                    s   t dS )a  
        Request a SAD token from the signing service, either freshly or to
        extend the current transaction.

        Depending on the lifecycle of this object, pre-fetched SAD values
        may be used. All authorization transaction management is left to
        implementing subclasses.

        :param hash_b64s:
            Base64-encoded hash values about to be signed.
        :return:
            Authorization data.
        N)NotImplementedErrorr    rs   r!   r!   r"   authorize_signature  s    z+CSCAuthorizationManager.authorize_signaturerZ   N)num_signaturespinotprs   descriptionclient_datar5   c                 C   sp   d| j ji}|dk	r$t|}||d< ||d< |dk	r<||d< |dk	rL||d< |dk	r\||d< |dk	rl||d< |S )	a  
        Format the parameters for a call to ``credentials/authorize``.

        :param num_signatures:
            The number of signatures to request authorisation for.
        :param pin:
            The user's PIN (if applicable).
        :param otp:
            The current value of an OTP token, provided by the user
            (if applicable).
        :param hash_b64s:
            An explicit list of base64-encoded hashes to be tied to the SAD.
            Is optional if the service's SCAL value is 1, i.e.
            when :attr:`~.CSCCredentialInfo.hash_pinning_required` is false.
        :param description:
            A free-form description of the authorisation request
            (optional).
        :param client_data:
            Custom vendor-specific data (if applicable).
        :return:
            A dict that, when encoded as a JSON object, be used as the request
            body for a call to ``credentials/authorize``.
        r@   NhashZnumSignaturesZPINZOTPrz   
clientData)r>   r   len)r    rw   rx   ry   rs   rz   r{   resultr!   r!   r"   format_csc_auth_request  s     ! z/CSCAuthorizationManager.format_csc_auth_request)r3   r5   c              
   C   s   zt | d }W n tk
r,   tdY nX z2t| dd}tjt d}|t	|d }W n, t
k
r } ztd|W 5 d}~X Y nX t||d	S )
a  
        Parse the response from a ``credentials/authorize`` call into
        a :class:`.CSCAuthorizationInfo` object.

        :param response_data:
            The decoded response JSON.
        :return:
            A :class:`.CSCAuthorizationInfo` object.
        SADz.Could not extract SAD value from auth responseZ	expiresIni  )tz)secondsz2Could not process expiresIn value in auth responseN)rm   rn   )r+   r\   r   r9   rb   r   nowtzlocalZget_localzoner   r]   r   )r3   rm   Zlifetime_secondsr   rn   rM   r!   r!   r"   parse_csc_auth_response  s    z/CSCAuthorizationManager.parse_csc_auth_responsec                 C   s   | j jS )z
        HTTP Header(s) necessary for authentication, to be passed with every
        request. By default, this delegates to
        :attr:`.CSCServiceSessionInfo.auth_headers`.

        :return:
            A ``dict`` of headers.
        )r>   r&   )r    r!   r!   r"   r&     s    
z$CSCAuthorizationManager.auth_headers)rZ   NNNNN)r'   r(   r)   r*   r   r   rq   r	   r+   r   rv   r9   r
   r;   r   staticmethodr   r-   r&   r!   r!   r!   r"   r   o  s4   	      
7c                       s>   e Zd ZdZeeed fddZee	 edddZ
  ZS )r   a  
    Simplistic :class:`.CSCAuthorizationManager` for use with pre-fetched
    signature activation data.

    This class is effectively only useful for CSC services that do not require
    SAD to be pinned to specific document hashes. It allows you to use a SAD
    that was fetched before starting the signing process, for a one-shot
    signature.

    This can simplify resource management in cases where obtaining a
    SAD is time-consuming, but the caller still wants the conveniences of
    pyHanko's high-level API without having to keep too many pyHanko objects
    in memory while waiting for a ``credentials/authorize`` call to go through.

    Legitimate uses are limited, but the implementation is trivial, so we
    provide it here.

    :param csc_session_info:
        General information about the CSC service and the credential.
    :param credential_info:
        Details about the credential.
    :param csc_auth_info:
        The pre-fetched signature activation data.
    )r>   rp   csc_auth_infoc                    s   t  || || _d| _d S )NF)superrq   r   _used)r    r>   rp   r   	__class__r!   r"   rq     s    z*PrefetchedSADAuthorizationManager.__init__rr   c                    s   | j rtdd| _ | jS )z
        Return the prefetched SAD, or raise an error if called twice.

        :param hash_b64s:
            List of hashes to be signed; ignored.
        :return:
            The prefetched authorisation data.
        zPrefetched SAD token is staleT)r   r   r   ru   r!   r!   r"   rv     s    z5PrefetchedSADAuthorizationManager.authorize_signature)r'   r(   r)   r*   r   r   r   rq   r	   r+   rv   __classcell__r!   r!   r   r"   r     s   c                   @   sj   e Zd ZU dZejed< eed< ee	dZ
ee ed< dZeed< dZeee  ed	< eed
ddZdS )_CSCBatchInfoz:
    Internal value type to track batch control data.
    notifiermd_algorithm)default_factory
b64_hashesF	initiatedNresults)b64_hashr5   c                 C   s   t | j}| j| |S rV   )r~   r   append)r    r   ixr!   r!   r"   addM  s    
z_CSCBatchInfo.add)r'   r(   r)   r*   asyncioEventr,   r+   r   r_   r   r	   r   r:   r   r
   bytesr9   r   r!   r!   r!   r"   r   -  s   

r   c                
       s   e Zd ZdZdejeeeee	e
 ee	e d fdd	Z fd
dZee
 e
edddZedddZdee
edddZdd ZedddZ  ZS )r   ar  
    Implements the :class:`~pyhanko.sign.signers.pdf_cms.Signer` interface
    for a remote CSC signing service.
    Requests are made asynchronously, using ``aiohttp``.

    :param session:
        The ``aiohttp`` session to use when performing queries.
    :param auth_manager:
        A :class:`.CSCAuthorizationManager` instance capable of procuring
        signature activation data from the signing service.
    :param sign_timeout:
        Timeout for signing operations, in seconds.
        Defaults to 300 seconds (5 minutes).
    :param prefer_pss:
        When signing using an RSA key, prefer PSS padding to legacy PKCS#1 v1.5
        padding. Default is ``False``. This option has no effect on non-RSA
        signatures.
    :param embed_roots:
        Option that controls whether or not additional self-signed certificates
        should be embedded into the CMS payload. The default is ``True``.
    :param client_data:
        CSC client data to add to any signing request(s), if applicable.
    :param batch_autocommit:
        Whether to automatically commit a signing transaction as soon as a
        batch is full. The default is ``True``.
        If ``False``, the caller has to trigger :meth:`commit` manually.
    :param batch_size:
        The number of signatures to sign in one transaction.
        This defaults to 1 (i.e. a separate ``signatures/signHash`` call is made
        for every signature).
    :param est_raw_signature_size:
        Estimated raw signature size (in bytes). Defaults to 512 bytes, which,
        combined with other built-in safety margins, should provide a generous
        overestimate.
    ,  FTN   )r=   auth_managersign_timeout
prefer_pssembed_rootsr{   batch_autocommit
batch_sizec
                    sX   |j }
|| _|| _|	| _|| _|| _|| _d | _|p6d| _t	 j
|||
j|
 d d S )NrZ   )r   r   r.   Zcert_registry)rp   r   r=   est_raw_signature_sizer   r{   r   _current_batchr   r   rq   r.   r7   )r    r=   r   r   r   r   r{   r   r   r   rp   r   r!   r"   rq   x  s    
zCSCSigner.__init__c                    sd   | j d k	r| j S t |}|d }| jjj}|j|kr`td|j dddd |D  d|S )N	algorithmzSignature mechanism z" is not supported, must be one of z, c                 s   s   | ]
}|V  qd S rV   r!   )rR   Zalgr!   r!   r"   rY     s     z?CSCSigner.get_signature_mechanism_for_digest.<locals>.<genexpr>.)	Zsignature_mechanismr   "get_signature_mechanism_for_digestr   rp   r0   rW   r   join)r    rf   r   Zresult_algo	supportedr   r!   r"   r     s    


"z,CSCSigner.get_signature_mechanism_for_digest)
tbs_hashesrf   r5   c                    s   |  |}| jj}| j|I dH }|j|jt|j|d j|d}|d j	dk	rt|d 
 }t|d|d< | jdk	r| j|d< |S )a{  
        Populate the request data for a CSC signing request

        :param tbs_hashes:
            Base64-encoded hashes that require signing.
        :param digest_algorithm:
            The digest algorithm to use.
        :return:
            A dict that, when encoded as a JSON object, be used as the request
            body for a call to ``signatures/signHash``.
        Nr   )r@   r   ZhashAlgoZsignAlgor|   
parametersrg   ZsignAlgoParamsr}   )r   r   r>   rv   r   rm   r   ZDigestAlgorithmIdZdottedrW   dumprP   ri   rk   r{   )r    r   rf   Z	mechanismsession_infoZ	auth_inforK   Z
params_derr!   r!   r"   format_csc_signing_req  s$    




z CSCSigner.format_csc_signing_reqr4   c                    s   | j d k	rH| j jrHtd | j j I d H  tdt| j  d q | j d k	r~| j }|j|krztd|j d| d|S t	t
 |d | _ }|S )Nz*Commit ongoing... Waiting for it to finishz$Done waiting for commit: new batch: )zUAll signatures in the same batch must use the same digest function; encountered both z and r   )r   r   )r   r   loggerdebugr   waitreprr   r   r   r   r   )r    rf   batchr!   r!   r"   _ensure_batch  s$    


zCSCSigner._ensure_batchrd   c              
      s   |rt | jS t||}| |I d H }||}| jr|| jd krz|  I d H  W n0 tk
r } zt	j
d|d W 5 d }~X Y nX |j I d H  |jstd|j| S )NrZ   zFailed to commit signatures)exc_infozNo signing results available)r   r   rl   r   r   r   r   commitr   r   errorr   r   r   )r    re   rf   dry_runZtbs_hashr   r   rM   r!   r!   r"   async_sign_raw  s    


 zCSCSigner.async_sign_rawc                    s\   | j }|dks|jdk	rdS |jrB|j I dH  |jsXtdnd|_| |I dH  dS )a  
        Commit the current batch by calling the ``signatures/signHash`` endpoint
        on the CSC service.

        This coroutine does not return anything; instead, it notifies all
        waiting signing coroutines that their signature has been fetched.
        NzCommit failedT)r   r   r   r   r   r   
_do_commit)r    r   r!   r!   r"   r     s    	
zCSCSigner.commit)r   c              
      sT  z:z| |j|jI dH }| jj}|d}| j	}|j
|| jj|d| jd4 I dH }| I dH }W 5 Q I dH R X |d }t|}	t|j}
|	|
krtd|
 d|	 dd	 |D }||_W nv tk
r    Y nb tttfk
r
 } ztd
|W 5 d}~X Y n0 tjk
r8 } ztd|W 5 d}~X Y nX W 5 d| _ |j  X dS )zc
        Internal commit routine that skips error handling and concurrency
        checks.
        Nzsignatures/signHashTrB   
signaturesz	Expected z signatures, got c                 S   s   g | ]}t |qS r!   )rP   rQ   )rR   sigr!   r!   r"   rS   6  s     z(CSCSigner._do_commit.<locals>.<listcomp>z3Expected response with b64-encoded signature valueszSignature request failed)r   r   setr   r   r   r   r>   r#   r=   rF   r&   r   rD   r~   r   r   r]   r\   r`   rG   rH   )r    r   rK   r   rJ   r=   rL   r3   Zsig_b64sZ
actual_lenexpected_lenr   rM   r!   r!   r"   r     sL     



 zCSCSigner._do_commit)r   FTNTNr   )F)r'   r(   r)   r*   rG   ClientSessionr   r9   r:   r
   r+   rq   r   r	   r;   r   r   r   r   r   r   r   r   r!   r!   r   r"   r   S  s@   (        0  )r<   )3r*   abcr   rP   loggingZdataclassesr   r   r   r   typingr   r   r   r	   r
   r   Z
asn1cryptor   r   Zcryptography.hazmat.primitivesr   Zpyhanko_certvalidator.registryr   r   Zpyhanko.signr   Zpyhanko.sign.generalr   r   rG   ImportError__all__	getLoggerr'   r   r   r   r   r9   r   rI   r   r+   rl   r   ABCr   r   r   r   r!   r!   r!   r"   <module>   s^   T

=6 )6 
5%