Path: blob/main/crypto/openssl/providers/implementations/encode_decode/ml_dsa_codecs.c
103954 views
/*1* Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.2*3* Licensed under the Apache License 2.0 (the "License"). You may not use4* this file except in compliance with the License. You can obtain a copy5* in the file LICENSE in the source distribution or at6* https://www.openssl.org/source/license.html7*/89#include <string.h>10#include <openssl/byteorder.h>11#include <openssl/err.h>12#include <openssl/proverr.h>13#include <openssl/x509.h>14#include <openssl/core_names.h>15#include "internal/encoder.h"16#include "prov/ml_dsa.h"17#include "ml_dsa_codecs.h"1819/*-20* Tables describing supported ASN.1 input/output formats.21*/2223/*-24* ML-DSA-44:25* Public key bytes: 1312 (0x0520)26* Private key bytes: 2560 (0x0a00)27*/28static const ML_COMMON_SPKI_FMT ml_dsa_44_spkifmt = {29{300x30,310x82,320x05,330x32,340x30,350x0b,360x06,370x09,380x60,390x86,400x48,410x01,420x65,430x03,440x04,450x03,460x11,470x03,480x82,490x05,500x21,510x00,52}53};54static const ML_COMMON_PKCS8_FMT ml_dsa_44_p8fmt[NUM_PKCS8_FORMATS] = {55{56"seed-priv",570x0a2a,580,590x30820a26,600x0420,616,620x20,630x04820a00,640x2a,650x0a00,660,670,68},69{70"priv-only",710x0a04,720,730x04820a00,740,750,760,770,780x04,790x0a00,800,810,82},83{ "oqskeypair", 0x0f24, 0, 0x04820f20, 0, 0, 0, 0, 0x04, 0x0a00, 0x0a04, 0x0520 },84{85"seed-only",860x0022,872,880x8020,890,902,910x20,920,930,940,950,960,97},98{99"bare-priv",1000x0a00,1014,1020,1030,1040,1050,1060,1070,1080x0a00,1090,1100,111},112{113"bare-seed",1140x0020,1154,1160,1170,1180,1190x20,1200,1210,1220,1230,1240,125},126};127128/*129* ML-DSA-65:130* Public key bytes: 1952 (0x07a0)131* Private key bytes: 4032 (0x0fc0)132*/133static const ML_COMMON_SPKI_FMT ml_dsa_65_spkifmt = {134{1350x30,1360x82,1370x07,1380xb2,1390x30,1400x0b,1410x06,1420x09,1430x60,1440x86,1450x48,1460x01,1470x65,1480x03,1490x04,1500x03,1510x12,1520x03,1530x82,1540x07,1550xa1,1560x00,157}158};159static const ML_COMMON_PKCS8_FMT ml_dsa_65_p8fmt[NUM_PKCS8_FORMATS] = {160{161"seed-priv",1620x0fea,1630,1640x30820fe6,1650x0420,1666,1670x20,1680x04820fc0,1690x2a,1700x0fc0,1710,1720,173},174{175"priv-only",1760x0fc4,1770,1780x04820fc0,1790,1800,1810,1820,1830x04,1840x0fc0,1850,1860,187},188{ "oqskeypair", 0x1764, 0, 0x04821760, 0, 0, 0, 0, 0x04, 0x0fc0, 0x0fc4, 0x07a0 },189{190"seed-only",1910x0022,1922,1930x8020,1940,1952,1960x20,1970,1980,1990,2000,2010,202},203{204"bare-priv",2050x0fc0,2064,2070,2080,2090,2100,2110,2120,2130x0fc0,2140,2150,216},217{218"bare-seed",2190x0020,2204,2210,2220,2230,2240x20,2250,2260,2270,2280,2290,230},231};232233/*-234* ML-DSA-87:235* Public key bytes: 2592 (0x0a20)236* Private key bytes: 4896 (0x1320)237*/238static const ML_COMMON_SPKI_FMT ml_dsa_87_spkifmt = {239{2400x30,2410x82,2420x0a,2430x32,2440x30,2450x0b,2460x06,2470x09,2480x60,2490x86,2500x48,2510x01,2520x65,2530x03,2540x04,2550x03,2560x13,2570x03,2580x82,2590x0a,2600x21,2610x00,262}263};264static const ML_COMMON_PKCS8_FMT ml_dsa_87_p8fmt[NUM_PKCS8_FORMATS] = {265{266"seed-priv",2670x134a,2680,2690x30821346,2700x0420,2716,2720x20,2730x04821320,2740x2a,2750x1320,2760,2770,278},279{280"priv-only",2810x1324,2820,2830x04821320,2840,2850,2860,2870,2880x04,2890x1320,2900,2910,292},293{ "oqskeypair", 0x1d44, 0, 0x04821d40, 0, 0, 0, 0, 0x04, 0x1320, 0x1324, 0x0a20 },294{295"seed-only",2960x0022,2972,2980x8020,2990,3002,3010x20,3020,3030,3040,3050,3060,307},308{309"bare-priv",3100x1320,3114,3120,3130,3140,3150,3160,3170,3180x1320,3190,3200,321},322{323"bare-seed",3240x0020,3254,3260,3270,3280,3290x20,3300,3310,3320,3330,3340,335},336};337338/* Indices of slots in the codec table below */339#define ML_DSA_44_CODEC 0340#define ML_DSA_65_CODEC 1341#define ML_DSA_87_CODEC 2342343/*344* Per-variant fixed parameters345*/346static const ML_COMMON_CODEC codecs[3] = {347{ &ml_dsa_44_spkifmt, ml_dsa_44_p8fmt },348{ &ml_dsa_65_spkifmt, ml_dsa_65_p8fmt },349{ &ml_dsa_87_spkifmt, ml_dsa_87_p8fmt }350};351352/* Retrieve the parameters of one of the ML-DSA variants */353static const ML_COMMON_CODEC *ml_dsa_get_codec(int evp_type)354{355switch (evp_type) {356case EVP_PKEY_ML_DSA_44:357return &codecs[ML_DSA_44_CODEC];358case EVP_PKEY_ML_DSA_65:359return &codecs[ML_DSA_65_CODEC];360case EVP_PKEY_ML_DSA_87:361return &codecs[ML_DSA_87_CODEC];362}363return NULL;364}365366ML_DSA_KEY *367ossl_ml_dsa_d2i_PUBKEY(const uint8_t *pk, int pk_len, int evp_type,368PROV_CTX *provctx, const char *propq)369{370OSSL_LIB_CTX *libctx = PROV_LIBCTX_OF(provctx);371const ML_COMMON_CODEC *codec;372const ML_DSA_PARAMS *params;373ML_DSA_KEY *ret;374375if ((params = ossl_ml_dsa_params_get(evp_type)) == NULL376|| (codec = ml_dsa_get_codec(evp_type)) == NULL)377return NULL;378if (pk_len != ML_COMMON_SPKI_OVERHEAD + (ossl_ssize_t)params->pk_len379|| memcmp(pk, codec->spkifmt->asn1_prefix, ML_COMMON_SPKI_OVERHEAD) != 0)380return NULL;381pk_len -= ML_COMMON_SPKI_OVERHEAD;382pk += ML_COMMON_SPKI_OVERHEAD;383384if ((ret = ossl_ml_dsa_key_new(libctx, propq, evp_type)) == NULL)385return NULL;386387if (!ossl_ml_dsa_pk_decode(ret, pk, (size_t)pk_len)) {388ERR_raise_data(ERR_LIB_PROV, PROV_R_BAD_ENCODING,389"error parsing %s public key from input SPKI",390params->alg);391ossl_ml_dsa_key_free(ret);392return NULL;393}394395return ret;396}397398ML_DSA_KEY *399ossl_ml_dsa_d2i_PKCS8(const uint8_t *prvenc, int prvlen,400int evp_type, PROV_CTX *provctx,401const char *propq)402{403const ML_DSA_PARAMS *v;404const ML_COMMON_CODEC *codec;405ML_COMMON_PKCS8_FMT_PREF *fmt_slots = NULL, *slot;406const ML_COMMON_PKCS8_FMT *p8fmt;407ML_DSA_KEY *key = NULL, *ret = NULL;408PKCS8_PRIV_KEY_INFO *p8inf = NULL;409const uint8_t *buf, *pos;410const X509_ALGOR *alg = NULL;411const char *formats;412int len, ptype;413uint32_t magic;414uint16_t seed_magic;415const uint8_t *seed = NULL;416const uint8_t *priv = NULL;417418/* Which ML-DSA variant? */419if ((v = ossl_ml_dsa_params_get(evp_type)) == NULL420|| (codec = ml_dsa_get_codec(evp_type)) == NULL)421return 0;422423/* Extract the key OID and any parameters. */424if ((p8inf = d2i_PKCS8_PRIV_KEY_INFO(NULL, &prvenc, prvlen)) == NULL)425return 0;426/* Shortest prefix is 4 bytes: seq tag/len + octet string tag/len */427if (!PKCS8_pkey_get0(NULL, &buf, &len, &alg, p8inf))428goto end;429/* Bail out early if this is some other key type. */430if (OBJ_obj2nid(alg->algorithm) != evp_type)431goto end;432433/* Get the list of enabled decoders. Their order is not important here. */434formats = ossl_prov_ctx_get_param(435provctx, OSSL_PKEY_PARAM_ML_DSA_INPUT_FORMATS, NULL);436fmt_slots = ossl_ml_common_pkcs8_fmt_order(v->alg, codec->p8fmt,437"input", formats);438if (fmt_slots == NULL)439goto end;440441/* Parameters must be absent. */442X509_ALGOR_get0(NULL, &ptype, NULL, alg);443if (ptype != V_ASN1_UNDEF) {444ERR_raise_data(ERR_LIB_PROV, PROV_R_UNEXPECTED_KEY_PARAMETERS,445"unexpected parameters with a PKCS#8 %s private key",446v->alg);447goto end;448}449if ((ossl_ssize_t)len < (ossl_ssize_t)sizeof(magic))450goto end;451452/* Find the matching p8 info slot, that also has the expected length. */453pos = OPENSSL_load_u32_be(&magic, buf);454for (slot = fmt_slots; (p8fmt = slot->fmt) != NULL; ++slot) {455if (len != (ossl_ssize_t)p8fmt->p8_bytes)456continue;457if (p8fmt->p8_shift == sizeof(magic)458|| (magic >> (p8fmt->p8_shift * 8)) == p8fmt->p8_magic) {459pos -= p8fmt->p8_shift;460break;461}462}463if (p8fmt == NULL464|| (p8fmt->seed_length > 0 && p8fmt->seed_length != ML_DSA_SEED_BYTES)465|| (p8fmt->priv_length > 0 && p8fmt->priv_length != v->sk_len)466|| (p8fmt->pub_length > 0 && p8fmt->pub_length != v->pk_len)) {467ERR_raise_data(ERR_LIB_PROV, PROV_R_ML_DSA_NO_FORMAT,468"no matching enabled %s private key input formats",469v->alg);470goto end;471}472473if (p8fmt->seed_length > 0) {474/* Check |seed| tag/len, if not subsumed by |magic|. */475if (pos + sizeof(uint16_t) == buf + p8fmt->seed_offset) {476pos = OPENSSL_load_u16_be(&seed_magic, pos);477if (seed_magic != p8fmt->seed_magic)478goto end;479} else if (pos != buf + p8fmt->seed_offset) {480goto end;481}482pos += ML_DSA_SEED_BYTES;483}484if (p8fmt->priv_length > 0) {485/* Check |priv| tag/len */486if (pos + sizeof(uint32_t) == buf + p8fmt->priv_offset) {487pos = OPENSSL_load_u32_be(&magic, pos);488if (magic != p8fmt->priv_magic)489goto end;490} else if (pos != buf + p8fmt->priv_offset) {491goto end;492}493pos += v->sk_len;494}495if (p8fmt->pub_length > 0) {496if (pos != buf + p8fmt->pub_offset)497goto end;498pos += v->pk_len;499}500if (pos != buf + len)501goto end;502503/*504* Collect the seed and/or key into a "decoded" private key object,505* to be turned into a real key on provider "load" or "import".506*/507if ((key = ossl_prov_ml_dsa_new(provctx, propq, evp_type)) == NULL)508goto end;509if (p8fmt->seed_length > 0)510seed = buf + p8fmt->seed_offset;511if (p8fmt->priv_length > 0)512priv = buf + p8fmt->priv_offset;513/* Any OQS public key content is ignored */514515if (ossl_ml_dsa_set_prekey(key, 0, 0,516seed, ML_DSA_SEED_BYTES, priv, v->sk_len))517ret = key;518519end:520OPENSSL_free(fmt_slots);521PKCS8_PRIV_KEY_INFO_free(p8inf);522if (ret == NULL)523ossl_ml_dsa_key_free(key);524return ret;525}526527/* Same as ossl_ml_dsa_encode_pubkey, but allocates the output buffer. */528int ossl_ml_dsa_i2d_pubkey(const ML_DSA_KEY *key, unsigned char **out)529{530const ML_DSA_PARAMS *params = ossl_ml_dsa_key_params(key);531const uint8_t *pk = ossl_ml_dsa_key_get_pub(key);532533if (pk == NULL) {534ERR_raise_data(ERR_LIB_PROV, PROV_R_NOT_A_PUBLIC_KEY,535"no %s public key data available", params->alg);536return 0;537}538if (out != NULL539&& (*out = OPENSSL_memdup(pk, params->pk_len)) == NULL)540return 0;541return (int)params->pk_len;542}543544/* Allocate and encode PKCS#8 private key payload. */545int ossl_ml_dsa_i2d_prvkey(const ML_DSA_KEY *key, uint8_t **out,546PROV_CTX *provctx)547{548const ML_DSA_PARAMS *params = ossl_ml_dsa_key_params(key);549const ML_COMMON_CODEC *codec;550ML_COMMON_PKCS8_FMT_PREF *fmt_slots, *slot;551const ML_COMMON_PKCS8_FMT *p8fmt;552uint8_t *buf = NULL, *pos;553const char *formats;554int len = ML_DSA_SEED_BYTES;555int ret = 0;556const uint8_t *seed = ossl_ml_dsa_key_get_seed(key);557const uint8_t *sk = ossl_ml_dsa_key_get_priv(key);558559/* Not ours to handle */560if ((codec = ml_dsa_get_codec(params->evp_type)) == NULL)561return 0;562563if (sk == NULL) {564ERR_raise_data(ERR_LIB_PROV, PROV_R_NOT_A_PRIVATE_KEY,565"no %s private key data available",566params->alg);567return 0;568}569570formats = ossl_prov_ctx_get_param(571provctx, OSSL_PKEY_PARAM_ML_DSA_OUTPUT_FORMATS, NULL);572fmt_slots = ossl_ml_common_pkcs8_fmt_order(params->alg, codec->p8fmt,573"output", formats);574if (fmt_slots == NULL)575return 0;576577/* If we don't have a seed, skip seedful entries */578for (slot = fmt_slots; (p8fmt = slot->fmt) != NULL; ++slot)579if (seed != NULL || p8fmt->seed_length == 0)580break;581/* No matching table entries, give up */582if (p8fmt == NULL583|| (p8fmt->seed_length > 0 && p8fmt->seed_length != ML_DSA_SEED_BYTES)584|| (p8fmt->priv_length > 0 && p8fmt->priv_length != params->sk_len)585|| (p8fmt->pub_length > 0 && p8fmt->pub_length != params->pk_len)) {586ERR_raise_data(ERR_LIB_PROV, PROV_R_ML_DSA_NO_FORMAT,587"no matching enabled %s private key output formats",588params->alg);589goto end;590}591len = p8fmt->p8_bytes;592593if (out == NULL) {594ret = len;595goto end;596}597598if ((pos = buf = OPENSSL_malloc((size_t)len)) == NULL)599goto end;600601switch (p8fmt->p8_shift) {602case 0:603pos = OPENSSL_store_u32_be(pos, p8fmt->p8_magic);604break;605case 2:606pos = OPENSSL_store_u16_be(pos, (uint16_t)p8fmt->p8_magic);607break;608case 4:609break;610default:611ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR,612"error encoding %s private key", params->alg);613goto end;614}615616if (p8fmt->seed_length != 0) {617/*618* Either the tag/len were already included in |magic| or they require619* us to write two bytes now.620*/621if (pos + sizeof(uint16_t) == buf + p8fmt->seed_offset)622pos = OPENSSL_store_u16_be(pos, p8fmt->seed_magic);623if (pos != buf + p8fmt->seed_offset) {624ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR,625"error encoding %s private key", params->alg);626goto end;627}628memcpy(pos, seed, ML_DSA_SEED_BYTES);629pos += ML_DSA_SEED_BYTES;630}631if (p8fmt->priv_length != 0) {632if (pos + sizeof(uint32_t) == buf + p8fmt->priv_offset)633pos = OPENSSL_store_u32_be(pos, p8fmt->priv_magic);634if (pos != buf + p8fmt->priv_offset) {635ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR,636"error encoding %s private key", params->alg);637goto end;638}639memcpy(pos, sk, params->sk_len);640pos += params->sk_len;641}642/* OQS form output with tacked-on public key */643if (p8fmt->pub_length != 0) {644/* The OQS pubkey is never separately DER-wrapped */645if (pos != buf + p8fmt->pub_offset) {646ERR_raise_data(ERR_LIB_PROV, ERR_R_INTERNAL_ERROR,647"error encoding %s private key", params->alg);648goto end;649}650memcpy(pos, ossl_ml_dsa_key_get_pub(key), params->pk_len);651pos += params->pk_len;652}653654if (pos == buf + len) {655*out = buf;656ret = len;657}658659end:660OPENSSL_free(fmt_slots);661if (ret == 0)662OPENSSL_free(buf);663return ret;664}665666int ossl_ml_dsa_key_to_text(BIO *out, const ML_DSA_KEY *key, int selection)667{668const ML_DSA_PARAMS *params;669const uint8_t *seed, *sk, *pk;670671if (out == NULL || key == NULL) {672ERR_raise(ERR_LIB_PROV, ERR_R_PASSED_NULL_PARAMETER);673return 0;674}675params = ossl_ml_dsa_key_params(key);676pk = ossl_ml_dsa_key_get_pub(key);677sk = ossl_ml_dsa_key_get_priv(key);678seed = ossl_ml_dsa_key_get_seed(key);679680if (pk == NULL) {681/* Regardless of the |selection|, there must be a public key */682ERR_raise_data(ERR_LIB_PROV, PROV_R_MISSING_KEY,683"no %s key material available", params->alg);684return 0;685}686687if ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0) {688if (sk == NULL) {689ERR_raise_data(ERR_LIB_PROV, PROV_R_MISSING_KEY,690"no %s key material available", params->alg);691return 0;692}693if (BIO_printf(out, "%s Private-Key:\n", params->alg) <= 0)694return 0;695if (seed != NULL && !ossl_bio_print_labeled_buf(out, "seed:", seed, ML_DSA_SEED_BYTES))696return 0;697if (!ossl_bio_print_labeled_buf(out, "priv:", sk, params->sk_len))698return 0;699} else if ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) {700if (BIO_printf(out, "%s Public-Key:\n", params->alg) <= 0)701return 0;702}703704if (!ossl_bio_print_labeled_buf(out, "pub:", pk, params->pk_len))705return 0;706707return 1;708}709710711