Skip to content

Commit

Permalink
Merge pull request #1 from veraison/sign1
Browse files Browse the repository at this point in the history
Sign1 (first pass)
  • Loading branch information
thomas-fossati committed Nov 23, 2020
2 parents 25dc96d + 2e48ce5 commit f9720fc
Show file tree
Hide file tree
Showing 15 changed files with 436 additions and 44 deletions.
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

0 comments on commit f9720fc

Please sign in to comment.