<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[13913] CalendarServer/trunk/calendarserver/tap</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/13913">13913</a></dd>
<dt>Author</dt> <dd>sagen@apple.com</dd>
<dt>Date</dt> <dd>2014-08-22 11:00:16 -0700 (Fri, 22 Aug 2014)</dd>
</dl>
<h3>Log Message</h3>
<pre>Detect missing, empty, or bogus TLS certificate files</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkcalendarservertapcaldavpy">CalendarServer/trunk/calendarserver/tap/caldav.py</a></li>
<li><a href="#CalendarServertrunkcalendarservertaptesttest_utilpy">CalendarServer/trunk/calendarserver/tap/test/test_util.py</a></li>
<li><a href="#CalendarServertrunkcalendarservertaputilpy">CalendarServer/trunk/calendarserver/tap/util.py</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcalendarservertapcaldavpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tap/caldav.py (13912 => 13913)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tap/caldav.py        2014-08-21 18:45:59 UTC (rev 13912)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py        2014-08-22 18:00:16 UTC (rev 13913)
</span><span class="lines">@@ -25,7 +25,7 @@
</span><span class="cx"> import sys
</span><span class="cx"> from collections import OrderedDict
</span><span class="cx"> from os import getuid, getgid, umask, remove, environ, stat, chown
</span><del>-from os.path import exists, basename, isfile
</del><ins>+from os.path import exists, basename
</ins><span class="cx"> import socket
</span><span class="cx"> from stat import S_ISSOCK
</span><span class="cx"> import time
</span><span class="lines">@@ -127,7 +127,7 @@
</span><span class="cx"> checkDirectories, getRootResource,
</span><span class="cx"> oracleConnectorFromConfig, pgConnectorFromConfig,
</span><span class="cx"> pgServiceFromConfig, getDBPool, MemoryLimitService,
</span><del>- storeFromConfig
</del><ins>+ storeFromConfig, getSSLPassphrase, PreFlightChecksStep
</ins><span class="cx"> )
</span><span class="cx"> try:
</span><span class="cx"> from calendarserver.version import version
</span><span class="lines">@@ -1582,6 +1582,10 @@
</span><span class="cx"> )
</span><span class="cx">
</span><span class="cx"> pps.addStep(
</span><ins>+ PreFlightChecksStep(config)
+ )
+
+ pps.addStep(
</ins><span class="cx"> UpgradeReleaseLockStep(store)
</span><span class="cx"> )
</span><span class="cx">
</span><span class="lines">@@ -2652,77 +2656,6 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><del>-def getSSLPassphrase(*ignored):
-
- if not config.SSLPrivateKey:
- return None
-
- if config.SSLCertAdmin and isfile(config.SSLCertAdmin):
- child = Popen(
- args=[
- "sudo", config.SSLCertAdmin,
- "--get-private-key-passphrase", config.SSLPrivateKey,
- ],
- stdout=PIPE, stderr=PIPE,
- )
- output, error = child.communicate()
-
- if child.returncode:
- log.error(
- "Could not get passphrase for {key}: {error}",
- key=config.SSLPrivateKey, error=error
- )
- else:
- log.info(
- "Obtained passphrase for {key}", key=config.SSLPrivateKey
- )
- return output.strip()
-
- if (
- config.SSLPassPhraseDialog and
- isfile(config.SSLPassPhraseDialog)
- ):
- sslPrivKey = open(config.SSLPrivateKey)
- try:
- keyType = None
- for line in sslPrivKey.readlines():
- if "-----BEGIN RSA PRIVATE KEY-----" in line:
- keyType = "RSA"
- break
- elif "-----BEGIN DSA PRIVATE KEY-----" in line:
- keyType = "DSA"
- break
- finally:
- sslPrivKey.close()
-
- if keyType is None:
- log.error(
- "Could not get private key type for {key}",
- key=config.SSLPrivateKey
- )
- else:
- child = Popen(
- args=[
- config.SSLPassPhraseDialog,
- "{}:{}".format(config.ServerHostName, config.SSLPort),
- keyType,
- ],
- stdout=PIPE, stderr=PIPE,
- )
- output, error = child.communicate()
-
- if child.returncode:
- log.error(
- "Could not get passphrase for {key}: {error}",
- key=config.SSLPrivateKey, error=error
- )
- else:
- return output.strip()
-
- return None
-
-
-
</del><span class="cx"> def getSystemIDs(userName, groupName):
</span><span class="cx"> """
</span><span class="cx"> Return the system ID numbers corresponding to either:
</span></span></pre></div>
<a id="CalendarServertrunkcalendarservertaptesttest_utilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tap/test/test_util.py (13912 => 13913)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tap/test/test_util.py        2014-08-21 18:45:59 UTC (rev 13912)
+++ CalendarServer/trunk/calendarserver/tap/test/test_util.py        2014-08-22 18:00:16 UTC (rev 13913)
</span><span class="lines">@@ -14,12 +14,18 @@
</span><span class="cx"> # limitations under the License.
</span><span class="cx"> ##
</span><span class="cx">
</span><del>-from calendarserver.tap.util import MemoryLimitService, Stepper
</del><ins>+import OpenSSL
+from calendarserver.tap.util import (
+ MemoryLimitService, Stepper, PreFlightChecksStep
+)
</ins><span class="cx"> from twistedcaldav.util import computeProcessCount
</span><span class="cx"> from twistedcaldav.test.util import TestCase
</span><span class="cx"> from twisted.internet.task import Clock
</span><span class="cx"> from twisted.internet.defer import succeed, inlineCallbacks
</span><ins>+from twisted.python.filepath import FilePath
+from twistedcaldav.config import ConfigDict, ConfigurationError
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> class ProcessCountTestCase(TestCase):
</span><span class="cx">
</span><span class="cx"> def test_count(self):
</span><span class="lines">@@ -37,10 +43,10 @@
</span><span class="cx"> (6, 2, 2, 2, 2, 6),
</span><span class="cx"> (1, 2, 1, 4, 99, 8),
</span><span class="cx">
</span><del>- (2, 1, 2, 2, 2, 2), # 2 cores, 2GB = 2
- (2, 1, 2, 2, 4, 2), # 2 cores, 4GB = 2
- (2, 1, 2, 8, 6, 8), # 8 cores, 6GB = 8
- (2, 1, 2, 8, 16, 8), # 8 cores, 16GB = 8
</del><ins>+ (2, 1, 2, 2, 2, 2), # 2 cores, 2GB = 2
+ (2, 1, 2, 2, 4, 2), # 2 cores, 4GB = 2
+ (2, 1, 2, 8, 6, 8), # 8 cores, 6GB = 8
+ (2, 1, 2, 8, 16, 8), # 8 cores, 16GB = 8
</ins><span class="cx"> )
</span><span class="cx">
</span><span class="cx"> for min, perCPU, perGB, cpu, mem, expected in data:
</span><span class="lines">@@ -89,10 +95,10 @@
</span><span class="cx"> """
</span><span class="cx"> data = {
</span><span class="cx"> # PID : (name, resident memory-in-bytes, virtual memory-in-bytes)
</span><del>- 101 : ("process #1", 10, 1010),
- 102 : ("process #2", 30, 1030),
- 103 : ("process #3", 50, 1050),
- 99 : ("memcached-Default", 10, 1010),
</del><ins>+ 101: ("process #1", 10, 1010),
+ 102: ("process #2", 30, 1030),
+ 103: ("process #3", 50, 1050),
+ 99: ("memcached-Default", 10, 1010),
</ins><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> processes = []
</span><span class="lines">@@ -200,7 +206,7 @@
</span><span class="cx"> StepFour(self._record, False)
</span><span class="cx"> )
</span><span class="cx"> result = (yield self.stepper.start("abc"))
</span><del>- self.assertEquals(result, "abc") # original result passed through
</del><ins>+ self.assertEquals(result, "abc") # original result passed through
</ins><span class="cx"> self.assertEquals(
</span><span class="cx"> self.history,
</span><span class="cx"> ['one success', 'two success', 'three success', 'four success'])
</span><span class="lines">@@ -224,7 +230,73 @@
</span><span class="cx"> self.stepper.addStep(StepThree(self._record, True))
</span><span class="cx"> self.stepper.addStep(StepFour(self._record, False))
</span><span class="cx"> result = (yield self.stepper.start("abc"))
</span><del>- self.assertEquals(result, None) # original result is gone
</del><ins>+ self.assertEquals(result, None) # original result is gone
</ins><span class="cx"> self.assertEquals(
</span><span class="cx"> self.history,
</span><span class="cx"> ['one success', 'two failure', 'three success', 'four failure'])
</span><ins>+
+
+class PreFlightChecksStepTestCase(TestCase):
+ """
+ Verify that missing, empty, or bogus TLS Certificates are detected
+ """
+
+ @inlineCallbacks
+ def test_missingCertificate(self):
+ step = PreFlightChecksStep(
+ ConfigDict(
+ {
+ "SSLCertificate": "missing",
+ }
+ )
+ )
+ try:
+ yield step.stepWithResult(None)
+ except ConfigurationError as e:
+ self.assertTrue("Missing" in str(e))
+ else:
+ self.fail("Did not raise ConfigurationError")
+
+
+ @inlineCallbacks
+ def test_emptyCertificate(self):
+ certFilePath = FilePath(self.mktemp())
+ certFilePath.setContent("")
+ step = PreFlightChecksStep(
+ ConfigDict(
+ {
+ "SSLCertificate": certFilePath.path,
+ }
+ )
+ )
+ try:
+ yield step.stepWithResult(None)
+ except ConfigurationError as e:
+ self.assertTrue("Empty" in str(e))
+ else:
+ self.fail("Did not raise ConfigurationError")
+
+
+ @inlineCallbacks
+ def test_bogusCertificate(self):
+ certFilePath = FilePath(self.mktemp())
+ certFilePath.setContent("bogus")
+ keyFilePath = FilePath(self.mktemp())
+ keyFilePath.setContent("bogus")
+ step = PreFlightChecksStep(
+ ConfigDict(
+ {
+ "SSLCertificate": certFilePath.path,
+ "SSLPrivateKey": keyFilePath.path,
+ "SSLAuthorityChain": "",
+ "SSLMethod": "SSLv3_METHOD",
+ "SSLCiphers": "ALL:!aNULL:!ADH:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM",
+ }
+ )
+ )
+ try:
+ yield step.stepWithResult(None)
+ except OpenSSL.SSL.Error:
+ pass
+ else:
+ self.fail("Did not raise OpenSSL.SSL.Error")
</ins></span></pre></div>
<a id="CalendarServertrunkcalendarservertaputilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tap/util.py (13912 => 13913)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tap/util.py        2014-08-21 18:45:59 UTC (rev 13912)
+++ CalendarServer/trunk/calendarserver/tap/util.py        2014-08-22 18:00:16 UTC (rev 13913)
</span><span class="lines">@@ -24,13 +24,19 @@
</span><span class="cx"> "getDBPool",
</span><span class="cx"> "FakeRequest",
</span><span class="cx"> "MemoryLimitService",
</span><ins>+ "PreFlightChecksStep",
+ "getSSLPassphrase",
</ins><span class="cx"> ]
</span><span class="cx">
</span><span class="cx"> import errno
</span><ins>+import OpenSSL
</ins><span class="cx"> import os
</span><ins>+import psutil
</ins><span class="cx"> from socket import fromfd, AF_UNIX, SOCK_STREAM, socketpair
</span><del>-import psutil
</del><ins>+from subprocess import Popen, PIPE
</ins><span class="cx">
</span><ins>+
+from twext.internet.ssl import ChainingOpenSSLContextFactory
</ins><span class="cx"> from twext.python.filepath import CachingFilePath as FilePath
</span><span class="cx"> from twext.python.log import Logger
</span><span class="cx"> from txweb2.auth.basic import BasicCredentialFactory
</span><span class="lines">@@ -98,6 +104,11 @@
</span><span class="cx"> from twext.who.checker import HTTPDigestCredentialChecker
</span><span class="cx"> from twisted.cred.error import UnauthorizedLogin
</span><span class="cx"> from txweb2.dav.auth import IPrincipalCredentials
</span><ins>+
+from twistedcaldav.config import ConfigurationError
+from twistedcaldav.stdconfig import config
+
+
</ins><span class="cx"> log = Logger()
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -952,7 +963,7 @@
</span><span class="cx"> "Server root",
</span><span class="cx"> # Require write access because one might not allow editing on /
</span><span class="cx"> access=os.W_OK,
</span><del>- wait=True # Wait in a loop until ServerRoot exists
</del><ins>+ wait=True # Wait in a loop until ServerRoot exists
</ins><span class="cx"> )
</span><span class="cx">
</span><span class="cx"> #
</span><span class="lines">@@ -1052,7 +1063,10 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> def defaultStepWithFailure(self, failure):
</span><del>- if failure.type != NotAllowedToUpgrade:
</del><ins>+ if failure.type not in (
+ NotAllowedToUpgrade, ConfigurationError,
+ OpenSSL.SSL.Error
+ ):
</ins><span class="cx"> log.failure("Step failure", failure=failure)
</span><span class="cx"> return failure
</span><span class="cx">
</span><span class="lines">@@ -1098,3 +1112,130 @@
</span><span class="cx"> self.deferred.callback(result)
</span><span class="cx">
</span><span class="cx"> return self.deferred
</span><ins>+
+
+class PreFlightChecksStep(object):
+ """
+ A place to make any other checks before finishing up the
+ PreProcessingService.
+ """
+
+ def __init__(self, config):
+ self.config = config
+
+
+ def stepWithResult(self, result):
+ self.verifyTLSCertificate()
+ return succeed(None)
+
+
+ def verifyTLSCertificate(self):
+ """
+ If a TLS certificate is configured, make sure it exists, is non empty,
+ and that it's valid.
+ """
+
+ if self.config.SSLCertificate:
+ if not os.path.exists(self.config.SSLCertificate):
+ log.error(
+ "The configured TLS certificate ({cert}) is missing",
+ cert=self.config.SSLCertificate
+ )
+ raise ConfigurationError("Missing certificate file")
+ else:
+ return
+
+ length = os.stat(self.config.SSLCertificate).st_size
+ if length == 0:
+ log.error(
+ "The configured TLS certificate ({cert}) is empty",
+ cert=self.config.SSLCertificate
+ )
+ raise ConfigurationError("Empty certificate file")
+
+ try:
+ ChainingOpenSSLContextFactory(
+ self.config.SSLPrivateKey,
+ self.config.SSLCertificate,
+ certificateChainFile=self.config.SSLAuthorityChain,
+ passwdCallback=getSSLPassphrase,
+ sslmethod=getattr(OpenSSL.SSL, self.config.SSLMethod),
+ ciphers=self.config.SSLCiphers.strip()
+ )
+ except Exception as e:
+ log.error(
+ "The configured TLS certificate ({cert}) cannot be used: {reason}",
+ cert=self.config.SSLCertificate,
+ reason=str(e)
+ )
+ raise
+
+
+def getSSLPassphrase(*ignored):
+
+ if not config.SSLPrivateKey:
+ return None
+
+ if config.SSLCertAdmin and os.path.isfile(config.SSLCertAdmin):
+ child = Popen(
+ args=[
+ "sudo", config.SSLCertAdmin,
+ "--get-private-key-passphrase", config.SSLPrivateKey,
+ ],
+ stdout=PIPE, stderr=PIPE,
+ )
+ output, error = child.communicate()
+
+ if child.returncode:
+ log.error(
+ "Could not get passphrase for {key}: {error}",
+ key=config.SSLPrivateKey, error=error
+ )
+ else:
+ log.info(
+ "Obtained passphrase for {key}", key=config.SSLPrivateKey
+ )
+ return output.strip()
+
+ if (
+ config.SSLPassPhraseDialog and
+ os.path.isfile(config.SSLPassPhraseDialog)
+ ):
+ sslPrivKey = open(config.SSLPrivateKey)
+ try:
+ keyType = None
+ for line in sslPrivKey.readlines():
+ if "-----BEGIN RSA PRIVATE KEY-----" in line:
+ keyType = "RSA"
+ break
+ elif "-----BEGIN DSA PRIVATE KEY-----" in line:
+ keyType = "DSA"
+ break
+ finally:
+ sslPrivKey.close()
+
+ if keyType is None:
+ log.error(
+ "Could not get private key type for {key}",
+ key=config.SSLPrivateKey
+ )
+ else:
+ child = Popen(
+ args=[
+ config.SSLPassPhraseDialog,
+ "{}:{}".format(config.ServerHostName, config.SSLPort),
+ keyType,
+ ],
+ stdout=PIPE, stderr=PIPE,
+ )
+ output, error = child.communicate()
+
+ if child.returncode:
+ log.error(
+ "Could not get passphrase for {key}: {error}",
+ key=config.SSLPrivateKey, error=error
+ )
+ else:
+ return output.strip()
+
+ return None
</ins></span></pre>
</div>
</div>
</body>
</html>