2016. szeptember 30., péntek

Üzenet hitelesítése Java és Go szervizek között

Java fejlesztés mellett időm egy részét Go programozással töltöm, és egy olyan feladaton volt szerencsém dolgozni, amely mindkét platformot érintette. Napjaink modern alkalmazásai kisebb szervízekre vannak bontva, és igen gyakori, hogy az egyes szervízek eltérő technológiával kerülnek implementálásra. Konkrét esetben az volt az elvárás, hogy a szervízek közti kommunikációt aláírással hitelesítsem, a küldő fél Javaban, míg a fogadó Goban írodott. Mivel nem valami egzotikus kérést kellett megvalósítani gondoltam másoknak is hasznos lehet a megoldás. Előljáróban még annyit kell tudni a rendszer architektúrájáról, hogy a Java kód indít virtuális gépeket, és az ezeken a gépeken futó Go szolgáltatáson keresztül végez beállítási műveleteket, ráadásul mindkét komponens nyílt forráskódú. Ezen két adottságból adódóan nem volt mód sem szimetrikus titkosítást használni, vagy egyéb más érzékeny adatot eljuttatni a futó virtuális gépre, sem pedig valami közös "trükköt" alkalmazni. Maradt az aszinkron kulcspárral történő aláírás, mi az RSA-t választottuk. Nem is szaporítanám a szót, ugorjunk fejest a kódba.

Kezdjük a fogadó féllel. A Go nyelv dokumentációját olvasva hamar ráakadhatunk, hogy létezik beépített crypto/rsa csomag. Nem bővelkedik a lehetőségekben, ugyanis csak PKCS#1-et támogat. Remélem nem spoiler, de a Go lesz a szűk keresztmetszet választható sztenderdek közül. Létezik persze külső csomag pl. PKCS#8 támogatással, de mi a biztonsági kockázatát kisebbnek ítéltük a beépített bár gyengébb eljárásnak, mint a külső kevesek által auditált megoldásnak. A crypto/rsa csomagnál maradva az egyetlen lehetőségünk, hogy PSS (Probabilistic signature scheme) aláírásokat hitelesítsünk a VerifyPSS metódussal. Szóval nincs más dolgunk mint az RSA kulcspár publikus részét eljuttatni a virtuális gépre, és már mehet is a hitelesítés.

// rawSign - base64 encoded representation of the signiture
// pubPem - public key in PEM format
// data - the signed data
func CheckSignature(rawSign string, pubPem []byte, data []byte) bool {
var err error
var sign []byte
var pub interface{}
sign, err = base64.StdEncoding.DecodeString(rawSign)
if err == nil {
block, _ := pem.Decode(pubPem)
if block != nil {
pub, err = x509.ParsePKIXPublicKey(block.Bytes)
if err == nil {
newHash := crypto.SHA256.New()
newHash.Write(data)
opts := rsa.PSSOptions{SaltLength: 20} // Java default salt length
err = rsa.VerifyPSS(pub.(*rsa.PublicKey), crypto.SHA256, newHash.Sum(nil), sign, &opts)
}
}
}
return err == nil
}
view raw check-sign.go hosted with ❤ by GitHub

Küldés során a kérés teljes törzsét írtuk alá, így nincs más dolgunk mint a kérésből kibányászni a törzset és ellenőrizni a hitelességét.
func Wrap(handler func(w http.ResponseWriter, req *http.Request), signatureKey []byte) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body := new(bytes.Buffer)
defer r.Body.Close()
ioutil.ReadAll(io.TeeReader(r.Body, body))
r.Body = ioutil.NopCloser(body) // We read the body twice, we have to wrap original ReadCloser
signature := strings.TrimSpace(r.Header.Get("signature"))
if err := CheckSignature(signature, signatureKey, body.Bytes()); err != nil {
// Error handling
w.WriteHeader(http.StatusNotAcceptable)
w.Write([]byte("406 Not Acceptable"))
return
}
http.HandlerFunc(handler).ServeHTTP(w, r)
})
}
view raw wrapper.go hosted with ❤ by GitHub

Valamint implementálni és regisztrálni a kérés feldolgozót.
func PostItHandler(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("ok"))
}
func RegisterHandler() {
signature, _ := ioutil.ReadFile("/path/of/public/key")
r := mux.NewRouter()
r.Handle("/postit", Wrap(PostItHandler, signature)).Methods("POST")
http.Handle("/", r)
http.ListenAndServe("8080", nil)
}

Természetesen tesztet is írtam az aláírás ellenőrzésére.
type TestWriter struct {
header http.Header
status int
message string
}
func (w *TestWriter) Header() http.Header {
return w.header
}
func (w *TestWriter) Write(b []byte) (int, error) {
w.message = string(b)
return len(b), nil
}
func (w *TestWriter) WriteHeader(s int) {
w.status = s
}
func TestWrapAllValid(t *testing.T) {
pk, _ := rsa.GenerateKey(rand.Reader, 1024)
pubDer, _ := x509.MarshalPKIXPublicKey(&pk.PublicKey)
pubPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Headers: nil, Bytes: pubDer})
content := "body"
newHash := crypto.SHA256.New()
newHash.Write([]byte(content))
opts := rsa.PSSOptions{SaltLength: 20}
sign, _ := rsa.SignPSS(rand.Reader, pk, crypto.SHA256, newHash.Sum(nil), &opts)
body := bytes.NewBufferString(content)
req, _ := http.NewRequest("GET", "http://valami", body)
req.Header.Add("signature", base64.StdEncoding.EncodeToString(sign))
writer := new(TestWriter)
writer.header = req.Header
handler := Wrap(func(w http.ResponseWriter, req *http.Request) {}, pubPem)
handler.ServeHTTP(writer, req)
if writer.status != 0 {
t.Errorf("writer.status 0 == %d", writer.status)
}
}

Miután megvagyunk a hitelesítéssel jöhet az aláírás Java oldalon. Kutattam egy darabig hogyan lehet PSS aláírást Java SE-vel generálni, de mivel a projektünknek már része volt a Bouncy Castle Crypto API, így kézenfekvő volt, hogy azt használjam fel.
// privateKeyPem - private key in PEM format
// data - data to signature
public static String generateSignature(String privateKeyPem, byte[] data) {
try (PEMParser pEMParser = new PEMParser(new StringReader(clarifyPemKey(privateKeyPem)))) {
PEMKeyPair pemKeyPair = (PEMKeyPair) pEMParser.readObject();
KeyFactory factory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(pemKeyPair.getPublicKeyInfo().getEncoded());
PublicKey publicKey = factory.generatePublic(publicKeySpec);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(pemKeyPair.getPrivateKeyInfo().getEncoded());
PrivateKey privateKey = factory.generatePrivate(privateKeySpec);
KeyPair kp = new KeyPair(publicKey, privateKey);
RSAPrivateKeySpec privKeySpec = factory.getKeySpec(kp.getPrivate(), RSAPrivateKeySpec.class);
PSSSigner signer = new PSSSigner(new RSAEngine(), new SHA256Digest(), 20); // be sure we use defautl salt lenght
signer.init(true, new RSAKeyParameters(true, privKeySpec.getModulus(), privKeySpec.getPrivateExponent()));
signer.update(data, 0, data.length);
byte[] signature = signer.generateSignature();
return BaseEncoding.base64().encode(signature);
} catch (NoSuchAlgorithmException | IOException | InvalidKeySpecException | CryptoException e) {
throw new RuntimeException(e);
}
}
private static String clarifyPemKey(String rawPem) {
return "-----BEGIN RSA PRIVATE KEY-----\n" + rawPem.replaceAll("-----(.*)-----|\n", "") + "\n-----END RSA PRIVATE KEY-----"; // PEMParser nem kedveli a sortöréseket
}

A Java oldali kulcspár generálással tele van az internet, azzal nem untatnák senkit.