This library provides a simple and nice C++ wrapper around these libraries, so that programmers can concentrate on functionality. It offers general support for PCSC-lite, OpenSSL, PKCS#11, plus specific functionality for the SuisseID.
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
596 lines
18 KiB
596 lines
18 KiB
/*! @file |
|
|
|
@id $Id$ |
|
*/ |
|
// 1 2 3 4 5 6 7 8 |
|
// 45678901234567890123456789012345678901234567890123456789012345678901234567890 |
|
|
|
#ifndef __SUISSEID_HXX__ |
|
#define __SUISSEID_HXX__ |
|
|
|
#include <cardos.hxx> |
|
#include <cryptoki.hxx> |
|
#include <pcsc.hxx> |
|
#include <mrw/vector.hxx> |
|
#include <mrw/shared.hxx> |
|
|
|
/*! @defgroup gsuisseid C++ library to access SuisseID smart cards |
|
|
|
This library allows access to the Swiss digital identity cards |
|
(SuisseID). |
|
|
|
You need to include @ref suisseid.hxx, then start with class @ref |
|
suisseid::Scanner to scan for a list of SuisseID cards on the system. |
|
|
|
@see http://www.suisseid.ch |
|
@see http://postsuisseid.ch |
|
|
|
*/ |
|
//@{ |
|
/*! @defgroup suisseidlib SuisseID Library */ |
|
/*! @defgroup suisseidtypes SuisseID C++ Types and Auxiliary */ |
|
/*! @defgroup suisseidconsts SuisseID C++ Constants */ |
|
/*! @defgroup suisseidexceptions SuisseID Exceptions */ |
|
/** @example suisse-id-demo.cxx |
|
|
|
Usage of @ref gsuisseid This is a comprehensive example how you |
|
can access a SuisseID and access to certificates on that card. |
|
|
|
First implement a status cycle, here for @c std::cin and @c |
|
std::cout as user interface in the @c suisse-id-demo.hxx header |
|
file: |
|
|
|
@include suisse-id-demo.hxx |
|
|
|
Then instanciate and use this class from your code: */ |
|
//@} |
|
|
|
/// @ref gsuisseid @copydoc gsuisseid |
|
namespace suisseid { |
|
|
|
|
|
/** @page init Initialize Card and Check Status |
|
|
|
An idea on how the smart card status could be evaluated is the |
|
following state machine: |
|
|
|
@dot |
|
digraph { |
|
transportState |
|
[URL="\ref cardos::Commands::transportState"]; |
|
transportPinRetries |
|
[URL="\ref cardos::Commands::transportPinRetries"]; |
|
pkcs15PinRetries |
|
[URL="\ref cardos::Commands::pkcs15PinRetries"]; |
|
sigGPinRetries |
|
[URL="\ref cardos::Commands::sigGPinRetries"]; |
|
pukRetries |
|
[URL="\ref cardos::Commands::pukRetries"]; |
|
changePin |
|
[URL="\ref cardos::Commands::changePin"]; |
|
broken [label="replace card"]; |
|
|
|
start -> transportState; |
|
transportPinRetries -> broken [label="-1"]; |
|
certsValid -> broken [label="false"]; |
|
pukRetries -> broken [label="-1"]; |
|
sigGPinRetries -> broken [label="-1"]; |
|
transportState -> transportPinRetries [label="true"]; |
|
transportPinRetries -> changePin [label=">-1"]; |
|
changePin -> transportState; |
|
transportState -> haveCerts [label="false"]; |
|
haveCerts -> installCerts [label="false"]; |
|
installCerts -> transportState; |
|
haveCerts -> certsValid [label="true"]; |
|
certsValid -> pkcs15PinRetries [label="true"]; |
|
pkcs15PinRetries -> sigGPinRetries [label=">-1"]; |
|
pkcs15PinRetries -> pukRetries [label="-1"]; |
|
pukRetries -> changePin [label=">-1"]; |
|
sigGPinRetries -> valid [label=">-1"]; |
|
{valid broken} -> end; |
|
|
|
{rank=same; valid broken} |
|
{rank=same; transportPinRetries haveCerts} |
|
{rank=same; certsValid installCerts changePin} |
|
} |
|
@enddot |
|
|
|
*/ |
|
|
|
//============================================================================ |
|
/*! @addtogroup suisseidexceptions */ |
|
//@{ |
|
|
|
//---------------------------------------------------------------------------- |
|
class exception: public std::exception { |
|
public: |
|
exception(const std::string& reason) throw(): |
|
_what("suisseid: "+reason) { |
|
CRYPTOLOG("ERROR: "<<what()); |
|
} |
|
~exception() throw() {} |
|
const char* what() const throw() { |
|
return _what.c_str(); |
|
} |
|
private: |
|
std::string _what; |
|
}; |
|
//---------------------------------------------------------------------------- |
|
class no_certfound: public exception { |
|
public: |
|
no_certfound(const std::string& label) throw(): |
|
exception("no certificate with label \""+label+"\" found") {} |
|
}; |
|
//---------------------------------------------------------------------------- |
|
class no_auth: public exception { |
|
public: |
|
no_auth() throw(): |
|
exception("no authentication certificate found") {} |
|
}; |
|
//---------------------------------------------------------------------------- |
|
class no_digsig: public exception { |
|
public: |
|
no_digsig() throw(): |
|
exception("no digital signature certificate found") {} |
|
}; |
|
//---------------------------------------------------------------------------- |
|
class slot_not_found: public exception { |
|
public: |
|
slot_not_found(const std::string& name) throw(): |
|
exception("matching cryptoki slot for "+name+" not found") {} |
|
}; |
|
//@} |
|
|
|
/*! @addtogroup suisseidtypes */ |
|
//@{ |
|
/// DER encoded binary certificate |
|
typedef std::string Certificate; |
|
/// List of DER encoded binary certificates |
|
typedef std::vector<Certificate> Certificates; |
|
//@} |
|
|
|
/*! @addtogroup suisseidconsts */ |
|
//@{ |
|
/// Label of the key for digital signature certificate. |
|
const std::string NON_REP = "SwissSign_nonRep "; |
|
/// Label of the key for authentication certificate |
|
const std::string DIG_SIG = "SwissSign_digSig "; |
|
/// Label of the key required for getting the certificates |
|
const std::string DATA_ENC = "SwissSign_dataEnc "; |
|
//@} |
|
|
|
/*! @addtogroup suisseidlib */ |
|
//@{ |
|
|
|
//! Represents a SuisseID Card |
|
/*! This is the parent class for special classes for the respecive |
|
SuisseID providers. */ |
|
class Card: public cardos::Commands { |
|
|
|
public: |
|
|
|
/// Status of the card's certificates |
|
/** @note by now, only @c MISSING and @c VALID is supported */ |
|
enum CertStatus { |
|
MISSING, ///< certificate is missing, needs initiatlization |
|
EXPIRES_SOON, ///< certificate will soon expire, needs renewal |
|
EXPIRED, ///< certificate is expired, needs new purchase |
|
REVOKED, ///< certificate has been revoked and is invalid |
|
VALID ///< certificate is valid |
|
}; |
|
|
|
public: |
|
|
|
/// Instanciation is done within suisseid::Scanner |
|
/** Instance requires a connenction to the reader an a cryptoky |
|
library. This is passes automatically when this class is |
|
instanciated through suisseid::Scanner. */ |
|
Card(mrw::Shared<pcsc::Connection::Reader> reader, |
|
const cryptoki::Library& cryptoki): |
|
cardos::Commands(reader), |
|
_cryptoki(cryptoki) { |
|
} |
|
virtual ~Card() {} |
|
|
|
/// Find the matching cryptoki::Slot for further access |
|
/** @throws slot_not_found if not exactly one matching slot exists */ |
|
cryptoki::Slot slot() { |
|
cryptoki::SlotList slots(_cryptoki.slotList(true, name())); |
|
if (slots.size()==1) return slots[0]; |
|
throw slot_not_found(name()); |
|
} |
|
|
|
/// Get the reader, needed for example to lock a transaction |
|
/** @begincode |
|
pcsc::Connection::Reader::Transaction lock(card.reader()); |
|
[... do some low level stuff ...] |
|
@endcode */ |
|
mrw::Shared<pcsc::Connection::Reader> reader() { |
|
return _reader; |
|
} |
|
|
|
/// Minimum allowed PIN length for this card. |
|
virtual unsigned int minimalPinLength() = 0; |
|
/// Maximum allowed PIN length for this card. |
|
virtual unsigned int maximalPinLength() = 0; |
|
|
|
//! Name of the token/slot |
|
const std::string& name() { |
|
return _reader->name; |
|
} |
|
|
|
/// Version of the card |
|
virtual std::string version() { |
|
return "<unknown>"; |
|
} |
|
|
|
/// Status of the certificates on the card |
|
virtual CertStatus certStatus() { |
|
return MISSING; |
|
} |
|
|
|
/// Starts and returns a cryptoki::Session. |
|
cryptoki::Session session() { |
|
return cryptoki::Session(slot()); |
|
} |
|
|
|
/// Starts a cryptoki::Session and returns cryptoki::Session::Info. |
|
cryptoki::Session::Info sessionInfo() { |
|
return session().getsessioninfo(); |
|
} |
|
|
|
/// @returns Certificates in DER format. |
|
Certificates certificates() { |
|
Certificates res; |
|
cryptoki::ObjectList certs |
|
(session().find(cryptoki::Attribute(CKA_CLASS) |
|
.from<CK_OBJECT_CLASS>(CKO_CERTIFICATE))); |
|
for (cryptoki::ObjectList::iterator cert(certs.begin()); |
|
cert!=certs.end(); ++cert) |
|
res.push_back(cert->attribute(CKA_VALUE).value); |
|
return res; |
|
} |
|
|
|
virtual Certificate certificate(const std::string& keylabel) { |
|
cryptoki::ObjectList keys // find keys with digsig-label |
|
(session().find(cryptoki::AttributeList() |
|
<<cryptoki::Attribute(CKA_CLASS) |
|
.from<CK_OBJECT_CLASS>(CKO_PUBLIC_KEY) |
|
<<cryptoki::Attribute(CKA_LABEL, keylabel))); |
|
for (cryptoki::ObjectList::iterator key(keys.begin()); |
|
key!=keys.end(); ++key) { |
|
cryptoki::Attribute id(key->attribute(CKA_ID)); |
|
cryptoki::ObjectList certs |
|
(session().find(cryptoki::AttributeList() |
|
<<cryptoki::Attribute(CKA_CLASS) |
|
.from<CK_OBJECT_CLASS>(CKO_CERTIFICATE) |
|
<<id)); |
|
for (cryptoki::ObjectList::iterator cert(certs.begin()); |
|
cert!=certs.end(); ++cert) { // return first matching cert |
|
return cert->attribute(CKA_VALUE).value; |
|
} |
|
} |
|
throw no_certfound(keylabel); |
|
} |
|
|
|
virtual Certificate authenticationCertificate() = 0; |
|
virtual Certificate digitalSignatureCertificate() = 0; |
|
|
|
protected: |
|
|
|
cryptoki::Library _cryptoki; |
|
|
|
}; |
|
|
|
//! Instance of a Post SuisseID smartcard. |
|
/*! A SuisseID card issued by Swiss Post. |
|
@see http://postsuisseid.ch */ |
|
class Post: public Card { |
|
|
|
public: |
|
|
|
/// @copydoc Card::Card |
|
Post(mrw::Shared<pcsc::Connection::Reader> reader, |
|
const cryptoki::Library& cryptoki): |
|
Card(reader, cryptoki), _minPinLen(0), _maxPinLen(-1) { |
|
} |
|
|
|
virtual unsigned int minimalPinLength() { |
|
if (_minPinLen==0) evaluatePinLengths(); |
|
return _minPinLen; |
|
} |
|
|
|
virtual unsigned int maximalPinLength() { |
|
if (_maxPinLen==-1) evaluatePinLengths(); |
|
return _maxPinLen; |
|
} |
|
|
|
virtual std::string version() { |
|
if (_version.size()) return _version; // cache the version |
|
return versionFromMFFile("5649"); |
|
} |
|
|
|
virtual CertStatus certStatus() { |
|
cryptoki::ObjectList certs |
|
(session().find(cryptoki::Attribute(CKA_CLASS) |
|
.from<CK_OBJECT_CLASS>(CKO_CERTIFICATE))); |
|
if (certs.size()==0) return MISSING; |
|
return VALID; |
|
} |
|
|
|
virtual Certificate authenticationCertificate() try { |
|
return certificate(DIG_SIG); |
|
} catch (const no_certfound&) { |
|
throw no_auth(); |
|
} |
|
|
|
virtual Certificate digitalSignatureCertificate() try { |
|
return certificate(NON_REP); |
|
} catch (const no_certfound&) { |
|
throw no_digsig(); |
|
} |
|
|
|
private: |
|
|
|
void evaluatePinLengths() { |
|
pcsc::Connection::Reader::Transaction lock(_reader); |
|
selectPkcs15File("4408"); |
|
cardos::BerValues res(readBerFile()); |
|
for (cardos::BerValues::iterator it(res.begin()); it!=res.end(); ++it) |
|
if ((*it)[0][0].string()=="PIN" || |
|
(*it)[0][0].string()=="Digital Signature PIN") { |
|
if ((*it)[2][0][2].ulong()>_minPinLen) |
|
_minPinLen = (*it)[2][0][2].ulong(); |
|
if ((*it)[2][0][4].ulong()<_maxPinLen) |
|
_maxPinLen = (*it)[2][0][4].ulong(); |
|
} |
|
} |
|
|
|
std::string versionFromMFFile(const std::string& file) { |
|
pcsc::Connection::Reader::Transaction lock(_reader); |
|
try { |
|
selectMfFile(file); |
|
return _version = cardos::BerValues(readBinary())[0].string(); |
|
} catch (...) { |
|
return _version = "<unknown>"; |
|
} |
|
} |
|
|
|
private: |
|
|
|
std::string _version; // version is cached |
|
unsigned int _minPinLen; // minimal PIN length is cached |
|
unsigned int _maxPinLen; // maximal PIN length is cached |
|
}; |
|
|
|
//! List of cards, returned by @ref suisseid::Scanner::scan. |
|
typedef std::vector<mrw::Shared<Card> > Cards; |
|
|
|
//! Auxiliary SuisseID card manager. |
|
/** Use this manager to scan your system for SuisseID cards. |
|
|
|
Usage Example: |
|
|
|
@code |
|
#include <suisseid.hxx> |
|
#include <iostream> |
|
|
|
[...] |
|
|
|
try { |
|
suisseid::Cards cards(suisseid::Scanner().scan()); |
|
for (auto card(cards.begin()); card!=cards.end(); ++card) |
|
std::cout<<"Found SuisseID: "<<(*card)->name()<<std::endl; |
|
return 0; |
|
} catch (std::exception& x) { |
|
std::cerr<<"**** ERROR in "<<*argv<<": "<<x.what()<<std::endl; |
|
return 1; |
|
} |
|
@endcode */ |
|
class Scanner { |
|
|
|
public: |
|
|
|
Scanner(const std::string& lib="libcvP11.so"): |
|
_cryptoki(lib) { |
|
} |
|
|
|
Scanner(const pcsc::Connection& pcsc, |
|
const std::string& lib="libcvP11.so"): |
|
_pcsc(pcsc), |
|
_cryptoki(lib) { |
|
} |
|
|
|
Scanner(const cryptoki::Library& cryptoki): |
|
_cryptoki(cryptoki) { |
|
} |
|
|
|
Scanner(const pcsc::Connection& pcsc, |
|
const cryptoki::Library& cryptoki): |
|
_pcsc(pcsc), |
|
_cryptoki(cryptoki) { |
|
} |
|
|
|
/// Scan for available known SuisseID cards on the system. |
|
/** @return List of detected SuisseID smart cards. */ |
|
Cards scan() { |
|
CRYPTOLOG("log"); |
|
Cards res; |
|
// By now, scan only for PostSuisseID; in future use factory pattern |
|
pcsc::Connection::Strings readers |
|
(_pcsc.getReadersWithAtr("4b53776973735369676e")); |
|
for (pcsc::Connection::Strings::iterator reader(readers.begin()); |
|
reader!=readers.end(); ++reader) { |
|
cryptoki::SlotList slots(_cryptoki.slotList(true, *reader)); |
|
if (slots.size()==1) |
|
res.push_back(dynamic_cast<Card*> |
|
(new Post(_pcsc.reader(*reader), _cryptoki))); |
|
} |
|
return res; |
|
} |
|
|
|
private: |
|
|
|
pcsc::Connection _pcsc; |
|
cryptoki::Library _cryptoki; |
|
|
|
}; |
|
|
|
class StatusCycle { |
|
|
|
public: |
|
|
|
StatusCycle(mrw::Shared<Card> card, unsigned int maxRetries = 20): |
|
_card(card), _maxRetries(maxRetries), _counter(0) { |
|
} |
|
|
|
~StatusCycle() {} |
|
|
|
bool run() { |
|
CRYPTOLOG("log"); |
|
_counter = 0; |
|
return start(); |
|
} |
|
|
|
protected: |
|
|
|
mrw::Shared<Card> card() { |
|
return _card; |
|
} |
|
|
|
/// @name Slots |
|
//@{ |
|
|
|
struct PinPukChange { |
|
std::string oldpin; |
|
std::string newpin; |
|
bool valid() { |
|
return oldpin.size() && newpin.size(); |
|
} |
|
}; |
|
|
|
virtual PinPukChange pinChange() { |
|
CRYPTOLOG("log"); |
|
return PinPukChange(); |
|
} |
|
|
|
virtual PinPukChange pinChangeTransportPin() { |
|
CRYPTOLOG("log"); |
|
return pinChange(); |
|
} |
|
|
|
virtual PinPukChange pinChangePuk() { |
|
CRYPTOLOG("log"); |
|
return pinChange(); |
|
} |
|
|
|
virtual void transportPinLocked() { |
|
CRYPTOLOG("log"); |
|
} |
|
virtual void pkcs15PinLocked() { |
|
CRYPTOLOG("log"); |
|
} |
|
virtual void sigGPinLocked() { |
|
CRYPTOLOG("log"); |
|
} |
|
virtual void pukLocked() { |
|
CRYPTOLOG("log"); |
|
} |
|
|
|
virtual void certsExpireSoon() { |
|
CRYPTOLOG("log"); |
|
} |
|
virtual void certsExpired() { |
|
CRYPTOLOG("log"); |
|
} |
|
virtual void certsRevoked() { |
|
CRYPTOLOG("log"); |
|
} |
|
|
|
/// install certificates on the card |
|
/** @param bool whether to force reinstallation of existin certificates |
|
@return @c true on success */ |
|
virtual bool installCerts(bool = true) { |
|
CRYPTOLOG("log"); |
|
return false; |
|
} |
|
|
|
//@} |
|
|
|
private: |
|
|
|
bool start() { |
|
CRYPTOLOG("log"); |
|
if (++_counter>_maxRetries) return false; |
|
if (_card->transportState()) |
|
return unlockTransportState(); |
|
else |
|
return checkPkcs15PinStatus(); |
|
} |
|
|
|
bool unlockTransportState() { |
|
CRYPTOLOG("log"); |
|
if (_card->transportPinRetries()<0) |
|
return transportPinLocked(), false; |
|
else |
|
return changeTransportPin(); |
|
} |
|
|
|
bool changeTransportPin() { |
|
CRYPTOLOG("log"); |
|
PinPukChange pins(pinChangeTransportPin()); |
|
if (!pins.valid()) return false; |
|
_card->changePins(pins.newpin, pins.oldpin); |
|
_card->unsetTransportState(); |
|
return start(); |
|
} |
|
|
|
bool checkPkcs15PinStatus() { |
|
CRYPTOLOG("log"); |
|
if (_card->pkcs15PinRetries()<0) |
|
return pkcs15PinLocked(), unlockPkcs15(); |
|
if (_card->pukRetries()<0) |
|
return pukLocked(), false; |
|
return checkCertificates(); |
|
} |
|
|
|
bool checkCertificates() { |
|
CRYPTOLOG("log"); |
|
switch (_card->certStatus()) { |
|
case Card::MISSING: return installCerts() && start(); |
|
case Card::EXPIRES_SOON: certsExpireSoon(); break; |
|
case Card::EXPIRED: return certsExpired(), false; |
|
case Card::REVOKED: return certsRevoked(), false; |
|
case Card::VALID: break; |
|
} |
|
return checkSigGPinStatus(); |
|
} |
|
|
|
bool checkSigGPinStatus() { |
|
CRYPTOLOG("log"); |
|
if (_card->sigGPinRetries()<0) |
|
return sigGPinLocked(), false; |
|
return true; |
|
} |
|
|
|
bool unlockPkcs15() { |
|
CRYPTOLOG("log"); |
|
if (_card->pukRetries()<0) |
|
return pukLocked(), false; |
|
PinPukChange pins(pinChangePuk()); |
|
if (!pins.valid()) return false; |
|
_card->changePins(pins.newpin, pins.oldpin); |
|
return start(); |
|
} |
|
|
|
mrw::Shared<Card> _card; |
|
unsigned int _maxRetries; |
|
unsigned int _counter; |
|
|
|
}; |
|
|
|
//@} |
|
} |
|
|
|
|
|
#endif
|
|
|