Add support for CPE digest authentication

This commit is contained in:
Shweelan Samson authored and Zaid Abdulla committed 2019-06-05 00:17:36 -07:00
1 parent f21ce3053a
commit 344ed3fdee
5 files changed
+228 -86

No files matched your search

+1
View File
@@ -27,6 +27,7 @@ const externals = [
"fs",
"cluster",
"os",
"tls",
"http",
"https",
"zlib",
+81 -45
View File
@@ -19,31 +19,9 @@
import { createHash, randomBytes, pbkdf2 } from "crypto";
export function parseAuthorizationHeader(authHeader): { method: string } {
authHeader = authHeader.trim();
const method = authHeader.split(" ", 1)[0];
const res = { method: method };
if (method === "Basic") {
// Inspired by https://github.com/jshttp/basic-auth
const USER_PASS_REGEX = /^([^:]*):(.*)$/;
const creds = USER_PASS_REGEX.exec(
Buffer.from(authHeader.slice(method.length + 1), "base64").toString()
);
if (!creds) throw new Error("Unable to parse auth header");
res["username"] = creds[1];
res["password"] = creds[2];
}
return res;
}
export function parseWwwAuthenticateHeader(authHeader): {} {
authHeader = authHeader.trim();
const method = authHeader.split(" ", 1)[0];
const res = { method: method };
const parts = authHeader.slice(method.length + 1).split(",");
function parseHeaderFeilds(str: string): {} {
const res = {};
const parts = str.split(",");
let part;
while ((part = parts.shift()) != null) {
@@ -72,33 +50,57 @@ export function parseWwwAuthenticateHeader(authHeader): {} {
return res;
}
export function parseAuthorizationHeader(authHeader): { method: string } {
authHeader = authHeader.trim();
const method = authHeader.split(" ", 1)[0];
const res = { method: method };
if (method === "Basic") {
// Inspired by https://github.com/jshttp/basic-auth
const USER_PASS_REGEX = /^([^:]*):(.*)$/;
const creds = USER_PASS_REGEX.exec(
Buffer.from(authHeader.slice(method.length + 1), "base64").toString()
);
if (!creds) throw new Error("Unable to parse auth header");
res["username"] = creds[1];
res["password"] = creds[2];
} else if (method === "Digest") {
Object.assign(res, parseHeaderFeilds(authHeader.slice(method.length + 1)));
}
return res;
}
export function parseWwwAuthenticateHeader(authHeader): {} {
authHeader = authHeader.trim();
const method = authHeader.split(" ", 1)[0];
const res = { method: method };
Object.assign(res, parseHeaderFeilds(authHeader.slice(method.length + 1)));
return res;
}
export function basic(username, password): string {
return "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
}
export function digest(
username,
password,
uri,
httpMethod,
body,
authHeader
username: string | Buffer,
realm: string | Buffer,
password: string | Buffer,
nonce: string | Buffer,
httpMethod: string | Buffer,
uri: string | Buffer,
qop?: string | Buffer,
body?: string | Buffer,
cnonce?: string | Buffer,
nc?: string | Buffer
): string {
const cnonce = "0a4f113b";
const nc = "00000001";
let qop;
if (authHeader.qop) {
if (authHeader.qop.indexOf(",") !== -1) qop = "auth";
// Either auth or auth-int, prefer auth
else qop = authHeader.qop;
}
const ha1 = createHash("md5");
ha1
.update(username)
.update(":")
.update(authHeader.realm)
.update(realm)
.update(":")
.update(password);
// TODO support "MD5-sess" algorithm directive
@@ -112,7 +114,7 @@ export function digest(
if (qop === "auth-int") {
const bodyHash = createHash("md5")
.update(body)
.update(body || "")
.digest("hex");
ha2.update(":").update(bodyHash);
}
@@ -123,7 +125,7 @@ export function digest(
hash
.update(ha1d)
.update(":")
.update(authHeader.nonce);
.update(nonce);
if (qop) {
hash
.update(":")
@@ -135,13 +137,47 @@ export function digest(
}
hash.update(":").update(ha2d);
return hash.digest("hex");
}
export function solveDigest(
username: string | Buffer,
password: string | Buffer,
uri: string | Buffer,
httpMethod: string | Buffer,
body: string | Buffer,
authHeader
): string {
const cnonce = randomBytes(8).toString("hex");
const nc = "00000001";
let qop;
if (authHeader.qop) {
if (authHeader.qop.indexOf(",") !== -1) qop = "auth";
// Either auth or auth-int, prefer auth
else qop = authHeader.qop;
}
const hash = digest(
username,
authHeader.realm,
password,
authHeader.nonce,
httpMethod,
uri,
qop,
body,
cnonce,
nc
);
let authString = `Digest username="${username}"`;
authString += `,realm="${authHeader.realm}"`;
authString += `,nonce="${authHeader.nonce}"`;
authString += `,uri="${uri}"`;
if (authHeader.algorithm) authString += `,algorithm=${authHeader.algorithm}`;
if (qop) authString += `,qop=${qop},nc=${nc},cnonce="${cnonce}"`;
authString += `,response="${hash.digest("hex")}"`;
authString += `,response="${hash}"`;
if (authHeader.opaque) authString += `,opaque="${authHeader.opaque}"`;
return authString;
+1 -1
View File
@@ -124,7 +124,7 @@ export async function httpConnectionRequest(
opts = Object.assign(
{
headers: {
Authorization: auth.digest(
Authorization: auth.solveDigest(
username,
password,
options.path,
+96 -40
View File
@@ -49,14 +49,17 @@ import {
import { IncomingMessage, ServerResponse } from "http";
import { Readable } from "stream";
import { promisify } from "util";
import { TLSSocket } from "tls";
const gzipPromisified = promisify(zlib.gzip);
const deflatePromisified = promisify(zlib.deflate);
const REALM = "GenieACS";
const MAX_CYCLES = 4;
const MAX_CONCURRENT_REQUESTS = +config.get("MAX_CONCURRENT_REQUESTS");
const currentSessions = new WeakMap<Socket, SessionContext>();
const sessionsNonces = new WeakMap<Socket, string>();
const stats = {
concurrentRequests: 0,
@@ -65,7 +68,10 @@ const stats = {
initiatedSessions: 0
};
async function authenticate(sessionContext: SessionContext): Promise<boolean> {
async function authenticate(
sessionContext: SessionContext,
body: string | Buffer
): Promise<boolean> {
const authExpression: Expression = localCache.getConfigExpression(
sessionContext.cacheSnapshot,
"cwmp.auth"
@@ -80,6 +86,21 @@ async function authenticate(sessionContext: SessionContext): Promise<boolean> {
);
}
if (authentication && authentication.method === "Digest") {
const sessionNonce = sessionsNonces.get(
sessionContext.httpRequest.connection
);
if (
!sessionNonce ||
authentication.nonce !== sessionNonce ||
(authentication.qop && (!authentication.cnonce || !authentication.nc))
)
return false;
authentication["body"] = body;
}
const now = Date.now();
const res = await evaluateAsync(
authExpression,
@@ -96,14 +117,33 @@ async function authenticate(sessionContext: SessionContext): Promise<boolean> {
const { fault, value } = await extensions.run(e.slice(2));
return fault ? null : value;
} else if (e[1] === "AUTH") {
if (authentication && authentication["method"] === "Basic") {
return (
authentication["username"] === e[2] &&
authentication["password"] === e[3]
);
} else {
return false;
const username = e[2];
const password = e[3];
if (username != null && password != null && authentication) {
if (authentication["method"] === "Basic") {
return (
authentication["username"] === e[2] &&
authentication["password"] === e[3]
);
}
if (authentication["method"] === "Digest") {
const expected = auth.digest(
username,
REALM,
password,
authentication["nonce"],
"POST",
authentication["uri"],
authentication["qop"],
authentication["body"],
authentication["cnonce"],
authentication["nc"]
);
return expected === authentication["response"];
}
}
return false;
}
}
return e;
@@ -898,7 +938,7 @@ async function cacheDueTasksAndFaultsAndOperations(
);
}
function reportBadState(sessionContext: SessionContext): void {
async function reportBadState(sessionContext: SessionContext): Promise<void> {
logger.accessError({
message: "Bad session state",
sessionContext: sessionContext
@@ -908,14 +948,45 @@ function reportBadState(sessionContext: SessionContext): void {
sessionContext.httpResponse.end("Bad session state");
}
async function responseUnauthorized(
sessionContext: SessionContext
): Promise<void> {
const authHeader = sessionContext.httpRequest.headers["authorization"];
const resHeaders = {};
if (authHeader) {
// Invalid credentials
logger.accessError({
message: "Authentication failure",
sessionContext: sessionContext
});
resHeaders["Connection"] = "close";
} else {
if (sessionContext.httpRequest.socket instanceof TLSSocket) {
resHeaders["WWW-Authenticate"] = `Basic realm="${REALM}"`;
} else {
const nonce = crypto.randomBytes(16).toString("hex");
sessionsNonces.set(sessionContext.httpRequest.connection, nonce);
let d = `Digest realm="${REALM}"`;
d += ',qop="auth,auth-int"';
d += `,nonce="${nonce}"`;
resHeaders["WWW-Authenticate"] = d;
}
currentSessions.set(sessionContext.httpRequest.connection, sessionContext);
}
sessionContext.httpResponse.writeHead(401, resHeaders);
sessionContext.httpResponse.end("Unauthorized");
}
async function processRequest(
sessionContext: SessionContext,
rpc: SoapMessage
): Promise<void> {
if (rpc.cpeRequest) {
if (rpc.cpeRequest.name === "Inform") {
if (sessionContext.state !== 0)
return void reportBadState(sessionContext);
if (sessionContext.state !== 0) return reportBadState(sessionContext);
logger.accessInfo({
sessionContext: sessionContext,
@@ -924,8 +995,7 @@ async function processRequest(
});
return inform(sessionContext, rpc);
} else if (rpc.cpeRequest.name === "TransferComplete") {
if (sessionContext.state !== 1)
return void reportBadState(sessionContext);
if (sessionContext.state !== 1) return reportBadState(sessionContext);
logger.accessInfo({
sessionContext: sessionContext,
@@ -957,12 +1027,12 @@ async function processRequest(
throw new Error("ACS method not supported");
}
} else if (rpc.cpeResponse) {
if (sessionContext.state !== 2) return void reportBadState(sessionContext);
if (sessionContext.state !== 2) return reportBadState(sessionContext);
await session.rpcResponse(sessionContext, rpc.id, rpc.cpeResponse);
return nextRpc(sessionContext);
} else if (rpc.cpeFault) {
if (sessionContext.state !== 2) return void reportBadState(sessionContext);
if (sessionContext.state !== 2) return reportBadState(sessionContext);
logger.accessWarn({
sessionContext: sessionContext,
@@ -978,7 +1048,7 @@ async function processRequest(
return nextRpc(sessionContext);
} else {
// CPE sent empty response
if (sessionContext.state !== 1) return void reportBadState(sessionContext);
if (sessionContext.state !== 1) return reportBadState(sessionContext);
sessionContext.state = 2;
const { faults, operations } = await session.timeoutOperations(
@@ -1121,13 +1191,15 @@ async function listenerAsync(
return processRequest(sessionContext, rpc);
}
const newConnection = !currentSessions.has(httpRequest.connection);
const sessionContext = await getSession(httpRequest.connection, sessionId);
if (sessionContext) {
sessionContext.httpRequest = httpRequest;
sessionContext.httpResponse = httpResponse;
if (
sessionContext.sessionId !== sessionId ||
(newConnection && sessionContext.sessionId !== sessionId) ||
sessionContext.lastActivity + sessionContext.timeout * 1000 < Date.now()
) {
logger.accessError({
@@ -1171,20 +1243,14 @@ async function listenerAsync(
}
if (sessionContext) {
// Authenticate in case of new connection or unauthenticated session
const authenticated =
sessionContext.state === 0 ? await authenticate(sessionContext) : true;
sessionContext.state === 0 || newConnection
? await authenticate(sessionContext, body)
: true;
if (!authenticated) {
logger.accessError({
message: "Authentication failure",
sessionContext: sessionContext
});
if (!authenticated) return responseUnauthorized(sessionContext);
httpResponse.writeHead(401);
httpResponse.end("Unauthorized");
currentSessions.set(httpRequest.connection, sessionContext);
return;
}
return parsedRpc(sessionContext, rpc, parseWarnings);
}
@@ -1262,18 +1328,8 @@ async function listenerAsync(
}
}
const authenticated = await authenticate(_sessionContext);
if (!authenticated) {
logger.accessError({
message: "Authentication failure",
sessionContext: _sessionContext
});
httpResponse.writeHead(401);
httpResponse.end("Unauthorized");
currentSessions.set(httpRequest.connection, _sessionContext);
return;
}
const authenticated = await authenticate(_sessionContext, body);
if (!authenticated) return responseUnauthorized(_sessionContext);
parsedRpc(_sessionContext, rpc, parseWarnings);
}
+49
View File
@@ -0,0 +1,49 @@
import ava from "ava";
import { randomBytes } from "crypto";
import * as auth from "../lib/auth";
ava("digest", t => {
const username = "test";
const password = "test";
const uri = "/";
const method = "POST";
const realm = "GeniceACS";
const nonce = randomBytes(16).toString("hex");
const body = randomBytes(128).toString();
const challenges = [
`Digest realm="${realm}",nonce="${nonce}"`,
`Digest realm="${realm}",nonce="${nonce}",qop="auth"`,
`Digest realm="${realm}",nonce="${nonce}",qop="auth-int"`
];
t.plan(challenges.length);
for (const challenge of challenges) {
const wwwAuthHeader = auth.parseWwwAuthenticateHeader(challenge);
const solution = auth.solveDigest(
username,
password,
uri,
method,
body,
wwwAuthHeader
);
const authHeader = auth.parseAuthorizationHeader(solution);
t.is(
authHeader["response"],
auth.digest(
username,
realm,
password,
nonce,
method,
uri,
authHeader["qop"],
body,
authHeader["cnonce"],
authHeader["nc"]
)
);
}
});