initial commit

master v1.0.0
Brad Rydzewski 6 years ago
commit 56c31260ea

@ -0,0 +1,11 @@
workspace:
base: /go
path: src/github.com/drone/go-license
pipeline:
build:
image: golang
commands:
- go get golang.org/x/crypto/ed25519
- go install ./...
- go test -v ./...

5
.gitignore vendored

@ -0,0 +1,5 @@
*.out
*.txt
id_ed25519
id_ed25519.pub
license.txt

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2018, Drone.IO Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,61 @@
// Copyright 2018 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"time"
"github.com/drone/go-license/license"
"github.com/drone/go-license/license/licenseutil"
)
var month = time.Hour * 24 * 31
var (
in = flag.String("in", "id_ed25519", "")
out = flag.String("out", "license.txt", "")
iss = flag.String("iss", "", "")
cus = flag.String("cus", "", "")
sub = flag.String("sub", "", "")
typ = flag.String("typ", "", "")
lim = flag.Int("lim", 0, "")
exp = flag.Duration("exp", month, "")
)
func main() {
flag.Parse()
privateKey, err := licenseutil.ReadPrivateKey(*in)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
license := &license.License{
Iss: *iss,
Cus: *cus,
Sub: *sub,
Typ: *typ,
Lim: *lim,
Exp: time.Now().UTC().Add(*exp),
Iat: time.Now().UTC(),
}
encoded, err := license.Encode(privateKey)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = ioutil.WriteFile(*out, encoded, 0644)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}

@ -0,0 +1,43 @@
// Copyright 2018 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"crypto/rand"
"encoding/base64"
"flag"
"fmt"
"io/ioutil"
"os"
"golang.org/x/crypto/ed25519"
)
var out = flag.String("out", "id_ed25519", "")
func main() {
flag.Parse()
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
publicKeyHex, privateKeyHex := base64.StdEncoding.EncodeToString(publicKey),
base64.StdEncoding.EncodeToString(privateKey)
err = ioutil.WriteFile(*out, []byte(privateKeyHex), 0644)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = ioutil.WriteFile(*out+".pub", []byte(publicKeyHex), 0644)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}

@ -0,0 +1,51 @@
// Copyright 2018 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"github.com/drone/go-license/license"
"github.com/drone/go-license/license/licenseutil"
)
var (
pub = flag.String("pub", "id_ed25519.pub", "")
file = flag.String("file", "license.txt", "")
addr = flag.String("addr", "http://localhost:9000", "")
)
func main() {
flag.Parse()
publicKey, err := licenseutil.ReadPublicKey(*pub)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
l, err := license.DecodeFile(*file, publicKey)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
buf := new(bytes.Buffer)
json.NewEncoder(buf).Encode(l)
res, err := http.Post(*addr, "application/json", buf)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
}

@ -0,0 +1,57 @@
// Copyright 2018 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"encoding/json"
"flag"
"net/http"
"time"
"github.com/drone/go-license/license"
"github.com/drone/go-license/license/licenseutil"
)
var monthly = time.Hour * 24 * 30
var (
addr = flag.String("addr", ":9000", "")
renewal = flag.Duration("dur", monthly, "")
signer = flag.String("signer", "id_ed25519", "")
)
func main() {
flag.Parse()
http.HandleFunc("/", handler)
http.ListenAndServe(*addr, nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
l := new(license.License)
err := json.NewDecoder(r.Body).Decode(l)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// TODO verify the License.Cus (customer id) is valid
// and in good standing prior to issuing a new license.
l.Exp = time.Now().UTC().Add(*renewal)
privateKey, err := licenseutil.ReadPrivateKey(*signer)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
encoded, err := l.Encode(privateKey)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(encoded)
}

@ -0,0 +1,40 @@
// Copyright 2018 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"github.com/drone/go-license/license"
"github.com/drone/go-license/license/licenseutil"
)
var (
pub = flag.String("pub", "id_ed25519.pub", "")
file = flag.String("file", "license.txt", "")
)
func main() {
flag.Parse()
publicKey, err := licenseutil.ReadPublicKey(*pub)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
l, err := license.DecodeFile(*file, publicKey)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(l)
}

@ -0,0 +1,91 @@
// Copyright 2018 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package license
import (
"bytes"
"encoding/json"
"encoding/pem"
"errors"
"io/ioutil"
"time"
"golang.org/x/crypto/ed25519"
)
var (
// ErrInvalidSignature indicates the license signature is invalid.
ErrInvalidSignature = errors.New("Invalid signature")
// ErrMalformedLicense indicates the license file is malformed.
ErrMalformedLicense = errors.New("Malformed License")
)
// License defines a software license key.
type License struct {
Iss string `json:"iss,omitempty"` // Issued By
Cus string `json:"cus,omitempty"` // Customer ID
Sub string `json:"sub,omitempty"` // Subscriber ID
Typ string `json:"typ,omitempty"` // License Type
Lim int `json:"lim,omitempty"` // License Limit (e.g. Seats)
Iat time.Time `json:"iat,omitempty"` // Issued At
Exp time.Time `json:"exp,omitempty"` // Expires At
Dat json.RawMessage `json:"dat,omitempty"` // Metadata
}
// Expired returns true if the license is expired.
func (l *License) Expired() bool {
return l.Exp.IsZero() == false && time.Now().After(l.Exp)
}
// Encode generates returns a PEM encoded license key that is
// signed with the ed25519 private key.
func (l *License) Encode(privateKey ed25519.PrivateKey) ([]byte, error) {
msg, err := json.Marshal(l)
if err != nil {
return nil, err
}
sig := ed25519.Sign(privateKey, msg)
buf := new(bytes.Buffer)
buf.Write(sig)
buf.Write(msg)
block := &pem.Block{
Type: "LICENSE KEY",
Bytes: buf.Bytes(),
}
return pem.EncodeToMemory(block), nil
}
// Decode decodes the PEM encoded license key and verifies
// the content signature using the ed25519 public key.
func Decode(data []byte, publicKey ed25519.PublicKey) (*License, error) {
block, _ := pem.Decode(data)
if block == nil || len(block.Bytes) < ed25519.SignatureSize {
return nil, ErrMalformedLicense
}
sig := block.Bytes[:ed25519.SignatureSize]
msg := block.Bytes[ed25519.SignatureSize:]
verified := ed25519.Verify(publicKey, msg, sig)
if !verified {
return nil, ErrInvalidSignature
}
out := new(License)
err := json.Unmarshal(msg, out)
return out, err
}
// DecodeFile decodes the PEM encoded license file and verifies
// the content signature using the ed25519 public key.
func DecodeFile(path string, publicKey ed25519.PublicKey) (*License, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return Decode([]byte(data), publicKey)
}

@ -0,0 +1,142 @@
// Copyright 2018 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package license
import (
"os"
"testing"
"time"
"github.com/drone/go-license/license/licenseutil"
)
func TestExpired(t *testing.T) {
license := &License{}
if license.Expired() {
t.Errorf("Expect zero value expiration to never expire")
}
license = &License{}
license.Exp = time.Now().Add(time.Hour)
if license.Expired() == true {
t.Errorf("Expect license is not expired")
}
license = &License{}
license.Exp = time.Now().Add(time.Hour * -1)
if license.Expired() == false {
t.Errorf("Expect license is expired")
}
}
func TestEncodeDecode(t *testing.T) {
privateKey, err := licenseutil.ReadPrivateKey("licenseutil/testdata/id_ed25519")
if err != nil {
t.Error(err)
}
publicKey, err := licenseutil.ReadPublicKey("licenseutil/testdata/id_ed25519.pub")
if err != nil {
t.Error(err)
}
license := &License{
Iss: "Acme, Inc",
Cus: "cus_CxoyqaC4p4Hjl0",
Sub: "sub_A4l9XkCxyZPcS2",
Typ: "trial",
Lim: 50,
Iat: time.Now().UTC(),
Exp: time.Now().Add(time.Hour).UTC(),
}
encoded, err := license.Encode(privateKey)
if err != nil {
t.Error(err)
}
decoded, err := Decode(encoded, publicKey)
if err != nil {
t.Error(err)
}
if got, want := decoded.Cus, license.Cus; got != want {
t.Errorf("Want license Cus %v, got %v", want, got)
}
if got, want := decoded.Exp, license.Exp; got != want {
t.Errorf("Want license Exp %v, got %v", want, got)
}
if got, want := decoded.Iat, license.Iat; got != want {
t.Errorf("Want license Iat %v, got %v", want, got)
}
if got, want := decoded.Iss, license.Iss; got != want {
t.Errorf("Want license Iss %v, got %v", want, got)
}
if got, want := decoded.Lim, license.Lim; got != want {
t.Errorf("Want license Lim %v, got %v", want, got)
}
if got, want := decoded.Sub, license.Sub; got != want {
t.Errorf("Want license Sub %v, got %v", want, got)
}
if got, want := decoded.Typ, license.Typ; got != want {
t.Errorf("Want license Sub %v, got %v", want, got)
}
}
func TestDecodeFile(t *testing.T) {
publicKey, err := licenseutil.ReadPublicKey("licenseutil/testdata/id_ed25519.pub")
if err != nil {
t.Error(err)
}
decoded, err := DecodeFile("licenseutil/testdata/license.txt", publicKey)
if err != nil {
t.Error(err)
}
if got, want := decoded.Cus, "cus_CxoyqaC4p4Hjl0"; got != want {
t.Errorf("Want license Cus %v, got %v", want, got)
}
if got, want := decoded.Sub, "sub_A4l9XkCxyZPcS2"; got != want {
t.Errorf("Want license Exp %v, got %v", want, got)
}
}
func TestDecodeFile_InvalidSignature(t *testing.T) {
publicKey, err := licenseutil.ReadPublicKey("licenseutil/testdata/id_ed25519.pub")
if err != nil {
t.Error(err)
}
_, err = DecodeFile("licenseutil/testdata/license_invalid_signature.txt", publicKey)
if err != ErrInvalidSignature {
t.Errorf("Expected invalid signature error")
}
}
func TestDecodeFile_InvalidEncoding(t *testing.T) {
publicKey, err := licenseutil.ReadPublicKey("licenseutil/testdata/id_ed25519.pub")
if err != nil {
t.Error(err)
}
_, err = DecodeFile("licenseutil/testdata/license_invalid_encoding.txt", publicKey)
if err != ErrMalformedLicense {
t.Errorf("Expected invalid signature error")
}
}
func TestDecodeFile_InvalidJson(t *testing.T) {
publicKey, err := licenseutil.ReadPublicKey("licenseutil/testdata/id_ed25519.pub")
if err != nil {
t.Error(err)
}
_, err = DecodeFile("licenseutil/testdata/license_invalid_json.txt", publicKey)
if err != ErrMalformedLicense {
t.Errorf("Expected invalid signature error")
}
}
func TestDecodeFile_PathError(t *testing.T) {
publicKey, err := licenseutil.ReadPublicKey("licenseutil/testdata/id_ed25519.pub")
if err != nil {
t.Error(err)
}
_, err = DecodeFile("path/does/not/exist", publicKey)
if _, ok := err.(*os.PathError); !ok {
t.Errorf("Expect path error")
}
}

@ -0,0 +1,58 @@
// Copyright 2018 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package licenseutil
//go:generate license-keygen -out testdata/id_ed25519
//go:generate license-create -in testdata/id_ed25519 -out testdata/license.txt -cus cus_CxoyqaC4p4Hjl0 -sub sub_A4l9XkCxyZPcS2
import (
"encoding/base64"
"io/ioutil"
"golang.org/x/crypto/ed25519"
)
// ReadPublicKey reads a base64-encoded public key file.
func ReadPublicKey(path string) (ed25519.PublicKey, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return DecodePublicKey(data)
}
// ReadPrivateKey reads a base64-encoded private key file.
func ReadPrivateKey(path string) (ed25519.PrivateKey, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return DecodePrivateKey(data)
}
// DecodePublicKey decodes a base64-encoded private key.
func DecodePublicKey(data []byte) (ed25519.PublicKey, error) {
decoded, err := decode(data)
if err != nil {
return nil, err
}
return ed25519.PublicKey(decoded), nil
}
// DecodePrivateKey decodes a base64-encoded private key.
func DecodePrivateKey(data []byte) (ed25519.PrivateKey, error) {
decoded, err := decode(data)
if err != nil {
return nil, err
}
return ed25519.PrivateKey(decoded), nil
}
func decode(b []byte) ([]byte, error) {
enc := base64.StdEncoding
buf := make([]byte, enc.DecodedLen(len(b)))
n, err := enc.Decode(buf, b)
return buf[:n], err
}

@ -0,0 +1,42 @@
// Copyright 2018 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package licenseutil
import (
"os"
"testing"
"golang.org/x/crypto/ed25519"
)
func TestReadKeyPair(t *testing.T) {
publicKey, err := ReadPublicKey("testdata/id_ed25519.pub")
if err != nil {
t.Error(err)
}
privateKey, err := ReadPrivateKey("testdata/id_ed25519")
if err != nil {
t.Error(err)
}
msg := []byte("hello world")
sig := ed25519.Sign(privateKey, msg)
if !ed25519.Verify(publicKey, msg, sig) {
t.Errorf("Cannot sign and verify. Are keys malformed?")
}
}
func TestReadPublicKey_NotFound(t *testing.T) {
_, err := ReadPublicKey("does/not/exist")
if _, ok := err.(*os.PathError); !ok {
t.Errorf("Expect path error")
}
}
func TestReadPrivateKey_NotFound(t *testing.T) {
_, err := ReadPrivateKey("does/not/exist")
if _, ok := err.(*os.PathError); !ok {
t.Errorf("Expect path error")
}
}
Loading…
Cancel
Save