Describe the signature scheme in docs/FORMAT.md as the code builds it (closes #187) #188

Open
clawbot wants to merge 1 commits from issue-187-signature-scheme-doc into next
5 changed files with 82 additions and 30 deletions
+37 -20
View File
@@ -36,8 +36,8 @@ The outer message contains:
| `sha256` | 104 | bytes | SHA-256 hash of the **compressed** `innerMessage` (corruption detection) |
| `uuid` | 105 | bytes | Random v4 UUID; must match the inner message UUID |
| `innerMessage` | 199 | bytes | Zstd-compressed serialized `MFFile` message |
| `signature` | 201 | bytes (optional) | OpenPGP detached signature (ASCII-armored or binary) |
| `signer` | 202 | bytes (optional) | Fingerprint of the signing key |
| `signature` | 201 | bytes (optional) | One OpenPGP signature over the canonical string, armored or binary |
| `signer` | 202 | bytes (optional) | Fingerprint of the signing primary key, in upper-case hex |
| `signingPubKey` | 203 | bytes (optional) | Full OpenPGP public key of the signing key (ASCII-armored or binary) |
### SHA-256 Hash
@@ -128,34 +128,51 @@ Implementations must be able to verify SHA-256 multihashes at minimum.
## Signature Scheme
Signing is optional. When present, the signature covers a canonical string
constructed as:
Signing is optional. A signature is stored only in the outer message's
`signature` field, never in a separate file. It is one OpenPGP signature over
the canonical string below, armored or binary, and does not contain the string,
which a verifier builds again from the outer message.
The canonical string is three parts separated by hyphens (`-`):
```
ZNAVSRFG-<UUID>-<SHA256>
```
Where:
- `ZNAVSRFG` is the magic, in ASCII.
- `<UUID>` is the 16 bytes of the outer message's `uuid` as 32 lower-case hex
digits, with no hyphens.
- `<SHA256>` is the lower-case hex of the outer message's `sha256` encoded as a
SHA-256 multihash (see Hash Format). That multihash is the algorithm code
`0x12`, the digest length `0x20` and the 32 bytes of the hash, so this part is
68 hex digits starting `1220`.
- `ZNAVSRFG` is the magic bytes string (literal ASCII)
- `<UUID>` is the hex-encoded UUID from the outer message
- `<SHA256>` is the hex-encoded SHA-256 hash from the outer message (covering
compressed data)
The signature covers exactly the 110 ASCII characters of the string, with no
line ending. For example, the UUID `b0fb6918-6298-443b-ada2-1b162f4dc0b3` and a
`sha256` of `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
in hex give this canonical string:
Components are separated by hyphens. The signature is an OpenPGP detached
signature over this canonical string, stored in the `signature` field of the
outer message. The signing key's public key goes in `signingPubKey` and its
fingerprint, in hex, in `signer`.
```
ZNAVSRFG-b0fb69186298443bada21b162f4dc0b3-1220e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```
The signature covers `innerMessage` only through `sha256`, so a verifier checks
`sha256` against `innerMessage` (see SHA-256 Hash) as well as the signature.
The signing key's public key goes in `signingPubKey` and the fingerprint of its
primary key, in upper-case hex, in `signer`. The reference implementation signs
only with version 4 keys, whose fingerprints are 40 hex digits, and writes both
the signature and the public key armored.
A verifier accepts a signed manifest only if `signingPubKey` holds exactly one
primary key, `signature` is one good signature over the canonical string made by
that key (or one of its subkeys), and `signer` is that key's fingerprint. The
reference implementation refuses to load a manifest that fails these checks;
`check` and `fetch` given `--require-signature` then compare the required
fingerprint with `signer`. It also refuses a manifest whose `signingPubKey`
holds a DSA key or subkey, or any secret key or subkey, since checking the
self-signatures of a DSA key or the numbers of a secret key can take hours when
those numbers are very large.
that key (or one of its subkeys), and `signer` is that primary key's
fingerprint, compared without regard to case. The reference implementation
refuses to load a manifest that fails these checks; `check` and `fetch` given
`--require-signature` then compare the required fingerprint with `signer`. It
also refuses a manifest whose `signingPubKey` holds a DSA key or subkey, or any
secret key or subkey, since checking the self-signatures of a DSA key or the
numbers of a secret key can take hours when those numbers are very large.
## Deterministic Serialization
+30
View File
@@ -0,0 +1,30 @@
//nolint:testpackage // white-box tests exercise unexported internals
package mfer
import (
"encoding/hex"
"testing"
"uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestSignatureString checks the canonical string for the UUID and sha256
// of the example in the Signature Scheme section of docs/FORMAT.md, which
// shows the same string.
func TestSignatureString(t *testing.T) {
t.Parallel()
manifestUUID := uuid.MustParse("b0fb6918-6298-443b-ada2-1b162f4dc0b3")
sha256Hash, err := hex.DecodeString(
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
require.NoError(t, err)
m := &manifest{pbOuter: &MFFileOuter{Uuid: manifestUUID[:], Sha256: sha256Hash}}
got, err := m.signatureString()
require.NoError(t, err)
assert.Equal(t, "ZNAVSRFG-b0fb69186298443bada21b162f4dc0b3-"+
"1220e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", got)
}
+6 -3
View File
@@ -223,11 +223,14 @@ type MFFileOuter struct {
// uuid must match the uuid in the inner message
Uuid []byte `protobuf:"bytes,105,opt,name=uuid,proto3" json:"uuid,omitempty"`
InnerMessage []byte `protobuf:"bytes,199,opt,name=innerMessage,proto3" json:"innerMessage,omitempty"`
//detached signature, ascii or binary
// one OpenPGP signature, armored or binary, over the canonical string
// ZNAVSRFG-<uuid>-<multihash of sha256>, with the uuid and the
// multihash in lower-case hex (see docs/FORMAT.md). A signature is
// never stored anywhere else.
Signature []byte `protobuf:"bytes,201,opt,name=signature,proto3,oneof" json:"signature,omitempty"`
//full GPG key id
// fingerprint of the signing primary key, in upper-case hex
Signer []byte `protobuf:"bytes,202,opt,name=signer,proto3,oneof" json:"signer,omitempty"`
//full GPG signing public key, ascii or binary
// OpenPGP public key of the signer, one primary key, armored or binary
SigningPubKey []byte `protobuf:"bytes,203,opt,name=signingPubKey,proto3,oneof" json:"signingPubKey,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
+8 -6
View File
@@ -32,15 +32,17 @@ message MFFileOuter {
bytes uuid = 105;
bytes innerMessage = 199;
// 2xx for optional manifest root attributes
// think we might use gosignify instead of gpg:
// github.com/frankbraun/gosignify
//detached signature, ascii or binary
// 2xx for optional manifest root attributes
// one OpenPGP signature, armored or binary, over the canonical string
// ZNAVSRFG-<uuid>-<multihash of sha256>, with the uuid and the
// multihash in lower-case hex (see docs/FORMAT.md). A signature is
// never stored anywhere else.
optional bytes signature = 201;
//full GPG key id
// fingerprint of the signing primary key, in upper-case hex
optional bytes signer = 202;
//full GPG signing public key, ascii or binary
// OpenPGP public key of the signer, one primary key, armored or binary
optional bytes signingPubKey = 203;
}
+1 -1
View File
@@ -1 +1 @@
3d4dcb0b2f4640dd3ae6bb48b833e9f26ae9771e2d3e88bd646e7f1724dda654 mf.proto
1304a5d944dc272e07b01b2b74c7fdfd3a478ff75efe9f5978cb2d513a5063bc mf.proto