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

Sign1 #1

Merged
merged 2 commits into from
Nov 23, 2020
Merged
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
35 changes: 35 additions & 0 deletions .github/workflows/ci-go-cover.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2020-present Montgomery Edwards⁴⁴⁸ (github.com/x448).
# This file is licensed under the MIT License. See LICENSE at https://github.com/x448/workflows for the full text.
#
# CI Go Cover 2020.1.28.
# This GitHub Actions workflow checks if Go (Golang) code coverage satisfies the required minimum.
# The required minimum is specified in the workflow name to keep badge.svg and verified minimum in sync.
#
# To help protect your privacy, this workflow avoids external services.
# This workflow simply runs `go test -short -cover` --> grep --> python.
# The python script is embedded and readable in this file.
#
# Steps to install and set minimum required coverage:
# 0. Copy this file to github.com/OWNER_NAME/REPO_NAME/.github/workflows/ci-go-cover.yml
# 1. Change workflow name from "cover 100%" to "cover ≥92.5%". Script will automatically use 92.5%.
# 2. Update README.md to use the new path to badge.svg because the path includes the workflow name.

name: cover ≥89%
on: [push]
jobs:

# Verify minimum coverage is reached using `go test -short -cover` on latest-ubuntu with default version of Go.
# The grep expression can't be too strict, it needed to be relaxed to work with different versions of Go.
cover:
name: Coverage
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install test cases
run: make install
- name: Go Coverage
run: |
go version
go test -short -cover | grep "^.*coverage:.*of statements$" | python -c "import os,re,sys; cover_rpt = sys.stdin.read(); print(cover_rpt) if len(cover_rpt) != 0 and len(cover_rpt.splitlines()) == 1 else sys.exit(1); min_cover = float(re.findall(r'\d*\.\d+|\d+', os.environ['GITHUB_WORKFLOW'])[0]); cover = float(re.findall(r'\d*\.\d+|\d+', cover_rpt)[0]); sys.exit(1) if (cover > 100) or (cover < min_cover) else sys.exit(0)"
shell: bash
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# GitHub Actions - CI for Go to build & test. See ci-go-cover.yml and linters.yml for code coverage and linters.
# Taken from: https://github.com/fxamacker/cbor/workflows/ci.yml (thanks!)
name: ci
on: [push]
jobs:

# Test on various OS with default Go version.
tests:
name: Test on ${{matrix.os}}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Get dependencies
run: go get -v -t -d ./...
- name: Build project
run: go build .
- name: Install test cases
run: make install
- name: Run tests
run: |
go version
go test -short -race -v .
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ install-go-fuzz:
fuzz: install-go-fuzz
mkdir -p workdir/corpus
cp samples/*.cose workdir/corpus
go-fuzz-build go.mozilla.org/cose
go-fuzz-build github.com/veraison/go-cose
go-fuzz -bin=./cose-fuzz.zip -workdir=workdir

lint:
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
# go-cose

[![CircleCI](https://circleci.com/gh/mozilla-services/go-cose.svg?style=svg)](https://circleci.com/gh/mozilla-services/go-cose)
[![Coverage Status](https://coveralls.io/repos/github/mozilla-services/go-cose/badge.svg)](https://coveralls.io/github/mozilla-services/go-cose)
[![GitHub CI](https://github.com/veraison/go-cose/workflows/ci/badge.svg)](https://github.com/veraison/go-cose/actions?query=workflow%3Aci)

[![Coverage Status](https://github.com/veraison/go-cose/workflows/cover%20%E2%89%A589%25/badge.svg)](https://github.com/veraison/go-cose/actions?query=workflow%3A%22cover%20%E2%89%A589%25%22)

A [COSE](https://tools.ietf.org/html/rfc8152) library for go.

It currently supports signing and verifying the SignMessage type with the ES{256,384,512} and PS256 algorithms.

[API docs](https://godoc.org/go.mozilla.org/cose)
[API docs](https://pkg.go.dev/github.com/veraison/go-cose)

## Usage

### Install

```console
go get -u go.mozilla.org/cose
go get -u github.com/veraison/go-cose
```

### Signing a message
Expand Down
131 changes: 112 additions & 19 deletions cbor.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,53 @@ import (
"github.com/pkg/errors"
)

// SignMessageCBORTag is the CBOR tag for a COSE SignMessage
// from https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml#tags
const SignMessageCBORTag = 98

var signMessagePrefix = []byte{
// 0b110_11000 major type 6 (tag) with additional information
// length 24 bits / 3 bytes (since tags are always uints)
//
// per https://tools.ietf.org/html/rfc7049#section-2.4
'\xd8',

// uint8_t with the tag value
SignMessageCBORTag,

// 0b100_00100 major type 4 (array) with additional
// information 4 for a 4-item array representing a COSE_Sign
// message
'\x84',
}
const (
// SignMessageCBORTag is the CBOR tag for a COSE SignMessage
// from https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml#tags
SignMessageCBORTag = 98

// Sign1MessageCBORTag is the CBOR tag for COSE Single Signer Data Object
Sign1MessageCBORTag = 18
)

var (
signMessagePrefix = []byte{
// 0b110_11000 major type 6 (tag) with additional information
// length 24 bits / 3 bytes (since tags are always uints)
//
// per https://tools.ietf.org/html/rfc7049#section-2.4
'\xd8',

// uint8_t with the tag value
SignMessageCBORTag,

// 0b100_00100 major type 4 (array) with additional
// information 4 for a 4-item array representing a COSE_Sign
// message
'\x84',
}

sign1MessagePrefix = []byte{
// tag(18)
'\xd2',

// array(4)
'\x84',
}
)

// IsSignMessage checks whether the prefix is 0xd8 0x62 for a COSE
// SignMessage
func IsSignMessage(data []byte) bool {
return bytes.HasPrefix(data, signMessagePrefix)
}

// IsSign1Message checks whether the prefix is 0xd2 0x84 for a COSE
// Sign1Message
func IsSign1Message(data []byte) bool {
return bytes.HasPrefix(data, sign1MessagePrefix)
}

// Readonly CBOR encoding and decoding modes.
var (
encMode, encModeError = initCBOREncMode()
Expand Down Expand Up @@ -246,3 +267,75 @@ func (message *SignMessage) UnmarshalCBOR(data []byte) (err error) {
}
return nil
}

type sign1Message struct {
_ struct{} `cbor:",toarray"`
Protected []byte
Unprotected map[interface{}]interface{}
Payload []byte
Signature []byte
}

// MarshalCBOR encodes Sign1Message.
func (message *Sign1Message) MarshalCBOR() ([]byte, error) {
// Verify Sign1Message headers.
if message.Headers == nil {
return nil, errors.New("cbor: Sign1Message has nil Headers")
}
dup := FindDuplicateHeader(message.Headers)
if dup != nil {
return nil, fmt.Errorf("cbor: Duplicate header %+v found", dup)
}

// Convert Sign1Message to sign1Message.
m := sign1Message{
Protected: message.Headers.EncodeProtected(),
Unprotected: message.Headers.EncodeUnprotected(),
Payload: message.Payload,
Signature: message.Signature,
}

// Marshal sign1Message with tag number 18.
return encMode.Marshal(cbor.Tag{Number: Sign1MessageCBORTag, Content: m})
}

// UnmarshalCBOR decodes data into Sign1Message.
func (message *Sign1Message) UnmarshalCBOR(data []byte) (err error) {
if message == nil {
return errors.New("cbor: UnmarshalCBOR on nil Sign1Message pointer")
}

// Decode to cbor.RawTag to extract tag number and tag content as []byte.
var raw cbor.RawTag
err = decMode.Unmarshal(data, &raw)
if err != nil {
return err
}

// Verify tag number.
if raw.Number != Sign1MessageCBORTag {
return fmt.Errorf("cbor: wrong tag number %d", raw.Number)
}

// Decode tag content to sign1Message.
var m sign1Message
err = decMode.Unmarshal(raw.Content, &m)
if err != nil {
return err
}

// Create Headers from sign1Message.
msgHeaders := &Headers{}
err = msgHeaders.Decode([]interface{}{m.Protected, m.Unprotected})
if err != nil {
return fmt.Errorf("cbor: %s", err.Error())
}

*message = Sign1Message{
Headers: msgHeaders,
Payload: m.Payload,
Signature: m.Signature,
}

return nil
}
44 changes: 44 additions & 0 deletions cbor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,50 @@ var CBORTestCases = []CBORTestCase{
// 80 # array(0) no signatures
[]byte("\xd8\x62\x84\x40\xa1\x01\x26\xf6\x80"),
},
{
"Sign1 message with EAT token",
Sign1Message{
Headers: &Headers{
Protected: map[interface{}]interface{}{"alg": "ES256"},
Unprotected: map[interface{}]interface{}{},
},
Payload: []byte{
0xa2, 0x3a, 0x00, 0x01, 0x24, 0xff, 0x4b, 0x6e, 0x6f, 0x6e, 0x63, 0x65,
0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x3a, 0x00, 0x01, 0x25, 0x00, 0x49,
0x75, 0x65, 0x69, 0x64, 0x5f, 0x75, 0x65, 0x69, 0x64,
},
Signature: []byte{
0x28, 0x2d, 0xe0, 0xba, 0xe5, 0x10, 0xff, 0x04, 0xc3, 0x52, 0xd7, 0xa3,
0xf7, 0x88, 0x46, 0x8a, 0xab, 0x0e, 0x04, 0x5c, 0xc4, 0x20, 0x38, 0x42,
0xdf, 0x4b, 0x5e, 0x13, 0x0e, 0xba, 0xc1, 0xe0, 0x0a, 0x43, 0x2d, 0xe0,
0x15, 0x3e, 0xf5, 0xb9, 0x8b, 0xb1, 0x8f, 0x76, 0x53, 0xab, 0x6d, 0xbb,
0x37, 0x7b, 0x77, 0x51, 0x92, 0x1c, 0x99, 0x95, 0x1b, 0x20, 0x79, 0x9d,
0x2e, 0xfb, 0xa6, 0xce,
},
},
// D2 # tag(18) COSE Sign1 tag
// 84 # array(4)
// 43 # bytes(3) protected headers
// A1 # map(1)
// 01 # unsigned(1) common header ID for alg
// 26 # negative(7) ES256 alg ID
// A0 # map(0) empty unprotected headers
// 58 21 # bytes(33) payload
// A23A0001... # EAT token
// 58 40 # bytes(64) signature
// 282DE0BA... # 128 bytes ES256 signature
[]byte{
0xd2, 0x84, 0x43, 0xa1, 0x01, 0x26, 0xa0, 0x58, 0x21, 0xa2, 0x3a, 0x00,
0x01, 0x24, 0xff, 0x4b, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x6f,
0x6e, 0x63, 0x65, 0x3a, 0x00, 0x01, 0x25, 0x00, 0x49, 0x75, 0x65, 0x69,
0x64, 0x5f, 0x75, 0x65, 0x69, 0x64, 0x58, 0x40, 0x28, 0x2d, 0xe0, 0xba,
0xe5, 0x10, 0xff, 0x04, 0xc3, 0x52, 0xd7, 0xa3, 0xf7, 0x88, 0x46, 0x8a,
0xab, 0x0e, 0x04, 0x5c, 0xc4, 0x20, 0x38, 0x42, 0xdf, 0x4b, 0x5e, 0x13,
0x0e, 0xba, 0xc1, 0xe0, 0x0a, 0x43, 0x2d, 0xe0, 0x15, 0x3e, 0xf5, 0xb9,
0x8b, 0xb1, 0x8f, 0x76, 0x53, 0xab, 0x6d, 0xbb, 0x37, 0x7b, 0x77, 0x51,
0x92, 0x1c, 0x99, 0x95, 0x1b, 0x20, 0x79, 0x9d, 0x2e, 0xfb, 0xa6, 0xce,
},
},
}

func MarshalsToExpectedBytes(t *testing.T, testCase CBORTestCase) {
Expand Down
8 changes: 7 additions & 1 deletion common_headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cose

import (
"fmt"

"github.com/pkg/errors"
)

Expand Down Expand Up @@ -299,7 +300,7 @@ func FindDuplicateHeader(headers *Headers) interface{} {
}
headers.Protected = CompressHeaders(headers.Protected)
headers.Unprotected = CompressHeaders(headers.Unprotected)
for k, _ := range headers.Protected {
for k := range headers.Protected {
_, ok := headers.Unprotected[k]
if ok {
return k
Expand All @@ -308,6 +309,11 @@ func FindDuplicateHeader(headers *Headers) interface{} {
return nil
}

// GetAlg returns the algorithm by label or int from the protected headers
func GetAlg(h *Headers) (alg *Algorithm, err error) {
return getAlg(h)
}

// getAlg returns the alg by label or int
// alg should only be in Protected headers so it does not check Unprotected headers
func getAlg(h *Headers) (alg *Algorithm, err error) {
Expand Down
38 changes: 28 additions & 10 deletions core.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@ import (
"crypto/rsa"
"crypto/subtle"
"encoding/base64"
"github.com/pkg/errors"
"io"
"math/big"

"github.com/pkg/errors"
)

// ContextSignature identifies the context of the signature as a
// COSE_Signature structure per
// https://tools.ietf.org/html/rfc8152#section-4.4
const ContextSignature = "Signature"
// Text strings identifying the context of the signature.
// See https://tools.ietf.org/html/rfc8152#section-4.4
const (
// "Signature" for signatures using the COSE_Signature structure.
ContextSignature = "Signature"

// "Signature1" for signatures using the COSE_Sign1 structure.
ContextSignature1 = "Signature1"
)

// Supported Algorithms
var (
Expand Down Expand Up @@ -51,6 +57,11 @@ type Signer struct {
alg *Algorithm
}

// GetAlg retrieves the algorithm associated with the Signer
func (s Signer) GetAlg() *Algorithm {
return s.alg
}

// RSAOptions are options for NewSigner currently just the RSA Key
// size
type RSAOptions struct {
Expand Down Expand Up @@ -249,7 +260,8 @@ func (v *Verifier) Verify(digest []byte, signature []byte) (err error) {

// buildAndMarshalSigStructure creates a Sig_structure, populates it
// with the appropriate fields, and marshals it to CBOR bytes
func buildAndMarshalSigStructure(bodyProtected, signProtected, external, payload []byte) (ToBeSigned []byte, err error) {
// Note that the signProtected parameter is ignored when ctxSignature is ContextSignature1.
func buildAndMarshalSigStructure(ctxSignature string, bodyProtected, signProtected, external, payload []byte) (ToBeSigned []byte, err error) {
// 1. Create a Sig_structure and populate it with the appropriate fields.
//
// Sig_structure = [
Expand All @@ -260,13 +272,19 @@ func buildAndMarshalSigStructure(bodyProtected, signProtected, external, payload
// payload : bstr
// ]
sigStructure := []interface{}{
ContextSignature,
ctxSignature,
bodyProtected, // message.headers.EncodeProtected(),
signProtected, // message.signatures[0].headers.EncodeProtected(),
external,
payload,
}

// The protected attributes from the signer structure field are omitted
// for the COSE_Sign1 signature structure.
if ctxSignature != ContextSignature1 {
// message.signatures[0].headers.EncodeProtected()
sigStructure = append(sigStructure, signProtected)
}
sigStructure = append(sigStructure, external)
sigStructure = append(sigStructure, payload)

// 2. Create the value ToBeSigned by encoding the Sig_structure to a
// byte string, using the encoding described in Section 14.
ToBeSigned, err = Marshal(sigStructure)
Expand Down
Loading