Introduction
Non-repudiation is the cryptographic property that lets a third party (such as a judge or auditor) prove that a specific person authored a specific message. The concept was formalised in ISO 7498-2 in 1989 alongside confidentiality, integrity, and authentication as one of the four classical security properties. In 2000 the United States passed the Electronic Signatures in Global and National Commerce Act (ESIGN) and the EU followed with the Electronic Signatures Directive 1999/93/EC, both of which make a digitally signed electronic message as legally binding as a handwritten signature on paper. A Java learner should care because non-repudiation is the single property that distinguishes a real payment system from a toy one, and because it is the entire reason asymmetric cryptography was invented.
This tutorial simulates a court scenario. Alice signs a payment order with her RSA private key. Bob, the recipient, presents the message and signature to a Judge who has only Alice's public certificate. The Judge verifies the signature, confirms the certificate chain, and rules that Alice authored the message. Alice then tries to deny the transaction by claiming her private key was stolen; the Judge dismisses the claim because possession of the private key is, by legal precedent, the holder's responsibility. The program also shows what happens when Mallory tries to forge a signature using only the public key: it fails catastrophically.
What is Non Repudiation?
Non-repudiation is the property that prevents the signer of a message from later denying having signed it. It requires three technical ingredients. First, the signature algorithm must be such that only the holder of the private key can produce a valid signature, which excludes MACs (where the verifier can also sign). Second, the public key must be bound to a real-world identity through an X.509 certificate chain that terminates at a trusted root, so that the verifier can prove "this signature was made by the private key corresponding to this certificate, and this certificate belongs to this person". Third, the signature must include an RFC 3161 trusted timestamp from a Time Stamping Authority, so the signer cannot later claim "I lost my key before that signature was made".
Non-repudiation is sometimes confused with authentication. Authentication answers "who sent this message to me right now"; non-repudiation answers "who can I prove to a third party wrote this message at a particular time". A TLS connection authenticates the server to the client, but the client cannot later prove to anyone that the server sent a specific record because the MAC key was shared and the client could have computed the MAC themselves. A signed email, by contrast, lets the recipient forward the message and signature to a court that can independently verify it.
Two non-repudiation levels are recognised by EU law: "advanced electronic signature" requires that the signature be uniquely linked to the signer and capable of identifying them; "qualified electronic signature" requires additionally that the signing key be stored on a tamper-evident hardware module and that the certificate be issued by a qualified trust service provider. Only qualified signatures get automatic legal equivalence to handwritten signatures under eIDAS Regulation 910/2014.
How Non Repudiation Works - Step by Step
Step 1: Key generation. Alice generates an RSA 2048-bit keypair (or, in production, applies for a qualified certificate from a CA such as DigiCert or DocuSign). Alice's private key never leaves her machine (or her HSM). The CA signs Alice's public key together with Alice's distinguished name to produce an X.509 certificate that binds the public key to Alice's identity.
Step 2: Signing. Alice hashes her payment order with SHA-256, applies EMSA-PSS encoding, raises the result to her private exponent d modulo n, and obtains the 256-byte signature. She attaches the signature and her certificate to the message and sends the bundle to Bob.
Step 3: Receipt. Bob verifies the signature using Alice's public key extracted from the certificate. Bob validates the certificate chain back to a trusted root. Bob stores the message, signature, and certificate as evidence.
Step 4: Dispute. Alice later claims she never authorised the payment. Bob produces the message, the signature, and the certificate to the Judge. The Judge repeats step 3: he extracts Alice's public key from the certificate, validates the certificate chain, re-runs the signature verification. Because only Alice's private key could have produced a signature that verifies under Alice's public key, and because Alice is responsible for protecting her private key, the Judge rules against Alice.
Step 5: Forgery attempt. Mallory, who is unrelated to Alice, knows only Alice's public key. Mallory tries to forge a signature on a fake message. Without the private exponent d, the only attack is to invert the RSA function, which is equivalent to factoring n. For a 2048-bit modulus this is computationally infeasible with current hardware (the largest publicly factored RSA modulus is 829 bits, achieved in 2020 with about 2,700 CPU-core-years of computation).
Java Program Demonstrating Non Repudiation
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.util.Date;
import sun.security.x509.X509CertImpl;
import sun.security.x509.X509CertInfo;
import sun.security.x509.CertificateValidity;
import sun.security.x509.CertificateSerialNumber;
import sun.security.x509.CertificateSubjectName;
import sun.security.x509.CertificateIssuerName;
import sun.security.x509.CertificateAlgorithmId;
import sun.security.x509.CertificateX509Key;
import sun.security.x509.AlgorithmId;
import javax.security.auth.x500.X500Principal;
public class NonRepudiationDemo {
public static void main(String[] args) throws Exception {
// ---- Alice generates a self-signed certificate for demo ----
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair aliceKp = kpg.generateKeyPair();
X500Principal subject = new X500Principal(
"CN=Alice Kumar, OU=Finance, O=IMCSEIAN, C=IN");
X509Certificate aliceCert = selfSignedCert(
aliceKp, subject, "SHA256withRSA", 365);
// Display the certificate so we know whose key we are dealing with.
System.out.println("==> Alice certificate");
System.out.println(" Subject: " + aliceCert.getSubjectX500Principal());
System.out.println(" Issuer: " + aliceCert.getIssuerX500Principal());
System.out.println(" Serial: " + aliceCert.getSerialNumber());
System.out.println(" Sig alg: " + aliceCert.getSigAlgName());
// ---- Alice signs a payment order -------------------------
byte[] paymentOrder = "Pay 1000 INR to Bob on 2024-11-01"
.getBytes("UTF-8");
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(aliceKp.getPrivate());
signer.update(paymentOrder);
byte[] signature = signer.sign();
System.out.println("\n==> Alice signs the payment order");
System.out.println(" Message: " + new String(paymentOrder, "UTF-8"));
System.out.println(" Signature: " + toHex(signature).substring(0, 64) + "...");
// ---- Bob receives and stores the evidence -----------------
System.out.println("\n==> Bob stores evidence");
System.out.println(" Saved: message bytes, signature bytes, Alice certificate.");
// ---- The Judge verifies the evidence ---------------------
System.out.println("\n==> Judge independently verifies");
Signature verifier = Signature.getInstance("SHA256withRSA");
verifier.initVerify(aliceCert.getPublicKey());
verifier.update(paymentOrder);
boolean ok = verifier.verify(signature);
System.out.println(" Signature verifies under Alice's public key: " + ok);
// Re-encode the certificate to byte form and re-parse to prove
// the Judge sees the same byte stream Bob saw (tamper-evidence).
byte[] certBytes = aliceCert.getEncoded();
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate judgeCert = (X509Certificate)
cf.generateCertificate(new ByteArrayInputStream(certBytes));
verifier.initVerify(judgeCert.getPublicKey());
verifier.update(paymentOrder);
boolean ok2 = verifier.verify(signature);
System.out.println(" Judge re-parsed cert and re-verified: " + ok2);
// ---- Alice tries to repudiate ----------------------------
System.out.println("\n==> Alice tries to repudiate");
System.out.println(" Alice: "I never signed that payment order."");
System.out.println(" Judge: "Only your private key could have produced "
+ "a signature that verifies under this certificate."");
System.out.println(" Judge: "Your certificate binds this public key "
+ "to your identity, and the signature is valid."");
System.out.println(" Judge: ruling in favour of Bob. Alice is liable.");
// ---- Mallory tries to forge ------------------------------
System.out.println("\n==> Mallory tries to forge a signature on a fake order");
KeyPair malloryKp = kpg.generateKeyPair();
byte[] fakeOrder = "Pay 100000 INR to Mallory on 2024-11-01"
.getBytes("UTF-8");
Signature mallorySig = Signature.getInstance("SHA256withRSA");
mallorySig.initSign(malloryKp.getPrivate());
mallorySig.update(fakeOrder);
byte[] fakeSignature = mallorySig.sign();
verifier.initVerify(aliceCert.getPublicKey());
verifier.update(fakeOrder);
boolean bad = verifier.verify(fakeSignature);
System.out.println(" Mallory's signature under Alice's public key: " + bad);
System.out.println(" Forgery rejected: Mallory's private key does not "
+ "match Alice's public key.");
}
// Build a self-signed X.509 v3 certificate in pure Java using the
// sun.security.x509 helper classes bundled with every OpenJDK.
static X509Certificate selfSignedCert(KeyPair kp, X500Principal subject,
String sigAlg, int days) throws Exception {
X509CertInfo info = new X509CertInfo();
Date from = new Date();
Date to = new Date(from.getTime() + days * 86400000L);
CertificateValidity validity = new CertificateValidity(from, to);
info.set(X509CertInfo.VALIDITY, validity);
info.set(X509CertInfo.SERIAL_NUMBER,
new CertificateSerialNumber(BigInteger.valueOf(System.currentTimeMillis())));
info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(subject));
info.set(X509CertInfo.ISSUER, new CertificateIssuerName(subject));
info.set(X509CertInfo.KEY, new CertificateX509Key(kp.getPublic()));
info.set(X509CertInfo.ALGORITHM_ID,
new CertificateAlgorithmId(AlgorithmId.get(sigAlg)));
X509CertImpl cert = new X509CertImpl(info);
cert.sign(kp.getPrivate(), sigAlg);
return cert;
}
static String toHex(byte[] b) {
StringBuilder sb = new StringBuilder();
for (byte x : b) {
sb.append(String.format("%02x", x & 0xff));
}
return sb.toString();
}
}
Code Explanation - Line by Line
The program starts by generating Alice's RSA 2048-bit keypair through KeyPairGenerator. In production this would come from a PKCS12 keystore created by keytool and signed by a real CA, but for the demo we create a self-signed certificate through the sun.security.x509 helper classes that ship inside every OpenJDK. The selfSignedCert() helper builds an X509CertInfo with the validity window, serial number, subject, issuer (equal to subject for self-signed), public key, and signature algorithm, then signs the certificate with the private key.
The signing step uses the same SHA256withRSA Signature instance as the previous tutorials. The initSign() call accepts a PrivateKey, the update() call feeds the message bytes, and the sign() call returns the 256-byte RSA-PKCS1-v1_5 signature. Bob's receipt is conceptual in the demo: he stores the message bytes, signature bytes, and certificate bytes. In a production e-discovery system those would land in a tamper-evident log (such as the Merkle Tree tutorial elsewhere in this series).
The Judge's verification step is the heart of non-repudiation. The Judge starts from scratch: he re-parses the certificate bytes through CertificateFactory.getInstance("X.509") to prove the bytes are byte-for-byte identical to what Bob stored, he extracts the public key from the re-parsed certificate, and he re-runs the verification. The output proves that the verification is reproducible: any party with the public key can confirm the signature, and the public key is bound to Alice's identity through the certificate.
The forgery attempt by Mallory demonstrates the asymmetry that makes non-repudiation possible. Mallory generates her own RSA keypair and signs the fake order with her own private key, then presents it to the Judge as if it were Alice's signature. The Judge verifies it under Alice's public key (which he extracted from Alice's certificate) and gets false, because Mallory's private key does not correspond to Alice's public key. Without factoring Alice's RSA modulus, Mallory cannot produce a signature that verifies under Alice's public key.
Sample Input and Output
==> Alice certificate Subject: CN=Alice Kumar, OU=Finance, O=IMCSEIAN, C=IN Issuer: CN=Alice Kumar, OU=Finance, O=IMCSEIAN, C=IN Serial: 1730514123456 Sig alg: SHA256withRSA ==> Alice signs the payment order Message: Pay 1000 INR to Bob on 2024-11-01 Signature: 4a8c...f3b2... ==> Bob stores evidence Saved: message bytes, signature bytes, Alice certificate. ==> Judge independently verifies Signature verifies under Alice's public key: true Judge re-parsed cert and re-verified: true ==> Alice tries to repudiate Alice: "I never signed that payment order." Judge: "Only your private key could have produced a signature that verifies under this certificate." Judge: "Your certificate binds this public key to your identity, and the signature is valid." Judge: ruling in favour of Bob. Alice is liable. ==> Mallory tries to forge a signature on a fake order Mallory's signature under Alice's public key: false Forgery rejected: Mallory's private key does not match Alice's public key.
The output proves three things at once: the Judge can independently verify the signature using only Alice's public certificate; Alice's repudiation defence is dismissed because the certificate binds the public key to her identity; and Mallory cannot forge a signature without Alice's private key.
Common Mistakes to Avoid
First, do not skip the timestamp. Without an RFC 3161 trusted timestamp countersignature, Alice can repudiate by saying "I lost my private key on 1 October, before the alleged signature was made on 1 November". With a timestamp from a TSA, the Judge can prove the signature existed at a particular moment while Alice still controlled her key. Second, do not store signatures in a database without storing the certificate chain alongside: if the CA's root expires or is revoked, the signature becomes unverifiable. Third, do not assume self-signed certificates give non-repudiation; only a certificate signed by a CA trusted by the Judge binds the public key to the real-world identity. Finally, do not sign the digest instead of the message: signing SHA-256(message) without proper padding (such as raw RSA without EMSA-PSS) is vulnerable to the Bleichenbacher 1998 existential forgery attack, where an attacker can compute a valid-looking signature on a digest of their choice without the private key.
Real-World Applications
DocuSign, Adobe Sign, and the EU's qualified signature providers all use RSASSA-PSS or ECDSA on a tamper-evident HSM to produce legally binding electronic signatures on PDF contracts. The Belgian eID card contains a qualified signature keypair on a secure element, and every Belgian citizen can sign legally binding documents by inserting the card into a smart card reader and entering their PIN. Bitcoin transactions are signed with ECDSA over secp256k1 (or Schnorr after Taproot), and the blockchain itself serves as the tamper-evident log that proves the signature existed at a particular block height. SWIFT MT103 payment messages between banks include a digital signature under the sender bank's private key for non-repudiation between correspondent banks.
Spring Security's SAML 2.0 module verifies digitally signed SAML assertions from identity providers, and the SAML specification mandates XMLDSig XML signatures with X.509 certificates for non-repudiation between federated organisations. The European eIDAS regulation mandates that all qualified electronic signatures must be cross-border-recognised by every EU member state, which is only possible because they all use the same underlying RSA-PSS or ECDSA algorithms defined in FIPS 186.
Conclusion
We saw that non-repudiation is the asymmetric property that lets a third party prove authorship of a message, and that it requires three ingredients: an asymmetric signature algorithm, a certificate binding the public key to a real identity, and a trusted timestamp. Next, study the Digital Signature vs MAC tutorial to see exactly where MACs fall short of non-repudiation, then read the Forward Secrecy tutorial to learn how ephemeral keys can give non-repudiation over a single session without ever persisting a long-term signature.
Comments
Comments
Post a Comment