Skip to content
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

any support for ASN.1 encoding signature? #55

Closed
tenmalin opened this issue Mar 18, 2016 · 10 comments
Closed

any support for ASN.1 encoding signature? #55

tenmalin opened this issue Mar 18, 2016 · 10 comments
Milestone

Comments

@tenmalin
Copy link

I wonder to know if the library supports ASN.1 format output of signature?

@tenmalin
Copy link
Author

Currently I write some customized wrapper by myself to solve this issue, but not sure @warner has a plan already.

@hfossli
Copy link

hfossli commented Aug 28, 2018

This works well for me. Is there any reason why this should not work? How about unsigned integers?

from ecdsa import SigningKey, VerifyingKey, NIST256p
import hashlib
import logging

logging.basicConfig(level=logging.DEBUG)

# Generate public & private keys
private_key = SigningKey.generate(curve=NIST256p)
public_key = private_key.get_verifying_key()

# Sign the "ABC" message, hashed with SHA-256
digest = "ABC".encode('utf-8')
signature = private_key.sign(digest, hashfunc=hashlib.sha256)

# Verify the "ABC" message, using the 
public_key.verify(signature, digest, hashfunc=hashlib.sha256)

# Encode the signature in ASN.1 format
raw_signature_hex = ''.join(x.encode('hex') for x in signature)
asn1_signature = "3046" + "022100" + raw_signature_hex[:64] + "022100" + raw_signature_hex[64:]

# Write keys to disk
open("private_key.pem","wb").write(private_key.to_pem())
open("public_key.pem","wb").write(public_key.to_pem())
open("data.bin","wb").write(digest)
open("data.sig","wb").write(asn1_signature)

logging.info('Digest      : ' + ''.join(x.encode('hex') for x in digest))
logging.info('Signature   : ' + asn1_signature)
logging.info('Public key  : \n' + public_key.to_pem())

@tomato42
Copy link
Member

tomato42 commented Aug 28, 2018

@hfossli this is P-256 specific, the ASN.1 encoding will fail with any curve that produces different length output

@hfossli
Copy link

hfossli commented Aug 28, 2018

Yep, thanks for pointing that out. 👍

@hfossli
Copy link

hfossli commented Aug 29, 2018

So, it seems my code has several flaws.

I decided to code the asn1 encoder in a language I was more familiar with. The asn1 encoding can look like this. Tested with 10 000 signatures.

    // https://crypto.stackexchange.com/a/1797
    public struct EcdsaAsn1Signature {
        
        static func smallestBigEndian(_ bytes: Data) -> Data {
            var smallest = bytes
            while smallest.first == 0x00 {
                smallest.removeFirst()
            }
            if let firstByte = smallest.first, firstByte > 0x7f {
                return Data(bytes: [0x00]) + smallest
            } else {
                return smallest
            }
        }
        
        static func isProperlyAsn1Encoded(_ bytes: Data) -> Bool {
            var parser = bytes
            guard bytes.count > 2 else { return false }
            guard parser.popFirst() == 0x30 else { return false }
            guard let sequenceLength = parser.popFirst() else { return false }
            guard parser.count == sequenceLength else { return false }
            guard parser.popFirst() == 0x02 else { return false }
            guard let rLength = parser.popFirst() else { return false }
            guard rLength < parser.count else { return false }
            parser.removeFirst(Int(rLength))
            guard parser.count > 2 else { return false }
            guard parser.popFirst() == 0x02 else { return false }
            guard let sLength = parser.popFirst() else { return false }
            guard sLength == parser.count else { return false }
            return true
        }
        
        static func encode(_ bytes: Data) -> Data {
            guard !isProperlyAsn1Encoded(bytes) else {
                return bytes
            }
            let r = smallestBigEndian(bytes.prefix(bytes.count / 2))
            let s = smallestBigEndian(bytes.suffix(bytes.count / 2))
            var asn1 = Data()
            asn1.append(0x30)
            asn1.append(UInt8(2 + r.count + 2 + s.count))
            asn1.append(0x02)
            asn1.append(UInt8(r.count))
            asn1.append(r)
            asn1.append(0x02)
            asn1.append(UInt8(s.count))
            asn1.append(s)
            return asn1
        }
    }

It should be fairly easy to port this to python. I don't think this code has a problem with being P-256 specific as it makes no assumptions about length.

@tomato42
Copy link
Member

I'm not so sure if it will work for P-521 - for it the the combined length will be 136, which is longer than 127, and if I recall my ASN.1 correctly, that requires different way to encode length...

@hfossli
Copy link

hfossli commented Aug 29, 2018

Thanks for letting me know. Good point!

@wiml
Copy link

wiml commented Oct 26, 2018

The ecdsa.der module has a few functions which should help in properly encoding and decoding the DER-wrapped signatures. You also might be able to use the util.sigencode_der function?

@tomato42 tomato42 added help wanted feature functionality to be implemented labels Jan 4, 2019
@tomato42 tomato42 added not a bug and removed feature functionality to be implemented help wanted labels Sep 27, 2019
@tomato42
Copy link
Member

As @wiml pointed out, this feature is already implemented, the example code from #55 (comment)
should look like this:

from ecdsa import SigningKey, VerifyingKey, NIST256p
from ecdsa.util import sigencode_der, sigdecode_der
import hashlib
import logging

logging.basicConfig(level=logging.DEBUG)

# Generate public & private keys
private_key = SigningKey.generate(curve=NIST256p)
public_key = private_key.get_verifying_key()

# Sign the "ABC" message, hashed with SHA-256
digest = "ABC".encode('utf-8')
signature = private_key.sign(digest, hashfunc=hashlib.sha256, sigencode=sigencode_der)

# Verify the "ABC" message, using the 
public_key.verify(signature, digest, hashfunc=hashlib.sha256, sigdecode=sigdecode_der)

# Write keys to disk
open("private_key.pem","wb").write(private_key.to_pem())
open("public_key.pem","wb").write(public_key.to_pem())
open("data.bin","wb").write(digest)
open("data.sig","wb").write(signature)

logging.info('Digest      : ' + digest.hex())
logging.info('Signature   : ' + signature.hex())
logging.info('Public key  : \n' + str(public_key.to_pem(), 'ascii'))

@tomato42
Copy link
Member

And please note that, in general, the signatures should be DER encoded, that means that 0 byte padding needs to be applied only if the first byte of encoded integer has value greater than 0x7f. In other words, the code from #55 (comment) will create malformed signatures (it does create valid BER encoded signatures, so implementations that expect BER will be able to verify them). sigencode_der will create correct DER encoded signatures in all cases.

That being said, sigdecode_der in 0.13.3 and earlier will sometimes accept malformed DER encoded signatures, see #114 and #115

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants