Skip to main content
Electronic trust, from hash to archive

The signing transaction: binding consent to a document

How a signing service ties one approval to one document: authentication vs identity, one-time codes, digest-bound authorization, replay and tamper-evident logs.

In this series
Part 5 of 9
Author
Lamine Diallo
Read time
17 min read
Published
On this page

Who this is for

Software engineers and technical decision-makers who have read Articles 1 to 4 and know what a signature, a certificate and an HSM are.

Read first

You will learn

  • Tell authentication from identity proofing, and intent to authenticate from consent to a document
  • Say what a one-time code proves, and why it cannot show which document was approved
  • Bind a signing authorization to a document digest, signer, credential, session, nonce and expiry, and explain why a version number is not enough
  • Build, replay and substitute an authorization in Python, and watch a hash-chained log break at the edited row
  • Match the main threats to a signing transaction to the control that stops or detects each one

The problem: a valid signature on the wrong document

Article 1 ended with a narrow guarantee: a valid signature proves that a key signed those exact bytes. Articles 2 to 4 tied the key to a name, kept it in a device and let Ibrahima check the certificate was still good. None of that tells him what Mariama agreed to.

Mariama reads a loan contract for 5,000,000 GNF and approves it. Before the key signs, something swaps the file for one that says 9,000,000 GNF. The key is hers, the certificate is valid, the HSM never let the key out, and the signature verifies. Every check from Articles 1 to 4 passes, on a document she never saw.

The gap is in the signing transaction: the steps between “Mariama opens the document” and “the key signs”. This article binds an approval to one document, says whom that binding restrains, and logs the transaction so tampering shows. The hands-on builds the binding in Python and attacks it.

Freeze the document first

A signature covers bytes, so the bytes must stop moving. Before any signer sees a document, the service fixes it and computes its SHA-256 digest, which becomes its identity. An edit creates a new document with a new digest, and every approval given for the old one is void.

An envelope groups the documents, signers, signing order and state. It is a workflow container, not a cryptographic object; each approval inside it points at a digest, not a file name.

Authentication is not identity proofing

NIST SP 800-63-4 defines authentication as “the process by which a claimant proves possession and control of one or more authenticators bound to a subscriber account to demonstrate that they are the subscriber associated with that account”. Identity proofing is “the processes used to collect, validate, and verify information about a subject to establish assurance in the subject’s claimed identity” (SP 800-63-4, glossary).

Side by side, the definitions draw a line (the contrast is ours). Authentication shows that the person at the keyboard controls the same account as before, not who that person is in real life. That is identity proofing, which Article 2 placed with the registration authority and Article 8 covers in depth.

Authentication also has a lifetime. A session “begins with an authentication event and ends with a session termination event” (glossary). At AAL2, NIST’s middle assurance level, the overall timeout should be no more than 24 hours and the inactivity timeout no more than one hour, and at least one authenticator shall be replay-resistant (SP 800-63B-4, §2.2.2, §2.2.3). Whoever holds a live session acts as the account holder, which is why the model below records the session in the authorization, so that one issued in a session that has since ended or timed out can be refused.

SP 800-63B-4 asks for authentication intent: the process “requires the claimant to respond explicitly to each authentication or reauthentication request”. Typing a one-time code is its example (§3.2.8). That is intent to log in, not consent to a document’s content (the distinction is ours).

Consent comes from the remote-signing standards. ETSI TS 119 431-1 states: “Signing keys shall be usable in only those cases for which the signer’s consent has been obtained” (SIG-6.3.1-09). Under its Normalized SSAS Policy, where the signer is a natural person and authentication is linked directly to their identity, it requires “an explicit action (not just a checkbox)” to approve “the content of the specific documents referenced in the SAD”, such as scrolling to the end before accepting or typing “I agree to sign this contract” (SIG-6.3.1-15). A signing screen has two jobs: show the right account holder is present, and capture explicit approval of specific bytes.

What a one-time code proves

In NIST’s terms, an OTP authenticator is a token or app holding a secret seed; typing its code proves “possession and control of the authenticator” (§3.1.4). TOTP, RFC 6238, is the common time-based form (RFC 6238). A code sent by SMS or voice call is an out-of-band secret instead, delivered to an out-of-band device, and the authentication must finish within 10 minutes (§3.1.3, §3.1.3.2). Phone-network codes are a restricted option that calls for watching SIM changes and number porting (§3.1.3.3), and email “SHALL NOT be used for out-of-band authentication” (§3.1.3.1).

Both kinds share two properties that matter for signing.

Single use is the verifier’s job. Verifiers “SHALL accept a given OTP only once while it is valid” (§3.1.4.2; §3.1.3.2 for out-of-band secrets). RFC 6238 says the verifier “MUST NOT accept the second attempt of the OTP after the successful validation has been issued for the first OTP” (§5.2). Nothing in the code itself stops a second use.

They are not phishing-resistant. “The manual entry does not bind the authenticator output to the specific session being authenticated” (§3.2.5). A fake site can ask Mariama for her code and relay it while it is still valid.

So a code proves control of an authenticator bound to an account. It says nothing about legal identity, as Article 2 showed in OTP ≠ legal identity. And on its own it names no document: nothing in six digits points at a contract (our inference from §3.2.5). The link has to come from elsewhere.

Binding the approval to the transaction

Remote-signing standards name that link. In the Cloud Signature Consortium (CSC) API, Signature Activation Data (SAD) is the “set of data used to control a given signature operation”, and a signature activation module “uses the SAD in order that the signing keys are used under sole control of the signer” (CSC API v2.0.0.2, §4.1). Article 3 met SAD in HSM ≠ authorization; here is how it is built. CSC section numbers in this article are v2.0.0.2’s; the current v2.2 renumbers some of them.

The CSC credentials/authorize call carries the credential ID, the number of signatures and the document hashes. The hashes allow “the server to bind the SAD to the hash(es), thus preventing an authorization to be used to sign a different content”; the SAD expires after 3,600 seconds by default (§11.6). The CSC specification summarises CEN EN 419 241-1’s SCAL2 level: the SAD “is linked to the document or the documents to be signed” (§8.2). The server invalidates the SAD once the authorized number of signatures is made (§11.9). TS 119 431-1 adds the session: where authentication is linked to the signer’s identity, the SAD shall contain the unique identifier of the signature session (SIG-6.3.1-11).

Payments reached the same idea: the EU’s 2018 strong-authentication rules require the code to be “specific to the amount of the payment transaction and the payee”, and “any change to the amount or the payee results in the invalidation of the authentication code” (Delegated Regulation 2018/389, art. 5(1)). Another sector’s rule, shown for comparison: approve this, and only this.

The article’s model combines those sources into one set of fields; no single standard lists it, so it is our synthesis.

Field What it pins down Where the idea comes from
Document digest These bytes and no others CSC hashes (§11.6)
Signer Whose approval this is The article’s model
Credential ID Which signing key may be used CSC credentialID (§11.6)
Session The signing session it belongs to TS 119 431-1, SIG-6.3.1-11 (its “signature session”); SP 800-63B-4 §5.1 for the login session
Nonce A value never used before, consumed on first use SP 800-63-4 glossary; SP 800-63B-4 §3.1.4.2
Expiry A short window of validity CSC expiresIn (§11.6); SP 800-63B-4 §3.1.3.2

The service signs the whole set with its own key, so no field can change without breaking the authorization. A nonce is “a value used in security protocols that is never repeated with the same key”, and it “is not necessarily unpredictable” (glossary). Uniqueness is what matters, and the verifier must remember consumed nonces.

The code authenticates Mariama; the authorization binds her approval to one digest. The article's model, drawn from CSC API §11.6, TS 119 431-1 §6.3.1 and SP 800-63B-4.

A version number is not a binding

“Mariama approves version 3 of the contract” sounds precise. It is not. A version number is a label the server assigns. If anything between approval and signing (a queue, storage, another component) swaps the bytes behind “v3”, an approval that names only “v3” still matches.

A digest is computed from the bytes. A different document gives, with very high probability, a different SHA-256 digest, as Article 1 showed with a one-byte edit. An approval bound to the digest refuses any substitute, including a genuine later version. That is the CSC’s “preventing an authorization to be used to sign a different content”, spelled out; the argument is ours. Keep version numbers for people; bind approvals to digests.

Who the authorization restrains

In this model the service signs the authorization with its own key, and the activation module checks that signature and the authorization’s fields against the request: digest, signer, credential, session, nonce and expiry. Nothing from Mariama reaches the module. That stops outside attackers: a replay, a document swapped in storage, an expired approval. It cannot restrain whoever controls the service: they hold the server key and can mint a fresh, valid authorization over the 9,000,000 GNF digest at will.

Restraining whoever runs the application in front takes authorization data the service cannot produce alone. In the CSC API, the signer’s authentication data and the document hashes go to the remote service hosting the activation module, which issues the SAD bound to those hashes; for SCAL2, “a two-factor authorization is needed” (§8.2, §11.6). The module uses the SAD so keys are used “under sole control of the signer” (§4.1), and TS 119 431-1 makes keys usable only with the signer’s consent (SIG-6.3.1-09). The application in front cannot mint a SAD without the signer (our reading of §8.2).

The audit trail makes misuse detectable, not impossible: a signature with no matching authentication in a log anchored outside the operator’s reach is evidence after the fact (Schneier and Kelsey, §1).

The signing transaction as a state machine

Seen from the service, a transaction moves through a few guarded states. This is the article’s model, not a standard’s list.

One transaction, seven states. An edit after sending starts over with a new digest; an approval never carries across.

The “next signer” loop runs in order for sequential signing; in parallel signing, each signer follows the path independently.

An audit log that shows tampering

The log that later answers who authenticated, when, and which digest was approved must resist editing, even by the service’s staff.

Schneier and Kelsey’s secure audit log makes entries written before a compromise “impossible for the attacker to read, and also impossible to undetectably modify or destroy”. It is “not a system to prevent all possible manipulations … this is a system to detect such manipulations after the fact” (Abstract, §1).

The core is a hash chain: each entry’s chain value is the hash of the previous chain value and the entry’s data, starting from a block of zeros (§3.1). Change one entry and every later value changes. Each value is authenticated with a MAC under a key that evolves after every entry, so “a correct MAC on any hash-chain value is essentially a MAC on all previous entries as well” (§3.5).

An attacker who takes over the logging machine “can delete a block of entries (or the entire log file), but he cannot create new entries” that pass (§3.1). Truncation is detectable only if something outside the log recorded where it ended; otherwise “an attacker can silently truncate the log” (§4.1).

Our inference: a bare, unkeyed chain in one place is not enough, since whoever can rewrite the store can recompute every link from the edited row. The chain needs an anchor outside that person’s reach: a MAC key they do not hold, a head value sealed or published elsewhere, or a timestamp. The hands-on chain is unkeyed on purpose, so you can see that limit.

Signature or seal?

What a signature means depends on whose key made it.

For comparison: seals in the EU's eIDAS regulation

The signatory of an electronic signature is “a natural person” (art. 3(9)); the creator of an electronic seal is “a legal person” (art. 3(24)), and a seal ensures the “origin and integrity” of the data it is attached to (art. 3(25)). A qualified seal enjoys “the presumption of integrity of the data and of correctness of the origin” (art. 35(2)). This is the EU’s framework, shown for comparison, not Guinean law. Source: eIDAS, consolidated 2024.

Our teaching point: an organisation’s key on a document behaves like a seal. It shows the organisation stands behind an unchanged document, not that a particular person approved it (Article 1’s point about one company-wide key). A person’s approval needs the consent binding above. For the paper equivalent, see the company stamp, and what replaces it online.

Threat scenarios

Each row pairs a threat with its control and where that control is stated.

Threat What stops or detects it Source
Replay of a captured authorization or code Single-use nonce; short expiry SP 800-63B-4 §3.2.7, §3.1.4.2; RFC 6238 §5.2; CSC §11.9
Document substitution, including a new “version” Authorization bound to the digest CSC §11.6; TS 119 431-1 SIG-6.3.1-13, -15; PSD2 RTS art. 5(1)
Cross-signer use: Mariama’s approval with another key Signer and credential ID in the signed authorization CSC §11.6; TS 119 431-1 SIG-6.3.1-11
Code relay: a fake site forwards Mariama’s code Not stopped by the code. The attacker can approve only documents already frozen and addressed to Mariama, but the approval is theirs, not hers (our note). Phishing-resistant authentication is the real control SP 800-63B-4 §3.2.5
Authentication fatigue: repeated approve prompts Require transfer of a secret; rate-limit prompts SP 800-63B-4 §3.1.3, §3.1.3.2
SIM swap or number porting on SMS codes Risk signals; an unrestricted alternative SP 800-63B-4 §3.1.3.3
Malicious or compromised service, or rogue HSM use: signing without her approval Not stopped by this model: the service holds the authorization key. Needs SAD from her own authentication, checked by a separate activation module; an anchored log makes misuse detectable. An HSM does neither TS 119 431-1 SIG-6.3.1-09; CSC §4.1, §8.2, §11.6; Schneier and Kelsey
Audit tampering by an insider Hash chain plus an outside anchor: detectable, not preventable Schneier and Kelsey §1, §3.1, §4.1
Stale session used to approve Reauthentication limits; the session ID in the authorization lets the verifier enforce them SP 800-63B-4 §2.2.3, §5.1

Mariama signs, Ibrahima checks

  1. Ibrahima’s company freezes the 5,000,000 GNF contract, computes its digest and sends the envelope to Mariama.
  2. Mariama authenticates; her session is sess-7f3a. She reads to the end and approves explicitly.
  3. The service builds an authorization over the digest, “Mariama”, cred-mariama-01, sess-7f3a, a random nonce and a two-minute expiry, and signs it with its own key.
  4. The signing side checks the authorization, consumes the nonce and signs. Each step goes into the hash-chained log.

A replay is refused because its nonce is spent; a fresh authorization presented with the 9,000,000 GNF contract, because the digest differs. An edited log row breaks the chain. Ibrahima checks the signature and certificate as in Articles 1 to 4, then the chain up to its anchor.

Try it: bind, replay, substitute, tamper

The script implements the model in Python. It runs in a throwaway container and generates every key on the spot; the script writes nothing to disk. Never use it with a real key.

  • build_authorization serialises the six fields canonically (sorted keys, no spaces) and signs them with an ECDSA P-256 server key, the scheme from Article 1.
  • verify_authorization checks the signature, then the nonce, then the digest, then the expiry, and records the nonce as consumed only if all pass.
  • Cases 1 to 3: verify, replay, then a fresh authorization (new session) presented with a different contract.
  • Case 4 builds a four-row log where each row’s hash covers the previous one, starting from 64 zeros and with no MAC, then edits row 2.

Three simplifications. The script signs the signer, credential and session but has no request to compare them against, so it exercises only the digest, nonce and expiry. And its used-nonce set lives in memory; a real verifier stores consumed nonces durably and consumes each one atomically. And the same server both issues and checks the authorization, so it shows the outside-attacker protection only.

Executable educational example · Docker · python:3.12-alpine (Python 3.12.14) · cryptography==50.0.1

Save the script as signing_transaction.py, then run, from the same directory:

docker run --rm --mount type=bind,src="$PWD/signing_transaction.py",dst=/in/signing_transaction.py,readonly -w /work python:3.12-alpine sh -c '
python3 --version &&
pip install cryptography==50.0.1 >/dev/null 2>&1 &&
python3 /in/signing_transaction.py'
import cryptography
print("cryptography", cryptography.__version__)

import hashlib
import json
import secrets
from datetime import datetime, timedelta, timezone

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature

CONTRACT = b"Contrat de pret 2026-001 : montant 5000000 GNF\n"
OTHER = b"Contrat de pret 2026-001 : montant 9000000 GNF\n"

server_key = ec.generate_private_key(ec.SECP256R1())
server_pub = server_key.public_key()

used_nonces = set()


def canonical(payload):
    return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()


def build_authorization(document, signer, credential_id, session, ttl_seconds=120):
    payload = {
        "document_digest": hashlib.sha256(document).hexdigest(),
        "signer": signer,
        "credential_id": credential_id,
        "session": session,
        "nonce": secrets.token_hex(16),
        "expiry": (datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)).isoformat(),
    }
    signature = server_key.sign(canonical(payload), ec.ECDSA(hashes.SHA256()))
    return payload, signature


def verify_authorization(payload, signature, document):
    try:
        server_pub.verify(signature, canonical(payload), ec.ECDSA(hashes.SHA256()))
    except InvalidSignature:
        return False, "signature invalid"
    if payload["nonce"] in used_nonces:
        return False, "nonce already used (replay)"
    if payload["document_digest"] != hashlib.sha256(document).hexdigest():
        return False, "document digest mismatch"
    if datetime.fromisoformat(payload["expiry"]) < datetime.now(timezone.utc):
        return False, "authorization expired"
    used_nonces.add(payload["nonce"])
    return True, "authorization valid"


# 1. Build + verify a signing authorization bound to the contract.
auth, sig = build_authorization(CONTRACT, "Mariama", "cred-mariama-01", "sess-7f3a")
ok, reason = verify_authorization(auth, sig, CONTRACT)
print(f"[1] first use: {'OK' if ok else 'REJECTED'} ({reason})")

# 2. Replay the same authorization: nonce already consumed above.
ok, reason = verify_authorization(auth, sig, CONTRACT)
print(f"[2] replay: {'OK' if ok else 'REJECTED'} ({reason})")

# 3. Substitute the document under a fresh authorization: digest won't match.
auth2, sig2 = build_authorization(CONTRACT, "Mariama", "cred-mariama-01", "sess-9b21")
ok, reason = verify_authorization(auth2, sig2, OTHER)
print(f"[3] substituted document: {'OK' if ok else 'REJECTED'} ({reason})")


# 4. Hash-chained audit log: 4 rows, tamper row 2, verification breaks there.
def row_hash(prev_hash, seq, event, actor):
    data = f"{prev_hash}|{seq}|{event}|{actor}".encode()
    return hashlib.sha256(data).hexdigest()


events = [
    ("session_started", "Ibrahima"),
    ("otp_verified", "Mariama"),
    ("authorization_signed", "server"),
    ("document_signed", "Mariama"),
]

log = []
prev = "0" * 64
for seq, (event, actor) in enumerate(events, start=1):
    row = {"seq": seq, "event": event, "actor": actor, "prev_hash": prev}
    row["hash"] = row_hash(prev, seq, event, actor)
    log.append(row)
    prev = row["hash"]


def verify_chain(log):
    prev = "0" * 64
    for row in log:
        expected = row_hash(prev, row["seq"], row["event"], row["actor"])
        if row["prev_hash"] != prev or row["hash"] != expected:
            return False, row["seq"]
        prev = row["hash"]
    return True, None

ok, broken_at = verify_chain(log)
print(f"[4a] audit log (4 rows, untouched): {'OK' if ok else f'BROKEN at row {broken_at}'}")

log[1]["event"] = "otp_verified_TAMPERED"
ok, broken_at = verify_chain(log)
print(f"[4b] audit log (row 2 edited): {'OK' if ok else f'BROKEN at row {broken_at}'}")

Expected output

Python 3.12.14
cryptography 50.0.1
[1] first use: OK (authorization valid)
[2] replay: REJECTED (nonce already used (replay))
[3] substituted document: REJECTED (document digest mismatch)
[4a] audit log (4 rows, untouched): OK
[4b] audit log (row 2 edited): BROKEN at row 2

These lines are stable across runs; Docker may print download progress first.

Try one change: after the tampering, recompute every hash from row 2 onwards and verify again. The chain passes: the unkeyed-chain limit, and why a real log needs an outside anchor.

Where SEDEYA fits

SEDEYA records each signing transaction in a hash-chained audit trail and seals that trail into the signed PDF, so the record travels with the document instead of living only in a database. Envelopes can be signed in sequence or in parallel. Signing keys are held in a hardware security module, accessed through PKCS#11, or in a KMS. Signatures are PAdES, up to the B-LTA level, and Ibrahima can check a signed PDF on SEDEYA’s public verification page, by upload or by scanning its QR code. Later articles in this series cover where that signature sits inside a PDF and how long it stays verifiable.

Risks and misconceptions

“The keys are in an HSM, so nobody can misuse them.”

The HSM protects the key, not the decision to use it. Consent, proven by authorization data the service cannot forge, does that (TS 119 431-1, SIG-6.3.1-09; CSC API §4.1).

Summary

  • Freeze the document first; its digest is its identity, and an edit voids old approvals.
  • Authentication shows control of an account’s authenticators; identity proofing establishes who the holder is.
  • A one-time code is not phishing-resistant, relies on the verifier for single use, and names no document.
  • A signing authorization binds the approval to digest, signer, credential, session, nonce and expiry. A version number cannot.
  • Signed by the service’s key, it stops outsiders, not the operator; that takes SAD from the signer’s own authentication.
  • A hash chain detects tampering, prevents none, and needs an outside anchor.

Check your understanding

Check your understanding

Mariama typed a correct code and the signature verifies with her certificate. Did she approve this exact contract?

Show the answer

Not from those facts. The code names no document, and the signature only proves her key signed these bytes. Ibrahima needs a record that her approval was bound to this document’s digest, in a log that shows no tampering.

Check your understanding

An authorization names 'contract v3' instead of a digest. What does that leave open?

Show the answer

Substitution. Whoever controls storage between approval and signing can put different bytes behind “v3”. Binding to the SHA-256 digest makes that substitute fail, as case 3 showed. It does not stop whoever holds the service’s key, who can simply issue a new authorization.

Check your understanding

After the tampering, you recompute every hash from row 2 onwards and the chain verifies. What was missing?

Show the answer

An anchor outside the store. Schneier and Kelsey use an evolving MAC key and a trusted party; a sealed or published head value, or a timestamp, plays a similar role for every entry up to the anchored head.

Next step

The authorization bound Mariama’s approval to a digest. Article 6 shows where the signed digest sits inside a PDF, and why it is not always the digest of the file she reviewed. Meanwhile, talk to us and we will show you a SEDEYA-signed PDF with its sealed audit trail, and how anyone can verify it.

References

  1. Digital Identity Guidelines: Authentication and Authenticator Management, SP 800-63B-4, NIST, July 2025 (§2.2.2, §2.2.3, §3.1.3, §3.1.3.1–3.1.3.3, §3.1.4, §3.1.4.2, §3.2.5, §3.2.7, §3.2.8, §5.1)
  2. Digital Identity Guidelines, SP 800-63-4, NIST, July 2025 (Glossary: authentication, identity proofing, nonce, replay attack, session)
  3. Architectures and protocols for remote signature applications (CSC API) v2.0.0.2, Cloud Signature Consortium, v2.0.0.2 (§4.1, §8.2, §11.6, §11.9)
  4. Policy and security requirements for TSPs; Part 1: TSP services operating a remote QSCD / SCDev, ETSI TS 119 431-1, ETSI, V1.3.1 (2024-12) (§6.3.1: SIG-6.3.1-09, -11, -13, -15)
  5. Commission Delegated Regulation (EU) 2018/389, regulatory technical standards for strong customer authentication (PSD2), Publications Office of the European Union, Original text, OJ L 69, 13 March 2018 (Art. 5(1))
  6. TOTP: Time-Based One-Time Password Algorithm, RFC 6238, IETF, May 2011 (§5.2)
  7. Secure Audit Logs to Support Computer Forensics, Bruce Schneier and John Kelsey, Counterpane, Counterpane version of the USENIX Security 1998 paper (Abstract, §1, §3.1, §3.5, §4.1)
  8. Regulation (EU) No 910/2014 (eIDAS), consolidated text, Publications Office of the European Union, Consolidated 18 October 2024 (Art. 3(9), 3(24), 3(25), art. 35(2))
  9. cryptography 50.0.1 changelog, Python Cryptographic Authority, 50.0.1, 25 August 2026