From f2a5878212d8bbc271085e4f6facf4cc7e6e821e Mon Sep 17 00:00:00 2001 From: Soatok Dreamseeker Date: Fri, 30 Jan 2026 09:50:07 -0500 Subject: [PATCH] Add support for age1pq public keys --- src/AuxDataTypes/AgeV1.php | 32 ++++++-- tests/AuxDataTypes/AgeV1Test.php | 128 +++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 8 deletions(-) diff --git a/src/AuxDataTypes/AgeV1.php b/src/AuxDataTypes/AgeV1.php index 98cd598..e2f2c2d 100644 --- a/src/AuxDataTypes/AgeV1.php +++ b/src/AuxDataTypes/AgeV1.php @@ -8,8 +8,16 @@ class AgeV1 implements ExtensionInterface { public const AUX_DATA_TYPE = 'age-v1'; - private const KEY_PREFIX = 'age1'; - private const KEY_LENGTH = 62; + + // Classic X25519 keys: 32-byte public key + private const KEY_PREFIX_CLASSIC = 'age1'; + private const KEY_LENGTH_CLASSIC = 62; + + // Post-quantum hybrid keys (MLKEM768-X25519): 1184 + 32 = 1216 byte public key + // HRP "age1pq" (6) + separator "1" (1) + data (1946) + checksum (6) = 1959 + private const KEY_PREFIX_PQ = 'age1pq1'; + private const KEY_LENGTH_PQ = 1959; + private string $lastRejection = ''; #[Override] @@ -42,15 +50,23 @@ public function isValid(string $auxData): bool return false; } - // Check prefix and basic format - if (!str_starts_with($auxData, self::KEY_PREFIX)) { + // Determine key type and validate accordingly + // Check post-quantum prefix first (it's longer and starts with 'age1') + if (str_starts_with($auxData, self::KEY_PREFIX_PQ)) { + if (strlen($auxData) !== self::KEY_LENGTH_PQ) { + $this->lastRejection = 'Incorrect post-quantum key length'; + return false; + } + } elseif (str_starts_with($auxData, self::KEY_PREFIX_CLASSIC)) { + if (strlen($auxData) !== self::KEY_LENGTH_CLASSIC) { + $this->lastRejection = 'Incorrect key length'; + return false; + } + } else { $this->lastRejection = 'Header is incorrect'; return false; } - if (strlen($auxData) !== self::KEY_LENGTH) { - $this->lastRejection = 'Incorrect key length'; - return false; - } + $decoded = $this->bech32Decode($auxData); return $decoded !== false; } diff --git a/tests/AuxDataTypes/AgeV1Test.php b/tests/AuxDataTypes/AgeV1Test.php index 2a56b07..93f3f3f 100644 --- a/tests/AuxDataTypes/AgeV1Test.php +++ b/tests/AuxDataTypes/AgeV1Test.php @@ -109,4 +109,132 @@ public function testEmptyStringIsRejected(): void { $this->assertFalse($this->validator->isValid('')); } + + // Post-quantum (MLKEM768-X25519) key tests + + public function testValidPostQuantumKey(): void + { + // Valid age1pq1 key generated with age-keygen -pq + $key = 'age1pq1dl3g5yxr5ur9yrmk4yswdjf7cvee2a7v3kh6qwaxsu4pvjav29vq56vz' + . 'nmwzps8eus8mdvahxr7zkch8r7xezx0l0vu99ypac73pp60t4e6fgxyp0dv7d3a' + . '6c3sqk2ynrpp98f7q03heaed2vfyx98c40h4wpfgdvsxmyy54n5ykk0svhstek4' + . 'sqcdpnwy9syxzczscz2642qmayq5ve6dremm4229tmwz7658yxmjwkva5y4s25r' + . '9gqehv7032qwenveemh9jk2sr8vp2yjtw90twpfefx244x8q369z8htnwhpk6nv' + . 'v22f8hfst9jtpaxak0pa4sw5a9e5z7jerhlmy0a90xpgy4kg3euxl6ptsanvks0' + . 'xs6lx026dz33jtnnc36x3j8awjcwtugp7x2t400af335j8pd0p8f6d8y7ry3fgh' + . 'vnq5yetrefw68dgvlcnd6g0tvfz7cf3ev0ca3jcds9rwruuaercma5jez0zsjkq' + . 'cha7e50xx3j4gx2kfcey53zzvyexzpdccnh9xm5xe3fqsn666dz4ssnv7ppyr55' + . '337k4vj0ypx8syreyevej2dsqds5qr4d762g9jjdpcykzgc8pfx24dg8astn9ep' + . '60vmnmd03zldd5y4tpxwxac47z6y3ewjmytqs30lukzw4yxq2efuqgv82da7xzl' + . 'k27gnyz7s43hyxr87ehwgcp3jh7dcn5ursgsfhaxsj9shgknwvvzkyj9z0v3kza' + . 'fd2clwhkh8cnjyj4mzfj9me60r82zquct9y02e6j77da53f38xsgut3evhpddyc' + . '87qm4w9cuc6sfwrg8tg8zwh55atnscecqmwpc3va4qwyawncqkf6jggemr7g3ge' + . 'rnn07wa9whpvncnc54tl9vtqlg5tv4yrz6sjnyaqglceek8rs8w5z2f8n3p4rkf' + . 'rsvzh3zu78xcfc0q579gvzz9m6uu53qg0mwcwwsz54x3ccsgzg5uavvftdtfuad' + . '09dns24tzx292ym0xlhcdwtwfkylu7q2hv6ydwpju55eqz9jwj5hw6mffjy3qtm' + . 'cw7sgm987guej6v4z7te2qcufp2vxarxsywwxrzya09fa5j854z4yzlu6vqknxm' + . 'esge92fhnkqyuj224t8q665d2s7t4dd4xx5hwzzmp0j2mqu6fksem0rnvxapqvw' + . 'k3rgcakpc5wvnkgrucda0lyq78m8rhp7974z9q6gg25plp9nsued6prk429gupe' + . '2wxqswc8yg9234txqsyfxjpeww9qlxftgtdsl8zce9jdm4vnlzhg3pnnxe9c3l2' + . 'zk4hkxgs64q3x3shja6ehwuc236glf9u0yh2g08w9pss8y3fscdyflyeu7wz9kv' + . 'sssp9wpry40qqjya94wa8x0rrz3ya4p6kx7xye098gxsxc7xsza544vnfxdz693' + . 'gu3r6fp2seqeafevl7qapv02mmcegk3t6v2ksrrfwg2jmtrmcjysm9vdtrjw25c' + . 'za7psy8ypdsehm5ppr0zl62tvgwwwp9ydtvm6mr39flxzqusdzpglzalsu3c0qh' + . '37cxkz0f0w3hv56rdcejfcnkdrss8dr6w7avas3f5auya7zccqt94w3wkpz2eks' + . '9rcn3ml83ra8vxkl89sk0qu78jzar4zhyk25r0pqv2cx5gpq04d5cvk3tjtmt92' + . 'dyckjzxgtmmpq8ac0eplxexfzwc57sfkd02snsqh4jfzrrst92as3pjsqjseaes' + . 'ygey0zk3tlmlghvxf43srgueudmx2qhwfycyesh6gfdjyrgnfkvzch62l6kp88s' + . 'jpzc0r48a9y34udtkvqlrfxa56esf9g4fuh6vvpllxltskam8p60axkgl25enmf' + . '42n074lmrwrd65tx5mlv0ggguhkh9ardv4nw9pjus440593gzctx9t5gt05xztv' + . 'g6wtkh'; + $this->assertTrue($this->validator->isValid($key)); + } + + public function testPostQuantumKeyWrongLengthIsRejected(): void + { + // Too short - missing characters at the end + $key = 'age1pq1dl3g5yxr5ur9yrmk4yswdjf7cvee2a7v3kh6qwaxsu4pvjav29vq56vz' + . 'nmwzps8eus8mdvahxr7zkch8r7xezx0l0vu99ypac73pp60t4e6fgxyp0dv7d3a' + . '6c3sqk2ynrpp98f7q03heaed2vfyx98c40h4wpfgdvsxmyy54n5ykk0svhstek4'; + $this->assertFalse($this->validator->isValid($key)); + $this->assertSame('Incorrect post-quantum key length', $this->validator->getRejectionReason()); + } + + public function testPostQuantumKeyTooLongIsRejected(): void + { + // Valid key with extra characters + $key = 'age1pq1dl3g5yxr5ur9yrmk4yswdjf7cvee2a7v3kh6qwaxsu4pvjav29vq56vz' + . 'nmwzps8eus8mdvahxr7zkch8r7xezx0l0vu99ypac73pp60t4e6fgxyp0dv7d3a' + . '6c3sqk2ynrpp98f7q03heaed2vfyx98c40h4wpfgdvsxmyy54n5ykk0svhstek4' + . 'sqcdpnwy9syxzczscz2642qmayq5ve6dremm4229tmwz7658yxmjwkva5y4s25r' + . '9gqehv7032qwenveemh9jk2sr8vp2yjtw90twpfefx244x8q369z8htnwhpk6nv' + . 'v22f8hfst9jtpaxak0pa4sw5a9e5z7jerhlmy0a90xpgy4kg3euxl6ptsanvks0' + . 'xs6lx026dz33jtnnc36x3j8awjcwtugp7x2t400af335j8pd0p8f6d8y7ry3fgh' + . 'vnq5yetrefw68dgvlcnd6g0tvfz7cf3ev0ca3jcds9rwruuaercma5jez0zsjkq' + . 'cha7e50xx3j4gx2kfcey53zzvyexzpdccnh9xm5xe3fqsn666dz4ssnv7ppyr55' + . '337k4vj0ypx8syreyevej2dsqds5qr4d762g9jjdpcykzgc8pfx24dg8astn9ep' + . '60vmnmd03zldd5y4tpxwxac47z6y3ewjmytqs30lukzw4yxq2efuqgv82da7xzl' + . 'k27gnyz7s43hyxr87ehwgcp3jh7dcn5ursgsfhaxsj9shgknwvvzkyj9z0v3kza' + . 'fd2clwhkh8cnjyj4mzfj9me60r82zquct9y02e6j77da53f38xsgut3evhpddyc' + . '87qm4w9cuc6sfwrg8tg8zwh55atnscecqmwpc3va4qwyawncqkf6jggemr7g3ge' + . 'rnn07wa9whpvncnc54tl9vtqlg5tv4yrz6sjnyaqglceek8rs8w5z2f8n3p4rkf' + . 'rsvzh3zu78xcfc0q579gvzz9m6uu53qg0mwcwwsz54x3ccsgzg5uavvftdtfuad' + . '09dns24tzx292ym0xlhcdwtwfkylu7q2hv6ydwpju55eqz9jwj5hw6mffjy3qtm' + . 'cw7sgm987guej6v4z7te2qcufp2vxarxsywwxrzya09fa5j854z4yzlu6vqknxm' + . 'esge92fhnkqyuj224t8q665d2s7t4dd4xx5hwzzmp0j2mqu6fksem0rnvxapqvw' + . 'k3rgcakpc5wvnkgrucda0lyq78m8rhp7974z9q6gg25plp9nsued6prk429gupe' + . '2wxqswc8yg9234txqsyfxjpeww9qlxftgtdsl8zce9jdm4vnlzhg3pnnxe9c3l2' + . 'zk4hkxgs64q3x3shja6ehwuc236glf9u0yh2g08w9pss8y3fscdyflyeu7wz9kv' + . 'sssp9wpry40qqjya94wa8x0rrz3ya4p6kx7xye098gxsxc7xsza544vnfxdz693' + . 'gu3r6fp2seqeafevl7qapv02mmcegk3t6v2ksrrfwg2jmtrmcjysm9vdtrjw25c' + . 'za7psy8ypdsehm5ppr0zl62tvgwwwp9ydtvm6mr39flxzqusdzpglzalsu3c0qh' + . '37cxkz0f0w3hv56rdcejfcnkdrss8dr6w7avas3f5auya7zccqt94w3wkpz2eks' + . '9rcn3ml83ra8vxkl89sk0qu78jzar4zhyk25r0pqv2cx5gpq04d5cvk3tjtmt92' + . 'dyckjzxgtmmpq8ac0eplxexfzwc57sfkd02snsqh4jfzrrst92as3pjsqjseaes' + . 'ygey0zk3tlmlghvxf43srgueudmx2qhwfycyesh6gfdjyrgnfkvzch62l6kp88s' + . 'jpzc0r48a9y34udtkvqlrfxa56esf9g4fuh6vvpllxltskam8p60axkgl25enmf' + . '42n074lmrwrd65tx5mlv0ggguhkh9ardv4nw9pjus440593gzctx9t5gt05xztv' + . 'g6wtkhxxx'; + $this->assertFalse($this->validator->isValid($key)); + $this->assertSame('Incorrect post-quantum key length', $this->validator->getRejectionReason()); + } + + public function testPostQuantumKeyInvalidChecksumIsRejected(): void + { + // Valid format but modified last character to break checksum + $key = 'age1pq1dl3g5yxr5ur9yrmk4yswdjf7cvee2a7v3kh6qwaxsu4pvjav29vq56vz' + . 'nmwzps8eus8mdvahxr7zkch8r7xezx0l0vu99ypac73pp60t4e6fgxyp0dv7d3a' + . '6c3sqk2ynrpp98f7q03heaed2vfyx98c40h4wpfgdvsxmyy54n5ykk0svhstek4' + . 'sqcdpnwy9syxzczscz2642qmayq5ve6dremm4229tmwz7658yxmjwkva5y4s25r' + . '9gqehv7032qwenveemh9jk2sr8vp2yjtw90twpfefx244x8q369z8htnwhpk6nv' + . 'v22f8hfst9jtpaxak0pa4sw5a9e5z7jerhlmy0a90xpgy4kg3euxl6ptsanvks0' + . 'xs6lx026dz33jtnnc36x3j8awjcwtugp7x2t400af335j8pd0p8f6d8y7ry3fgh' + . 'vnq5yetrefw68dgvlcnd6g0tvfz7cf3ev0ca3jcds9rwruuaercma5jez0zsjkq' + . 'cha7e50xx3j4gx2kfcey53zzvyexzpdccnh9xm5xe3fqsn666dz4ssnv7ppyr55' + . '337k4vj0ypx8syreyevej2dsqds5qr4d762g9jjdpcykzgc8pfx24dg8astn9ep' + . '60vmnmd03zldd5y4tpxwxac47z6y3ewjmytqs30lukzw4yxq2efuqgv82da7xzl' + . 'k27gnyz7s43hyxr87ehwgcp3jh7dcn5ursgsfhaxsj9shgknwvvzkyj9z0v3kza' + . 'fd2clwhkh8cnjyj4mzfj9me60r82zquct9y02e6j77da53f38xsgut3evhpddyc' + . '87qm4w9cuc6sfwrg8tg8zwh55atnscecqmwpc3va4qwyawncqkf6jggemr7g3ge' + . 'rnn07wa9whpvncnc54tl9vtqlg5tv4yrz6sjnyaqglceek8rs8w5z2f8n3p4rkf' + . 'rsvzh3zu78xcfc0q579gvzz9m6uu53qg0mwcwwsz54x3ccsgzg5uavvftdtfuad' + . '09dns24tzx292ym0xlhcdwtwfkylu7q2hv6ydwpju55eqz9jwj5hw6mffjy3qtm' + . 'cw7sgm987guej6v4z7te2qcufp2vxarxsywwxrzya09fa5j854z4yzlu6vqknxm' + . 'esge92fhnkqyuj224t8q665d2s7t4dd4xx5hwzzmp0j2mqu6fksem0rnvxapqvw' + . 'k3rgcakpc5wvnkgrucda0lyq78m8rhp7974z9q6gg25plp9nsued6prk429gupe' + . '2wxqswc8yg9234txqsyfxjpeww9qlxftgtdsl8zce9jdm4vnlzhg3pnnxe9c3l2' + . 'zk4hkxgs64q3x3shja6ehwuc236glf9u0yh2g08w9pss8y3fscdyflyeu7wz9kv' + . 'sssp9wpry40qqjya94wa8x0rrz3ya4p6kx7xye098gxsxc7xsza544vnfxdz693' + . 'gu3r6fp2seqeafevl7qapv02mmcegk3t6v2ksrrfwg2jmtrmcjysm9vdtrjw25c' + . 'za7psy8ypdsehm5ppr0zl62tvgwwwp9ydtvm6mr39flxzqusdzpglzalsu3c0qh' + . '37cxkz0f0w3hv56rdcejfcnkdrss8dr6w7avas3f5auya7zccqt94w3wkpz2eks' + . '9rcn3ml83ra8vxkl89sk0qu78jzar4zhyk25r0pqv2cx5gpq04d5cvk3tjtmt92' + . 'dyckjzxgtmmpq8ac0eplxexfzwc57sfkd02snsqh4jfzrrst92as3pjsqjseaes' + . 'ygey0zk3tlmlghvxf43srgueudmx2qhwfycyesh6gfdjyrgnfkvzch62l6kp88s' + . 'jpzc0r48a9y34udtkvqlrfxa56esf9g4fuh6vvpllxltskam8p60axkgl25enmf' + . '42n074lmrwrd65tx5mlv0ggguhkh9ardv4nw9pjus440593gzctx9t5gt05xztv' + . 'g6wtkz'; + $this->assertFalse($this->validator->isValid($key)); + $this->assertSame('invalid bech32 checksum', $this->validator->getRejectionReason()); + } }