From 32984d8fc3dbb90a3fafb69fece0134f1ea790f9 Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 1 Oct 2014 15:56:26 +0300
Subject: [PATCH] Add functions for dealing with PGP armor header lines to
 pgcrypto.

This add a new pgp_armor_headers function to extract armor headers from an
ASCII-armored blob, and a new overloaded variant of the armor function, for
constructing an ASCII-armor with extra headers.

Marko Tiikkaja and me.
---
 .gitattributes                                |   1 +
 contrib/pgcrypto/Makefile                     |   3 +-
 contrib/pgcrypto/expected/pgp-armor.out       | 268 ++++++++++++++++++
 contrib/pgcrypto/pgcrypto--1.1--1.2.sql       |  14 +
 .../{pgcrypto--1.1.sql => pgcrypto--1.2.sql}  |  10 +
 contrib/pgcrypto/pgcrypto.control             |   2 +-
 contrib/pgcrypto/pgp-armor.c                  | 119 +++++++-
 contrib/pgcrypto/pgp-pgsql.c                  | 203 ++++++++++++-
 contrib/pgcrypto/pgp.h                        |   5 +-
 contrib/pgcrypto/sql/pgp-armor.sql            | 158 +++++++++++
 doc/src/sgml/pgcrypto.sgml                    |  28 +-
 11 files changed, 804 insertions(+), 7 deletions(-)
 create mode 100644 contrib/pgcrypto/pgcrypto--1.1--1.2.sql
 rename contrib/pgcrypto/{pgcrypto--1.1.sql => pgcrypto--1.2.sql} (94%)

diff --git a/.gitattributes b/.gitattributes
index ff96567ca54..9466800e12a 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -14,6 +14,7 @@ README.*	conflict-marker-size=32
 # Certain data files that contain special whitespace, and other special cases
 *.data						-whitespace
 contrib/tsearch2/sql/tsearch2.sql		whitespace=space-before-tab,blank-at-eof,-blank-at-eol
+contrib/pgcrypto/sql/pgp-armor.sql		whitespace=-blank-at-eol
 doc/bug.template				whitespace=space-before-tab,-blank-at-eof,blank-at-eol
 src/backend/catalog/sql_features.txt		whitespace=space-before-tab,blank-at-eof,-blank-at-eol
 src/backend/tsearch/hunspell_sample.affix	whitespace=-blank-at-eof
diff --git a/contrib/pgcrypto/Makefile b/contrib/pgcrypto/Makefile
index b6c94844b2c..18bad1a05f9 100644
--- a/contrib/pgcrypto/Makefile
+++ b/contrib/pgcrypto/Makefile
@@ -26,7 +26,8 @@ MODULE_big	= pgcrypto
 OBJS		= $(SRCS:.c=.o) $(WIN32RES)
 
 EXTENSION = pgcrypto
-DATA = pgcrypto--1.1.sql pgcrypto--1.0--1.1.sql pgcrypto--unpackaged--1.0.sql
+DATA = pgcrypto--1.2.sql pgcrypto--1.1--1.2.sql pgcrypto--1.0--1.1.sql \
+	pgcrypto--unpackaged--1.0.sql
 PGFILEDESC = "pgcrypto - cryptographic functions"
 
 REGRESS = init md5 sha1 hmac-md5 hmac-sha1 blowfish rijndael \
diff --git a/contrib/pgcrypto/expected/pgp-armor.out b/contrib/pgcrypto/expected/pgp-armor.out
index c95549412e2..89d410a7dc0 100644
--- a/contrib/pgcrypto/expected/pgp-armor.out
+++ b/contrib/pgcrypto/expected/pgp-armor.out
@@ -102,3 +102,271 @@ em9va2E=
 -----END PGP MESSAGE-----
 ');
 ERROR:  Corrupt ascii-armor
+-- corrupt (no space after the colon)
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+foo:
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+ERROR:  Corrupt ascii-armor
+-- corrupt (no empty line)
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+ERROR:  Corrupt ascii-armor
+-- no headers
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+ key | value 
+-----+-------
+(0 rows)
+
+-- header with empty value
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+foo: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+ key | value 
+-----+-------
+ foo | 
+(1 row)
+
+-- simple
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+fookey: foovalue
+barkey: barvalue
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+  key   |  value   
+--------+----------
+ fookey | foovalue
+ barkey | barvalue
+(2 rows)
+
+-- insane keys, part 1
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+insane:key : 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+     key     | value 
+-------------+-------
+ insane:key  | 
+(1 row)
+
+-- insane keys, part 2
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+insane:key : text value here
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+     key     |      value      
+-------------+-----------------
+ insane:key  | text value here
+(1 row)
+
+-- long value
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 76 characters long, but it should still parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+ key  |                                                      value                                                      
+------+-----------------------------------------------------------------------------------------------------------------
+ long | this value is more than 76 characters long, but it should still parse correctly as that's permitted by RFC 4880
+(1 row)
+
+-- long value, split up
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 76 characters long, but it should still 
+long: parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+ key  |                              value                               
+------+------------------------------------------------------------------
+ long | this value is more than 76 characters long, but it should still 
+ long | parse correctly as that's permitted by RFC 4880
+(2 rows)
+
+-- long value, split up, part 2
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 
+long: 76 characters long, but it should still 
+long: parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+ key  |                      value                      
+------+-------------------------------------------------
+ long | this value is more than 
+ long | 76 characters long, but it should still 
+ long | parse correctly as that's permitted by RFC 4880
+(3 rows)
+
+-- long value, split up, part 3
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+emptykey: 
+long: this value is more than 
+emptykey: 
+long: 76 characters long, but it should still 
+emptykey: 
+long: parse correctly as that''s permitted by RFC 4880
+emptykey: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+   key    |                      value                      
+----------+-------------------------------------------------
+ emptykey | 
+ long     | this value is more than 
+ emptykey | 
+ long     | 76 characters long, but it should still 
+ emptykey | 
+ long     | parse correctly as that's permitted by RFC 4880
+ emptykey | 
+(7 rows)
+
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+Comment: dat1.blowfish.sha1.mdc.s2k3.z0
+
+jA0EBAMCfFNwxnvodX9g0jwB4n4s26/g5VmKzVab1bX1SmwY7gvgvlWdF3jKisvS
+yA6Ce1QTMK3KdL2MPfamsTUSAML8huCJMwYQFfE=
+=JcP+
+-----END PGP MESSAGE-----
+');
+   key   |             value              
+---------+--------------------------------
+ Comment | dat1.blowfish.sha1.mdc.s2k3.z0
+(1 row)
+
+-- test CR+LF line endings
+select * from pgp_armor_headers(replace('
+-----BEGIN PGP MESSAGE-----
+fookey: foovalue
+barkey: barvalue
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', E'\n', E'\r\n'));
+  key   |  value   
+--------+----------
+ fookey | foovalue
+ barkey | barvalue
+(2 rows)
+
+-- test header generation
+select armor('zooka', array['foo'], array['bar']);
+            armor            
+-----------------------------
+ -----BEGIN PGP MESSAGE-----+
+ foo: bar                   +
+                            +
+ em9va2E=                   +
+ =D5cR                      +
+ -----END PGP MESSAGE-----  +
+ 
+(1 row)
+
+select armor('zooka', array['Version', 'Comment'], array['Created by pgcrypto', 'PostgreSQL, the world''s most advanced open source database']);
+                                armor                                
+---------------------------------------------------------------------
+ -----BEGIN PGP MESSAGE-----                                        +
+ Version: Created by pgcrypto                                       +
+ Comment: PostgreSQL, the world's most advanced open source database+
+                                                                    +
+ em9va2E=                                                           +
+ =D5cR                                                              +
+ -----END PGP MESSAGE-----                                          +
+ 
+(1 row)
+
+select * from pgp_armor_headers(
+  armor('zooka', array['Version', 'Comment'],
+                 array['Created by pgcrypto', 'PostgreSQL, the world''s most advanced open source database']));
+   key   |                           value                            
+---------+------------------------------------------------------------
+ Version | Created by pgcrypto
+ Comment | PostgreSQL, the world's most advanced open source database
+(2 rows)
+
+-- error/corner cases
+select armor('', array['foo'], array['too', 'many']);
+ERROR:  mismatched array dimensions
+select armor('', array['too', 'many'], array['foo']);
+ERROR:  mismatched array dimensions
+select armor('', array[['']], array['foo']);
+ERROR:  wrong number of array subscripts
+select armor('', array['foo'], array[['']]);
+ERROR:  wrong number of array subscripts
+select armor('', array[null], array['foo']);
+ERROR:  null value not allowed for header key
+select armor('', array['foo'], array[null]);
+ERROR:  null value not allowed for header value
+select armor('', '[0:0]={"foo"}', array['foo']);
+            armor            
+-----------------------------
+ -----BEGIN PGP MESSAGE-----+
+ foo: foo                   +
+                            +
+ =twTO                      +
+ -----END PGP MESSAGE-----  +
+ 
+(1 row)
+
+select armor('', array['foo'], '[0:0]={"foo"}');
+            armor            
+-----------------------------
+ -----BEGIN PGP MESSAGE-----+
+ foo: foo                   +
+                            +
+ =twTO                      +
+ -----END PGP MESSAGE-----  +
+ 
+(1 row)
+
+select armor('', array[E'embedded\nnewline'], array['foo']);
+ERROR:  header key must not contain newlines
+select armor('', array['foo'], array[E'embedded\nnewline']);
+ERROR:  header value must not contain newlines
+select armor('', array['embedded: colon+space'], array['foo']);
+ERROR:  header key must not contain ": "
diff --git a/contrib/pgcrypto/pgcrypto--1.1--1.2.sql b/contrib/pgcrypto/pgcrypto--1.1--1.2.sql
new file mode 100644
index 00000000000..753e1693847
--- /dev/null
+++ b/contrib/pgcrypto/pgcrypto--1.1--1.2.sql
@@ -0,0 +1,14 @@
+/* contrib/pgcrypto/pgcrypto--1.1--1.2.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pgcrypto UPDATE TO '1.2'" to load this file. \quit
+
+CREATE FUNCTION armor(bytea, text[], text[])
+RETURNS text
+AS 'MODULE_PATHNAME', 'pg_armor'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_armor_headers(text, key OUT text, value OUT text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pgp_armor_headers'
+LANGUAGE C IMMUTABLE STRICT;
diff --git a/contrib/pgcrypto/pgcrypto--1.1.sql b/contrib/pgcrypto/pgcrypto--1.2.sql
similarity index 94%
rename from contrib/pgcrypto/pgcrypto--1.1.sql
rename to contrib/pgcrypto/pgcrypto--1.2.sql
index a260857d302..370a9a19cdf 100644
--- a/contrib/pgcrypto/pgcrypto--1.1.sql
+++ b/contrib/pgcrypto/pgcrypto--1.2.sql
@@ -201,7 +201,17 @@ RETURNS text
 AS 'MODULE_PATHNAME', 'pg_armor'
 LANGUAGE C IMMUTABLE STRICT;
 
+CREATE FUNCTION armor(bytea, text[], text[])
+RETURNS text
+AS 'MODULE_PATHNAME', 'pg_armor'
+LANGUAGE C IMMUTABLE STRICT;
+
 CREATE FUNCTION dearmor(text)
 RETURNS bytea
 AS 'MODULE_PATHNAME', 'pg_dearmor'
 LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_armor_headers(text, key OUT text, value OUT text)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pgp_armor_headers'
+LANGUAGE C IMMUTABLE STRICT;
diff --git a/contrib/pgcrypto/pgcrypto.control b/contrib/pgcrypto/pgcrypto.control
index 7f79d044ab3..bb6885bc1ba 100644
--- a/contrib/pgcrypto/pgcrypto.control
+++ b/contrib/pgcrypto/pgcrypto.control
@@ -1,5 +1,5 @@
 # pgcrypto extension
 comment = 'cryptographic functions'
-default_version = '1.1'
+default_version = '1.2'
 module_pathname = '$libdir/pgcrypto'
 relocatable = true
diff --git a/contrib/pgcrypto/pgp-armor.c b/contrib/pgcrypto/pgp-armor.c
index ec647f0f3f2..24eb42fa891 100644
--- a/contrib/pgcrypto/pgp-armor.c
+++ b/contrib/pgcrypto/pgp-armor.c
@@ -178,7 +178,7 @@ b64_dec_len(unsigned srclen)
  * PGP armor
  */
 
-static const char *armor_header = "-----BEGIN PGP MESSAGE-----\n\n";
+static const char *armor_header = "-----BEGIN PGP MESSAGE-----\n";
 static const char *armor_footer = "\n-----END PGP MESSAGE-----\n";
 
 /* CRC24 implementation from rfc2440 */
@@ -204,17 +204,24 @@ crc24(const uint8 *data, unsigned len)
 }
 
 void
-pgp_armor_encode(const uint8 *src, int len, StringInfo dst)
+pgp_armor_encode(const uint8 *src, unsigned len, StringInfo dst,
+				 int num_headers, char **keys, char **values)
 {
+	int			n;
 	int			res;
 	unsigned	b64len;
 	unsigned	crc = crc24(src, len);
 
 	appendStringInfoString(dst, armor_header);
 
+	for (n = 0; n < num_headers; n++)
+		appendStringInfo(dst, "%s: %s\n", keys[n], values[n]);
+	appendStringInfoChar(dst, '\n');
+
 	/* make sure we have enough room to b64_encode() */
 	b64len = b64_enc_len(len);
 	enlargeStringInfo(dst, (int) b64len);
+
 	res = b64_encode(src, len, (uint8 *) dst->data + dst->len);
 	if (res > b64len)
 		elog(FATAL, "overflow - encode estimate too small");
@@ -371,3 +378,111 @@ pgp_armor_decode(const uint8 *src, int len, StringInfo dst)
 out:
 	return res;
 }
+
+/*
+ * Extracts all armor headers from an ASCII-armored input.
+ *
+ * Returns 0 on success, or PXE_* error code on error. On success, the
+ * number of headers and their keys and values are returned in *nheaders,
+ * *nkeys and *nvalues.
+ */
+int
+pgp_extract_armor_headers(const uint8 *src, unsigned len,
+						  int *nheaders, char ***keys, char ***values)
+{
+	const uint8 *data_end = src + len;
+	const uint8 *p;
+	const uint8 *base64_start;
+	const uint8 *armor_start;
+	const uint8 *armor_end;
+	Size		armor_len;
+	char	   *line;
+	char	   *nextline;
+	char	   *eol,
+				*colon;
+	int			hlen;
+	char	   *buf;
+	int			hdrlines;
+	int			n;
+
+	/* armor start */
+	hlen = find_header(src, data_end, &armor_start, 0);
+	if (hlen <= 0)
+		return PXE_PGP_CORRUPT_ARMOR;
+	armor_start += hlen;
+
+	/* armor end */
+	hlen = find_header(armor_start, data_end, &armor_end, 1);
+	if (hlen <= 0)
+		return PXE_PGP_CORRUPT_ARMOR;
+
+	/* Count the number of armor header lines. */
+	hdrlines = 0;
+	p = armor_start;
+	while (p < armor_end && *p != '\n' && *p != '\r')
+	{
+		p = memchr(p, '\n', armor_end - p);
+		if (!p)
+			return PXE_PGP_CORRUPT_ARMOR;
+
+		/* step to start of next line */
+		p++;
+		hdrlines++;
+	}
+	base64_start = p;
+
+	/*
+	 * Make a modifiable copy of the part of the input that contains the
+	 * headers. The returned key/value pointers will point inside the buffer.
+	 */
+	armor_len = base64_start - armor_start;
+	buf = palloc(armor_len + 1);
+	memcpy(buf, armor_start, armor_len);
+	buf[armor_len] = '\0';
+
+	/* Allocate return arrays */
+	*keys = (char **) palloc(hdrlines * sizeof(char *));
+	*values = (char **) palloc(hdrlines * sizeof(char *));
+
+	/*
+	 * Split the header lines at newlines and ": " separators, and collect
+	 * pointers to the keys and values in the return arrays.
+	 */
+	n = 0;
+	line = buf;
+	for (;;)
+	{
+		/* find end of line */
+		eol = strchr(line, '\n');
+		if (!eol)
+			break;
+		nextline = eol + 1;
+		/* if the line ends in CR + LF, strip the CR */
+		if (eol > line && *(eol - 1) == '\r')
+			eol--;
+		*eol = '\0';
+
+		/* find colon+space separating the key and value */
+		colon = strstr(line, ": ");
+		if (!colon)
+			return PXE_PGP_CORRUPT_ARMOR;
+		*colon = '\0';
+
+		/* shouldn't happen, we counted the number of lines beforehand */
+		if (n >= hdrlines)
+			elog(ERROR, "unexpected number of armor header lines");
+
+		(*keys)[n] = line;
+		(*values)[n] = colon + 2;
+		n++;
+
+		/* step to start of next line */
+		line = nextline;
+	}
+
+	if (n != hdrlines)
+		elog(ERROR, "unexpected number of armor header lines");
+
+	*nheaders = n;
+	return 0;
+}
diff --git a/contrib/pgcrypto/pgp-pgsql.c b/contrib/pgcrypto/pgp-pgsql.c
index 5d2d4655d18..20708731403 100644
--- a/contrib/pgcrypto/pgp-pgsql.c
+++ b/contrib/pgcrypto/pgp-pgsql.c
@@ -32,8 +32,11 @@
 #include "postgres.h"
 
 #include "lib/stringinfo.h"
+#include "catalog/pg_type.h"
 #include "mb/pg_wchar.h"
 #include "utils/builtins.h"
+#include "utils/array.h"
+#include "funcapi.h"
 
 #include "mbuf.h"
 #include "px.h"
@@ -56,6 +59,7 @@ PG_FUNCTION_INFO_V1(pgp_key_id_w);
 
 PG_FUNCTION_INFO_V1(pg_armor);
 PG_FUNCTION_INFO_V1(pg_dearmor);
+PG_FUNCTION_INFO_V1(pgp_armor_headers);
 
 /*
  * Mix a block of data into RNG.
@@ -148,6 +152,19 @@ convert_to_utf8(text *src)
 	return convert_charset(src, GetDatabaseEncoding(), PG_UTF8);
 }
 
+static bool
+string_is_ascii(const char *str)
+{
+	const char *p;
+
+	for (p = str; *p; p++)
+	{
+		if (IS_HIGHBIT_SET(*p))
+			return false;
+	}
+	return true;
+}
+
 static void
 clear_and_pfree(text *p)
 {
@@ -816,6 +833,100 @@ pgp_pub_decrypt_text(PG_FUNCTION_ARGS)
  * Wrappers for PGP ascii armor
  */
 
+/*
+ * Helper function for pgp_armor. Converts arrays of keys and values into
+ * plain C arrays, and checks that they don't contain invalid characters.
+ */
+static int
+parse_key_value_arrays(ArrayType *key_array, ArrayType *val_array,
+					   char ***p_keys, char ***p_values)
+{
+	int		nkdims = ARR_NDIM(key_array);
+	int		nvdims = ARR_NDIM(val_array);
+	char   **keys,
+		   **values;
+	Datum  *key_datums,
+		   *val_datums;
+	bool   *key_nulls,
+		   *val_nulls;
+	int		key_count,
+			val_count;
+	int		i;
+
+	if (nkdims > 1 || nkdims != nvdims)
+		ereport(ERROR,
+				(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+				errmsg("wrong number of array subscripts")));
+	if (nkdims == 0)
+		return 0;
+
+	deconstruct_array(key_array,
+					  TEXTOID, -1, false, 'i',
+					  &key_datums, &key_nulls, &key_count);
+
+	deconstruct_array(val_array,
+					  TEXTOID, -1, false, 'i',
+					  &val_datums, &val_nulls, &val_count);
+
+	if (key_count != val_count)
+		ereport(ERROR,
+				(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+				 errmsg("mismatched array dimensions")));
+
+	keys = (char **) palloc(sizeof(char *) * key_count);
+	values = (char **) palloc(sizeof(char *) * val_count);
+
+	for (i = 0; i < key_count; i++)
+	{
+		char *v;
+
+		/* Check that the key doesn't contain anything funny */
+		if (key_nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+					 errmsg("null value not allowed for header key")));
+
+		v = TextDatumGetCString(key_datums[i]);
+
+		if (!string_is_ascii(v))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("header key must not contain non-ASCII characters")));
+		if (strstr(v, ": "))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("header key must not contain \": \"")));
+		if (strchr(v, '\n'))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("header key must not contain newlines")));
+		keys[i] = v;
+
+		/* And the same for the value */
+		if (val_nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+					 errmsg("null value not allowed for header value")));
+
+		v = TextDatumGetCString(val_datums[i]);
+
+		if (!string_is_ascii(v))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("header value must not contain non-ASCII characters")));
+		if (strchr(v, '\n'))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("header value must not contain newlines")));
+
+		values[i] = v;
+	}
+
+	*p_keys = keys;
+	*p_values = values;
+	return key_count;
+}
+
 Datum
 pg_armor(PG_FUNCTION_ARGS)
 {
@@ -823,13 +934,27 @@ pg_armor(PG_FUNCTION_ARGS)
 	text	   *res;
 	int			data_len;
 	StringInfoData buf;
+	int			num_headers;
+	char	  **keys = NULL,
+			  **values = NULL;
 
 	data = PG_GETARG_BYTEA_P(0);
 	data_len = VARSIZE(data) - VARHDRSZ;
+	if (PG_NARGS() == 3)
+	{
+		num_headers = parse_key_value_arrays(PG_GETARG_ARRAYTYPE_P(1),
+											 PG_GETARG_ARRAYTYPE_P(2),
+											 &keys, &values);
+	}
+	else if (PG_NARGS() == 1)
+		num_headers = 0;
+	else
+		elog(ERROR, "unexpected number of arguments %d", PG_NARGS());
 
 	initStringInfo(&buf);
 
-	pgp_armor_encode((uint8 *) VARDATA(data), data_len, &buf);
+	pgp_armor_encode((uint8 *) VARDATA(data), data_len, &buf,
+					 num_headers, keys, values);
 
 	res = palloc(VARHDRSZ + buf.len);
 	SET_VARSIZE(res, VARHDRSZ + buf.len);
@@ -868,6 +993,82 @@ pg_dearmor(PG_FUNCTION_ARGS)
 	PG_RETURN_TEXT_P(res);
 }
 
+/* cross-call state for pgp_armor_headers */
+typedef struct
+{
+	int			nheaders;
+	char	  **keys;
+	char	  **values;
+} pgp_armor_headers_state;
+
+Datum
+pgp_armor_headers(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	pgp_armor_headers_state *state;
+	char	   *utf8key;
+	char	   *utf8val;
+	HeapTuple	tuple;
+	TupleDesc	tupdesc;
+	AttInMetadata *attinmeta;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		text	   *data = PG_GETARG_TEXT_PP(0);
+		int			res;
+		MemoryContext oldcontext;
+
+		funcctx = SRF_FIRSTCALL_INIT();
+
+		/* we need the state allocated in the multi call context */
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		/* Build a tuple descriptor for our result type */
+		if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+			elog(ERROR, "return type must be a row type");
+
+		attinmeta = TupleDescGetAttInMetadata(tupdesc);
+		funcctx->attinmeta = attinmeta;
+
+		state = (pgp_armor_headers_state *) palloc(sizeof(pgp_armor_headers_state));
+
+		res = pgp_extract_armor_headers((uint8 *) VARDATA_ANY(data),
+										VARSIZE_ANY_EXHDR(data),
+										&state->nheaders, &state->keys,
+										&state->values);
+		if (res < 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_EXTERNAL_ROUTINE_INVOCATION_EXCEPTION),
+					 errmsg("%s", px_strerror(res))));
+
+		MemoryContextSwitchTo(oldcontext);
+		funcctx->user_fctx = state;
+	}
+
+	funcctx = SRF_PERCALL_SETUP();
+	state = (pgp_armor_headers_state *) funcctx->user_fctx;
+
+	if (funcctx->call_cntr >= state->nheaders)
+		SRF_RETURN_DONE(funcctx);
+	else
+	{
+		char	  *values[2];
+
+		/* we assume that the keys (and values) are in UTF-8. */
+		utf8key = state->keys[funcctx->call_cntr];
+		utf8val = state->values[funcctx->call_cntr];
+
+		values[0] = pg_any_to_server(utf8key, strlen(utf8key), PG_UTF8);
+		values[1] = pg_any_to_server(utf8val, strlen(utf8val), PG_UTF8);
+
+		/* build a tuple */
+		tuple = BuildTupleFromCStrings(funcctx->attinmeta, values);
+		SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple));
+	}
+}
+
+
+
 /*
  * Wrappers for PGP key id
  */
diff --git a/contrib/pgcrypto/pgp.h b/contrib/pgcrypto/pgp.h
index cecd1814956..398f21bca2e 100644
--- a/contrib/pgcrypto/pgp.h
+++ b/contrib/pgcrypto/pgp.h
@@ -276,8 +276,11 @@ void		pgp_cfb_free(PGP_CFB *ctx);
 int			pgp_cfb_encrypt(PGP_CFB *ctx, const uint8 *data, int len, uint8 *dst);
 int			pgp_cfb_decrypt(PGP_CFB *ctx, const uint8 *data, int len, uint8 *dst);
 
-void		pgp_armor_encode(const uint8 *src, int len, StringInfo dst);
+void		pgp_armor_encode(const uint8 *src, unsigned len, StringInfo dst,
+							 int num_headers, char **keys, char **values);
 int			pgp_armor_decode(const uint8 *src, int len, StringInfo dst);
+int			pgp_extract_armor_headers(const uint8 *src, unsigned len,
+									  int *nheaders, char ***keys, char ***values);
 
 int			pgp_compress_filter(PushFilter **res, PGP_Context *ctx, PushFilter *dst);
 int			pgp_decompress_filter(PullFilter **res, PGP_Context *ctx, PullFilter *src);
diff --git a/contrib/pgcrypto/sql/pgp-armor.sql b/contrib/pgcrypto/sql/pgp-armor.sql
index 71ffba26a0a..a277a1894c4 100644
--- a/contrib/pgcrypto/sql/pgp-armor.sql
+++ b/contrib/pgcrypto/sql/pgp-armor.sql
@@ -56,3 +56,161 @@ em9va2E=
 =ZZZZ
 -----END PGP MESSAGE-----
 ');
+
+-- corrupt (no space after the colon)
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+foo:
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- corrupt (no empty line)
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- no headers
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- header with empty value
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+foo: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- simple
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+fookey: foovalue
+barkey: barvalue
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- insane keys, part 1
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+insane:key : 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- insane keys, part 2
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+insane:key : text value here
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- long value
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 76 characters long, but it should still parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- long value, split up
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 76 characters long, but it should still 
+long: parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- long value, split up, part 2
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 
+long: 76 characters long, but it should still 
+long: parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- long value, split up, part 3
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+emptykey: 
+long: this value is more than 
+emptykey: 
+long: 76 characters long, but it should still 
+emptykey: 
+long: parse correctly as that''s permitted by RFC 4880
+emptykey: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+select * from pgp_armor_headers('
+-----BEGIN PGP MESSAGE-----
+Comment: dat1.blowfish.sha1.mdc.s2k3.z0
+
+jA0EBAMCfFNwxnvodX9g0jwB4n4s26/g5VmKzVab1bX1SmwY7gvgvlWdF3jKisvS
+yA6Ce1QTMK3KdL2MPfamsTUSAML8huCJMwYQFfE=
+=JcP+
+-----END PGP MESSAGE-----
+');
+
+-- test CR+LF line endings
+select * from pgp_armor_headers(replace('
+-----BEGIN PGP MESSAGE-----
+fookey: foovalue
+barkey: barvalue
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', E'\n', E'\r\n'));
+
+-- test header generation
+select armor('zooka', array['foo'], array['bar']);
+select armor('zooka', array['Version', 'Comment'], array['Created by pgcrypto', 'PostgreSQL, the world''s most advanced open source database']);
+select * from pgp_armor_headers(
+  armor('zooka', array['Version', 'Comment'],
+                 array['Created by pgcrypto', 'PostgreSQL, the world''s most advanced open source database']));
+
+-- error/corner cases
+select armor('', array['foo'], array['too', 'many']);
+select armor('', array['too', 'many'], array['foo']);
+select armor('', array[['']], array['foo']);
+select armor('', array['foo'], array[['']]);
+select armor('', array[null], array['foo']);
+select armor('', array['foo'], array[null]);
+select armor('', '[0:0]={"foo"}', array['foo']);
+select armor('', array['foo'], '[0:0]={"foo"}');
+select armor('', array[E'embedded\nnewline'], array['foo']);
+select armor('', array['foo'], array[E'embedded\nnewline']);
+select armor('', array['embedded: colon+space'], array['foo']);
diff --git a/doc/src/sgml/pgcrypto.sgml b/doc/src/sgml/pgcrypto.sgml
index 544a1f8346a..f0928f80fe2 100644
--- a/doc/src/sgml/pgcrypto.sgml
+++ b/doc/src/sgml/pgcrypto.sgml
@@ -691,13 +691,39 @@ pgp_key_id(bytea) returns text
    </indexterm>
 
 <synopsis>
-armor(data bytea) returns text
+armor(data bytea [ , keys text[], values text[] ]) returns text
 dearmor(data text) returns bytea
 </synopsis>
    <para>
     These functions wrap/unwrap binary data into PGP ASCII-armor format,
     which is basically Base64 with CRC and additional formatting.
    </para>
+
+   <para>
+    If the <parameter>keys</> and <parameter>values</> arrays are specified,
+    an <firstterm>armor header</> is added to the armored format for each
+    key/value pair. Both arrays must be single-dimensional, and they must
+    be of the same length.  The keys and values cannot contain any non-ASCII
+    characters.
+   </para>
+  </sect3>
+
+  <sect3>
+   <title><function>pgp_armor_headers</function></title>
+
+   <indexterm>
+    <primary>pgp_armor_headers</primary>
+   </indexterm>
+
+<synopsis>
+pgp_armor_headers(data text, key out text, value out text) returns setof record
+</synopsis>
+   <para>
+    <function>pgp_armor_headers()</> extracts the armor headers from
+    <parameter>data</>.  The return value is a set of rows with two columns,
+    key and value.  If the keys or values contain any non-ASCII characters,
+    they are treated as UTF-8.
+   </para>
   </sect3>
 
   <sect3>
-- 
GitLab