Статьи

Добавление пользовательских типов OpenSSL в PostgreSQL

PostgreSQL поддерживает пользовательские типы (UDT). Эти типы могут использоваться для обеспечения безопасности типов для пользовательских функций, когда в противном случае мы были бы вынуждены использовать простые объекты BLOB.

Это связано со значительными затратами. Многие базы данных поддерживают UDT, но детали реализации сильно различаются, поэтому существует значительная степень привязки к поставщикам. Кроме того, UDT на языке C требует развертывания с помощью расширений PostgreSQL, содержащих разделяемые библиотеки, что редко доступно при использовании SAAS, например облака Amazon. Это заставляет нас поддерживать наши собственные серверы баз данных, а не полагаться на провайдера SAAS.

С другой стороны, определяемые пользователем типы и функции дают нам гораздо большую гибкость, например, централизованное расположение с выделенным криптографическим оборудованием.

Я использую библиотеку OpenSSL по той простой причине, что она уже включена в PostgreSQL для поддержки зашифрованных каналов. Это устраняет необходимость беспокоиться о библиотечных зависимостях или юридических ограничениях на криптографическое программное обеспечение — я делаю разумное предположение, что это программное обеспечение разрешено использовать в любом месте, где уже используется библиотека OpenSSL.

мотивация

Сайты очень часто хранят цифровые сертификаты x509 в виде большого двоичного объекта с рядом дополнительных столбцов, используемых для индексации и поиска.

1
2
3
4
5
6
CREATE TABLE certs (
   cert       BLOB NOT NULL,
   name       VARCHAR[100] NOT NULL,
   not_before TIMESTAMP NOT NULL,
   not_after  TIMESTAMP NOT NULL
);

Проблема заключается в том, что между сертификатом и индексированными полями нет никакой согласованности. Злоумышленник вряд ли сможет указать другой сертификат, но его нельзя исключить, а результаты могут быть катастрофическими.

Гораздо лучшим решением является использование триггеров при вставке и обновлении

01
02
03
04
05
06
07
08
09
10
11
12
-- 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».)

Мы не обязаны использовать пользовательский тип, но это позволяет нам быть более точными при определении наших вспомогательных функций. Это также намного удобнее для пользователя, так как они могут вставлять и извлекать стандартные значения PEM OpenSSL вместо того, чтобы иметь дело с BLOB.

В долгосрочной перспективе мы можем создавать пользовательские функции, которые могут использовать типы OpenSSL для выполнения реальной работы. Мы также можем применять более сложные проверки, которые включают внутренние запросы, например, гарантируя, что любой добавленный или измененный сертификат подписан другим сертификатом в базе данных. Это выходит за рамки этой записи в блоге.

OpenSSL движки

Широко известно, что OpenSSL поддерживает криптографическое оборудование через интерфейс «движка». Поведение по умолчанию состоит в использовании программной реализации, но с хорошо написанным кодом должно быть просто использовать вместо этого выделенное криптографическое оборудование.

Это может привести к значительному увеличению производительности. Реализация программного обеспечения адекватна для разработки и небольших сайтов, но может стать ограничивающим фактором на больших сайтах. Аппаратные решения, которые уменьшают эту проблему.

Более тонкий момент заключается в том, что ключи шифрования являются конфиденциальной информацией, и многие организации не хотят, чтобы они были раскрыты ни при каких условиях. Нет файлов, нет веб-сервисов. Аппаратные реализации имеют возможность генерировать свои собственные ключи шифрования, и механизм раскрытия ключей отсутствует. (Самое большее, есть способ клонировать ключи от одного аппаратного устройства до второго.)

дизайн

Дизайн основан на простом соображении — я хочу хранить только действительные объекты. Лучший способ убедиться в этом состоит в том, чтобы пользовательские типы преобразовывали внешние значения PEM во внутренние объекты OpenSSL и наоборот. Мы можем выполнить дополнительные проверки работоспособности, но это не обязательно.

Это имеет один неприятный недостаток — традиционные ключи не могут быть зашифрованы. Я не считаю это проблемой, так как традиционные ключи не должны использоваться в любом случае (IMHO) — они должны храниться в контейнерах, таких как хранилища ключей (PKCS8 и PKCS12).

При этом традиционные ключи настолько просты, что являются отличной платформой для развития наших навыков в PGXS.

Ограничения реализации

Ограничения, налагаемые PostgreSQL, указаны в разделе 35.9.5 руководства http://www.postgresql.org/docs/9.4/static/xfunc-c.html . Большинство из них обрабатываются инструментами PGXN — подробности см. В моей предыдущей записи в блоге .

Особенно неприятным ограничением является то, что PostgreSQL, как и большинство долго работающих сервисов, имеет собственную библиотеку управления памятью. OpenSSL по умолчанию использует стандартную библиотеку управления памятью. Это можно переопределить с помощью функции CRYPTO_set_mem_functions, но опасно вызывать это на сервере, потому что вы не знаете, какие объекты уже были созданы, например, при установлении безопасного соединения с базой данных. Это может привести к сбою сервера, если объект malloc’ed, но затем pfree’d.

Вместо этого мы должны быть очень осторожны, чтобы всегда преобразовывать и освобождать любой объект, созданный библиотекой OpenSSL.

Определение UDT пары ключей RSA

Теперь мы готовы создать UDT пары ключей RSA. Как упоминалось ранее, ключи должны храниться в объекте PKCS8 или PKCS12 вместо традиционного объекта пары ключей RSA, поскольку последний должен храниться в незашифрованном виде.

Начнем с определения самого RSA UDT.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
--
-- 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 довольно скучный, поэтому давайте также определим две пользовательские функции:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
--
-- 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 поддерживает это с составными типами. Вы можете легко получить доступ к полям.

01
02
03
04
05
06
07
08
09
10
11
12
13
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 — я не могу представить его где-либо, кроме как в хранимых процедурах. Но это хороший инструмент в вашем наборе инструментов.

Реализация UDT пары ключей RSA (ВХОД / ВЫХОД)

Реализация основных методов INPUT и OUTPUT проста, если вы понимаете библиотеку OpenSSL. Публичные функции:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
 * 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);
}

Я не проверял значение NULL, так как пользовательская функция была объявлена ​​STRICT, и она загромождает код. На практике это не принесет большого вреда, если всегда проверять нулевое значение. Основным выводом является то, что я создаю объект OpenSSL, использую его, а затем сразу же отказываюсь от него.

Эти открытые методы используют четыре статических вспомогательных метода:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/*
 * 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, но есть два простых замечания, которые сделают ее намного понятнее. Во-первых, все операции ввода / вывода обрабатываются с помощью абстракции «Основной ввод / вывод (BIO)». Поначалу это больно, но это позволяет вам комбинировать манипуляции с данными, такие как сжатие и шифрование.

Во-вторых, объекты читаются и пишутся с использованием функций d2i и i2d . Это «DER к внутреннему» и «внутреннее к DER», соответственно. Форматы PEM и DER идентичны — PEM кодируется в base-64 и имеет краткий комментарий с описанием содержимого, но никакой дополнительной информации нет.

Остальное вы получаете из опыта, справочных страниц и поисковик для примеров.

Генерация новой пары ключей

Вперед к интересным пользовательским функциям. Сначала создайте новую пару ключей. Это не то, что вы будете делать очень часто, но это очень удобно во время разработки и тестирования, поскольку очень быстро генерировать 256-битные ключи RSA.

Открытый метод просто гарантирует, что у нас есть действительные и вменяемые значения. Предупреждение выводится, если запрашивается маленькая клавиша, но это не запрещено. Это было бы легко сделать — используйте ERROR вместо INFO.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
 * 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, глядя на этот код.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
 * 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-хранилище.

Результаты — это то, что мы ожидаем.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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=#

Исходный код и другие ресурсы

Приведенный выше исходный код доступен по адресу https://github.com/beargiles/pgopenssltypes . Эта работа в значительной степени находится в стадии разработки, но я публикую ее, поскольку эта идея была в моем блоге более года, и скоро я буду заниматься другими задачами.

Вот хорошая статья 2007 года: http://linuxgazette.net/142/peterson.html .