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 v3.public support #1

Merged
merged 6 commits into from
May 14, 2022
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go: [1.16.x, 1.17.x]
go: [1.16.x, 1.17.x, 1.18.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,6 @@ If everything succeeds, the value in `parsedToken` will be equivalent to that in
## Version 4
Version 4 is fully supported.
## Version 3
Version 3 supports only local mode (so far).
Version 3 is fully supported.
## Version 2
Version 2 is fully supported.
6 changes: 6 additions & 0 deletions claims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,12 @@ func TestAllClaimsPassV3(t *testing.T) {
token.SetIssuedAt(time.Now())

key := NewV3SymmetricKey()
secretKey := NewV3AsymmetricSecretKey()

encrypted := token.V3Encrypt(key, nil)

signed := token.V3Sign(secretKey, nil)

parser := NewParser()
parser.AddRule(ForAudience("a"))
parser.AddRule(IdentifiedBy("b"))
Expand All @@ -67,6 +70,9 @@ func TestAllClaimsPassV3(t *testing.T) {

_, err := parser.ParseV3Local(key, encrypted, nil)
require.NoError(t, err)

_, err = parser.ParseV3Public(secretKey.Public(), signed, nil)
require.NoError(t, err)
}

func TestAllClaimsPassV4(t *testing.T) {
Expand Down
12 changes: 12 additions & 0 deletions message.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ func (m message) v2Decrypt(key V2SymmetricKey) (*Token, error) {
return packet.token()
}

// V3Verify will verify a v4 public paseto message. Will return a pointer to
// the verified token (but not validated with rules) if successful, or error in
// the event of failure.
func (m message) v3Verify(key V3AsymmetricPublicKey, implicit []byte) (*Token, error) {
packet, err := v3PublicVerify(m, key, implicit)
if err != nil {
return nil, err
}

return packet.token()
}

// V3Decrypt will decrypt a v3 local paseto message. Will return a pointer to
// the decrypted token (but not validated with rules) if successful, or error in
// the event of failure.
Expand Down
16 changes: 16 additions & 0 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ func (p Parser) ParseV3Local(key V3SymmetricKey, tainted string, implicit []byte
return p.validate(*token)
}

// ParseV3Public will parse and verify a v3 public paseto and validate against
// any parser rules. Error if parsing, verification, or any rule fails.
func (p Parser) ParseV3Public(key V3AsymmetricPublicKey, tainted string, implicit []byte) (*Token, error) {
message, err := newMessage(V3Public, tainted)
if err != nil {
return nil, err
}

token, err := message.v3Verify(key, implicit)
if err != nil {
return nil, err
}

return p.validate(*token)
}

// ParseV4Local will parse and decrypt a v4 local paseto and validate against
// any parser rules. Error if parsing, decryption, or any rule fails.
func (p Parser) ParseV4Local(key V4SymmetricKey, tainted string, implicit []byte) (*Token, error) {
Expand Down
8 changes: 8 additions & 0 deletions paseto.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ var (
V2Public = Protocol{Version2, Public}
// V3Local represents a v3 protocol in local mode
V3Local = Protocol{Version3, Local}
// V3Public represents a v3 protocol in public mode
V3Public = Protocol{Version3, Public}
// V4Local represents a v4 protocol in local mode
V4Local = Protocol{Version4, Local}
// V4Public represents a v4 protocol in public mode
Expand Down Expand Up @@ -67,6 +69,8 @@ func NewProtocol(version Version, purpose Purpose) (Protocol, error) {
return Protocol{}, errors.New("Unsupported PASETO purpose")
case Local:
return V3Local, nil
case Public:
return V2Public, nil
}
case Version4:
switch purpose {
Expand Down Expand Up @@ -114,6 +118,8 @@ func (p Protocol) newPayload(bytes []byte) (payload, error) {
return nil, errors.New("Unsupported PASETO purpose")
case Local:
return newV3LocalPayload(bytes)
case Public:
return newV3PublicPayload(bytes)
}
case Version4:
switch p.purpose {
Expand Down Expand Up @@ -141,6 +147,8 @@ func protocolForPayload(payload payload) (Protocol, error) {
return V2Public, nil
case v3LocalPayload:
return V3Local, nil
case v3PublicPayload:
return V3Public, nil
case v4LocalPayload:
return V4Local, nil
case v4PublicPayload:
Expand Down
9 changes: 9 additions & 0 deletions token.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ func (t Token) V2Encrypt(key V2SymmetricKey) string {
return v2LocalEncrypt(t.packet(), key, nil).encoded()
}

// V3Sign signs the token, using the given key and implicit bytes. Implicit
// bytes are bytes used to calculate the signature, but which are not present in
// the final token.
// Implicit must be reprovided for successful verification, and can not be
// recovered.
func (t Token) V3Sign(key V3AsymmetricSecretKey, implicit []byte) string {
return v3PublicSign(t.packet(), key, implicit).encoded()
}

// V3Encrypt signs the token, using the given key and implicit bytes. Implicit
// bytes are bytes used to calculate the encrypted token, but which are not
// present in the final token (or its decrypted value).
Expand Down
57 changes: 57 additions & 0 deletions v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,71 @@ package paseto
import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/hmac"
"crypto/rand"
"crypto/sha512"
"math/big"

"aidanwoods.dev/go-paseto/internal/encoding"
"aidanwoods.dev/go-paseto/internal/random"
"github.com/pkg/errors"
)

func v3PublicSign(packet packet, key V3AsymmetricSecretKey, implicit []byte) message {
data, footer := packet.content, packet.footer
header := []byte(V3Public.Header())

m2 := encoding.Pae(key.Public().compressed(), header, data, footer, implicit)

hash := sha512.Sum384(m2)

r, s, err := ecdsa.Sign(rand.Reader, &key.material, hash[:])
if err != nil {
panic("Failed to sign")
}

var rBytes [48]byte
var sBytes [48]byte

r.FillBytes(rBytes[:])
s.FillBytes(sBytes[:])

sig := append(rBytes[:], sBytes[:]...)

if len(sig) != 96 {
panic("Bad signature length")
}

var signature [96]byte
copy(signature[:], sig)

return newMessageFromPayload(v3PublicPayload{data, signature}, footer)
}

func v3PublicVerify(msg message, key V3AsymmetricPublicKey, implicit []byte) (packet, error) {
payload, ok := msg.p.(v3PublicPayload)
if msg.header() != V3Public.Header() || !ok {
return packet{}, errors.Errorf("Cannot decrypt message with header: %s", msg.header())
}

header, footer := []byte(msg.header()), msg.footer
data := payload.message

m2 := encoding.Pae(key.compressed(), header, data, footer, implicit)

hash := sha512.Sum384(m2)

r := new(big.Int).SetBytes(payload.signature[:48])
s := new(big.Int).SetBytes(payload.signature[48:])

if !ecdsa.Verify(&key.material, hash[:], r, s) {
return packet{}, errors.Errorf("Bad signature")
}

return packet{data, footer}, nil
}

func v3LocalEncrypt(p packet, key V3SymmetricKey, implicit []byte, unitTestNonce []byte) message {
var nonce [32]byte
random.UseProvidedOrFillBytes(unitTestNonce, nonce[:])
Expand Down
97 changes: 97 additions & 0 deletions v3_keys.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,112 @@
package paseto

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"io"
"math/big"

"aidanwoods.dev/go-paseto/internal/random"
"github.com/pkg/errors"
"golang.org/x/crypto/hkdf"
)

// V3AsymmetricPublicKey v3 public public key
type V3AsymmetricPublicKey struct {
material ecdsa.PublicKey
}

// NewV3AsymmetricPublicKeyFromHex Construct a v3 public key from hex
func NewV3AsymmetricPublicKeyFromHex(hexEncoded string) (V3AsymmetricPublicKey, error) {
publicKeyBytes, err := hex.DecodeString(hexEncoded)

if err != nil {
// even though we return error, return a random key here rather than
// a nil key
return NewV3AsymmetricSecretKey().Public(), err
}

if len(publicKeyBytes) != 49 {
// even though we return error, return a random key here rather than
// a nil key
return NewV3AsymmetricSecretKey().Public(), errors.New("Key incorrect length")
}

publicKey := new(ecdsa.PublicKey)
publicKey.Curve = elliptic.P384()
publicKey.X, publicKey.Y = elliptic.UnmarshalCompressed(elliptic.P384(), publicKeyBytes)

return V3AsymmetricPublicKey{*publicKey}, nil
}

func (k V3AsymmetricPublicKey) compressed() []byte {
return elliptic.MarshalCompressed(elliptic.P384(), k.material.X, k.material.Y)
}

// ExportHex export a V3AsymmetricPublicKey to hex for storage
func (k V3AsymmetricPublicKey) ExportHex() string {
return hex.EncodeToString(k.compressed())
}

// V3AsymmetricSecretKey v3 public private key
type V3AsymmetricSecretKey struct {
material ecdsa.PrivateKey
}

// Public returns the corresponding public key for a secret key
func (k V3AsymmetricSecretKey) Public() V3AsymmetricPublicKey {
return V3AsymmetricPublicKey{k.material.PublicKey}
}

// ExportHex export a V3AsymmetricSecretKey to hex for storage
func (k V3AsymmetricSecretKey) ExportHex() string {
return hex.EncodeToString(k.material.D.Bytes())
}

// NewV3AsymmetricSecretKey generate a new secret key for use with asymmetric
// cryptography. Don't forget to export the public key for sharing, DO NOT share
// this secret key.
func NewV3AsymmetricSecretKey() V3AsymmetricSecretKey {
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)

if err != nil {
panic("CSPRNG failure")
}

return V3AsymmetricSecretKey{*privateKey}
}

// NewV3AsymmetricSecretKeyFromHex creates a secret key from hex
func NewV3AsymmetricSecretKeyFromHex(hexEncoded string) (V3AsymmetricSecretKey, error) {
secretBytes, err := hex.DecodeString(hexEncoded)

if err != nil {
// even though we return error, return a random key here rather than
// a nil key
return NewV3AsymmetricSecretKey(), err
}

if len(secretBytes) != 48 {
// even though we return error, return a random key here rather than
// a nil key
return NewV3AsymmetricSecretKey(), errors.New("Key incorrect length")
}

privateKey := new(ecdsa.PrivateKey)
privateKey.D = new(big.Int).SetBytes(secretBytes)

publicKey := new(ecdsa.PublicKey)
publicKey.Curve = elliptic.P384()
publicKey.X, publicKey.Y = publicKey.Curve.ScalarBaseMult(privateKey.D.Bytes())

privateKey.PublicKey = *publicKey

return V3AsymmetricSecretKey{*privateKey}, nil
}

// V3SymmetricKey v3 local symmetric key
type V3SymmetricKey struct {
material [32]byte
Expand Down
25 changes: 25 additions & 0 deletions v3_payloads.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ import (
"github.com/pkg/errors"
)

type v3PublicPayload struct {
message []byte
signature [96]byte
}

func (p v3PublicPayload) bytes() []byte {
return append(p.message, p.signature[:]...)
}

func newV3PublicPayload(bytes []byte) (v3PublicPayload, error) {
signatureOffset := len(bytes) - 96

if signatureOffset < 0 {
return v3PublicPayload{}, errors.New("Payload is not long enough to be a valid Paseto message")
}

message := make([]byte, len(bytes)-96)
copy(message, bytes[:signatureOffset])

var signature [96]byte
copy(signature[:], bytes[signatureOffset:])

return v3PublicPayload{message, signature}, nil
}

type v3LocalPayload struct {
nonce [32]byte
cipherText []byte
Expand Down
Loading