// file : openssl/agent/pkcs11/private-key.cxx -*- C++ -*- // copyright : Copyright (c) 2014-2019 Code Synthesis Ltd // license : MIT; see accompanying LICENSE file #include #include // memset(), strlen() #include #define API_STRING(S) api_string (S, sizeof (S)) namespace openssl { namespace agent { namespace pkcs11 { using namespace std; static void close_session (CK_SESSION_HANDLE* p) { if (p != nullptr) { api ()->C_CloseSession (*p); delete p; } } private_key:: private_key (): session_ (nullptr, close_session) { } private_key:: private_key (private_key&& k): private_key () { *this = move (k); } private_key& private_key:: operator= (private_key&& k) { description = move (k.description); session_ = move (k.session_); handle_ = k.handle_; simulate_ = move (k.simulate_); return *this; } private_key:: private_key (const identity& idn, const access& acc, const char* pin, optional sim) : private_key () { simulate_ = move (sim); // Can't initialize because of delegate ctor. // Verify the identity and access attributes. // if (idn.type && *idn.type != "private") throw invalid_argument ("unexpected object type " + *idn.type); if (pin == nullptr && acc.pin_value) pin = acc.pin_value->c_str (); if (pin == nullptr) throw invalid_argument ("PIN is required"); auto no_key = [] () { throw runtime_error ("no matching private key found"); }; auto simulate = [this, &no_key] () { assert (simulate_ && session_ == nullptr); switch (*simulate_) { case simulate_outcome::success: { description = "simulated private key"; return; } case simulate_outcome::failure: { no_key (); } } }; // Get the Cryptoki functions pointer. // CK_FUNCTION_LIST* fs; { path p; if (acc.module_path) { if (acc.module_name) p = *acc.module_path / (*acc.module_name + ".so"); else if (file_exists (*acc.module_path)) p = *acc.module_path; else p = *acc.module_path / "opensc-pkcs11.so"; } else if (acc.module_name) p = *acc.module_name + ".so"; else p = path ("opensc-pkcs11.so"); // Ignore non-existent PKCS#11 module in the simulated mode. Note // that if the module exists we will simulate as far as possible, // up to opening token sessions (but not logging into them). // fs = api (p, simulate_.has_value ()); if (fs == nullptr) { assert (simulate_); simulate (); return; } } // Search for the private key. // // The overall plan is to match the PKCS#11 module, go though all the // matching slots it provides, log into all the matching tokens these // slots may contain and see if the tokens contain a matching RSA // private keys. Fail if unable to log into the matching tokens or // none or multiple private keys match. Otherwise keep both the // matching token session and the key handle. // auto mult_keys = [] () { throw runtime_error ("multiple private keys match"); }; auto match = [] (const auto& attr, const auto& value) { return !attr || *attr == value; }; // Match the module. // CK_INFO pi; CK_RV r (fs->C_GetInfo (&pi)); if (r != CKR_OK) throw_api_error (r, "unable to obtain PKCS#11 module info"); if (!match (idn.library_manufacturer, API_STRING (pi.manufacturerID)) || !match (idn.library_version, library_version (pi.libraryVersion.major, pi.libraryVersion.minor)) || !match (idn.library_description, API_STRING (pi.libraryDescription))) no_key (); // Traverse through slots. // // In the PKCS#11 terminology a slot is something that may have token // be inserted into it. Not to be confused with Yubikey's 9a, 9c, etc // slots. // // Note that a set of slots is checked at the time when // C_GetSlotList() is called to obtain slots count (pSlotList argument // is NULL and the tokenPresent argument is false). // CK_ULONG nslots; r = fs->C_GetSlotList (false /* tokenPresent */, nullptr /* pSlotList */, &nslots); if (r != CKR_OK) throw_api_error (r, "unable to obtain slots count"); vector slot_ids (nslots); r = fs->C_GetSlotList (false /* tokenPresent */, slot_ids.data (), &nslots); if (r != CKR_OK) throw_api_error (r, "unable to obtain slot ids"); for (CK_SLOT_ID sid: slot_ids) { // Match the slot information. // if (!match (idn.slot_id, sid)) continue; CK_SLOT_INFO si; r = fs->C_GetSlotInfo (sid, &si); if (r != CKR_OK) throw_api_error ( r, "unable to obtain slot " + to_string (sid) + " info"); if ((si.flags & CKF_TOKEN_PRESENT) == 0 || !match (idn.slot_manufacturer, API_STRING (si.manufacturerID)) || !match (idn.slot_description, API_STRING (si.slotDescription))) continue; auto slot_desc = [&sid, &si] () { string d (API_STRING (si.slotDescription)); return "slot " + to_string (sid) + " (" + (!d.empty () ? d : API_STRING (si.manufacturerID)) + ")"; }; // Match the token information. // CK_TOKEN_INFO ti; r = fs->C_GetTokenInfo (sid, &ti); if (r == CKR_TOKEN_NOT_PRESENT || r == CKR_TOKEN_NOT_RECOGNIZED) continue; if (r != CKR_OK) throw_api_error ( r, "unable to obtain token info for " + slot_desc ()); if ((ti.flags & CKF_TOKEN_INITIALIZED) == 0 || (ti.flags & CKF_USER_PIN_INITIALIZED) == 0 || (ti.flags & CKF_LOGIN_REQUIRED) == 0 || (ti.flags & CKF_PROTECTED_AUTHENTICATION_PATH) != 0 || // Pinpad? !match (idn.serial, API_STRING (ti.serialNumber)) || !match (idn.token, API_STRING (ti.label)) || !match (idn.model, API_STRING (ti.model)) || !match (idn.manufacturer, API_STRING (ti.manufacturerID))) continue; auto token_desc = [&ti] () { string r ("token "); string l (API_STRING (ti.label)); r += !l.empty () ? "'" + l + "'" : "'" + API_STRING (ti.model) + "' by " + API_STRING (ti.manufacturerID); return r; }; // Search for the matching RSA private key in the token. // // Open the read-only session with the token. Note that we can't see // private objects until login. // CK_SESSION_HANDLE sh; r = fs->C_OpenSession (sid, CKF_SERIAL_SESSION, nullptr /* pApplication */, nullptr /* Notify */, &sh); if (r == CKR_DEVICE_REMOVED || r == CKR_TOKEN_NOT_PRESENT || r == CKR_TOKEN_NOT_RECOGNIZED) continue; if (r != CKR_OK) throw_api_error ( r, "unable to open session with " + token_desc ()); session_ptr session (new CK_SESSION_HANDLE (sh), close_session); // Log into the token unless simulating. // if (simulate_) continue; // Note that all sessions with a token share login, so need to login // once per all token sessions. // // Also note that there is no need to logout explicitly if you want // to keep all token sessions logged in during their lifetime. // r = fs->C_Login ( *session, CKU_USER, reinterpret_cast (const_cast (pin)), strlen (pin)); // We can fail because of the wrong PIN or trying to apply the PIN // to a wrong token. Should we just skip the token if that's the // case? Probably we should throw. Otherwise who knows how many pins // we will reset going forward. // if (r != CKR_USER_ALREADY_LOGGED_IN && r != CKR_OK) throw_api_error (r, "unable to login to " + token_desc ()); // Search for the private key. // // Fill the search attributes. // CK_OBJECT_CLASS oc (CKO_PRIVATE_KEY); CK_KEY_TYPE kt (CKK_RSA); vector sa ({{CKA_CLASS, &oc, sizeof (oc)}, {CKA_KEY_TYPE, &kt, sizeof (kt)}}); if (idn.id) sa.push_back ( CK_ATTRIBUTE {CKA_ID, const_cast (idn.id->data ()), idn.id->size ()}); if (idn.object) sa.push_back (CK_ATTRIBUTE {CKA_LABEL, const_cast (idn.object->c_str ()), idn.object->size ()}); // Initialize the search. // r = fs->C_FindObjectsInit (*session, sa.data (), sa.size ()); if (r != CKR_OK) throw_api_error ( r, "unable to enumerate private keys in " + token_desc ()); // Finally, search for the key. // // Note that we will query 2 keys to handle the 'multiple keys // match' case. // CK_OBJECT_HANDLE key (0); { session_ptr search_deleter ( session.get (), [] (CK_SESSION_HANDLE* p) {api ()->C_FindObjectsFinal (*p);}); CK_ULONG n; CK_OBJECT_HANDLE keys[2]; r = fs->C_FindObjects (*session, keys, 2 /* ulMaxObjectCount */, &n); if (r != CKR_OK) throw_api_error ( r, "unable to obtain private key handles from " + token_desc ()); if (n == 1) key = keys[0]; // Exactly one key matches. else if (n == 0) continue; // No key matches. else mult_keys (); // Multiple keys match. } if (session_ != nullptr) mult_keys (); // Produce description for the found key. // description = "private key "; { CK_ATTRIBUTE attr {CKA_LABEL, nullptr, 0}; r = fs->C_GetAttributeValue (*session, key, &attr, 1 /* ulCount */); if (r == CKR_OK && attr.ulValueLen != 0) { vector label (attr.ulValueLen); attr.pValue = label.data (); r = fs->C_GetAttributeValue (*session, key, &attr, 1 /* ulCount */); if (r == CKR_OK) description += "'" + string (label.data (), label.size ()) + "' "; } } description += "in " + token_desc (); // Note that for Yubikey 4 we cannot rely on the key's // CKA_ALWAYS_AUTHENTICATE attribute value for detecting if // authentication is required for the sign operation. For the // private key imported into the 9c (SIGN key) slot with the // --pin-policy=never option the sign operation doesn't require // authentication but the CKA_ALWAYS_AUTHENTICATE value is still on. // This seems to be some yubico-piv-tool issue (or deliberate // behavior) as for the 9a (PIV AUTH key) slot the sign // authentication is not required and the CKA_ALWAYS_AUTHENTICATE // value is off. This issue makes it impossible to evaluate if the // key requires sign authentication before the sign attempt. This is // probably not a big deal as, if we want, we can always check this // using some dummy data. // #if 0 CK_BBOOL always_auth (CK_FALSE); CK_ATTRIBUTE attr { CKA_ALWAYS_AUTHENTICATE, &always_auth, sizeof (always_auth)}; r = fs->C_GetAttributeValue (*session, key, &attr, 1 /* ulCount */); if (r != CKR_OK) throw_api_error ( r, "unable to obtain 'always auth' attribute for " + description); #endif // Despite the fact that the key is found we will continue to iterate // over slots to make sure that a single key matches the identity // attributes. // session_ = move (session); handle_ = key; } if (simulate_) simulate (); else if (session_ == nullptr) no_key (); } vector private_key:: sign (const vector& data, const optional& sim) { assert (!empty ()); if (sim && *sim == simulate_outcome::failure) throw runtime_error ("unable to sign using " + description); if (sim || simulate_) { // Otherwise would fail in the private_key constructor. // assert (!simulate_ || *simulate_ == simulate_outcome::success); return vector ({ 's', 'i', 'g', 'n', 'a', 't', 'u', 'r', 'e', '\n'}); } CK_FUNCTION_LIST* fs (api ()); CK_MECHANISM mech; memset (&mech, 0, sizeof (mech)); mech.mechanism = CKM_RSA_PKCS; CK_RV r (fs->C_SignInit (*session_, &mech, handle_)); if (r != CKR_OK) throw_api_error ( r, "unable to init sign operation using " + description); for (vector signature; true; signature_size_ *= 2) { signature.resize (signature_size_); CK_ULONG n (signature_size_); r = fs->C_Sign (*session_, reinterpret_cast ( const_cast (data.data ())), data.size (), reinterpret_cast ( signature.data ()), &n); if (r == CKR_BUFFER_TOO_SMALL) continue; if (r != CKR_OK) throw_api_error (r, "unable to sign using " + description); assert (n != 0 && n <= signature_size_); signature.resize (n); return signature; } } } } }