Configuring security settings in the registry has its advantages: it doesn't require any special work on the part of the component developer, and it allows the administrator great flexibility in configuring the security settings. As you know, component activation security and identity control is always configured using the registry. However, declarative security isn't always the best answer for all security concerns. Certain features of the COM+ security model can be accessed only via a programming interface. For example, it might sometimes be necessary to temporarily increase the security of sensitive data transmitted across the network. In these cases, taking programmatic control of security settings offers a solution. The best answer for most components is a combined approach—using declarative security for most security jobs and programmatic security for more specialized tasks that require a finer degree of control than that available using the registry settings.
The COM+ security infrastructure is initialized on a per-process basis at start-up. The CoInitializeSecurity function sets the default security values for the process. If an application does not call CoInitializeSecurity, COM+ calls the function automatically the first time an interface pointer is marshaled into or out of an apartment (or context) in the process. Attempting to call CoInitializeSecurity after marshaling takes place yields the infamous RPC_E_TOO_LATE error. Thus, programs that want to call CoInitializeSecurity explicitly are advised to do so immediately after calling CoInitializeEx. This rule makes it difficult to call CoInitializeSecurity from languages such as Microsoft Visual Basic and Java, where the virtual machine might call CoInitializeSecurity before running any application code. Note that CoInitializeSecurity is called only once per process, not in each thread that calls CoInitializeEx.
For applications that do not call CoInitializeSecurity, COM+ calls this function with parameters obtained from the security settings in the registry. In this way, legacy components written prior to the advent of the COM+ security model are not left unsecured.9 Although most of the security settings can be configured using the registry, it is often desirable to have programmatic control over the security environment so that a component can override both the machinewide default and the component-specific security settings configured in the registry by an administrator. The declaration of the CoInitializeSecurity function is shown here:
HRESULT __stdcall CoInitializeSecurity( PSECURITY_DESCRIPTOR pSecDesc, // Server LONG cAuthSvc, // Server SOLE_AUTHENTICATION_SERVICE *asAuthSvc, // Server void *pReserved1, // NULL DWORD dwAuthnLevel, // Client/Server DWORD dwImpLevel, // Client SOLE_AUTHENTICATION_LIST *pAuthList, // Client DWORD dwCapabilities, // Client/Server void *pReserved3); // NULL |
The following discussion examines these parameters in detail, with the exception of parameters 4 and 9, which are reserved and must be set to NULL.
The first parameter of CoInitializeSecurity, pSecDesc, is declared as a PSECURITY_DESCRIPTOR, which is simply a pointer to void (void*). This polymorphic argument defines the component's access permissions in one of three ways. Typically, pSecDesc points to a Win32 security descriptor that COM+ uses to check access permissions on new connections. The pSecDesc parameter can also point to a globally unique identifier (GUID) that references an AppID in the registry where declarative security information is stored, or it can point to an implementation of the IAccessControl interface. Figure 18-7 shows the three ways that the polymorphic pSecDesc parameter can be used to perform access control.
Figure 18-7. Three ways to perform access control using the pSecDesc parameter.
CoInitializeSecurity interprets the pSecDesc parameter based on the value of the dwCapabilities parameter. If dwCapabilities contains the EOAC_APPID flag, pSecDesc must point to a GUID of an AppID in the registry. In this case, COM+ obtains all the security settings from the registry and all other parameters of the CoInitializeSecurity function are ignored. If the EOAC_APPID flag is set in the dwCapabilities parameter but the pSecDesc parameter is NULL, CoInitializeSecurity looks for the .exe name of the process in the HKEY_CLASSES_ROOT\AppID section of the registry and uses the AppID stored there. This behavior is identical to the default behavior obtained when you allow COM+ to call CoInitializeSecurity automatically.
If the EOAC_ACCESS_CONTROL flag is set in the dwCapabilities parameter, CoInitializeSecurity interprets pSecDesc as a pointer to a COM+ object that implements the IAccessControl interface. COM+ calls this implementation of IAccessControl to determine access permissions at run time. If neither the EOAC_APPID nor EOAC_ACCESS_CONTROL flag is set in the dwCapabilities parameter, CoInitializeSecurity interprets pSecDesc as a pointer to a Win32 security descriptor structure that is used for access checking. If pSecDesc is NULL, no ACL checking is performed.
The second parameter of CoInitializeSecurity, cAuthSvc, specifies the number of authentication services that are being registered. A value of 0 means that no authentication services are being registered and the process cannot receive secure calls; a value of -1 instructs COM+ to choose which authentication services to register. The third parameter, asAuthSvc, is a pointer to an array of SOLE_AUTHENTICATION_SERVICE structures, each of which identifies one authentication service to be registered. If -1 was passed as the cAuthSvc parameter to instruct COM+ to choose the authentication services, the parameter must be NULL. The definition of the SOLE_AUTHENTICATION_SERVICE structure is shown here:
typedef struct tagSOLE_AUTHENTICATION_SERVICE { DWORD dwAuthnSvc; // RPC_C_AUTHN_xxx DWORD dwAuthzSvc; // RPC_C_AUTHZ_xxx OLECHAR *pPrincipalName; // Should be NULL HRESULT hr; } SOLE_AUTHENTICATION_SERVICE; |
The first field of the SOLE_AUTHENTICATION_SERVICE structure, dwAuthnSvc, specifies which authentication service should be used to authenticate client calls; this field can be set to one of the constants shown in the following table. The authentication service specified by CoInitializeSecurity determines which security providers are used for incoming calls; outgoing calls can use any security provider installed on the machine.
RPC_C_AUTHN_xxx Flag | Description |
---|---|
RPC_C_AUTHN_NONE | No authentication. |
RPC_C_AUTHN_DCE_PRIVATE | Distributed Computing Environment (DCE) private key authentication. |
RPC_C_AUTHN_DCE_PUBLIC | DCE public key authentication. |
RPC_C_AUTHN_GSS_NEGOTIATE | Snego security support provider. |
RPC_C_AUTHN_WINNT | NTLMSSP. |
RPC_C_AUTHN_GSS_KERBEROS | Kerberos authentication. |
RPC_C_AUTHN_DEFAULT | COM+ uses its normal security blanket negotiation algorithm to pick an authentication service. |
The second field of the SOLE_AUTHENTICATION_SERVICE structure, dwAuthzSvc, indicates the authorization service to be used by the server. This field can be set to one of the constants shown in the following table. Note that the RPC_C_AUTHN_WINNT and RPC_C_AUTHN_GSS_KERBEROS authentication packages do not use an authorization service, so this field must be set to RPC_C_AUTHZ_NONE when you use NTLMSSP or Kerberos authentication.
RPC_C_AUTHZ_xxx Flag | Description |
---|---|
RPC_C_AUTHZ_NONE | Server performs no authorization. |
RPC_C_AUTHZ_NAME | Server performs authorization based on the client's principal name. |
RPC_C_AUTHZ_DCE | Server performs authorization checking using the client's DCE privilege attribute certificate information, which is sent to the server with each RPC made using the binding handle. |
RPC_C_AUTHZ_DEFAULT | COM+ uses its normal security blanket negotiation algorithm to pick an authorization service. |
The third field, pPrincipalName, defines the principal name to be used with the authentication service. The NTLMSSP and Kerberos authentication packages ignore this parameter, assuming the current user identifier, so most applications set this value to the constant COLE_DEFAULT_PRINCIPAL. The last field, hr, contains the HRESULT value indicating the status of the call to register this authentication service. If the asAuthSvc parameter is not NULL and CoInitializeSecurity cannot successfully register any of the authentication services specified in the list, the RPC_E_NO_GOOD_SECURITY_PACKAGES error is returned. You should check the SOLE_AUTHENTICATION_SERVICE.hr attribute for error codes specific to each authentication service.
The fifth parameter of CoInitializeSecurity, dwAuthnLevel, specifies the default authentication level. This parameter can be set to one of the RPC_C_AUTHN_LEVEL_xxx flags shown in the table on page 535. Client applications set the dwAuthnLevel parameter to determine the default authentication level for outgoing calls. The dwAuthnLevel setting specified in the component's call to CoInitializeSecurity becomes the minimum level at which client calls will be accepted. Any calls arriving at an authentication level below the minimum watermark specified by the component will fail. When making a connection between a particular client and a particular component, COM+ automatically negotiates the actual authentication level to be the higher of the two settings. In this way, the server does not need to reject client calls because they arrive at an authentication level below the minimum level required by the component. Also note that, by default, IUnknown calls are made at the authentication level specified in the call to CoInitializeSecurity.
If the first parameter passed to CoInitializeSecurity, pSecDesc, is a valid pointer to a Win32 security descriptor, a GUID, or an implementation of the IAccessControl interface, the dwAuthnLevel parameter cannot be set to RPC_C_AUTHN_LEVEL_NONE. On the other hand, if pSecDesc is NULL, no ACL checking is performed and therefore the dwAuthnLevel parameter can be set to RPC_C_AUTHN_LEVEL_NONE, indicating that anonymous access is permitted.
The sixth parameter of CoInitializeSecurity, dwImpLevel, specifies the default impersonation level for proxies. This parameter can be set to one of the RPC_C_IMP_LEVEL_xxx flags shown in the table on page 536. The dwImpLevel setting specified in the client's call to CoInitializeSecurity specifies the default impersonation level that the client grants to the component. Applications should set this value carefully since, by default, all IUnknown calls are made at the impersonation level set by the client's call to CoInitializeSecurity. The dwImpLevel parameter is not used on the server side.
The seventh parameter, pAuthList, must be set to NULL on Windows NT systems. In Windows 2000, the pAuthList parameter points to a SOLE_AUTHENTICATION_LIST structure, which contains a pointer to an array of SOLE_AUTHENTICATION_INFO structures, as shown in Figure 18-8. This list contains the default authentication information to use with each authentication service. Each SOLE_AUTHENTICATION_INFO structure identifies an authentication service (dwAuthnSvc—one of the RPC_C_AUTHN_LEVEL_xxx flags on page 535), authorization service (dwAuthzSvc —one of the RPC_C_IMP_LEVEL_xxx flags on page 536), and a pointer to authentication information (pAuthInfo) whose type is determined by the type of authentication service.
For the NTLMSSP and Kerberos security packages, this value points to the SEC_WINNT_AUTH_IDENTITY_W structure containing the user name and password. For Snego, the pAuthInfo parameter should be NULL or point to a SEC_WINNT_AUTH_IDENTITY_EXW structure, in which case the structure's PackageList member must point to a string containing a comma-delimited list of authentication packages; the PackageListLength member should contain the number of bytes in the PackageList string. If pAuthInfo is NULL, Snego automatically picks a number of authentication services to try from those available on the client machine.
The client specifies these values in the call to CoInitializeSecurity so that when COM+ negotiates the default authentication service for a proxy, it uses the default information specified in the pAuthInfo parameter for that authentication service. If the pAuthInfo parameter for the desired authentication service is NULL, COM+ uses the process identity to represent the client. Applications that don't fill in the SEC_WINNT_AUTH_IDENTITY_W structure can simply set the pAuthInfo pointer to COLE_DEFAULT_AUTHINFO (-1).
The eighth parameter, dwCapabilities, can be used to set additional client-side and server-side capabilities. This value can be composed of a combination of the values from the EOLE_AUTHENTICATION_CAPABILITIES enumeration shown in the table on page 558. CoInitializeSecurity interprets the data pointed to by the pSecDesc parameter based on the flags set in the dwCapabilities parameter. If pSecDesc points to a GUID, the EOAC_APPID flag must be set; if pSecDesc points to an implementation of the IAccessControl interface, the EOAC_ACCESS_CONTROL flag must be set. By default, if neither the EOAC_APPID nor the EOAC_ACCESS_CONTROL flag is set in the dwCapabilities parameter, pSecDesc is assumed to point to a Win32 security descriptor structure. Note that the EOAC_APPID and EOAC_ACCESS_CONTROL flags are mutually exclusive, as are EOAC_STATIC_CLOAKING and EOAC_DYNAMIC_CLOAKING.
Figure 18-8. The pAuthList parameter of CoInitializeSecurity points to the SOLE_AUTHENTICATION_LIST structure that contains an array of SOLE_AUTHENTICATION_INFO structures.
EOLE_AUTHENTICATION_CAPABILITIES Enumeration | Description |
---|---|
EOAC_NONE | No capability flags. |
EOAC_DEFAULT | Tells COM+ to pick the capabilities using its normal security blanket negotiation algorithm. |
EOAC_MUTUAL_AUTH | Not used. Mutual authentication is supported automatically by some authorization services. |
EOAC_STATIC_CLOAKING | Tells COM+ that calls should be made under the identity of the client thread token. The client's identity is determined during the first call on a proxy and whenever the IClientSecurity::SetBlanket method is called. |
EOAC_DYNAMIC_CLOAKING | Tells COM+ that calls should be made under the identity of the client thread token. The client's identity is determined during every call on a proxy. |
EOAC_SECURE_REFS | Causes COM+ to perform additional callbacks to authenticate distributed reference count calls, to ensure that objects are not released maliciously. |
EOAC_ACCESS_CONTROL | CoInitializeSecurity expects pSecDesc to point to an implementation of the IAccessControl interface. COM+ uses this pointer to call the IAccessControl::IsAccessAllowed method when performing security checks. |
EOAC_APPID | CoInitializeSecurity expects pSecDesc to point to a GUID that is installed in the HKEY_CLASSES_ROOT\AppID section of the registry. CoInitializeSecurity uses the security information read from the registry. |
EOAC_DISABLE_AAA | Causes any activation in which a server process would be launched under the caller's identity (activate-as-activator, also known as launching user in the Distributed COM Configuration utility) to fail with E_ACCESSDENIED. This value allows an application that runs under a privileged account (such as LocalSystem) to prevent its identity from being used to launch untrusted components. |
COM+ uses security blanket negotiation to select the appropriate security settings when it first instantiates a proxy. The client and server security blankets are defined by their calls to CoInitializeSecurity on start-up. When instantiating a proxy, COM+ compares the settings of the server's security blanket with those of the client in order to select appropriate values for the proxy's default security blanket. COM+ picks an authentication service that is available to both the client and the server. Then it chooses an authorization service and principal name that work with the selected authentication service. For the authentication level, COM+ uses the formula max(client, server). The impersonation level and other flags used are those given by the client, and the authentication identity used is that given by the client for the selected authentication service. These negotiated values are assigned to the newly created proxy and affect all calls made on the proxy unless they are overridden by a client call to IClientSecurity::SetBlanket.10
The CoQueryAuthenticationServices function retrieves the list of authentication services that were registered when the process called CoInitializeSecurity. This information is primarily of interest to custom marshaling code that needs to determine what principal names an application can use. The declaration of the CoQueryAuthenticationServices function is shown here:
HRESULT __stdcall CoQueryAuthenticationServices( DWORD *pcAuthSvc, SOLE_AUTHENTICATION_SERVICE** asAuthSvc); |
Earlier in this chapter, we explained how to use the DCOMAccessControl object and its implementation of the IAccessControl and IPersistStream interfaces to read and write access permissions from the registry. Although the IAccessControl interface can be useful in some cases, it is not designed for this type of administration and configuration code. It is primarily intended for use by code that needs to perform programmatic access checking.
As you know, the first parameter of the CoInitializeSecurity function can point to a Win32 security descriptor, an AppID, or an implementation of the IAccessControl interface. For CoInitializeSecurity to accept an IAccessControl interface pointer as the first parameter, the dwCapabilities parameter must have the EOAC_ACCESS_CONTROL flag set. When a pointer to an access control object is passed to CoInitializeSecurity, COM+ calls the object's IAccessControl::IsAccessAllowed method to perform access checking at run time. This method determines whether the trustee (user account, group account, or logon session) has access to the object and then simply returns a value of true or false, indicating that permission is granted or denied.
You can use the system implementation of the IAccessControl interface provided by the DCOMAccessControl object for this purpose. First, you instantiate the object by calling CoCreateInstance, and then you request a pointer to the IAccessControl interface. Then you call any of the first five methods of the IAccessControl interface (GrantAccessRights, SetAccessRights, SetOwner,11 RevokeAccessRights, and GetAllAccessRights) to configure the process-wide access permissions. Finally, you call the CoInitializeSecurity function, passing the IAccessControl pointer in the first parameter and the EOAC_ACCESS_CONTROL flag in the last parameter. After you call the CoInitializeSecurity security function, the access control object can be released using the IUnknown::Release method; following the reference counting rules of COM+, CoInitializeSecurity calls the AddRef method internally. COM+ performs all access checking by calling the IAccessControl::IsAccessAllowed method. The implementation of this method provided by CLSID_DCOMAccessControl uses the access permissions configured in the object to determine whether to grant or deny access to individual trustees.
The following code illustrates these steps by configuring an access control object that grants access to the system account and the Everyone group but denies access to a specific user account. Explicit calls to methods of the IAccessControl interface are shown in boldface.
// Create a DCOMAccessControl object, and get its IAccessControl // interface pointer. IAccessControl* pAccessControl = NULL; hr = CoCreateInstance(CLSID_DCOMAccessControl, NULL, CLSCTX_INPROC_SERVER, IID_IAccessControl, (void**)&pAccessControl); // Set up the property list. We use the NULL property because we // are trying to adjust the security of the object itself. ACTRL_ACCESSW access; ACTRL_PROPERTY_ENTRYW propEntry; access.cEntries = 1; access.pPropertyAccessList = &propEntry; ACTRL_ACCESS_ENTRY_LISTW entryList; propEntry.lpProperty = NULL; propEntry.pAccessEntryList = &entryList; propEntry.fListFlags = 0; // Set up the ACL for the default property. ACTRL_ACCESS_ENTRYW entry; entryList.cEntries = 1; entryList.pAccessList = &entry; // Set up the ACE. entry.fAccessFlags = ACTRL_ACCESS_ALLOWED; entry.Access = COM_RIGHTS_EXECUTE; entry.ProvSpecificAccess = 0; entry.Inheritance = NO_INHERITANCE; entry.lpInheritProperty = NULL; // Windows NT requires the system account to have access. entry.Trustee.pMultipleTrustee = NULL; entry.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE; entry.Trustee.TrusteeForm = TRUSTEE_IS_NAME; entry.Trustee.TrusteeType = TRUSTEE_IS_USER; entry.Trustee.ptstrName = L"NT Authority\\System"; // Setting access rights: allow access to // NT Authority\System. hr = pAccessControl->SetAccessRights(&access); // Deny access to an individual user. entry.fAccessFlags = ACTRL_ACCESS_DENIED; entry.Trustee.TrusteeType = TRUSTEE_IS_USER; entry.Trustee.ptstrName = L"Domain\\User"; hr = pAccessControl->GrantAccessRights(&access); // Grant access to everyone. entry.fAccessFlags = ACTRL_ACCESS_ALLOWED; entry.Trustee.TrusteeType = TRUSTEE_IS_GROUP; entry.Trustee.ptstrName = L"*"; hr = pAccessControl->GrantAccessRights(&access); // Call CoInitializeSecurity and pass a pointer to the access // control object. hr = CoInitializeSecurity(pAccessControl, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_IMP_LEVEL_IDENTIFY, NULL, EOAC_ACCESS_CONTROL, NULL); // Release the access control object. CoInitializeSecurity holds // a reference. pAccessControl->Release(); |
Besides using the system-provided DCOMAccessControl object, you can provide CoInitializeSecurity with a custom implementation of the IAccessControl interface. The first five methods of IAccessControl must be implemented so that the caller can configure access permissions. How this access control information is stored internally in the object is entirely implementation-dependent. The last method, IAccessControl::IsAccessAllowed, is called by COM+ when an access check is required to determine whether a client has sufficient rights. Note that implementations of IAccessControl must be completely thread-safe because COM+ can call the access control object on any thread, at any time.
Although time and space do not permit us to provide a more complete implementation of the IAccessControl interface, allow us to humbly present CMyAccessControl. CMyAccessControl is a C++ class that returns E_NOTIMPL for all the methods of the IAccessControl interface except one: IsAccessAllowed. The implementation of the IAccessControl::IsAccessAllowed method is shown in the following code. As you can probably guess by looking at the code, each client access causes the method to display a message box asking the user whether permission should be granted or denied. If the user clicks Yes, IsAccessAllowed returns TRUE in the pfAccessAllowed parameter; otherwise, it returns FALSE. This security system is ironclad.
HRESULT CMyAccessControl::IsAccessAllowed(PTRUSTEEW pTrustee, LPWSTR lpProperty, ACCESS_RIGHTS AccessRights, BOOL* pfAccessAllowed) { if(MessageBoxW(NULL, pTrustee->ptstrName, L"Grant permission?", MB_SERVICE_NOTIFICATION|MB_SETFOREGROUND|MB_YESNO) == IDYES) *pfAccessAllowed = TRUE; else *pfAccessAllowed = FALSE; return S_OK; } |
Before CoInitializeSecurity is called, a static instance of the CMyAccessControl object is created. The address of this simple access control object is then passed as the first parameter of CoInitializeSecurity, as shown in the following code. COM+ later calls the CMyAccessControl::IsAccessAllowed method to determine whether prospective clients should be granted or denied access.
CMyAccessControl ac; hr = CoInitializeSecurity(&ac, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_CONNECT, RPC_C_IMP_LEVEL_IDENTIFY, NULL, EOAC_ACCESS_CONTROL, NULL); |
Our discussion of security settings to this point has centered on access permissions, which can be configured in the registry or by calling CoInitializeSecurity. Recall that launch permissions can be configured only in the registry because the server's SCM needs this information before a component is launched. The client, of course, is running when it issues an activation request using one of the COM+ object instantiation functions, which gives the client the opportunity to specify authentication settings that will be used by the client machine's SCM when making the remote activation request to the server machine's SCM.
Imagine that a component's security settings have been configured in such a way that user Joe is granted launch permission and both user Joe and user Julie are granted access permission. A client process running under the security credentials of user Julie wants to launch and access the component. Unfortunately, Julie has not been granted launch permission, so the client's call to the CoCreateInstanceEx function fails with the error E_ACCESSDENIED. However, if the client process (running under the security credentials of Julie) happens to know the password for user account Joe, the client can use the security credentials of Joe when making the launch request. This launch request will be successful because the administrator has granted launch permission to Joe.
All of the standard instantiation functions, such as CoCreateInstanceEx and its friends CoGetClassObject, CoGetInstanceFromFile, and CoGetInstanceFromIStorage, accept an argument of the type COSERVERINFO. CoGetObject and many other moniker functions use the BIND_OPTS2 structure, which contains a pointer to the COSERVERINFO structure. COSERVERINFO contains two interesting fields: the name of the server machine on which the object should be instantiated12 and a pointer to authentication information provided in the form of a COAUTHINFO structure. The COAUTHINFO structure in turn contains a pointer to a COAUTHIDENTITY structure. The definitions of the COSERVERINFO, COAUTHINFO, and COAUTHIDENTITY structures are shown in IDL notation in Figure 18-9.
The first two parameters of the COAUTHINFO structure, dwAuthnSvc and dwAuthzSvc, specify which authentication and authorization services should be used to authenticate the client. Each field can be set to one of the RPC_C_AUTHN_xxx and RPC_C_AUTHZ_xxx flags shown in the tables on page 554. The third parameter of the COAUTHINFO structure, pwszServerPrincName, points to a string indicating the server principal name to use with the authentication service. If you're using the NTLMSSP authentication service, the principal name is ignored. If you're using the Kerberos authentication service, the principal name specified should be the name of the machine account on which you're launching the component.
The fourth and fifth parameters of the COAUTHINFO structure, dwAuthnLevel and dwImpersonationLevel, specify the authentication and impersonation levels. These fields can be set to one of the progressively higher levels of authentication and impersonation shown in the tables on pages 535 and 536. Typically, the impersonation level must be set to at least RPC_C_IMP_LEVEL_IMPERSONATE because the system needs an impersonation token to create a process on behalf of the client. If you're using Kerberos and specify the impersonation level RPC_C_IMP_LEVEL_DELEGATE, the machine account named by the pwszServerPrincName property must have the Computer Is Trusted For Delegation setting configured in Active Directory. The last parameter of the COAUTHINFO structure, dwCapabilities, defines flags that indicate further capabilities of the proxy. Currently, no capability flags are defined, so this flag must be set to EOAC_NONE.
Figure 18-9. The relationship between the COSERVERINFO, COAUTHINFO, and COAUTHIDENTITY structures.
The sixth parameter of the COAUTHINFO structure, pAuthIdentityData, points to a COAUTHIDENTITY structure that establishes the identity of the client. You can use the COAUTHIDENTITY structure to pass a particular user name and password to COM+ for the purpose of authentication. The User field specifies the user name, the Domain field specifies the domain or workgroup to which the user belongs, and the Password field contains the user's password. The Flags field specifies whether the strings are stored in Unicode (SEC_WINNT_AUTH_IDENTITY_UNICODE) or ASCII (SEC_WINNT_AUTH_IDENTITY_ANSI). Since all COM+ functions work with Unicode strings, the Flags field must be set to SEC_WINNT_AUTH_IDENTITY_UNICODE. The corresponding string length fields indicate the string length minus the terminating null character.
When, as is typically the case, the COAUTHINFO pointer in the COSERVERINFO structure passed to CoCreateInstanceEx and company is set to NULL, COM+ uses the default values for the COAUTHINFO structure based on the default machine security configured in the registry. The following code fragment shows how a client process can use the COAUTHINFO and COAUTHIDENTITY structures to specify its activation credentials when using the CoCreateInstanceEx function to instantiate an object on a remote machine:
COAUTHIDENTITY AuthIdentity; AuthIdentity.User = L"User"; AuthIdentity.UserLength = wcslen(L"User"); AuthIdentity.Domain = L"Domain"; AuthIdentity.DomainLength = wcslen(L"Domain"); AuthIdentity.Password = L"Password"; AuthIdentity.PasswordLength = wcslen(L"Password"); AuthIdentity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE; COAUTHINFO AuthInfo; AuthInfo.dwAuthnSvc = RPC_C_AUTHN_GSS_KERBEROS; AuthInfo.dwAuthzSvc = RPC_C_AUTHZ_NONE; AuthInfo.pwszServerPrincName = L"Domain\\MachineName"; AuthInfo.dwAuthnLevel = RPC_C_AUTHN_LEVEL_CONNECT; AuthInfo.dwImpersonationLevel = RPC_C_IMP_LEVEL_IMPERSONATE; AuthInfo.pAuthIdentityData = &AuthIdentity; AuthInfo.dwCapabilities = EOAC_NONE; COSERVERINFO ServerInfo; ServerInfo.dwReserved1 = 0; ServerInfo.pwszName = L"RemoteServerName"; ServerInfo.pAuthInfo = &AuthInfo; ServerInfo.dwReserved2 = 0; MULTI_QI qi; qi.pIID = &IID_IUnknown; qi.pItf = NULL; qi.hr = 0; HRESULT hr = CoCreateInstanceEx(CLSID_InsideCOM, NULL, CLSCTX_REMOTE_SERVER, &ServerInfo, 1, &qi); |
A server can enforce higher levels of security on a per-method basis using the IServerSecurity interface. A component uses this interface to identify the client and impersonate the client's security credentials. The stub implements the IServerSecurity interface, so there is typically no reason to implement this interface unless you're using custom marshaling. The IDL definition of the IServerSecurity interface is shown below in IDL notation.
interface IServerSecurity : IUnknown { // Called by the server to find out about the client that // has invoked one of its methods HRESULT QueryBlanket( [out] DWORD *pAuthnSvc, [out] DWORD *pAuthzSvc, [out] OLECHAR **pServerPrincName, [out] DWORD *pAuthnLevel, [out] DWORD *pImpLevel, [out] void **pPrivs, [out] DWORD *pCapabilities); // Allows a server to impersonate a client for the duration // of a call HRESULT ImpersonateClient(); // Restores the authentication information on a thread to // the process's identity HRESULT RevertToSelf(); // Indicates whether the server is currently impersonating // the client BOOL IsImpersonating(); } |
To obtain a pointer to the stub's implementation of the IServerSecurity interface, the server process calls the function CoGetCallContext, as shown in the following code. Note that CoGetCallContext can be called only from within a method invoked by a client.
HRESULT MyObject::MyMethod() { IServerSecurity* pServerSecurity; HRESULT hr = CoGetCallContext(IID_IServerSecurity, (void**)&pServerSecurity); // Use IServerSecurity interface pointer. pServerSecurity->Release(); return S_OK; } |
Using the IServerSecurity interface pointer, the server can call any of the four methods of the interface. To make this easier, COM+ provides several helper functions that call CoGetCallContext to obtain the IServerSecurity interface pointer, call one of its methods, and then release the interface pointer. The helper functions are listed in the following table, along with their interface method counterparts.
IServerSecurity Method | Equivalent API Function |
---|---|
QueryBlanket | CoQueryClientBlanket |
ImpersonateClient | CoImpersonateClient |
RevertToSelf | CoRevertToSelf |
IsImpersonating | (none) |
The server uses the IServerSecurity::QueryBlanket method to find out about the client that invoked the current method. This technique can be useful for determining the security credentials of the client and then taking special action that depends on the user identity of the client process. The following code fragment uses the QueryBlanket method to obtain and display information about the client's security blanket:
HRESULT CInsideCOM::Sum(int x, int y, int* retval) { DWORD AuthnSvc; DWORD AuthzSvc; OLECHAR* ServerPrincName; DWORD AuthnLevel; RPC_AUTHZ_HANDLE Privs; DWORD Capabilities; hr = CoQueryClientBlanket(&AuthnSvc, &AuthzSvc, &ServerPrincName, &AuthnLevel, NULL, &Privs, &Capabilities); // Code omitted here that displays the authentication and // authorization packages... // Display the current principal name. wprintf(L"ServerPrincName %s\n", ServerPrincName); // Free the memory allocated by QueryBlanket. CoTaskMemFree(ServerPrincNam); // Code omitted here that displays the // authentication level... // Display the domain\user information. wprintf(L"Privs %s\n", Privs); *retval = x + y; return S_OK; } |
Note that implementations of the IUnknown::QueryInterface method must never perform access control checking. COM+ requires an object that supports a particular interface identifier (IID) to always return success when queried for that IID. Besides, checking access permissions in QueryInterface does not provide any real security. If client A has a legal ISum interface pointer to a component, it can hand that interface pointer to client B without any calls back to the component. Also, COM+ caches interface pointers and does not necessarily call the component's QueryInterface method for each client call.
Using the IServerSecurity interface pointer, the server can call the IServerSecurity::ImpersonateClient method to temporarily assume the security credentials of the client.13 While impersonating the client's security credentials, the server is limited by the impersonation level granted by the client. For example, if the client has limited the impersonation level to RPC_C_IMP_LEVEL_IDENTIFY, the server can impersonate the client only for the purpose of checking permissions using a Win32 API function such as AccessCheck. If the client has granted the server RPC_C_IMP_LEVEL_IMPERSONATE rights, the server can access system objects such as local files using the credentials of the client, but not any network resources. If the server is running on the same machine as the client, the server can access network resources as the client. In either case, only one machine hop is permitted, after which the server can access only local resources using the client's credentials.
In Windows 2000, which supports the impersonation level RPC_C_IMP_LEVEL_DELEGATE, a server with this right can impersonate the client's security credentials when making cross-machine calls. Any number of machine hops is supported by delegate-level impersonation. In order for delegate-level impersonation to work, however, several requirements must be met. The client account that will be delegated must not be marked Account Is Sensitive And Can Not Be Delegated in Active Directory, and the account under which the server executes must be marked Account Is Trusted For Delegation. Also, because Kerberos support is required, the client, server, and all downstream servers must be running Windows 2000 in a Windows 2000 domain.
The primary reason to use impersonation is so that access checks are performed against the client's identity. Imagine that a client calls an object and asks it to read some data from a file. If the client has access rights to the file but the server does not, the server's attempt to read from the file will fail unless impersonation is activated. One can also imagine the reverse situation, in which the client is forbidden to access the file but the server has the necessary permissions; in this case, the server's attempt to read from the file will succeed and the client will receive unauthorized data unless impersonation is activated. As you can see, the access rights of the server process might be diminished or expanded depending on the rights of the client that is being impersonated.
Normally, when a method executes, the primary access token of the server's process is used to determine what access rights are available when the thread interacts with a securable object. However, when the thread on which the method is executing is impersonating the client, it is granted a special impersonation token representing the client's security context in addition to the primary access token of the process. While the server impersonates the client, the thread's impersonation token is used for all access checking. When the server finishes impersonating the client, it calls the IServerSecurity::RevertToSelf method to revert to the primary access token of the process, thereby restoring its own security credentials. Regardless of the impersonation level permitted by the client, the impersonation information lasts only until the end of the method. If, after impersonating the client, the server neglects to call the RevertToSelf method prior to the completion of the method, COM+ restores the server's security credentials automatically.
Imagine a scenario in which client A calls server B, which in turn calls server C. When client A calls server B, the access token for server B is used for access checking. If client A sets impersonate-level or delegate-level impersonation and server B impersonates client A, client A's ACL is used for access checking. But what happens if server B calls server C while impersonating client A? Assuming that client A has set delegate-level impersonation, this will work, but the access token for process B will be used when making the call to server C. This means that server C will see the identity of its caller as server B—not client A.
This might seem odd since the idea of delegate-level impersonation is to enable an object at the end of a call chain to impersonate its caller (the client) at the very start of the call chain. When the server impersonates the client, the client's ACL is used for access checking. But if a server makes calls to a downstream server while impersonating the client, its process token represents the identity of the caller to the downstream server. This is done for reasons of compatibility with the behavior of COM prior to Windows 2000. In Windows NT 4.0 Kerberos and later, delegate-level impersonation was not available, so this was really not an issue. In order to not break existing COM applications in Windows 2000, COM+ does not change the existing semantics of impersonation.
To achieve true delegation of security principals, COM+ has introduced the idea of cloaking. Fundamentally, cloaking does what delegate-level impersonation is supposed to do: it controls which identity is set on the proxy when you make a call, and it controls which identity the server sees when it impersonates. When a server process impersonates a client and then makes calls to downstream servers, the impersonating server's thread token is used. Let's return to the scenario in which client A calls server B, which in turn calls server C. If client A sets delegate-level impersonation before calling server B, and server B sets the cloaking flag and impersonates client A before calling server C, server C will think that its caller is client A and will be able to perform any actions permitted to client A. Thus, we achieve the true delegation of security principals. If server B neglects to set the cloaking flag before calling server C, server C will see server B as its client, even if client A has set delegate-level impersonation.
Cloaking can be set in two ways: the process can set a cloaking flag in the call to CoInitializeSecurity, or the cloaking attribute can be set on an individual proxy by calling CoSetProxyBlanket. The two cloaking flags supported in Windows 2000 are EOAC_STATIC_CLOAKING and EOAC_DYNAMIC_CLOAKING. Dynamic cloaking is the option that is typically used, and this is the way most people expect delegate-level impersonation to work. When server B sets the dynamic cloaking flag, impersonates client A, and then calls server C, server C sees client A as the identity of its caller. In other words, the current impersonation token, if available, is always used in the case of dynamic cloaking. While dynamic cloaking can have performance overhead, it provides the flexibility that is usually required by circumstances that necessitate the use of impersonation in the first place.
Static cloaking determines the identity of the caller during the first call on a proxy or whenever CoSetProxyBlanket is called, and that identity is used on all subsequent method calls. Imagine that server B sets static cloaking and makes a call to server C, thereby setting server C's proxy in server B's address space to the identity of server B. Later, client A calls server B, which impersonates client A and makes a call to server C. Server C sees as its caller the identity of server B, since that identity was fixed during the previous call. Now, if server C sets the cloaking attribute and calls server D, server D sees server B as its caller.
IClientSecurity, the twin of IServerSecurity, is an interface that client processes use to adjust security settings on the proxy of a remote object. As with the IServerSecurity interface, there is typically no need to implement the IClientSecurity interface, since all proxies generated by the MIDL compiler automatically support it, as does the Automation marshaler employed by components using type library marshaling. The IClientSecurity interface is shown here in IDL notation:
interface IClientSecurity : IUnknown { // Retrieves the current authentication information HRESULT QueryBlanket( [in] IUnknown *pProxy, [out] DWORD *pAuthnSvc, [out] DWORD *pAuthzSvc, [out] OLECHAR **pServerPrincName, [out] DWORD *pAuthnLevel, [out] DWORD *pImpLevel, [out] void **pAuthInfo, [out] DWORD *pCapabilites); // Sets the authentication information that will be used // to make calls on the specified proxy HRESULT SetBlanket( [in] IUnknown *pProxy, [in] DWORD AuthnSvc, [in] DWORD AuthzSvc, [in] OLECHAR *pServerPrincName, [in] DWORD AuthnLevel, [in] DWORD ImpLevel, [in] void *pAuthInfo, [in] DWORD Capabilities); // Makes a copy of the specified proxy HRESULT CopyProxy( [in] IUnknown *pProxy, [out] IUnknown **ppCopy); } |
You can use the methods of the IClientSecurity interface to examine (IClientSecurity::QueryBlanket) or modify (IClientSecurity::SetBlanket) the current security settings for a particular connection to an out-of-process object. One typical use of the IClientSecurity interface is for the client process to escalate the authentication level used by a particular interface. Most interfaces of an object are rather pedestrian, but one interface could require the client to submit sensitive data such as the user's credit card information. In this case, it might make sense to establish a default authentication level of RPC_C_AUTHN_LEVEL_CONNECT but use the IClientSecurity::SetBlanket method to raise that setting to RPC_C_AUTHN_LEVEL_PKT_PRIVACY on the interface dealing with the credit card information. The IClientSecurity::SetBlanket method can never set the authentication level lower than was specified by the component in its call to CoInitializeSecurity.
The IClientSecurity::SetBlanket method can also be used to set the static or dynamic cloaking flags discussed previously. The code fragment below shows a proxy being set with the dynamic cloaking attribute. Note that for all attributes of the proxy that are not being adjusted in this call, we simply pass the default flags. Although security settings assigned to the proxy by the SetBlanket method are not subject to the COM+ security blanket negotiation algorithm described previously, this algorithm is used to decide the value for all default parameters such as RPC_C_AUTHN_LEVEL_DEFAULT.
hr = pClientSecurity->SetBlanket(pInterface, RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHZ_DEFAULT, COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_DELEGATE, COLE_DEFAULT_AUTHINFO, EOAC_DYNAMIC_CLOAKING); |
The IClientSecurity::CopyProxy method makes a private copy of the proxy. If you call IClientSecurity::SetBlanket on an interface pointer, the security settings will also affect all other code in the client process using that interface pointer. To limit the scope of the security settings, the client can make a copy of the proxy before adjusting the security blanket. In this way, the client receives a pointer to another proxy through which the object can be invoked. Adjusting the security settings for this proxy does not affect any other code running in the client process.
Obtaining a pointer to the proxy-supplied implementation of the IClientSecurity interface is as simple as calling the IUnknown::QueryInterface method, as shown in the following code. This interface is always available for out-of-process objects that employ standard or type library marshaling. If the IUnknown::QueryInterface call for IClientSecurity fails, the object is either inprocess or custom marshaled. Custom marshalers can implement the IClientSecurity interface for consistency if necessary.
IClientSecurity* pClientSecurity; HRESULT hr = pUnknown->QueryInterface(IID_IClientSecurity, (void**)&pClientSecurity); if(FAILED(hr)) cout << "QueryInterface for IClientSecurity failed." << endl; // Use the IClientSecurity interface pointer. pClientSecurity->Release(); |
Note that the IClientSecurity::SetBlanket method returns an error if you set the EOAC_SECURE_REFS, EOAC_ACCESS_CONTROL, or EOAC_APPID flags in the dwCapabilities parameter. These settings are valid for use only when you call CoInitializeSecurity. As with the IServerSecurity interface, COM+ provides several helper functions that assist in calling the methods of the IClientSecurity interface. These functions are shown in the following table, along with their equivalent methods.
IClientSecurity Method | Equivalent API Function |
---|---|
QueryBlanket | CoQueryProxyBlanket |
SetBlanket | CoSetProxyBlanket |
CopyProxy | CoCopyProxy |