Windows Vista부터 바뀐 Credential Provider 예제 분석 (1)
8년 전에 보아둔 것을 이제서야 확인하게 되는군요. ^^
Credential Provider Samples
; https://www.sysnet.pe.kr/2/1/337
Windows XP/2003까지 GINA라고 불리던 인증 모듈이 비스타부터 새로운 체계의 "Credential Provider"로 바뀌었습니다. 관련해서 예제 프로젝트를 다음의 링크에서 다운로드할 수 있습니다.
Windows Vista Credential Provider Samples (이 글의 첨부 파일에 포함)
; http://www.microsoft.com/en-us/download/details.aspx?id=4057
압축 해제 후 함께 첨부되어 있는 "Windows Vista Credential Provider Samples Overview UPDATE.doc" 문서를 보시면 대충 어떤 구조인지 눈에 들어올 텐데요. 어쨌든 나름대로 정리를 해보겠습니다. ^^
가장 기본적인 예제는 역시 SampleCredentialProvider 프로젝트입니다. SampleCredentialProvider 폴더를 열어 SampleCredentialProvider.sln 파일을 Visual Studio에서 로드한 다음 빌드하면 SampleCredentialProvider.dll 파일이 생성됩니다.
SampleCredentialProvider는 크게 다음의 2가지 인터페이스를 구현합니다.
- ICredentialProvider
가용한 Credential 개체를 열람하는 기능, SampleCredentialProvider 예제에서는 "Administrator", "Guest" 계정만 구현
쉽게 말해서, 윈도우 시스템은 사용자가 만든 DLL로부터 ICredentialProvider 인터페이스를 구현한 개체를 얻고
그것으로부터 등록된 사용자 계정을 열람.
- ICredentialProviderCredential
인증 절차를 수행하는 동안 각각의 Credential이 갖춰야 할 필수 기능을 노출
(즉, 사용자에 대한 인증 기능을 구현)
ICredentialProviderCredential 인터페이스를 구현한 개체 하나는 사용자 계정 하나를 의미합니다. 예를 들어, ICredentialProviderCredential::GetBitmapValue 메서드에서 반환하는 이미지는 로그인 화면에서 해당 사용자 계정을 나타낼 때 보여지는 이미지를 반환하게 됩니다. SampleCredentialProvider 예제에서는 "tileimage.bmp" 파일이 리소스로 포함되어 있는데, 이것으로 "Administrator", "Guest" 계정 2개에 대해 같은 이미지를 반환하도록 작성되어 있기 때문에 다음과 같이 로그인 화면에서 2개 계정 모두 같은 이미지가 나타납니다.
정리해 보면, ICredentialProvider는 컬렉션 컨테이너라고 보시면 되고 그 요소는 ICredentialProviderCredential 인터페이스를 구현하는 정도!
SampleCredentialProvider 예제의 코드는 COM 개체를 노출하지만 regsvr32.exe 등록 과정을 통하지는 않습니다. 그래서 'SampleCredentialProvider.def' 파일에 등록과 관련된 함수는 누락되어 있습니다.
LIBRARY SAMPLECREDENTIALPROVIDER.DLL
EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
대신 등록을 위한 Register.reg 파일과,
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{b84ca702-35a8-4e67-8d2a-6c2807b297d3}]
@="SampleCredentialProvider"
[HKEY_CLASSES_ROOT\CLSID\{b84ca702-35a8-4e67-8d2a-6c2807b297d3}]
@="SampleCredentialProvider"
[HKEY_CLASSES_ROOT\CLSID\{b84ca702-35a8-4e67-8d2a-6c2807b297d3}\InprocServer32]
@="SampleCredentialProvider.dll"
"ThreadingModel"="Apartment"
등록 해제를 위한 Unregister.reg 파일이 제공됩니다.
Windows Registry Editor Version 5.00
[-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{b84ca702-35a8-4e67-8d2a-6c2807b297d3}]
보시는 바와 같이 "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers" 쪽에 우리가 만든 사용자 정의 Credential Provider를 등록하고, DLL을 system32 폴더에 복사해서 InprocServer32에서 경로 없이 등록했어도 찾을 수 있도록 합니다.
그런데, 왜 불편하게 regsvr32.exe를 이용해 등록할 수 있도록 DllRegisterServer 함수를 export 안 해주고 이런 식으로 예제를 구성한 것일까요? 아마도, 간결한 예제를 유지하기 위해서인 듯합니다. ATL 라이브러리를 포함하면 DllRegisterServer 함수의 구현이 쉽겠지만 의존성이 생기고, ATL 라이브러리를 포함시키지 않으면 예제에 .rgs 등록 코드까지 추가해야 하는데 이는 엄밀히 Credential Provider 예제와는 무관하기 때문입니다.
참고로, InprocServer32에 경로를 명시해도 됩니다.
[HKEY_CLASSES_ROOT\CLSID\{b84ca702-35a8-4e67-8d2a-6c2807b297d3}\InprocServer32]
@="c:\\temp\\SampleCredentialProvider.dll"
"ThreadingModel"="Apartment"
실제 제품 배포 시에는 system32 (또는 Program Files)에다 하는 것이 보안상 좋겠지만 개발 시에는 이렇게 해서 테스트/디버깅하는 것이 더 편합니다.
Dll.cpp 파일과 Dll.h 파일은 DllMain을 구현하고, DllCanUnloadNow, DllGetClassObject 함수 구현을 포함합니다.
운영체제는 레지스트리를 통해서 SampleCredentialProvider.dll 파일을 로드할 것이고, export 함수 중에 DllGetClassObject를 호출합니다.
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
return CClassFactory_CreateInstance(rclsid, riid, ppv);
}
HRESULT CClassFactory_CreateInstance(REFCLSID rclsid, REFIID riid, void** ppv)
{
HRESULT hr;
if (CLSID_CSampleProvider == rclsid)
{
CClassFactory* pcf = new CClassFactory;
if (pcf)
{
hr = pcf->QueryInterface(riid, ppv);
pcf->Release();
}
else
{
hr = E_OUTOFMEMORY;
}
}
else
{
hr = CLASS_E_CLASSNOTAVAILABLE;
}
return hr;
}
보시는 바와 같이 우리가 만든 Credential Provider COM 개체를 만들어 주는 CClassFactory라는 COM Class Factory 개체를 생성해서 반환합니다.
class CClassFactory : public IClassFactory
{
public:
// IUnknown
STDMETHOD_(ULONG, AddRef)() { ... }
STDMETHOD_(ULONG, Release)() { ... }
STDMETHOD (QueryInterface)(REFIID riid, void** ppv) { ... }
// IClassFactory
STDMETHOD (CreateInstance)(IUnknown* pUnkOuter, REFIID riid, void** ppv)
{
HRESULT hr;
if (!pUnkOuter)
{
hr = CSampleProvider_CreateInstance(riid, ppv);
}
else
{
hr = CLASS_E_NOAGGREGATION;
}
return hr;
}
STDMETHOD (LockServer)(BOOL bLock) { ... }
private:
CClassFactory() : _cRef(1) {}
~CClassFactory(){}
private:
LONG _cRef;
friend HRESULT CClassFactory_CreateInstance(REFCLSID rclsid, REFIID riid, void** ppv);
};
전형적인 Class Factory 코드를 따르고 있으며 CreateInstance에서 CSampleProvider.cpp에 구현된 CSampleProvider_CreateInstance 함수를 호출하고 있습니다.
만약 여러분만의 Credential Provider 모듈을 만든다면 위의 Dll.h, Dll.cpp 파일은 거의 그대로 사용해도 됩니다. 단지 CLSID_CSampleProvider와 그에 대한 GUID 값만 살짝 바꿔 주시고 나머지 2개의 파일(CSampleCredential.cpp, CSampleProvider.cpp)만 자신의 시나리오에 맞게 작성하시면 됩니다.
이제, 시스템에 등록된 "계정"들을 열람하는 CSampleProvider 코드를 살펴보겠습니다. Credential Provider 개체는 반드시 ICredentialProvider 인터페이스를 구현해야 합니다.
ICredentialProvider : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE SetUsageScenario(
/* [in] */ CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
/* [in] */ DWORD dwFlags) = 0;
virtual HRESULT STDMETHODCALLTYPE SetSerialization(
/* [in] */ const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcs) = 0;
virtual HRESULT STDMETHODCALLTYPE Advise(
/* [in] */ ICredentialProviderEvents *pcpe,
/* [in] */ UINT_PTR upAdviseContext) = 0;
virtual HRESULT STDMETHODCALLTYPE UnAdvise( void) = 0;
virtual HRESULT STDMETHODCALLTYPE GetFieldDescriptorCount(
/* [out] */ DWORD *pdwCount) = 0;
virtual HRESULT STDMETHODCALLTYPE GetFieldDescriptorAt(
/* [in] */ DWORD dwIndex,
/* [out] */ CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR **ppcpfd) = 0;
virtual HRESULT STDMETHODCALLTYPE GetCredentialCount(
/* [out] */ DWORD *pdwCount,
/* [out] */ DWORD *pdwDefault,
/* [out] */ BOOL *pbAutoLogonWithDefault) = 0;
virtual HRESULT STDMETHODCALLTYPE GetCredentialAt(
/* [in] */ DWORD dwIndex,
/* [out] */ ICredentialProviderCredential **ppcpc) = 0;
};
다음은 CSampleProvider.h 파일의 개략적인 코드입니다.
#include <credentialprovider.h>
#include <windows.h>
#include <strsafe.h>
#include "CSampleCredential.h"
#include "helpers.h"
#define MAX_CREDENTIALS 3
#define MAX_DWORD 0xffffffff // maximum DWORD
class CSampleProvider : public ICredentialProvider
{
public:
// 전형적인 IUnknown 구현 코드
STDMETHOD_(ULONG, AddRef)() { return _cRef++; }
STDMETHOD_(ULONG, Release)() { ... }
STDMETHOD (QueryInterface)(REFIID riid, void** ppv)
{
HRESULT hr;
if (IID_IUnknown == riid ||
IID_ICredentialProvider == riid)
{
*ppv = this;
reinterpret_cast<IUnknown*>(*ppv)->AddRef();
hr = S_OK;
}
else
{
*ppv = NULL;
hr = E_NOINTERFACE;
}
return hr;
}
public:
// ICredentialProvider 인터페이스의 메서드
IFACEMETHODIMP SetUsageScenario(CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus, DWORD dwFlags);
IFACEMETHODIMP SetSerialization(const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs);
IFACEMETHODIMP Advise(__in ICredentialProviderEvents* pcpe, UINT_PTR upAdviseContext);
IFACEMETHODIMP UnAdvise();
IFACEMETHODIMP GetFieldDescriptorCount(__out DWORD* pdwCount);
IFACEMETHODIMP GetFieldDescriptorAt(DWORD dwIndex, __deref_out CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR** ppcpfd);
IFACEMETHODIMP GetCredentialCount(__out DWORD* pdwCount,
__out DWORD* pdwDefault,
__out BOOL* pbAutoLogonWithDefault);
IFACEMETHODIMP GetCredentialAt(DWORD dwIndex,
__out ICredentialProviderCredential** ppcpc);
// CClassFactory의 CreateInstance 메서드에서 Credential Provider 개체를 생성할 때 사용한 메서드
friend HRESULT CSampleProvider_CreateInstance(REFIID riid, __deref_out void** ppv);
protected:
CSampleProvider();
__override ~CSampleProvider();
private:
// ...[생략]...
};
가능한 실행 순서에 따라 한번 따라가 보겠습니다. 우선, Class Factory에서 CSampleProvider_CreateInstance 메서드를 불러서 우리가 제공하는 Credential Provider 개체를 생성해서 윈도우 시스템에 반환합니다.
HRESULT CSampleProvider_CreateInstance(REFIID riid, void** ppv)
{
HRESULT hr;
CSampleProvider* pProvider = new CSampleProvider();
if (pProvider)
{
hr = pProvider->QueryInterface(riid, ppv);
pProvider->Release();
}
else
{
hr = E_OUTOFMEMORY;
}
return hr;
}
위의 구현 코드 역시 전형적인 재사용 예제 코드이므로 그냥 사용하시면 됩니다. CSampleProvider 개체까지 생성되었으니, 이제 ICredentialProvider 인터페이스의 구현에 윈도우 시스템과 상호작용하는 부분만 남았습니다.
이에 대한 구체적인 실행 순서는 이후의 글에서 별도로 다루겠습니다.
Credential Provider의 특성상 디버깅 시에 DLL의 "잠김"에 유의해야 합니다. 다행히 윈도우 시스템이 항상 로드해서 잠그진 않고 필요할 때만 DLL을 로드하는데, 예를 들어 "로그인 화면"으로 빠진 경우에만 DLL이 잠기므로 그 외에는 빌드하고 곧바로 배포할 수 있으므로 개발상 불편함이 GINA에 비해 많이 줄었습니다. 또한,
CredUIPromptForWindowsCredentials Win32 API를 이용하면 F5 디버깅까지 가능합니다. 이에 대해서는 다음번에 알아보겠습니다.
마치기 전에, 한 가지 주의해야 할 사항이 있는데요.
SampleCredentialProvider 예제 프로젝트는 MSVCR???.dll에 의존성을 가지고 있습니다. 가령 Visual Studio 2013으로 빌드하면 MSVCR120.dll로 의존성을 갖는데, 문제는 대상 운영체제가 msvcr120.dll을 가지고 있지 않는 경우가 있다는 점입니다. 따라서, 만약 SampleCredentialProvider 프로젝트 기반으로 바이너리를 만들었으면 반드시 설치 프로그램에서 msvcr120.dll을 함께 배포하든가, 아니면 이에 대한 의존성을 제거해야 합니다.
수동으로 구성해 본 VC++ 프로젝트 설정: ReleaseMinDependency
; https://www.sysnet.pe.kr/2/0/800
참고로, MSDN Magazine에 보면 스마트 카드 인증 모듈을 구현한 예제 코드를 소개하고 있으니 이것도 참조해 보시면 이해가 좀 더 잘 될 것입니다.
Windows Vista용 자격 증명 공급자로 사용자 지정 로그인 환경 만들기
; https://learn.microsoft.com/ko-kr/archive/msdn-magazine/2007/january/custom-login-experiences-credential-providers-in-windows-vista
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]