Java tutorial
/* * Copyright 2012-2013 Alfresco Software Limited. * * Licensed under the GNU Affero General Public License, Version 3.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.gnu.org/licenses/agpl-3.0.html * * If you do not wish to be bound to the terms of the AGPL v3.0, * A commercial license may be obtained by contacting the author. * * 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. * * This file is part of an unsupported extension to Alfresco. * */ package org.alfresco.extension.countersign.signature; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Serializable; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.Signature; import java.security.SignatureException; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Calendar; import java.util.HashMap; import java.util.Map; import java.util.Properties; import javax.imageio.ImageIO; import javax.security.auth.x500.X500Principal; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.extension.countersign.model.CounterSignSignatureModel; import org.alfresco.extension.countersign.service.CounterSignService; import org.alfresco.model.ContentModel; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.QName; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bouncycastle.asn1.x509.X509Extensions; import org.bouncycastle.openssl.PEMReader; import org.bouncycastle.openssl.PEMWriter; import org.bouncycastle.x509.X509V3CertificateGenerator; import org.bouncycastle.x509.extension.AuthorityKeyIdentifierStructure; import org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure; public class RepositoryManagedSignatureProvider implements SignatureProvider { private static Log logger = LogFactory.getLog(RepositoryManagedSignatureProvider.class); private ServiceRegistry serviceRegistry; private String user; private Properties config; private CounterSignService counterSignService; public RepositoryManagedSignatureProvider(ServiceRegistry serviceRegistry, CounterSignService counterSignService, String user, Properties config) { this.config = config; this.serviceRegistry = serviceRegistry; this.user = user; this.counterSignService = counterSignService; // check to see if this user has the signer aspect. If not, add it. // this is temporary until I get the management interface sorted out. if (!signatureAvailable()) { NodeRef person = serviceRegistry.getPersonService().getPerson(user); if (person != null) { serviceRegistry.getNodeService().addAspect(person, CounterSignSignatureModel.ASPECT_SIGNER, null); } } } @Override public KeyStore getUserKeyStore(String storePassword) { try { NodeRef person = serviceRegistry.getPersonService().getPerson(user); if (person == null) { return null; } KeyStore keystore = KeyStore.getInstance("pkcs12"); // check to see if a keystore exists for this user NodeRef keystoreNode = counterSignService.getSignatureArtifact(person, CounterSignSignatureModel.ASSOC_SIGNERKEYSTORE); // if no keystore, create one, persist it and associate it with the user if (keystoreNode == null) { keystore = createUserKeyStore(person, storePassword); } else { // open the reader to the key and load it ContentReader keyReader = serviceRegistry.getContentService().getReader(keystoreNode, ContentModel.PROP_CONTENT); keystore.load(keyReader.getContentInputStream(), storePassword.toCharArray()); } // return the keystore return keystore; } catch (KeyStoreException kse) { throw new AlfrescoRuntimeException(kse.getMessage()); } catch (java.security.cert.CertificateException ce) { throw new AlfrescoRuntimeException(ce.getMessage()); } catch (NoSuchAlgorithmException nsaex) { throw new AlfrescoRuntimeException(nsaex.getMessage()); } catch (IOException ioex) { throw new AlfrescoRuntimeException(ioex.getMessage()); } catch (NoSuchProviderException nspex) { throw new AlfrescoRuntimeException(nspex.getMessage()); } } @Override public BufferedImage getSignatureImage() { NodeRef person = serviceRegistry.getPersonService().getPerson(user); if (person == null) { return null; } NodeRef sigImage = counterSignService.getSignatureArtifact(person, CounterSignSignatureModel.ASSOC_SIGNERSIGNATUREIMAGE); if (sigImage != null) { ContentReader imageReader = serviceRegistry.getContentService().getReader(sigImage, ContentModel.PROP_CONTENT); try { return ImageIO.read(imageReader.getContentInputStream()); } catch (IOException ioex) { logger.warn("Could not retrieve signature image as a child of person: " + ioex); // generate a default image? } } return null; } @Override public String getSignatureSource() { NodeRef person = serviceRegistry.getPersonService().getPerson(user); if (person == null) { return null; } NodeRef sigImage = counterSignService.getSignatureArtifact(person, CounterSignSignatureModel.ASSOC_SIGNERSIGNATUREIMAGE); if (sigImage != null) { return String.valueOf(serviceRegistry.getNodeService().getProperty(sigImage, CounterSignSignatureModel.PROP_SIGNATUREJSON)); } return null; } @Override public void saveSignatureImage(BufferedImage image, String source) { // save the signature image as a child of the person NodeRef person = serviceRegistry.getPersonService().getPerson(user); if (person != null) { NodeRef sigNode = counterSignService.getSignatureArtifact(person, CounterSignSignatureModel.ASSOC_SIGNERSIGNATUREIMAGE); if (sigNode == null) { // set up JSON source as property Map<QName, Serializable> sigProps = new HashMap<QName, Serializable>(); sigProps.put(CounterSignSignatureModel.PROP_SIGNATUREJSON, source); QName assocQName = QName.createQName(CounterSignSignatureModel.COUNTERSIGN_SIGNATURE_MODEL_1_0_URI, QName.createValidLocalName(user + "-signatureimage")); ChildAssociationRef sigChildRef = serviceRegistry.getNodeService().createNode(person, CounterSignSignatureModel.ASSOC_SIGNERSIGNATUREIMAGE, assocQName, CounterSignSignatureModel.TYPE_SIGNATUREIMAGE, sigProps); sigNode = sigChildRef.getChildRef(); } else { serviceRegistry.getNodeService().setProperty(sigNode, CounterSignSignatureModel.PROP_SIGNATUREJSON, source); } // get a writer, store the image content ContentWriter writer = serviceRegistry.getContentService().getWriter(sigNode, ContentModel.PROP_CONTENT, true); try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(image, "png", baos); writer.putContent(new ByteArrayInputStream(baos.toByteArray())); PermissionService ps = serviceRegistry.getPermissionService(); ps.clearPermission(sigNode, PermissionService.ALL_AUTHORITIES); ps.setInheritParentPermissions(sigNode, false); } catch (IOException ioex) { logger.warn("Could not save signature image as child of person: " + ioex); } } } @Override public boolean signatureAvailable() { // signature is only available if this user has the "signer" aspect applied. NodeRef person = serviceRegistry.getPersonService().getPerson(user); if (person != null && serviceRegistry.getNodeService().hasAspect(person, CounterSignSignatureModel.ASPECT_SIGNER)) { // now check to see if the user has a keystore available. If so, then // they can sign without creating a new keystore. NodeRef keystoreNode = counterSignService.getSignatureArtifact(person, CounterSignSignatureModel.ASSOC_SIGNERKEYSTORE); if (keystoreNode != null && serviceRegistry.getNodeService().exists(keystoreNode)) { return true; } } return false; } /** * Get the user's public key * */ public PublicKey getPublicKey() { NodeRef person = serviceRegistry.getPersonService().getPerson(user); NodeRef keyNode = counterSignService.getSignatureArtifact(person, CounterSignSignatureModel.ASSOC_SIGNERPUBLICKEY); if (keyNode != null) { PEMReader parser = null; try { ContentReader keyReader = serviceRegistry.getContentService().getReader(keyNode, ContentModel.PROP_CONTENT); parser = new PEMReader(new InputStreamReader(keyReader.getContentInputStream())); PublicKey key = (PublicKey) parser.readObject(); parser.close(); return key; } catch (Exception ioex) { logger.warn("Error reading user public key: " + ioex.getLocalizedMessage()); } finally { try { if (parser != null) parser.close(); } catch (IOException ioex) { logger.warn("Error closing PEMReader"); } } } return null; } @Override public boolean validateSignature(byte[] sig, byte[] hash) { String alg = config.getProperty(RepositoryManagedSignatureProviderFactory.SIGNATURE_ALGORITHM); String prov = config.getProperty(RepositoryManagedSignatureProviderFactory.JAVA_SIGNATURE_PROVIDER); boolean valid = false; try { Signature validate = Signature.getInstance(alg, prov); validate.initVerify(getPublicKey()); validate.update(hash); valid = validate.verify(sig); } catch (NoSuchProviderException nspe) { throw new AlfrescoRuntimeException("Provider: " + prov + " was not found: " + nspe.getMessage()); } catch (NoSuchAlgorithmException nsae) { throw new AlfrescoRuntimeException("Algorithm: " + alg + " is not available: " + nsae.getMessage()); } catch (SignatureException se) { valid = false; } catch (InvalidKeyException ike) { valid = false; } return valid; } /** * Sign a hash using the user's private key * * @param hash * @param key * @return * @throws Exception */ public byte[] signHash(byte[] hash, String password) throws Exception { String alg = config.getProperty(RepositoryManagedSignatureProviderFactory.SIGNATURE_ALGORITHM); String prov = config.getProperty(RepositoryManagedSignatureProviderFactory.JAVA_SIGNATURE_PROVIDER); String alias = config.getProperty(RepositoryManagedSignatureProviderFactory.ALIAS); KeyStore ks = getUserKeyStore(password); PrivateKey key = (PrivateKey) ks.getKey(alias, password.toCharArray()); Signature signer = Signature.getInstance(alg, prov); signer.initSign(key); signer.update(hash); return signer.sign(); } /** * Compute a hash of the input stream using the configured algorithm * * @param contentSteam * @return a hash of the content stream */ public byte[] computeHash(InputStream contentStream) { String alg = config.getProperty(RepositoryManagedSignatureProviderFactory.HASH_ALGORITHM); MessageDigest messageDigest = null; try { messageDigest = MessageDigest.getInstance(alg); } catch (NoSuchAlgorithmException e) { logger.error("Unable to process algorithm type: " + alg); return null; } messageDigest.reset(); byte[] buffer = new byte[1024]; int bytesRead = -1; try { while ((bytesRead = contentStream.read(buffer)) > -1) { messageDigest.update(buffer, 0, bytesRead); } } catch (IOException e) { logger.error("Unable to read content stream.", e); return null; } finally { try { contentStream.close(); } catch (IOException e) { } } byte[] digest = messageDigest.digest(); return convertByteArrayToHex(digest).getBytes(); } private String convertByteArrayToHex(byte[] array) { StringBuffer hashValue = new StringBuffer(); for (int i = 0; i < array.length; i++) { String hex = Integer.toHexString(0xFF & array[i]); if (hex.length() == 1) { hashValue.append('0'); } hashValue.append(hex); } return hashValue.toString().toUpperCase(); } /** * Save the user's public key, associated as a child with the person * that owns it. Make this world-readable, as anybody should be able * to use it for validating signatures. * * @param person * @param publicKey */ private void saveUserPublicKey(NodeRef person, PublicKey publicKey) { NodeRef keyNode = counterSignService.getSignatureArtifact(person, CounterSignSignatureModel.ASSOC_SIGNERPUBLICKEY); if (keyNode == null) { QName assocQName = QName.createQName(CounterSignSignatureModel.COUNTERSIGN_SIGNATURE_MODEL_1_0_URI, QName.createValidLocalName(user + "-publickey")); ChildAssociationRef keyChildRef = serviceRegistry.getNodeService().createNode(person, CounterSignSignatureModel.ASSOC_SIGNERPUBLICKEY, assocQName, CounterSignSignatureModel.TYPE_PUBLICKEY); keyNode = keyChildRef.getChildRef(); } // get a writer, store the public key ContentWriter writer = serviceRegistry.getContentService().getWriter(keyNode, ContentModel.PROP_CONTENT, true); PEMWriter pem = null; try { //set the encoding and write out the key serviceRegistry.getNodeService().setProperty(keyNode, CounterSignSignatureModel.PROP_KEYENCODING, "PEM"); pem = new PEMWriter(new OutputStreamWriter(writer.getContentOutputStream())); pem.writeObject(publicKey); } catch (IOException ioex) { logger.error("Error writing public key to PEMWriter: " + ioex); } finally { if (pem != null) try { pem.close(); } catch (Exception ex) { logger.error("Error closing PEMWriter"); } } // Ensure that the cert is readable by anybody, this is important // for later validation of signed content. Can't validate without the // public key PermissionService ps = serviceRegistry.getPermissionService(); ps.setPermission(keyNode, PermissionService.ALL_AUTHORITIES, PermissionService.READ, true); ps.setInheritParentPermissions(keyNode, false); } /** * Save the Java KeyStore, associated as a child with the person that owns this keystore * * @param person * @param keystore * @throws IOException * @throws CertificateException * @throws NoSuchAlgorithmException * @throws KeyStoreException */ private void saveUserKeyStore(NodeRef person, KeyStore keystore, String password) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { NodeRef keystoreNode = counterSignService.getSignatureArtifact(person, CounterSignSignatureModel.ASSOC_SIGNERKEYSTORE); if (keystoreNode == null) { QName assocQName = QName.createQName(CounterSignSignatureModel.COUNTERSIGN_SIGNATURE_MODEL_1_0_URI, QName.createValidLocalName(user + "-signaturekeystore")); ChildAssociationRef keyChildRef = serviceRegistry.getNodeService().createNode(person, CounterSignSignatureModel.ASSOC_SIGNERKEYSTORE, assocQName, CounterSignSignatureModel.TYPE_PKCS12); keystoreNode = keyChildRef.getChildRef(); } // get a writer, store the user keystore ContentWriter writer = serviceRegistry.getContentService().getWriter(keystoreNode, ContentModel.PROP_CONTENT, true); ByteArrayOutputStream baos = new ByteArrayOutputStream(); keystore.store(baos, password.toCharArray()); writer.putContent(new ByteArrayInputStream(baos.toByteArray())); // now set the permissions on this node so that only the person that owns it // can access it. PermissionService.ALL_AUTHORITIES = GROUP_EVERYONE // this should leave the owner permissions intact PermissionService ps = serviceRegistry.getPermissionService(); ps.clearPermission(keystoreNode, PermissionService.ALL_AUTHORITIES); ps.setInheritParentPermissions(keystoreNode, false); } /** * Create a keystore for this user to be used for document signing, store it associated with the user's * person node * * @param person * @param password * * @return a Java KeyStore object suitable for document signing * @throws NoSuchAlgorithmException * @throws NoSuchProviderException * @throws KeyStoreException * @throws IOException * @throws CertificateException */ private KeyStore createUserKeyStore(NodeRef person, String password) throws NoSuchAlgorithmException, NoSuchProviderException, KeyStoreException, CertificateException, IOException { // get the alias from the configuration String alias = config.getProperty(RepositoryManagedSignatureProviderFactory.ALIAS); // initialize key generator KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); SecureRandom random = SecureRandom.getInstance("SHA1PRNG", "SUN"); keyGen.initialize(2048, random); // generate a keypair KeyPair pair = keyGen.generateKeyPair(); PrivateKey priv = pair.getPrivate(); PublicKey pub = pair.getPublic(); // generate the user certificate Certificate cert = generateCertificate(pair, person); // get the ca cert used to sign and create cert chain KeyStore trustedKs = getTrustedKeyStore(); Certificate[] caChain = getCaCertChain(trustedKs); Certificate[] certChain = new Certificate[caChain.length + 1]; certChain[0] = cert; for (int i = 0; i < caChain.length; i++) { certChain[i + 1] = caChain[i]; } // create keystore, adding private key and cert chain KeyStore ks = KeyStore.getInstance("pkcs12"); ks.load(null, password.toCharArray()); ks.setKeyEntry(alias, priv, password.toCharArray(), certChain); // save the keystore saveUserKeyStore(person, ks, password); // also save the public key separately, will need it // for later validaiton activities saveUserPublicKey(person, pub); // return the generated keystore return ks; } /** * Generate an X509 cert for use as the keystore cert chain * * @param keyPair * @return */ private X509Certificate generateCertificate(KeyPair keyPair, NodeRef person) { X509Certificate cert = null; int validDuration = Integer .parseInt(config.getProperty(RepositoryManagedSignatureProviderFactory.VALID_DURATION)); // get user's first and last name Map<QName, Serializable> props = serviceRegistry.getNodeService().getProperties(person); String firstName = String.valueOf(props.get(ContentModel.PROP_FIRSTNAME)); String lastName = String.valueOf(props.get(ContentModel.PROP_LASTNAME)); // backdate the start date by a day Calendar start = Calendar.getInstance(); start.add(Calendar.DATE, -1); java.util.Date startDate = start.getTime(); // what is the end date for this cert's validity? Calendar end = Calendar.getInstance(); end.add(Calendar.DATE, validDuration); java.util.Date endDate = end.getTime(); try { // This code works with newer versions of the BouncyCastle libraries, but not // the (severely outdated) version that ships with Alfresco /*X509v1CertificateBuilder certBuilder = new JcaX509v1CertificateBuilder( new X500Principal("CN=" + firstName + " " + lastName), BigInteger.ONE, startDate, cal.getTime(), new X500Principal("CN=" + firstName + " " + lastName), keyPair.getPublic()); AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1withRSA"); AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); AsymmetricKeyParameter keyParam = PrivateKeyFactory.createKey(keyPair.getPrivate().getEncoded()); ContentSigner sigGen = new BcRSAContentSignerBuilder(sigAlgId, digAlgId).build(keyParam); X509CertificateHolder certHolder = certBuilder.build(sigGen); // now lets convert this thing back to a regular old java cert CertificateFactory cf = CertificateFactory.getInstance("X.509"); InputStream certIs = new ByteArrayInputStream(certHolder.getEncoded()); cert = (X509Certificate) cf.generateCertificate(certIs); certIs.close();*/ X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); X500Principal subjectName = new X500Principal("CN=" + firstName + " " + lastName); certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); certGen.setNotBefore(startDate); certGen.setNotAfter(endDate); certGen.setSubjectDN(subjectName); certGen.setPublicKey(keyPair.getPublic()); certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); // if we are actually generating a trusted cert, the action is a little different boolean generateTrusted = Boolean.parseBoolean( config.getProperty(RepositoryManagedSignatureProviderFactory.ENABLE_TRUSTED_CERTS)); if (generateTrusted) { KeyStore trustedKs = getTrustedKeyStore(); PrivateKey caKey = getCaKey(trustedKs); X509Certificate caCert = getCaCert(trustedKs); // set the issuer of the generated cert to the subject of the ca cert X500Principal caSubject = caCert.getSubjectX500Principal(); certGen.setIssuerDN(caSubject); //add the required extensions for the new cert certGen.addExtension(X509Extensions.AuthorityKeyIdentifier, false, new AuthorityKeyIdentifierStructure(caCert)); certGen.addExtension(X509Extensions.SubjectKeyIdentifier, false, new SubjectKeyIdentifierStructure(keyPair.getPublic())); cert = certGen.generate(caKey, "BC"); //verify the cert cert.verify(caCert.getPublicKey()); } else { certGen.setIssuerDN(subjectName); cert = certGen.generate(keyPair.getPrivate(), "BC"); } } catch (CertificateException ce) { logger.error("CertificateException creating or validating X509 certificate for user: " + ce); throw new AlfrescoRuntimeException(ce.getMessage()); } catch (Exception ex) { logger.error("Unknown exception creating or validating X509 certificate for user : " + ex); ex.printStackTrace(); } return cert; } /** * Get the Certificate Authority private key * * @return */ private PrivateKey getCaKey(KeyStore trustedKs) { PrivateKey caKey = null; String keyAlias = config.getProperty(RepositoryManagedSignatureProviderFactory.TRUSTED_KEY_ALIAS); String keyPassword = config.getProperty(RepositoryManagedSignatureProviderFactory.TRUSTED_KEY_PASSWORD); try { caKey = (PrivateKey) trustedKs.getKey(keyAlias, keyPassword.toCharArray()); } catch (KeyStoreException kse) { throw new AlfrescoRuntimeException(kse.getMessage()); } catch (UnrecoverableKeyException uke) { throw new AlfrescoRuntimeException(uke.getMessage()); } catch (NoSuchAlgorithmException nsae) { throw new AlfrescoRuntimeException(nsae.getMessage()); } return caKey; } /** * Get the Certificate Authority public key certificate * * @return */ private X509Certificate getCaCert(KeyStore trustedKs) { X509Certificate caCert = null; String certAlias = config.getProperty(RepositoryManagedSignatureProviderFactory.TRUSTED_CERT_ALIAS); try { caCert = (X509Certificate) trustedKs.getCertificate(certAlias); } catch (KeyStoreException kse) { throw new AlfrescoRuntimeException(kse.getMessage()); } return caCert; } /** * Get the certificate chain for the CA certificate * * @param trustedKs * @return */ private Certificate[] getCaCertChain(KeyStore trustedKs) { Certificate[] caCertChain = null; String certAlias = config.getProperty(RepositoryManagedSignatureProviderFactory.TRUSTED_CERT_ALIAS); try { caCertChain = trustedKs.getCertificateChain(certAlias); } catch (KeyStoreException kse) { throw new AlfrescoRuntimeException(kse.getMessage()); } return caCertChain; } /** * Get the trusted keystore as configured in the extension properties. * * @return */ private KeyStore getTrustedKeyStore() { try { String keystorePassword = config .getProperty(RepositoryManagedSignatureProviderFactory.TRUSTED_KEYSTORE_PASSWORD); String keystorePath = config .getProperty(RepositoryManagedSignatureProviderFactory.TRUSTED_KEYSTORE_PATH); KeyStore keystore = KeyStore.getInstance("pkcs12"); FileInputStream keyStream = new FileInputStream(keystorePath); keystore.load(keyStream, keystorePassword.toCharArray()); // return the keystore return keystore; } catch (KeyStoreException kse) { throw new AlfrescoRuntimeException(kse.getMessage()); } catch (java.security.cert.CertificateException ce) { throw new AlfrescoRuntimeException(ce.getMessage()); } catch (NoSuchAlgorithmException nsaex) { throw new AlfrescoRuntimeException(nsaex.getMessage()); } catch (IOException ioex) { throw new AlfrescoRuntimeException(ioex.getMessage()); } } public void setServiceRegistry(ServiceRegistry serviceRegistry) { this.serviceRegistry = serviceRegistry; } }