├── LICENSE ├── NOTICE ├── README.md ├── cmd └── ja4x │ └── main.go ├── example_com.pem ├── go.mod └── ja4x.go /LICENSE: -------------------------------------------------------------------------------- 1 | Please also see the NOTICE file for additional license conditions relating to JA4X. 2 | 3 | --- 4 | 5 | Copyright 2023 driftnet.io 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | JA4X itself is covered by the FoxIO License. A copy is reproduced below. 2 | 3 | See https://github.com/FoxIO-LLC/ja4 for more details. 4 | 5 | --- 6 | 7 | FoxIO License 1.1 8 | Licensor: FoxIO, LLC 9 | Software: JA4+ (JA4S, JA4H, JA4L, JA4X, JA4SSH) 10 | 11 | This license was created by FoxIO, LLC. You may use the text of this license for your own 12 | software as long as you change the name of the license, and change the licensor and software 13 | above to refer to you and your software. You may state that your license is based on the FoxIO 14 | License 1.0, as long as you clearly identify any other changes you make to the license. 15 | 16 | 1. Acceptance 17 | In order to get any license under these terms, you must agree to them as both strict obligations 18 | and conditions to all your licenses. 19 | 20 | 2. Copyright License 21 | The licensor grants you a copyright license to use and modify the software, only for non-commercial 22 | purposes. The licensor grants you a copyright license to distribute the software to others 23 | only for non-commercial purposes. “Non-commercial purposes” include personal use by an individual, 24 | academic research and development, and testing and evaluation of the software for your own 25 | internal use, and excludes any use for which you charge fees or anything else of value, 26 | directly or indirectly, for use of or access to the software. Using the software for your own 27 | internal business purposes in a manner where you do not directly monetize the software is a 28 | non-commercial purpose. Providing the software on a hosted or managed service basis to others 29 | is not a non-commercial purpose. Providing maintenance, support or development services for 30 | the software to others, or using the software to enable others to provide such services for 31 | the software to you, is not a non-commercial purpose. 32 | 33 | You must ensure that anyone who gets a copy of any part of the software from you also gets a 34 | copy of these license terms or the following URL https://github.com/FoxIO-LLC/ja4/blob/main/LICENSE, 35 | and you must retain all copyright, patent or other intellectual property notices placed on 36 | the software by licensor. 37 | 38 | 3. Patent License 39 | The licensor grants you a patent license for the software that covers patent claims the 40 | licensor can license, or becomes able to license, that you would necessarily infringe by 41 | using the software in the manner allowed under this license for non-commercial purposes. 42 | This license does not grant you any right to practice any patent rights for any invention 43 | not fully embodied in the software in the form provided by the licensor. 44 | 45 | 4. No Other Rights 46 | These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or 47 | prevent the licensor from granting licenses to anyone else. These terms do not imply any other 48 | licenses. 49 | 50 | 5. Patent Defense 51 | If you make any written claim that the software infringes or contributes to infringement of any 52 | patent, your patent license for the software granted under these terms ends immediately. If your 53 | company makes such a claim, your patent license ends immediately for work on behalf of your company. 54 | 55 | 6. Violations 56 | The first time you are notified in writing that you have violated any of these terms, or done 57 | anything with the software not covered by your licenses, your licenses can nonetheless continue 58 | if you come into full compliance with these terms, take practical steps to correct past violations, 59 | and provide a written statement that all such past violations have been corrected within 30 days 60 | after receiving notice. Otherwise, all your licenses end immediately. 61 | 62 | 7. Duration 63 | Your licenses for a particular version of the software will continue until the end of life of 64 | that version of the software, or earlier as described in the Violations section above. 65 | 66 | 8. No Liability 67 | As far as the law allows, the software comes as is, without any warranty or condition, and the 68 | licensor will not be liable to you for any damages arising out of these terms or the use or nature 69 | of the software, under any kind of legal claim. 70 | 71 | 9. Definitions 72 | The “Licensor” is the individual or entity offering these terms, and the “Software” is the 73 | software the licensor makes available under these terms. 74 | 75 | “You” refers to the individual or entity agreeing to these terms. 76 | 77 | “Your company” is any legal entity, sole proprietorship, or other kind of organization that you 78 | work for, plus all organizations that have control over, are under the control of, or are under 79 | common control with that organization. “Control” means ownership of substantially all the assets 80 | of an entity, or the power to direct its management and policies by vote, contract, or otherwise. 81 | Control can be direct or indirect. 82 | 83 | “Your licenses” are all the licenses granted to you for the software under these terms. 84 | 85 | “Use” means anything you do with the software requiring one of your licenses. 86 | 87 | “End of Life” for a version of the software is a date publicly announced by the licensor on which 88 | the licensor intends to cease maintenance of that version of the software. 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JA4X for Go 2 | 3 | An implementation of the [JA4X hash algorithm](https://github.com/FoxIO-LLC/ja4) in Go. 4 | 5 | # Command-line usage 6 | 7 | Compile: 8 | 9 | ``` 10 | go build -o ja4x cmd/ja4x/main.go 11 | ``` 12 | 13 | Extract JA4X for a certificate: 14 | 15 | ``` 16 | ./ja4x example_com.pem 17 | ja4x: a373a9f83c6b_2bab15409345_7bf9a7bf7029 18 | ``` 19 | 20 | Extract JA4X for a certificate, including raw version: 21 | 22 | ``` 23 | ./ja4x -r example_com.pem 24 | ja4x: a373a9f83c6b_2bab15409345_7bf9a7bf7029 25 | ja4x_r: 550406,55040a,550403_550406,550408,550407,55040a,550403_551d23,551d0e,551d11,551d0f,551d25,551d1f,551d20,2b06010505070101,551d13,2b06010401d679020402 26 | ``` 27 | 28 | Certificates can be supplied raw, or PEM encoded. 29 | 30 | # Usage as a library 31 | 32 | Add to your project: 33 | 34 | ``` 35 | go get github.com/driftnet-io/go-ja4x 36 | ``` 37 | 38 | Then, assuming `cert` is an `*x509.Certificate`, 39 | 40 | ``` 41 | ja4xHash := ja4x.JA4X(cert) 42 | ``` 43 | 44 | or with the raw version, 45 | 46 | ``` 47 | ja4xHash, ja4xRawHash := ja4x.JA4XWithRaw(cert) 48 | ``` 49 | 50 | # Licensing 51 | 52 | The code in this repository is MIT licensed. However, JA4X itself is subject to additional restricitons. Please see the NOTICE file for further details. 53 | -------------------------------------------------------------------------------- /cmd/ja4x/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | 11 | "github.com/driftnet-io/go-ja4x" 12 | ) 13 | 14 | func main() { 15 | 16 | raw := flag.Bool("r", false, "Output raw JA4X hash") 17 | flag.Parse() 18 | 19 | args := flag.Args() 20 | if len(args) != 1 { 21 | log.Fatal("usage: ja4x-hash [-r] x509-cert-file") 22 | } 23 | 24 | certBytes, err := os.ReadFile(args[0]) 25 | if err != nil { 26 | log.Fatal("error reading cert file", err) 27 | } 28 | 29 | // Is this PEM? Otherwise treat as a raw certificate. 30 | block, _ := pem.Decode(certBytes) 31 | if block != nil { 32 | certBytes = block.Bytes 33 | } 34 | 35 | cert, err := x509.ParseCertificate(certBytes) 36 | if err != nil { 37 | log.Fatal("failed to parse certificate", err) 38 | } 39 | 40 | if *raw { 41 | hash, raw := ja4x.JA4XWithRaw(cert) 42 | fmt.Println("ja4x:", hash) 43 | fmt.Println("ja4x_r:", raw) 44 | 45 | } else { 46 | fmt.Println("ja4x:", ja4x.JA4X(cert)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example_com.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIHSjCCBjKgAwIBAgIQDB/LGEUYx+OGZ0EjbWtz8TANBgkqhkiG9w0BAQsFADBP 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSkwJwYDVQQDEyBE 4 | aWdpQ2VydCBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTAeFw0yMzAxMTMwMDAwMDBa 5 | Fw0yNDAyMTMyMzU5NTlaMIGWMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv 6 | cm5pYTEUMBIGA1UEBxMLTG9zIEFuZ2VsZXMxQjBABgNVBAoMOUludGVybmV0wqBD 7 | b3Jwb3JhdGlvbsKgZm9ywqBBc3NpZ25lZMKgTmFtZXPCoGFuZMKgTnVtYmVyczEY 8 | MBYGA1UEAxMPd3d3LmV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 9 | MIIBCgKCAQEAwoB3iVm4RW+6StkR+nutx1fQevu2+t0Fu6KBcbvhfyHSXy7w0nJO 10 | dTT4jWLjStpRkNQBPZwMwHH35i+21gdnJtDe/xfO8IX9McFmyodlBUcqX8CruIzD 11 | v9AXf2OjXPBG+4aq+03XKl5/muATl32++301Vw1dXoGYNeoWQqLTsHT3WS3tOOf+ 12 | ehuzNuZ+rj+ephaD3lMBToEArrtC9R91KTTN6YSAOK48NxTA8CfOMFK5itxfIqB5 13 | +E9OSQTidXyqLyoeA+xxTKMqYfxvypEek1oueAhY9u67NCBdmuavxtfyvwp7+o6S 14 | d+NsewxAhmRKFexw13KOYzDhC+9aMJcuJQIDAQABo4ID2DCCA9QwHwYDVR0jBBgw 15 | FoAUt2ui6qiqhIx56rTaD5iyxZV2ufQwHQYDVR0OBBYEFLCTP+gXgv1ssrYXh8vj 16 | gP6CmwGeMIGBBgNVHREEejB4gg93d3cuZXhhbXBsZS5vcmeCC2V4YW1wbGUubmV0 17 | ggtleGFtcGxlLmVkdYILZXhhbXBsZS5jb22CC2V4YW1wbGUub3Jngg93d3cuZXhh 18 | bXBsZS5jb22CD3d3dy5leGFtcGxlLmVkdYIPd3d3LmV4YW1wbGUubmV0MA4GA1Ud 19 | DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwgY8GA1Ud 20 | HwSBhzCBhDBAoD6gPIY6aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 21 | VExTUlNBU0hBMjU2MjAyMENBMS00LmNybDBAoD6gPIY6aHR0cDovL2NybDQuZGln 22 | aWNlcnQuY29tL0RpZ2lDZXJ0VExTUlNBU0hBMjU2MjAyMENBMS00LmNybDA+BgNV 23 | HSAENzA1MDMGBmeBDAECAjApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2lj 24 | ZXJ0LmNvbS9DUFMwfwYIKwYBBQUHAQEEczBxMCQGCCsGAQUFBzABhhhodHRwOi8v 25 | b2NzcC5kaWdpY2VydC5jb20wSQYIKwYBBQUHMAKGPWh0dHA6Ly9jYWNlcnRzLmRp 26 | Z2ljZXJ0LmNvbS9EaWdpQ2VydFRMU1JTQVNIQTI1NjIwMjBDQTEtMS5jcnQwCQYD 27 | VR0TBAIwADCCAX8GCisGAQQB1nkCBAIEggFvBIIBawFpAHYA7s3QZNXbGs7FXLed 28 | tM0TojKHRny87N7DUUhZRnEftZsAAAGFq0gFIwAABAMARzBFAiEAqt+fK6jFdGA6 29 | tv0EWt9rax0WYBV4re9jgZgq0zi42QUCIEBh1yKpPvgX1BreE0wBUmriOVUhJS77 30 | KgF193fT2877AHcAc9meiRtMlnigIH1HneayxhzQUV5xGSqMa4AQesF3crUAAAGF 31 | q0gFnwAABAMASDBGAiEA12SUFK5rgLqRzvgcr7ZzV4nl+Zt9lloAzRLfPc7vSPAC 32 | IQCXPbwScx1rE+BjFawZlVjLj/1PsM0KQQcsfHDZJUTLwAB2AEiw42vapkc0D+Vq 33 | AvqdMOscUgHLVt0sgdm7v6s52IRzAAABhatIBV4AAAQDAEcwRQIhAN5bhHthoyWM 34 | J3CQB/1iYFEhMgUVkFhHDM/nlE9ThCwhAiAPvPJXyp7a2kzwJX3P7fqH5Xko3rPh 35 | CzRoXYd6W+QkCjANBgkqhkiG9w0BAQsFAAOCAQEAWeRK2KmCuppK8WMMbXYmdbM8 36 | dL7F9z2nkZL4zwYtWBDt87jW/Gz/E5YyzU/phySFC3SiwvYP9afYfXaKrunJWCtu 37 | AG+5zSTuxELFTBaFnTRhOSO/xo6VyYSpsuVBD0R415W5z9l0v1hP5xb/fEAwxGxO 38 | Ik3Lg2c6k78rxcWcGvJDoSU7hPb3U26oha7eFHSRMAYN8gfUxAi6Q2TF4j/arMVB 39 | r6Q36EJ2dPcTu0p9NlmBm8dE34lzuTNC6GDCTWFdEloQ9u//M4kUUOjWn8a5XCs1 40 | 263t3Ta2JfKViqxpP5r+GvgVKG3qGFrC0mIYr0B4tfpeCY9T+cz4I6GDMSP0xg== 41 | -----END CERTIFICATE----- 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/driftnet-io/go-ja4x 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.0 6 | -------------------------------------------------------------------------------- /ja4x.go: -------------------------------------------------------------------------------- 1 | package ja4x 2 | 3 | import ( 4 | "crypto/sha256" 5 | "crypto/x509" 6 | "crypto/x509/pkix" 7 | "encoding/asn1" 8 | "encoding/hex" 9 | "strings" 10 | ) 11 | 12 | // JA4X produces a JA4X hash for a parsed X509 certificate. 13 | func JA4X(cert *x509.Certificate) string { 14 | rawComps := components(cert) 15 | 16 | var hashComps []string 17 | for _, c := range rawComps { 18 | hashComps = append(hashComps, hash12(c)) 19 | } 20 | 21 | return strings.Join(hashComps, "_") 22 | } 23 | 24 | // JA4XWithRaw produces a JA4X hash for a parsed X509 certificate. 25 | // It also returns the hash in a raw form. 26 | func JA4XWithRaw(cert *x509.Certificate) (string, string) { 27 | rawComps := components(cert) 28 | 29 | var hashComps []string 30 | for _, c := range rawComps { 31 | hashComps = append(hashComps, hash12(c)) 32 | } 33 | 34 | return strings.Join(hashComps, "_"), strings.Join(rawComps, "_") 35 | } 36 | 37 | // components produces raw JA4X hash components from a parsed X509 certificate 38 | func components(cert *x509.Certificate) []string { 39 | var comps []string 40 | 41 | // We can't use Go's RDN functions for issuer and subject because 42 | // (a) they reorder RDNs and (b) they skip certain empty values. 43 | // See function ToRDNSequence() in package crypto/x509/pkix for details. 44 | // Instead we need to parse the raw sequences ourselves. 45 | for _, raw := range [][]byte{cert.RawIssuer, cert.RawSubject} { 46 | var rdnSeq pkix.RDNSequence 47 | var rdnHex []string 48 | 49 | if _, err := asn1.Unmarshal(raw, &rdnSeq); err == nil { 50 | for _, rdnSet := range rdnSeq { 51 | for _, rdn := range rdnSet { 52 | // The JA4X specification uses raw bytes from the ASN.1 body. 53 | if b := getASN1Body(rdn.Type); b != nil { 54 | rdnHex = append(rdnHex, hex.EncodeToString(b)) 55 | } 56 | } 57 | } 58 | } 59 | 60 | comps = append(comps, strings.Join(rdnHex, ",")) 61 | } 62 | 63 | // Go stores the extensions in a suitable raw format. 64 | var extHex []string 65 | for _, ext := range cert.Extensions { 66 | if b := getASN1Body(ext.Id); b != nil { 67 | extHex = append(extHex, hex.EncodeToString(b)) 68 | } 69 | } 70 | 71 | comps = append(comps, strings.Join(extHex, ",")) 72 | 73 | return comps 74 | } 75 | 76 | // getASN1Body returns the body of a marshalled ASN.1 sequence, ignoring errors 77 | func getASN1Body(oid asn1.ObjectIdentifier) []byte { 78 | 79 | var rawValue asn1.RawValue 80 | m, _ := asn1.Marshal(oid) 81 | asn1.Unmarshal(m, &rawValue) 82 | 83 | return rawValue.Bytes 84 | } 85 | 86 | // hash12 is the first 12 characters of the SHA256 of a string, 87 | // with the exception that empty strings hash to all zeros instead of e3b0c44298fc. 88 | func hash12(s string) string { 89 | 90 | if s == "" { 91 | return "000000000000" 92 | } 93 | 94 | hash := sha256.Sum256([]byte(s)) 95 | return hex.EncodeToString(hash[:])[:12] 96 | } 97 | --------------------------------------------------------------------------------