Skip to content

Commit 644e4ff

Browse files
authored
Add decoding and encoding of ObjectIdentifier ASN.1 records (#1849)
* Initial OID code * - Rename `Asn1OidRecord` to `Asn1ObjectIdentifierRecord` - Some cleanup * Add OID encoding * Add doxygen documentation * Remove redundant include * Fix * Address PR comments
1 parent a87c74a commit 644e4ff

File tree

5 files changed

+392
-0
lines changed

5 files changed

+392
-0
lines changed

Packet++/header/Asn1Codec.h

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,4 +574,91 @@ namespace pcpp
574574
return {};
575575
}
576576
};
577+
578+
/// @class Asn1ObjectIdentifier
579+
/// Represents an ASN.1 Object Identifier (OID).
580+
class Asn1ObjectIdentifier
581+
{
582+
friend class Asn1ObjectIdentifierRecord;
583+
584+
public:
585+
/// Construct an OID from an encoded byte buffer
586+
/// @param[in] data The byte buffer of the encoded OID data
587+
/// @param[in] dataLen The byte buffer size
588+
explicit Asn1ObjectIdentifier(const uint8_t* data, size_t dataLen);
589+
590+
/// Construct an OID from its string representation (e.g., "1.2.840.113549").
591+
/// @param[in] oidString The string representation of the OID
592+
/// @throws std::invalid_argument if the string is malformed or contains invalid components
593+
explicit Asn1ObjectIdentifier(const std::string& oidString);
594+
595+
/// @return A const reference to the internal vector of components
596+
const std::vector<uint32_t>& getComponents() const
597+
{
598+
return m_Components;
599+
}
600+
601+
/// Equality operator to compare two OIDs
602+
/// @param[in] other Another Asn1ObjectIdentifier instance
603+
bool operator==(const Asn1ObjectIdentifier& other) const
604+
{
605+
return m_Components == other.m_Components;
606+
}
607+
608+
/// Inequality operator to compare two OIDs
609+
/// @param[in] other Another Asn1ObjectIdentifier instance
610+
bool operator!=(const Asn1ObjectIdentifier& other) const
611+
{
612+
return m_Components != other.m_Components;
613+
}
614+
615+
/// Convert the OID to its string representation (e.g., "1.2.840.113549")
616+
/// @return A string representing the OID
617+
std::string toString() const;
618+
619+
/// Encode the OID to a byte buffer
620+
/// @return A byte buffer containing the encoded OID value
621+
std::vector<uint8_t> toBytes() const;
622+
623+
friend std::ostream& operator<<(std::ostream& os, const Asn1ObjectIdentifier& oid)
624+
{
625+
return os << oid.toString();
626+
}
627+
628+
protected:
629+
Asn1ObjectIdentifier() = default;
630+
631+
private:
632+
std::vector<uint32_t> m_Components;
633+
};
634+
635+
/// @class Asn1ObjectIdentifierRecord
636+
/// Represents an ASN.1 record with a value of type ObjectIdentifier
637+
class Asn1ObjectIdentifierRecord : public Asn1PrimitiveRecord
638+
{
639+
friend class Asn1Record;
640+
641+
public:
642+
/// A constructor to create a ObjectIdentifier record
643+
/// @param[in] value The ObjectIdentifier (OID) to set as the record value
644+
explicit Asn1ObjectIdentifierRecord(const Asn1ObjectIdentifier& value);
645+
646+
/// @return The OID value of this record
647+
const Asn1ObjectIdentifier& getValue()
648+
{
649+
decodeValueIfNeeded();
650+
return m_Value;
651+
}
652+
653+
protected:
654+
void decodeValue(uint8_t* data, bool lazy) override;
655+
std::vector<uint8_t> encodeValue() const override;
656+
657+
std::vector<std::string> toStringList() override;
658+
659+
private:
660+
Asn1ObjectIdentifier m_Value;
661+
662+
Asn1ObjectIdentifierRecord() = default;
663+
};
577664
} // namespace pcpp

Packet++/src/Asn1Codec.cpp

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,11 @@ namespace pcpp
341341
newRecord = new Asn1NullRecord();
342342
break;
343343
}
344+
case Asn1UniversalTagType::ObjectIdentifier:
345+
{
346+
newRecord = new Asn1ObjectIdentifierRecord();
347+
break;
348+
}
344349
default:
345350
{
346351
newRecord = new Asn1GenericRecord();
@@ -771,4 +776,186 @@ namespace pcpp
771776
m_ValueLength = 0;
772777
m_TotalLength = 2;
773778
}
779+
780+
Asn1ObjectIdentifier::Asn1ObjectIdentifier(const uint8_t* data, size_t dataLen)
781+
{
782+
// A description of OID encoding can be found here:
783+
// https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier?redirectedfrom=MSDN
784+
785+
if (!data || dataLen == 0)
786+
{
787+
throw std::invalid_argument("Malformed OID: Not enough bytes for the first component");
788+
}
789+
790+
size_t currentByteIndex = 0;
791+
std::vector<uint32_t> components;
792+
793+
uint8_t firstByte = data[currentByteIndex++];
794+
// Decode the first byte: first_component * 40 + second_component
795+
components.push_back(static_cast<uint32_t>(firstByte / 40));
796+
components.push_back(static_cast<uint32_t>(firstByte % 40));
797+
798+
uint32_t currentComponentValue = 0;
799+
bool componentStarted = false;
800+
801+
// Process remaining bytes using base-128 encoding
802+
while (currentByteIndex < dataLen)
803+
{
804+
uint8_t byte = data[currentByteIndex++];
805+
806+
// Shift previous bits left by 7 and append lower 7 bits
807+
currentComponentValue = (currentComponentValue << 7) | (byte & 0x7f);
808+
componentStarted = true;
809+
810+
// If the MSB is 0, this is the final byte of the current value
811+
if ((byte & 0x80) == 0)
812+
{
813+
components.push_back(currentComponentValue);
814+
currentComponentValue = 0;
815+
componentStarted = false;
816+
}
817+
}
818+
819+
if (componentStarted)
820+
{
821+
throw std::invalid_argument("Malformed OID: Incomplete component at end of data");
822+
}
823+
824+
m_Components = components;
825+
}
826+
827+
Asn1ObjectIdentifier::Asn1ObjectIdentifier(const std::string& oidString)
828+
{
829+
std::vector<uint32_t> components;
830+
std::istringstream stream(oidString);
831+
std::string token;
832+
833+
while (std::getline(stream, token, '.'))
834+
{
835+
if (token.empty())
836+
{
837+
throw std::invalid_argument("Malformed OID: empty component");
838+
}
839+
840+
unsigned long long value;
841+
try
842+
{
843+
value = std::stoull(token);
844+
}
845+
catch (const std::exception&)
846+
{
847+
throw std::invalid_argument("Malformed OID: invalid component");
848+
}
849+
850+
if (value > std::numeric_limits<uint32_t>::max())
851+
{
852+
throw std::invalid_argument("Malformed OID: component out of uint32_t range");
853+
}
854+
855+
components.push_back(static_cast<uint32_t>(value));
856+
}
857+
858+
if (components.size() < 2)
859+
{
860+
throw std::invalid_argument("Malformed OID: an OID must have at least two components");
861+
}
862+
863+
if (components[0] > 2)
864+
{
865+
throw std::invalid_argument("Malformed OID: first component must be 0, 1, or 2");
866+
}
867+
868+
if ((components[0] == 0 || components[0] == 1) && components[1] >= 40)
869+
{
870+
throw std::invalid_argument(
871+
"Malformed OID: second component must be less than 40 when first component is 0 or 1");
872+
}
873+
874+
m_Components = components;
875+
}
876+
877+
std::string Asn1ObjectIdentifier::toString() const
878+
{
879+
if (m_Components.empty())
880+
{
881+
return "";
882+
}
883+
884+
std::ostringstream stream;
885+
stream << m_Components[0];
886+
887+
for (size_t i = 1; i < m_Components.size(); ++i)
888+
{
889+
stream << "." << m_Components[i];
890+
}
891+
return stream.str();
892+
}
893+
894+
std::vector<uint8_t> Asn1ObjectIdentifier::toBytes() const
895+
{
896+
// A description of OID encoding can be found here:
897+
// https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier?redirectedfrom=MSDN
898+
899+
if (m_Components.size() < 2)
900+
{
901+
throw std::runtime_error("OID must have at least two components to encode.");
902+
}
903+
904+
std::vector<uint8_t> encoded;
905+
906+
// Encode the first two components into one byte
907+
uint32_t firstComponent = m_Components[0];
908+
uint32_t secondComponent = m_Components[1];
909+
encoded.push_back(static_cast<uint8_t>(firstComponent * 40 + secondComponent));
910+
911+
// Encode remaining components using base-128 encoding
912+
for (size_t i = 2; i < m_Components.size(); ++i)
913+
{
914+
uint32_t currentComponent = m_Components[i];
915+
std::vector<uint8_t> temp;
916+
917+
// At least one byte must be generated even if value is 0
918+
do
919+
{
920+
temp.push_back(static_cast<uint8_t>(currentComponent & 0x7F));
921+
currentComponent >>= 7;
922+
} while (currentComponent > 0);
923+
924+
// Set continuation bits (MSB) for all but the last byte
925+
for (size_t j = temp.size(); j-- > 0;)
926+
{
927+
uint8_t byte = temp[j];
928+
if (j != 0)
929+
{
930+
byte |= 0x80;
931+
}
932+
encoded.push_back(byte);
933+
}
934+
}
935+
936+
return encoded;
937+
}
938+
939+
Asn1ObjectIdentifierRecord::Asn1ObjectIdentifierRecord(const Asn1ObjectIdentifier& value)
940+
: Asn1PrimitiveRecord(Asn1UniversalTagType::ObjectIdentifier)
941+
{
942+
m_Value = value;
943+
m_ValueLength = value.toBytes().size();
944+
m_TotalLength = m_ValueLength + 2;
945+
}
946+
947+
void Asn1ObjectIdentifierRecord::decodeValue(uint8_t* data, bool lazy)
948+
{
949+
m_Value = Asn1ObjectIdentifier(data, m_ValueLength);
950+
}
951+
952+
std::vector<uint8_t> Asn1ObjectIdentifierRecord::encodeValue() const
953+
{
954+
return m_Value.toBytes();
955+
}
956+
957+
std::vector<std::string> Asn1ObjectIdentifierRecord::toStringList()
958+
{
959+
return { Asn1Record::toStringList().front() + ", Value: " + getValue().toString() };
960+
}
774961
} // namespace pcpp

Tests/Packet++Test/TestDefinition.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ PTF_TEST_CASE(SmtpEditTests);
262262
// Implemented in Asn1Tests.cpp
263263
PTF_TEST_CASE(Asn1DecodingTest);
264264
PTF_TEST_CASE(Asn1EncodingTest);
265+
PTF_TEST_CASE(Asn1ObjectIdentifierTest);
265266

266267
// Implemented in LdapTests.cpp
267268
PTF_TEST_CASE(LdapParsingTest);

0 commit comments

Comments
 (0)