forked from hyperledger-archives/aries-framework-go
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Support ECDSA Secp256k1 linked data signature suite
closes hyperledger-archives#1529 Signed-off-by: Dmitriy Kinoshenko <dkinoshenko@gmail.com>
- Loading branch information
Showing
7 changed files
with
496 additions
and
8 deletions.
There are no files selected for viewing
103 changes: 103 additions & 0 deletions
103
pkg/doc/signature/suite/ecdsasecp256k1signature2019/public_key_verifier.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/* | ||
Copyright SecureKey Technologies Inc. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package ecdsasecp256k1signature2019 | ||
|
||
import ( | ||
"crypto" | ||
"crypto/ecdsa" | ||
"errors" | ||
"fmt" | ||
"math/big" | ||
|
||
"github.com/btcsuite/btcd/btcec" | ||
|
||
sigverifier "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier" | ||
) | ||
|
||
const ( | ||
secp256k1KeySize = 32 | ||
) | ||
|
||
// PublicKeyVerifier verifies a secp256k1 signature taking public key bytes and JSON Web Key as input. | ||
// NOTE: this verifier is present for backward compatibility reasons and can be removed later. | ||
// Please use CryptoVerifier or your own verifier. | ||
type PublicKeyVerifier struct { | ||
} | ||
|
||
// Verify will verify a signature. | ||
func (v *PublicKeyVerifier) Verify(pubKey *sigverifier.PublicKey, msg, signature []byte) error { | ||
err := v.validatePublicKey(pubKey) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if len(signature) != 2*secp256k1KeySize { | ||
return errors.New("ecdsa: invalid signature size") | ||
} | ||
|
||
curve := btcec.S256() | ||
|
||
btcecPubKey, err := btcec.ParsePubKey(pubKey.Value, curve) | ||
if err != nil { | ||
return errors.New("ecdsa: invalid public key") | ||
} | ||
|
||
ecdsaPubKey := btcecPubKey.ToECDSA() | ||
hasher := crypto.SHA256.New() | ||
|
||
_, err = hasher.Write(msg) | ||
if err != nil { | ||
return errors.New("ecdsa: hash error") | ||
} | ||
|
||
hash := hasher.Sum(nil) | ||
|
||
r := big.NewInt(0).SetBytes(signature[:secp256k1KeySize]) | ||
s := big.NewInt(0).SetBytes(signature[secp256k1KeySize:]) | ||
|
||
verified := ecdsa.Verify(ecdsaPubKey, hash, r, s) | ||
if !verified { | ||
return errors.New("ecdsa: invalid signature") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (v *PublicKeyVerifier) validatePublicKey(pubKey *sigverifier.PublicKey) error { | ||
// A presence of JSON Web Key is mandatory (due to EcdsaSecp256k1VerificationKey2019 type). | ||
if pubKey.JWK == nil { | ||
return ErrJWKNotPresent | ||
} | ||
|
||
if pubKey.Type != jwkType { | ||
return ErrTypeNotEcdsaSecp256k1VerificationKey2019 | ||
} | ||
|
||
if pubKey.JWK.Kty != "EC" { | ||
return fmt.Errorf("unsupported key type: '%s'", pubKey.JWK.Kty) | ||
} | ||
|
||
if pubKey.JWK.Crv != "" && pubKey.JWK.Crv != "secp256k1" { | ||
return fmt.Errorf("ecdsa: not secp256k1 curve: '%s'", pubKey.JWK.Crv) | ||
} | ||
|
||
if pubKey.JWK.Algorithm != "" && pubKey.JWK.Algorithm != "ES256K" { | ||
return fmt.Errorf("ecdsa: not ES256K EC algorithm: '%s'", pubKey.JWK.Algorithm) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
var ( | ||
// ErrJWKNotPresent is returned when no JWK is defined in a public key | ||
// (must be defined for EcdsaSecp256k1VerificationKey2019). | ||
ErrJWKNotPresent = errors.New("JWK is not present") | ||
|
||
// ErrTypeNotEcdsaSecp256k1VerificationKey2019 is returned when a public key passed for signature verification | ||
// has a type different from EcdsaSecp256k1VerificationKey2019. | ||
ErrTypeNotEcdsaSecp256k1VerificationKey2019 = errors.New("a type of public key is not EcdsaSecp256k1VerificationKey2019") //nolint:lll | ||
) |
185 changes: 185 additions & 0 deletions
185
pkg/doc/signature/suite/ecdsasecp256k1signature2019/public_key_verifier_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
/* | ||
Copyright SecureKey Technologies Inc. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package ecdsasecp256k1signature2019 | ||
|
||
import ( | ||
"crypto" | ||
"crypto/ecdsa" | ||
"crypto/rand" | ||
"testing" | ||
|
||
"github.com/btcsuite/btcd/btcec" | ||
gojose "github.com/square/go-jose/v3" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/hyperledger/aries-framework-go/pkg/doc/jose" | ||
sigverifier "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier" | ||
) | ||
|
||
func TestPublicKeyVerifier_Verify(t *testing.T) { | ||
btcecPrivKey, err := btcec.NewPrivateKey(btcec.S256()) | ||
require.NoError(t, err) | ||
|
||
ecdsaPrivKey := btcecPrivKey.ToECDSA() | ||
|
||
ecdsaPubKey := &ecdsaPrivKey.PublicKey | ||
|
||
msg := []byte("test message") | ||
|
||
pubKeyBytes := btcecPrivKey.PubKey().SerializeCompressed() | ||
|
||
pubKey := &sigverifier.PublicKey{ | ||
Type: "EcdsaSecp256k1VerificationKey2019", | ||
Value: pubKeyBytes, | ||
|
||
JWK: &jose.JWK{ | ||
JSONWebKey: gojose.JSONWebKey{ | ||
Algorithm: "ES256K", | ||
}, | ||
Crv: "secp256k1", | ||
Kty: "EC", | ||
}, | ||
} | ||
|
||
v := &PublicKeyVerifier{} | ||
signature := getSignature(&ecdsa.PrivateKey{ | ||
PublicKey: ecdsa.PublicKey{ | ||
Curve: ecdsaPubKey.Curve, | ||
X: ecdsaPubKey.X, | ||
Y: ecdsaPubKey.Y, | ||
}, | ||
D: btcecPrivKey.D, | ||
}, msg) | ||
|
||
v = &PublicKeyVerifier{} | ||
signature = getSignature(ecdsaPrivKey, msg) | ||
|
||
err = v.Verify(pubKey, msg, signature) | ||
require.NoError(t, err) | ||
|
||
t.Run("undefined JWK", func(t *testing.T) { | ||
verifyError := v.Verify(&sigverifier.PublicKey{ | ||
Type: "EcdsaSecp256k1VerificationKey2019", | ||
Value: pubKeyBytes, | ||
}, msg, signature) | ||
require.Error(t, verifyError) | ||
require.Equal(t, verifyError, ErrJWKNotPresent) | ||
}) | ||
|
||
t.Run("JWK is invalid type", func(t *testing.T) { | ||
verifyError := v.Verify(&sigverifier.PublicKey{ | ||
Type: "Ed25519Signature2018", | ||
Value: pubKeyBytes, | ||
JWK: &jose.JWK{}, | ||
}, msg, signature) | ||
require.Error(t, verifyError) | ||
require.Equal(t, verifyError, ErrTypeNotEcdsaSecp256k1VerificationKey2019) | ||
}) | ||
|
||
t.Run("JWK with unsupported key type", func(t *testing.T) { | ||
verifyError := v.Verify(&sigverifier.PublicKey{ | ||
Type: "EcdsaSecp256k1VerificationKey2019", | ||
Value: pubKeyBytes, | ||
JWK: &jose.JWK{ | ||
Kty: "unknown", | ||
}, | ||
}, msg, signature) | ||
require.Error(t, verifyError) | ||
require.EqualError(t, verifyError, "unsupported key type: 'unknown'") | ||
}) | ||
|
||
t.Run("invalid curve", func(t *testing.T) { | ||
verifyError := v.Verify(&sigverifier.PublicKey{ | ||
Type: "EcdsaSecp256k1VerificationKey2019", | ||
Value: pubKeyBytes, | ||
JWK: &jose.JWK{ | ||
JSONWebKey: gojose.JSONWebKey{ | ||
Algorithm: "ES256K", | ||
}, | ||
Crv: "unsupported", | ||
Kty: "EC", | ||
}, | ||
}, msg, signature) | ||
require.Error(t, verifyError) | ||
require.EqualError(t, verifyError, "ecdsa: not secp256k1 curve: 'unsupported'") | ||
}) | ||
|
||
t.Run("invalid algorithm", func(t *testing.T) { | ||
verifyError := v.Verify(&sigverifier.PublicKey{ | ||
Type: "EcdsaSecp256k1VerificationKey2019", | ||
Value: pubKeyBytes, | ||
JWK: &jose.JWK{ | ||
JSONWebKey: gojose.JSONWebKey{ | ||
Algorithm: "ES512", | ||
}, | ||
Crv: "secp256k1", | ||
Kty: "EC", | ||
}, | ||
}, msg, signature) | ||
require.Error(t, verifyError) | ||
require.EqualError(t, verifyError, "ecdsa: not ES256K EC algorithm: 'ES512'") | ||
}) | ||
|
||
t.Run("invalid public key", func(t *testing.T) { | ||
verifyError := v.Verify(&sigverifier.PublicKey{ | ||
Type: "EcdsaSecp256k1VerificationKey2019", | ||
Value: []byte("invalid public key"), | ||
JWK: &jose.JWK{ | ||
JSONWebKey: gojose.JSONWebKey{ | ||
Algorithm: "ES256K", | ||
}, | ||
Crv: "secp256k1", | ||
Kty: "EC", | ||
}, | ||
}, msg, signature) | ||
require.Error(t, verifyError) | ||
require.EqualError(t, verifyError, "ecdsa: invalid public key") | ||
}) | ||
|
||
t.Run("invalid signature", func(t *testing.T) { | ||
verifyError := v.Verify(pubKey, msg, []byte("signature of invalid size")) | ||
require.Error(t, verifyError) | ||
require.EqualError(t, verifyError, "ecdsa: invalid signature size") | ||
|
||
emptySig := make([]byte, 64) | ||
verifyError = v.Verify(pubKey, msg, emptySig) | ||
require.Error(t, verifyError) | ||
require.EqualError(t, verifyError, "ecdsa: invalid signature") | ||
}) | ||
} | ||
|
||
func getSignature(privKey *ecdsa.PrivateKey, payload []byte) []byte { | ||
hasher := crypto.SHA256.New() | ||
|
||
_, err := hasher.Write(payload) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
hashed := hasher.Sum(nil) | ||
|
||
r, s, err := ecdsa.Sign(rand.Reader, privKey, hashed) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
curveBits := privKey.Curve.Params().BitSize | ||
|
||
keyBytes := curveBits / 8 | ||
if curveBits%8 > 0 { | ||
keyBytes++ | ||
} | ||
|
||
copyPadded := func(source []byte, size int) []byte { | ||
dest := make([]byte, size) | ||
copy(dest[size-len(source):], source) | ||
|
||
return dest | ||
} | ||
|
||
return append(copyPadded(r.Bytes(), keyBytes), copyPadded(s.Bytes(), keyBytes)...) | ||
} |
70 changes: 70 additions & 0 deletions
70
pkg/doc/signature/suite/ecdsasecp256k1signature2019/suite.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/* | ||
Copyright SecureKey Technologies Inc. All Rights Reserved. | ||
SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
// Package ecdsasecp256k1signature2019 implements the EcdsaSecp256k1Signature2019 signature suite | ||
// for the Linked Data Signatures specification (https://w3c-dvcg.github.io/lds-ecdsa-secp256k1-2019/). | ||
// It uses the RDF Dataset Normalization Algorithm to transform the input document into its canonical form. | ||
// It uses SHA-256 [RFC6234] as the message digest algorithm. | ||
// Supported signature algorithms depend on the signer/verifier provided as options to the New(). | ||
package ecdsasecp256k1signature2019 | ||
|
||
import ( | ||
"crypto/sha256" | ||
|
||
"github.com/piprate/json-gold/ld" | ||
|
||
"github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite" | ||
) | ||
|
||
// Suite implements EcdsaSecp256k1Signature2019 signature suite. | ||
type Suite struct { | ||
suite.SignatureSuite | ||
} | ||
|
||
const ( | ||
signatureType = "EcdsaSecp256k1Signature2019" | ||
jwkType = "EcdsaSecp256k1VerificationKey2019" | ||
format = "application/n-quads" | ||
algorithm = "URDNA2015" | ||
) | ||
|
||
// New an instance of Linked Data Signatures for JWS suite. | ||
func New(opts ...suite.Opt) *Suite { | ||
s := &Suite{} | ||
|
||
suite.InitSuiteOptions(&s.SignatureSuite, opts...) | ||
|
||
return s | ||
} | ||
|
||
// GetCanonicalDocument will return normalized/canonical version of the document. | ||
// EcdsaSecp256k1Signature2019 signature suite uses RDF Dataset Normalization as canonicalization algorithm. | ||
func (s *Suite) GetCanonicalDocument(doc map[string]interface{}) ([]byte, error) { | ||
proc := ld.NewJsonLdProcessor() | ||
options := ld.NewJsonLdOptions("") | ||
options.ProcessingMode = ld.JsonLd_1_1 | ||
options.Format = format | ||
options.ProduceGeneralizedRdf = true | ||
options.Algorithm = algorithm | ||
|
||
canonicalDoc, err := proc.Normalize(doc, options) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return []byte(canonicalDoc.(string)), nil | ||
} | ||
|
||
// GetDigest returns document digest. | ||
func (s *Suite) GetDigest(doc []byte) []byte { | ||
digest := sha256.Sum256(doc) | ||
return digest[:] | ||
} | ||
|
||
// Accept will accept only EcdsaSecp256k1Signature2019 signature type. | ||
func (s *Suite) Accept(t string) bool { | ||
return t == signatureType | ||
} |
Oops, something went wrong.