Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
254 changes: 253 additions & 1 deletion cmd/sign1util/main.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,165 @@
package main

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"os"
"strings"

"github.com/Microsoft/cosesign1go/pkg/cosesign1"
didx509resolver "github.com/Microsoft/didx509go/pkg/did-x509-resolver"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)

// jwk is a minimal JSON Web Key representation used to parse CCF transparency
// service /jwks responses.
type jwk struct {
Kty string `json:"kty"`
Crv string `json:"crv"`
Kid string `json:"kid"`
X string `json:"x"`
Y string `json:"y"`
}

type jwkSet struct {
Keys []jwk `json:"keys"`
}

func jwkToPublicKey(k jwk) (crypto.PublicKey, error) {
if k.Kty != "EC" {
return nil, fmt.Errorf("unsupported kty %q", k.Kty)
}
var curve elliptic.Curve
switch k.Crv {
case "P-256":
curve = elliptic.P256()
case "P-384":
curve = elliptic.P384()
case "P-521":
curve = elliptic.P521()
default:
return nil, fmt.Errorf("unsupported curve %q", k.Crv)
}
xBytes, err := base64.RawURLEncoding.DecodeString(k.X)
if err != nil {
return nil, fmt.Errorf("decoding x: %w", err)
}
yBytes, err := base64.RawURLEncoding.DecodeString(k.Y)
if err != nil {
return nil, fmt.Errorf("decoding y: %w", err)
}
return &ecdsa.PublicKey{
Curve: curve,
X: new(big.Int).SetBytes(xBytes),
Y: new(big.Int).SetBytes(yBytes),
}, nil
}

// fetchIssuerJWKS GETs https://<issuer>/jwks and returns the keys keyed by
// their `kid`.
//
// CCF nodes serve TLS using a self-signed certificate whose authenticity is
// backed by SNP attestation rather than a public CA, so TLS verification is
// skipped here. A production verifier should validate the node's attestation
// evidence instead.
func fetchIssuerJWKS(issuer string) (map[string]crypto.PublicKey, error) {
url := "https://" + issuer + "/jwks"
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // CCF uses self-signed certs
},
}
Comment thread
micromaomao marked this conversation as resolved.
Outdated
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("GET %s: %w", url, err)
Comment thread
micromaomao marked this conversation as resolved.
Outdated
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET %s: status %d", url, resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", url, err)
}
var set jwkSet
if err := json.Unmarshal(body, &set); err != nil {
return nil, fmt.Errorf("parsing %s: %w", url, err)
}
out := make(map[string]crypto.PublicKey, len(set.Keys))
for i, k := range set.Keys {
pub, err := jwkToPublicKey(k)
if err != nil {
return nil, fmt.Errorf("key %d (kid=%s): %w", i, k.Kid, err)
}
out[k.Kid] = pub
}
return out, nil
}

// fetchCCFReceiptKeys returns a kid->PublicKey map by fetching the JWKS for
// each unique receipt issuer.
func fetchCCFReceiptKeys(receipts []cosesign1.ParsedCOSEReceipt) (map[string]crypto.PublicKey, error) {
seen := map[string]bool{}
keys := map[string]crypto.PublicKey{}
for _, r := range receipts {
if r.Issuer == "" {
return nil, fmt.Errorf("receipt has no issuer; cannot fetch JWKS")
}
if seen[r.Issuer] {
continue
}
seen[r.Issuer] = true
issuerKeys, err := fetchIssuerJWKS(r.Issuer)
if err != nil {
return nil, err
}
for kid, k := range issuerKeys {
keys[kid] = k
}
}
return keys, nil
Comment thread
micromaomao marked this conversation as resolved.
Outdated
}

// formatValue formats a CBOR-decoded value in a human-readable way that
// preserves integer keys (unlike JSON).
func formatValue(v interface{}) string {
switch v := v.(type) {
case map[interface{}]interface{}:
parts := make([]string, 0, len(v))
for key, val := range v {
parts = append(parts, fmt.Sprintf("%s: %s", formatValue(key), formatValue(val)))
}
return "{" + strings.Join(parts, ", ") + "}"
case []interface{}:
parts := make([]string, 0, len(v))
for _, val := range v {
parts = append(parts, formatValue(val))
}
return "[" + strings.Join(parts, ", ") + "]"
case []byte:
return fmt.Sprintf("0x%x", v)
case string:
return fmt.Sprintf("%q", v)
case nil:
return "null"
default:
return fmt.Sprintf("%v", v)
}
}

func printKeyValue(indent string, k, v interface{}) {
fmt.Fprintf(os.Stdout, "%s%v: %s\n", indent, k, formatValue(v))
}

func checkCoseSign1(inputFilename string, chainFilename string, didString string, verbose bool) (*cosesign1.UnpackedCoseSign1, error) {
coseBlob, err := os.ReadFile(inputFilename)
if err != nil {
Expand All @@ -33,13 +183,57 @@ func checkCoseSign1(inputFilename string, chainFilename string, didString string
}

fmt.Fprint(os.Stdout, "checkCoseSign1 passed\n")

// If the envelope carries COSE Receipts, validate each against the CCF
// ledger profile.
var receiptKeys map[string]crypto.PublicKey
if len(unpacked.Receipts) > 0 {
receiptKeys, err = fetchCCFReceiptKeys(unpacked.Receipts)
if err != nil {
fmt.Fprintf(os.Stdout, "fetching CCF receipt keys failed - %s\n", err)
return nil, fmt.Errorf("fetching CCF receipt keys: %w", err)
}
for i, r := range unpacked.Receipts {
if err := r.Validate(receiptKeys); err != nil {
fmt.Fprintf(os.Stdout, "CCF receipt %d from %s validation failed - %s\n", i, r.Issuer, err)
return nil, fmt.Errorf("CCF receipt %d from %s validation failed: %w", i, r.Issuer, err)
}
fmt.Fprintf(os.Stdout, "CCF receipt %d from %s validation passed\n", i, r.Issuer)
}
}
if verbose {
fmt.Fprintf(os.Stdout, "iss: %s\n", unpacked.Issuer)
fmt.Fprintf(os.Stdout, "feed: %s\n", unpacked.Feed)
fmt.Fprintf(os.Stdout, "cty: %s\n", unpacked.ContentType)
fmt.Fprintf(os.Stdout, "pubkey: %s\n", unpacked.Pubkey)
fmt.Fprintf(os.Stdout, "pubcert: %s\n", unpacked.Pubcert)
fmt.Fprintf(os.Stdout, "payload:\n%s\n", string(unpacked.Payload[:]))
fmt.Fprintf(os.Stdout, "all protected headers:\n")
isHashEnvelop := false
for k, v := range unpacked.Protected {
if k, ok := k.(int64); ok && (k == cosesign1.COSE_Header_x5chain || k == cosesign1.COSE_Header_x5t) {
fmt.Fprintf(os.Stdout, " %d: ...\n", k)
continue
}
if k, ok := k.(int64); ok && k == cosesign1.COSE_Header_PreimageContentType {
isHashEnvelop = true
Comment thread
micromaomao marked this conversation as resolved.
Outdated
}
printKeyValue(" ", k, v)
}
fmt.Fprintf(os.Stdout, "all unprotected headers:\n")
for k, v := range unpacked.Unprotected {
if k, ok := k.(int64); ok && k == cosesign1.COSE_Header_Receipts {
fmt.Fprintf(os.Stdout, " %d: ...\n", k)
continue
}
printKeyValue(" ", k, v)
}
fmt.Fprintf(os.Stdout, "payload:\n")
if isHashEnvelop {
fmt.Fprintf(os.Stdout, "%x", unpacked.Payload[:])
} else {
fmt.Fprintf(os.Stdout, "%s", string(unpacked.Payload))
}
fmt.Fprintf(os.Stdout, "\n")
}
if len(didString) > 0 {
if len(chainPEMString) == 0 {
Expand All @@ -53,6 +247,48 @@ func checkCoseSign1(inputFilename string, chainFilename string, didString string
fmt.Fprintf(os.Stdout, "DID resolvers failed: err: %s\n", err.Error())
}
}

for i, receipt := range unpacked.Receipts {
if !verbose {
continue
}
msg := receipt.Message
fmt.Fprintf(os.Stdout, "receipt %d:\n", i)
fmt.Fprintf(os.Stdout, " protected headers:\n")
for k, v := range msg.Headers.Protected {
if k, ok := k.(int64); ok && k == cosesign1.COSE_Header_kid {
fmt.Fprintf(os.Stdout, " %d: %q\n", k, v.([]byte))
Comment thread
micromaomao marked this conversation as resolved.
Outdated
continue
}
printKeyValue(" ", k, v)
}
fmt.Fprintf(os.Stdout, " unprotected headers:\n")
for k, v := range msg.Headers.Unprotected {
if k, ok := k.(int64); ok && k == cosesign1.COSE_Header_vdp {
m, ok := v.(map[interface{}]interface{})
if !ok {
fmt.Fprintf(os.Stdout, " %d: ... (failed to parse)\n", k)
continue
}
fmt.Fprintf(os.Stdout, " %d:\n", k)
for k, v := range m {
if k, ok := k.(int64); ok && k == cosesign1.COSE_ProofInclusion {
fmt.Fprintf(os.Stdout, " %d: inclusion proof\n", k)
continue
}
if k, ok := k.(int64); ok && k == cosesign1.COSE_ProofConsistency {
fmt.Fprintf(os.Stdout, " %d: consistency proof\n", k)
continue
}
printKeyValue(" ", k, v)
}
continue
}
printKeyValue(" ", k, v)
}
fmt.Fprintf(os.Stdout, " payload: %q\n", msg.Payload)
}

return unpacked, err
}

Expand Down Expand Up @@ -367,6 +603,22 @@ var chainCmd = cli.Command{
func main() {
app := cli.NewApp()
app.Name = "sign1util"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "log-level",
Usage: "logrus log level [trace|debug|info|warn|error]",
EnvVar: "LOG_LEVEL",
Value: "info",
},
}
app.Before = func(ctx *cli.Context) error {
lvl, err := logrus.ParseLevel(ctx.GlobalString("log-level"))
if err != nil {
return fmt.Errorf("invalid --log-level: %w", err)
}
logrus.SetLevel(lvl)
return nil
}
app.Commands = []cli.Command{
createCmd,
checkCmd,
Expand Down
Loading
Loading