PIVCardTest.java

/*
 * Copyright 2024 Emmanuel Bourg
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.jsign.jca;

import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Set;
import javax.crypto.Cipher;
import javax.smartcardio.CardException;

import org.junit.Test;

import net.jsign.DigestAlgorithm;

import static net.jsign.jca.PIVCard.Key.*;
import static org.junit.Assert.*;
import static org.junit.Assume.*;

public class PIVCardTest {

    public static void assumeCardPresent() throws Exception {
        try {
            assumeTrue("PIV card not found", SmartCard.getTerminal("Yubikey") != null);
        } catch (CardException e) {
            assumeNoException("PIV card not found", e);
        }
    }

    @Test
    public void testGetCard() throws Exception {
        assumeCardPresent();

        assertNotNull("card not found", PIVCard.getCard());
    }

    @Test
    public void testSignRSA() throws Exception {
        assumeCardPresent();

        PIVCard card = PIVCard.getCard();
        assertNotNull("card not found", card);
        card.verify("123456");

        PIVCard.Key key = SIGNATURE;
        int keyLength = card.getKeyInfo(key).size;

        byte[] message = "Hello PIV card".getBytes();
        byte[] result = card.sign(key, message);

        assertNotNull("result", result);

        assertEquals("result length (bits)", keyLength, result.length * 8);

        // decrypt the message with the public key
        PublicKey publicKey = card.getCertificate(key).getPublicKey();
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        cipher.update(result);
        byte[] decrypted = cipher.doFinal();

        assertArrayEquals("encrypted message", message, decrypted);
    }

    @Test
    public void testSignSHA1withECDSA() throws Exception {
        testSignECDSA(DigestAlgorithm.SHA1);
    }

    @Test
    public void testSignSHA256withECDSA() throws Exception {
        testSignECDSA(DigestAlgorithm.SHA256);
    }

    @Test
    public void testSignSHA384withECDSA() throws Exception {
        testSignECDSA(DigestAlgorithm.SHA384);
    }

    @Test
    public void testSignSHA512withECDSA() throws Exception {
        testSignECDSA(DigestAlgorithm.SHA512);
    }

    public void testSignECDSA(DigestAlgorithm digestAlgorithm) throws Exception {
        assumeCardPresent();

        PIVCard card = PIVCard.getCard();
        assertNotNull("card not found", card);
        card.verify("123456");

        PIVCard.Key key = CARD_AUTHENTICATION;
        int keyLength = card.getKeyInfo(key).size;

        byte[] message = "Hello PIV card".getBytes();

        byte[] digest = digestAlgorithm.getMessageDigest().digest(message);
        if (digest.length > keyLength / 8) {
            digest = Arrays.copyOf(digest, keyLength / 8);
        }

        byte[] result = card.sign(key, digest);

        assertNotNull("result", result);

        // verify the signature
        Signature signature = Signature.getInstance(digestAlgorithm.name() + "withECDSA");
        signature.initVerify(card.getCertificate(key));
        signature.update(message);
        assertTrue(signature.getAlgorithm() + " signature verification failed", signature.verify(result));
    }

    @Test
    public void testGetData() throws Exception {
        assumeCardPresent();

        PIVCard card = PIVCard.getCard();
        assertNotNull("card not found", card);

        byte[] result = card.getData(0x005FC102); // Cardholder UUID
        assertNotNull("result", result);
        assertTrue("result length", result.length >= 61);
    }

    @Test
    public void testGetVersion() throws Exception {
        assumeCardPresent();

        PIVCard pivcard = PIVCard.getCard();
        assertNotNull("card not found", pivcard);

        String version = pivcard.getVersion();
        assertNotNull("version", version);
    }

    @Test
    public void testGetAvailableKeys() throws Exception {
        assumeCardPresent();

        PIVCard card = PIVCard.getCard();
        assertNotNull("card not found", card);

        Set<PIVCard.Key> keys = card.getAvailableKeys();

        assertNotNull("keys", keys);
        assertEquals("number of keys", 2, keys.size());
    }

    @Test
    public void testGetKeyInfo() throws Exception {
        assumeCardPresent();

        PIVCard card = PIVCard.getCard();
        assertNotNull("card not found", card);

        PIVCard.KeyInfo info = card.getKeyInfo(SIGNATURE);
        assertNotNull("key info not found", info);
        assertEquals("Algorithm", "RSA", info.algorithm);
        assertEquals("Size", 2048, info.size);
        assertEquals("RSA-2048 identifier", 7, info.algorithmId);
    }

    @Test
    public void testGetCertificate() throws Exception {
        assumeCardPresent();

        PIVCard card = PIVCard.getCard();
        assertNotNull("card not found", card);

        X509Certificate certificate = (X509Certificate) card.getCertificate(SIGNATURE);
        assertNotNull("certificate not found", certificate);
        assertEquals("subject", "CN=Jsign Code Signing Test Certificate 2024 (RSA)", certificate.getSubjectDN().getName());
    }

    @Test
    public void testGetKey() {
        assertNull(PIVCard.Key.of(null));
        assertNull(PIVCard.Key.of("JSIGN"));

        for (PIVCard.Key key : PIVCard.Key.values()) {
            assertEquals(key.name() + " key with uppercase name", key, PIVCard.Key.of(key.name()));
            assertEquals(key.name() + " key with lowercase name", key, PIVCard.Key.of(key.name().toLowerCase()));
            assertEquals(key.name() + " key with alias", key, PIVCard.Key.of(key.alias));

            String slot = Integer.toHexString(key.slot);
            assertEquals(key.name() + " key with slot " + slot, key, PIVCard.Key.of(slot));
        }
    }
}