Add support for CPE digest authentication
This commit is contained in:
1 parent
f21ce3053a
commit
344ed3fdee
5 files changed
+228
-86
No files matched your search
@@ -27,6 +27,7 @@ const externals = [
|
||||
"fs",
|
||||
"cluster",
|
||||
"os",
|
||||
"tls",
|
||||
"http",
|
||||
"https",
|
||||
"zlib",
|
||||
|
||||
+81
-45
@@ -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;
|
||||
|
||||
@@ -124,7 +124,7 @@ export async function httpConnectionRequest(
|
||||
opts = Object.assign(
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.digest(
|
||||
Authorization: auth.solveDigest(
|
||||
username,
|
||||
password,
|
||||
options.path,
|
||||
|
||||
+96
-40
@@ -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);
|
||||
}
|
||||
@@ -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"]
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user