Java tutorial
/* Copyright 2016 Danish Maritime Authority. * * 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.maritimecloud.identityregistry.keycloak.spi.authenticators.certificate.utils; import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.*; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Encoding; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1String; import org.bouncycastle.asn1.DERTaggedObject; import org.bouncycastle.asn1.DERUTF8String; import org.bouncycastle.asn1.DERUniversalString; import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.x500.RDN; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.cert.CertException; import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; import org.bouncycastle.operator.ContentVerifierProvider; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; import org.bouncycastle.util.encoders.Hex; import org.jboss.logging.Logger; /** * Simplified version of the CertificateUtil in Maritime Cloud Identity Registry code base */ public class CertificateUtil { private static final Logger logger = Logger.getLogger(CertificateUtil.class); public static final String ROOT_CERT_ALIAS = "rootcert"; public static final String INTERMEDIATE_CERT_ALIAS = "imcert"; public static final String BC_PROVIDER_NAME = "BC"; public static final String KEYSTORE_TYPE = "jks"; public static final String SIGNER_ALGORITHM = "SHA224withECDSA"; //@Value("${net.maritimecloud.idreg.certs.root-keystore}") private String ROOT_KEYSTORE_PATH; //@Value("${net.maritimecloud.idreg.certs.it-keystore}") private String INTERMEDIATE_KEYSTORE_PATH; //@Value("${net.maritimecloud.idreg.certs.keystore-password}") private String KEYSTORE_PASSWORD; //@Value("${net.maritimecloud.idreg.certs.truststore}") private String TRUSTSTORE_PATH; //@Value("${net.maritimecloud.idreg.certs.truststore-password}") private String TRUSTSTORE_PASSWORD; // OIDs used for the extra info stored in the SubjectAlternativeName extension // Generate more random OIDs at http://www.itu.int/en/ITU-T/asn1/Pages/UUID/generate_uuid.aspx public static final String MC_OID_FLAGSTATE = "2.25.323100633285601570573910217875371967771"; public static final String MC_OID_CALLSIGN = "2.25.208070283325144527098121348946972755227"; public static final String MC_OID_IMO_NUMBER = "2.25.291283622413876360871493815653100799259"; public static final String MC_OID_MMSI_NUMBER = "2.25.328433707816814908768060331477217690907"; // See http://www.shipais.com/doc/Pifaq/1/22/ and https://help.marinetraffic.com/hc/en-us/articles/205579997-What-is-the-significance-of-the-AIS-SHIPTYPE-number- public static final String MC_OID_AIS_SHIPTYPE = "2.25.107857171638679641902842130101018412315"; public static final String MC_OID_MRN = "2.25.271477598449775373676560215839310464283"; public static final String MC_OID_PERMISSIONS = "2.25.174437629172304915481663724171734402331"; public static final String MC_OID_PORT_OF_REGISTER = "2.25.285632790821948647314354670918887798603"; public CertificateUtil(String truststorePath, String truststorePassword) { TRUSTSTORE_PATH = truststorePath; TRUSTSTORE_PASSWORD = truststorePassword; } public X509Certificate getCertFromString(String certificateHeader) { CertificateFactory certificateFactory; try { certificateFactory = CertificateFactory.getInstance("X.509"); } catch (CertificateException e) { logger.error("Exception while creating CertificateFactory", e); return null; } // nginx forwards the certificate in a header by replacing new lines with whitespaces // (2 or more). Also replace tabs, which nginx sometimes sends instead of whitespaces. // Special changes for keycloak: Wildfly(?) seems to strip "unneeded" double whitespaces //String certificateContent = certificateHeader.replaceAll("\\s{2,}", System.lineSeparator()).replaceAll("\\t+", System.lineSeparator()); String certificateContent = certificateHeader.replaceAll("\\s", System.lineSeparator()).replaceAll("\\t+", System.lineSeparator()); if (certificateContent == null || certificateContent.length() < 10) { logger.debug("No certificate content found"); return null; } // Special changes for keycloak: Restore the first and the last whitespace (-----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----) certificateContent = certificateContent.replace("-----BEGIN\nCERTIFICATE-----", "-----BEGIN CERTIFICATE-----"); certificateContent = certificateContent.replace("-----END\nCERTIFICATE-----", "-----END CERTIFICATE-----"); X509Certificate userCertificate = null; try { userCertificate = (X509Certificate) certificateFactory .generateCertificate(new ByteArrayInputStream(certificateContent.getBytes("ISO-8859-11"))); } catch (CertificateException | UnsupportedEncodingException e) { logger.error("Exception while converting certificate extracted from header", e); return null; } logger.debug("Certificate was extracted from the header"); return userCertificate; } public Map<String, String> getUserFromCert(X509Certificate userCertificate) { Map<String, String> user = new HashMap<>(); String certDN = userCertificate.getSubjectDN().getName(); X500Name x500name = new X500Name(certDN); logger.warn("Parsed certificate, DN: " + certDN); String fullname = getElement(x500name, BCStyle.CN); user.put("fullname", fullname); String combinedOrg = getElement(x500name, BCStyle.O); user.put("email", getElement(x500name, BCStyle.EmailAddress)); // Extract first and last name from full name String lastName = ""; String firstName = ""; if (fullname.split("\\w+").length > 1) { lastName = fullname.substring(fullname.lastIndexOf(" ") + 1); firstName = fullname.substring(0, fullname.lastIndexOf(' ')); } else { firstName = fullname; } user.put("lastName", lastName); user.put("firstName", firstName); String[] orgNames = combinedOrg.split(";"); String orgShortName = orgNames[0].toLowerCase(); user.put("orgShortName", orgShortName); user.put("orgFullName", orgNames[1]); // prefix orgUserName with org shortname if not already done String orgUserName = getElement(x500name, BCStyle.UID).toLowerCase(); if (!orgUserName.startsWith(orgShortName + ".")) { orgUserName = orgShortName.toLowerCase() + "." + orgUserName; } user.put("orgUserName", orgUserName); user.put("type", getElement(x500name, BCStyle.OU)); // Extract info from Subject Alternative Name extension Collection<List<?>> san = null; try { san = userCertificate.getSubjectAlternativeNames(); } catch (CertificateParsingException e) { logger.warn("could not extract info from Subject Alternative Names - will be ignored."); } // Check that the certificate includes the SubjectAltName extension if (san != null) { // Use the type OtherName to search for the certified server name for (List item : san) { Integer type = (Integer) item.get(0); if (type == 0) { // Type OtherName found so return the associated value ASN1InputStream decoder = null; String oid = ""; String value = ""; try { // Value is encoded using ASN.1 so decode it to get it out again decoder = new ASN1InputStream((byte[]) item.toArray()[1]); DLSequence seq = (DLSequence) decoder.readObject(); ASN1ObjectIdentifier asnOID = (ASN1ObjectIdentifier) seq.getObjectAt(0); ASN1Encodable encoded = seq.getObjectAt(1); encoded = ((DERTaggedObject) encoded).getObject(); encoded = ((DERTaggedObject) encoded).getObject(); oid = asnOID.getId(); value = ((DERUTF8String) encoded).getString(); } catch (UnsupportedEncodingException e) { logger.error("Error decoding subjectAltName" + e.getLocalizedMessage(), e); continue; } catch (Exception e) { logger.error("Error decoding subjectAltName" + e.getLocalizedMessage(), e); continue; } finally { if (decoder != null) { try { decoder.close(); } catch (IOException e) { } } } logger.debug("oid: " + oid + ", value: " + value); switch (oid) { case MC_OID_FLAGSTATE: case MC_OID_CALLSIGN: case MC_OID_IMO_NUMBER: case MC_OID_MMSI_NUMBER: case MC_OID_AIS_SHIPTYPE: case MC_OID_PORT_OF_REGISTER: logger.debug("Ship specific OIDs are ignored"); break; case MC_OID_MRN: // We only support 1 mrn user.put("mrn", value); break; case MC_OID_PERMISSIONS: user.put("permissions", value); break; default: logger.error("Unknown OID!"); break; } } else { // Other types are not supported so ignore them logger.warn("SubjectAltName of invalid type found: " + type); } } } return user; } private Certificate getRootCertificate() { logger.debug(TRUSTSTORE_PATH); FileInputStream is; try { is = new FileInputStream(TRUSTSTORE_PATH); } catch (FileNotFoundException e) { logger.error("Could not open truststore", e); return null; } KeyStore keystore; try { keystore = KeyStore.getInstance(KEYSTORE_TYPE); keystore.load(is, TRUSTSTORE_PASSWORD.toCharArray()); //KeyStore.ProtectionParameter protParam = new KeyStore.PasswordProtection(TRUSTSTORE_PASSWORD.toCharArray()); Certificate rootCert = keystore.getCertificate(ROOT_CERT_ALIAS); return rootCert; } catch (NoSuchAlgorithmException | CertificateException | IOException | KeyStoreException e) { logger.error("Could not load root certificate", e); return null; } } public boolean verifyCertificate(X509Certificate certToVerify) { Certificate rootCert = getRootCertificate(); JcaX509CertificateHolder certHolder; try { certHolder = new JcaX509CertificateHolder(certToVerify); } catch (CertificateEncodingException e) { logger.error("Could not create JcaX509CertificateHolder", e); return false; } PublicKey pubKey = rootCert.getPublicKey(); if (pubKey == null) { return false; } ContentVerifierProvider contentVerifierProvider = null; try { contentVerifierProvider = new JcaContentVerifierProviderBuilder().setProvider(BC_PROVIDER_NAME) .build(pubKey); } catch (OperatorCreationException e) { logger.error("Could not create ContentVerifierProvider from public key", e); return false; } if (contentVerifierProvider == null) { return false; } try { if (certHolder.isSignatureValid(contentVerifierProvider)) { return true; } } catch (CertException e) { logger.error("Error when trying to validate signature", e); return false; } return true; } /** * Extract a value from the DN extracted from a certificate * * @param x500name * @param style * @return */ public static String getElement(X500Name x500name, ASN1ObjectIdentifier style) { try { RDN cn = x500name.getRDNs(style)[0]; return valueToString(cn.getFirst().getValue()); } catch (ArrayIndexOutOfBoundsException e) { return ""; } } /** * Simplified version of IETFUtils.valueToString where some "special" chars was escaped * @param value * @return */ public static String valueToString(ASN1Encodable value) { StringBuffer vBuf = new StringBuffer(); if (value instanceof ASN1String && !(value instanceof DERUniversalString)) { String v = ((ASN1String) value).getString(); vBuf.append(v); } else { try { vBuf.append("#" + bytesToString(Hex.encode(value.toASN1Primitive().getEncoded(ASN1Encoding.DER)))); } catch (IOException e) { throw new IllegalArgumentException("Other value has no encoded form"); } } logger.debug(vBuf.toString()); return vBuf.toString().trim(); } private static String bytesToString(byte[] data) { char[] cs = new char[data.length]; for (int i = 0; i != cs.length; i++) { cs[i] = (char) (data[i] & 0xff); } return new String(cs); } }