xref: /freebsd/crypto/openssl/providers/implementations/encode_decode/ml_dsa_codecs.c (revision 07940d1d85eb338853fcba0697c6b9a96412a7f2)
1 /*
2  * Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
3  *
4  * Licensed under the Apache License 2.0 (the "License").  You may not use
5  * this file except in compliance with the License.  You can obtain a copy
6  * in the file LICENSE in the source distribution or at
7  * https://www.openssl.org/source/license.html
8  */
9 
10 #include <string.h>
11 #include <openssl/byteorder.h>
12 #include <openssl/err.h>
13 #include <openssl/proverr.h>
14 #include <openssl/x509.h>
15 #include <openssl/core_names.h>
16 #include "internal/encoder.h"
17 #include "prov/ml_dsa.h"
18 #include "ml_dsa_codecs.h"
19 
20 /*-
21  * Tables describing supported ASN.1 input/output formats.
22  */
23 
24 /*-
25  * ML-DSA-44:
26  * Public key bytes:  1312 (0x0520)
27  * Private key bytes: 2560 (0x0a00)
28  */
29 static const ML_COMMON_SPKI_FMT ml_dsa_44_spkifmt = {
30     {
31         0x30,
32         0x82,
33         0x05,
34         0x32,
35         0x30,
36         0x0b,
37         0x06,
38         0x09,
39         0x60,
40         0x86,
41         0x48,
42         0x01,
43         0x65,
44         0x03,
45         0x04,
46         0x03,
47         0x11,
48         0x03,
49         0x82,
50         0x05,
51         0x21,
52         0x00,
53     }
54 };
55 static const ML_COMMON_PKCS8_FMT ml_dsa_44_p8fmt[NUM_PKCS8_FORMATS] = {
56     {
57         "seed-priv",
58         0x0a2a,
59         0,
60         0x30820a26,
61         0x0420,
62         6,
63         0x20,
64         0x04820a00,
65         0x2a,
66         0x0a00,
67         0,
68         0,
69     },
70     {
71         "priv-only",
72         0x0a04,
73         0,
74         0x04820a00,
75         0,
76         0,
77         0,
78         0,
79         0x04,
80         0x0a00,
81         0,
82         0,
83     },
84     { "oqskeypair", 0x0f24, 0, 0x04820f20, 0, 0, 0, 0, 0x04, 0x0a00, 0x0a04, 0x0520 },
85     {
86         "seed-only",
87         0x0022,
88         2,
89         0x8020,
90         0,
91         2,
92         0x20,
93         0,
94         0,
95         0,
96         0,
97         0,
98     },
99     {
100         "bare-priv",
101         0x0a00,
102         4,
103         0,
104         0,
105         0,
106         0,
107         0,
108         0,
109         0x0a00,
110         0,
111         0,
112     },
113     {
114         "bare-seed",
115         0x0020,
116         4,
117         0,
118         0,
119         0,
120         0x20,
121         0,
122         0,
123         0,
124         0,
125         0,
126     },
127 };
128 
129 /*
130  * ML-DSA-65:
131  * Public key bytes:  1952 (0x07a0)
132  * Private key bytes: 4032 (0x0fc0)
133  */
134 static const ML_COMMON_SPKI_FMT ml_dsa_65_spkifmt = {
135     {
136         0x30,
137         0x82,
138         0x07,
139         0xb2,
140         0x30,
141         0x0b,
142         0x06,
143         0x09,
144         0x60,
145         0x86,
146         0x48,
147         0x01,
148         0x65,
149         0x03,
150         0x04,
151         0x03,
152         0x12,
153         0x03,
154         0x82,
155         0x07,
156         0xa1,
157         0x00,
158     }
159 };
160 static const ML_COMMON_PKCS8_FMT ml_dsa_65_p8fmt[NUM_PKCS8_FORMATS] = {
161     {
162         "seed-priv",
163         0x0fea,
164         0,
165         0x30820fe6,
166         0x0420,
167         6,
168         0x20,
169         0x04820fc0,
170         0x2a,
171         0x0fc0,
172         0,
173         0,
174     },
175     {
176         "priv-only",
177         0x0fc4,
178         0,
179         0x04820fc0,
180         0,
181         0,
182         0,
183         0,
184         0x04,
185         0x0fc0,
186         0,
187         0,
188     },
189     { "oqskeypair", 0x1764, 0, 0x04821760, 0, 0, 0, 0, 0x04, 0x0fc0, 0x0fc4, 0x07a0 },
190     {
191         "seed-only",
192         0x0022,
193         2,
194         0x8020,
195         0,
196         2,
197         0x20,
198         0,
199         0,
200         0,
201         0,
202         0,
203     },
204     {
205         "bare-priv",
206         0x0fc0,
207         4,
208         0,
209         0,
210         0,
211         0,
212         0,
213         0,
214         0x0fc0,
215         0,
216         0,
217     },
218     {
219         "bare-seed",
220         0x0020,
221         4,
222         0,
223         0,
224         0,
225         0x20,
226         0,
227         0,
228         0,
229         0,
230         0,
231     },
232 };
233 
234 /*-
235  * ML-DSA-87:
236  * Public key bytes:  2592 (0x0a20)
237  * Private key bytes: 4896 (0x1320)
238  */
239 static const ML_COMMON_SPKI_FMT ml_dsa_87_spkifmt = {
240     {
241         0x30,
242         0x82,
243         0x0a,
244         0x32,
245         0x30,
246         0x0b,
247         0x06,
248         0x09,
249         0x60,
250         0x86,
251         0x48,
252         0x01,
253         0x65,
254         0x03,
255         0x04,
256         0x03,
257         0x13,
258         0x03,
259         0x82,
260         0x0a,
261         0x21,
262         0x00,
263     }
264 };
265 static const ML_COMMON_PKCS8_FMT ml_dsa_87_p8fmt[NUM_PKCS8_FORMATS] = {
266     {
267         "seed-priv",
268         0x134a,
269         0,
270         0x30821346,
271         0x0420,
272         6,
273         0x20,
274         0x04821320,
275         0x2a,
276         0x1320,
277         0,
278         0,
279     },
280     {
281         "priv-only",
282         0x1324,
283         0,
284         0x04821320,
285         0,
286         0,
287         0,
288         0,
289         0x04,
290         0x1320,
291         0,
292         0,
293     },
294     { "oqskeypair", 0x1d44, 0, 0x04821d40, 0, 0, 0, 0, 0x04, 0x1320, 0x1324, 0x0a20 },
295     {
296         "seed-only",
297         0x0022,
298         2,
299         0x8020,
300         0,
301         2,
302         0x20,
303         0,
304         0,
305         0,
306         0,
307         0,
308     },
309     {
310         "bare-priv",
311         0x1320,
312         4,
313         0,
314         0,
315         0,
316         0,
317         0,
318         0,
319         0x1320,
320         0,
321         0,
322     },
323     {
324         "bare-seed",
325         0x0020,
326         4,
327         0,
328         0,
329         0,
330         0x20,
331         0,
332         0,
333         0,
334         0,
335         0,
336     },
337 };
338 
339 /* Indices of slots in the codec table below */
340 #define ML_DSA_44_CODEC 0
341 #define ML_DSA_65_CODEC 1
342 #define ML_DSA_87_CODEC 2
343 
344 /*
345  * Per-variant fixed parameters
346  */
347 static const ML_COMMON_CODEC codecs[3] = {
348     { &ml_dsa_44_spkifmt, ml_dsa_44_p8fmt },
349     { &ml_dsa_65_spkifmt, ml_dsa_65_p8fmt },
350     { &ml_dsa_87_spkifmt, ml_dsa_87_p8fmt }
351 };
352 
353 /* Retrieve the parameters of one of the ML-DSA variants */
354 static const ML_COMMON_CODEC *ml_dsa_get_codec(int evp_type)
355 {
356     switch (evp_type) {
357     case EVP_PKEY_ML_DSA_44:
358         return &codecs[ML_DSA_44_CODEC];
359     case EVP_PKEY_ML_DSA_65:
360         return &codecs[ML_DSA_65_CODEC];
361     case EVP_PKEY_ML_DSA_87:
362         return &codecs[ML_DSA_87_CODEC];
363     }
364     return NULL;
365 }
366 
367 ML_DSA_KEY *
368 ossl_ml_dsa_d2i_PUBKEY(const uint8_t *pk, int pk_len, int evp_type,
369     PROV_CTX *provctx, const char *propq)
370 {
371     OSSL_LIB_CTX *libctx = PROV_LIBCTX_OF(provctx);
372     const ML_COMMON_CODEC *codec;
373     const ML_DSA_PARAMS *params;
374     ML_DSA_KEY *ret;
375 
376     if ((params = ossl_ml_dsa_params_get(evp_type)) == NULL
377         || (codec = ml_dsa_get_codec(evp_type)) == NULL)
378         return NULL;
379     if (pk_len != ML_COMMON_SPKI_OVERHEAD + (ossl_ssize_t)params->pk_len
380         || memcmp(pk, codec->spkifmt->asn1_prefix, ML_COMMON_SPKI_OVERHEAD) != 0)
381         return NULL;
382     pk_len -= ML_COMMON_SPKI_OVERHEAD;
383     pk += ML_COMMON_SPKI_OVERHEAD;
384 
385     if ((ret = ossl_ml_dsa_key_new(libctx, propq, evp_type)) == NULL)
386         return NULL;
387 
388     if (!ossl_ml_dsa_pk_decode(ret, pk, (size_t)pk_len)) {
389         ERR_raise_data(ERR_LIB_PROV, PROV_R_BAD_ENCODING,
390             "error parsing %s public key from input SPKI",
391             params->alg);
392         ossl_ml_dsa_key_free(ret);
393         return NULL;
394     }
395 
396     return ret;
397 }
398 
399 ML_DSA_KEY *
400 ossl_ml_dsa_d2i_PKCS8(const uint8_t *prvenc, int prvlen,
401     int evp_type, PROV_CTX *provctx,
402     const char *propq)
403 {
404     const ML_DSA_PARAMS *v;
405     const ML_COMMON_CODEC *codec;
406     ML_COMMON_PKCS8_FMT_PREF *fmt_slots = NULL, *slot;
407     const ML_COMMON_PKCS8_FMT *p8fmt;
408     ML_DSA_KEY *key = NULL, *ret = NULL;
409     PKCS8_PRIV_KEY_INFO *p8inf = NULL;
410     const uint8_t *buf, *pos;
411     const X509_ALGOR *alg = NULL;
412     const char *formats;
413     int len, ptype;
414     uint32_t magic;
415     uint16_t seed_magic;
416     const uint8_t *seed = NULL;
417     const uint8_t *priv = NULL;
418 
419     /* Which ML-DSA variant? */
420     if ((v = ossl_ml_dsa_params_get(evp_type)) == NULL
421         || (codec = ml_dsa_get_codec(evp_type)) == NULL)
422         return 0;
423 
424     /* Extract the key OID and any parameters. */
425     if ((p8inf = d2i_PKCS8_PRIV_KEY_INFO(NULL, &prvenc, prvlen)) == NULL)
426         return 0;
427     /* Shortest prefix is 4 bytes: seq tag/len  + octet string tag/len */
428     if (!PKCS8_pkey_get0(NULL, &buf, &len, &alg, p8inf))
429         goto end;
430     /* Bail out early if this is some other key type. */
431     if (OBJ_obj2nid(alg->algorithm) != evp_type)
432         goto end;
433 
434     /* Get the list of enabled decoders. Their order is not important here. */
435     formats = ossl_prov_ctx_get_param(
436         provctx, OSSL_PKEY_PARAM_ML_DSA_INPUT_FORMATS, NULL);
437     fmt_slots = ossl_ml_common_pkcs8_fmt_order(v->alg, codec->p8fmt,
438         "input", formats);
439     if (fmt_slots == NULL)
440         goto end;
441 
442     /* Parameters must be absent. */
443     X509_ALGOR_get0(NULL, &ptype, NULL, alg);
444     if (ptype != V_ASN1_UNDEF) {
445         ERR_raise_data(ERR_LIB_PROV, PROV_R_UNEXPECTED_KEY_PARAMETERS,
446             "unexpected parameters with a PKCS#8 %s private key",
447             v->alg);
448         goto end;
449     }
450     if ((ossl_ssize_t)len < (ossl_ssize_t)sizeof(magic))
451         goto end;
452 
453     /* Find the matching p8 info slot, that also has the expected length. */
454     pos = OPENSSL_load_u32_be(&magic, buf);
455     for (slot = fmt_slots; (p8fmt = slot->fmt) != NULL; ++slot) {
456         if (len != (ossl_ssize_t)p8fmt->p8_bytes)
457             continue;
458         if (p8fmt->p8_shift == sizeof(magic)
459             || (magic >> (p8fmt->p8_shift * 8)) == p8fmt->p8_magic) {
460             pos -= p8fmt->p8_shift;
461             break;
462         }
463     }
464     if (p8fmt == NULL
465         || (p8fmt->seed_length > 0 && p8fmt->seed_length != ML_DSA_SEED_BYTES)
466         || (p8fmt->priv_length > 0 && p8fmt->priv_length != v->sk_len)
467         || (p8fmt->pub_length > 0 && p8fmt->pub_length != v->pk_len)) {
468         ERR_raise_data(ERR_LIB_PROV, PROV_R_ML_DSA_NO_FORMAT,
469             "no matching enabled %s private key input formats",
470             v->alg);
471         goto end;
472     }
473 
474     if (p8fmt->seed_length > 0) {
475         /* Check |seed| tag/len, if not subsumed by |magic|. */
476         if (pos + sizeof(uint16_t) == buf + p8fmt->seed_offset) {
477             pos = OPENSSL_load_u16_be(&seed_magic, pos);
478             if (seed_magic != p8fmt->seed_magic)
479                 goto end;
480         } else if (pos != buf + p8fmt->seed_offset) {
481             goto end;
482         }
483         pos += ML_DSA_SEED_BYTES;
484     }
485     if (p8fmt->priv_length > 0) {
486         /* Check |priv| tag/len */
487         if (pos + sizeof(uint32_t) == buf + p8fmt->priv_offset) {
488             pos = OPENSSL_load_u32_be(&magic, pos);
489             if (magic != p8fmt->priv_magic)
490                 goto end;
491         } else if (pos != buf + p8fmt->priv_offset) {
492             goto end;
493         }
494         pos += v->sk_len;
495     }
496     if (p8fmt->pub_length > 0) {
497         if (pos != buf + p8fmt->pub_offset)
498             goto end;
499         pos += v->pk_len;
500     }
501     if (pos != buf + len)
502         goto end;
503 
504     /*
505      * Collect the seed and/or key into a "decoded" private key object,
506      * to be turned into a real key on provider "load" or "import".
507      */
508     if ((key = ossl_prov_ml_dsa_new(provctx, propq, evp_type)) == NULL)
509         goto end;
510     if (p8fmt->seed_length > 0)
511         seed = buf + p8fmt->seed_offset;
512     if (p8fmt->priv_length > 0)
513         priv = buf + p8fmt->priv_offset;
514     /* Any OQS public key content is ignored */
515 
516     if (ossl_ml_dsa_set_prekey(key, 0, 0,
517             seed, ML_DSA_SEED_BYTES, priv, v->sk_len))
518         ret = key;
519 
520 end:
521     OPENSSL_free(fmt_slots);
522     PKCS8_PRIV_KEY_INFO_free(p8inf);
523     if (ret == NULL)
524         ossl_ml_dsa_key_free(key);
525     return ret;
526 }
527 
528 /* Same as ossl_ml_dsa_encode_pubkey, but allocates the output buffer. */
529 int ossl_ml_dsa_i2d_pubkey(const ML_DSA_KEY *key, unsigned char **out)
530 {
531     const ML_DSA_PARAMS *params = ossl_ml_dsa_key_params(key);
532     const uint8_t *pk = ossl_ml_dsa_key_get_pub(key);
533 
534     if (pk == NULL) {
535         ERR_raise_data(ERR_LIB_PROV, PROV_R_NOT_A_PUBLIC_KEY,
536             "no %s public key data available", params->alg);
537         return 0;
538     }
539     if (out != NULL
540         && (*out = OPENSSL_memdup(pk, params->pk_len)) == NULL)
541         return 0;
542     return (int)params->pk_len;
543 }
544 
545 /* Allocate and encode PKCS#8 private key payload. */
546 int ossl_ml_dsa_i2d_prvkey(const ML_DSA_KEY *key, uint8_t **out,
547     PROV_CTX *provctx)
548 {
549     const ML_DSA_PARAMS *params = ossl_ml_dsa_key_params(key);
550     const ML_COMMON_CODEC *codec;
551     ML_COMMON_PKCS8_FMT_PREF *fmt_slots, *slot;
552     const ML_COMMON_PKCS8_FMT *p8fmt;
553     uint8_t *buf = NULL, *pos;
554     const char *formats;
555     int len = ML_DSA_SEED_BYTES;
556     int ret = 0;
557     const uint8_t *seed = ossl_ml_dsa_key_get_seed(key);
558     const uint8_t *sk = ossl_ml_dsa_key_get_priv(key);
559 
560     /* Not ours to handle */
561     if ((codec = ml_dsa_get_codec(params->evp_type)) == NULL)
562         return 0;
563 
564     if (sk == NULL) {
565         ERR_raise_data(ERR_LIB_PROV, PROV_R_NOT_A_PRIVATE_KEY,
566             "no %s private key data available",
567             params->alg);
568         return 0;
569     }
570 
571     formats = ossl_prov_ctx_get_param(
572         provctx, OSSL_PKEY_PARAM_ML_DSA_OUTPUT_FORMATS, NULL);
573     fmt_slots = ossl_ml_common_pkcs8_fmt_order(params->alg, codec->p8fmt,
574         "output", formats);
575     if (fmt_slots == NULL)
576         return 0;
577 
578     /* If we don't have a seed, skip seedful entries */
579     for (slot = fmt_slots; (p8fmt = slot->fmt) != NULL; ++slot)
580         if (seed != NULL || p8fmt->seed_length == 0)
581             break;
582     /* No matching table entries, give up */
583     if (p8fmt == NULL
584         || (p8fmt->seed_length > 0 && p8fmt->seed_length != ML_DSA_SEED_BYTES)
585         || (p8fmt->priv_length > 0 && p8fmt->priv_length != params->sk_len)
586         || (p8fmt->pub_length > 0 && p8fmt->pub_length != params->pk_len)) {
587         ERR_raise_data(ERR_LIB_PROV, PROV_R_ML_DSA_NO_FORMAT,
588             "no matching enabled %s private key output formats",
589             params->alg);
590         goto end;
591     }
592     len = p8fmt->p8_bytes;
593 
594     if (out == NULL) {
595         ret = len;
596         goto end;
597     }
598 
599     if ((pos = buf = OPENSSL_malloc((size_t)len)) == NULL)
600         goto end;
601 
602     switch (p8fmt->p8_shift) {
603     case 0:
604         pos = OPENSSL_store_u32_be(pos, p8fmt->p8_magic);
605         break;
606     case 2:
607         pos = OPENSSL_store_u16_be(pos, (uint16_t)p8fmt->p8_magic);
608         break;
609     case 4:
610         break;
611     default:
612         ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR,
613             "error encoding %s private key", params->alg);
614         goto end;
615     }
616 
617     if (p8fmt->seed_length != 0) {
618         /*
619          * Either the tag/len were already included in |magic| or they require
620          * us to write two bytes now.
621          */
622         if (pos + sizeof(uint16_t) == buf + p8fmt->seed_offset)
623             pos = OPENSSL_store_u16_be(pos, p8fmt->seed_magic);
624         if (pos != buf + p8fmt->seed_offset) {
625             ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR,
626                 "error encoding %s private key", params->alg);
627             goto end;
628         }
629         memcpy(pos, seed, ML_DSA_SEED_BYTES);
630         pos += ML_DSA_SEED_BYTES;
631     }
632     if (p8fmt->priv_length != 0) {
633         if (pos + sizeof(uint32_t) == buf + p8fmt->priv_offset)
634             pos = OPENSSL_store_u32_be(pos, p8fmt->priv_magic);
635         if (pos != buf + p8fmt->priv_offset) {
636             ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR,
637                 "error encoding %s private key", params->alg);
638             goto end;
639         }
640         memcpy(pos, sk, params->sk_len);
641         pos += params->sk_len;
642     }
643     /* OQS form output with tacked-on public key */
644     if (p8fmt->pub_length != 0) {
645         /* The OQS pubkey is never separately DER-wrapped */
646         if (pos != buf + p8fmt->pub_offset) {
647             ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR,
648                 "error encoding %s private key", params->alg);
649             goto end;
650         }
651         memcpy(pos, ossl_ml_dsa_key_get_pub(key), params->pk_len);
652         pos += params->pk_len;
653     }
654 
655     if (pos == buf + len) {
656         *out = buf;
657         ret = len;
658     }
659 
660 end:
661     OPENSSL_free(fmt_slots);
662     if (ret == 0)
663         OPENSSL_free(buf);
664     return ret;
665 }
666 
667 int ossl_ml_dsa_key_to_text(BIO *out, const ML_DSA_KEY *key, int selection)
668 {
669     const ML_DSA_PARAMS *params;
670     const uint8_t *seed, *sk, *pk;
671 
672     if (out == NULL || key == NULL) {
673         ERR_raise(ERR_LIB_PROV, ERR_R_PASSED_NULL_PARAMETER);
674         return 0;
675     }
676     params = ossl_ml_dsa_key_params(key);
677     pk = ossl_ml_dsa_key_get_pub(key);
678     sk = ossl_ml_dsa_key_get_priv(key);
679     seed = ossl_ml_dsa_key_get_seed(key);
680 
681     if (pk == NULL) {
682         /* Regardless of the |selection|, there must be a public key */
683         ERR_raise_data(ERR_LIB_PROV, PROV_R_MISSING_KEY,
684             "no %s key material available", params->alg);
685         return 0;
686     }
687 
688     if ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) {
689         if (sk == NULL) {
690             ERR_raise_data(ERR_LIB_PROV, PROV_R_MISSING_KEY,
691                 "no %s key material available", params->alg);
692             return 0;
693         }
694         if (BIO_printf(out, "%s Private-Key:\n", params->alg) <= 0)
695             return 0;
696         if (seed != NULL && !ossl_bio_print_labeled_buf(out, "seed:", seed, ML_DSA_SEED_BYTES))
697             return 0;
698         if (!ossl_bio_print_labeled_buf(out, "priv:", sk, params->sk_len))
699             return 0;
700     } else if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) {
701         if (BIO_printf(out, "%s Public-Key:\n", params->alg) <= 0)
702             return 0;
703     }
704 
705     if (!ossl_bio_print_labeled_buf(out, "pub:", pk, params->pk_len))
706         return 0;
707 
708     return 1;
709 }
710