Skip to content

Commit

Permalink
security invoker support for dynamic views pg 15+ (#82)
Browse files Browse the repository at this point in the history
* security invoker support for dynamic views pg 15+

* only fire security invoker tests on pg15

* quiet test runner.

* convert tests to use pg_prove
  • Loading branch information
michelp authored May 29, 2023
1 parent c443019 commit b84ee30
Show file tree
Hide file tree
Showing 28 changed files with 296 additions and 181 deletions.
6 changes: 5 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ RUN chown postgres:postgres /home/postgres

RUN curl -s -L https://github.com/theory/pgtap/archive/v1.2.0.tar.gz | tar zxvf - && cd pgtap-1.2.0 && make && make install
RUN curl -s -L https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz | tar zxvf - && cd libsodium-1.0.18 && ./configure && make check && make -j 4 install
RUN cpan App::cpanminus && cpan TAP::Parser::SourceHandler::pgTAP
RUN cpan App::cpanminus && cpan TAP::Parser::SourceHandler::pgTAP && cpan App::prove

RUN git clone --depth 1 https://github.com/lacanoid/pgddl.git
RUN cd pgddl && make && make install && cd ..

RUN mkdir "/home/postgres/pgsodium"
WORKDIR "/home/postgres/pgsodium"
COPY . .
Expand Down
4 changes: 2 additions & 2 deletions META.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "pgsodium",
"abstract": "Postgres extension for libsodium functions",
"description": "pgsodium is a PostgreSQL extension that exposes modern libsodium based cryptographic functions to SQL.",
"version": "3.1.6",
"version": "3.1.7",
"maintainer": [
"Michel Pelletier <[email protected]>"
],
Expand All @@ -13,7 +13,7 @@
"abstract": "Postgres extension for libsodium functions",
"file": "src/pgsodium.h",
"docfile": "README.md",
"version": "3.1.6"
"version": "3.1.7"
}
},
"prereqs": {
Expand Down
2 changes: 1 addition & 1 deletion example/tce.sql
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,6 @@ CREATE TABLE "tce-example"."bob-testt" (
);

SECURITY LABEL FOR pgsodium ON COLUMN "bob-testt"."secret2-test" IS
'ENCRYPT WITH KEY COLUMN secret2_key_id-test ASSOCIATED (associated2-test) NONCE nonce2-test';
'ENCRYPT WITH KEY COLUMN secret2_key_id-test ASSOCIATED (associated2-test) NONCE nonce2-test SECURITY INVOKER';

select pgsodium.update_masks(true);
2 changes: 1 addition & 1 deletion pgsodium.control
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# pgsodium extension
comment = 'Postgres extension for libsodium functions'
default_version = '3.1.6'
default_version = '3.1.7'
relocatable = false
schema = pgsodium
181 changes: 181 additions & 0 deletions sql/pgsodium--3.1.6--3.1.7.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
CREATE OR REPLACE VIEW pgsodium.masking_rule AS
WITH const AS (
SELECT
'encrypt +with +key +id +([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'
AS pattern_key_id,
'encrypt +with +key +column +([\w\"\-$]+)'
AS pattern_key_id_column,
'(?<=associated) +\(([\w\"\-$, ]+)\)'
AS pattern_associated_columns,
'(?<=nonce) +([\w\"\-$]+)'
AS pattern_nonce_column,
'(?<=decrypt with view) +([\w\"\-$]+\.[\w\"\-$]+)'
AS pattern_view_name,
'(?<=security invoker)'
AS pattern_security_invoker
),
rules_from_seclabels AS (
SELECT
sl.objoid AS attrelid,
sl.objsubid AS attnum,
c.relnamespace::regnamespace,
c.relname,
a.attname,
pg_catalog.format_type(a.atttypid, a.atttypmod),
sl.label AS col_description,
(regexp_match(sl.label, k.pattern_key_id_column, 'i'))[1] AS key_id_column,
(regexp_match(sl.label, k.pattern_key_id, 'i'))[1] AS key_id,
(regexp_match(sl.label, k.pattern_associated_columns, 'i'))[1] AS associated_columns,
(regexp_match(sl.label, k.pattern_nonce_column, 'i'))[1] AS nonce_column,
coalesce((regexp_match(sl2.label, k.pattern_view_name, 'i'))[1],
c.relnamespace::regnamespace || '.' || quote_ident('decrypted_' || c.relname)) AS view_name,
100 AS priority,
(regexp_match(sl.label, k.pattern_security_invoker, 'i'))[1] IS NOT NULL AS security_invoker
FROM const k,
pg_catalog.pg_seclabel sl
JOIN pg_catalog.pg_class c ON sl.classoid = c.tableoid AND sl.objoid = c.oid
JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid AND sl.objsubid = a.attnum
LEFT JOIN pg_catalog.pg_seclabel sl2 ON sl2.objoid = c.oid AND sl2.objsubid = 0
WHERE a.attnum > 0
AND c.relnamespace::regnamespace != 'pg_catalog'::regnamespace
AND NOT a.attisdropped
AND sl.label ilike 'ENCRYPT%'
AND sl.provider = 'pgsodium'
)
SELECT
DISTINCT ON (attrelid, attnum) *
FROM rules_from_seclabels
ORDER BY attrelid, attnum, priority DESC;

CREATE OR REPLACE FUNCTION pgsodium.mask_role(masked_role regrole, source_name text, view_name text)
RETURNS void AS
$$
BEGIN
EXECUTE format(
'GRANT SELECT ON pgsodium.key TO %s',
masked_role);

EXECUTE format(
'GRANT pgsodium_keyiduser, pgsodium_keyholder TO %s',
masked_role);

EXECUTE format(
'GRANT ALL ON %s TO %s',
view_name,
masked_role);
RETURN;
END
$$
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path='pg_catalog'
;

CREATE OR REPLACE FUNCTION pgsodium.create_mask_view(relid oid, subid integer, debug boolean = false)
RETURNS void AS
$$
DECLARE
m record;
body text;
source_name text;
view_owner regrole = session_user;
rule pgsodium.masking_rule;
privs aclitem[];
priv record;
BEGIN
SELECT DISTINCT * INTO STRICT rule FROM pgsodium.masking_rule WHERE attrelid = relid AND attnum = subid;

source_name := relid::regclass::text;

BEGIN
SELECT relacl INTO STRICT privs FROM pg_catalog.pg_class WHERE oid = rule.view_name::regclass::oid;
EXCEPTION
WHEN undefined_table THEN
SELECT relacl INTO STRICT privs FROM pg_catalog.pg_class WHERE oid = relid;
END;

body = format(
$c$
DROP VIEW IF EXISTS %1$s;
CREATE VIEW %1$s %5$s AS SELECT %2$s
FROM %3$s;
ALTER VIEW %1$s OWNER TO %4$s;
$c$,
rule.view_name,
pgsodium.decrypted_columns(relid),
source_name,
view_owner,
CASE WHEN rule.security_invoker THEN 'WITH (security_invoker=true)' ELSE '' END
);
IF debug THEN
RAISE NOTICE '%', body;
END IF;
EXECUTE body;

FOR priv IN SELECT * FROM pg_catalog.aclexplode(privs) LOOP
body = format(
$c$
GRANT %s ON %s TO %s;
$c$,
priv.privilege_type,
rule.view_name,
priv.grantee::regrole::text
);
IF debug THEN
RAISE NOTICE '%', body;
END IF;
EXECUTE body;
END LOOP;

FOR m IN SELECT * FROM pgsodium.mask_columns where attrelid = relid LOOP
IF m.key_id IS NULL AND m.key_id_column is NULL THEN
CONTINUE;
ELSE
body = format(
$c$
DROP FUNCTION IF EXISTS %1$s."%2$s_encrypt_secret_%3$s"() CASCADE;

CREATE OR REPLACE FUNCTION %1$s."%2$s_encrypt_secret_%3$s"()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $t$
BEGIN
%4$s;
RETURN new;
END;
$t$;

ALTER FUNCTION %1$s."%2$s_encrypt_secret_%3$s"() OWNER TO %5$s;

DROP TRIGGER IF EXISTS "%2$s_encrypt_secret_trigger_%3$s" ON %6$s;

CREATE TRIGGER "%2$s_encrypt_secret_trigger_%3$s"
BEFORE INSERT OR UPDATE OF "%3$s" ON %6$s
FOR EACH ROW
EXECUTE FUNCTION %1$s."%2$s_encrypt_secret_%3$s" ();
$c$,
rule.relnamespace,
rule.relname,
m.attname,
pgsodium.encrypted_column(relid, m),
view_owner,
source_name
);
if debug THEN
RAISE NOTICE '%', body;
END IF;
EXECUTE body;
END IF;
END LOOP;

raise notice 'about to masking role % %', source_name, rule.view_name;
PERFORM pgsodium.mask_role(oid::regrole, source_name, rule.view_name)
FROM pg_roles WHERE pgsodium.has_mask(oid::regrole, source_name);

RETURN;
END
$$
LANGUAGE plpgsql
VOLATILE
SET search_path='pg_catalog'
;
2 changes: 1 addition & 1 deletion test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ do
sleep 3;
echo running tests

$EXEC psql -q -U "$SU" -f /home/postgres/pgsodium/test/test.sql
$EXEC pg_prove -U "$SU" /home/postgres/pgsodium/test/test.sql

echo destroying test container and image
docker rm --force "$DB_HOST"
Expand Down
9 changes: 0 additions & 9 deletions test/aead.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
BEGIN;
SELECT plan(14);

SELECT crypto_aead_ietf_keygen() aeadkey \gset
SELECT crypto_aead_ietf_noncegen() aeadnonce \gset
Expand Down Expand Up @@ -60,12 +58,7 @@ SELECT crypto_aead_det_encrypt(
SELECT is(crypto_aead_det_decrypt(:'detaead2', NULL, :'detkey'::bytea),
'bob is your uncle', 'crypto_aead_det_decrypt with NULL associated');

SELECT * FROM finish();
ROLLBACK;

\if :serverkeys
BEGIN;
SELECT plan(10);
SET ROLE pgsodium_keyiduser;

SELECT crypto_aead_ietf_encrypt(
Expand Down Expand Up @@ -121,6 +114,4 @@ SELECT throws_ok(format($$select crypto_aead_ietf_decrypt('bob is your uncle', '
'P0002', 'query returned no rows', 'crypto_aead_ietf_decrypt invalid uuid');

RESET ROLE;
SELECT * FROM finish();
ROLLBACK;
\endif
9 changes: 0 additions & 9 deletions test/auth.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
BEGIN;
SELECT plan(9);

SELECT crypto_auth_keygen() authkey \gset

Expand Down Expand Up @@ -32,14 +30,9 @@ SELECT throws_ok($$select crypto_auth_verify('sig', NULL, 'bad_key'::bytea)$$,
SELECT throws_ok($$select crypto_auth_verify('sig', 'bob is your uncle', NULL::bytea)$$,
'22000', 'pgsodium_crypto_auth_verify: key cannot be NULL', 'crypto_auth_verify null key');

SELECT * FROM finish();
ROLLBACK;

\if :serverkeys

BEGIN;
SELECT plan(5);

SELECT crypto_auth('bob is your uncle', 1) auth_mac_by_id \gset

SELECT throws_ok($$select crypto_auth(NULL, 1::bigint)$$,
Expand All @@ -60,6 +53,4 @@ SELECT crypto_auth('bobo is your monkey', :'auth_key_id'::uuid) auth_mac_by_uuid
SELECT ok(crypto_auth_verify(:'auth_mac_by_uuid', 'bobo is your monkey', :'auth_key_id'::uuid),
'crypto_auth_verify by uuid');

SELECT * FROM finish();
ROLLBACK;
\endif
4 changes: 0 additions & 4 deletions test/box.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
BEGIN;
SELECT plan(18);

SELECT crypto_box_noncegen() boxnonce \gset
select crypto_box_new_seed() boxseed \gset
Expand Down Expand Up @@ -64,5 +62,3 @@ SELECT throws_ok(format($$select crypto_box_seal_open(%L, %L, 'bad_key')$$, :'se
SELECT throws_ok(format($$select crypto_box_seal_open('foo', %L, %L)$$, :'bob_public', :'bob_secret'),
'22000', 'pgsodium_crypto_box_seal_open: invalid message', 'crypto_box_seal_open invalid message');

SELECT * FROM finish();
ROLLBACK;
5 changes: 0 additions & 5 deletions test/derive.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
\if :serverkeys
BEGIN;
SELECT plan(7);

select is(derive_key(1), derive_key(1), 'derived key are equal by id');
select throws_ok($$select derive_key(NULL)$$, '22000', 'pgsodium_derive: key id cannot be NULL', 'null key id');
Expand All @@ -9,8 +7,5 @@ select throws_ok($$select derive_key(1, 64, NULL)$$, '22000', 'pgsodium_derive:
select isnt(derive_key(1), derive_key(2), 'disequal derived key');
select is(length(derive_key(2, 64)), 64, 'key len is 64 bytes');
select isnt(derive_key(2, 32, 'foozball'), derive_key(2, 32), 'disequal context');
SELECT * FROM finish();

SELECT * FROM finish();
ROLLBACK;
\endif
10 changes: 0 additions & 10 deletions test/hash.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
BEGIN;
SELECT plan(4);

SELECT crypto_generichash_keygen() generickey \gset

Expand All @@ -17,12 +15,7 @@ SELECT lives_ok(format($$select crypto_shorthash('bob is your uncle', %L::bytea)
SELECT throws_ok($$select crypto_shorthash('bob is your uncle', 's'::bytea)$$,
'22000', 'pgsodium_crypto_shorthash: invalid key', 'crypto_shorthash invalid key');

SELECT * FROM finish();
ROLLBACK;

\if :serverkeys
BEGIN;
SELECT plan(4);

SELECT lives_ok(format($$select crypto_shorthash('bob is your uncle', 42)$$), 'crypto_shorthash_by_id');
SELECT lives_ok(format($$select crypto_shorthash('bob is your uncle', 42, '12345678')$$), 'crypto_shorthash by id context');
Expand All @@ -33,7 +26,4 @@ SELECT lives_ok(format($$select crypto_shorthash('bob is your uncle', %L::uuid)$
select id as generichash_key_id from create_key('generichash') \gset
SELECT lives_ok(format($$select crypto_generichash('bob is your uncle', %L::uuid)$$, :'generichash_key_id'), 'crypto_generichash by uuid');

SELECT * FROM finish();
ROLLBACK;

\endif
4 changes: 0 additions & 4 deletions test/helpers.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
BEGIN;
SELECT plan(3);

select sodium_bin2base64('bob is your uncle') basebob \gset
select throws_ok('select sodium_bin2base64(NULL)',
Expand All @@ -10,5 +8,3 @@ select is(sodium_base642bin(:'basebob'), 'bob is your uncle'::bytea, 'base64');
select throws_ok('select sodium_base642bin(NULL)',
'22000', 'pgsodium_sodium_base642bin: base64 cannot be NULL', 'sodium_base642bin null input');

SELECT * FROM finish();
ROLLBACK;
10 changes: 0 additions & 10 deletions test/hmac.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
BEGIN;
SELECT plan(14);

select crypto_auth_hmacsha512_keygen() hmac512key \gset
select crypto_auth_hmacsha512('food', :'hmac512key'::bytea) hmac512 \gset
Expand Down Expand Up @@ -37,14 +35,8 @@ select throws_ok($$select crypto_auth_hmacsha256_verify('bad', NULL::bytea, 'bad
select throws_ok($$select crypto_auth_hmacsha256_verify('bad', 'bad', NULL::bytea)$$, '22000',
'pgsodium_crypto_auth_hmacsha256_verify: key cannot be NULL', 'hmac256_verify null key');

SELECT * FROM finish();
ROLLBACK;

\if :serverkeys

BEGIN;
SELECT plan(22);

select crypto_auth_hmacsha512('food', 42) hmac512 \gset

select throws_ok($$select crypto_auth_hmacsha512(NULL::bytea, 1)$$, '22000',
Expand Down Expand Up @@ -103,6 +95,4 @@ select crypto_auth_hmacsha256('food', :'extkey256_id'::uuid) hmac256 \gset
select is(crypto_auth_hmacsha256_verify(:'hmac256', 'food', :'extkey256_id'::uuid), true, 'external hmac256 verified');
select is(crypto_auth_hmacsha256_verify(:'hmac256', 'fo0d', :'extkey256_id'::uuid), false, 'external hmac256 not verified');

SELECT * FROM finish();
ROLLBACK;
\endif
Loading

0 comments on commit b84ee30

Please sign in to comment.