Merge pull request #22 from MetaMask/crypto-readme
document the crypto package
This commit is contained in:
93
crypto/Readme.md
Normal file
93
crypto/Readme.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Crypto package
|
||||
|
||||
This crypto package is a thin ergonomic layer on top of the normal golang crypto packages or `x/crypto`.
|
||||
|
||||
It aims to solve the following problems with the standard crypto packages:
|
||||
- different algorithms have different APIs and ergonomics, which makes it hard to use them interchangeably
|
||||
- occasionally, it's quite hard to figure out how to do simple tasks (like encoding/decoding keys)
|
||||
- it's still necessary to make some educated choices (e.g. which hash function to use for signatures)
|
||||
- sometimes features are left out (e.g. ed25519 to X25519 for key exchange, secp256k1...)
|
||||
- some hash functions are not available in the standard library with no easy way to extend it (e.g. KECCAK-256)
|
||||
|
||||
To do so, this package provides and implements a set of shared interfaces for all algorithms. As not all algorithms
|
||||
support all features (e.g. RSA keys don't support key exchange), some interfaces are optionally implemented.
|
||||
|
||||
An additional benefit of shared interfaces is that a shared test suite can be written to test all algorithms, which this
|
||||
package does.
|
||||
|
||||
Note: this is not a dig or a criticism of the golang crypto packages, just an attempt to make them easier to use.
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
// This example demonstrates how to use the crypto package without going over all the features.
|
||||
// We will use P-256 keys, but they all work the same way (although not all have all the features).
|
||||
|
||||
// 0: Generate a key pair
|
||||
pubAlice, privAlice, err := p256.GenerateKeyPair()
|
||||
handleErr(err)
|
||||
|
||||
// 1: Serialize a key, read it back, verify it's the same
|
||||
privAliceBytes := privAlice.ToPKCS8DER()
|
||||
privAlice2, err := p256.PrivateKeyFromPKCS8DER(privAliceBytes)
|
||||
handleErr(err)
|
||||
fmt.Println("Keys are equals:", privAlice.Equal(privAlice2))
|
||||
|
||||
// 2: Sign a message, verify the signature.
|
||||
// Signatures can be made in raw bytes (SignToBytes) or ASN.1 DER format (SignToASN1).
|
||||
msg := []byte("hello world")
|
||||
sig, err := privAlice.SignToBytes(msg)
|
||||
handleErr(err)
|
||||
fmt.Println("Signature is valid:", pubAlice.VerifyBytes(msg, sig))
|
||||
|
||||
// 3: Signatures are done with an opinionated default configuration, but you can override it.
|
||||
// For example, the default hash function for P-256 is SHA-256, but you can use SHA-384 instead.
|
||||
opts := []crypto.SigningOption{crypto.WithSigningHash(crypto.SHA384)}
|
||||
sig384, err := privAlice.SignToBytes(msg, opts...)
|
||||
handleErr(err)
|
||||
fmt.Println("Signature is valid (SHA-384):", pubAlice.VerifyBytes(msg, sig384, opts...))
|
||||
|
||||
// 4: Key exchange: generate a second key-pair and compute a shared secret.
|
||||
// ⚠️ Security Warning: The shared secret returned by key agreement should NOT be used directly as an encryption key.
|
||||
// It must be processed through a Key Derivation Function (KDF) such as HKDF before being used in cryptographic protocols.
|
||||
// Using the raw shared secret directly can lead to security vulnerabilities.
|
||||
pubBob, privBob, err := p256.GenerateKeyPair()
|
||||
handleErr(err)
|
||||
shared1, err := privAlice.KeyExchange(pubBob)
|
||||
handleErr(err)
|
||||
shared2, err := privBob.KeyExchange(pubAlice)
|
||||
handleErr(err)
|
||||
fmt.Println("Shared secrets are identical:", bytes.Equal(shared1, shared2))
|
||||
|
||||
// 5: Bonus: one very annoying thing in cryptographic protocols is that the other side needs to know the configuration
|
||||
// you used for your signature. Having defaults or implied config only work sor far.
|
||||
// To solve this problem, this package integrates varsig: a format to describe the signing configuration. This varsig
|
||||
// can be attached to the signature, and the other side doesn't have to guess any more. Here is how it works:
|
||||
varsigBytes := privAlice.Varsig(opts...).Encode()
|
||||
fmt.Println("Varsig:", base64.StdEncoding.EncodeToString(varsigBytes))
|
||||
sig, err = privAlice.SignToBytes(msg, opts...)
|
||||
handleErr(err)
|
||||
varsigDecoded, err := varsig.Decode(varsigBytes)
|
||||
handleErr(err)
|
||||
fmt.Println("Signature with varsig is valid:", pubAlice.VerifyBytes(msg, sig, crypto.WithVarsig(varsigDecoded)))
|
||||
|
||||
// Output:
|
||||
// Keys are equals: true
|
||||
// Signature is valid: true
|
||||
// Signature is valid (SHA-384): true
|
||||
// Shared secrets are identical: true
|
||||
// Varsig: NAHsAYAkIF8=
|
||||
// Signature with varsig is valid: true
|
||||
```
|
||||
|
||||
## Supported Cryptographic Algorithms
|
||||
|
||||
| Algorithm | Signature Format | Public Key Formats | Private Key Formats | Key Agreement |
|
||||
|-----------------|-------------------|-------------------------------------|---------------------------|----------------|
|
||||
| Ed25519 | Raw bytes, ASN.1 | Raw bytes, X.509 DER/PEM, Multibase | Raw bytes, PKCS#8 DER/PEM | ✅ (via X25519) |
|
||||
| ECDSA P-256 | Raw bytes, ASN.1 | Raw bytes, X.509 DER/PEM, Multibase | Raw bytes, PKCS#8 DER/PEM | ✅ |
|
||||
| ECDSA P-384 | Raw bytes, ASN.1 | Raw bytes, X.509 DER/PEM, Multibase | Raw bytes, PKCS#8 DER/PEM | ✅ |
|
||||
| ECDSA P-521 | Raw bytes, ASN.1 | Raw bytes, X.509 DER/PEM, Multibase | Raw bytes, PKCS#8 DER/PEM | ✅ |
|
||||
| ECDSA secp256k1 | Raw bytes, ASN.1 | Raw bytes, X.509 DER/PEM, Multibase | Raw bytes, PKCS#8 DER/PEM | ✅ |
|
||||
| RSA | PKCS#1 v1.5 ASN.1 | X.509 DER/PEM, Multibase | PKCS#8 DER/PEM | ❌ |
|
||||
| X25519 | ❌ | Raw bytes, X.509 DER/PEM, Multibase | Raw bytes, PKCS#8 DER/PEM | ✅ |
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/MetaMask/go-did-it/crypto"
|
||||
)
|
||||
|
||||
// TestHarness describes a keypair implementation for the test/bench suites to operate on.
|
||||
type TestHarness[PubT crypto.PublicKey, PrivT crypto.PrivateKey] struct {
|
||||
Name string
|
||||
|
||||
@@ -38,6 +39,7 @@ type TestHarness[PubT crypto.PublicKey, PrivT crypto.PrivateKey] struct {
|
||||
SignatureBytesSize int
|
||||
}
|
||||
|
||||
// TestSuite runs a set of tests against a keypair implementation.
|
||||
func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, harness TestHarness[PubT, PrivT]) {
|
||||
stats := struct {
|
||||
bytesPubSize int
|
||||
@@ -337,6 +339,7 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har
|
||||
})
|
||||
}
|
||||
|
||||
// BenchSuite runs a set of benchmarks on a keypair implementation.
|
||||
func BenchSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](b *testing.B, harness TestHarness[PubT, PrivT]) {
|
||||
b.Run("GenerateKeyPair", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
79
crypto/example_test.go
Normal file
79
crypto/example_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package crypto_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/ucan-wg/go-varsig"
|
||||
|
||||
"github.com/MetaMask/go-did-it/crypto"
|
||||
"github.com/MetaMask/go-did-it/crypto/p256"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
// This example demonstrates how to use the crypto package without going over all the features.
|
||||
// We will use P-256 keys, but they all work the same way (although not all have all the features).
|
||||
|
||||
// 0: Generate a key pair
|
||||
pubAlice, privAlice, err := p256.GenerateKeyPair()
|
||||
handleErr(err)
|
||||
|
||||
// 1: Serialize a key, read it back, verify it's the same
|
||||
privAliceBytes := privAlice.ToPKCS8DER()
|
||||
privAlice2, err := p256.PrivateKeyFromPKCS8DER(privAliceBytes)
|
||||
handleErr(err)
|
||||
fmt.Println("Keys are equals:", privAlice.Equal(privAlice2))
|
||||
|
||||
// 2: Sign a message, verify the signature.
|
||||
// Signatures can be made in raw bytes (SignToBytes) or ASN.1 DER format (SignToASN1).
|
||||
msg := []byte("hello world")
|
||||
sig, err := privAlice.SignToBytes(msg)
|
||||
handleErr(err)
|
||||
fmt.Println("Signature is valid:", pubAlice.VerifyBytes(msg, sig))
|
||||
|
||||
// 3: Signatures are done with an opinionated default configuration, but you can override it.
|
||||
// For example, the default hash function for P-256 is SHA-256, but you can use SHA-384 instead.
|
||||
opts := []crypto.SigningOption{crypto.WithSigningHash(crypto.SHA384)}
|
||||
sig384, err := privAlice.SignToBytes(msg, opts...)
|
||||
handleErr(err)
|
||||
fmt.Println("Signature is valid (SHA-384):", pubAlice.VerifyBytes(msg, sig384, opts...))
|
||||
|
||||
// 4: Key exchange: generate a second key-pair and compute a shared secret.
|
||||
// ⚠️ Security Warning: The shared secret returned by key agreement should NOT be used directly as an encryption key.
|
||||
// It must be processed through a Key Derivation Function (KDF) such as HKDF before being used in cryptographic protocols.
|
||||
// Using the raw shared secret directly can lead to security vulnerabilities.
|
||||
pubBob, privBob, err := p256.GenerateKeyPair()
|
||||
handleErr(err)
|
||||
shared1, err := privAlice.KeyExchange(pubBob)
|
||||
handleErr(err)
|
||||
shared2, err := privBob.KeyExchange(pubAlice)
|
||||
handleErr(err)
|
||||
fmt.Println("Shared secrets are identical:", bytes.Equal(shared1, shared2))
|
||||
|
||||
// 5: Bonus: one very annoying thing in cryptographic protocols is that the other side needs to know the configuration
|
||||
// you used for your signature. Having defaults or implied config only work sor far.
|
||||
// To solve this problem, this package integrates varsig: a format to describe the signing configuration. This varsig
|
||||
// can be attached to the signature, and the other side doesn't have to guess any more. Here is how it works:
|
||||
varsigBytes := privAlice.Varsig(opts...).Encode()
|
||||
fmt.Println("Varsig:", base64.StdEncoding.EncodeToString(varsigBytes))
|
||||
sig, err = privAlice.SignToBytes(msg, opts...)
|
||||
handleErr(err)
|
||||
varsigDecoded, err := varsig.Decode(varsigBytes)
|
||||
handleErr(err)
|
||||
fmt.Println("Signature with varsig is valid:", pubAlice.VerifyBytes(msg, sig, crypto.WithVarsig(varsigDecoded)))
|
||||
|
||||
// Output:
|
||||
// Keys are equals: true
|
||||
// Signature is valid: true
|
||||
// Signature is valid (SHA-384): true
|
||||
// Shared secrets are identical: true
|
||||
// Varsig: NAHsAYAkIF8=
|
||||
// Signature with varsig is valid: true
|
||||
}
|
||||
|
||||
func handleErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
// As the standard crypto library prohibits from registering additional hash algorithm (like keccak),
|
||||
// below is essentially an extension of that mechanism to allow it.
|
||||
|
||||
// Hash is similar to crypto.Hash but can be extended with more values.
|
||||
type Hash uint
|
||||
|
||||
const (
|
||||
@@ -50,6 +51,7 @@ func (h Hash) HashFunc() Hash {
|
||||
return h
|
||||
}
|
||||
|
||||
// String returns the name of the hash function.
|
||||
func (h Hash) String() string {
|
||||
if h < maxStdHash {
|
||||
return stdcrypto.Hash(h).String()
|
||||
@@ -75,6 +77,7 @@ func (h Hash) New() hash.Hash {
|
||||
panic("requested hash function #" + strconv.Itoa(int(h)) + " is unavailable")
|
||||
}
|
||||
|
||||
// ToVarsigHash returns the corresponding varsig.Hash value.
|
||||
func (h Hash) ToVarsigHash() varsig.Hash {
|
||||
if h == MD5SHA1 {
|
||||
panic("no multihash/multicodec value exists for MD5+SHA1")
|
||||
@@ -85,6 +88,7 @@ func (h Hash) ToVarsigHash() varsig.Hash {
|
||||
panic("requested hash #" + strconv.Itoa(int(h)) + " is unavailable")
|
||||
}
|
||||
|
||||
// FromVarsigHash converts a varsig.Hash value to the corresponding Hash value.
|
||||
func FromVarsigHash(h varsig.Hash) Hash {
|
||||
switch h {
|
||||
case varsig.HashMd4:
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/MetaMask/go-did-it/crypto/x25519"
|
||||
)
|
||||
|
||||
// PrivateJwk is a JWK holding a private key
|
||||
type PrivateJwk struct {
|
||||
Privkey crypto.PrivateKey
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
// - https://www.rfc-editor.org/rfc/rfc7517#section-4 (JWK)
|
||||
// - https://www.iana.org/assignments/jose/jose.xhtml#web-key-types (key parameters)
|
||||
|
||||
// PublicJwk is a JWK holding a public key
|
||||
type PublicJwk struct {
|
||||
Pubkey crypto.PublicKey
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/ucan-wg/go-varsig"
|
||||
)
|
||||
|
||||
// SigningOpts contains the resulting signature configuration.
|
||||
type SigningOpts struct {
|
||||
hash Hash
|
||||
payloadEncoding varsig.PayloadEncoding
|
||||
@@ -14,6 +15,7 @@ type SigningOpts struct {
|
||||
keyLen uint64
|
||||
}
|
||||
|
||||
// CollectSigningOptions collects the signing options into a SigningOpts.
|
||||
func CollectSigningOptions(opts []SigningOption) SigningOpts {
|
||||
res := SigningOpts{}
|
||||
for _, opt := range opts {
|
||||
@@ -22,6 +24,7 @@ func CollectSigningOptions(opts []SigningOption) SigningOpts {
|
||||
return res
|
||||
}
|
||||
|
||||
// HashOrDefault returns the hash algorithm to be used for signatures, or the default if not specified.
|
||||
func (opts SigningOpts) HashOrDefault(_default Hash) Hash {
|
||||
if opts.hash == 0 {
|
||||
return _default
|
||||
@@ -29,6 +32,7 @@ func (opts SigningOpts) HashOrDefault(_default Hash) Hash {
|
||||
return opts.hash
|
||||
}
|
||||
|
||||
// PayloadEncoding returns the encoding used on the message before signing it.
|
||||
func (opts SigningOpts) PayloadEncoding() varsig.PayloadEncoding {
|
||||
if opts.payloadEncoding == 0 {
|
||||
return varsig.PayloadEncodingVerbatim
|
||||
@@ -36,6 +40,7 @@ func (opts SigningOpts) PayloadEncoding() varsig.PayloadEncoding {
|
||||
return opts.payloadEncoding
|
||||
}
|
||||
|
||||
// VarsigMatch returns true if the given varsig parameters match the signing options.
|
||||
func (opts SigningOpts) VarsigMatch(algo varsig.Algorithm, curve uint64, keyLength uint64) bool {
|
||||
// This is relatively ugly, but we get cyclic import otherwise
|
||||
switch opts.algo {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Package crypto is a thin ergonomic layer on top of the normal golang crypto packages or `x/crypto`.
|
||||
//
|
||||
// It aims to solve the following problems with the standard crypto packages:
|
||||
// - different algorithms have different APIs and ergonomics, which makes it hard to use them interchangeably
|
||||
// - occasionally, it's quite hard to figure out how to do simple tasks (like encoding/decoding keys)
|
||||
// - it's still necessary to make some educated choices (e.g. which hash function to use for signatures)
|
||||
// - sometimes features are left out (e.g. ed25519 to X25519 for key exchange, secp256k1...)
|
||||
// - some hash functions are not available in the standard library with no easy way to extend it (e.g. KECCAK-256)
|
||||
//
|
||||
// To do so, this package provides and implements a set of shared interfaces for all algorithms. As not all algorithms
|
||||
// support all features (e.g. RSA keys don't support key exchange), some interfaces are optionally implemented.
|
||||
//
|
||||
// An additional benefit of shared interfaces is that a shared test suite can be written to test all algorithms, which this
|
||||
// package does.
|
||||
package crypto
|
||||
|
||||
type PublicKey interface {
|
||||
|
||||
Reference in New Issue
Block a user