|
|
|
// 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"
|
|
|
|
}
|