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

Reply via email to