The COM+ Security Model
Gets You out of the Security Programming Business
|COM+ security insulates developers from the Windows and RPC security mechanisms so that as these technologies evolve, components will be able to derive benefits from the new security services automatically. This security model is built around the Security Support Provider Interface.|
This article assumes you're familiar with C++, COM, Security|
Guy Eddon (firstname.lastname@example.org) is a senior developer at CodeMarine, a leading development and training institute specializing in COM+. This article is drawn from his book,
Inside COM+ Base Services, which will be released by Microsoft Press this fall, and his upcoming book, Inside COM+ Component Services, also to be published by Microsoft Press.|
Security is one area of COM+ that can bite you where you least expect it. COM+ offers component developers various declarative and programmatic security options. The distributed security model supported by Microsoft® Windows® 2000 focuses on two primary areas: authentication and access control. Authentication is usually encountered when a user attempts to log on to a network or call a process on another machine. In such cases, the system wants to be sure that the user is who she claims to be. Access control in Windows enables applications to selectively grant or deny certain users access to specific services. While any executable code presents a potential security risk, Windows lessens this risk by limiting the access permissions of a given process, depending on the user account it is running under.
The COM+ Security Model
When you are learning about the COM+ security model, it is helpful to understand what resources must be secured. Here are the primary goals of the COM+ security model:
Activation control specifies who is permitted to launch components. Once a component has been launched, access control determines who can touch the component's objects. In some cases, it might be acceptable for certain users to have access to certain areas of functionality while other services of a component are restricted. For example, perhaps all users connecting over the Web are permitted to access certain areas of the component's functionality, but other services are reserved for authorized users only.
- Activation control
- Access control
- Authentication control
- Identity control
Authentication control is used to ensure that a network transmission is authentic and to protect the data from unauthorized viewers. Different authentication levels can be set so that all the data is encrypted before being transmitted.
Identity control specifies the security credentials under which the component will execute. These credentials can be a specific user account configured for this purpose or the security credentials of the client application.
Security information for COM+ components is configured in two ways: declarative security and programmatic security. Declarative security settings are configured in the COM+ catalog external to the component. A system administrator typically configures declarative security. Programmatic security is incorporated into a component by the developer. Activation, access, authentication, and identity security settings for a component can be configured in the declarative manner via the COM+ catalog, using the Component Services administrative tool or the Distributed COM Configuration utility (dcomcnfg.exe). The Distributed COM Configuration utility is now used to manage unconfigured components. Access and authentication security can also be controlled programmatically by using several interfaces and helper functions provided by COM+. Activation and identity security cannot be controlled programmatically because these settings must be specified before a component is launched.
The COM+ Security Packages
Perhaps you are wondering why COM+ needs a special security model. Windows is a secure operating system platform, so why can't COM+ components simply take advantage of that security model? The answer is that they canbut they shouldn't. Neither Windows 95 nor Windows 98 supports many of these security functions. COM+ requires a security model that can be supported on all platforms on which COM+ services are available.
COM+ defines a higher-level security model by abstracting the underlying security mechanisms of both Windows and Remote Procedure Calls (RPCs)which explains why many of the security flags used by COM+ have been borrowed from the RPC security infrastructure. Aside from platform independence, the COM+ security model insulates component developers from the Windows and RPC security mechanisms so that as these technologies evolve, components will be able to derive benefits from the new security services automatically. This flexible security model is built around the Security Support Provider Interface (SSPI), a standard API designed to support the security needs of apps running in a distributed environment.
The design of SSPI can be compared with the architecture of Open Database Connectivity (OA§"). ODBC was designed to insulate database application developers from a specific back-end server by offering numerous ODBC drivers that implement the standard ODBC API, but communicate with a specific end server using its proprietary interface. To support the varying security requirements of different applications, different vendors can implement the SSPI API in a DLL called a Security Service Provider (SSP). An application written to work with one security provider would require few or no modifications to work with another security provider. All security providers must implement SSPI, but internally they are free to use different mechanisms to enforce security.
The primary SSPs available to COM+ applications are the Windows NT® LAN Manager Security Support Provider (NTLMSSP), Kerberos, and Simple Negotiation Mechanism (Snego). Understanding the general features of each authentication package can help you make a choice that is right for the security needs of your application. Once a security package has been chosen, however, COM+ insulates you from the internals of the various authentication protocols, so here I'll focus primarily on the higher-level security model defined by COM+. For an in-depth
discussion of the Kerberos security protocol, see David Chappell's article, "Exploring Kerberos, the Protocol for Distributed Security in Windows 2000" (MSJ, August 1999).
Prior to Windows 2000, only one SSPI-compliant
security provider was available: NTLMSSP. This security package is based on the NTLM authentication protocol, which employs a challenge-response protocol to authenticate clients. In the challenge-response model, the server issues a challenge to the client that consists of some data. The client uses an encoded version of the user's password to encrypt that data. This encrypted challenge is then sent back to the server, where it is decrypted and compared with the original data sent to the client. If the data is the same, the user is authenticated. The user's password is never sent across the network by the NTLM challenge-response protocol. For this reason the server cannot use the client's password during impersonation to access network resources for which the client may have the necessary permissions.
Windows 2000 offers both NTLMSSP and a new security support provider that implements the Kerberos network authentication service version 5. Kerberos is a more sophisticated authentication protocol than NTLM, and supports features such as mutual authentication and the delegation of the security information from one machine to another. This can allow a server process to cloak itself with the client's identity when making remote calls to another service. In Windows 2000, Kerberos will be the default security protocol of the operating system.
While NTLMSSP and Kerberos are real authentication packages that support SSPI, the Snego package is used to negotiate between real SSPs. This pseudo-SSP does not provide any actual authentication features of its ownits only use is to help applications select a real SSP. Currently, applications can use Snego to negotiate between the NTLMSSP and Kerberos protocols. For Snego to work properly, both the client and server must use the Snego pseudo-SSP. Snego will select an actual authentication protocol by comparing the authentication packages available on the client machine with those available on the server machine.
As it does for many other aspects of COM+, the COM+ catalog contains a great deal of information relating to
the COM+ security model. Many of the COM+ security settings can be controlled by setting various options in the catalog. Security settings can also be configured programmatically, but there are decided advantages to using the catalog.
By manipulating the catalog, a knowledgeable system administrator can flexibly configure and customize the security environment. But the best thing about configuring security settings in the catalog is that COM+ will enforce all of these settings automatically. This technique has the effect of reducing the amount of security-related code you need to writea definite plus. For example, you could specify that a user named Mary or users belonging to the Accountants group are not permitted to launch or access a particular component. You need never worry that Mary or Accountants will be able to use the component.
The easiest place to begin exploring the COM+ security model is with the Component Services administration tool. The declarative security information can be neatly divided into default security and component security. Default security specifies the default launch and access settings for all components running on the local machine that do not override these settings. Component security settings can be used to provide security for a specific component, thereby overriding the default security settings. Programmatic security (discussed later) can be used to override both default and component security settings in the registry. Let's begin by examining the default security settings.
Launching the Component Services administration tool and selecting the Default Properties tab displays the options shown in Figure 1. These options enable the administrator to set the default authentication and impersonation options on a machine-wide basis. The "Enable Distributed COM on this computer" option is the master switch of DCOM. If this box is not checked, all remote calls to and from the machine are rejected. When the system is first installed, this option is enabled. The "Enable COM Internet Services on this computer" option determines whether COM+ Internet Services (CIS) is available; by default it is disabled.
Figure 1 Component Services Default Properties
The Default Authentication Level setting specifies the base authentication level that will be used on this system, assuming that a component does not override the value programmatically or through other declarative settings. When the system is first installed, this setting is configured for connect-level authentication. The possible authentication levels and their attributes are shown in Figure 2.
The Default Impersonation Level setting specifies the base impersonation level that clients running on this system will grant to their servers, again assuming that a component does not override this value. Impersonation levels are used to protect the client from rogue components. From the client's point of view, anonymous-level impersonation is the most secure because the component cannot obtain any information about the client. (Note that anonymous-level impersonation is not supported for remote calls.) With each successive impersonation level, a component is granted further liberties with the client's security credentials. When the system is first installed, this setting is configured for identify-level impersonation. The possible impersonation levels and their attributes are shown in Figure 3. Note that Windows NT supports only the RPC_C_
IMP_LEVEL_IDENTIFY and RPC_C_IMP_LEVEL_IMPERSONATE impersonation levels; Windows 2000 adds support for the RPC_C_IMP_LEVEL_DELEGATE impersonation level when using the Kerberos security protocol.
The "Provide additional security for reference tracking" option indicates whether calls to the IUnknown::AddRef and IUnknown::Release methods are secured. When the system is first installed, this option is turned off. Checking this option causes COM+ to perform additional callbacks
to authenticate distributed reference count calls, making
sure that objects are not released maliciously. This option will improve the security of the system, but will slow execution speed.
Figure 4 The Default Security Tab
The Component Services administration tool's Default Security tab, shown in Figure 4, enables the administrator to configure default access and launch permissions on a machine-wide basis. These settings are used for components that do not provide their own settings. Clicking the Edit Default button presents a list of users and user groups that can be explicitly allowed or denied permission. When the system is first installed, only administrators, the system account, and the interactive user have launch permission. It is generally not recommended to change these values; instead of changing the machine-wide default settings that affect all components, it is preferable to adjust the security settings using
the role-based mechanism described later. The system account is a highly privileged local account used by system proces-
ses. It must always have
access permissions because the Service Control Manager (SCM; rpcss.dll) runs in this account.
Configuring Component Identity
The Identity tab of the properties for a COM+
application, shown in Figure 5, enables the administrator to determine in which user account the
app will execute. The Identity tab provides two possible settings for defining the user account: Interactive user and This user.
Figure 5 The Identity Tab
When it is configured to run as the interactive user, the component will be run under the identity of the user currently logged on, which means that the component has access to the interactive desktop visible to the user. The problems with configuring a component for execution as the interactive user are threefold. First, a user must be logged on in order for the component to execute. Second, you never know who will log on, and thus the component might have many rights (if the administrator is logged on) or few rights (if a guest is logged on). Third, if the user logs off while the component is running, the component dies. This option is most useful for a system such as a distributed whiteboard-style drawing application that needs to interact with the user, as well as for debugging purposes. It is not recommended for other types of server or middle-tier components.
The second identity option is to configure the component for execution under a specific user account. When an attempt is made to launch the component, COM+ will automatically initiate a system logon using the specified user account by calling the Win32® API function LogonUser, followed by a call to the CreateProcessAsUser function. As part of the logon procedure, a new, noninteractive window station will be created for use by the component. This setting is often the best option for components that will serve many client programs simultaneously, as all instances of the component will be loaded into one window station. User accounts that are used by COM+ for logon purposes must be assigned the special privilege Log On As A Batch Job or COM+ will not be able to successfully log in using the account. The Component Services administration tool automatically grants this privilege to any user account specified on the Identity tab.
The key to understanding COM+ security is to grasp the simple yet powerful concept of roles. Roles are central to the flexible, declarative security model employed by most COM+ objects. A role is a symbolic name that abstracts and identifies a logical group of userssimilar to the idea of a user group in Windows 2000. The difference is that a user group is global in a Windows 2000 domain, while roles are configured for specific COM+ applications. When a COM+ object is deployed, the administrator can create certain roles and then bind those roles to specific users and user groups. For example, a banking application might define roles and permissions for tellers and for managers. During deployment, the administrator can assign users Fred and Jane to the role of tellers and assign executive management to the role of managers. Fred and Jane can access certain coclasses in the banking package, while executive managers can access all coclasses.
It's even possible to configure role-based security on a per-method or per-interface rather than a per-coclass or per-application basis. The administrator can completely configure declarative security; it does not require any work by the component developer. This means that when designing an interface that will be implemented in a configured component, you should try to factor security decisions at the method level. For example, perhaps tellers can authorize withdrawals and transfers of up to $5,000; only a manager can execute a withdrawal or transfer of amounts above $5,000. To make this scenario work when setting security options declaratively, you'll need to define two separate withdraw methods: one for tellers and one for managers. On the other hand, you might decide that it's better to design a single withdrawal method and make the withdrawal limit decision in the code. Declarative security as configured by the administrator does not offer the fine degree of control required in this example.
Configuring security settings declaratively 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 are always configured declaratively. However, declarative security isn't always the best answer. Certain features of the COM+ security model can be accessed only via a programming interface. For example, it might sometimes become 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 requiring a finer degree of control.
Let's return to the example of bank tellers and a manager. Since declarative security does not allow you to configure roles within a single method, you can take control of this programmatically. To enable this type of programmatic authorization, the context object implemented by COM+ offers the IObjectContext::IsSecurityEnabled and IObjectContext::IsCallerInRole methods. The IsCallerInRole method interrogates the caller to determine if that user was assigned to a specific role. Here's how this could be enforced inside the COM+ application:
Public Function Withdraw(HowMuch as Double) As Boolean
Dim oc As ObjectContext
Set oc = GetObjectContext()
If HowMuch > 5000 Then
If oc.IsCallerInRole("Managers") = True Then
' Proceed with operation and return success
Withdraw = True
' Deny access and return failure
Withdraw = False
' Withdrawal of less than $5000
' Proceed with operation and return success
Withdraw = True
In cases which role-based security is disabled, the IsCallerInRole method always returns true, which can lead the component to grant permissions to ineligible users. To overcome this problem, the IObjectContext::IsSecurityEnabled method can be called to determine whether role-based security is currently being enforced by COM+. Thus, the method shown earlier might be rewritten to call the IsSecurityEnabled function as follows:
Public Function Withdraw(HowMuch as Double) As Boolean
Dim oc As ObjectContext
Set oc = GetObjectContext()
If oc.IsSecurityEnabled = False Then
' Security is not currently available
Withdraw = False
If HowMuch > 5000 Then
If oc.IsCallerInRole("Managers") = True Then
' Proceed with operation and return success
Withdraw = True
' Deny access and return failure
Withdraw = False
' Withdrawal of less than $5000
' Proceed with operation and return success
Withdraw = True
For components requiring greater control over the security model than offered by declarative security and the IsSecurityEnabled and IsCallerInRole methods of the IObjectContext interface, the context object also implements the ISecurityProperty interface. A COM+ object can use the methods of the ISecurityProperty interface to obtain precise information about the identity of its caller stored in the context object. The ISecurityProperty interface is defined in IDL notation like so:
interface ISecurityProperty : IUnknown
HRESULT GetDirectCallerSID(PSID* pSID);
HRESULT GetDirectCreatorSID(PSID* pSID);
HRESULT GetOriginalCallerSID(PSID* pSID);
HRESULT GetOriginalCreatorSID(PSID* pSID);
HRESULT ReleaseSID(PSID pSID);
Notice that all the methods of the ISecurityProperty interface work with a security identifier (SID), a unique value that identifies a specific user or user group. Because they specifically identify a unique user, SIDs do not have the flexibility of the role-based security promoted by COM+. Once an SID is obtained from a method of the ISecurityProperty interface, the COM+ object can use this value when calling the security functions of the Win32 API. In this way, the richness and complexity of the Windows security model is available to configured components. Components written in Visual Basic® do not have much use for SIDs, and are instead provided with the user name identified by the SID.
The CoInitializeSecurity Function
The COM+ security infrastructure is initialized on a per-process basis at startup. The CoInitializeSecurity function sets the default security values for the process. If an application does not call CoInitializeSecurity, COM+ will call the function automatically the first time an interface pointer is marshaled into or out of an apartment (or context) in the process. In fact, configured COM+ components are not permitted to call CoInitializeSecurity since the COM+ surrogate calls this function on startup. Although configured components do not call CoInitializeSecurity themselves, it is worthwhile to understand how this fundamental function works, since it goes to the very heart of the COM+ security model.
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 first parameter of CoInitializeSecurity, pSecDesc, is declared as a PSECURITY_ DESCRIPTOR, which is simply a pointer to 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+ will use 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 inter-face. Figure 6 shows the three different ways in which the polymorphic pSecDesc parameter can be used to perform access control.
Figure 6 Three Access Control Options Using pSecDesc
CoInitializeSecurity interprets the pSecDesc parameter based on the value of the dwCapabilities parameter. If the dwCapabilities parameter contains the EOAC_APPID flag, then 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 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, then CoInitializeSecurity interprets pSecDesc as a pointer to a COM+ object that implements the IAccessControl interface. COM+ will call this implementation of IAccessControl to determine access permissions at runtime. If neither the EOAC_APPID nor EOAC_ACCESS_CONTROL flag is set in the dwCapabilities parameter, then CoInitializeSecurity interprets pSecDesc as a pointer to a Win32 security descriptor structure that will be used for access checking. If pSecDesc is null, then no ACL checking will be performed.
The second parameter of CoInitializeSecurity, cAuthSvc, specifies the number of authentication services being registered. Zero means that no authentication services are being registered and the process will not be able to 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), then the asAuthSvc parameter must be null. The definition of the SOLE_AUTHENTICATION_SERVICE structure is:
typedef struct tagSOLE_AUTHENTICATION_SERVICE
DWORD dwAuthnSvc; // RPC_C_AUTHN_xxx
DWORD dwAuthzSvc; // RPC_C_AUTHZ_xxx
OLECHAR *pPrincipalName; // Should be NULL
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 Figure 7. The authentication service specified by CoInitializeSecurity determines which security providers are used for incoming calls; outgoing calls may use any security provider installed on the machine.
The second field of the SOLE_AUTHENTICATION_SERVICE structure, dwAuthzSvc, indicates the authorization service to be used by the server. The RPC_C_AUTHN_WINNT and RPC_C_AUTHN_GSS_KERBEROS authentication packages do not utilize an authorization service, and therefore this field must be set to RPC_C_AUTHZ_NONE when you are using NTLM or Kerberos authentication.
The third field of the structure, pPrincipalName, defines the principal name to be used with the authentication service. 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 is unable to successfully register any of the authentication services specified in the list, then 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.
Authentication and Impersonation Levels
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 Figure 2. Client applications set the dwAuthnLevel parameter to determine the default authentication level used 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 simply because they arrive
at an authentication level below the minimum level required by the component. By default, IUnknown calls are made at the authentication level specified in the call to CoInitializeSecurity.
The sixth parameter of CoInitializeSecurity, dwImpLevel, specifies the default impersonation level for proxies. This parameter can also be set to one of the RPC_C_IMP_
LEVEL_xxx flags shown in Figure 3. 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.
Figure 8 Authentication Structures
The seventh parameter, pAuthList, must be set to null on Windows NT 4.0-based 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 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), authorization service (dwAuthzSvc, another one of the RPC_C_IMP_LEVEL_xxx flags), and a pointer to authentication information (pAuthInfo) whose type is determined by the type of authentication service.
For the NTLM 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 either 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, and the PackageListLength member should contain the number of bytes in the PackageList string. If pAuthInfo is null, Snego will automatically pick a number of authentication services to try from those available on the client machine.
The client specifies these values in the call to CoInitializeSecurity. 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+ will use 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 in Figure 9.
Security Blanket Negotiation
Security blanket negotiation is the process that COM+ uses to select the appropriate security settings when first instantiating a proxy. The client and server security blankets are defined by their calls to CoInitializeSecurity on startup. Then when instantiating a proxy, COM+ compares the settings of the server's security blanket with those of the client to select values for the proxy's default security blanket. COM+ picks an authentication service that is available to both the client and the server. COM+ 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 overridden by a client call to IClientSecurity::SetBlanket.
The COAUTHINFO Structure
The security settings discussed to this point have 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 COM+ catalog because the server's SCM will need this information before a component is launched. For unconfigured components, launch permissions are read from the registry. 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. Now 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, and thus the client's call to the CoCreateInstanceEx function would fail 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 Joe's security credentials 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 functionssuch as CoCreateInstanceEx and its friends CoGetClassObject, CoGetInstanceFromFile, and CoGetInstanceFromIStorageaccept 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 instantiated 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 10.
Figure 10 COSERVERINFO Structure Definitions
The first two parameters of the COAUTHINFO structure, dwAuthnSvc and dwAuthzSvc, specify which authentication and authorization services should be used to authenticate the client. dwAuthnSvc can be set to one of the RPC_C_AUTHN_xxx flags shown in Figure 7.
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 are using the NTLMSSP authentication service, the principal name is ignored. If you are using the Kerberos authentication service, then the principal name specified should be the name of the machine account on which you are 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 Figure 2 and Figure 3. 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 are using Kerberos authentication and specify the impersonation level RPC_C_IMP_LEVEL_DELEGATE, then the machine account named by the pwszServerPrincName property must have the "Computer is trusted for delegation" setting configured in the Active Directory™ directory service.
The last parameter of the COAUTHINFO structure, dwCapabilities, defines flags that indicate further capabilities of the proxy. Currently, no capability flags are defined and so this flag must be set to EOAC_NONE.
The sixth parameter of the COAUTHINFO structure, pAuthIdentityData, points to a COAUTHIDENTITY structure that establishes the identity of the client. The COAUTHIDENTITY structure allows you 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 typical, 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. Figure 11 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.
The IServerSecurity Interface
A server can enforce higher levels of security on a per-method basis using the IServerSecurity interface. IServerSecurity is used by a component 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 are using custom marshaling. The IDL definition of the IServerSecurity interface is shown in Figure 12.
To obtain a pointer to the stub's implementation of the IServerSecurity interface, the server process calls the function CoGetCallContext. Note that CoGetCallContext can be called only from within a method invoked by a client.
HRESULT hr = CoGetCallContext(IID_IServerSecurity,
// Use IServerSecurity interface pointer.
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 Figure 13, along with their interface method counterparts.
The IServerSecurity::QueryBlanket method is used by the server to find out about the client that has 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. Figure 14 uses the QueryBlanket method to obtain and display information about the client's security blanket.
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. Additionally, COM+ caches interface pointers and does not necessarily call the component's QueryInterface method for each client call.
Impersonating the Client
Using the IServerSecurity interface pointer, the server can call the IServerSecurity::ImpersonateClient method to temporarily assume the security credentials of the client. While impersonating using 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, then the server could access network resources as the client; in either case only one machine hop is permitted, after which only local resources can be accessed using the client's credentials.
In Windows 2000, which supports the impersonation level RPC_C_IMP_LEVEL_DELEGATE, a server with
this right could 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, several requirements must be met. The client account that will be delegated must not be marked "Account is sensitive and cannot be delegated" in the Active Directory service; 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. You can also imagine the reverse situation where 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 may be either diminished or expanded depending on the rights of the client 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. When impersonating, the thread on which the method is executing is granted a special impersonation token representing the client's security context in addition to the primary access token of the process. During impersonation, the thread's impersonation token is used for all access checking.
When the server has finished 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 will last only until the end of the method. If, after impersonating the client, the server has neglected to call the RevertToSelf method prior to the completion of the method, COM+ will restore the server's security credentials automatically.
Imagine a scenario where 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 or delegate-level impersonation, and server B impersonates client A, then client A's ACL will be used for access checking. But what happens when 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 Bnot client A.
This may seem odd since the idea of delegate-level impersonation is to enable an object at the end of a call chain to identify as 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 immediate server's process token is used to represent 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 hence delegate-level impersonation, was not available, so this was really not an issue. So as not to break existing COM applications in Windows 2000, COM+ does not change the existing semantics of impersonation.
To achieve true delegation of security principals, COM+ introduces the idea of cloaking. Cloaking does what delegate-level impersonation is supposed to: it controls what identity is set on the proxy when you make a call and what identity the server sees when it impersonates. To do this, cloaking uses the thread token of a server process when impersonating calls to downstream servers.
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, then server C will think its caller is client A and will be able to perform any actions permitted to client A. Thus, you achieved the true delegation of security principals. If server B neglects to set the cloaking flag before calling server C, then 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 more common, so it's 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, if available, the current impersonation token 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. That identity is used on all subsequent method calls. Imagine that server B sets static cloaking and makes a call to server C, thereby fixing 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 the identity of server B as its caller since that identity was fixed during the previous call. Now, if server C sets the cloaking attribute and calls server D, server D will see server B as its caller.
The IClientSecurity Interface
IClientSecurity, the twin of IServerSecurity, is an interface used by client processes 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 in Figure 15 in IDL notation.
The methods of the IClientSecurity interface can be used to examine (via IClientSecurity::QueryBlanket) or modify (via 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 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 following code fragment shows a proxy being set with the dynamic cloaking attribute. Attributes of the proxy that are not being adjusted in this call are simply passed 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_
hr = pClientSecurity->SetBlanket(pInterface,
The IClientSecurity::CopyProxy method is used to make a private copy of the proxy. If you call IClientSecurity::SetBlanket on an interface pointer, the security settings will affect all other code in the client process using that interface pointer as well. 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 here:
HRESULT hr = pUnknown->QueryInterface(
cout << "QueryInterface for IClientSecurity
failed." << endl;
// Use the IClientSecurity interface pointer.
This interface will always be available for cross-apartment calls using standard or type library marshaling. If the IUnknown::QueryInterface call for IClientSecurity fails, the object is either in-process or custom marshaled. Custom marshalers can implement the IClientSecurity interface for consistency if necessary.
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 are calling 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 Figure 16, along with their equivalent methods.
The focus on security in COM+ is simplicity. As with other areas of COM+, the goal of COM+ security is to let you avoid having to develop this code yourself. Security has perhaps a well-deserved reputation for being complicated since it's typically the last thing that developers work on, and when they do it often breaks the application. But I hope I've managed to convince you that with the security mechanisms provided by COM+, you can practically get out of the security programming business. Configure access control with COM+ roles, take advantage of the security provided by Kerberos in Windows 2000, and just use the few programming idioms necessary to handle the various cases that I've shown.