Torc  0.1
torchttpservernonce.cpp
Go to the documentation of this file.
1 /* Class TorcHTTPServerNonce
2 *
3 * This file is part of the Torc project.
4 *
5 * Copyright (C) Mark Kendall 2012-18
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
20 * USA.
21 */
22 
23 // Qt
24 #include <QUuid>
25 #include <QCryptographicHash>
26 
27 // Torc
28 #include "torclogging.h"
29 #include "torcuser.h"
30 #include "torchttpservernonce.h"
31 
40 // Currently at least, most interaction is via WebSockets, which are
41 // authorised once on creation.
42 // Some (older?) browsers seem to request a new nonce for every request - so again, try and limit the nonce
43 // count to something reasonable/manageable. (maybe an implementation issue that is causing the browser
44 // to re-issue)
45 
46 void TorcHTTPServerNonce::ProcessDigestAuth(TorcHTTPRequest &Request, bool Check /*=false*/)
47 {
48  static QHash<QString,TorcHTTPServerNonce> nonces;
49  static QByteArray token = QUuid::createUuid().toByteArray();
50  static quint64 nonceCounter = 0;
51  static QMutex lock(QMutex::Recursive);
52 
53  QDateTime current = QDateTime::currentDateTime();
54 
55  // Set digest authentication headers
56  if (!Check)
57  {
58  // try and build a unique nonce
59  QByteArray tag = QByteArray::number(current.toMSecsSinceEpoch()) + Request.GetCache().toLocal8Bit() + token;
60  QString nonce;
61 
62  {
63  QMutexLocker locker(&lock);
64  do
65  {
66  QByteArray hash(tag + QByteArray::number(++nonceCounter));
67  nonce = QString(QCryptographicHash::hash(hash, QCryptographicHash::Md5).toHex());
68  } while (nonces.contains(nonce));
69  }
70 
71  TorcHTTPServerNonce nonceobj(current);
72  lock.lock();
73  nonces.insert(nonce, nonceobj);
74  // NB SHA-256 doesn't seem to be implemented anywhere yet - so just offer MD5
75  // should probably use insertMulti for SetResponseHeader
76  QString auth = QStringLiteral("Digest realm=\"%1\", qop=\"auth\", algorithm=MD5, nonce=\"%2\", opaque=\"%3\"%4")
77  .arg(TORC_REALM, nonce, nonceobj.GetOpaque(),
78  Request.IsAuthorised() == HTTPAuthorisedStale ? QStringLiteral(", stale=\"true\"") : QStringLiteral(""));
79  lock.unlock();
80  Request.SetResponseHeader(QStringLiteral("WWW-Authenticate"), auth);
81  }
82  // Check digest authentication
83  else
84  {
85  // expire old here. The authentication check is performed first (and on every request)
86  {
87  QMutexLocker locker(&lock);
88  QMutableHashIterator<QString,TorcHTTPServerNonce> it(nonces);
89  while (it.hasNext())
90  {
91  it.next();
92  if (it.value().IsOutOfDate(current))
93  it.remove();
94  }
95  }
96 
97  // remove leading 'Digest' and split out parameters
98  QStringList authentication = Request.Headers().value(QStringLiteral("Authorization")).mid(6).trimmed().split(',', QString::SkipEmptyParts);
99 
100  // create a filtered hash of the parameters
101  QHash<QString,QString> params;
102  foreach (const QString &auth, authentication)
103  {
104  // various parameters can contain an '=' in the body, so only search for the first '='
105  QString key = auth.section('=', 0, 0).trimmed().toLower();
106  QString value = auth.section('=', 1).trimmed();
107  value.remove(QStringLiteral("\""));
108  params.insert(key, value);
109  }
110 
111  // we need username, realm, nonce, uri, qop, algorithm, nc, cnonce, response and opaque...
112  if (params.size() < 10)
113  {
114  LOG(VB_NETWORK, LOG_DEBUG, QStringLiteral("Digest response received too few parameters"));
115  return;
116  }
117 
118  // check for presence of each
119  if (!params.contains(QStringLiteral("username")) || !params.contains(QStringLiteral("realm")) || !params.contains(QStringLiteral("nonce")) ||
120  !params.contains(QStringLiteral("uri")) || !params.contains(QStringLiteral("qop")) || !params.contains(QStringLiteral("algorithm")) ||
121  !params.contains(QStringLiteral("nc")) || !params.contains(QStringLiteral("cnonce")) || !params.contains(QStringLiteral("response")) ||
122  !params.contains(QStringLiteral("opaque")))
123  {
124  LOG(VB_NETWORK, LOG_DEBUG, QStringLiteral("Did not receive expected paramaters"));
125  return;
126  }
127 
128  // username must match
129  if (TorcUser::GetName() != params.value(QStringLiteral("username")))
130  {
131  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Expected '%1' username, got '%2'").arg(TorcUser::GetName(), params.value(QStringLiteral("username"))));
132  return;
133  }
134 
135  // we check the digest early, even though it is an expensive operation, as it will automatically
136  // confirm a number of the params are correct and if the respone is calculated correctly but the nonce
137  // is not recognised (i.e. stale), we can respond with stale="true', as per the spec, and the client
138  // can resubmit without prompting the user for credentials (i.e. the client has proved it knows the correct
139  // credentials but the nonce is out of date)
140  QString URI = Request.GetUrl();
141  QString noncestr = params.value(QStringLiteral("nonce"));
142  QString ncstr = params.value(QStringLiteral("nc"));
143  QString second = QStringLiteral("%1:%2").arg(TorcHTTPRequest::RequestTypeToString(Request.GetHTTPRequestType()), URI);
144  QByteArray hash1 = TorcUser::GetCredentials();
145  QByteArray hash2 = QCryptographicHash::hash(second.toLatin1(), QCryptographicHash::Md5).toHex();
146  QString third = QStringLiteral("%1:%2:%3:%4:%5:%6").arg(QString(hash1), noncestr, ncstr, params.value(QStringLiteral("cnonce")), QStringLiteral("auth"), QString(hash2));
147  QByteArray hash3 = QCryptographicHash::hash(third.toLatin1(), QCryptographicHash::Md5).toHex();
148 
149  if (hash3 != params.value(QStringLiteral("response")))
150  {
151  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Digest hash failed"));
152  return;
153  }
154 
155  // the uri MUST match the uri provided in the standard HTTP header, otherwise we could be authorise
156  // access to the wrong resource
157  if (URI != params.value(QStringLiteral("uri")))
158  {
159  LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("URI mismatch between HTTP request and WWW-Authenticate header"));
160  return;
161  }
162 
163  // we now don't need to check username, realm, password, method, URI or qop - as the hash check
164  // would have failed. Likewise if algorithm was incorrect, we would have calculated the hash incorrectly.
165  // The client has notionally verified its validity.
166 
167  {
168  QMutexLocker locker(&lock);
169 
170  // find the nonce
171  QHash<QString,TorcHTTPServerNonce>::const_iterator it = nonces.constFind(noncestr);
172  if (it == nonces.constEnd())
173  {
174  LOG(VB_NETWORK, LOG_DEBUG, QStringLiteral("Failed to find nonce '%1'").arg(noncestr));
175  // if we got this far the nonce was valid but old, so set Stale and ask for re-auth
177  return;
178  }
179 
180  TorcHTTPServerNonce nonce = it.value();
181 
182  // match opaque
183  if (it.value().GetOpaque() != params.value(QStringLiteral("opaque")))
184  {
185  // this is an error
186  LOG(VB_NETWORK, LOG_DEBUG, QStringLiteral("Failed to match opaque"));
187  return;
188  }
189 
190  // parse nonse count from hex
191  bool ok = false;
192  quint64 nc = ncstr.toInt(&ok, 16);
193  if (!ok)
194  {
195  LOG(VB_NETWORK, LOG_DEBUG, QStringLiteral("Failed to parse nonce count"));
196  return;
197  }
198 
199  // check nonce count
200  if (!nonce.UseOnce(nc, current))
201  {
203  LOG(VB_NETWORK, LOG_DEBUG, QStringLiteral("Nonce count use failed"));
204  return;
205  }
206  }
207 
208  Request.Authorise(HTTPAuthorised);
209  }
210 }
211 
213  : m_expired(false),
214  m_opaque(),
215  m_startMs(0),
216  m_startTime(),
217  m_useCount(0),
218  m_lifetimeInSeconds(0),
219  m_lifetimeInRequests(0)
220 {
221  LOG(VB_GENERAL, LOG_ERR, QStringLiteral("Invalid TorcHTTPServerNonce"));
222 }
223 
225  : m_expired(false),
226  m_opaque(QCryptographicHash::hash(QByteArray::number(qrand()), QCryptographicHash::Md5).toHex()),
227  m_startMs(Time.time().msecsSinceStartOfDay()),
228  m_startTime(Time),
229  m_useCount(0),
230  m_lifetimeInSeconds(DEFAULT_NONCE_LIFETIME_SECONDS),
231  m_lifetimeInRequests(DEFAULT_NONCE_LIFETIME_REQUESTS)
232 {
233 }
234 
236 {
237  return m_opaque;
238 }
239 
240 bool TorcHTTPServerNonce::UseOnce(quint64 ClientCount, const QDateTime &Current)
241 {
242  m_useCount++;
243 
244  // the nc value is designed to prevent replay attacks. Hence we should only ever see the same nc
245  // value once. In theory it should start at 1 and increase monotonically. In practice, we may miss
246  // requests and/or they may arrive out of order.
247  // We make no attempt to track individual nc values or out of order requests. If the nc value
248  // is greater than or equal to the expected value, we allow it. Otherwise we set Stale to true,
249  // which will trigger the client to re-authenticate.
250 
251  // I can't see any reference on how to deal with nonce count wrapping (even though highly unlikely)
252  // so assume it wraps to 1... max value is 8 character hex i.e. ffffffff or 32bit integer. For safety,
253  // invalidate the nonce.
254  if (m_useCount > 0xffffffff)
255  {
256  m_expired = true;
257  m_useCount = 1;
258  return false;
259  }
260 
261  if (m_useCount <= ClientCount)
262  {
263  // update to the client's count. This MAY invalidate out of order requests.
264  m_useCount = ClientCount;
265 
266  // check for total usage (zero request lifetime implies unlimited usage)
267  if (m_lifetimeInRequests > 0 && m_useCount >= m_lifetimeInRequests)
268  {
269  m_expired = true;
270  return false;
271  }
272 
273  // keep the nonce alive
274  m_startTime = Current;
275  return true;
276  }
277 
278  // unexpected nc value (less than expected)
279  m_expired = true;
280  return false;
281 }
282 
283 bool TorcHTTPServerNonce::IsOutOfDate(const QDateTime &Current)
284 {
285  // request lifetime is checked when actually used, so only check time here
286  // this is the main mechanism for managing the size of the nonce list
287  if (!m_expired)
288  if (Current > m_startTime.addSecs(m_lifetimeInSeconds))
289  m_expired = true;
290  return m_expired;
291 }
A class to encapsulate an incoming HTTP request.
HTTPRequestType GetHTTPRequestType(void) const
static QString RequestTypeToString(HTTPRequestType Type)
QString GetCache(void) const
bool IsOutOfDate(const QDateTime &Current)
static void ProcessDigestAuth(TorcHTTPRequest &Request, bool Check=false)
A server nonce for Digest Access Authentication.
HTTPAuthorisation IsAuthorised(void) const
QString GetOpaque(void) const
#define DEFAULT_NONCE_LIFETIME_REQUESTS
const QMap< QString, QString > & Headers(void) const
static QByteArray GetCredentials(void)
Definition: torcuser.cpp:81
#define DEFAULT_NONCE_LIFETIME_SECONDS
static QString GetName(void)
Definition: torcuser.cpp:69
void SetResponseHeader(const QString &Header, const QString &Value)
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: torclogging.h:20
#define TORC_REALM
Definition: torclocaldefs.h:9
QString GetUrl(void) const
bool UseOnce(quint64 ClientCount, const QDateTime &Current)
void Authorise(HTTPAuthorisation Authorisation)