Skip to content

Commit

Permalink
introduce the concept and support for assertions with no authority, f…
Browse files Browse the repository at this point in the history
…irst pass at serial-request
  • Loading branch information
pedronis committed Jul 29, 2016
1 parent cf8a3f7 commit 1f88b31
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 20 deletions.
87 changes: 71 additions & 16 deletions asserts/asserts.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ import (
"strconv"
)

type typeFlags int

const (
freestanding typeFlags = iota + 1
)

// AssertionType describes a known assertion type with its name and metadata.
type AssertionType struct {
// Name of the type.
Expand All @@ -37,21 +43,27 @@ type AssertionType struct {
PrimaryKey []string

assembler func(assert assertionBase) (Assertion, error)
flags typeFlags
}

// Understood assertion types.
var (
AccountType = &AssertionType{"account", []string{"account-id"}, assembleAccount}
AccountKeyType = &AssertionType{"account-key", []string{"account-id", "public-key-id"}, assembleAccountKey}
ModelType = &AssertionType{"model", []string{"series", "brand-id", "model"}, assembleModel}
SerialType = &AssertionType{"serial", []string{"brand-id", "model", "serial"}, assembleSerial}
SnapDeclarationType = &AssertionType{"snap-declaration", []string{"series", "snap-id"}, assembleSnapDeclaration}
SnapBuildType = &AssertionType{"snap-build", []string{"snap-sha3-384"}, assembleSnapBuild}
SnapRevisionType = &AssertionType{"snap-revision", []string{"snap-sha3-384"}, assembleSnapRevision}
AccountType = &AssertionType{"account", []string{"account-id"}, assembleAccount, 0}
AccountKeyType = &AssertionType{"account-key", []string{"account-id", "public-key-id"}, assembleAccountKey, 0}
ModelType = &AssertionType{"model", []string{"series", "brand-id", "model"}, assembleModel, 0}
SerialType = &AssertionType{"serial", []string{"brand-id", "model", "serial"}, assembleSerial, 0}
SnapDeclarationType = &AssertionType{"snap-declaration", []string{"series", "snap-id"}, assembleSnapDeclaration, 0}
SnapBuildType = &AssertionType{"snap-build", []string{"snap-sha3-384"}, assembleSnapBuild, 0}
SnapRevisionType = &AssertionType{"snap-revision", []string{"snap-sha3-384"}, assembleSnapRevision, 0}

// ...
)

// Freestanding assertion types (on the wire and/or self-signed).
var (
SerialRequestType = &AssertionType{"serial-request", nil, assembleSerialRequest, freestanding}
)

var typeRegistry = map[string]*AssertionType{
AccountType.Name: AccountType,
AccountKeyType.Name: AccountKeyType,
Expand All @@ -60,6 +72,8 @@ var typeRegistry = map[string]*AssertionType{
SnapDeclarationType.Name: SnapDeclarationType,
SnapBuildType.Name: SnapBuildType,
SnapRevisionType.Name: SnapRevisionType,
// freestanding
SerialRequestType.Name: SerialRequestType,
}

// Type returns the AssertionType with name or nil
Expand Down Expand Up @@ -189,10 +203,10 @@ var _ Assertion = (*assertionBase)(nil)
// previous level introduction (the " "*baseindent " -" bit)
// length minus 1.
//
// The following headers are mandatory:
// In general the following headers are mandatory:
//
// type
// authority-id (the signer id)
// authority-id (the authority id, must be left of freestanding assertions though)
//
// Further for a given assertion type all the primary key headers
// must be non empty and must not contain '/'.
Expand Down Expand Up @@ -439,10 +453,6 @@ func assemble(headers map[string]interface{}, body, content, signature []byte) (
return nil, fmt.Errorf("assertion body length and declared body-length don't match: %v != %v", len(body), length)
}

if _, err := checkNotEmptyString(headers, "authority-id"); err != nil {
return nil, fmt.Errorf("assertion: %v", err)
}

typ, err := checkNotEmptyString(headers, "type")
if err != nil {
return nil, fmt.Errorf("assertion: %v", err)
Expand All @@ -452,6 +462,17 @@ func assemble(headers map[string]interface{}, body, content, signature []byte) (
return nil, fmt.Errorf("unknown assertion type: %q", typ)
}

if assertType.flags&freestanding == 0 {
if _, err := checkNotEmptyString(headers, "authority-id"); err != nil {
return nil, fmt.Errorf("assertion: %v", err)
}
} else {
_, ok := headers["authority-id"]
if ok {
return nil, fmt.Errorf("freestanding %q assertion cannot have authority-id set", assertType.Name)
}
}

for _, primKey := range assertType.PrimaryKey {
if _, err := checkPrimaryKey(headers, primKey); err != nil {
return nil, fmt.Errorf("assertion %s: %v", assertType.Name, err)
Expand Down Expand Up @@ -490,6 +511,8 @@ func assembleAndSign(assertType *AssertionType, headers map[string]interface{},
return nil, err
}

withAuthority := assertType.flags&freestanding == 0

err = checkHeaders(headers)
if err != nil {
return nil, err
Expand All @@ -502,8 +525,15 @@ func assembleAndSign(assertType *AssertionType, headers map[string]interface{},
finalHeaders["type"] = assertType.Name
finalHeaders["body-length"] = strconv.Itoa(bodyLength)

if _, err := checkNotEmptyString(finalHeaders, "authority-id"); err != nil {
return nil, err
if withAuthority {
if _, err := checkNotEmptyString(finalHeaders, "authority-id"); err != nil {
return nil, err
}
} else {
_, ok := finalHeaders["authority-id"]
if ok {
return nil, fmt.Errorf("freestanding %q assertion cannot have authority-id set", assertType.Name)
}
}

revision, err := checkRevision(finalHeaders)
Expand All @@ -514,7 +544,10 @@ func assembleAndSign(assertType *AssertionType, headers map[string]interface{},
buf := bytes.NewBufferString("type: ")
buf.WriteString(assertType.Name)

writeHeader(buf, finalHeaders, "authority-id")
if withAuthority {
writeHeader(buf, finalHeaders, "authority-id")
}

if revision > 0 {
writeHeader(buf, finalHeaders, "revision")
} else {
Expand Down Expand Up @@ -581,6 +614,14 @@ func assembleAndSign(assertType *AssertionType, headers map[string]interface{},
return assert, nil
}

// FreestandingSign assembles a freestanding assertion with the provided information and signs it with the given private key.
func FreestandingSign(assertType *AssertionType, headers map[string]interface{}, body []byte, privKey PrivateKey) (Assertion, error) {
if assertType.flags&freestanding == 0 {
return nil, fmt.Errorf("cannot sign non-freestanding (i.e. with a definite authority) assertions with FreestandingSign")
}
return assembleAndSign(assertType, headers, body, privKey)
}

// Encode serializes an assertion.
func Encode(assert Assertion) []byte {
content, signature := assert.Signature()
Expand Down Expand Up @@ -637,3 +678,17 @@ func (enc *Encoder) Encode(assert Assertion) error {
encoded := Encode(assert)
return enc.append(encoded)
}

// SignatureCheck checks the signature of the assertion against the given public key. Useful for freestanding assertions.
func SignatureCheck(assert Assertion, pubKey PublicKey) error {
content, encodedSig := assert.Signature()
sig, err := decodeSignature(encodedSig)
if err != nil {
return err
}
err = pubKey.verify(content, sig)
if err != nil {
return fmt.Errorf("failed signature verification: %v", err)
}
return nil
}
34 changes: 34 additions & 0 deletions asserts/asserts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,17 @@ func (as *assertsSuite) TestDecodeInvalid(c *C) {
}
}

func (as *assertsSuite) TestDecodeFreestandingInvalid(c *C) {
invalid := "type: test-only-freestanding\n" +
"authority-id: auth-id1\n" +
"hdr: FOO" +
"\n\n" +
"openpgp c2ln"

_, err := asserts.Decode([]byte(invalid))
c.Check(err, ErrorMatches, `freestanding "test-only-freestanding" assertion cannot have authority-id set`)
}

func checkContent(c *C, a asserts.Assertion, encoded string) {
expected, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
Expand Down Expand Up @@ -537,3 +548,26 @@ func (as *assertsSuite) TestAssembleHeadersCheck(c *C) {
_, err := asserts.Assemble(headers, nil, cont, nil)
c.Check(err, ErrorMatches, `header "revision": header values must be strings or nested lists with strings as the only scalars: 5`)
}

func (as *assertsSuite) TestFreestandingSignMisuse(c *C) {
_, err := asserts.FreestandingSign(asserts.TestOnlyType, nil, nil, testPrivKey1)
c.Check(err, ErrorMatches, `cannot sign non-freestanding \(i\.e\. with a definite authority\) assertions with FreestandingSign`)

_, err = asserts.FreestandingSign(asserts.TestOnlyFreestandingType,
map[string]interface{}{
"authority-id": "auth-id1",
"hdr": "FOO",
}, nil, testPrivKey1)
c.Check(err, ErrorMatches, `freestanding "test-only-freestanding" assertion cannot have authority-id set`)
}

func (ss *serialSuite) TestSignatureCheckError(c *C) {
sreq, err := asserts.FreestandingSign(asserts.TestOnlyFreestandingType,
map[string]interface{}{
"hdr": "FOO",
}, nil, testPrivKey1)
c.Assert(err, IsNil)

err = asserts.SignatureCheck(sreq, testPrivKey2.PublicKey())
c.Check(err, ErrorMatches, `failed signature verification:.*`)
}
8 changes: 7 additions & 1 deletion asserts/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,18 @@ func (db *Database) Check(assert Assertion) error {
// It will return an error when trying to add an older revision of the assertion than the one currently stored.
func (db *Database) Add(assert Assertion) error {
assertType := assert.Type()

keyLen := len(assertType.PrimaryKey)
if keyLen == 0 {
return fmt.Errorf("cannot add %q assertion with zero-length primary key", assertType.Name)
}

err := db.Check(assert)
if err != nil {
return err
}

keyValues := make([]string, len(assertType.PrimaryKey))
keyValues := make([]string, keyLen)
for i, k := range assertType.PrimaryKey {
keyVal := assert.HeaderString(k)
if keyVal == "" {
Expand Down
11 changes: 11 additions & 0 deletions asserts/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,17 @@ func (safs *signAddFindSuite) TestAddSuperseding(c *C) {
c.Check(err, ErrorMatches, "revision 0 is older than current revision 1")
}

func (safs *signAddFindSuite) TestAddFreestandingNoPrimaryKey(c *C) {
headers := map[string]interface{}{
"hdr": "FOO",
}
a, err := asserts.FreestandingSign(asserts.TestOnlyFreestandingType, headers, nil, testPrivKey0)
c.Assert(err, IsNil)

err = safs.db.Add(a)
c.Assert(err, ErrorMatches, `cannot add "test-only-freestanding" assertion with zero-length primary key`)
}

func (safs *signAddFindSuite) TestFindNotFound(c *C) {
headers := map[string]interface{}{
"authority-id": "canonical",
Expand Down
58 changes: 58 additions & 0 deletions asserts/device_asserts.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,61 @@ func assembleSerial(assert assertionBase) (Assertion, error) {
pubKey: pubKey,
}, nil
}

// SerialRequest holds a serial-request assertion, which is a self-signed request to obtain a full device identity bound to the device public key.
type SerialRequest struct {
assertionBase
pubKey PublicKey
}

// BrandID returns the brand identifier of the device making the request.
func (sreq *SerialRequest) BrandID() string {
return sreq.HeaderString("brand-id")
}

// Model returns the model name identifier of the device making the request.
func (sreq *SerialRequest) Model() string {
return sreq.HeaderString("model")
}

// NonceTicket returns the nonce/ticket for the request.
func (sreq *SerialRequest) NonceTicket() string {
return sreq.HeaderString("nonce-ticket")
}

// DeviceKey returns the public key of the device making the request.
func (sreq *SerialRequest) DeviceKey() PublicKey {
return sreq.pubKey
}

func assembleSerialRequest(assert assertionBase) (Assertion, error) {
_, err := checkNotEmptyString(assert.headers, "brand-id")
if err != nil {
return nil, err
}

_, err = checkNotEmptyString(assert.headers, "model")
if err != nil {
return nil, err
}

_, err = checkNotEmptyString(assert.headers, "nonce-ticket")
if err != nil {
return nil, err
}

encodedKey, err := checkNotEmptyString(assert.headers, "device-key")
if err != nil {
return nil, err
}
pubKey, err := decodePublicKey([]byte(encodedKey))
if err != nil {
return nil, err
}

// ignore extra headers and non-empty body for future compatibility
return &SerialRequest{
assertionBase: assert,
pubKey: pubKey,
}, nil
}
62 changes: 61 additions & 1 deletion asserts/device_asserts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ func (ss *serialSuite) TestDecodeOK(c *C) {
}

const (
serialErrPrefix = "assertion serial: "
serialErrPrefix = "assertion serial: "
serialReqErrPrefix = "assertion serial-request: "
)

func (ss *serialSuite) TestDecodeInvalid(c *C) {
Expand Down Expand Up @@ -231,3 +232,62 @@ func (ss *serialSuite) TestDecodeInvalid(c *C) {
c.Check(err, ErrorMatches, serialErrPrefix+test.expectedErr)
}
}

func (ss *serialSuite) TestSerialRequestHappy(c *C) {
sreq, err := asserts.FreestandingSign(asserts.SerialRequestType,
map[string]interface{}{
"brand-id": "brand-id1",
"model": "baz-3000",
// TODO add key hash header
"device-key": ss.encodedDevKey,
"nonce-ticket": "NONCE-TICKET",
}, []byte("HW-DETAILS"), ss.deviceKey)
c.Assert(err, IsNil)

// roundtrip
a, err := asserts.Decode(asserts.Encode(sreq))
c.Assert(err, IsNil)

sreq2, ok := a.(*asserts.SerialRequest)
c.Assert(ok, Equals, true)

// standalone signature check
err = asserts.SignatureCheck(sreq2, sreq2.DeviceKey())
c.Check(err, IsNil)

c.Check(sreq2.BrandID(), Equals, "brand-id1")
c.Check(sreq2.Model(), Equals, "baz-3000")
c.Check(sreq2.NonceTicket(), Equals, "NONCE-TICKET")
}

func (ss *serialSuite) TestSerialRequestDecodeInvalid(c *C) {
encoded := "type: serial-request\n" +
"brand-id: brand-id1\n" +
"model: baz-3000\n" +
"device-key:\n DEVICEKEY\n" +
"nonce-ticket: NONCE-TICKET\n" +
"body-length: 2\n\n" +
"HW" +
"\n\n" +
"openpgp c2ln"

invalidTests := []struct{ original, invalid, expectedErr string }{
{"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
{"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
{"model: baz-3000\n", "", `"model" header is mandatory`},
{"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
{"nonce-ticket: NONCE-TICKET\n", "", `"nonce-ticket" header is mandatory`},
{"nonce-ticket: NONCE-TICKET\n", "nonce-ticket: \n", `"nonce-ticket" header should not be empty`},
{"device-key:\n DEVICEKEY\n", "", `"device-key" header is mandatory`},
{"device-key:\n DEVICEKEY\n", "device-key: \n", `"device-key" header should not be empty`},
{"device-key:\n DEVICEKEY\n", "device-key: openpgp ZZZ\n", `public key: could not decode base64 data:.*`},
}

for _, test := range invalidTests {
invalid := strings.Replace(encoded, test.original, test.invalid, 1)
invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)

_, err := asserts.Decode([]byte(invalid))
c.Check(err, ErrorMatches, serialReqErrPrefix+test.expectedErr)
}
}
Loading

0 comments on commit 1f88b31

Please sign in to comment.