001/*
002 * Copyright 2024 Emmanuel Bourg
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package net.jsign.jca;
018
019import java.io.IOException;
020import java.security.GeneralSecurityException;
021import java.security.KeyStoreException;
022import java.security.UnrecoverableKeyException;
023import java.security.cert.Certificate;
024import java.security.cert.X509Certificate;
025import java.util.Arrays;
026import java.util.HashMap;
027import java.util.LinkedHashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031import java.util.function.Function;
032import java.util.stream.Collectors;
033import javax.smartcardio.CardException;
034
035import org.bouncycastle.asn1.ASN1Encoding;
036import org.bouncycastle.asn1.DERNull;
037import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
038import org.bouncycastle.asn1.x509.DigestInfo;
039
040import net.jsign.DigestAlgorithm;
041
042/**
043 * Signing service using an PIV smart card. PIV cards contain up to 24 keys usable to signing,
044 * along with the X.509 certificates.
045 *
046 * @since 6.0
047 */
048public class PIVCardSigningService implements SigningService {
049
050    private final PIVCard card;
051
052    /** Source for the certificates */
053    private final Function<String, Certificate[]> certificateStore;
054
055    public PIVCardSigningService(String cardname, String pin, Function<String, Certificate[]> certificateStore) throws CardException {
056        PIVCard card = PIVCard.getCard(cardname);
057        if (card == null) {
058            throw new CardException("PIV card not found");
059        }
060
061        this.certificateStore = certificateStore;
062        this.card = card;
063        this.card.verify(pin);
064    }
065
066    @Override
067    public String getName() {
068        return "PIV";
069    }
070
071    @Override
072    public List<String> aliases() throws KeyStoreException {
073        try {
074            Set<PIVCard.Key> keys = card.getAvailableKeys();
075            return keys.stream().map(Enum::name).collect(Collectors.toList());
076        } catch (CardException e) {
077            throw new KeyStoreException(e);
078        }
079    }
080
081    @Override
082    public Certificate[] getCertificateChain(String alias) throws KeyStoreException {
083        Map<String, Certificate> certificates = new LinkedHashMap<>();
084
085        PIVCard.Key key = PIVCard.Key.of(alias);
086        if (key == null) {
087            return null;
088        }
089
090        // add the certificate from the card
091        try {
092            Certificate certificate = card.getCertificate(key);
093            if (certificate == null) {
094                return null;
095            }
096            String subject = ((X509Certificate) certificate).getSubjectX500Principal().getName();
097            certificates.put(subject, certificate);
098        } catch (CardException e) {
099            throw new KeyStoreException(e);
100        }
101
102        // add the certificates from the certificate store
103        if (certificateStore != null) {
104            for (Certificate certificate : certificateStore.apply(alias)) {
105                String subject = ((X509Certificate) certificate).getSubjectX500Principal().getName();
106                certificates.put(subject, certificate);
107            }
108        } else {
109            // add the certificates from the slots 82-95 if related (see https://support.yubico.com/hc/en-us/articles/360016614840)
110            certificates.putAll(getCertificateChain(certificates.values().iterator().next()));
111        }
112
113        return certificates.values().toArray(new Certificate[0]);
114    }
115
116    /**
117     * Returns the certificate chain stored in the slots 82-95 of the PIV card for the specified certificate.
118     */
119    private Map<String, X509Certificate> getCertificateChain(Certificate certificate) {
120        Map<String, X509Certificate> chain = new LinkedHashMap<>();
121
122        Map<String, X509Certificate> certificates = new HashMap<>();
123
124        PIVCard.Key[] retiredKeys = PIVCard.Key.values();
125        retiredKeys = Arrays.copyOfRange(retiredKeys, 4, retiredKeys.length);
126
127        String issuer = ((X509Certificate) certificate).getIssuerX500Principal().getName();
128        keys: for (PIVCard.Key retiredKey : retiredKeys) {
129            try {
130                X509Certificate extraCertificate = (X509Certificate) card.getCertificate(retiredKey);
131                if (extraCertificate != null) {
132                    certificates.put(extraCertificate.getSubjectX500Principal().getName(), extraCertificate);
133
134                    // build the chain as far as possible from the extra certificates found
135                    X509Certificate issuerCertificate;
136                    while ((issuerCertificate = certificates.remove(issuer)) != null) {
137                        chain.put(issuer, issuerCertificate);
138                        if (issuer.equals(issuerCertificate.getIssuerX500Principal().getName())) {
139                            // root certificate found, stop iterating the keys
140                            break keys;
141                        }
142                        issuer = extraCertificate.getIssuerX500Principal().getName();
143                    }
144                }
145            } catch (CardException e) {
146                // ignore
147            }
148        }
149
150        return chain;
151    }
152
153    @Override
154    public SigningServicePrivateKey getPrivateKey(String alias, char[] password) throws UnrecoverableKeyException {
155        PIVCard.Key key = PIVCard.Key.of(alias);
156
157        try {
158            Certificate certificate = card.getCertificate(key);
159            return new SigningServicePrivateKey(alias, certificate.getPublicKey().getAlgorithm(), this);
160        } catch (CardException e) {
161            throw (UnrecoverableKeyException) new UnrecoverableKeyException("Unable to retrieve the info for key " + alias).initCause(e);
162        }
163    }
164
165    @Override
166    public byte[] sign(SigningServicePrivateKey privateKey, String algorithm, byte[] data) throws GeneralSecurityException {
167        DigestAlgorithm digestAlgorithm = DigestAlgorithm.of(algorithm.substring(0, algorithm.toLowerCase().indexOf("with")));
168        byte[] digest = digestAlgorithm.getMessageDigest().digest(data);
169
170        PIVCard.Key key = PIVCard.Key.of(privateKey.getId());
171
172        try {
173            PIVCard.KeyInfo keyInfo = card.getKeyInfo(key);
174
175            byte[] content;
176            if ("RSA".equals(privateKey.getAlgorithm())) {
177                // RSA
178                DigestInfo digestInfo = new DigestInfo(new AlgorithmIdentifier(digestAlgorithm.oid, DERNull.INSTANCE), digest);
179                content = digestInfo.getEncoded(ASN1Encoding.DER);
180            } else {
181                // ECDSA
182                if (digest.length > keyInfo.size / 8) {
183                    content = Arrays.copyOf(digest, keyInfo.size / 8);
184                } else {
185                    content = digest;
186                }
187            }
188            return  card.sign(key, content);
189        } catch (CardException | IOException e) {
190            throw new GeneralSecurityException(e);
191        }
192    }
193}