Alon Bar-Lev has uploaded a new change for review. Change subject: uutil: add universal ticketing utilities ......................................................................
uutil: add universal ticketing utilities base is the websocket proxy ticket, with symmetric operations in both java and python. a new certificate field was added to allow x509 trust with eku without need to specify certificate at decoder. removal of insecure mode as not required after bootstrapping the feature. library is useful for: 1. websocket proxy 2. serial console proxy 3. sso Change-Id: Ibc87ea4a4ea540dfea09e40f4396d6918e34e019 Signed-off-by: Alon Bar-Lev <[email protected]> --- M backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/SignStringQuery.java D backend/manager/modules/utils/src/main/java/org/ovirt/engine/core/utils/crypt/TicketUtils.java M backend/manager/modules/uutils/pom.xml A backend/manager/modules/uutils/src/main/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketDecoder.java A backend/manager/modules/uutils/src/main/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketEncoder.java A backend/manager/modules/uutils/src/test/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketTest.java A backend/manager/modules/uutils/src/test/resources/ticket/ca1.jks A backend/manager/modules/uutils/src/test/resources/ticket/ca1test1.p12 A backend/manager/modules/uutils/src/test/resources/ticket/ca1test2.p12 A backend/manager/modules/uutils/src/test/resources/ticket/ca2.jks M ovirt-engine.spec.in A packaging/pythonlib/ovirt_engine/ticket.py M packaging/services/ovirt-websocket-proxy/ovirt-websocket-proxy.conf.in M packaging/services/ovirt-websocket-proxy/ovirt-websocket-proxy.py M packaging/setup/plugins/ovirt-engine-setup/websocket_proxy/config.py 15 files changed, 542 insertions(+), 174 deletions(-) git pull ssh://gerrit.ovirt.org:29418/ovirt-engine refs/changes/01/36401/1 diff --git a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/SignStringQuery.java b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/SignStringQuery.java index 3ceb146..ba2e39d 100644 --- a/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/SignStringQuery.java +++ b/backend/manager/modules/bll/src/main/java/org/ovirt/engine/core/bll/SignStringQuery.java @@ -1,9 +1,17 @@ package org.ovirt.engine.core.bll; +import org.ovirt.engine.core.common.config.Config; +import org.ovirt.engine.core.common.config.ConfigValues; import org.ovirt.engine.core.common.queries.SignStringParameters; -import org.ovirt.engine.core.utils.crypt.TicketUtils; +import org.ovirt.engine.core.utils.crypt.EngineEncryptionUtils; +import org.ovirt.engine.core.uutils.crypto.ticket.TicketEncoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class SignStringQuery<P extends SignStringParameters> extends QueriesCommandBase<P> { + + private static final Logger log = LoggerFactory.getLogger(SignStringQuery.class); + public SignStringQuery(P parameters) { super(parameters); } @@ -13,14 +21,17 @@ getQueryReturnValue().setSucceeded(false); try { - TicketUtils ticketUtils = TicketUtils.getInstanceForEngineStoreSigning(); - - String ticket = ticketUtils.generateTicket(getParameters().getString()); - getQueryReturnValue().setReturnValue(ticket); - + getQueryReturnValue().setReturnValue( + new TicketEncoder( + EngineEncryptionUtils.getPrivateKeyEntry().getCertificate(), + EngineEncryptionUtils.getPrivateKeyEntry().getPrivateKey(), + Config.<Integer> getValue (ConfigValues.WebSocketProxyTicketValiditySeconds) + ).encode(getParameters().getString()) + ); getQueryReturnValue().setSucceeded(true); } catch (Exception e) { - log.error("Error when signing string: " + e.getMessage()); + log.error("Ticket encoding failed: {}", e.getMessage()); + log.debug("Exception", e); } } diff --git a/backend/manager/modules/utils/src/main/java/org/ovirt/engine/core/utils/crypt/TicketUtils.java b/backend/manager/modules/utils/src/main/java/org/ovirt/engine/core/utils/crypt/TicketUtils.java deleted file mode 100644 index fb1c7a3..0000000 --- a/backend/manager/modules/utils/src/main/java/org/ovirt/engine/core/utils/crypt/TicketUtils.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.ovirt.engine.core.utils.crypt; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.SecureRandom; -import java.security.Signature; -import java.security.SignatureException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.TimeZone; - -import org.apache.commons.codec.binary.Base64; -import org.ovirt.engine.core.common.config.Config; -import org.ovirt.engine.core.common.config.ConfigValues; - -public class TicketUtils { - - private PrivateKey key; - private final int lifetime; - - /** - * Creates a TicketUtils instance for - */ - public static TicketUtils getInstanceForEngineStoreSigning() throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { - return new TicketUtils( - EngineEncryptionUtils.getPrivateKeyEntry().getPrivateKey(), - Config.<Integer> getValue (ConfigValues.WebSocketProxyTicketValiditySeconds) - ); - } - - public TicketUtils(PrivateKey key, int lifetime) { - this.lifetime = lifetime; - this.key = key; - } - - public String generateTicket(String data) - throws - NoSuchAlgorithmException, - SignatureException, - InvalidKeyException { - Base64 base64 = new Base64(0); - Map<String, String> map = new HashMap<String, String>(); - - /* - * Add signed fields - */ - byte[] random = new byte[8]; - SecureRandom.getInstance("SHA1PRNG").nextBytes(random); - map.put("salt", base64.encodeToString(random)); - - SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss"); - df.setTimeZone(TimeZone.getTimeZone("UTC")); - map.put( - "validFrom", - df.format(new Date()) - ); - - Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - cal.add(Calendar.SECOND, lifetime); - map.put( - "validTo", - df.format(cal.getTime()) - ); - - map.put("data", data); - - /* - * Calculate signature on fields in map - */ - Signature signature = Signature.getInstance("SHA1withRSA"); - signature.initSign(key); - StringBuilder fields = new StringBuilder(); - for (Map.Entry<String, String> entry : map.entrySet()) { - if (fields.length() > 0) { - fields.append(","); - } - fields.append(entry.getKey()); - signature.update(entry.getValue().getBytes(Charset.forName("UTF-8"))); - } - - /* - * Add unsigned fields - */ - map.put("signedFields", fields.toString()); - map.put("signature", base64.encodeToString(signature.sign())); - - /* - * Create json - */ - StringBuilder ret = new StringBuilder(); - ret.append("{"); - boolean first = true; - for (Map.Entry<String, String> entry : map.entrySet()) { - if (first) { - first = false; - } - else { - ret.append(","); - } - ret.append(String.format("\"%s\":\"%s\"", entry.getKey(), entry.getValue())); - } - ret.append("}"); - - return base64.encodeToString(ret.toString().getBytes(Charset.forName("UTF-8"))); - } - -} - diff --git a/backend/manager/modules/uutils/pom.xml b/backend/manager/modules/uutils/pom.xml index 18dd723..c0ba828 100644 --- a/backend/manager/modules/uutils/pom.xml +++ b/backend/manager/modules/uutils/pom.xml @@ -36,6 +36,11 @@ <scope>provided</scope> </dependency> + <dependency> + <groupId>org.codehaus.jackson</groupId> + <artifactId>jackson-mapper-asl</artifactId> + </dependency> + </dependencies> <build> diff --git a/backend/manager/modules/uutils/src/main/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketDecoder.java b/backend/manager/modules/uutils/src/main/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketDecoder.java new file mode 100644 index 0000000..be074d3 --- /dev/null +++ b/backend/manager/modules/uutils/src/main/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketDecoder.java @@ -0,0 +1,129 @@ +package org.ovirt.engine.core.uutils.crypto.ticket; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; + +import org.apache.commons.codec.binary.Base64; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.type.TypeFactory; +import org.ovirt.engine.core.uutils.crypto.CertificateChain; + +public class TicketDecoder { + + private static final String DATE_FORMAT = "yyyyMMddHHmmss"; + + private final Set<TrustAnchor> trustAnchors; + private final String eku; + private final Certificate peer; + private final int tollerance; + + public TicketDecoder(KeyStore trustStore, String eku, Certificate peer, int tollerance) throws KeyStoreException { + if (trustStore == null) { + trustAnchors = null; + } else { + trustAnchors = CertificateChain.keyStoreToTrustAnchors(trustStore); + } + this.eku = eku; + this.peer = peer; + this.tollerance = tollerance; + } + + public TicketDecoder(KeyStore trustStore, String eku, Certificate peer) throws KeyStoreException { + this(trustStore, eku, peer, 0); + } + + public TicketDecoder(KeyStore trustStore, String eku, int tollerance) throws KeyStoreException { + this(trustStore, eku, null, tollerance); + } + + public TicketDecoder(Certificate peer, int tollerance) throws KeyStoreException { + this(null, null, peer, tollerance); + } + + public TicketDecoder(KeyStore trustStore, String eku) throws KeyStoreException { + this(trustStore, eku, null); + } + + public TicketDecoder(Certificate peer) throws KeyStoreException { + this(null, null, peer); + } + + public String decode(String ticket) + throws GeneralSecurityException, IOException { + Certificate cert; + + Map<String, String> map = new ObjectMapper().readValue( + Base64.decodeBase64(ticket), + TypeFactory.defaultInstance().constructMapType(HashMap.class, String.class, String.class) + ); + + if (peer != null) { + cert = peer; + } else { + try (InputStream is = new ByteArrayInputStream(map.get("certificate").getBytes(Charset.forName("UTF-8")))) { + cert = CertificateFactory.getInstance("X.509").generateCertificate(is); + } + } + + if (trustAnchors != null) { + CertificateChain.buildCertPath(Arrays.asList(cert), trustAnchors); + } + + if (eku != null) { + if (!((X509Certificate)cert).getExtendedKeyUsage().contains(eku)) { + throw new GeneralSecurityException("Certificate is not authorized for action"); + } + } + + List<String> signedFields = Arrays.asList(map.get("signedFields").trim().split("\\s*,\\s*")); + if (!signedFields.containsAll(Arrays.asList("salt", "data"))) { + throw new GeneralSecurityException("Invalid ticket"); + } + + Signature sig = Signature.getInstance(String.format("%swith%s", map.get("digest"), cert.getPublicKey().getAlgorithm())); + sig.initVerify(cert.getPublicKey()); + for (String field : signedFields) { + byte[] buf = map.get(field).getBytes(Charset.forName("UTF-8")); + sig.update(buf); + } + if (!sig.verify(Base64.decodeBase64(map.get("signature")))) { + throw new GeneralSecurityException("Invalid ticket signature"); + } + + try { + DateFormat df = new SimpleDateFormat(DATE_FORMAT); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + Date validFrom = df.parse(map.get("validFrom")); + Date validTo = df.parse(map.get("validTo")); + Date now = new Date(); + if (! (validFrom.getTime() - tollerance <= now.getTime() && now.getTime() <= validTo.getTime() + tollerance)) { + throw new GeneralSecurityException("Ticket lifetime expired"); + } + } catch (ParseException e) { + throw new GeneralSecurityException(e); + } + + return map.get("data"); + } + +} diff --git a/backend/manager/modules/uutils/src/main/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketEncoder.java b/backend/manager/modules/uutils/src/main/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketEncoder.java new file mode 100644 index 0000000..3e7f67f --- /dev/null +++ b/backend/manager/modules/uutils/src/main/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketEncoder.java @@ -0,0 +1,97 @@ +package org.ovirt.engine.core.uutils.crypto.ticket; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.cert.Certificate; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +import org.apache.commons.codec.binary.Base64; +import org.codehaus.jackson.map.ObjectMapper; + +public class TicketEncoder { + + private static final String DATE_FORMAT = "yyyyMMddHHmmss"; + + private Certificate cert; + private PrivateKey key; + private final int lifetime; + + public TicketEncoder(Certificate cert, PrivateKey key, int lifetime) { + this.lifetime = lifetime; + this.cert = cert; + this.key = key; + } + + public TicketEncoder(Certificate cert, PrivateKey key) { + this(cert, key, 5); + } + + public String encode(String data) + throws GeneralSecurityException, IOException { + + Base64 base64 = new Base64(0); + Map<String, String> map = new HashMap<String, String>(); + + byte[] random = new byte[8]; + SecureRandom.getInstance("SHA1PRNG").nextBytes(random); + map.put("salt", base64.encodeToString(random)); + map.put("digest", "sha1"); + + DateFormat df = new SimpleDateFormat(DATE_FORMAT); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + map.put( + "validFrom", + df.format(new Date()) + ); + + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.add(Calendar.SECOND, lifetime); + map.put( + "validTo", + df.format(cal.getTime()) + ); + + map.put("data", data); + + /* + * Calculate signature on fields in map + */ + Signature signature = Signature.getInstance(String.format("%swith%s", map.get("digest"), key.getAlgorithm())); + signature.initSign(key); + StringBuilder fields = new StringBuilder(); + for (Map.Entry<String, String> entry : map.entrySet()) { + if (fields.length() > 0) { + fields.append(","); + } + fields.append(entry.getKey()); + signature.update(entry.getValue().getBytes(Charset.forName("UTF-8"))); + } + + /* + * Add unsigned fields + */ + map.put("signedFields", fields.toString()); + map.put("signature", base64.encodeToString(signature.sign())); + map.put("certificate", String.format( + ( + "-----BEGIN CERTIFICATE-----\n" + + "%s" + + "-----END CERTIFICATE-----\n" + ), + new Base64(76).encodeToString(cert.getEncoded()) + )); + + return base64.encodeToString(new ObjectMapper().writeValueAsString(map).getBytes(Charset.forName("UTF-8"))); + } +} + diff --git a/backend/manager/modules/uutils/src/test/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketTest.java b/backend/manager/modules/uutils/src/test/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketTest.java new file mode 100644 index 0000000..6c39a55 --- /dev/null +++ b/backend/manager/modules/uutils/src/test/java/org/ovirt/engine/core/uutils/crypto/ticket/TicketTest.java @@ -0,0 +1,159 @@ +package org.ovirt.engine.core.uutils.crypto.ticket; + +import static org.junit.Assert.assertEquals; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.codec.binary.Base64; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.type.TypeFactory; +import org.junit.Test; + +public class TicketTest { + + private static KeyStore getKeyStore(String storeType, String store, String password) throws Exception { + KeyStore ks = KeyStore.getInstance(storeType); + try (InputStream is = ClassLoader.getSystemResourceAsStream(store)) { + ks.load(is, password.toCharArray()); + } + return ks; + } + + private static KeyStore.PrivateKeyEntry getPrivateKeyEntry(KeyStore ks, String alias, String password) throws Exception { + return (KeyStore.PrivateKeyEntry)ks.getEntry(alias, new KeyStore.PasswordProtection(password.toCharArray())); + } + + @Test + public void testByCertificate() throws Exception { + final String content = "testByCertificate"; + KeyStore.PrivateKeyEntry entry = getPrivateKeyEntry(getKeyStore("PKCS12", "ticket/ca1test1.p12", "test1"), "1", "test1"); + String ticket = new TicketEncoder(entry.getCertificate(), entry.getPrivateKey(), 30).encode(content); + assertEquals(content, new TicketDecoder(entry.getCertificate()).decode(ticket)); + assertEquals( + content, + new TicketDecoder( + getKeyStore("JKS", "ticket/ca1.jks", "changeit"), + null, + entry.getCertificate() + ).decode(ticket) + ); + } + + @Test(expected=GeneralSecurityException.class) + public void testByCertificateFailCertificate() throws Exception { + final String content = "testByCertificate"; + KeyStore.PrivateKeyEntry entry1 = getPrivateKeyEntry(getKeyStore("PKCS12", "ticket/ca1test1.p12", "test1"), "1", "test1"); + KeyStore.PrivateKeyEntry entry2 = getPrivateKeyEntry(getKeyStore("PKCS12", "ticket/ca1test2.p12", "test2"), "1", "test2"); + assertEquals( + content, new TicketDecoder( + entry2.getCertificate() + ).decode( + new TicketEncoder(entry1.getCertificate(), entry1.getPrivateKey()).encode(content) + ) + ); + } + + @Test(expected=GeneralSecurityException.class) + public void testByCertificateFailCA() throws Exception { + final String content = "testByCertificate"; + KeyStore.PrivateKeyEntry entry = getPrivateKeyEntry(getKeyStore("PKCS12", "ticket/ca1test1.p12", "test1"), "1", "test1"); + assertEquals( + content, new TicketDecoder( + getKeyStore("JKS", "ticket/ca2.jks", "changeit"), + null, + entry.getCertificate() + ).decode( + new TicketEncoder(entry.getCertificate(), entry.getPrivateKey()).encode(content) + ) + ); + } + + @Test + public void testByEKU() throws Exception { + final String content = "testByEKU"; + KeyStore.PrivateKeyEntry entry = getPrivateKeyEntry(getKeyStore("PKCS12", "ticket/ca1test2.p12", "test2"), "1", "test2"); + assertEquals( + content, new TicketDecoder( + getKeyStore("JKS", "ticket/ca1.jks", "changeit"), + "1.2.3.4" + ).decode( + new TicketEncoder(entry.getCertificate(), entry.getPrivateKey()).encode(content) + ) + ); + } + + @Test(expected=GeneralSecurityException.class) + public void testByEKUFailEKU() throws Exception { + final String content = "testByEKU"; + KeyStore.PrivateKeyEntry entry = getPrivateKeyEntry(getKeyStore("PKCS12", "ticket/ca1test2.p12", "test2"), "1", "test2"); + assertEquals( + content, new TicketDecoder( + getKeyStore("JKS", "ticket/ca1.jks", "changeit"), + "1.2.3.4.5" + ).decode( + new TicketEncoder(entry.getCertificate(), entry.getPrivateKey()).encode(content) + ) + ); + } + + @Test(expected=GeneralSecurityException.class) + public void testByEKUFailCA() throws Exception { + final String content = "testByEKU"; + KeyStore.PrivateKeyEntry entry = getPrivateKeyEntry(getKeyStore("PKCS12", "ticket/ca1test2.p12", "test2"), "1", "test2"); + assertEquals( + content, new TicketDecoder( + getKeyStore("JKS", "ticket/ca2.jks", "changeit"), + "1.2.3.4" + ).decode( + new TicketEncoder(entry.getCertificate(), entry.getPrivateKey()).encode(content) + ) + ); + } + + @Test + public void testSalt() throws Exception { + final String content = "testSalt"; + KeyStore.PrivateKeyEntry entry = getPrivateKeyEntry(getKeyStore("PKCS12", "ticket/ca1test2.p12", "test2"), "1", "test2"); + + Set<String> salt = new HashSet<>(); + final int n = 10; + for (int i = 0; i < n;i++) { + salt.add( + new ObjectMapper().<Map<String, String>>readValue( + Base64.decodeBase64(new TicketEncoder(entry.getCertificate(), entry.getPrivateKey()).encode(content)), + TypeFactory.defaultInstance().constructMapType(HashMap.class, String.class, String.class) + ).get("salt") + ); + } + assertEquals(n, salt.size()); + } + + @Test(expected=GeneralSecurityException.class) + public void testContentFail() throws Exception { + KeyStore.PrivateKeyEntry entry = getPrivateKeyEntry(getKeyStore("PKCS12", "ticket/ca1test2.p12", "test2"), "1", "test2"); + + Map<String, String> map = new ObjectMapper().readValue( + Base64.decodeBase64(new TicketEncoder(entry.getCertificate(), entry.getPrivateKey()).encode("content")), + TypeFactory.defaultInstance().constructMapType(HashMap.class, String.class, String.class) + ); + map.put("data", "Content"); + String modifiedTicket = new Base64(0).encodeToString(new ObjectMapper().writeValueAsString(map).getBytes(Charset.forName("UTF-8"))); + + new TicketDecoder(getKeyStore("JKS", "ticket/ca1.jks", "changeit"), "1.2.3.4").decode(modifiedTicket); + } + + @Test(expected=GeneralSecurityException.class) + public void testValidToFail() throws Exception { + KeyStore.PrivateKeyEntry entry = getPrivateKeyEntry(getKeyStore("PKCS12", "ticket/ca1test2.p12", "test2"), "1", "test2"); + String ticket = new TicketEncoder(entry.getCertificate(), entry.getPrivateKey(), 1).encode("content"); + Thread.sleep(2000); + new TicketDecoder(getKeyStore("JKS", "ticket/ca1.jks", "changeit"), "1.2.3.4").decode(ticket); + } +} diff --git a/backend/manager/modules/uutils/src/test/resources/ticket/ca1.jks b/backend/manager/modules/uutils/src/test/resources/ticket/ca1.jks new file mode 100644 index 0000000..7c84bf3 --- /dev/null +++ b/backend/manager/modules/uutils/src/test/resources/ticket/ca1.jks Binary files differ diff --git a/backend/manager/modules/uutils/src/test/resources/ticket/ca1test1.p12 b/backend/manager/modules/uutils/src/test/resources/ticket/ca1test1.p12 new file mode 100644 index 0000000..82de504 --- /dev/null +++ b/backend/manager/modules/uutils/src/test/resources/ticket/ca1test1.p12 Binary files differ diff --git a/backend/manager/modules/uutils/src/test/resources/ticket/ca1test2.p12 b/backend/manager/modules/uutils/src/test/resources/ticket/ca1test2.p12 new file mode 100644 index 0000000..0d899eb --- /dev/null +++ b/backend/manager/modules/uutils/src/test/resources/ticket/ca1test2.p12 Binary files differ diff --git a/backend/manager/modules/uutils/src/test/resources/ticket/ca2.jks b/backend/manager/modules/uutils/src/test/resources/ticket/ca2.jks new file mode 100644 index 0000000..2003dee --- /dev/null +++ b/backend/manager/modules/uutils/src/test/resources/ticket/ca2.jks Binary files differ diff --git a/ovirt-engine.spec.in b/ovirt-engine.spec.in index c39e652..17a1bf3 100644 --- a/ovirt-engine.spec.in +++ b/ovirt-engine.spec.in @@ -301,6 +301,7 @@ %package lib Summary: %{ovirt_product_name_short} library Group: %{ovirt_product_group} +Requires: m2crypto Requires: python-daemon %description lib @@ -516,7 +517,6 @@ Summary: %{ovirt_product_name_short} Websocket Proxy Group: %{ovirt_product_group} Requires: %{name}-lib >= %{version}-%{release} -Requires: m2crypto Requires: numpy Requires: python-websockify Requires: %{name}-setup-plugin-websocket-proxy >= %{version}-%{release} diff --git a/packaging/pythonlib/ovirt_engine/ticket.py b/packaging/pythonlib/ovirt_engine/ticket.py new file mode 100644 index 0000000..9f1a8d6 --- /dev/null +++ b/packaging/pythonlib/ovirt_engine/ticket.py @@ -0,0 +1,121 @@ +import base64 +import datetime +import json + + +from M2Crypto import EVP, Rand, X509 + + +class TicketEncoder(): + + @staticmethod + def _formatDate(d): + return d.strftime("%Y%m%d%H%M%S") + + def __init__(self, cert, key, lifetime=5): + self._lifetime = lifetime + self._x509 = X509.load_cert(cert) + self._pkey = EVP.load_key(key) + + def encode(self, data): + d = { + 'salt': base64.b64encode(Rand.rand_bytes(8)), + 'digest': 'sha1', + 'validFrom': self._formatDate(datetime.datetime.utcnow()), + 'validTo': self._formatDate( + datetime.datetime.utcnow() + datetime.timedelta( + seconds=self._lifetime + ) + ), + 'data': data + } + + self._pkey.reset_context(md=d['digest']) + self._pkey.sign_init() + fields = [] + for k, v in d.items(): + fields.append(k) + self._pkey.sign_update(v) + + d['signedFields'] = ','.join(fields) + d['signature'] = base64.b64encode(self._pkey.sign_final()) + d['certificate'] = self._x509.as_pem() + + return base64.b64encode(json.dumps(d)) + + +class TicketDecoder(): + + _peer = None + _ca = None + + @staticmethod + def _parseDate(d): + return datetime.datetime.strptime(d, '%Y%m%d%H%M%S') + + @staticmethod + def _verifyCertificate(ca, x509): + if x509.verify(ca.get_pubkey()) == 0: + raise ValueError('Untrusted certificate') + + if not ( + x509.get_not_before().get_datetime().replace(tzinfo=None) <= + datetime.datetime.utcnow() <= + x509.get_not_after().get_datetime().replace(tzinfo=None) + ): + raise ValueError('Certificate expired') + + def __init__(self, ca, eku, peer=None): + self._eku = eku + if peer is not None: + self._peer = X509.load_cert_string(peer) + if ca is not None: + self._ca = X509.load_cert(ca) + + def decode(self, ticket): + decoded = json.loads(base64.b64decode(ticket)) + + if self._peer is not None: + x509 = self._peer + else: + x509 = X509.load_cert_string( + decoded['certificate'].encode('utf8') + ) + + if self._ca is not None: + self._verifyCertificate(self._ca, x509) + + if self._eku is not None: + if self._eku not in x509.get_ext( + 'extendedKeyUsage' + ).get_value().split(','): + raise ValueError('Certificate is not authorized for action') + + signedFields = decoded['signedFields'].split(',') + if len( + set(['salt', 'data']) & + set(signedFields) + ) == 0: + raise ValueError('Invalid ticket') + + pkey = x509.get_pubkey() + pkey.reset_context(md=decoded['digest']) + pkey.verify_init() + for field in signedFields: + pkey.verify_update(decoded[field.strip()].encode('utf8')) + if pkey.verify_final( + base64.b64decode(decoded['signature']) + ) != 1: + raise ValueError('Invalid ticket signature') + + if not ( + self._parseDate(decoded['validFrom']) <= + datetime.datetime.utcnow() <= + self._parseDate(decoded['validTo']) + ): + raise ValueError('Ticket life time expired') + + return decoded['data'] + + +# vim: expandtab tabstop=4 shiftwidth=4 diff --git a/packaging/services/ovirt-websocket-proxy/ovirt-websocket-proxy.conf.in b/packaging/services/ovirt-websocket-proxy/ovirt-websocket-proxy.conf.in index 61abb45..fc9f2bb 100644 --- a/packaging/services/ovirt-websocket-proxy/ovirt-websocket-proxy.conf.in +++ b/packaging/services/ovirt-websocket-proxy/ovirt-websocket-proxy.conf.in @@ -6,7 +6,6 @@ SOURCE_IS_IPV6=False SSL_CERTIFICATE= SSL_KEY= -FORCE_DATA_VERIFICATION=False CERT_FOR_DATA_VERIFICATION= SSL_ONLY=False TRACE_ENABLE=False diff --git a/packaging/services/ovirt-websocket-proxy/ovirt-websocket-proxy.py b/packaging/services/ovirt-websocket-proxy/ovirt-websocket-proxy.py index 4477f23..a1057f5 100755 --- a/packaging/services/ovirt-websocket-proxy/ovirt-websocket-proxy.py +++ b/packaging/services/ovirt-websocket-proxy/ovirt-websocket-proxy.py @@ -18,14 +18,11 @@ import sys import signal import gettext -import base64 import json -import datetime import urllib _ = lambda m: gettext.dgettext(message=m, domain='ovirt-engine') -from M2Crypto import X509 import websockify @@ -34,6 +31,7 @@ from ovirt_engine import configfile from ovirt_engine import service +from ovirt_engine import ticket class OvirtWebSocketProxy(websockify.WebSocketProxy): @@ -60,43 +58,6 @@ target_port = connection_data['port'].encode('utf8') self.ssl_target = connection_data['ssl_target'] return (target_host, target_port) - - -class TicketDecoder(object): - - def __init__( - self, - insecure, - certificate - ): - self._insecure = insecure - if not insecure: - self._key = X509.load_cert( - certificate, - X509.FORMAT_PEM, - ).get_pubkey() - - def decode(self, ticket): - decoded = json.loads(base64.b64decode(ticket)) - if not self._insecure: - self._key.verify_init() - for field in decoded['signedFields'].split(','): - self._key.verify_update(decoded[field].encode('utf8')) - if self._key.verify_final( - base64.b64decode(decoded['signature']) - ) != 1: - raise ValueError('Invalid ticket signature') - if ( - datetime.datetime.utcnow() - - datetime.datetime.strptime(decoded['validFrom'], '%Y%m%d%H%M%S') - ) < datetime.timedelta(seconds=-5): # tolerance to the past - raise ValueError('Ticket life time expired') - if ( - datetime.datetime.strptime(decoded['validTo'], '%Y%m%d%H%M%S') - - datetime.datetime.utcnow() - ) < datetime.timedelta(): - raise ValueError('Ticket life time expired') - return decoded['data'] class Daemon(service.Daemon): @@ -186,18 +147,22 @@ # WORKAROUND-END try: + with open( + self._config.get( + 'CERT_FOR_DATA_VERIFICATION' + ) + ) as f: + peer = f.read() + OvirtWebSocketProxy( listen_host=self._config.get('PROXY_HOST'), listen_port=self._config.get('PROXY_PORT'), source_is_ipv6=self._config.getboolean('SOURCE_IS_IPV6'), verbose=self.debug, - ticketDecoder=TicketDecoder( - insecure=not self._config.getboolean( - 'FORCE_DATA_VERIFICATION' - ), - certificate=self._config.get( - 'CERT_FOR_DATA_VERIFICATION' - ) + ticketDecoder=ticket.TicketDecoder( + ca=None, + eku=None, + peer=peer, ), cert=self._config.get('SSL_CERTIFICATE'), key=self._config.get('SSL_KEY'), diff --git a/packaging/setup/plugins/ovirt-engine-setup/websocket_proxy/config.py b/packaging/setup/plugins/ovirt-engine-setup/websocket_proxy/config.py index 7591b89..1e14d50 100644 --- a/packaging/setup/plugins/ovirt-engine-setup/websocket_proxy/config.py +++ b/packaging/setup/plugins/ovirt-engine-setup/websocket_proxy/config.py @@ -245,7 +245,6 @@ "PROXY_PORT={port}\n" "SSL_CERTIFICATE={certificate}\n" "SSL_KEY={key}\n" - "FORCE_DATA_VERIFICATION=True\n" "CERT_FOR_DATA_VERIFICATION={engine_cert}\n" "SSL_ONLY=True\n" ).format( -- To view, visit http://gerrit.ovirt.org/36401 To unsubscribe, visit http://gerrit.ovirt.org/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ibc87ea4a4ea540dfea09e40f4396d6918e34e019 Gerrit-PatchSet: 1 Gerrit-Project: ovirt-engine Gerrit-Branch: master Gerrit-Owner: Alon Bar-Lev <[email protected]> _______________________________________________ Engine-patches mailing list [email protected] http://lists.ovirt.org/mailman/listinfo/engine-patches
