Skip to content

Commit f2a5878

Browse files
committed
Add support for age1pq public keys
1 parent 57e71d7 commit f2a5878

File tree

2 files changed

+152
-8
lines changed

2 files changed

+152
-8
lines changed

src/AuxDataTypes/AgeV1.php

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@
88
class AgeV1 implements ExtensionInterface
99
{
1010
public const AUX_DATA_TYPE = 'age-v1';
11-
private const KEY_PREFIX = 'age1';
12-
private const KEY_LENGTH = 62;
11+
12+
// Classic X25519 keys: 32-byte public key
13+
private const KEY_PREFIX_CLASSIC = 'age1';
14+
private const KEY_LENGTH_CLASSIC = 62;
15+
16+
// Post-quantum hybrid keys (MLKEM768-X25519): 1184 + 32 = 1216 byte public key
17+
// HRP "age1pq" (6) + separator "1" (1) + data (1946) + checksum (6) = 1959
18+
private const KEY_PREFIX_PQ = 'age1pq1';
19+
private const KEY_LENGTH_PQ = 1959;
20+
1321
private string $lastRejection = '';
1422

1523
#[Override]
@@ -42,15 +50,23 @@ public function isValid(string $auxData): bool
4250
return false;
4351
}
4452

45-
// Check prefix and basic format
46-
if (!str_starts_with($auxData, self::KEY_PREFIX)) {
53+
// Determine key type and validate accordingly
54+
// Check post-quantum prefix first (it's longer and starts with 'age1')
55+
if (str_starts_with($auxData, self::KEY_PREFIX_PQ)) {
56+
if (strlen($auxData) !== self::KEY_LENGTH_PQ) {
57+
$this->lastRejection = 'Incorrect post-quantum key length';
58+
return false;
59+
}
60+
} elseif (str_starts_with($auxData, self::KEY_PREFIX_CLASSIC)) {
61+
if (strlen($auxData) !== self::KEY_LENGTH_CLASSIC) {
62+
$this->lastRejection = 'Incorrect key length';
63+
return false;
64+
}
65+
} else {
4766
$this->lastRejection = 'Header is incorrect';
4867
return false;
4968
}
50-
if (strlen($auxData) !== self::KEY_LENGTH) {
51-
$this->lastRejection = 'Incorrect key length';
52-
return false;
53-
}
69+
5470
$decoded = $this->bech32Decode($auxData);
5571
return $decoded !== false;
5672
}

tests/AuxDataTypes/AgeV1Test.php

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,132 @@ public function testEmptyStringIsRejected(): void
109109
{
110110
$this->assertFalse($this->validator->isValid(''));
111111
}
112+
113+
// Post-quantum (MLKEM768-X25519) key tests
114+
115+
public function testValidPostQuantumKey(): void
116+
{
117+
// Valid age1pq1 key generated with age-keygen -pq
118+
$key = 'age1pq1dl3g5yxr5ur9yrmk4yswdjf7cvee2a7v3kh6qwaxsu4pvjav29vq56vz'
119+
. 'nmwzps8eus8mdvahxr7zkch8r7xezx0l0vu99ypac73pp60t4e6fgxyp0dv7d3a'
120+
. '6c3sqk2ynrpp98f7q03heaed2vfyx98c40h4wpfgdvsxmyy54n5ykk0svhstek4'
121+
. 'sqcdpnwy9syxzczscz2642qmayq5ve6dremm4229tmwz7658yxmjwkva5y4s25r'
122+
. '9gqehv7032qwenveemh9jk2sr8vp2yjtw90twpfefx244x8q369z8htnwhpk6nv'
123+
. 'v22f8hfst9jtpaxak0pa4sw5a9e5z7jerhlmy0a90xpgy4kg3euxl6ptsanvks0'
124+
. 'xs6lx026dz33jtnnc36x3j8awjcwtugp7x2t400af335j8pd0p8f6d8y7ry3fgh'
125+
. 'vnq5yetrefw68dgvlcnd6g0tvfz7cf3ev0ca3jcds9rwruuaercma5jez0zsjkq'
126+
. 'cha7e50xx3j4gx2kfcey53zzvyexzpdccnh9xm5xe3fqsn666dz4ssnv7ppyr55'
127+
. '337k4vj0ypx8syreyevej2dsqds5qr4d762g9jjdpcykzgc8pfx24dg8astn9ep'
128+
. '60vmnmd03zldd5y4tpxwxac47z6y3ewjmytqs30lukzw4yxq2efuqgv82da7xzl'
129+
. 'k27gnyz7s43hyxr87ehwgcp3jh7dcn5ursgsfhaxsj9shgknwvvzkyj9z0v3kza'
130+
. 'fd2clwhkh8cnjyj4mzfj9me60r82zquct9y02e6j77da53f38xsgut3evhpddyc'
131+
. '87qm4w9cuc6sfwrg8tg8zwh55atnscecqmwpc3va4qwyawncqkf6jggemr7g3ge'
132+
. 'rnn07wa9whpvncnc54tl9vtqlg5tv4yrz6sjnyaqglceek8rs8w5z2f8n3p4rkf'
133+
. 'rsvzh3zu78xcfc0q579gvzz9m6uu53qg0mwcwwsz54x3ccsgzg5uavvftdtfuad'
134+
. '09dns24tzx292ym0xlhcdwtwfkylu7q2hv6ydwpju55eqz9jwj5hw6mffjy3qtm'
135+
. 'cw7sgm987guej6v4z7te2qcufp2vxarxsywwxrzya09fa5j854z4yzlu6vqknxm'
136+
. 'esge92fhnkqyuj224t8q665d2s7t4dd4xx5hwzzmp0j2mqu6fksem0rnvxapqvw'
137+
. 'k3rgcakpc5wvnkgrucda0lyq78m8rhp7974z9q6gg25plp9nsued6prk429gupe'
138+
. '2wxqswc8yg9234txqsyfxjpeww9qlxftgtdsl8zce9jdm4vnlzhg3pnnxe9c3l2'
139+
. 'zk4hkxgs64q3x3shja6ehwuc236glf9u0yh2g08w9pss8y3fscdyflyeu7wz9kv'
140+
. 'sssp9wpry40qqjya94wa8x0rrz3ya4p6kx7xye098gxsxc7xsza544vnfxdz693'
141+
. 'gu3r6fp2seqeafevl7qapv02mmcegk3t6v2ksrrfwg2jmtrmcjysm9vdtrjw25c'
142+
. 'za7psy8ypdsehm5ppr0zl62tvgwwwp9ydtvm6mr39flxzqusdzpglzalsu3c0qh'
143+
. '37cxkz0f0w3hv56rdcejfcnkdrss8dr6w7avas3f5auya7zccqt94w3wkpz2eks'
144+
. '9rcn3ml83ra8vxkl89sk0qu78jzar4zhyk25r0pqv2cx5gpq04d5cvk3tjtmt92'
145+
. 'dyckjzxgtmmpq8ac0eplxexfzwc57sfkd02snsqh4jfzrrst92as3pjsqjseaes'
146+
. 'ygey0zk3tlmlghvxf43srgueudmx2qhwfycyesh6gfdjyrgnfkvzch62l6kp88s'
147+
. 'jpzc0r48a9y34udtkvqlrfxa56esf9g4fuh6vvpllxltskam8p60axkgl25enmf'
148+
. '42n074lmrwrd65tx5mlv0ggguhkh9ardv4nw9pjus440593gzctx9t5gt05xztv'
149+
. 'g6wtkh';
150+
$this->assertTrue($this->validator->isValid($key));
151+
}
152+
153+
public function testPostQuantumKeyWrongLengthIsRejected(): void
154+
{
155+
// Too short - missing characters at the end
156+
$key = 'age1pq1dl3g5yxr5ur9yrmk4yswdjf7cvee2a7v3kh6qwaxsu4pvjav29vq56vz'
157+
. 'nmwzps8eus8mdvahxr7zkch8r7xezx0l0vu99ypac73pp60t4e6fgxyp0dv7d3a'
158+
. '6c3sqk2ynrpp98f7q03heaed2vfyx98c40h4wpfgdvsxmyy54n5ykk0svhstek4';
159+
$this->assertFalse($this->validator->isValid($key));
160+
$this->assertSame('Incorrect post-quantum key length', $this->validator->getRejectionReason());
161+
}
162+
163+
public function testPostQuantumKeyTooLongIsRejected(): void
164+
{
165+
// Valid key with extra characters
166+
$key = 'age1pq1dl3g5yxr5ur9yrmk4yswdjf7cvee2a7v3kh6qwaxsu4pvjav29vq56vz'
167+
. 'nmwzps8eus8mdvahxr7zkch8r7xezx0l0vu99ypac73pp60t4e6fgxyp0dv7d3a'
168+
. '6c3sqk2ynrpp98f7q03heaed2vfyx98c40h4wpfgdvsxmyy54n5ykk0svhstek4'
169+
. 'sqcdpnwy9syxzczscz2642qmayq5ve6dremm4229tmwz7658yxmjwkva5y4s25r'
170+
. '9gqehv7032qwenveemh9jk2sr8vp2yjtw90twpfefx244x8q369z8htnwhpk6nv'
171+
. 'v22f8hfst9jtpaxak0pa4sw5a9e5z7jerhlmy0a90xpgy4kg3euxl6ptsanvks0'
172+
. 'xs6lx026dz33jtnnc36x3j8awjcwtugp7x2t400af335j8pd0p8f6d8y7ry3fgh'
173+
. 'vnq5yetrefw68dgvlcnd6g0tvfz7cf3ev0ca3jcds9rwruuaercma5jez0zsjkq'
174+
. 'cha7e50xx3j4gx2kfcey53zzvyexzpdccnh9xm5xe3fqsn666dz4ssnv7ppyr55'
175+
. '337k4vj0ypx8syreyevej2dsqds5qr4d762g9jjdpcykzgc8pfx24dg8astn9ep'
176+
. '60vmnmd03zldd5y4tpxwxac47z6y3ewjmytqs30lukzw4yxq2efuqgv82da7xzl'
177+
. 'k27gnyz7s43hyxr87ehwgcp3jh7dcn5ursgsfhaxsj9shgknwvvzkyj9z0v3kza'
178+
. 'fd2clwhkh8cnjyj4mzfj9me60r82zquct9y02e6j77da53f38xsgut3evhpddyc'
179+
. '87qm4w9cuc6sfwrg8tg8zwh55atnscecqmwpc3va4qwyawncqkf6jggemr7g3ge'
180+
. 'rnn07wa9whpvncnc54tl9vtqlg5tv4yrz6sjnyaqglceek8rs8w5z2f8n3p4rkf'
181+
. 'rsvzh3zu78xcfc0q579gvzz9m6uu53qg0mwcwwsz54x3ccsgzg5uavvftdtfuad'
182+
. '09dns24tzx292ym0xlhcdwtwfkylu7q2hv6ydwpju55eqz9jwj5hw6mffjy3qtm'
183+
. 'cw7sgm987guej6v4z7te2qcufp2vxarxsywwxrzya09fa5j854z4yzlu6vqknxm'
184+
. 'esge92fhnkqyuj224t8q665d2s7t4dd4xx5hwzzmp0j2mqu6fksem0rnvxapqvw'
185+
. 'k3rgcakpc5wvnkgrucda0lyq78m8rhp7974z9q6gg25plp9nsued6prk429gupe'
186+
. '2wxqswc8yg9234txqsyfxjpeww9qlxftgtdsl8zce9jdm4vnlzhg3pnnxe9c3l2'
187+
. 'zk4hkxgs64q3x3shja6ehwuc236glf9u0yh2g08w9pss8y3fscdyflyeu7wz9kv'
188+
. 'sssp9wpry40qqjya94wa8x0rrz3ya4p6kx7xye098gxsxc7xsza544vnfxdz693'
189+
. 'gu3r6fp2seqeafevl7qapv02mmcegk3t6v2ksrrfwg2jmtrmcjysm9vdtrjw25c'
190+
. 'za7psy8ypdsehm5ppr0zl62tvgwwwp9ydtvm6mr39flxzqusdzpglzalsu3c0qh'
191+
. '37cxkz0f0w3hv56rdcejfcnkdrss8dr6w7avas3f5auya7zccqt94w3wkpz2eks'
192+
. '9rcn3ml83ra8vxkl89sk0qu78jzar4zhyk25r0pqv2cx5gpq04d5cvk3tjtmt92'
193+
. 'dyckjzxgtmmpq8ac0eplxexfzwc57sfkd02snsqh4jfzrrst92as3pjsqjseaes'
194+
. 'ygey0zk3tlmlghvxf43srgueudmx2qhwfycyesh6gfdjyrgnfkvzch62l6kp88s'
195+
. 'jpzc0r48a9y34udtkvqlrfxa56esf9g4fuh6vvpllxltskam8p60axkgl25enmf'
196+
. '42n074lmrwrd65tx5mlv0ggguhkh9ardv4nw9pjus440593gzctx9t5gt05xztv'
197+
. 'g6wtkhxxx';
198+
$this->assertFalse($this->validator->isValid($key));
199+
$this->assertSame('Incorrect post-quantum key length', $this->validator->getRejectionReason());
200+
}
201+
202+
public function testPostQuantumKeyInvalidChecksumIsRejected(): void
203+
{
204+
// Valid format but modified last character to break checksum
205+
$key = 'age1pq1dl3g5yxr5ur9yrmk4yswdjf7cvee2a7v3kh6qwaxsu4pvjav29vq56vz'
206+
. 'nmwzps8eus8mdvahxr7zkch8r7xezx0l0vu99ypac73pp60t4e6fgxyp0dv7d3a'
207+
. '6c3sqk2ynrpp98f7q03heaed2vfyx98c40h4wpfgdvsxmyy54n5ykk0svhstek4'
208+
. 'sqcdpnwy9syxzczscz2642qmayq5ve6dremm4229tmwz7658yxmjwkva5y4s25r'
209+
. '9gqehv7032qwenveemh9jk2sr8vp2yjtw90twpfefx244x8q369z8htnwhpk6nv'
210+
. 'v22f8hfst9jtpaxak0pa4sw5a9e5z7jerhlmy0a90xpgy4kg3euxl6ptsanvks0'
211+
. 'xs6lx026dz33jtnnc36x3j8awjcwtugp7x2t400af335j8pd0p8f6d8y7ry3fgh'
212+
. 'vnq5yetrefw68dgvlcnd6g0tvfz7cf3ev0ca3jcds9rwruuaercma5jez0zsjkq'
213+
. 'cha7e50xx3j4gx2kfcey53zzvyexzpdccnh9xm5xe3fqsn666dz4ssnv7ppyr55'
214+
. '337k4vj0ypx8syreyevej2dsqds5qr4d762g9jjdpcykzgc8pfx24dg8astn9ep'
215+
. '60vmnmd03zldd5y4tpxwxac47z6y3ewjmytqs30lukzw4yxq2efuqgv82da7xzl'
216+
. 'k27gnyz7s43hyxr87ehwgcp3jh7dcn5ursgsfhaxsj9shgknwvvzkyj9z0v3kza'
217+
. 'fd2clwhkh8cnjyj4mzfj9me60r82zquct9y02e6j77da53f38xsgut3evhpddyc'
218+
. '87qm4w9cuc6sfwrg8tg8zwh55atnscecqmwpc3va4qwyawncqkf6jggemr7g3ge'
219+
. 'rnn07wa9whpvncnc54tl9vtqlg5tv4yrz6sjnyaqglceek8rs8w5z2f8n3p4rkf'
220+
. 'rsvzh3zu78xcfc0q579gvzz9m6uu53qg0mwcwwsz54x3ccsgzg5uavvftdtfuad'
221+
. '09dns24tzx292ym0xlhcdwtwfkylu7q2hv6ydwpju55eqz9jwj5hw6mffjy3qtm'
222+
. 'cw7sgm987guej6v4z7te2qcufp2vxarxsywwxrzya09fa5j854z4yzlu6vqknxm'
223+
. 'esge92fhnkqyuj224t8q665d2s7t4dd4xx5hwzzmp0j2mqu6fksem0rnvxapqvw'
224+
. 'k3rgcakpc5wvnkgrucda0lyq78m8rhp7974z9q6gg25plp9nsued6prk429gupe'
225+
. '2wxqswc8yg9234txqsyfxjpeww9qlxftgtdsl8zce9jdm4vnlzhg3pnnxe9c3l2'
226+
. 'zk4hkxgs64q3x3shja6ehwuc236glf9u0yh2g08w9pss8y3fscdyflyeu7wz9kv'
227+
. 'sssp9wpry40qqjya94wa8x0rrz3ya4p6kx7xye098gxsxc7xsza544vnfxdz693'
228+
. 'gu3r6fp2seqeafevl7qapv02mmcegk3t6v2ksrrfwg2jmtrmcjysm9vdtrjw25c'
229+
. 'za7psy8ypdsehm5ppr0zl62tvgwwwp9ydtvm6mr39flxzqusdzpglzalsu3c0qh'
230+
. '37cxkz0f0w3hv56rdcejfcnkdrss8dr6w7avas3f5auya7zccqt94w3wkpz2eks'
231+
. '9rcn3ml83ra8vxkl89sk0qu78jzar4zhyk25r0pqv2cx5gpq04d5cvk3tjtmt92'
232+
. 'dyckjzxgtmmpq8ac0eplxexfzwc57sfkd02snsqh4jfzrrst92as3pjsqjseaes'
233+
. 'ygey0zk3tlmlghvxf43srgueudmx2qhwfycyesh6gfdjyrgnfkvzch62l6kp88s'
234+
. 'jpzc0r48a9y34udtkvqlrfxa56esf9g4fuh6vvpllxltskam8p60axkgl25enmf'
235+
. '42n074lmrwrd65tx5mlv0ggguhkh9ardv4nw9pjus440593gzctx9t5gt05xztv'
236+
. 'g6wtkz';
237+
$this->assertFalse($this->validator->isValid($key));
238+
$this->assertSame('invalid bech32 checksum', $this->validator->getRejectionReason());
239+
}
112240
}

0 commit comments

Comments
 (0)