/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#if USE_CRYPTO_MSCAPI // WinCrypt headers for PDF signing // Note: this uses Windows 7 APIs and requires the relevant data types #include <prewin.h> #include <wincrypt.h> #include <postwin.h> #include <comphelper/windowserrorstring.hxx> #endif
// ASN.1 used in the (much simpler) time stamp request. From RFC3161 // and other sources.
/* AlgorithmIdentifier ::= SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY DEFINED BY algorithm OPTIONAL } -- contains a value of the type -- registered for use with the -- algorithm object identifier value
/* Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
*/
/* TSAPolicyId ::= OBJECT IDENTIFIER
TimeStampReq ::= SEQUENCE { version INTEGER { v1(1) }, messageImprint MessageImprint, --a hash algorithm OID and the hash value of the data to be --time-stamped reqPolicy TSAPolicyId OPTIONAL, nonce INTEGER OPTIONAL, certReq BOOLEAN DEFAULT FALSE, extensions [0] IMPLICIT Extensions OPTIONAL }
*/
/** * General name, defined by RFC 3280.
*/ struct GeneralName
{
CERTName name;
};
/** * List of general names (only one for now), defined by RFC 3280.
*/ struct GeneralNames
{
GeneralName names;
};
/** * Supplies different fields to identify a certificate, defined by RFC 5035.
*/ struct IssuerSerial
{
GeneralNames issuer;
SECItem serialNumber;
};
/** * Supplies different fields that are used to identify certificates, defined by * RFC 5035.
*/ struct ESSCertIDv2
{
SECAlgorithmID hashAlgorithm;
SECItem certHash;
IssuerSerial issuerSerial;
};
/** * This attribute uses the ESSCertIDv2 structure, defined by RFC 5035.
*/ struct SigningCertificateV2
{
ESSCertIDv2** certs;
OUString PKIStatusToString(int n)
{ switch (n)
{ case 0: return u"granted"_ustr; case 1: return u"grantedWithMods"_ustr; case 2: return u"rejection"_ustr; case 3: return u"waiting"_ustr; case 4: return u"revocationWarning"_ustr; case 5: return u"revocationNotification"_ustr; default: return"unknown (" + OUString::number(n) + ")";
}
}
OUString PKIStatusInfoToString(const PKIStatusInfo& rStatusInfo)
{
OUString result = u"{status="_ustr; if (rStatusInfo.status.len == 1)
result += PKIStatusToString(rStatusInfo.status.data[0]); else
result += "unknown (len=" + OUString::number(rStatusInfo.status.len);
// FIXME: Perhaps look at rStatusInfo.statusString.data but note // that we of course can't assume it contains proper UTF-8. After // all, it is data from an external source. Also, RFC3161 claims // it should be a SEQUENCE (1..MAX) OF UTF8String, but another // source claimed it would be a single UTF8String, hmm?
// FIXME: Worth it to decode failInfo to cleartext, probably not at least as long as this is only for a SAL_INFO
result += "}";
return result;
}
// SEC_StringToOID() and NSS_CMSSignerInfo_AddUnauthAttr() are // not exported from libsmime, so copy them here. Sigh.
/* find oidtag of attr */
type = my_NSS_CMSAttribute_GetType(attr);
/* see if we have one already */
oattr = my_NSS_CMSAttributeArray_FindAttrByOidTag(*attrs, type, PR_FALSE);
PORT_Assert (oattr == NULL); if (oattr != nullptr) goto loser; /* XXX or would it be better to replace it? */
/* no, shove it in */ if (my_NSS_CMSArray_Add(poolp, reinterpret_cast<void ***>(attrs), static_cast<void *>(attr)) != SECSuccess) goto loser;
// Attach NULL data as detached data if (NSS_CMSContentInfo_SetContent_Data(result, cms_cinfo, nullptr, PR_TRUE) != SECSuccess)
{
SAL_WARN("svl.crypto", "NSS_CMSContentInfo_SetContent_Data failed");
NSS_CMSSignedData_Destroy(*cms_sd);
NSS_CMSMessage_Destroy(result); return nullptr;
}
// workaround: with legacy "dbm:", NSS can't find the private key - try out // if it works, and fallback if it doesn't. if (SECKEYPrivateKey * pPrivateKey = PK11_FindKeyByAnyCert(cert, nullptr))
{ if (!comphelper::LibreOfficeKit::isActive())
{ // pPrivateKey only exists in the memory in the LOK case, don't delete it.
SECKEY_DestroyPrivateKey(pPrivateKey);
}
*cms_signer = NSS_CMSSignerInfo_Create(result, cert, SEC_OID_SHA256);
} else
{
pPrivateKey = PK11_FindKeyByDERCert(cert->slot, cert, nullptr);
SECKEYPublicKey *const pPublicKey = CERT_ExtractPublicKey(cert); if (pPublicKey && pPrivateKey)
{
*cms_signer = NSS_CMSSignerInfo_CreateWithSubjKeyID(result, &cert->subjectKeyID, pPublicKey, pPrivateKey, SEC_OID_SHA256);
SECKEY_DestroyPrivateKey(pPrivateKey);
SECKEY_DestroyPublicKey(pPublicKey); if (*cms_signer)
{ // this is required in NSS_CMSSignerInfo_IncludeCerts() // (and NSS_CMSSignerInfo_GetSigningCertificate() doesn't work)
(**cms_signer).cert = CERT_DupCertificate(cert);
}
}
} if (!*cms_signer)
{
SAL_WARN("svl.crypto", "NSS_CMSSignerInfo_Create failed");
NSS_CMSSignedData_Destroy(*cms_sd);
NSS_CMSMessage_Destroy(result); return nullptr;
}
/// Counts how many bytes are needed to encode a given length.
size_t GetDERLengthOfLength(size_t nLength)
{
size_t nRet = 1;
if(nLength > 127)
{ while (nLength >> (nRet * 8))
++nRet; // Long form means one additional byte: the length of the length and // the length itself.
++nRet;
} return nRet;
}
/// Writes the length part of the header. void WriteDERLength(SvStream& rStream, size_t nLength)
{
size_t nLengthOfLength = GetDERLengthOfLength(nLength); if (nLengthOfLength == 1)
{ // We can use the short form.
rStream.WriteUInt8(nLength); return;
}
// 0x80 means that the we use the long form: the first byte is the length // of length with the highest bit set to 1, not the actual length.
rStream.WriteUInt8(0x80 | (nLengthOfLength - 1)); for (size_t i = 1; i < nLengthOfLength; ++i)
rStream.WriteUInt8(nLength >> ((nLengthOfLength - i - 1) * 8));
}
constunsigned nASN1_INTEGER = 0x02; constunsigned nASN1_OCTET_STRING = 0x04; constunsigned nASN1_NULL = 0x05; constunsigned nASN1_OBJECT_IDENTIFIER = 0x06; constunsigned nASN1_SEQUENCE = 0x10; /// An explicit tag on a constructed value. constunsigned nASN1_TAGGED_CONSTRUCTED = 0xa0; constunsigned nASN1_CONSTRUCTED = 0x20;
/// Create payload for the 'signing-certificate' signed attribute. bool CreateSigningCertificateAttribute(voidconst * pDerEncoded, int nDerEncoded, PCCERT_CONTEXT pCertContext, SvStream& rEncodedCertificate)
{ // CryptEncodeObjectEx() does not support encoding arbitrary ASN.1 // structures, like SigningCertificateV2 from RFC 5035, so let's build it // manually.
// Count the certificate hash and put it to aHash. // 2.16.840.1.101.3.4.2.1, i.e. sha256.
std::vector<unsignedchar> aSHA256{0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01};
DWORD nHash = 0; if (!CryptGetHashParam(hHash, HP_HASHVAL, nullptr, &nHash, 0))
{
SAL_WARN("svl.crypto", "CryptGetHashParam() failed to provide the hash length"); returnfalse;
}
std::vector<unsignedchar> aHash(nHash); if (!CryptGetHashParam(hHash, HP_HASHVAL, aHash.data(), &nHash, 0))
{
SAL_WARN("svl.crypto", "CryptGetHashParam() failed to provide the hash"); returnfalse;
}
// Collect info for IssuerSerial.
BYTE* pIssuer = pCertContext->pCertInfo->Issuer.pbData;
DWORD nIssuer = pCertContext->pCertInfo->Issuer.cbData;
BYTE* pSerial = pCertContext->pCertInfo->SerialNumber.pbData;
DWORD nSerial = pCertContext->pCertInfo->SerialNumber.cbData; // pSerial is LE, aSerial is BE.
std::vector<BYTE> aSerial(nSerial); for (size_t i = 0; i < nSerial; ++i)
aSerial[i] = *(pSerial + nSerial - i - 1);
// We now have all the info to count the lengths. // The layout of the payload is: // SEQUENCE: SigningCertificateV2 // SEQUENCE: SEQUENCE OF ESSCertIDv2 // SEQUENCE: ESSCertIDv2 // SEQUENCE: AlgorithmIdentifier // OBJECT: algorithm // NULL: parameters // OCTET STRING: certHash // SEQUENCE: IssuerSerial // SEQUENCE: GeneralNames // cont [ 4 ]: Name // SEQUENCE: Issuer blob // INTEGER: CertificateSerialNumber
// The context unit is milliseconds, PR_Now() unit is microseconds. if (m_rSigningContext.m_nSignatureTime)
{
now = m_rSigningContext.m_nSignatureTime * 1000;
} else
{
m_rSigningContext.m_nSignatureTime = now / 1000;
}
if (!m_rSigningContext.m_xCertificate.is())
{
m_rSigningContext.m_aDigest = std::move(aHashResult); // No certificate is provided: don't actually sign -- just update the context with the // parameters for the signing and return. returnfalse;
}
// Possibly it would work to even just pass NULL for the password callback function and its // argument here. After all, at least with the hardware token and associated software I tested // with, the software itself pops up a dialog asking for the PIN (password). But I am not going // to test it and risk locking up my token...
SAL_INFO("svl.crypto", "TimeStampResp received and decoded, status=" << PKIStatusInfoToString(response.status));
if (response.status.status.len != 1 ||
(response.status.status.data[0] != 0 && response.status.status.data[0] != 1))
{
SAL_WARN("svl.crypto", "Timestamp request was not granted"); returnfalse;
}
// timestamp.type filled in below
// Not sure if we actually need two entries in the values array, now when valuesp is an // array too, the pointer to the values array followed by a null pointer. But I don't feel // like experimenting.
values[0] = response.timeStampToken;
values[1].type = siBuffer;
values[1].data = nullptr;
values[1].len = 0;
// there are no unauthenticated attributes at the moment // if this ever changes, make sure to preserve them
cms_recode_attribute **existing_attrs = (*decoded_signerinfos)->unAuthAttr ;
if (existing_attrs) while (*existing_attrs)
updated_attrs.push_back(*existing_attrs++);
if (!CryptMsgGetParam(hMsg, dwEncodedMessageParamType, 0, nullptr, &nSigLen))
{
SAL_WARN("svl.crypto", "Reading the encoded message via CryptMsgGetParam() failed: " << comphelper::WindowsErrorString(GetLastError())); if (pTsContext)
CryptMemFree(pTsContext);
CryptMsgClose(hMsg); returnfalse;
}
if (nSigLen*2 > MAX_SIGNATURE_CONTENT_LENGTH)
{
SAL_WARN("svl.crypto", "Signature requires more space (" << nSigLen*2 << ") than we reserved (" << MAX_SIGNATURE_CONTENT_LENGTH << ")"); if (pTsContext)
CryptMemFree(pTsContext);
CryptMsgClose(hMsg); returnfalse;
}
namespace
{ #if USE_CRYPTO_NSS /// Similar to NSS_CMSAttributeArray_FindAttrByOidTag(), but works directly with a SECOidData.
NSSCMSAttribute* CMSAttributeArray_FindAttrByOidData(NSSCMSAttribute** attrs, SECOidData const * oid, PRBool only)
{
NSSCMSAttribute* attr1, *attr2;
if (attrs == nullptr) return nullptr;
if (oid == nullptr) return nullptr;
while ((attr1 = *attrs++) != nullptr)
{ if (attr1->type.len == oid->oid.len && PORT_Memcmp(attr1->type.data,
oid->oid.data,
oid->oid.len) == 0) break;
}
if (attr1 == nullptr) return nullptr;
if (!only) return attr1;
while ((attr2 = *attrs++) != nullptr)
{ if (attr2->type.len == oid->oid.len && PORT_Memcmp(attr2->type.data,
oid->oid.data,
oid->oid.len) == 0) break;
}
DWORD nActualHash = 0; if (!CryptGetHashParam(hHash, HP_HASHVAL, nullptr, &nActualHash, 0))
{
SAL_WARN("svl.crypto", "CryptGetHashParam() failed to provide the hash length"); returnfalse;
}
std::vector<unsignedchar> aActualHash(nActualHash); if (!CryptGetHashParam(hHash, HP_HASHVAL, aActualHash.data(), &nActualHash, 0))
{
SAL_WARN("svl.crypto", "CryptGetHashParam() failed to provide the hash"); returnfalse;
}
auto pCMSSignedData = static_cast<NSSCMSSignedData*>(NSS_CMSContentInfo_GetContent(pCMSContentInfo)); if (!pCMSSignedData)
{
SAL_WARN("svl.crypto", "ValidateSignature: NSS_CMSContentInfo_GetContent() failed"); returnfalse;
}
// Import certificates from the signed data temporarily, so it'll be // possible to verify the signature, even if we didn't have the certificate // previously.
std::vector<CERTCertificate*> aDocumentCertificates; if (auto aCerts = pCMSSignedData->rawCerts) { while (*aCerts)
aDocumentCertificates.push_back(CERT_NewTempCertificate(CERT_GetDefaultCertDB(), *aCerts++, nullptr, 0, 0));
}
// Map a sign algorithm to a digest algorithm. // See NSS_CMSUtil_MapSignAlgs(), which is private to us. switch (eOidTag)
{ case SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION:
eOidTag = SEC_OID_SHA1; break; case SEC_OID_PKCS1_SHA256_WITH_RSA_ENCRYPTION:
eOidTag = SEC_OID_SHA256; break; case SEC_OID_PKCS1_SHA512_WITH_RSA_ENCRYPTION:
eOidTag = SEC_OID_SHA512; break; default: break;
}
PRTime nSigningTime; // This may fail, in which case the date should be taken from the PDF's dictionary's "M" key, // so not critical for PDF at least. if (NSS_CMSSignerInfo_GetSigningTime(pCMSSignerInfo, &nSigningTime) == SECSuccess)
{ // First convert the UTC UNIX timestamp to a tools::DateTime. // nSigningTime is in microseconds.
DateTime aDateTime = DateTime::CreateFromUnixTime(static_cast<double>(nSigningTime) / 1000000);
// Then convert to a local UNO DateTime.
aDateTime.ConvertToLocalTime();
rInformation.stDateTime = aDateTime.GetUNODateTime(); if (rInformation.ouDateTime.isEmpty())
{
OUStringBuffer rBuffer;
rBuffer.append(static_cast<sal_Int32>(aDateTime.GetYear()));
rBuffer.append('-'); if (aDateTime.GetMonth() < 10)
rBuffer.append('0');
rBuffer.append(static_cast<sal_Int32>(aDateTime.GetMonth()));
rBuffer.append('-'); if (aDateTime.GetDay() < 10)
rBuffer.append('0');
rBuffer.append(static_cast<sal_Int32>(aDateTime.GetDay()));
rInformation.ouDateTime = rBuffer.makeStringAndClear();
}
}
// Everything went fine
PORT_Free(pActualResultBuffer);
HASH_Destroy(pHASHContext);
NSS_CMSSignerInfo_Destroy(pCMSSignerInfo); for (auto pDocumentCertificate : aDocumentCertificates)
CERT_DestroyCertificate(pDocumentCertificate);
returntrue;
#elif USE_CRYPTO_MSCAPI // ends USE_CRYPTO_NSS
// Open a message for decoding.
HCRYPTMSG hMsg = CryptMsgOpenToDecode(PKCS_7_ASN_ENCODING | X509_ASN_ENCODING,
CMSG_DETACHED_FLAG,
0,
0,
nullptr,
nullptr); if (!hMsg)
{
SAL_WARN("svl.crypto", "ValidateSignature: CryptMsgOpenToDecode() failed"); returnfalse;
}
// Update the message with the encoded header blob. if (!CryptMsgUpdate(hMsg, aSignature.data(), aSignature.size(), TRUE))
{
SAL_WARN("svl.crypto", "ValidateSignature, CryptMsgUpdate() for the header failed: " << comphelper::WindowsErrorString(GetLastError())); returnfalse;
}
if (!bNonDetached)
{ // Update the message with the content blob. if (!CryptMsgUpdate(hMsg, aData.data(), aData.size(), FALSE))
{
SAL_WARN("svl.crypto", "ValidateSignature, CryptMsgUpdate() for the content failed: " << comphelper::WindowsErrorString(GetLastError())); returnfalse;
}
if (!CryptMsgUpdate(hMsg, nullptr, 0, TRUE))
{
SAL_WARN("svl.crypto", "ValidateSignature, CryptMsgUpdate() for the last content failed: " << comphelper::WindowsErrorString(GetLastError())); returnfalse;
}
} // Get the CRYPT_ALGORITHM_IDENTIFIER from the message.
DWORD nDigestID = 0; if (!CryptMsgGetParam(hMsg, CMSG_SIGNER_HASH_ALGORITHM_PARAM, 0, nullptr, &nDigestID))
{
SAL_WARN("svl.crypto", "ValidateSignature: CryptMsgGetParam() failed: " << comphelper::WindowsErrorString(GetLastError())); returnfalse;
}
std::unique_ptr<BYTE[]> pDigestBytes(new BYTE[nDigestID]); if (!CryptMsgGetParam(hMsg, CMSG_SIGNER_HASH_ALGORITHM_PARAM, 0, pDigestBytes.get(), &nDigestID))
{
SAL_WARN("svl.crypto", "ValidateSignature: CryptMsgGetParam() failed: " << comphelper::WindowsErrorString(GetLastError())); returnfalse;
} auto pDigestID = reinterpret_cast<CRYPT_ALGORITHM_IDENTIFIER*>(pDigestBytes.get()); if (std::string_view(szOID_NIST_sha256) == pDigestID->pszObjId)
rInformation.nDigestID = xml::crypto::DigestID::SHA256; elseif (std::string_view(szOID_RSA_SHA1RSA) == pDigestID->pszObjId || std::string_view(szOID_OIWSEC_sha1) == pDigestID->pszObjId)
rInformation.nDigestID = xml::crypto::DigestID::SHA1; else // Don't error out here, we can still verify the message digest correctly, just the digest ID won't be set.
SAL_WARN("svl.crypto", "ValidateSignature: unhandled algorithm identifier '"<<pDigestID->pszObjId<<"'");
// Get the signer CERT_INFO from the message.
DWORD nSignerCertInfo = 0; if (!CryptMsgGetParam(hMsg, CMSG_SIGNER_CERT_INFO_PARAM, 0, nullptr, &nSignerCertInfo))
{
SAL_WARN("svl.crypto", "ValidateSignature: CryptMsgGetParam() failed"); returnfalse;
}
std::unique_ptr<BYTE[]> pSignerCertInfoBuf(new BYTE[nSignerCertInfo]); if (!CryptMsgGetParam(hMsg, CMSG_SIGNER_CERT_INFO_PARAM, 0, pSignerCertInfoBuf.get(), &nSignerCertInfo))
{
SAL_WARN("svl.crypto", "ValidateSignature: CryptMsgGetParam() failed"); returnfalse;
}
PCERT_INFO pSignerCertInfo = reinterpret_cast<PCERT_INFO>(pSignerCertInfoBuf.get());
// Open a certificate store in memory using CERT_STORE_PROV_MSG, which // initializes it with the certificates from the message.
HCERTSTORE hStoreHandle = CertOpenStore(CERT_STORE_PROV_MSG,
PKCS_7_ASN_ENCODING | X509_ASN_ENCODING,
0,
0,
hMsg); if (!hStoreHandle)
{
SAL_WARN("svl.crypto", "ValidateSignature: CertOpenStore() failed"); returnfalse;
}
if (!bNonDetached || VerifyNonDetachedSignature(aData, aContentParam))
{ // Use the CERT_INFO from the signer certificate to verify the signature. if (CryptMsgControl(hMsg, 0, CMSG_CTRL_VERIFY_SIGNATURE, pSignerCertContext->pCertInfo))
rInformation.nStatus = xml::crypto::SecurityOperationStatus_OPERATION_SUCCEEDED;
}
// Check if we have a signing certificate attribute.
DWORD nSignedAttributes = 0; if (CryptMsgGetParam(hMsg, CMSG_SIGNER_AUTH_ATTR_PARAM, 0, nullptr, &nSignedAttributes))
{
std::unique_ptr<BYTE[]> pSignedAttributesBuf(new BYTE[nSignedAttributes]); if (!CryptMsgGetParam(hMsg, CMSG_SIGNER_AUTH_ATTR_PARAM, 0, pSignedAttributesBuf.get(), &nSignedAttributes))
{
SAL_WARN("svl.crypto", "ValidateSignature: CryptMsgGetParam() authenticated failed"); returnfalse;
} auto pSignedAttributes = reinterpret_cast<PCRYPT_ATTRIBUTES>(pSignedAttributesBuf.get()); for (size_t nAttr = 0; nAttr < pSignedAttributes->cAttr; ++nAttr)
{
CRYPT_ATTRIBUTE& rAttr = pSignedAttributes->rgAttr[nAttr]; /* * id-aa-signingCertificateV2 OBJECT IDENTIFIER ::= * { iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs9(9) * smime(16) id-aa(2) 47 }
*/ if (std::string_view("1.2.840.113549.1.9.16.2.47") == rAttr.pszObjId)
{
rInformation.bHasSigningCertificate = true; break;
}
}
}
// Get the unauthorized attributes.
nSignedAttributes = 0; if (CryptMsgGetParam(hMsg, CMSG_SIGNER_UNAUTH_ATTR_PARAM, 0, nullptr, &nSignedAttributes))
{
std::unique_ptr<BYTE[]> pSignedAttributesBuf(new BYTE[nSignedAttributes]); if (!CryptMsgGetParam(hMsg, CMSG_SIGNER_UNAUTH_ATTR_PARAM, 0, pSignedAttributesBuf.get(), &nSignedAttributes))
{
SAL_WARN("svl.crypto", "ValidateSignature: CryptMsgGetParam() unauthenticated failed"); returnfalse;
} auto pSignedAttributes = reinterpret_cast<PCRYPT_ATTRIBUTES>(pSignedAttributesBuf.get()); for (size_t nAttr = 0; nAttr < pSignedAttributes->cAttr; ++nAttr)
{
CRYPT_ATTRIBUTE& rAttr = pSignedAttributes->rgAttr[nAttr]; // Timestamp blob if (std::string_view("1.2.840.113549.1.9.16.2.14") == rAttr.pszObjId)
{
PCRYPT_TIMESTAMP_CONTEXT pTsContext; if (!CryptVerifyTimeStampSignature(rAttr.rgValue->pbData, rAttr.rgValue->cbData, nullptr, 0, nullptr, &pTsContext, nullptr, nullptr))
{
SAL_WARN("svl.crypto", "CryptMsgUpdate failed: " << comphelper::WindowsErrorString(GetLastError())); break;
}
// Then convert to a local UNO DateTime.
aDateTime.ConvertToLocalTime();
rInformation.stDateTime = aDateTime.GetUNODateTime(); if (rInformation.ouDateTime.isEmpty())
{
OUStringBuffer rBuffer;
rBuffer.append(static_cast<sal_Int32>(aDateTime.GetYear()));
rBuffer.append('-'); if (aDateTime.GetMonth() < 10)
rBuffer.append('0');
rBuffer.append(static_cast<sal_Int32>(aDateTime.GetMonth()));
rBuffer.append('-'); if (aDateTime.GetDay() < 10)
rBuffer.append('0');
rBuffer.append(static_cast<sal_Int32>(aDateTime.GetDay()));
rInformation.ouDateTime = rBuffer.makeStringAndClear();
} break;
}
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.