@@ -413,6 +413,90 @@ def to_bytes_der(self) -> bytes:
413413 encoding = serialization .Encoding .DER , format = serialization .PublicFormat .SubjectPublicKeyInfo
414414 )
415415
416+ @classmethod
417+ def _encode_vlq (cls , value : int ) -> bytes :
418+ """
419+ Encode a value in Variable-Length Quantity (VLQ) format.
420+ """
421+ if value < 0 :
422+ raise ValueError ("VLQ value must be non-negative" )
423+ if value == 0 :
424+ return b"\x00 "
425+
426+ encoded = bytearray ()
427+ while value > 0 :
428+ encoded .insert (0 , value & 0x7F )
429+ value >>= 7
430+
431+ # Set the high bit (0x80) on all bytes except the last
432+ for i in range (len (encoded ) - 1 ):
433+ encoded [i ] |= 0x80
434+
435+ return bytes (encoded )
436+
437+ @classmethod
438+ def _encode_der_length (cls , length : int ) -> bytes :
439+ """Encode a DER length field per X.690 standards."""
440+ if length < 0 :
441+ raise ValueError ("DER length must be non-negative" )
442+ if length < 0x80 :
443+ return bytes ([length ])
444+
445+ length_bytes = length .to_bytes ((length .bit_length () + 7 ) // 8 , "big" )
446+ return bytes ([0x80 | len (length_bytes )]) + length_bytes
447+
448+ @classmethod
449+ def _encode_der_oid (cls , oid : str ) -> bytes :
450+ """
451+ Encode a dotted OID string into DER OID bytes including tag and length.
452+ """
453+ parts = [int (part ) for part in oid .split ("." )]
454+
455+ is_valid = len (parts ) >= 2 and parts [0 ] in (0 , 1 , 2 ) and parts [1 ] >= 0 and (parts [0 ] == 2 or parts [1 ] < 40 )
456+ if not is_valid :
457+ raise ValueError (f"Invalid OID structure for '{ oid } '" )
458+
459+ first , second = parts [0 ], parts [1 ]
460+
461+ # Encode the combined root using VLQ to handle edge cases correctly
462+ encoded = bytearray (cls ._encode_vlq (40 * first + second ))
463+
464+ for value in parts [2 :]:
465+ encoded .extend (cls ._encode_vlq (value ))
466+
467+ return bytes ([0x06 ]) + cls ._encode_der_length (len (encoded )) + bytes (encoded )
468+
469+ @classmethod
470+ def _encode_der_sequence (cls , content : bytes ) -> bytes :
471+ """Encode DER SEQUENCE tag, length, and content per X.690 standards."""
472+ return bytes ([0x30 ]) + cls ._encode_der_length (len (content )) + content
473+
474+ @classmethod
475+ def _encode_der_bit_string (cls , content : bytes ) -> bytes :
476+ """
477+ Encode DER BIT STRING tag, length, and content per X.690 standards.
478+ """
479+ payload = bytes ([0x00 ]) + content
480+ return bytes ([0x03 ]) + cls ._encode_der_length (len (payload )) + payload
481+
482+ def to_bytes_der_ecdsa_compressed (self ) -> bytes :
483+ """
484+ Returns DER-encoded SubjectPublicKeyInfo for secp256k1 using a compressed SEC1 point.
485+ Raises:
486+ ValueError: If this key is not ECDSA secp256k1.
487+ """
488+ if not self .is_ecdsa ():
489+ raise ValueError ("Compressed ECDSA DER export is only supported for ECDSA keys" )
490+
491+ # id-ecPublicKey + secp256k1 OID algorithm identifier
492+ algorithm_id = self ._encode_der_sequence (
493+ self ._encode_der_oid ("1.2.840.10045.2.1" ) + self ._encode_der_oid ("1.3.132.0.10" )
494+ )
495+ compressed_point = self .to_bytes_ecdsa (compressed = True )
496+ subject_public_key = self ._encode_der_bit_string (compressed_point )
497+
498+ return self ._encode_der_sequence (algorithm_id + subject_public_key )
499+
416500 #
417501 # ---------------------------------
418502 # Type-specific (Ed25519, ECDSA secp256k1) to hex string.
@@ -440,6 +524,12 @@ def to_string_der(self) -> str:
440524 """
441525 return self .to_bytes_der ().hex ()
442526
527+ def to_string_der_ecdsa_compressed (self ) -> str :
528+ """
529+ Returns DER SPKI hex for ECDSA secp256k1 using a compressed SEC1 point.
530+ """
531+ return self .to_bytes_der_ecdsa_compressed ().hex ()
532+
443533 def to_string_raw (self ) -> str :
444534 """
445535 Catch all ed25519 or ecdsa for convenience.
0 commit comments