Skip to content

Commit 5857207

Browse files
authored
Add RSA private-key COSE_Key round-trip (RFC 8230 d,p,q,qInv) (#34) (#46)
2 parents 467669f + 614ad23 commit 5857207

6 files changed

Lines changed: 172 additions & 25 deletions

File tree

include/wolfcose/settings.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,13 @@ extern "C" {
190190
defined(WOLFCOSE_HAVE_PS512)
191191
#define WOLFCOSE_HAVE_RSAPSS
192192
#endif
193+
/* Private RSA round-trip needs wc_export_int + RsaKey.u; else public-only. */
194+
#if defined(WOLFCOSE_HAVE_RSAPSS) && !defined(WOLFCOSE_RSA_PUBLIC_ONLY) && \
195+
!defined(WOLFSSL_RSA_PUBLIC_ONLY) && \
196+
(defined(HAVE_ECC) || defined(WOLFSSL_EXPORT_INT)) && \
197+
(defined(WOLFSSL_KEY_GEN) || defined(OPENSSL_EXTRA) || !defined(RSA_LOW_MEM))
198+
#define WOLFCOSE_HAVE_RSA_PRIVATE_KEY
199+
#endif
193200
#if defined(WOLFCOSE_HAVE_ECDSA) || defined(WOLFCOSE_HAVE_EDDSA) || \
194201
defined(WOLFCOSE_HAVE_ED448) || defined(WOLFCOSE_HAVE_RSAPSS) || \
195202
defined(WOLFCOSE_HAVE_MLDSA)
@@ -466,7 +473,12 @@ extern "C" {
466473
#endif
467474
#ifndef WOLFCOSE_MAX_MAP_ITEMS
468475
#if defined(WOLFCOSE_MIN_BUFFERS)
469-
#define WOLFCOSE_MAX_MAP_ITEMS 8u
476+
/* Full private RSA key = 9 entries (kty,n,e,d,p,q,qInv,kid,alg). */
477+
#if defined(WOLFCOSE_HAVE_RSAPSS)
478+
#define WOLFCOSE_MAX_MAP_ITEMS 9u
479+
#else
480+
#define WOLFCOSE_MAX_MAP_ITEMS 8u
481+
#endif
470482
#else
471483
#define WOLFCOSE_MAX_MAP_ITEMS 16u
472484
#endif

include/wolfcose/wolfcose.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ extern "C" {
240240
#define WOLFCOSE_KEY_LABEL_Y (-3)
241241
#define WOLFCOSE_KEY_LABEL_D (-4)
242242
#define WOLFCOSE_KEY_LABEL_K (-1) /* Symmetric key value */
243+
#define WOLFCOSE_KEY_LABEL_RSA_P (-4) /* RFC 8230: first prime */
244+
#define WOLFCOSE_KEY_LABEL_RSA_Q (-5) /* RFC 8230: second prime */
245+
#define WOLFCOSE_KEY_LABEL_RSA_QINV (-8) /* RFC 8230: CRT coefficient */
243246

244247
/* AES-GCM constants */
245248
#define WOLFCOSE_AES_GCM_TAG_SZ 16

scripts/cmdline-test.sh

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,29 @@ for A in $SIGN_ALGS; do
6868
fi
6969
done
7070

71-
# RSA-PSS keygen works, but COSE_Key currently stores only the public part
72-
# (n,e,d) — full private round-trip (p,q,qInv) is pending issue #34 — so we
73-
# only smoke-test keygen here, not sign/verify.
74-
echo "== RSA-PSS: keygen only (round-trip pending #34) =="
71+
# Public-only RSA builds can't sign a decoded key, so skip; the self-test
72+
# still covers RSA signing.
73+
echo "== RSA-PSS: keygen -> sign -> verify -> self-test =="
7574
for A in PS256 PS384 PS512; do
76-
K="$WORK/rsa.key"
75+
K="$WORK/rsa.key"; C="$WORK/rsa.cose"
7776
if ! keygen_or "$A" "$K" 0; then continue; fi
78-
skip "$A sign/verify" "round-trip pending #34"
79-
ok "$A keygen"
77+
if "$TOOL" sign -k "$K" -a "$A" -i "$IN" -o "$C" >/dev/null 2>&1; then
78+
if "$TOOL" verify -k "$K" -i "$C" >/dev/null 2>&1; then
79+
ok "$A sign/verify"
80+
else
81+
bad "$A verify"
82+
fi
83+
elif [ "$(wc -c < "$K")" -lt 500 ]; then
84+
# Public-only key (n,e only) is ~271B; a full private key is ~924B.
85+
skip "$A sign/verify" "public-only RSA COSE_Key build"
86+
else
87+
bad "$A sign"
88+
fi
89+
if "$TOOL" test -a "$A" >/dev/null 2>&1; then
90+
ok "$A self-test"
91+
else
92+
bad "$A self-test"
93+
fi
8094
done
8195

8296
echo "== Tamper detection: corrupted COSE_Sign1 must NOT verify =="

src/wolfcose.c

Lines changed: 120 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,40 @@ static int wolfCose_EncodeKeyOptionalFields(WOLFCOSE_CBOR_CTX* ctx,
13001300
return ret;
13011301
}
13021302

1303+
#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY
1304+
/* RFC 8230: write one RSA component (label + bstr) from its mp_int. */
1305+
static int wolfCose_EncodeRsaMp(WOLFCOSE_CBOR_CTX* ctx, int64_t label,
1306+
mp_int* a, word32 keySz)
1307+
{
1308+
int ret = WOLFCOSE_SUCCESS;
1309+
word32 len = keySz;
1310+
1311+
if (keySz == 0u) {
1312+
ret = WOLFCOSE_E_INVALID_ARG;
1313+
}
1314+
if (ret == WOLFCOSE_SUCCESS) {
1315+
ret = wc_CBOR_EncodeInt(ctx, label);
1316+
}
1317+
if (ret == WOLFCOSE_SUCCESS) {
1318+
ret = wolfCose_CBOR_EncodeHead(ctx, WOLFCOSE_CBOR_BSTR,
1319+
(uint64_t)keySz);
1320+
}
1321+
if (ret == WOLFCOSE_SUCCESS) {
1322+
if ((ctx->idx + (size_t)keySz) > ctx->bufSz) {
1323+
ret = WOLFCOSE_E_BUFFER_TOO_SMALL;
1324+
}
1325+
else if (wc_export_int(a, &ctx->buf[ctx->idx], &len, keySz,
1326+
WC_TYPE_UNSIGNED_BIN) != 0) {
1327+
ret = WOLFCOSE_E_CRYPTO;
1328+
}
1329+
else {
1330+
ctx->idx += (size_t)len;
1331+
}
1332+
}
1333+
return ret;
1334+
}
1335+
#endif /* WOLFCOSE_HAVE_RSA_PRIVATE_KEY */
1336+
13031337
int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz,
13041338
size_t* outLen)
13051339
{
@@ -1412,19 +1446,40 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz,
14121446
#endif /* HAVE_ECC */
14131447
#ifdef WOLFCOSE_HAVE_RSAPSS
14141448
if (key->kty == WOLFCOSE_KTY_RSA) {
1415-
/* RFC 8230: {1:3, -1:n_bstr, -2:e_bstr [, -3:d_bstr]}
1416-
* Export n and d directly into output buffer to avoid
1417-
* large stack allocations (RSA-4096 modulus = 512 bytes). */
1449+
/* RFC 8230: {1:3, -1:n, -2:e [, -3:d, -4:p, -5:q, -8:qInv]}.
1450+
* Export large components straight into the output buffer to
1451+
* avoid stack copies (RSA-4096 modulus = 512 bytes). */
14181452
uint8_t eBuf[8]; /* exponent, typically 3 bytes */
14191453
word32 eLen = (word32)sizeof(eBuf);
14201454
word32 nLen;
14211455
size_t hdrPos;
14221456
size_t mapEntries;
1457+
int rsaPriv = 0;
1458+
#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY
1459+
word32 halfSz = 0;
1460+
#endif
14231461

1462+
if (key->key.rsa == NULL) {
1463+
ret = WOLFCOSE_E_INVALID_ARG;
1464+
}
1465+
/* Private round-trip needs the CRT export; else public-only. */
1466+
#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY
1467+
if ((ret == WOLFCOSE_SUCCESS) && (key->hasPrivate != 0u)) {
1468+
rsaPriv = 1;
1469+
#ifdef WOLF_CRYPTO_CB
1470+
/* Device-backed keys have no local CRT to export. */
1471+
if (key->key.rsa->devId != INVALID_DEVID) {
1472+
rsaPriv = 0;
1473+
}
1474+
#endif
1475+
}
1476+
#endif
14241477
/* Get n directly into output buffer, e into small stack buf */
1425-
mapEntries = (key->hasPrivate != 0u) ? (size_t)4 : (size_t)3;
1478+
mapEntries = (rsaPriv != 0) ? (size_t)7 : (size_t)3;
14261479
mapEntries += wolfCose_KeyOptionalEntries(key);
1427-
ret = wc_CBOR_EncodeMapStart(&ctx, mapEntries);
1480+
if (ret == WOLFCOSE_SUCCESS) {
1481+
ret = wc_CBOR_EncodeMapStart(&ctx, mapEntries);
1482+
}
14281483

14291484
/* 1: kty */
14301485
if (ret == WOLFCOSE_SUCCESS) {
@@ -1477,7 +1532,7 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz,
14771532
ret = wc_CBOR_EncodeBstr(&ctx, eBuf, (size_t)eLen);
14781533
}
14791534
/* -3: d (private exponent, optional) — direct export */
1480-
if ((ret == WOLFCOSE_SUCCESS) && (key->hasPrivate != 0u)) {
1535+
if ((ret == WOLFCOSE_SUCCESS) && (rsaPriv != 0)) {
14811536
ret = wc_CBOR_EncodeInt(&ctx,
14821537
(int64_t)WOLFCOSE_KEY_LABEL_Y);
14831538
if (ret == WOLFCOSE_SUCCESS) {
@@ -1555,6 +1610,30 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz,
15551610
}
15561611
}
15571612
}
1613+
#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY
1614+
/* -4 p, -5 q, -8 qInv: CRT factors so a decoded key can sign. */
1615+
if ((ret == WOLFCOSE_SUCCESS) && (rsaPriv != 0)) {
1616+
int modSz = wc_RsaEncryptSize(key->key.rsa);
1617+
if (modSz <= 0) {
1618+
ret = WOLFCOSE_E_CRYPTO;
1619+
}
1620+
else {
1621+
halfSz = ((word32)modSz + 1u) / 2u;
1622+
}
1623+
}
1624+
if ((ret == WOLFCOSE_SUCCESS) && (rsaPriv != 0)) {
1625+
ret = wolfCose_EncodeRsaMp(&ctx, WOLFCOSE_KEY_LABEL_RSA_P,
1626+
&key->key.rsa->p, halfSz);
1627+
}
1628+
if ((ret == WOLFCOSE_SUCCESS) && (rsaPriv != 0)) {
1629+
ret = wolfCose_EncodeRsaMp(&ctx, WOLFCOSE_KEY_LABEL_RSA_Q,
1630+
&key->key.rsa->q, halfSz);
1631+
}
1632+
if ((ret == WOLFCOSE_SUCCESS) && (rsaPriv != 0)) {
1633+
ret = wolfCose_EncodeRsaMp(&ctx, WOLFCOSE_KEY_LABEL_RSA_QINV,
1634+
&key->key.rsa->u, halfSz);
1635+
}
1636+
#endif /* WOLFCOSE_HAVE_RSA_PRIVATE_KEY */
15581637

15591638
if (ret == WOLFCOSE_SUCCESS) {
15601639
*outLen = ctx.idx;
@@ -1851,10 +1930,16 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz)
18511930
size_t xLen = 0;
18521931
const uint8_t* yData = NULL; /* EC2: y coord, RSA: d (private exp) */
18531932
size_t yLen = 0;
1854-
const uint8_t* dData = NULL; /* EC2/OKP: private key */
1933+
const uint8_t* dData = NULL; /* EC2/OKP: private key, RSA: p */
18551934
size_t dLen = 0;
18561935
const uint8_t* nData = NULL; /* RSA: n (modulus) */
18571936
size_t nLen = 0;
1937+
#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY
1938+
const uint8_t* qData = NULL; /* RSA: q (second prime) */
1939+
size_t qLen = 0;
1940+
const uint8_t* qiData = NULL; /* RSA: qInv (CRT coefficient) */
1941+
size_t qiLen = 0;
1942+
#endif
18581943
WOLFCOSE_HDR_STATE keyLabelState;
18591944

18601945
if ((key == NULL) || (in == NULL) || (inSz == 0u)) {
@@ -1975,6 +2060,16 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz)
19752060
(label == WOLFCOSE_KEY_LABEL_D)) {
19762061
ret = wc_CBOR_DecodeBstr(&ctx, &dData, &dLen);
19772062
}
2063+
#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY
2064+
else if ((ret == WOLFCOSE_SUCCESS) &&
2065+
(label == WOLFCOSE_KEY_LABEL_RSA_Q)) {
2066+
ret = wc_CBOR_DecodeBstr(&ctx, &qData, &qLen);
2067+
}
2068+
else if ((ret == WOLFCOSE_SUCCESS) &&
2069+
(label == WOLFCOSE_KEY_LABEL_RSA_QINV)) {
2070+
ret = wc_CBOR_DecodeBstr(&ctx, &qiData, &qiLen);
2071+
}
2072+
#endif
19782073
else {
19792074
if (ret == WOLFCOSE_SUCCESS) {
19802075
ret = wc_CBOR_Skip(&ctx);
@@ -2072,10 +2167,27 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz)
20722167
#endif
20732168
#ifdef WOLFCOSE_HAVE_RSAPSS
20742169
if ((key->kty == WOLFCOSE_KTY_RSA) && (key->key.rsa != NULL)) {
2075-
/* RFC 8230: -1=n(bstr), -2=e(bstr), -3=d(bstr) */
2170+
/* RFC 8230: -1 n, -2 e, -3 d, -4 p, -5 q, -8 qInv. */
20762171
if ((nData == NULL) || (xData == NULL)) {
20772172
ret = WOLFCOSE_E_COSE_BAD_HDR;
20782173
}
2174+
#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY
2175+
else if ((yData != NULL) && (dData != NULL) &&
2176+
(qData != NULL) && (qiData != NULL)) {
2177+
INJECT_FAILURE(WOLF_FAIL_RSA_PUBLIC_DECODE, -1)
2178+
ret = wc_RsaPrivateKeyDecodeRaw(nData, (word32)nLen,
2179+
xData, (word32)xLen, yData, (word32)yLen,
2180+
qiData, (word32)qiLen, dData, (word32)dLen,
2181+
qData, (word32)qLen, NULL, 0, NULL, 0,
2182+
key->key.rsa);
2183+
if (ret != 0) {
2184+
ret = WOLFCOSE_E_CRYPTO;
2185+
}
2186+
else {
2187+
key->hasPrivate = 1u;
2188+
}
2189+
}
2190+
#endif /* WOLFCOSE_HAVE_RSA_PRIVATE_KEY */
20792191
else {
20802192
INJECT_FAILURE(WOLF_FAIL_RSA_PUBLIC_DECODE, -1)
20812193
{
@@ -2086,9 +2198,6 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz)
20862198
ret = WOLFCOSE_E_CRYPTO;
20872199
}
20882200
else {
2089-
/* TODO(#34): public key only. RSA private round-trip
2090-
* needs n,e,d,p,q,qInv via wc_RsaPrivateKeyDecodeRaw;
2091-
* the COSE_Key format must also carry p,q,qInv. */
20922201
key->hasPrivate = 0u;
20932202
}
20942203
}

tests/test_cose.c

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,6 +1681,16 @@ static void test_cose_key_rsa(void)
16811681
size_t cLen = 0;
16821682
WOLFCOSE_KEY key2;
16831683
RsaKey rsaKey2;
1684+
WOLFCOSE_KEY* signKey;
1685+
WOLFCOSE_KEY* verifyKey;
1686+
int expectPriv;
1687+
1688+
/* Capable builds sign with the decoded key; public-only the original. */
1689+
#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY
1690+
signKey = &key2; verifyKey = &key; expectPriv = 1;
1691+
#else
1692+
signKey = &key; verifyKey = &key2; expectPriv = 0;
1693+
#endif
16841694

16851695
ret = wc_CoseKey_Encode(&key, cbuf, sizeof(cbuf), &cLen);
16861696
TEST_ASSERT(ret == 0 && cLen > 0, "key rsa encode");
@@ -1691,11 +1701,12 @@ static void test_cose_key_rsa(void)
16911701
ret = wc_CoseKey_Decode(&key2, cbuf, cLen);
16921702
TEST_ASSERT(ret == 0 && key2.kty == WOLFCOSE_KTY_RSA &&
16931703
key2.alg == WOLFCOSE_ALG_PS256 &&
1704+
key2.hasPrivate == expectPriv &&
16941705
key2.kidLen == (sizeof(kid) - 1u) &&
16951706
memcmp(key2.kid, kid, sizeof(kid) - 1u) == 0,
16961707
"key rsa decode");
16971708

1698-
/* Verify decoded key can sign/verify */
1709+
/* #34: a private round-trip signs with the decoded key. */
16991710
/* empty-brace-scan: allow - test-local temporary scope */
17001711
{
17011712
uint8_t payload[] = "RSA key round-trip";
@@ -1706,17 +1717,15 @@ static void test_cose_key_rsa(void)
17061717
size_t decPayloadLen = 0;
17071718
WOLFCOSE_HDR hdr;
17081719

1709-
/* Sign with original key */
1710-
ret = wc_CoseSign1_Sign(&key, WOLFCOSE_ALG_PS256,
1720+
ret = wc_CoseSign1_Sign(signKey, WOLFCOSE_ALG_PS256,
17111721
NULL, 0, payload, sizeof(payload) - 1,
17121722
NULL, 0, /* detachedPayload, detachedLen */
17131723
NULL, 0, /* extAad, extAadLen */
17141724
scratch, sizeof(scratch),
17151725
out, sizeof(out), &outLen, &rng);
17161726
TEST_ASSERT(ret == 0, "key rsa rt sign");
17171727

1718-
/* Verify with decoded key (public only) */
1719-
ret = wc_CoseSign1_Verify(&key2, out, outLen,
1728+
ret = wc_CoseSign1_Verify(verifyKey, out, outLen,
17201729
NULL, 0, /* detachedPayload, detachedLen */
17211730
NULL, 0, /* extAad, extAadLen */
17221731
scratch, sizeof(scratch),

tools/wolfcose_tool.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
/* ML-DSA-87: pub=2592 + priv=4896 + CBOR overhead */
7474
#define WOLFCOSE_TOOL_MAX_KEY 8192
7575
#else
76-
/* RSA-2048 private COSE_Key: n+d plus e2/n2/p/q export scratch */
76+
/* RSA-2048 private COSE_Key: n,e,d,p,q,qInv plus export scratch */
7777
#define WOLFCOSE_TOOL_MAX_KEY 4096
7878
#endif
7979
#endif

0 commit comments

Comments
 (0)