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 support for AIP-80 parsing and formatting #107

Merged
merged 2 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ adheres to the format set out by [Keep a Changelog](https://keepachangelog.com/e

# Unreleased

- Add AIP-80 support for Ed25519 and Secp256k1 private keys

# v1.2.0 (11/15/2024)

- [`Fix`][`Breaking`] Fix MultiKey implementation to be more consistent with the rest of the SDKs
Expand Down
14 changes: 12 additions & 2 deletions crypto/ed25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import (
"crypto/ed25519"
"errors"
"fmt"
"io"

"github.com/aptos-labs/aptos-go-sdk/bcs"
"github.com/aptos-labs/aptos-go-sdk/internal/util"
"github.com/hdevalence/ed25519consensus"
"io"
)

//region Ed25519PrivateKey
Expand Down Expand Up @@ -151,6 +152,10 @@ func (key *Ed25519PrivateKey) Bytes() []byte {
// Implements:
// - [CryptoMaterial]
func (key *Ed25519PrivateKey) FromBytes(bytes []byte) (err error) {
bytes, err = ParsePrivateKey(bytes, PrivateKeyVariantEd25519, false)
if err != nil {
return err
}
if len(bytes) != ed25519.SeedSize {
return fmt.Errorf("invalid ed25519 private key size %d", len(bytes))
}
Expand All @@ -166,14 +171,19 @@ func (key *Ed25519PrivateKey) ToHex() string {
return util.BytesToHex(key.Bytes())
}

// ToAIP80 formats the private key to AIP-80 compliant string
func (key *Ed25519PrivateKey) ToAIP80() (formattedString string, err error) {
return FormatPrivateKey(key.ToHex(), PrivateKeyVariantEd25519)
}

// FromHex sets the [Ed25519PrivateKey] to the bytes represented by the hex string, with or without a leading 0x
//
// Errors if the hex string is not valid, or if the bytes length is not [ed25519.SeedSize].
//
// Implements:
// - [CryptoMaterial]
func (key *Ed25519PrivateKey) FromHex(hexStr string) (err error) {
bytes, err := util.ParseHex(hexStr)
bytes, err := ParsePrivateKey(hexStr, PrivateKeyVariantEd25519)
if err != nil {
return err
}
Expand Down
13 changes: 9 additions & 4 deletions crypto/ed25519_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package crypto

import (
"crypto/ed25519"
"testing"

"github.com/aptos-labs/aptos-go-sdk/bcs"
"github.com/aptos-labs/aptos-go-sdk/internal/util"
"github.com/stretchr/testify/assert"
"testing"
)

const testEd25519PrivateKey = "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5"
const testEd25519PrivateKey = "ed25519-priv-0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5"
const testEd25519PrivateKeyHex = "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5"
const testEd25519PublicKey = "0xde19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c"
const testEd25519Address = "0x978c213990c4833df71548df7ce49d54c759d6b6d932de22b24d56060b7af2aa"
const testEd25519Message = "0x68656c6c6f20776f726c64"
Expand All @@ -18,7 +20,7 @@ func TestEd25519Keys(t *testing.T) {
testEd25519PrivateKeyBytes := []byte{0xc5, 0x33, 0x8c, 0xd2, 0x51, 0xc2, 0x2d, 0xaa, 0x8c, 0x9c, 0x9c, 0xc9, 0x4f, 0x49, 0x8c, 0xc8, 0xa5, 0xc7, 0xe1, 0xd2, 0xe7, 0x52, 0x87, 0xa5, 0xdd, 0xa9, 0x10, 0x96, 0xfe, 0x64, 0xef, 0xa5}

// First ensure bytes and hex are the same
readBytes, err := util.ParseHex(testEd25519PrivateKey)
readBytes, err := util.ParseHex(testEd25519PrivateKeyHex)
assert.NoError(t, err)
assert.Equal(t, testEd25519PrivateKeyBytes, readBytes)

Expand All @@ -33,7 +35,10 @@ func TestEd25519Keys(t *testing.T) {

// The outputs should match as well
assert.Equal(t, privateKey.Bytes(), testEd25519PrivateKeyBytes)
assert.Equal(t, privateKey.ToHex(), testEd25519PrivateKey)
assert.Equal(t, privateKey.ToHex(), testEd25519PrivateKeyHex)
formattedString, err := privateKey.ToAIP80()
assert.NoError(t, err)
assert.Equal(t, formattedString, testEd25519PrivateKey)

// Auth key should match
assert.Equal(t, testEd25519Address, privateKey.AuthKey().ToHex())
Expand Down
93 changes: 93 additions & 0 deletions crypto/privateKey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package crypto

import (
"fmt"
"strings"

"github.com/aptos-labs/aptos-go-sdk/internal/util"
)

// PrivateKeyVariant represents the type of private key
type PrivateKeyVariant string

const (
PrivateKeyVariantEd25519 PrivateKeyVariant = "ed25519"
PrivateKeyVariantSecp256k1 PrivateKeyVariant = "secp256k1"
)

// AIP80Prefixes contains the AIP-80 compliant prefixes for each private key type
var AIP80Prefixes = map[PrivateKeyVariant]string{
PrivateKeyVariantEd25519: "ed25519-priv-",
PrivateKeyVariantSecp256k1: "secp256k1-priv-",
}

// FormatPrivateKey formats a hex input to an AIP-80 compliant string
func FormatPrivateKey(privateKey any, keyType PrivateKeyVariant) (formattedString string, err error) {
aip80Prefix := AIP80Prefixes[keyType]

var hexStr string
switch v := privateKey.(type) {
case string:
// Remove the prefix if it exists
if strings.HasPrefix(v, aip80Prefix) {
parts := strings.Split(v, "-")
v = parts[2]
}

// If it's already a string, just ensure it's properly formatted
var strBytes, err = util.ParseHex(v)
if err != nil {
return "", err
}

// Reformat to have 0x prefix
hexStr = util.BytesToHex(strBytes)
case []byte:
hexStr = util.BytesToHex(v)
default:
return "", fmt.Errorf("unsupported private key type: must be string or []byte")
}

return fmt.Sprintf("%s%s", aip80Prefix, hexStr), nil
}

// ParseHexInput parses a hex input that may be bytes, hex string, or an AIP-80 compliant string to bytes.
//
// You may optionally pass in a boolean to strictly enforce AIP-80 compliance.
func ParsePrivateKey(value any, keyType PrivateKeyVariant, strict ...bool) (bytes []byte, err error) {
aip80Prefix := AIP80Prefixes[keyType]

// Get the first boolean if it exists, otherwise nil
var strictness *bool = nil
if len(strict) > 1 {
return nil, fmt.Errorf("strictness must be a single boolean")
} else if len(strict) == 1 {
strictness = &strict[0]
}

switch v := value.(type) {
case string:
if (strictness == nil || !*strictness) && !strings.HasPrefix(v, aip80Prefix) {
GhostWalker562 marked this conversation as resolved.
Show resolved Hide resolved
bytes, err := util.ParseHex(v)
if err != nil {
return nil, err
}

// If strictness is not explicitly false, warn about non-AIP-80 compliance
if strictness == nil {
fmt.Printf("[Aptos SDK] It is recommended that private keys are AIP-80 compliant (https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-80.md). You can fix the private key by formatting it with crypto.FormatPrivateKey")
}

return bytes, nil
} else if strings.HasPrefix(v, aip80Prefix) {
// Parse for AIP-80 compliant String input
parts := strings.Split(v, "-")
return util.ParseHex(parts[2])
}
return nil, fmt.Errorf("invalid hex string input while parsing private key. Must be AIP-80 compliant string")
case []byte:
return v, nil
default:
return nil, fmt.Errorf("unsupported private key type: must be string or []byte")
}
}
12 changes: 11 additions & 1 deletion crypto/secp256k1.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package crypto
import (
"crypto/ecdsa"
"fmt"

"github.com/aptos-labs/aptos-go-sdk/bcs"
"github.com/aptos-labs/aptos-go-sdk/internal/util"
ethCrypto "github.com/ethereum/go-ethereum/crypto"
Expand Down Expand Up @@ -98,6 +99,10 @@ func (key *Secp256k1PrivateKey) Bytes() []byte {
// Implements:
// - [CryptoMaterial]
func (key *Secp256k1PrivateKey) FromBytes(bytes []byte) (err error) {
bytes, err = ParsePrivateKey(bytes, PrivateKeyVariantSecp256k1, false)
if err != nil {
return err
}
GhostWalker562 marked this conversation as resolved.
Show resolved Hide resolved
if len(bytes) != Secp256k1PrivateKeyLength {
return fmt.Errorf("invalid secp256k1 private key size %d", len(bytes))
}
Expand All @@ -117,6 +122,11 @@ func (key *Secp256k1PrivateKey) ToHex() string {
return util.BytesToHex(key.Bytes())
}

// ToAIP80 formats the private key to AIP-80 compliant string
func (key *Secp256k1PrivateKey) ToAIP80() (formattedString string, err error) {
return FormatPrivateKey(key.ToHex(), PrivateKeyVariantSecp256k1)
}

//endregion

// FromHex populates the [Secp256k1PrivateKey] from a hex string
Expand All @@ -126,7 +136,7 @@ func (key *Secp256k1PrivateKey) ToHex() string {
// Implements:
// - [CryptoMaterial]
func (key *Secp256k1PrivateKey) FromHex(hexStr string) (err error) {
bytes, err := util.ParseHex(hexStr)
bytes, err := ParsePrivateKey(hexStr, PrivateKeyVariantSecp256k1)
if err != nil {
return err
}
Expand Down
13 changes: 9 additions & 4 deletions crypto/secp256k1_test.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
package crypto

import (
"testing"

"github.com/aptos-labs/aptos-go-sdk/bcs"
"github.com/aptos-labs/aptos-go-sdk/internal/util"
"github.com/stretchr/testify/assert"
"testing"
)

const (
testSecp256k1PrivateKey = "0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e"
testSecp256k1PrivateKey = "secp256k1-priv-0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e"
testSecp256k1PrivateKeyHex = "0xd107155adf816a0a94c6db3c9489c13ad8a1eda7ada2e558ba3bfa47c020347e"
testSecp256k1PublicKey = "0x04acdd16651b839c24665b7e2033b55225f384554949fef46c397b5275f37f6ee95554d70fb5d9f93c5831ebf695c7206e7477ce708f03ae9bb2862dc6c9e033ea"
testSecp256k1Address = "0x5792c985bc96f436270bd2a3c692210b09c7febb8889345ceefdbae4bacfe498"
testSecp256k1MessageEncoded = "0x68656c6c6f20776f726c64"
testSecp256k1Signature = "0xd0d634e843b61339473b028105930ace022980708b2855954b977da09df84a770c0b68c29c8ca1b5409a5085b0ec263be80e433c83fcf6debb82f3447e71edca"
)

func TestSecp256k1Keys(t *testing.T) {
testSecp256k1PrivateKeyBytes, err := util.ParseHex(testSecp256k1PrivateKey)
testSecp256k1PrivateKeyBytes, err := util.ParseHex(testSecp256k1PrivateKeyHex)
assert.NoError(t, err)

// Either bytes or hex should work
Expand All @@ -30,7 +32,10 @@ func TestSecp256k1Keys(t *testing.T) {

// The outputs should match as well
assert.Equal(t, privateKey.Bytes(), testSecp256k1PrivateKeyBytes)
assert.Equal(t, privateKey.ToHex(), testSecp256k1PrivateKey)
assert.Equal(t, privateKey.ToHex(), testSecp256k1PrivateKeyHex)
formattedString, err := privateKey.ToAIP80()
assert.NoError(t, err)
assert.Equal(t, formattedString, testSecp256k1PrivateKey)

// Auth key should match
singleSender := SingleSigner{privateKey}
Expand Down
2 changes: 1 addition & 1 deletion examples/fungible_asset/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/aptos-labs/aptos-go-sdk/crypto"
)

const testEd25519PrivateKey = "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5"
const testEd25519PrivateKey = "ed25519-priv-0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5"
const rupeePublisherAddress = "0x978c213990c4833df71548df7ce49d54c759d6b6d932de22b24d56060b7af2aa"

// These come from fungible_asset.json
Expand Down