Table of Contents

Signature numérique

Faire signer un PDF par signature numérique : le SDK certifie le PDF avec un certificat, puis l'envoie au signataire. C'est le mode de signature recommandé.

Prérequis

  • Le module de signature de votre langage : SecureExchanges.Signature.NetCore en .NET 8 (SecureExchanges.Signature en .NET Framework), secure-exchanges-sdk[signature] en Python ; il est inclus dans le paquet PHP. Voir Choisir son paquet.
  • Un navigateur Chromium sur la machine qui certifie : le SDK le pilote sans interface.
    • .NET : voir Docker et Linux pour un serveur Linux ou un conteneur.
    • Python : playwright install chromium, une fois par machine (environ 150 Mo).
    • PHP : Chrome ou Chromium installé ; hors des emplacements habituels, donnez son chemin dans SE_CHROME_PATH.
  • Vos identifiants chargés : voir Identifiants et environnements.

Les six étapes

  1. Lire le PDF et calculer son empreinte SHA-512. Elle désigne le fichier dans les étapes suivantes.
  2. Placer les zones à signer avec ZoneBuilder : signature, date, initiales, champs. Voir Zones de signature.
  3. Associer les zones au signataire, pour ce fichier.
  4. Obtenir le jeton de certification du document avec GetCertificationToken.
  5. Certifier le PDF avec CertifyPkiPdf de PkiFileHelper.
  6. Envoyer le PDF certifié avec MultiRecipientMessage, en le passant dans CertifiedPdfs.

Exemple complet

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using SecureExchangesSDK.Helpers;
using SecureExchangesSDK.Models;
using SecureExchangesSDK.Models.Args;
using SecureExchangesSDK.Models.Entity;
using SecureExchangesSDK.Models.JSON;
using SecureExchangesSDK.Models.Transport;
using SecureExchangesSDK.SecureExchanges;
using SecureExchangesSignatureSDK.Helpers;
using SecureExchangesSignatureSDK.Models.Args;

internal static class CertificationPki
{
    private static async Task EnvoyerPourSignature(string cheminPdf, string signataire)
    {
        // Vos identifiants viennent de votre coffre de secrets, jamais du code.
        Guid serial = LireSecret("SecureExchanges:Serial");
        Guid apiUser = LireSecret("SecureExchanges:ApiUser");
        Guid apiPassword = LireSecret("SecureExchanges:ApiPassword");
        var endpoint = new Uri("https://www.secure-exchanges.com/_api/0217/0217.asmx");

        // 1. Lire le PDF et calculer son empreinte SHA-512 : elle désigne le fichier dans les étapes suivantes.
        byte[] pdf = File.ReadAllBytes(cheminPdf);
        string sha512 = CryptoHelper.GetSHA512HashOfBytes(pdf);

        // 2. Placer les zones à signer (voir « Zones de signature »).
        SignZoneDetails[] zones;
        using (var zoneBuilder = new ZoneBuilder(pdf, isPki: true))
        {
            zones = new[]
            {
                zoneBuilder.CreateFieldSignZone(page: 1, fieldName: "signature1", x: 60, y: 400, width: 200, height: 50),
                zoneBuilder.CreateFieldDateSignZone(page: 1, fieldName: "date1", x: 60, y: 500, width: 100, height: 25),
            };
        }

        // 3. Associer les zones au signataire.
        var zonesParSignataire = new List<RecipientZoneDefinition>
        {
            new RecipientZoneDefinition
            {
                Email = signataire,
                ZonesDefByFile = new[]
                {
                    new FileZoneDefinition
                    {
                        UniqueName = sha512,
                        IsPKIFile = true,
                        ZonesDef = new SignZoneDefinition { Mode = "define", Zones = zones }
                    }
                }
            }
        };

        // 4. Obtenir le jeton de certification du document.
        var jeton = SignHelper.GetCertificationToken(new GetCertificationTokenSDKArgs(
            endpoint, serial, apiUser, apiPassword, sha512, CertificationTokenType.CertifyDocument, "fr-CA"));
        if (jeton.Status != 200)
            throw new InvalidOperationException($"Jeton de certification refusé, statut {jeton.Status}");

        // 5. Certifier le PDF. PkiFileHelper pilote un navigateur sans interface : le libérer après usage.
        CertifiedPdfContainer pdfCertifie;
        await using (var pki = new PkiFileHelper(isPreview: false, culture: "fr-CA"))
        {
            pdfCertifie = await pki.CertifyPkiPdf(new CertifyPdfArgs
            {
                File = new FileArgs(pdf, Path.GetFileName(cheminPdf), "application/pdf"),
                Recipients = new List<string> { signataire },
                CertifyData = jeton.CertificationData,
                RecipientZoneDefinitions = zonesParSignataire
            });
        }

        // 6. Envoyer le PDF certifié au signataire.
        var args = new MutliRecipientArgs(
            endPointUri: endpoint, serial: serial, user: apiUser, psw: apiPassword,
            recipients: new List<RecipientInfo> { new RecipientInfo { Email = signataire } },
            message: "Veuillez signer le document ci-joint.",
            subject: "Document à signer",
            password: null, filesList: null,
            sendMode: SendMethodEnum.onlyEmail, getBackHtml: false,
            showSubject: true, getNotify: true, cultureID: "fr-CA",
            maximumOpenTime: 5, minutesExpiration: 10080);
        args.CertifiedPdfs = new List<CertifiedPdfContainer> { pdfCertifie };
        args.OwnerDontNeedToSign = true;   // sinon vous signez aussi, en dernier : false par défaut en .NET

        var response = MessageHelper.MultiRecipientMessage(args);
        if (response.Status != 200)
            throw new InvalidOperationException($"Envoi refusé, statut {response.Status}");
    }

    private static Guid LireSecret(string nom)
    {
        // À remplacer : lecture dans votre gestionnaire de secrets (Azure Key Vault, etc.).
        throw new NotImplementedException(nom);
    }
}
Note

En .NET Framework (paquet SecureExchanges.Signature), PkiFileHelper se libère avec using plutôt qu'avec await using.

À savoir

Point Détail
Le même PDF à chaque étape L'empreinte SHA-512 désigne le fichier : ne le modifiez pas entre son calcul et la certification.
Préproduction Passez isPreview à true dans votre configuration de test, avec vos identifiants de préproduction. Il doit suivre le même environnement que vos autres appels : true avec la configuration de préproduction, false en production.
Champs déjà présents dans le PDF Avec DetectExistingFields (true par défaut), ils sont détectés ; DefineRecipients permet de les répartir entre les signataires.
Ressources PkiFileHelper lance un navigateur : libérez-le dès la certification faite, comme dans l'exemple.
Plusieurs signataires Ajoutez une définition de zones par signataire, et tous les signataires dans la liste des destinataires, dans l'ordre de signature : voir Plusieurs signataires.
Options sans PKI DoNotEncryptPDF et DoNotAppendCertificateToFile sont obsolètes et n'ont pas à être utilisées. Voir Passer à la signature numérique.

Plusieurs signataires

Les signatures se font l'une après l'autre, dans l'ordre de la liste des destinataires :

  1. Le premier signataire reçoit le courriel dès l'envoi.
  2. Chaque signataire suivant le reçoit quand le précédent a signé.
  3. Quand tout est signé, les signataires et le propriétaire de la licence reçoivent les documents signés.

Il n'y a pas d'envoi en parallèle : l'ordre de la liste est l'ordre de signature. Pour aviser des personnes qui ne signent pas, voir Destinataires en copie.

Le propriétaire de la licence

Le propriétaire de la licence qui envoie le message est toujours ajouté en dernier au circuit. OwnerDontNeedToSign décide s'il doit signer :

Valeur Effet
true Il n'a pas à signer : il reçoit seulement les documents signés à la fin.
false Il signe en dernier, après tous les autres. Il lui faut alors ses propres zones, sinon l'envoi est refusé.
Attention

La valeur par défaut n'est pas la même partout : false en .NET, true en Python et en PHP. Fixez-la toujours explicitement, comme dans l'exemple.

Options de signature

SignatureOptions, de type SignOptions, règle trois aspects du circuit : les modes de signature offerts, un fichier que le signataire doit joindre, et des destinataires en copie qui ne signent pas. Il n'a d'effet que sur un envoi avec des documents à signer. L'exemple reprend l'étape 6 de l'exemple complet :

using System;
using System.Collections.Generic;
using SecureExchangesSDK.Helpers;
using SecureExchangesSDK.Models;
using SecureExchangesSDK.Models.Args;
using SecureExchangesSDK.Models.Entity;
using SecureExchangesSDK.Models.Enum;
using SecureExchangesSDK.Models.JSON;
using SecureExchangesSDK.Models.Transport;
using SecureExchangesSDK.SecureExchanges;

internal static class OptionsSignature
{
    // Étape 6 de l'exemple complet, avec les options de signature : pdfCertifie vient de l'étape 5.
    private static void EnvoyerAvecOptions(CertifiedPdfContainer pdfCertifie, string signataire,
        string enCopie, string enCopieCachee)
    {
        // Vos identifiants viennent de votre coffre de secrets, jamais du code.
        Guid serial = LireSecret("SecureExchanges:Serial");
        Guid apiUser = LireSecret("SecureExchanges:ApiUser");
        Guid apiPassword = LireSecret("SecureExchanges:ApiPassword");
        var endpoint = new Uri("https://www.secure-exchanges.com/_api/0217/0217.asmx");

        var options = new SignOptions
        {
            // Mode permis pour la signature et les initiales : ici, tracer seulement.
            SignatureRestriction = SignatureModeRestriction.Trace,
            // Le signataire doit joindre un fichier pour terminer : un seul signataire dans ce cas.
            UploadFileOptions = new SignOptionsFileUpload
            {
                UploadMode = ReplyFileUploadMode.UploadMandatory,
                AuthorizedExtensions = new[] { ".pdf", ".jpg", ".png" },
                Messages = new List<ReplyOptionsMessage>
                {
                    new ReplyOptionsMessage { CultureID = "fr-CA", Message = "Joignez l'annexe B remplie." },
                    new ReplyOptionsMessage { CultureID = "en-CA", Message = "Attach the completed Schedule B." }
                },
                NoEditor = true
            },
            // Avisés dès l'envoi, ils reçoivent les documents signés à la fin, sans signer.
            CCRecipients = new List<Recipient> { new Recipient { Email = enCopie } },
            // Comme les précédents, mais masqués aux signataires.
            BCCRecipients = new List<Recipient> { new Recipient { Email = enCopieCachee } }
        };

        var args = new MutliRecipientArgs(
            endPointUri: endpoint, serial: serial, user: apiUser, psw: apiPassword,
            recipients: new List<RecipientInfo> { new RecipientInfo { Email = signataire } },
            message: "Veuillez signer le document ci-joint, puis y joindre l'annexe B remplie.",
            subject: "Document à signer",
            password: null, filesList: null,
            sendMode: SendMethodEnum.onlyEmail, getBackHtml: false,
            showSubject: true, getNotify: true, cultureID: "fr-CA",
            maximumOpenTime: 5, minutesExpiration: 10080);
        args.CertifiedPdfs = new List<CertifiedPdfContainer> { pdfCertifie };
        args.SignatureOptions = options;
        args.OwnerDontNeedToSign = true;   // sinon vous signez aussi, en dernier : false par défaut en .NET

        var response = MessageHelper.MultiRecipientMessage(args);
        if (response.Status != 200)
            throw new InvalidOperationException($"Envoi refusé, statut {response.Status}");
    }

    private static Guid LireSecret(string nom)
    {
        // À remplacer : lecture dans votre gestionnaire de secrets (Azure Key Vault, etc.).
        throw new NotImplementedException(nom);
    }
}

Modes de signature permis

SignatureRestriction désigne les modes que le signataire peut employer pour sa signature et ses initiales. Le sceau, toujours une image, n'est pas visé. La restriction vaut pour tous les signataires du circuit.

Valeur Mode offert
NoRestriction Tous les modes. C'est la valeur par défaut.
Text Saisir son nom au clavier : il est rendu en écriture cursive.
Trace Tracer sa signature à la souris ou au doigt.
Picture Téléverser une image de sa signature, en PNG ou en JPEG, de 500 Ko au plus.

Les valeurs se combinent avec l'opérateur | : Text et Trace ensemble offrent la saisie et le tracé.

En PHP, combinez leurs ->value avec l'opérateur | : SignatureModeRestriction::TEXT->value | SignatureModeRestriction::TRACE->value offre la saisie et le tracé. Une valeur qui ne correspond à aucun mode est refusée avant l'envoi.

Fichier à joindre

UploadFileOptions reçoit un SignOptionsFileUpload, qui demande au signataire de joindre des fichiers à ses documents signés.

Propriété Rôle
UploadMode NoUpload, la valeur par défaut : aucun fichier ne peut être joint. UploadOptional : le signataire peut en joindre. UploadMandatory : il ne peut pas terminer sans en joindre au moins un.
AuthorizedExtensions Extensions permises, avec le point, par exemple .pdf. Un fichier d'une autre extension est refusé ; les documents signés ne sont pas visés. Sans liste, toute extension est permise.
Messages La consigne affichée au signataire : un ReplyOptionsMessage par culture. Seule s'affiche la consigne dont la culture est exactement celle de la page, fr-CA ou en-CA : fournissez les deux. Écrivez du texte simple.
NoEditor true : le signataire ne peut pas écrire de message, il joint seulement des fichiers.
NoReply Sans effet en signature.

Messages accepte aussi des dictionnaires en Python, et des tableaux en PHP, avec les clés CultureID et Message.

Important

UploadOptional et UploadMandatory exigent un seul signataire. Avec plusieurs destinataires, l'envoi est refusé.

Destinataires en copie

CCRecipients et BCCRecipients ajoutent des destinataires qui ne signent pas :

  1. Dès l'envoi, ils sont avisés que des documents sont à signer. Ils ne peuvent pas les signer.
  2. Quand tout est signé, ils reçoivent les documents signés.

Ceux de BCCRecipients sont masqués : les signataires ne les voient pas, et le courriel final ne les nomme pas.

Chaque destinataire en copie a une adresse courriel, un numéro de téléphone, ou les deux. Avec un numéro seulement, il est joint par SMS ; avec les deux, il reçoit le courriel et un code par SMS.

En .NET, ce sont des Recipient de l'espace de noms SecureExchangesSDK.SecureExchanges : seuls Email et Phone comptent.

En Python, ce sont des Recipient, exportés par secure_exchanges_sdk.models.entity.

En PHP, ce sont des RecipientInfo, comme pour les signataires.

Erreurs fréquentes

Symptôme Cause probable Solution
Échec au lancement du navigateur Chromium absent ou dépendances système manquantes Voir Docker et Linux.
Envoi refusé : « Not all recipients have fields defined for all files » OwnerDontNeedToSign à false, le défaut en .NET, sans zones pour le propriétaire Passez-le à true, ou ajoutez ses zones.
Envoi refusé, message commençant par « The signature file upload mode » UploadOptional ou UploadMandatory avec plusieurs signataires Gardez un seul signataire, ou passez UploadMode à NoUpload.
Jeton de certification refusé Identifiants, environnement ou licence Voir Codes de statut et Licence et quotas.
Les zones n'apparaissent pas Zones associées à un autre fichier ou à une autre adresse Vérifiez l'empreinte SHA-512 et l'adresse du signataire.

Et ensuite