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