How to Test Java Cryptography APIs with Sample Keys and Certificates

Cryptography tests are easiest to trust when they use controlled keys, certificates and byte sequences. Java provides mature APIs through the Java Cryptography Architecture (JCA) and Java Cryptography Extension (JCE), but those APIs can still produce fragile tests when key generation, encoding, provider selection and certificate validation are left implicit.

A useful test suite checks both successful and rejected operations. It should prove that a signature verifies with the matching public key, that altered data fails verification, that an encrypted payload can be decrypted by the intended recipient, and that an expired or incorrectly issued certificate is refused. Sample material makes these cases repeatable without exposing production secrets.

Test subject Recommended sample material Main assertion
Message digest Fixed UTF-8 byte array The expected digest is produced
Digital signature Generated key pair or PEM fixture Valid data verifies; changed data fails
Symmetric encryption Fixed key and IV for test scope Decryption returns the original bytes
Certificate parsing PEM or DER certificate fixture Subject, issuer and validity are read correctly
Trust validation Small test certificate chain Trusted and untrusted paths are distinguished
Key storage Temporary PKCS#12 keystore Entries can be loaded with the right password

Build A Controlled Cryptography Test Fixture

Start by defining the cryptographic contract before writing test setup. Record the algorithm, transformation, key size, character encoding, certificate type and provider assumptions. For example, SHA256withRSA describes a signature algorithm, while RSA/ECB/OAEPWithSHA-256AndMGF1Padding describes an encryption transformation. These names are easy to confuse, and a test that silently uses a provider default can pass on one machine and fail in a deployment pipeline.

For application tests, a generated key pair created in @BeforeEach is often sufficient. For interoperability tests, store a deliberately created public and private key in src/test/resources. Fixed fixtures are valuable when your service must consume keys from another system, such as a payment gateway or an Australian government integration. Keep private fixtures test-only, clearly labelled, and excluded from packaged artefacts.

A small Java helper can make the setup explicit:

static KeyPair rsaKeyPair() throws GeneralSecurityException {
    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
    generator.initialize(2048);
    return generator.generateKeyPair();
}

static byte[] sign(PrivateKey key, byte[] message)
        throws GeneralSecurityException {
    Signature signature = Signature.getInstance("SHA256withRSA");
    signature.initSign(key);
    signature.update(message);
    return signature.sign();
}

Tests should avoid relying on the default platform charset. Use StandardCharsets.UTF_8, and compare byte arrays with an assertion designed for bytes rather than converting binary data into an accidental string. Also distinguish a Java Key object from its encoded form: getEncoded() may return PKCS#8 for a private key, X.509 SubjectPublicKeyInfo for a public key, or null for certain provider-backed keys.

Generate Sample Keys Without Making Tests Flaky

Randomness is central to cryptography, yet uncontrolled randomness makes diagnosis harder. For ordinary unit tests, KeyPairGenerator with a secure random source is appropriate because the test only needs a valid key. The test should assert behaviour, not a particular modulus or encoded key. Generating fresh material also reduces the risk that one stale fixture hides a defect.

Some tests need repeatable random output, especially when checking a known encoding or a protocol vector. In that case, pass a test-controlled SecureRandom only where the API supports it, or use published vectors with fixed inputs and expected outputs. Do not replace randomness with new Random() in production code or pretend that deterministic bytes represent secure key generation. A deterministic source belongs in a tightly scoped test seam.

Negative cases deserve the same attention as the happy path. Verify that a signature made with one private key does not validate with a different public key. Try an empty message, a one-byte change, a truncated signature and a signature with an extra byte. For RSA, include a test that rejects a key below the minimum size your policy permits. For elliptic-curve keys, ensure the named curve is supported by every runtime used in delivery.

Mocking a cryptographic provider usually gives weak confidence. It can help test an error path, such as a missing algorithm, but it cannot prove that key encodings or padding rules work. A practical approach is to use real JCA implementations for unit and integration tests, then add a small compatibility matrix for the providers you actually deploy. This matters when a Java upgrade changes provider behaviour in a build running on a developer laptop in Brisbane and in a Linux runner elsewhere.

Create And Validate Certificates In Tests

A certificate test needs more than a subject name. Important fields include the issuer, serial number, validity interval, public-key algorithm, key usage, extended key usage and signature algorithm. Parse PEM input by removing its armour and Base64-decoding the content, or use CertificateFactory directly with an input stream containing the PEM representation.

CertificateFactory factory = CertificateFactory.getInstance("X.509");
try (InputStream input = Files.newInputStream(path)) {
    X509Certificate certificate =
            (X509Certificate) factory.generateCertificate(input);

    certificate.checkValidity();
    certificate.verify(issuerCertificate.getPublicKey());
}

checkValidity() checks the certificate against the current clock, which can make tests fail near a boundary. Prefer certificates whose validity comfortably surrounds the test date, or use a validation API that accepts an explicit validation time. A certificate that expires at 10:00 am can produce an unpleasant intermittent failure when a CI agent runs close to that instant. Avoid scattering clock manipulation across tests; inject a Clock into application code and keep certificate-library calls isolated.

For certificate creation, many projects use Bouncy Castle’s certificate builder in test scope because the standard JDK does not offer a convenient general-purpose certificate issuer API. Generate a root certificate, an intermediate certificate and a leaf certificate with explicit basic constraints and key usage. Then test the chain with CertPathValidator and a TrustAnchor. Include a second chain signed by an unknown root, a leaf with the wrong hostname, and an expired certificate.

Hostname verification is separate from signature verification. A certificate can be correctly signed and still be unsuitable for api.example.com because its Subject Alternative Name contains another DNS name. Test the actual hostname-verification component used by the client rather than asserting only that certificate.verify() succeeds. This distinction is significant for services exposed through Australian cloud regions, where an endpoint may change behind a load balancer while its trust chain remains valid.

Exercise Encryption, Signatures And Key Stores

For digital signatures, test the complete byte-level flow: create the exact message bytes, sign them with the private key, verify with the public key, and confirm that modifications fail. Add cases for a wrong algorithm, a wrong key type and malformed Base64 input. An assertion should also distinguish an invalid signature from an operational failure such as an unavailable provider, because those failures require different handling and logging.

For encryption, test confidentiality and compatibility separately. With authenticated encryption such as AES-GCM, assert that changing the ciphertext, nonce or authentication tag causes decryption to fail. Never reuse a nonce with the same key in application code. A test may use a fixed nonce to produce a known answer, but that fixture should be clearly marked as a test vector and never copied into runtime configuration.

Key stores deserve integration coverage because passwords, aliases and entry types are frequent sources of mistakes. Create a temporary PKCS#12 store, write a private-key entry with its certificate chain, close it, reload it, and assert that the alias and key type are correct. Test a missing alias, a wrong password and a certificate-only entry. Use JUnit’s temporary directory support so the file is removed after the test, including on a developer workstation in Perth or a shared CI agent in Sydney.

When comparing encrypted output, do not require two ciphertexts to be identical unless the test is specifically checking a deterministic vector. Secure encryption normally includes random nonces or padding, so two correct executions can produce different bytes. Decrypt both results and compare the plaintext, while separately checking nonce handling, authentication and key agreement.

Keep Cryptography Tests Portable And Honest

Provider and runtime differences should be visible in the test design. Specify algorithms by standard names, avoid unnecessary provider registration, and fail clearly when a required algorithm is unavailable. If the application must use Bouncy Castle or a hardware security module, register that provider in a controlled test fixture and include at least one test that proves the expected provider is being used.

Separate fast unit tests from slower security integration tests. Unit tests can cover signing, parsing and validation with in-memory objects. Integration tests can load real PEM files, build certificate chains, exercise a PKCS#12 store and make a client perform TLS hostname verification. This split keeps a Gradle or Maven build responsive while ensuring that packaging and deployment assumptions are tested before release.

Never place production private keys, customer certificates or copied secrets in a repository. A test certificate should contain synthetic names such as service.test, and its private key should be disposable. Add repository scanning and dependency checks to the build, especially if your team handles financial or health information under Australian privacy obligations. APRA-regulated organisations and public-sector suppliers commonly require stronger evidence of key handling than a green unit-test report alone.

The most valuable suite explains why each fixture exists. Keep a short comment beside unusual parameters, record certificate creation commands or test builders, and assert meaningful properties instead of implementation details. A reliable cryptography test uses disposable sample keys, explicit encodings, controlled time, real providers and deliberate failure cases. The key thing to remember is that cryptographic testing proves the behaviour of your integration around trusted primitives; it does not replace sound key management or the security review of the system using them.