diff --git a/ws-security-common/src/main/java/org/apache/wss4j/common/WSS4JConstants.java b/ws-security-common/src/main/java/org/apache/wss4j/common/WSS4JConstants.java index 66e3e3d20..af54a2090 100644 --- a/ws-security-common/src/main/java/org/apache/wss4j/common/WSS4JConstants.java +++ b/ws-security-common/src/main/java/org/apache/wss4j/common/WSS4JConstants.java @@ -158,6 +158,11 @@ protected WSS4JConstants() { "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"; public static final String ECDSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512"; + // see RFC 9231 for these algorithm definitions + public static final String ED25519 = + "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519"; + public static final String ED448 = + "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448"; public static final String MGF_SHA1 = "http://www.w3.org/2009/xmlenc11#mgf1sha1"; public static final String MGF_SHA224 = "http://www.w3.org/2009/xmlenc11#mgf1sha224"; @@ -273,4 +278,4 @@ protected WSS4JConstants() { URI_SOAP11_ENV, URI_SOAP12_ENV, }; -} \ No newline at end of file +} diff --git a/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/DERDecoder.java b/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/DERDecoder.java index b08031ff3..78a89267b 100644 --- a/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/DERDecoder.java +++ b/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/DERDecoder.java @@ -45,6 +45,8 @@ public class DERDecoder { public static final byte TYPE_OCTET_STRING = 0x04; /** DER type identifier for a sequence value */ public static final byte TYPE_SEQUENCE = 0x30; + /** DER type identifier for ASN.1 "OBJECT IDENTIFIER" value. */ + public static final byte TYPE_OBJECT_IDENTIFIER = 0x06; private byte[] arr; private int pos; diff --git a/ws-security-common/src/test/resources/keys/wss-eddsa.p12 b/ws-security-common/src/test/resources/keys/wss-eddsa.p12 new file mode 100644 index 000000000..3e02ef16e Binary files /dev/null and b/ws-security-common/src/test/resources/keys/wss-eddsa.p12 differ diff --git a/ws-security-common/src/test/resources/wss-eddsa.properties b/ws-security-common/src/test/resources/wss-eddsa.properties new file mode 100644 index 000000000..5f97a4a2f --- /dev/null +++ b/ws-security-common/src/test/resources/wss-eddsa.properties @@ -0,0 +1,4 @@ +org.apache.wss4j.crypto.provider=org.apache.wss4j.common.crypto.Merlin +org.apache.wss4j.crypto.merlin.keystore.type=PKCS12 +org.apache.wss4j.crypto.merlin.keystore.password=security +org.apache.wss4j.crypto.merlin.keystore.file=keys/wss-eddsa.p12 diff --git a/ws-security-dom/src/main/java/org/apache/wss4j/dom/message/WSSecSignature.java b/ws-security-dom/src/main/java/org/apache/wss4j/dom/message/WSSecSignature.java index b296ff901..74fbd1b4d 100644 --- a/ws-security-dom/src/main/java/org/apache/wss4j/dom/message/WSSecSignature.java +++ b/ws-security-dom/src/main/java/org/apache/wss4j/dom/message/WSSecSignature.java @@ -21,6 +21,7 @@ import java.security.NoSuchProviderException; import java.security.Provider; +import java.security.PublicKey; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; @@ -46,6 +47,7 @@ import org.apache.wss4j.common.WSS4JConstants; import org.apache.wss4j.common.crypto.Crypto; import org.apache.wss4j.common.crypto.CryptoType; +import org.apache.wss4j.common.crypto.DERDecoder; import org.apache.wss4j.common.ext.WSSecurityException; import org.apache.wss4j.common.token.BinarySecurity; import org.apache.wss4j.common.token.DOMX509Data; @@ -887,6 +889,12 @@ private X509Certificate[] getSigningCerts() throws WSSecurityException { sigAlgo = WSConstants.RSA; } else if (pubKeyAlgo.equalsIgnoreCase("EC")) { sigAlgo = WSConstants.ECDSA_SHA256; + } else if (pubKeyAlgo.equalsIgnoreCase("Ed25519")) { + sigAlgo = WSConstants.ED25519; + } else if (pubKeyAlgo.equalsIgnoreCase("ED448")) { + sigAlgo = WSConstants.ED448; + } else if (pubKeyAlgo.equalsIgnoreCase("EdDSA")) { + sigAlgo = getSigAlgorithmForEdDSAKey(certs[0].getPublicKey()); } else { throw new WSSecurityException( WSSecurityException.ErrorCode.FAILURE, @@ -898,6 +906,51 @@ private X509Certificate[] getSigningCerts() throws WSSecurityException { return certs; } + /** + * The method returns EdDSA signature algorithm URI for public key type (Ed25519 or Ed448). + * + * @param publicKey the public key to get the algorithm from + * @return the signature algorithm URI (ED25519 or ED448) for the EdDSA public key + * @throws WSSecurityException if the algorithm cannot be determined + */ + private static String getSigAlgorithmForEdDSAKey(PublicKey publicKey) throws WSSecurityException { + + if (!"x.509".equalsIgnoreCase(publicKey.getFormat())) { + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "unknownAlgorithm", + new Object[]{"Unknown cert format!"}); + } + + DERDecoder decoder = new DERDecoder(publicKey.getEncoded()); + // find TYPE_OBJECT_IDENTIFIER (OID) for the public key algorithm + decoder.expect(decoder.TYPE_SEQUENCE); + decoder.getLength(); + decoder.expect(decoder.TYPE_SEQUENCE); + decoder.getLength(); + decoder.expect(decoder.TYPE_OBJECT_IDENTIFIER); + int size = decoder.getLength(); + if (size != 3) { + LOG.debug("Invalid ECDSA Public key OID byte size: [{}]", size); + throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY, "invalidCert"); + } + + // The first two nodes 1.3 of the OID are encoded onto a single byte. The first node is multiplied by 40 + // and the result is added to the value of the second node 3 which gives 43 or 0x2B. + decoder.expect(43); + // The second byte is expected 101 from the OID 1.3.101 (EdDSA) + decoder.expect(101); + // The third byte defines algorithm 112 is for Ed25519 and 113 is for Ed448 + byte algDef = decoder.getBytes(1)[0]; + switch (algDef) { + case 112: + return WSConstants.ED25519; + case 113: + return WSConstants.ED448; + default: + throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY, "unknownAlgorithm", + new Object[]{"Invalid ECDSA Public key OID!"}); + } + } + public boolean isIncludeSignatureToken() { return includeSignatureToken; } diff --git a/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/SignatureCertTest.java b/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/SignatureCertTest.java index 985424272..81c1777c3 100644 --- a/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/SignatureCertTest.java +++ b/ws-security-dom/src/test/java/org/apache/wss4j/dom/message/SignatureCertTest.java @@ -36,16 +36,17 @@ import org.apache.wss4j.dom.handler.WSHandlerConstants; import org.apache.wss4j.dom.handler.WSHandlerResult; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.Test; import org.w3c.dom.Document; +import javax.security.auth.x500.X500Principal; +import java.security.Security; import java.security.cert.X509Certificate; import java.util.Collections; import java.util.Properties; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; /** * This is a test for WSS-40. Essentially it just tests that a message is signed using a @@ -81,11 +82,18 @@ public class SignatureCertTest { private WSSecurityEngine secEngine = new WSSecurityEngine(); private Crypto crypto; private Crypto cryptoCA; + private boolean isJDK16up; public SignatureCertTest() throws Exception { WSSConfig.init(); crypto = CryptoFactory.getInstance("wss40.properties"); cryptoCA = CryptoFactory.getInstance("wss40CA.properties"); + try { + int javaVersion = Integer.getInteger("java.specification.version", 0); + isJDK16up = javaVersion >= 16; + } catch (NumberFormatException ex) { + LOG.warn("Error in retrieving the java version: [{}]", ex.getMessage()); + } } /** @@ -340,6 +348,103 @@ public void testExpiredCertInKeystore() throws Exception { } } + /** + * The Ed25519 KeyValue test. + */ + @Test + public void testED25519SignatureDirectReference() throws Exception { + try { + // not needed after JDK 16 + if (!isJDK16up) { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG); + WSSecHeader secHeader = new WSSecHeader(doc); + secHeader.insertSecurityHeader(); + + Crypto ed_crypto = CryptoFactory.getInstance("wss-eddsa.properties"); + + WSSecSignature builder = new WSSecSignature(secHeader); + builder.setUserInfo("ed25519", "security"); + builder.setKeyIdentifierType(WSConstants.BST_DIRECT_REFERENCE); + Document signedDoc = builder.build(ed_crypto); + // test the algorithm attribute + String outputString = + XMLUtils.prettyDocumentToString(signedDoc); + if (LOG.isDebugEnabled()) { + LOG.debug(outputString); + } + + assertTrue(outputString.contains("Algorithm=\"http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519\"")); + + final WSHandlerResult results = verify(signedDoc, ed_crypto); + + WSSecurityEngineResult actionResult = + results.getActionResults().get(WSConstants.SIGN).get(0); + assertNotNull(actionResult); + + java.security.Principal principal = + (java.security.Principal) actionResult.get(WSSecurityEngineResult.TAG_PRINCIPAL); + assertTrue(principal instanceof X500Principal); + X500Principal x500Principal = (X500Principal) principal; + assertEquals(new X500Principal("CN=ED25519,O=EDELIVERY,C=EU"), x500Principal); + + } finally { + if (!isJDK16up) { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } + } + } + + /** + * Successful ECKeyValue test. + */ + @Test + public void testED448KeyValue() throws Exception { + try { + // not needed after JDK 16 + if (!isJDK16up) { + Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + + Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG); + WSSecHeader secHeader = new WSSecHeader(doc); + secHeader.insertSecurityHeader(); + + Crypto ed_crypto = CryptoFactory.getInstance("wss-eddsa.properties"); + + WSSecSignature builder = new WSSecSignature(secHeader); + builder.setUserInfo("ed448", "security"); + builder.setKeyIdentifierType(WSConstants.X509_KEY_IDENTIFIER); + Document signedDoc = builder.build(ed_crypto); + + String outputString = + XMLUtils.prettyDocumentToString(signedDoc); + if (LOG.isDebugEnabled()) { + LOG.debug(outputString); + } + + assertTrue(outputString.contains("Algorithm=\"http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448\"")); + + final WSHandlerResult results = verify(signedDoc, ed_crypto); + + WSSecurityEngineResult actionResult = + results.getActionResults().get(WSConstants.SIGN).get(0); + assertNotNull(actionResult); + + java.security.Principal principal = + (java.security.Principal) actionResult.get(WSSecurityEngineResult.TAG_PRINCIPAL); + assertTrue(principal instanceof X500Principal); + X500Principal x500Principal = (X500Principal) principal; + assertEquals(new X500Principal("CN=ED448, O=EDELIVERY, C=EU"), x500Principal); + } finally { + if (!isJDK16up) { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } + } + } + /** * Verifies the soap envelope *
@@ -361,4 +466,4 @@ private WSHandlerResult verify(Document doc, Crypto crypto) throws Exception { } -} \ No newline at end of file +}