Skip to content
Open
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: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/elazarl/goproxy v1.7.2 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/getkin/kin-openapi v0.132.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand Down Expand Up @@ -69,6 +70,7 @@ require (
github.com/unknwon/com v1.0.1 // indirect
github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect
github.com/urfave/cli v1.22.16 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
Expand Down Expand Up @@ -235,6 +237,8 @@ github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP9
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
Expand Down
147 changes: 147 additions & 0 deletions pkg/plugin/authentication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package plugin

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
"sort"
"strings"
"time"
)

// GetXSnDate formats the provided time value using the HTTP-date format expected
// by SolarNetwork (for example "Fri, 03 Mar 2017 04:36:28 GMT").
func GetXSnDate(t time.Time) string {
return t.UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")
}

// hmacSHA256 returns the HMAC SHA-256 digest of content using secret as the key.
func hmacSHA256(secret, content []byte) []byte {
d := hmac.New(sha256.New, secret)
d.Write(content)
return d.Sum(nil)
}

// GenerateSigningKey derives the signing key for a request using the token
// secret, the request time, and the literal request string (typically
// "snws2_request").
func GenerateSigningKey(secret string, t time.Time, request string) []byte {
date := t.UTC().Format("20060102")
innerSecret := []byte("SNWS2" + secret)

inner := hmacSHA256(innerSecret, []byte(date))
return hmacSHA256(inner, []byte(request))
}

// GenerateSigningKeyHex is a convenience wrapper around GenerateSigningKey that
// returns the key as a lower-case hex string.
func GenerateSigningKeyHex(secret string, t time.Time, request string) string {
return hex.EncodeToString(GenerateSigningKey(secret, t, request))
}

// GenerateSigningMessage builds the signing message using the provided time
// value and canonical request string.
func GenerateSigningMessage(t time.Time, canonicalRequest string) string {
digest := sha256.Sum256([]byte(canonicalRequest))
return fmt.Sprintf(
"SNWS2-HMAC-SHA256\n%s\n%x",
t.UTC().Format("20060102T150405Z"),
digest,
)
}

// OrderQueryParameters sorts query parameters and encodes them per RFC 3986,
// returning a deterministic query string. The original ordering of duplicate
// keys is preserved.
func OrderQueryParameters(q string) string {
if strings.TrimSpace(q) == "" {
return ""
}

values, err := url.ParseQuery(q)
if err != nil {
// Best-effort handling: fall back to returning the original string when
// parsing fails so callers can decide how to handle invalid input.
return q
}

keys := make([]string, 0, len(values))
for k := range values {
keys = append(keys, k)
}
sort.Strings(keys)

var b strings.Builder
first := true
for _, key := range keys {
for _, val := range values[key] {
if !first {
b.WriteByte('&')
} else {
first = false
}
b.WriteString(url.QueryEscape(key))
b.WriteByte('=')
b.WriteString(url.QueryEscape(val))
}
}

return b.String()
}

// GenerateCanonicalRequestMessage constructs the canonical request message used
// when computing the signing string.
func GenerateCanonicalRequestMessage(method, path, parameters string, signedHeaders map[string]string, body string) string {
var b strings.Builder

b.WriteString(strings.ToUpper(method))
b.WriteByte('\n')
b.WriteString(path)
b.WriteByte('\n')
b.WriteString(OrderQueryParameters(parameters))
b.WriteByte('\n')

keys := sortedLowerKeys(signedHeaders)
for _, k := range keys {
b.WriteString(k)
b.WriteByte(':')
b.WriteString(strings.TrimSpace(signedHeaders[k]))
b.WriteByte('\n')
}

b.WriteString(strings.Join(keys, ";"))
b.WriteByte('\n')

digest := sha256.Sum256([]byte(body))
b.WriteString(hex.EncodeToString(digest[:]))

return b.String()
}

// GenerateSignature creates the hex-encoded HMAC SHA-256 signature for the
// provided message using the supplied key.
func GenerateSignature(message []byte, key []byte) string {
return hex.EncodeToString(hmacSHA256(key, message))
}

// GenerateAuthHeader builds the full Authorization header value for a request.
func GenerateAuthHeader(token, secret, method, path, params string, signedHeaders map[string]string, body string, t time.Time) string {
canonical := GenerateCanonicalRequestMessage(method, path, params, signedHeaders, body)
key := GenerateSigningKey(secret, t, "snws2_request")
msg := GenerateSigningMessage(t, canonical)
sig := GenerateSignature([]byte(msg), key)

headerNames := strings.Join(sortedLowerKeys(signedHeaders), ";")
return fmt.Sprintf("SNWS2 Credential=%s,SignedHeaders=%s,Signature=%s", token, headerNames, sig)
}

func sortedLowerKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, strings.ToLower(strings.TrimSpace(k)))
}
sort.Strings(keys)
return keys
}
132 changes: 132 additions & 0 deletions pkg/plugin/authentication_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package plugin

import (
"testing"
"time"
)

func TestGetXSnDate(t *testing.T) {
ts := time.Date(2017, 3, 3, 4, 36, 28, 0, time.UTC)
if got := GetXSnDate(ts); got != "Fri, 03 Mar 2017 04:36:28 GMT" {
t.Fatalf("unexpected X-SN-Date: %s", got)
}
}

func TestOrderQueryParameters(t *testing.T) {
cases := []struct {
name string
input string
expected string
}{
{"empty", "", ""},
{"sorted", "foo=1&bar=2&baz=3", "bar=2&baz=3&foo=1"},
{"repeated", "foo=2&foo=1&bar=3", "bar=3&foo=2&foo=1"},
{"escaped", "sourceId=/foo/bar", "sourceId=%2Ffoo%2Fbar"},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := OrderQueryParameters(tc.input); got != tc.expected {
t.Fatalf("expected %q, got %q", tc.expected, got)
}
})
}
}

func TestGenerateSigningKey(t *testing.T) {
ts := time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC)
key := GenerateSigningKeyHex("ABC123", ts, "snws2_request")
const expected = "1f96b28b651285e49d06989aebaee169fa67a5f6a07fb72a8325fce83b425ad6"
if key != expected {
t.Fatalf("expected signing key %s, got %s", expected, key)
}
}

func TestGenerateCanonicalRequestMessage(t *testing.T) {
headers := map[string]string{
"host": "data.solarnetwork.net",
"x-sn-date": "Fri, 03 Mar 2017 04:36:28 GMT",
}
got := GenerateCanonicalRequestMessage(
"GET",
"/solarquery/api/v1/sec/datum/meta/50",
"sourceId=Foo",
headers,
"",
)

const expected = `GET
/solarquery/api/v1/sec/datum/meta/50
sourceId=Foo
host:data.solarnetwork.net
x-sn-date:Fri, 03 Mar 2017 04:36:28 GMT
host;x-sn-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`

if got != expected {
t.Fatalf("canonical request mismatch\nexpected:\n%s\n\ngot:\n%s", expected, got)
}
}

func TestGenerateSigningMessage(t *testing.T) {
ts := time.Date(2017, 3, 3, 4, 36, 28, 0, time.UTC)
canonical := `GET
/solarquery/api/v1/sec/datum/meta/50
sourceId=Foo
host:data.solarnetwork.net
x-sn-date:Fri, 03 Mar 2017 04:36:28 GMT
host;x-sn-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`

got := GenerateSigningMessage(ts, canonical)
const expected = `SNWS2-HMAC-SHA256
20170303T043628Z
8f732085380ed6dc18d8556a96c58c820b0148852a61b3c828cb9cfd233ae05f`

if got != expected {
t.Fatalf("unexpected signing message:\n%s", got)
}
}

func TestGenerateSignature(t *testing.T) {
ts := time.Date(2017, 3, 3, 4, 36, 28, 0, time.UTC)
canonical := `GET
/solarquery/api/v1/sec/datum/meta/50
sourceId=Foo
host:data.solarnetwork.net
x-sn-date:Fri, 03 Mar 2017 04:36:28 GMT
host;x-sn-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`

signingKey := GenerateSigningKey("ABC123", ts, "snws2_request")
msg := GenerateSigningMessage(ts, canonical)
sig := GenerateSignature([]byte(msg), signingKey)

const expected = "bdab8efeb14032700de12cd2899fcfaf4e8e45c4935936338b9e108fb7ea613e"
if sig != expected {
t.Fatalf("expected signature %s, got %s", expected, sig)
}
}

func TestGenerateAuthHeader(t *testing.T) {
ts := time.Date(2017, 3, 3, 4, 36, 28, 0, time.UTC)
headers := map[string]string{
"host": "data.solarnetwork.net",
"x-sn-date": "Fri, 03 Mar 2017 04:36:28 GMT",
}
auth := GenerateAuthHeader(
"test-token",
"ABC123",
"GET",
"/solarquery/api/v1/sec/datum/meta/50",
"sourceId=Foo",
headers,
"",
ts,
)

const expected = "SNWS2 Credential=test-token,SignedHeaders=host;x-sn-date,Signature=bdab8efeb14032700de12cd2899fcfaf4e8e45c4935936338b9e108fb7ea613e"
if auth != expected {
t.Fatalf("unexpected auth header: %s", auth)
}
}
Loading