Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Andrew D. Yates
committed
Aug 15, 2010
0 parents
commit 6035f89
Showing
3 changed files
with
214 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
*.pyc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
xmldsig | ||
Sign and verify digital signatures in native Python with RSA | ||
|
||
Copyright © 2010 Andrew D. Yates | ||
All Rights Reserved | ||
|
||
Working Version 0 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
#!/usr/bin/python2.5 | ||
# -*- coding: utf-8 -*- | ||
# Copyright © 2010 Andrew D. Yates | ||
# All Rights Reserved | ||
"""XMLDSig: Sign and Verify XML digital cryptographic signatures. | ||
xmldsig is a minimal implementation of bytestring cryptographic | ||
digital signatures which I have written to handle the Google | ||
Application Single Sign On service in Security Assertion Markup | ||
Language. (Google Apps, SSO, SAML respectively). | ||
XMLDSIG is a ridiculous but necessary protocol for identification and | ||
authentication in computer systems. Why is XMLDSIG ridiculous? The | ||
defining feature of XML is that its exact bytestring of the message is | ||
arbitrary. The defining feature of a cryptographic signature is that | ||
the exact bytestring of the message is critical. Only by what I can | ||
imagine to be some "practical man's" increasingly pathetic sophistry | ||
to assert himself over the ideas which he derides as "of no practical | ||
importance" could two such elegant ideas be so crudely forced together | ||
and then ---with so much energy--- propped, pushed, and defended. | ||
In this module, all XML must be in Bytestring XML Format: | ||
Bytestring XML Format | ||
===================== | ||
* XML is a utf-8 encoded bytestring. | ||
* XML namespaces must explicitly define all xmlns prefix names | ||
* XML is in minimum whitespace representation. | ||
* <Signature> always signs the entire xml string | ||
* signed XML must be in "Canonicalization" (c14n) form | ||
see: http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments | ||
* <Signature> is always enveloped as the first child of root | ||
see: http://www.w3.org/2000/09/xmldsig#enveloped-signature | ||
Note that whitespace, character case, and encoding are significant in | ||
Bytestring XML: e.g. "<b>text</b>" is not the same as "<b> text</b>". | ||
References | ||
========== | ||
* [DI] | ||
http://www.di-mgt.com.au/xmldsig.html | ||
Signing an XML document using XMLDSIG | ||
* [RFC 2437] | ||
http://www.ietf.org/rfc/rfc2437.txt | ||
PKCS #1: RSA Cryptography Specifications | ||
* [RFC 3275] | ||
http://www.ietf.org/rfc/rfc3275.txt | ||
(Extensible Markup Language) XML-Signature Syntax and Processing | ||
* [RSA-SHA1] | ||
http://www.w3.org/TR/2008/REC-xmldsig-core-20080610/#sec-PKCS1 | ||
XML Signature Syntax and Processing (Second Edition) | ||
Section: 6.4.2 PKCS1 (RSA-SHA1) | ||
""" | ||
|
||
import hashlib | ||
import re | ||
# change to string literal | ||
import os.path | ||
|
||
# change to python string insertion | ||
from google.appengine.ext.webapp import template | ||
|
||
|
||
RX_ROOT = re.compile('<[^> ]+ ?([^>]*)>') | ||
RX_NS = re.compile('xmlns:[^> ]+') | ||
RX_SIGNATURE = re.compile('<Signature.*?</Signature>') | ||
RX_SIGNED_INFO = re.compile('<SignedInfo.*?</SignedInfo>') | ||
RX_SIG_VALUE = re.compile('<SignatureValue[^>]*>([^>]+)</SignatureValue>') | ||
|
||
PATH = os.path.dirname(os.path.abspath(__file__)) | ||
SIGNED_INFO = os.path.join(PATH, "templates/signed_info.xml") | ||
SIGNATURE = os.path.join(PATH, "templates/signature.xml") | ||
|
||
# SHA1 digest with ASN.1 BER SHA1 algorithm designator prefix [RSA-SHA1] | ||
PREFIX = '\x30\x21\x30\x09\x06\x05\x2B\x0E\x03\x02\x1A\x05\x00\x04\x14' | ||
|
||
PTN_SIGNEDINFO_XML = \ | ||
'<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"{% if xmlns_attr %} {% endif %}{{ xmlns_attr }}><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"></CanonicalizationMethod><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></SignatureMethod><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod><DigestValue>{{ digest_value }}</DigestValue></Reference></SignedInfo>' | ||
|
||
PTN_SIGNATURE_XML = \ | ||
'<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">{{ signed_info_xml }}<SignatureValue>{{ signature_value }}</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>{{ modulus }}</Modulus><Exponent>{{ exponent }}</Exponent></RSAKeyValue></KeyValue></KeyInfo></Signature>' | ||
|
||
b64e = lambda s: s.encode('base64').strip() | ||
b64d = lambda s: s.decode('base64').strip() | ||
|
||
|
||
def sign(xml, f_private, modulus, exponent): | ||
"""Return xmldsig XML string from xml_string of XML. | ||
Args: | ||
xml: str of bytestring xml to sign | ||
f_private: func | ||
modulus: str | ||
exponent: str | ||
Returns: | ||
str: signed bytestring xml | ||
""" | ||
signed_info_xml = _signed_info(xml) | ||
signed = _signed_value(signed_info_xml, len(modulus)) | ||
signature_value = f_private(signed) | ||
|
||
signature_cxt = { | ||
'signed_info_xml': signed_info_xml, | ||
'signature_value': b64e(signature_value), | ||
'modulus': b64e(modulus), | ||
'exponent': b64e(exponent), | ||
} | ||
signature_xml = template.render(SIGNATURE, signature_cxt) | ||
|
||
# insert xmldsig after first '>' in message | ||
signed_xml = xml.replace('>', '>'+signature_xml, 1) | ||
return signed_xml | ||
|
||
|
||
def verify(xml, f_public, modulus): | ||
"""Return if <Signature> in `xml` matches `xml` with `key`. | ||
Args: | ||
xml: str of XML with xmldsig <Signature> element | ||
f_public: func | ||
modulus: str | ||
Returns: | ||
bool: signature for `xml` is valid | ||
""" | ||
signature = RX_SIGNATURE.search(xml).group(0) | ||
unsigned_xml = xml.replace(signature, '') | ||
|
||
# retreive the signed value in `xml` using `key` | ||
signature_value = RX_SIG_VALUE.search(signature).group(1) | ||
expected = f_public(b64d(signature_value)) | ||
|
||
# compute the actual signed value for `xml` | ||
signed_info_xml = _signed_info(unsigned_xml) | ||
actual = _signed_value(signed_info_xml, len(modulus)) | ||
|
||
return expected == actual | ||
|
||
|
||
def _digest(data): | ||
"""SHA1 hash digest of message data. | ||
Implements RFC2437, 9.2.1 EMSA-PKCS1-v1_5, Step 1. for "Hash = SHA1" | ||
Args: | ||
data: str of bytes to digest | ||
Returns: | ||
str: of bytes of digest from `data` | ||
""" | ||
hasher = hashlib.sha1() | ||
hasher.update(data) | ||
return hasher.digest() | ||
|
||
|
||
def _get_xmlns_prefixes(xml): | ||
"""Return string of root namespace prefix attributes in given order. | ||
Args: | ||
xml: str of bytestring xml | ||
Returns: | ||
str: [xmlns:prefix="uri"] list ordered as in `xml` | ||
""" | ||
root_attr = RX_ROOT.match(xml).group(1) | ||
ns_attrs = [a for a in root_attr.split(' ') if RX_NS.match(a)] | ||
return ' '.join(ns_attrs) | ||
|
||
|
||
def _signed_info(xml): | ||
"""Return <SignedInfo> for bytestring xml. | ||
Args: | ||
xml: str of bytestring | ||
Returns: | ||
str: xml bytestring of <SignedInfo> computed from `xml` | ||
""" | ||
signed_info_cxt = { | ||
'xmlns_attr': _get_xmlns_prefixes(xml), | ||
'digest_value': b64e(_digest(xml)), | ||
} | ||
signed_info_xml = template.render(SIGNED_INFO, signed_info_cxt) | ||
return signed_info_xml | ||
|
||
|
||
def _signed_value(data, key_size): | ||
"""Return unencrypted rsa-sha1 signature value `padded_digest` from `data`. | ||
The resulting signed value will be in the form: | ||
(01 | FF* | 00 | prefix | digest) [RSA-SHA1] | ||
where "digest" is of the generated c14n xml for <SignedInfo>. | ||
Args: | ||
data: str of bytes to sign | ||
key_size: int of number of bytes in crytographic key | ||
Returns: | ||
str: rsa-sha1 signature value of `data` | ||
""" | ||
asn_digest = PREFIX + _digest(data) | ||
|
||
# Pad to "one octet shorter than the RSA modulus" [RSA-SHA1] | ||
padded_size = key_size - 1 | ||
pad_size = padded_size - len(asn_digest) - 2 | ||
pad = '\x01' + '\xFF' * pad_size + '\x00' | ||
padded_digest = pad + asn_digest | ||
|
||
return padded_digest | ||
|