Skip to content

Instantly share code, notes, and snippets.

@pgstenberg
Last active March 24, 2023 08:48
Show Gist options
  • Save pgstenberg/4320c0fce981811a8f72c02c9456d4b4 to your computer and use it in GitHub Desktop.
Save pgstenberg/4320c0fce981811a8f72c02c9456d4b4 to your computer and use it in GitHub Desktop.
Example how to sign a SAML assertion using dsig
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