diff --git a/core/src/main/java/jenkins/security/RSAConfidentialKey.java b/core/src/main/java/jenkins/security/RSAConfidentialKey.java new file mode 100644 index 0000000000000000000000000000000000000000..c8a065f10b5ea17a2d9ec2a93f63daf55ad2350d --- /dev/null +++ b/core/src/main/java/jenkins/security/RSAConfidentialKey.java @@ -0,0 +1,111 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security; + +import org.apache.commons.codec.binary.Base64; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; + +/** + * RSA public/private key pair as {@link ConfidentialKey}. + * + *

+ * As per the design principle of {@link ConfidentialKey}, not exposing {@link PrivateKey} directly. + * Define subtypes for different use cases. + * + * @author Kohsuke Kawaguchi + */ +public abstract class RSAConfidentialKey extends ConfidentialKey { + private RSAPrivateKey priv; + private RSAPublicKey pub; + + public RSAConfidentialKey(String id) { + super(id); + } + + public RSAConfidentialKey(Class owner, String shortName) { + this(owner.getName() + '.' + shortName); + } + + /** + * Obtains the private key (lazily.) + *

+ * This method is not publicly exposed as per the design principle of {@link ConfidentialKey}. + * Instead of exposing private key, define methods that use them in specific way, such as + * {@link RSADigitalSignatureConfidentialKey}. + * + * @throws Error + * If key cannot be loaded for some reasons, we fail. + */ + protected synchronized RSAPrivateKey getPrivateKey() { + try { + if (priv == null) { + byte[] payload = load(); + if (payload == null) { + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); + gen.initialize(2048, new SecureRandom()); // going beyond 2048 requires crypto extension + KeyPair keys = gen.generateKeyPair(); + priv = (RSAPrivateKey) keys.getPrivate(); + pub = (RSAPublicKey) keys.getPublic(); + store(priv.getEncoded()); + } else { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + priv = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(payload)); + + RSAPrivateCrtKey pks = (RSAPrivateCrtKey) priv; + pub = (RSAPublicKey) keyFactory.generatePublic( + new RSAPublicKeySpec(pks.getModulus(), pks.getPublicExponent())); + } + } + return priv; + } catch (IOException e) { + throw new Error("Failed to load the key: " + getId(), e); + } catch (GeneralSecurityException e) { + throw new Error("Failed to load the key: " + getId(), e); + } + } + + public RSAPublicKey getPublicKey() { + getPrivateKey(); + return pub; + } + + /** + * Gets base64-encoded public key. + */ + public String getEncodedPublicKey() { + return new String(Base64.encodeBase64(getPublicKey().getEncoded())); + } +} diff --git a/core/src/main/java/jenkins/security/RSADigitalSignatureConfidentialKey.java b/core/src/main/java/jenkins/security/RSADigitalSignatureConfidentialKey.java new file mode 100644 index 0000000000000000000000000000000000000000..5ff1b55bdc8d93d7d4340ef0cc8421d41390d7a4 --- /dev/null +++ b/core/src/main/java/jenkins/security/RSADigitalSignatureConfidentialKey.java @@ -0,0 +1,63 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security; + +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.Signature; +import java.security.interfaces.RSAPrivateKey; + +/** + * RSA digital signature as {@link ConfidentialKey} to prevent accidental leak of private key. + * + * @author Kohsuke Kawaguchi + */ +public class RSADigitalSignatureConfidentialKey extends RSAConfidentialKey { + public RSADigitalSignatureConfidentialKey(String id) { + super(id); + } + + public RSADigitalSignatureConfidentialKey(Class owner, String shortName) { + super(owner, shortName); + } + + /** + * Sign a message and base64 encode the signature. + */ + public String sign(String msg) { + try { + RSAPrivateKey key = getPrivateKey(); + Signature sig = Signature.getInstance(SIGNING_ALGORITHM + "with" + key.getAlgorithm()); + sig.initSign(key); + sig.update(msg.getBytes("UTF-8")); + return hudson.remoting.Base64.encode(sig.sign()); + } catch (GeneralSecurityException e) { + throw new SecurityException(e); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); // UTF-8 is mandatory + } + } + + static final String SIGNING_ALGORITHM = "SHA256"; +} diff --git a/core/src/test/groovy/jenkins/security/RSAConfidentialKeyTest.groovy b/core/src/test/groovy/jenkins/security/RSAConfidentialKeyTest.groovy new file mode 100644 index 0000000000000000000000000000000000000000..ed9ef183fed5746c8f7ed285ce4446e0f11fdc05 --- /dev/null +++ b/core/src/test/groovy/jenkins/security/RSAConfidentialKeyTest.groovy @@ -0,0 +1,48 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security + +import org.junit.Rule +import org.junit.Test + +/** + * + * + * @author Kohsuke Kawaguchi + */ +class RSAConfidentialKeyTest { + @Rule + public ConfidentialStoreRule store = new ConfidentialStoreRule() + + def key = new RSAConfidentialKey("test") {} + + @Test + void loadingExistingKey() { + // this second key of the same ID will cause it to load the key from the disk + def key2 = new RSAConfidentialKey("test") {} + + assert key.privateKey==key2.privateKey; + assert key.publicKey ==key2.publicKey; + } +} diff --git a/core/src/test/groovy/jenkins/security/RSADigitalSignatureConfidentialKeyTest.groovy b/core/src/test/groovy/jenkins/security/RSADigitalSignatureConfidentialKeyTest.groovy new file mode 100644 index 0000000000000000000000000000000000000000..63df480550fb69a06cfd197384730137cc96875f --- /dev/null +++ b/core/src/test/groovy/jenkins/security/RSADigitalSignatureConfidentialKeyTest.groovy @@ -0,0 +1,54 @@ +/* + * The MIT License + * + * Copyright (c) 2015, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins.security + +import hudson.remoting.Base64 +import org.junit.Rule +import org.junit.Test + +import java.security.Signature + +/** + * + * + * @author Kohsuke Kawaguchi + */ +class RSADigitalSignatureConfidentialKeyTest { + @Rule + public ConfidentialStoreRule store = new ConfidentialStoreRule() + + def key = new RSADigitalSignatureConfidentialKey("test"); + + @Test + void dsigSignAndVerify() { + def msg = key.sign("Hello world"); + println msg; + + def sig = Signature.getInstance("SHA256withRSA"); + sig.initVerify(key.publicKey); + sig.update(msg.getBytes("UTF-8")); + + assert sig.verify(Base64.decode(msg)) + } +}