Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/crypto/Signer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "Conf.h"
#include "crypto/Digest.h"
#include "crypto/X509Cert.h"
#include "util/XMLText.h"
#include "util/log.h"

#include <openssl/x509.h>
Expand All @@ -35,6 +36,17 @@
using namespace digidoc;
using namespace std;

static void requireXMLText(string_view value, string_view field)

Check notice

Code scanning / CodeQL

Unused static variable Note

Static variable requireXMLText is never read.
{
const util::XMLTextError error = util::validateXMLText(value);
if(!error)
return;
if(error.reason == util::XMLTextError::Reason::InvalidUTF8)
THROW("Invalid UTF-8 at byte offset %zu in %.*s", error.offset, int(field.size()), field.data());
THROW("XML 1.0 character U+%04X at byte offset %zu is not allowed in %.*s",
unsigned(error.codePoint), error.offset, int(field.size()), field.data());
}

class Signer::Private
{
public:
Expand Down Expand Up @@ -73,10 +85,17 @@
* @param stateOrProvince
* @param postalCode
* @param countryName
* The strings must be UTF-8 encoded and contain only XML 1.0 characters.
* @throws Exception if a string is not valid UTF-8 or contains a character disallowed by XML 1.0.
*/
void Signer::setSignatureProductionPlace(const string &city,
const string &stateOrProvince, const string &postalCode, const string &countryName)
{
requireXMLText(city, "signature production city");
requireXMLText(stateOrProvince, "signature production state or province");
requireXMLText(postalCode, "signature production postal code");
requireXMLText(countryName, "signature production country name");

d->city = city;
d->stateOrProvince = stateOrProvince;
d->postalCode = postalCode;
Expand All @@ -91,10 +110,18 @@
* @param stateOrProvince
* @param postalCode
* @param countryName
* The strings must be UTF-8 encoded and contain only XML 1.0 characters.
* @throws Exception if a string is not valid UTF-8 or contains a character disallowed by XML 1.0.
*/
void Signer::setSignatureProductionPlaceV2(const string &city, const string &streetAddress,
const string &stateOrProvince, const string &postalCode, const string &countryName)
{
requireXMLText(city, "signature production city");
requireXMLText(streetAddress, "signature production street address");
requireXMLText(stateOrProvince, "signature production state or province");
requireXMLText(postalCode, "signature production postal code");
requireXMLText(countryName, "signature production country name");

if(!streetAddress.empty())
setENProfile(true);
d->city = city;
Expand Down Expand Up @@ -197,9 +224,14 @@
/**
* Sets signature roles according XAdES standard. The parameter may contain the signer’s role and optionally the signer’s resolution. Note that only one signer role value (i.e. one &lt;ClaimedRole&gt; XML element) should be used.
* If the signer role contains both role and resolution then they must be separated with a slash mark, e.g. “role / resolution”.
* The strings must be UTF-8 encoded and contain only XML 1.0 characters.
* @throws Exception if a string is not valid UTF-8 or contains a character disallowed by XML 1.0.
*/
void Signer::setSignerRoles(const vector<string> &signerRoles)
{
for(const string &role: signerRoles)
requireXMLText(role, "signer role");

d->signerRoles = signerRoles;
}

Expand Down
99 changes: 99 additions & 0 deletions src/util/XMLText.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* libdigidocpp
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/

#pragma once

#include <cstddef>
#include <cstdint>
#include <string_view>

namespace digidoc::util
{
struct XMLTextError
{
enum class Reason
{
None,
InvalidUTF8,
InvalidXMLCharacter
};

constexpr explicit operator bool() const noexcept { return reason != Reason::None; }

Reason reason {Reason::None};
size_t offset {};
uint32_t codePoint {};
};

inline XMLTextError validateXMLText(std::string_view text) noexcept
{
for(size_t offset = 0; offset < text.size();)
{
const auto lead = static_cast<uint8_t>(text[offset]);
uint32_t codePoint {};
size_t length {};
uint32_t minimum {};
if(lead <= 0x7F)
{
codePoint = lead;
length = 1;
}
else if(lead >= 0xC2 && lead <= 0xDF)
{
codePoint = lead & 0x1F;
length = 2;
minimum = 0x80;
}
else if(lead >= 0xE0 && lead <= 0xEF)
{
codePoint = lead & 0x0F;
length = 3;
minimum = 0x800;
}
else if(lead >= 0xF0 && lead <= 0xF4)
{
codePoint = lead & 0x07;
length = 4;
minimum = 0x10000;
}
else
{
return {XMLTextError::Reason::InvalidUTF8, offset};
}

if(length > text.size() - offset)
return {XMLTextError::Reason::InvalidUTF8, offset};
for(size_t i = 1; i < length; ++i)
{
const auto continuation = static_cast<uint8_t>(text[offset + i]);
if((continuation & 0xC0) != 0x80)
return {XMLTextError::Reason::InvalidUTF8, offset};
codePoint = (codePoint << 6) | (continuation & 0x3F);
}
if(codePoint < minimum || codePoint > 0x10FFFF ||
(codePoint >= 0xD800 && codePoint <= 0xDFFF))
return {XMLTextError::Reason::InvalidUTF8, offset};
if(codePoint != 0x09 && codePoint != 0x0A && codePoint != 0x0D &&
(codePoint < 0x20 || codePoint == 0xFFFE || codePoint == 0xFFFF))
return {XMLTextError::Reason::InvalidXMLCharacter, offset, codePoint};
offset += length;
}
return {};
}
}
108 changes: 108 additions & 0 deletions test/libdigidocpp_boost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include <crypto/PKCS12Signer.h>
#include <crypto/X509Crypto.h>
#include <util/DateTime.h>
#include <util/XMLText.h>

namespace digidoc
{
Expand Down Expand Up @@ -115,6 +116,30 @@
};
BOOST_CHECK_EQUAL(signature, sig);
}

BOOST_AUTO_TEST_CASE(signerXMLTextValidation)
{
PKCS12Signer signer{"signer1.p12", "signer1"};
signer.setSignerRoles({"Role1"});
signer.setSignatureProductionPlaceV2("Tartu", "Street", "Tartumaa", "12345", "Estonia");

const string invalidControl{"Role\x1B", 5};
BOOST_CHECK_THROW(signer.setSignerRoles({invalidControl}), Exception);
BOOST_CHECK_EQUAL(signer.signerRoles(), vector<string>{"Role1"});
BOOST_CHECK_THROW(signer.setSignatureProductionPlace(
invalidControl, "Tartumaa", "12345", "Estonia"), Exception);
BOOST_CHECK_EQUAL(signer.city(), "Tartu");

const string invalidUTF8{"Street\xED\xA0\x80", 9};
BOOST_CHECK_THROW(signer.setSignatureProductionPlaceV2(
"Tartu", invalidUTF8, "Tartumaa", "12345", "Estonia"), Exception);
BOOST_CHECK_EQUAL(signer.streetAddress(), "Street");

// Signer metadata is raw UTF-8. XML-looking input is literal text and will
// be escaped by libxml2 when it is eventually inserted into the document.
BOOST_CHECK_NO_THROW(signer.setSignerRoles({"&#x1B;"}));
BOOST_CHECK_EQUAL(signer.signerRoles(), vector<string>{"&#x1B;"});
}
BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE(X509CertSuite)
Expand Down Expand Up @@ -706,6 +731,89 @@
BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE(XMLTestSuite)
BOOST_AUTO_TEST_CASE(XMLTextValidation)
{
const auto invalidCharacter = util::validateXMLText(string{"A\x1B" "B", 3});
BOOST_REQUIRE(invalidCharacter);
BOOST_CHECK(invalidCharacter.reason == util::XMLTextError::Reason::InvalidXMLCharacter);
BOOST_CHECK_EQUAL(invalidCharacter.offset, 1U);
BOOST_CHECK_EQUAL(invalidCharacter.codePoint, 0x1BU);

for(unsigned char value = 0; value < 0x20; ++value)
{
const auto error = util::validateXMLText(string{char(value)});
if(value == 0x09 || value == 0x0A || value == 0x0D)
BOOST_CHECK(!error);
else
BOOST_CHECK(error.reason == util::XMLTextError::Reason::InvalidXMLCharacter);
}

const string invalidUTF8{"A\xED\xA0\x80" "B", 5}; // UTF-8 encoded surrogate U+D800
const auto invalidEncoding = util::validateXMLText(invalidUTF8);
BOOST_REQUIRE(invalidEncoding);
BOOST_CHECK(invalidEncoding.reason == util::XMLTextError::Reason::InvalidUTF8);
BOOST_CHECK_EQUAL(invalidEncoding.offset, 1U);

const string legal = "\t\n\r\x7F"
"\xED\x9F\xBF" // U+D7FF
"\xEE\x80\x80" // U+E000
"\xEF\xBF\xBD" // U+FFFD
"\xF0\x90\x80\x80" // U+10000
"\xF4\x8F\xBF\xBF"; // U+10FFFF
BOOST_CHECK(!util::validateXMLText(legal));

const string invalid[] = {

Check notice

Code scanning / CodeQL

Unused static variable Note test

Static variable invalid is never read.
string{"\0", 1},
string{"\xC0\x80", 2}, // Overlong U+0000
string{"\x80", 1}, // Isolated continuation byte
string{"\xE2\x82", 2}, // Truncated sequence
string{"\xF4\x90\x80\x80", 4}, // Above U+10FFFF
string{"\xEF\xBF\xBE", 3}, // U+FFFE
string{"\xEF\xBF\xBF", 3} // U+FFFF
};
for(const string &value: invalid)
BOOST_CHECK(util::validateXMLText(value));
}

BOOST_AUTO_TEST_CASE(XMLTextEscapingAndParsing)
{
const string raw = "Jüri & Mari <Manager> \"'";
auto doc = XMLDocument::create("root");
XMLNode root = doc;
BOOST_CHECK_NO_THROW(root = raw);

string serialized;
BOOST_REQUIRE(doc.save([&serialized](const char *data, size_t size) {
serialized.append(data, size);
}));
BOOST_CHECK(serialized.contains("&amp;"));
BOOST_CHECK(serialized.contains("&lt;Manager&gt;"));

istringstream input{serialized};
auto parsed = XMLDocument::openStream(input, {"root"});
BOOST_CHECK_EQUAL(string(string_view(parsed)), raw);

// The API accepts raw UTF-8, not pre-escaped XML. A character-reference-like
// string is escaped as literal text and cannot smuggle an illegal character.
root = "&#x1B;";
serialized.clear();
BOOST_REQUIRE(doc.save([&serialized](const char *data, size_t size) {
serialized.append(data, size);
}));
BOOST_CHECK(serialized.contains("&amp;#x1B;"));

istringstream encodedInput{serialized};
parsed = XMLDocument::openStream(encodedInput, {"root"});
BOOST_CHECK_EQUAL(string(string_view(parsed)), "&#x1B;");

istringstream legalReference{"<root>&#xE4;</root>"};
parsed = XMLDocument::openStream(legalReference, {"root"});
BOOST_CHECK_EQUAL(string(string_view(parsed)), "ä");

istringstream illegalReference{"<root>&#x1B;</root>"};
BOOST_CHECK_THROW(XMLDocument::openStream(illegalReference, {"root"}), Exception);
}

BOOST_AUTO_TEST_CASE(XMLBomb)
{
BOOST_CHECK_THROW(XMLDocument("xml-bomb-attr.xml"), Exception);
Expand Down