PostgreSQL支持用户定义类型(UDT)。 当否则我们将不得不使用简单的BLOB对象时,可以使用这些类型为用户定义的函数提供类型安全性。
这付出了巨大的代价。 许多数据库支持UDT,但是实现细节差异很大,因此存在大量的供应商锁定。 另外,C语言UDT需要通过包含共享库的PostgreSQL扩展进行部署,并且在使用SAAS(例如,Amazon云)时很少使用。 这迫使我们维护自己的数据库服务器,而不是依赖SAAS提供程序。
另一方面,用户定义的类型和功能为我们提供了更大的灵活性,例如,具有专用加密硬件的集中位置。
我之所以使用OpenSSL库,原因很简单,因为PostgreSQL已经包含了该库来支持加密通道。 这样就不必担心库的依赖性或对加密软件的法律限制了–我做出合理的假设,即该软件在已经使用OpenSSL库的任何位置合法使用。
动机
对于站点来说,将x509数字证书存储为blob,其中包含许多用于索引和搜索的附加列是非常常见的。
CREATE TABLE certs (
cert BLOB NOT NULL,
name VARCHAR[100] NOT NULL,
not_before TIMESTAMP NOT NULL,
not_after TIMESTAMP NOT NULL
);
问题在于证书和索引字段之间没有强制执行一致性。 攻击者不太可能插入其他证书,但不能排除它,结果可能是灾难性的。
更好的解决方案是在插入和更新时使用触发器
-- create trigger
CREATE CONSTRAINT TRIGGER cert_update() BEFORE INSERT OR UPDATE
ON certs NOT DEFERRABLE FOR EACH ROW
EXECUTE PROCEDURE cert_update_proc ();
-- create function that ensures indexed fields reflect cert
CREATE OR REPLACE FUNCTION cert_update_proc RETURNING trigger $$
BEGIN
INSERT INTO certs(cert, X509_name(cert), X509_not_before(cert), X509_not_after(cert));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
这样可以确保攻击者在未更新所有索引值的情况下永远无法替换证书。 对索引字段的更新将被忽略。 这并不是100%的攻击-一名熟练的攻击者可能会丢弃该触发器-但这比依靠用户维护此信息要安全得多。 请记住,包括触发器在内的模式应由与应用程序数据库用户不同的数据库用户拥有,这样,受感染的应用程序就无法删除触发器。
(表定义或存储过程还可以执行完整性检查,例如,确保“ not_after”时间戳严格晚于“ not_before”时间戳。)
我们没有被迫使用用户定义的类型,但是在定义助手功能时,它可以使我们更加精确。 它也更加用户友好,因为它们可以插入和检索标准的OpenSSL PEM值,而不用处理BLOB。
从长远来看,我们可以创建用户定义的函数,这些函数可以使用OpenSSL类型执行实际工作。 我们还可以应用涉及内部查询的更复杂的检查,例如,确保添加或修改的任何证书都由数据库中的另一个证书签名。 这超出了本博客条目的范围。
OpenSSL引擎
众所周知,OpenSSL通过“引擎”接口支持加密硬件。 默认行为是使用软件实现,但是对于编写良好的代码,应该直接使用专用加密硬件。
这样可以显着提高性能。 该软件实现对于开发和小型站点是足够的,但在大型站点中可能成为限制因素。 减少此问题的硬件解决方案。
更为微妙的一点是,加密密钥是敏感信息,许多组织都不希望它们在任何情况下都公开。 没有文件,没有网络服务。 硬件实现具有生成自己的加密密钥的能力,并且没有用于公开密钥的机制。 (最多有一种方法可以将密钥从一个硬件设备克隆到另一个。)
设计
设计基于一个简单的考虑–我只想存储有效的对象。 确保这一点的最佳方法是让用户定义的类型将外部PEM值转换为内部OpenSSL对象,反之亦然。 我们可以执行其他完整性检查,但这不是必需的。
这有一个不幸的缺点–传统密钥无法加密。 我不认为这是个问题,因为无论如何都不应该使用传统密钥(IMHO)–它们应该存储在诸如密钥库(PKCS8和PKCS12)之类的容器中。
也就是说,传统的密钥是如此简单,以至于它们是开发我们的PGXS技能的绝佳平台。
实施约束
PostgreSQL施加的约束在手册http://www.postgresql.org/docs/9.4/static/xfunc-c.html的 35.9.5节中指定。 其中大多数由PGXN的工具处理-有关详细信息,请参阅我之前的博客文章 。
CRYPTO_set_mem_functions函数覆盖它,但是在服务器中调用它是危险的,因为您不知道已经创建了哪些对象,例如,在建立与数据库的安全连接时。 如果对对象进行了malloc分配,然后对pfree分配了对象,则可能导致服务器崩溃。
相反,我们必须非常小心,始终转换和释放由OpenSSL库创建的任何对象。
定义RSA密钥对UDT
现在,我们准备创建RSA密钥对UDT。 如前所述,密钥应存储在PKCS8或PKCS12对象中,而不是存储在传统RSA密钥对对象中,因为后者必须未加密存储。
我们首先定义RSA UDT本身。
--
-- Create shell type.
--
CREATE TYPE RSA;
--
-- Create function that converts string to internal format.
--
CREATE OR REPLACE FUNCTION rsa_in(cstring)
RETURNS RSA
AS 'pgopenssltypes', 'rsa_in'
LANGUAGE C IMMUTABLE STRICT;
--
-- Create function that converts internal format to string.
--
CREATE OR REPLACE FUNCTION rsa_out(RSA)
RETURNS CSTRING
AS 'pgopenssltypes', 'rsa_out'
LANGUAGE C IMMUTABLE STRICT;
--
-- Redefine type with necessary functions.
--
CREATE TYPE RSA (
INPUT = rsa_in,
OUTPUT = rsa_out
);
UDT可以指定大约十二个C函数(请参阅CREATE TYPE ),但是仅有的两个必需函数是INPUT和OUTPUT函数,它们在C字符串和bytea表示之间转换对象。
普通的UDT非常无聊,因此我们还定义两个用户定义的函数:
--
-- Generate RSA keypair. This is an expensive operation
-- so it should not be called casually.
--
CREATE OR REPLACE FUNCTION rsa_generate_keypair(int)
RETURNS RSA
AS 'pgopenssltypes', 'rsa_generate_keypair'
LANGUAGE C IMMUTABLE STRICT;
CREATE TYPE RSA_INFO AS (
BITS int,
N BN,
E BN,
D BN,
P BN,
Q BN
);
--
-- Get details about RSA keypair.
--
CREATE OR REPLACE FUNCTION rsa_get_details(RSA)
RETURNS RSA_INFO
AS 'pgopenssltypes', 'rsa_get_details'
LANGUAGE C IMMUTABLE STRICT;
RSA_INFO是复合类型 。 无法从一个函数返回多个列,但是有时值本质上是相关的,并且您希望将它们作为一个单元返回。 PostgreSQL支持复合类型。 您可以轻松访问这些字段。
bgiles=# SELECT rsa_get_details(rsa_generate_keypair(256)) AS details INTO sample;
bgiles=# \d sample
Table "public.sample"
Column | Type | Modifiers
---------+----------+-----------
details | rsa_info |
bgiles=# SELECT (details).P, (details).Q FROM sample;
p | q
-----------------------------------------+-----------------------------------------
331128053999826595053108455708184431513 | 294756634092692440982306957700237950609
(1 row)
在这种情况下,没有理由要保留ROW_INFO对象–除了在存储过程中的任何地方,我都无法想象使用它。 但这是工具箱中的好工具。
实施RSA密钥对UDT(INPUT / OUTPUT)
一旦了解了OpenSSL库,基本INPUT和OUTPUT方法的实现就很简单。 公共功能是:
/*
* Read PEM format.
*/
PG_FUNCTION_INFO_V1(rsa_in);
Datum rsa_in(PG_FUNCTION_ARGS) {
char *txt;
bytea *result;
RSA *rsa;
// write RSA keypair into buffer
rsa = rsa_from_string(txt);
result = rsa_to_bytea(rsa);
RSA_free(rsa);
// return bytea
PG_RETURN_BYTEA_P(result);
}
/*
* Write PEM format.
*/
PG_FUNCTION_INFO_V1(rsa_out);
Datum rsa_out(PG_FUNCTION_ARGS) {
bytea *raw;
char *result;
RSA *rsa;
// write RSA keypair into buffer
rsa = rsa_from_bytea(raw);
result = rsa_to_string(rsa);
RSA_free(rsa);
PG_RETURN_CSTRING(result);
}
由于用户定义的函数被声明为STRICT,并且代码混乱,因此我没有检查NULL值。 实际上,始终检查一个空值不会有多大危害。 主要的收获是,我创建了一个OpenSSL对象,使用它,然后立即将其丢弃。
此公共方法使用四种静态便捷方法:
/*
* Convert string to RSA.
*/
static RSA * rsa_from_string(const char *txt) {
BIO *inp;
RSA *rsa = RSA_new();
inp = BIO_new_mem_buf((char *) txt, strlen(txt));
PEM_read_bio_RSAPrivateKey(inp, &rsa, 0, NULL);
BIO_free(inp);
return rsa;
}
/*
* Convert bytea to RSA.
*/
static RSA * rsa_from_bytea(const bytea *raw) {
BIO *bio;
RSA *rsa;
// convert into RSA keypair
bio = BIO_new_mem_buf(VARDATA(raw), VARSIZE(raw) - VARHDRSZ);
BIO_set_close(bio, BIO_NOCLOSE);
rsa = RSA_new();
d2i_RSAPrivateKey_bio(bio, &rsa);
BIO_free(bio);
if (rsa == NULL) {
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED), errmsg(
"unable to decode RSA keypair record")));
}
return rsa;
}
/*
* Convert RSA to string.
*/
static char * rsa_to_string(const RSA *rsa) {
BIO *bio;
int len;
char *ptr, *result;
// write RSA keypair into buffer
// arguments: ..., cipher, keyptr, keylen, passwd_cb, passwd_cb_data
bio = BIO_new(BIO_s_mem());
PEM_write_bio_RSAPrivateKey(bio, (RSA *) rsa, NULL, NULL, 0, NULL, NULL);
// create results.
len = BIO_number_written(bio);
BIO_get_mem_data(bio, &ptr);
result = palloc(len + 1);
strncpy(result, ptr, len);
result[len] = '';
BIO_free(bio);
return result;
}
/*
* Convert RSA to bytea.
*/
static bytea * rsa_to_bytea(const RSA *rsa) {
BIO *bio;
int len;
bytea *result;
char *ptr;
// write RSA keypair into buffer
bio = BIO_new(BIO_s_mem());
i2d_RSAPrivateKey_bio(bio, (RSA *) rsa);
// create bytea results.
len = BIO_number_written(bio);
BIO_get_mem_data(bio, &ptr);
result = (bytea *) palloc(len + VARHDRSZ);
memcpy(VARDATA(result), ptr, len);
SET_VARSIZE(result, len + VARHDRSZ);
BIO_free(bio);
return result;
}
bytea是一种PostgreSQL类型,包含可变数量的内存。 前四个字节(VARHDRSZ)是长度,并且数据本身是通过便捷宏(VARDATA)访问的。 因为我只存储一个值,所以我直接使用宏,所以更复杂的对象可以定义类型并将其转换为bytea对象。
如果您不熟悉OpenSSL库,则此代码可能令人困惑,但是有两个简单的观察结果会使它更加清晰。 首先,所有I / O均通过“基本输入/输出(BIO)”抽象处理。 刚开始时很痛苦,但是它允许您堆叠数据操作,例如压缩和加密。
d2i和i2d函数读取和写入对象。 它们分别是“ DER到内部”和“ DER的内部”。 PEM和DER格式相同– PEM是base-64编码的,并简短地描述了内容,但是没有其他信息。
您可以从经验,手册页和谷歌搜索中获取其余信息。
生成新的密钥对
继续介绍有趣的用户定义功能。 首先是生成一个新的密钥对。 这不是您经常要做的事情,但是在开发和测试过程中非常方便,因为生成256位RSA密钥的速度非常快。
公共方法只是确保我们具有有效和合理的值。 如果要求提供小钥匙,则会打印警告,但不是禁止的。 这很容易做到–使用ERROR而不是INFO。
/**
* Generate a random keypair
*/
PG_FUNCTION_INFO_V1(rsa_generate_keypair);
Datum rsa_generate_keypair(PG_FUNCTION_ARGS) {
bytea *result;
int bits;
RSA *rsa;
bits = PG_GETARG_INT32(0);
if (bits <= 0) {
bits = 2048;
}
if (bits < 2048) {
// elog(INFO, "RSA keys should be at least 2048 bits.")
ereport(INFO,
(errcode(ERRCODE_CHECK_VIOLATION,
errmsg("RSA keys should be at least 2048 bits.")));
}
rsa = rsa_generate_keypair_internal(bits);
result = rsa_to_bytea(rsa);
RSA_free(rsa);
// return bytea
PG_RETURN_BYTEA_P(result);
}
/*
* actual key generation
*/
RSA * rsa_generate_keypair_internal(int bits) {
BIGNUM *ep;
RSA *rsa;
rsa = RSA_new();
ep = BN_new();
BN_dec2bn(&ep, "65537");
RSA_generate_key_ex(rsa, bits, ep, NULL);
BN_free(ep);
return rsa;
}
该密钥使用标准指数– 65537(0x10001)–因为使其可配置几乎没有价值。
检索密钥对详细信息
检索密钥对的详细信息要复杂一些,您肯定希望在查看此代码时阅读PostgreSQL文档。
/**
* Get details about an RSA keypair
*/
PG_FUNCTION_INFO_V1( rsa_get_details);
Datum rsa_get_details( PG_FUNCTION_ARGS) {
bytea *raw;
RSA *rsa;
TupleDesc desc;
HeapTuple tuple;
Datum *values;
bool *retNulls;
// check for null value.
raw = PG_GETARG_BYTEA_P(0);
if (raw == NULL || VARSIZE(raw) == VARHDRSZ) {
PG_RETURN_NULL();
}
// read keypair, verify success.
rsa = rsa_from_bytea(raw);
if (rsa == NULL) {
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED), errmsg(
"unable to decode RSA keypair record")));
PG_RETURN_NULL();
}
// read details about return value.
if (get_call_result_type(fcinfo, NULL, &desc) != TYPEFUNC_COMPOSITE) {
RSA_free(rsa);
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg(
"function returning record called in context "
"that cannot accept type record")));
}
desc = BlessTupleDesc(desc);
// these values are freed by PostgreSQL
values = (Datum *) palloc(6 * sizeof(Datum));
retNulls = (bool *) palloc(6 * sizeof(bool));
// set return values
values[0] = Int32GetDatum(8 * RSA_size(rsa));
retNulls[0] = false;
if (rsa->n == NULL) {
retNulls[1] = true;
} else {
retNulls[1] = false;
values[1] = BnGetDatum(rsa->n);
}
if (rsa->e == NULL) {
retNulls[2] = true;
} else {
retNulls[2] = false;
values[2] = BnGetDatum(rsa->e);
}
if (rsa->d == NULL) {
retNulls[3] = true;
} else {
retNulls[3] = false;
values[3] = BnGetDatum(rsa->d);
}
if (rsa->p == NULL) {
retNulls[4] = true;
} else {
retNulls[4] = false;
values[4] = BnGetDatum(rsa->p);
}
if (rsa->q == NULL) {
retNulls[5] = true;
} else {
retNulls[5] = false;
values[5] = BnGetDatum(rsa->q);
}
RSA_free(rsa);
// convert to tuple.
tuple = heap_form_tuple(desc, values, retNulls);
FreeTupleDesc(desc);
// return datum.
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
}
这是一个长函数,不熟悉调用,但遵循通常的模式。 读取数据,对其进行验证,对其进行处理,准备结果,返回结果。
此功能指的是“ BN” UDT。 我不在这里包括,但是它是OpenSSL“ BIGNUM”类型的包装。 如果您有兴趣,请在我的git存储库中找到详细信息。
结果就是我们所期望的。
bgiles=# select * from sample;
details
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
(256,97602190650652191344500377647616852664474030847991993174417850011862837141417,65535,49816062791999163016436913587169239410285445043144541244445777903161695571583,331128053999826595053108455708184431513,294756634092692440982306957700237950609)
(1 row)
bgiles=# select (details).bits from sample;
bits
------
256
(1 row)
bgiles=# select (details).e from sample;
e
-------
65535
(1 row)
bgiles=# select (details).d from sample;
d
-------------------------------------------------------------------------------
49816062791999163016436913587169239410285445043144541244445777903161695571583
(1 row)
bgiles=#