On this page
- The problem: a file can say anything
- Electronic signature or digital signature?
- Hashing: a fixed-size fingerprint of the bytes
- Key pairs: one to sign, one to check
- Signing and verifying
- Mariama signs, Ibrahima checks
- Try it: sign, verify, tamper
- With OpenSSL
- With Python
- Where SEDEYA fits
- Risks and misconceptions
- Summary
- Check your understanding
- Next step
Who this is for
Software engineers and technical decision-makers with no cryptography background.
Read first
None, start here.
You will learn
- Tell an electronic signature (a legal notion) from a digital signature (a cryptographic mechanism)
- Explain what SHA-256 does to a document and why a one-byte change produces a different digest
- Describe how a private key signs and a public key verifies, without the "encrypting the hash" shortcut
- Sign, verify and tamper with a file yourself using OpenSSL and Python
- Say what a valid signature proves, and what it cannot prove about who signed
The problem: a file can say anything
Mariama runs a small business in Conakry. She agrees to a loan of 5,000,000 GNF with Ibrahima’s company, and the contract travels as a file. A file is just bytes, and bytes are easy to change. If someone edits one digit on the way, the amount becomes 6,000,000 GNF and the file still opens and still looks professional.
On paper you would compare the ink, the initials on every page, the copy each side kept. For a file, Ibrahima needs two answers he can check himself:
- Is this exactly the file that was signed, byte for byte?
- Which key signed it?
A digital signature answers both. It does not, on its own, answer a third question that people often assume it does: which person was behind that key. This article shows how the mechanism works, runs it on a real file, and marks where its guarantees stop.
Electronic signature or digital signature?
The two terms are often used as synonyms. They are not the same kind of thing.
An electronic signature is a legal notion. Guinea’s law on electronic transactions defines « signature électronique » in its first article as « toute donnée qui résulte d’un procédé fiable d’identification, de nature à garantir ou authentifier son lien avec l’acte auquel elle s’attache » (L/2016/035/AN, art. 1). In our translation: any data resulting from a reliable identification process, capable of guaranteeing or authenticating its link with the act it is attached to.
That definition names no algorithm. The law does not define “signature numérique” (digital signature) at all. Our reading, which is not legal advice, is that the definition is technology-neutral: it asks for a reliable process and a link to the document, and leaves the technique open.
A digital signature is a cryptographic mechanism. The Digital Signature Standard defines it as “the result of a cryptographic transformation of data that, when properly implemented, provides a mechanism for verifying origin authentication, data integrity, and signatory non-repudiation” (FIPS 186-5, §2.1). It is one way to produce what the law calls a signature électronique, and the one this series is about.
Article 34 goes further. Its fifth paragraph gives a secure signature linked to a qualified certificate the same probative force or value as a handwritten one. Its first paragraph also uses handwritten-signature wording, under different conditions: a reliable, secure device under the signatory’s exclusive control, and a « certificat numérique ». How the two paragraphs relate is a question for a Guinean lawyer, and none of this is legal advice. Article 34 also says a signature may not be refused solely because it is electronic (art. 34). Both paragraphs name a certificate among their conditions, so a digital signature on its own meets neither. For more on the Guinean text, see what Guinean law says about electronic signatures.
For comparison: the EU's eIDAS regulation
eIDAS is also technology-neutral. It defines an electronic signature as “data in electronic form which is attached to or logically associated with other data in electronic form and which is used by the signatory to sign” (art. 3(10)), and the phrase “digital signature” does not appear in its consolidated text. It then defines advanced and qualified signatures (art. 3(11)–(12), art. 26), and only a qualified electronic signature has “the equivalent legal effect of a handwritten signature” (art. 25(2)). One requirement for an advanced signature is that “any subsequent change in the data is detectable” (art. 26(1)(d)); in practice a digital signature is the usual way to meet it, though the regulation does not say so. This is the EU’s framework, shown for comparison, not a description of Guinean law. Source: eIDAS, consolidated 2024.
A scanned image of a handwritten signature is neither of these. It proves nothing about the file around it, which is why a scanned signature is not an electronic signature. For the signer’s view of a complete signing flow, from the link to the sealed file, see how an electronic signature actually works. The rest of this article opens up the cryptography inside that flow.
Hashing: a fixed-size fingerprint of the bytes
A digital signature does not work on the document directly. It first reduces the document to a short value called a message digest, using a hash function.
SHA-256 is one of the secure hash algorithms specified in FIPS 180-4, alongside SHA-1, SHA-224, SHA-384, SHA-512, SHA-512/224 and SHA-512/256. It accepts a message of fewer than 2^64 bits and always outputs a 256-bit digest: 32 bytes, usually written as 64 hexadecimal characters (FIPS 180-4, §1). A one-page contract and a 10 MB scan both come out as 32 bytes.
The standard calls these algorithms “secure” because it is computationally infeasible:
- to find a message that produces a given digest (preimage resistance);
- to find two different messages that produce the same digest (collision resistance).
NIST SP 800-57 uses those two names (§5.6.1.2). The practical consequence, in FIPS 180-4’s words: “any change to a message will, with a very high probability, result in a different message digest”, and when the hash is used with a digital signature algorithm this “will result in a verification failure” (FIPS 180-4, item 3).
For signatures, SHA-256 gives at most 128 bits of security strength. SHA-1, by contrast, “has been demonstrated to provide less than 80 bits of security for digital signatures” (SP 800-57, §5.6.1.2, Table 3). That is why you should not sign anything new with SHA-1.
Key pairs: one to sign, one to check
A signature scheme uses a key pair. The private key generates signatures and stays with its owner. The public key “corresponds to but is not the same as the private key” and can be given to anyone, because it only verifies (FIPS 186-5, announcement item 3). Anyone can verify; only the holder of the private key can sign.
FIPS 186-5 approves three families:
- ECDSA, the elliptic-curve algorithm used in the hands-on below (§6.4). Its curves come from SP 800-186. P-256 is one of them, rated at the 128-bit security strength and allowed for ECDSA (SP 800-186, §3.2.1.3). With SHA-256, also at 128 bits, it is a matched pair, which is what FIPS 186-5 recommends (FIPS 186-5, §6.1.1) (SP 800-57, §5.6.1.1).
- RSA, specified in PKCS #1 (RFC 8017). FIPS 186-5 requires a modulus of at least 2048 bits. SP 800-57 rates RSA-2048 at 112 bits and RSA-3072 at 128 bits of security.
- EdDSA. Unlike the other two, pure EdDSA does not pre-hash the message; the rest of this article describes the hash-then-sign flow used by RSA, ECDSA and HashEdDSA.
DSA, the older algorithm, is no longer approved for generating signatures, only for verifying old ones (FIPS 186-5, §4). And a signature key pair “shall not be used for any other purpose”, such as key establishment (§3).
Signing and verifying
Here is the whole flow, as FIPS 186-5 describes it for hash-then-sign schemes (§3, §3.2, §3.3).
Signing. The signer hashes the document, then runs the signature algorithm on the digest with the private key. The result is the signature. The signer may verify its own signature immediately, as a check against computation errors (§3.2).
Sending. The document and the signature both go to the verifier. The signature does not hide the document; anyone with the file can still read it.
Verifying. The verifier hashes the received document again with the same hash function, “not on the received digital signature”. It then runs the verification algorithm on that digest, the signature and the claimed signer’s public key. The output is accept or reject (§3.3).
For ECDSA, the steps inside “sign” and “verify” look like this. You do not need to follow the algebra; the shape is what matters.
Conceptual pseudocode
# ECDSA over curve P-256 with SHA-256 (FIPS 186-5, §6.4.1 and §6.4.2, simplified)
# G = the curve's base point, n = its order, d = private key, Q = [d]G = public key
sign(M, d):
e = integer(SHA256(M))
k = fresh secret random number in [1, n-1] # new for every signature
r = x([k]G) mod n
s = k^-1 * (e + r*d) mod n
destroy k
return (r, s) # the signature is two integers
verify(M, (r, s), Q):
reject unless 1 <= r, s <= n-1
e = integer(SHA256(M)) # the verifier hashes M itself
u = e * s^-1 mod n
v = r * s^-1 mod n
R1 = [u]G + [v]Q
accept if r == x(R1) mod n, else reject
Two details follow from this. The number k must be new for every signature and protected like the private key; a weak k can reveal the private key. Deterministic ECDSA (RFC 6979) derives k from the message and the key to avoid depending on a good random source (§6.3). And because k is random, signing the same file twice gives two different signatures that both verify (each run also uses a new key). You will see that if you run the examples below more than once.
Article 6 shows where this signature sits inside a PDF.
Mariama signs, Ibrahima checks
Back to the loan. The contract is one line of text:
Contrat de pret 2026-001 : montant 5000000 GNF
- Mariama generates a P-256 key pair. She keeps the private key and gives Ibrahima the public key.
- She hashes the contract with SHA-256 and signs the digest with her private key. She sends the contract and the signature.
- Ibrahima hashes the contract he received, and verifies the signature against that digest with Mariama’s public key. The result is “Verified OK”.
- Someone edits the amount on the way:
5000000becomes6000000. One byte differs. - Ibrahima runs the same check. The digest is different, so the verification fails.
What the failure tells Ibrahima is narrow: this signature does not verify for these bytes with this key. It does not tell him which byte changed, or whether the file or the signature was the thing altered (FIPS 186-5, §3.3). His only safe move is to reject the file and ask for a fresh copy.
Try it: sign, verify, tamper
Everything below runs in throwaway containers with keys generated on the spot. Never use these commands with a real key.
With OpenSSL
Executable educational example · Docker · alpine:3.22 · OpenSSL 3.5.8 25 Aug 2026
docker run --rm -w /w alpine:3.22 sh -c '
apk add --no-cache openssl >/dev/null && openssl version &&
printf "Contrat de pret 2026-001 : montant 5000000 GNF\n" > contrat.txt &&
openssl dgst -sha256 contrat.txt &&
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out mariama.key &&
openssl pkey -in mariama.key -pubout -out mariama.pub &&
openssl dgst -sha256 -sign mariama.key -out contrat.sig contrat.txt &&
openssl dgst -sha256 -verify mariama.pub -signature contrat.sig contrat.txt;
sed -i "s/5000000/6000000/" contrat.txt && openssl dgst -sha256 contrat.txt;
openssl dgst -sha256 -verify mariama.pub -signature contrat.sig contrat.txt; echo "exit=$?"'
The script prints the OpenSSL version, hashes the contract, generates Mariama’s key pair (mariama.key stays private, mariama.pub is shared), signs, and verifies. Then sed changes the amount, and it hashes and verifies again. If Docker has to download the image first, its progress lines appear before this output.
Expected output
OpenSSL 3.5.8 25 Aug 2026 (Library: OpenSSL 3.5.8 25 Aug 2026)
SHA2-256(contrat.txt)= 0c53f84a6928871a965328cc983d0a20d07f16dd69a3b273847aa2a9f54cff4c
Verified OK
SHA2-256(contrat.txt)= 4f4190dbac57acddb0cd0d887a89bb70753c88c56e153c4fe043178492db1c4f
20ED09B8FFFF0000:error:030000EA:digital envelope routines:EVP_DigestVerifyFinal:provider signature failure:crypto/evp/m_sigver.c:703:ECDSA digest_verify_final:
Verification failure
exit=1The two digests are the ones in the diagram above. The digest of a given file is the same on every run and on every machine. The signature file, contrat.sig, will differ each time, because of the random k. The error line before “Verification failure” is OpenSSL’s internal diagnostic. The part to check is the last two lines: the check failed, and the command exited with status 1, which a script can test. alpine:3.22 tracks the latest 3.22.x build, so the OpenSSL version line and the error-line prefix (20ED09B8FFFF0000) may differ from run to run and between machines: the prefix changes on every run, and the OpenSSL version depends on the Alpine package repository at install time.
With Python
Executable educational example · Docker · python:3.12-alpine · cryptography==50.0.1
Save the script below as sign_verify.py, then run:
docker run --rm --mount type=bind,src="$PWD/sign_verify.py",dst=/w/sign_verify.py,readonly -w /w python:3.12-alpine sh -c '
pip install cryptography==50.0.1 >/dev/null 2>&1 &&
python3 sign_verify.py'
import cryptography
print("cryptography", cryptography.__version__)
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
original = b"Contrat de pret 2026-001 : montant 5000000 GNF\n"
tampered = b"Contrat de pret 2026-001 : montant 6000000 GNF\n"
import hashlib
print("SHA2-256(original)=", hashlib.sha256(original).hexdigest())
print("SHA2-256(tampered)=", hashlib.sha256(tampered).hexdigest())
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
signature = private_key.sign(original, ec.ECDSA(hashes.SHA256()))
print("signature bytes (hex):", signature.hex())
# Verify against the original bytes: should succeed silently
public_key.verify(signature, original, ec.ECDSA(hashes.SHA256()))
print("Verified OK")
# Verify against tampered bytes: should raise InvalidSignature
try:
public_key.verify(signature, tampered, ec.ECDSA(hashes.SHA256()))
print("UNEXPECTED: verification succeeded on tampered bytes")
except InvalidSignature as e:
print("InvalidSignature raised as expected:", repr(e))
SECP256R1 is the library’s name for P-256. verify returns nothing on success and raises InvalidSignature on failure, so a forgotten try cannot silently accept a bad signature.
Expected output
cryptography 50.0.1
SHA2-256(original)= 0c53f84a6928871a965328cc983d0a20d07f16dd69a3b273847aa2a9f54cff4c
SHA2-256(tampered)= 4f4190dbac57acddb0cd0d887a89bb70753c88c56e153c4fe043178492db1c4f
signature bytes (hex): 30450220583e9cf400b0c965161bf157b15df4fb4dbdcccf754b44c6e498c6245b700cd2022100c2d2c256856560cc6e8404f772d34e3173e0fc3d5b43854e5a7b4b4791fc93a1
Verified OK
InvalidSignature raised as expected: InvalidSignature()The digests match the OpenSSL run exactly: same bytes in, same digest out. The signature bytes will be different on your machine. They encode the two integers r and s.
Where SEDEYA fits
The same hash-then-sign mechanism sits underneath a SEDEYA signature. SEDEYA signs PDFs in the PAdES format, up to the B-LTA level, with RFC 3161 timestamps and the validation data embedded in the file. The signing keys are held in a hardware security module (PKCS#11) or a KMS. And Ibrahima does not need OpenSSL to check a document: he can upload the PDF to SEDEYA’s public verification page, or scan the QR code printed on it. Later articles in this series explain each of those pieces.
Risks and misconceptions
“Signing means encrypting the hash with the private key.”
ECDSA has no encryption step at all: signing is the computation shown above. For RSA the arithmetic of signing and decryption is the same exponentiation, but RFC 8017 keeps them as separate primitives “intended for different purposes” (§5.2), and real RSA signatures add an encoding step (PSS or PKCS #1 v1.5). Say “sign with the private key”.
“A valid signature proves who signed.”
It proves that whoever held that private key signed those exact bytes. FIPS 186-5 is blunt about the gap: without assurance that the signer owns the key pair, “the problem of forging a signature is reduced to the problem of falsely claiming an identity”. Anyone can generate a key pair and claim to be someone else. That assurance “can only be provided if the owner’s identity and public key are bound together, such as in a certificate issued from a public key infrastructure” (§3). FIPS 186-5 also separates verifying a signature from validating it: before trusting a verified signature, the verifier needs assurance of the signer’s identity, the public key’s validity, and that the signer held the private key when signing (§3.3). Certificates are the subject of Article 2.
“A company-wide signing key shows which customer approved.”
FIPS 186-5 says the key pair owner “is the only entity that is authorized to use the private key” (§3). It follows that a signature made with one organisation-wide key shows that the organisation’s key signed. On its own, the signature cannot show which customer or employee caused it. The same logic applies when a trusted party generates a key for someone: that party “needs to be trusted not to masquerade as the owner, even though the trusted party knows the private key” (§3.1).
“A failed check shows what was changed.”
“If the verification process fails, no inference can be made as to whether the data is correct” (FIPS 186-5, §3.3). You learn only that this signature does not verify for these bytes with this key.
“One changed byte gives a completely random digest.”
FIPS 180-4 promises only that any change will “with a very high probability, result in a different message digest”. The two digests in the run above look unrelated, and that is an observation about this run, not a property you can quote from the standard.
“A hash is encrypted data you can decrypt.”
A hash function uses no key and is one-way (FIPS 180-4, §1). There is nothing to decrypt.
“PKCS #1 v1.5 signatures are broken.”
RFC 8017 says “no attacks are known against RSASSA-PKCS1-v1_5”, and requires RSASSA-PSS in new applications “in the interest of increased robustness” (§8). Do not confuse this with the separate v1.5 encryption padding.
“When a key expires, its old signatures stop being valid.”
A key’s cryptoperiod is “the time span during which a specific key is authorized for use”. SP 800-57 suggests one to three years for a private signing key, which must then be destroyed, but “several years” for the public key that verifies its signatures (§5.3.6). The public key can keep verifying after the private key has stopped signing. Reliability does weaken with time, and a cryptographic timestamp lets a verifier accept signatures shown to have been made while the private key was still in use (§5.3.6). Article 7 covers timestamps.
Any electronic signature does not equal a handwritten one
Under Guinean law, art. 34 uses handwritten-signature wording twice, with different conditions: a secure signature linked to a qualified certificate (al. 5), and a reliable, secure device under the signatory’s exclusive control with a « certificat numérique » (al. 1). How the two relate is open (L/2016/035/AN, art. 34). Under eIDAS, only a qualified electronic signature has “the equivalent legal effect of a handwritten signature” (art. 25(2)). A correct digital signature does not by itself meet either set of conditions.
One more limit to keep in view: the security-strength estimates in SP 800-57 “will be significantly affected when quantum computing becomes a practical consideration” (§5.6.1.1).
Summary
- An electronic signature is a legal notion; Guinean law defines it by a reliable identification process linked to the document. A digital signature is a cryptographic mechanism and one way to produce it.
- SHA-256 reduces any document to a 256-bit digest. Any change gives, with very high probability, a different digest.
- The private key signs the digest; the public key verifies it. The verifier always hashes the document itself.
- A one-byte edit made verification fail in both OpenSSL and Python.
- A valid signature proves that a key signed those bytes. Linking the key to a person needs something more, such as a certificate.
- A failed check says only “does not verify”, not what changed.
Check your understanding
Check your understanding
Ibrahima receives a contract and a signature. Why does he hash the contract himself instead of using a digest the sender provides?
Show the answer
Because the signature only protects the bytes he actually holds if he computes their digest himself. FIPS 186-5 has the verifier hash the received data again with the same hash function. A digest supplied by the sender could describe a different file.
Check your understanding
The signature verifies with a public key that came attached to an email claiming to be from Mariama. What has Ibrahima proved?
Show the answer
Only that whoever holds the matching private key signed these bytes. Nothing yet ties that key to Mariama; anyone can generate a key pair and claim a name. He needs a trusted binding between her identity and the public key, such as a certificate. That is Article 2.
Check your understanding
You run the OpenSSL example twice. The digests are identical but contrat.sig differs. Is something broken?
Show the answer
No. SHA-256 is deterministic, so the same bytes give the same digest. ECDSA uses a fresh secret
random number k for each signature (and the script also generates a new key each run), so the
signature bytes change every time. Every one of them verifies.
Next step
A valid signature proves which key signed, not who holds it. Article 2 covers the certificates and PKI that bind a public key to a person. Want to see these checks run on a real signed PDF? Talk to us and we will show you how SEDEYA signs a document and how anyone can verify it.
References
- Secure Hash Standard (SHS), FIPS 180-4, NIST, August 2015 (Explanation (item 3); §1)
- Digital Signature Standard (DSS), FIPS 186-5, NIST, 3 February 2023 (§2.1, §3, §3.1–3.3, §4, §5.1, §5.4, §6.1.1, §6.2–6.4)
- Recommendations for Discrete Logarithm-based Cryptography: Elliptic Curve Domain Parameters, SP 800-186, NIST, February 2023 (§3.2.1.3; Tables 1–2)
- Recommendation for Key Management: Part 1 – General, SP 800-57 Part 1 Rev. 5, NIST, May 2020 (§5.3, §5.3.4, §5.3.6, §5.6.1)
- PKCS #1: RSA Cryptography Specifications Version 2.2, RFC 8017, IETF, November 2016 (§5.2, §8)
- Loi L/2016/035/AN relative aux transactions électroniques, République de Guinée (Journal Officiel, hosted by ARPT), 28 July 2016 (Art. 1, art. 33, art. 34)
- Regulation (EU) No 910/2014 (eIDAS), consolidated text, Publications Office of the European Union, Consolidated 18 October 2024 (Art. 3(9)–(12), art. 25, art. 26)

