Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prime256v1 identities. #14

Merged
merged 2 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ jobs:
- uses: aviate-labs/[email protected]
with:
dfx-version: 0.18.0
- run: mv ic/testdata/networks.json $HOME/.config/dfx/networks.json
- run: make test
37 changes: 32 additions & 5 deletions agent_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package agent_test

import (
"crypto/ed25519"
"crypto/rand"
"encoding/json"
"fmt"
"github.com/aviate-labs/agent-go"
Expand Down Expand Up @@ -51,9 +49,38 @@ func Example_json() {
// {"e8s":0}
}

func Example_query() {
publicKey, privateKey, _ := ed25519.GenerateKey(rand.Reader)
id, _ := identity.NewEd25519Identity(publicKey, privateKey)
func Example_query_prime256v1() {
id, _ := identity.NewRandomPrime256v1Identity()
ledgerID, _ := principal.Decode("ryjl3-tyaaa-aaaaa-aaaba-cai")
a, _ := agent.New(agent.Config{Identity: id})
var balance struct {
E8S uint64 `ic:"e8s"`
}
_ = a.Query(ledgerID, "account_balance_dfx", []any{map[string]any{
"account": "9523dc824aa062dcd9c91b98f4594ff9c6af661ac96747daef2090b7fe87037d",
}}, []any{&balance})
fmt.Println(balance.E8S)
// Output:
// 0
}

func Example_query_ed25519() {
id, _ := identity.NewRandomEd25519Identity()
ledgerID, _ := principal.Decode("ryjl3-tyaaa-aaaaa-aaaba-cai")
a, _ := agent.New(agent.Config{Identity: id})
var balance struct {
E8S uint64 `ic:"e8s"`
}
_ = a.Query(ledgerID, "account_balance_dfx", []any{map[string]any{
"account": "9523dc824aa062dcd9c91b98f4594ff9c6af661ac96747daef2090b7fe87037d",
}}, []any{&balance})
fmt.Println(balance.E8S)
// Output:
// 0
}

func Example_query_secp256k1() {
id, _ := identity.NewRandomSecp256k1Identity()
ledgerID, _ := principal.Decode("ryjl3-tyaaa-aaaaa-aaaba-cai")
a, _ := agent.New(agent.Config{Identity: id})
var balance struct {
Expand Down
5 changes: 5 additions & 0 deletions ic/testdata/networks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"local": {
"bind": "127.0.0.1:8080"
}
}
8 changes: 8 additions & 0 deletions identity/anonymous.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import (
// AnonymousIdentity is an identity that does not sign messages.
type AnonymousIdentity struct{}

func (id AnonymousIdentity) Verify(_, _ []byte) bool {
return true
}

func (id AnonymousIdentity) ToPEM() ([]byte, error) {
return nil, nil
}

// PublicKey returns the public key of the identity.
func (id AnonymousIdentity) PublicKey() []byte {
return nil
Expand Down
9 changes: 7 additions & 2 deletions identity/ed25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ func NewEd25519Identity(publicKey ed25519.PublicKey, privateKey ed25519.PrivateK

// NewEd25519IdentityFromPEM creates a new identity from the given PEM file.
func NewEd25519IdentityFromPEM(data []byte) (*Ed25519Identity, error) {
block, _ := pem.Decode(data)
if block.Type != "PRIVATE KEY" {
block, remainder := pem.Decode(data)
if block == nil || len(remainder) != 0 || block.Type != "PRIVATE KEY" {
return nil, fmt.Errorf("invalid pem file")
}
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
Expand Down Expand Up @@ -85,6 +85,11 @@ func (id Ed25519Identity) Sign(data []byte) []byte {
return ed25519.Sign(id.privateKey, data)
}

// Verify verifies the given signature.
func (id Ed25519Identity) Verify(data, signature []byte) bool {
return ed25519.Verify(id.publicKey, data, signature)
}

// ToPEM returns the PEM representation of the identity.
func (id Ed25519Identity) ToPEM() ([]byte, error) {
data, err := x509.MarshalPKCS8PrivateKey(id.privateKey)
Expand Down
11 changes: 11 additions & 0 deletions identity/ed25519_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ func TestNewEd25519Identity(t *testing.T) {
t.Error()
}
}

func TestEd25519Identity_Sign(t *testing.T) {
id, err := NewRandomEd25519Identity()
if err != nil {
t.Fatal(err)
}
data := []byte("hello")
if !id.Verify(data, id.Sign(data)) {
t.Error()
}
}
4 changes: 4 additions & 0 deletions identity/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ type Identity interface {
Sign(msg []byte) []byte
// PublicKey returns the public key of the identity.
PublicKey() []byte
// Verify verifies the signature of the given message.
Verify(msg, sig []byte) bool
// ToPEM returns the PEM representation of the identity.
ToPEM() ([]byte, error)
}
98 changes: 98 additions & 0 deletions identity/prime256v1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package identity

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"fmt"
"github.com/aviate-labs/agent-go/principal"
"math/big"
)

var prime256v1OID = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7}

type Prime256v1Identity struct {
privateKey *ecdsa.PrivateKey
publicKey *ecdsa.PublicKey
}

func NewRandomPrime256v1Identity() (*Prime256v1Identity, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
return NewPrime256v1Identity(privateKey), nil
}

func (id Prime256v1Identity) Sender() principal.Principal {
return principal.NewSelfAuthenticating(id.PublicKey())
}

func (id Prime256v1Identity) Sign(msg []byte) []byte {
hashData := sha256.Sum256(msg)
sigR, sigS, _ := ecdsa.Sign(rand.Reader, id.privateKey, hashData[:])
var buffer [64]byte
r := sigR.Bytes()
s := sigS.Bytes()
copy(buffer[(32-len(r)):], r)
copy(buffer[(64-len(s)):], s)
return buffer[:]
}

func (id Prime256v1Identity) Verify(msg, sig []byte) bool {
r := new(big.Int).SetBytes(sig[:32])
s := new(big.Int).SetBytes(sig[32:])
hashData := sha256.Sum256(msg)
return ecdsa.Verify(id.publicKey, hashData[:], r, s)
}

func NewPrime256v1Identity(privateKey *ecdsa.PrivateKey) *Prime256v1Identity {
return &Prime256v1Identity{
privateKey: privateKey,
publicKey: privateKey.Public().(*ecdsa.PublicKey),
}
}

func NewPrime256v1IdentityFromPEM(data []byte) (*Prime256v1Identity, error) {
block, remainder := pem.Decode(data)
if block == nil || block.Type != "EC PRIVATE KEY" || len(remainder) != 0 {
return nil, fmt.Errorf("invalid pem file")
}
privateKey, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return NewPrime256v1Identity(privateKey), nil
}

func derEncodePrime256v1PublicKey(key *ecdsa.PublicKey) ([]byte, error) {
return asn1.Marshal(ecPublicKey{
Metadata: []asn1.ObjectIdentifier{
ecPublicKeyOID,
prime256v1OID,
},
PublicKey: asn1.BitString{
Bytes: marshal(elliptic.P256(), key.X, key.Y),
},
})
}

func (id Prime256v1Identity) PublicKey() []byte {
der, _ := derEncodePrime256v1PublicKey(id.publicKey)
return der
}

func (id Prime256v1Identity) ToPEM() ([]byte, error) {
data, err := x509.MarshalECPrivateKey(id.privateKey)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: data,
}), nil
}
54 changes: 54 additions & 0 deletions identity/prime256v1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package identity

import (
"bytes"
"encoding/hex"
"testing"
)

func TestNewPrime256v1Identity(t *testing.T) {
id, _ := NewRandomPrime256v1Identity()
data, err := id.ToPEM()
if err != nil {
t.Fatal(err)
}
id_, err := NewPrime256v1IdentityFromPEM(data)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(id.privateKey.D.Bytes(), id_.privateKey.D.Bytes()) {
t.Error()
}
if !bytes.Equal(id.PublicKey(), id_.PublicKey()) {
t.Error()
}
}

func TestPrime256v1Identity_Sign(t *testing.T) {
id, err := NewRandomPrime256v1Identity()
if err != nil {
t.Fatal(err)
}
data := []byte("hello")
if !id.Verify(data, id.Sign(data)) {
t.Error()
}
}

func TestNewPrime256v1IdentityFromPEM(t *testing.T) {
pem := `
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIL1ybmbwx+uKYsscOZcv71MmKhrNqfPP0ke1unET5AY4oAoGCCqGSM49
AwEHoUQDQgAEUbbZV4NerZTPWfbQ749/GNLu8TaH8BUS/I7/+ipsu+MPywfnBFIZ
Sks4xGbA/ZbazsrMl4v446U5UIVxCGGaKw==
-----END EC PRIVATE KEY-----
`
der, _ := hex.DecodeString("3059301306072a8648ce3d020106082a8648ce3d0301070342000451b6d957835ead94cf59f6d0ef8f7f18d2eef13687f01512fc8efffa2a6cbbe30fcb07e70452194a4b38c466c0fd96dacecacc978bf8e3a53950857108619a2b")
id, err := NewPrime256v1IdentityFromPEM([]byte(pem))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(id.PublicKey(), der) {
t.Fatal("public key mismatch")
}
}
23 changes: 17 additions & 6 deletions identity/secp256k1.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"github.com/aviate-labs/agent-go/principal"
"github.com/aviate-labs/secp256k1"
"math/big"
"slices"
)

Expand All @@ -17,8 +18,8 @@ func derEncodeSecp256k1PublicKey(key *secp256k1.PublicKey) ([]byte, error) {
point := key.ToECDSA()
return asn1.Marshal(ecPublicKey{
Metadata: []asn1.ObjectIdentifier{
{1, 2, 840, 10045, 2, 1}, // ec.PublicKey
secp256k1OID, // Secp256k1
ecPublicKeyOID,
secp256k1OID,
},
PublicKey: asn1.BitString{
Bytes: marshal(secp256k1.S256(), point.X, point.Y),
Expand Down Expand Up @@ -80,10 +81,8 @@ func (id Secp256k1Identity) Sender() principal.Principal {
}

func (id Secp256k1Identity) Sign(msg []byte) []byte {
hash := sha256.New()
hash.Write(msg)
hashData := hash.Sum(nil)
sig, _ := id.privateKey.Sign(hashData)
hashData := sha256.Sum256(msg)
sig, _ := id.privateKey.Sign(hashData[:])
var buffer [64]byte
r := sig.R.Bytes()
s := sig.S.Bytes()
Expand Down Expand Up @@ -121,6 +120,18 @@ func (id Secp256k1Identity) ToPEM() ([]byte, error) {
), nil
}

// Verify verifies the signature of the given message.
func (id Secp256k1Identity) Verify(msg, sig []byte) bool {
signature := secp256k1.Signature{
R: new(big.Int).SetBytes(sig[:32]),
S: new(big.Int).SetBytes(sig[32:]),
}
hashData := sha256.Sum256(msg)
return signature.Verify(hashData[:], id.publicKey)
}

var ecPublicKeyOID = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}

type ecPrivateKey struct {
Version int
PrivateKey []byte
Expand Down
33 changes: 33 additions & 0 deletions identity/secp256k1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package identity

import (
"bytes"
"encoding/hex"
"testing"
)

Expand All @@ -22,3 +23,35 @@ func TestNewSecp256k1Identity(t *testing.T) {
t.Error()
}
}

func TestSecp256k1Identity_Sign(t *testing.T) {
id, err := NewRandomSecp256k1Identity()
if err != nil {
t.Fatal(err)
}
data := []byte("hello")
if !id.Verify(data, id.Sign(data)) {
t.Error()
}
}

func TestNewSecp256k1IdentityFromPEM(t *testing.T) {
pem := `
-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIAgy7nZEcVHkQ4Z1Kdqby8SwyAiyKDQmtbEHTIM+WNeBoAcGBSuBBAAK
oUQDQgAEgO87rJ1ozzdMvJyZQ+GABDqUxGLvgnAnTlcInV3NuhuPv4O3VGzMGzeB
N3d26cRxD99TPtm8uo2OuzKhSiq6EQ==
-----END EC PRIVATE KEY-----
`
der, _ := hex.DecodeString("3056301006072a8648ce3d020106052b8104000a0342000480ef3bac9d68cf374cbc9c9943e180043a94c462ef8270274e57089d5dcdba1b8fbf83b7546ccc1b3781377776e9c4710fdf533ed9bcba8d8ebb32a14a2aba11")
id, err := NewSecp256k1IdentityFromPEM([]byte(pem))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(id.PublicKey(), der) {
t.Fatal("public key mismatch")
}
}
Loading