https://github.com/OpenVPN-Community/openvpn-radiusplugin/pull/9

commit bcd7cf10a3c8ad75cfe9231865df0aa43482bd91
Author: Samuel Thibault <samuel.thibault@ens-lyon.org>
Date:   Sun Oct 20 16:27:32 2024 +0200

    Add MessageAuthenticator support
    
    To address RadiusBLAST vulnerability.

diff --git a/RadiusClass/RadiusAttribute.cpp b/RadiusClass/RadiusAttribute.cpp
index da7d6d3..d3ca5d7 100755
--- a/RadiusClass/RadiusAttribute.cpp
+++ b/RadiusClass/RadiusAttribute.cpp
@@ -382,6 +382,14 @@ int RadiusAttribute::setValue(char *value)
 				
 			}
 			break;
+		case	ATTRIB_Message_Authenticator:
+			if(!(this->value=new Octet [16]))
+			{
+				return ALLOC_ERROR;
+			}
+			memcpy(this->value, value, 16);
+			this->length=(Octet)16;
+			break;
 			
 		//for datatype integer/enum	
 		case	ATTRIB_NAS_Port:					 
diff --git a/RadiusClass/RadiusConfig.cpp b/RadiusClass/RadiusConfig.cpp
index 18702bd..54c19c9 100755
--- a/RadiusClass/RadiusConfig.cpp
+++ b/RadiusClass/RadiusConfig.cpp
@@ -171,6 +171,17 @@ int RadiusConfig::parseConfigFile(const char * configfile)
 					{
 						tmpServer->setWait(atoi(line.substr(5).c_str()));
 					}
+					if (strncmp(line.c_str(),"requirema=",10)==0)
+					{
+						if (strcmp(line.substr(10).c_str(),"yes") == 0)
+							tmpServer->setRequireMA(1);
+						else if (strcmp(line.substr(10).c_str(),"no") == 0)
+							tmpServer->setRequireMA(0);
+						else if (strcmp(line.substr(10).c_str(),"auto") == 0)
+							tmpServer->setRequireMA(-1);
+						else
+							return PARSING_ERROR;
+					}
 				}
 				if(strstr(line.c_str(),"}"))
 				{
diff --git a/RadiusClass/RadiusPacket.cpp b/RadiusClass/RadiusPacket.cpp
index d97dcd1..3b1726c 100755
--- a/RadiusClass/RadiusPacket.cpp
+++ b/RadiusClass/RadiusPacket.cpp
@@ -65,6 +65,7 @@ RadiusPacket::RadiusPacket(Octet code)
 	this->length=sizeof(Octet)*(RADIUS_PACKET_AUTHENTICATOR_LEN+4);
 	this->sendbuffer=NULL;
 	this->sendbufferlen=0;
+	this->message_authenticator=0;
 	this->recvbuffer=NULL;
 	this->recvbufferlen=0;
 	this->sock=0;
@@ -195,6 +196,12 @@ int RadiusPacket::shapeRadiusPacket(const char * sharedsecret)
 	//add the attributes to the buffer
 	for (multimap<Octet, RadiusAttribute>::iterator it = attribs.begin(); it != attribs.end(); it++)
 	{
+		if (it->second.getType()==ATTRIB_Message_Authenticator)
+		{
+			// Record offset for filling it below.
+			this->message_authenticator = this->sendbufferlen+2;
+		}
+
 		//if the attribute is a password, build the hashedpassword
 		if (it->second.getType()==ATTRIB_User_Password)
 		{
@@ -406,6 +413,11 @@ int RadiusPacket::radiusSend(list<RadiusServer>::iterator server)
 		this->calcacctdigest(server->getSharedSecret().c_str());
 	
 	}
+
+	if (this->code==ACCESS_REQUEST && this->message_authenticator)
+	{
+		this->calcmadigest(server->getSharedSecret().c_str());
+	}
 	
 	//save the authenticator field for packet authentication on receiving a packet
 	memcpy(this->authenticator, this->req_authenticator, 16);
@@ -526,7 +538,7 @@ int RadiusPacket::radiusReceive(list<RadiusServer> *serverlist)
 					return UNSHAPE_ERROR;
 				}
 				
-				if (this->authenticateReceivedPacket(server->getSharedSecret().c_str())!=0)
+				if (this->authenticateReceivedPacket(&*(server))!=0)
 				{
 					
 					return WRONG_AUTHENTICATOR_IN_RECV_PACKET;
@@ -599,6 +611,44 @@ void RadiusPacket::calcacctdigest(const char *secret)
 	gcry_md_close(context);
 }
 
+/** Sets the message authenticator field if the packet is
+ * an access request. It is a MD5 hash over the whole packet
+ * (the authenticator field itself is set to 0) with the shared
+ * secret used as Hmac key.
+ * The authenticator is updated in the message authenticator option.
+ * @param secret The shared secret of the server in plaintext.
+ */
+void RadiusPacket::calcmadigest(const char *secret)
+{
+	//Octet		digest[MD5_DIGEST_LENGTH];
+	gcry_md_hd_t	context;
+
+	//Zero out the auth_vector in the received packet.
+	//Then calculate the MD5 HMAC with the shared secret as key.
+
+	memset((this->sendbuffer+this->message_authenticator), 0, 16);
+	//build the hash
+	if (!gcry_control (GCRYCTL_ANY_INITIALIZATION_P))
+	{ /* No other library has already initialized libgcrypt. */
+
+	  gcry_control(GCRYCTL_SET_THREAD_CBS,&gcry_threads_pthread);
+
+	  if (!gcry_check_version (NEED_LIBGCRYPT_VERSION) )
+	    {
+		cerr << "libgcrypt is too old (need " << NEED_LIBGCRYPT_VERSION << ", have " << gcry_check_version (NULL) << ")\n";
+	    }
+	    /* Disable secure memory.  */
+          gcry_control (GCRYCTL_DISABLE_SECMEM, 0);
+	  gcry_control (GCRYCTL_INITIALIZATION_FINISHED);
+	}
+	gcry_md_open (&context, GCRY_MD_MD5, GCRY_MD_FLAG_HMAC);
+	gcry_md_setkey(context, secret, strlen(secret));
+	gcry_md_write(context, this->sendbuffer, this->length);
+	//copy the digest to the packet
+	memcpy(this->sendbuffer+this->message_authenticator, gcry_md_read(context, GCRY_MD_MD5), 16);
+	gcry_md_close(context);
+}
+
 
 /** Returns a pointer to the authenticator field.
  * @return A pointer to the authenticator field.
@@ -653,9 +703,11 @@ pair<multimap<Octet,RadiusAttribute>::iterator,multimap<Octet,RadiusAttribute>::
  * @return A an integer, 0 if the authenticator field is ok, else WRONG_AUTHENTICATOR_IN_RECV_PACKET.
  */
 
-int	RadiusPacket::authenticateReceivedPacket(const char *secret)
+int	RadiusPacket::authenticateReceivedPacket(RadiusServer *server)
 {
+	const char *secret = server->getSharedSecret().c_str();
 	gcry_md_hd_t	context;
+	int res;
 	
 	Octet * cpy_recvpacket;
 	//make a copy of the received packet 
@@ -684,21 +736,53 @@ int	RadiusPacket::authenticateReceivedPacket(const char *secret)
 	gcry_md_open (&context, GCRY_MD_MD5, 0);
 	gcry_md_write(context, cpy_recvpacket, this->recvbufferlen);
 	gcry_md_write(context, secret, strlen(secret));
-	
-	delete[] cpy_recvpacket;
-	
+
 	//compare the received and the built authenticator
-	if (memcmp(this->recvbuffer+4, gcry_md_read(context, GCRY_MD_MD5), 16)!=0)
+	res = memcmp(this->recvbuffer+4, gcry_md_read(context, GCRY_MD_MD5), 16);
+	gcry_md_close(context);
+
+	if (res!=0)
 	{
-		gcry_md_close(context);
+		//Failed
+		delete[] cpy_recvpacket;
 		return WRONG_AUTHENTICATOR_IN_RECV_PACKET;
 	}
-	else
-	{ 
+
+	if (server->getRequireMA() == -1)
+		//We did't know yet whether this server sends Message-Authenticator
+		server->setRequireMA(this->recvbuffer[20] == 80);
+
+	if (server->getRequireMA() == 1)
+	{
+		//This server normally sends Message-Authenticator, check that
+		if (this->recvbufferlen < 20 + 18 || this->recvbuffer[20] != 80 || this->recvbuffer[21] != 18)
+		{
+			//No MA!
+			delete[] cpy_recvpacket;
+			return WRONG_AUTHENTICATOR_IN_RECV_PACKET;
+		}
+
+		//Clear MA hash for computation
+		memset(cpy_recvpacket+20+2, 0, 16);
+		//Hash
+		gcry_md_open (&context, GCRY_MD_MD5, GCRY_MD_FLAG_HMAC);
+		gcry_md_setkey(context, secret, strlen(secret));
+		gcry_md_write(context, cpy_recvpacket, this->recvbufferlen);
+
+		//Compare
+		res = memcmp(this->recvbuffer+20+2, gcry_md_read(context, GCRY_MD_MD5), 16);
 		gcry_md_close(context);
-		return 0;
+
+		if (res != 0)
+		{
+			//Failed
+			delete[] cpy_recvpacket;
+			return WRONG_AUTHENTICATOR_IN_RECV_PACKET;
+		}
 	}
-		
+
+	delete[] cpy_recvpacket;
+	return 0;
 }
 
 
diff --git a/RadiusClass/RadiusPacket.h b/RadiusClass/RadiusPacket.h
index 7b02408..847c23d 100755
--- a/RadiusClass/RadiusPacket.h
+++ b/RadiusClass/RadiusPacket.h
@@ -73,10 +73,13 @@ private:
 	
 	Octet				*sendbuffer;  			/**<Buffer for sending the packet over the network.*/
 	int					sendbufferlen; 			/**<Length of the buffer.*/
+	int					message_authenticator;	/**<Offset of the message authenticator digest, if any.*/
 	Octet				*recvbuffer;  			/**<Buffer for recveing the packet over the network.*/
 	int					recvbufferlen; 			/**<Length of the buffer.*/
 	void            	calcacctdigest(const char *secret); /**Method to generate the hash 
 	for the authenticator in Accounting-Requests.*/
+	void            	calcmadigest(const char *secret); /**Method to generate the hash 
+	for the message authenticator in Access-Requests.*/
 	
 	//private functions
 	void 			getRandom(int len, Octet *num);
@@ -101,7 +104,7 @@ public:
 	
 	int				getCode(void);
 	
-	int				authenticateReceivedPacket(const char *secret);
+	int				authenticateReceivedPacket(RadiusServer *server);
 	
 	pair<multimap<Octet,RadiusAttribute>::iterator,multimap<Octet,RadiusAttribute>::iterator> findAttributes(int type);
 	
diff --git a/RadiusClass/RadiusServer.cpp b/RadiusClass/RadiusServer.cpp
index 27f2011..a90c9b3 100755
--- a/RadiusClass/RadiusServer.cpp
+++ b/RadiusClass/RadiusServer.cpp
@@ -32,13 +32,14 @@
 	 * @param int wait : The time (in seconds) to wait on a response of the radius server.
 	 */
 RadiusServer::RadiusServer(string name, string secret,
-	int authport,  int acctport, int retry, int wait)
+	int authport,  int acctport, int retry, int wait, int requirema)
 {
 	this->acctport=acctport;
 	this->authport=authport;
 	this->name=name;
 	this->retry=retry;
 	this->wait=wait;
+	this->requirema=requirema;
 	this->sharedsecret=secret;
 	
 	
@@ -62,6 +63,7 @@ RadiusServer &RadiusServer::operator=(const RadiusServer &s)
 	this->retry=s.retry;
 	this->acctport=s.acctport;
 	this->authport=s.authport;
+	this->requirema=s.requirema;
 	this->sharedsecret=s.sharedsecret;
 	return (*this);
 }
@@ -186,6 +188,24 @@ void RadiusServer::setWait(int w)
 	}
 }
 
+/** The getter method for the private member requirema
+ * @return A integer 0 when MA Message-Authenticator is not required, 1 if it
+ * is required, -1 if automatically determined from first answer.
+ */
+int RadiusServer::getRequireMA(void)
+{
+	return this->requirema;
+}
+
+
+/** The setter method for the private member requirema
+ * @param ma integer 0 when MA Message-Authenticator is not required, 1 if it
+ */
+void RadiusServer::setRequireMA(int ma)
+{
+	this->requirema=ma;
+}
+
 ostream& operator << (ostream& os, RadiusServer& server)
 {
      os << "\n\nRadiusServer:";
@@ -194,6 +214,7 @@ ostream& operator << (ostream& os, RadiusServer& server)
      os << "\nAccounting-Port: " << server.acctport;
      os << "\nRetries: " << server.retry;
      os << "\nWait: " << server.wait;
+     os << "\nRequireMA: " << server.requirema;
      os << "\nSharedSecret: *******";
  	return os;
  	
diff --git a/RadiusClass/RadiusServer.h b/RadiusClass/RadiusServer.h
index f29f5ca..73d0dbc 100755
--- a/RadiusClass/RadiusServer.h
+++ b/RadiusClass/RadiusServer.h
@@ -36,11 +36,12 @@ private:
 	int 	retry; 				/**< The number of retries how many times a radius ticket is send to the server, if it doesn#t answer.*/
 	string sharedsecret;		/**< The sharedsecret, the maximum space is 16 chars.*/
 	int 	wait;				/**< The time to wait for a response of the server.*/
+	int 	requirema;			/**< Whether to required Message-Authenticator from the server.*/
 
 public:
 	
 	
-	RadiusServer(string name="127.0.0.1",string secret = "", int authport=1812, int acctport=1813, int retry=3, int wait=1);
+	RadiusServer(string name="127.0.0.1",string secret = "", int authport=1812, int acctport=1813, int retry=3, int wait=1, int requirema=-1);
 	~RadiusServer();
 	RadiusServer &operator=(const RadiusServer &);
 	
@@ -62,6 +63,9 @@ public:
 	string getName();
 	void setName(string);
 	
+	void setRequireMA(int);
+	int getRequireMA(void);
+
 	friend ostream& operator << (ostream& os, RadiusServer& server);
 };
 
diff --git a/RadiusClass/exampleconfig b/RadiusClass/exampleconfig
index 6a303d8..ec8e3ec 100644
--- a/RadiusClass/exampleconfig
+++ b/RadiusClass/exampleconfig
@@ -11,6 +11,7 @@ server
 	retry=1
 	wait=1
 	sharedsecret=testpw
+	requirema=auto
 
 }
 
@@ -22,4 +23,5 @@ server
 	retry=1
 	wait=1
 	sharedsecret=xxxx
+	requirema=auto
 }
diff --git a/UserAuth.cpp b/UserAuth.cpp
index 8b44256..525b80d 100755
--- a/UserAuth.cpp
+++ b/UserAuth.cpp
@@ -48,8 +48,10 @@ int UserAuth::sendAcceptRequestPacket(PluginContext * context)
 {
 	list<RadiusServer> * serverlist;
 	list<RadiusServer>::iterator server;
+	std::string		zero("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 16);
 	RadiusPacket		packet(ACCESS_REQUEST);
-	RadiusAttribute		ra1(ATTRIB_User_Name,this->getUsername().c_str()),
+	RadiusAttribute		ra0(ATTRIB_Message_Authenticator,zero),
+				ra1(ATTRIB_User_Name,this->getUsername().c_str()),
 				ra2(ATTRIB_User_Password),
 				ra3(ATTRIB_NAS_Port,this->getPortnumber()),
 				ra4(ATTRIB_Calling_Station_Id,this->getCallingStationId()),
@@ -72,8 +74,13 @@ int UserAuth::sendAcceptRequestPacket(PluginContext * context)
 	
 	if (DEBUG (context->getVerbosity()))
 		cerr << getTime() << "RADIUS-PLUGIN: Build password packet:  password: *****, sharedSecret: *****.\n";
-	
+
 	//add the attributes
+	if(packet.addRadiusAttribute(&ra0))
+	{
+		cerr << getTime() << "RADIUS-PLUGIN: Fail to add attribute ATTRIB_Message_Authenticator.\n";
+	}
+
 	ra2.setValue(this->password);
 	if(packet.addRadiusAttribute(&ra1))
 	{
diff --git a/radiusplugin.cnf b/radiusplugin.cnf
index c7ff9a0..f6988bc 100755
--- a/radiusplugin.cnf
+++ b/radiusplugin.cnf
@@ -97,6 +97,9 @@ server
 	wait=1
 	# The shared secret.
 	sharedsecret=testpw
+	# Whether to require Message-Authenticator (yes) or not (no), or require if first answer included it (auto)
+	# For better security against RadiusBLAST, set to yes once it is confirmed that your radius server always sends it
+	requirema=auto
 }
 
 server
@@ -113,5 +116,8 @@ server
 	wait=1
 	# The shared secret.
 	sharedsecret=testpw
+	# Whether to require Message-Authenticator (yes) or not (no), or require if first answer included it (auto)
+	# For better security against RadiusBLAST, set to yes once it is confirmed that your radius server always sends it
+	requirema=auto
 }
 
