From 60c3ae76941886bbb84ca37cc71ffbfe3dba29f2 Mon Sep 17 00:00:00 2001 From: Tal Moran Date: Wed, 11 Nov 2015 16:17:14 +0200 Subject: [PATCH 1/6] Added eclipse project files to gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 93ba99d..7cc989d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ .gradle .idea build +bin +.settings +.classpath +.project out *.iml *.ipr From 0e69214f30288563e27aaa74d6e35e01cd971470 Mon Sep 17 00:00:00 2001 From: Tal Moran Date: Wed, 11 Nov 2015 18:25:17 +0200 Subject: [PATCH 2/6] Added basic OpenSSL CA with generated intermediate CAs and certificates for testing purposes --- .../src/test/resources/certs/README.md | 6 ++ .../user1-key-with-password-secret.pem | 8 +++ .../certs/enduser-certs/user1-key.pem | 5 ++ .../resources/certs/enduser-certs/user1.crt | 19 ++++++ .../resources/certs/enduser-certs/user1.csr | 9 +++ .../certs/enduser-certs/user2-key.pem | 5 ++ .../resources/certs/enduser-certs/user2.crt | 19 ++++++ .../resources/certs/enduser-certs/user2.csr | 9 +++ .../resources/certs/enduser-certs/user2.der | Bin 0 -> 794 bytes .../certs/intermediate-ca-1/1000.pem | 19 ++++++ .../certs/intermediate-ca-1/1001.pem | 19 ++++++ .../certs/intermediate-ca-1/certindex | 2 + .../certs/intermediate-ca-1/certindex.attr | 1 + .../intermediate-ca-1/certindex.attr.old | 1 + .../certs/intermediate-ca-1/certindex.old | 1 + .../certs/intermediate-ca-1/certserial | 1 + .../certs/intermediate-ca-1/certserial.old | 1 + .../certs/intermediate-ca-1/crlnumber | 1 + .../intermediate-ca-1-private-key.pem | 5 ++ .../intermediate-ca-1/intermediate-ca-1.crt | 21 ++++++ .../intermediate-ca-1/intermediate-ca-1.csr | 10 +++ .../openssl-intermediate-ca.conf | 46 +++++++++++++ .../src/test/resources/certs/root-ca/1000.pem | 21 ++++++ .../test/resources/certs/root-ca/certindex | 1 + .../resources/certs/root-ca/certindex.attr | 1 + .../resources/certs/root-ca/certindex.old | 0 .../test/resources/certs/root-ca/certserial | 1 + .../resources/certs/root-ca/certserial.old | 1 + .../test/resources/certs/root-ca/crlnumber | 1 + .../resources/certs/root-ca/openssl-ca.conf | 61 ++++++++++++++++++ ...ot-ca-private-key-with-password-secret.pem | 8 +++ .../certs/root-ca/root-ca-private-key.der | Bin 0 -> 118 bytes .../certs/root-ca/root-ca-private-key.pem | 5 ++ .../test/resources/certs/root-ca/root-ca.crt | 17 +++++ .../src/test/resources/certs/secp256k1.pem | 3 + 35 files changed, 328 insertions(+) create mode 100644 meerkat-common/src/test/resources/certs/README.md create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user1-key-with-password-secret.pem create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user1-key.pem create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user1.crt create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user1.csr create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user2-key.pem create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user2.crt create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user2.csr create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user2.der create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/1000.pem create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/1001.pem create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/certindex create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/certindex.attr create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/certindex.attr.old create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/certindex.old create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/certserial create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/certserial.old create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/crlnumber create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/intermediate-ca-1-private-key.pem create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/intermediate-ca-1.crt create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/intermediate-ca-1.csr create mode 100644 meerkat-common/src/test/resources/certs/intermediate-ca-1/openssl-intermediate-ca.conf create mode 100644 meerkat-common/src/test/resources/certs/root-ca/1000.pem create mode 100644 meerkat-common/src/test/resources/certs/root-ca/certindex create mode 100644 meerkat-common/src/test/resources/certs/root-ca/certindex.attr create mode 100644 meerkat-common/src/test/resources/certs/root-ca/certindex.old create mode 100644 meerkat-common/src/test/resources/certs/root-ca/certserial create mode 100644 meerkat-common/src/test/resources/certs/root-ca/certserial.old create mode 100644 meerkat-common/src/test/resources/certs/root-ca/crlnumber create mode 100644 meerkat-common/src/test/resources/certs/root-ca/openssl-ca.conf create mode 100644 meerkat-common/src/test/resources/certs/root-ca/root-ca-private-key-with-password-secret.pem create mode 100644 meerkat-common/src/test/resources/certs/root-ca/root-ca-private-key.der create mode 100644 meerkat-common/src/test/resources/certs/root-ca/root-ca-private-key.pem create mode 100644 meerkat-common/src/test/resources/certs/root-ca/root-ca.crt create mode 100644 meerkat-common/src/test/resources/certs/secp256k1.pem diff --git a/meerkat-common/src/test/resources/certs/README.md b/meerkat-common/src/test/resources/certs/README.md new file mode 100644 index 0000000..f7283d9 --- /dev/null +++ b/meerkat-common/src/test/resources/certs/README.md @@ -0,0 +1,6 @@ +Certs and private keys for testing generated using OpenSSL + +.crt and .pem files are in PEM format +.der files are in binary DER format + +files that have a name of the form *-with-password-xxxx.pem are encrypted with the password xxxx diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user1-key-with-password-secret.pem b/meerkat-common/src/test/resources/certs/enduser-certs/user1-key-with-password-secret.pem new file mode 100644 index 0000000..e859995 --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user1-key-with-password-secret.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,243D718A0D80C59590E582A26E87A49C + +RG6ITUTIdbJdWYX57oMn3tTCzHJSTjXAIZLjoVxy/v4UFYjluaFhGonIlbH1q2pP +ueu29Q3eT6144ypB8ARUJ1x0kRX1OL9zNHgdF9ulrCf9/nhGyC2nL+tHZ0YPbxoQ ++6yCQcRWvjUXLVzPEUnwMuHXJDpaXES8X0R4CISQKIA= +-----END EC PRIVATE KEY----- diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user1-key.pem b/meerkat-common/src/test/resources/certs/enduser-certs/user1-key.pem new file mode 100644 index 0000000..6619e37 --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user1-key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQge8JqCoaLoZq61aQki5Xm +GppcfAAkhHDGNQw/wLof5LmhRANCAAQJD1kW6BsNkRY9tslaugpOJOaoKX4uBz4S +Q96lPaPWkatNVgQchwNeB/hdjZwNuwE7A7XAwr69HFmhXRhsM005 +-----END PRIVATE KEY----- diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user1.crt b/meerkat-common/src/test/resources/certs/enduser-certs/user1.crt new file mode 100644 index 0000000..d80093f --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user1.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFjCCArygAwIBAgICEAAwCgYIKoZIzj0EAwIwgYIxKTAnBgNVBAMMIE1lZXJr +YXQgVm90aW5nIEludGVybWVkaWF0ZSBDQSAxMRMwEQYDVQQIDApTb21lLVN0YXRl +MQswCQYDVQQGEwJJTDEVMBMGA1UECgwMSURDIEhlcnpsaXlhMRwwGgYDVQQLDBNN +ZWVya2F0IFZvdGluZyBUZWFtMB4XDTE1MTExMTE2MTM1NFoXDTI1MTEwODE2MTM1 +NFowbjEaMBgGA1UEAwwRUG9sbGluZyBTdGF0aW9uIDExEzARBgNVBAgMClNvbWUt +U3RhdGUxCzAJBgNVBAYTAklMMRUwEwYDVQQKDAxJREMgSGVyemxpeWExFzAVBgNV +BAsMDk1lZXJrYXQgVm90aW5nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAECQ9ZFugb +DZEWPbbJWroKTiTmqCl+Lgc+EkPepT2j1pGrTVYEHIcDXgf4XY2cDbsBOwO1wMK+ +vRxZoV0YbDNNOaOCATYwggEyMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFLamS8o2 +hFNd0vWy/irEBNWVNwFXMB8GA1UdIwQYMBaAFBeyv0c75eT6PNumHo9TZ2B9vtcp +MAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATBEBgNVHR8EPTA7MDmg +N6A1hjNodHRwOi8vY3JsLmZhY3RjZW50ZXIub3JnL21lZXJrYXQtaW50ZXJtZWRp +YXRlMS5jcmwwegYIKwYBBQUHAQEEbjBsMEEGCCsGAQUFBzAChjVodHRwOi8vcGtp +LmZhY3RjZW50ZXIub3JnL21lZXJrYXQtaW50ZXJtZWRpYXRlLWNhLmNydDAnBggr +BgEFBQcwAYYbaHR0cDovL29jc3AuZmFjdGNlbnRlci5vcmcvMAoGCCqGSM49BAMC +A0gAMEUCIQD6QbhNNmB3AVVqhmXuiLA7WF6raShw6n0g/VloVGQebQIgEvxYclpO +MMynt5wH6X65rtn4Q1EGaDMvNbFweCDsldk= +-----END CERTIFICATE----- diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user1.csr b/meerkat-common/src/test/resources/certs/enduser-certs/user1.csr new file mode 100644 index 0000000..20e1efc --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user1.csr @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBOjCB4QIBADCBgTELMAkGA1UEBhMCSUwxEzARBgNVBAgMClNvbWUtU3RhdGUx +ETAPBgNVBAcMCEhlcnpsaXlhMRUwEwYDVQQKDAxJREMgSGVyemxpeWExFzAVBgNV +BAsMDk1lZXJrYXQgVm90aW5nMRowGAYDVQQDDBFQb2xsaW5nIFN0YXRpb24gMTBW +MBAGByqGSM49AgEGBSuBBAAKA0IABAkPWRboGw2RFj22yVq6Ck4k5qgpfi4HPhJD +3qU9o9aRq01WBByHA14H+F2NnA27ATsDtcDCvr0cWaFdGGwzTTmgADAKBggqhkjO +PQQDAgNIADBFAiEA8gmIhALr7O5M1QLReGH3jheildTIr1mDWl14WyMf9U4CIF23 +mInyo4VqNHLzxMLg5Cn3Oddokng3OXa63y4nTfv+ +-----END CERTIFICATE REQUEST----- diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user2-key.pem b/meerkat-common/src/test/resources/certs/enduser-certs/user2-key.pem new file mode 100644 index 0000000..2d31bb8 --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user2-key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgYpBEO+XWm/n6VPeMVK76 +mrZkDTpiwLsDykG7M4fU5RKhRANCAAR71/kVGyA3hdxcLBBT3NPQF6R3LholmLRN +qhnvHqzJWuy7ev+Xbuxtt9AN0ajyeFDy8Oe1bUSidnLyQi+nXC0f +-----END PRIVATE KEY----- diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user2.crt b/meerkat-common/src/test/resources/certs/enduser-certs/user2.crt new file mode 100644 index 0000000..a211365 --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user2.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFjCCArygAwIBAgICEAEwCgYIKoZIzj0EAwIwgYIxKTAnBgNVBAMMIE1lZXJr +YXQgVm90aW5nIEludGVybWVkaWF0ZSBDQSAxMRMwEQYDVQQIDApTb21lLVN0YXRl +MQswCQYDVQQGEwJJTDEVMBMGA1UECgwMSURDIEhlcnpsaXlhMRwwGgYDVQQLDBNN +ZWVya2F0IFZvdGluZyBUZWFtMB4XDTE1MTExMTE2MjAzM1oXDTI1MTEwODE2MjAz +M1owbjEaMBgGA1UEAwwRUG9sbGluZyBTdGF0aW9uIDIxEzARBgNVBAgMClNvbWUt +U3RhdGUxCzAJBgNVBAYTAklMMRUwEwYDVQQKDAxJREMgSGVyemxpeWExFzAVBgNV +BAsMDk1lZXJrYXQgVm90aW5nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEe9f5FRsg +N4XcXCwQU9zT0Bekdy4aJZi0TaoZ7x6syVrsu3r/l27sbbfQDdGo8nhQ8vDntW1E +onZy8kIvp1wtH6OCATYwggEyMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFKCdquYj +DGHqAHt+4PIDlw0h2UvuMB8GA1UdIwQYMBaAFBeyv0c75eT6PNumHo9TZ2B9vtcp +MAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATBEBgNVHR8EPTA7MDmg +N6A1hjNodHRwOi8vY3JsLmZhY3RjZW50ZXIub3JnL21lZXJrYXQtaW50ZXJtZWRp +YXRlMS5jcmwwegYIKwYBBQUHAQEEbjBsMEEGCCsGAQUFBzAChjVodHRwOi8vcGtp +LmZhY3RjZW50ZXIub3JnL21lZXJrYXQtaW50ZXJtZWRpYXRlLWNhLmNydDAnBggr +BgEFBQcwAYYbaHR0cDovL29jc3AuZmFjdGNlbnRlci5vcmcvMAoGCCqGSM49BAMC +A0gAMEUCIQDpo5B0vvEJSax3YzOMfE8l0pfDUIKLdBWJVGeq0VLtIgIgVr0+4/0e +n+R+l1OVOLh2GirloOgbv5Ch5BQ2pQNAG2Y= +-----END CERTIFICATE----- diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user2.csr b/meerkat-common/src/test/resources/certs/enduser-certs/user2.csr new file mode 100644 index 0000000..bb3c2d0 --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user2.csr @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBOzCB4QIBADCBgTELMAkGA1UEBhMCSUwxEzARBgNVBAgMClNvbWUtU3RhdGUx +ETAPBgNVBAcMCEhlcnpsaXlhMRUwEwYDVQQKDAxJREMgSGVyemxpeWExFzAVBgNV +BAsMDk1lZXJrYXQgVm90aW5nMRowGAYDVQQDDBFQb2xsaW5nIFN0YXRpb24gMjBW +MBAGByqGSM49AgEGBSuBBAAKA0IABHvX+RUbIDeF3FwsEFPc09AXpHcuGiWYtE2q +Ge8erMla7Lt6/5du7G230A3RqPJ4UPLw57VtRKJ2cvJCL6dcLR+gADAKBggqhkjO +PQQDAgNJADBGAiEA6Ls/ojRaZT+u4YeOBYcPbRcJE3jSTe1Sm/lR7fDyEhMCIQCk +UOca+e2b8+CqM3CURBv6TqUMmZ3HeMRvEAxFPqOWSw== +-----END CERTIFICATE REQUEST----- diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user2.der b/meerkat-common/src/test/resources/certs/enduser-certs/user2.der new file mode 100644 index 0000000000000000000000000000000000000000..8e784093b249900ab82c852d5257c6e338711969 GIT binary patch literal 794 zcmXqLViq%KV%oESnTe5!iAjLbfQyYotIgw_EekV~L1UAlrhz&eb0`ZlkAiP%YEgD# zi9%R@NoHQUf@fYyYEf=#N@ik7s)Dnlf}x?XfgngJ2MWL72`${0w2gt>Wy(d-CGP0Tfr6X!KFH3R`OBLidO zC~;mRAlJYG$~DL{lroS2>1XB<49L&P0XYt2cV>Q`f)V+S5;qWqIEoL|Q3hcK0&MKy zAYx)(1!MycOjE+F?oy!-*mz3*Csm|EqyGruC+?tb7Z+2Jx zpPu(7cl!n2iz_}=1bq7Nd~2@DqOzh-PWsDZbmbQ}F`5}PF&Y{00Iii3W@P-&!eqc; zAPeI0v52vVESS6MnKDn}D~9U22cMXy^D5r-erF&Ll2&GsFc51H5#O}m-TLX1UpBXw z$@K@PC)Dn{u4%vxQozpwj7`=B;Lw*+qo`~hvA#t01me4gqr(^mZ8swk{1_djD z1ON)5L<2$q1fnR&U|n`@kTkW(@ Date: Thu, 12 Nov 2015 03:05:19 +0200 Subject: [PATCH 3/6] Initial signature implementation with some tests --- meerkat-common/build.gradle | 4 + .../java/meerkat/crypto/DigitalSignature.java | 61 ++++- .../crypto/concrete/ECDSASignature.java | 239 ++++++++++++++++++ .../meerkat/crypto/concrete/SHA256Digest.java | 11 +- .../src/main/proto/meerkat/crypto.proto | 6 +- .../crypto/concrete/TestECDSASignature.java | 79 ++++++ .../user1-key-with-password-secret.p12 | Bin 0 -> 1301 bytes .../certs/enduser-certs/user1-key.der | Bin 0 -> 118 bytes .../certs/enduser-certs/user1-pubkey.pem | 4 + .../certs/enduser-certs/user2-pubkey.pem | 4 + .../certs/signed-messages/helloworld.txt | 1 + .../signed-messages/helloworld.txt.sha256sig | Bin 0 -> 72 bytes 12 files changed, 403 insertions(+), 6 deletions(-) create mode 100644 meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java create mode 100644 meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user1-key-with-password-secret.p12 create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user1-key.der create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user1-pubkey.pem create mode 100644 meerkat-common/src/test/resources/certs/enduser-certs/user2-pubkey.pem create mode 100644 meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt create mode 100644 meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt.sha256sig diff --git a/meerkat-common/build.gradle b/meerkat-common/build.gradle index 9768dd8..b744fcd 100644 --- a/meerkat-common/build.gradle +++ b/meerkat-common/build.gradle @@ -45,6 +45,10 @@ dependencies { // Google protobufs compile 'com.google.protobuf:protobuf-java:3.+' + // Crypto + compile 'org.bouncycastle:bcprov-jdk15on:1.53' + compile 'org.bouncycastle:bcpkix-jdk15on:1.53' + testCompile 'junit:junit:4.+' runtime 'org.codehaus.groovy:groovy:2.4.+' diff --git a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java index 691b492..cc9151e 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java @@ -2,16 +2,69 @@ package meerkat.crypto; import com.google.protobuf.Message; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.SignatureException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; import java.util.List; import static meerkat.protobuf.Crypto.*; /** * Created by talm on 25/10/15. * - * Sign arrays of messages + * Sign and verifyarrays of messages */ -public interface DigitalSignature { // Extends SCAPI DigitalSignature - public Signature sign(List msg); +public interface DigitalSignature { + final public static String CERTIFICATE_ENCODING_X509 = "X.509"; + + /** + * Load a set of certificates from an input stream. + * This will consume the entire stream. + * Certificates can be either DER-encoded (binary) or PEM (base64) encoded. + * This may be called multiple times to load several different certificates. + * It must be called before calling {@link #verify(Signature, List)}. + * @param certStream source from which certificates are loaded + * @throws CertificateException on parsing errors + */ + public void loadVerificationCertificates(InputStream certStream) + throws CertificateException; + + /** + * Clear the loaded verification certificates. + */ + public void clearVerificationCertificates(); + + /** + * Verify a signature on a list of messages. + * The signature is computed on the serialized messages, separated with + * the {@link Digest#CONCAT_MARKER} string. + * For the signature to be valid, the verification key corresponding + * to the signature must have been loaded using {@link #loadVerificationCertificates(InputStream)}; + * otherwise a CertificateException is thrown + * + * @param sig The signature on the messages + * @param msgs The messages themselves + * @return + * @throws CertificateException the certificate required for verification was not loaded. + */ + public boolean verify(Signature sig, List msgs) + throws CertificateException, InvalidKeyException; + + /** + * Loads a private signing key. The certificate must be in PKCS12 format and include both + * the public and private keys. + * This method must be called before calling {@link #sign(List)} + * Calling this method again will replace the key. + * + * @param keyStream + */ + public void loadSigningCertificate(InputStream keyStream, char[] password) + throws IOException, CertificateException, UnrecoverableKeyException; + + public Signature sign(List msg) throws SignatureException; + + public void clearSigningKey(); - public boolean verify(Signature sig, List msgs); } diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java new file mode 100644 index 0000000..a947e15 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java @@ -0,0 +1,239 @@ +package meerkat.crypto.concrete; + +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.*; + +import com.google.protobuf.ByteString; +import meerkat.crypto.Digest; +import meerkat.protobuf.Crypto; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.protobuf.Message; + +import meerkat.crypto.DigitalSignature; +import meerkat.protobuf.Crypto.Signature; +import org.bouncycastle.util.io.pem.*; +import org.bouncycastle.openssl.*; + + +/** + * Sign and verify digital signatures. + *

+ * This class is not thread-safe (each thread should have its own instance). + */ +public class ECDSASignature implements DigitalSignature { + final Logger logger = LoggerFactory.getLogger(getClass()); + + final public static String KEYSTORE_TYPE = "PKCS12"; + final public static String DEFAULT_SIGNATURE_ALGORITHM = "SHA256withECDSA"; + + SHA256Digest digest = new SHA256Digest(); + + Map loadedCertificates = new HashMap<>(); + + ByteString loadedSigningKeyId = null; + + /** + * The actual signing implementation. + */ + java.security.Signature signer; + + + /** + * Compute a fingerprint of a public key as a SHA256 hash. + * + * @param pubKey + * @return + */ + public ByteString computePublicKeyID(PublicKey pubKey) { + digest.reset(); + byte[] data = pubKey.getEncoded(); + digest.update(data); + return ByteString.copyFrom(digest.digest()); + } + + public ECDSASignature(java.security.Signature signer) { + this.signer = signer; + } + + public ECDSASignature() { + try { + this.signer = java.security.Signature.getInstance(DEFAULT_SIGNATURE_ALGORITHM); + } catch (NoSuchAlgorithmException e) { + // Should never happen + logger.error("Couldn't find implementation for {} signatures: {}", DEFAULT_SIGNATURE_ALGORITHM, e); + } + } + + @Override + public void loadVerificationCertificates(InputStream certStream) + throws CertificateException { + CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_ENCODING_X509); + Collection certs = certificateFactory.generateCertificates(certStream); + for (Certificate cert : certs) { + // Just checking + if (!(cert instanceof X509Certificate)) { + logger.error("Certificate must be in X509 format; got {} instead!", cert.getClass().getCanonicalName()); + continue; + } + PublicKey pubKey = cert.getPublicKey(); + ByteString keyId = computePublicKeyID(pubKey); + loadedCertificates.put(keyId, pubKey); + } + } + + @Override + public void clearVerificationCertificates() { + loadedCertificates.clear(); + } + + + public void updateSigner(List msgs) throws SignatureException { + if (msgs == null) + return; + boolean first = true; + for (Message msg : msgs) { + if (!first) { + signer.update(Digest.CONCAT_MARKER); + } + first = false; + signer.update(msg.toByteString().asReadOnlyByteBuffer()); + } + } + + public void updateSigner(InputStream in) throws IOException, SignatureException { + ByteString inStr = ByteString.readFrom(in); + signer.update(inStr.asReadOnlyByteBuffer()); + } + + public Signature sign() throws SignatureException { + Signature.Builder sig = Signature.newBuilder(); + sig.setType(Crypto.SignatureType.ECDSA); + sig.setData(ByteString.copyFrom(signer.sign())); + sig.setSignerId(loadedSigningKeyId); + return sig.build(); + } + + public void initVerify(Signature sig) + throws CertificateException, InvalidKeyException { + PublicKey pubKey = loadedCertificates.get(sig.getSignerId()); + if (pubKey == null) { + logger.warn("No public key loaded for ID {}!", sig.getSignerId()); + throw new CertificateException("No public key loaded for " + sig.getSignerId()); + } + signer.initVerify(pubKey); + } + + public boolean verify(Signature sig) { + try { + return signer.verify(sig.getData().toByteArray()); + } catch (SignatureException e) { + // Should never happen! + logger.error("Signature exception: {}", e); + return false; + } + } + + @Override + public boolean verify(Signature sig, List msgs) + throws CertificateException { + try { + initVerify(sig); + updateSigner(msgs); + return verify(sig); + } catch (InvalidKeyException e) { + logger.error("Invalid key exception: {}", e); + return false; + } catch (SignatureException e) { + // Should never happen! + logger.error("Signature exception: {}", e); + return false; + } + } + + @Override + public void loadSigningCertificate(InputStream keyStream, char[] password) + throws IOException, CertificateException, UnrecoverableKeyException { + try { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); + + keyStore.load(keyStream, password); + + KeyStore.ProtectionParameter protParam = + new KeyStore.PasswordProtection(password); + + // Iterate through all aliases until we find the first privatekey + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + try { + KeyStore.Entry entry = keyStore.getEntry(alias, protParam); + if (entry instanceof KeyStore.PrivateKeyEntry) { + KeyStore.PrivateKeyEntry privEntry = (KeyStore.PrivateKeyEntry) entry; + PrivateKey privKey = privEntry.getPrivateKey(); + PublicKey pubKey = privEntry.getCertificate().getPublicKey(); + loadedSigningKeyId = computePublicKeyID(pubKey); + signer.initSign(privKey); + return; + } + } catch (InvalidKeyException e) { + logger.info("Read invalid key: {}", e); + } catch (UnrecoverableEntryException e) { + logger.info("Read unrecoverable entry: {}", e); + } + + } + } catch (KeyStoreException e) { + logger.error("Keystore exception: {}", e); + } catch (NoSuchAlgorithmException e) { + logger.error("NoSuchAlgorithmException exception: {}", e); + throw new CertificateException(e); + } + throw new UnrecoverableKeyException("Didn't find valid private key entry in stream!"); + } + + + @Override + public Signature sign(List msgs) { + try { + updateSigner(msgs); + return sign(); + } catch (SignatureException e) { + logger.error("Signature exception: {}", e); + return null; + } + } + + + public Signature sign(InputStream in) throws IOException { + try { + updateSigner(in); + return sign(); + } catch (SignatureException e) { + logger.error("Signature exception: {}", e); + return null; + } + } + + public void clearSigningKey() { + try { + // TODO: Check if this really clears the key from memory + if (loadedSigningKeyId != null) + signer.initSign(null); + loadedSigningKeyId = null; + } catch (InvalidKeyException e) { + // Do nothing + } + } + + +} diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java index 092aab4..8e72f26 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java @@ -1,5 +1,6 @@ package meerkat.crypto.concrete; +import com.google.protobuf.ByteString; import com.google.protobuf.Message; import meerkat.crypto.Digest; import org.slf4j.Logger; @@ -32,7 +33,7 @@ public class SHA256Digest implements Digest { public SHA256Digest() { this(true); } - /** + /**SHA * Instantiate with the default (SHA-256) algorithm, * or create an empty class (for cloning) */ @@ -58,6 +59,14 @@ public class SHA256Digest implements Digest { hash.update(msg.toByteString().asReadOnlyByteBuffer()); } + final public void update(ByteString msg) { + hash.update(msg.asReadOnlyByteBuffer()); + } + + final public void update(byte[] msg) { + hash.update(msg); + } + @Override public void reset() { hash.reset(); diff --git a/meerkat-common/src/main/proto/meerkat/crypto.proto b/meerkat-common/src/main/proto/meerkat/crypto.proto index e1475da..feafc1c 100644 --- a/meerkat-common/src/main/proto/meerkat/crypto.proto +++ b/meerkat-common/src/main/proto/meerkat/crypto.proto @@ -13,14 +13,18 @@ enum SignatureType { message Signature { SignatureType type = 1; - // Data encoding depends on type; default is x509 BER-encoded + // Data encoding depends on type; default is DER-encoded bytes data = 2; + + // ID of the signer (should be the fingerprint of the signature verification key) + bytes signer_id = 3; } // Public key used to verify signatures message SignatureVerificationKey { SignatureType type = 1; + // Data encoding depends on type; default is x509 DER-encoded bytes data = 2; } diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java new file mode 100644 index 0000000..99dbee1 --- /dev/null +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java @@ -0,0 +1,79 @@ +package meerkat.crypto.concrete; + +import com.google.protobuf.ByteString; +import meerkat.protobuf.Crypto; +import org.junit.Test; + +import java.io.InputStream; + +import static org.junit.Assert.assertTrue; + +/** + * Created by talm on 12/11/15. + */ +public class TestECDSASignature { + public static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12"; + public static String KEYFILE_PASSWORD = "secret"; + + public static String CERT1_PEM_EXAMPLE = "/certs/enduser-certs/user1.crt"; + public static String CERT2_DER_EXAMPLE = "/certs/enduser-certs/user2.der"; + + public static String MSG_PLAINTEXT_EXAMPLE = "/certs/signed-messages/helloworld.txt"; + public static String MSG_SIG_EXAMPLE = "/certs/signed-messages/helloworld.txt.sha256sig"; + + + @Test + public void testLoadSignatureKey() throws Exception { + InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); + char[] password = KEYFILE_PASSWORD.toCharArray(); + + ECDSASignature sig = new ECDSASignature(); + + sig.loadSigningCertificate(keyStream, password); + keyStream.close(); + } + + @Test + public void loadPEMVerificationKey() throws Exception { + InputStream certStream = getClass().getResourceAsStream(CERT1_PEM_EXAMPLE); + + ECDSASignature sig = new ECDSASignature(); + + sig.loadVerificationCertificates(certStream); + certStream.close(); + } + + @Test + public void loadDERVerificationKey() throws Exception { + InputStream certStream = getClass().getResourceAsStream(CERT2_DER_EXAMPLE); + + ECDSASignature sig = new ECDSASignature(); + + sig.loadVerificationCertificates(certStream); + certStream.close(); + } + + + @Test + public void verifyValidSig() throws Exception { + InputStream certStream = getClass().getResourceAsStream(CERT1_PEM_EXAMPLE); + InputStream msgStream = getClass().getResourceAsStream(MSG_PLAINTEXT_EXAMPLE); + InputStream sigStream = getClass().getResourceAsStream(MSG_SIG_EXAMPLE); + + ECDSASignature signer = new ECDSASignature(); + + signer.loadVerificationCertificates(certStream); + certStream.close(); + + Crypto.Signature.Builder sig = Crypto.Signature.newBuilder(); + sig.setType(Crypto.SignatureType.ECDSA); + sig.setSignerId(signer.loadedCertificates.entrySet().iterator().next().getKey()); + sig.setData(ByteString.readFrom(sigStream)); + + Crypto.Signature builtSig = sig.build(); + signer.initVerify(builtSig); + signer.updateSigner(msgStream); + assertTrue("Signature did not verify!", signer.verify(builtSig)); + } + +} diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user1-key-with-password-secret.p12 b/meerkat-common/src/test/resources/certs/enduser-certs/user1-key-with-password-secret.p12 new file mode 100644 index 0000000000000000000000000000000000000000..6281f9d5ae0e130fe5b1456eb0b5fe67d6a732df GIT binary patch literal 1301 zcmV+w1?u`Rf&~!*0Ru3C1lI-$Duzgg_YDCD0ic2e$OM7}#4v&cz%YUXzXk~^hDe6@ z4FLxRpn?OiFoFZD0s#Opf&--n2`Yw2hW8Bt2LUh~1_~;MNQUw(I~ zGNqQ80s;sCfPw>fJ8IJVIDMrkZhe|Dcn7wOpkFb&nQsnx({lQXdj=~1Z>P^E;doZ* zv^|Qre2mu}l0c6v|0~oKWBBOK8*=5Jild)#4v?aDwN``n6B$6xSk~u}+?&ax zlwts59AB{8;;#rbw487L(9Wre6xF`a^s8CwqgKVx4bAb}SMgr>!U?K}$rk<@6GHm3(d8`H%~l$ z;1RZ*6$nseSyB}}%ph@;F$_sgCf8fDieC2G`|s5F2G?|d&4xE%m|a`pQ1eb`ayqRy0+=Wu1s{Kz0UmC2>y=-5@#qQvR? z(DSz1619xxwYC;D$XyQ(2az?1>+T$V3lv9XV{qPjAoNzOJ#10@BF>&F8Mqtt& zJ5kY<;o$BdlPt^E7W0y({xi_nZ}P26iD8ZdeK`Pdk$CRy^IFHM|aG($JymNP+!NI=l^e^0imC%Fk-6@5uo-5Ty45{-izn znL6c#0n@os>1$X{dzRObEl0XzGK$R?^layAJO>?w!dYaa&&WU)q^_8+BEFpEelROk zHUG42!V-YfsNn)1;{V~Xh<}UvTzVJ-3O8k@nt-0FS{57m(HKpFZ`vOV?zwBtG0;!@7FoEVUf#U`XDuzgg_YDCI z3IPJ3fvhlrsW2P{3Mz(3hW8Bt3;_c$4g?6=j(pEfEK9Hg0tf&Efr!OXhx8-^4UkvZ zzzvrEI@yGPc4F;YV)mgGv-X0(P!`(!;HNE!VkNP_mMQw!%>b-Yt3-nZdiJg}rx-#E zgR@?`g_C^>a)-q5J~_h* zDyXPua$eO}E_qsiM^K1^TJ<2yqhB#4Fe3&DDuzgg_YDCF6)_eB6y$?U5+Bl2@(8Je zOt_{aDZ-v4r7$rtAutIB1uG5%0vZJX1QbJiOe+w{yU;)Per>RxI@lTl5>5mNlvBCc LE?!A(0s;sCJrq%4 literal 0 HcmV?d00001 diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user1-key.der b/meerkat-common/src/test/resources/certs/enduser-certs/user1-key.der new file mode 100644 index 0000000000000000000000000000000000000000..6f174060b7c8dce9c94288ad1ddd589e1860dfcd GIT binary patch literal 118 zcmV-+0Ez!FbOHeZ1R#6DY6^ynp_;nYq$G=#<{Fw@d;lbbaK<$ZKft;loJ`zLTr9Gq8k*iHs1RRG0UI+MH Yjhqd;0XqY=z{0-099f}V7;H05IqJ77b^rhX literal 0 HcmV?d00001 diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user1-pubkey.pem b/meerkat-common/src/test/resources/certs/enduser-certs/user1-pubkey.pem new file mode 100644 index 0000000..1c0a0c1 --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user1-pubkey.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAECQ9ZFugbDZEWPbbJWroKTiTmqCl+Lgc+ +EkPepT2j1pGrTVYEHIcDXgf4XY2cDbsBOwO1wMK+vRxZoV0YbDNNOQ== +-----END PUBLIC KEY----- diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user2-pubkey.pem b/meerkat-common/src/test/resources/certs/enduser-certs/user2-pubkey.pem new file mode 100644 index 0000000..5d86d4c --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user2-pubkey.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEe9f5FRsgN4XcXCwQU9zT0Bekdy4aJZi0 +TaoZ7x6syVrsu3r/l27sbbfQDdGo8nhQ8vDntW1EonZy8kIvp1wtHw== +-----END PUBLIC KEY----- diff --git a/meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt b/meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt @@ -0,0 +1 @@ + diff --git a/meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt.sha256sig b/meerkat-common/src/test/resources/certs/signed-messages/helloworld.txt.sha256sig new file mode 100644 index 0000000000000000000000000000000000000000..7e717d1464909238bd5109ce395c000d0b8993e7 GIT binary patch literal 72 zcmV-O0Jr}zMgk!KnCy29jbLAEfY6rmN9)dUoPGXw^%FnnxKkCyx{;&j0wDm{rw5v( e5Yt;i`~ Date: Thu, 12 Nov 2015 11:06:38 +0200 Subject: [PATCH 4/6] Support JDK 7 by loading bouncycastle --- .../crypto/concrete/ECDSASignature.java | 6 +-- .../crypto/concrete/GlobalCryptoSetup.java | 38 +++++++++++++++++++ .../meerkat/crypto/concrete/SHA256Digest.java | 2 +- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java index a947e15..e204aec 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java @@ -21,8 +21,8 @@ import com.google.protobuf.Message; import meerkat.crypto.DigitalSignature; import meerkat.protobuf.Crypto.Signature; -import org.bouncycastle.util.io.pem.*; -import org.bouncycastle.openssl.*; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; /** @@ -30,7 +30,7 @@ import org.bouncycastle.openssl.*; *

* This class is not thread-safe (each thread should have its own instance). */ -public class ECDSASignature implements DigitalSignature { +public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignature { final Logger logger = LoggerFactory.getLogger(getClass()); final public static String KEYSTORE_TYPE = "PKCS12"; diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java new file mode 100644 index 0000000..764fd8d --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java @@ -0,0 +1,38 @@ +package meerkat.crypto.concrete; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.Security; + +/** + * A class that performs required crypto setup + */ +public class GlobalCryptoSetup { + final static Logger logger = LoggerFactory.getLogger(GlobalCryptoSetup.class); + + static boolean loadedBouncyCastle = false; + + public static boolean hasSecp256k1Curve() { + // For now we just check if the java version is at least 8 + String[] version = System.getProperty("java.version").split("\\."); + int major = Integer.parseInt(version[0]); + int minor = Integer.parseInt(version[1]); + return ((major > 1) || ((major > 0) && (minor > 7))); + } + + public static void doSetup() { + // Make bouncycastle our default provider if we're running on a JVM version < 8 + // (earlier version don't support the EC curve we use for signatures) + if (!hasSecp256k1Curve() && !loadedBouncyCastle) { + loadedBouncyCastle = true; + Security.insertProviderAt(new BouncyCastleProvider(), 1); + logger.info("Using BouncyCastle instead of native provider to support secp256k1 named curve"); + } + } + + public GlobalCryptoSetup() { + doSetup(); + } +} diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java index 8e72f26..dc1d2ba 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java @@ -12,7 +12,7 @@ import java.security.NoSuchAlgorithmException; /** * Created by talm on 11/9/15. */ -public class SHA256Digest implements Digest { +public class SHA256Digest extends GlobalCryptoSetup implements Digest { final Logger logger = LoggerFactory.getLogger(getClass()); public static final String SHA256 = "SHA-256"; From b839447f870c1f4d0ee78432bc179f4645e5880c Mon Sep 17 00:00:00 2001 From: Tal Moran Date: Thu, 12 Nov 2015 12:33:49 +0200 Subject: [PATCH 5/6] Added logback config file --- .../src/main/resources/logback.groovy | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 meerkat-common/src/main/resources/logback.groovy diff --git a/meerkat-common/src/main/resources/logback.groovy b/meerkat-common/src/main/resources/logback.groovy new file mode 100644 index 0000000..1eb8d7d --- /dev/null +++ b/meerkat-common/src/main/resources/logback.groovy @@ -0,0 +1,46 @@ + + +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.filter.ThresholdFilter +import ch.qos.logback.core.ConsoleAppender +import ch.qos.logback.core.util.Duration +import static ch.qos.logback.classic.Level.* + +if (System.getProperty("log.debug") != null) { + println "Logback configuration debugging enabled" + + statusListener(OnConsoleStatusListener) +} + +def LOG_LEVEL = toLevel(System.getProperty("log.level"), INFO) + +def haveBeagle = System.getProperty("log.beagle") != null +def logOps = System.getProperty("log.ops") != null + +appender("CONSOLE", ConsoleAppender) { + + filter(ThresholdFilter) { + level = toLevel(System.getProperty("log.level"), DEBUG) + } + + encoder(PatternLayoutEncoder) { + pattern = "%d{HH:mm:ss.SSS} [%thread %file:%line] %-5level %logger{0} - %msg%n" + } +} + +def appenders = [ "CONSOLE" ] + +if (haveBeagle) { + appender("SOCKET", SocketAppender) { + includeCallerData = true + remoteHost = "localhost" + port = 4321 + reconnectionDelay = new Duration(10000) + } + + appenders += ["SOCKET"] +} + +root(LOG_LEVEL, appenders) + + From baba4df3a979227d958fc28d711c3a9e1006bb0e Mon Sep 17 00:00:00 2001 From: Tal Moran Date: Thu, 12 Nov 2015 16:06:58 +0200 Subject: [PATCH 6/6] Used API compatible with PCKS11 (e.g., smartcards); refactored --- .../java/meerkat/crypto/DigitalSignature.java | 57 +++-- .../crypto/concrete/ECDSASignature.java | 241 ++++++++++-------- .../src/main/java/meerkat/util/Hex.java | 26 ++ .../src/main/resources/logback.groovy | 2 +- .../crypto/concrete/TestECDSASignature.java | 99 ++++++- 5 files changed, 300 insertions(+), 125 deletions(-) create mode 100644 meerkat-common/src/main/java/meerkat/util/Hex.java diff --git a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java index cc9151e..eda41a2 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java @@ -2,9 +2,12 @@ package meerkat.crypto; import com.google.protobuf.Message; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; import java.io.IOException; import java.io.InputStream; import java.security.InvalidKeyException; +import java.security.KeyStore; import java.security.SignatureException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; @@ -24,7 +27,7 @@ public interface DigitalSignature { * This will consume the entire stream. * Certificates can be either DER-encoded (binary) or PEM (base64) encoded. * This may be called multiple times to load several different certificates. - * It must be called before calling {@link #verify(Signature, List)}. + * It must be called before calling {@link #verify()}. * @param certStream source from which certificates are loaded * @throws CertificateException on parsing errors */ @@ -36,35 +39,55 @@ public interface DigitalSignature { */ public void clearVerificationCertificates(); + /** - * Verify a signature on a list of messages. - * The signature is computed on the serialized messages, separated with - * the {@link Digest#CONCAT_MARKER} string. - * For the signature to be valid, the verification key corresponding - * to the signature must have been loaded using {@link #loadVerificationCertificates(InputStream)}; - * otherwise a CertificateException is thrown + * Add msg to the content stream to be verified / signed. Each message is always (automatically) + * prepended with its length as a 32-bit unsigned integer in network byte order. * - * @param sig The signature on the messages - * @param msgs The messages themselves - * @return - * @throws CertificateException the certificate required for verification was not loaded. + * @param msg + * @throws SignatureException */ - public boolean verify(Signature sig, List msgs) + public void updateContent(Message msg) throws SignatureException; + + + /** + * Sign the content that was added. + * @return + * @throws SignatureException + */ + Signature sign() throws SignatureException; + + /** + * Initialize the verifier with the certificate whose Id is in sig. + * @param sig + * @throws CertificateException + * @throws InvalidKeyException + */ + void initVerify(Signature sig) throws CertificateException, InvalidKeyException; /** - * Loads a private signing key. The certificate must be in PKCS12 format and include both - * the public and private keys. + * Verify the updated content using the initialized signature. + * @return + */ + public boolean verify(); + + /** + * Loads a private signing key. The keystore must include both the public and private + * key parts. * This method must be called before calling {@link #sign(List)} * Calling this method again will replace the key. * - * @param keyStream + * @param keyStoreBuilder A keystore builder that can be used to load a keystore. */ - public void loadSigningCertificate(InputStream keyStream, char[] password) + public void loadSigningCertificate(KeyStore.Builder keyStoreBuilder) throws IOException, CertificateException, UnrecoverableKeyException; - public Signature sign(List msg) throws SignatureException; + + /** + * Clear the signing key (will require authentication to use again). + */ public void clearSigningKey(); } diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java index e204aec..4fb8de5 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java @@ -1,19 +1,16 @@ package meerkat.crypto.concrete; -import java.io.IOError; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.security.*; +import java.security.cert.*; import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; import java.util.*; import com.google.protobuf.ByteString; import meerkat.crypto.Digest; import meerkat.protobuf.Crypto; +import meerkat.util.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,15 +19,18 @@ import com.google.protobuf.Message; import meerkat.crypto.DigitalSignature; import meerkat.protobuf.Crypto.Signature; -import org.bouncycastle.jce.provider.BouncyCastleProvider; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; /** * Sign and verify digital signatures. - *

+ *

* This class is not thread-safe (each thread should have its own instance). */ -public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignature { +public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignature { final Logger logger = LoggerFactory.getLogger(getClass()); final public static String KEYSTORE_TYPE = "PKCS12"; @@ -38,27 +38,39 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu SHA256Digest digest = new SHA256Digest(); - Map loadedCertificates = new HashMap<>(); + Map loadedCertificates = new HashMap<>(); + + /** + * Signature currently loaded (will be used in calls to {@link #verify()}). + */ + ByteString loadedSignature = null; ByteString loadedSigningKeyId = null; /** - * The actual signing implementation. + * The actual signing implementation. (used for both signing and verifying) */ java.security.Signature signer; /** - * Compute a fingerprint of a public key as a SHA256 hash. + * Compute a fingerprint of a cert as a SHA256 hash. * - * @param pubKey + * @param cert * @return */ - public ByteString computePublicKeyID(PublicKey pubKey) { - digest.reset(); - byte[] data = pubKey.getEncoded(); - digest.update(data); - return ByteString.copyFrom(digest.digest()); + public ByteString computeCertificateFingerprint(Certificate cert) { + try { + digest.reset(); + byte[] data = cert.getEncoded(); + digest.update(data); + return ByteString.copyFrom(digest.digest()); + } catch (CertificateEncodingException e) { + // Shouldn't happen + logger.error("Certificate encoding error", e); + return ByteString.EMPTY; + } + } public ECDSASignature(java.security.Signature signer) { @@ -70,7 +82,7 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu this.signer = java.security.Signature.getInstance(DEFAULT_SIGNATURE_ALGORITHM); } catch (NoSuchAlgorithmException e) { // Should never happen - logger.error("Couldn't find implementation for {} signatures: {}", DEFAULT_SIGNATURE_ALGORITHM, e); + logger.error("Couldn't find implementation for " + DEFAULT_SIGNATURE_ALGORITHM + " signatures", e); } } @@ -86,8 +98,8 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu continue; } PublicKey pubKey = cert.getPublicKey(); - ByteString keyId = computePublicKeyID(pubKey); - loadedCertificates.put(keyId, pubKey); + ByteString keyId = computeCertificateFingerprint(cert); + loadedCertificates.put(keyId, cert); } } @@ -97,17 +109,21 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu } - public void updateSigner(List msgs) throws SignatureException { - if (msgs == null) - return; - boolean first = true; - for (Message msg : msgs) { - if (!first) { - signer.update(Digest.CONCAT_MARKER); - } - first = false; - signer.update(msg.toByteString().asReadOnlyByteBuffer()); - } + /** + * Add the list of messages to the stream that is being verified/signed. + * Messages are separated with {@link Digest#CONCAT_MARKER} + * + * @param msg + * @throws SignatureException + */ + @Override + public void updateContent(Message msg) throws SignatureException { + assert msg != null; + int len = msg.getSerializedSize(); + + byte[] lenBytes = { (byte) ((len >>> 24) & 0xff), (byte) ((len >>> 16) & 0xff), (byte) ((len >>> 8) & 0xff), (byte) (len & 0xff) }; + signer.update(lenBytes); + signer.update(msg.toByteString().asReadOnlyByteBuffer()); } public void updateSigner(InputStream in) throws IOException, SignatureException { @@ -115,6 +131,7 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu signer.update(inStr.asReadOnlyByteBuffer()); } + @Override public Signature sign() throws SignatureException { Signature.Builder sig = Signature.newBuilder(); sig.setType(Crypto.SignatureType.ECDSA); @@ -123,105 +140,121 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatu return sig.build(); } + @Override public void initVerify(Signature sig) - throws CertificateException, InvalidKeyException { - PublicKey pubKey = loadedCertificates.get(sig.getSignerId()); - if (pubKey == null) { - logger.warn("No public key loaded for ID {}!", sig.getSignerId()); - throw new CertificateException("No public key loaded for " + sig.getSignerId()); - } - signer.initVerify(pubKey); - } - - public boolean verify(Signature sig) { - try { - return signer.verify(sig.getData().toByteArray()); - } catch (SignatureException e) { - // Should never happen! - logger.error("Signature exception: {}", e); - return false; + throws CertificateException, InvalidKeyException { + Certificate cert = loadedCertificates.get(sig.getSignerId()); + if (cert == null) { + logger.warn("No certificate loaded for ID {}!", sig.getSignerId()); + throw new CertificateException("No certificate loaded for " + sig.getSignerId()); } + signer.initVerify(cert.getPublicKey()); + loadedSignature = sig.getData(); + loadedSigningKeyId = null; } @Override - public boolean verify(Signature sig, List msgs) - throws CertificateException { + public boolean verify() { try { - initVerify(sig); - updateSigner(msgs); - return verify(sig); - } catch (InvalidKeyException e) { - logger.error("Invalid key exception: {}", e); - return false; + return signer.verify(loadedSignature.toByteArray()); } catch (SignatureException e) { - // Should never happen! - logger.error("Signature exception: {}", e); + // Happens only if signature is invalid! + logger.error("Signature exception", e); return false; } } + /** + * Utility method to more easily deal with simple password-protected files. + * + * @param password + * @return + */ + public CallbackHandler getFixedPasswordHandler(final char[] password) { + return new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof PasswordCallback) { + PasswordCallback passwordCallback = (PasswordCallback) callback; + logger.debug("Requested password ({})", passwordCallback.getPrompt()); + passwordCallback.setPassword(password); + } + } + + } + }; + } + + /** + * Load a keystore from an input stream in PKCS12 format. + * + * @param keyStream + * @param password + * @return + * @throws IOException + * @throws CertificateException + * @throws KeyStoreException + * @throws NoSuchAlgorithmException + */ + public KeyStore.Builder getPKCS12KeyStoreBuilder(InputStream keyStream, char[] password) + throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); + keyStore.load(keyStream, password); + return KeyStore.Builder.newInstance(keyStore, new KeyStore.CallbackHandlerProtection(getFixedPasswordHandler(password))); + } + + + /** + * For now we only support PKCS12. + * TODO: Support for PKCS11 as well. + * + * @param keyStoreBuilder + * @throws IOException + * @throws CertificateException + * @throws UnrecoverableKeyException + */ @Override - public void loadSigningCertificate(InputStream keyStream, char[] password) + public void loadSigningCertificate(KeyStore.Builder keyStoreBuilder) throws IOException, CertificateException, UnrecoverableKeyException { try { - KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); - keyStore.load(keyStream, password); - - KeyStore.ProtectionParameter protParam = - new KeyStore.PasswordProtection(password); + KeyStore keyStore = keyStoreBuilder.getKeyStore(); // Iterate through all aliases until we find the first privatekey Enumeration aliases = keyStore.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); - try { - KeyStore.Entry entry = keyStore.getEntry(alias, protParam); - if (entry instanceof KeyStore.PrivateKeyEntry) { - KeyStore.PrivateKeyEntry privEntry = (KeyStore.PrivateKeyEntry) entry; - PrivateKey privKey = privEntry.getPrivateKey(); - PublicKey pubKey = privEntry.getCertificate().getPublicKey(); - loadedSigningKeyId = computePublicKeyID(pubKey); - signer.initSign(privKey); - return; - } - } catch (InvalidKeyException e) { - logger.info("Read invalid key: {}", e); - } catch (UnrecoverableEntryException e) { - logger.info("Read unrecoverable entry: {}", e); - } + logger.trace("Testing keystore entry {}", alias); + try { + Certificate cert = keyStore.getCertificate(alias); + logger.trace("keystore entry {}, has cert type {}", alias, cert.getClass()); + Key key = keyStore.getKey(alias, null); + logger.trace("keystore entry {}, has key type {}", alias, key.getClass()); + if (key instanceof PrivateKey) { + loadedSigningKeyId = computeCertificateFingerprint(cert); + signer.initSign((PrivateKey) key); + logger.debug("Loaded signing key with ID {}", Hex.encode(loadedSigningKeyId)); + return; + } else { + logger.info("Certificate {} in keystore does not have a private key", cert.toString()); + } + } catch(InvalidKeyException e) { + logger.info("Read invalid key", e); + } catch(UnrecoverableEntryException e) { + logger.info("Read unrecoverable entry", e); + } } + } catch (KeyStoreException e) { - logger.error("Keystore exception: {}", e); + logger.error("Keystore exception", e); } catch (NoSuchAlgorithmException e) { - logger.error("NoSuchAlgorithmException exception: {}", e); + logger.error("NoSuchAlgorithmException exception", e); throw new CertificateException(e); } - throw new UnrecoverableKeyException("Didn't find valid private key entry in stream!"); - } - - - @Override - public Signature sign(List msgs) { - try { - updateSigner(msgs); - return sign(); - } catch (SignatureException e) { - logger.error("Signature exception: {}", e); - return null; - } - } - - - public Signature sign(InputStream in) throws IOException { - try { - updateSigner(in); - return sign(); - } catch (SignatureException e) { - logger.error("Signature exception: {}", e); - return null; - } + logger.error("Didn't find valid private key entry in keystore"); + throw new UnrecoverableKeyException("Didn't find valid private key entry in keystore!"); } public void clearSigningKey() { diff --git a/meerkat-common/src/main/java/meerkat/util/Hex.java b/meerkat-common/src/main/java/meerkat/util/Hex.java new file mode 100644 index 0000000..2d0e4ef --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/util/Hex.java @@ -0,0 +1,26 @@ +package meerkat.util; + +import com.google.protobuf.ByteString; + +/** + * Convert to/from Hex + */ +public class Hex { + /** + * Encode a {@link ByteString} as a hex string. + * @param str + * @return + */ + public static String encode(ByteString str) { + StringBuilder s = new StringBuilder(); + for (byte b : str) { + s.append(Integer.toHexString(((int) b) & 0xff)); + } + return s.toString(); + } + + public static String encode(byte[] bytes) { + return encode(ByteString.copyFrom(bytes)); + } +} + diff --git a/meerkat-common/src/main/resources/logback.groovy b/meerkat-common/src/main/resources/logback.groovy index 1eb8d7d..076b6c4 100644 --- a/meerkat-common/src/main/resources/logback.groovy +++ b/meerkat-common/src/main/resources/logback.groovy @@ -20,7 +20,7 @@ def logOps = System.getProperty("log.ops") != null appender("CONSOLE", ConsoleAppender) { filter(ThresholdFilter) { - level = toLevel(System.getProperty("log.level"), DEBUG) + level = toLevel(System.getProperty("log.level"), TRACE) } encoder(PatternLayoutEncoder) { diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java index 99dbee1..4fbb35c 100644 --- a/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/TestECDSASignature.java @@ -2,10 +2,16 @@ package meerkat.crypto.concrete; import com.google.protobuf.ByteString; import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting; import org.junit.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.InputStream; +import java.security.KeyStore; +import java.util.Arrays; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** @@ -21,15 +27,18 @@ public class TestECDSASignature { public static String MSG_PLAINTEXT_EXAMPLE = "/certs/signed-messages/helloworld.txt"; public static String MSG_SIG_EXAMPLE = "/certs/signed-messages/helloworld.txt.sha256sig"; + public static String HELLO_WORLD = "hello world!"; + @Test - public void testLoadSignatureKey() throws Exception { + public void loadSignatureKey() throws Exception { InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); char[] password = KEYFILE_PASSWORD.toCharArray(); ECDSASignature sig = new ECDSASignature(); - sig.loadSigningCertificate(keyStream, password); + KeyStore.Builder keyStore = sig.getPKCS12KeyStoreBuilder(keyStream, password); + sig.loadSigningCertificate(keyStore); keyStream.close(); } @@ -73,7 +82,91 @@ public class TestECDSASignature { Crypto.Signature builtSig = sig.build(); signer.initVerify(builtSig); signer.updateSigner(msgStream); - assertTrue("Signature did not verify!", signer.verify(builtSig)); + assertTrue("Signature did not verify!", signer.verify()); } + @Test + public void verifyInvalidSig() throws Exception { + InputStream certStream = getClass().getResourceAsStream(CERT1_PEM_EXAMPLE); + InputStream msgStream = getClass().getResourceAsStream(MSG_PLAINTEXT_EXAMPLE); + InputStream sigStream = getClass().getResourceAsStream(MSG_SIG_EXAMPLE); + + ECDSASignature signer = new ECDSASignature(); + + signer.loadVerificationCertificates(certStream); + certStream.close(); + + Crypto.Signature.Builder sig = Crypto.Signature.newBuilder(); + sig.setType(Crypto.SignatureType.ECDSA); + sig.setSignerId(signer.loadedCertificates.entrySet().iterator().next().getKey()); + byte[] sigData = ByteString.readFrom(sigStream).toByteArray(); + ++sigData[0]; + + sig.setData(ByteString.copyFrom(sigData)); + + + Crypto.Signature builtSig = sig.build(); + signer.initVerify(builtSig); + signer.updateSigner(msgStream); + assertFalse("Bad Signature passed verification!", signer.verify()); + } + + + @Test + public void verifyInvalidMsg() throws Exception { + InputStream certStream = getClass().getResourceAsStream(CERT1_PEM_EXAMPLE); + InputStream msgStream = getClass().getResourceAsStream(MSG_PLAINTEXT_EXAMPLE); + InputStream sigStream = getClass().getResourceAsStream(MSG_SIG_EXAMPLE); + + ECDSASignature signer = new ECDSASignature(); + + signer.loadVerificationCertificates(certStream); + certStream.close(); + + Crypto.Signature.Builder sig = Crypto.Signature.newBuilder(); + sig.setType(Crypto.SignatureType.ECDSA); + sig.setSignerId(signer.loadedCertificates.entrySet().iterator().next().getKey()); + sig.setData(ByteString.readFrom(sigStream)); + byte[] msgData = ByteString.readFrom(msgStream).toByteArray(); + ++msgData[0]; + + Crypto.Signature builtSig = sig.build(); + signer.initVerify(builtSig); + signer.updateSigner(msgStream); + assertFalse("Signature doesn't match message but passed verification!", signer.verify()); + } + + + + @Test + public void signAndVerify() throws Exception { + InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); + char[] password = KEYFILE_PASSWORD.toCharArray(); + + ECDSASignature signer = new ECDSASignature(); + + KeyStore.Builder keyStore = signer.getPKCS12KeyStoreBuilder(keyStream, password); + signer.loadSigningCertificate(keyStore); + + + Voting.UnsignedBulletinBoardMessage.Builder unsignedMsgBuilder = Voting.UnsignedBulletinBoardMessage.newBuilder(); + unsignedMsgBuilder.setData(ByteString.copyFromUtf8(HELLO_WORLD)); + unsignedMsgBuilder.addTags("Tag1"); + unsignedMsgBuilder.addTags("Tag2"); + unsignedMsgBuilder.addTags("Tag3"); + + Voting.UnsignedBulletinBoardMessage usMsg = unsignedMsgBuilder.build(); + + signer.updateContent(usMsg); + Crypto.Signature sig = signer.sign(); + + signer.loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE)); + + signer.initVerify(sig); + signer.updateContent(usMsg); + assertTrue("Couldn't verify signature on ", signer.verify()); + } + + + }