PgNa:使用LibNa实现PostgreSQL的现代密码学

2020-06-13 08:50:00

Travis CI使用PostgreSQL 13、12、11和10的官方扩展坞图像进行了测试。需要libNa>;=1.0.18。除了libNa库和它的开发头文件之外,您可能还需要PostgreSQL头文件(通常位于';-dev;包中)来构建扩展。

pgTAP测试可以使用';sudo-u postgres pg_proven test.sql&39;运行,也可以在独立的Docker映像中运行。如果您安装了docker来运行所有测试,则运行./test.sh。请注意,这将针对四个不同主要版本的PostgreSQL运行测试和下载docker映像,因此它需要一段时间,并且在您第一次运行它时需要大量的网络带宽。

pgNa参数以及内容和键的返回值都是byteA类型。如果您希望对一般内容使用文本或varchar值,则必须确保它们的编码正确。ENCODE()和DECODE()和CONVERT_TO()/CONVERT_FROM()二进制字符串函数可以从文本转换为字节。没有转义或Unicode字符的简单的文本字符串将由数据库隐式转换,这就是测试中如何完成的,以节省时间,但是如果您希望使用pgNa而不出现转换错误,那么您确实应该显式转换您的文本内容。

大多数libnataAPI都可以作为SQL函数使用。成对生成的密钥作为记录类型返回,例如:

pgNa小心地使用内存清理回调将释放时使用的所有已分配内存清零。一般说来,将机密存储在数据库本身是一个坏主意,虽然这样做可以小心,但风险更高。为了帮助解决此问题,pgsodium有一个可选的服务器密钥管理功能,可以在引导时加载服务器密钥。

如果您将pgNa添加到您的shared_preload_library配置中,并在您的postgres共享扩展目录中放置一个特殊的脚本,则服务器可以在serverstart上预加载一个libNa密钥。无法从SQL访问密钥。使用服务器秘密密钥的唯一方法是使用下一节中所示的pgnatrave()从它派生其他密钥。

服务器管理的密钥是完全可选的,pgNa仍然可以在不将其放入Shared_PRELOAD_LIBRARY中的情况下使用,您只需提供您自己的密钥管理即可。如果选择不使用服务器管理的密钥,请跳到API用法部分。

有关返回libNa密钥的示例脚本,请参见文件pgnatagetkey.sample。脚本必须在一行上发出十六进制编码的32字节(64个字符)字符串。请勿在不替换您自己的密钥的情况下使用此文件。编辑文件以添加您自己的密钥,删除退出行,删除.sample后缀,并使文件成为可执行文件(在unixen chmod+x pgnatagetkey上)。

接下来,将pgNa放在您的共享预加载库中。对于dockertainers,您可以在运行之后附加以下内容:

您可以根据需要编辑脚本以获取或生成密钥。pgNa可用于生成具有SELECT编码的新随机密钥(随机字节_buf(32),#39;十六进制)。其他常见模式包括在引导时提示输入密钥,从sshserver或托管云秘密系统获取密钥,或者使用命令行工具从硬件安全模块获取密钥。

使用libNa key DerivationFunctions通过id和可选上下文从主服务器秘密密钥导出新密钥。密钥ID只是大整数。如果您知道密钥ID、密钥长度(默认为32字节)和上下文(默认为pgNa&39;),您就可以确定地生成派生密钥。

派生密钥可用于加密数据或用作种子,以便使用crypto_sign_Seed_keypair()或crypto_box_Seed_keypair()确定地生成密钥对。明智的做法是不存储这些秘密,而只存储或推断密钥ID、长度和上下文。如果攻击者窃取了您的数据库映像,即使他们知道密钥ID、长度和上下文,也无法生成密钥,因为他们没有服务器密钥。

密钥ID、密钥长度和上下文可以是秘密的,如果您存储了它们,那么如果登录的数据库用户有权调用pgNa_Derate()函数,那么他们可能会生成密钥。向客户端保密密钥ID和/或长度上下文可以避免这种可能性,并确保正确设置数据库安全模型,以便只向与加密API交互的用户提供可能的最低权限。

密钥轮换由您决定,无论您想要从一个密钥转到下一个密钥。一个简单的策略是递增密钥ID并重新加密,从N到N+1。较新的密钥将具有递增的ID,您总是可以知道密钥被取代的顺序。

派生上下文是一个8字节的字节。相同的密钥ID在不同的上下文中生成不同的密钥。缺省上下文是ASCII编码字节PGNa。您可以自由使用任何8字节上下文来确定键的范围,但请记住,它必须是有效的8字节字节,可以自动正确转换为简单的ASCII字符串。有关其他字符的编码信息,请参阅encode()和decode()以及Convert_to()/Convert_from()二进制字符串函数。给定每个上下文一个大整数键空间和2^64个上下文,可派生的键空间非常大。

#SELECT PgNa_Derate(1);pgsodium_derive--\x84fa0487750d27386ad6235fc0c4bf3a9aa2c3ccb0e32b405b66e69d5021247b#SELECT PgNa_Derate(1,64);pgsodium_derive--。-\xc58cbe0522ac4875707722251e53c0f0cfd8e8b76b133f399e2c64c9999f01cb1216d2ccfe9448ed8c225c8ba5db9b093ff5c1beb2d1fd612a38f40e362073fb#SELECT PGNa_DRIVE(1,32,';__auth__';);pgsodium_derive--\xa9aadb2331324f399fb58576c69f51727901c651c970f3ef6cff47066ea92e95

派生密钥可以在用于对称加密的crypto_Secretbox_*函数中直接使用,或者用作使用例如crypto_box_Seed_new_keypair()和crypto_sign_Seed_new_keypair()生成其他密钥对的种子。

下面是一个示例脚本,它加密表中的一列,并提供执行动态解密的视图。每行存储用于派生加密密钥的随机数和密钥ID。

CREATE SCHEMA pgNa;使用SCHEMA pgNa创建扩展pgNa;CREATE TABLE TEST(id bigSerial主键,key_id bigint NOT NULL DEFAULT 1,NONCE BYTEA NOT NULL,DATA BYTEA);CREATE VIEW TEST_VIEW AS SELECT ID,CONVERT_FROM(pgsodium.crypto_secubox_open(data,nonce,pgsodium.pgNa_Derate(Key_Id)),';utf8&39;)作为test中的数据;CREATE OR REPLACE。BEGIN INSERT INSERT INTO TEST(NONCE)VALUES(New_Nonce)将id返回test_id;更新测试集data=pgsodium.crypto_Secretbox(CONVERT_TO(new.data,';utf8';),new_nonce,pgsodium.pgNa_派生(Key_Id))WHERE id=test_id;return new;end;$$;CREATE TRIGGER TEST_ENCRYPT_TRIGGER INSTEAD OF INSERT ON TEST_VIEW执行函数test_。

将视图当作普通表使用,但是下面的表是加密的。请注意,在下面的示例中,没有存储的、向SQL公开的或能够记录的密钥,只使用了基于密钥ID的派生密钥。

触发TEST_ENCRYPT_TRIGGER代替包装器TEST_VIEW上的INSERT,新插入的行用从存储的KEY_ID派生的密钥加密,该密钥默认为1。

#INSERT INSERT INTO TEST_VIEW(DATA)值(';这是一个';),(';这是两个';);#SELECT*FROM TEST;id|Key_id|Nonce|data-+-+--+。-3|1|\xa6b9c4bfbfe194541faa21f2d31565babff1a250a010fa79|\xb1d0432b173eb7fbef315ba5dd961454a4e2eef1332f9847eaef68 4|1|\x0ad82e537d5422966c110ed65f60c6bada57c0be73476950|\x8c29b12778b6bb5873c9f7fa123c4f105d6eb16e0c54dfae93da10#SELECT*FROM TEST_VIEW;ID|DATA-+-3|这是1个4|这是2个。

可以使用轮换函数来完成密钥轮换,该轮换函数将使用新的密钥ID重新加密行:

CREATE OR REPLACE函数Rotate_Key(test_id bigint,new_key bigint)返回空语言plpgsql为$$DECLARE NEW_Nonce BYTEA;BEGIN NEW_Nonce=pgsodium.crypto_Secretbox_Noncegen();更新测试集NONCE=new_nonce,key_id=new_key,data=pgsodium.crypto_secbox(pgsodium.crypto_Secretbox_open(test.data,test.nonce,

通过传递行ID和新的密钥ID来调用旋转函数。旧的行将用旧的派生密钥解密,然后用新的派生密钥加密。

#SELECT ROTATE_KEY(3,2);ROTATE_KEY-#SELECT*FROM TEST;id|Key_id|Nonce|data-+-+--+。-4|1|\x0ad82e537d5422966c110ed65f60c6bada57c0be73476950|\x8c29b12778b6bb5873c9f7fa123c4f105d6eb16e0c54dfae93da10 3|2|\x775f6b2fb01195f8646656d7588e581856ea44353332068e|\x27da7b96f4eb611a0c8ad8e4cee0988714d14e830a9aaf8f282c2a#SELECT*FROM TEST_VIEW;ID|数据-+-4|这是2|这是1。

如果攻击者获取表或数据库的转储,他们将无法派生用于加密数据的密钥,因为他们将没有根服务器管理的密钥,该密钥永远不会泄露给SQL。有关详细信息,请参阅示例文件。

下面是test.sql中的一个用法示例,它使用命令行psql客户端命令(以反斜杠开头)创建从Alice到Bob的密钥对和加密消息。

--为bob和alice生成公钥和私钥对--\gset[前缀]是一个psql命令,它将创建本地脚本变量SELECT public,secrefrom crypto_box_new_keypair()\gset bob_select public,secret from crypto_box_new_keypair()\gset alice_--创建boxnonceSELECT crypto_box_noncegen()boxnonce\gset--alice加密。,:';bob_public';,::';)box\gset--Bob使用他的密钥、随机数和Alice';的公钥SELECT CRYPTO_BOX_OPEN(:';box';,:';boxnonce';,:';alice_public';,:';bob_ret';)。

注意:在上面的示例中,数据库中没有存储任何机密,但是它们是由发送到服务器的psql客户端插入到SQL中的,因此它们可能会显示在数据库日志中。您可以通过使用派生密钥来避免这种情况。

如果您选择使用自己的密钥,一种更偏执的方法是将密钥保存在外部存储中,并禁用日志记录,同时使用set local将密钥注入局部变量。如果数据库的图像被黑客攻击或窃取,攻击者将无法获得密钥。

要禁用键注入的日志记录,还可以使用set local禁用LOG_STATEMENT,然后重新启用正常日志记录。如下所示。设置LOG_STATEMENT需要超级用户权限:

--必须在事务块BEGIN中设置LOCAL;--为Bob和Alice生成boxnonce以及公钥和私钥对--这将创建发送回客户端但不存储的秘密--或记录秘密。请确保您正在使用加密的数据库连接!SELECT CRYPTO_BOX_NONCEGEN()boxnonce\gsetSELECT public,Secret from crypto_box_new_keypair()\gset bob_select public,secret from crypto_box_new_keypair()\gset Alice_--关闭日志记录并将密码注入SET LOCAL会话,然后继续记录。Set local log_Statement=';None';;ALICE_SECRET';;RESET LOG_STATEMENT;--现在调用`Current_Setting()`函数来获取密钥,这些密钥不会--存储在数据库中,而只存储在会话内存中,当会话关闭时,它们将不再可访问。--Alice使用她的密钥和他的公钥为Bob加密盒SELECT CRYPTO_BOX(';Bob是您的叔叔&39;:';boxnonce&39;,:';bob。app.alice_secret&39;)::bytea)BOX\gset--Bob使用他的密钥和爱丽丝的公钥解密BOX。SELECT CRYPTO_BOX_OPEN(:';BOX';,:';BOXNONCE';,:';ALICE_PUBLIC&39;,current_setting(';app.bob_secret';)::bytea);COMMIT;

对于额外的偏执,您可以使用一个函数来检查正在使用的连接是安全的还是Unix域套接字。

CREATE Function IS_SSL_OR_DOMAIN_SOCKET()将boolLANGUAGE plpgsql返回为$$DECLARE ADDR TEXT;SSL TEXT;BEGIN SELECT inet_CLIENT_ADDR()INTO ADDR;SELECT CURRENT_SETTING(';SSL';,TRUE)INTO SSL;如果找不到OR((SSL IS NULL或SSL!=';ON';)AND(addr IS NOT NULL OR Length(Addr)!=0)),则返回FALSE;END IF;RETURN。

当然,这并不能保证秘密不会以某种方式泄露出去,但如果您从不存储秘密,而只通过安全通道将其发送回客户端,例如使用上面所示的psql client\gset命令,或者只存储添加的密钥ID和上下文,则它会很有用。

下面的参考文献改编自并使用了一些与libNa C APIDocumentation相同的语言。有关算法和其他LibNa特定细节的详细信息,请参阅这些文档。

该库提供了一组函数来生成不可预测的数据,适用于创建密钥。

函数的作用是:返回一个介于0和上限(不包括在内)之间的不可预知的值。与随机字节_随机()%UPPER_BIND不同,它保证可能输出值的均匀分布,即使当UPPER_BIND不是2的幂时也是如此。请注意,UPERBIND<;2只留下一个元素可供选择,即0。

随机字节_buf_defiristic()返回一个大小字节,该字节包含在不知道种子的情况下与随机字节无法区分的字节。对于给定的种子,此函数将始终输出相同的序列。大小最大可达2^38(256 GB)。

crypto_Secretbox_keygen()-->;byta crypto_Secretbox_Noncegen()->;bytecrypto_secto box(message byteA,nonce bytea,key bytea)->;bytecrypto_Secretbox_open(密文byteA,nonce bytea,key bytea)->;byteA。

crypto_Secretbox_noncegen()生成一个随机随机数,该随机数将在加密消息时使用。为了安全起见,每个随机数只能使用一次,尽管它不是秘密。随机数的目的是增加消息的随机性,以便用相同密钥多次加密的同一消息将产生不同的密文。

crypto_Secretbox()使用先前生成的随机数和密钥对消息进行加密。可以使用crypto_Secretbox_open()对加密的消息进行解密。请注意,要解密消息,需要原始的随机数。

crypto_auth_keygen()->;byta crypto_auth(message byteA,key byteA)->;byTea crypto_auth_ify(mac byteA,message byteA,key bytea)->;布尔值。

crypto_auth()生成用于组合消息和密钥的身份验证标记(Mac)。这不会加密消息;它只是提供了一种证明消息没有被篡改的方法。要验证以这种方式标记的消息,请使用crypto_auth_ify()。此函数是确定性的:对于给定的消息和密钥,生成的MAC将始终相同。

请注意,这需要访问密钥,这通常不应该共享。如果许多用户需要验证消息,使用公钥签名通常比共享私钥要好。

crypto_auth_ify()验证给定的mac(Authationtag)是否与提供的消息和密钥匹配。这告诉我们原始消息没有被篡改。

crypto_box_new_keypair()-->;crypto_box_keypair crypto_box_Noncegen()->;bytecrypto_box(message bytea,nonce byTea,public bytee,secrebytee)->;bytecrypto_box_open(密文byteA,nonce byTea,public bytea,secreybytee)->;byTea

crypto_box_new_keypair()返回用于公钥加密的新的、随机生成的密钥对。公钥可以与任何人共享。绝对不能共享密钥。

crypto_box_noncegen()生成将在加密消息时使用的随机随机数。为了安全起见,每个随机数只能使用一次,尽管这不是秘密。随机数的目的是增加消息的随机性,使得用相同密钥多次加密的同一消息将产生不同的密文。

crypto_box()使用随机数、意向接收方的公钥和发送方的私钥对消息进行加密。所得到的密文只能由预期的接收者使用他们的密钥来解密。现时值必须与密文一起发送。

crypto_box_open()解密使用crypto_box()加密的密文。它以密文、随机数、发送者的公钥和接收者的秘密密钥为参数,并返回原始消息。请注意,接收方应确保公钥属于发送方。

crypto_sign_new_keypair()->;crypto_sign_keypair组合模式函数:crypto_sign(message byteA,key byteA)->;byta crypto_sign_open(sign_message byteA,key byteA)->;byta分离模式函数:crypto_sign_disached(message byteA,key byteA)->;byta crypto_sign_ify_disached(sig byteA,message byteA,BYTEA CRYPTO_SIGN_UPDATE(状态BYTEA,消息BYTEA)->;BYTEA CRYPTO_SIGN_FINAL_CREATE(状态BYTEA,密钥BYTEA)->;BYTEA CRYPTO_SIGN_FINAL_VERIFY(状态BYTEA,签名BYTEA,密钥BYTEA)->;布尔。

这些函数用于验证消息来自特定的发起者(您拥有其公钥的秘密密钥的持有者),并且没有被篡改。

crypto_sign_new_keypair()返回新的、随机生成的公钥签名密钥对。公钥可以与任何人共享。绝对不能共享密钥。

CRYPTO_SIGN()和CRYTO_SIGN_VERIFY()在组合模式下运行。在此模式下,正在签名的消息与其签名合并为一个单元。

crypto_sign()使用签名者的密钥创建签名,并将该密钥添加到消息前面。可以使用crypto_sign_open()对结果进行身份验证。

crypto_sign_open()接受由crypto_sign()创建的签名消息,使用发送方的公钥检查其有效性,如果有效则返回原始消息,否则引发数据异常。

CRYPTO_SIGN_DETACHED()和CRYPTO_SIGN_VERIFY_DETACHED()在分离模式下运行。在这种模式下,消息与签名保持独立。当希望对已存储的对象进行签名时,或者在一个对象需要多个签名的情况下,这会很有用。

crypto_sign_disached()使用签名者的密钥为消息生成签名。结果是独立于消息而存在的签名,可以使用crypto_sign_Verify_Detached()进行验证。

CRYPTO_SIGN_VERIFY_DETACHED()用于验证由CRYPTO_SIGN_DETACHED()重新生成的签名。它采用生成的签名、原始消息和签名者的公钥,如果签名与消息和密钥匹配,则返回TRUE,否则返回FALSE。

CRYPTO_SIGN_INIT()、CRYPTO_SIGN_UPDATE()、CRYPTO_SIGN_FINAL_CREATE()、CRYPTO_SIGN_FINAL_VERIFY()和聚合CRYPTO_SIGN_UPDATE_AGG()多部分消息的句柄签名。要创建或验证多方消息的签名,请使用crypto_sign_init()来启动该过程,然后每条消息。

..