Skip to content

Commit

Permalink
Merge pull request #462 from okp4/feat/crypto-verify
Browse files Browse the repository at this point in the history
🧠 Logic: 🔑 implement `verify_signature`
  • Loading branch information
ccamel committed Oct 27, 2023
2 parents c51665e + e84d018 commit 7b8bea0
Show file tree
Hide file tree
Showing 16 changed files with 990 additions and 49 deletions.
78 changes: 77 additions & 1 deletion docs/predicate/predicates.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,83 @@ Examples:
- did_components('did:example:123456?versionId=1', did(Method, ID, Path, Query, Fragment)).
# Reconstruct a DID from its components.
- did_components(DID, did('example', '123456', null, 'versionId=1', _42)).
- did_components(DID, did('example', '123456', _, 'versionId=1', _42)).
```

## ecdsa_verify/4

ecdsa_verify/4 determines if a given signature is valid as per the ECDSA algorithm for the provided data, using the specified public key.

The signature is as follows:

```text
ecdsa_verify(+PubKey, +Data, +Signature, +Options), which is semi-deterministic.
```

Where:

- PubKey is the 33\-byte compressed public key, as specified in section 4.3.6 of ANSI X9.62.

- Data is the hash of the signed message, which can be either an atom or a list of bytes.

- Signature represents the ASN.1 encoded signature corresponding to the Data.

- Options are additional configurations for the verification process. Supported options include: encoding\(\+Format\) which specifies the encoding used for the data, and type\(\+Alg\) which chooses the algorithm within the ECDSA family \(see below for details\).

For Format, the supported encodings are:

- hex \(default\), the hexadecimal encoding represented as an atom.
- octet, the plain byte encoding depicted as a list of integers ranging from 0 to 255.

For Alg, the supported algorithms are:

- secp256r1 \(default\): Also known as P\-256 and prime256v1.
- secp256k1: The Koblitz elliptic curve used in Bitcoin's public\-key cryptography.

Examples:

```text
# Verify a signature for hexadecimal data using the ECDSA secp256r1 algorithm.
- ecdsa_verify([127, ...], '9b038f8ef6918cbb56040dfda401b56b...', [23, 56, ...], encoding(hex))
# Verify a signature for binary data using the ECDSA secp256k1 algorithm.
- ecdsa_verify([127, ...], [56, 90, ..], [23, 56, ...], [encoding(octet), type(secp256k1)])
```

## eddsa_verify/4

eddsa_verify/4 determines if a given signature is valid as per the EdDSA algorithm for the provided data, using the specified public key.

The signature is as follows:

```text
eddsa_verify(+PubKey, +Data, +Signature, +Options) is semi-det
```

Where:

- PubKey is the encoded public key as a list of bytes.
- Data is the message to verify, represented as either a hexadecimal atom or a list of bytes. It's important that the message isn't pre\-hashed since the Ed25519 algorithm processes messages in two passes when signing.
- Signature represents the signature corresponding to the data, provided as a list of bytes.
- Options are additional configurations for the verification process. Supported options include: encoding\(\+Format\) which specifies the encoding used for the Data, and type\(\+Alg\) which chooses the algorithm within the EdDSA family \(see below for details\).

For Format, the supported encodings are:

- hex \(default\), the hexadecimal encoding represented as an atom.
- octet, the plain byte encoding depicted as a list of integers ranging from 0 to 255.

For Alg, the supported algorithms are:

- ed25519 \(default\): The EdDSA signature scheme using SHA\-512 \(SHA\-2\) and Curve25519.

Examples:

```text
# Verify a signature for a given hexadecimal data.
- eddsa_verify([127, ...], '9b038f8ef6918cbb56040dfda401b56b...', [23, 56, ...], [encoding(hex), type(ed25519)])
# Verify a signature for binary data.
- eddsa_verify([127, ...], [56, 90, ..], [23, 56, ...], [encoding(octet), type(ed25519)])
```

## hex_bytes/2
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ require (
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.15.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJLNCEi7YHVMkwwtfSr2k9splgdSM=
github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8=
github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM=
github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
Expand Down
2 changes: 2 additions & 0 deletions x/logic/interpreter/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ var registry = map[string]any{
"json_prolog/2": predicate.JSONProlog,
"uri_encoded/3": predicate.URIEncoded,
"read_string/3": predicate.ReadString,
"eddsa_verify/4": predicate.EDDSAVerify,
"ecdsa_verify/4": predicate.ECDSAVerify,
}

// RegistryNames is the list of the predicate names in the Registry.
Expand Down
2 changes: 1 addition & 1 deletion x/logic/predicate/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func TestBech32(t *testing.T) {
},
{
query: `bech32_address(-('okp4', ['8956',167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: failed to convert term to bytes list: invalid term type in list engine.Atom, only integer allowed"),
wantError: fmt.Errorf("bech32_address/2: failed to convert term to bytes list: invalid term type in list at position 1: engine.Atom, only engine.Integer allowed"),
wantSuccess: false,
},
{
Expand Down
36 changes: 19 additions & 17 deletions x/logic/predicate/atom.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,31 @@ import (
"github.com/ichiban/prolog/engine"
)

// AtomPair are terms with principal functor (-)/2.
// For example, the term -(A, B) denotes the pair of elements A and B.
var AtomPair = engine.NewAtom("-")
var (
// AtomPair are terms with principal functor (-)/2.
// For example, the term -(A, B) denotes the pair of elements A and B.
AtomPair = engine.NewAtom("-")

// AtomJSON are terms with principal functor json/1.
// It is used to represent json objects.
var AtomJSON = engine.NewAtom("json")
// AtomJSON are terms with principal functor json/1.
// It is used to represent json objects.
AtomJSON = engine.NewAtom("json")

// AtomAt are terms with principal functor (@)/1.
// It is used to represent special values in json objects.
var AtomAt = engine.NewAtom("@")
// AtomAt are terms with principal functor (@)/1.
// It is used to represent special values in json objects.
AtomAt = engine.NewAtom("@")

// AtomTrue is the term true.
var AtomTrue = engine.NewAtom("true")
// AtomTrue is the term true.
AtomTrue = engine.NewAtom("true")

// AtomFalse is the term false.
var AtomFalse = engine.NewAtom("false")
// AtomFalse is the term false.
AtomFalse = engine.NewAtom("false")

// AtomEmptyArray is the term [].
var AtomEmptyArray = engine.NewAtom("[]")
// AtomEmptyArray is the term [].
AtomEmptyArray = engine.NewAtom("[]")

// AtomNull is the term null.
var AtomNull = engine.NewAtom("null")
// AtomNull is the term null.
AtomNull = engine.NewAtom("null")
)

// MakeNull returns the compound term @(null).
// It is used to represent the null value in json objects.
Expand Down
135 changes: 133 additions & 2 deletions x/logic/predicate/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"context"
"encoding/hex"
"fmt"
"slices"
"strings"

"github.com/ichiban/prolog/engine"

"github.com/cometbft/cometbft/crypto"
cometcrypto "github.com/cometbft/cometbft/crypto"

"github.com/okp4/okp4d/x/logic/util"
)
Expand Down Expand Up @@ -35,7 +37,7 @@ func SHAHash(vm *engine.VM, data, hash engine.Term, cont engine.Cont, env *engin
var result []byte
switch d := env.Resolve(data).(type) {
case engine.Atom:
result = crypto.Sha256([]byte(d.String()))
result = cometcrypto.Sha256([]byte(d.String()))
return engine.Unify(vm, hash, BytesToList(result), cont, env)
default:
return engine.Error(fmt.Errorf("sha_hash/2: invalid data type: %T, should be Atom", d))
Expand Down Expand Up @@ -97,3 +99,132 @@ func HexBytes(vm *engine.VM, hexa, bts engine.Term, cont engine.Cont, env *engin
}
})
}

// EDDSAVerify determines if a given signature is valid as per the EdDSA algorithm for the provided data, using the
// specified public key.
//
// The signature is as follows:
//
// eddsa_verify(+PubKey, +Data, +Signature, +Options) is semi-det
//
// Where:
// - PubKey is the encoded public key as a list of bytes.
// - Data is the message to verify, represented as either a hexadecimal atom or a list of bytes.
// It's important that the message isn't pre-hashed since the Ed25519 algorithm processes
// messages in two passes when signing.
// - Signature represents the signature corresponding to the data, provided as a list of bytes.
// - Options are additional configurations for the verification process. Supported options include:
// encoding(+Format) which specifies the encoding used for the Data, and type(+Alg) which chooses the algorithm
// within the EdDSA family (see below for details).
//
// For Format, the supported encodings are:
//
// - hex (default), the hexadecimal encoding represented as an atom.
// - octet, the plain byte encoding depicted as a list of integers ranging from 0 to 255.
//
// For Alg, the supported algorithms are:
//
// - ed25519 (default): The EdDSA signature scheme using SHA-512 (SHA-2) and Curve25519.
//
// Examples:
//
// # Verify a signature for a given hexadecimal data.
// - eddsa_verify([127, ...], '9b038f8ef6918cbb56040dfda401b56b...', [23, 56, ...], [encoding(hex), type(ed25519)])
//
// # Verify a signature for binary data.
// - eddsa_verify([127, ...], [56, 90, ..], [23, 56, ...], [encoding(octet), type(ed25519)])
func EDDSAVerify(_ *engine.VM, key, data, sig, options engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return xVerify("eddsa_verify/4", key, data, sig, options, util.Ed25519, []util.Alg{util.Ed25519}, cont, env)
}

// ECDSAVerify determines if a given signature is valid as per the ECDSA algorithm for the provided data, using the
// specified public key.
//
// The signature is as follows:
//
// ecdsa_verify(+PubKey, +Data, +Signature, +Options), which is semi-deterministic.
//
// Where:
//
// - PubKey is the 33-byte compressed public key, as specified in section 4.3.6 of ANSI X9.62.
//
// - Data is the hash of the signed message, which can be either an atom or a list of bytes.
//
// - Signature represents the ASN.1 encoded signature corresponding to the Data.
//
// - Options are additional configurations for the verification process. Supported options include:
// encoding(+Format) which specifies the encoding used for the data, and type(+Alg) which chooses the algorithm
// within the ECDSA family (see below for details).
//
// For Format, the supported encodings are:
//
// - hex (default), the hexadecimal encoding represented as an atom.
// - octet, the plain byte encoding depicted as a list of integers ranging from 0 to 255.
//
// For Alg, the supported algorithms are:
//
// - secp256r1 (default): Also known as P-256 and prime256v1.
// - secp256k1: The Koblitz elliptic curve used in Bitcoin's public-key cryptography.
//
// Examples:
//
// # Verify a signature for hexadecimal data using the ECDSA secp256r1 algorithm.
// - ecdsa_verify([127, ...], '9b038f8ef6918cbb56040dfda401b56b...', [23, 56, ...], encoding(hex))
//
// # Verify a signature for binary data using the ECDSA secp256k1 algorithm.
// - ecdsa_verify([127, ...], [56, 90, ..], [23, 56, ...], [encoding(octet), type(secp256k1)])
func ECDSAVerify(_ *engine.VM, key, data, sig, options engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return xVerify("ecdsa_verify/4", key, data, sig, options, util.Secp256r1, []util.Alg{util.Secp256r1, util.Secp256k1}, cont, env)
}

// xVerify return `true` if the Signature can be verified as the signature for Data, using the given PubKey for a
// considered algorithm.
// This is a generic predicate implementation that can be used to verify any signature.
func xVerify(functor string, key, data, sig, options engine.Term, defaultAlgo util.Alg,
algos []util.Alg, cont engine.Cont, env *engine.Env,
) *engine.Promise {
typeOpt := engine.NewAtom("type")
return engine.Delay(func(ctx context.Context) *engine.Promise {
typeTerm, err := util.GetOptionWithDefault(typeOpt, options, engine.NewAtom(defaultAlgo.String()), env)
if err != nil {
return engine.Error(fmt.Errorf("%s: %w", functor, err))
}
typeAtom, err := util.ResolveToAtom(env, typeTerm)
if err != nil {
return engine.Error(fmt.Errorf("%s: %w", functor, err))
}

if idx := slices.IndexFunc(algos, func(a util.Alg) bool { return a.String() == typeAtom.String() }); idx == -1 {
return engine.Error(fmt.Errorf("%s: invalid type: %s. Possible values: %s",
functor,
typeAtom.String(),
strings.Join(util.Map(algos, func(a util.Alg) string { return a.String() }), ", ")))
}

decodedKey, err := TermToBytes(key, AtomEncoding.Apply(AtomOctet), env)
if err != nil {
return engine.Error(fmt.Errorf("%s: failed to decode public key: %w", functor, err))
}

decodedData, err := TermToBytes(data, options, env)
if err != nil {
return engine.Error(fmt.Errorf("%s: failed to decode data: %w", functor, err))
}

decodedSignature, err := TermToBytes(sig, AtomEncoding.Apply(AtomOctet), env)
if err != nil {
return engine.Error(fmt.Errorf("%s: failed to decode signature: %w", functor, err))
}

r, err := util.VerifySignature(util.Alg(typeAtom.String()), decodedKey, decodedData, decodedSignature)
if err != nil {
return engine.Error(fmt.Errorf("%s: failed to verify signature: %w", functor, err))
}

if !r {
return engine.Bool(false)
}

return cont(env)
})
}
Loading

0 comments on commit 7b8bea0

Please sign in to comment.