Last active
March 24, 2023 08:48
-
-
Save pgstenberg/4320c0fce981811a8f72c02c9456d4b4 to your computer and use it in GitHub Desktop.
Example how to sign a SAML assertion using dsig
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
import org.w3c.dom.Document | |
import org.w3c.dom.Element | |
import java.io.OutputStream | |
import java.security.KeyFactory | |
import java.security.KeyPair | |
import java.security.cert.CertificateFactory | |
import java.security.cert.X509Certificate | |
import java.security.interfaces.RSAPrivateKey | |
import java.security.interfaces.RSAPublicKey | |
import java.security.spec.PKCS8EncodedKeySpec | |
import java.security.spec.X509EncodedKeySpec | |
import java.util.* | |
import javax.xml.crypto.dsig.* | |
import javax.xml.crypto.dsig.dom.DOMSignContext | |
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec | |
import javax.xml.crypto.dsig.spec.TransformParameterSpec | |
import javax.xml.parsers.DocumentBuilder | |
import javax.xml.parsers.DocumentBuilderFactory | |
import javax.xml.transform.Transformer | |
import javax.xml.transform.TransformerFactory | |
import javax.xml.transform.dom.DOMSource | |
import javax.xml.transform.stream.StreamResult | |
fun main(args: Array<String>) { | |
val public_certificate = """ | |
-----BEGIN CERTIFICATE----- | |
MIIDWzCCAkOgAwIBAgIIK6BsmD3Zm+EwDQYJKoZIhvcNAQELBQAwMTELMAkGA1UEBhMCc2UxDzA | |
NBgNVBAoTBmN1cml0eTERMA8GA1UEAxMIQVBKS0RFU0swHhcNMjIxMjIwMDcyMTMzWhcNMjcxMj | |
E5MDcyMTMzWjAxMQswCQYDVQQGEwJzZTEPMA0GA1UEChMGY3VyaXR5MREwDwYDVQQDEwhBUEpLR | |
EVTSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN61QSw/skyFNcgm+czUBcjbcn8w | |
KBWO9yF7Y08oGFmajFDHOaoJSPQsW9INgXCH58MAmSLTj0Mqc7XIjUpYgg+Hvqlow5UqGn4y0qa | |
7kJ68k17/MgduPsmPaZh47Q3d1ebcS98P2dnrzH3S/hHye1JMB+bN5YEcEvYS4ECLRacmrMp614 | |
SUV25skg1mTKRJTHsXrgjayIhBCVWU0II9lX5lFncJ9bOVCns2wb14zIRrvam0Ev10tg3NnwYB1 | |
R8UIa6gzQ+uLoNergTbwK02zTUFuuoqFq2G0f2ZbFPYM6g7DwisoTT1u+2aauBovXy7QltL8Sxj | |
TpufInowr/jIRhcCAwEAAaN3MHUwHQYDVR0OBBYEFJdrfKQ5E9XhbeciA4fNRT/U1Q3tMAsGA1U | |
dDwQEAwIF4DAkBgNVHREEHTAbgghBUEpLREVTS4IJbG9jYWxob3N0hwR/AAABMAwGA1UdEwEB/w | |
QCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAC+2xt8KKGxhmYx/L | |
ox2ezxOO2TIWX+J3H9cQ8oHDuntVHHLWGl8i7bz4mbp/S+Xv2zNO/QC76F2eT6Ev6kQWWzRnoa0 | |
DHzz1efKjmekyzGCGFo4VhedlOvF6EJLTrCJz5AjNF61vG6cO6epS7VHEO1AeCFATqSHEuKisPs | |
hvb93xyUKNsdMLCichS/XCTI9dkrANqYjT6b0xPqyngax5ao1jn2HA+XO34ar/T1MECyAWJ7nME | |
skeuY245T5VM3OmXd8Jr70JnId1nXP8JC0Z3sOlluA/t9nofZNapp9PjkXD+YaPH/URsplVS4Qx | |
azXlqHA54gg2BpK3KdsaFY1EpM= | |
-----END CERTIFICATE----- | |
""".trimIndent() | |
val public_key = """ | |
-----BEGIN PUBLIC KEY----- | |
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3rVBLD+yTIU1yCb5zNQF | |
yNtyfzAoFY73IXtjTygYWZqMUMc5qglI9Cxb0g2BcIfnwwCZItOPQypztciNSliC | |
D4e+qWjDlSoafjLSpruQnryTXv8yB24+yY9pmHjtDd3V5txL3w/Z2evMfdL+EfJ7 | |
UkwH5s3lgRwS9hLgQItFpyasynrXhJRXbmySDWZMpElMexeuCNrIiEEJVZTQgj2V | |
fmUWdwn1s5UKezbBvXjMhGu9qbQS/XS2Dc2fBgHVHxQhrqDND64ug16uBNvArTbN | |
NQW66ioWrYbR/ZlsU9gzqDsPCKyhNPW77Zpq4Gi9fLtCW0vxLGNOm58iejCv+MhG | |
FwIDAQAB | |
-----END PUBLIC KEY----- | |
""".trimIndent() | |
val private_key = """ | |
-----BEGIN PRIVATE KEY----- | |
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDetUEsP7JMhTXIJvnM1AXI23J | |
/MCgVjvche2NPKBhZmoxQxzmqCUj0LFvSDYFwh+fDAJki049DKnO1yI1KWIIPh76paMOVKhp+Mt | |
Kmu5CevJNe/zIHbj7Jj2mYeO0N3dXm3EvfD9nZ68x90v4R8ntSTAfmzeWBHBL2EuBAi0WnJqzKe | |
teElFdubJINZkykSUx7F64I2siIQQlVlNCCPZV+ZRZ3CfWzlQp7NsG9eMyEa72ptBL9dLYNzZ8G | |
AdUfFCGuoM0Pri6DXq4E28CtNs01BbrqKhathtH9mWxT2DOoOw8IrKE09bvtmmrgaL18u0JbS/E | |
sY06bnyJ6MK/4yEYXAgMBAAECggEAAqaNnAU2DgsX1MYB+xoa54UVG8Zq87a74j4htHN5trdMLD | |
nyyb9Kiv1sKlfWzowPihabu/pgniAHOIamh9f91El9T27bxQ63OgFI2Isq8Xi1GFBZPBVn0eZPD | |
22BBMU7IoBEtubtZNaVnHnCZFxKc3RMM8cHkD3RS/R1js8ZiR+7B5vtTKLyFBSGPff5SlUngYN7 | |
j+eW5QCNc9zhYxwmLkTYxU6YFLt2EomnoM/HUxzYfY/MHfwJy29FX7n3rtkTWK0iZ0uLewgeK3e | |
6j1Goc19f/HV0Eq8ABvhi3aCTP4JOR/AwiY/DC8ojtKN62A73dGRkNbSGLvFIJVaNcY8qgQKBgQ | |
Dr+1n0w1qC2Fcev1YSRyvwSnwY3jiNo4aqKEcqT35Dht77hB6E6SEU05eyKg0wWVWJr6p730oyy | |
8hZAhhEDtxO7DzY9PWCkcQxqLuSq6SQbnEctG+aAjnZrFGY5mqIGK6+Ajbmpvi2u7HFVT7d9ver | |
tKMQtV4PT+co/iv+RK0vZwKBgQDxmaUutqSXNBSwMHQ+26fqaWQXVjw5YvC0X7zUJm2Ab09IMNS | |
qxqotczKy3nIhIIc8YFfO+/tS20VWGSI2Ngp3mYkDt57RK3/VxIc6IlH8G/iaU3NrjrkLXPXUFF | |
cQnsq+kM7YLL3ONCUzurFDcspP+BrlBzCbXkYO8UlcVWH10QKBgQCk7L9bBCk+50pED/9suNcpk | |
jUXAEBQJWiZhZrvJC2friQrbpQR2gkn0BXmC+O51cWle+NPvafSxn+YTZF+B1DLy+lezBzGC3Au | |
MLofcNyLoNRm9mhFH6ckzX0dunPb+DwwScXq/+k1dQpyWvicEt3X4GBS7h713qc1DCbdB0xuowK | |
BgQDwpTexBd9/dDK/JCRFkAj7Jiq6S/0EtBZJs6qkLfqYGUcBAxJxYByV1M7E92j6sinB67zKwJ | |
ae+yVfEv3OvZlDc7zT5QveENPuGykOsKy0zy+amFC465pJRTjfG7t1JJWRpy9Ah6AvSiVcFzMFm | |
csGSHyRb83sk8R4kcGepLVEYQKBgHgmvK1fHdWGPS0Kzf+fBeWH6hJJ36t1gFZZxHNZUXYw3q1u | |
SB/lPEFKi32R2DvS0PVKGEiIS1rkrgeNgAlYH5Louon9m3XfE0y+4OFz5vTmgUtG3ejSnayNNB/ | |
qfS+KlFn0siOw07e9XmJdE4ocupSzeXXWQWmorSsxexUKAtM2 | |
-----END PRIVATE KEY----- | |
""" | |
val assertion = """ | |
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_6Mlr7m8BpxJt0iu6wlI5bVB3qbItEYM3j1gm" IssueInstant="2023-03-24T09:17:35Z" Version="2.0"> | |
<saml2:Issuer>https://localhost:8443/oauth/v2/oauth-token</saml2:Issuer> | |
<saml2:Subject> | |
<saml2:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">hanlar</saml2:NameID> | |
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> | |
<saml2:SubjectConfirmationData InResponseTo="" NotOnOrAfter="2023-03-24T10:17:35Z" Recipient="https://sambi.test.com/SP/login"></saml2:SubjectConfirmationData> | |
</saml2:SubjectConfirmation> | |
</saml2:Subject> | |
<saml2:Conditions NotBefore="2023-03-24T09:17:35Z" NotOnOrAfter="2023-03-24T10:17:35Z"> | |
<saml2:AudienceRestriction> | |
<saml2:Audience>https://sambi.test.com/SP</saml2:Audience> | |
</saml2:AudienceRestriction> | |
</saml2:Conditions> | |
<saml2:AuthnStatement AuthnInstant="2023-03-24T09:17:35Z" SessionNotOnOrAfter="2023-03-24T10:17:35Z"> | |
<saml2:AuthnContext> | |
<saml2:AuthnContextClassRef>https://id.sambi.se/loa/loa3</saml2:AuthnContextClassRef> | |
</saml2:AuthnContext> | |
</saml2:AuthnStatement> | |
<saml2:AttributeStatement> | |
<saml2:Attribute FriendlyName="healthcareProfessionalLicenseIdentityNumber" Name="http://sambi.se/attributes/1/healthcareProfessionalLicenseIdentityNumber"> | |
<saml2:AttributeValue xmlns:xsi="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" xsi:type="xsd:string">test2</saml2:AttributeValue> | |
</saml2:Attribute> | |
<saml2:Attribute FriendlyName="mail" Name="http://sambi.se/attributes/1/mail"> | |
<saml2:AttributeValue xmlns:xsi="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" xsi:type="xsd:string">test6@test.se</saml2:AttributeValue> | |
</saml2:Attribute> | |
<saml2:Attribute FriendlyName="surname" Name="http://sambi.se/attributes/1/surname"> | |
<saml2:AttributeValue xmlns:xsi="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" xsi:type="xsd:string">test3</saml2:AttributeValue> | |
</saml2:Attribute> | |
<saml2:Attribute FriendlyName="givenName" Name="http://sambi.se/attributes/1/givenName"> | |
<saml2:AttributeValue xmlns:xsi="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" xsi:type="xsd:string">test4</saml2:AttributeValue> | |
</saml2:Attribute> | |
<saml2:Attribute FriendlyName="pharmacyIdentifier" Name="http://sambi.se/attributes/1/pharmacyIdentifier"> | |
<saml2:AttributeValue xmlns:xsi="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" xsi:type="xsd:string">test5</saml2:AttributeValue> | |
</saml2:Attribute> | |
<saml2:Attribute FriendlyName="healthcareProfessionalLicense" Name="http://sambi.se/attributes/1/healthcareProfessionalLicense"> | |
<saml2:AttributeValue xmlns:xsi="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" xsi:type="xsd:string">test</saml2:AttributeValue> | |
</saml2:Attribute> | |
</saml2:AttributeStatement> | |
</saml2:Assertion> | |
""".trimIndent() | |
// Create a namespace aware factory and load assertion data | |
val dbf = DocumentBuilderFactory.newInstance() | |
dbf.isNamespaceAware = true; | |
val builder: DocumentBuilder = dbf.newDocumentBuilder() | |
val doc: Document = builder.parse(assertion.byteInputStream()) | |
// Load private key | |
val privateKeySpec = PKCS8EncodedKeySpec(Base64.getMimeDecoder().decode(private_key | |
.replace("-----BEGIN PRIVATE KEY-----\n", "") | |
.replace("-----END PRIVATE KEY-----", "") | |
)) | |
val privKey: RSAPrivateKey = KeyFactory.getInstance("RSA").generatePrivate(privateKeySpec) as RSAPrivateKey | |
// Load public key | |
val publicKeySpec = X509EncodedKeySpec(Base64.getMimeDecoder().decode(public_key | |
.replace("-----BEGIN PUBLIC KEY-----\n", "") | |
.replace("-----END PUBLIC KEY-----", "") | |
)) | |
val pubKey: RSAPublicKey = KeyFactory.getInstance("RSA").generatePublic(publicKeySpec) as RSAPublicKey | |
// Load certificate | |
val certificate: X509Certificate = (CertificateFactory.getInstance("X.509") | |
.generateCertificate(public_certificate.byteInputStream()) as X509Certificate) | |
val kp: KeyPair = KeyPair(pubKey, privKey) | |
val xsf = XMLSignatureFactory.getInstance("DOM") | |
// Create reference for signing | |
val ref: Reference = xsf.newReference( | |
"#" + doc.documentElement.getAttribute("ID"), | |
xsf.newDigestMethod(DigestMethod.SHA1, null), listOf( | |
xsf.newTransform( | |
Transform.ENVELOPED, | |
null as TransformParameterSpec? | |
) | |
), null, null | |
) | |
// Create signedinfo with correct canonicalization and signature method, with reference to document to be signed. | |
val si: SignedInfo = xsf.newSignedInfo( | |
xsf.newCanonicalizationMethod( | |
CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS, | |
null as C14NMethodParameterSpec? | |
), | |
xsf.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null), listOf(ref) | |
) | |
val kif = xsf.keyInfoFactory | |
// Create new signature using loaded keypair and certificate | |
val signature = xsf.newXMLSignature(si, xsf.keyInfoFactory.newKeyInfo( | |
listOf(kif.newX509Data(listOf(certificate)) | |
))) | |
// Create signcontext and set id in NS so reference can be found using URI | |
val dsc = DOMSignContext(kp.private, doc.documentElement) | |
dsc.defaultNamespacePrefix = "ds" | |
val element: Element = doc.documentElement as Element | |
dsc.setIdAttributeNS(element, null, "ID") | |
// Do the actual signing | |
signature.sign(dsc); | |
// Send signed document to stdout | |
val os: OutputStream = System.out | |
val tf: TransformerFactory = TransformerFactory.newInstance() | |
val trans: Transformer = tf.newTransformer() | |
trans.transform(DOMSource(doc), StreamResult(os)) | |
// Validate output on https://samltool.io/ | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment