Skip to content

Add decoding and encoding of ObjectIdentifier ASN.1 records #1849

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 19, 2025
Merged
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
87 changes: 87 additions & 0 deletions Packet++/header/Asn1Codec.h
Original file line number Diff line number Diff line change
Expand Up @@ -574,4 +574,91 @@ namespace pcpp
return {};
}
};

/// @class Asn1ObjectIdentifier
/// Represents an ASN.1 Object Identifier (OID).
class Asn1ObjectIdentifier
{
friend class Asn1ObjectIdentifierRecord;

public:
/// Construct an OID from an encoded byte buffer
/// @param[in] data The byte buffer of the encoded OID data
/// @param[in] dataLen The byte buffer size
explicit Asn1ObjectIdentifier(const uint8_t* data, size_t dataLen);

/// Construct an OID from its string representation (e.g., "1.2.840.113549").
/// @param[in] oidString The string representation of the OID
/// @throws std::invalid_argument if the string is malformed or contains invalid components
explicit Asn1ObjectIdentifier(const std::string& oidString);

/// @return A const reference to the internal vector of components
const std::vector<uint32_t>& getComponents() const
{
return m_Components;
}

/// Equality operator to compare two OIDs
/// @param[in] other Another Asn1ObjectIdentifier instance
bool operator==(const Asn1ObjectIdentifier& other) const
{
return m_Components == other.m_Components;
}

/// Inequality operator to compare two OIDs
/// @param[in] other Another Asn1ObjectIdentifier instance
bool operator!=(const Asn1ObjectIdentifier& other) const
{
return m_Components != other.m_Components;
}

/// Convert the OID to its string representation (e.g., "1.2.840.113549")
/// @return A string representing the OID
std::string toString() const;

/// Encode the OID to a byte buffer
/// @return A byte buffer containing the encoded OID value
std::vector<uint8_t> toBytes() const;

friend std::ostream& operator<<(std::ostream& os, const Asn1ObjectIdentifier& oid)
{
return os << oid.toString();
}

protected:
Asn1ObjectIdentifier() = default;

private:
std::vector<uint32_t> m_Components;
};

/// @class Asn1ObjectIdentifierRecord
/// Represents an ASN.1 record with a value of type ObjectIdentifier
class Asn1ObjectIdentifierRecord : public Asn1PrimitiveRecord
{
friend class Asn1Record;

public:
/// A constructor to create a ObjectIdentifier record
/// @param[in] value The ObjectIdentifier (OID) to set as the record value
explicit Asn1ObjectIdentifierRecord(const Asn1ObjectIdentifier& value);

/// @return The OID value of this record
const Asn1ObjectIdentifier& getValue()
{
decodeValueIfNeeded();
return m_Value;
}

protected:
void decodeValue(uint8_t* data, bool lazy) override;
std::vector<uint8_t> encodeValue() const override;

std::vector<std::string> toStringList() override;

private:
Asn1ObjectIdentifier m_Value;

Asn1ObjectIdentifierRecord() = default;
};
} // namespace pcpp
187 changes: 187 additions & 0 deletions Packet++/src/Asn1Codec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ namespace pcpp
newRecord = new Asn1NullRecord();
break;
}
case Asn1UniversalTagType::ObjectIdentifier:
{
newRecord = new Asn1ObjectIdentifierRecord();
break;
}
default:
{
newRecord = new Asn1GenericRecord();
Expand Down Expand Up @@ -771,4 +776,186 @@ namespace pcpp
m_ValueLength = 0;
m_TotalLength = 2;
}

Asn1ObjectIdentifier::Asn1ObjectIdentifier(const uint8_t* data, size_t dataLen)
{
// A description of OID encoding can be found here:
// https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier?redirectedfrom=MSDN

if (!data || dataLen == 0)
{
throw std::invalid_argument("Malformed OID: Not enough bytes for the first component");
}

size_t currentByteIndex = 0;
std::vector<uint32_t> components;

uint8_t firstByte = data[currentByteIndex++];
// Decode the first byte: first_component * 40 + second_component
components.push_back(static_cast<uint32_t>(firstByte / 40));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider explaining what the 40 magic number is in the comments.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added documentation here: d690947

components.push_back(static_cast<uint32_t>(firstByte % 40));

uint32_t currentComponentValue = 0;
bool componentStarted = false;

// Process remaining bytes using base-128 encoding
while (currentByteIndex < dataLen)
{
uint8_t byte = data[currentByteIndex++];

// Shift previous bits left by 7 and append lower 7 bits
currentComponentValue = (currentComponentValue << 7) | (byte & 0x7f);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe leave some comments here? Like doing which step in the spec.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added documentation here: d690947

componentStarted = true;

// If the MSB is 0, this is the final byte of the current value
if ((byte & 0x80) == 0)
{
components.push_back(currentComponentValue);
currentComponentValue = 0;
componentStarted = false;
}
}

if (componentStarted)
{
throw std::invalid_argument("Malformed OID: Incomplete component at end of data");
}

m_Components = components;
}

Asn1ObjectIdentifier::Asn1ObjectIdentifier(const std::string& oidString)
{
std::vector<uint32_t> components;
std::istringstream stream(oidString);
std::string token;

while (std::getline(stream, token, '.'))
{
if (token.empty())
{
throw std::invalid_argument("Malformed OID: empty component");
}

unsigned long long value;
try
{
value = std::stoull(token);
}
catch (const std::exception&)
{
throw std::invalid_argument("Malformed OID: invalid component");
}

if (value > std::numeric_limits<uint32_t>::max())
{
throw std::invalid_argument("Malformed OID: component out of uint32_t range");
}

components.push_back(static_cast<uint32_t>(value));
}

if (components.size() < 2)
{
throw std::invalid_argument("Malformed OID: an OID must have at least two components");
}

if (components[0] > 2)
{
throw std::invalid_argument("Malformed OID: first component must be 0, 1, or 2");
}

if ((components[0] == 0 || components[0] == 1) && components[1] >= 40)
{
throw std::invalid_argument(
"Malformed OID: second component must be less than 40 when first component is 0 or 1");
}

m_Components = components;
}

std::string Asn1ObjectIdentifier::toString() const
{
if (m_Components.empty())
{
return "";
}

std::ostringstream stream;
stream << m_Components[0];

for (size_t i = 1; i < m_Components.size(); ++i)
{
stream << "." << m_Components[i];
}
return stream.str();
}

std::vector<uint8_t> Asn1ObjectIdentifier::toBytes() const
{
// A description of OID encoding can be found here:
// https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier?redirectedfrom=MSDN

if (m_Components.size() < 2)
{
throw std::runtime_error("OID must have at least two components to encode.");
}

std::vector<uint8_t> encoded;

// Encode the first two components into one byte
uint32_t firstComponent = m_Components[0];
uint32_t secondComponent = m_Components[1];
encoded.push_back(static_cast<uint8_t>(firstComponent * 40 + secondComponent));

// Encode remaining components using base-128 encoding
for (size_t i = 2; i < m_Components.size(); ++i)
{
uint32_t currentComponent = m_Components[i];
std::vector<uint8_t> temp;

// At least one byte must be generated even if value is 0
do
{
temp.push_back(static_cast<uint8_t>(currentComponent & 0x7F));
currentComponent >>= 7;
} while (currentComponent > 0);

// Set continuation bits (MSB) for all but the last byte
for (size_t j = temp.size(); j-- > 0;)
{
uint8_t byte = temp[j];
if (j != 0)
{
byte |= 0x80;
}
encoded.push_back(byte);
}
}

return encoded;
}

Asn1ObjectIdentifierRecord::Asn1ObjectIdentifierRecord(const Asn1ObjectIdentifier& value)
: Asn1PrimitiveRecord(Asn1UniversalTagType::ObjectIdentifier)
{
m_Value = value;
m_ValueLength = value.toBytes().size();
m_TotalLength = m_ValueLength + 2;
}

void Asn1ObjectIdentifierRecord::decodeValue(uint8_t* data, bool lazy)
{
m_Value = Asn1ObjectIdentifier(data, m_ValueLength);
}

std::vector<uint8_t> Asn1ObjectIdentifierRecord::encodeValue() const
{
return m_Value.toBytes();
}

std::vector<std::string> Asn1ObjectIdentifierRecord::toStringList()
{
return { Asn1Record::toStringList().front() + ", Value: " + getValue().toString() };
}
} // namespace pcpp
1 change: 1 addition & 0 deletions Tests/Packet++Test/TestDefinition.h
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ PTF_TEST_CASE(SmtpEditTests);
// Implemented in Asn1Tests.cpp
PTF_TEST_CASE(Asn1DecodingTest);
PTF_TEST_CASE(Asn1EncodingTest);
PTF_TEST_CASE(Asn1ObjectIdentifierTest);

// Implemented in LdapTests.cpp
PTF_TEST_CASE(LdapParsingTest);
Expand Down
Loading
Loading