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 | |
} |
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) | |
}) | |
} |
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.