diff --git a/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/CertificateFormat.java b/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/CertificateFormat.java new file mode 100644 index 00000000000..f9315e9f1a2 --- /dev/null +++ b/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/CertificateFormat.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.certificate.api; + +/** + * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) + * @author GraviteeSource Team + */ +public interface CertificateFormat { + + String SSH_RSA = "SSH-RSA"; + String PEM = "PEM"; +} diff --git a/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/CertificateKey.java b/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/CertificateKey.java new file mode 100644 index 00000000000..dd7dcaf441c --- /dev/null +++ b/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/CertificateKey.java @@ -0,0 +1,66 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.certificate.api; + +import java.util.Map; + +/** + * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) + * @author GraviteeSource Team + */ +public class CertificateKey { + + private String fmt; + private String payload; + private Map metadata; + + public CertificateKey() { + } + + public CertificateKey(String fmt, String payload) { + this.fmt = fmt; + this.payload = payload; + } + + public CertificateKey(String fmt, String payload, Map metadata) { + this(fmt, payload); + this.metadata = metadata; + } + + public String getFmt() { + return fmt; + } + + public void setFmt(String fmt) { + this.fmt = fmt; + } + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } +} diff --git a/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/CertificateProvider.java b/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/CertificateProvider.java index 72973abfdeb..f182a47a5cb 100644 --- a/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/CertificateProvider.java +++ b/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/CertificateProvider.java @@ -19,6 +19,9 @@ import io.reactivex.Flowable; import io.reactivex.Single; +import java.util.Collections; +import java.util.List; + /** * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) * @author GraviteeSource Team @@ -34,4 +37,8 @@ public interface CertificateProvider { CertificateMetadata certificateMetadata(); String signatureAlgorithm(); + + default Single> publicKeys() { + return Single.just(Collections.emptyList()); + } } diff --git a/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/RSAKeyUtils.java b/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/RSAKeyUtils.java new file mode 100644 index 00000000000..5bbaefdd7b4 --- /dev/null +++ b/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/RSAKeyUtils.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.certificate.api; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.interfaces.RSAPublicKey; +import java.util.Base64; + +/** + * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) + * @author GraviteeSource Team + */ +public final class RSAKeyUtils { + + public static String toSSHRSAString(RSAPublicKey publicKey) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + /* encode the "ssh-rsa" string */ + byte[] sshrsa = new byte[]{0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a'}; + out.write(sshrsa); + /* Encode the public exponent */ + BigInteger e = publicKey.getPublicExponent(); + byte[] data = e.toByteArray(); + encodeUInt32(data.length, out); + out.write(data); + /* Encode the modulus */ + BigInteger m = publicKey.getModulus(); + data = m.toByteArray(); + encodeUInt32(data.length, out); + out.write(data); + return Base64.getEncoder().encodeToString(out.toByteArray()); + } + + private static void encodeUInt32(int value, OutputStream out) throws IOException { + byte[] tmp = new byte[4]; + tmp[0] = (byte)((value >>> 24) & 0xff); + tmp[1] = (byte)((value >>> 16) & 0xff); + tmp[2] = (byte)((value >>> 8) & 0xff); + tmp[3] = (byte)(value & 0xff); + out.write(tmp); + } +} diff --git a/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/X509CertUtils.java b/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/X509CertUtils.java new file mode 100644 index 00000000000..cb254cbc803 --- /dev/null +++ b/gravitee-am-certificate/gravitee-am-certificate-api/src/main/java/io/gravitee/am/certificate/api/X509CertUtils.java @@ -0,0 +1,209 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.certificate.api; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; + +/** + * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) + * @author GraviteeSource Team + */ +public final class X509CertUtils { + + private static final Base64.Encoder b64Enc = Base64.getEncoder(); + private static final Base64.Decoder b64Dec = Base64.getDecoder(); + + /** + * The PEM start marker. + */ + public static final String PEM_BEGIN_MARKER = "-----BEGIN CERTIFICATE-----"; + + /** + * The PEM end marker. + */ + public static final String PEM_END_MARKER = "-----END CERTIFICATE-----"; + + /** + * Parses a DER-encoded X.509 certificate. + * + * @param derEncodedCert The DER-encoded X.509 certificate, as a byte + * array. May be {@code null}. + * + * @return The X.509 certificate, {@code null} if not specified or + * parsing failed. + */ + public static X509Certificate parse(final byte[] derEncodedCert) { + + try { + return parseWithException(derEncodedCert); + } catch (CertificateException e) { + return null; + } + } + + + /** + * Parses a DER-encoded X.509 certificate with exception handling. + * + * @param derEncodedCert The DER-encoded X.509 certificate, as a byte + * array. Empty or {@code null} if not specified. + * + * @return The X.509 certificate, {@code null} if not specified. + * + * @throws CertificateException If parsing failed. + */ + public static X509Certificate parseWithException(final byte[] derEncodedCert) + throws CertificateException { + + if (derEncodedCert == null || derEncodedCert.length == 0) { + return null; + } + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + final java.security.cert.Certificate cert = cf.generateCertificate(new ByteArrayInputStream(derEncodedCert)); + + if (! (cert instanceof X509Certificate)) { + throw new CertificateException("Not a X.509 certificate: " + cert.getType()); + } + + return (X509Certificate)cert; + } + + + /** + * Parses a PEM-encoded X.509 certificate. + * + * @param pemEncodedCert The PEM-encoded X.509 certificate, as a + * string. Empty or {@code null} if not + * specified. + * + * @return The X.509 certificate, {@code null} if parsing failed. + */ + public static X509Certificate parse(final String pemEncodedCert) { + + if (pemEncodedCert == null || pemEncodedCert.isEmpty()) { + return null; + } + + final int markerStart = pemEncodedCert.indexOf(PEM_BEGIN_MARKER); + + if (markerStart < 0) { + return null; + } + + String buf = pemEncodedCert.substring(markerStart + PEM_BEGIN_MARKER.length()); + + final int markerEnd = buf.indexOf(PEM_END_MARKER); + + if (markerEnd < 0) { + return null; + } + + buf = buf.substring(0, markerEnd); + + buf = buf.replaceAll("\\s", ""); + + return parse(b64Dec.decode(buf)); + } + + + /** + * Parses a PEM-encoded X.509 certificate with exception handling. + * + * @param pemEncodedCert The PEM-encoded X.509 certificate, as a + * string. Empty or {@code null} if not + * specified. + * + * @return The X.509 certificate, {@code null} if parsing failed. + */ + public static X509Certificate parseWithException(final String pemEncodedCert) + throws CertificateException { + + if (pemEncodedCert == null || pemEncodedCert.isEmpty()) { + return null; + } + + final int markerStart = pemEncodedCert.indexOf(PEM_BEGIN_MARKER); + + if (markerStart < 0) { + throw new CertificateException("PEM begin marker not found"); + } + + String buf = pemEncodedCert.substring(markerStart + PEM_BEGIN_MARKER.length()); + + final int markerEnd = buf.indexOf(PEM_END_MARKER); + + if (markerEnd < 0) { + throw new CertificateException("PEM end marker not found"); + } + + buf = buf.substring(0, markerEnd); + + buf = buf.replaceAll("\\s", ""); + + return parseWithException(b64Dec.decode(buf)); + } + + + /** + * Returns the specified X.509 certificate as PEM-encoded string. + * + * @param cert The X.509 certificate. Must not be {@code null}. + * + * @return The PEM-encoded X.509 certificate, {@code null} if encoding + * failed. + */ + public static String toPEMString(final X509Certificate cert) { + return toPEMString(cert, true); + } + + + /** + * Returns the specified X.509 certificate as PEM-encoded string. + * + * @param cert The X.509 certificate. Must not be + * {@code null}. + * @param withLineBreaks {@code false} to suppress line breaks. + * + * @return The PEM-encoded X.509 certificate, {@code null} if encoding + * failed. + */ + public static String toPEMString(final X509Certificate cert, final boolean withLineBreaks) { + + StringBuilder sb = new StringBuilder(); + sb.append(PEM_BEGIN_MARKER); + + if (withLineBreaks) + sb.append('\n'); + + try { + sb.append(b64Enc.encodeToString(cert.getEncoded())); + } catch (CertificateEncodingException e) { + return null; + } + + if (withLineBreaks) + sb.append('\n'); + + sb.append(PEM_END_MARKER); + return sb.toString(); + } +} diff --git a/gravitee-am-certificate/gravitee-am-certificate-javakeystore/src/main/java/io/gravitee/am/certificate/javakeystore/provider/JavaKeyStoreProvider.java b/gravitee-am-certificate/gravitee-am-certificate-javakeystore/src/main/java/io/gravitee/am/certificate/javakeystore/provider/JavaKeyStoreProvider.java index f2ac748a7ad..330bbcf3569 100644 --- a/gravitee-am-certificate/gravitee-am-certificate-javakeystore/src/main/java/io/gravitee/am/certificate/javakeystore/provider/JavaKeyStoreProvider.java +++ b/gravitee-am-certificate/gravitee-am-certificate-javakeystore/src/main/java/io/gravitee/am/certificate/javakeystore/provider/JavaKeyStoreProvider.java @@ -16,9 +16,7 @@ package io.gravitee.am.certificate.javakeystore.provider; import com.nimbusds.jose.jwk.JWKSet; -import io.gravitee.am.certificate.api.CertificateMetadata; -import io.gravitee.am.certificate.api.CertificateProvider; -import io.gravitee.am.certificate.api.DefaultKey; +import io.gravitee.am.certificate.api.*; import io.gravitee.am.certificate.javakeystore.JavaKeyStoreConfiguration; import io.gravitee.am.common.jwt.SignatureAlgorithm; import io.gravitee.am.model.jose.JWK; @@ -28,15 +26,16 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; -import java.io.*; -import java.math.BigInteger; -import java.security.*; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPublicKey; -import java.util.Base64; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -48,10 +47,10 @@ public class JavaKeyStoreProvider implements CertificateProvider, InitializingBe private KeyPair keyPair; private JWKSet jwkSet; - private String publicKey; private Set keys; private SignatureAlgorithm signature = SignatureAlgorithm.RS256; private io.gravitee.am.certificate.api.Key certificateKey; + private List certificateKeys; @Autowired private JavaKeyStoreConfiguration configuration; @@ -64,33 +63,36 @@ public void afterPropertiesSet() throws Exception { Object file = certificateMetadata.getMetadata().get(CertificateMetadata.FILE); Objects.requireNonNull(file, "A jks file is required to use Java KeyStore certificate"); - byte[] jksFile = (byte[]) file; - InputStream is = new ByteArrayInputStream(jksFile); - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(is, configuration.getStorepass().toCharArray()); - // generate JWK set - jwkSet = JWKSet.load(keystore, name -> configuration.getKeypass().toCharArray()); - keys = getKeys(); - // generate Key pair - Key key = keystore.getKey(configuration.getAlias(), configuration.getKeypass().toCharArray()); - if (key instanceof PrivateKey) { - // Get certificate of public key - Certificate cert = keystore.getCertificate(configuration.getAlias()); - // Get Signing Algorithm name - if (cert instanceof X509Certificate) { - signature = getSignature(((X509Certificate) cert).getSigAlgName()); + try (InputStream is = new ByteArrayInputStream((byte[]) file)) { + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, configuration.getStorepass().toCharArray()); + // generate JWK set + // TODO : should be moved to the gravitee-am-jwt module + jwkSet = JWKSet.load(keystore, name -> configuration.getKeypass().toCharArray()); + keys = getKeys(); + // generate Key pair + Key key = keystore.getKey(configuration.getAlias(), configuration.getKeypass().toCharArray()); + if (key instanceof PrivateKey) { + // Get certificate of public key + Certificate cert = keystore.getCertificate(configuration.getAlias()); + // create key pair + keyPair = new KeyPair(cert.getPublicKey(), (PrivateKey) key); + // create key + certificateKey = new DefaultKey(configuration.getAlias(), keyPair); + // update metadata + certificateMetadata.getMetadata().put(CertificateMetadata.DIGEST_ALGORITHM_NAME, signature.getDigestName()); + // generate public certificate keys + certificateKeys = new ArrayList<>(); + // get Signing Algorithm name + if (cert instanceof X509Certificate) { + signature = getSignature(((X509Certificate) cert).getSigAlgName()); + String pem = X509CertUtils.toPEMString((X509Certificate) cert); + certificateKeys.add(new CertificateKey(CertificateFormat.PEM, pem)); + } + certificateKeys.add(new CertificateKey(CertificateFormat.SSH_RSA, RSAKeyUtils.toSSHRSAString((RSAPublicKey) keyPair.getPublic()))); + } else { + throw new IllegalArgumentException("A RSA Signer must be supplied"); } - certificateMetadata.getMetadata().put(CertificateMetadata.DIGEST_ALGORITHM_NAME, signature.getDigestName()); - // Get public key - PublicKey publicKey = cert.getPublicKey(); - // create key pair - keyPair = new KeyPair(publicKey, (PrivateKey) key); - // create key - certificateKey = new DefaultKey(configuration.getAlias(), keyPair); - // get public key - this.publicKey = getPublicKey(); - } else { - throw new IllegalArgumentException("A RSA Signer must be supplied"); } } @@ -101,7 +103,19 @@ public Single key() { @Override public Single publicKey() { - return Single.just(publicKey); + // fallback to ssh-rsa + return Single.just( + certificateKeys + .stream() + .filter(c -> c.getFmt().equals(CertificateFormat.SSH_RSA)) + .map(CertificateKey::getPayload) + .findFirst() + .get()); + } + + @Override + public Single> publicKeys() { + return Single.just(certificateKeys); } @Override @@ -114,37 +128,11 @@ public CertificateMetadata certificateMetadata() { return certificateMetadata; } - private String getPublicKey() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - /* encode the "ssh-rsa" string */ - byte[] sshrsa = new byte[]{0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a'}; - out.write(sshrsa); - /* Encode the public exponent */ - BigInteger e = ((RSAPublicKey) keyPair.getPublic()).getPublicExponent(); - byte[] data = e.toByteArray(); - encodeUInt32(data.length, out); - out.write(data); - /* Encode the modulus */ - BigInteger m = ((RSAPublicKey) keyPair.getPublic()).getModulus(); - data = m.toByteArray(); - encodeUInt32(data.length, out); - out.write(data); - return Base64.getEncoder().encodeToString(out.toByteArray()); - } - private Set getKeys() { return jwkSet.toPublicJWKSet().getKeys().stream().map(this::convert).collect(Collectors.toSet()); } - private void encodeUInt32(int value, OutputStream out) throws IOException { - byte[] tmp = new byte[4]; - tmp[0] = (byte)((value >>> 24) & 0xff); - tmp[1] = (byte)((value >>> 16) & 0xff); - tmp[2] = (byte)((value >>> 8) & 0xff); - tmp[3] = (byte)(value & 0xff); - out.write(tmp); - } - + // TODO : should be moved to the gravitee-am-jwt module private JWK convert(com.nimbusds.jose.jwk.JWK nimbusJwk) { RSAKey jwk = new RSAKey(); if (nimbusJwk.getKeyType() != null) { diff --git a/gravitee-am-certificate/gravitee-am-certificate-pkcs12/src/main/java/io/gravitee/am/certificate/pkcs12/provider/PKCS12Provider.java b/gravitee-am-certificate/gravitee-am-certificate-pkcs12/src/main/java/io/gravitee/am/certificate/pkcs12/provider/PKCS12Provider.java index 4a24d0f018a..1b20ca5b2f4 100644 --- a/gravitee-am-certificate/gravitee-am-certificate-pkcs12/src/main/java/io/gravitee/am/certificate/pkcs12/provider/PKCS12Provider.java +++ b/gravitee-am-certificate/gravitee-am-certificate-pkcs12/src/main/java/io/gravitee/am/certificate/pkcs12/provider/PKCS12Provider.java @@ -16,9 +16,7 @@ package io.gravitee.am.certificate.pkcs12.provider; import com.nimbusds.jose.jwk.JWKSet; -import io.gravitee.am.certificate.api.CertificateMetadata; -import io.gravitee.am.certificate.api.CertificateProvider; -import io.gravitee.am.certificate.api.DefaultKey; +import io.gravitee.am.certificate.api.*; import io.gravitee.am.certificate.pkcs12.PKCS12Configuration; import io.gravitee.am.common.jwt.SignatureAlgorithm; import io.gravitee.am.model.jose.JWK; @@ -28,15 +26,16 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; -import java.io.*; -import java.math.BigInteger; -import java.security.*; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPublicKey; -import java.util.Base64; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -50,10 +49,10 @@ public class PKCS12Provider implements CertificateProvider, InitializingBean { private KeyPair keyPair; private JWKSet jwkSet; - private String publicKey; private Set keys; private SignatureAlgorithm signature = SignatureAlgorithm.RS256; private io.gravitee.am.certificate.api.Key certificateKey; + private List certificateKeys; @Autowired private PKCS12Configuration configuration; @@ -71,6 +70,7 @@ public void afterPropertiesSet() throws Exception { keystore.load(is, configuration.getStorepass().toCharArray()); // generate JWK set + // TODO : should be moved to the gravitee-am-jwt module jwkSet = JWKSet.load(keystore, name -> configuration.getKeypass().toCharArray()); keys = getKeys(); // generate Key pair @@ -78,19 +78,21 @@ public void afterPropertiesSet() throws Exception { if (key instanceof PrivateKey) { // Get certificate of public key Certificate cert = keystore.getCertificate(configuration.getAlias()); + // create key pair + keyPair = new KeyPair(cert.getPublicKey(), (PrivateKey) key); + // create key + certificateKey = new DefaultKey(configuration.getAlias(), keyPair); + // update metadata + certificateMetadata.getMetadata().put(CertificateMetadata.DIGEST_ALGORITHM_NAME, signature.getDigestName()); + // generate public certificate keys + certificateKeys = new ArrayList<>(); // Get Signing Algorithm name if (cert instanceof X509Certificate) { signature = getSignature(((X509Certificate) cert).getSigAlgName()); + String pem = X509CertUtils.toPEMString((X509Certificate) cert); + certificateKeys.add(new CertificateKey(CertificateFormat.PEM, pem)); } - certificateMetadata.getMetadata().put(CertificateMetadata.DIGEST_ALGORITHM_NAME, signature.getDigestName()); - // Get public key - PublicKey publicKey = cert.getPublicKey(); - // create key pair - keyPair = new KeyPair(publicKey, (PrivateKey) key); - // create key - certificateKey = new DefaultKey(configuration.getAlias(), keyPair); - // get public key - this.publicKey = getPublicKey(); + certificateKeys.add(new CertificateKey(CertificateFormat.SSH_RSA, RSAKeyUtils.toSSHRSAString((RSAPublicKey) keyPair.getPublic()))); } else { throw new IllegalArgumentException("A RSA Signer must be supplied"); } @@ -104,7 +106,19 @@ public Single key() { @Override public Single publicKey() { - return Single.just(publicKey); + // fallback to ssh-rsa + return Single.just( + certificateKeys + .stream() + .filter(c -> c.getFmt().equals(CertificateFormat.SSH_RSA)) + .map(CertificateKey::getPayload) + .findFirst() + .get()); + } + + @Override + public Single> publicKeys() { + return Single.just(certificateKeys); } @Override @@ -117,37 +131,11 @@ public CertificateMetadata certificateMetadata() { return certificateMetadata; } - private String getPublicKey() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - /* encode the "ssh-rsa" string */ - byte[] sshrsa = new byte[]{0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a'}; - out.write(sshrsa); - /* Encode the public exponent */ - BigInteger e = ((RSAPublicKey) keyPair.getPublic()).getPublicExponent(); - byte[] data = e.toByteArray(); - encodeUInt32(data.length, out); - out.write(data); - /* Encode the modulus */ - BigInteger m = ((RSAPublicKey) keyPair.getPublic()).getModulus(); - data = m.toByteArray(); - encodeUInt32(data.length, out); - out.write(data); - return Base64.getEncoder().encodeToString(out.toByteArray()); - } - private Set getKeys() { return jwkSet.toPublicJWKSet().getKeys().stream().map(this::convert).collect(Collectors.toSet()); } - private void encodeUInt32(int value, OutputStream out) throws IOException { - byte[] tmp = new byte[4]; - tmp[0] = (byte)((value >>> 24) & 0xff); - tmp[1] = (byte)((value >>> 16) & 0xff); - tmp[2] = (byte)((value >>> 8) & 0xff); - tmp[3] = (byte)(value & 0xff); - out.write(tmp); - } - + // TODO : should be moved to the gravitee-am-jwt module private JWK convert(com.nimbusds.jose.jwk.JWK nimbusJwk) { RSAKey jwk = new RSAKey(); if (nimbusJwk.getKeyType() != null) { diff --git a/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/environments/domains/CertificateResource.java b/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/environments/domains/CertificateResource.java index 57935199fd1..fd8e72210e2 100644 --- a/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/environments/domains/CertificateResource.java +++ b/gravitee-am-management-api/gravitee-am-management-api-rest/src/main/java/io/gravitee/am/management/handlers/management/api/resources/organizations/environments/domains/CertificateResource.java @@ -15,6 +15,7 @@ */ package io.gravitee.am.management.handlers.management.api.resources.organizations.environments.domains; +import io.gravitee.am.certificate.api.CertificateKey; import io.gravitee.am.certificate.api.CertificateProvider; import io.gravitee.am.identityprovider.api.User; import io.gravitee.am.management.handlers.management.api.resources.AbstractResource; @@ -116,6 +117,31 @@ public void getPublicKey( .subscribe(response::resume, response::resume); } + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("keys") + @ApiOperation(value = "Get the certificate public keys", + notes = "User must have the DOMAIN[READ] permission on the specified domain " + + "or DOMAIN[READ] permission on the specified environment " + + "or DOMAIN[READ] permission on the specified organization") + @ApiResponses({ + @ApiResponse(code = 200, message = "Certificate keys successfully fetched", response = CertificateKey.class, responseContainer = "List"), + @ApiResponse(code = 500, message = "Internal server error")}) + public void getPublicKeys( + @PathParam("organizationId") String organizationId, + @PathParam("environmentId") String environmentId, + @PathParam("domain") String domain, + @PathParam("certificate") String certificate, + @Suspended final AsyncResponse response) { + + // FIXME: should we create a DOMAIN_CERTIFICATE_KEY permission instead ? + checkAnyPermission(organizationId, environmentId, domain, Permission.DOMAIN, Acl.READ) + .andThen(certificateManager.getCertificateProvider(certificate) + .switchIfEmpty(Maybe.error(new BadRequestException("No certificate provider found for the certificate " + certificate))) + .flatMapSingle(CertificateProvider::publicKeys)) + .subscribe(response::resume, response::resume); + } + @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) diff --git a/gravitee-am-ui/src/app/domain/applications/application/advanced/certificates/certificates.component.html b/gravitee-am-ui/src/app/domain/applications/application/advanced/certificates/certificates.component.html index c2f7008b329..2ae027f7795 100644 --- a/gravitee-am-ui/src/app/domain/applications/application/advanced/certificates/certificates.component.html +++ b/gravitee-am-ui/src/app/domain/applications/application/advanced/certificates/certificates.component.html @@ -31,11 +31,15 @@
Certificate
- - Public key - - - +
+
+ + {{certificatePublicKey.fmt}} + + + +
+
{ this.application = data; this.route.snapshot.data['application'] = this.application; - this.certificatePublicKey = null; + this.certificatePublicKeys = []; this.formChanged = false; this.snackbarService.open('Application updated'); if (this.application.certificate) { - this.publicKey(this.application.certificate); + this.publicKeys(this.application.certificate); } }); } @@ -69,9 +69,9 @@ export class ApplicationCertificatesComponent implements OnInit { this.snackbarService.open(message); } - private publicKey(certificateId) { - this.certificateService.publicKey(this.domainId, certificateId).subscribe(response => { - this.certificatePublicKey = response; + private publicKeys(certificateId) { + this.certificateService.publicKeys(this.domainId, certificateId).subscribe(response => { + this.certificatePublicKeys = response; }); } } diff --git a/gravitee-am-ui/src/app/domain/settings/certificates/certificates.component.ts b/gravitee-am-ui/src/app/domain/settings/certificates/certificates.component.ts index 67296a03f99..e4b384ab528 100644 --- a/gravitee-am-ui/src/app/domain/settings/certificates/certificates.component.ts +++ b/gravitee-am-ui/src/app/domain/settings/certificates/certificates.component.ts @@ -55,10 +55,10 @@ export class DomainSettingsCertificatesComponent implements OnInit { publicKey(id, event) { event.preventDefault(); - this.certificateService.publicKey(this.domainId, id).subscribe(response => { + this.certificateService.publicKeys(this.domainId, id).subscribe(response => { this.openPublicKeyInfo(response, false); }, error => { - this.openPublicKeyInfo('Failed to load public key', true); + this.openPublicKeyInfo([], true); }); } @@ -90,10 +90,10 @@ export class DomainSettingsCertificatesComponent implements OnInit { }); } - openPublicKeyInfo(publicKey, error) { - const dialogRef = this.dialog.open(CertitificatePublicKeyDialog); + openPublicKeyInfo(publicKeys, error) { + const dialogRef = this.dialog.open(CertitificatePublicKeyDialog, { width : '700px' }); dialogRef.componentInstance.title = 'Public certificate key'; - dialogRef.componentInstance.message = publicKey; + dialogRef.componentInstance.certificateKeys = publicKeys; dialogRef.componentInstance.error = error; } } @@ -105,12 +105,12 @@ export class DomainSettingsCertificatesComponent implements OnInit { }) export class CertitificatePublicKeyDialog { public title: string; - public message: string; + public certificateKeys: any[] = []; public error: boolean; constructor(public dialogRef: MatDialogRef, private snackbarService: SnackbarService) {} - keyCopied() { - this.snackbarService.open('Key copied to the clipboard'); + valueCopied(message) { + this.snackbarService.open(message); } } diff --git a/gravitee-am-ui/src/app/domain/settings/certificates/dialog/public-key.component.html b/gravitee-am-ui/src/app/domain/settings/certificates/dialog/public-key.component.html index 30005624429..cc0e4185d4b 100644 --- a/gravitee-am-ui/src/app/domain/settings/certificates/dialog/public-key.component.html +++ b/gravitee-am-ui/src/app/domain/settings/certificates/dialog/public-key.component.html @@ -18,10 +18,15 @@

{{title}}

-

{{message}}

-
-
{{ message }}
- +

Failed to load public key

+
+
+ + {{certificatePublicKey.fmt}} + + + +
diff --git a/gravitee-am-ui/src/app/services/certificate.service.ts b/gravitee-am-ui/src/app/services/certificate.service.ts index 774a00eabc9..89cbb3a1d90 100644 --- a/gravitee-am-ui/src/app/services/certificate.service.ts +++ b/gravitee-am-ui/src/app/services/certificate.service.ts @@ -47,8 +47,8 @@ export class CertificateService { return this.http.delete(this.certificatesURL + domainId + "/certificates/" + id); } - publicKey(domainId, id): Observable { - return this.http.get(this.certificatesURL + domainId + "/certificates/" + id + "/key", {responseType: 'text'}); + publicKeys(domainId, id): Observable { + return this.http.get(this.certificatesURL + domainId + "/certificates/" + id + "/keys"); } }