Skip to content

Commit

Permalink
[APP] Serialize challenge as protobuf, and remove snappy (no real gain).
Browse files Browse the repository at this point in the history
  • Loading branch information
Normand, Thibault committed Aug 24, 2017
1 parent 7e3629a commit b5b9daa
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 27 deletions.
73 changes: 49 additions & 24 deletions challenge.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package anvil

import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"

"github.com/dchest/uniuri"
"github.com/golang/snappy"

"golang.org/x/crypto/ed25519"

"go.zenithar.org/anvil/forge"
"go.zenithar.org/anvil/internal"
)

var (
// ErrExpiredChallenge raised when trying to tap an expired challenge
ErrExpiredChallenge = errors.New("anvil: Challenge is expired")
)

// Meld a challenge from given credentials
Expand All @@ -33,59 +38,79 @@ func Meld(principal, password, challenge string) (string, error) {
}

// Forge a challenge
func Forge(principal string) (string, error) {
// Generate a token
claims := map[string]interface{}{
"n": uniuri.NewLen(20),
"i": time.Now().UTC().Unix(),
"e": time.Now().Add(2 * time.Minute).UTC().Unix(),
"p": principal,
func Forge(principal string, opts ...forge.Option) (string, string, error) {
// Default Setings
dopts := &forge.Options{
IDGenerator: forge.DefaultSessionGenerator,
Expiration: 2 * time.Minute,
}

// Encode to JSON
payload, err := json.Marshal(&claims)
if err != nil {
return "", fmt.Errorf("anvil: Unable to generate a challenge for given principal, %v", err)
// Apply param functions
for _, o := range opts {
o(dopts)
}

// Build the challenge
challenge := internal.Challenge{
SessionId: dopts.IDGenerator(),
Principal: principal,
IssuedAt: time.Now().UTC().Unix(),
Expiration: time.Now().Add(dopts.Expiration).UTC().Unix(),
}

// Marshal challenge
payload, err := internal.Marshal(&challenge)

// Return challenge
return toOKP(snappy.Encode(nil, payload)), nil
return payload, challenge.SessionId, err
}

// Tap checks for challenge
func Tap(token string) (bool, error) {
func Tap(token string) (bool, string, string, error) {
// Split challenge in parts
parts := strings.SplitN(token, ".", 3)

// Must have 3 parts (publicKey, challenge, signature)
if len(parts) != 3 {
return false, fmt.Errorf("anvil: Invalid challenge, it must contains 3 parts")
return false, "", "", fmt.Errorf("anvil: Invalid challenge, it must contains 3 parts")
}

// Decode PublicKey
publicKeyRaw, err := fromOKP(parts[0])
if err != nil {
return false, fmt.Errorf("anvil: Invalid public key, %v", err)
return false, "", "", fmt.Errorf("anvil: Invalid public key, %v", err)
}
if len(publicKeyRaw) != ed25519.PublicKeySize {
return false, fmt.Errorf("anvil: Invalid public key size")
return false, "", "", fmt.Errorf("anvil: Invalid public key size")
}

// Decode challenge
tokenRaw, err := fromOKP(parts[1])
if err != nil {
return false, fmt.Errorf("anvil: Invalid challenge, could not decode body, %v", err)
return false, "", "", fmt.Errorf("anvil: Unable to decode challenge, %v", err)
}

// Decode signature
signatureRaw, err := fromOKP(parts[2])
if err != nil {
return false, fmt.Errorf("anvil: Invalid challenge signature encoding, %v", err)
return false, "", "", fmt.Errorf("anvil: Invalid challenge signature encoding, %v", err)
}
if len(signatureRaw) != ed25519.SignatureSize {
return false, fmt.Errorf("anvil: Invalid challenge signature size")
return false, "", "", fmt.Errorf("anvil: Invalid challenge signature size")
}

// Umarshal challenge
var challenge internal.Challenge
err = internal.Unmarshal(parts[1], &challenge)
if err != nil {
return false, "", "", fmt.Errorf("anvil: Unable to unmarshall challenge, %v", err)
}

// Check challenge expiration
if challenge.IsExpired() {
return false, challenge.SessionId, challenge.Principal, ErrExpiredChallenge
}

// Check ed25519 signature
return ed25519.Verify(publicKeyRaw[:], tokenRaw, signatureRaw), nil
return ed25519.Verify(publicKeyRaw[:], tokenRaw, signatureRaw), challenge.SessionId, challenge.Principal, nil
}
16 changes: 13 additions & 3 deletions challenge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,23 @@ import (
func TestForge(t *testing.T) {
RegisterTestingT(t)

challenge, err := anvil.Forge("toto")
challenge, sessionId, err := anvil.Forge("toto")
Expect(err).To(BeNil(), "Error shoul be nil")
Expect(challenge).ToNot(BeNil(), "Challenge should not be nil")
Expect(challenge).ToNot(BeEmpty(), "Challenge should not be empty")
Expect(sessionId).ToNot(BeEmpty(), "Session Id should not be empty")
}

func TestFullChallenge(t *testing.T) {
RegisterTestingT(t)

challenge, err := anvil.Forge("toto")
challenge, fsessionId, err := anvil.Forge("toto")
Expect(err).To(BeNil(), "Error shoul be nil")
Expect(challenge).ToNot(BeNil(), "Challenge should not be nil")
Expect(challenge).ToNot(BeEmpty(), "Challenge should not be empty")
Expect(fsessionId).ToNot(BeEmpty(), "Session Id should not be empty")

log.Printf("SessionID : %s\n", fsessionId)
log.Printf("Challenge : %s\n", challenge)

token, err := anvil.Meld("toto", "foo", challenge)
Expand All @@ -35,7 +38,14 @@ func TestFullChallenge(t *testing.T) {

log.Printf("Token : %s\n", token)

valid, err := anvil.Tap(token)
valid, sessionId, principal, err := anvil.Tap(token)
Expect(err).To(BeNil(), "Error should be nil")
Expect(valid).To(BeTrue(), "Token tap should be true")
Expect(principal).To(Equal("toto"), "Principal should equal toto")
Expect(sessionId).ToNot(BeEmpty(), "Session identifier should not be empty")

log.Printf("SessionID : %s\n", sessionId)
Expect(fsessionId).To(Equal(sessionId), "Session Id should be equal")
log.Printf("Principal : %s\n", principal)
Expect(principal).To(Equal("toto"), "Principal should equal given one")
}
47 changes: 47 additions & 0 deletions forge/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package forge

import (
"time"

"github.com/dchest/uniuri"
)

// SessionIDGenerator is the contract for Session ID generation implementation
type SessionIDGenerator func() string

// Options for challenge forging
type Options struct {
Expiration time.Duration
IDGenerator SessionIDGenerator
}

// Option defines forge option contract option function
type Option func(*Options)

// WithExpiration defines the expiration interval
func WithExpiration(expiration time.Duration) Option {
return func(opts *Options) {
opts.Expiration = expiration
}
}

// WithSessionIDGenerator defines the SessionId generation function
func WithSessionIDGenerator(generator SessionIDGenerator) Option {
return func(opts *Options) {
opts.IDGenerator = generator
}
}

// WithRandomSessionID defines the SessionId generation
func WithRandomSessionID() Option {
return func(opts *Options) {
opts.IDGenerator = DefaultSessionGenerator
}
}

var (
// DefaultSessionGenerator defines the default session id generator
DefaultSessionGenerator = func() string {
return uniuri.NewLen(64)
}
)
8 changes: 8 additions & 0 deletions internal/challenge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package internal

import "time"

// IsExpired retruns the challenge expiration status
func (ch *Challenge) IsExpired() bool {
return time.Now().UTC().After(time.Unix(ch.Expiration, 0).UTC())
}
27 changes: 27 additions & 0 deletions internal/codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package internal

import (
"encoding/base64"

"github.com/gogo/protobuf/proto"
)

// Marshal converts a protobuf message to a URL legal string.
func Marshal(message proto.Message) (string, error) {
data, err := proto.Marshal(message)
if err != nil {
return "", err
}

return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data), nil
}

// Unmarshal decodes a protobuf message.
func Unmarshal(s string, message proto.Message) error {
data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(s)
if err != nil {
return err
}

return proto.Unmarshal(data, message)
}
91 changes: 91 additions & 0 deletions internal/types.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions internal/types.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
syntax = "proto3";

// Package internal holds protobuf types used by the server
package internal;

// Challenge is the authentication challenge to be used by client
message Challenge {
string session_id = 1;
int64 issued_at = 2;
int64 expiration = 3;
string principal = 4;
}

0 comments on commit b5b9daa

Please sign in to comment.