You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
snapd-extra-utils/snapd-seed-glue/assertions.go

238 lines
8.9 KiB

// Copyright (C) 2024 Simon Quigley <tsimonq2@ubuntu.com>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 3
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
package main
import (
"encoding/base64"
"encoding/hex"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/store"
"gopkg.in/yaml.v3"
)
// downloadAssertions dynamically fetches the necessary assertions and saves them to a file.
// It ensures that the account-key assertion is written first in the .assert file.
func downloadAssertions(storeClient *store.Store, snapInfo *snap.Info, downloadDir string) error {
// Define the path for the assertions file
assertionsPath := filepath.Join(downloadDir, fmt.Sprintf("%s_%d.assert", snapInfo.SuggestedName, snapInfo.Revision.N))
// Extract necessary fields from snapInfo
snapSHA := snapInfo.Sha3_384
snapID := snapInfo.SnapID
publisherID := snapInfo.Publisher.ID
series := "16" // Consider making this dynamic if possible
// Define assertion types
assertionTypes := map[string]*asserts.AssertionType{
"snap-revision": asserts.SnapRevisionType,
"snap-declaration": asserts.SnapDeclarationType,
"account-key": asserts.AccountKeyType,
"account": asserts.AccountType,
}
// Open the assertions file for writing
assertionsFile, err := os.Create(assertionsPath)
if err != nil {
return fmt.Errorf("failed to create assertions file: %w", err)
}
defer assertionsFile.Close()
// Step 1: Fetch snap-declaration assertion
snapDecl, err := storeClient.Assertion(assertionTypes["snap-declaration"], []string{series, snapID}, nil)
if err != nil {
return fmt.Errorf("failed to fetch snap-declaration assertion for snap %s: %w", snapInfo.SuggestedName, err)
}
// Step 2: Extract sign-key-sha3-384 from snap-declaration
signKey, ok := snapDecl.Header("sign-key-sha3-384").(string)
if !ok || signKey == "" {
return fmt.Errorf("snap-declaration assertion missing 'sign-key-sha3-384' header for snap %s", snapInfo.SuggestedName)
}
// Step 3: Fetch account-key assertion using sign-key-sha3-384 (no decoding)
accountKeyAssertion, err := storeClient.Assertion(assertionTypes["account-key"], []string{signKey}, nil)
if err != nil {
return fmt.Errorf("failed to fetch account-key assertion for snap %s: %w", snapInfo.SuggestedName, err)
}
// Step 4: Fetch account assertion using publisher-id
accountAssertion, err := storeClient.Assertion(assertionTypes["account"], []string{publisherID}, nil)
if err != nil {
return fmt.Errorf("failed to fetch account assertion for snap %s: %w", snapInfo.SuggestedName, err)
}
// Step 5: Fetch snap-revision assertion
snapSHA384Bytes, err := hex.DecodeString(snapSHA)
if err != nil {
return fmt.Errorf("error decoding SHA3-384 hex string for snap %s: %w", snapInfo.SuggestedName, err)
}
snapSHA384Base64 := base64.RawURLEncoding.EncodeToString(snapSHA384Bytes)
//revisionKey := fmt.Sprintf("%s/global-upload", snapSHA384Base64)
revisionKey := fmt.Sprintf("%s/", snapSHA384Base64)
snapRevisionAssertion, err := storeClient.Assertion(assertionTypes["snap-revision"], []string{revisionKey}, nil)
if err != nil {
verboseLog("Failed to fetch snap-revision assertion for snap %s: %v", snapInfo.SuggestedName, err)
// Proceeding without snap-revision might be acceptable based on your use-case
}
// Step 6: Write assertions in the desired order
// 1. account-key
writeAssertion("account-key", accountKeyAssertion, assertionsFile)
// 2. account
writeAssertion("account", accountAssertion, assertionsFile)
// 3. snap-declaration
writeAssertion("snap-declaration", snapDecl, assertionsFile)
// 4. snap-revision (if fetched successfully)
if snapRevisionAssertion != nil {
writeAssertion("snap-revision", snapRevisionAssertion, assertionsFile)
}
verboseLog("Assertions downloaded and saved to: %s", assertionsPath)
return nil
}
func writeAssertion(assertionType string, assertion asserts.Assertion, file *os.File) {
fieldOrder := map[string][]string{
"account-key": {
"type", "authority-id", "revision", "public-key-sha3-384",
"account-id", "name", "since", "body-length", "sign-key-sha3-384",
},
"account": {
"type", "authority-id", "revision", "account-id", "display-name",
"timestamp", "username", "validation", "sign-key-sha3-384",
},
"snap-declaration": {
"type", "format", "authority-id", "revision", "series", "snap-id",
"aliases", "auto-aliases", "plugs", "publisher-id", "slots",
"snap-name", "timestamp", "sign-key-sha3-384",
},
"snap-revision": {
"type", "authority-id", "snap-sha3-384", "developer-id",
"provenance", "snap-id", "snap-revision", "snap-size",
"timestamp", "sign-key-sha3-384",
},
}
body := assertion.Body()
bodyLength := len(body)
headers := assertion.Headers()
// Only write the account assertion if it is not Canonical
if assertionType == "account" {
value, exists := headers["username"]
if exists && value == "canonical" {
return
}
}
// provenance seems to be a field only available in newer snap revisions
// For snaps published in 2023 or earlier, do not include this field
timestamp, exists := headers["timestamp"]
if assertionType == "snap-revision" && exists {
layout := time.RFC3339
parsedTime, _ := time.Parse(layout, timestamp.(string))
thresholdTime := time.Date(2023, time.December, 9, 0, 0, 0, 0, time.UTC)
if parsedTime.Before(thresholdTime) || parsedTime.Equal(thresholdTime) {
delete(headers, "provenance")
}
}
// Write headers in the specified order
for _, key := range fieldOrder[assertionType] {
value, exists := headers[key]
if !exists || value == "" {
continue
}
if key == "type" {
fmt.Fprintf(file, "%s: %s\n", key, assertionType)
} else if key == "body-length" && bodyLength > 0 {
file.WriteString(fmt.Sprintf("body-length: %d\n", bodyLength))
continue
} else if isComplexField(key) {
fmt.Fprintf(file, "%s:\n", key)
serializeComplexField(value, file)
} else {
fmt.Fprintf(file, "%s: %s\n", key, value)
}
}
file.WriteString("\n")
// Write the body if it exists
if bodyLength > 0 {
file.Write(body)
file.WriteString("\n\n")
}
// Write the signature
_, signature := assertion.Signature()
file.Write(signature)
if assertionType != "snap-revision" {
file.WriteString("\n")
}
}
func serializeComplexField(value interface{}, file *os.File) {
var buf strings.Builder
encoder := yaml.NewEncoder(&buf)
encoder.SetIndent(2)
defer encoder.Close()
// Encode the value directly
if err := encoder.Encode(value); err != nil {
log.Fatalf("Error encoding YAML: %v", err)
}
// Write the serialized YAML to the file with proper indentation
lines := strings.Split(buf.String(), "\n")
for _, line := range lines {
if line == "" {
continue
}
line = strings.ReplaceAll(line, `"true"`, "true")
line = strings.ReplaceAll(line, `"false"`, "false")
line = strings.ReplaceAll(line, `'*'`, "*")
// Check for dashes indicating list items
if strings.HasPrefix(strings.TrimSpace(line), "-") && strings.Contains(line, ":") {
before, after, found := strings.Cut(line, "- ")
if found {
file.WriteString(fmt.Sprintf(" %s-\n", before))
if after != "" {
file.WriteString(fmt.Sprintf(" %s%s\n", before, after))
}
} else {
file.WriteString(fmt.Sprintf(" %s-\n", line))
}
} else if strings.TrimSpace(line) != "" {
// For any other non-empty lines, indent them correctly
file.WriteString(fmt.Sprintf(" %s\n", line))
}
}
}
// isComplexField checks if a field is complex (nested) in YAML
func isComplexField(key string) bool {
return key == "aliases" || key == "auto-aliases" || key == "plugs" || key == "slots" || key == "allow-installation" || key == "allow-connection"
}