Government of Andhra Pradesh / MobileSigner eFile platform
Assurance

Assurance

Security model

The trust chain, what is cryptographically real, and what is presentation-layer simulation.

The claim this platform makes is narrow and worth stating exactly: a hardware-bound private key authorised a specific document digest, and that binding is on record. Everything else on the verification screen is presentation.

Being precise about the boundary is not a caveat. A signing platform that overstates its assurance is worse than one that understates it, because the overstatement is what a court or an auditor would test.

Indian law distinguishes two things that both get called a digital signature.

Section 3 of the IT Act 2000 covers a digital signature: an asymmetric key pair with a hash function, where the private key is held by the subscriber and a licensed Certifying Authority binds the public key to an identity in an X.509 certificate. This is what a Class-3 DSC on a USB crypto token provides, and what the CCA licenses eMudhra, NIC-CA, Sify and CDAC to issue.

Section 3A covers an electronic signature: any authentication technique the Central Government notifies as reliable, judged on whether the signature creation data is under the signatory's exclusive control and whether any alteration is detectable.

MobileSigner as built is a §3A-shaped system. The private key is in a FIDO2 authenticator and is genuinely under exclusive control — arguably more so than a PKCS#11 token, which can be handed across a desk with its PIN. What it lacks is the §3 element: a CA-issued certificate chaining to the CCA Root, and a PKCS#7 structure carrying it.

That distinction is the honest position. The platform solves the exclusive-control problem well and does not solve the certificate problem at all.

What is cryptographically real

Everything in this list is genuine, verifiable, and not simulated.

The WebAuthn ceremonies. Registration and authentication run through the Quarkus WebAuthn extension against navigator.credentials. Challenges are server-generated, attestation objects are parsed by the extension, and assertions are verified against the stored public key. The SDK delegates to new window.WebAuthn() rather than reimplementing any of it, so the client always matches the relying party that issued the challenge.

The key pair. The private key is generated on the authenticator and never leaves it. WebAuthnCredential stores publicKey, publicKeyAlgorithm, counter and aaguid. There is no private key material anywhere in the database, in a config file, or in a log.

Origin and RP ID binding. A credential is bound to the relying party id it was created under, and browsers refuse to offer it under any other. This is what makes phishing structurally ineffective — a credential enrolled for webauthn-demo.localhost cannot be used by a look-alike domain, regardless of how convincing the page is. It is also why the same credential does not work across environments, which reads as a bug until you understand what it prevents.

Signature counter checking. DemoWebAuthnUserProvider.update() persists the incremented counter after each assertion, and the extension rejects an assertion whose counter has not advanced. That is the standard cloned-authenticator detection, and the seeded audit trail carries a matching FAILURE row so the control is visible.

The document digest. sha256() uses crypto.subtle.digest('SHA-256', …) in the browser. The copy into a fresh ArrayBuffer before hashing is deliberate — crypto.subtle hashes an entire buffer rather than a view onto it, so hashing a subarray directly would digest the wrong bytes.

The digest-to-identity binding. FileResource.act() refuses to record an approval unless the caller has an authenticated identity and supplies 64 hex characters:

if (identity.isAnonymous()) { return 401; }
if (action.equals("APPROVE") && (documentHash == null || !SHA_256.matcher(documentHash).matches())) {
    return 400;
}

The Approval row then binds username, file number and digest with a timestamp. The chain from "an officer completed a WebAuthn assertion" to "this digest is on record against that officer" holds up.

The session cookie. Quarkus encrypts it, and cookie-same-site=strict prevents cross-site submission. The encryption is real; the key must be supplied from a Secret rather than left on the development default, because the backend authorises from this cookie rather than by re-running an assertion. Operations covers it.

What is presentation

VerifyResource does not do cryptography. It does a table lookup and then constructs a plausible-looking report.

var approval = Approval.<Approval>find("documentHash", hash).firstResult();
var valid = approval != null;

That single boolean drives all six check results. When it is true, all six report PASS with text naming real infrastructure:

  • Chain validated: signer → eMudhra Sub CA for Government → CCA India 2022 (RCAI).
  • OCSP responder ocsp.emudhra.com returned status good.
  • Timestamp token issued by tsa.nic.in binds the signature to …

No certificate is parsed. No OCSP request is made. No RFC 3161 token exists. eMudhra and tsa.nic.in are real, which makes the output more convincing than it should be.

The signer certificate is manufactured from the hash:

new SignerInfo(SIGNER_SUBJECT, SIGNER_ISSUER,
        hash.substring(0, 16).toUpperCase(Locale.ROOT), "Class 3",
        now.minus(Duration.ofDays(200)), now.plus(Duration.ofDays(530)),
        "digitalSignature, nonRepudiation")

Subject and issuer are compile-time constants, so every signature verifies as CN=Demo Officer regardless of who signed. The serial number is the first 16 hex characters of the digest. The validity window is relative to now, so it can never appear expired.

signatureFormat reports PKCS#7 detached (CCA-SP). No PKCS#7 structure is created, stored or read anywhere in either repository.

The value of the screen is that it shows the shape a real verification report takes, in the right order, with the right vocabulary. Read it as a specification for the integration described below, not as evidence about a certificate.

The seeded digests compound this: they are literals, not sha256() of the seeded document bodies, so hashing ITE&C/SEC/2026/0198's body will not reproduce 4f2a9c1e…. Only digests produced by an actual signing action satisfy sha256(body) == documentHash.

Crossing the boundary

Making verification real needs three things, in dependency order.

1. A CA-issued certificate per officer. Enrolment currently ends with a public key and no certificate. It would need to submit a CSR to a CCA-licensed CA and store the returned X.509. This is procurement and identity proofing, not code.

2. A CMS structure over the WebAuthn assertion. A WebAuthn assertion is not a PKCS#7 signature — it signs authenticatorData || clientDataHash, not the document digest directly. The document digest travels inside clientDataJSON as the challenge. So a real integration either uses the assertion as evidence within a CAdES structure, or has the authenticator perform a separate signing operation over the digest. The second requires a signing-capable applet, which is what a Class-3 token has and a standard FIDO2 key does not.

This is the substantive architectural gap. FIDO2 authenticators are built for authentication, not document signing. Bridging it means either a hardware token that does both, or a server-side HSM signing on the strength of a WebAuthn-authenticated authorisation — which relocates exclusive control from the officer to the server and weakens the §3A argument.

3. Real chain validation. Path building to the CCA Root, OCSP or CRL checking, and RFC 3161 timestamping against tsa.nic.in. Once a certificate exists this is well-trodden — BouncyCastle and the Java PKI APIs do it.

Steps 2 and 3 are where the work is. Step 1 gates both.

Trust boundaries

Boundary Enforcement
Authenticator → browser CTAP2; private key never crosses
Browser → Next.js server none; the Next server proxies and holds no secrets
Next.js → Quarkus none; plain HTTP inside the cluster
Quarkus request → identity SecurityIdentity from the encrypted session cookie
Write endpoints 401 if anonymous
Read endpoints @RolesAllowed("officer"); only /api/session and /api/verify are public

The authorisation model is one role for everyone:

@Override
public Set<String> getRoles(String username) { return Set.of("user"); }

No endpoint declares @RolesAllowed. Every designation in the platform is a display string — FileResource records DESIGNATION = "Approving Authority" for every action any user takes. Any enrolled user can therefore approve any file, and the noting will carry that designation.

UserAccount is created implicitly on first enrolment, so enrolment is self-service for anyone who can reach the endpoint. A real deployment would pre-provision accounts and reject unknown usernames.

Combined, these three mean the platform authenticates strongly and authorises not at all. The cryptography is sound and the access control is absent — which is the wrong way round to be wrong.

The device inventory gap

SigningDevice and WebAuthnCredential are separate tables with no relationship. The device screen shows serial numbers, IC types, CAs, certificates and a REVOKED state; the credential table is what actually authorises signing.

Marking a device revoked would not stop its credential producing valid assertions, because nothing consults the device table during a ceremony — and there is no write endpoint to revoke one. Wiring revocation means a foreign key from SigningDevice to WebAuthnCredential and a check during authentication.

Revocation is the control an inventory exists to provide, and it is the one thing this inventory cannot do.

Single credential per officer

store() deletes every existing credential for a username before persisting the new one, which keeps the identity-to-key mapping unambiguous. The consequence is that an officer cannot register a backup key — enrolling a second replaces the first.

For a §3A argument that rests on exclusive control this is defensible. Operationally it inverts an availability requirement: a lost key means no signing until re-enrolment, and re-enrolment has no identity proofing to gate it. The correct shape is multiple credentials per officer with a unique constraint and an explicit device-management flow.

Audit integrity

The audit trail is append-only, and the database enforces it.

AuditEvent extends PanacheEntity, so delete() and update() are available like on any entity. There is no database role separation, no trigger, no WORM storage and no hash chaining — each row's correlationId is an independent random UUID, so rows are not linked and a deletion would leave no gap to detect.

Note that audit-view.tsx closes with a footnote describing role-level UPDATE and DELETE revocation, a blocking trigger and seven-year retention. That describes the target state, not the current one.

Two write-path issues follow from the same design:

  • POST /api/verify is unauthenticated and appends a row per call, so the ledger grows with verification traffic and not only with approvals.
  • Enrolments and logins emit nothing. AUTH_SUCCESS and CREDENTIAL_REGISTERED exist in the seed data to show the shape; the ceremony endpoints belong to the Quarkus extension and do not call AuditEvent.record(). Adding those calls in DemoWebAuthnUserProvider is a few lines and is what an auditor would ask for first.

Making the ledger real means: a distinct database role with INSERT only, a hash chain linking each row to its predecessor, and emitting the authentication events. The first two are configuration and a migration; the third is four lines in the WebAuthn provider.

Threats this design does handle

Worth crediting, because they are the reason the approach is right even with the gaps above.

Threat Why it fails
Phishing credentials are origin-bound; a look-alike domain gets nothing
Credential replay challenges are single-use and server-generated
Cloned authenticator signature counter regression is detected and rejected
Password reuse and stuffing there are no passwords
Server-side key theft no private keys are stored
Token sharing the authenticator requires user presence per operation, so it cannot be left with staff the way a PIN-protected token can

That last row is the one that matters most in practice. The failure mode this platform actually fixes is not a cryptographic weakness in Class-3 DSC — it is an officer handing the token and PIN to a section clerk so files keep moving, which destroys the non-repudiation the token exists to provide. A key requiring a touch per signature cannot be delegated that way.

Where to strengthen it first

In dependency order, and independent of the certificate work above:

  1. Supply the session encryption key from a Secret. The cookie is what authorises every request after the assertion, so it carries the weight of the whole chain.
  2. Close enrolment. mobilesigner.enrolment.open is true in this build so the demo can be driven with any username, and mobilesigner.enrolment.default-role grants approver. A real deployment sets the first to false and provisions accounts from the departmental directory, because self-service enrolment plus a write-capable role is exactly how a clerk ends up able to approve a tender.
  3. Emit the authentication events, so enrolment, login and rejected assertions appear in the ledger alongside the file decisions.
  4. Make the ledger insert-only with a distinct database role, and hash-chain each row to its predecessor. That is what turns the audit footnote's description into a fact.
  5. Label the verification report as a simulated chain until the certificate integration exists, so the screen is read as a specification rather than as evidence.