But, What Does A Digital Signature Actually Look Like?

I created the asecuritysite.com web site as I found there were very few sites which have practical implementations of the core methods. As…

Photo by Kelly Sikkema on Unsplash

But, What Does A Digital Signature Actually Look Like?

I created the asecuritysite.com website as I found there were very few sites that have practical implementations of the core methods. As a teacher, too, I believe that I should not give students something that I don’t know how to implement myself. And so the Web site has grown, as my knowledge of the field has grown. For me, it is a scratchpad for ideas, and where I want to show that the implementation of fairly complex cryptography methods is actually quite easy to understand. Along with this, things become real when they are made practical.

And so I get a good deal of questions, and one just said, “What does a digital signature actually look like?”.

Well, without going into detail, the most common digital signature is ECDSA (Elliptic Curve Digital Signature Algorithm). Basically, it takes a message (M), a private key (sk), and a random value (k), and produces a digital signature of r and s. These are just two integer values. To check the signature, we take the message (M), the associated public key (pk), r and s, and perform a validation test, and, if it passes, the message has a correct signature.

But what does the signature actually look like?

So what does the signature actually look like? Is it just two numbers r and s that we add to the message? In many cases, it takes the form of the DER format, and which uses ASN.1 to define abstract types and values. Basically, it takes the two integer values (r and s) and encapsulates them into a more structured format. This format can be read by any computer system.

One of the most basic types is SEQUENCE and is an ordered collection of one or more types. In DER, SEQUENCE is identified with a tag of “30”, and followed by a byte value for the length of the object defined. The other common types are OBJECT IDENTIFIER (and which has a tag of “06”), a BIT STRING (and which has a tag of “03”) and INTEGER (and which has a tag of “02”). In the case of a signature, we just use the INTEGER definition for the values.

A NIST P-192 signature

So here is an example DER signature from NIST P-192 (and which uses 192-bit integer values) [here]:

3035021900935f599bbdb30fc81a8b9de2f82311c6fa704838b53f9d7a0218267e3abb5bc3a44b0e368442ed3699b23ce87a28bc32cc53

We first encounter the SEQUENCE (“30”), and then the next byte defines the length of the values which come next. In this case, “0x35” is 53 bytes. If you count the number of bytes after 35, you will find there are 53 bytes (or 106 hex characters) [here]:

30 35
02 19 00935f599bbdb30fc81a8b9de2f82311c6fa704838b53f9d7a
02 18 267e3abb5bc3a44b0e368442ed3699b23ce87a28bc32cc53

Next, we have a “02” tag, and then a “19”, and where the “19” value identifies 25 bytes (or 50 hex characters). We can then read r as the next 50 hex characters. Next, we have another “02” tag, and then a “19”, and where the “18” value identifies 24 bytes (or 48 hex characters). We can then read r as the next 48 hex characters. So, can we check the size of the integers produced? Well, we just multiply the number of bytes by 8, and we will determine this. Thus r is 25 bytes long, but the first byte is a zero, and s is 24 bytes, so the length of the values of r and s are 24 bytes long. This gives us 192 bits and which fits with the curve (P-192).

A secp256k1 signature

Now, we will try a signature from a common curve (secp256k1) and which uses 256-bit values [here]:

3046022100e4e87c417196c6e5cd63f93e94929ccda6d04fc0a7446922baf3070e854ec4f4
022100a1ecd098008329de9bc93fb2ded6aaceecc921f7183d6b3cfc673b3ef8af219e

Again we can parse, and notice that the integer values are longer this time [here]:

30 46 
02 21 00 e4e87c417196c6e5cd63f93e94929ccda6d04fc0a7446922baf3070e854ec4f4
02 21 00a1ecd098008329de9bc93fb2ded6aaceecc921f7183d6b3cfc673b3ef8af219e

In fact, we have 0x21 bytes for the values of r and s, but the first byte is a zero, so we actually have 0x20 bytes, and which is 32 bytes. This will give us 256 bits, and which fits with the curve.

Coding

The code involves just parses for the DER format, and is [here]:

import asn1
import binascii
from pem import class_id_to_string,tag_id_to_string,value_to_string
import sys
import base64

der='3046022100e4e87c417196c6e5cd63f93e94929ccda6d04fc0a7446922baf3070e854ec4f4022100a1ecd098008329de9bc93fb2ded6aaceecc921f7183d6b3cfc673b3ef8af219e'
# See https://asecuritysite.com/encryption/sigs2

indent=0

if (len(sys.argv)>1):
der=str(sys.argv[1])

def make_pem(st):
bff="-----BEGIN PUBLIC KEY-----\n"
bff=bff+base64.b64encode(st).decode()+"\n"
bff=bff+"-----END PUBLIC KEY-----\n"
print (bff)

def read_pem(data):
"""Read PEM formatted input."""
data = data.replace("\n","")
data = data.replace("","")
data = data.replace("-----BEGIN PUBLIC KEY-----","")
data = data.replace("-----END PUBLIC KEY-----","")
return binascii.hexlify(base64.b64decode(data))

def show_asn1(string, indent=0):

while not string.eof():
tag = string.peek()
if tag.typ == asn1.Types.Primitive:
tag, value = string.read()
print(' ' * indent,end='')
print('[{}] {}: {}'.format(class_id_to_string(tag.cls), tag_id_to_string(tag.nr),value_to_string(tag.nr, value)))

if (tag.nr==4):
private_key=binascii.hexlify(value)
print(' ' * indent,end='')
print("Private key: ",private_key.decode())

if (tag.nr==3):

res=binascii.hexlify(value).decode()
length=len(res)
if (res.__contains__('10001')): # RSA

rtn=res[1:].find("02")
print (res[rtn+3:rtn+5])
byte = int(res[rtn+3:rtn+5],16)-1

rtn=res[1:].find("00")
N=res[rtn+3:rtn+3+(byte)*2]
e=res[length-5:]
print(' ' * indent,end='')
print(f"RSA Modulus ({len(N)*4}) bits: {N}")
print(f"RSA e: {e}")
else : # ECC
public_key_x=res[4:length//2]
public_key_y=res[length//2:]
print(' ' * indent,end='')
print(f"Public key ({public_key_x}, {public_key_y})")

elif tag.typ == asn1.Types.Constructed:
print(' ' * indent,end='')
print('[{}] {}'.format(class_id_to_string(tag.cls), tag_id_to_string(tag.nr)))
string.enter()
show_asn1(string, indent + 2)
string.leave()

Print=True
if (len(der)>500): Print=False

if (der.__contains__("BEGIN")):
print("Found PEM")
der=read_pem(der)

if (Print): print (f"PEM: {der}\n")

st=binascii.unhexlify(der)
decoder = asn1.Decoder()
decoder.start(st)
show_asn1(decoder)
print()

if (Print): make_pem(st)

Conclusions

And there you go … the magic of (r,s).