1 /* 2 * Copyright 2025-2026 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 { 0x30, 0x82, 0x05, 0x32, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 31 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x11, 0x03, 0x82, 0x05, 32 0x21, 0x00 } 33 }; 34 static const ML_COMMON_PKCS8_FMT ml_dsa_44_p8fmt[NUM_PKCS8_FORMATS] = { 35 { "seed-priv", 0x0a2a, 0, 0x30820a26, 0x0420, 6, 0x20, 0x04820a00, 0x2a, 0x0a00, 0, 0 }, 36 { "priv-only", 0x0a04, 0, 0x04820a00, 0, 0, 0, 0, 0x04, 0x0a00, 0, 0 }, 37 { "oqskeypair", 0x0f24, 0, 0x04820f20, 0, 0, 0, 0, 0x04, 0x0a00, 0x0a04, 0x0520 }, 38 { "seed-only", 0x0022, 2, 0x8020, 0, 2, 0x20, 0, 0, 0, 0, 0 }, 39 { "bare-priv", 0x0a00, 4, 0, 0, 0, 0, 0, 0, 0x0a00, 0, 0 }, 40 { "bare-seed", 0x0020, 4, 0, 0, 0, 0x20, 0, 0, 0, 0, 0 }, 41 }; 42 43 /* 44 * ML-DSA-65: 45 * Public key bytes: 1952 (0x07a0) 46 * Private key bytes: 4032 (0x0fc0) 47 */ 48 static const ML_COMMON_SPKI_FMT ml_dsa_65_spkifmt = { 49 { 0x30, 0x82, 0x07, 0xb2, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 50 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12, 0x03, 0x82, 0x07, 51 0xa1, 0x00 } 52 }; 53 static const ML_COMMON_PKCS8_FMT ml_dsa_65_p8fmt[NUM_PKCS8_FORMATS] = { 54 { "seed-priv", 0x0fea, 0, 0x30820fe6, 0x0420, 6, 0x20, 0x04820fc0, 0x2a, 0x0fc0, 0, 0 }, 55 { "priv-only", 0x0fc4, 0, 0x04820fc0, 0, 0, 0, 0, 0x04, 0x0fc0, 0, 0 }, 56 { "oqskeypair", 0x1764, 0, 0x04821760, 0, 0, 0, 0, 0x04, 0x0fc0, 0x0fc4, 0x07a0 }, 57 { "seed-only", 0x0022, 2, 0x8020, 0, 2, 0x20, 0, 0, 0, 0, 0 }, 58 { "bare-priv", 0x0fc0, 4, 0, 0, 0, 0, 0, 0, 0x0fc0, 0, 0 }, 59 { "bare-seed", 0x0020, 4, 0, 0, 0, 0x20, 0, 0, 0, 0, 0 }, 60 }; 61 62 /*- 63 * ML-DSA-87: 64 * Public key bytes: 2592 (0x0a20) 65 * Private key bytes: 4896 (0x1320) 66 */ 67 static const ML_COMMON_SPKI_FMT ml_dsa_87_spkifmt = { 68 { 0x30, 0x82, 0x0a, 0x32, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 69 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x13, 0x03, 0x82, 0x0a, 70 0x21, 0x00 } 71 }; 72 static const ML_COMMON_PKCS8_FMT ml_dsa_87_p8fmt[NUM_PKCS8_FORMATS] = { 73 { "seed-priv", 0x134a, 0, 0x30821346, 0x0420, 6, 0x20, 0x04821320, 0x2a, 0x1320, 0, 0 }, 74 { "priv-only", 0x1324, 0, 0x04821320, 0, 0, 0, 0, 0x04, 0x1320, 0, 0 }, 75 { "oqskeypair", 0x1d44, 0, 0x04821d40, 0, 0, 0, 0, 0x04, 0x1320, 0x1324, 0x0a20 }, 76 { "seed-only", 0x0022, 2, 0x8020, 0, 2, 0x20, 0, 0, 0, 0, 0 }, 77 { "bare-priv", 0x1320, 4, 0, 0, 0, 0, 0, 0, 0x1320, 0, 0 }, 78 { "bare-seed", 0x0020, 4, 0, 0, 0, 0x20, 0, 0, 0, 0, 0 }, 79 }; 80 81 /* Indices of slots in the codec table below */ 82 #define ML_DSA_44_CODEC 0 83 #define ML_DSA_65_CODEC 1 84 #define ML_DSA_87_CODEC 2 85 86 /* 87 * Per-variant fixed parameters 88 */ 89 static const ML_COMMON_CODEC codecs[3] = { 90 { &ml_dsa_44_spkifmt, ml_dsa_44_p8fmt }, 91 { &ml_dsa_65_spkifmt, ml_dsa_65_p8fmt }, 92 { &ml_dsa_87_spkifmt, ml_dsa_87_p8fmt } 93 }; 94 95 /* Retrieve the parameters of one of the ML-DSA variants */ 96 static const ML_COMMON_CODEC *ml_dsa_get_codec(int evp_type) 97 { 98 switch (evp_type) { 99 case EVP_PKEY_ML_DSA_44: 100 return &codecs[ML_DSA_44_CODEC]; 101 case EVP_PKEY_ML_DSA_65: 102 return &codecs[ML_DSA_65_CODEC]; 103 case EVP_PKEY_ML_DSA_87: 104 return &codecs[ML_DSA_87_CODEC]; 105 } 106 return NULL; 107 } 108 109 ML_DSA_KEY * 110 ossl_ml_dsa_d2i_PUBKEY(const uint8_t *pk, int pk_len, int evp_type, 111 PROV_CTX *provctx, const char *propq) 112 { 113 OSSL_LIB_CTX *libctx = PROV_LIBCTX_OF(provctx); 114 const ML_COMMON_CODEC *codec; 115 const ML_DSA_PARAMS *params; 116 ML_DSA_KEY *ret; 117 118 if ((params = ossl_ml_dsa_params_get(evp_type)) == NULL 119 || (codec = ml_dsa_get_codec(evp_type)) == NULL) 120 return NULL; 121 if (pk_len != ML_COMMON_SPKI_OVERHEAD + (ossl_ssize_t)params->pk_len 122 || memcmp(pk, codec->spkifmt->asn1_prefix, ML_COMMON_SPKI_OVERHEAD) != 0) 123 return NULL; 124 pk_len -= ML_COMMON_SPKI_OVERHEAD; 125 pk += ML_COMMON_SPKI_OVERHEAD; 126 127 if ((ret = ossl_ml_dsa_key_new(libctx, propq, evp_type)) == NULL) 128 return NULL; 129 130 if (!ossl_ml_dsa_pk_decode(ret, pk, (size_t)pk_len)) { 131 ERR_raise_data(ERR_LIB_PROV, PROV_R_BAD_ENCODING, 132 "error parsing %s public key from input SPKI", 133 params->alg); 134 ossl_ml_dsa_key_free(ret); 135 return NULL; 136 } 137 138 return ret; 139 } 140 141 ML_DSA_KEY * 142 ossl_ml_dsa_d2i_PKCS8(const uint8_t *prvenc, int prvlen, 143 int evp_type, PROV_CTX *provctx, 144 const char *propq) 145 { 146 const ML_DSA_PARAMS *v; 147 const ML_COMMON_CODEC *codec; 148 ML_COMMON_PKCS8_FMT_PREF *fmt_slots = NULL, *slot; 149 const ML_COMMON_PKCS8_FMT *p8fmt; 150 ML_DSA_KEY *key = NULL, *ret = NULL; 151 PKCS8_PRIV_KEY_INFO *p8inf = NULL; 152 const uint8_t *buf, *pos; 153 const X509_ALGOR *alg = NULL; 154 const char *formats; 155 int len, ptype; 156 uint32_t magic; 157 uint16_t seed_magic; 158 const uint8_t *seed = NULL; 159 const uint8_t *priv = NULL; 160 161 /* Which ML-DSA variant? */ 162 if ((v = ossl_ml_dsa_params_get(evp_type)) == NULL 163 || (codec = ml_dsa_get_codec(evp_type)) == NULL) 164 return 0; 165 166 /* Extract the key OID and any parameters. */ 167 if ((p8inf = d2i_PKCS8_PRIV_KEY_INFO(NULL, &prvenc, prvlen)) == NULL) 168 return 0; 169 /* Shortest prefix is 4 bytes: seq tag/len + octet string tag/len */ 170 if (!PKCS8_pkey_get0(NULL, &buf, &len, &alg, p8inf)) 171 goto end; 172 /* Bail out early if this is some other key type. */ 173 if (OBJ_obj2nid(alg->algorithm) != evp_type) 174 goto end; 175 176 /* Get the list of enabled decoders. Their order is not important here. */ 177 formats = ossl_prov_ctx_get_param( 178 provctx, OSSL_PKEY_PARAM_ML_DSA_INPUT_FORMATS, NULL); 179 fmt_slots = ossl_ml_common_pkcs8_fmt_order(v->alg, codec->p8fmt, 180 "input", formats); 181 if (fmt_slots == NULL) 182 goto end; 183 184 /* Parameters must be absent. */ 185 X509_ALGOR_get0(NULL, &ptype, NULL, alg); 186 if (ptype != V_ASN1_UNDEF) { 187 ERR_raise_data(ERR_LIB_PROV, PROV_R_UNEXPECTED_KEY_PARAMETERS, 188 "unexpected parameters with a PKCS#8 %s private key", 189 v->alg); 190 goto end; 191 } 192 if ((ossl_ssize_t)len < (ossl_ssize_t)sizeof(magic)) 193 goto end; 194 195 /* Find the matching p8 info slot, that also has the expected length. */ 196 pos = OPENSSL_load_u32_be(&magic, buf); 197 for (slot = fmt_slots; (p8fmt = slot->fmt) != NULL; ++slot) { 198 if (len != (ossl_ssize_t)p8fmt->p8_bytes) 199 continue; 200 if (p8fmt->p8_shift == sizeof(magic) 201 || (magic >> (p8fmt->p8_shift * 8)) == p8fmt->p8_magic) { 202 pos -= p8fmt->p8_shift; 203 break; 204 } 205 } 206 if (p8fmt == NULL 207 || (p8fmt->seed_length > 0 && p8fmt->seed_length != ML_DSA_SEED_BYTES) 208 || (p8fmt->priv_length > 0 && p8fmt->priv_length != v->sk_len) 209 || (p8fmt->pub_length > 0 && p8fmt->pub_length != v->pk_len)) { 210 ERR_raise_data(ERR_LIB_PROV, PROV_R_ML_DSA_NO_FORMAT, 211 "no matching enabled %s private key input formats", 212 v->alg); 213 goto end; 214 } 215 216 if (p8fmt->seed_length > 0) { 217 /* Check |seed| tag/len, if not subsumed by |magic|. */ 218 if (pos + sizeof(uint16_t) == buf + p8fmt->seed_offset) { 219 pos = OPENSSL_load_u16_be(&seed_magic, pos); 220 if (seed_magic != p8fmt->seed_magic) 221 goto end; 222 } else if (pos != buf + p8fmt->seed_offset) { 223 goto end; 224 } 225 pos += ML_DSA_SEED_BYTES; 226 } 227 if (p8fmt->priv_length > 0) { 228 /* Check |priv| tag/len */ 229 if (pos + sizeof(uint32_t) == buf + p8fmt->priv_offset) { 230 pos = OPENSSL_load_u32_be(&magic, pos); 231 if (magic != p8fmt->priv_magic) 232 goto end; 233 } else if (pos != buf + p8fmt->priv_offset) { 234 goto end; 235 } 236 pos += v->sk_len; 237 } 238 if (p8fmt->pub_length > 0) { 239 if (pos != buf + p8fmt->pub_offset) 240 goto end; 241 pos += v->pk_len; 242 } 243 if (pos != buf + len) 244 goto end; 245 246 /* 247 * Collect the seed and/or key into a "decoded" private key object, 248 * to be turned into a real key on provider "load" or "import". 249 */ 250 if ((key = ossl_prov_ml_dsa_new(provctx, propq, evp_type)) == NULL) 251 goto end; 252 if (p8fmt->seed_length > 0) 253 seed = buf + p8fmt->seed_offset; 254 if (p8fmt->priv_length > 0) 255 priv = buf + p8fmt->priv_offset; 256 /* Any OQS public key content is ignored */ 257 258 if (ossl_ml_dsa_set_prekey(key, 0, 0, 259 seed, ML_DSA_SEED_BYTES, priv, v->sk_len)) 260 ret = key; 261 262 end: 263 OPENSSL_free(fmt_slots); 264 PKCS8_PRIV_KEY_INFO_free(p8inf); 265 if (ret == NULL) 266 ossl_ml_dsa_key_free(key); 267 return ret; 268 } 269 270 /* Same as ossl_ml_dsa_encode_pubkey, but allocates the output buffer. */ 271 int ossl_ml_dsa_i2d_pubkey(const ML_DSA_KEY *key, unsigned char **out) 272 { 273 const ML_DSA_PARAMS *params = ossl_ml_dsa_key_params(key); 274 const uint8_t *pk = ossl_ml_dsa_key_get_pub(key); 275 276 if (pk == NULL) { 277 ERR_raise_data(ERR_LIB_PROV, PROV_R_NOT_A_PUBLIC_KEY, 278 "no %s public key data available", params->alg); 279 return 0; 280 } 281 if (out != NULL 282 && (*out = OPENSSL_memdup(pk, params->pk_len)) == NULL) 283 return 0; 284 return (int)params->pk_len; 285 } 286 287 /* Allocate and encode PKCS#8 private key payload. */ 288 int ossl_ml_dsa_i2d_prvkey(const ML_DSA_KEY *key, uint8_t **out, 289 PROV_CTX *provctx) 290 { 291 const ML_DSA_PARAMS *params = ossl_ml_dsa_key_params(key); 292 const ML_COMMON_CODEC *codec; 293 ML_COMMON_PKCS8_FMT_PREF *fmt_slots, *slot; 294 const ML_COMMON_PKCS8_FMT *p8fmt; 295 uint8_t *buf = NULL, *pos; 296 const char *formats; 297 int len = ML_DSA_SEED_BYTES; 298 int ret = 0; 299 const uint8_t *seed = ossl_ml_dsa_key_get_seed(key); 300 const uint8_t *sk = ossl_ml_dsa_key_get_priv(key); 301 302 /* Not ours to handle */ 303 if ((codec = ml_dsa_get_codec(params->evp_type)) == NULL) 304 return 0; 305 306 if (sk == NULL) { 307 ERR_raise_data(ERR_LIB_PROV, PROV_R_NOT_A_PRIVATE_KEY, 308 "no %s private key data available", 309 params->alg); 310 return 0; 311 } 312 313 formats = ossl_prov_ctx_get_param( 314 provctx, OSSL_PKEY_PARAM_ML_DSA_OUTPUT_FORMATS, NULL); 315 fmt_slots = ossl_ml_common_pkcs8_fmt_order(params->alg, codec->p8fmt, 316 "output", formats); 317 if (fmt_slots == NULL) 318 return 0; 319 320 /* If we don't have a seed, skip seedful entries */ 321 for (slot = fmt_slots; (p8fmt = slot->fmt) != NULL; ++slot) 322 if (seed != NULL || p8fmt->seed_length == 0) 323 break; 324 /* No matching table entries, give up */ 325 if (p8fmt == NULL 326 || (p8fmt->seed_length > 0 && p8fmt->seed_length != ML_DSA_SEED_BYTES) 327 || (p8fmt->priv_length > 0 && p8fmt->priv_length != params->sk_len) 328 || (p8fmt->pub_length > 0 && p8fmt->pub_length != params->pk_len)) { 329 ERR_raise_data(ERR_LIB_PROV, PROV_R_ML_DSA_NO_FORMAT, 330 "no matching enabled %s private key output formats", 331 params->alg); 332 goto end; 333 } 334 len = p8fmt->p8_bytes; 335 336 if (out == NULL) { 337 ret = len; 338 goto end; 339 } 340 341 if ((pos = buf = OPENSSL_malloc((size_t)len)) == NULL) 342 goto end; 343 344 switch (p8fmt->p8_shift) { 345 case 0: 346 pos = OPENSSL_store_u32_be(pos, p8fmt->p8_magic); 347 break; 348 case 2: 349 pos = OPENSSL_store_u16_be(pos, (uint16_t)p8fmt->p8_magic); 350 break; 351 case 4: 352 break; 353 default: 354 ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR, 355 "error encoding %s private key", params->alg); 356 goto end; 357 } 358 359 if (p8fmt->seed_length != 0) { 360 /* 361 * Either the tag/len were already included in |magic| or they require 362 * us to write two bytes now. 363 */ 364 if (pos + sizeof(uint16_t) == buf + p8fmt->seed_offset) 365 pos = OPENSSL_store_u16_be(pos, p8fmt->seed_magic); 366 if (pos != buf + p8fmt->seed_offset) { 367 ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR, 368 "error encoding %s private key", params->alg); 369 goto end; 370 } 371 memcpy(pos, seed, ML_DSA_SEED_BYTES); 372 pos += ML_DSA_SEED_BYTES; 373 } 374 if (p8fmt->priv_length != 0) { 375 if (pos + sizeof(uint32_t) == buf + p8fmt->priv_offset) 376 pos = OPENSSL_store_u32_be(pos, p8fmt->priv_magic); 377 if (pos != buf + p8fmt->priv_offset) { 378 ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR, 379 "error encoding %s private key", params->alg); 380 goto end; 381 } 382 memcpy(pos, sk, params->sk_len); 383 pos += params->sk_len; 384 } 385 /* OQS form output with tacked-on public key */ 386 if (p8fmt->pub_length != 0) { 387 /* The OQS pubkey is never separately DER-wrapped */ 388 if (pos != buf + p8fmt->pub_offset) { 389 ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR, 390 "error encoding %s private key", params->alg); 391 goto end; 392 } 393 memcpy(pos, ossl_ml_dsa_key_get_pub(key), params->pk_len); 394 pos += params->pk_len; 395 } 396 397 if (pos == buf + len) { 398 *out = buf; 399 ret = len; 400 } 401 402 end: 403 OPENSSL_free(fmt_slots); 404 if (ret == 0) 405 OPENSSL_free(buf); 406 return ret; 407 } 408 409 int ossl_ml_dsa_key_to_text(BIO *out, const ML_DSA_KEY *key, int selection) 410 { 411 const ML_DSA_PARAMS *params; 412 const uint8_t *seed, *sk, *pk; 413 414 if (out == NULL || key == NULL) { 415 ERR_raise(ERR_LIB_PROV, ERR_R_PASSED_NULL_PARAMETER); 416 return 0; 417 } 418 params = ossl_ml_dsa_key_params(key); 419 pk = ossl_ml_dsa_key_get_pub(key); 420 sk = ossl_ml_dsa_key_get_priv(key); 421 seed = ossl_ml_dsa_key_get_seed(key); 422 423 if (pk == NULL) { 424 /* Regardless of the |selection|, there must be a public key */ 425 ERR_raise_data(ERR_LIB_PROV, PROV_R_MISSING_KEY, 426 "no %s key material available", params->alg); 427 return 0; 428 } 429 430 if ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) { 431 if (sk == NULL) { 432 ERR_raise_data(ERR_LIB_PROV, PROV_R_MISSING_KEY, 433 "no %s key material available", params->alg); 434 return 0; 435 } 436 if (BIO_printf(out, "%s Private-Key:\n", params->alg) <= 0) 437 return 0; 438 if (seed != NULL && !ossl_bio_print_labeled_buf(out, "seed:", seed, ML_DSA_SEED_BYTES)) 439 return 0; 440 if (!ossl_bio_print_labeled_buf(out, "priv:", sk, params->sk_len)) 441 return 0; 442 } else if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) { 443 if (BIO_printf(out, "%s Public-Key:\n", params->alg) <= 0) 444 return 0; 445 } 446 447 if (!ossl_bio_print_labeled_buf(out, "pub:", pk, params->pk_len)) 448 return 0; 449 450 return 1; 451 } 452