Repository: randombit/botan
Pull request #4816: Add OID related methods to FFI
Author: login: arckoor
Created: 2025-04-02T15:28:08Z | Merged: 2025-04-21T10:08:48Z

Adds various OID related methods, like parsing to and from strings, comparisons and the like.


Diff:
diff --git a/doc/api_ref/ffi.rst b/doc/api_ref/ffi.rst
index d91d6f58ca3..eb160c3484d 100644
--- a/doc/api_ref/ffi.rst
+++ b/doc/api_ref/ffi.rst
@@ -646,7 +646,7 @@ KDF
 Multiple Precision Integers
 ----------------------------------------
 
-.. versionadded: 2.1.0
+.. versionadded:: 2.1.0
 
 .. cpp:type:: opaque* botan_mp_t
 
@@ -834,6 +834,46 @@ Password Hashing
    if the combination is not valid (but otherwise well formed),
    negative on error.
 
+
+Object Identifiers
+----------------------------------------
+
+.. versionadded:: 3.8.0
+
+.. cpp:type:: opaque* botan_asn1_oid_t
+
+   An opaque data type for an object identifier. Don't mess with it.
+
+.. cpp:function:: int botan_oid_destroy(botan_asn1_oid_t oid)
+
+   Destroy an object.
+
+.. cpp:function:: int botan_oid_from_string(botan_asn1_oid_t* oid, const char* oid_str)
+
+   Create an OID from a string, either dot notation (e.g. '1.2.3.4') or a registered name (e.g. 'RSA')
+
+.. cpp:function:: int botan_oid_register(botan_asn1_oid_t oid, const char* name)
+
+   Register an OID so that it may later be retrieved by name
+
+.. cpp:function:: int botan_oid_view_string(botan_asn1_oid_t oid, botan_view_ctx ctx, botan_view_str_fn view)
+
+   View the OID in dot notation
+
+.. cpp:function:: int botan_oid_view_name(botan_asn1_oid_t oid, botan_view_ctx ctx, botan_view_str_fn view)
+
+   View the OID as a name if it has one, otherwise as dot notation
+
+.. cpp:function:: int botan_oid_equal(botan_asn1_oid_t a, botan_asn1_oid_t b)
+
+   Three way comparison: set result to -1 if ``a`` is less than ``b``,
+   0 if ``a`` is equal to ``b``, and 1 if ``a`` is greater than ``b``.
+
+.. cpp:function:: int botan_oid_cmp(int* result, botan_asn1_oid_t a, botan_asn1_oid_t b)
+
+   Return 1 if ``a`` is equal to ``b``, 0 if ``a`` is not equal to ``b``
+
+
 Public Key Creation, Import and Export
 ----------------------------------------
 
@@ -966,7 +1006,7 @@ Public Key Creation, Import and Export
 
    Deprecated, use ``botan_privkey_export_encrypted_msec`` or ``botan_privkey_export_encrypted_iter``
 
-.. cpp::function:: int botan_privkey_export_encrypted_pbkdf_msec(botan_privkey_t key,
+.. cpp:function:: int botan_privkey_export_encrypted_pbkdf_msec(botan_privkey_t key, \
                                                         uint8_t out[], size_t* out_len, \
                                                         botan_rng_t rng, \
                                                         const char* passphrase, \
@@ -982,7 +1022,7 @@ Public Key Creation, Import and Export
     ``cipher_algo`` must specify a CBC mode cipher (such as "AES-128/CBC") or as
     a Botan-specific extension a GCM mode may be used.
 
-.. cpp::function:: int botan_privkey_export_encrypted_pbkdf_iter(botan_privkey_t key, \
+.. cpp:function:: int botan_privkey_export_encrypted_pbkdf_iter(botan_privkey_t key, \
                                                         uint8_t out[], size_t* out_len, \
                                                         botan_rng_t rng, \
                                                         const char* passphrase, \
@@ -1003,6 +1043,19 @@ Public Key Creation, Import and Export
     Read an algorithm specific field from the private key object, placing it into output.
     For example "p" or "q" for RSA keys, or "x" for DSA keys or ECC keys.
 
+.. cpp:function:: int botan_privkey_oid(botan_asn1_oid_t* oid, botan_privkey_t key)
+
+   Get the key's associated OID.
+
+.. cpp:function:: int botan_privkey_stateful_operation(botan_privkey_t key, int* out)
+
+   Checks whether a key is stateful and set ``out`` to 1 if it is, 0 otherwise.
+
+.. cpp:function:: int botan_privkey_remaining_operations(botan_privkey_t key, uint64_t* out)
+
+   Set ``out`` to the number of remaining operations.
+   If the key is not stateful, an error will be returned.
+
 .. cpp:type:: opaque* botan_pubkey_t
 
    An opaque data type for a public key. Don't mess with it.
@@ -1043,6 +1096,10 @@ Public Key Creation, Import and Export
     Read an algorithm specific field from the public key object, placing it into output.
     For example "n" or "e" for RSA keys or "p", "q", "g", and "y" for DSA keys.
 
+.. cpp:function:: int botan_pubkey_oid(botan_asn1_oid_t* oid, botan_privkey_t key)
+
+   Get the key's associated OID.
+
 RSA specific functions
 ----------------------------------------
 
diff --git a/doc/api_ref/python.rst b/doc/api_ref/python.rst
index 6bc369ef13e..a7719f742e0 100644
--- a/doc/api_ref/python.rst
+++ b/doc/api_ref/python.rst
@@ -344,6 +344,10 @@ Public Key
      vary depending on the algorithm. For example RSA public modulus can be
      extracted with ``rsa_key.get_field("n")``.
 
+  .. py:method:: object_identifier()
+
+     Returns the associated OID
+
   .. py:method:: fingerprint(hash = 'SHA-256')
 
      Returns a hash of the public key
@@ -462,6 +466,10 @@ Private Key
      extracted with ``rsa_key.get_field("p")``. This function can also be
      used to extract the public parameters.
 
+  .. py:method:: object_identifier()
+
+     Returns the associated OID
+
   .. py:method:: stateful_operation()
      Return whether the key is stateful or not.
 
@@ -570,6 +578,29 @@ Multiple Precision Integers (MPI)
       Return the greatest common divisor of ``self`` and ``other``
 
 
+Object Identifiers (OID)
+-------------------------------------
+.. versionadded:: 3.8.0
+
+.. py:class:: OID(object)
+
+   .. py:classmethod:: from_string(value)
+
+      Create a new OID from dot notation or from a known name
+
+   .. py:method:: to_string()
+
+      Export the OID in dot notation
+
+   .. py:method:: to_name()
+
+      Export the OID as a name if it has one, else in dot notation
+
+   .. py:method:: register(name)
+
+      Register the OID so that it may later be retrieved by the given name
+
+
 Format Preserving Encryption (FE1 scheme)
 -----------------------------------------
 .. versionadded:: 2.8.0
diff --git a/src/lib/asn1/asn1_obj.h b/src/lib/asn1/asn1_obj.h
index e22c8d91f9c..593e9e457f2 100644
--- a/src/lib/asn1/asn1_obj.h
+++ b/src/lib/asn1/asn1_obj.h
@@ -216,7 +216,7 @@ class BOTAN_PUBLIC_API(2, 0) BER_Bad_Tag final : public BER_Decoding_Error {
 class BOTAN_PUBLIC_API(2, 0) OID final : public ASN1_Object {
    public:
       /**
-      * Create an uninitialied OID object
+      * Create an uninitialised OID object
       */
       explicit OID() = default;
 
diff --git a/src/lib/ffi/ffi.h b/src/lib/ffi/ffi.h
index ddcfacfc962..3614f597ba0 100644
--- a/src/lib/ffi/ffi.h
+++ b/src/lib/ffi/ffi.h
@@ -1110,6 +1110,55 @@ int botan_bcrypt_generate(
 */
 BOTAN_FFI_EXPORT(2, 0) int botan_bcrypt_is_valid(const char* pass, const char* hash);
 
+/*
+* OIDs
+*/
+
+typedef struct botan_asn1_oid_struct* botan_asn1_oid_t;
+
+/**
+* @returns negative number on error, or zero on success
+*/
+BOTAN_FFI_EXPORT(3, 8) int botan_oid_destroy(botan_asn1_oid_t oid);
+
+/**
+* Create an OID from a string, either dot notation (e.g. '1.2.3.4') or a registered name (e.g. 'RSA')
+* @param oid hanlder to the resulting OID
+* @param oid_str the name of the OID to create
+* @returns negative number on error, or zero on success
+*/
+BOTAN_FFI_EXPORT(3, 8) int botan_oid_from_string(botan_asn1_oid_t* oid, const char* oid_str);
+
+/**
+* Registers an OID so that it may later be retrieved by name
+* @returns negative number on error, or zero on success
+*/
+BOTAN_FFI_EXPORT(3, 8) int botan_oid_register(botan_asn1_oid_t oid, const char* name);
+
+/**
+* View an OID in dot notation
+*/
+BOTAN_FFI_EXPORT(3, 8) int botan_oid_view_string(botan_asn1_oid_t oid, botan_view_ctx ctx, botan_view_str_fn view);
+
+/**
+* View an OIDs registered name if it exists, else its dot notation
+*/
+BOTAN_FFI_EXPORT(3, 8) int botan_oid_view_name(botan_asn1_oid_t oid, botan_view_ctx ctx, botan_view_str_fn view);
+
+/**
+* @returns 0 if a != b
+* @returns 1 if a == b
+* @returns negative number on error
+*/
+BOTAN_FFI_EXPORT(3, 8) int botan_oid_equal(botan_asn1_oid_t a, botan_asn1_oid_t b);
+
+/**
+* Sets @param result to comparison result:
+* -1 if a < b, 0 if a == b, 1 if a > b
+* @returns negative number on error or zero on success
+*/
+BOTAN_FFI_EXPORT(3, 8) int botan_oid_cmp(int* result, botan_asn1_oid_t a, botan_asn1_oid_t b);
+
 /*
 * Public/private key creation, import, ...
 */
@@ -1384,6 +1433,15 @@ BOTAN_FFI_EXPORT(2, 0) int botan_pubkey_get_field(botan_mp_t output, botan_pubke
 
 BOTAN_FFI_EXPORT(2, 0) int botan_privkey_get_field(botan_mp_t output, botan_privkey_t key, const char* field_name);
 
+/*
+* Get the OID from public or private keys
+*/
+BOTAN_FFI_EXPORT(3, 8)
+int botan_pubkey_oid(botan_asn1_oid_t* oid, botan_pubkey_t key);
+
+BOTAN_FFI_EXPORT(3, 8)
+int botan_privkey_oid(botan_asn1_oid_t* oid, botan_privkey_t key);
+
 /**
 * Checks whether a key is stateful and sets
 * @param out to 1 if it is, or 0 if the key is not stateful
diff --git a/src/lib/ffi/ffi_oid.cpp b/src/lib/ffi/ffi_oid.cpp
new file mode 100644
index 00000000000..6ba7a9353e7
--- /dev/null
+++ b/src/lib/ffi/ffi_oid.cpp
@@ -0,0 +1,82 @@
+/*
+* (C) 2025 Jack Lloyd
+* (C) 2025 Dominik Schricker
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/ffi.h>
+
+#include <botan/pk_keys.h>
+#include <botan/internal/ffi_oid.h>
+#include <botan/internal/ffi_pkey.h>
+#include <botan/internal/ffi_util.h>
+
+extern "C" {
+
+using namespace Botan_FFI;
+
+int botan_oid_destroy(botan_asn1_oid_t oid) {
+   return BOTAN_FFI_CHECKED_DELETE(oid);
+}
+
+int botan_oid_from_string(botan_asn1_oid_t* oid_obj, const char* oid_str) {
+   return ffi_guard_thunk(__func__, [=]() -> int {
+      if(oid_obj == nullptr || oid_str == nullptr) {
+         return BOTAN_FFI_ERROR_NULL_POINTER;
+      }
+      Botan::OID oid;
+      // This returns a Lookup_Error if an unknown name is passed,
+      // which would get turned into NOT_IMPLEMENTED
+      try {
+         oid = Botan::OID::from_string(oid_str);
+      } catch(Botan::Lookup_Error&) {
+         return BOTAN_FFI_ERROR_BAD_PARAMETER;
+      }
+      std::unique_ptr<Botan::OID> oid_ptr = std::make_unique<Botan::OID>(std::move(oid));
+      *oid_obj = new botan_asn1_oid_struct(std::move(oid_ptr));
+      return BOTAN_FFI_SUCCESS;
+   });
+}
+
+int botan_oid_register(botan_asn1_oid_t oid, const char* name) {
+   return BOTAN_FFI_VISIT(oid, [=](const auto& o) -> int {
+      if(name == nullptr) {
+         return BOTAN_FFI_ERROR_NULL_POINTER;
+      }
+      Botan::OID::register_oid(o, name);
+      return BOTAN_FFI_SUCCESS;
+   });
+}
+
+int botan_oid_view_string(botan_asn1_oid_t oid, botan_view_ctx ctx, botan_view_str_fn view) {
+   return BOTAN_FFI_VISIT(oid, [=](const auto& o) -> int { return invoke_view_callback(view, ctx, o.to_string()); });
+}
+
+int botan_oid_view_name(botan_asn1_oid_t oid, botan_view_ctx ctx, botan_view_str_fn view) {
+   return BOTAN_FFI_VISIT(
+      oid, [=](const auto& o) -> int { return invoke_view_callback(view, ctx, o.to_formatted_string()); });
+}
+
+int botan_oid_equal(botan_asn1_oid_t a_w, botan_asn1_oid_t b_w) {
+   return BOTAN_FFI_VISIT(a_w, [=](const auto& a) -> int { return a == safe_get(b_w); });
+}
+
+int botan_oid_cmp(int* result, botan_asn1_oid_t a_w, botan_asn1_oid_t b_w) {
+   return BOTAN_FFI_VISIT(a_w, [=](auto& a) {
+      if(result == nullptr) {
+         return BOTAN_FFI_ERROR_NULL_POINTER;
+      }
+      Botan::OID b = safe_get(b_w);
+      // we don't have .cmp for OID
+      if(a == b) {
+         *result = 0;
+      } else if(a < b) {
+         *result = -1;
+      } else {
+         *result = 1;
+      }
+      return BOTAN_FFI_SUCCESS;
+   });
+}
+}
diff --git a/src/lib/ffi/ffi_oid.h b/src/lib/ffi/ffi_oid.h
new file mode 100644
index 00000000000..dfe2ee84b26
--- /dev/null
+++ b/src/lib/ffi/ffi_oid.h
@@ -0,0 +1,19 @@
+/*
+* (C) 2025 Jack Lloyd
+* (C) 2025 Dominik Schricker
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_FFI_OID_H_
+#define BOTAN_FFI_OID_H_
+
+#include <botan/asn1_obj.h>
+#include <botan/internal/ffi_util.h>
+
+extern "C" {
+
+BOTAN_FFI_DECLARE_STRUCT(botan_asn1_oid_struct, Botan::OID, 0x9217DA20);
+}
+
+#endif
diff --git a/src/lib/ffi/ffi_pkey.cpp b/src/lib/ffi/ffi_pkey.cpp
index 582e7d83c81..ca4edad2aca 100644
--- a/src/lib/ffi/ffi_pkey.cpp
+++ b/src/lib/ffi/ffi_pkey.cpp
@@ -12,6 +12,7 @@
 #include <botan/pk_keys.h>
 #include <botan/pkcs8.h>
 #include <botan/x509_key.h>
+#include <botan/internal/ffi_oid.h>
 #include <botan/internal/ffi_pkey.h>
 #include <botan/internal/ffi_rng.h>
 #include <botan/internal/ffi_util.h>
@@ -342,6 +343,32 @@ int botan_privkey_view_encrypted_pem(botan_privkey_t key,
    });
 }
 
+int botan_pubkey_oid(botan_asn1_oid_t* oid, botan_pubkey_t key) {
+   return BOTAN_FFI_VISIT(key, [=](const auto& k) {
+      if(oid == nullptr) {
+         return BOTAN_FFI_ERROR_NULL_POINTER;
+      }
+
+      std::unique_ptr<Botan::OID> o = std::make_unique<Botan::OID>(k.object_identifier());
+      *oid = new botan_asn1_oid_struct(std::move(o));
+
+      return BOTAN_FFI_SUCCESS;
+   });
+}
+
+int botan_privkey_oid(botan_asn1_oid_t* oid, botan_privkey_t key) {
+   return BOTAN_FFI_VISIT(key, [=](const auto& k) {
+      if(oid == nullptr) {
+         return BOTAN_FFI_ERROR_NULL_POINTER;
+      }
+
+      std::unique_ptr<Botan::OID> o = std::make_unique<Botan::OID>(k.object_identifier());
+      *oid = new botan_asn1_oid_struct(std::move(o));
+
+      return BOTAN_FFI_SUCCESS;
+   });
+}
+
 int botan_privkey_stateful_operation(botan_privkey_t key, int* out) {
    return BOTAN_FFI_VISIT(key, [=](const auto& k) {
       if(out == nullptr) {
diff --git a/src/lib/ffi/info.txt b/src/lib/ffi/info.txt
index ce47216274b..105fc79ada3 100644
--- a/src/lib/ffi/info.txt
+++ b/src/lib/ffi/info.txt
@@ -9,6 +9,7 @@ brief -> "C API for Botan's functionality"
 
 <header:internal>
 ffi_mp.h
+ffi_oid.h
 ffi_pkey.h
 ffi_rng.h
 ffi_util.h
diff --git a/src/python/botan3.py b/src/python/botan3.py
index 7984d32538d..2360d07fb70 100755
--- a/src/python/botan3.py
+++ b/src/python/botan3.py
@@ -132,7 +132,7 @@ def ffi_api(fn, args, allowed_errors=None):
     dll.botan_error_last_exception_message.argtypes = []
     dll.botan_error_last_exception_message.restype = c_char_p
 
-    # These are generated using src/scripts/ffi_decls.py:
+    # These are generated using src/scripts/dev_tools/gen_ffi_decls.py:
     ffi_api(dll.botan_constant_time_compare, [c_void_p, c_void_p, c_size_t], [-1])
     ffi_api(dll.botan_scrub_mem, [c_void_p, c_size_t])
 
@@ -269,6 +269,15 @@ def ffi_api(fn, args, allowed_errors=None):
             [c_char_p, POINTER(c_size_t), c_char_p, c_void_p, c_size_t, c_uint32])
     ffi_api(dll.botan_bcrypt_is_valid, [c_char_p, c_char_p])
 
+    # OID
+    ffi_api(dll.botan_oid_destroy, [c_void_p])
+    ffi_api(dll.botan_oid_from_string, [c_void_p, c_char_p])
+    ffi_api(dll.botan_oid_register, [c_void_p, c_char_p])
+    ffi_api(dll.botan_oid_view_string, [c_void_p, c_void_p, VIEW_STR_CALLBACK])
+    ffi_api(dll.botan_oid_view_name, [c_void_p, c_void_p, VIEW_STR_CALLBACK])
+    ffi_api(dll.botan_oid_equal, [c_void_p, c_void_p])
+    ffi_api(dll.botan_oid_cmp, [POINTER(c_int), c_void_p, c_void_p])
+
     #  PUBKEY
     ffi_api(dll.botan_privkey_create, [c_void_p, c_char_p, c_char_p, c_void_p])
     ffi_api(dll.botan_privkey_check_key, [c_void_p, c_void_p, c_uint32], [-1])
@@ -320,6 +329,8 @@ def ffi_api(fn, args, allowed_errors=None):
     ffi_api(dll.botan_pubkey_destroy, [c_void_p])
     ffi_api(dll.botan_pubkey_get_field, [c_void_p, c_void_p, c_char_p])
     ffi_api(dll.botan_privkey_get_field, [c_void_p, c_void_p, c_char_p])
+    ffi_api(dll.botan_pubkey_oid, [c_void_p, c_void_p])
+    ffi_api(dll.botan_privkey_oid, [c_void_p, c_void_p])
     ffi_api(dll.botan_privkey_stateful_operation, [c_void_p, POINTER(c_int)])
     ffi_api(dll.botan_privkey_remaining_operations, [c_void_p, POINTER(c_uint64)])
     ffi_api(dll.botan_privkey_load_rsa, [c_void_p, c_void_p, c_void_p, c_void_p])
@@ -1167,113 +1178,114 @@ def kdf(algo, secret, out_len, salt, label):
 # Public key
 #
 class PublicKey: # pylint: disable=invalid-name
-
-    def __init__(self, obj=c_void_p(0)):
+    def __init__(self, obj=None):
+        if not obj:
+            obj = c_void_p(0)
         self.__obj = obj
 
     @classmethod
     def load(cls, val):
-        obj = c_void_p(0)
+        pub = PublicKey()
         bits = _ctype_bits(val)
-        _DLL.botan_pubkey_load(byref(obj), bits, len(bits))
-        return PublicKey(obj)
+        _DLL.botan_pubkey_load(byref(pub.handle_()), bits, len(bits))
+        return pub
 
     @classmethod
     def load_rsa(cls, n, e):
-        obj = c_void_p(0)
+        pub = PublicKey()
         n = MPI(n)
         e = MPI(e)
-        _DLL.botan_pubkey_load_rsa(byref(obj), n.handle_(), e.handle_())
-        return PublicKey(obj)
+        _DLL.botan_pubkey_load_rsa(byref(pub.handle_()), n.handle_(), e.handle_())
+        return pub
 
     @classmethod
     def load_dsa(cls, p, q, g, y):
-        obj = c_void_p(0)
+        pub = PublicKey()
         p = MPI(p)
         q = MPI(q)
         g = MPI(g)
         y = MPI(y)
-        _DLL.botan_pubkey_load_dsa(byref(obj), p.handle_(), q.handle_(), g.handle_(), y.handle_())
-        return PublicKey(obj)
+        _DLL.botan_pubkey_load_dsa(byref(pub.handle_()), p.handle_(), q.handle_(), g.handle_(), y.handle_())
+        return pub
 
     @classmethod
     def load_dh(cls, p, g, y):
-        obj = c_void_p(0)
+        pub = PublicKey()
         p = MPI(p)
         g = MPI(g)
         y = MPI(y)
-        _DLL.botan_pubkey_load_dh(byref(obj), p.handle_(), g.handle_(), y.handle_())
-        return PublicKey(obj)
+        _DLL.botan_pubkey_load_dh(byref(pub.handle_()), p.handle_(), g.handle_(), y.handle_())
+        return pub
 
     @classmethod
     def load_elgamal(cls, p, q, g, y):
-        obj = c_void_p(0)
+        pub = PublicKey()
         p = MPI(p)
         q = MPI(q)
         g = MPI(g)
         y = MPI(y)
-        _DLL.botan_pubkey_load_elgamal(byref(obj), p.handle_(), q.handle_(), g.handle_(), y.handle_())
-        return PublicKey(obj)
+        _DLL.botan_pubkey_load_elgamal(byref(pub.handle_()), p.handle_(), q.handle_(), g.handle_(), y.handle_())
+        return pub
 
     @classmethod
     def load_ecdsa(cls, curve, pub_x, pub_y):
-        obj = c_void_p(0)
+        pub = PublicKey()
         pub_x = MPI(pub_x)
         pub_y = MPI(pub_y)
-        _DLL.botan_pubkey_load_ecdsa(byref(obj), pub_x.handle_(), pub_y.handle_(), _ctype_str(curve))
-        return PublicKey(obj)
+        _DLL.botan_pubkey_load_ecdsa(byref(pub.handle_()), pub_x.handle_(), pub_y.handle_(), _ctype_str(curve))
+        return pub
 
     @classmethod
     def load_ecdh(cls, curve, pub_x, pub_y):
-        obj = c_void_p(0)
+        pub = PublicKey()
         pub_x = MPI(pub_x)
         pub_y = MPI(pub_y)
-        _DLL.botan_pubkey_load_ecdh(byref(obj), pub_x.handle_(), pub_y.handle_(), _ctype_str(curve))
-        return PublicKey(obj)
+        _DLL.botan_pubkey_load_ecdh(byref(pub.handle_()), pub_x.handle_(), pub_y.handle_(), _ctype_str(curve))
+        return pub
 
     @classmethod
     def load_sm2(cls, curve, pub_x, pub_y):
-        obj = c_void_p(0)
+        pub = PublicKey()
         pub_x = MPI(pub_x)
         pub_y = MPI(pub_y)
-        _DLL.botan_pubkey_load_sm2(byref(obj), pub_x.handle_(), pub_y.handle_(), _ctype_str(curve))
-        return PublicKey(obj)
+        _DLL.botan_pubkey_load_sm2(byref(pub.handle_()), pub_x.handle_(), pub_y.handle_(), _ctype_str(curve))
+        return pub
 
     @classmethod
     def load_kyber(cls, key):
-        obj = c_void_p(0)
-        _DLL.botan_pubkey_load_kyber(byref(obj), key, len(key))
-        return PublicKey(obj)
+        pub = PublicKey()
+        _DLL.botan_pubkey_load_kyber(byref(pub.handle_()), key, len(key))
+        return pub
 
     @classmethod
     def load_ml_kem(cls, mlkem_mode, key):
-        obj = c_void_p(0)
-        _DLL.botan_pubkey_load_ml_kem(byref(obj), key, len(key), _ctype_str(mlkem_mode))
-        return PublicKey(obj)
+        pub = PublicKey()
+        _DLL.botan_pubkey_load_ml_kem(byref(pub.handle_()), key, len(key), _ctype_str(mlkem_mode))
+        return pub
 
     @classmethod
     def load_ml_dsa(cls, mldsa_mode, key):
-        obj = c_void_p(0)
-        _DLL.botan_pubkey_load_ml_dsa(byref(obj), key, len(key), _ctype_str(mldsa_mode))
-        return PublicKey(obj)
+        pub = PublicKey()
+        _DLL.botan_pubkey_load_ml_dsa(byref(pub.handle_()), key, len(key), _ctype_str(mldsa_mode))
+        return pub
 
     @classmethod
     def load_slh_dsa(cls, slhdsa_mode, key):
-        obj = c_void_p(0)
-        _DLL.botan_pubkey_load_slh_dsa(byref(obj), key, len(key), _ctype_str(slhdsa_mode))
-        return PublicKey(obj)
+        pub = PublicKey()
+        _DLL.botan_pubkey_load_slh_dsa(byref(pub.handle_()), key, len(key), _ctype_str(slhdsa_mode))
+        return pub
 
     @classmethod
     def load_frodokem(cls, frodo_mode, key):
-        obj = c_void_p(0)
-        _DLL.botan_pubkey_load_frodokem(byref(obj), key, len(key), _ctype_str(frodo_mode))
-        return PublicKey(obj)
+        pub = PublicKey()
+        _DLL.botan_pubkey_load_frodokem(byref(pub.handle_()), key, len(key), _ctype_str(frodo_mode))
+        return pub
 
     @classmethod
     def load_classic_mceliece(cls, cmce_mode, key):
-        obj = c_void_p(0)
-        _DLL.botan_pubkey_load_classic_mceliece(byref(obj), key, len(key), _ctype_str(cmce_mode))
-        return PublicKey(obj)
+        pub = PublicKey()
+        _DLL.botan_pubkey_load_classic_mceliece(byref(pub.handle_()), key, len(key), _ctype_str(cmce_mode))
+        return pub
 
     def __del__(self):
         _DLL.botan_pubkey_destroy(self.__obj)
@@ -1326,6 +1338,11 @@ def get_field(self, field_name):
         _DLL.botan_pubkey_get_field(v.handle_(), self.__obj, _ctype_str(field_name))
         return int(v)
 
+    def object_identifier(self):
+        oid = OID()
+        _DLL.botan_pubkey_oid(byref(oid.handle_()), self.__obj)
+        return oid
+
     def get_public_point(self):
         return _call_fn_viewing_vec(lambda vc, vfn: _DLL.botan_pubkey_view_ec_public_point(self.__obj, vc, vfn))
 
@@ -1333,17 +1350,18 @@ def get_public_point(self):
 # Private Key
 #
 class PrivateKey:
-
-    def __init__(self, obj=c_void_p(0)):
+    def __init__(self, obj=None):
+        if not obj:
+            obj = c_void_p(0)
         self.__obj = obj
 
     @classmethod
     def load(cls, val, passphrase=""):
-        obj = c_void_p(0)
+        priv = PrivateKey()
         rng_obj = c_void_p(0) # unused in recent versions
         bits = _ctype_bits(val)
-        _DLL.botan_privkey_load(byref(obj), rng_obj, bits, len(bits), _ctype_str(passphrase))
-        return PrivateKey(obj)
+        _DLL.botan_privkey_load(byref(priv.handle_()), rng_obj, bits, len(bits), _ctype_str(passphrase))
+        return priv
 
     @classmethod
     def create(cls, algo, params, rng_obj):
@@ -1365,104 +1383,104 @@ def create(cls, algo, params, rng_obj):
             algo = 'McEliece'
             params = "%d,%d" % (params[0], params[1])
 
-        obj = c_void_p(0)
-        _DLL.botan_privkey_create(byref(obj), _ctype_str(algo), _ctype_str(params), rng_obj.handle_())
-        return PrivateKey(obj)
+        priv = PrivateKey()
+        _DLL.botan_privkey_create(byref(priv.handle_()), _ctype_str(algo), _ctype_str(params), rng_obj.handle_())
+        return priv
 
     @classmethod
     def load_rsa(cls, p, q, e):
-        obj = c_void_p(0)
+        priv = PrivateKey()
         p = MPI(p)
         q = MPI(q)
         e = MPI(e)
-        _DLL.botan_privkey_load_rsa(byref(obj), p.handle_(), q.handle_(), e.handle_())
-        return PrivateKey(obj)
+        _DLL.botan_privkey_load_rsa(byref(priv.handle_()), p.handle_(), q.handle_(), e.handle_())
+        return priv
 
     @classmethod
     def load_dsa(cls, p, q, g, x):
-        obj = c_void_p(0)
+        priv = PrivateKey()
         p = MPI(p)
         q = MPI(q)
         g = MPI(g)
         x = MPI(x)
-        _DLL.botan_privkey_load_dsa(byref(obj), p.handle_(), q.handle_(), g.handle_(), x.handle_())
-        return PrivateKey(obj)
+        _DLL.botan_privkey_load_dsa(byref(priv.handle_()), p.handle_(), q.handle_(), g.handle_(), x.handle_())
+        return priv
 
     @classmethod
     def load_dh(cls, p, g, x):
-        obj = c_void_p(0)
+        priv = PrivateKey()
         p = MPI(p)
         g = MPI(g)
         x = MPI(x)
-        _DLL.botan_privkey_load_dh(byref(obj), p.handle_(), g.handle_(), x.handle_())
-        return PrivateKey(obj)
+        _DLL.botan_privkey_load_dh(byref(priv.handle_()), p.handle_(), g.handle_(), x.handle_())
+        return priv
 
     @classmethod
     def load_elgamal(cls, p, q, g, x):
-        obj = c_void_p(0)
+        priv = PrivateKey()
         p = MPI(p)
         q = MPI(q)
         g = MPI(g)
         x = MPI(x)
-        _DLL.botan_privkey_load_elgamal(byref(obj), p.handle_(), q.handle_(), g.handle_(), x.handle_())
-        return PrivateKey(obj)
+        _DLL.botan_privkey_load_elgamal(byref(priv.handle_()), p.handle_(), q.handle_(), g.handle_(), x.handle_())
+        return priv
 
     @classmethod
     def load_ecdsa(cls, curve, x):
-        obj = c_void_p(0)
+        priv = PrivateKey()
         x = MPI(x)
-        _DLL.botan_privkey_load_ecdsa(byref(obj), x.handle_(), _ctype_str(curve))
-        return PrivateKey(obj)
+        _DLL.botan_privkey_load_ecdsa(byref(priv.handle_()), x.handle_(), _ctype_str(curve))
+        return priv
 
     @classmethod
     def load_ecdh(cls, curve, x):
-        obj = c_void_p(0)
+        priv = PrivateKey()
         x = MPI(x)
-        _DLL.botan_privkey_load_ecdh(byref(obj), x.handle_(), _ctype_str(curve))
-        return PrivateKey(obj)
+        _DLL.botan_privkey_load_ecdh(byref(priv.handle_()), x.handle_(), _ctype_str(curve))
+        return priv
 
     @classmethod
     def load_sm2(cls, curve, x):
-        obj = c_void_p(0)
+        priv = PrivateKey()
         x = MPI(x)
-        _DLL.botan_privkey_load_sm2(byref(obj), x.handle_(), _ctype_str(curve))
-        return PrivateKey(obj)
+        _DLL.botan_privkey_load_sm2(byref(priv.handle_()), x.handle_(), _ctype_str(curve))
+        return priv
 
     @classmethod
     def load_kyber(cls, key):
-        obj = c_void_p(0)
-        _DLL.botan_privkey_load_kyber(byref(obj), key, len(key))
-        return PrivateKey(obj)
+        priv = PrivateKey()
+        _DLL.botan_privkey_load_kyber(byref(priv.handle_()), key, len(key))
+        return priv
 
     @classmethod
     def load_ml_kem(cls, mlkem_mode, key):
-        obj = c_void_p(0)
-        _DLL.botan_privkey_load_ml_kem(byref(obj), key, len(key), _ctype_str(mlkem_mode))
-        return PrivateKey(obj)
+        priv = PrivateKey()
+        _DLL.botan_privkey_load_ml_kem(byref(priv.handle_()), key, len(key), _ctype_str(mlkem_mode))
+        return priv
 
     @classmethod
     def load_ml_dsa(cls, mldsa_mode, key):
-        obj = c_void_p(0)
-        _DLL.botan_privkey_load_ml_dsa(byref(obj), key, len(key), _ctype_str(mldsa_mode))
-        return PrivateKey(obj)
+        priv = PrivateKey()
+        _DLL.botan_privkey_load_ml_dsa(byref(priv.handle_()), key, len(key), _ctype_str(mldsa_mode))
+        return priv
 
     @classmethod
     def load_slh_dsa(cls, slh_dsa, key):
-        obj = c_void_p(0)
-        _DLL.botan_privkey_load_slh_dsa(byref(obj), key, len(key), _ctype_str(slh_dsa))
-        return PrivateKey(obj)
+        priv = PrivateKey()
+        _DLL.botan_privkey_load_slh_dsa(byref(priv.handle_()), key, len(key), _ctype_str(slh_dsa))
+        return priv
 
     @classmethod
     def load_frodokem(cls, frodo_mode, key):
-        obj = c_void_p(0)
-        _DLL.botan_privkey_load_frodokem(byref(obj), key, len(key), _ctype_str(frodo_mode))
-        return PrivateKey(obj)
+        priv = PrivateKey()
+        _DLL.botan_privkey_load_frodokem(byref(priv.handle_()), key, len(key), _ctype_str(frodo_mode))
+        return priv
 
     @classmethod
     def load_classic_mceliece(cls, cmce_mode, key):
-        obj = c_void_p(0)
-        _DLL.botan_privkey_load_classic_mceliece(byref(obj), key, len(key), _ctype_str(cmce_mode))
-        return PrivateKey(obj)
+        priv = PrivateKey()
+        _DLL.botan_privkey_load_classic_mceliece(byref(priv.handle_()), key, len(key), _ctype_str(cmce_mode))
+        return priv
 
     def __del__(self):
         _DLL.botan_privkey_destroy(self.__obj)
@@ -1479,9 +1497,9 @@ def algo_name(self):
         return _call_fn_returning_str(32, lambda b, bl: _DLL.botan_privkey_algo_name(self.__obj, b, bl))
 
     def get_public_key(self):
-        pub = c_void_p(0)
-        _DLL.botan_privkey_export_pubkey(byref(pub), self.__obj)
-        return PublicKey(pub)
+        pub = PublicKey()
+        _DLL.botan_privkey_export_pubkey(byref(pub.handle_()), self.__obj)
+        return pub
 
     def to_der(self):
         return _call_fn_viewing_vec(lambda vc, vfn: _DLL.botan_privkey_view_der(self.__obj, vc, vfn))
@@ -1519,6 +1537,11 @@ def get_field(self, field_name):
         _DLL.botan_privkey_get_field(v.handle_(), self.__obj, _ctype_str(field_name))
         return int(v)
 
+    def object_identifier(self):
+        oid = OID()
+        _DLL.botan_privkey_oid(byref(oid.handle_()), self.__obj)
+        return oid
+
     def stateful_operation(self):
         r = c_int(0)
         _DLL.botan_privkey_stateful_operation(self.__obj, byref(r))
@@ -2123,6 +2146,58 @@ def clear_bit(self, bit):
     def set_bit(self, bit):
         _DLL.botan_mp_set_bit(self.__obj, c_size_t(bit))
 
+
+class OID:
+    def __init__(self, obj=None):
+        if not obj:
+            obj = c_void_p(0)
+        self.__obj = obj
+
+    def __del__(self):
+        _DLL.botan_oid_destroy(self.__obj)
+
+    def handle_(self):
+        return self.__obj
+
+    @classmethod
+    def from_string(cls, value):
+        oid = OID()
+        _DLL.botan_oid_from_string(byref(oid.handle_()), _ctype_str(value))
+        return oid
+
+    def to_string(self):
+        return _call_fn_viewing_str(lambda vc, vfn: _DLL.botan_oid_view_string(self.__obj, vc, vfn))
+
+    def to_name(self):
+        return _call_fn_viewing_str(lambda vc, vfn: _DLL.botan_oid_view_name(self.__obj, vc, vfn))
+
+    def register(self, name):
+        _DLL.botan_oid_register(self.__obj, _ctype_str(name))
+
+    def cmp(self, other):
+        r = c_int(0)
+        _DLL.botan_oid_cmp(byref(r), self.__obj, other.handle_())
+        return r.value
+
+    def __eq__(self, other):
+        return self.cmp(other) == 0
+
+    def __ne__(self, other):
+        return self.cmp(other) != 0
+
+    def __lt__(self, other):
+        return self.cmp(other) < 0
+
+    def __le__(self, other):
+        return self.cmp(other) <= 0
+
+    def __gt__(self, other):
+        return self.cmp(other) > 0
+
+    def __ge__(self, other):
+        return self.cmp(other) >= 0
+
+
 class FormatPreservingEncryptionFE1:
 
     def __init__(self, modulus, key, rounds=5, compat_mode=False):
diff --git a/src/scripts/test_python.py b/src/scripts/test_python.py
index afb9ff079a2..bdc0acd51b8 100644
--- a/src/scripts/test_python.py
+++ b/src/scripts/test_python.py
@@ -1045,6 +1045,43 @@ def test_classic_mceliece_raw_keys(self):
         self.assertEqual(sk_read.to_raw(), sk_bits)
         self.assertEqual(pk_read.to_raw(), pk_bits)
 
+    def test_oids(self):
+        oid = botan.OID.from_string("1.2.3.4.5")
+        self.assertEqual(oid.to_string(), "1.2.3.4.5")
+
+        new_oid = botan.OID.from_string("1.2.3.4.5.6.7.8")
+        new_oid.register("random-name-that-definitely-has-no-oid")
+        new_oid_from_string = botan.OID.from_string("random-name-that-definitely-has-no-oid")
+        self.assertEqual(new_oid, new_oid_from_string)
+        self.assertEqual(new_oid.to_string(), new_oid_from_string.to_string())
+        self.assertEqual(new_oid.to_name(), new_oid_from_string.to_name())
+
+        oid_rsa = botan.OID.from_string("RSA")
+        self.assertEqual(oid_rsa.to_string(), "1.2.840.113549.1.1.1")
+        self.assertEqual(oid_rsa.to_name(), "RSA")
+
+        oid_a = botan.OID.from_string("1.2.3.4.5.6")
+        oid_b = botan.OID.from_string("1.2.3.4.5.6")
+        oid_c = botan.OID.from_string("1.2.3.4")
+        self.assertEqual(oid_a, oid_b)
+        self.assertNotEqual(oid_a, oid_c)
+        self.assertTrue(oid_a > oid_c)
+        self.assertTrue(oid_a >= oid_c)
+        self.assertTrue(oid_c < oid_a)
+        self.assertTrue(oid_c <= oid_a)
+
+        rng = botan.RandomNumberGenerator()
+        priv = botan.PrivateKey.create("RSA", "1024", rng)
+        oid_rsa_priv = priv.object_identifier()
+        self.assertEqual(oid_rsa_priv, oid_rsa)
+        self.assertEqual(oid_rsa_priv.to_string(), oid_rsa.to_string())
+        self.assertEqual(oid_rsa_priv.to_name(), oid_rsa.to_name())
+
+        pub = priv.get_public_key()
+        oid_rsa_pub = pub.object_identifier()
+        self.assertEqual(oid_rsa_pub, oid_rsa)
+        self.assertEqual(oid_rsa_pub.to_string(), oid_rsa.to_string())
+        self.assertEqual(oid_rsa_pub.to_name(), oid_rsa_pub.to_name())
 
 class BotanPythonZfecTests(unittest.TestCase):
     """
diff --git a/src/tests/test_ffi.cpp b/src/tests/test_ffi.cpp
index 732824b0796..ea1c7af9264 100644
--- a/src/tests/test_ffi.cpp
+++ b/src/tests/test_ffi.cpp
@@ -3407,6 +3407,34 @@ class ViewBytesSink final {
       std::vector<uint8_t> m_buf;
 };
 
+/**
+ * See ViewBytesSink for how to use this. Works for `botan_view_str_fn` instead.
+*/
+class ViewStringSink final {
+   public:
+      void* delegate() { return this; }
+
+      botan_view_str_fn callback() { return &write_fn; }
+
+      std::string_view get() { return m_str; }
+
+   private:
+      static int write_fn(void* ctx, const char* str, size_t len) {
+         if(!ctx || !str) {
+            return BOTAN_FFI_ERROR_NULL_POINTER;
+         }
+
+         auto* sink = static_cast<ViewStringSink*>(ctx);
+         // discard the null terminator
+         sink->m_str = std::string(str, len - 1);
+
+         return 0;
+      }
+
+   private:
+      std::string m_str;
+};
+
 /**
  * Base class for roundtrip tests of FFI bindings for Key Encapsulation Mechanisms.
  */
@@ -4147,6 +4175,106 @@ class FFI_DH_Test final : public FFI_Test {
       }
 };
 
+class FFI_OID_Test final : public FFI_Test {
+   public:
+      std::string name() const override { return "FFI OID"; }
+
+      void ffi_test(Test::Result& result, botan_rng_t rng) override {
+         botan_asn1_oid_t oid;
+         botan_asn1_oid_t new_oid;
+         botan_asn1_oid_t new_oid_from_string;
+         botan_asn1_oid_t oid_a;
+         botan_asn1_oid_t oid_b;
+         botan_asn1_oid_t oid_c;
+
+         TEST_FFI_FAIL("empty oid", botan_oid_from_string, (&oid, ""));
+         TEST_FFI_OK(botan_oid_from_string, (&oid, "1.2.3.4.5"));
+
+         TEST_FFI_RC(BOTAN_FFI_ERROR_BAD_PARAMETER, botan_oid_from_string, (&new_oid, "a.a.a"));
+         TEST_FFI_RC(BOTAN_FFI_ERROR_BAD_PARAMETER, botan_oid_from_string, (&new_oid, "0.40"));
+         TEST_FFI_RC(
+            BOTAN_FFI_ERROR_BAD_PARAMETER, botan_oid_from_string, (&new_oid, "random-name-that-definitely-has-no-oid"));
+
+         TEST_FFI_OK(botan_oid_from_string, (&new_oid, "1.2.3.4.5.6.7.8"));
+         TEST_FFI_OK(botan_oid_register, (new_oid, "random-name-that-definitely-has-no-oid"));
+
+         TEST_FFI_OK(botan_oid_from_string, (&new_oid_from_string, "random-name-that-definitely-has-no-oid"));
+         TEST_FFI_RC(1, botan_oid_equal, (new_oid, new_oid_from_string));
+
+         TEST_FFI_OK(botan_oid_from_string, (&oid_a, "1.2.3.4.5.6"));
+         TEST_FFI_OK(botan_oid_from_string, (&oid_b, "1.2.3.4.5.6"));
+         TEST_FFI_OK(botan_oid_from_string, (&oid_c, "1.2.3.4.4"));
+
+         TEST_FFI_RC(1, botan_oid_equal, (oid_a, oid_b));
+         TEST_FFI_RC(0, botan_oid_equal, (oid_a, oid_c));
+
+         int res;
+
+         TEST_FFI_OK(botan_oid_cmp, (&res, oid_a, oid_b));
+         result.confirm("oid_a and oid_b are equal", res == 0);
+
+         TEST_FFI_OK(botan_oid_cmp, (&res, oid_a, oid_c));
+         result.confirm("oid_a is bigger", res == 1);
+
+         TEST_FFI_OK(botan_oid_cmp, (&res, oid_c, oid_a));
+         result.confirm("oid_c is smaller", res == -1);
+
+         TEST_FFI_OK(botan_oid_destroy, (oid));
+         TEST_FFI_OK(botan_oid_destroy, (new_oid));
+         TEST_FFI_OK(botan_oid_destroy, (new_oid_from_string));
+         TEST_FFI_OK(botan_oid_destroy, (oid_a));
+         TEST_FFI_OK(botan_oid_destroy, (oid_b));
+         TEST_FFI_OK(botan_oid_destroy, (oid_c));
+
+         botan_privkey_t priv;
+         if(TEST_FFI_INIT(botan_privkey_create_rsa, (&priv, rng, 1024))) {
+            TEST_FFI_OK(botan_privkey_check_key, (priv, rng, 0));
+
+            const std::string oid_rsa_expexted = "1.2.840.113549.1.1.1";
+
+            botan_asn1_oid_t rsa_oid_priv;
+            botan_asn1_oid_t rsa_oid_pub;
+            botan_asn1_oid_t rsa_oid_expected;
+            botan_asn1_oid_t rsa_oid_from_name;
+
+            TEST_FFI_RC(BOTAN_FFI_ERROR_NULL_POINTER, botan_oid_from_string, (&rsa_oid_expected, nullptr));
+            TEST_FFI_RC(BOTAN_FFI_ERROR_NULL_POINTER, botan_oid_from_string, (nullptr, "1.2.3.4.5"));
+            TEST_FFI_OK(botan_oid_from_string, (&rsa_oid_expected, oid_rsa_expexted.c_str()));
+            TEST_FFI_OK(botan_privkey_oid, (&rsa_oid_priv, priv));
+
+            TEST_FFI_RC(1, botan_oid_equal, (rsa_oid_priv, rsa_oid_expected));
+
+            botan_pubkey_t pub;
+            TEST_FFI_OK(botan_privkey_export_pubkey, (&pub, priv));
+
+            TEST_FFI_OK(botan_pubkey_oid, (&rsa_oid_pub, pub));
+            TEST_FFI_RC(1, botan_oid_equal, (rsa_oid_pub, rsa_oid_expected));
+
+            ViewStringSink oid_string;
+            TEST_FFI_OK(botan_oid_view_string, (rsa_oid_expected, oid_string.delegate(), oid_string.callback()));
+            std::string oid_actual = {oid_string.get().begin(), oid_string.get().end()};
+
+            result.test_eq("oid to string", oid_actual, oid_rsa_expexted);
+
+            TEST_FFI_OK(botan_oid_from_string, (&rsa_oid_from_name, "RSA"));
+            TEST_FFI_RC(1, botan_oid_equal, (rsa_oid_expected, rsa_oid_from_name));
+
+            ViewStringSink rsa_name;
+            TEST_FFI_OK(botan_oid_view_name, (rsa_oid_from_name, rsa_name.delegate(), rsa_name.callback()));
+            std::string rsa_name_string = {rsa_name.get().begin(), rsa_name.get().end()};
+            result.test_eq("oid to name", rsa_name_string, "RSA");
+
+            TEST_FFI_OK(botan_oid_destroy, (rsa_oid_priv));
+            TEST_FFI_OK(botan_oid_destroy, (rsa_oid_pub));
+            TEST_FFI_OK(botan_oid_destroy, (rsa_oid_expected));
+            TEST_FFI_OK(botan_oid_destroy, (rsa_oid_from_name));
+
+            TEST_FFI_OK(botan_pubkey_destroy, (pub));
+            TEST_FFI_OK(botan_privkey_destroy, (priv));
+         }
+      }
+};
+
 BOTAN_REGISTER_TEST("ffi", "ffi_utils", FFI_Utils_Test);
 BOTAN_REGISTER_TEST("ffi", "ffi_rng", FFI_RNG_Test);
 BOTAN_REGISTER_TEST("ffi", "ffi_rsa_cert", FFI_RSA_Cert_Test);
@@ -4196,6 +4324,7 @@ BOTAN_REGISTER_TEST("ffi", "ffi_frodokem", FFI_FrodoKEM_Test);
 BOTAN_REGISTER_TEST("ffi", "ffi_cmce", FFI_Classic_McEliece_Test);
 BOTAN_REGISTER_TEST("ffi", "ffi_elgamal", FFI_ElGamal_Test);
 BOTAN_REGISTER_TEST("ffi", "ffi_dh", FFI_DH_Test);
+BOTAN_REGISTER_TEST("ffi", "ffi_oid", FFI_OID_Test);
 
 #endif
 


Reviews:
user: login: arckoor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T15:31:44Z
user: login: arckoor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T15:32:11Z
user: login: randombit
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-03T01:40:29Z
user: login: randombit
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-03T01:46:46Z
user: login: arckoor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T08:55:44Z
user: login: arckoor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T15:38:18Z
user: login: randombit
body: LGTM @arckoor thanks! The changes to the Python wrappers re `c_void_p` are optional (but would be appreciated for sure)
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-18T11:25:02Z

Review comments:
diff_hunk: @@ -0,0 +1,85 @@
+/*
+* (C) 2017 Jack Lloyd
+* (C) 2025 Dominik Schricker
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/ffi.h>
+
+#include <botan/pk_keys.h>
+#include <botan/internal/ffi_oid.h>
+#include <botan/internal/ffi_pkey.h>
+#include <botan/internal/ffi_util.h>
+
+extern "C" {
+
+using namespace Botan_FFI;
+
+int botan_oid_from_string(botan_asn1_oid_t* oid_obj, const char* oid_str) {
+   return ffi_guard_thunk(__func__, [=]() -> int {
+      if(oid_obj == nullptr) {
+         return BOTAN_FFI_ERROR_NULL_POINTER;
+      }
+
+      *oid_obj = nullptr;
+
+      std::unique_ptr<Botan::OID> oid = std::make_unique<Botan::OID>(oid_str);
+      if(oid && !oid->empty()) {
+         *oid_obj = new botan_asn1_oid_struct(std::move(oid));
+         return BOTAN_FFI_SUCCESS;
+      } else {
+         return BOTAN_FFI_ERROR_INVALID_INPUT;
+      }
+   });
+}
+
+// todo string view thingy
+// also todo parse RSA and other such names
+
+int botan_oid_equal(botan_asn1_oid_t a_w, botan_asn1_oid_t b_w) {
+   return BOTAN_FFI_VISIT(a_w, [=](const auto& a) -> int { return a == safe_get(b_w); });
+}
+
+int botan_oid_cmp(int* result, botan_asn1_oid_t a_w, botan_asn1_oid_t b_w) {
path: src/lib/ffi/ffi_oid.cpp
user: login: arckoor
body: The implementation for this is basically stolen from `botan_mp_cmp`, but one thing I did notice is that `* result` isn't checked anywhere, not in the macro, nor within the function, i.e. this can segfault. Is this intended? There are a few other functions that also behave like this, e.g. `botan_mp_num_bytes`
created_at: 2025-04-02T15:31:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+/*
+* (C) 2017 Jack Lloyd
+* (C) 2025 Dominik Schricker
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_FFI_OID_H_
+#define BOTAN_FFI_OID_H_
+
+#include <botan/asn1_obj.h>
+#include <botan/internal/ffi_util.h>
+
+extern "C" {
+
+// TODO get a proper magic number from somewhere:tm:
+BOTAN_FFI_DECLARE_STRUCT(botan_asn1_oid_struct, Botan::OID, 0x123456);
path: src/lib/ffi/ffi_oid.h
user: login: arckoor
body: How should I create / compute the magic number here?
created_at: 2025-04-02T15:32:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+/*
+* (C) 2017 Jack Lloyd
+* (C) 2025 Dominik Schricker
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_FFI_OID_H_
+#define BOTAN_FFI_OID_H_
+
+#include <botan/asn1_obj.h>
+#include <botan/internal/ffi_util.h>
+
+extern "C" {
+
+// TODO get a proper magic number from somewhere:tm:
+BOTAN_FFI_DECLARE_STRUCT(botan_asn1_oid_struct, Botan::OID, 0x123456);
path: src/lib/ffi/ffi_oid.h
user: login: randombit
body: Any random 32-bit integer (eg `botan rng --format=hex 4`). Main point is that it does not collide with any other FFI magic number, so someone accidentally using the wrong type gets a reasonable error rather than a memory corruption. Secondary point is tht it isn't likely otherwise to collide with memory in a random process memory (eg all 0s is valid 32-bit integer, doesn't collide with other uses, but can easily collide with something randomly on a heap)
created_at: 2025-04-03T01:40:28Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,85 @@
+/*
+* (C) 2017 Jack Lloyd
+* (C) 2025 Dominik Schricker
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/ffi.h>
+
+#include <botan/pk_keys.h>
+#include <botan/internal/ffi_oid.h>
+#include <botan/internal/ffi_pkey.h>
+#include <botan/internal/ffi_util.h>
+
+extern "C" {
+
+using namespace Botan_FFI;
+
+int botan_oid_from_string(botan_asn1_oid_t* oid_obj, const char* oid_str) {
+   return ffi_guard_thunk(__func__, [=]() -> int {
+      if(oid_obj == nullptr) {
+         return BOTAN_FFI_ERROR_NULL_POINTER;
+      }
+
+      *oid_obj = nullptr;
+
+      std::unique_ptr<Botan::OID> oid = std::make_unique<Botan::OID>(oid_str);
+      if(oid && !oid->empty()) {
+         *oid_obj = new botan_asn1_oid_struct(std::move(oid));
+         return BOTAN_FFI_SUCCESS;
+      } else {
+         return BOTAN_FFI_ERROR_INVALID_INPUT;
+      }
+   });
+}
+
+// todo string view thingy
+// also todo parse RSA and other such names
+
+int botan_oid_equal(botan_asn1_oid_t a_w, botan_asn1_oid_t b_w) {
+   return BOTAN_FFI_VISIT(a_w, [=](const auto& a) -> int { return a == safe_get(b_w); });
+}
+
+int botan_oid_cmp(int* result, botan_asn1_oid_t a_w, botan_asn1_oid_t b_w) {
path: src/lib/ffi/ffi_oid.cpp
user: login: randombit
body: `result` should be checked to not be null with the appropriate error code returned if it is. The general intention is, if a FFI function is called with a pointer value that is null when it should not be, we return the dedicated null pointer error code - any function that's missing such a check has a bug.

This is a lot stricter than the C++ API; there we might at times detect/reject a null pointer but it's not assured. FFI is in a weird place because it's commonly called from languages with effectively much weaker type safety than C++, so it goes to some efforts to make errors like this non-fatal.
created_at: 2025-04-03T01:46:46Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,85 @@
+/*
+* (C) 2017 Jack Lloyd
+* (C) 2025 Dominik Schricker
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/ffi.h>
+
+#include <botan/pk_keys.h>
+#include <botan/internal/ffi_oid.h>
+#include <botan/internal/ffi_pkey.h>
+#include <botan/internal/ffi_util.h>
+
+extern "C" {
+
+using namespace Botan_FFI;
+
+int botan_oid_from_string(botan_asn1_oid_t* oid_obj, const char* oid_str) {
+   return ffi_guard_thunk(__func__, [=]() -> int {
+      if(oid_obj == nullptr) {
+         return BOTAN_FFI_ERROR_NULL_POINTER;
+      }
+
+      *oid_obj = nullptr;
+
+      std::unique_ptr<Botan::OID> oid = std::make_unique<Botan::OID>(oid_str);
+      if(oid && !oid->empty()) {
+         *oid_obj = new botan_asn1_oid_struct(std::move(oid));
+         return BOTAN_FFI_SUCCESS;
+      } else {
+         return BOTAN_FFI_ERROR_INVALID_INPUT;
+      }
+   });
+}
+
+// todo string view thingy
+// also todo parse RSA and other such names
+
+int botan_oid_equal(botan_asn1_oid_t a_w, botan_asn1_oid_t b_w) {
+   return BOTAN_FFI_VISIT(a_w, [=](const auto& a) -> int { return a == safe_get(b_w); });
+}
+
+int botan_oid_cmp(int* result, botan_asn1_oid_t a_w, botan_asn1_oid_t b_w) {
path: src/lib/ffi/ffi_oid.cpp
user: login: arckoor
body: Alright, then I'll fix it for this PR's methods only for now, and in a separate PR I can maybe go through `ffi.h` and fix the rest.
created_at: 2025-04-03T08:55:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2123,6 +2143,59 @@ def clear_bit(self, bit):
     def set_bit(self, bit):
         _DLL.botan_mp_set_bit(self.__obj, c_size_t(bit))
 
+
+class OID:
+    def __init__(self, obj=None):
+        if not obj:
+            obj = c_void_p(0)
+        self.__obj = obj
path: src/python/botan3.py
user: login: arckoor
body: I intended to use the same `def __init__(self, obj=c_void_p(0)):` that `PublicKey` and `PrivateKey` use, but as it turns out python does _not_ create a new `c_void_p` everytime this is called, but instead creates it only once and then reuses it, leading to every instance of `OID` writing to the same memory. I would consider this unintended behavior, and thus solved it like so. `PrivateKey` and `PublicKey` get around this by meticulously creating new `c_void_p`s, and then passing that to their constructor. Should I adapt these two in the OID way, or leave them as is?
created_at: 2025-04-07T15:38:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2123,6 +2143,59 @@ def clear_bit(self, bit):
     def set_bit(self, bit):
         _DLL.botan_mp_set_bit(self.__obj, c_size_t(bit))
 
+
+class OID:
+    def __init__(self, obj=None):
+        if not obj:
+            obj = c_void_p(0)
+        self.__obj = obj
path: src/python/botan3.py
user: login: randombit
body: Yes I ran into exactly this issue at some point and was quite surprised! Using `=None` as here is probably the way to go.
created_at: 2025-04-18T11:24:04Z
author_association: OWNER

PR comments:
user: login: coveralls
created_at: 2025-04-02T17:10:22Z
body: 
[![Coverage Status](https://coveralls.io/builds/73293161/badge)](https://coveralls.io/builds/73293161)

coverage: 91.32% (+0.01%) from 91.309%
when pulling **abf346372183cc7c6813389746ad35db348303a6 on arckoor:oid-utils**
into **7f66c45b37b6beb016e49f38de0e438ec2be44bf on randombit:master**.

author_association: NONE
user: login: arckoor
created_at: 2025-04-07T17:35:22Z
body: I have frankly no idea why `Clang Tidy` is failing :/
author_association: CONTRIBUTOR
user: login: arckoor
created_at: 2025-04-17T17:28:10Z
body: I snuck in the missing documentation for `stateful_operations` in ffi.rst and fixed some syntax errors there while I was at it
author_association: CONTRIBUTOR
user: login: randombit
created_at: 2025-04-18T11:25:22Z
body: `clang-tidy` failure is unrelated, I will address in another PR
author_association: OWNER
user: login: arckoor
created_at: 2025-04-18T17:43:24Z
body: @randombit I've modified PubKey and PrivKey to correctly init the `c_void_p`, can be merged from my side
author_association: CONTRIBUTOR