├── .gitignore ├── anonymous-prod-key.pem ├── anonymous-prod-cert.pem ├── README.md ├── handysignatur.py ├── a-sign-SSL-07.pem └── demo.py /.gitignore: -------------------------------------------------------------------------------- 1 | digitales-amt-binding-uuid.txt 2 | digitales-amt-key.pem 3 | digitales-amt-csr.pem 4 | digitales-amt-cert.pem 5 | eAusweise-key.pem 6 | eAusweise-csr.pem 7 | eAusweise-cert.pem 8 | -------------------------------------------------------------------------------- /anonymous-prod-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | ME4CAQAwEAYHKoZIzj0CAQYFK4EEACIENzA1AgEBBDCHLUYcHyPcpSRERZKhiaWJ 3 | l6A1iK9K4zpruHkD2Bx4FXcOI6S5fZoSj+jbdiuq7XY= 4 | -----END PRIVATE KEY----- 5 | -------------------------------------------------------------------------------- /anonymous-prod-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC7DCCAnKgAwIBAgIIClg/s9EPLdowCgYIKoZIzj0EAwMwZjELMAkGA1UEBhMC 3 | QVQxITAfBgNVBAoTGEJ1bmRlc3JlY2hlbnplbnRydW0gR21iSDEPMA0GA1UECxMG 4 | QlJaIENBMSMwIQYDVQQDExpCUlotU2VydmljZS1MZXZlbDItQ0EtMy0wMTAeFw0y 5 | MjA1MDIxMTU4MjVaFw0yNzA1MDMxMTU4MjVaMFgxCzAJBgNVBAYTAkFUMSEwHwYD 6 | VQQKExhCdW5kZXNyZWNoZW56ZW50cnVtIEdtYkgxJjAkBgNVBAMTHUJSWi5UQy5B 7 | V1AtUFJPRC1Bbm9ueW1vdXMtMDAxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEqx1R 8 | XHC1U5gx5Mw+6h2CHihSFPJHBBAozoC8lpbWCETLtCFrYcq2yzo8VhtlgQf8gNDV 9 | hBMWXf9JGs57XLns9JR+WAX0O4eJNzZUSFtLRWO6zVzmeEhYp1J3uwouMW5No4H6 10 | MIH3MDsGCCsGAQUFBwEBBC8wLTArBggrBgEFBQcwAYYfaHR0cDovL2JyemNhb2Nz 11 | cC5icnouZ3YuYXQvb2NzcDAdBgNVHQ4EFgQUqYfjpx5JwJM3odTI2Lajb2Hos9Mw 12 | HwYDVR0jBBgwFoAUs5RX9j78/sydpDZitufW6siCH+EwGAYDVR0gBBEwDzANBgsr 13 | BgEEAbM4j10BATA2BgNVHR8ELzAtMCugKaAnhiVodHRwOi8vYnJ6Y2FjcmwuYnJ6 14 | Lmd2LmF0L2NybC8zc2wyLjAxMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAK 15 | BggrBgEFBQcDAjAKBggqhkjOPQQDAwNoADBlAjB9kByORTJ7jJukHVuk4iU2lCUM 16 | qGeRb5QlGGSAIjz5A9WM/Lr0Eg6r/R8szIxY4PMCMQDo/BwWEDatgBr9jJcz//7D 17 | /Qbim8z+40gfDn82MqdvHy119eQm0NGmeTBZDX6H2sc= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Reverse-Analysis of the Austrian "ID Wallet" (eAusweise / Digitales Amt) 2 | 3 | This is an ongoing effort for documenting and understanding the Austrian "ID Wallet" apps: 4 | - [*Digitales Amt*](https://play.google.com/store/apps/details?id=at.gv.oe.app&hl=en&gl=US) 5 | - [*eAusweise*](https://play.google.com/store/apps/details?id=at.gv.oe.awp.eausweise&hl=de&gl=US) 6 | 7 | First, the user logs in at the *Digitales Amt* app. Then, the *eAusweise* app is set-up, which sends an intent to the first app. 8 | 9 | Based on a reverse-analysis, the `./demo.py` script re-implements this procedure. After a successfull run, it provides you with **TLS Client Certificates** (and keys) for use with: 10 | - The *Digitales Amt* backend API endpoints 11 | - The *eAusweise* backend API endpoints 12 | 13 | You will need some eIDAS compliant identity token during the process, such as the german national identity card. 14 | 15 | ## Handysignatur 16 | 17 | Need a Handysignatur test account? [Here](https://www.a-trust.at/testidentitaetenmanagement) you can get a test account on the production infrastructure using an eIDAS identity token (such as the german ID card). On an iPhone, I logged into the *Digitales Amt* app using the "EU-Login" and then accessed the test identity management on my phone. 18 | -------------------------------------------------------------------------------- /handysignatur.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | 3 | import requests 4 | import random 5 | from lxml import etree as ET 6 | from base64 import b64decode, b64encode 7 | 8 | SALT = b'anugBygKOMdtx9AJbhrN5b1sObQ5AyISZbKf1hxiPA73IYFc9I1QggKwgVaO48vsAM75MLkXPm75gNaZxUdDeWHI1aYYOdJxEkE55DgqjzMSv44GgvMIDk6NWy1CSHyHURSRaQjMn6NdaXpa0xdlfdrxOcQx3ajHNqqCHUPJV7BkOtHJaV2rPmhWzEOlD9yFCUWRDenT1mwumnuxbisDqOWtlPWQ293zjWZEV0Wm0eXA1oC7Cz6ZkV0c2f4QIhasWGbm3H1p0Q308J70G666JkaZ2hkmI4AQeAjcnWCuIlSIqGBg5aEQuyjs3jeb5EPtT2xXUVpVew02IIF8X9OePuO5VOyN8WIPtKN1Wxt6qYznjMkKY6qVAwHt9FsKTBo5AHbv02jARUFWzIf5WR3OWL61maEo6rNOl1OjUA0UlSNgJHDNaLwrFZD3AguMoGB7X8hBDCYfapywZ365c3hSZcieQ9QGQWdYXJYWEk9I2ZXf7729l2ZKGbqfiwnjSRNw7ofZEd8I0WAS2reVz34MLAK5oEE0LINjphBWVged2XfEXY95orEfQIIBWdrogtqjOLIhtm6wYMJR27Y8jmADlGOzbC7egGbv5frNyQ36pmgaRwqZgYlZSPAjo8mbZuKUsrRGBQACIv99odb8w4idBawvMs0RNGlH2MEcsgXxEkLf7HNyFNRjdNt6tn2cBCsrVl9YiYZfbcSciMPdor03Y0uv9mZs2nCPcTi4rp4tc4WyZB3vY9vvHRTFNqrfhC7awHmMJNZXPBz3oQmZ02ZYtZhXQ8K6AFb2oLqks8M06MVvlaYTbB9RVQbxWU4yG7N4aiyl02A5D78v6GYQuQJGf2E4ZQk9abaHujqDpWWCfBAhKjvHXnlpPa6V7Llj5fkDQJgA5DecBVk3UVn9Yn6ZAg57VUyy0JgxTHKcnUnEDEOUiBkBAHD95cRaOA4YdvOrDHEWcwBZKlutMmjWFhUW09M9wtJp9dr6CimpkBlgHzYVwKB6hCUUwh6pQyCF' 9 | ATRUST_VERSION_NAME = "2.9.11" 10 | 11 | def minifyXML(xml): 12 | return ET.tostring(ET.XML(xml, ET.XMLParser(remove_blank_text=True))).decode() 13 | 14 | headers = { 15 | 'Host': 'api.a-trust.at', 16 | 'User-Agent': 'Handy-Signatur-App/2.9.11', 17 | 'Connection': 'Keep-Alive', 18 | } 19 | 20 | command = """ 21 | 22 | 0 23 | 0 24 | emulator64_x86_64_arm64 25 | 857f87de-81a6-44f4-8e70-03d7b1911cfa 26 | """ 27 | 28 | nonce = random.randint(0, 2**64) 29 | 30 | data = { 31 | 'command': minifyXML(command), 32 | 'nonce': nonce.to_bytes(8, 'little').hex().upper(), 33 | } 34 | 35 | response = requests.post('https://api.a-trust.at/WebTanApp/AppAktivierungStart.ashx', headers=headers, data=data, verify='a-sign-SSL-07.pem') 36 | 37 | resp = b64decode(ET.XML(response.text).xpath("/tanapp/b64Response/text()")[0]) 38 | data = ET.XML(resp.decode()) 39 | 40 | session_id = data.xpath("/tanapp/sessionid/text()")[0] 41 | app_token = data.xpath("/tanapp/apptoken/text()")[0] 42 | g = int(data.xpath("/tanapp/response/Response/G/text()")[0]) 43 | p = int(data.xpath("/tanapp/response/Response/P/text()")[0]) 44 | pubkeyA = int(data.xpath("/tanapp/response/Response/PubKeyA/text()")[0]) 45 | 46 | from cryptography.hazmat.primitives.asymmetric.dh import DHParameterNumbers, DHPublicNumbers 47 | 48 | parameter_numbers = DHParameterNumbers(p=p, g=g) 49 | params = parameter_numbers.parameters() 50 | privkey = params.generate_private_key() 51 | shared_secret = pow(pubkeyA, privkey.private_numbers().x, p) 52 | shared_secret = str(shared_secret).encode() 53 | 54 | from cryptography.hazmat.primitives import hashes 55 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 56 | 57 | kdf = PBKDF2HMAC( 58 | algorithm=hashes.SHA256(), 59 | length=32, 60 | salt=SALT, 61 | iterations=25000 62 | ) 63 | 64 | encryption_key = kdf.derive(shared_secret) 65 | 66 | system_info_xml = f""" 67 | 68 | {ATRUST_VERSION_NAME} 69 | Android 70 | 31 71 | """ 72 | 73 | import os 74 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 75 | 76 | iv = os.urandom(16) 77 | encryptor = Cipher(algorithms.AES(encryption_key), modes.GCM(iv)).encryptor() 78 | 79 | pubkey_b = str(privkey.public_key().public_numbers().y) 80 | mini = "\n" + minifyXML(system_info_xml.encode()) 81 | 82 | enc_init_data = b64encode( 83 | encryptor.update( 84 | mini.encode() 85 | ) + encryptor.finalize() + encryptor.tag 86 | ).decode() 87 | 88 | from urllib.parse import quote_plus 89 | 90 | command = f""" 91 | 92 | 0 93 | 1 94 | { pubkey_b } 95 | { quote_plus(enc_init_data) } 96 | { quote_plus(b64encode(iv).decode()) } 97 | """ 98 | 99 | nonce = random.randint(0, 2**64) 100 | postData = f"AppToken={app_token}&Command={minifyXML(command)}&nonce={random.randbytes(8).hex().upper()}" 101 | 102 | params = { 103 | 'step': '1', 104 | 'sid': session_id, 105 | } 106 | 107 | headers = { 108 | 'Host': 'api.a-trust.at', 109 | 'User-Agent': 'Handy-Signatur-App/2.9.11', 110 | 'Content-Type': 'application/x-www-form-urlencoded', 111 | 'Connection': 'Keep-Alive', 112 | # 'Accept-Encoding': 'gzip, deflate', 113 | # 'Content-Length': '1074', 114 | } 115 | 116 | response = requests.post( 117 | 'https://api.a-trust.at/WebTanApp/AppAktivierungCommand.ashx', 118 | params=params, 119 | headers=headers, 120 | data=postData, 121 | verify='a-sign-SSL-07.pem', 122 | ) 123 | 124 | print(response) 125 | print(response.headers) 126 | print(response.text) 127 | -------------------------------------------------------------------------------- /a-sign-SSL-07.pem: -------------------------------------------------------------------------------- 1 | Certificate: 2 | Data: 3 | Version: 3 (0x2) 4 | Serial Number: 1408559753 (0x53f4ea89) 5 | Signature Algorithm: sha256WithRSAEncryption 6 | Issuer: C = AT, O = A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH, OU = A-Trust-Root-07, CN = A-Trust-Root-07 7 | Validity 8 | Not Before: May 17 11:23:22 2018 GMT 9 | Not After : Nov 19 10:23:22 2036 GMT 10 | Subject: C = AT, O = A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH, OU = A-Trust-Root-07, CN = A-Trust-Root-07 11 | Subject Public Key Info: 12 | Public Key Algorithm: rsaEncryption 13 | Public-Key: (4096 bit) 14 | Modulus: 15 | 00:a3:07:be:5e:01:fa:25:d8:05:8f:34:7f:91:13: 16 | 28:1f:5a:b9:c3:22:40:ef:2c:40:4a:a3:50:46:5d: 17 | 61:b8:d9:38:28:bb:96:5e:3f:0d:f1:a9:94:74:a6: 18 | 6f:8d:90:4a:6a:80:fd:0d:d9:ab:2a:02:bd:70:08: 19 | e8:49:a1:ea:9b:f7:e6:13:9b:de:1f:29:00:f1:a8: 20 | b5:1e:1a:78:4e:a2:c0:d7:05:a6:b7:c7:61:53:b5: 21 | a0:85:91:b5:5a:54:3e:f7:cf:7d:fb:52:8e:12:70: 22 | 38:cc:9b:45:0c:df:89:95:a7:77:f8:e5:67:03:5b: 23 | 91:c2:01:d4:5b:0a:7b:7f:7d:58:2e:ac:0b:c3:b9: 24 | 77:3b:11:96:ee:9b:fc:e9:27:78:4b:d3:2b:d6:24: 25 | be:02:7c:a8:89:23:75:d7:1e:25:1f:5f:62:92:a1: 26 | 22:f4:7e:c9:03:49:08:91:c5:99:cd:95:32:d2:5e: 27 | 7f:9f:29:48:28:c9:91:57:42:73:fa:7a:c1:b1:dd: 28 | 02:dc:02:a7:54:02:62:f3:84:93:00:58:c7:62:57: 29 | cc:da:19:68:fb:28:50:bb:04:6d:67:4b:84:81:e3: 30 | 30:dc:7c:2f:7f:d1:2e:7a:2e:c5:62:a3:88:0a:d3: 31 | 5a:87:78:e8:f6:a7:51:2b:2d:72:83:a4:0d:8c:e4: 32 | c9:e6:3d:a5:7c:ce:c1:c3:33:86:68:35:52:59:52: 33 | 18:93:ff:62:c9:6a:53:71:b7:b3:51:7e:80:a9:b9: 34 | df:6a:fb:5d:1d:40:c6:57:b3:54:81:ba:46:4d:81: 35 | 5d:bc:d7:57:2b:ac:99:a7:92:e2:2a:9c:e6:5a:f2: 36 | 70:36:33:6b:5a:21:c7:0f:b7:c4:f2:57:00:35:ea: 37 | 74:24:94:2b:9f:06:eb:ce:ad:e3:d6:e5:e9:74:59: 38 | 50:b1:89:ef:7c:e5:5c:8a:d5:51:b5:e4:61:4f:6c: 39 | d3:24:c9:e4:c2:bb:87:28:ae:b9:e6:9b:e3:4a:68: 40 | 4e:a3:96:59:65:46:5d:00:79:70:60:5d:68:4e:a3: 41 | 9c:9d:90:19:a8:6e:53:06:98:a6:79:54:a7:12:62: 42 | 76:a1:0e:3d:d6:6b:b7:4d:2e:aa:be:df:9d:a9:56: 43 | 1f:1d:47:63:ac:be:00:b2:c4:4c:4f:d6:5d:b7:b2: 44 | eb:89:88:f6:72:65:5e:e1:f4:f1:ed:0c:66:a5:28: 45 | 51:67:7d:ac:a4:4b:aa:5a:45:9c:40:27:48:c6:f0: 46 | 39:1c:3b:85:53:bb:f1:b0:82:f0:06:4f:08:56:4c: 47 | ac:f1:49:62:2e:d1:19:02:98:44:88:b6:9a:35:33: 48 | f2:f7:8d:ce:bc:e3:3a:57:09:9f:0e:7d:f0:d8:21: 49 | 13:b5:61 50 | Exponent: 65537 (0x10001) 51 | X509v3 extensions: 52 | X509v3 Subject Key Identifier: 53 | 44:C0:11:AD:53:27:87:F4 54 | X509v3 Key Usage: critical 55 | Certificate Sign, CRL Sign 56 | X509v3 Basic Constraints: critical 57 | CA:TRUE 58 | Signature Algorithm: sha256WithRSAEncryption 59 | Signature Value: 60 | 9f:47:48:ea:f2:bb:b4:56:b9:c7:3d:30:40:d4:d3:a3:95:96: 61 | 18:f4:11:bf:84:b8:64:16:82:0a:a6:fa:84:95:fb:bc:9c:fb: 62 | a4:7c:38:ad:4d:54:81:16:34:60:d7:ef:50:99:ff:88:d2:2f: 63 | cf:a4:10:c8:87:02:60:19:79:1f:75:da:e0:83:71:73:ec:c9: 64 | 80:92:21:8c:40:d5:e9:e7:96:02:54:08:9a:ba:42:6f:bf:a5: 65 | 72:64:f9:ea:ff:16:d6:81:ef:96:ce:64:f2:8f:de:5c:15:8f: 66 | 9f:18:63:86:5e:a5:c5:59:a7:ed:c6:08:60:64:fc:98:95:7a: 67 | 01:f7:0d:3d:85:ca:32:eb:6d:21:b5:50:dc:96:8a:5a:41:73: 68 | d5:49:6e:a0:a5:38:58:b5:7e:c8:19:f4:fd:05:cb:fe:7f:c8: 69 | d4:4a:11:8d:07:3f:47:6b:48:0f:db:06:c4:ff:63:24:7c:f0: 70 | d7:24:7b:92:e5:e8:93:7d:9c:f9:5d:30:8e:eb:92:c3:30:9a: 71 | 60:61:e0:1e:b5:c4:95:21:f5:53:d6:d3:98:1d:df:79:a3:6b: 72 | 80:6d:97:a7:32:fd:c2:4a:69:fb:4c:9e:ab:3f:34:ad:76:fb: 73 | b0:9a:3d:86:3d:ef:c0:a6:ec:17:26:99:84:8b:28:b4:d1:c1: 74 | f3:a2:2c:01:a1:78:be:1a:b4:77:d5:98:cd:e3:72:f5:64:22: 75 | ba:5c:89:56:44:8d:3f:3d:eb:26:82:74:bb:fa:9e:f1:15:32: 76 | 16:6c:13:97:a0:9c:a8:bd:c8:0f:75:a6:43:2d:70:6e:d0:6c: 77 | 2d:c3:dd:57:67:59:6a:aa:fd:9f:f3:d2:05:9b:0d:da:af:e9: 78 | e7:23:31:91:e6:aa:59:bc:e8:71:ce:e8:36:99:d4:db:f7:9d: 79 | a2:6d:9a:92:f0:7d:86:62:39:4a:98:37:59:c4:4d:95:af:d6: 80 | 7b:75:d0:77:8f:81:a9:ab:b5:6c:1c:3a:b0:50:0d:03:d3:23: 81 | 8d:86:13:2e:86:d1:81:bb:dc:75:3c:c7:b4:4e:97:75:ec:a7: 82 | b0:44:33:15:21:ae:b7:13:9d:36:a6:4c:29:a6:99:a8:5e:a5: 83 | 14:1c:43:12:05:f6:d6:c2:d7:2a:4f:e7:5d:22:17:f5:3e:37: 84 | cf:96:22:06:21:32:7b:05:53:ae:67:81:0f:f8:4c:e5:3f:c8: 85 | 76:d2:6f:3b:90:c7:90:e8:9a:c5:f8:18:de:54:76:d9:45:5b: 86 | 9c:5f:9a:69:b5:9e:00:e4:9b:8b:7b:73:4e:ba:59:cb:e4:c2: 87 | f3:61:dd:4e:24:39:45:86:d2:9c:5b:1b:8c:84:ea:26:22:2f: 88 | e2:16:cb:f2:d6:d7:61:88 89 | -----BEGIN CERTIFICATE----- 90 | MIIFzDCCA7SgAwIBAgIEU/TqiTANBgkqhkiG9w0BAQsFADCBizELMAkGA1UEBhMC 91 | QVQxSDBGBgNVBAoMP0EtVHJ1c3QgR2VzLiBmLiBTaWNoZXJoZWl0c3N5c3RlbWUg 92 | aW0gZWxla3RyLiBEYXRlbnZlcmtlaHIgR21iSDEYMBYGA1UECwwPQS1UcnVzdC1S 93 | b290LTA3MRgwFgYDVQQDDA9BLVRydXN0LVJvb3QtMDcwHhcNMTgwNTE3MTEyMzIy 94 | WhcNMzYxMTE5MTAyMzIyWjCBizELMAkGA1UEBhMCQVQxSDBGBgNVBAoMP0EtVHJ1 95 | c3QgR2VzLiBmLiBTaWNoZXJoZWl0c3N5c3RlbWUgaW0gZWxla3RyLiBEYXRlbnZl 96 | cmtlaHIgR21iSDEYMBYGA1UECwwPQS1UcnVzdC1Sb290LTA3MRgwFgYDVQQDDA9B 97 | LVRydXN0LVJvb3QtMDcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCj 98 | B75eAfol2AWPNH+REygfWrnDIkDvLEBKo1BGXWG42Tgou5ZePw3xqZR0pm+NkEpq 99 | gP0N2asqAr1wCOhJoeqb9+YTm94fKQDxqLUeGnhOosDXBaa3x2FTtaCFkbVaVD73 100 | z337Uo4ScDjMm0UM34mVp3f45WcDW5HCAdRbCnt/fVgurAvDuXc7EZbum/zpJ3hL 101 | 0yvWJL4CfKiJI3XXHiUfX2KSoSL0fskDSQiRxZnNlTLSXn+fKUgoyZFXQnP6esGx 102 | 3QLcAqdUAmLzhJMAWMdiV8zaGWj7KFC7BG1nS4SB4zDcfC9/0S56LsVio4gK01qH 103 | eOj2p1ErLXKDpA2M5MnmPaV8zsHDM4ZoNVJZUhiT/2LJalNxt7NRfoCpud9q+10d 104 | QMZXs1SBukZNgV2811crrJmnkuIqnOZa8nA2M2taIccPt8TyVwA16nQklCufBuvO 105 | rePW5el0WVCxie985VyK1VG15GFPbNMkyeTCu4corrnmm+NKaE6jllllRl0AeXBg 106 | XWhOo5ydkBmoblMGmKZ5VKcSYnahDj3Wa7dNLqq+352pVh8dR2OsvgCyxExP1l23 107 | suuJiPZyZV7h9PHtDGalKFFnfaykS6paRZxAJ0jG8DkcO4VTu/GwgvAGTwhWTKzx 108 | SWIu0RkCmESItpo1M/L3jc684zpXCZ8OffDYIRO1YQIDAQABozYwNDARBgNVHQ4E 109 | CgQIRMARrVMnh/QwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ 110 | KoZIhvcNAQELBQADggIBAJ9HSOryu7RWucc9MEDU06OVlhj0Eb+EuGQWggqm+oSV 111 | +7yc+6R8OK1NVIEWNGDX71CZ/4jSL8+kEMiHAmAZeR912uCDcXPsyYCSIYxA1enn 112 | lgJUCJq6Qm+/pXJk+er/FtaB75bOZPKP3lwVj58YY4ZepcVZp+3GCGBk/JiVegH3 113 | DT2FyjLrbSG1UNyWilpBc9VJbqClOFi1fsgZ9P0Fy/5/yNRKEY0HP0drSA/bBsT/ 114 | YyR88Ncke5Ll6JN9nPldMI7rksMwmmBh4B61xJUh9VPW05gd33mja4Btl6cy/cJK 115 | aftMnqs/NK12+7CaPYY978Cm7BcmmYSLKLTRwfOiLAGheL4atHfVmM3jcvVkIrpc 116 | iVZEjT896yaCdLv6nvEVMhZsE5egnKi9yA91pkMtcG7QbC3D3VdnWWqq/Z/z0gWb 117 | Ddqv6ecjMZHmqlm86HHO6DaZ1Nv3naJtmpLwfYZiOUqYN1nETZWv1nt10HePgamr 118 | tWwcOrBQDQPTI42GEy6G0YG73HU8x7ROl3Xsp7BEMxUhrrcTnTamTCmmmahepRQc 119 | QxIF9tbC1ypP510iF/U+N8+WIgYhMnsFU65ngQ/4TOU/yHbSbzuQx5DomsX4GN5U 120 | dtlFW5xfmmm1ngDkm4t7c066WcvkwvNh3U4kOUWG0pxbG4yE6iYiL+IWy/LW12GI 121 | -----END CERTIFICATE----- 122 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | 3 | # Example code to retrieve a Binding Certificate "Bindungs-Zertifikat" for use with the eAusweise / Digitales Amt 4 | # Usage: python3 ./demo.py 5 | # Dependencies: cryptography 6 | # 7 | # Yes, the code is terrible 8 | # I suggest you to unomment the proxies to inspect and study responses / replies with some MITM proxy (like burp) 9 | 10 | from cryptography import x509 11 | from cryptography.x509.oid import NameOID 12 | from cryptography.hazmat.primitives import hashes 13 | from cryptography.hazmat.primitives import hashes 14 | from cryptography.hazmat.primitives.asymmetric import ec 15 | from cryptography.hazmat.primitives import serialization 16 | from uuid import uuid4 17 | from base64 import b64decode,b64encode,urlsafe_b64encode 18 | import requests 19 | import json 20 | import time 21 | from os.path import isfile 22 | 23 | proxies = { 24 | 'http': 'http://127.0.0.1:8080', 25 | 'https': 'http://127.0.0.1:8080', 26 | } 27 | 28 | verify = 'burp.pem' 29 | 30 | digitalesAmtFiles = [ 31 | "digitales-amt-binding-uuid.txt", 32 | "digitales-amt-cert.pem", 33 | "digitales-amt-csr.pem", 34 | "digitales-amt-key.pem" 35 | ] 36 | 37 | if any([not isfile(f) for f in digitalesAmtFiles]): 38 | print("Need to perform initial registration!") 39 | headers = { 40 | 'Host': 'eid.oesterreich.gv.at', 41 | 'Accept': 'application/json', 42 | 'Accept-Language': 'en-US', 43 | 'X-Requested-With': 'Android Binding Library 2.0.14', 44 | 'User-Agent': 'okhttp/4.9.3', 45 | 'Connection': 'close', 46 | } 47 | 48 | params = { 49 | 'os': 'android', 50 | 'packageName': 'at.gv.oe.app', 51 | } 52 | 53 | sess = requests.Session() 54 | response = sess.get('https://eid.oesterreich.gv.at/bindingservice/params', params=params, headers=headers, proxies=proxies, verify=verify) 55 | s = response.text.split(".")[1] 56 | claims = json.loads(b64decode(s + '=' * (-len(s) % 4)).decode()) 57 | 58 | private_key = ec.generate_private_key( 59 | ec.SECP256R1() 60 | ) 61 | 62 | # Write our key to disk for safe keeping 63 | with open("digitales-amt-key.pem", "wb") as f: 64 | f.write(private_key.private_bytes( 65 | encoding=serialization.Encoding.PEM, 66 | format=serialization.PrivateFormat.TraditionalOpenSSL, 67 | encryption_algorithm=serialization.NoEncryption(), 68 | )) 69 | 70 | # Generate a CSR 71 | x509_name = x509.Name.from_rfc4514_string(claims['subject']) 72 | CN = x509_name.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value 73 | O = x509_name.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)[0].value 74 | C = x509_name.get_attributes_for_oid(NameOID.COUNTRY_NAME)[0].value 75 | 76 | csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ 77 | # The order matters! 78 | x509.NameAttribute(NameOID.COMMON_NAME, CN), 79 | x509.NameAttribute(NameOID.ORGANIZATION_NAME, O), 80 | x509.NameAttribute(NameOID.COUNTRY_NAME, C), 81 | # Sign the CSR with our private key. 82 | ])).sign(private_key, hashes.SHA256()) 83 | 84 | # Write our CSR out to disk. 85 | with open("digitales-amt-csr.pem", "wb") as f: 86 | f.write(csr.public_bytes(serialization.Encoding.PEM)) 87 | 88 | previousAppId = str(uuid4()) 89 | currentAppId = str(uuid4()) 90 | 91 | json_data = {'params': response.text, 92 | 'deviceInfo': { 93 | 'osType': 'android', 94 | 'packageName': 'at.gv.oe.app', 95 | 'osVersion': '12', 96 | 'patchLevel': '2021-12-01', 97 | 'deviceName': 'emulator64_x86_64_arm64', 98 | }, 99 | 'previousAppId': previousAppId, 100 | 'currentAppId': currentAppId, 101 | 'csr': b64encode(csr.public_bytes(serialization.Encoding.DER)).decode(), 102 | 'attestationChain': [ 103 | # Here we fool the backend, because we're re-using a completely different attestation ;) 104 | 'MIIDNTCCAtugAwIBAgIBATAKBggqhkjOPQQDAjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFTATBgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDE7MDkGA1UEAwwyQW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBJbnRlcm1lZGlhdGUwIBcNNzAwMTAxMDAwMDAwWhgPOTk5OTEyMzEyMzU5NTlaMB8xHTAbBgNVBAMMFEFuZHJvaWQgS2V5c3RvcmUgS2V5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdZOF6P17EOaOcqs7/2yq6MT3QM4hssfRdee9Mdu951uAJZtgUOL1oA/dZvlEqkdyBNgAjbp3HGwXn29VcScU+aOCAZowggGWMA4GA1UdDwEB/wQEAwIHgDCCAYIGCisGAQQB1nkCAREEggFyMIIBbgIBBAoBAAIBKQoBAARyQ049QmluZHVuZ3MtWmVydGlmaWthdC1jOTY4MDg3YzhkMTY1YWJlMTU4Njg2YzNkNGZmMTA2MyxPPVJlcHVibGlrIE9lc3RlcnJlaWNoICh2ZXJ0cmV0ZW4gZHVyY2ggQktBIHVuZCBCTURXKSxDPUFUBAAwgeehCDEGAgECAgEDogMCAQOjBAICAQClCzEJAgEAAgECAgEEqgMCAQG/g3gDAgECv4U9CAIGAYYY26Agv4U+AwIBAL+FQEwwSgQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQAKAQIEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv4VBBQIDAdTAv4VCBQIDAxWAv4VFQAQ+MDwxFjAUBAxhdC5ndi5vZS5hcHACBHiHI2UxIgQgjaw8bOWne7y3B4+hMH4EI6mY6XHwH0O2E43S52TT3/0wADAKBggqhkjOPQQDAgNIADBFAiEAt6zwukwgTLbwZwJAbev19JuUtyQTsnf+snGeZ6WdEQsCIA7jwtGEf8kOYW19oo8TiCjXqw7u3hHx8N5U4o4A87p+', 105 | 'MIICeDCCAh6gAwIBAgICEAEwCgYIKoZIzj0EAwIwgZgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQxMzAxBgNVBAMMKkFuZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gUm9vdDAeFw0xNjAxMTEwMDQ2MDlaFw0yNjAxMDgwMDQ2MDlaMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTswOQYDVQQDDDJBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIEludGVybWVkaWF0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOueefhCY1msyyqRTImGzHCtkGaTgqlzJhP+rMv4ISdMIXSXSir+pblNf2bU4GUQZjW8U7ego6ZxWD7bPhGuEBSjZjBkMB0GA1UdDgQWBBQ//KzWGrE6noEguNUlHMVlux6RqTAfBgNVHSMEGDAWgBTIrel3TEXDo88NFhDkeUM6IVowzzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIChDAKBggqhkjOPQQDAgNIADBFAiBLipt77oK8wDOHri/AiZi03cONqycqRZ9pDMfDktQPjgIhAO7aAV229DLp1IQ7YkyUBO86fMy9Xvsiu+f+uXc/WT/7', 106 | 'MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYDVQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3QwHhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTATBgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwqQW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0OBBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0WEOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqGSM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBNC/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw==', 107 | ], 108 | } 109 | 110 | 111 | bindingUuid = claims['postUrl'][len('binding/'):] 112 | 113 | headers = { 114 | 'Host': 'eid.oesterreich.gv.at', 115 | 'Accept': 'application/json', 116 | 'Accept-Language': 'en-US', 117 | 'X-Requested-With': 'Android Binding Library 2.0.14', 118 | 'Content-Type': 'application/json; charset=utf-8', 119 | 'User-Agent': 'okhttp/4.9.3', 120 | 'Connection': 'close', 121 | } 122 | 123 | url = 'https://eid.oesterreich.gv.at/bindingservice/' + claims['postUrl'] 124 | 125 | response = sess.post(url, 126 | headers=headers, 127 | json=json_data, 128 | verify=verify, 129 | proxies=proxies, 130 | allow_redirects=False, 131 | ) 132 | 133 | headers = { 134 | 'Host': 'eid.oesterreich.gv.at', 135 | 'Accept': 'application/json', 136 | 'Sl2clienttype': 'nativeApp', 137 | 'X-Moa-Vda': '0', 138 | 'Accept-Language': 'de', 139 | 'User-Agent': 'okhttp/4.9.3', 140 | 'Connection': 'close', 141 | } 142 | 143 | redirect = response.headers['location'] 144 | response = sess.get(redirect, 145 | headers=headers, 146 | verify=verify, 147 | proxies=proxies, 148 | allow_redirects=False, 149 | ) 150 | 151 | resp = json.loads(response.text) 152 | pendingid = resp['params']['pendingid'] 153 | pendingReqID = resp['params']['pendingReqID'] 154 | 155 | headers = { 156 | 'Host': 'eid.oesterreich.gv.at', 157 | 'Accept': 'application/json', 158 | 'Sl2clienttype': 'nativeApp', 159 | 'X-Moa-Vda': '0', 160 | 'Accept-Language': 'de', 161 | 'User-Agent': 'okhttp/4.9.3', 162 | 'Connection': 'close', 163 | } 164 | 165 | data = { 166 | 'pendingid': pendingid, 167 | 'pendingReqID': pendingReqID, 168 | 'useeIDAS': 'true', 169 | } 170 | 171 | response = sess.post( 172 | 'https://eid.oesterreich.gv.at/authHandler/public/secure/process', 173 | headers=headers, 174 | data=data, 175 | verify=verify, 176 | proxies=proxies, 177 | allow_redirects=False, 178 | ) 179 | 180 | resp = json.loads(response.text) 181 | pendingid = resp['params']['pendingid'] 182 | pendingReqID = resp['params']['pendingReqID'] 183 | 184 | 185 | print("Open https://oesterreich.gv.at/eu-login in a browser") 186 | print("And enter QR Code contents:") 187 | eidasWebSynch = input() 188 | print("In the browser, performing login using eIDAS") 189 | 190 | headers = { 191 | 'Host': 'eid.oesterreich.gv.at', 192 | 'Accept': 'application/json', 193 | 'Sl2clienttype': 'nativeApp', 194 | 'X-Moa-Vda': '0', 195 | 'Accept-Language': 'de', 196 | 'User-Agent': 'okhttp/4.9.3', 197 | 'Connection': 'close', 198 | } 199 | 200 | data = { 201 | 'pendingid': pendingid, 202 | 'qrCodeResult': eidasWebSynch, 203 | } 204 | 205 | response = sess.post( 206 | 'https://eid.oesterreich.gv.at/authHandler//public/secure/process', 207 | headers=headers, 208 | data=data, 209 | verify=verify, 210 | proxies=proxies, 211 | allow_redirects=False, 212 | ) 213 | 214 | resp = json.loads(response.text) 215 | pendingid = resp['params']['pendingid'] 216 | 217 | finished = False 218 | 219 | while not finished: 220 | headers = { 221 | 'Host': 'eid.oesterreich.gv.at', 222 | 'X-Binding-Wait_auth': pendingid, 223 | 'Accept': 'application/json', 224 | 'Sl2clienttype': 'nativeApp', 225 | 'X-Moa-Vda': '0', 226 | 'Accept-Language': 'de', 227 | 'User-Agent': 'okhttp/4.9.3', 228 | 'Connection': 'close', 229 | } 230 | 231 | response = sess.get( 232 | 'https://eid.oesterreich.gv.at/authHandler//public/secure/binding/waiting/authfinished', 233 | headers=headers, 234 | verify=verify, 235 | proxies=proxies, 236 | allow_redirects=False, 237 | ) 238 | 239 | finished = response.status_code != 503 240 | time.sleep(1) 241 | 242 | l = response.headers['location'] 243 | 244 | headers = { 245 | 'Host': 'eid.oesterreich.gv.at', 246 | 'Accept': 'application/json', 247 | 'Sl2clienttype': 'nativeApp', 248 | 'X-Moa-Vda': '0', 249 | 'Accept-Language': 'de', 250 | 'User-Agent': 'okhttp/4.9.3', 251 | 'Connection': 'close', 252 | } 253 | 254 | response = sess.get( 255 | l, 256 | headers=headers, 257 | verify=verify, 258 | proxies=proxies, 259 | allow_redirects=False, 260 | ) 261 | 262 | l = response.headers['location'] 263 | 264 | headers = { 265 | 'Host': 'eid.oesterreich.gv.at', 266 | 'Accept': 'application/json', 267 | 'Accept-Language': 'en-US', 268 | 'X-Requested-With': 'Android Binding Library 2.0.14', 269 | 'Content-Type': 'application/json; charset=utf-8', 270 | 'User-Agent': 'okhttp/4.9.3', 271 | 'Connection': 'close', 272 | } 273 | 274 | response = sess.post( 275 | l + "/" + bindingUuid + "/" + currentAppId, 276 | headers=headers, 277 | verify=verify, 278 | proxies=proxies, 279 | allow_redirects=False, 280 | ) 281 | 282 | d = json.loads(response.text) 283 | cert = x509.load_der_x509_certificate(b64decode(d['certificate'])) 284 | 285 | with open("digitales-amt-cert.pem", "wb") as f: 286 | f.write(cert.public_bytes(serialization.Encoding.PEM)) 287 | 288 | with open("digitales-amt-binding-uuid.txt", "w") as f: 289 | f.write(bindingUuid) 290 | 291 | print("key => digitales-amt-key.pem") 292 | print("crt => digitales-amt-cert.pem") 293 | print("bindingUuid => digitales-amt-binding-uuid.txt") 294 | print("Digitales Amt Registration Done") 295 | 296 | eAusweise = requests.Session() 297 | bindingUuid = open('digitales-amt-binding-uuid.txt').read() 298 | cert = x509.load_pem_x509_certificate(open('digitales-amt-cert.pem', 'rb').read()) 299 | key = serialization.load_pem_private_key(open('digitales-amt-key.pem', 'rb').read(), password=None) 300 | 301 | headers = { 302 | 'Host': 'identity.awp.oesterreich.gv.at', 303 | 'User-Agent': 'eAusweise/1.1.0+304911 (Android 12; sdk_gphone64_x86_64)', 304 | 'Connection': 'close', 305 | } 306 | 307 | response = eAusweise.get('https://identity.awp.oesterreich.gv.at/login', headers=headers, verify=verify, proxies=proxies, allow_redirects=False) 308 | l = response.headers['location'] 309 | 310 | headers = { 311 | 'Host': 'eid.oesterreich.gv.at', 312 | 'Accept': 'application/json', 313 | 'Sl2clienttype': 'nativeApp', 314 | 'X-Binding-Token': bindingUuid, 315 | 'X-Moa-Vda': '0', 316 | 'Accept-Language': 'de', 317 | 'User-Agent': 'okhttp/4.9.3', 318 | 'Connection': 'close', 319 | } 320 | 321 | response = eAusweise.get( 322 | l, 323 | headers=headers, 324 | verify=verify, 325 | proxies=proxies, 326 | allow_redirects=True, 327 | ) 328 | resp = json.loads(response.text) 329 | 330 | pendingid = resp['params']['pendingid'] 331 | pendingReqID = resp['params']['pendingReqID'] 332 | 333 | # fetch challenge 334 | headers = { 335 | 'Host': 'eid.oesterreich.gv.at', 336 | 'Accept': 'application/json', 337 | 'Sl2clienttype': 'nativeApp', 338 | 'X-Binding-Token': 'f0be51ae-5b7f-4e40-8a97-8724fb3c789a', 339 | 'X-Moa-Vda': '0', 340 | 'Accept-Language': 'de', 341 | 'User-Agent': 'okhttp/4.9.3', 342 | 'Connection': 'close', 343 | } 344 | 345 | data = { 346 | 'pendingid': pendingid, 347 | 'useBindingAuth': 'true', 348 | 'pendingReqID': pendingReqID, 349 | 'storeConsent': 'true', 350 | } 351 | 352 | response = eAusweise.post( 353 | 'https://eid.oesterreich.gv.at/authHandler/public/secure/process', 354 | headers=headers, 355 | data=data, 356 | verify=verify, 357 | proxies=proxies, 358 | allow_redirects=True, 359 | ) 360 | 361 | resp = json.loads(response.text) 362 | challenge = resp['challenge']['challenge'] 363 | issuedAt = resp['challenge']['issuedAt'] 364 | pendingid = resp['params']['pendingid'] 365 | pendingReqID = resp['params']['pendingReqID'] 366 | response.close() 367 | 368 | head = json.dumps({ 369 | "x5c": [ b64encode(cert.public_bytes(serialization.Encoding.DER)).decode() ], 370 | "typ": "bindingAuth", 371 | "alg": "ES256", 372 | }, separators=(",",":")).encode() 373 | 374 | pay = json.dumps({ 375 | "challenge": challenge, 376 | "issuedAt": issuedAt, 377 | }, separators=(",",":")).encode() 378 | 379 | head_b64 = urlsafe_b64encode(head).replace(b'=', b'') 380 | pay_b64 = urlsafe_b64encode(pay).replace(b'=', b'') 381 | sig = key.sign(head_b64 + b'.' + pay_b64, ec.ECDSA(hashes.SHA256())) 382 | from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature 383 | r,s = decode_dss_signature(sig) 384 | sig1 = r.to_bytes(32, 'big') + s.to_bytes(32, 'big') 385 | jwt = head_b64 + b'.' + pay_b64 + b'.' + urlsafe_b64encode(sig1).replace(b'=', b'') 386 | 387 | headers = { 388 | 'Host': 'eid.oesterreich.gv.at', 389 | 'Accept': 'application/json', 390 | 'Sl2clienttype': 'nativeApp', 391 | 'X-Binding-Token': bindingUuid, 392 | 'X-Moa-Vda': '0', 393 | 'Accept-Language': 'de', 394 | 'User-Agent': 'okhttp/4.9.3', 395 | 'Connection': 'close', 396 | } 397 | 398 | 399 | data = { 400 | 'challengeResponse': jwt.decode(), 401 | 'pendingid': pendingid, 402 | 'pendingReqID': pendingReqID, 403 | } 404 | 405 | response = eAusweise.post( 406 | 'https://eid.oesterreich.gv.at/authHandler/public/secure/process', 407 | headers=headers, 408 | data=data, 409 | verify=verify, 410 | proxies=proxies, 411 | allow_redirects=False, 412 | ) 413 | 414 | l = response.headers['location'] 415 | response.close() 416 | 417 | # Yes, this code is horrible, I warned you! 418 | response = eAusweise.get( 419 | l, 420 | headers=headers, 421 | verify=verify, 422 | proxies=proxies, 423 | allow_redirects=False, 424 | ) 425 | 426 | l = response.headers['location'] 427 | response.close() 428 | 429 | response = eAusweise.get( 430 | l, 431 | headers=headers, 432 | verify=verify, 433 | proxies=proxies, 434 | allow_redirects=False, 435 | ) 436 | 437 | l = response.headers['location'] 438 | response.close() 439 | 440 | response = eAusweise.get( 441 | "https://eid.oesterreich.gv.at" + l, 442 | headers=headers, 443 | verify=verify, 444 | proxies=proxies, 445 | allow_redirects=False, 446 | ) 447 | response.close() 448 | 449 | headers = { 450 | 'Host': 'identity.awp.oesterreich.gv.at', 451 | 'User-Agent': 'eAusweise/1.1.0+304911 (Android 12; sdk_gphone64_x86_64)', 452 | 'Connection': 'close', 453 | } 454 | l = response.headers['location'] 455 | s = requests.Session() 456 | response = eAusweise.get( 457 | l, 458 | headers=headers, 459 | allow_redirects=False, 460 | ) 461 | 462 | resp = json.loads(response.text) 463 | registrationToken = resp['registrationToken'] 464 | 465 | headers = { 466 | 'Host': 'backend.awp.oesterreich.gv.at', 467 | 'User-Agent': 'okhttp/4.10.0', 468 | 'Connection': 'close', 469 | } 470 | 471 | url = "https://backend.awp.oesterreich.gv.at/backend/app/api/v11/registrations/" + registrationToken 472 | 473 | response = requests.get(url, 474 | headers=headers, 475 | allow_redirects=False, 476 | cert=("anonymous-prod-cert.pem", "anonymous-prod-key.pem"), 477 | ) 478 | 479 | resp = json.loads(response.text) 480 | assert(resp['registered'] == True) 481 | 482 | private_key = ec.generate_private_key( 483 | ec.SECP256R1() 484 | ) 485 | 486 | # Write our key to disk for safe keeping 487 | with open("eAusweise-key.pem", "wb") as f: 488 | f.write(private_key.private_bytes( 489 | encoding=serialization.Encoding.PEM, 490 | format=serialization.PrivateFormat.TraditionalOpenSSL, 491 | encryption_algorithm=serialization.NoEncryption(), 492 | )) 493 | 494 | # Generate a CSR 495 | csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ 496 | x509.NameAttribute(NameOID.COMMON_NAME, "fuck-the.gov"), 497 | # Sign the CSR with our private key. 498 | ])).sign(private_key, hashes.SHA256()) 499 | 500 | # Write our CSR out to disk. 501 | with open("eAusweise-csr.pem", "wb") as f: 502 | f.write(csr.public_bytes(serialization.Encoding.PEM)) 503 | 504 | json_data = { 505 | 'csr': csr.public_bytes(serialization.Encoding.PEM).decode(), 506 | 'device': { 507 | 'manufacturer': 'Google', 508 | 'name': 'sdk_gphone64_x86_64', 509 | 'pushMessageType': 'gcm', 510 | 'pushMessageToken': '', 511 | }, 512 | 'user': {}, 513 | 'password': None, 514 | } 515 | 516 | response = requests.post( 517 | url, 518 | headers=headers, 519 | json=json_data, 520 | cert=("anonymous-prod-cert.pem", "anonymous-prod-key.pem"), 521 | ) 522 | 523 | resp = json.loads(response.text) 524 | 525 | with open("eAusweise-cert.pem", "w") as f: 526 | f.write(resp['cert']) 527 | 528 | print("Done eAusweise Login") 529 | print("Usage example: curl --key eAusweise-key.pem --cert eAusweise-cert.pem https://backend.awp.oesterreich.gv.at/backend/app/api/v11/users/status") 530 | --------------------------------------------------------------------------------