]>
Commit | Line | Data |
---|---|---|
2af4a5fc MF |
1 | #!/usr/bin/python2.4 |
2 | # | |
3 | # Copyright 2009 Google Inc. All Rights Reserved. | |
4 | # | |
5 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | # you may not use this file except in compliance with the License. | |
7 | # You may obtain a copy of the License at | |
8 | # | |
9 | # http://www.apache.org/licenses/LICENSE-2.0 | |
10 | # | |
11 | # Unless required by applicable law or agreed to in writing, software | |
12 | # distributed under the License is distributed on an "AS IS" BASIS, | |
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | # See the License for the specific language governing permissions and | |
15 | # limitations under the License. | |
16 | ||
17 | """Implementation of Magic Signatures low level operations. | |
18 | ||
19 | See Magic Signatures RFC for specification. This implements | |
20 | the cryptographic layer of the spec, essentially signing and | |
21 | verifying byte buffers using a public key algorithm. | |
22 | """ | |
23 | ||
24 | __author__ = 'jpanzer@google.com (John Panzer)' | |
25 | ||
26 | ||
27 | import base64 | |
28 | import re | |
29 | ||
30 | # PyCrypto: Note that this is not available in the | |
31 | # downloadable GAE SDK, must be installed separately. | |
32 | # See http://code.google.com/p/googleappengine/issues/detail?id=2493 | |
33 | # for why this is most easily installed under the | |
34 | # project's path rather than somewhere more sane. | |
35 | import Crypto.PublicKey | |
36 | import Crypto.PublicKey.RSA | |
37 | from Crypto.Util import number | |
38 | ||
39 | import hashlib | |
40 | ||
41 | ||
42 | # Note that PyCrypto is a very low level library and its documentation | |
43 | # leaves something to be desired. As a cheat sheet, for the RSA | |
44 | # algorithm, here's a decoding of terminology: | |
45 | # n - modulus (public) | |
46 | # e - public exponent | |
47 | # d - private exponent | |
48 | # (n, e) - public key | |
49 | # (n, d) - private key | |
50 | # (p, q) - the (private) primes from which the keypair is derived. | |
51 | ||
52 | # Thus a public key is a tuple (n,e) and a public/private key pair | |
53 | # is a tuple (n,e,d). Often the exponent is 65537 so for convenience | |
54 | # we default e=65537 in this code. | |
55 | ||
56 | ||
57 | def GenSampleSignature(text): | |
58 | """Demo using a hard coded, test public/private keypair.""" | |
59 | demo_keypair = ('RSA.mVgY8RN6URBTstndvmUUPb4UZTdwvwmddSKE5z_jvKUEK6yk1' | |
60 | 'u3rrC9yN8k6FilGj9K0eeUPe2hf4Pj-5CmHww==' | |
61 | '.AQAB' | |
62 | '.Lgy_yL3hsLBngkFdDw1Jy9TmSRMiH6yihYetQ8jy-jZXdsZXd8V5' | |
63 | 'ub3kuBHHk4M39i3TduIkcrjcsiWQb77D8Q==') | |
64 | ||
65 | signer = SignatureAlgRsaSha256(demo_keypair) | |
66 | return signer.Sign(text) | |
67 | ||
68 | ||
69 | # Utilities | |
70 | def _NumToB64(num): | |
71 | """Turns a bignum into a urlsafe base64 encoded string.""" | |
72 | return base64.urlsafe_b64encode(number.long_to_bytes(num)) | |
73 | ||
74 | ||
75 | def _B64ToNum(b64): | |
76 | """Turns a urlsafe base64 encoded string into a bignum.""" | |
77 | print(b64) | |
78 | return number.bytes_to_long(base64.urlsafe_b64decode(b64)) | |
79 | ||
80 | # Patterns for parsing serialized keys | |
81 | _WHITESPACE_RE = re.compile(r'\s+') | |
82 | _KEY_RE = re.compile( | |
83 | r"""RSA\. | |
84 | (?P<mod>[^\.]+) | |
85 | \. | |
86 | (?P<exp>[^\.]+) | |
87 | (?:\. | |
88 | (?P<private_exp>[^\.]+) | |
89 | )?""", | |
90 | re.VERBOSE) | |
91 | ||
92 | ||
93 | # Implementation of the Magic Envelope signature algorithm | |
94 | class SignatureAlgRsaSha256(object): | |
95 | """Signature algorithm for RSA-SHA256 Magic Envelope.""" | |
96 | ||
97 | def __init__(self, rsa_key): | |
98 | """Initializes algorithm with key information. | |
99 | ||
100 | Args: | |
101 | rsa_key: Key in either string form or a tuple in the | |
102 | format expected by Crypto.PublicKey.RSA. | |
103 | Raises: | |
104 | ValueError: The input format was incorrect. | |
105 | """ | |
106 | if isinstance(rsa_key, tuple): | |
107 | self.keypair = Crypto.PublicKey.RSA.construct(rsa_key) | |
108 | else: | |
109 | self._InitFromString(rsa_key) | |
110 | ||
111 | def ToString(self, full_key_pair=True): | |
112 | """Serializes key to a safe string storage format. | |
113 | ||
114 | Args: | |
115 | full_key_pair: Whether to save the private key portion as well. | |
116 | Returns: | |
117 | The string representation of the key in the format: | |
118 | ||
119 | RSA.mod.exp[.optional_private_exp] | |
120 | ||
121 | Each component is a urlsafe-base64 encoded representation of | |
122 | the corresponding RSA key field. | |
123 | """ | |
124 | mod = _NumToB64(self.keypair.n) | |
125 | exp = '.' + _NumToB64(self.keypair.e) | |
126 | private_exp = '' | |
127 | if full_key_pair and self.keypair.d: | |
128 | private_exp = '.' + _NumToB64(self.keypair.d) | |
129 | return 'RSA.' + mod + exp + private_exp | |
130 | ||
131 | def _InitFromString(self, text): | |
132 | """Parses key from a standard string storage format. | |
133 | ||
134 | Args: | |
135 | text: The key in text form. See ToString for description | |
136 | of expected format. | |
137 | Raises: | |
138 | ValueError: The input format was incorrect. | |
139 | """ | |
140 | # First, remove all whitespace: | |
141 | text = re.sub(_WHITESPACE_RE, '', text) | |
142 | #throw away first item and convert from base64 to long | |
143 | items = [_B64ToNum(item) for item in text.split('.')[1:]] | |
144 | self.keypair = Crypto.PublicKey.RSA.construct(items) | |
145 | ||
146 | def GetName(self): | |
147 | """Returns string identifier for algorithm used.""" | |
148 | return 'RSA-SHA256' | |
149 | ||
150 | def _MakeEmsaMessageSha256(self, msg, modulus_size, logf=None): | |
151 | """Algorithm EMSA_PKCS1-v1_5 from PKCS 1 version 2. | |
152 | ||
153 | This is derived from keyczar code, and implements the | |
154 | additional ASN.1 compatible magic header bytes and | |
155 | padding needed to implement PKCS1-v1_5. | |
156 | ||
157 | Args: | |
158 | msg: The message to sign. | |
159 | modulus_size: The size of the key (in bits) used. | |
160 | Returns: | |
161 | The byte sequence of the message to be signed. | |
162 | """ | |
163 | magic_sha256_header = [0x30, 0x31, 0x30, 0xd, 0x6, 0x9, 0x60, 0x86, 0x48, | |
164 | 0x1, 0x65, 0x3, 0x4, 0x2, 0x1, 0x5, 0x0, 0x4, 0x20] | |
165 | ||
166 | hash_of_msg = hashlib.sha256(msg).digest() #??? | |
167 | ||
168 | self._Log(logf, 'sha256 digest of msg %s: %s' % (msg, hash_of_msg)) | |
169 | ||
170 | encoded = bytes(magic_sha256_header) + hash_of_msg | |
171 | msg_size_bits = modulus_size + 8-(modulus_size % 8) # Round up to next byte | |
172 | ||
173 | pad_string = b'\xFF' * (msg_size_bits // 8 - len(encoded) - 3) | |
174 | return b'\x00\x01' + pad_string + b'\x00' + encoded | |
175 | ||
176 | def _Log(self, logf, s): | |
177 | """Append message to log if log exists.""" | |
178 | print(s) | |
179 | if logf: | |
180 | logf(s + '\n') | |
181 | ||
182 | def Sign(self, bytes_to_sign, logf=None): | |
183 | """Signs the bytes using PKCS-v1_5. | |
184 | ||
185 | Args: | |
186 | bytes_to_sign: The bytes to be signed. | |
187 | Returns: | |
188 | The signature in base64url encoded format. | |
189 | """ | |
190 | # Implements PKCS1-v1_5 w/SHA256 over the bytes, and returns | |
191 | # the result as a base64url encoded bignum. | |
192 | ||
193 | self._Log(logf, 'bytes_to_sign = %s' % bytes_to_sign) | |
194 | self._Log(logf, 'len = %d' % len(bytes_to_sign)) | |
195 | ||
196 | self._Log(logf, 'keypair size : %s' % self.keypair.size()) | |
197 | ||
198 | # Generate the PKCS1-v1_5 compatible message, which includes | |
199 | # magic ASN.1 bytes and padding: | |
200 | emsa_msg = self._MakeEmsaMessageSha256(bytes_to_sign, self.keypair.size(), logf) | |
201 | # TODO(jpanzer): Check whether we need to use max keysize above | |
202 | # or just keypair.size | |
203 | ||
204 | self._Log(logf, 'emsa_msg = %s' % emsa_msg) | |
205 | ||
206 | # Compute the signature: | |
207 | signature_long = self.keypair.sign(emsa_msg, None)[0] | |
208 | ||
209 | # Encode the signature as armored text: | |
210 | signature_bytes = number.long_to_bytes(signature_long) | |
211 | ||
212 | self._Log(logf, 'signature_bytes = [%s]' % signature_bytes) | |
213 | ||
214 | return base64.urlsafe_b64encode(signature_bytes) | |
215 | ||
216 | def Verify(self, signed_bytes, signature_b64): | |
217 | """Determines the validity of a signature over a signed buffer of bytes. | |
218 | ||
219 | Args: | |
220 | signed_bytes: string The buffer of bytes the signature_b64 covers. | |
221 | signature_b64: string The putative signature, base64-encoded, to check. | |
222 | Returns: | |
223 | True if the request validated, False otherwise. | |
224 | """ | |
225 | # Generate the PKCS1-v1_5 compatible message, which includes | |
226 | # magic ASN.1 bytes and padding: | |
227 | emsa_msg = self._MakeEmsaMessageSha256(signed_bytes, | |
228 | self.keypair.size()) | |
229 | ||
230 | # Get putative signature: | |
231 | putative_signature = base64.urlsafe_b64decode(signature_b64) | |
232 | putative_signature = number.bytes_to_long(putative_signature) | |
233 | ||
234 | # Verify signature given public key: | |
235 | return self.keypair.verify(emsa_msg, (putative_signature,)) |