JavaSE 6基於JSR105的XML簽名之實踐篇

xuniji123發表於2007-01-14
我們開始分析一個實際的XML簽名示例應用程式。

  一、 密碼學金鑰和證照

  現在,我們已經準備好我們的XML簽名示例應用程式。

  讓我們首先分析下列XML文件-./etc/invoice.xml:

<?XML version="1.0" encoding="UTF-8" standalone="no"?>
<invoice XMLns=">
<items>
 <item>
  <desc>Applied Cryptography</desc>
  <type>book</type>
  <unitprice>44.50</unitprice>
  <quantity>1</quantity>
 </item>
</items>
<creditcard>
 <number>123456789</number>
 <expiry>10/20/2009</expiry>
 <lastname>John</lastname>
 <firstname>Smith</firstname>
</creditcard>
</invoice>

  我們計劃使用一個XML簽名對它進行簽名並且希望使用一個基於一個公共金鑰的簽名方法。

  讓我們先生成密碼學金鑰。為此,我們可以使用JDK中提供的keytool工具-把該程式移動到./etc資料夾下,並且執行下列命令:

keytool -genkey -keysize 512 -sigalg DSA -dname "cn=Young Yang, ou=Architecture, o=Company, L=New York, ST=NY, c=US" -alias biz -keypass kp1234 -keystore bizkeystore -storepass sp1234 -validity 180

  這個命令能夠建立金鑰並預以儲存-名字為bizkeystore,儲存在工作目錄./etc下,並且指定它的口令為sp1234。它還生成一個針對實體(它包含有一個卓著的名字-Young Yang)的公有/私有金鑰對。【注意】,這裡使用DSA金鑰生成演算法來建立公有/私有金鑰-都為512位長。
上面的命令進一步建立了一個自簽名的證照,這是使用SHA1的DSA演算法(JSR-105註釋中的DSA_SHA1,其中包括了公共金鑰和前面那個卓著名字資訊)實現的。這個證照將保持180天的有效期並且關聯與一個金鑰儲存檔案(此處引用的別名為"biz")中的私有金鑰。該私有金鑰被賦予口令kp1234。

  我們的示例中包括一個簡單的Java類-KeyStoreInfo,用於把儲存於前面的金鑰儲存檔案中的金鑰和證照資訊輸出到System.out;這個類也用於應用程式從中取得金鑰對-這裡的私有和公共金鑰匹配作為輸入引數指定的條件。為了試驗它能夠輸出包含在前面儲存檔案bizkeystore中的資訊,讀者可以執行Ant目標ksInfo。

  下列程式碼片斷顯示KeyStoreInfo中的用來檢索一個KeyPair的方法:

public static KeyPair getKeyPair(String store,String sPass,String kPass,String alias)
throws CertificateException,
IOException,
UnrecoverableKeyException,
KeyStoreException,
NoSuchAlgorithmException{
 KeyStore ks = loadKeyStore(store,sPass);
 KeyPair keyPair = null;
 Key key = null;
 PublicKey publicKey = null;
 PrivateKey privateKey = null;
 if (ks.containsAlias(alias)){
  key = ks.getKey(alias,kPass.toCharArray());
  if (key instanceof PrivateKey){
   Certificate cert = ks.getCertificate(alias);
   publicKey = cert.getPublicKey();
   privateKey = (PrivateKey)key;
   return new KeyPair(publicKey,privateKey);
  }else{
   return null;
  }
 } else {
  return null;
 }
}

  藉助於一個KeyPair,我們可以容易地得到PrivateKey和PublicKey-透過呼叫相應的操作getPrivate()和getPublic()實現。

  為了從KeyStore中得到一個PublicKey,我們並不真正需要在上面的方法中所要求的金鑰口令,而這正是下列方法所實現的:

public static PublicKey getPublicKey(String store,
String sPass, String alias)
throws KeyStoreException,
NoSuchAlgorithmException,
CertificateException,
IOException{
 KeyStore ks = loadKeyStore(store, sPass);
 Certificate cert = ks.getCertificate(alias);
 return cert.getPublicKey();
}

  在上面兩部分程式碼片斷中,方法KeyStore loadKeyStore(String store,String sPass)是一個工具函式,用於例項化一個KeyStore物件,並且從檔案系統載入入口。我們以如下方式實現它:

private static KeyStore loadKeyStore(String store, String sPass)
throws KeyStoreException,
NoSuchAlgorithmException,
CertificateException,
IOException{
 KeyStore myKS = KeyStore.getInstance("JKS");
 FileInputStream fis = new FileInputStream(store);
 myKS.load(fis,sPass.toCharArray());
 fis.close();
 return myKS;
}


  伴隨JDK提供的keytool還可以把儲存在一個金鑰儲存檔案內的證照輸出到系統檔案中。例如,為了建立一個包含X509證照(關聯於別名為biz的金鑰入口)的biz.cer檔案,我們可以從資料夾./etcdirectory下執行下列命令:

keytool -export -alias biz -file biz.cer -keystore bizkeystore -storepass sp1234

  這個證照實現認證我們討論上面的公共金鑰。

  我們還在示例中包括了一個Java類-CertificateInfo,用於把一個證照中的一些有趣的資訊輸出到System.out。為了試驗這一點,讀者可以執行Ant目標certInfo。然而,要理解該程式碼及其輸出,必須具有DSA和RSA演算法的基本知識。當然,讀者可以安全地繞過這個程式而繼續閱讀本文後面的內容。

二、 生成一個Enveloping簽名  這一節討論藉助於JSR-105 API及其預設實現來實現對invoice.xml檔案的簽名。

  我們的示例中建立了一個enveloping簽名。注意,當你想使用在一種detached或enveloped簽名情形下時,也僅需對本例作一些細微修改。

  下面,讓我們分析程式Sign.java,它能夠生成invoice.xml檔案的XML簽名。

public class Sign {
 public static void main(String[] args) throws Exception {
  String input = "./etc/invoice.xml ";
  String output = "./etc/signature.xml";
  if (args.length > 2) {
   input = args[0];
   output = args[1];
  }
  //準備
  DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
  dbf.setNamespaceAware(true);
  //步驟1
  String providerName = System.getProperty("jsr105Provider","org.jcp.XML.dsig.internal.dom.XMLDSigRI");
  XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM",(Provider) Class.forName(providerName).newInstance());
  //步驟2
  Reference ref = fac.newReference("#invoice",fac.newDigestMethod(DigestMethod.SHA1, null));
  //步驟3
  Document XML = dbf.newDocumentBuilder().parse(new File(input));
  Node invoice = XML.getDocumentElement();
  XMLStructure content = new DOMStructure(invoice);
  XMLObject obj = fac.newXMLObject(Collections.singletonList(content),"invoice", null, null);
  //步驟4
  SignedInfo si = fac.newSignedInfo(fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS,
(C14NMethodParameterSpec) null),
fac.newSignatureMethod(SignatureMethod.DSA_SHA1, null),
Collections.singletonList(ref));
  //步驟5,分為情形5.0或5.1
  PrivateKey privateKey = null;
  //情形5.0
  privateKey = KeyStoreInfo.getPrivateKey("./etc/bizkeystore","sp1234","kp1234", "biz");
  //情形5.1,分為情形5.1.1或5.1.2

  //情形5.1.1
  //KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
  //kpg.initialize(512);
  //KeyPair kp = kpg.generateKeyPair();

  //情形5.1.2
  // KeyPair kp = KeyStoreInfo.getKeyPair("./etc/bizkeystore", "sp1234",
  // "kp1234","biz");

  //如果針對情形5.1,請去掉下面一行中的註釋
  // privateKey = kp.getPrivate();

  //步驟6,分為情形6.0,6.1或6.2

  //情形6.0,如果針對情形6.1或6.2也使用下面這一行
  KeyInfo ki = null;

  //如果針對情形6.1或6.2請去掉下面一行中的註釋
  // KeyInfoFactory kif = fac.getKeyInfoFactory();

  //情形6.1
  // KeyValue kv = kif.newKeyValue(kp.getPublic());
  // ki = kif.newKeyInfo(Collections.singletonList(kv));

  //情形6.2
  // CertificateFactory cf = CertificateFactory.getInstance("X.509");
  // FileInputStream fis = new FileInputStream("./etc/biz.cer");
  // java.security.cert.Certificate cert = cf.generateCertificate(fis);
  // fis.close();
  // X509Data x509d = kif.newX509Data(Collections.singletonList(cert));
  // ki = kif.newKeyInfo(Collections.singletonList(x509d));

  //步驟7
  XMLSignature signature = fac.newXMLSignature(si, ki,Collections.singletonList(obj), null, null);

  //步驟8
  Document doc = dbf.newDocumentBuilder().newDocument();
  DOMSignContext dsc = new DOMSignContext(privateKey, doc);

  //步驟9
  signature.sign(dsc);
  //轉換成一個xml文件
  TransformerFactory tf = TransformerFactory.newInstance();
  Transformer trans = tf.newTransformer();
  trans.transform(new DOMSource(doc),new StreamResult(new FileOutputStream(output)));
 }
}

  為了試驗這個程式,讀者可以執行Ant目標籤名-它將建立一個XML文件./etc/signature.xml。這就是所謂的XML簽名。為了保持我們的程式碼更為整潔和集中,我們省略了分析XML和轉換DOM樹中所有相關的格式設定。結果是,signature.xml檔案成為一個有些凌亂的文字檔案。

  現在,讓我們詳細分析一下這個程式來說明如何在JSR-105中對一個XML簽名進行簽名。

  簽名invoice.xm的過程可以分解為如下九個步驟。

  【步驟1】載入一個XMLSignatureFactory例項。這個工廠類將負責構建幾乎所有主要的物件-我們在JSR-105中API中處理XML簽名時需要使用這些物件,除了那些與KeyInfo相關的物件之外。

  【步驟2】選擇一個digest方法並建立相應的Reference物件。我們使用在〖步驟1〗中建立的XMLSignatureFactory例項來建立DigestMethod和Reference物件。

  在XMLSignatureFactory中的針對DigestMethod物件的工廠操作如下所示:

public abstract DigestMethod newDigestMethod(String algorithm,
DigestMethodParameterSpec params) throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException

  【注意】這個params引數用來指定digest演算法可能需要的引數;在SHA-1,SHA-256或SHA-512的情況下,我們可以使用null。

  為了建立一個Reference物件,XMLSignatureFactory提供了四種操作:

public abstract Reference newReference(String uri, DigestMethod dm);
public abstract Reference newReference(String uri, DigestMethod dm,
List transforms, String type, String id);
public abstract Reference newReference(String uri, DigestMethod dm,
List transforms, String type, String id, byte[] digestValue);
......

  為了全面地理解在那些操作中的輸入引數的意思,我們需要分析一下在W3C建議中的Reference元素的XML模式定義:

<element name="Reference" type="ds:ReferenceType"/>
<complexType name="ReferenceType">
 <sequence>
  <element ref="ds:Transforms" minOccurs="0"/>
  <element ref="ds:DigestMethod"/>
  <element ref="ds:DigestValue"/>
 </sequence>
<attribute name="Id" type="ID" use="optional"/>
<attribute name="URI" type="anyURI" use="optional"/>
<attribute name="Type" type="anyURI" use="optional"/>
</complexType>

  其中,URI屬性參考Reference相應的資料物件。

  對於我們的示例來說,我們使用SHA-1作為digest方法,並且使用#invoice來在相同的XML簽名文件(它包含這個Reference物件的XML描述)中引用一個元素。由#invoice所引用的元素正是我們要在下一步所要討論的內容。

[@more@]【步驟3】載入invoice.xml並且用一個XMLObject物件把它包裝起來。注意,並非所有的簽名生成過程都要求這個步驟。XMLObject在JSR-105中對於我們以前簡短地討論過的可選的Object元素進行建模。該Object元素具有下列模式定義:

<element name="Object" type="ds:ObjectType"/>
<complexType name="ObjectType" mixed="true">
 <sequence minOccurs="0" maxOccurs="unbounded">
  <any namespace="##any" processContents="lax"/>
 </sequence>
 <attribute name="Id" type="ID" use="optional"/>
 <attribute name="MimeType" type="string" use="optional"/>
 <attribute name="Encoding" type="anyURI" use="optional"/>
</complexType>

  XMLSignatureFactory提供下列方法來建立一個XMLObject例項:

public abstract XMLObject newXMLObject(List content, String id,String mimeType,String encoding)

  我們使用一個DOMStructure物件來包裝invoice.xml的根結點。在JSR-105中定義的DOMStructure可以幫助從原始待簽名的XML文件中把結點匯入到JSR-105執行時刻。

  我們指定#invoice作為結果物件元素的id。JSR-105實現知道在步驟2中建立的引用物件參考invoice.xml文件,因為這個id把它們連結在一起(在Reference一邊,URI屬性指向這個id)。

  【步驟4】建立SignedInfo物件。在W3C建議中,SignedInfo元素具有下列模式定義:

<element name="SignedInfo" type="ds:SignedInfoType"/>
<complexType name="SignedInfoType">
 <sequence>
  <element ref="ds:CanonicalizationMethod"/>
  <element ref="ds:SignatureMethod"/>
  <element ref="ds:Reference" maxOccurs="unbounded"/>
 </sequence>
 <attribute name="Id" type="ID" use="optional"/>
</complexType>

  為了建立一個SignedInfo物件,我們需要在〖步驟2〗中建立的Reference;我們還需要兩個例項-一個是CanonicalizationMethod的例項,另一個是SignatureMethod的例項。我們建議感興趣的讀者參考一下規範說明書從而對這四種XML規範演算法有一個更為精確的瞭解;在此,我們只是簡單地指出,在我們決定選擇一個特定的演算法-alg後,後面對XMLSignatureFactory的一個例項(即fac)的呼叫將會建立CanonicalizationMethod的例項:

fac.newCanonicalizationMethod(alg,null)

  我們可以建立一個SignatureMethod例項-透過呼叫下列在XMLSigantureFactory中定義的操作:

public abstract SignatureMethod newSignatureMethod(String algorithm,
SignatureMethodParameterSpec params) throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException

  在我們的示例中,我們擁有一個DSA型別金鑰對;因此,我們需要選擇一個基於DSA的演算法-例如DSA_SHA1。對於DSA-SHA1,我們可以把params引數設定為null。

  為了建立一個SignedInfo例項,XMLSignatureFactory定義瞭如下兩個工廠方法:

public abstract SignedInfo newSignedInfo(CanonicalizationMethod cm,
SignatureMethod sm, List references);
public abstract SignedInfo newSignedInfo(CanonicalizationMethod cm,
SignatureMethod sm, List references, String id).


  在第二個工廠方法中的第二個引數-id響應於XML簽名文件中的SignedInfo元素的Id屬性。

【步驟5】-獲得簽名私有金鑰。

  在我們的示例中,我們展示了三種不同的方法來得到該私有金鑰。在第一種方法中,我們呼叫我們的KeyStoreInfo類的getPrivateKey()方法來檢索我們使用keytool建立的DSA型別私有金鑰,並且把它儲存在金鑰儲存檔案-bizkeystore(前面的5.0情形)中。為了獲得該私有金鑰,我們還可以從bizkeystore中檢索該KeyPair-透過呼叫KeyStoreInfo的getKeyPair()方法,然後呼叫KeyPair例項(5.1.2情形)的getPrivate()。另一方面,JCA提供了一個名字為KeyPairGenerator的類用於根據需要隨時動態地建立一個KeyPair,這正是Sign.java中的情形5.1.1提到的情況。

  讀者還應該注意,JSR-105允許透過一個KeySelector物件獲得私有金鑰。我們在下節討論KeySelector時還要詳細分析。

  【步驟6】建立一個KeyInfo物件。這一步是可選的,就象KeyInfo作為簽名元素中的一個元素是可選的一樣。在我們的示例的情形6.0下,我們使KeyInfo成為null;這樣以來,可以完全從結果XML簽名中忽略它。

  W3C建議和JSR-105定義RSA的KeyValues以及DSA型別for wrapping,respectively,RSA和DSA公共金鑰,並允許它們成為KeyInfo的內容。我們的示例中的情形6.1從我們以前使用JDK keytool生成的公共金鑰中建立一個KeyValue物件,並且把它放到一個KeyInfo物件。後面,當討論我們的核心校驗程式時,我們將看到它如何使用這樣的一個KeyInfo物件來檢索公共金鑰以用於簽名校驗。

  在JSR-105中,我們透過呼叫一個KeyInfoFactory例項中的操作建立了KeyValue和KeyInfo物件。其中,KeyInfoFactory負責建立所有主要的與KeyInfo相關的物件-例如KeyName,KeyValue,X509Data等。我們可以以與我們在〖步驟1〗得到XMLSignatureFactory例項相同的方式得到一個KeyInfoFactory例項。我們的示例呼叫XMLSignatureFactory物件的getKeyInfoFactory()方法取得KeyInfoFactory例項。

  我們的示例的情形6.2將建立一個X509Data物件-使用我們以前藉助於工具keytool從bizkeystore中匯出的證照biz.cer,然後把這個物件作為內容放入一個KeyInfo物件中。再次,後面我們將討論的核心校驗程式將證明我們如何從這樣的一個KeyInfo物件中取得用於簽名校驗的公共金鑰。

  【步驟7】建立一個XMLSignature物件。在JSR-105中,XMLSignature介面為W3C中建議的簽名元素實現了建模。我們已經在前面看到該簽名元素的結構。為了建立一個XMLSiganture例項,我們可以在XMLSignatureFactory中呼叫下列兩個方法之一:

public abstract XMLSignature newXMLSignature(SignedInfo si, KeyInfo ki);
public abstract XMLSignature newXMLSignature(SignedInfo si, KeyInfo ki,
List objects, String id, String signatureValueId).

  第二個方法中的id和signatureValueId引數將成為結果XML簽名文件中的XML元素ID。在我們的示例中,該XML簽名將擁有一個Object元素;因此,我們需要使用第二個工廠方法。

  【步驟8】例項化一個DOMSignContext物件,並且使用它註冊私有金鑰。XMLSignContext介面(DOMSignContext實現它)包含用於生成XML的上下文資訊簽名。

  DOMSignContext提供了幾種形式的構造器-簽名應用程式用來註冊要使用的私有金鑰,並且這也是我們的示例中所採用的方法。

  在繼續討論簽名過程的最後步驟之前,我們需要指出XMLSignContext和DOMSignContext例項都可能包含特定於它們所使用的XML簽名結構的資訊和狀態。該JSR-105規範中宣告:如果一個XMLSignContext(或DOMSignContext)與不同的簽名結構一起使用,那麼,結果將是無法預料的。例如,我們不應該使用相同的XMLSignContext(或DOMSignContext)例項來簽名兩個不同的XMLSignature物件。

  【步驟9】簽名。XMLSignature介面中的sign()操作實現簽名XMLSignature。其實,該方法還實現若干操作,包括基於相應的digest方法計算所有引用的digest值,並且基於該簽名方法和私有金鑰計算簽名值。該簽名值被XMLSignature例項中的嵌入式SignatureValue類所捕獲,而對XMLSignature例項的getSignatureValue()方法的呼叫將返回使用結果值填充的SignatureValue物件。

  在我們的簽名程式的最後,我們把XMLSignature編排成一個XML文件-signature.xml。

三、 XML簽名核心校驗  在前面一節中,我們把invoice.xml文件簽名成一個在signature.xml檔案中捕獲的enveloping XML簽名。

  為了校驗該簽名,我們可以使用下列程式-Validate.java:

public class Validate {
 public static void main(String[] args) throws Exception {
  //第一步
  String providerName = System.getProperty("jsr105Provider","org.jcp.XML.dsig.internal.dom.XMLDSigRI");
  XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM",(Provider) Class.forName(providerName).newInstance());
  //第二步
  DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
  dbf.setNamespaceAware(true);
  Document doc = dbf.newDocumentBuilder().parse(new FileInputStream(args[0]));
  //第三步
  NodeList nl = doc.getElementsByTagNameNS(XMLSignature.xmlNS,"Signature");
  if (nl.getLength() == 0) {
   throw new Exception("Cannot find Signature element!");
  }
  //第四步,分為情形4.0,4.1,4.2或4.3
  //第4.0種情形
  DOMValidateContext valContext = new DOMValidateContext(new KeyStoreKeySelector(), nl.item(0));
  //第4.1種情形,需要Sign.java中的第6.1種情形
  // DOMValidateContext valContext = new DOMValidateContext(
  // new KeyValueKeySelector(), nl.item(0));
  //第4.2種情形,需要Sign.java中的第6.2種情形
  // KeyStore ks = KeyStore.getInstance("JKS");
  // FileInputStream fis = new FileInputStream("./etc/bizkeystore");
  // ks.load(fis,"sp1234".toCharArray());
  // fis.close();
  // X509KeySelector x509ks = new X509KeySelector(ks);
  // DOMValidateContext valContext = new DOMValidateContext(x509ks, nl.item(0));
  //第4.3中情形
  // PublicKey pKey = KeyStoreInfo.getPublicKey("./etc/bizkeystore",
  // "sp1234", "biz");
  //第五步
  XMLSignature signature = fac.unmarshalXMLSignature(valContext);
  //XMLSignature signature = fac.unmarshalXMLSignature(new DOMStructure(nl.item(0)));
  //第六步
  boolean coreValidity = signature.validate(valContext);
  //檢查核心校驗狀態
  if (coreValidity == false) {
   System.err.println("Signature failed core validation!");
   boolean sv = signature.getSignatureValue().validate(valContext);
   System.out.println("Signature validation status: " + sv);
   //每一個Reference的檢查校驗狀態
   Iterator i = signature.getSignedInfo().getReferences().iterator();
   for (int j = 0; i.hasNext(); j++) {
    boolean refValid = ((Reference) i.next()).validate(valContext);
    System.out.println("Reference (" + j + ") validation status: "+ refValid);
   }
  } else {
   System.out.println("Signature passed core validation!");
  }
 }
}

  要試驗這個程式,讀者可以執行Ant目標校驗。該程式把核心校驗狀態列印到System.out。如果簽名是有效的,將輸出"Signature passed core validation!";否則,輸出結果中將展示引用和簽名的校驗狀態;而這樣以來,我們就可以準確地搞清楚是它們其中的哪一些導致了此次失敗。

  校驗signature.xml的過程可以分解成六個步驟。

  步驟1-載入一個XMLSignatureFactory例項,這一步與在簽名程式中是一樣的。

  步驟2-載入要校驗的XML簽名。在這一步中,我們需要把包含XML簽名的XML載入到記憶體中並且把該XML文件轉換成一棵DOM樹。

  步驟3-識別DOM樹中的簽名結點。簽名是在名稱空間中定義的,它被描述為在JSR-105中的XMLSignature介面的靜態變數XMLNS。

  步驟4-建立一個DOMValidateContext例項。

  一個校驗上下文中的一項最關鍵的資訊顯然是金鑰。我們可以使用DOMValidateContext並透過兩種不同的方法來註冊公共金鑰。在第一種方法中,如果校驗應用程式已經擁有公共金鑰,它可以把該金鑰直接透過下列DOMValidateContext的構造器放入上下文中:

public DOMValidateContext(Key validatingKey,Node node)

  這正是在我們的示例中的情形4.3。

  第二個方法將使用DOMValidateContext註冊一個KeySelector,並且讓該KeySelector選擇公共金鑰-基於在要校驗的XMLSignature物件中可用的資訊。在JSR-105中,KeySelector是一個定義了兩個操作的抽象類:

public abstract KeySelectorResult select(KeyInfo keyInfo, Purpose purpose,
AlgorithmMethod method, XMLCryptoContext context)
throws KeySelectorException
public static KeySelector singletonKeySelector(Key key)

  第二個操作建立一個總是返回相同金鑰的KeySelector。第一個操作試圖選擇一個金鑰-它能夠滿足作為輸出傳遞的要求。

  KeySelectorResult是JSR-105中的一個介面-該規範中要求這個介面包含一個使用KeySelector選擇的Key值。在我們的示例中,我們使用SimpleKeySelectorResult類實現這個介面-簡單地包裝選擇的公共金鑰。

  在我們的示例中,我們實現並利用三個不同的KeySelectors來說明一個校驗應用程式工作的一些情形。

  在情形4.0中,KeyStoreKeySelector基於輸入引數從一個Key儲存中檢索公共金鑰。

  在情形4.1中,KeyValueKeySelector基於在輸入KeyInfo物件(它應該包含一個KeyValue物件作為它的內容的一部分;請參考Sign.java中的情形6.1)中的KeyValue資訊選擇一個鍵值。

  在情形4.2中,X509KeySelector基於包含在KeyInfo物件(它應該包含一個X509Data物件作為它的內容的一部分;請參考Sign.java中的情形6.2)中的X509Data及其它資訊選擇一個鍵。我們使用的是JSR-105中的X509KeySelector-其原作者是Sean Mullan。在此,我們稍微修改了一下其中的私有certSelect()方法以便它可以適合於我們使用keytool生成的證照。

  既然簽名中的KeyInfo可能包含各種資訊,顯然,一個應用程式必須選擇一個KeySelector實現-由它來使用包含在它將處理的KeyInfos中的資訊。

  步驟5-把簽名結點反編排成一個XMLSiganture物件。在上一步驟中,我們把signature.xml檔案載入進一棵DOM樹-由相應於樹中的Signature元素的結點所標識,並且使用一個DOMValidateContext和KeySelector(或私有金鑰)註冊該結點。為了校驗該XML簽名,我們需要把Signature結點反編排為一個XMLSignature物件。這是透過呼叫下列XMLSignatureFactory操作實現的:

public abstract XMLSignature unmarshalXMLSignature(XMLValidateContext context)
throws MarshalException

  步驟6-校驗XML簽名。這是透過呼叫XMLSignature例項的validate()方法實現的-以DOMValidateContext作為唯一的輸入引數。

  該validate()方法根據在W3C建議中定義的核心校驗過程校驗XML簽名。如前面所提及,這個過程包括兩個部分。其一是校驗所有的參考。在JSR-105中,這可以透過呼叫Reference介面的validate()操作來實現-以相關的校驗上下文作為輸入引數。

  該核心校驗的第二部分是簽名校驗-校驗規範的SignedInfo元素的簽名值。藉助於JSR-105,我們可以顯式地完成這一部分-透過呼叫與XMLSignature例項相關聯的SignatureValue物件的validate()方法實現,並以相關的校驗上下文作為輸入引數。

  在我們的示例中,我們使用這樣的知識來輸出每一個Reference和SigantureValue的校驗狀態-當XML簽名(核心)校驗失敗時;這樣以來,我們就可以得到導致失敗的更為詳細的資訊。

四、 修改XML簽名  為了表明該校驗程式確實能夠捕獲對生成的XML簽名的修改,我們可以在我們的示例中建立一個Tamper.java程式,允許我們修改清單中的信用卡號(或signature.xml檔案中的SignatureValue元素)。

  這個程式使用XML簽名文件和一個布林值作為引數。當該布林引數為true時,程式改變信用卡號;否則,它修改簽名值。

public class Tamper {
 public static void main(String[] args) throws Exception {
  String sigfile = "etc/signature.xml";

  //決定要修改的標誌-Reference或SignatureValue
  boolean tamperRef = true ;

  if (args.length >= 2) {
   sigfile = args[0];
   tamperRef = Boolean.parseBoolean(args[1]);
  }
  File file = new File(sigfile);
  DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
  dbf.setNamespaceAware(true);
  Document signature = dbf.newDocumentBuilder().parse(file);

  if (tamperRef){
   //修改信用卡號
   NodeList targets =signature.getDocumentElement().getElementsByTagName("number");
   Node number = targets.item(0);
   if (!number.getTextContent().equals("987654321")){
    number.setTextContent("987654321");
   }else{
    number.setTextContent("000000000");
   }
  }else{
   //修改SignatureValue(第一位元組)
   BASE64Encoder en = new BASE64Encoder();
   BASE64Decoder de = new BASE64Decoder();
   NodeList sigValues =signature.getDocumentElement().getElementsByTagName("SignatureValue");
   Node sigValue = sigValues.item(0);
   byte[] oldValue = de.decodeBuffer(sigValue.getTextContent());
   if (oldValue[0]!= 111){
    oldValue[0] = (byte)111;
   }else{
    oldValue[0] = (byte)112;
   }
   sigValue.setTextContent(en.encode(oldValue));
  }
  TransformerFactory tf = TransformerFactory.newInstance();
  Transformer trans = tf.newTransformer();
  trans.transform(new DOMSource(signature),new StreamResult(new FileOutputStream(file)));
 }
}

  為了執行它,讀者可以執行經修改的Ant目標。在執行這個修改的程式後,如果我們再次執行該校驗程式,核心校驗將失敗,並且System.out將分別輸出引用和簽名校驗的狀態。隨著第二個Boolean輸入引數值的不同,引用與/或簽名校驗可能報告失敗。

  至此,我們已經討論完本文中的示例應用程式。

  五、 結論

  XML簽名和JSR-105中都包含了大量的內容,在一篇小小的文章中我們是無法全面涉及它們(例如transforms,canonicalization方法,還有Manifest和SignatureProperties元素)的。而且,我們也根本沒有深入分析digest和簽名演算法。感興趣的讀者應該進一步參考W3C建議,JSR-105 API文件以及相關密碼學資訊。

  W3C建議中僅要求實現基於SHA-1雜湊函式支援digest和簽名方法。注意,一個由王小云教授率領的中國數學研究小組已經攻克了一些包括SHA-1在內廣泛應用的雜湊函式中的安全問題。儘管他們的結果還不是立即意味著-現在SHA-1在XML簽名方面的使用還不是不安全的;但是,密碼學專家們確實推薦在新的應用程式和系統中應該考慮使用更為安全的演算法。

  讀者還應該明白,儘管JCP計劃隨同JAVA SE 6一起發行JSR-105,但是JSR-105要求全面相容實現對JDK1.4及其高版本的支援。因此,在使用JDK 1.4開發的應用程式中使用這一技術應該是沒有問題的。

2007年01月11日 08:56

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8271432/viewspace-890419/,如需轉載,請註明出處,否則將追究法律責任。

相關文章