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}