Task 1: Using a keystore

Alice’s private key is stored in a Java KeyStore (JKS) file. The server already knows the corresponding public key. Alice will prove possession of the private key by signing a fresh challenge.

For this warm-up, the store password is classroom-store and the key password is warmup-key-2026. JKS can use separate passwords for file integrity and private-key protection.

Inspect the keystore

$ keytool -list -storetype JKS -keystore data/artifacts/warmup.jks -storepass classroom-store

Find the alias alice and the PrivateKeyEntry. The associated certificate contains the public key; the private key itself is protected inside the file.

Explanation:

  • keytool -list displays entries without exporting their private material.
  • -storetype JKS tells Java to parse this legacy Java KeyStore format.
  • -keystore selects the lab file, while -storepass supplies its integrity password. It is not necessarily the password that protects Alice’s private key.

About the JKS migration warning. keytool warns that JKS is proprietary and recommend PKCS#12. Following our NDSS work, Mind Your Keys? A Security Evaluation of Java Keystores, this warning was introduced alongside the fixes and mitigations developed after responsible disclosure of weaknesses in Java keystore implementations. See also the open NDSS presentation slides. In this lab, we use JKS to demonstrate how quickly weak passwords can be brute-forced. Its legacy password protection has a fixed work factor that cannot be configured, which is one reason modern tools favor PKCS#12.

Sign a fresh challenge

This task uses a simple public-key challenge-response protocol. A Python helper generates a fresh challenge, and a Java program signs it using the user’s private key stored in the keystore.

$ python3 tools/labctl.py challenge warmup

$ LAB_KEY_PASSWORD=warmup-key-2026 java java/LabSigner.java data/artifacts/warmup.jks work/challenge.txt work/signature.b64

Explanation:

  • labctl.py challenge warmup asks the local server for a fresh, single-use challenge and writes it to work/challenge.txt.
  • LAB_KEY_PASSWORD=... sets the private-key password only for the Java program; it is not stored in the shell environment afterwards.
  • java java/LabSigner.java loads the private key from the JKS file data/artifacts/warmup.jks, signs the challenge work/challenge.txt, and store the Base64-encoded signature in work/signature.b64

Observe integrity of the signed message

$ openssl base64 -d -A -in work/signature.b64 -out work/signature.bin

$ openssl dgst -sha256 -verify data/artifacts/warmup-public.pem -signature work/signature.bin work/challenge.txt

The first command decodes the Base64 signature; the second verifies it with the public key. You should see Verified OK. Now verify the same signature against a different message:

$ (cat work/challenge.txt; echo -n 'x') > work/altered.txt

$ openssl dgst -sha256 -verify data/artifacts/warmup-public.pem -signature work/signature.bin work/altered.txt

The first command appends an x to the challenge data and saves the modified message as work/altered.txt. The subsequent signature check must fail because a signature is valid only for the exact message bytes that were signed.

Exercise 1

Practical exercise: test what the server accepts. Use the original work/challenge.txt and work/signature.b64 produced above; do not recreate them yet.

  1. Keep that original pair unchanged. First try two rejected submissions using separate, disposable challenges:
$ python3 tools/labctl.py challenge warmup --out work/other-challenge.txt

$ python3 tools/labctl.py submit --challenge work/other-challenge.txt --signature work/signature.b64

Explanation:

  • The first command requests a new challenge and saves the message in work/other-challenge.txt and its identifier in work/other-challenge.json.
  • The second command runs the submit action. --challenge selects the challenge file, from which labctl.py locates the adjacent .json record, while --signature selects the Base64 signature to submit. The tool sends the challenge identifier and signature to the server.

Here, the new challenge is deliberately combined with the signature created for the original challenge, so the server must reject it.

$ python3 tools/labctl.py challenge warmup --out work/bad-signature-challenge.txt

$ printf "AAAA" > work/bad-signature.b64

$ python3 tools/labctl.py submit --challenge work/bad-signature-challenge.txt --signature work/bad-signature.b64

Explanation: These commands request a fresh challenge, create a deliberately invalid Base64 signature, and submit the pair. The server must reject the invalid signature.

  1. Finally, request and sign a fresh challenge because the original challenge may have expired while you completed the experiments:
    $ python3 tools/labctl.py challenge warmup
    
    $ LAB_KEY_PASSWORD=warmup-key-2026 java java/LabSigner.java data/artifacts/warmup.jks work/challenge.txt work/signature.b64

    Submit the fresh pair, then immediately replay the same pair:

    $ python3 tools/labctl.py submit --challenge work/challenge.txt --signature work/signature.b64
    
    $ python3 tools/labctl.py submit --challenge work/challenge.txt --signature work/signature.b64

    The first submission returns the password for Task 2. The second is rejected because the server has already consumed the challenge. Record the Task 2 password before continuing.

    Explanation: Both commands submit the same challenge and signature. The first valid submission consumes the fresh challenge; the replay demonstrates that a valid signature cannot be reused.

Reference: Oracle: keytool.