EternalWindows
SSPI / ハンドシェイク
対応するサーバーはこちら

SSPIによる認証は、クライアントとサーバーがデータを交換することで行われます。 具体的にどのようなデータが交換されるかはSSPによって異なりますが、 最終的にコンテキストが作成されるという点は、どのようなSSPを使用する場合でも共通しています。 コンテキストが完全に作成された場合、それは認証が成功したことを意味し、 コンテキストに格納される暗号鍵を通じて暗号化を行うことができるようになります。 コンテキストを作成するために行う、 クライアントとサーバーの一連のデータ交換はハンドシェイクと呼ばれます。

SSPIにおけるハンドシェイクは、クライアントを先頭に行われます。 クライアントはInitializeSecurityContextを呼び出して、 サーバーに送信するためのデータを作成します。

SECURITY_STATUS SEC_Entry InitializeSecurityContext(
  PCredHandle phCredential,
  PCtxtHandle phContext,
  SEC_CHAR *pszTargetName,
  ULONG fContextReq,
  ULONG Reserved1,
  ULONG TargetDataRep,
  PSecBufferDesc pInput,
  ULONG Reserved2,
  PCtxtHandle phNewContext,
  PSecBufferDesc pOutput,
  PULONG pfContextAttr,
  PTimeStamp ptsExpiry
);

phCredentialは、クレデンシャルハンドルのアドレスを指定します。 phContextは、コンテキストハンドルのアドレスを指定します。 コンテキストハンドルは、作成されたコンテキストを表すハンドルです。 pszTargetNameは、サーバーを識別する文字列を指定します。 具体的にどのような形式になるかは、SSPによって異なります。 Reserved1は、予約されているため0を指定します。 fContextReqは、コンテキストに要求したい定数を指定します。 TargetDataRepは、バイトオーダーを表す定数を指定します。 通常は、SECURITY_NETWORK_DREPを指定します。 pInputは、サーバーによって作成されたデータを指定します。 Reserved2は、予約されているため0を指定します。 phNewContextは、このInitializeSecurityContextの呼び出しで新しく作成されたコンテキストハンドルを 受け取る変数のアドレスを指定します。 pOutputは、サーバーに送信するためのデータを受け取るバッファを指定します。 pfContextAttrは、コンテキストに割り当てられた定数を受け取る変数のアドレスを指定します。 ptsExpiryは、コンテキストの有効期限切れ時間を受け取る変数のアドレスを指定します。 不要な場合は、NULLを指定することができます。

InitializeSecurityContext(及びサーバーが呼び出すAcceptSecurityContext)は、 データをSecBufferDesc構造体で表しています。 この構造体は、次のように定義されています。

typedef struct _SecBufferDesc {
  ULONG      ulVersion;
  ULONG      cBuffers;
  PSecBuffer pBuffers;
} SecBufferDesc, *PSecBufferDesc

ulVersionは、SECBUFFER_VERSIONを指定します。 cBuffersは、pBuffersに指定したSecBuffer構造体の数を指定します。 pBuffersは、SecBuffer構造体の配列を指定します。 実際にデータを格納するのはこのSecBuffer構造体であり、 SecBufferDescは複数のSecBuffer構造体を1つとして扱うために存在しています。 SecBuffer構造体は、次のように定義されています。

typedef struct _SecBuffer {
  ULONG cbBuffer;
  ULONG BufferType;
  PVOID pvBuffer;
} SecBuffer, *PSecBuffer;

cbBufferは、pvBufferに指定したバッファのサイズを指定します。 BufferTypeは、バッファの種類を表す定数を指定します。 pvBufferは、データを格納したバッファを指定します。

InitializeSecurityContextはデータやコンテキストを作成するだけであり、 実際にサーバーとデータの交換を行うのは、アプリケーションの役割であることに注意してください。 この作業分担により、アプリケーションはWinsockや名前付きパイプなどの任意のネットワークAPIを使用して、 サーバーと通信することができます。 また、アプリケーションは、送受信することになるデータの中身を意識する必要はありません。 単純にサーバーから送られてきたデータをInitializeSecurityContextに指定し、 クライアントから送られてきたデータをAcceptSecurityContextに指定するだけで問題ありません。

不要になったコンテキストハンドルは、DeleteSecurityContextで削除します。

SECURITY_STATUS SEC_Entry DeleteSecurityContext(
  PCtxtHandle phContext
);

phContextは、コンテキストハンドルを指定します。

今回のプログラムは、クライアントにおけるハンドシェイクの例を示しています。 実際にサーバーと通信するコードは含まれていないため、実行しても失敗することになります。

#define  SECURITY_WIN32
#include <windows.h>
#include <security.h>

#pragma comment (lib, "secur32.lib")

BOOL ClientHandshake(PCredHandle phCredential, PCtxtHandle phContext);
void SendData(LPVOID lpData, ULONG uSize);
LPVOID ReceiveData(ULONG *lpuSize);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	CredHandle hCredential;
	CtxtHandle hContext;
	TimeStamp  ts;

	if (AcquireCredentialsHandle(NULL, TEXT("NTLM"), SECPKG_CRED_OUTBOUND, NULL, NULL, NULL, NULL, &hCredential, &ts) != SEC_E_OK) {
		MessageBox(NULL, TEXT("クレデンシャルハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	if (!ClientHandshake(&hCredential, &hContext)) {
		FreeCredentialsHandle(&hCredential);
		MessageBox(NULL, TEXT("認証に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	DeleteSecurityContext(&hContext);
	FreeCredentialsHandle(&hCredential);

	return 0;
}

BOOL ClientHandshake(PCredHandle phCredential, PCtxtHandle phContext)
{
	BOOL            bFirst = TRUE;
	ULONG           uSize;
	ULONG           uAttributes = ISC_REQ_STREAM;
	SecBuffer       sbOut[1];
	SecBuffer       sbIn[1];
	SecBufferDesc   sbdOut;
	SecBufferDesc   sbdIn;
	SECURITY_STATUS ss = SEC_I_CONTINUE_NEEDED;
	
	while (ss == SEC_I_CONTINUE_NEEDED) {
		if (!bFirst) {
			sbIn[0].pvBuffer   = ReceiveData(&uSize);
			sbIn[0].cbBuffer   = uSize;
			sbIn[0].BufferType = SECBUFFER_TOKEN;

			sbdIn.ulVersion = SECBUFFER_VERSION;
			sbdIn.cBuffers  = 1;
			sbdIn.pBuffers  = sbIn;
		}
		
		sbOut[0].cbBuffer   = 0;
		sbOut[0].BufferType = SECBUFFER_TOKEN;
		sbOut[0].pvBuffer   = NULL;

		sbdOut.ulVersion = SECBUFFER_VERSION;
		sbdOut.cBuffers  = 1;
		sbdOut.pBuffers  = sbOut;
		
		ss = InitializeSecurityContext(phCredential, bFirst ? NULL : phContext, NULL, uAttributes | ISC_REQ_ALLOCATE_MEMORY,
			0, SECURITY_NETWORK_DREP, bFirst ? NULL : &sbdIn, 0, phContext, &sbdOut, &uAttributes, NULL);
		
		if (sbOut[0].cbBuffer != 0) {
			SendData(sbOut[0].pvBuffer, sbOut[0].cbBuffer);
			FreeContextBuffer(sbOut[0].pvBuffer);
		}

		bFirst = FALSE;
	}

	return ss == SEC_E_OK;
}

void SendData(LPVOID lpData, ULONG uSize)
{

}

LPVOID ReceiveData(ULONG *lpuSize)
{
	*lpuSize = 0;

	return NULL;
}

既に述べたように、SSPIはデータの送信や受信までは行いませんから、 こうした処理はアプリケーションが用意しなければなりません。 SendDataはデータを通信相手に送信する関数であり、 ReceiveDataは通信相手からデータを受信する関数です。 ただし、今回はInitializeSecurityContextを呼び出すためのコードに集中したいため、 SendDataとReceiveDataの存在はダミーとなっています。

WinMainで行っていることは、クレデンシャルハンドルの取得と、 ハンドシェイクを行うClientHandshake関数の呼び出しです。 今回は、NTLM認証を行うため、AcquireCredentialsHandleの第2引数にNTLMを指定しています。 ClientHandshakeの第1引数はクレデンシャルハンドルであり、 第2引数は関数内部で作成されたコンテキストハンドルを受け取ります。 コンテキストハンドルは、CtxtHandle構造体で表されます。 ClientHandshakeの内部を順に見ていきます。

BOOL            bFirst = TRUE;
ULONG           uSize;
ULONG           uAttributes = ISC_REQ_STREAM;
SecBuffer       sbOut[1];
SecBuffer       sbIn[1];
SecBufferDesc   sbdOut;
SecBufferDesc   sbdIn;
SECURITY_STATUS ss = SEC_I_CONTINUE_NEEDED;

bFirstは、サーバーにデータを送信するのが初めての場合にTRUEになります。 これがTRUEである場合は、サーバーからのデータが届いていないことを意味するため、 InitializeSecurityContextの呼び出しが変化することになります。 uSizeは、サーバーから受信したデータのサイズを格納します。 uAttributesは、コンテキストに要求したい定数を指定します。 ISC_REQ_STREAMは、ストリーム通信を行うことを意味します。 sbOutはサーバーに送信するためのデータを格納し、 sbInはサーバーから受信したデータを格納します。 sbdOutはsbOutを格納し、sbdInはsbInを格納します。 ssは、InitializeSecurityContextの戻り値を格納します。 これがSEC_I_CONTINUE_NEEDEDである場合は、 まだコンテキストが完成していないことを意味するため、 引き続きInitializeSecurityContextを呼び出してサーバーとのハンドシェイクを行います。

ハンドシェイクは、InitializeSecurityContextを呼び出してデータを作成し、 これをサーバーに送信することで行われます。 ただし、これだけではサーバーからの応答を考慮したデータを作成できないため、 2回目以降のInitializeSecurityContextの呼び出しでは、 サーバーから取得したデータをInitializeSecurityContextに指定することになります。 InitializeSecurityContextの呼び出しに必要なsbdInとsbdOutは、次のように初期化されています。

if (!bFirst) {
	ULONG uSize;

	sbIn[0].pvBuffer   = ReceiveData(&uSize);
	sbIn[0].cbBuffer   = uSize;
	sbIn[0].BufferType = SECBUFFER_TOKEN;

	sbdIn.ulVersion = SECBUFFER_VERSION;
	sbdIn.cBuffers  = 1;
	sbdIn.pBuffers  = sbIn;
}

sbOut[0].cbBuffer   = 0;
sbOut[0].BufferType = SECBUFFER_TOKEN;
sbOut[0].pvBuffer   = NULL;

sbdOut.ulVersion = SECBUFFER_VERSION;
sbdOut.cBuffers  = 1;
sbdOut.pBuffers  = sbOut;

bFirstがFALSEである場合は2回目以降のループであり、 既にInitializeSecurityContextを呼び出してデータをサーバーに送信しているはずですから、 サーバーからの応答がクライアントに届いているはずです。 これをReceiveDataで取得し、取得したデータをsbIn[0].pvBufferに指定します。 cbBufferにはデータのサイズを指定し、BufferTypeにはSECBUFFER_TOKENを指定します。 sbInの初期化が終われば、このアドレスをsbdInに指定します。 sbOutは、サーバーに送信するデータを格納しますが、pvBufferにはNULLを指定しています。 理由は、送信するデータがInitializeSecurityContextによって作成されるからです。 sbOutの初期化が終われば、このアドレスをsbdOutに指定します。 InitializeSecurityContextの呼び出しは、次のようになっています。

ss = InitializeSecurityContext(phCredential, bFirst ? NULL : phContext, NULL, uAttributes | ISC_REQ_ALLOCATE_MEMORY,
	0, SECURITY_NETWORK_DREP, bFirst ? NULL : &sbdIn, 0, phContext, &sbdOut, &uAttributes, NULL);

第2引数はコンテキストハンドルを指定しますが、 bFirstがTRUEの場合はNULLを指定します。 これは、1回目の呼び出し時点ではコンテキストハンドルが作成されていないからです。 第7引数はサーバーのデータを指定しますが、 これについてもbFirstがTRUEの場合はNULLを指定します。 この場合は、サーバーからデータがまだ送られていないはずですから、 有効なデータを指定することはできません。 第4引数は、コンテキストに要求する定数を指定しますが、 ISC_REQ_ALLOCATE_MEMORYはuAttributesに格納していません。 これはデータを格納するためのメモリをSSPIに確保してもらうための定数であり、 コンテキストに割り当てても意味がないため、 別途指定するようにしています。 InitializeSecurityContextが制御を返せば、作成されたデータをサーバーに送信します。

if (sbOut[0].cbBuffer != 0) {
	SendData(sbOut[0].pvBuffer, sbOut[0].cbBuffer);
	FreeContextBuffer(sbOut[0].pvBuffer);
}

bFirst = FALSE;

cbBufferが0でない場合は、データがSSPIによって作成されたことを意味するため、 これをSendDataでサーバーに送信します。 データの送信を終えたら、これは不要になるためFreeContextBufferで開放します。 bFirstをFALSEにしているのは、1回目のInitializeSecurityContextの呼び出しを終えたからです。 次に、InitializeSecurityContextが呼ばれるのは、サーバーから送られてきたデータを取得してからになります。


戻る