本文代码参考了别人的一些 代码,还有加上自己的理解及一些改进。有问题欢迎大家讨论。
大致思路:
1. 生成私钥公钥,分别经base64 encode后存放本地文件夹。
2. 加签: 对xml文档经私钥加签名。签名元素整个再经base64 encode,创建新的 标签塞入encode后的值且替换原来整个的签名。将加签名后的文档保存到本地文件。
3. 验签: 取加签后的文档, 把的值经base64 decode还原为原签名元素,再把替换为转后真正的签名元素,然后验证签名文档。
4. 文件demo
好了,上干货
KryptoUtil: 用来生成公钥私钥对,并保存到本地文件。同时提供根据文件路径拿到公私钥对象的接口。
SignatureUtil: 提供一些公共接口,比如把Node/Document 转换成string 或者把string反转换成 Node/Element/Document,或者把本地文件转换成document及把document转换成本地文件存放
XmlDigitalSignatureGenerator:生成加签后的xml文件
XmlDigitalSignatureVerifier:验证签名后的xml文件
/**
* use to generate public key and priviate key
*
* @author as
* @version $Id: KryptoUtil.java, v 0.1 29 Jun 2016 16:31:54 as Exp $
*/
public class KryptoUtil {
/**
* Name of the algorithm
*/
private static final String ALGORITHM = "RSA";
/**
* This method is used to generate key pair based upon the provided
* algorithm
*
* @return KeyPair
*/
private KeyPair generateKeyPairs() {
KeyPair keyPair = null;
KeyPairGenerator keyGen;
try {
keyGen = KeyPairGenerator.getInstance(ALGORITHM);
keyGen.initialize(2048);
keyPair = keyGen.genKeyPair();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return keyPair;
}
/**
* Method used to store Private and Public Keys inside a directory
*
* @param dirPath to store the keys
*/
public void storeKeyPairs(String dirPath) {
KeyPair keyPair = generateKeyPairs();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
storeKeys(dirPath + File.separator + "publickey.pem", publicKey);
storeKeys(dirPath + File.separator + "privatekey.pem", privateKey);
}
/**
* Method used to store the key(Public/Private)
*
* @param filePath , name of the file
* @param key
*/
private void storeKeys(String filePath, Key key) {
byte[] keyBytes = key.getEncoded();
File file = new File(filePath);
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
OutputStream outStream = null;
try {
outStream = new FileOutputStream(filePath);
//base64 encode before store it
// System.out.println("keyBytes -> string: " + new String(keyBytes));
// System.out.println("base64 encode keyBytes -> string: " + Base64.encodeBase64String(keyBytes));
outStream.write(Base64.encodeBase64String(keyBytes).getBytes());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (outStream != null) {
try {
outStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* Method used to retrieve the keys in the form byte array
*
* @param filePath of the key
* @return byte array
*/
private byte[] getKeyData(String filePath) {
File file = new File(filePath);
byte[] buffer = new byte[(int) file.length()];
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
fis.read(buffer);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return buffer;
}
/**
* Method used to get the generated Private Key
*
* @param filePath of the PrivateKey file
* @return PrivateKey
*/
public PrivateKey getStoredPrivateKey(String filePath) {
PrivateKey privateKey = null;
// System.out.println("private key string " + new String(getKeyData(filePath)));
// System.out.println("private key string " + new String(getKeyData(filePath)));
byte[] keydata = Base64.decodeBase64(new String(getKeyData(filePath)));
PKCS8EncodedKeySpec encodedPrivateKey = new PKCS8EncodedKeySpec(keydata);
KeyFactory keyFactory = null;
try {
keyFactory = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
try {
privateKey = keyFactory.generatePrivate(encodedPrivateKey);
} catch (InvalidKeySpecException e) {
e.printStackTrace();
}
return privateKey;
}
/**
* Method used to get the generated Public Key
*
* @param filePath of the PublicKey file
* @return PublicKey
*/
public PublicKey getStoredPublicKey(String filePath) {
PublicKey publicKey = null;
// System.out.println("private key string " + new String(getKeyData(filePath)));
// System.out.println("private key string " + new String(getKeyData(filePath)));
byte[] keydata = Base64.decodeBase64(new String(getKeyData(filePath)));
KeyFactory keyFactory = null;
try {
keyFactory = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
X509EncodedKeySpec encodedPublicKey = new X509EncodedKeySpec(keydata);
try {
publicKey = keyFactory.generatePublic(encodedPublicKey);
} catch (NullPointerException npe) {
npe.printStackTrace();
} catch (InvalidKeySpecException e) {
e.printStackTrace();
}
return publicKey;
}
}
public class SignatureUtil {
/**
* Method used to get the XML document by parsing
*
* @param xmlFilePath , file path of the XML document
* @return Document
*/
public static Document getXmlDocument(String xmlFilePath) {
Document doc = null;
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
//if doesn't setNamespaceAware(true);
//getElementsByTagNameNS(XMLSignature.XMLNS, "Signature") may be unavailable
dbf.setNamespaceAware(true);
try {
doc = dbf.newDocumentBuilder().parse(new FileInputStream(xmlFilePath));
} catch (ParserConfigurationException ex) {
ex.printStackTrace();
} catch (FileNotFoundException ex) {
ex.printStackTrace();
} catch (SAXException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
return doc;
}
/**
* Convert node to string
*
* @param node
* @return String
*/
public static String convertNodeToString(Node node) {
StringWriter sw = new StringWriter();
try {
Transformer t = TransformerFactory.newInstance().newTransformer();
// t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
// t.setOutputProperty(OutputKeys.INDENT, "yes");
t.transform(new DOMSource(node), new StreamResult(sw));
} catch (TransformerException te) {
System.out.println("nodeToString Transformer Exception");
}
return sw.getBuffer().toString();
}
/**
* convert string to Element
*
* @param xmlStr
* @return Element
*/
public static Element convertStringToElement(String xmlStr) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
//if doesn't setNamespaceAware(true);
//getElementsByTagNameNS(XMLSignature.XMLNS, "Signature") may be unavailable
factory.setNamespaceAware(true);
DocumentBuilder builder;
try {
builder = factory.newDocumentBuilder();
Document doc = builder.parse( new InputSource( new StringReader( xmlStr ) ) );
return doc.getDocumentElement();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* convert String to Document
*
* @param xmlStr
* @return Document
*/
public static Document convertStringToDocument(String xmlStr) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
//if doesn't setNamespaceAware(true);
//getElementsByTagNameNS(XMLSignature.XMLNS, "Signature") may be unavailable
factory.setNamespaceAware(true);
DocumentBuilder builder;
try {
builder = factory.newDocumentBuilder();
Document doc = builder.parse( new InputSource( new StringReader( xmlStr ) ) );
return doc;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* convert Document to String
*
* @param doc
* @return String
*/
public static String convertDocumentToString(Document doc) {
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer;
try {
transformer = tf.newTransformer();
// below code to remove XML declaration
// transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(doc), new StreamResult(writer));
String output = writer.getBuffer().toString();
return output;
} catch (TransformerException e) {
e.printStackTrace();
}
return null;
}
/*
* Method used to store the signed XMl document
*/
public static void storeSignedDoc(Document doc, String destnSignedXmlFilePath) {
TransformerFactory transFactory = TransformerFactory.newInstance();
Transformer trans = null;
File file = new File(destnSignedXmlFilePath);
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
trans = transFactory.newTransformer();
} catch (TransformerConfigurationException ex) {
ex.printStackTrace();
}
try {
StreamResult streamRes = new StreamResult(file);
trans.transform(new DOMSource(doc), streamRes);
} catch (TransformerException ex) {
ex.printStackTrace();
}
System.out.println("XML file with attached digital signature generated successfully ...");
}
}
package com.tech.diudiu.signature.demo3;
/**
*
* @author as
* @version $Id: XmlDigitalSignatureGenerator.java, v 0.1 2016年7月1日 下午2:25:48 as Exp $
*/
@SuppressWarnings("restriction")
public class XmlDigitalSignatureGenerator {
/**
* Method used to get the KeyInfo
*
* @param xmlSigFactory
* @param publicKeyPath
* @return KeyInfo
*/
private static KeyInfo getKeyInfo(XMLSignatureFactory xmlSigFactory, String publicKeyPath) {
KeyInfo keyInfo = null;
KeyValue keyValue = null;
PublicKey publicKey = new KryptoUtil().getStoredPublicKey(publicKeyPath);
KeyInfoFactory keyInfoFact = xmlSigFactory.getKeyInfoFactory();
try {
keyValue = keyInfoFact.newKeyValue(publicKey);
} catch (KeyException ex) {
ex.printStackTrace();
}
keyInfo = keyInfoFact.newKeyInfo(Collections.singletonList(keyValue));
return keyInfo;
}
/**
*
*
* @param originalXmlFilePath
* @param destnSignedXmlFilePath
* @param privateKeyFilePath
* @param publicKeyFilePath
* @param isBase64
*/
public static void generateXMLDigitalSignature(String originalXmlFilePath,
String destnSignedXmlFilePath,
String privateKeyFilePath, String publicKeyFilePath, boolean isBase64) {
//Get the XML Document object
Document doc = SignatureUtil.getXmlDocument(originalXmlFilePath);
//Create XML Signature Factory
XMLSignatureFactory xmlSigFactory = XMLSignatureFactory.getInstance("DOM");
PrivateKey privateKey = new KryptoUtil().getStoredPrivateKey(privateKeyFilePath);
DOMSignContext domSignCtx = new DOMSignContext(privateKey, doc.getDocumentElement());
Reference ref = null;
SignedInfo signedInfo = null;
try {
ref = xmlSigFactory.newReference("", xmlSigFactory.newDigestMethod(DigestMethod.SHA256,
null), Collections.singletonList(xmlSigFactory.newTransform(Transform.ENVELOPED,
(TransformParameterSpec) null)), null, null);
signedInfo = xmlSigFactory
.newSignedInfo(xmlSigFactory.newCanonicalizationMethod(
CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null),
xmlSigFactory.newSignatureMethod(SignatureMethod.RSA_SHA1, null), Collections
.singletonList(ref));
} catch (NoSuchAlgorithmException ex) {
ex.printStackTrace();
} catch (InvalidAlgorithmParameterException ex) {
ex.printStackTrace();
}
//Pass the Public Key File Path
KeyInfo keyInfo = getKeyInfo(xmlSigFactory, publicKeyFilePath);
//Create a new XML Signature
XMLSignature xmlSignature = xmlSigFactory.newXMLSignature(signedInfo, keyInfo);
try {
//Sign the document
xmlSignature.sign(domSignCtx);
} catch (MarshalException ex) {
ex.printStackTrace();
} catch (XMLSignatureException ex) {
ex.printStackTrace();
}
if (isBase64) {
//encode the signature element before store it
try {
Element root = doc.getDocumentElement();
NodeList signatures = root.getElementsByTagName("Signature");
if (signatures.getLength() != 1) {
throw new Exception("Signature error");
}else {
Node signature = signatures.item(0);
Element sign2 = doc.createElement("signature");
String sigContent = SignatureUtil.convertNodeToString(signature);
sign2.setTextContent(Base64.encodeBase64String(sigContent.getBytes("utf-8")));
root.replaceChild(sign2, signature);
}
} catch (Exception e) {
// TODO: handle exception
}
}
//Store the digitally signed document inta a location
SignatureUtil.storeSignedDoc(doc, destnSignedXmlFilePath);
}
}
/**
*
* @author as
* @version $Id: XmlDigitalSignatureVerifier.java, v 0.1 2016年7月1日 下午2:26:57 as Exp $
*/
@SuppressWarnings("restriction")
public class XmlDigitalSignatureVerifier {
/**
*
*
* @param signedXmlFilePath
* @param pubicKeyFilePath
* @param isBase64
* @return boolean
* @throws Exception
*/
public static boolean isXmlDigitalSignatureValid(String signedXmlFilePath,
String pubicKeyFilePath, boolean isBase64) throws Exception {
Document doc = SignatureUtil.getXmlDocument(signedXmlFilePath);
if (isBase64) {
//encode the signature element before store it
try {
Element root = doc.getDocumentElement();
NodeList signatures = root.getElementsByTagName("signature");
if (signatures.getLength() != 1) {
throw new Exception("Signature error");
}else {
Node signature = signatures.item(0);
String signatureContent = new String(Base64.decodeBase64(signature.getTextContent()));
Node newChild = doc.importNode(SignatureUtil.convertStringToElement(signatureContent), true);
root.replaceChild(newChild, signature);
}
} catch (Exception e) {
e.printStackTrace();
}
}
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
if (nl.getLength() == 0) {
throw new Exception("No XML Digital Signature Found, document is discarded");
}
PublicKey publicKey = new KryptoUtil().getStoredPublicKey(pubicKeyFilePath);
DOMValidateContext valContext = new DOMValidateContext(publicKey, nl.item(0));
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
XMLSignature signature = fac.unmarshalXMLSignature(valContext);
return signature.validate(valContext);
}
}
**
文件demo
**
私钥:privatekey.pem
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCdHF222UUcghltzBX4y6JJ/FAvmNG+GzcEMGkec2pKmLekJ4IhgYZVuhx8g1B3saZYKFn/uD0dghNA/QGmKoBHxyRWeBBagUaxlRswFY7tu3TH0pr7odKyxQf3P+8qd1iuu+lqASsWwoiNQPoy4G7+IX5/mpDcFOaVdGB6vbuiuLkNL6Gobt/Rih9jsIQoUKdqSXJBmptZro+EqtsX9djikaMUDuxVIejM15EnQbIsskmdAP1Li9EF9KEypWZdg6AUAzxxe6+2leO6myq4MnXIJQwYcTlw0FaFyTfLC0iDyD39apdSC96ePDFOsLtmjErTI1rOH0UuQ6lVnMl64OlxAgMBAAECggEAQsMQC6guFxwtxBuKZqvZrcjbJi6xSjB0Y4JS2Q7AZyyYNNhZEvXgaMNqYXrHkeVaw3F6NRFMXyKsHdyle7/pvRahmWDxyhPPP50V7YvrrZgP2u3iz/UG/NJ/PRDdB9WpSK8vU81lSwmlTykCaOY4JCOFhqHe1rbG+VF3whCoEE/1yOHb8Z+LgN589gP0ujARfV7Bb70wEKiq82Mu30Ypzgkh/f12xvxxW36inGcvWlc3bc1rINwUtckkGjXSooLPkEBLr7ZxuzwDH4QZaWV1gm32zVH/0tg6DroqFEOkxwjHROMLvYBYZnFbZc9+l45XEYOP1iSgVWNbp/LCucUnlQKBgQDZiMcPvupau0yGNQ6OeCDngYDY89QqrP4ctWAivB9Exn2zjbQJSMEmjObc/qM+bvsPRGWu5yt+QeJ+TI367XIbi2HlZF/OHt0v5LHnOpwZaqEngsJrihkJJxzWbDUR2nKI+md7mDLE/9afX8kcfX1sW8YljSyGItxU6n3EKs/LewKBgQC45F8c0Icz8QVCcBCs++WyyaWFtc8xcPWUQq7n8/RHUHT4ftmNMjPqOmzDiYlgSPnPkRFm6jmuqfKD8Kd+ez4JNR5IvQ9dwf2bDEXu5bO2jaIbNnN5m6smll5GKCwD6btaq/BEanPF10Nr8HpIQxbhCp78X6foA9wG+oTuWF5lAwKBgGg9yA4+kXULHf+N+zzAShxGDaVpKnmMznSZYckjVuSRLXlWZTixuYUsrhCmoTlw2zQUtZlbw5bMIaoeK0dcc3rJjw7qy5ItbhbeQ3YnutffJfyWB26bDaY6LPJ/herZQFttixtJ8sI+Su2ya+AQLoM3cztZbEWW/PhgKgZP8aVzAoGAMxyGoouO5K/7OTuLkJZuEDeoN0dkSJGmgd7RRTPuUoyOWWePsnWvXk/aY0+xF1n1HQlIWz/ixEMc1JaBZvig1KDHh2okRlHrTqJc7sa3IH8U0hsCXxrGfHtTAmf2ivphHZasa+1Vpdp1O/CVjUZmm7145+F1pDD08UTt3Im3RosCgYAoHq+hZYGKMK1q+s6SiwRCARccbDAM+xqc8oGJD2+lsH4LcQG+NYWl3feJ7xi+U3ffkAK7fynHngEnrFD7ZKT0Ta00l7A7rYadsKaqdAbCfp8ppkd5txKhdpl0eit4Ckd2FtzZwOHZNLqKWAvbDKUkP2I6b3JcT3w4j02MqR0lxA==
公钥publickey.pem:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnRxdttlFHIIZbcwV+MuiSfxQL5jRvhs3BDBpHnNqSpi3pCeCIYGGVbocfINQd7GmWChZ/7g9HYITQP0BpiqAR8ckVngQWoFGsZUbMBWO7bt0x9Ka+6HSssUH9z/vKndYrrvpagErFsKIjUD6MuBu/iF+f5qQ3BTmlXRger27ori5DS+hqG7f0YofY7CEKFCnaklyQZqbWa6PhKrbF/XY4pGjFA7sVSHozNeRJ0GyLLJJnQD9S4vRBfShMqVmXYOgFAM8cXuvtpXjupsquDJ1yCUMGHE5cNBWhck3ywtIg8g9/WqXUgvenjwxTrC7ZoxK0yNazh9FLkOpVZzJeuDpcQIDAQAB
源文件:original.xml
<?xml version="1.0"?>
<student>
<desc id="request">
<head>
<id>ididididi</id>
<name>Your Name here</name>
</head>
<course>
<english>37</english>
<math>99</math>
<art>0</art>
</course>
</desc>
</student>
数字签名后文件:originalWithSign.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?><student>
<desc id="request">
<head>
<id>ididididi</id>
<name>Your Name here</name>
</head>
<course>
<english>37</english>
<math>99</math>
<art>0</art>
</course>
</desc>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><DigestValue>cOI6m7AfoVFkF4DV5ZINFtoldbUKtSHgQtk/NhtvU9U=</DigestValue></Reference></SignedInfo><SignatureValue>HgvGkJzuZVJ57yHrk0xf3FRvZ7lpQT2nj7BShOOxOvIXRlyGGbkox9JahMh/BJTfP/xv1hDyg+o1
h5uPtuowGjG1Pg+LSrl46v9Byxr0KJXZcZ6UotoO6rndkp63fIrBNlzTaLutiBsxRggZFubSboAJ
svbQk+k6oIbWhfa85KwyxeN/2rPIMo72Lz4qkhkwIZjv+4b3ye0+rRZrFvsKzDjxUnwXn5jPjasn
/8OrQGn/uGEGKA9lj0uJobAiVFAaDyKY12FmSz1Htik12mCh7TwEScHFtWHvB6JHEbOTGxn3bX3H
LDEb5Zroh1sPArECIh/uW+5YZWXfIRPU/gMAzg==</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>nRxdttlFHIIZbcwV+MuiSfxQL5jRvhs3BDBpHnNqSpi3pCeCIYGGVbocfINQd7GmWChZ/7g9HYIT
QP0BpiqAR8ckVngQWoFGsZUbMBWO7bt0x9Ka+6HSssUH9z/vKndYrrvpagErFsKIjUD6MuBu/iF+
f5qQ3BTmlXRger27ori5DS+hqG7f0YofY7CEKFCnaklyQZqbWa6PhKrbF/XY4pGjFA7sVSHozNeR
J0GyLLJJnQD9S4vRBfShMqVmXYOgFAM8cXuvtpXjupsquDJ1yCUMGHE5cNBWhck3ywtIg8g9/WqX
UgvenjwxTrC7ZoxK0yNazh9FLkOpVZzJeuDpcQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue></KeyValue></KeyInfo></Signature></student>
base64 endoce 后签名文件originalWithSignBase64.xml:
<?xml version="1.0" encoding="UTF-8" standalone="no"?><student>
<desc id="request">
<head>
<id>ididididi</id>
<name>Your Name here</name>
</head>
<course>
<english>37</english>
<math>99</math>
<art>0</art>
</course>
</desc>
<signature>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48U2lnbmF0dXJlIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48U2lnbmVkSW5mbz48Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnL1RSLzIwMDEvUkVDLXhtbC1jMTRuLTIwMDEwMzE1Ii8+PFNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPjxSZWZlcmVuY2UgVVJJPSIiPjxUcmFuc2Zvcm1zPjxUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjwvVHJhbnNmb3Jtcz48RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PERpZ2VzdFZhbHVlPmNPSTZtN0Fmb1ZGa0Y0RFY1WklORnRvbGRiVUt0U0hnUXRrL05odHZVOVU9PC9EaWdlc3RWYWx1ZT48L1JlZmVyZW5jZT48L1NpZ25lZEluZm8+PFNpZ25hdHVyZVZhbHVlPkhndkdrSnp1WlZKNTd5SHJrMHhmM0ZSdlo3bHBRVDJuajdCU2hPT3hPdklYUmx5R0dia294OUphaE1oL0JKVGZQL3h2MWhEeWcrbzENCmg1dVB0dW93R2pHMVBnK0xTcmw0NnY5Qnl4cjBLSlhaY1o2VW90b082cm5ka3A2M2ZJckJObHpUYUx1dGlCc3hSZ2daRnViU2JvQUoNCnN2YlFrK2s2b0liV2hmYTg1S3d5eGVOLzJyUElNbzcyTHo0cWtoa3dJWmp2KzRiM3llMCtyUlpyRnZzS3pEanhVbndYbjVqUGphc24NCi84T3JRR24vdUdFR0tBOWxqMHVKb2JBaVZGQWFEeUtZMTJGbVN6MUh0aWsxMm1DaDdUd0VTY0hGdFdIdkI2SkhFYk9UR3huM2JYM0gNCkxERWI1WnJvaDFzUEFyRUNJaC91Vys1WVpXWGZJUlBVL2dNQXpnPT08L1NpZ25hdHVyZVZhbHVlPjxLZXlJbmZvPjxLZXlWYWx1ZT48UlNBS2V5VmFsdWU+PE1vZHVsdXM+blJ4ZHR0bEZISUlaYmN3VitNdWlTZnhRTDVqUnZoczNCREJwSG5OcVNwaTNwQ2VDSVlHR1Zib2NmSU5RZDdHbVdDaFovN2c5SFlJVA0KUVAwQnBpcUFSOGNrVm5nUVdvRkdzWlViTUJXTzdidDB4OUthKzZIU3NzVUg5ei92S25kWXJydnBhZ0VyRnNLSWpVRDZNdUJ1L2lGKw0KZjVxUTNCVG1sWFJnZXIyN29yaTVEUytocUc3ZjBZb2ZZN0NFS0ZDbmFrbHlRWnFiV2E2UGhLcmJGL1hZNHBHakZBN3NWU0hvek5lUg0KSjBHeUxMSkpuUUQ5UzR2UkJmU2hNcVZtWFlPZ0ZBTThjWHV2dHBYanVwc3F1REoxeUNVTUdIRTVjTkJXaGNrM3l3dElnOGc5L1dxWA0KVWd2ZW5qd3hUckM3Wm94SzB5TmF6aDlGTGtPcFZaekpldURwY1E9PTwvTW9kdWx1cz48RXhwb25lbnQ+QVFBQjwvRXhwb25lbnQ+PC9SU0FLZXlWYWx1ZT48L0tleVZhbHVlPjwvS2V5SW5mbz48L1NpZ25hdHVyZT4=</signature></student>