Skip to content

Commit

Permalink
🔖 v1.0.0 🎉
Browse files Browse the repository at this point in the history
  • Loading branch information
satorunooshie committed Jul 19, 2022
1 parent 8029ebc commit 6f1e140
Show file tree
Hide file tree
Showing 17 changed files with 925 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/actions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: actions
on: [push, pull_request]
jobs:
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
check-latest: true
go-version-file: go.mod
- uses: golangci/golangci-lint-action@v3
test:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
check-latest: true
go-version-file: go.mod
- name: Test
run: go test -v ./...

128 changes: 128 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# github.com/satorunooshie/asn
[![Go Reference](https://pkg.go.dev/badge/github.com/satorunooshie/asn.svg)](https://pkg.go.dev/github.com/satorunooshie/asn)

Library for validation of App Store Server Notifications V2.

# Usage

- Set by file(s).
Use NewFileRootCAFetcher.

- Set by url(s).
Use NewHTTPRootCAFetcher.

- Set by raw(s).
Use NewRawRootCAFetcher.

Recommended for use with the [jwx](https://github.com/lestrrat-go/jwx) created by [lestrrat-go](https://github.com/lestrrat-go).

```go
package asn

import (
_ "embed"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"

"github.com/lestrrat-go/jwx/v2/jws"
)

//go:embed testdata/Root-CA.cer
var rootCA []byte

//go:embed testdata/request.txt
var request []byte

//go:embed testdata/raw.txt
var raw []byte

const (
emptyCerPath = "testdata/empty.cer"
cerPath = "testdata/Root-CA.cer"
cerURL = "https://www.apple.com/certificateauthority/AppleRootCA-G3.cer"
)

func path(filename string) string {
return filepath.Join(filename)
}

type fakeAppleServer struct{}

func (*fakeAppleServer) RoundTrip(r *http.Request) (*http.Response, error) {
res := httptest.NewRecorder()
if r.URL.Host == "www.apple.com" {
_, _ = res.Write(rootCA)
}
if strings.Contains(r.URL.String(), "notfound") {
res.WriteHeader(http.StatusNotFound)
}
return res.Result(), nil
}

func ExampleNewKeyProvider_byFile() {
kp := NewKeyProvider(NewFileRootCAFetcher(cerPath))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}

func ExampleNewKeyProvider_byFiles() {
emptyCerPath := path(emptyCerPath)
optional := []string{cerPath}
kp := NewKeyProvider(NewFileRootCAFetcher(emptyCerPath, optional...))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}

func ExampleNewKeyProvider_byUrl() {
client := &http.Client{
Transport: &fakeAppleServer{},
}
kp := NewKeyProvider(NewHTTPRootCAFetcher(client, cerURL))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}

func ExampleNewKeyProvider_byUrls() {
const url = "http://localhost:8080/test.cer"
client := &http.Client{
Transport: &fakeAppleServer{},
}
optional := []string{cerURL}
kp := NewKeyProvider(NewHTTPRootCAFetcher(client, url, optional...))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}

func ExampleNewKeyProvider_byRaw() {
kp := NewKeyProvider(NewRawRootCAFetcher(raw))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}

func ExampleNewKeyProvider_byRaws() {
kp := NewKeyProvider(NewRawRootCAFetcher([]byte(`test`), raw))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}
```
108 changes: 108 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package asn

import (
_ "embed"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"

"github.com/lestrrat-go/jwx/v2/jws"
)

//go:embed testdata/Root-CA.cer
var rootCA []byte

//go:embed testdata/request.txt
var request []byte

//go:embed testdata/raw.txt
var raw []byte

const (
emptyCerPath = "testdata/empty.cer"
cerPath = "testdata/Root-CA.cer"
cerURL = "https://www.apple.com/certificateauthority/AppleRootCA-G3.cer"
)

func path(filename string) string {
return filepath.Join(filename)
}

type fakeAppleServer struct{}

func (*fakeAppleServer) RoundTrip(r *http.Request) (*http.Response, error) {
res := httptest.NewRecorder()
if r.URL.Host == "www.apple.com" {
_, _ = res.Write(rootCA)
}
if strings.Contains(r.URL.String(), "notfound") {
res.WriteHeader(http.StatusNotFound)
}
return res.Result(), nil
}

func ExampleNewKeyProvider_byFile() {
kp := NewKeyProvider(NewFileRootCAFetcher(cerPath))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}

func ExampleNewKeyProvider_byFiles() {
emptyCerPath := path(emptyCerPath)
optional := []string{cerPath}
kp := NewKeyProvider(NewFileRootCAFetcher(emptyCerPath, optional...))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}

func ExampleNewKeyProvider_byUrl() {
client := &http.Client{
Transport: &fakeAppleServer{},
}
kp := NewKeyProvider(NewHTTPRootCAFetcher(client, cerURL))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}

func ExampleNewKeyProvider_byUrls() {
const url = "http://localhost:8080/test.cer"
client := &http.Client{
Transport: &fakeAppleServer{},
}
optional := []string{cerURL}
kp := NewKeyProvider(NewHTTPRootCAFetcher(client, url, optional...))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}

func ExampleNewKeyProvider_byRaw() {
kp := NewKeyProvider(NewRawRootCAFetcher(raw))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}

func ExampleNewKeyProvider_byRaws() {
kp := NewKeyProvider(NewRawRootCAFetcher([]byte(`test`), raw))
opts := []jws.VerifyOption{jws.WithKeyProvider(kp)}
verified, err := jws.Verify(request, opts...)
fmt.Println(string(verified), err)
// Output:
// {"notificationType":"DID_CHANGE_RENEWAL_PREF","subtype":"DOWNGRADE","notificationUUID":"c92e001c-96d2-9ou5-q92p-32a5fy0d6g78","notificationVersion":"2.0","data":{"appAppleId":982253034,"bundleId":"hogehoge","bundleVersion":"269822910.1","environment":"Production","signedRenewalInfo":"...","signedTransactionInfo":"..."}} <nil>
}
123 changes: 123 additions & 0 deletions fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package asn

import (
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
"os"
)

// RootCAFetcher is an interface that fetches Root CA.
type RootCAFetcher interface {
// Fetch returns Root CAs.
Fetch(ctx context.Context) ([][]byte, error)
}

// HTTPRootCAFetcher implements RootCAFetcher via HTTP.
type HTTPRootCAFetcher struct {
client *http.Client
urls []string
}

// NewHTTPRootCAFetcher returns a new HTTPRootCAFetcher.
// At least one url that returns Root CA must be set.
// Optional string argument is for setting multiple urls that returns Root CA.
// if *http.Client is nil, http.DefaultClient is used.
func NewHTTPRootCAFetcher(client *http.Client, url string, optional ...string) *HTTPRootCAFetcher {
if client == nil {
client = http.DefaultClient
}
return &HTTPRootCAFetcher{
client: client,
urls: append([]string{url}, optional...),
}
}

// Fetch returns the Root CAs that were successfully fetched via http and an error
// if there's a problem with http access.
func (f *HTTPRootCAFetcher) Fetch(ctx context.Context) ([][]byte, error) {
rootCAs := make([][]byte, 0, len(f.urls))
for _, url := range f.urls {
b, err := f.fetch(ctx, url)
if err != nil {
return rootCAs, err
}
dbuf := make([]byte, base64.StdEncoding.EncodedLen(len(b)))
base64.StdEncoding.Encode(dbuf, b)
rootCAs = append(rootCAs, dbuf)
}
return rootCAs, nil
}

func (f *HTTPRootCAFetcher) fetch(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := f.client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("url: %s, code: %d", url, resp.StatusCode)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return b, nil
}

// FileRootCAFetcher implements RootCAFetcher via files.
type FileRootCAFetcher struct {
paths []string
rootCA [][]byte
}

// NewFileRootCAFetcher returns a new FileRootCAFetcher.
// Optional string argument is for setting multiple Root CA file paths.
// At least one Root CA file path must be set.
func NewFileRootCAFetcher(cerpath string, optional ...string) *FileRootCAFetcher {
return &FileRootCAFetcher{paths: append([]string{cerpath}, optional...)}
}

// Fetch returns the Root CAs from files set by NewFileRootCAFetcher.
// If there's cache, returns cache.
func (f *FileRootCAFetcher) Fetch(context.Context) ([][]byte, error) {
if f.rootCA != nil {
return f.rootCA, nil
}
for _, path := range f.paths {
b, err := os.ReadFile(path)
if err != nil {
return f.rootCA, err
}
dbuf := make([]byte, base64.StdEncoding.EncodedLen(len(b)))
base64.StdEncoding.Encode(dbuf, b)
f.rootCA = append(f.rootCA, dbuf)
}
return f.rootCA, nil
}

// RawRootCAFetcher implements RootCAFetcher via raw bytes.
type RawRootCAFetcher struct {
rootCA [][]byte
}

// NewRawRootCAFetcher returns a new RawRootCAFetcher.
// At least one certificate must be set as a byte string.
// Optional byte slice argument is for setting multiple Root CAs.
func NewRawRootCAFetcher(rootCA []byte, optional ...[]byte) *RawRootCAFetcher {
return &RawRootCAFetcher{rootCA: append([][]byte{rootCA}, optional...)}
}

// Fetch returns the Root CAs set by NewRawRootCAFetcher.
func (f *RawRootCAFetcher) Fetch(context.Context) ([][]byte, error) {
return f.rootCA, nil
}
Loading

0 comments on commit 6f1e140

Please sign in to comment.