Recently I have heard a few questions regarding the scope of the SYMMETRIC KEY key-ring, especially when using modules (i.e. stored procedures) to open a key. One particular topic that got my attention is the impression that the OPEN SYMMETRIC KEY call may “leak outside the module” (i.e. the key will remain opened) if the SYMMETRIC KEY is not closed inside the module.
In the OPEN SYMMETRIC KEY topic in BOL (under Remarks section) we documented that the opened key is bound to the session, not to the execution context (including a module frame) and that it will remain opened until the key is explicitly closed (using CLOSE SYMMETRIC KEY) or the session is terminated. This is indeed the designed behavior.
What does this mean when opening a symmetric key on a module that is not supposed to “leak” the key? It means that it is necessary to make sure that the key is explicitly closed; there is no mechanism to bind the OPEN SYMMETRIC KEY call to the module in SQL Server 2005.
For example:
CREATE CERTIFICATE [cert_keyring_demo]
WITH SUBJECT = 'key ring demo'
go
CREATE SYMMETRIC KEY [symkey_keyring_demo]
WITH ALGORITHM = AES_192
ENCRYPTION BY CERTIFICATE [cert_keyring_demo]
CREATE USER [lowpriv_user] WITHOUT LOGIN
CREATE PROC [sp_openkey]
-- We will be runnign this module under an impersonated context
WITH EXECUTE AS OWNER
AS
OPEN SYMMETRIC KEY [symkey_keyring_demo]
DECRYPTION BY CERTIFICATE [cert_keyring_demo]
-- Notice that the key is not being closed on purpose
--
-- Grant minimum privielges
GRANT EXECUTE ON [dbo].[sp_openkey] TO [lowpriv_user]
GRANT VIEW DEFINITION ON SYMMETRIC KEY::[symkey_keyring_demo] TO [lowpriv_user]
EXECUTE AS USER = 'lowpriv_user'
SELECT * FROM sys.openkeys
-- fails with error 15151
-- This will succeed
EXEC [dbo].[sp_openkey]
-- And we can verify that the key is opened on our session.
-- and we can encrypt & decrypt
declare @blob varbinary(1000)
declare @pt varchar(1000)
SET @blob = encryptbykey( key_guid( 'symkey_keyring_demo'), 'data' )
SET @pt = convert( varchar(1000), decryptbykey( @blob ))
SELECT @pt, @blob
-- We can swithc context...
REVERT
-- and verify that the key ring is still opened
-- And the key remains opened until we close it
-- or we terminate the session
CLOSE SYMMETRIC KEY symkey_keyring_demo
Obviously in this example the intention was to keep the key opened after the module ends, but it is possible that this may happen by mistake and we “leak the key” as an undesired error (i.e. a bug in the application), for example:
CREATE TABLE [dbo].[tabl_keyring_demo]( id int IDENTITY PRIMARY KEY,
data varbinary(1000), LastUsedDate datetime )
INSERT INTO [dbo].[tabl_keyring_demo]
VALUES ( encryptbykey( key_guid( 'symkey_keyring_demo'), 'lowpriv_user' ), GetDate())
VALUES ( encryptbykey( key_guid( 'symkey_keyring_demo'), 'outdated_user' ), GetDate())
CLOSE SYMMETRIC KEY [symkey_keyring_demo]
CREATE PROC [sp_keyring_demo2]( @id int )
-- The intention of this SP is to decrypt data, but close the key
-- before leaving the module frame
declare @username varchar(1000)
if( EXISTS(SELECT count(*) FROM [dbo].[tabl_keyring_demo] WHERE Id = @id))
BEGIN
DECRYPTION BY CERTIFICATE [cert_keyring_demo];
SELECT @username = convert( varchar(1000), decryptbykey( data ))
FROM [dbo].[tabl_keyring_demo] WHERE Id = @id;
-- For demonstration purposes, I will add a DDL statetemnt that I know
-- will fail the second time I execute the module
if( @username is not null )
EXECUTE AS USER = @username
--... do something interesting under this context
END
-- Updating LastUsedDate column
UPDATE [dbo].[tabl_keyring_demo] SET LastUsedDate = GetDate() WHERE Id = @id;
CLOSE SYMMETRIC KEY [symkey_keyring_demo];
GRANT EXECUTE ON [dbo].[sp_keyring_demo2] TO [lowpriv_user]
--Let's give it a try...
-- This one will work as expected
EXEC [dbo].[sp_keyring_demo2] 1
-- and no keys in the key ring
-- But this one will fail with error 15517
-- This error will terminate the current batch,
-- and the key will not close properly
EXEC [dbo].[sp_keyring_demo2] 2
--Clean up
Notice that this particular SP was not properly designed, and it keeps the key opened far longer than needed and that the error handing for the code segment that may fail. The following modification in the code is an improvement over the original one:
ALTER PROC [sp_keyring_demo2]( @id int )
SELECT @username = convert( varchar(1000), DecryptByKeyAutoCert( cert_id('cert_keyring_demo'), null, data ))
BEGIN TRY
END TRY
BEGIN CATCH
print 'Unexpected error'
print error_message()
-- add code to handle error properly here
RAISERROR( 'Error in impersonated context', 16, 1 )
END CATCH
-- succeeds
-- fails gracefully
As you can see, the current design decision of binding the key ring to the session may affect the way you need to design modules that open symmeric keys, especially during scenarios where the key should only be opened on behalf of the caller exclusively during a module execution. It is very important to understand the limitations and behavior of the system in case of an error and taking it into consideration when designing an application that uses OPEN SYMMETRIC KEY.
A few tips for modules that open symmetric keys on behalf of the caller:
· Whenever is possible use the DecryptByKeyAuto* built-ins instead of the regular DecryptByKey one, that way the key is implicitly opened by the built-in and there is no need to explicitly open it on your module
· Have as little code as possible between Opening and Closing the symmetric key to minimize logic errors that may lead to opening the key without closing it properly.
· Make sure your code logic will handle errors properly. You can use TRY/CATCH in SQL to avoid an early termination of the module.
I hope this information will be useful; as always feel free to ask any questions or send us feedback regarding this topic, we will be glad to help.
-Raul Garcia
SDE/T
SQL Server Engine