From de91921561092689369b56c54950474e0a86e66f Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Mon, 15 Oct 2018 21:08:04 +0300 Subject: Add implementation --- openssl/agent/pkcs11/private-key.cxx | 492 +++++++++++++++++++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 openssl/agent/pkcs11/private-key.cxx (limited to 'openssl/agent/pkcs11/private-key.cxx') diff --git a/openssl/agent/pkcs11/private-key.cxx b/openssl/agent/pkcs11/private-key.cxx new file mode 100644 index 0000000..e85d815 --- /dev/null +++ b/openssl/agent/pkcs11/private-key.cxx @@ -0,0 +1,492 @@ +// file : openssl/agent/pkcs11/private-key.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2018 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); + } + + 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; + } + } + } + } +} -- cgit v1.1