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

JWS-based Notation Go Library #11

Merged
merged 37 commits into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
89a842e
clean up registry releated packages
shizhMSFT Sep 13, 2021
c9dcbb0
clean up jwt signature format
shizhMSFT Sep 13, 2021
be6fee7
update notation interface
shizhMSFT Sep 13, 2021
5afcf6b
add JWS utilities
shizhMSFT Sep 13, 2021
318864f
go mod update
shizhMSFT Sep 14, 2021
9bd0102
add ASN.1 BER pacakge
shizhMSFT Sep 14, 2021
ad97595
mark MediaType as required
shizhMSFT Sep 16, 2021
0afde77
add OID package
shizhMSFT Sep 16, 2021
bf4a797
add cms structures
shizhMSFT Sep 16, 2021
98c10d0
refined verifier interface
shizhMSFT Sep 17, 2021
cbe226d
verify cms
shizhMSFT Sep 17, 2021
16e9eb8
add pki package
shizhMSFT Sep 18, 2021
8018382
refactor hash code
shizhMSFT Sep 18, 2021
e49757b
add timestamp package
shizhMSFT Sep 18, 2021
00c639a
test package timestamp and fix bugs
shizhMSFT Sep 18, 2021
f878164
refine comments for jwsutil
shizhMSFT Sep 22, 2021
a690cdb
sign artifact
shizhMSFT Sep 22, 2021
00bec6b
add cryptoutil package
shizhMSFT Sep 22, 2021
373328b
add dependency bot
shizhMSFT Sep 22, 2021
1fcfbad
add test action
shizhMSFT Sep 22, 2021
b19e52c
verify the timestamp token
shizhMSFT Sep 23, 2021
6a323b6
fix line-endings for cryptoutil
shizhMSFT Sep 23, 2021
b0822ad
check line endings
shizhMSFT Sep 23, 2021
7255ac1
add Makefile
shizhMSFT Sep 23, 2021
817598f
verify artifact
shizhMSFT Sep 23, 2021
f942387
test and fix bugs in envelope
shizhMSFT Sep 24, 2021
e10b7a7
basic tests on jws
shizhMSFT Sep 24, 2021
1567f6c
add timestamptest package
shizhMSFT Sep 24, 2021
caf8ae3
test signing with timestamp
shizhMSFT Sep 24, 2021
cf3667e
apply CertReq in timestamptest
shizhMSFT Sep 24, 2021
13fa1f7
Bump github.com/golang-jwt/jwt/v4 from 4.0.0 to 4.1.0
dependabot[bot] Sep 27, 2021
c9e09cb
improve comments and error messages
shizhMSFT Sep 27, 2021
2d8a548
make exp optional
shizhMSFT Sep 28, 2021
9b2fdcf
deprecate key id support
shizhMSFT Sep 28, 2021
a5b0a73
correct documentation
shizhMSFT Sep 30, 2021
30812bc
verify the signing cert
shizhMSFT Sep 30, 2021
9894901
verify http timestamp endpoint
shizhMSFT Sep 30, 2021
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
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
42 changes: 42 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: test

on:
push:
branches: main
pull_request:
branches: main

jobs:
build:
name: Continuous Testing
runs-on: ubuntu-20.04
strategy:
matrix:
go-version: [1.17]
fail-fast: true
steps:
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Check out code
uses: actions/checkout@v2
- name: Cache Go modules
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Build and test
run: make test
line_endings:
name: Check Line Endings
runs-on: ubuntu-20.04
strategy:
fail-fast: true
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Check line endings
run: make check-line-endings
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}'

.PHONY: all
all: check-line-endings test

.PHONY: test
test: ## run unit tests
go test ./...

.PHONY: clean
clean:
git status --ignored --short | grep '^!! ' | sed 's/!! //' | xargs rm -rf

.PHONY: check-line-endings
check-line-endings: ## check line endings
! find . -name "*.go" -type f -exec file "{}" ";" | grep CRLF

.PHONY: fix-line-endings
fix-line-endings: ## fix line endings
find . -type f -name "*.go" -exec sed -i -e "s/\r//g" {} +
31 changes: 31 additions & 0 deletions crypto/cryptoutil/cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cryptoutil

import (
"crypto/x509"
"encoding/pem"
"os"
)

// ReadCertificateFile reads a certificate PEM file.
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
func ReadCertificateFile(path string) ([]*x509.Certificate, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return ParseCertificatePEM(data)
}

// ParseCertificatePEM parses a certificate PEM.
func ParseCertificatePEM(data []byte) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
block, rest := pem.Decode(data)
for block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
certs = append(certs, cert)
block, rest = pem.Decode(rest)
}
return certs, nil
}
36 changes: 36 additions & 0 deletions crypto/cryptoutil/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cryptoutil

import (
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
)

// ReadPrivateKeyFile reads a key PEM file as a signing key.
func ReadPrivateKeyFile(path string) (crypto.PrivateKey, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return ParsePrivateKeyPEM(data)
}

// ParsePrivateKeyPEM parses a PEM as a signing key.
func ParsePrivateKeyPEM(data []byte) (crypto.PrivateKey, error) {
block, _ := pem.Decode(data)
if block == nil {
return nil, errors.New("no PEM data found")
}
switch block.Type {
case "PRIVATE KEY":
return x509.ParsePKCS8PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
}
return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type)
}
53 changes: 53 additions & 0 deletions crypto/jwsutil/envelope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package jwsutil

import "encoding/json"

// Envelope contains a common payload signed by multiple signatures.
type Envelope struct {
Payload string `json:"payload,omitempty"`
Signatures []Signature `json:"signatures,omitempty"`
}

// Size returns the number of enclosed signatures.
func (e Envelope) Size() int {
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
return len(e.Signatures)
}

// Open opens the evelope and returns the first or default complete signature.
func (e Envelope) Open() CompleteSignature {
if len(e.Signatures) == 0 {
return CompleteSignature{
Payload: e.Payload,
}
}
return CompleteSignature{
Payload: e.Payload,
Signature: e.Signatures[0],
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
}
}

// UnmarshalJSON parses the JSON serialized JWS.
// Reference: RFC 7515 7.2 JWS JSON Serialization.
func (e *Envelope) UnmarshalJSON(data []byte) error {
var combined struct {
CompleteSignature
Signatures []Signature `json:"signatures"`
}
if err := json.Unmarshal(data, &combined); err != nil {
return ErrInvalidJSONSerialization
}
if len(combined.Signatures) == 0 {
*e = Envelope{
Payload: combined.Payload,
Signatures: []Signature{
combined.Signature,
},
}
} else {
*e = Envelope{
Payload: combined.Payload,
Signatures: combined.Signatures,
}
}
return nil
}
102 changes: 102 additions & 0 deletions crypto/jwsutil/envelope_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package jwsutil

import (
"encoding/json"
"reflect"
"testing"
)

func TestEnvelope_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
data string
want Envelope
}{
{
name: "General JWS JSON Serialization Syntax (multiple signatures)",
data: `{
"payload": "test payload",
"signatures": [
{
"protected": "protected foo",
"header": {"unprotected": "foo"},
"signature": "signature foo"
},
{
"protected": "protected bar",
"header": {"unprotected": "bar"},
"signature": "signature bar"
}
]
}`,
want: Envelope{
Payload: "test payload",
Signatures: []Signature{
{
Protected: "protected foo",
Unprotected: []byte(`{"unprotected": "foo"}`),
Signature: "signature foo",
},
{
Protected: "protected bar",
Unprotected: []byte(`{"unprotected": "bar"}`),
Signature: "signature bar",
},
},
},
},
{
name: "General JWS JSON Serialization Syntax (single signature)",
data: `{
"payload": "test payload",
"signatures": [
{
"protected": "protected foo",
"header": {"unprotected": "foo"},
"signature": "signature foo"
}
]
}`,
want: Envelope{
Payload: "test payload",
Signatures: []Signature{
{
Protected: "protected foo",
Unprotected: []byte(`{"unprotected": "foo"}`),
Signature: "signature foo",
},
},
},
},
{
name: "Flattened JWS JSON Serialization Syntax",
data: `{
"payload": "test payload",
"protected": "protected foo",
"header": {"unprotected": "foo"},
"signature": "signature foo"
}`,
want: Envelope{
Payload: "test payload",
Signatures: []Signature{
{
Protected: "protected foo",
Unprotected: []byte(`{"unprotected": "foo"}`),
Signature: "signature foo",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got Envelope
if err := json.Unmarshal([]byte(tt.data), &got); err != nil {
t.Fatalf("Envelope.UnmarshalJSON() error = %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Envelope.UnmarshalJSON() = %v, want %v", got, tt.want)
}
})
}
}
9 changes: 9 additions & 0 deletions crypto/jwsutil/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package jwsutil

import "errors"

// Common errors
var (
ErrInvalidCompactSerialization = errors.New("invalid compact serialization")
ErrInvalidJSONSerialization = errors.New("invalid JSON serialization")
)
56 changes: 56 additions & 0 deletions crypto/jwsutil/signature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Package jwsutil provides serialization utilities for JWT libraries to comfort JWS.
// Reference: RFC 7515 JSON Web Signature (JWS).
package jwsutil

import (
"encoding/json"
"strings"
)

// Signature represents a detached signature.
type Signature struct {
Protected string `json:"protected,omitempty"`
Unprotected json.RawMessage `json:"header,omitempty"`
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
Signature string `json:"signature,omitempty"`
}

// CompleteSignature represents a clear signed signature.
// A CompleteSignature can be viewed as an envelope with a single signature in
// flattened JWS JSON serialization syntax.
// Reference: RFC 7515 7.2 JWS JSON Serialization.
type CompleteSignature struct {
Payload string `json:"payload,omitempty"`
Signature
}

// Parse parses the compact serialized JWS.
// Reference: RFC 7515 7.1 JWS Compact Serialization.
func ParseCompact(serialized string) (CompleteSignature, error) {
parts := strings.Split(serialized, ".")
if len(parts) != 3 {
return CompleteSignature{}, ErrInvalidCompactSerialization
}
return CompleteSignature{
Payload: parts[1],
Signature: Signature{
Protected: parts[0],
Signature: parts[2],
},
}, nil
}

// SerializeCompact serialize the signature in JWS Compact Serialization
// Reference: RFC 7515 7.1 JWS Compact Serialization.
func (s CompleteSignature) SerializeCompact() string {
return strings.Join([]string{s.Protected, s.Payload, s.Signature.Signature}, ".")
}

// Enclose packs the signature into an envelope.
func (s CompleteSignature) Enclose() Envelope {
return Envelope{
Payload: s.Payload,
Signatures: []Signature{
s.Signature,
},
}
}
Loading